/** * AKSRT After-Sale — 前台售后弹窗(申请 / 超时效提示 / 售后详细) * * 与「禁封/举报」链路同构(report.js / user-ban.js): * - [data-aksrt-after-sale-apply] → 申请售后弹窗(超时效则改为超时效提示弹窗) * - [data-aksrt-after-sale-detail] → 售后详细弹窗(可再次申请 / 不可再申请提示) * 两个入口的 data-* 属性携带订单与售后状态载荷(同 report.js 的 data-aksrt-report), * 点击即同步弹窗,不做前置请求(避免首次点击无反馈)。 * - 提交走 REST /aksrt/v1/orders/{order_number}/after-sale * - 举证图片走 /orders/{order_number}/after-sale/upload-image(复用评论图片上传校验) * * 按钮形态由订单视图模型的 after_sale 状态决定(UserOrders::after_sale_state): * 已申请 → 售后详细;未申请且在时效内 → 申请售后;超时效 → 申请售后(点击弹超时效提示)。 * 弹窗统一经 AksrtModalRuntime.open 直出 HTML:桌面为主题 modal-mini 几何 * (400px)+ 蓝色炫彩标题区(modal-colorful-header colorful-bg jb-blue), * 移动端由 mobileBottom 底部抽屉接管。 * * @package AKSRT */ (function () { 'use strict'; var API_ROOT = (function () { var d = window.aksrtData || {}; var raw = d.apiUrl || d.restUrl || d.rest_url || '/index.php?rest_route=/aksrt/v1'; return String(raw).replace(/\/$/, ''); })(); var NONCE = (window.aksrtData && window.aksrtData.restNonce) || ''; var MAX_IMAGES = 3; // 不可再申请售后(工单未结束 / 已完结 / 超过时效)时的固定提示。 var REAPPLY_BLOCKED_TEXT = '该无法再次申请售后,该订单已售后,暂不支持再次申请售后。'; var STATUS_LABELS = { pending: '待审核', processing: '处理中', approved: '已通过', rejected: '已拒绝', cancelled: '已取消', completed: '已完成', }; function settings() { var d = window.aksrtData && window.aksrtData.afterSale; return d && typeof d === 'object' ? d : {}; } function reasonOptions() { var list = settings().reasons; if (!Array.isArray(list)) return []; return list .map(function (item) { return { slug: String((item && (item.slug || item.value)) || ''), name: String((item && (item.name || item.label || item.t)) || ''), }; }) .filter(function (item) { return item.name !== ''; }); } function materialOptions() { var list = settings().materials; if (!Array.isArray(list)) return []; return list.map(function (item) { return String(item == null ? '' : item); }) .filter(function (text) { return text !== ''; }); } function escapeHtml(value) { return String(value == null ? '' : value) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function colorfulHeader(title, icon) { return ''; } function orderAmountText(order) { var raw = String(order && order.amount != null ? order.amount : '0'); if (order && order.is_points) { return escapeHtml(String(order.currency_symbol || '积分')) + ' ' + (/^\d+$/.test(raw) ? raw : '0'); } return '¥' + raw; } function aftersaleAmountText(value, order) { var amount = String(value == null ? '' : value); return order && order.is_points ? amount + ' ' + String((order && order.currency_symbol) || '积分') : '¥' + amount; } function orderSummaryHtml(order) { if (!order) return ''; return '
' + '
商品' + '' + escapeHtml(order.product_name || '订单') + '
' + '
订单号' + '' + escapeHtml(order.order_number || '') + '
' + '
实付' + '' + orderAmountText(order) + '
' + '
'; } function buildApplyHtml(order) { var cfg = settings(); var reasons = reasonOptions(); var materials = materialOptions(); var html = '
'; html += colorfulHeader('申请售后', 'fa-undo-alt'); html += '
'; // 滚动容器与禁封申诉弹窗一致:no-scrollbar 完全隐藏滚动条(含细滚动条)+ 50vh 内层滚动。 html += '
'; html += orderSummaryHtml(order); if (reasons.length) { html += '
'; html += '
售后原因
'; html += '
'; reasons.forEach(function (reason) { html += '' + escapeHtml(reason.name) + ''; }); html += '
'; } materials.forEach(function (label) { html += '
' + escapeHtml(label) + '
' + '
'; }); var placeholder = String(cfg.reasonPlaceholder || '必填,请您详细填写申请原因'); html += '
申请说明
' + '
'; if (cfg.imgEnabled) { html += '
' + '
上传图片
' + '
' + '' + '
最多上传 ' + MAX_IMAGES + ' 张图片
' + '
'; } // 申请售后说明(后台 after_sale_desc,服务端已 wp_kses_post)。 var desc = String(cfg.desc || ''); if (desc) { html += '
' + desc + '
'; } html += '
'; html += ''; html += '
'; return html; } function detailRow(label, value) { if (value === '' || value == null) return ''; return '
' + escapeHtml(label) + '' + '' + escapeHtml(value) + '
'; } function buildDetailHtml(order) { var afterSale = (order && order.after_sale) || {}; var latest = afterSale.latest || {}; var status = String(latest.status || ''); var html = '
'; html += colorfulHeader('售后详细', 'fa-clipboard-list'); html += '
'; html += orderSummaryHtml(order); html += '
' + '售后状态' + '' + escapeHtml(STATUS_LABELS[status] || status) + '' + '
'; html += '
'; html += detailRow('申请时间', String(latest.created_at || '')); html += detailRow('退款金额', aftersaleAmountText(latest.refund_amount, order)); html += detailRow('处理备注', String(latest.admin_note || '')); html += detailRow('完成时间', String(latest.completed_at || '')); html += '
'; if (latest.reason) { html += '
申请原因
' + '
' + escapeHtml(latest.reason).replace(/\r\n|\r|\n/g, '
') + '
'; } var images = Array.isArray(latest.images) ? latest.images : []; if (images.length) { html += '
举证图片
'; images.forEach(function (url) { html += ''; }); html += '
'; } html += '
'; if (afterSale.can_reapply) { html += ''; } else { // 不可再申请售后(工单未结束 / 已完结 / 超过时效):仅提示 + 我知道了。 html += '
' + '
' + escapeHtml(REAPPLY_BLOCKED_TEXT) + '
' + '' + '
'; } html += '
'; return html; } function buildExpiredHtml() { var text = String(settings().expiredText || '抱歉,您的订单已超过售后申请时效'); var html = '
'; html += colorfulHeader('售后提示', 'fa-clock'); // 超过时效提示(后台 after_sale_expired_text,服务端已 wp_kses_post,支持 HTML)。 html += '
' + text + '
'; html += ''; html += '
'; return html; } function openModal(options) { if (!window.AksrtModalRuntime || typeof window.AksrtModalRuntime.open !== 'function') { notyf('售后组件加载失败,请刷新页面后重试', 'danger'); return null; } var shell = window.AksrtModalRuntime.open({ html: options.html, className: options.className, kind: options.kind, mobileBottom: true, }); bindShell(shell); return shell; } function openApply(order) { return openModal({ html: buildApplyHtml(order), className: 'aksrt-after-sale-apply-modal', kind: 'after-sale-apply', }); } function openDetail(order) { return openModal({ html: buildDetailHtml(order), className: 'aksrt-after-sale-detail-modal', kind: 'after-sale-detail', }); } function openExpired() { return openModal({ html: buildExpiredHtml(), className: 'aksrt-after-sale-expired-modal', kind: 'after-sale-expired', }); } /** * 解析按钮 data-* 上的售后载荷(与举报链路 data-aksrt-report 同模式): * 入口数据随列表一起渲染,点击即可同步弹窗,不做前置请求。 */ function parsePayload(raw) { if (!raw) return null; try { var data = JSON.parse(raw); return data && typeof data === 'object' ? data : null; } catch (error) { return null; } } // 申请售后入口:超时效 → 超时效提示;已申请 → 售后详细;否则 → 申请表单。 function openApplyFlow(payload) { var afterSale = payload && payload.after_sale ? payload.after_sale : null; if (!payload || !afterSale || !afterSale.enabled) { notyf('售后申请暂不可用', 'danger'); return; } if (afterSale.expired) { openExpired(); return; } if (afterSale.has_application) { openDetail(payload); return; } openApply(payload); } function openDetailFlow(payload) { var afterSale = payload && payload.after_sale ? payload.after_sale : null; if (!payload || !afterSale || !afterSale.enabled || !afterSale.has_application) { notyf('暂无售后记录', 'danger'); return; } openDetail(payload); } function collectImages(shell) { return Array.prototype.map.call( shell.querySelectorAll('.preview .preview-item img'), function (img) { return img.getAttribute('src') || ''; } ).filter(function (url) { return url !== ''; }); } function uploadImage(shell, file) { var form = shell.querySelector('[data-aksrt-after-sale-order]'); var preview = shell.querySelector('.form-upload .preview'); var orderNumber = form ? form.getAttribute('data-aksrt-after-sale-order') : ''; if (!preview || !orderNumber) return Promise.resolve(); if (collectImages(shell).length >= MAX_IMAGES) { notyf('最多上传 ' + MAX_IMAGES + ' 张图片', 'danger'); return Promise.resolve(); } var body = new FormData(); body.append('image', file); return window.fetch(API_ROOT + '/orders/' + encodeURIComponent(orderNumber) + '/after-sale/upload-image', { method: 'POST', headers: { 'X-WP-Nonce': NONCE }, body: body, }) .then(function (res) { return res.json(); }) .then(function (result) { var url = result && result.success && result.data && result.data.url; if (!url) { notyf((result && result.message) || '图片上传失败', 'danger'); return; } if (collectImages(shell).length >= MAX_IMAGES) { notyf('最多上传 ' + MAX_IMAGES + ' 张图片', 'danger'); return; } var item = document.createElement('div'); item.className = 'preview-item'; var img = document.createElement('img'); img.src = url; img.alt = ''; var remove = document.createElement('button'); remove.type = 'button'; remove.className = 'remove'; remove.setAttribute('data-aksrt-after-sale-image-remove', ''); remove.setAttribute('aria-label', '删除图片'); remove.innerHTML = '×'; remove.addEventListener('click', function () { item.remove(); }); item.appendChild(img); item.appendChild(remove); preview.insertBefore(item, preview.querySelector('.add')); }) .catch(function () { notyf('图片上传失败,请重试', 'danger'); }); } function composeReason(shell) { var parts = []; var active = shell.querySelector('[data-aksrt-after-sale-reason].active'); if (active) { var label = active.getAttribute('data-aksrt-after-sale-reason') || ''; if (label) parts.push(label); } var materials = []; Array.prototype.forEach.call(shell.querySelectorAll('[data-aksrt-after-sale-material]'), function (input) { var value = String(input.value || '').trim(); if (value === '') return; materials.push(String(input.getAttribute('data-aksrt-after-sale-material') || '') + ':' + value); }); if (materials.length) parts.push(materials.join('\n')); var reasonInput = shell.querySelector('[name="reason"]'); var reasonText = reasonInput ? String(reasonInput.value || '').trim() : ''; if (reasonText) parts.push(reasonText); return parts.join('\n'); } function submitApply(shell) { var form = shell.querySelector('[data-aksrt-after-sale-order]'); var orderNumber = form ? form.getAttribute('data-aksrt-after-sale-order') : ''; var reasonInput = shell.querySelector('[name="reason"]'); var reasonText = reasonInput ? String(reasonInput.value || '').trim() : ''; if (!reasonText) { notyf('请填写申请说明', 'danger'); if (reasonInput) reasonInput.focus(); return; } // 预置原因被后台清空时不强制选择;有配置则必须选中一项(与举报弹窗一致)。 if (shell.querySelector('[data-aksrt-after-sale-reason]') && !shell.querySelector('[data-aksrt-after-sale-reason].active')) { notyf('请选择售后原因', 'danger'); return; } var submitBtn = shell.querySelector('[data-aksrt-after-sale-submit]'); if (submitBtn) submitBtn.disabled = true; notyf('正在提交售后申请…', 'load', 3000, 'aksrt-after-sale-submit'); window.fetch(API_ROOT + '/orders/' + encodeURIComponent(orderNumber) + '/after-sale', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': NONCE, }, body: JSON.stringify({ reason: composeReason(shell), images: collectImages(shell), }), }) .then(function (res) { return res.json(); }) .then(function (result) { if (result && result.success) { window.AksrtModalRuntime.close('submit'); notyf('售后申请已提交', 'success'); if (window.AKSRTOrderCenter && typeof window.AKSRTOrderCenter.refresh === 'function') { window.AKSRTOrderCenter.refresh(orderNumber); } } else { if (submitBtn) submitBtn.disabled = false; notyf((result && result.message) || '售后申请提交失败', 'danger'); } }) .catch(function () { if (submitBtn) submitBtn.disabled = false; notyf('网络错误,请重试', 'danger'); }) .then(function () { var toast = document.getElementById('aksrt-after-sale-submit'); if (toast) toast.remove(); }); } function bindShell(shell) { if (!shell) return; shell.addEventListener('click', function (event) { // 原因徽章:单选 active(与举报弹窗同语义,重复点击已选徽章保持选中)。 var chip = event.target.closest('[data-aksrt-after-sale-reason]'); if (chip) { event.preventDefault(); Array.prototype.forEach.call(shell.querySelectorAll('[data-aksrt-after-sale-reason].active'), function (el) { el.classList.remove('active'); }); chip.classList.add('active'); return; } var addBtn = event.target.closest('[data-aksrt-after-sale-add]'); if (addBtn) { event.preventDefault(); var fileInput = shell.querySelector('[data-aksrt-after-sale-file]'); if (fileInput) fileInput.click(); return; } var removeBtn = event.target.closest('[data-aksrt-after-sale-image-remove]'); if (removeBtn) { event.preventDefault(); var item = removeBtn.closest('.preview-item'); if (item) item.remove(); return; } var submitBtn = event.target.closest('[data-aksrt-after-sale-submit]'); if (submitBtn) { event.preventDefault(); submitApply(shell); return; } // 再次申请售后:载荷已随按钮渲染,直接开申请表单(时效/工单状态随列表刷新更新)。 var reapplyBtn = event.target.closest('[data-aksrt-after-sale-reapply]'); if (reapplyBtn) { event.preventDefault(); var payload = parsePayload(reapplyBtn.getAttribute('data-aksrt-after-sale-payload')); window.AksrtModalRuntime.close('replace'); openApplyFlow(payload); } }); shell.addEventListener('change', function (event) { var fileInput = event.target.closest('[data-aksrt-after-sale-file]'); if (!fileInput || !fileInput.files || !fileInput.files.length) return; Array.prototype.forEach.call(fileInput.files, function (file) { uploadImage(shell, file); }); fileInput.value = ''; }); } function initAfterSaleButtons() { if (document.documentElement.dataset.aksrtAfterSaleInitialized === '1') return; document.documentElement.dataset.aksrtAfterSaleInitialized = '1'; document.addEventListener('click', function (event) { var apply = event.target.closest('[data-aksrt-after-sale-apply]'); if (apply) { event.preventDefault(); openApplyFlow(parsePayload(apply.getAttribute('data-aksrt-after-sale-apply'))); return; } var detail = event.target.closest('[data-aksrt-after-sale-detail]'); if (detail) { event.preventDefault(); openDetailFlow(parsePayload(detail.getAttribute('data-aksrt-after-sale-detail'))); } }); } jQuery(document).off('auto_fun.aksrt-after-sale').on('auto_fun.aksrt-after-sale', initAfterSaleButtons); // 委托监听不依赖 DOM 扫描,脚本加载时立即挂上;auto_fun 钩子保留作幂等重扫。 initAfterSaleButtons(); })();