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:
2026-10-06 22:21:47 +08:00
parent b447c41f13
commit 1a9a2762f3
19 changed files with 1835 additions and 86 deletions
+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.