feat(payment): 新增「售后策略」配置与前台售后弹窗链路

- 后台「支付&付费 → 售后策略」:售后功能开关、申请时效、超时效提示(HTML)、
  预置原因、申请售后说明、所需材料、申请说明占位文案、可上传图片开关;
  PHP schema 与 SPA schema 1:1 对齐,group 字段支持拖拽排序
- PaymentRuntime 新增售后设置解析(缺键回退 schema 默认、HTML 走 kses、
  中文原因稳定 slug)与 after_sale_expired 时效判定(支付时间起算)
- 售后工单支持举证图片(order_aftersale.images 列,最多 3 张,
  复用评论图片上传校验)与上传路由;订单视图模型输出 after_sale 按钮状态
- 前台申请 / 超时效提示 / 售后详细三个独立弹窗(modal-mini + 蓝色炫彩头),
  订单详情弹窗不再内嵌售后表单并清理相关死代码
- 新增 after-sale-policy 契约测试与运行时 harness,527 项契约测试通过
This commit is contained in:
kate522 committed 2026-10-06 22:21:47 +08:00
1 parent b447c41f13
commit 1a9a2762f3
19 files changed
+1835 -86

No files matched your search

+83 -30
View File
@@ -507,38 +507,47 @@
</div>
</div>
<!-- Repeater (array of objects) — inline expand (default) -->
<!-- Repeater (array of objects) — inline expand (default, draggable) -->
<div v-else-if="(schema.type === 'repeater' || schema.type === 'group') && schema.ui !== 'list-modal'" class="aksrt-field__repeater">
<div
v-for="(item, index) in repeaterItems"
:key="item._id || index"
class="aksrt-field__repeater-item"
<draggable
v-model="repeaterDragList"
:item-key="repeaterDragKey"
handle=".aksrt-field__repeater-handle"
ghost-class="aksrt-field__repeater-ghost"
animation="180"
>
<div class="aksrt-field__repeater-header">
<span class="aksrt-field__repeater-index">#{{ index + 1 }}</span>
<n-button
size="tiny"
type="error"
text
:disabled="schema.disabled || reachedRepeaterMinimum"
@click="removeRepeaterItem(index)"
>
<FaIcon icon="fa fas fa-trash-alt" :size="12" />
</n-button>
</div>
<div class="aksrt-field__repeater-fields">
<template v-for="field in (schema.fields || [])" :key="field.key">
<AksField
v-if="matchDependency(field.dependsOn, item)"
:schema="field"
:model-value="item[field.key]"
:form-data="formData"
:disabled="schema.disabled"
@update:model-value="updateRepeaterItem(index, field.key, $event)"
/>
</template>
</div>
</div>
<template #item="{ element, index }">
<div class="aksrt-field__repeater-item">
<div class="aksrt-field__repeater-header">
<div class="aksrt-field__repeater-title">
<i class="fa fas fa-grip-vertical aksrt-field__repeater-handle" aria-hidden="true"></i>
<span class="aksrt-field__repeater-index">#{{ index + 1 }}</span>
</div>
<n-button
size="tiny"
type="error"
text
:disabled="schema.disabled || reachedRepeaterMinimum"
@click="removeRepeaterItem(index)"
>
<FaIcon icon="fa fas fa-trash-alt" :size="12" />
</n-button>
</div>
<div class="aksrt-field__repeater-fields">
<template v-for="field in (schema.fields || [])" :key="field.key">
<AksField
v-if="matchDependency(field.dependsOn, element)"
:schema="field"
:model-value="element[field.key]"
:form-data="formData"
:disabled="schema.disabled"
@update:model-value="updateRepeaterItem(index, field.key, $event)"
/>
</template>
</div>
</div>
</template>
</draggable>
<n-button
size="small"
dashed
@@ -688,6 +697,7 @@ import FaIconPicker from './FaIconPicker.vue'
import AksMediaField from './AksMediaField.vue'
import AksGalleryField from './AksGalleryField.vue'
import AksCodeEditor from './AksCodeEditor.vue'
import draggable from 'vuedraggable'
import api from '@/api'
const props = defineProps({
@@ -720,6 +730,31 @@ const repeaterItems = computed(() => Array.isArray(props.modelValue) ? props.mod
const reachedRepeaterMinimum = computed(() => repeaterLength.value <= Number(props.schema.min || 0))
const reachedRepeaterMaximum = computed(() => Number(props.schema.max || 0) > 0 && repeaterLength.value >= Number(props.schema.max))
/** 拖拽视图:vuedraggable 以 modelValue 模式工作(内部复制后回传 update:modelValue)。 */
const repeaterDragList = computed({
get: () => (Array.isArray(props.modelValue) ? props.modelValue : []),
set: (value) => emit('update:modelValue', Array.isArray(value) ? value : []),
})
/**
* 行 key:优先用行自身 _id;服务端历史数据没有 _id 时按对象引用派生稳定 key,
* 避免每次输入都重算 key 导致子字段重挂载、输入焦点丢失。
*/
const dragRowKeys = new WeakMap()
let dragRowKeySeed = 0
function repeaterDragKey(item) {
if (item && typeof item === 'object') {
if (item._id) return item._id
let key = dragRowKeys.get(item)
if (!key) {
key = `rpt_drag_${++dragRowKeySeed}`
dragRowKeys.set(item, key)
}
return key
}
return String(item)
}
function fieldsetValue(field) {
const current = props.modelValue && typeof props.modelValue === 'object' && !Array.isArray(props.modelValue)
? props.modelValue
@@ -1431,9 +1466,27 @@ function confirmRepeaterModal() {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-bottom: 8px;
}
.aksrt-field__repeater-title {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.aksrt-field__repeater-handle {
cursor: grab;
color: var(--aksrt-color-text-tertiary);
}
.aksrt-field__repeater-ghost {
opacity: 0.45;
border-color: var(--aksrt-color-primary);
}
.aksrt-field__repeater-index {
font-size: 12px;
font-weight: 600;
@@ -88,5 +88,16 @@ export const paymentSection = {
canonicalDomain: 'payment',
section: 'withdrawal',
}),
schemaPage({
key: 'payment-after-sale',
name: 'PaymentAfterSale',
label: '售后策略',
route: '/payment/after-sale',
status: 'available',
title: '售后策略',
desc: '售后功能开关、申请时效、超时效提示与申请表单(预置原因、所需材料、图片上传)配置。',
canonicalDomain: 'payment',
section: 'after_sale',
}),
],
}
@@ -0,0 +1,80 @@
/**
* 售后策略 — 售后开关、申请时效、超时效提示与申请表单配置。
*
* 字段键、顺序、类型与默认值需与 src/Admin/Schema/SchemaPayment.php 的
* after_sale 分区保持 1:1(tests/widget-system/after-sale-policy-contract.test.cjs 守护)。
*/
export const afterSaleSchema = {
key: 'payment.after_sale',
title: '售后策略',
icon: 'fa fas fa-undo-alt',
description: '售后功能开关、申请时效、超时效提示与申请表单(预置原因、申请说明、所需材料、图片上传)配置。',
defaults: {
after_sale_s: true,
after_sale_time_limit: 7,
after_sale_expired_text: '<div>抱歉,您的订单已超过售后申请时效</div>',
after_sale_reasons: [
{ t: '质量问题' },
{ t: '假冒品牌' },
{ t: '其他原因' },
],
after_sale_desc: '',
after_sale_materials: [
{ t: '联系邮箱' },
{ t: '联系电话' },
],
after_sale_reason_placeholder: '必填,请您详细填写申请原因',
after_sale_img_s: true,
},
groups: [
{
key: 'main',
title: '售后功能',
icon: 'fa fas fa-undo-alt',
fields: [
{ key: 'after_sale_s', label: '售后功能', type: 'switch', default: true, help: '开启后用户可以申请售后;关闭后不再处理售后,前台「我的订单」不再显示申请售后按钮' },
{ key: 'after_sale_time_limit', label: '申请售后时效(天)', type: 'number', default: 7, min: 0, max: 3650, help: '订单支付后多少天内可以申请售后,超过时效将无法申请;0 表示不限制' },
{ key: 'after_sale_expired_text', label: '超过时效提示', type: 'textarea', sanitize_type: 'html', default: '<div>抱歉,您的订单已超过售后申请时效</div>', rows: 3, help: '订单超过售后时效后,用户点击申请售后时弹窗提示的内容(支持HTML代码,请注意代码规范)' },
],
},
{
key: 'form',
title: '申请表单',
icon: 'fa fas fa-file-alt',
fields: [
{
key: 'after_sale_reasons',
label: '预置原因',
type: 'group',
button_title: '添加原因',
help: '用户申请售后时可选择的原因',
default: [
{ t: '质量问题' },
{ t: '假冒品牌' },
{ t: '其他原因' },
],
fields: [
{ key: 't', label: '原因', type: 'input', default: '' },
],
},
{ key: 'after_sale_desc', label: '申请售后说明', type: 'textarea', sanitize_type: 'html', default: '', rows: 5, help: '用户申请售后时显示的内容(支持HTML代码,请注意代码规范)' },
{
key: 'after_sale_materials',
label: '申请所需材料',
type: 'group',
button_title: '添加材料',
help: '用户提交售后申请时需要提供的资料,如联系邮箱、联系电话;此处的信息收集,仅用于售后联系与审核',
default: [
{ t: '联系邮箱' },
{ t: '联系电话' },
],
fields: [
{ key: 't', label: '资料项', type: 'input', default: '' },
],
},
{ key: 'after_sale_reason_placeholder', label: '申请说明占位文案', type: 'input', default: '必填,请您详细填写申请原因', help: '前台申请售后弹窗中「申请说明」输入框的提示文字' },
{ key: 'after_sale_img_s', label: '申请售后可上传图片', type: 'switch', default: true, help: '开启后申请售后弹窗中出现图片上传入口(最多上传3张)' },
],
},
],
}
@@ -9,6 +9,7 @@ import { revenueSchema } from './revenue.js'
import { affiliateSchema } from './affiliate.js'
import { gatewaySchema } from './gateway.js'
import { withdrawalSchema } from './withdrawal.js'
import { afterSaleSchema } from './afterSale.js'
export const paymentSchemas = {
knowledge: knowledgeSchema,
@@ -18,4 +19,5 @@ export const paymentSchemas = {
affiliate: affiliateSchema,
gateway: gatewaySchema,
withdrawal: withdrawalSchema,
after_sale: afterSaleSchema,
}
+184 -5
View File
@@ -6429,14 +6429,15 @@ body.aksrt-modal-runtime-open {
color: var(--color-ink, #111);
}
:is(.order-comment-from, .aksrt-report-modal) .form-upload .preview {
:is(.order-comment-from, .aksrt-report-modal, .aksrt-after-sale-apply-modal) .form-upload .preview {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
:is(.order-comment-from, .aksrt-report-modal) .form-upload .preview .add,
:is(.order-comment-from, .aksrt-report-modal) .form-upload .preview .preview-item {
:is(.order-comment-from, .aksrt-report-modal, .aksrt-after-sale-apply-modal) .form-upload .preview .add,
:is(.order-comment-from, .aksrt-report-modal, .aksrt-after-sale-apply-modal) .form-upload .preview .preview-item {
width: 80px;
height: 80px;
border-radius: 10px;
@@ -6452,14 +6453,14 @@ body.aksrt-modal-runtime-open {
此前这里的 left/top:50% + translate 版本与该规则叠加导致过约束,
加号被推到 75%/75% 只剩“L”形,故整体删除、统一走主样式包。 */
:is(.order-comment-from, .aksrt-report-modal) .form-upload .preview .preview-item img {
:is(.order-comment-from, .aksrt-report-modal, .aksrt-after-sale-apply-modal) .form-upload .preview .preview-item img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
:is(.order-comment-from, .aksrt-report-modal) .form-upload .preview .preview-item .remove {
:is(.order-comment-from, .aksrt-report-modal, .aksrt-after-sale-apply-modal) .form-upload .preview .preview-item .remove {
position: absolute;
top: 2px;
right: 2px;
@@ -6474,7 +6475,7 @@ body.aksrt-modal-runtime-open {
cursor: pointer;
}
:is(.order-comment-from, .aksrt-report-modal) .form-upload .hide {
:is(.order-comment-from, .aksrt-report-modal, .aksrt-after-sale-apply-modal) .form-upload .hide {
display: none !important;
}
@@ -6490,6 +6491,184 @@ body.aksrt-modal-runtime-open {
}
}
/* ---- 售后弹窗(申请 / 售后详细 / 超时效提示,参考主题 modal-mini 几何 1:1)----
三个弹窗与禁封申诉弹窗同几何:data_class='modal-mini'(宽 400px),圆角与
底色走 --main-radius / --main-bg-color;移动端由 --bottom 底部抽屉接管。 */
.aksrt-after-sale-apply-modal:not(.aksrt-modal-runtime--bottom) .aksrt-modal-runtime__dialog,
.aksrt-after-sale-detail-modal:not(.aksrt-modal-runtime--bottom) .aksrt-modal-runtime__dialog,
.aksrt-after-sale-expired-modal:not(.aksrt-modal-runtime--bottom) .aksrt-modal-runtime__dialog {
width: min(400px, calc(100vw - 32px));
border-radius: var(--main-radius, 8px);
background: var(--main-bg-color, #fff);
}
.aksrt-after-sale-apply-modal .aksrt-modal-runtime__skeleton,
.aksrt-after-sale-detail-modal .aksrt-modal-runtime__skeleton,
.aksrt-after-sale-expired-modal .aksrt-modal-runtime__skeleton {
padding: 24px 18px;
}
/* 蓝色炫彩标题区(参考主题 zib_get_modal_colorful_header('jb-blue', ...)):120px
绝对定位横幅 + 旋转彩斑,后续内容整体下移 120px,与禁封记录弹窗同构。 */
.aksrt-after-sale-apply-modal .modal-colorful-header,
.aksrt-after-sale-detail-modal .modal-colorful-header,
.aksrt-after-sale-expired-modal .modal-colorful-header,
.aksrt-after-sale-apply-modal .colorful-bg,
.aksrt-after-sale-detail-modal .colorful-bg,
.aksrt-after-sale-expired-modal .colorful-bg {
position: relative;
overflow: hidden;
color: var(--this-color);
background: var(--this-bg);
background-size: 120%;
}
.aksrt-after-sale-apply-modal .modal-colorful-header,
.aksrt-after-sale-detail-modal .modal-colorful-header,
.aksrt-after-sale-expired-modal .modal-colorful-header {
position: absolute;
top: 0;
right: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
height: 120px;
border-radius: var(--main-radius, 8px) var(--main-radius, 8px) 0 0;
}
.aksrt-after-sale-apply-modal .modal-colorful-header + *,
.aksrt-after-sale-detail-modal .modal-colorful-header + *,
.aksrt-after-sale-expired-modal .modal-colorful-header + * {
margin-top: 120px;
}
.aksrt-after-sale-apply-modal .modal-colorful-header > .close,
.aksrt-after-sale-detail-modal .modal-colorful-header > .close,
.aksrt-after-sale-expired-modal .modal-colorful-header > .close {
position: absolute;
top: 10px;
right: 10px;
z-index: 9;
color: var(--this-color);
font-size: 18px;
opacity: 0.6;
}
.aksrt-after-sale-apply-modal .colorful-bg > .colorful-make,
.aksrt-after-sale-apply-modal .colorful-bg > .colorful-make::before,
.aksrt-after-sale-apply-modal .colorful-bg > .colorful-make::after,
.aksrt-after-sale-detail-modal .colorful-bg > .colorful-make,
.aksrt-after-sale-detail-modal .colorful-bg > .colorful-make::before,
.aksrt-after-sale-detail-modal .colorful-bg > .colorful-make::after,
.aksrt-after-sale-expired-modal .colorful-bg > .colorful-make,
.aksrt-after-sale-expired-modal .colorful-bg > .colorful-make::before,
.aksrt-after-sale-expired-modal .colorful-bg > .colorful-make::after {
position: absolute;
color: var(--this-color);
background: var(--this-bg);
border-radius: 100%;
content: '';
}
.aksrt-after-sale-apply-modal .colorful-bg > .colorful-make,
.aksrt-after-sale-detail-modal .colorful-bg > .colorful-make,
.aksrt-after-sale-expired-modal .colorful-bg > .colorful-make {
top: 60%;
bottom: 0;
width: 323px;
height: 290px;
animation: aksrt-ban-colorful 120s linear infinite;
}
.aksrt-after-sale-apply-modal .colorful-bg > .colorful-make::before,
.aksrt-after-sale-detail-modal .colorful-bg > .colorful-make::before,
.aksrt-after-sale-expired-modal .colorful-bg > .colorful-make::before {
top: -325px;
right: -558px;
bottom: 0;
width: 677px;
height: 300px;
opacity: 0.6;
transform: rotate(341deg);
}
.aksrt-after-sale-apply-modal .colorful-bg > .colorful-make::after,
.aksrt-after-sale-detail-modal .colorful-bg > .colorful-make::after,
.aksrt-after-sale-expired-modal .colorful-bg > .colorful-make::after {
top: -170px;
bottom: 0;
left: -262px;
width: 402px;
height: 214px;
opacity: 0.4;
transform: rotate(326deg);
}
/* 售后原因徽章:与举报弹窗一致,可换行且间距走 mr6/mb6 工具类。 */
.aksrt-after-sale-apply-modal .form-but-radio .badg {
cursor: pointer;
user-select: none;
}
/* 订单摘要与售后明细行 */
.aksrt-after-sale-summary__row,
.aksrt-after-sale-detail__row {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
margin-bottom: 6px;
}
.aksrt-after-sale-summary__row:last-child,
.aksrt-after-sale-detail__row:last-child {
margin-bottom: 0;
}
.aksrt-after-sale-summary__value,
.aksrt-after-sale-detail__value {
min-width: 0;
text-align: right;
word-break: break-all;
}
.aksrt-after-sale-detail__statusRow {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.aksrt-after-sale-reason-text {
color: var(--color-ink-2, #4e5358);
background: var(--surface-2, #eee);
border-radius: 6px;
padding: 10px 12px;
}
.aksrt-after-sale-images {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.aksrt-after-sale-images img {
width: 72px;
height: 72px;
object-fit: cover;
border-radius: 6px;
border: 1px solid var(--color-line, #e5e7eb);
display: block;
}
.aksrt-after-sale-status.is-pending { color: var(--color-warning, #f59e0b); }
.aksrt-after-sale-status.is-processing { color: var(--color-info, #3b82f6); }
.aksrt-after-sale-status.is-approved,
.aksrt-after-sale-status.is-completed { color: var(--color-success, #10b981); }
.aksrt-after-sale-status.is-rejected,
.aksrt-after-sale-status.is-cancelled { color: var(--color-error, #ef4444); }
/* source: assets/css/frontend/notyf.css */
/**
* legacy-compatible frontend notifications.
+1 -1
View File
@@ -1 +1 @@
:root{--color-primary: #f04494;--color-primary-hover: #e0307e;--color-primary-active: #c9206d;--color-primary-soft: rgba(240, 68, 148, .1);--color-primary-softer: rgba(240, 68, 148, .05);--color-on-primary: #FFFFFF;--color-ink: #333333;--color-ink-2: #4e5358;--color-ink-3: #777777;--color-ink-4: #999999;--surface-page: #f5f6f7;--surface-1: #FFFFFF;--surface-2: #eeeeee;--surface-3: #e9ecef;--surface-4: #f8f8f8;--color-line: rgba(50, 50, 50, .06);--color-line-soft: rgba(0, 0, 0, .03);--color-line-stronger: rgba(50, 50, 50, .12);--color-success: #10B981;--color-warning: #F59E0B;--color-error: #EF4444;--color-info: #3B82F6;--color-accent: #18a058;--color-accent-soft: rgba(24, 160, 88, .12);--color-accent-shadow: rgba(24, 160, 88, .28);--font-display: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;--font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;--font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;--font-mono: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;--text-xs: .75rem;--text-sm: .875rem;--text-base: .875rem;--text-md: 1.125rem;--text-lg: 1.25rem;--text-xl: 1.5rem;--text-2xl: 1.875rem;--text-3xl: 2.25rem;--text-4xl: 3rem;--text-5xl: 3.75rem;--leading-tight: 1.2;--leading-snug: 1.4;--leading-normal: 1.6;--leading-relaxed: 1.6;--space-1: 4px;--space-2: 8px;--space-3: 12px;--space-4: 16px;--space-5: 20px;--space-6: 24px;--space-7: 32px;--space-8: 40px;--space-9: 48px;--space-10: 64px;--space-11: 80px;--space-12: 96px;--radius-xs: 3px;--radius-sm: 4px;--radius: 8px;--radius-lg: 8px;--radius-xl: 12px;--radius-2xl: 16px;--radius-pill: 100px;--radius-avatar: 50%;--shadow-xs: 0 1px 2px rgba(0, 0, 0, .03);--shadow-sm: 0 0 10px rgba(116, 116, 116, .08);--shadow: 0 0 10px rgba(116, 116, 116, .12);--shadow-lg: 0 12px 32px rgba(18, 18, 18, .1);--shadow-drawer: 0 18px 60px rgba(0, 0, 0, .2);--nav-height: 60px;--container: 1200px;--container-wide: 1440px;--container-read: 760px;--container-page: 880px;--sidebar-width: 311px;--content-gap: 24px;--section-gap: 24px;--sidebar-bp: 992px;--ease: cubic-bezier(.4, 0, .2, 1);--ease-out: cubic-bezier(.16, 1, .3, 1);--ease-spring: cubic-bezier(.34, 1.56, .64, 1);--dur-fast: .15s;--dur: .25s;--dur-slow: .4s;--focus-ring: 0 0 2px 3px rgba(253, 83, 161, .4);--z-dropdown: 100;--z-sticky: 200;--z-fixed: 300;--z-overlay: 400;--z-modal: 500;--z-popover: 600;--z-toast: 700;--z-tooltip: 800;--radius-btn: 4px;--btn-sm-h: 28px;--btn-md-h: 34px;--btn-lg-h: 44px;--card-padding: 15px;--card-padding-sm: 10px;--card-gap: 16px;--card-gap-lg: 20px;--color-primary-light: var(--color-primary-soft);--color-surface: var(--surface-1);--color-line-strong: var(--color-line);--color-bg-hover: var(--surface-2);--color-border-light: var(--color-line-soft);--color-fill: var(--surface-2);--color-warning-bg: #fffbe6;--color-warning-text: #ad6800;--text-secondary: var(--color-ink-3);--text-tertiary: var(--color-ink-4);--surface-active: var(--surface-2);--surface-card: var(--surface-1);--surface-hover: var(--surface-2);--surface-secondary: var(--surface-2);--radius-code: var(--radius);--radius-full: 100px;--shadow-xl: 0 0 10px rgba(116, 116, 116, .12);--space-16: 64px;--z-floating: 900;--transition-fast: .15s ease;--transition-base: .25s ease;--header-bg: var(--blur-bg);--header-color: var(--color-ink-2);--footer-bg: var(--surface-1);--footer-color: var(--color-ink-4);--blur-bg: rgba(255, 255, 255, .8);--float-btn-bg: rgba(200, 200, 200, .4);--font-heading: var(--font-ui);--radius-card: var(--radius);--radius-md: var(--radius);--color-primary-50: var(--color-primary-soft);--color-primary-dark: var(--color-primary-active);--shadow-md: var(--shadow);--mini-radius: calc(var(--radius) / 1.4);--aksrt-avatar-size-xs: 24px;--aksrt-avatar-size-sm: 32px;--aksrt-avatar-size: 40px;--aksrt-avatar-size-lg: 56px;--aksrt-avatar-size-xl: 80px;--aksrt-card-thumb-scale: 56.25%;--aksrt-rose: #f43f5e;--aksrt-rose-soft: rgba(244, 63, 94, .12);--aksrt-blue: #3b82f6;--aksrt-blue-soft: rgba(59, 130, 246, .12);--aksrt-green: #10b981;--aksrt-green-soft: rgba(16, 185, 129, .12);--aksrt-orange: #f97316;--aksrt-orange-soft:rgba(249, 115, 22, .12);--aksrt-purple: #8b5cf6;--aksrt-purple-soft:rgba(139, 92, 246, .12);--aksrt-cyan: #06b6d4;--aksrt-cyan-soft: rgba(6, 182, 212, .12);--aksrt-yellow: #eab308;--aksrt-yellow-soft:rgba(234, 179, 8, .12);--aksrt-red: #ef4444;--aksrt-red-soft: rgba(239, 68, 68, .12);--aksrt-teal: #14b8a6;--aksrt-teal-soft: rgba(20, 184, 166, .12);--aksrt-indigo: #6366f1;--aksrt-indigo-soft:rgba(99, 102, 241, .12);--aksrt-pink: #ec4899;--aksrt-pink-soft: rgba(236, 72, 153, .12);--aksrt-vip-gold-1: #f6d365;--aksrt-vip-gold-2: #fda085;--aksrt-grad-sunset-1: #f093fb;--aksrt-grad-sunset-2: #f5576c;--aksrt-graLine truncated
:root{--color-primary: #f04494;--color-primary-hover: #e0307e;--color-primary-active: #c9206d;--color-primary-soft: rgba(240, 68, 148, .1);--color-primary-softer: rgba(240, 68, 148, .05);--color-on-primary: #FFFFFF;--color-ink: #333333;--color-ink-2: #4e5358;--color-ink-3: #777777;--color-ink-4: #999999;--surface-page: #f5f6f7;--surface-1: #FFFFFF;--surface-2: #eeeeee;--surface-3: #e9ecef;--surface-4: #f8f8f8;--color-line: rgba(50, 50, 50, .06);--color-line-soft: rgba(0, 0, 0, .03);--color-line-stronger: rgba(50, 50, 50, .12);--color-success: #10B981;--color-warning: #F59E0B;--color-error: #EF4444;--color-info: #3B82F6;--color-accent: #18a058;--color-accent-soft: rgba(24, 160, 88, .12);--color-accent-shadow: rgba(24, 160, 88, .28);--font-display: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;--font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;--font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;--font-mono: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;--text-xs: .75rem;--text-sm: .875rem;--text-base: .875rem;--text-md: 1.125rem;--text-lg: 1.25rem;--text-xl: 1.5rem;--text-2xl: 1.875rem;--text-3xl: 2.25rem;--text-4xl: 3rem;--text-5xl: 3.75rem;--leading-tight: 1.2;--leading-snug: 1.4;--leading-normal: 1.6;--leading-relaxed: 1.6;--space-1: 4px;--space-2: 8px;--space-3: 12px;--space-4: 16px;--space-5: 20px;--space-6: 24px;--space-7: 32px;--space-8: 40px;--space-9: 48px;--space-10: 64px;--space-11: 80px;--space-12: 96px;--radius-xs: 3px;--radius-sm: 4px;--radius: 8px;--radius-lg: 8px;--radius-xl: 12px;--radius-2xl: 16px;--radius-pill: 100px;--radius-avatar: 50%;--shadow-xs: 0 1px 2px rgba(0, 0, 0, .03);--shadow-sm: 0 0 10px rgba(116, 116, 116, .08);--shadow: 0 0 10px rgba(116, 116, 116, .12);--shadow-lg: 0 12px 32px rgba(18, 18, 18, .1);--shadow-drawer: 0 18px 60px rgba(0, 0, 0, .2);--nav-height: 60px;--container: 1200px;--container-wide: 1440px;--container-read: 760px;--container-page: 880px;--sidebar-width: 311px;--content-gap: 24px;--section-gap: 24px;--sidebar-bp: 992px;--ease: cubic-bezier(.4, 0, .2, 1);--ease-out: cubic-bezier(.16, 1, .3, 1);--ease-spring: cubic-bezier(.34, 1.56, .64, 1);--dur-fast: .15s;--dur: .25s;--dur-slow: .4s;--focus-ring: 0 0 2px 3px rgba(253, 83, 161, .4);--z-dropdown: 100;--z-sticky: 200;--z-fixed: 300;--z-overlay: 400;--z-modal: 500;--z-popover: 600;--z-toast: 700;--z-tooltip: 800;--radius-btn: 4px;--btn-sm-h: 28px;--btn-md-h: 34px;--btn-lg-h: 44px;--card-padding: 15px;--card-padding-sm: 10px;--card-gap: 16px;--card-gap-lg: 20px;--color-primary-light: var(--color-primary-soft);--color-surface: var(--surface-1);--color-line-strong: var(--color-line);--color-bg-hover: var(--surface-2);--color-border-light: var(--color-line-soft);--color-fill: var(--surface-2);--color-warning-bg: #fffbe6;--color-warning-text: #ad6800;--text-secondary: var(--color-ink-3);--text-tertiary: var(--color-ink-4);--surface-active: var(--surface-2);--surface-card: var(--surface-1);--surface-hover: var(--surface-2);--surface-secondary: var(--surface-2);--radius-code: var(--radius);--radius-full: 100px;--shadow-xl: 0 0 10px rgba(116, 116, 116, .12);--space-16: 64px;--z-floating: 900;--transition-fast: .15s ease;--transition-base: .25s ease;--header-bg: var(--blur-bg);--header-color: var(--color-ink-2);--footer-bg: var(--surface-1);--footer-color: var(--color-ink-4);--blur-bg: rgba(255, 255, 255, .8);--float-btn-bg: rgba(200, 200, 200, .4);--font-heading: var(--font-ui);--radius-card: var(--radius);--radius-md: var(--radius);--color-primary-50: var(--color-primary-soft);--color-primary-dark: var(--color-primary-active);--shadow-md: var(--shadow);--mini-radius: calc(var(--radius) / 1.4);--aksrt-avatar-size-xs: 24px;--aksrt-avatar-size-sm: 32px;--aksrt-avatar-size: 40px;--aksrt-avatar-size-lg: 56px;--aksrt-avatar-size-xl: 80px;--aksrt-card-thumb-scale: 56.25%;--aksrt-rose: #f43f5e;--aksrt-rose-soft: rgba(244, 63, 94, .12);--aksrt-blue: #3b82f6;--aksrt-blue-soft: rgba(59, 130, 246, .12);--aksrt-green: #10b981;--aksrt-green-soft: rgba(16, 185, 129, .12);--aksrt-orange: #f97316;--aksrt-orange-soft:rgba(249, 115, 22, .12);--aksrt-purple: #8b5cf6;--aksrt-purple-soft:rgba(139, 92, 246, .12);--aksrt-cyan: #06b6d4;--aksrt-cyan-soft: rgba(6, 182, 212, .12);--aksrt-yellow: #eab308;--aksrt-yellow-soft:rgba(234, 179, 8, .12);--aksrt-red: #ef4444;--aksrt-red-soft: rgba(239, 68, 68, .12);--aksrt-teal: #14b8a6;--aksrt-teal-soft: rgba(20, 184, 166, .12);--aksrt-indigo: #6366f1;--aksrt-indigo-soft:rgba(99, 102, 241, .12);--aksrt-pink: #ec4899;--aksrt-pink-soft: rgba(236, 72, 153, .12);--aksrt-vip-gold-1: #f6d365;--aksrt-vip-gold-2: #fda085;--aksrt-grad-sunset-1: #f093fb;--aksrt-grad-sunset-2: #f5576c;--aksrt-graLine truncated
+184 -5
View File
@@ -552,14 +552,15 @@ body.aksrt-modal-runtime-open {
color: var(--color-ink, #111);
}
:is(.order-comment-from, .aksrt-report-modal) .form-upload .preview {
:is(.order-comment-from, .aksrt-report-modal, .aksrt-after-sale-apply-modal) .form-upload .preview {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
:is(.order-comment-from, .aksrt-report-modal) .form-upload .preview .add,
:is(.order-comment-from, .aksrt-report-modal) .form-upload .preview .preview-item {
:is(.order-comment-from, .aksrt-report-modal, .aksrt-after-sale-apply-modal) .form-upload .preview .add,
:is(.order-comment-from, .aksrt-report-modal, .aksrt-after-sale-apply-modal) .form-upload .preview .preview-item {
width: 80px;
height: 80px;
border-radius: 10px;
@@ -575,14 +576,14 @@ body.aksrt-modal-runtime-open {
此前这里的 left/top:50% + translate 版本与该规则叠加导致过约束,
加号被推到 75%/75% 只剩“L”形,故整体删除、统一走主样式包。 */
:is(.order-comment-from, .aksrt-report-modal) .form-upload .preview .preview-item img {
:is(.order-comment-from, .aksrt-report-modal, .aksrt-after-sale-apply-modal) .form-upload .preview .preview-item img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
:is(.order-comment-from, .aksrt-report-modal) .form-upload .preview .preview-item .remove {
:is(.order-comment-from, .aksrt-report-modal, .aksrt-after-sale-apply-modal) .form-upload .preview .preview-item .remove {
position: absolute;
top: 2px;
right: 2px;
@@ -597,7 +598,7 @@ body.aksrt-modal-runtime-open {
cursor: pointer;
}
:is(.order-comment-from, .aksrt-report-modal) .form-upload .hide {
:is(.order-comment-from, .aksrt-report-modal, .aksrt-after-sale-apply-modal) .form-upload .hide {
display: none !important;
}
@@ -612,3 +613,181 @@ body.aksrt-modal-runtime-open {
width: calc(100vw - 20px);
}
}
/* ---- 售后弹窗(申请 / 售后详细 / 超时效提示,参考主题 modal-mini 几何 1:1)----
三个弹窗与禁封申诉弹窗同几何:data_class='modal-mini'(宽 400px),圆角与
底色走 --main-radius / --main-bg-color;移动端由 --bottom 底部抽屉接管。 */
.aksrt-after-sale-apply-modal:not(.aksrt-modal-runtime--bottom) .aksrt-modal-runtime__dialog,
.aksrt-after-sale-detail-modal:not(.aksrt-modal-runtime--bottom) .aksrt-modal-runtime__dialog,
.aksrt-after-sale-expired-modal:not(.aksrt-modal-runtime--bottom) .aksrt-modal-runtime__dialog {
width: min(400px, calc(100vw - 32px));
border-radius: var(--main-radius, 8px);
background: var(--main-bg-color, #fff);
}
.aksrt-after-sale-apply-modal .aksrt-modal-runtime__skeleton,
.aksrt-after-sale-detail-modal .aksrt-modal-runtime__skeleton,
.aksrt-after-sale-expired-modal .aksrt-modal-runtime__skeleton {
padding: 24px 18px;
}
/* 蓝色炫彩标题区(参考主题 zib_get_modal_colorful_header('jb-blue', ...)):120px
绝对定位横幅 + 旋转彩斑,后续内容整体下移 120px,与禁封记录弹窗同构。 */
.aksrt-after-sale-apply-modal .modal-colorful-header,
.aksrt-after-sale-detail-modal .modal-colorful-header,
.aksrt-after-sale-expired-modal .modal-colorful-header,
.aksrt-after-sale-apply-modal .colorful-bg,
.aksrt-after-sale-detail-modal .colorful-bg,
.aksrt-after-sale-expired-modal .colorful-bg {
position: relative;
overflow: hidden;
color: var(--this-color);
background: var(--this-bg);
background-size: 120%;
}
.aksrt-after-sale-apply-modal .modal-colorful-header,
.aksrt-after-sale-detail-modal .modal-colorful-header,
.aksrt-after-sale-expired-modal .modal-colorful-header {
position: absolute;
top: 0;
right: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
height: 120px;
border-radius: var(--main-radius, 8px) var(--main-radius, 8px) 0 0;
}
.aksrt-after-sale-apply-modal .modal-colorful-header + *,
.aksrt-after-sale-detail-modal .modal-colorful-header + *,
.aksrt-after-sale-expired-modal .modal-colorful-header + * {
margin-top: 120px;
}
.aksrt-after-sale-apply-modal .modal-colorful-header > .close,
.aksrt-after-sale-detail-modal .modal-colorful-header > .close,
.aksrt-after-sale-expired-modal .modal-colorful-header > .close {
position: absolute;
top: 10px;
right: 10px;
z-index: 9;
color: var(--this-color);
font-size: 18px;
opacity: 0.6;
}
.aksrt-after-sale-apply-modal .colorful-bg > .colorful-make,
.aksrt-after-sale-apply-modal .colorful-bg > .colorful-make::before,
.aksrt-after-sale-apply-modal .colorful-bg > .colorful-make::after,
.aksrt-after-sale-detail-modal .colorful-bg > .colorful-make,
.aksrt-after-sale-detail-modal .colorful-bg > .colorful-make::before,
.aksrt-after-sale-detail-modal .colorful-bg > .colorful-make::after,
.aksrt-after-sale-expired-modal .colorful-bg > .colorful-make,
.aksrt-after-sale-expired-modal .colorful-bg > .colorful-make::before,
.aksrt-after-sale-expired-modal .colorful-bg > .colorful-make::after {
position: absolute;
color: var(--this-color);
background: var(--this-bg);
border-radius: 100%;
content: '';
}
.aksrt-after-sale-apply-modal .colorful-bg > .colorful-make,
.aksrt-after-sale-detail-modal .colorful-bg > .colorful-make,
.aksrt-after-sale-expired-modal .colorful-bg > .colorful-make {
top: 60%;
bottom: 0;
width: 323px;
height: 290px;
animation: aksrt-ban-colorful 120s linear infinite;
}
.aksrt-after-sale-apply-modal .colorful-bg > .colorful-make::before,
.aksrt-after-sale-detail-modal .colorful-bg > .colorful-make::before,
.aksrt-after-sale-expired-modal .colorful-bg > .colorful-make::before {
top: -325px;
right: -558px;
bottom: 0;
width: 677px;
height: 300px;
opacity: 0.6;
transform: rotate(341deg);
}
.aksrt-after-sale-apply-modal .colorful-bg > .colorful-make::after,
.aksrt-after-sale-detail-modal .colorful-bg > .colorful-make::after,
.aksrt-after-sale-expired-modal .colorful-bg > .colorful-make::after {
top: -170px;
bottom: 0;
left: -262px;
width: 402px;
height: 214px;
opacity: 0.4;
transform: rotate(326deg);
}
/* 售后原因徽章:与举报弹窗一致,可换行且间距走 mr6/mb6 工具类。 */
.aksrt-after-sale-apply-modal .form-but-radio .badg {
cursor: pointer;
user-select: none;
}
/* 订单摘要与售后明细行 */
.aksrt-after-sale-summary__row,
.aksrt-after-sale-detail__row {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
margin-bottom: 6px;
}
.aksrt-after-sale-summary__row:last-child,
.aksrt-after-sale-detail__row:last-child {
margin-bottom: 0;
}
.aksrt-after-sale-summary__value,
.aksrt-after-sale-detail__value {
min-width: 0;
text-align: right;
word-break: break-all;
}
.aksrt-after-sale-detail__statusRow {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.aksrt-after-sale-reason-text {
color: var(--color-ink-2, #4e5358);
background: var(--surface-2, #eee);
border-radius: 6px;
padding: 10px 12px;
}
.aksrt-after-sale-images {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.aksrt-after-sale-images img {
width: 72px;
height: 72px;
object-fit: cover;
border-radius: 6px;
border: 1px solid var(--color-line, #e5e7eb);
display: block;
}
.aksrt-after-sale-status.is-pending { color: var(--color-warning, #f59e0b); }
.aksrt-after-sale-status.is-processing { color: var(--color-info, #3b82f6); }
.aksrt-after-sale-status.is-approved,
.aksrt-after-sale-status.is-completed { color: var(--color-success, #10b981); }
.aksrt-after-sale-status.is-rejected,
.aksrt-after-sale-status.is-cancelled { color: var(--color-error, #ef4444); }
+519
View File
@@ -0,0 +1,519 @@
/**
* AKSRT After-Sale — 前台售后弹窗(申请 / 超时效提示 / 售后详细)
*
* 与「禁封/举报」链路同构(report.js / user-ban.js):
* - [data-aksrt-after-sale-apply] → 申请售后弹窗(超时效则改为超时效提示弹窗)
* - [data-aksrt-after-sale-detail] → 售后详细弹窗(可再次申请 / 不可再申请提示)
* - 提交走 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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function colorfulHeader(title, icon) {
return '<div class="modal-colorful-header colorful-bg jb-blue">' +
'<button type="button" class="close" data-aksrt-modal-close aria-label="关闭"><svg class="ic-close" aria-hidden="true"><use xlink:href="#icon-close"></use></svg></button>' +
'<div class="colorful-make"></div>' +
'<div class="text-center"><div class="em2x"><i class="fa fas ' + escapeHtml(icon) + '" aria-hidden="true"></i></div>' +
'<div class="mt10 em12 padding-w10">' + escapeHtml(title) + '</div></div></div>';
}
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 '<div class="muted-box padding-10 mb20 em09 aksrt-after-sale-summary">' +
'<div class="aksrt-after-sale-summary__row"><span class="muted-2-color">商品</span>' +
'<span class="aksrt-after-sale-summary__value">' + escapeHtml(order.product_name || '订单') + '</span></div>' +
'<div class="aksrt-after-sale-summary__row"><span class="muted-2-color">订单号</span>' +
'<span class="aksrt-after-sale-summary__value">' + escapeHtml(order.order_number || '') + '</span></div>' +
'<div class="aksrt-after-sale-summary__row"><span class="muted-2-color">实付</span>' +
'<span class="aksrt-after-sale-summary__value">' + orderAmountText(order) + '</span></div>' +
'</div>';
}
function buildApplyHtml(order) {
var cfg = settings();
var reasons = reasonOptions();
var materials = materialOptions();
var html = '<div class="aksrt-after-sale-apply-modal">';
html += colorfulHeader('申请售后', 'fa-undo-alt');
html += '<form class="aksrt-after-sale-form" data-aksrt-after-sale-order="' + escapeHtml((order && order.order_number) || '') + '" novalidate>';
html += '<div class="scroll-y mini-scrollbar max-vh5">';
html += orderSummaryHtml(order);
if (reasons.length) {
html += '<div class="mb20">';
html += '<div class="em09 muted-2-color mb6">售后原因</div>';
html += '<div class="form-radio form-but-radio">';
reasons.forEach(function (reason) {
html += '<span class="mr6 mb6 badg p2-10 pointer" data-aksrt-after-sale-reason="' + escapeHtml(reason.name) + '" data-reason-slug="' + escapeHtml(reason.slug) + '">' + escapeHtml(reason.name) + '</span>';
});
html += '</div></div>';
}
materials.forEach(function (label) {
html += '<div class="mb10"><div class="muted-2-color mb6">' + escapeHtml(label) + '</div>' +
'<input class="form-control" type="text" maxlength="120" data-aksrt-after-sale-material="' + escapeHtml(label) + '" placeholder="请输入' + escapeHtml(label) + '"></div>';
});
var placeholder = String(cfg.reasonPlaceholder || '必填,请您详细填写申请原因');
html += '<div class="mb10"><div class="muted-2-color mb6">申请说明</div>' +
'<textarea class="form-control" name="reason" rows="3" maxlength="2000" required placeholder="' + escapeHtml(placeholder) + '"></textarea></div>';
if (cfg.imgEnabled) {
html += '<div class="form-upload mb10">' +
'<div class="em09 muted-2-color mb6">上传图片</div>' +
'<div class="preview"><div class="add" data-aksrt-after-sale-add role="button" tabindex="0" aria-label="上传图片"></div></div>' +
'<input class="hide" type="file" accept="image/gif,image/jpeg,image/jpg,image/png,image/webp" data-aksrt-after-sale-file>' +
'<div class="muted-2-color em09 mt6">最多上传 ' + MAX_IMAGES + ' 张图片</div>' +
'</div>';
}
// 申请售后说明(后台 after_sale_desc,服务端已 wp_kses_post)。
var desc = String(cfg.desc || '');
if (desc) {
html += '<div class="em09 padding-h10 muted-box mb10">' + desc + '</div>';
}
html += '</div>';
html += '<div class="but-average modal-buts">' +
'<button type="button" class="but" data-aksrt-modal-close>取消</button>' +
'<button type="button" class="but c-blue" data-aksrt-after-sale-submit><i class="fa fas fa-check" aria-hidden="true"></i>确认提交</button>' +
'</div>';
html += '</form></div>';
return html;
}
function detailRow(label, value) {
if (value === '' || value == null) return '';
return '<div class="aksrt-after-sale-detail__row"><span class="muted-2-color">' + escapeHtml(label) + '</span>' +
'<span class="aksrt-after-sale-detail__value">' + escapeHtml(value) + '</span></div>';
}
function buildDetailHtml(order) {
var afterSale = (order && order.after_sale) || {};
var latest = afterSale.latest || {};
var status = String(latest.status || '');
var html = '<div class="aksrt-after-sale-detail-modal">';
html += colorfulHeader('售后详细', 'fa-clipboard-list');
html += '<div class="scroll-y mini-scrollbar max-vh5 aksrt-after-sale-detail__body">';
html += orderSummaryHtml(order);
html += '<div class="aksrt-after-sale-detail__statusRow">' +
'<span class="muted-2-color em09">售后状态</span>' +
'<span class="badg p2-10 aksrt-after-sale-status is-' + escapeHtml(status) + '">' + escapeHtml(STATUS_LABELS[status] || status) + '</span>' +
'</div>';
html += '<div class="aksrt-after-sale-detail__rows">';
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 += '</div>';
if (latest.reason) {
html += '<div class="mb10"><div class="muted-2-color em09 mb6">申请原因</div>' +
'<div class="em09 aksrt-after-sale-reason-text">' + escapeHtml(latest.reason).replace(/\r\n|\r|\n/g, '<br>') + '</div></div>';
}
var images = Array.isArray(latest.images) ? latest.images : [];
if (images.length) {
html += '<div class="mb10"><div class="muted-2-color em09 mb6">举证图片</div><div class="aksrt-after-sale-images">';
images.forEach(function (url) {
html += '<a href="' + escapeHtml(url) + '" target="_blank" rel="noopener noreferrer"><img src="' + escapeHtml(url) + '" alt=""></a>';
});
html += '</div></div>';
}
html += '</div>';
if (afterSale.can_reapply) {
html += '<div class="but-average modal-buts">' +
'<button type="button" class="but" data-aksrt-modal-close>关闭</button>' +
'<button type="button" class="but c-blue" data-aksrt-after-sale-reapply="' + escapeHtml((order && order.order_number) || '') + '"><i class="fa fas fa-undo-alt" aria-hidden="true"></i>再次申请售后</button>' +
'</div>';
} else {
// 不可再申请售后(工单未结束 / 已完结 / 超过时效):仅提示 + 我知道了。
html += '<div class="aksrt-after-sale-detail__blocked">' +
'<div class="c-yellow box-body">' + escapeHtml(REAPPLY_BLOCKED_TEXT) + '</div>' +
'<div class="but-average modal-buts mt10"><button type="button" class="but c-blue" data-aksrt-modal-close>我知道了</button></div>' +
'</div>';
}
html += '</div>';
return html;
}
function buildExpiredHtml() {
var text = String(settings().expiredText || '抱歉,您的订单已超过售后申请时效');
var html = '<div class="aksrt-after-sale-expired-modal">';
html += colorfulHeader('售后提示', 'fa-clock');
// 超过时效提示(后台 after_sale_expired_text,服务端已 wp_kses_post,支持 HTML)。
html += '<div class="aksrt-after-sale-expired__body em09">' + text + '</div>';
html += '<div class="but-average modal-buts"><button type="button" class="but c-blue" data-aksrt-modal-close>我知道了</button></div>';
html += '</div>';
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',
});
}
function loadOrder(number) {
if (!window.AksrtRequest || typeof window.AksrtRequest.request !== 'function') {
return Promise.reject(new Error('请求组件加载失败,请刷新页面后重试'));
}
return window.AksrtRequest.request('/orders/' + encodeURIComponent(number), { method: 'GET' })
.then(function (result) { return (result && result.data) || null; });
}
function openApplyFlow(number) {
if (!number) return;
loadOrder(number).then(function (order) {
var afterSale = order && order.after_sale ? order.after_sale : null;
if (!order || !afterSale || !afterSale.enabled) {
notyf('售后申请暂不可用', 'danger');
return;
}
if (afterSale.expired) { openExpired(); return; }
if (afterSale.has_application) { openDetail(order); return; }
openApply(order);
}).catch(function (error) {
notyf((error && error.message) || '订单加载失败,请刷新后重试', 'danger');
});
}
function openDetailFlow(number) {
if (!number) return;
loadOrder(number).then(function (order) {
var afterSale = order && order.after_sale ? order.after_sale : null;
if (!order || !afterSale || !afterSale.enabled || !afterSale.has_application) {
notyf('暂无售后记录', 'danger');
return;
}
openDetail(order);
}).catch(function (error) {
notyf((error && error.message) || '订单加载失败,请刷新后重试', 'danger');
});
}
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 = '&times;';
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 number = reapplyBtn.getAttribute('data-aksrt-after-sale-reapply') || '';
window.AksrtModalRuntime.close('replace');
openApplyFlow(number);
}
});
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(apply.getAttribute('data-aksrt-after-sale-apply') || '');
return;
}
var detail = event.target.closest('[data-aksrt-after-sale-detail]');
if (detail) {
event.preventDefault();
openDetailFlow(detail.getAttribute('data-aksrt-after-sale-detail') || '');
}
});
}
jQuery(document).off('auto_fun.aksrt-after-sale').on('auto_fun.aksrt-after-sale', initAfterSaleButtons);
})();
+29 -35
View File
@@ -7,6 +7,7 @@
"use strict";
var instances = new WeakMap();
var liveStates = [];
function request(path, body, method, operationKey) {
var options = { method: method || "POST" };
@@ -199,7 +200,8 @@
html += '<button class="btn-outline" type="button" data-aksrt-modal-close>关闭</button>';
if (allowedActions.can_cancel) html += '<button class="btn-ghost" type="button" data-aksrt-order-cancel="' + escapeHtml(order.order_number) + '">关闭订单</button>';
if (allowedActions.can_pay) html += '<button class="btn-primary" type="button" data-aksrt-order-pay="' + escapeHtml(order.order_number) + '" data-aksrt-order-method="' + escapeHtml(paymentMethod.value || "") + '">继续支付</button>';
if (allowedActions.can_after_sale) html += '<form class="aksrt-order-detail__after-sale-form" data-aksrt-after-sale-form="' + escapeHtml(order.order_number) + '"><label>申请售后原因<textarea name="reason" required maxlength="2000" rows="3"></textarea></label><button class="btn-ghost" type="submit">提交售后申请</button></form>';
// 售后申请为独立弹窗,不再占用订单详情(order-after-sale.js)。
html += afterSaleButtons(order.order_number, order.after_sale || {});
html += "</footer>";
session.content.innerHTML = html;
@@ -275,10 +277,24 @@
function actionHtml(order) {
var allowedActions = order.allowed_actions || {};
var afterSale = order.after_sale || {};
var html = '<button class="btn-outline" type="button" data-aksrt-order-detail="' + escapeHtml(order.order_number) + '">订单详情</button>';
if (allowedActions.can_cancel) html += '<button class="btn-ghost" type="button" data-aksrt-order-cancel="' + escapeHtml(order.order_number) + '">关闭订单</button>';
if (allowedActions.can_pay) html += '<button class="btn-primary" type="button" data-aksrt-order-pay="' + escapeHtml(order.order_number) + '" data-aksrt-order-method="' + escapeHtml((order.payment_method || {}).value || "") + '">继续支付</button>';
if (allowedActions.can_after_sale) html += '<button class="btn-outline" type="button" data-aksrt-order-detail="' + escapeHtml(order.order_number) + '">申请售后</button>';
// 售后入口:申请售后 / 售后详细走 order-after-sale.js 的独立弹窗。
if (afterSale.enabled) html += afterSaleButtons(order.order_number, afterSale);
return html;
}
// 售后按钮(列表卡片与订单详情弹窗共用):已申请 → 售后详细,否则 → 申请售后。
function afterSaleButtons(orderNumber, afterSale) {
var html = "";
if (!afterSale || !afterSale.enabled) return html;
if (afterSale.has_application) {
html += '<button class="btn-outline" type="button" data-aksrt-after-sale-detail="' + escapeHtml(orderNumber) + '">售后详细</button>';
return html;
}
html += '<button class="btn-outline" type="button" data-aksrt-after-sale-apply="' + escapeHtml(orderNumber) + '">申请售后</button>';
return html;
}
@@ -555,42 +571,12 @@
if (cancel && root.contains(cancel)) { event.preventDefault(); closeOrder(state, cancel); }
};
state.onRootSubmit = function (event) {
var afterSaleForm = closest(event.target, "[data-aksrt-after-sale-form]");
if (afterSaleForm) {
event.preventDefault();
var number = afterSaleForm.getAttribute("data-aksrt-after-sale-form") || "";
var reason = afterSaleForm.querySelector("[name=reason]");
if (!number || !reason || !String(reason.value || "").trim()) return;
request(orderPath(number, "/after-sale"), { reason: String(reason.value).trim() }, "POST", window.AksrtRequest.createIdempotencyKey()).then(function () {
notyf("售后申请已提交", "success");
invalidateOrder(number);
openDetail(state, number);
refreshView(state);
}).catch(function (error) { notyf(errorMessage(error, "售后申请提交失败"), "danger"); });
return;
}
var form = closest(event.target, "[data-aksrt-orders-search]");
if (!form || !root.contains(form)) return;
event.preventDefault();
var input = form.querySelector("#aksrt-order-number");
loadView(state, { orderNumber: input ? String(input.value || "").trim() : "", page: 1 });
};
state.onDocumentSubmit = function (event) {
var afterSaleForm = closest(event.target, "[data-aksrt-after-sale-form]");
if (!afterSaleForm || root.contains(afterSaleForm) || event.__aksrtAfterSaleHandled) return;
var number = afterSaleForm.getAttribute("data-aksrt-after-sale-form") || "";
if (!activeDetail(state, number)) return;
event.__aksrtAfterSaleHandled = true;
event.preventDefault();
var reason = afterSaleForm.querySelector("[name=reason]");
if (!number || !reason || !String(reason.value || "").trim()) return;
request(orderPath(number, "/after-sale"), { reason: String(reason.value).trim() }, "POST", window.AksrtRequest.createIdempotencyKey()).then(function () {
notyf("售后申请已提交", "success");
invalidateOrder(number);
openDetail(state, number);
refreshView(state);
}).catch(function (error) { notyf(errorMessage(error, "售后申请提交失败"), "danger"); });
};
state.onModalClose = function (event) {
var detail = event && event.detail ? event.detail : {};
if (!state.detailSession || detail.session !== state.detailSession) return;
@@ -648,11 +634,11 @@
root.addEventListener("click", state.onRootClick);
root.addEventListener("submit", state.onRootSubmit);
document.addEventListener("submit", state.onDocumentSubmit);
document.addEventListener("aksrt:modal:close", state.onModalClose);
document.addEventListener("click", state.onDocumentClick);
state.countdownTimer = window.setInterval(function () { updateCountdowns(state); }, 1000);
instances.set(root, state);
liveStates.push(state);
}
function rootsIn(root) {
@@ -674,14 +660,22 @@
if (activeDetail(state, state.detailOrderNumber)) window.AksrtModalRuntime.close("replace");
item.removeEventListener("click", state.onRootClick);
item.removeEventListener("submit", state.onRootSubmit);
document.removeEventListener("submit", state.onDocumentSubmit);
document.removeEventListener("aksrt:modal:close", state.onModalClose);
document.removeEventListener("click", state.onDocumentClick);
window.clearInterval(state.countdownTimer);
instances.delete(item);
liveStates = liveStates.filter(function (entry) { return entry !== state; });
});
}
window.AKSRTOrderCenter = { init: init, destroy: destroy };
// refresh 供售后弹窗(order-after-sale.js)提交/取消后刷新列表与统计。
function refresh(orderNumber) {
invalidateOrder(orderNumber);
liveStates.forEach(function (state) {
refreshView(state);
});
}
window.AKSRTOrderCenter = { init: init, destroy: destroy, refresh: refresh };
jQuery(document).off("auto_fun.aksrt-order-center").on("auto_fun.aksrt-order-center", function () { init(document); });
}());