' + escapeHtml(order.product_name || "订单") + "
订单号 " + escapeHtml(order.order_number) + "
diff --git a/assets/css/frontend/modal-runtime.css b/assets/css/frontend/modal-runtime.css index 5252ffcc..feb3c5dd 100644 --- a/assets/css/frontend/modal-runtime.css +++ b/assets/css/frontend/modal-runtime.css @@ -1,804 +1,804 @@ -/** - * Modal runtime + ajax list primitives (shared). - * - * @package AKSRT - */ - -/* ---- portal token bridge ---- - * - * 弹窗由 runtime portal 到
,因此它位于任何「页面级 token 作用域」之外。 - * 典型陷阱:路由样式把语义别名挂在页面上,例如 - * - * .aksrt-orders-page { --orders-ink: var(--color-ink, #333); } - * - * 弹窗内部再去 `var(--orders-ink)` 时该变量不存在,整条声明会被静默丢弃—— - * 表现为边框消失、次级文字变透明,而不是抛错,排查成本很高。 - * - * 因此共享调色板在这里声明:本壳层永远在 portal 内,任何弹窗都能直接消费 - * `--modal-*`。路由样式应把自己的局部别名映射到这里,而不是映射到页面级变量。 - */ -.aksrt-modal-runtime { - --modal-ink: var(--color-ink, #333333); - --modal-ink-2: var(--color-ink-2, #4e5358); - --modal-muted: var(--color-ink-3, #777777); - --modal-line: var(--color-line-stronger, rgba(50, 50, 50, 0.12)); - --modal-line-soft: var(--color-line, rgba(50, 50, 50, 0.06)); - --modal-surface: var(--surface-1, #ffffff); - --modal-surface-2: var(--surface-2, #eeeeee); - --modal-surface-3: var(--surface-4, #f8f8f8); - --modal-primary: var(--color-primary, #f04494); - --modal-on-primary: var(--color-on-primary, #ffffff); - --modal-warning: var(--color-warning, #f59e0b); - --modal-success: var(--color-success, #10b981); - --modal-danger: var(--color-danger, #ef4444); -} - -/* ---- scroll lock ---- */ -html.aksrt-modal-runtime-open, -body.aksrt-modal-runtime-open { - overflow: hidden; -} - -/* ---- 对话框宽度类不得作用于壳 ---- - 触发器沿用参考主题的 data-class 语义(modal-mini/full-sm 等), - 但这些类落在 fixed 全屏壳上时,.modal-mini 的 width:400px + - auto 边距会把壳压成居中单列,蒙版随之只覆盖一列。 */ -.aksrt-modal-runtime.modal-mini, -.aksrt-modal-runtime.full-sm { - width: auto; - max-width: none; - margin: 0; -} - -/* ---- modal runtime shell ---- */ -.aksrt-modal-runtime { - position: fixed; - inset: 0; - z-index: 100050; - display: flex; - align-items: center; - justify-content: center; - opacity: 0; - visibility: hidden; - transition: opacity 0.2s ease, visibility 0.2s ease; - pointer-events: none; -} - -.aksrt-modal-runtime.is-open { - opacity: 1; - visibility: visible; - pointer-events: auto; -} - -.aksrt-modal-runtime__overlay { - position: absolute; - inset: 0; - /* 参考主题 .modal-backdrop 同款:纯黑 50%,无模糊、无色调偏移 */ - background: rgba(0, 0, 0, 0.5); -} - -.aksrt-modal-runtime__dialog { - position: relative; - z-index: 1; - width: min(440px, calc(100vw - 32px)); - max-height: min(88vh, 720px); - overflow: auto; - background: var(--surface-1, #fff); - border-radius: 16px; - box-shadow: 0 18px 48px rgba(15, 18, 28, 0.22); - transform: translateY(12px) scale(0.98); - transition: transform 0.22s cubic-bezier(0.22, 1, 0.36, 1); - color: var(--color-ink, #1f2937); -} - -.aksrt-modal-runtime.is-open .aksrt-modal-runtime__dialog { - transform: translateY(0) scale(1); -} - -.aksrt-modal-runtime--bottom { - align-items: flex-end; -} - -.aksrt-modal-runtime--bottom .aksrt-modal-runtime__dialog { - width: 100%; - max-width: 100%; - border-radius: 16px 16px 0 0; - max-height: 90vh; - transform: translateY(24px); -} - -.aksrt-modal-runtime--bottom.is-open .aksrt-modal-runtime__dialog { - transform: translateY(0); -} - -.aksrt-modal-runtime__header { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - padding: 16px 18px 0; -} - -.aksrt-modal-runtime__title { - margin: 0; - font-size: 17px; - line-height: 1.35; -} - -.aksrt-modal-runtime__content { - padding: 18px 18px 16px; -} - -.aksrt-modal-runtime__loading, -.aksrt-modal-runtime__error-body { - padding: 28px 8px; - text-align: center; - color: var(--color-ink-2, #6b7280); - font-size: 14px; -} - -.aksrt-modal-runtime__skeleton { - display: grid; - gap: 12px; - padding: 8px 2px; -} - -.aksrt-modal-runtime__skeleton-line, -.aksrt-modal-runtime__skeleton-block { - display: block; - border-radius: 8px; - background: linear-gradient(90deg, rgba(148, 163, 184, 0.16), rgba(148, 163, 184, 0.3), rgba(148, 163, 184, 0.16)); - background-size: 200% 100%; - animation: aksrt-modal-skeleton 1.2s ease-in-out infinite; -} - -.aksrt-modal-runtime__skeleton-line { - width: 72%; - height: 12px; -} - -.aksrt-modal-runtime__skeleton-line.is-wide { - width: 92%; - height: 18px; -} - -.aksrt-modal-runtime__skeleton-line.is-short { - width: 44%; -} - -.aksrt-modal-runtime__skeleton-block { - width: 100%; - min-height: 84px; -} - -.aksrt-modal-runtime__retry { - cursor: pointer; -} - -@keyframes aksrt-modal-skeleton { - 0% { background-position: 100% 0; } - 100% { background-position: -100% 0; } -} - -.aksrt-modal-runtime .touch.border-title, -.aksrt-modal-runtime .border-title { - font-size: 16px; - font-weight: 700; - margin: 0 0 14px; - display: flex; - align-items: baseline; - flex-wrap: wrap; - gap: 6px; -} - -.aksrt-modal-runtime .aksrt-modal-runtime__close { - position: absolute; - top: 10px; - right: 10px; - width: 32px; - height: 32px; - border: 0; - border-radius: 999px; - background: transparent; - color: var(--color-ink-3, #9ca3af); - cursor: pointer; - font-size: 18px; - line-height: 1; -} - -.aksrt-modal-runtime .but-average.modal-buts, -.aksrt-modal-runtime .modal-buts { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 10px; - margin-top: 16px; -} - -.aksrt-modal-runtime .but-average.modal-buts .but, -.aksrt-modal-runtime .modal-buts .but, -.aksrt-modal-runtime .modal-buts button { - min-height: 40px; - border-radius: 10px; - border: 1px solid var(--color-line, #e5e7eb); - background: var(--surface-4, #f3f4f6); - color: var(--color-ink, #111); - font-weight: 600; - cursor: pointer; -} - -.aksrt-modal-runtime .modal-buts .but.c-blue, -.aksrt-modal-runtime .modal-buts button.c-blue, -.aksrt-modal-runtime .modal-buts .btn-primary { - background: var(--color-primary, #f04494); - border-color: color-mix(in srgb, var(--color-primary, #f04494) 70%, #000 0%); - color: var(--color-on-primary, #fff); -} - -/* ---- 签到详情弹窗(参考主题 refresh_modal modal-mini 同构壳) ---- - 内容为服务端 HTML(modal-colorful-header + modal-body),与参考主题 - .modal-dialog.modal-mini 几何 1:1:宽 400px、圆角 var(--main-radius)。 */ -.aksrt-modal-runtime.aksrt-checkin-details-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-modal-runtime.aksrt-checkin-details-modal .aksrt-modal-runtime__content { - padding: 0; -} - -.aksrt-modal-runtime.aksrt-checkin-details-modal .aksrt-modal-runtime__skeleton { - padding: 24px 18px; -} - -/* ---- ajax list ---- */ -.ajaxpager.is-ajax-loading { - position: relative; - min-height: 80px; -} - -/* 骨架默认收起,由 JS 加 .is-loading 展开(参考主题 post_ajax 的 beforeSend - remove + fadeIn / success .hide() 语义等价物)。 - 这里不依赖 .ajaxpager 祖先:首页 tab pane、归档流、评论列表的 loader 层级各不 - 相同,而且并非每个 loader 都在 bootstrap 的 .tab-pane 里(bootstrap 只负责 - 隐藏 tab-pane 内的骨架),统一按类名自身控制。 */ -.post_ajax_loader { - display: none; -} - -.post_ajax_loader.is-loading { - display: block; - animation: aksrtFadeIn 0.2s ease both; -} - -.ajax-item.is-fading { - opacity: 0.35; - transition: opacity 0.15s ease; -} - -/* ---- 举报弹窗(参考主题报告弹窗几何 1:1)---- - 宽度对齐参考主题 Bootstrap 默认模态(>=768px 600px),圆角/底色走 - --main-radius / --main-bg-color;移动端仍由 --bottom 底部抽屉接管。 */ -.aksrt-report-modal:not(.aksrt-modal-runtime--bottom) .aksrt-modal-runtime__dialog { - width: min(600px, calc(100vw - 32px)); - border-radius: var(--main-radius, 8px); - background: var(--main-bg-color, #fff); -} - -/* edit-header:参考主题弹窗头(关闭钮靠右,红色警示徽标 + 标题居左)。 */ -.aksrt-report-modal .edit-header { - position: relative; - padding-right: 32px; -} - -.aksrt-report-modal .edit-header .close { - position: absolute; - right: 0; - top: 50%; - transform: translateY(-50%); - float: none; - font-size: 20px; - line-height: 1; - cursor: pointer; - background: none; - border: 0; - padding: 2px 6px; -} - -/* 原因徽章行:与参考主题一致,徽章可换行且间距走 mr6/mb6 工具类。 */ -.aksrt-report-modal .form-but-radio .badg { - cursor: pointer; - user-select: none; -} - -/* ---- 禁封用户弹窗(参考主题 set_user_ban 模态框几何 1:1)---- - 参考主题 zib_get_edit_user_ban_link 未传 data-class → 走 Bootstrap 默认 - .modal-dialog(>=768px 宽 600px);data-height=360 → .modal-content 初始 - 高度 360 后动画至内容高度;mobile-bottom → <769px 底部抽屉。 - 此处以 min-height:360px 复刻初始高度契约,桌面宽取 600px。 */ -.aksrt-user-ban-modal:not(.aksrt-modal-runtime--bottom) .aksrt-modal-runtime__dialog { - width: min(600px, calc(100vw - 32px)); - border-radius: var(--main-radius, 8px); - background: var(--main-bg-color, #fff); -} - -/* ---- 查看禁封状态 / 禁封申诉 / 禁封记录弹窗(参考主题 modal-mini 几何 1:1)---- - 参考主题三个链接均传 data_class='modal-mini'(宽 400px)、data-height=240。 */ -.aksrt-user-ban-info-modal:not(.aksrt-modal-runtime--bottom) .aksrt-modal-runtime__dialog, -.aksrt-user-ban-appeal-modal:not(.aksrt-modal-runtime--bottom) .aksrt-modal-runtime__dialog, -.aksrt-user-ban-log-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-user-ban-info-modal .aksrt-modal-runtime__skeleton, -.aksrt-user-ban-appeal-modal .aksrt-modal-runtime__skeleton, -.aksrt-user-ban-log-modal .aksrt-modal-runtime__skeleton { - padding: 24px 18px; -} - -.aksrt-user-ban-modal .aksrt-modal-runtime__skeleton { - padding: 24px 18px; -} - -/* edit-header:与禁封表单头部同构(关闭钮靠右,红色警示徽标 + 标题居左)。 */ -.aksrt-user-ban-modal .edit-header, -.aksrt-user-ban-appeal-modal .edit-header { - position: relative; - padding-right: 32px; -} - -.aksrt-user-ban-modal .edit-header .close, -.aksrt-user-ban-appeal-modal .edit-header .close { - position: absolute; - right: 0; - top: 50%; - transform: translateY(-50%); - float: none; - font-size: 20px; - line-height: 1; - cursor: pointer; - background: none; - border: 0; - padding: 2px 6px; -} - -/* 解封视图多彩横幅(参考主题 zib_get_modal_colorful_header('c-yellow', ...)): - 120px 高绝对定位横幅 + 旋转彩斑,后续表单整体下移 120px。 */ -.aksrt-user-ban-modal .modal-colorful-header, -.aksrt-user-ban-modal .colorful-bg, -.aksrt-user-ban-info-modal .modal-colorful-header, -.aksrt-user-ban-info-modal .colorful-bg, -.aksrt-user-ban-log-modal .modal-colorful-header, -.aksrt-user-ban-log-modal .colorful-bg { - position: relative; - overflow: hidden; - color: var(--this-color); - background: var(--this-bg); - background-size: 120%; -} - -.aksrt-user-ban-modal .modal-colorful-header, -.aksrt-user-ban-info-modal .modal-colorful-header, -.aksrt-user-ban-log-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-user-ban-modal .modal-colorful-header + *, -.aksrt-user-ban-info-modal .modal-colorful-header + *, -.aksrt-user-ban-log-modal .modal-colorful-header + * { - margin-top: 120px; -} - -.aksrt-user-ban-modal .modal-colorful-header > .close, -.aksrt-user-ban-info-modal .modal-colorful-header > .close, -.aksrt-user-ban-log-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-user-ban-modal .colorful-bg > .colorful-make, -.aksrt-user-ban-modal .colorful-bg > .colorful-make::before, -.aksrt-user-ban-modal .colorful-bg > .colorful-make::after, -.aksrt-user-ban-info-modal .colorful-bg > .colorful-make, -.aksrt-user-ban-info-modal .colorful-bg > .colorful-make::before, -.aksrt-user-ban-info-modal .colorful-bg > .colorful-make::after, -.aksrt-user-ban-log-modal .colorful-bg > .colorful-make, -.aksrt-user-ban-log-modal .colorful-bg > .colorful-make::before, -.aksrt-user-ban-log-modal .colorful-bg > .colorful-make::after { - position: absolute; - color: var(--this-color); - background: var(--this-bg); - border-radius: 100%; - content: ''; -} - -.aksrt-user-ban-modal .colorful-bg > .colorful-make, -.aksrt-user-ban-info-modal .colorful-bg > .colorful-make, -.aksrt-user-ban-log-modal .colorful-bg > .colorful-make { - top: 60%; - bottom: 0; - width: 323px; - height: 290px; - animation: aksrt-ban-colorful 120s linear infinite; -} - -.aksrt-user-ban-modal .colorful-bg > .colorful-make::before, -.aksrt-user-ban-info-modal .colorful-bg > .colorful-make::before, -.aksrt-user-ban-log-modal .colorful-bg > .colorful-make::before { - top: -325px; - right: -558px; - bottom: 0; - width: 677px; - height: 300px; - opacity: 0.6; - transform: rotate(341deg); -} - -.aksrt-user-ban-modal .colorful-bg > .colorful-make::after, -.aksrt-user-ban-info-modal .colorful-bg > .colorful-make::after, -.aksrt-user-ban-log-modal .colorful-bg > .colorful-make::after { - top: -170px; - bottom: 0; - left: -262px; - width: 402px; - height: 214px; - opacity: 0.4; - transform: rotate(326deg); -} - -.aksrt-user-ban-modal .colorful-bg > .colorful-make + *, -.aksrt-user-ban-info-modal .colorful-bg > .colorful-make + *, -.aksrt-user-ban-log-modal .colorful-bg > .colorful-make + * { - position: relative; -} - -/* 单选徽章行(参考主题 .form-but-radio 契约):input 由 main.css 隐藏, - label 可点;选中态由 main.css 的 input:checked + .but-radio 驱动。 */ -.aksrt-user-ban-modal .form-but-radio label { - cursor: pointer; -} - -.aksrt-user-ban-modal .form-but-radio .but-radio { - user-select: none; -} - -/* 开关行(删除全部内容 / 禁止用户申诉):原生 checkbox 隐藏, - 视觉态由 main.css 的 input:checked ~ .form-switch 驱动。 */ -.aksrt-user-ban-modal .aksrt-user-ban-form label { - user-select: none; -} - -@keyframes aksrt-ban-colorful { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } -} - -/* ---- order review modal content ---- */ -.order-comment-from .shop-score-box { - display: flex; - align-items: center; - gap: 8px; - min-height: 36px; -} - -.order-comment-from .score-title { - width: 4.5em; - flex: 0 0 auto; - font-size: 13px; - color: var(--color-ink-2, #6b7280); -} - -.order-comment-from .score-icon { - display: inline-flex; - gap: 4px; - align-items: center; - height: 30px; -} - -.order-comment-from .icon-star { - display: inline-flex; - width: 22px; - height: 22px; - color: #d1d5db; - cursor: pointer; - transition: color 0.12s ease, transform 0.12s ease; -} - -.order-comment-from .icon-star svg { - width: 22px; - height: 22px; - fill: currentColor; -} - -.order-comment-from .icon-star.active, -.order-comment-from .icon-star.hover { - color: #fc7049; -} - -.order-comment-from .icon-star.hover { - transform: scale(1.08); -} - -.order-comment-from .score-text { - min-width: 3em; - font-size: 13px; - color: var(--color-ink-2, #6b7280); -} - -.order-comment-from textarea.form-control { - width: 100%; - border: 1px solid var(--color-line, #e5e7eb); - border-radius: 10px; - padding: 10px 12px; - font-size: 14px; - resize: vertical; - min-height: 96px; - background: var(--surface-1, #fff); - color: var(--color-ink, #111); -} - -: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, .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; - border: 1px dashed var(--color-line, #d1d5db); - background: var(--surface-4, #f9fafb); - position: relative; - overflow: hidden; - cursor: pointer; -} - -/* 加号图标不在此重复定义:main.css 已有参考主题原生实现 - (.preview .add:before/:after —— 25px 圆角杠,inset:0 + margin:auto 居中)。 - 此前这里的 left/top:50% + translate 版本与该规则叠加导致过约束, - 加号被推到 75%/75% 只剩“L”形,故整体删除、统一走主样式包。 */ - -: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, .aksrt-after-sale-apply-modal) .form-upload .preview .preview-item .remove { - position: absolute; - top: 2px; - right: 2px; - width: 20px; - height: 20px; - border: 0; - border-radius: 999px; - background: rgba(0, 0, 0, 0.55); - color: #fff; - font-size: 12px; - line-height: 1; - cursor: pointer; -} - -:is(.order-comment-from, .aksrt-report-modal, .aksrt-after-sale-apply-modal) .form-upload .hide { - display: none !important; -} - -.order-comment-from .review-error { - color: #ef4444; - font-size: 13px; - margin-top: 8px; -} - -@media (max-width: 768px) { - .aksrt-modal-runtime:not(.aksrt-modal-runtime--bottom) .aksrt-modal-runtime__dialog { - 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-order-detail-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, -.aksrt-order-detail-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, -.aksrt-order-detail-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 + *, -.aksrt-order-detail-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, -.aksrt-order-detail-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-order-detail-modal .colorful-bg > .colorful-make, -.aksrt-after-sale-expired-modal .colorful-bg > .colorful-make::before, -.aksrt-order-detail-modal .colorful-bg > .colorful-make::before, -.aksrt-after-sale-expired-modal .colorful-bg > .colorful-make::after, -.aksrt-order-detail-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, -.aksrt-order-detail-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, -.aksrt-order-detail-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, -.aksrt-order-detail-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); } +/** + * Modal runtime + ajax list primitives (shared). + * + * @package AKSRT + */ + +/* ---- portal token bridge ---- + * + * 弹窗由 runtime portal 到 ,因此它位于任何「页面级 token 作用域」之外。 + * 典型陷阱:路由样式把语义别名挂在页面上,例如 + * + * .aksrt-orders-page { --orders-ink: var(--color-ink, #333); } + * + * 弹窗内部再去 `var(--orders-ink)` 时该变量不存在,整条声明会被静默丢弃—— + * 表现为边框消失、次级文字变透明,而不是抛错,排查成本很高。 + * + * 因此共享调色板在这里声明:本壳层永远在 portal 内,任何弹窗都能直接消费 + * `--modal-*`。路由样式应把自己的局部别名映射到这里,而不是映射到页面级变量。 + */ +.aksrt-modal-runtime { + --modal-ink: var(--color-ink, #333333); + --modal-ink-2: var(--color-ink-2, #4e5358); + --modal-muted: var(--color-ink-3, #777777); + --modal-line: var(--color-line-stronger, rgba(50, 50, 50, 0.12)); + --modal-line-soft: var(--color-line, rgba(50, 50, 50, 0.06)); + --modal-surface: var(--surface-1, #ffffff); + --modal-surface-2: var(--surface-2, #eeeeee); + --modal-surface-3: var(--surface-4, #f8f8f8); + --modal-primary: var(--color-primary, #f04494); + --modal-on-primary: var(--color-on-primary, #ffffff); + --modal-warning: var(--color-warning, #f59e0b); + --modal-success: var(--color-success, #10b981); + --modal-danger: var(--color-danger, #ef4444); +} + +/* ---- scroll lock ---- */ +html.aksrt-modal-runtime-open, +body.aksrt-modal-runtime-open { + overflow: hidden; +} + +/* ---- 对话框宽度类不得作用于壳 ---- + 触发器沿用参考主题的 data-class 语义(modal-mini/full-sm 等), + 但这些类落在 fixed 全屏壳上时,.modal-mini 的 width:400px + + auto 边距会把壳压成居中单列,蒙版随之只覆盖一列。 */ +.aksrt-modal-runtime.modal-mini, +.aksrt-modal-runtime.full-sm { + width: auto; + max-width: none; + margin: 0; +} + +/* ---- modal runtime shell ---- */ +.aksrt-modal-runtime { + position: fixed; + inset: 0; + z-index: 100050; + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + visibility: hidden; + transition: opacity 0.2s ease, visibility 0.2s ease; + pointer-events: none; +} + +.aksrt-modal-runtime.is-open { + opacity: 1; + visibility: visible; + pointer-events: auto; +} + +.aksrt-modal-runtime__overlay { + position: absolute; + inset: 0; + /* 参考主题 .modal-backdrop 同款:纯黑 50%,无模糊、无色调偏移 */ + background: rgba(0, 0, 0, 0.5); +} + +.aksrt-modal-runtime__dialog { + position: relative; + z-index: 1; + width: min(440px, calc(100vw - 32px)); + max-height: min(88vh, 720px); + overflow: auto; + background: var(--surface-1, #fff); + border-radius: 16px; + box-shadow: 0 18px 48px rgba(15, 18, 28, 0.22); + transform: translateY(12px) scale(0.98); + transition: transform 0.22s cubic-bezier(0.22, 1, 0.36, 1); + color: var(--color-ink, #1f2937); +} + +.aksrt-modal-runtime.is-open .aksrt-modal-runtime__dialog { + transform: translateY(0) scale(1); +} + +.aksrt-modal-runtime--bottom { + align-items: flex-end; +} + +.aksrt-modal-runtime--bottom .aksrt-modal-runtime__dialog { + width: 100%; + max-width: 100%; + border-radius: 16px 16px 0 0; + max-height: 90vh; + transform: translateY(24px); +} + +.aksrt-modal-runtime--bottom.is-open .aksrt-modal-runtime__dialog { + transform: translateY(0); +} + +.aksrt-modal-runtime__header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 16px 18px 0; +} + +.aksrt-modal-runtime__title { + margin: 0; + font-size: 17px; + line-height: 1.35; +} + +.aksrt-modal-runtime__content { + padding: 18px 18px 16px; +} + +.aksrt-modal-runtime__loading, +.aksrt-modal-runtime__error-body { + padding: 28px 8px; + text-align: center; + color: var(--color-ink-2, #6b7280); + font-size: 14px; +} + +.aksrt-modal-runtime__skeleton { + display: grid; + gap: 12px; + padding: 8px 2px; +} + +.aksrt-modal-runtime__skeleton-line, +.aksrt-modal-runtime__skeleton-block { + display: block; + border-radius: 8px; + background: linear-gradient(90deg, rgba(148, 163, 184, 0.16), rgba(148, 163, 184, 0.3), rgba(148, 163, 184, 0.16)); + background-size: 200% 100%; + animation: aksrt-modal-skeleton 1.2s ease-in-out infinite; +} + +.aksrt-modal-runtime__skeleton-line { + width: 72%; + height: 12px; +} + +.aksrt-modal-runtime__skeleton-line.is-wide { + width: 92%; + height: 18px; +} + +.aksrt-modal-runtime__skeleton-line.is-short { + width: 44%; +} + +.aksrt-modal-runtime__skeleton-block { + width: 100%; + min-height: 84px; +} + +.aksrt-modal-runtime__retry { + cursor: pointer; +} + +@keyframes aksrt-modal-skeleton { + 0% { background-position: 100% 0; } + 100% { background-position: -100% 0; } +} + +.aksrt-modal-runtime .touch.border-title, +.aksrt-modal-runtime .border-title { + font-size: 16px; + font-weight: 700; + margin: 0 0 14px; + display: flex; + align-items: baseline; + flex-wrap: wrap; + gap: 6px; +} + +.aksrt-modal-runtime .aksrt-modal-runtime__close { + position: absolute; + top: 10px; + right: 10px; + width: 32px; + height: 32px; + border: 0; + border-radius: 999px; + background: transparent; + color: var(--color-ink-3, #9ca3af); + cursor: pointer; + font-size: 18px; + line-height: 1; +} + +.aksrt-modal-runtime .but-average.modal-buts, +.aksrt-modal-runtime .modal-buts { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px; + margin-top: 16px; +} + +.aksrt-modal-runtime .but-average.modal-buts .but, +.aksrt-modal-runtime .modal-buts .but, +.aksrt-modal-runtime .modal-buts button { + min-height: 40px; + border-radius: 10px; + border: 1px solid var(--color-line, #e5e7eb); + background: var(--surface-4, #f3f4f6); + color: var(--color-ink, #111); + font-weight: 600; + cursor: pointer; +} + +.aksrt-modal-runtime .modal-buts .but.c-blue, +.aksrt-modal-runtime .modal-buts button.c-blue, +.aksrt-modal-runtime .modal-buts .btn-primary { + background: var(--color-primary, #f04494); + border-color: color-mix(in srgb, var(--color-primary, #f04494) 70%, #000 0%); + color: var(--color-on-primary, #fff); +} + +/* ---- 签到详情弹窗(参考主题 refresh_modal modal-mini 同构壳) ---- + 内容为服务端 HTML(modal-colorful-header + modal-body),与参考主题 + .modal-dialog.modal-mini 几何 1:1:宽 400px、圆角 var(--main-radius)。 */ +.aksrt-modal-runtime.aksrt-checkin-details-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-modal-runtime.aksrt-checkin-details-modal .aksrt-modal-runtime__content { + padding: 0; +} + +.aksrt-modal-runtime.aksrt-checkin-details-modal .aksrt-modal-runtime__skeleton { + padding: 24px 18px; +} + +/* ---- ajax list ---- */ +.ajaxpager.is-ajax-loading { + position: relative; + min-height: 80px; +} + +/* 骨架默认收起,由 JS 加 .is-loading 展开(参考主题 post_ajax 的 beforeSend + remove + fadeIn / success .hide() 语义等价物)。 + 这里不依赖 .ajaxpager 祖先:首页 tab pane、归档流、评论列表的 loader 层级各不 + 相同,而且并非每个 loader 都在 bootstrap 的 .tab-pane 里(bootstrap 只负责 + 隐藏 tab-pane 内的骨架),统一按类名自身控制。 */ +.post_ajax_loader { + display: none; +} + +.post_ajax_loader.is-loading { + display: block; + animation: aksrtFadeIn 0.2s ease both; +} + +.ajax-item.is-fading { + opacity: 0.35; + transition: opacity 0.15s ease; +} + +/* ---- 举报弹窗(参考主题报告弹窗几何 1:1)---- + 宽度对齐参考主题 Bootstrap 默认模态(>=768px 600px),圆角/底色走 + --main-radius / --main-bg-color;移动端仍由 --bottom 底部抽屉接管。 */ +.aksrt-report-modal:not(.aksrt-modal-runtime--bottom) .aksrt-modal-runtime__dialog { + width: min(600px, calc(100vw - 32px)); + border-radius: var(--main-radius, 8px); + background: var(--main-bg-color, #fff); +} + +/* edit-header:参考主题弹窗头(关闭钮靠右,红色警示徽标 + 标题居左)。 */ +.aksrt-report-modal .edit-header { + position: relative; + padding-right: 32px; +} + +.aksrt-report-modal .edit-header .close { + position: absolute; + right: 0; + top: 50%; + transform: translateY(-50%); + float: none; + font-size: 20px; + line-height: 1; + cursor: pointer; + background: none; + border: 0; + padding: 2px 6px; +} + +/* 原因徽章行:与参考主题一致,徽章可换行且间距走 mr6/mb6 工具类。 */ +.aksrt-report-modal .form-but-radio .badg { + cursor: pointer; + user-select: none; +} + +/* ---- 禁封用户弹窗(参考主题 set_user_ban 模态框几何 1:1)---- + 参考主题 zib_get_edit_user_ban_link 未传 data-class → 走 Bootstrap 默认 + .modal-dialog(>=768px 宽 600px);data-height=360 → .modal-content 初始 + 高度 360 后动画至内容高度;mobile-bottom → <769px 底部抽屉。 + 此处以 min-height:360px 复刻初始高度契约,桌面宽取 600px。 */ +.aksrt-user-ban-modal:not(.aksrt-modal-runtime--bottom) .aksrt-modal-runtime__dialog { + width: min(600px, calc(100vw - 32px)); + border-radius: var(--main-radius, 8px); + background: var(--main-bg-color, #fff); +} + +/* ---- 查看禁封状态 / 禁封申诉 / 禁封记录弹窗(参考主题 modal-mini 几何 1:1)---- + 参考主题三个链接均传 data_class='modal-mini'(宽 400px)、data-height=240。 */ +.aksrt-user-ban-info-modal:not(.aksrt-modal-runtime--bottom) .aksrt-modal-runtime__dialog, +.aksrt-user-ban-appeal-modal:not(.aksrt-modal-runtime--bottom) .aksrt-modal-runtime__dialog, +.aksrt-user-ban-log-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-user-ban-info-modal .aksrt-modal-runtime__skeleton, +.aksrt-user-ban-appeal-modal .aksrt-modal-runtime__skeleton, +.aksrt-user-ban-log-modal .aksrt-modal-runtime__skeleton { + padding: 24px 18px; +} + +.aksrt-user-ban-modal .aksrt-modal-runtime__skeleton { + padding: 24px 18px; +} + +/* edit-header:与禁封表单头部同构(关闭钮靠右,红色警示徽标 + 标题居左)。 */ +.aksrt-user-ban-modal .edit-header, +.aksrt-user-ban-appeal-modal .edit-header { + position: relative; + padding-right: 32px; +} + +.aksrt-user-ban-modal .edit-header .close, +.aksrt-user-ban-appeal-modal .edit-header .close { + position: absolute; + right: 0; + top: 50%; + transform: translateY(-50%); + float: none; + font-size: 20px; + line-height: 1; + cursor: pointer; + background: none; + border: 0; + padding: 2px 6px; +} + +/* 解封视图多彩横幅(参考主题 zib_get_modal_colorful_header('c-yellow', ...)): + 120px 高绝对定位横幅 + 旋转彩斑,后续表单整体下移 120px。 */ +.aksrt-user-ban-modal .modal-colorful-header, +.aksrt-user-ban-modal .colorful-bg, +.aksrt-user-ban-info-modal .modal-colorful-header, +.aksrt-user-ban-info-modal .colorful-bg, +.aksrt-user-ban-log-modal .modal-colorful-header, +.aksrt-user-ban-log-modal .colorful-bg { + position: relative; + overflow: hidden; + color: var(--this-color); + background: var(--this-bg); + background-size: 120%; +} + +.aksrt-user-ban-modal .modal-colorful-header, +.aksrt-user-ban-info-modal .modal-colorful-header, +.aksrt-user-ban-log-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-user-ban-modal .modal-colorful-header + *, +.aksrt-user-ban-info-modal .modal-colorful-header + *, +.aksrt-user-ban-log-modal .modal-colorful-header + * { + margin-top: 120px; +} + +.aksrt-user-ban-modal .modal-colorful-header > .close, +.aksrt-user-ban-info-modal .modal-colorful-header > .close, +.aksrt-user-ban-log-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-user-ban-modal .colorful-bg > .colorful-make, +.aksrt-user-ban-modal .colorful-bg > .colorful-make::before, +.aksrt-user-ban-modal .colorful-bg > .colorful-make::after, +.aksrt-user-ban-info-modal .colorful-bg > .colorful-make, +.aksrt-user-ban-info-modal .colorful-bg > .colorful-make::before, +.aksrt-user-ban-info-modal .colorful-bg > .colorful-make::after, +.aksrt-user-ban-log-modal .colorful-bg > .colorful-make, +.aksrt-user-ban-log-modal .colorful-bg > .colorful-make::before, +.aksrt-user-ban-log-modal .colorful-bg > .colorful-make::after { + position: absolute; + color: var(--this-color); + background: var(--this-bg); + border-radius: 100%; + content: ''; +} + +.aksrt-user-ban-modal .colorful-bg > .colorful-make, +.aksrt-user-ban-info-modal .colorful-bg > .colorful-make, +.aksrt-user-ban-log-modal .colorful-bg > .colorful-make { + top: 60%; + bottom: 0; + width: 323px; + height: 290px; + animation: aksrt-ban-colorful 120s linear infinite; +} + +.aksrt-user-ban-modal .colorful-bg > .colorful-make::before, +.aksrt-user-ban-info-modal .colorful-bg > .colorful-make::before, +.aksrt-user-ban-log-modal .colorful-bg > .colorful-make::before { + top: -325px; + right: -558px; + bottom: 0; + width: 677px; + height: 300px; + opacity: 0.6; + transform: rotate(341deg); +} + +.aksrt-user-ban-modal .colorful-bg > .colorful-make::after, +.aksrt-user-ban-info-modal .colorful-bg > .colorful-make::after, +.aksrt-user-ban-log-modal .colorful-bg > .colorful-make::after { + top: -170px; + bottom: 0; + left: -262px; + width: 402px; + height: 214px; + opacity: 0.4; + transform: rotate(326deg); +} + +.aksrt-user-ban-modal .colorful-bg > .colorful-make + *, +.aksrt-user-ban-info-modal .colorful-bg > .colorful-make + *, +.aksrt-user-ban-log-modal .colorful-bg > .colorful-make + * { + position: relative; +} + +/* 单选徽章行(参考主题 .form-but-radio 契约):input 由 main.css 隐藏, + label 可点;选中态由 main.css 的 input:checked + .but-radio 驱动。 */ +.aksrt-user-ban-modal .form-but-radio label { + cursor: pointer; +} + +.aksrt-user-ban-modal .form-but-radio .but-radio { + user-select: none; +} + +/* 开关行(删除全部内容 / 禁止用户申诉):原生 checkbox 隐藏, + 视觉态由 main.css 的 input:checked ~ .form-switch 驱动。 */ +.aksrt-user-ban-modal .aksrt-user-ban-form label { + user-select: none; +} + +@keyframes aksrt-ban-colorful { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +/* ---- order review modal content ---- */ +.order-comment-from .shop-score-box { + display: flex; + align-items: center; + gap: 8px; + min-height: 36px; +} + +.order-comment-from .score-title { + width: 4.5em; + flex: 0 0 auto; + font-size: 13px; + color: var(--color-ink-2, #6b7280); +} + +.order-comment-from .score-icon { + display: inline-flex; + gap: 4px; + align-items: center; + height: 30px; +} + +.order-comment-from .icon-star { + display: inline-flex; + width: 22px; + height: 22px; + color: #d1d5db; + cursor: pointer; + transition: color 0.12s ease, transform 0.12s ease; +} + +.order-comment-from .icon-star svg { + width: 22px; + height: 22px; + fill: currentColor; +} + +.order-comment-from .icon-star.active, +.order-comment-from .icon-star.hover { + color: #fc7049; +} + +.order-comment-from .icon-star.hover { + transform: scale(1.08); +} + +.order-comment-from .score-text { + min-width: 3em; + font-size: 13px; + color: var(--color-ink-2, #6b7280); +} + +.order-comment-from textarea.form-control { + width: 100%; + border: 1px solid var(--color-line, #e5e7eb); + border-radius: 10px; + padding: 10px 12px; + font-size: 14px; + resize: vertical; + min-height: 96px; + background: var(--surface-1, #fff); + color: var(--color-ink, #111); +} + +: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, .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; + border: 1px dashed var(--color-line, #d1d5db); + background: var(--surface-4, #f9fafb); + position: relative; + overflow: hidden; + cursor: pointer; +} + +/* 加号图标不在此重复定义:main.css 已有参考主题原生实现 + (.preview .add:before/:after —— 25px 圆角杠,inset:0 + margin:auto 居中)。 + 此前这里的 left/top:50% + translate 版本与该规则叠加导致过约束, + 加号被推到 75%/75% 只剩“L”形,故整体删除、统一走主样式包。 */ + +: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, .aksrt-after-sale-apply-modal) .form-upload .preview .preview-item .remove { + position: absolute; + top: 2px; + right: 2px; + width: 20px; + height: 20px; + border: 0; + border-radius: 999px; + background: rgba(0, 0, 0, 0.55); + color: #fff; + font-size: 12px; + line-height: 1; + cursor: pointer; +} + +:is(.order-comment-from, .aksrt-report-modal, .aksrt-after-sale-apply-modal) .form-upload .hide { + display: none !important; +} + +.order-comment-from .review-error { + color: #ef4444; + font-size: 13px; + margin-top: 8px; +} + +@media (max-width: 768px) { + .aksrt-modal-runtime:not(.aksrt-modal-runtime--bottom) .aksrt-modal-runtime__dialog { + 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-order-detail-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, +.aksrt-order-detail-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, +.aksrt-order-detail-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 + *, +.aksrt-order-detail-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, +.aksrt-order-detail-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-order-detail-modal .colorful-bg > .colorful-make, +.aksrt-after-sale-expired-modal .colorful-bg > .colorful-make::before, +.aksrt-order-detail-modal .colorful-bg > .colorful-make::before, +.aksrt-after-sale-expired-modal .colorful-bg > .colorful-make::after, +.aksrt-order-detail-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, +.aksrt-order-detail-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, +.aksrt-order-detail-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, +.aksrt-order-detail-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); } diff --git a/assets/css/routes/orders.css b/assets/css/routes/orders.css index 4b3ac8f4..ce2833ff 100644 --- a/assets/css/routes/orders.css +++ b/assets/css/routes/orders.css @@ -1,204 +1,204 @@ -/* Knowledge-payment order center */ -.aksrt-orders-page { - --orders-ink: var(--color-ink, #1d2738); - --orders-muted: var(--color-ink-3, #758096); - --orders-line: color-mix(in srgb, var(--color-line, #e1e6ef) 88%, transparent); - padding: 0; - color: var(--orders-ink); -} - -.uc-orders-panel__head { - display: grid; - grid-template-columns: minmax(0, 1fr) minmax(285px, 42%); - gap: 28px; - align-items: end; - padding: 4px 0 22px; - border-bottom: 1px solid var(--orders-line); -} - -.aksrt-orders-page__eyebrow { - margin: 0 0 7px; - color: var(--color-primary, #3b82f6); - font-size: 11px; - font-weight: 750; - letter-spacing: .14em; - text-transform: uppercase; -} - -.uc-orders-panel__title { display: flex; align-items: center; gap: 9px; font-size: 21px; } -.uc-orders-panel__title-icon { color: var(--color-primary, #3b82f6); font-size: 17px; } -.uc-orders-panel__title-wrap > p:last-child { margin-bottom: 0; } - -.aksrt-orders-page__stats { - display: grid; - grid-template-columns: repeat(3, 1fr); - border: 1px solid var(--orders-line); - border-radius: 14px; - background: var(--surface-1, #fff); - overflow: hidden; -} -.aksrt-orders-page__stat { display: grid; gap: 5px; padding: 13px 15px; border-right: 1px solid var(--orders-line); } -.aksrt-orders-page__stat:last-child { border-right: 0; } -.aksrt-orders-page__stat span { color: var(--orders-muted); font-size: 11px; font-weight: 600; } -.aksrt-orders-page__stat strong { font-size: 21px; line-height: 1; font-variant-numeric: tabular-nums; } -.aksrt-orders-page__stat strong.is-pending { color: var(--color-warning, #c87819); } -.aksrt-orders-page__stat strong.is-paid { color: var(--color-success, #168560); } - -.aksrt-orders-page__search { display: flex; gap: 8px; align-items: center; margin: 20px 0 12px; padding: 7px; border: 1px solid var(--orders-line); border-radius: 12px; background: var(--surface-1, #fff); } -.aksrt-orders-page__search > i { margin-left: 7px; color: var(--orders-muted); font-size: 13px; } -.aksrt-orders-page__search input { flex: 1; min-width: 0; border: 0; outline: 0; background: transparent; color: var(--orders-ink); font-size: 13px; } -.aksrt-orders-page__search button, .aksrt-orders-page__card-actions .btn-primary, .aksrt-orders-page__card-actions .btn-outline, .aksrt-orders-page__card-actions .btn-ghost, .aksrt-orders-page__empty .btn-outline { display: inline-flex; align-items: center; justify-content: center; min-height: 34px; padding: 0 13px; border-radius: 8px; border: 1px solid transparent; font-size: 12px; font-weight: 650; text-decoration: none; cursor: pointer; } -.aksrt-orders-page .btn-primary { background: var(--color-primary, #3b82f6); color: var(--color-on-primary, #fff); } -.aksrt-orders-page .btn-outline { border-color: var(--orders-line); background: var(--surface-1, #fff); color: var(--orders-ink); } -.aksrt-orders-page .btn-ghost { background: transparent; color: var(--orders-muted); } -.aksrt-orders-page__filter-toggle { background: var(--surface-2, #f4f6f9); color: var(--orders-ink); white-space: nowrap; } - -.aksrt-orders-page__filter-panel { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin: -2px 0 14px; padding: 12px; border-radius: 10px; background: var(--surface-2, #f4f6f9); } -.aksrt-orders-page__filter-panel[hidden] { display: none !important; } -.aksrt-orders-page__filter-group { display: flex; flex: 1 1 100%; gap: 10px; align-items: center; } -.aksrt-orders-page__filter-label { flex: 0 0 58px; color: var(--orders-muted); font-size: 12px; font-weight: 650; } -.aksrt-orders-page__status-filters, .aksrt-orders-page__type-filters { display: flex; flex-wrap: wrap; gap: 6px; } -.aksrt-orders-page__filter-chip { display: inline-flex; gap: 5px; align-items: center; padding: 5px 9px; border: 1px solid var(--orders-line); border-radius: 999px; background: var(--surface-1, #fff); color: var(--orders-muted); font: inherit; font-size: 12px; text-decoration: none; cursor: pointer; } -.aksrt-orders-page__filter-chip.is-active { border-color: var(--color-primary, #3b82f6); background: color-mix(in srgb, var(--color-primary, #3b82f6) 10%, var(--surface-1, #fff)); color: var(--color-primary, #3b82f6); font-weight: 700; } -.aksrt-orders-page__filter-count { display: inline-grid; min-width: 17px; height: 17px; padding: 0 4px; place-items: center; border-radius: 999px; background: var(--color-warning, #c87819); color: #fff; font-size: 10px; line-height: 1; } -.aksrt-orders-page__clear { margin-left: auto; padding: 0; border: 0; background: transparent; color: var(--color-primary, #3b82f6); font: inherit; font-size: 12px; text-decoration: none; cursor: pointer; } - -.aksrt-orders-page__list { display: grid; gap: 10px; } -.aksrt-orders-page__card { overflow: hidden; border: 1px solid var(--orders-line); border-radius: 13px; background: var(--surface-1, #fff); transition: border-color .18s ease, transform .18s ease, box-shadow .18s ease; } -.aksrt-orders-page__card:hover { border-color: color-mix(in srgb, var(--color-primary, #3b82f6) 36%, var(--orders-line)); box-shadow: 0 8px 22px rgba(30, 48, 79, .07); transform: translateY(-1px); } -.aksrt-orders-page__card-rail { display: flex; gap: 10px; align-items: center; padding: 9px 14px; background: var(--surface-2, #f7f8fb); color: var(--orders-muted); font-size: 11px; } -.aksrt-orders-page__card-rail time { margin-left: auto; font-variant-numeric: tabular-nums; } -.aksrt-orders-page__card-status { font-weight: 700; } -.aksrt-orders-page__card-status.is-pending { color: var(--color-warning, #c87819); } -.aksrt-orders-page__card-status.is-paid { color: var(--color-success, #168560); } -.aksrt-orders-page__card-main { display: grid; grid-template-columns: 52px minmax(0, 1fr) auto; gap: 13px; align-items: center; padding: 14px; } -.aksrt-orders-page__card-img, .aksrt-orders-page__card-art { width: 52px; height: 52px; border-radius: 10px; object-fit: cover; } -.aksrt-orders-page__card-art { display: grid; place-items: center; color: var(--color-primary, #3b82f6); background: color-mix(in srgb, var(--color-primary, #3b82f6) 10%, var(--surface-2, #f4f6f9)); } -.aksrt-orders-page__card-copy { min-width: 0; } -.aksrt-orders-page__card-copy h3 { display: -webkit-box; margin: 0 0 5px; overflow: hidden; color: var(--orders-ink); font-size: 14px; line-height: 1.35; -webkit-box-orient: vertical; -webkit-line-clamp: 2; } -.aksrt-orders-page__card-copy p { margin: 0; color: var(--orders-muted); font-family: var(--font-mono, monospace); font-size: 11px; } -.aksrt-orders-page__card-value { display: grid; gap: 3px; text-align: right; } -.aksrt-orders-page__card-value strong { color: var(--orders-ink); font-size: 15px; font-variant-numeric: tabular-nums; } -.aksrt-orders-page__card-value span { color: var(--orders-muted); font-size: 11px; } -.aksrt-orders-page__card-actions { display: flex; gap: 7px; align-items: center; justify-content: flex-end; padding: 9px 14px; border-top: 1px solid var(--orders-line); } -.aksrt-orders-page__countdown { margin-right: auto; color: var(--color-warning, #c87819); font-size: 11px; font-variant-numeric: tabular-nums; } - -.aksrt-orders-page__pagination { display: flex; justify-content: center; gap: 6px; margin-top: 20px; } -.aksrt-orders-page__page-chip { display: grid; width: 31px; height: 31px; padding: 0; place-items: center; border: 1px solid var(--orders-line); border-radius: 8px; background: var(--surface-1, #fff); color: var(--orders-muted); font: inherit; font-size: 12px; text-decoration: none; cursor: pointer; } -.aksrt-orders-page__page-chip.is-active { border-color: var(--color-primary, #3b82f6); background: var(--color-primary, #3b82f6); color: #fff; } -.aksrt-orders-page [data-aksrt-order-list], .aksrt-orders-page__stats, .aksrt-orders-page__pagination { transition: opacity .16s ease; } -.aksrt-orders-page.is-loading [data-aksrt-order-list], .aksrt-orders-page.is-loading .aksrt-orders-page__stats, .aksrt-orders-page.is-loading .aksrt-orders-page__pagination { opacity: .55; pointer-events: none; } -.aksrt-orders-page__empty { padding: 52px 20px; border: 1px dashed var(--orders-line); border-radius: 13px; background: var(--surface-2, #f7f8fb); text-align: center; } -.aksrt-orders-page__empty i { display: block; margin-bottom: 12px; color: var(--color-primary, #3b82f6); font-size: 30px; } -.aksrt-orders-page__empty h3 { margin: 0 0 7px; font-size: 15px; }.aksrt-orders-page__empty p { margin: 0 0 16px; color: var(--orders-muted); font-size: 13px; } - -/* ---- Order detail modal (front-end order center) ---- */ -/* 弹窗 portal 到 ,读不到页面上的 `--orders-*` 别名,因此把本模块的局部 - 别名映射到 modal-runtime.css 声明的共享 portal 基座 `--modal-*`; - 路由文件不应再自行推导调色板值。 */ -.aksrt-order-detail-modal { - --detail-ink: var(--modal-ink); - --detail-muted: var(--modal-muted); - --detail-line: var(--modal-line); - --detail-surface: var(--modal-surface); - --detail-surface-2: var(--modal-surface-2); - --detail-surface-3: var(--modal-surface-3); - --detail-primary: var(--modal-primary); - --detail-warning: var(--modal-warning); - --detail-success: var(--modal-success); -} - -/* 订单详情弹窗:与售后弹窗统一为 modal-mini 几何(400px);标题区改用蓝色炫彩头,运行时不再渲染默认标题栏。 */ -.aksrt-order-detail-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-order-detail-modal .aksrt-modal-runtime__content { display: flex; flex-direction: column; gap: 18px; padding: 16px 18px 0; } - -.aksrt-order-detail__skeleton { display: grid; gap: 12px; padding: 4px 0; } - -/* Status banner — a single local variable drives the whole colour scheme. */ -.aksrt-order-detail__hero { --hero-accent: var(--detail-primary); --hero-text: color-mix(in srgb, var(--hero-accent) 72%, var(--detail-ink)); display: flex; align-items: center; gap: 13px; padding: 15px; border: 1px solid color-mix(in srgb, var(--hero-accent) 20%, transparent); border-radius: 14px; background: color-mix(in srgb, var(--hero-accent) 12%, var(--detail-surface)); } -.aksrt-order-detail__hero.is-pending { --hero-accent: var(--detail-warning); } -.aksrt-order-detail__hero.is-paid { --hero-accent: var(--detail-success); } -.aksrt-order-detail__hero.is-refunded, .aksrt-order-detail__hero.is-closed { --hero-accent: var(--detail-muted); } -.aksrt-order-detail__hero-icon { display: grid; flex: 0 0 auto; width: 42px; height: 42px; place-items: center; border-radius: 12px; background: color-mix(in srgb, var(--hero-accent) 16%, transparent); color: var(--hero-text); font-size: 19px; } -.aksrt-order-detail__hero-copy { display: grid; flex: 1 1 auto; gap: 4px; min-width: 0; } -.aksrt-order-detail__status { color: var(--hero-text); font-size: 15px; font-weight: 750; line-height: 1.2; } -.aksrt-order-detail__desc { margin: 0; color: var(--detail-muted); font-size: 12px; line-height: 1.45; } -.aksrt-order-detail__countdown { color: var(--hero-text); font-size: 12px; font-variant-numeric: tabular-nums; } -.aksrt-order-detail__hero-amount { display: grid; flex: 0 0 auto; gap: 3px; justify-items: end; text-align: right; } -.aksrt-order-detail__hero-amount strong { color: var(--detail-ink); font-size: 21px; font-weight: 750; line-height: 1.1; letter-spacing: -.01em; font-variant-numeric: tabular-nums; } -.aksrt-order-detail__hero-amount span { color: var(--detail-muted); font-size: 11px; } - -.aksrt-order-detail__group { display: grid; gap: 10px; } -.aksrt-order-detail__group-title { display: flex; align-items: center; gap: 8px; margin: 0; color: var(--detail-ink); font-size: 13px; font-weight: 700; line-height: 1.3; } -.aksrt-order-detail__group-title::before { content: ""; width: 3px; height: 13px; border-radius: 2px; background: var(--detail-primary); } - -.aksrt-order-detail__product { display: grid; grid-template-columns: 54px minmax(0, 1fr); gap: 12px; align-items: center; padding: 12px; border: 1px solid var(--detail-line); border-radius: 12px; background: var(--detail-surface); } -.aksrt-order-detail__thumb { width: 54px; height: 54px; border-radius: 10px; object-fit: cover; } -.aksrt-order-detail__thumb--empty { display: grid; place-items: center; background: var(--detail-surface-2); color: var(--detail-primary); font-size: 19px; } -.aksrt-order-detail__product-copy { display: grid; gap: 6px; min-width: 0; } -.aksrt-order-detail__product-title { display: -webkit-box; overflow: hidden; color: var(--detail-ink); font-size: 14px; line-height: 1.45; -webkit-box-orient: vertical; -webkit-line-clamp: 2; } -.aksrt-order-detail__product-meta { display: inline-flex; justify-self: start; padding: 2px 8px; border-radius: 999px; background: color-mix(in srgb, var(--detail-primary) 9%, var(--detail-surface-2)); color: var(--detail-primary); font-size: 11px; font-weight: 650; line-height: 1.5; } - -.aksrt-order-detail__list { display: grid; margin: 0; overflow: hidden; border: 1px solid var(--detail-line); border-radius: 12px; background: var(--detail-surface); } -.aksrt-order-detail__list > div { display: grid; grid-template-columns: 84px minmax(0, 1fr); align-items: center; gap: 12px; padding: 11px 13px; border-bottom: 1px solid var(--detail-line); } -.aksrt-order-detail__list > div:last-child { border-bottom: 0; } -.aksrt-order-detail__list dt { color: var(--detail-muted); font-size: 12.5px; } -.aksrt-order-detail__list dd { display: flex; align-items: center; justify-content: flex-end; gap: 8px; min-width: 0; margin: 0; color: var(--detail-ink); font-size: 13px; font-variant-numeric: tabular-nums; text-align: right; } -.aksrt-order-detail__value-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.aksrt-order-detail__value-text.is-mono { font-family: var(--font-mono, monospace); font-size: 12.5px; letter-spacing: .01em; } -.aksrt-order-detail__copy { display: inline-grid; flex: 0 0 auto; width: 24px; height: 24px; padding: 0; place-items: center; border: 1px solid var(--detail-line); border-radius: 7px; background: var(--detail-surface-3); color: var(--detail-muted); font-size: 11px; line-height: 1; cursor: pointer; transition: color .15s ease, border-color .15s ease; } -.aksrt-order-detail__copy:hover { border-color: color-mix(in srgb, var(--detail-primary) 45%, var(--detail-line)); color: var(--detail-primary); } -.aksrt-order-detail__copy.is-copied { border-color: color-mix(in srgb, var(--detail-success) 45%, var(--detail-line)); color: var(--detail-success); } - -.aksrt-order-detail__refresh-state { display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: center; gap: 10px; padding: 12px 13px; border: 1px solid color-mix(in srgb, var(--detail-warning) 26%, transparent); border-radius: 12px; background: color-mix(in srgb, var(--detail-warning) 10%, var(--detail-surface-2)); color: var(--detail-ink); font-size: 12.5px; } -.aksrt-order-detail__refresh-state > i { color: var(--detail-warning); font-size: 15px; } -.aksrt-order-detail__refresh-state p { margin: 0; } -.aksrt-order-detail__refresh-state button { grid-column: 2; justify-self: start; } - -/* 底部操作区:按钮排布沿用主题 .but-average.modal-buts(禁封申诉弹窗同式),此处仅保留吸底定位与通栏背景。 - 主题 modal-buts 自带 margin-top:16px,吸底场景下用更高优先级清掉,避免内容与吸底条之间出现空隙。 */ -.aksrt-order-detail-modal .aksrt-order-detail__actions { position: sticky; bottom: 0; z-index: 2; margin: 0 -18px; padding: 13px 18px calc(13px + env(safe-area-inset-bottom)); border-top: 1px solid var(--detail-line); background: var(--detail-surface); } -.aksrt-order-detail-modal .aksrt-order-detail__actions.but-average.modal-buts { margin-top: 0; } - -/* Orders extensions */ -.aksrt-orders-page__extra-nav { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 12px; } -.aksrt-orders-page__extra-link { padding: 6px 12px; border-radius: 999px; background: var(--surface-2, #f3f4f6); color: inherit; text-decoration: none; font-size: 13px; } -.aksrt-orders-page__extra-link.is-active { background: var(--focus-color, var(--color-primary)); color: #fff; } -.aksrt-as-list { display: grid; gap: 12px; } -.aksrt-as-card { padding: 14px; border: 1px solid var(--main-border-color, #eee); border-radius: 10px; background: var(--main-bg-color, #fff); } -.aksrt-as-card__head { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-bottom: 6px; } -.aksrt-as-card__status { margin-left: auto; color: var(--focus-color, #2f6df6); font-weight: 600; } -.aksrt-as-timeline { margin: 10px 0 0; padding-left: 18px; color: var(--muted-2-color, #888); font-size: 12px; } -.aksrt-as-card__actions { display: flex; gap: 8px; margin-top: 10px; } -.aksrt-coupon-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 12px; } -.aksrt-coupon-card { padding: 12px; border: 1px dashed var(--focus-color, #2f6df6); border-radius: 10px; background: color-mix(in srgb, var(--focus-color, #2f6df6) 6%, #fff); } -.aksrt-coupon-card__value { color: var(--focus-color, #2f6df6); font-size: 20px; font-weight: 700; } -.aksrt-coupon-card__code { margin: 6px 0; font-family: ui-monospace, monospace; } -.aksrt-coupon-card__meta { margin-bottom: 4px; color: var(--muted-2-color, #888); font-size: 12px; } -.aksrt-orders-page__field { margin-bottom: 12px; } -.aksrt-orders-page__input, .aksrt-orders-page__select, .aksrt-orders-page__textarea { box-sizing: border-box; width: 100%; padding: 8px 10px; border: 1px solid var(--main-border-color, #ddd); border-radius: 6px; } -@media (max-width: 767px) { .uc-orders-panel__head { grid-template-columns: 1fr; gap: 17px; }.aksrt-orders-page__stats { max-width: none; }.aksrt-orders-page__search { flex-wrap: wrap; }.aksrt-orders-page__search input { min-width: 150px; }.aksrt-orders-page__search .btn-primary { margin-left: auto; }.aksrt-orders-page__filter-toggle { margin-left: auto; }.aksrt-orders-page__card-rail time { width: 100%; margin-left: 0; order: 3; }.aksrt-orders-page__card-main { grid-template-columns: 48px minmax(0, 1fr); }.aksrt-orders-page__card-img, .aksrt-orders-page__card-art { width: 48px; height: 48px; }.aksrt-orders-page__card-value { grid-column: 2; text-align: left; }.aksrt-orders-page__card-actions > :not(.aksrt-orders-page__countdown) { flex: 1 1 auto; }.aksrt-orders-page__countdown { width: 100%; }} -html.dark .aksrt-orders-page__stat, html.dark .aksrt-orders-page__card { background: var(--surface-1, #1a1d24); } html.dark .aksrt-orders-page__card-rail, html.dark .aksrt-orders-page__filter-panel { background: var(--surface-2, #12151b); } - -/* ---- 售后记录卡片(订单详情弹窗) ---- */ -.aksrt-order-detail__after-sales { display: grid; gap: 10px; margin: 0; padding: 0; list-style: none; } -.aksrt-order-detail__after-sale { --as-accent: var(--detail-muted); display: grid; gap: 10px; padding: 13px 14px; border: 1px solid var(--detail-line); border-left: 3px solid var(--as-accent); border-radius: 12px; background: var(--detail-surface); } -.aksrt-order-detail__after-sale.is-pending { --as-accent: var(--detail-warning); } -.aksrt-order-detail__after-sale.is-processing { --as-accent: var(--detail-primary); } -.aksrt-order-detail__after-sale.is-approved, .aksrt-order-detail__after-sale.is-completed { --as-accent: var(--detail-success); } -.aksrt-order-detail__after-sale.is-rejected, .aksrt-order-detail__after-sale.is-cancelled { --as-accent: var(--modal-danger, #ef4444); } -.aksrt-order-detail__after-sale-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; } -.aksrt-order-detail__after-sale-badge { padding: 3px 11px; border-radius: 999px; background: color-mix(in srgb, var(--as-accent) 12%, transparent); color: var(--as-accent); font-size: 12px; font-weight: 700; line-height: 1.5; } -.aksrt-order-detail__after-sale-head time { color: var(--detail-muted); font-size: 11.5px; font-variant-numeric: tabular-nums; } -.aksrt-order-detail__after-sale-reason { margin: 0; color: var(--detail-ink); font-size: 13px; line-height: 1.65; white-space: pre-wrap; word-break: break-word; } -.aksrt-order-detail__after-sale-meta { display: grid; margin: 0; overflow: hidden; border: 1px solid var(--detail-line); border-radius: 10px; background: var(--detail-surface-3); } -.aksrt-order-detail__after-sale-meta > div { display: grid; grid-template-columns: 76px minmax(0, 1fr); gap: 10px; padding: 9px 12px; border-bottom: 1px solid var(--detail-line); font-size: 12.5px; } -.aksrt-order-detail__after-sale-meta > div:last-child { border-bottom: 0; } -.aksrt-order-detail__after-sale-meta dt { color: var(--detail-muted); } -.aksrt-order-detail__after-sale-meta dd { margin: 0; color: var(--detail-ink); font-size: 12.5px; text-align: right; word-break: break-word; } -.aksrt-order-detail__after-sale-field { color: var(--detail-muted); } -.aksrt-order-detail__after-sale-foot { display: flex; flex-wrap: wrap; gap: 8px; } +/* Knowledge-payment order center */ +.aksrt-orders-page { + --orders-ink: var(--color-ink, #1d2738); + --orders-muted: var(--color-ink-3, #758096); + --orders-line: color-mix(in srgb, var(--color-line, #e1e6ef) 88%, transparent); + padding: 0; + color: var(--orders-ink); +} + +.uc-orders-panel__head { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(285px, 42%); + gap: 28px; + align-items: end; + padding: 4px 0 22px; + border-bottom: 1px solid var(--orders-line); +} + +.aksrt-orders-page__eyebrow { + margin: 0 0 7px; + color: var(--color-primary, #3b82f6); + font-size: 11px; + font-weight: 750; + letter-spacing: .14em; + text-transform: uppercase; +} + +.uc-orders-panel__title { display: flex; align-items: center; gap: 9px; font-size: 21px; } +.uc-orders-panel__title-icon { color: var(--color-primary, #3b82f6); font-size: 17px; } +.uc-orders-panel__title-wrap > p:last-child { margin-bottom: 0; } + +.aksrt-orders-page__stats { + display: grid; + grid-template-columns: repeat(3, 1fr); + border: 1px solid var(--orders-line); + border-radius: 14px; + background: var(--surface-1, #fff); + overflow: hidden; +} +.aksrt-orders-page__stat { display: grid; gap: 5px; padding: 13px 15px; border-right: 1px solid var(--orders-line); } +.aksrt-orders-page__stat:last-child { border-right: 0; } +.aksrt-orders-page__stat span { color: var(--orders-muted); font-size: 11px; font-weight: 600; } +.aksrt-orders-page__stat strong { font-size: 21px; line-height: 1; font-variant-numeric: tabular-nums; } +.aksrt-orders-page__stat strong.is-pending { color: var(--color-warning, #c87819); } +.aksrt-orders-page__stat strong.is-paid { color: var(--color-success, #168560); } + +.aksrt-orders-page__search { display: flex; gap: 8px; align-items: center; margin: 20px 0 12px; padding: 7px; border: 1px solid var(--orders-line); border-radius: 12px; background: var(--surface-1, #fff); } +.aksrt-orders-page__search > i { margin-left: 7px; color: var(--orders-muted); font-size: 13px; } +.aksrt-orders-page__search input { flex: 1; min-width: 0; border: 0; outline: 0; background: transparent; color: var(--orders-ink); font-size: 13px; } +.aksrt-orders-page__search button, .aksrt-orders-page__card-actions .btn-primary, .aksrt-orders-page__card-actions .btn-outline, .aksrt-orders-page__card-actions .btn-ghost, .aksrt-orders-page__empty .btn-outline { display: inline-flex; align-items: center; justify-content: center; min-height: 34px; padding: 0 13px; border-radius: 8px; border: 1px solid transparent; font-size: 12px; font-weight: 650; text-decoration: none; cursor: pointer; } +.aksrt-orders-page .btn-primary { background: var(--color-primary, #3b82f6); color: var(--color-on-primary, #fff); } +.aksrt-orders-page .btn-outline { border-color: var(--orders-line); background: var(--surface-1, #fff); color: var(--orders-ink); } +.aksrt-orders-page .btn-ghost { background: transparent; color: var(--orders-muted); } +.aksrt-orders-page__filter-toggle { background: var(--surface-2, #f4f6f9); color: var(--orders-ink); white-space: nowrap; } + +.aksrt-orders-page__filter-panel { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin: -2px 0 14px; padding: 12px; border-radius: 10px; background: var(--surface-2, #f4f6f9); } +.aksrt-orders-page__filter-panel[hidden] { display: none !important; } +.aksrt-orders-page__filter-group { display: flex; flex: 1 1 100%; gap: 10px; align-items: center; } +.aksrt-orders-page__filter-label { flex: 0 0 58px; color: var(--orders-muted); font-size: 12px; font-weight: 650; } +.aksrt-orders-page__status-filters, .aksrt-orders-page__type-filters { display: flex; flex-wrap: wrap; gap: 6px; } +.aksrt-orders-page__filter-chip { display: inline-flex; gap: 5px; align-items: center; padding: 5px 9px; border: 1px solid var(--orders-line); border-radius: 999px; background: var(--surface-1, #fff); color: var(--orders-muted); font: inherit; font-size: 12px; text-decoration: none; cursor: pointer; } +.aksrt-orders-page__filter-chip.is-active { border-color: var(--color-primary, #3b82f6); background: color-mix(in srgb, var(--color-primary, #3b82f6) 10%, var(--surface-1, #fff)); color: var(--color-primary, #3b82f6); font-weight: 700; } +.aksrt-orders-page__filter-count { display: inline-grid; min-width: 17px; height: 17px; padding: 0 4px; place-items: center; border-radius: 999px; background: var(--color-warning, #c87819); color: #fff; font-size: 10px; line-height: 1; } +.aksrt-orders-page__clear { margin-left: auto; padding: 0; border: 0; background: transparent; color: var(--color-primary, #3b82f6); font: inherit; font-size: 12px; text-decoration: none; cursor: pointer; } + +.aksrt-orders-page__list { display: grid; gap: 10px; } +.aksrt-orders-page__card { overflow: hidden; border: 1px solid var(--orders-line); border-radius: 13px; background: var(--surface-1, #fff); transition: border-color .18s ease, transform .18s ease, box-shadow .18s ease; } +.aksrt-orders-page__card:hover { border-color: color-mix(in srgb, var(--color-primary, #3b82f6) 36%, var(--orders-line)); box-shadow: 0 8px 22px rgba(30, 48, 79, .07); transform: translateY(-1px); } +.aksrt-orders-page__card-rail { display: flex; gap: 10px; align-items: center; padding: 9px 14px; background: var(--surface-2, #f7f8fb); color: var(--orders-muted); font-size: 11px; } +.aksrt-orders-page__card-rail time { margin-left: auto; font-variant-numeric: tabular-nums; } +.aksrt-orders-page__card-status { font-weight: 700; } +.aksrt-orders-page__card-status.is-pending { color: var(--color-warning, #c87819); } +.aksrt-orders-page__card-status.is-paid { color: var(--color-success, #168560); } +.aksrt-orders-page__card-main { display: grid; grid-template-columns: 52px minmax(0, 1fr) auto; gap: 13px; align-items: center; padding: 14px; } +.aksrt-orders-page__card-img, .aksrt-orders-page__card-art { width: 52px; height: 52px; border-radius: 10px; object-fit: cover; } +.aksrt-orders-page__card-art { display: grid; place-items: center; color: var(--color-primary, #3b82f6); background: color-mix(in srgb, var(--color-primary, #3b82f6) 10%, var(--surface-2, #f4f6f9)); } +.aksrt-orders-page__card-copy { min-width: 0; } +.aksrt-orders-page__card-copy h3 { display: -webkit-box; margin: 0 0 5px; overflow: hidden; color: var(--orders-ink); font-size: 14px; line-height: 1.35; -webkit-box-orient: vertical; -webkit-line-clamp: 2; } +.aksrt-orders-page__card-copy p { margin: 0; color: var(--orders-muted); font-family: var(--font-mono, monospace); font-size: 11px; } +.aksrt-orders-page__card-value { display: grid; gap: 3px; text-align: right; } +.aksrt-orders-page__card-value strong { color: var(--orders-ink); font-size: 15px; font-variant-numeric: tabular-nums; } +.aksrt-orders-page__card-value span { color: var(--orders-muted); font-size: 11px; } +.aksrt-orders-page__card-actions { display: flex; gap: 7px; align-items: center; justify-content: flex-end; padding: 9px 14px; border-top: 1px solid var(--orders-line); } +.aksrt-orders-page__countdown { margin-right: auto; color: var(--color-warning, #c87819); font-size: 11px; font-variant-numeric: tabular-nums; } + +.aksrt-orders-page__pagination { display: flex; justify-content: center; gap: 6px; margin-top: 20px; } +.aksrt-orders-page__page-chip { display: grid; width: 31px; height: 31px; padding: 0; place-items: center; border: 1px solid var(--orders-line); border-radius: 8px; background: var(--surface-1, #fff); color: var(--orders-muted); font: inherit; font-size: 12px; text-decoration: none; cursor: pointer; } +.aksrt-orders-page__page-chip.is-active { border-color: var(--color-primary, #3b82f6); background: var(--color-primary, #3b82f6); color: #fff; } +.aksrt-orders-page [data-aksrt-order-list], .aksrt-orders-page__stats, .aksrt-orders-page__pagination { transition: opacity .16s ease; } +.aksrt-orders-page.is-loading [data-aksrt-order-list], .aksrt-orders-page.is-loading .aksrt-orders-page__stats, .aksrt-orders-page.is-loading .aksrt-orders-page__pagination { opacity: .55; pointer-events: none; } +.aksrt-orders-page__empty { padding: 52px 20px; border: 1px dashed var(--orders-line); border-radius: 13px; background: var(--surface-2, #f7f8fb); text-align: center; } +.aksrt-orders-page__empty i { display: block; margin-bottom: 12px; color: var(--color-primary, #3b82f6); font-size: 30px; } +.aksrt-orders-page__empty h3 { margin: 0 0 7px; font-size: 15px; }.aksrt-orders-page__empty p { margin: 0 0 16px; color: var(--orders-muted); font-size: 13px; } + +/* ---- Order detail modal (front-end order center) ---- */ +/* 弹窗 portal 到 ,读不到页面上的 `--orders-*` 别名,因此把本模块的局部 + 别名映射到 modal-runtime.css 声明的共享 portal 基座 `--modal-*`; + 路由文件不应再自行推导调色板值。 */ +.aksrt-order-detail-modal { + --detail-ink: var(--modal-ink); + --detail-muted: var(--modal-muted); + --detail-line: var(--modal-line); + --detail-surface: var(--modal-surface); + --detail-surface-2: var(--modal-surface-2); + --detail-surface-3: var(--modal-surface-3); + --detail-primary: var(--modal-primary); + --detail-warning: var(--modal-warning); + --detail-success: var(--modal-success); +} + +/* 订单详情弹窗:与售后弹窗统一为 modal-mini 几何(400px);标题区改用蓝色炫彩头,运行时不再渲染默认标题栏。 */ +.aksrt-order-detail-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-order-detail-modal .aksrt-modal-runtime__content { display: flex; flex-direction: column; gap: 18px; padding: 16px 18px 0; } + +.aksrt-order-detail__skeleton { display: grid; gap: 12px; padding: 4px 0; } + +/* Status banner — a single local variable drives the whole colour scheme. */ +.aksrt-order-detail__hero { --hero-accent: var(--detail-primary); --hero-text: color-mix(in srgb, var(--hero-accent) 72%, var(--detail-ink)); display: flex; align-items: center; gap: 13px; padding: 15px; border: 1px solid color-mix(in srgb, var(--hero-accent) 20%, transparent); border-radius: 14px; background: color-mix(in srgb, var(--hero-accent) 12%, var(--detail-surface)); } +.aksrt-order-detail__hero.is-pending { --hero-accent: var(--detail-warning); } +.aksrt-order-detail__hero.is-paid { --hero-accent: var(--detail-success); } +.aksrt-order-detail__hero.is-refunded, .aksrt-order-detail__hero.is-closed { --hero-accent: var(--detail-muted); } +.aksrt-order-detail__hero-icon { display: grid; flex: 0 0 auto; width: 42px; height: 42px; place-items: center; border-radius: 12px; background: color-mix(in srgb, var(--hero-accent) 16%, transparent); color: var(--hero-text); font-size: 19px; } +.aksrt-order-detail__hero-copy { display: grid; flex: 1 1 auto; gap: 4px; min-width: 0; } +.aksrt-order-detail__status { color: var(--hero-text); font-size: 15px; font-weight: 750; line-height: 1.2; } +.aksrt-order-detail__desc { margin: 0; color: var(--detail-muted); font-size: 12px; line-height: 1.45; } +.aksrt-order-detail__countdown { color: var(--hero-text); font-size: 12px; font-variant-numeric: tabular-nums; } +.aksrt-order-detail__hero-amount { display: grid; flex: 0 0 auto; gap: 3px; justify-items: end; text-align: right; } +.aksrt-order-detail__hero-amount strong { color: var(--detail-ink); font-size: 21px; font-weight: 750; line-height: 1.1; letter-spacing: -.01em; font-variant-numeric: tabular-nums; } +.aksrt-order-detail__hero-amount span { color: var(--detail-muted); font-size: 11px; } + +.aksrt-order-detail__group { display: grid; gap: 10px; } +.aksrt-order-detail__group-title { display: flex; align-items: center; gap: 8px; margin: 0; color: var(--detail-ink); font-size: 13px; font-weight: 700; line-height: 1.3; } +.aksrt-order-detail__group-title::before { content: ""; width: 3px; height: 13px; border-radius: 2px; background: var(--detail-primary); } + +.aksrt-order-detail__product { display: grid; grid-template-columns: 54px minmax(0, 1fr); gap: 12px; align-items: center; padding: 12px; border: 1px solid var(--detail-line); border-radius: 12px; background: var(--detail-surface); } +.aksrt-order-detail__thumb { width: 54px; height: 54px; border-radius: 10px; object-fit: cover; } +.aksrt-order-detail__thumb--empty { display: grid; place-items: center; background: var(--detail-surface-2); color: var(--detail-primary); font-size: 19px; } +.aksrt-order-detail__product-copy { display: grid; gap: 6px; min-width: 0; } +.aksrt-order-detail__product-title { display: -webkit-box; overflow: hidden; color: var(--detail-ink); font-size: 14px; line-height: 1.45; -webkit-box-orient: vertical; -webkit-line-clamp: 2; } +.aksrt-order-detail__product-meta { display: inline-flex; justify-self: start; padding: 2px 8px; border-radius: 999px; background: color-mix(in srgb, var(--detail-primary) 9%, var(--detail-surface-2)); color: var(--detail-primary); font-size: 11px; font-weight: 650; line-height: 1.5; } + +.aksrt-order-detail__list { display: grid; margin: 0; overflow: hidden; border: 1px solid var(--detail-line); border-radius: 12px; background: var(--detail-surface); } +.aksrt-order-detail__list > div { display: grid; grid-template-columns: 84px minmax(0, 1fr); align-items: center; gap: 12px; padding: 11px 13px; border-bottom: 1px solid var(--detail-line); } +.aksrt-order-detail__list > div:last-child { border-bottom: 0; } +.aksrt-order-detail__list dt { color: var(--detail-muted); font-size: 12.5px; } +.aksrt-order-detail__list dd { display: flex; align-items: center; justify-content: flex-end; gap: 8px; min-width: 0; margin: 0; color: var(--detail-ink); font-size: 13px; font-variant-numeric: tabular-nums; text-align: right; } +.aksrt-order-detail__value-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.aksrt-order-detail__value-text.is-mono { font-family: var(--font-mono, monospace); font-size: 12.5px; letter-spacing: .01em; } +.aksrt-order-detail__copy { display: inline-grid; flex: 0 0 auto; width: 24px; height: 24px; padding: 0; place-items: center; border: 1px solid var(--detail-line); border-radius: 7px; background: var(--detail-surface-3); color: var(--detail-muted); font-size: 11px; line-height: 1; cursor: pointer; transition: color .15s ease, border-color .15s ease; } +.aksrt-order-detail__copy:hover { border-color: color-mix(in srgb, var(--detail-primary) 45%, var(--detail-line)); color: var(--detail-primary); } +.aksrt-order-detail__copy.is-copied { border-color: color-mix(in srgb, var(--detail-success) 45%, var(--detail-line)); color: var(--detail-success); } + +.aksrt-order-detail__refresh-state { display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: center; gap: 10px; padding: 12px 13px; border: 1px solid color-mix(in srgb, var(--detail-warning) 26%, transparent); border-radius: 12px; background: color-mix(in srgb, var(--detail-warning) 10%, var(--detail-surface-2)); color: var(--detail-ink); font-size: 12.5px; } +.aksrt-order-detail__refresh-state > i { color: var(--detail-warning); font-size: 15px; } +.aksrt-order-detail__refresh-state p { margin: 0; } +.aksrt-order-detail__refresh-state button { grid-column: 2; justify-self: start; } + +/* 底部操作区:按钮排布沿用主题 .but-average.modal-buts(禁封申诉弹窗同式),此处仅保留吸底定位与通栏背景。 + 主题 modal-buts 自带 margin-top:16px,吸底场景下用更高优先级清掉,避免内容与吸底条之间出现空隙。 */ +.aksrt-order-detail-modal .aksrt-order-detail__actions { position: sticky; bottom: 0; z-index: 2; margin: 0 -18px; padding: 13px 18px calc(13px + env(safe-area-inset-bottom)); border-top: 1px solid var(--detail-line); background: var(--detail-surface); } +.aksrt-order-detail-modal .aksrt-order-detail__actions.but-average.modal-buts { margin-top: 0; } + +/* Orders extensions */ +.aksrt-orders-page__extra-nav { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 12px; } +.aksrt-orders-page__extra-link { padding: 6px 12px; border-radius: 999px; background: var(--surface-2, #f3f4f6); color: inherit; text-decoration: none; font-size: 13px; } +.aksrt-orders-page__extra-link.is-active { background: var(--focus-color, var(--color-primary)); color: #fff; } +.aksrt-as-list { display: grid; gap: 12px; } +.aksrt-as-card { padding: 14px; border: 1px solid var(--main-border-color, #eee); border-radius: 10px; background: var(--main-bg-color, #fff); } +.aksrt-as-card__head { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-bottom: 6px; } +.aksrt-as-card__status { margin-left: auto; color: var(--focus-color, #2f6df6); font-weight: 600; } +.aksrt-as-timeline { margin: 10px 0 0; padding-left: 18px; color: var(--muted-2-color, #888); font-size: 12px; } +.aksrt-as-card__actions { display: flex; gap: 8px; margin-top: 10px; } +.aksrt-coupon-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 12px; } +.aksrt-coupon-card { padding: 12px; border: 1px dashed var(--focus-color, #2f6df6); border-radius: 10px; background: color-mix(in srgb, var(--focus-color, #2f6df6) 6%, #fff); } +.aksrt-coupon-card__value { color: var(--focus-color, #2f6df6); font-size: 20px; font-weight: 700; } +.aksrt-coupon-card__code { margin: 6px 0; font-family: ui-monospace, monospace; } +.aksrt-coupon-card__meta { margin-bottom: 4px; color: var(--muted-2-color, #888); font-size: 12px; } +.aksrt-orders-page__field { margin-bottom: 12px; } +.aksrt-orders-page__input, .aksrt-orders-page__select, .aksrt-orders-page__textarea { box-sizing: border-box; width: 100%; padding: 8px 10px; border: 1px solid var(--main-border-color, #ddd); border-radius: 6px; } +@media (max-width: 767px) { .uc-orders-panel__head { grid-template-columns: 1fr; gap: 17px; }.aksrt-orders-page__stats { max-width: none; }.aksrt-orders-page__search { flex-wrap: wrap; }.aksrt-orders-page__search input { min-width: 150px; }.aksrt-orders-page__search .btn-primary { margin-left: auto; }.aksrt-orders-page__filter-toggle { margin-left: auto; }.aksrt-orders-page__card-rail time { width: 100%; margin-left: 0; order: 3; }.aksrt-orders-page__card-main { grid-template-columns: 48px minmax(0, 1fr); }.aksrt-orders-page__card-img, .aksrt-orders-page__card-art { width: 48px; height: 48px; }.aksrt-orders-page__card-value { grid-column: 2; text-align: left; }.aksrt-orders-page__card-actions > :not(.aksrt-orders-page__countdown) { flex: 1 1 auto; }.aksrt-orders-page__countdown { width: 100%; }} +html.dark .aksrt-orders-page__stat, html.dark .aksrt-orders-page__card { background: var(--surface-1, #1a1d24); } html.dark .aksrt-orders-page__card-rail, html.dark .aksrt-orders-page__filter-panel { background: var(--surface-2, #12151b); } + +/* ---- 售后记录卡片(订单详情弹窗) ---- */ +.aksrt-order-detail__after-sales { display: grid; gap: 10px; margin: 0; padding: 0; list-style: none; } +.aksrt-order-detail__after-sale { --as-accent: var(--detail-muted); display: grid; gap: 10px; padding: 13px 14px; border: 1px solid var(--detail-line); border-left: 3px solid var(--as-accent); border-radius: 12px; background: var(--detail-surface); } +.aksrt-order-detail__after-sale.is-pending { --as-accent: var(--detail-warning); } +.aksrt-order-detail__after-sale.is-processing { --as-accent: var(--detail-primary); } +.aksrt-order-detail__after-sale.is-approved, .aksrt-order-detail__after-sale.is-completed { --as-accent: var(--detail-success); } +.aksrt-order-detail__after-sale.is-rejected, .aksrt-order-detail__after-sale.is-cancelled { --as-accent: var(--modal-danger, #ef4444); } +.aksrt-order-detail__after-sale-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; } +.aksrt-order-detail__after-sale-badge { padding: 3px 11px; border-radius: 999px; background: color-mix(in srgb, var(--as-accent) 12%, transparent); color: var(--as-accent); font-size: 12px; font-weight: 700; line-height: 1.5; } +.aksrt-order-detail__after-sale-head time { color: var(--detail-muted); font-size: 11.5px; font-variant-numeric: tabular-nums; } +.aksrt-order-detail__after-sale-reason { margin: 0; color: var(--detail-ink); font-size: 13px; line-height: 1.65; white-space: pre-wrap; word-break: break-word; } +.aksrt-order-detail__after-sale-meta { display: grid; margin: 0; overflow: hidden; border: 1px solid var(--detail-line); border-radius: 10px; background: var(--detail-surface-3); } +.aksrt-order-detail__after-sale-meta > div { display: grid; grid-template-columns: 76px minmax(0, 1fr); gap: 10px; padding: 9px 12px; border-bottom: 1px solid var(--detail-line); font-size: 12.5px; } +.aksrt-order-detail__after-sale-meta > div:last-child { border-bottom: 0; } +.aksrt-order-detail__after-sale-meta dt { color: var(--detail-muted); } +.aksrt-order-detail__after-sale-meta dd { margin: 0; color: var(--detail-ink); font-size: 12.5px; text-align: right; word-break: break-word; } +.aksrt-order-detail__after-sale-field { color: var(--detail-muted); } +.aksrt-order-detail__after-sale-foot { display: flex; flex-wrap: wrap; gap: 8px; } diff --git a/assets/js/order-after-sale.js b/assets/js/order-after-sale.js index 2251386f..e72cd06b 100644 --- a/assets/js/order-after-sale.js +++ b/assets/js/order-after-sale.js @@ -1,546 +1,546 @@ -/** - * 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 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 '' + escapeHtml(meta.desc) + "
"; - if (status === "pending" && timeLeft > 0) { - html += '剩余支付时间 ' + timeLeft + " 秒"; - } - html += "原因:' + escapeHtml(reasonLabel) + "
"; - if (applyDesc !== "") html += '申请理由:' + escapeHtml(applyDesc).replace(/\r\n|\r|\n/g, "
") + "
' + escapeHtml(item.reason).replace(/\r\n|\r|\n/g, "
") + "
支付状态可能仍在同步,请刷新后查看。
"; - html += ''; - html += "订单不存在
'; - return; - } - renderDetail(state, data, session); - }, - onData: function (data) { - if (refresh) { - delete state.operationKeys[actionKey]; - invalidateOrder(orderNumber, data); - } - }, - }; - } - - function openDetail(state, orderNumber) { - if (!orderNumber) return; - state.detailSession = null; - state.detailReplacement = false; - state.detailReplacementSession = null; - state.detailOrderNumber = orderNumber; - window.AksrtModalRuntime.openRemote(detailOptions(state, orderNumber, false)); - } - - function refreshDetail(state, orderNumber) { - if (!orderNumber || !activeDetail(state, orderNumber)) return; - state.detailReplacement = true; - state.detailReplacementSession = state.detailSession; - var refreshPromise = window.AksrtModalRuntime.openRemote(detailOptions(state, orderNumber, true)); - refreshPromise.then(function () { - state.detailReplacement = false; - state.detailReplacementSession = null; - }, function () { - state.detailReplacement = false; - state.detailReplacementSession = null; - }); - } - - function currentViewPath(state) { - var query = new URLSearchParams(); - if (state.query.status && state.query.status !== "all") query.set("order_status", state.query.status); - if (state.query.type) query.set("order_type", state.query.type); - if (state.query.orderNumber) query.set("order_number", state.query.orderNumber); - if (Number(state.query.page) > 1) query.set("order_page", String(state.query.page)); - return "/orders/view" + (query.toString() ? "?" + query.toString() : ""); - } - - function actionHtml(order) { - var allowedActions = order.allowed_actions || {}; - var afterSale = order.after_sale || {}; - var html = ''; - if (allowedActions.can_cancel) html += ''; - if (allowedActions.can_pay) html += ''; - // 售后入口:申请售后 / 售后详细走 order-after-sale.js 的独立弹窗。 - if (afterSale.enabled) html += afterSaleButtons(order); - return html; - } - - // 售后按钮(列表卡片与订单详情弹窗共用):已申请 → 售后详细,否则 → 申请售后。 - // 载荷与 PHP 模板同构(order-after-sale.js 同步消费),点击即弹窗、无前置请求。 - function afterSalePayload(order) { - var afterSale = order.after_sale || {}; - return escapeHtml(JSON.stringify({ - order_number: order.order_number || "", - product_name: order.product_name || "", - amount: order.amount || "0", - is_points: !!order.is_points, - currency_symbol: order.currency_symbol || "", - status_label: order.status_label || "", - after_sale: afterSale, - })); - } - - function afterSaleButtons(order) { - var afterSale = order.after_sale || {}; - var html = ""; - if (!afterSale.enabled) return html; - var payload = afterSalePayload(order); - if (afterSale.has_application) { - html += ''; - return html; - } - html += ''; - return html; - } - - function cardHtml(order) { - var image = order.image - ? '订单号 " + escapeHtml(order.order_number) + "
换个条件试试,或清除当前筛选。
'; - list.classList.remove("aksrt-orders-page__list"); - list.classList.add("aksrt-orders-page__empty"); - } else { - list.classList.remove("aksrt-orders-page__empty"); - list.classList.add("aksrt-orders-page__list"); - list.innerHTML = items.map(cardHtml).join(""); - } - renderPagination(state, view); - syncControls(state); - updateCountdowns(state); - } - - function syncControls(state) { - state.root.querySelectorAll("[data-aksrt-order-filter]").forEach(function (button) { - var field = button.getAttribute("data-aksrt-order-filter"); - var expected = field === "status" ? state.query.status : state.query.type; - var active = button.getAttribute("data-aksrt-order-filter-value") === expected; - button.classList.toggle("is-active", active); - button.setAttribute("aria-pressed", active ? "true" : "false"); - }); - var input = state.root.querySelector("#aksrt-order-number"); - if (input && input.value !== state.query.orderNumber) input.value = state.query.orderNumber; - } - - function setViewLoading(state, loading) { - state.root.classList.toggle("is-loading", loading); - state.root.setAttribute("aria-busy", loading ? "true" : "false"); - } - - function loadView(state, patch) { - Object.keys(patch || {}).forEach(function (key) { state.query[key] = patch[key]; }); - var requestId = ++state.viewRequest; - setViewLoading(state, true); - return request(currentViewPath(state), undefined, "GET").then(function (result) { - if (requestId !== state.viewRequest) return null; - if (!result || !result.data) throw new Error("刷新失败"); - renderView(state, result.data); - return result.data; - }).catch(function (error) { - if (requestId === state.viewRequest) notyf(errorMessage(error, "订单加载失败,请稍后重试"), "danger"); - return null; - }).finally(function () { - if (requestId === state.viewRequest) setViewLoading(state, false); - }); - } - - function refreshView(state) { - return loadView(state); - } - - function startPayment(state, button) { - var number = button.getAttribute("data-aksrt-order-pay") || ""; - var method = button.getAttribute("data-aksrt-order-method") || ""; - if (!number || button.disabled) return; - var actionKey = "pay:" + number; - if (!state.operationKeys[actionKey]) state.operationKeys[actionKey] = window.AksrtRequest.createIdempotencyKey(); - button.disabled = true; - request(orderPath(number, "/pay"), method ? { payment_method: method } : {}, "POST", state.operationKeys[actionKey]).then(function (result) { - delete state.operationKeys[actionKey]; - var data = result.data || {}; - invalidateOrder(number, data.order); - if (data.redirect && data.payment_url) { - window.location.assign(data.payment_url); - return; - } - notyf(data.message || "支付成功", "success"); - refreshView(state); - if (activeDetail(state, number)) openDetail(state, number); - }).catch(function (error) { - button.disabled = false; - notyf(errorMessage(error, "网络错误,请稍后重试"), "danger"); - }); - } - - function closeOrder(state, button) { - var number = button.getAttribute("data-aksrt-order-cancel") || ""; - if (!number || button.disabled) return; - if (button.getAttribute("data-aksrt-confirm") !== "true") { - button.setAttribute("data-aksrt-confirm", "true"); - button.textContent = "确认关闭"; - return; - } - button.disabled = true; - var actionKey = "cancel:" + number; - if (!state.operationKeys[actionKey]) state.operationKeys[actionKey] = window.AksrtRequest.createIdempotencyKey(); - request(orderPath(number, "/cancel"), {}, "POST", state.operationKeys[actionKey]).then(function (result) { - delete state.operationKeys[actionKey]; - var data = result.data || {}; - invalidateOrder(number, data.order); - notyf("订单已关闭", "success"); - refreshView(state); - if (activeDetail(state, number)) openDetail(state, number); - }).catch(function (error) { - button.disabled = false; - button.removeAttribute("data-aksrt-confirm"); - button.textContent = "关闭订单"; - notyf(errorMessage(error, "网络错误,请稍后重试"), "danger"); - }); - } - - function copyOrderValue(button) { - var value = button.getAttribute("data-aksrt-order-copy") || ""; - if (!value) return; - var icon = button.querySelector("i"); - var settle = function (ok) { - notyf(ok ? "订单号已复制" : "复制失败,请手动选择复制", ok ? "success" : "danger"); - if (!ok) return; - button.classList.add("is-copied"); - if (icon) icon.className = "fa fas fa-check"; - window.setTimeout(function () { - button.classList.remove("is-copied"); - if (icon) icon.className = "fa fas fa-copy"; - }, 1600); - }; - var fallback = function () { - try { - var field = document.createElement("textarea"); - field.value = value; - field.setAttribute("readonly", "readonly"); - field.style.position = "fixed"; - field.style.top = "-1000px"; - field.style.opacity = "0"; - document.body.appendChild(field); - field.select(); - document.execCommand("copy"); - document.body.removeChild(field); - settle(true); - } catch (error) { - settle(false); - } - }; - if (navigator.clipboard && navigator.clipboard.writeText) { - navigator.clipboard.writeText(value).then(function () { settle(true); }, fallback); - return; - } - fallback(); - } - - function closest(target, selector) { - return target && target.closest ? target.closest(selector) : null; - } - - function initRoot(root) { - if (instances.has(root)) return; - var state = { - root: root, - detailOrderNumber: "", - detailSession: null, - detailReplacement: false, - detailReplacementSession: null, - countdownTimer: null, - operationKeys: {}, - query: { status: "all", type: "", orderNumber: "", page: 1 }, - viewRequest: 0, - }; - - state.onRootClick = function (event) { - var filterToggle = closest(event.target, "[data-aksrt-orders-filter-toggle]"); - if (filterToggle && root.contains(filterToggle)) { - var panel = root.querySelector("[data-aksrt-orders-filter-panel]"); - var expanded = filterToggle.getAttribute("aria-expanded") === "true"; - filterToggle.setAttribute("aria-expanded", String(!expanded)); - if (panel) panel.hidden = expanded; - return; - } - var filter = closest(event.target, "[data-aksrt-order-filter]"); - if (filter && root.contains(filter)) { - event.preventDefault(); - var field = filter.getAttribute("data-aksrt-order-filter"); - var patch = { page: 1 }; - patch[field] = filter.getAttribute("data-aksrt-order-filter-value") || ""; - loadView(state, patch); - return; - } - var page = closest(event.target, "[data-aksrt-order-page]"); - if (page && root.contains(page)) { - event.preventDefault(); - loadView(state, { page: Math.max(1, Number(page.getAttribute("data-aksrt-order-page") || 1)) }); - return; - } - var clear = closest(event.target, "[data-aksrt-orders-clear]"); - if (clear && root.contains(clear)) { - event.preventDefault(); - loadView(state, { status: "all", type: "", orderNumber: "", page: 1 }); - return; - } - var detail = closest(event.target, "[data-aksrt-order-detail]"); - if (detail && root.contains(detail)) { openDetail(state, detail.getAttribute("data-aksrt-order-detail")); return; } - var pay = closest(event.target, "[data-aksrt-order-pay]"); - if (pay && root.contains(pay)) { event.preventDefault(); startPayment(state, pay); return; } - var cancel = closest(event.target, "[data-aksrt-order-cancel]"); - if (cancel && root.contains(cancel)) { event.preventDefault(); closeOrder(state, cancel); } - }; - state.onRootSubmit = function (event) { - 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.onModalClose = function (event) { - var detail = event && event.detail ? event.detail : {}; - if (!state.detailSession || detail.session !== state.detailSession) return; - if (detail.reason === "replace" && state.detailReplacement && detail.session === state.detailReplacementSession) return; - state.detailSession = null; - state.detailOrderNumber = ""; - state.detailReplacement = false; - state.detailReplacementSession = null; - }; - state.onDocumentClick = function (event) { - var afterSaleCancel = closest(event.target, "[data-aksrt-after-sale-cancel]"); - if (afterSaleCancel && !event.__aksrtAfterSaleCancelHandled) { - var afterSaleNumber = afterSaleCancel.getAttribute("data-aksrt-after-sale-cancel") || ""; - if (activeDetail(state, afterSaleNumber)) { - event.__aksrtAfterSaleCancelHandled = true; - event.preventDefault(); - afterSaleCancel.disabled = true; - request(orderPath(afterSaleNumber, "/after-sale"), undefined, "DELETE", window.AksrtRequest.createIdempotencyKey()).then(function () { - notyf("售后申请已取消", "success"); - invalidateOrder(afterSaleNumber); - openDetail(state, afterSaleNumber); - refreshView(state); - }).catch(function (error) { - afterSaleCancel.disabled = false; - notyf(errorMessage(error, "取消申请失败"), "danger"); - }); - return; - } - } - if (!state.detailSession || window.AksrtModalRuntime.getActive() !== state.detailSession.shell) return; - var copy = closest(event.target, "[data-aksrt-order-copy]"); - if (copy) { - event.preventDefault(); - copyOrderValue(copy); - return; - } - var refresh = closest(event.target, "[data-aksrt-order-refresh]"); - if (refresh) { - event.preventDefault(); - refreshDetail(state, refresh.getAttribute("data-aksrt-order-refresh") || ""); - return; - } - var pay = closest(event.target, "[data-aksrt-order-pay]"); - if (pay && !root.contains(pay)) { - event.preventDefault(); - startPayment(state, pay); - return; - } - var cancel = closest(event.target, "[data-aksrt-order-cancel]"); - if (cancel && !root.contains(cancel)) { - event.preventDefault(); - closeOrder(state, cancel); - } - }; - - root.addEventListener("click", state.onRootClick); - root.addEventListener("submit", state.onRootSubmit); - 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) { - if (!root) return []; - var roots = []; - if (root.matches && root.matches("[data-aksrt-order-center]")) roots.push(root); - if (root.querySelectorAll) root.querySelectorAll("[data-aksrt-order-center]").forEach(function (item) { roots.push(item); }); - return roots; - } - - function init(root) { - rootsIn(root || document).forEach(initRoot); - } - - function destroy(root) { - rootsIn(root).forEach(function (item) { - var state = instances.get(item); - if (!state) return; - if (activeDetail(state, state.detailOrderNumber)) window.AksrtModalRuntime.close("replace"); - item.removeEventListener("click", state.onRootClick); - item.removeEventListener("submit", state.onRootSubmit); - 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; }); - }); - } - - // 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); }); -}()); +/** + * Knowledge-payment order center interactions. + * + * @package AKSRT + */ +(function () { + "use strict"; + + var instances = new WeakMap(); + var liveStates = []; + + function request(path, body, method, operationKey) { + var options = { method: method || "POST" }; + if (body !== undefined) options.body = JSON.stringify(body || {}); + if (operationKey) options.headers = { "Idempotency-Key": operationKey }; + return window.AksrtRequest.request(path, options); + } + + function orderPath(orderNumber, suffix) { + return "/orders/" + encodeURIComponent(orderNumber || "") + (suffix || ""); + } + + function errorMessage(error, fallback) { + if (error && error.message) return String(error.message); + return fallback || "请求失败"; + } + + function escapeHtml(value) { + return String(value == null ? "" : value) + .replace(/&/g, "&").replace(//g, ">").replace(/\"/g, """) + .replace(/'/g, "'"); + } + + function formatMoney(order) { + var raw = String(order && order.amount != null ? order.amount : "0"); + if (order && order.is_points) { + return escapeHtml(order.currency_symbol || "积分") + " " + (/^\d+$/.test(raw) ? raw : "0"); + } + var match = /^(0|[1-9]\d*)(?:\.(\d{1,2}))?$/.exec(raw); + var amount = match ? match[1] + "." + (match[2] || "").padEnd(2, "0") : "0.00"; + return "¥" + amount; + } + + function orderTag(orderNumber) { + return "order:" + String(orderNumber || "").toLowerCase(); + } + + function invalidateOrder(orderNumber, order) { + var tags = order && Array.isArray(order.invalidation_tags) ? order.invalidation_tags : [ + "orders", + orderTag(orderNumber), + ]; + window.AksrtModalRuntime.invalidate(tags); + } + + function activeDetail(state, number) { + return state.detailOrderNumber === number + && !!state.detailSession + && !state.detailSession.closed + && state.detailSession.options + && state.detailSession.options.kind === "orders" + && window.AksrtModalRuntime.getActive() === state.detailSession.shell; + } + + function detailSkeleton() { + return '' + escapeHtml(meta.desc) + "
"; + if (status === "pending" && timeLeft > 0) { + html += '剩余支付时间 ' + timeLeft + " 秒"; + } + html += "原因:' + escapeHtml(reasonLabel) + "
"; + if (applyDesc !== "") html += '申请理由:' + escapeHtml(applyDesc).replace(/\r\n|\r|\n/g, "
") + "
' + escapeHtml(item.reason).replace(/\r\n|\r|\n/g, "
") + "
支付状态可能仍在同步,请刷新后查看。
"; + html += ''; + html += "订单不存在
'; + return; + } + renderDetail(state, data, session); + }, + onData: function (data) { + if (refresh) { + delete state.operationKeys[actionKey]; + invalidateOrder(orderNumber, data); + } + }, + }; + } + + function openDetail(state, orderNumber) { + if (!orderNumber) return; + state.detailSession = null; + state.detailReplacement = false; + state.detailReplacementSession = null; + state.detailOrderNumber = orderNumber; + window.AksrtModalRuntime.openRemote(detailOptions(state, orderNumber, false)); + } + + function refreshDetail(state, orderNumber) { + if (!orderNumber || !activeDetail(state, orderNumber)) return; + state.detailReplacement = true; + state.detailReplacementSession = state.detailSession; + var refreshPromise = window.AksrtModalRuntime.openRemote(detailOptions(state, orderNumber, true)); + refreshPromise.then(function () { + state.detailReplacement = false; + state.detailReplacementSession = null; + }, function () { + state.detailReplacement = false; + state.detailReplacementSession = null; + }); + } + + function currentViewPath(state) { + var query = new URLSearchParams(); + if (state.query.status && state.query.status !== "all") query.set("order_status", state.query.status); + if (state.query.type) query.set("order_type", state.query.type); + if (state.query.orderNumber) query.set("order_number", state.query.orderNumber); + if (Number(state.query.page) > 1) query.set("order_page", String(state.query.page)); + return "/orders/view" + (query.toString() ? "?" + query.toString() : ""); + } + + function actionHtml(order) { + var allowedActions = order.allowed_actions || {}; + var afterSale = order.after_sale || {}; + var html = ''; + if (allowedActions.can_cancel) html += ''; + if (allowedActions.can_pay) html += ''; + // 售后入口:申请售后 / 售后详细走 order-after-sale.js 的独立弹窗。 + if (afterSale.enabled) html += afterSaleButtons(order); + return html; + } + + // 售后按钮(列表卡片与订单详情弹窗共用):已申请 → 售后详细,否则 → 申请售后。 + // 载荷与 PHP 模板同构(order-after-sale.js 同步消费),点击即弹窗、无前置请求。 + function afterSalePayload(order) { + var afterSale = order.after_sale || {}; + return escapeHtml(JSON.stringify({ + order_number: order.order_number || "", + product_name: order.product_name || "", + amount: order.amount || "0", + is_points: !!order.is_points, + currency_symbol: order.currency_symbol || "", + status_label: order.status_label || "", + after_sale: afterSale, + })); + } + + function afterSaleButtons(order) { + var afterSale = order.after_sale || {}; + var html = ""; + if (!afterSale.enabled) return html; + var payload = afterSalePayload(order); + if (afterSale.has_application) { + html += ''; + return html; + } + html += ''; + return html; + } + + function cardHtml(order) { + var image = order.image + ? '订单号 " + escapeHtml(order.order_number) + "
换个条件试试,或清除当前筛选。
'; + list.classList.remove("aksrt-orders-page__list"); + list.classList.add("aksrt-orders-page__empty"); + } else { + list.classList.remove("aksrt-orders-page__empty"); + list.classList.add("aksrt-orders-page__list"); + list.innerHTML = items.map(cardHtml).join(""); + } + renderPagination(state, view); + syncControls(state); + updateCountdowns(state); + } + + function syncControls(state) { + state.root.querySelectorAll("[data-aksrt-order-filter]").forEach(function (button) { + var field = button.getAttribute("data-aksrt-order-filter"); + var expected = field === "status" ? state.query.status : state.query.type; + var active = button.getAttribute("data-aksrt-order-filter-value") === expected; + button.classList.toggle("is-active", active); + button.setAttribute("aria-pressed", active ? "true" : "false"); + }); + var input = state.root.querySelector("#aksrt-order-number"); + if (input && input.value !== state.query.orderNumber) input.value = state.query.orderNumber; + } + + function setViewLoading(state, loading) { + state.root.classList.toggle("is-loading", loading); + state.root.setAttribute("aria-busy", loading ? "true" : "false"); + } + + function loadView(state, patch) { + Object.keys(patch || {}).forEach(function (key) { state.query[key] = patch[key]; }); + var requestId = ++state.viewRequest; + setViewLoading(state, true); + return request(currentViewPath(state), undefined, "GET").then(function (result) { + if (requestId !== state.viewRequest) return null; + if (!result || !result.data) throw new Error("刷新失败"); + renderView(state, result.data); + return result.data; + }).catch(function (error) { + if (requestId === state.viewRequest) notyf(errorMessage(error, "订单加载失败,请稍后重试"), "danger"); + return null; + }).finally(function () { + if (requestId === state.viewRequest) setViewLoading(state, false); + }); + } + + function refreshView(state) { + return loadView(state); + } + + function startPayment(state, button) { + var number = button.getAttribute("data-aksrt-order-pay") || ""; + var method = button.getAttribute("data-aksrt-order-method") || ""; + if (!number || button.disabled) return; + var actionKey = "pay:" + number; + if (!state.operationKeys[actionKey]) state.operationKeys[actionKey] = window.AksrtRequest.createIdempotencyKey(); + button.disabled = true; + request(orderPath(number, "/pay"), method ? { payment_method: method } : {}, "POST", state.operationKeys[actionKey]).then(function (result) { + delete state.operationKeys[actionKey]; + var data = result.data || {}; + invalidateOrder(number, data.order); + if (data.redirect && data.payment_url) { + window.location.assign(data.payment_url); + return; + } + notyf(data.message || "支付成功", "success"); + refreshView(state); + if (activeDetail(state, number)) openDetail(state, number); + }).catch(function (error) { + button.disabled = false; + notyf(errorMessage(error, "网络错误,请稍后重试"), "danger"); + }); + } + + function closeOrder(state, button) { + var number = button.getAttribute("data-aksrt-order-cancel") || ""; + if (!number || button.disabled) return; + if (button.getAttribute("data-aksrt-confirm") !== "true") { + button.setAttribute("data-aksrt-confirm", "true"); + button.textContent = "确认关闭"; + return; + } + button.disabled = true; + var actionKey = "cancel:" + number; + if (!state.operationKeys[actionKey]) state.operationKeys[actionKey] = window.AksrtRequest.createIdempotencyKey(); + request(orderPath(number, "/cancel"), {}, "POST", state.operationKeys[actionKey]).then(function (result) { + delete state.operationKeys[actionKey]; + var data = result.data || {}; + invalidateOrder(number, data.order); + notyf("订单已关闭", "success"); + refreshView(state); + if (activeDetail(state, number)) openDetail(state, number); + }).catch(function (error) { + button.disabled = false; + button.removeAttribute("data-aksrt-confirm"); + button.textContent = "关闭订单"; + notyf(errorMessage(error, "网络错误,请稍后重试"), "danger"); + }); + } + + function copyOrderValue(button) { + var value = button.getAttribute("data-aksrt-order-copy") || ""; + if (!value) return; + var icon = button.querySelector("i"); + var settle = function (ok) { + notyf(ok ? "订单号已复制" : "复制失败,请手动选择复制", ok ? "success" : "danger"); + if (!ok) return; + button.classList.add("is-copied"); + if (icon) icon.className = "fa fas fa-check"; + window.setTimeout(function () { + button.classList.remove("is-copied"); + if (icon) icon.className = "fa fas fa-copy"; + }, 1600); + }; + var fallback = function () { + try { + var field = document.createElement("textarea"); + field.value = value; + field.setAttribute("readonly", "readonly"); + field.style.position = "fixed"; + field.style.top = "-1000px"; + field.style.opacity = "0"; + document.body.appendChild(field); + field.select(); + document.execCommand("copy"); + document.body.removeChild(field); + settle(true); + } catch (error) { + settle(false); + } + }; + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(value).then(function () { settle(true); }, fallback); + return; + } + fallback(); + } + + function closest(target, selector) { + return target && target.closest ? target.closest(selector) : null; + } + + function initRoot(root) { + if (instances.has(root)) return; + var state = { + root: root, + detailOrderNumber: "", + detailSession: null, + detailReplacement: false, + detailReplacementSession: null, + countdownTimer: null, + operationKeys: {}, + query: { status: "all", type: "", orderNumber: "", page: 1 }, + viewRequest: 0, + }; + + state.onRootClick = function (event) { + var filterToggle = closest(event.target, "[data-aksrt-orders-filter-toggle]"); + if (filterToggle && root.contains(filterToggle)) { + var panel = root.querySelector("[data-aksrt-orders-filter-panel]"); + var expanded = filterToggle.getAttribute("aria-expanded") === "true"; + filterToggle.setAttribute("aria-expanded", String(!expanded)); + if (panel) panel.hidden = expanded; + return; + } + var filter = closest(event.target, "[data-aksrt-order-filter]"); + if (filter && root.contains(filter)) { + event.preventDefault(); + var field = filter.getAttribute("data-aksrt-order-filter"); + var patch = { page: 1 }; + patch[field] = filter.getAttribute("data-aksrt-order-filter-value") || ""; + loadView(state, patch); + return; + } + var page = closest(event.target, "[data-aksrt-order-page]"); + if (page && root.contains(page)) { + event.preventDefault(); + loadView(state, { page: Math.max(1, Number(page.getAttribute("data-aksrt-order-page") || 1)) }); + return; + } + var clear = closest(event.target, "[data-aksrt-orders-clear]"); + if (clear && root.contains(clear)) { + event.preventDefault(); + loadView(state, { status: "all", type: "", orderNumber: "", page: 1 }); + return; + } + var detail = closest(event.target, "[data-aksrt-order-detail]"); + if (detail && root.contains(detail)) { openDetail(state, detail.getAttribute("data-aksrt-order-detail")); return; } + var pay = closest(event.target, "[data-aksrt-order-pay]"); + if (pay && root.contains(pay)) { event.preventDefault(); startPayment(state, pay); return; } + var cancel = closest(event.target, "[data-aksrt-order-cancel]"); + if (cancel && root.contains(cancel)) { event.preventDefault(); closeOrder(state, cancel); } + }; + state.onRootSubmit = function (event) { + 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.onModalClose = function (event) { + var detail = event && event.detail ? event.detail : {}; + if (!state.detailSession || detail.session !== state.detailSession) return; + if (detail.reason === "replace" && state.detailReplacement && detail.session === state.detailReplacementSession) return; + state.detailSession = null; + state.detailOrderNumber = ""; + state.detailReplacement = false; + state.detailReplacementSession = null; + }; + state.onDocumentClick = function (event) { + var afterSaleCancel = closest(event.target, "[data-aksrt-after-sale-cancel]"); + if (afterSaleCancel && !event.__aksrtAfterSaleCancelHandled) { + var afterSaleNumber = afterSaleCancel.getAttribute("data-aksrt-after-sale-cancel") || ""; + if (activeDetail(state, afterSaleNumber)) { + event.__aksrtAfterSaleCancelHandled = true; + event.preventDefault(); + afterSaleCancel.disabled = true; + request(orderPath(afterSaleNumber, "/after-sale"), undefined, "DELETE", window.AksrtRequest.createIdempotencyKey()).then(function () { + notyf("售后申请已取消", "success"); + invalidateOrder(afterSaleNumber); + openDetail(state, afterSaleNumber); + refreshView(state); + }).catch(function (error) { + afterSaleCancel.disabled = false; + notyf(errorMessage(error, "取消申请失败"), "danger"); + }); + return; + } + } + if (!state.detailSession || window.AksrtModalRuntime.getActive() !== state.detailSession.shell) return; + var copy = closest(event.target, "[data-aksrt-order-copy]"); + if (copy) { + event.preventDefault(); + copyOrderValue(copy); + return; + } + var refresh = closest(event.target, "[data-aksrt-order-refresh]"); + if (refresh) { + event.preventDefault(); + refreshDetail(state, refresh.getAttribute("data-aksrt-order-refresh") || ""); + return; + } + var pay = closest(event.target, "[data-aksrt-order-pay]"); + if (pay && !root.contains(pay)) { + event.preventDefault(); + startPayment(state, pay); + return; + } + var cancel = closest(event.target, "[data-aksrt-order-cancel]"); + if (cancel && !root.contains(cancel)) { + event.preventDefault(); + closeOrder(state, cancel); + } + }; + + root.addEventListener("click", state.onRootClick); + root.addEventListener("submit", state.onRootSubmit); + 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) { + if (!root) return []; + var roots = []; + if (root.matches && root.matches("[data-aksrt-order-center]")) roots.push(root); + if (root.querySelectorAll) root.querySelectorAll("[data-aksrt-order-center]").forEach(function (item) { roots.push(item); }); + return roots; + } + + function init(root) { + rootsIn(root || document).forEach(initRoot); + } + + function destroy(root) { + rootsIn(root).forEach(function (item) { + var state = instances.get(item); + if (!state) return; + if (activeDetail(state, state.detailOrderNumber)) window.AksrtModalRuntime.close("replace"); + item.removeEventListener("click", state.onRootClick); + item.removeEventListener("submit", state.onRootSubmit); + 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; }); + }); + } + + // 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); }); +}()); diff --git a/src/Core/DBMigrator.php b/src/Core/DBMigrator.php index d36ded0d..e6e1acf0 100644 --- a/src/Core/DBMigrator.php +++ b/src/Core/DBMigrator.php @@ -1,44 +1,44 @@ -get_charset_collate(); - $prefix = $wpdb->prefix; - - // ---------- 1. 统一资产账户与不可变账本 ---------- - $sql_asset_account = "CREATE TABLE {$prefix}aksrt_asset_account ( - user_id BIGINT UNSIGNED NOT NULL, - asset VARCHAR(16) NOT NULL, - balance_minor BIGINT UNSIGNED NOT NULL DEFAULT 0, - version BIGINT UNSIGNED NOT NULL DEFAULT 0, - updated_at DATETIME(6) NOT NULL, - PRIMARY KEY (user_id, asset), - KEY idx_asset_balance (asset, balance_minor) - ) ENGINE=InnoDB {$charset_collate};"; - dbDelta($sql_asset_account); - - $sql_asset_transaction = "CREATE TABLE {$prefix}aksrt_asset_transaction ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - operation_key VARCHAR(191) NOT NULL, - payload_hash CHAR(64) NOT NULL, - reason VARCHAR(64) NOT NULL, - ref_type VARCHAR(32) DEFAULT NULL, - ref_key VARCHAR(191) DEFAULT NULL, - actor_user_id BIGINT UNSIGNED NOT NULL DEFAULT 0, - metadata LONGTEXT DEFAULT NULL, - ip VARCHAR(64) DEFAULT NULL, - ua VARCHAR(255) DEFAULT NULL, - created_at DATETIME(6) NOT NULL, - PRIMARY KEY (id), - UNIQUE KEY uniq_operation_key (operation_key), - KEY idx_reason_time (reason, created_at), - KEY idx_reference (ref_type, ref_key) - ) ENGINE=InnoDB {$charset_collate};"; - dbDelta($sql_asset_transaction); - - $sql_asset_entry = "CREATE TABLE {$prefix}aksrt_asset_entry ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - transaction_id BIGINT UNSIGNED NOT NULL, - line_no SMALLINT UNSIGNED NOT NULL, - user_id BIGINT UNSIGNED NOT NULL, - asset VARCHAR(16) NOT NULL, - entry_type VARCHAR(32) NOT NULL, - delta_minor BIGINT NOT NULL, - balance_after_minor BIGINT UNSIGNED NOT NULL, - counterparty_user_id BIGINT UNSIGNED DEFAULT NULL, - remark VARCHAR(255) DEFAULT NULL, - created_at DATETIME(6) NOT NULL, - PRIMARY KEY (id), - UNIQUE KEY uniq_transaction_line (transaction_id, line_no), - KEY idx_user_asset_time (user_id, asset, created_at, id), - KEY idx_transaction (transaction_id) - ) ENGINE=InnoDB {$charset_collate};"; - dbDelta($sql_asset_entry); - - // 后台「资金流水」页的默认查询是 WHERE 1=1 ORDER BY created_at DESC, id DESC LIMIT n - // 外加同条件的 COUNT(*),且界面上的日期区间筛选也只有 created_at 可用。 - // 索引必须能以 created_at 为前导列,否则每次翻页都是全表扫描 + filesort, - // 而 idx_flow_event_time 的前导列是 event_type(筛选时可空),救不了这两条查询。 - $sql_financial_flow = "CREATE TABLE {$prefix}aksrt_financial_flow ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - flow_number VARCHAR(32) NOT NULL, - operation_key VARCHAR(191) NOT NULL, - event_type VARCHAR(64) NOT NULL, - ref_type VARCHAR(32) DEFAULT NULL, - ref_key VARCHAR(191) DEFAULT NULL, - order_number VARCHAR(32) DEFAULT NULL, - withdrawal_number VARCHAR(32) DEFAULT NULL, - ledger_transaction_id BIGINT UNSIGNED DEFAULT NULL, - provider_transaction_id VARCHAR(128) DEFAULT NULL, - metadata LONGTEXT DEFAULT NULL, - created_at DATETIME(6) NOT NULL, - PRIMARY KEY (id), - UNIQUE KEY uniq_flow_number (flow_number), - UNIQUE KEY uniq_flow_operation (operation_key), - UNIQUE KEY uniq_flow_ledger_transaction (ledger_transaction_id), - KEY idx_flow_created (created_at), - KEY idx_flow_event_time (event_type, created_at), - KEY idx_flow_order (order_number), - KEY idx_flow_withdrawal (withdrawal_number), - KEY idx_flow_reference (ref_type, ref_key), - KEY idx_flow_provider_transaction (provider_transaction_id) - ) ENGINE=InnoDB {$charset_collate};"; - dbDelta($sql_financial_flow); - - $sql_financial_flow_entry = "CREATE TABLE {$prefix}aksrt_financial_flow_entry ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - flow_id BIGINT UNSIGNED NOT NULL, - line_no SMALLINT UNSIGNED NOT NULL, - user_id BIGINT UNSIGNED NOT NULL DEFAULT 0, - asset VARCHAR(16) NOT NULL, - direction VARCHAR(8) NOT NULL, - entry_type VARCHAR(32) NOT NULL, - delta_minor BIGINT NOT NULL, - balance_after_minor BIGINT UNSIGNED DEFAULT NULL, - counterparty_user_id BIGINT UNSIGNED DEFAULT NULL, - remark VARCHAR(255) DEFAULT NULL, - PRIMARY KEY (id), - UNIQUE KEY uniq_flow_line (flow_id, line_no), - KEY idx_flow_entry_user (user_id, flow_id), - KEY idx_flow_entry_asset (asset, flow_id), - KEY idx_flow_entry_direction (direction, flow_id), - KEY idx_flow_entry_counterparty (counterparty_user_id, flow_id) - ) ENGINE=InnoDB {$charset_collate};"; - dbDelta($sql_financial_flow_entry); - - // ---------- 2. 签到 ---------- - $sql_signin = "CREATE TABLE {$prefix}aksrt_signin_log ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - user_id BIGINT UNSIGNED NOT NULL, - signin_date DATE NOT NULL, - streak INT NOT NULL DEFAULT 1, - reward INT NOT NULL DEFAULT 0, - PRIMARY KEY (id), - UNIQUE KEY uniq_user_date (user_id, signin_date), - KEY idx_user (user_id) - ) ENGINE=InnoDB {$charset_collate};"; - dbDelta($sql_signin); - - // ---------- 3. 下载 token ---------- - $sql_dl_token = "CREATE TABLE {$prefix}aksrt_dl_token ( - token CHAR(48) NOT NULL, - user_id BIGINT UNSIGNED NOT NULL, - post_id BIGINT UNSIGNED NOT NULL, - source_index INT NOT NULL DEFAULT 0, - cost INT NOT NULL DEFAULT 0, - ip_bind VARCHAR(64) DEFAULT NULL, - ua_hash CHAR(40) DEFAULT NULL, - used_count INT NOT NULL DEFAULT 0, - max_uses INT NOT NULL DEFAULT 1, - expires_at DATETIME NOT NULL, - created_at DATETIME NOT NULL, - PRIMARY KEY (token), - KEY idx_expires (expires_at), - KEY idx_user (user_id), - KEY idx_post (post_id) - ) ENGINE=InnoDB {$charset_collate};"; - dbDelta($sql_dl_token); - - // ---------- 5. 用户通知收件箱 ---------- - $sql_notification = "CREATE TABLE {$prefix}aksrt_notification ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - user_id BIGINT UNSIGNED NOT NULL, - type VARCHAR(32) NOT NULL, - title VARCHAR(191) NOT NULL, - description TEXT DEFAULT NULL, - target_url TEXT DEFAULT NULL, - is_read TINYINT(1) NOT NULL DEFAULT 0, - created_at DATETIME NOT NULL, - read_at DATETIME DEFAULT NULL, - PRIMARY KEY (id), - KEY idx_user_created (user_id, created_at), - KEY idx_user_read (user_id, is_read) - ) ENGINE=InnoDB {$charset_collate};"; - dbDelta($sql_notification); - - // ---------- 8. 订单表 ---------- - $sql_order = "CREATE TABLE {$prefix}aksrt_order ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - order_number VARCHAR(32) NOT NULL, - request_key VARCHAR(191) DEFAULT NULL, - user_id BIGINT UNSIGNED NOT NULL, - product_type VARCHAR(32) NOT NULL DEFAULT '', - product_id BIGINT UNSIGNED NOT NULL DEFAULT 0, - amount DECIMAL(10,2) NOT NULL DEFAULT 0, - currency VARCHAR(8) NOT NULL DEFAULT 'CNY', - status VARCHAR(20) NOT NULL DEFAULT 'pending', - shipping_status TINYINT NOT NULL DEFAULT 0, - payment_method VARCHAR(32) NOT NULL DEFAULT '', - payment_transaction_id VARCHAR(128) NOT NULL DEFAULT '', - created_at DATETIME NOT NULL, - paid_at DATETIME DEFAULT NULL, - delivered_at DATETIME DEFAULT NULL, - completed_at DATETIME DEFAULT NULL, - cancelled_at DATETIME DEFAULT NULL, - refunded_at DATETIME DEFAULT NULL, - expires_at DATETIME DEFAULT NULL, - note TEXT DEFAULT NULL, - PRIMARY KEY (id), - UNIQUE KEY uniq_order_number (order_number), - UNIQUE KEY uniq_request_key (request_key), - KEY idx_user (user_id), - KEY idx_user_status_created (user_id, status, created_at), - KEY idx_status (status), - KEY idx_created (created_at), - KEY idx_product (product_type, product_id) - ) ENGINE=InnoDB {$charset_collate};"; - dbDelta($sql_order); - - // ---------- order after-sales ---------- - $sql_order_aftersale = "CREATE TABLE {$prefix}aksrt_order_aftersale ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - order_number VARCHAR(32) NOT NULL, - user_id BIGINT UNSIGNED NOT NULL, - aftersale_type VARCHAR(24) NOT NULL DEFAULT 'refund', - reason TEXT NOT NULL, - apply_form LONGTEXT DEFAULT NULL, - images LONGTEXT DEFAULT NULL, - refund_amount VARCHAR(32) NOT NULL DEFAULT '0', - status VARCHAR(20) NOT NULL DEFAULT 'pending', - admin_note TEXT DEFAULT NULL, - gateway_request_id VARCHAR(128) DEFAULT NULL, - gateway_refund_id VARCHAR(128) DEFAULT NULL, - gateway_result LONGTEXT DEFAULT NULL, - created_at DATETIME NOT NULL, - reviewed_at DATETIME DEFAULT NULL, - completed_at DATETIME DEFAULT NULL, - PRIMARY KEY (id), - KEY idx_order_status (order_number, status), - KEY idx_user_created (user_id, created_at), - KEY idx_status_created (status, created_at) - ) ENGINE=InnoDB {$charset_collate};"; - dbDelta($sql_order_aftersale); - - // ---------- 9. 作者收益台账 ---------- - $sql_revenue = "CREATE TABLE {$prefix}aksrt_revenue ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - author_id BIGINT UNSIGNED NOT NULL DEFAULT 0, - order_number VARCHAR(32) NOT NULL DEFAULT '', - post_id BIGINT UNSIGNED NOT NULL DEFAULT 0, - buyer_id BIGINT UNSIGNED NOT NULL DEFAULT 0, - currency VARCHAR(8) NOT NULL DEFAULT 'CNY', - original_amount DECIMAL(18,2) NOT NULL DEFAULT 0, - share_ratio DECIMAL(5,2) NOT NULL DEFAULT 0, - author_level TINYINT UNSIGNED NOT NULL DEFAULT 0, - amount DECIMAL(18,2) NOT NULL DEFAULT 0, - type VARCHAR(32) NOT NULL DEFAULT 'order', - status VARCHAR(20) NOT NULL DEFAULT 'pending', - reversal_amount DECIMAL(18,2) NOT NULL DEFAULT 0, - error_message TEXT DEFAULT NULL, - audit_json LONGTEXT DEFAULT NULL, - created_at DATETIME NOT NULL, - settled_at DATETIME DEFAULT NULL, - reversed_at DATETIME DEFAULT NULL, - PRIMARY KEY (id), - UNIQUE KEY uniq_creator_order (author_id, order_number), - KEY idx_author (author_id), - KEY idx_order (order_number), - KEY idx_post (post_id), - KEY idx_buyer (buyer_id), - KEY idx_currency (currency), - KEY idx_status (status), - KEY idx_created (created_at) - ) ENGINE=InnoDB {$charset_collate};"; - dbDelta($sql_revenue); - - // ---------- 10. 提现台账 ---------- - $sql_withdrawal = "CREATE TABLE {$prefix}aksrt_withdrawal ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - withdrawal_number VARCHAR(32) NOT NULL, - user_id BIGINT UNSIGNED NOT NULL, - amount DECIMAL(10,2) NOT NULL DEFAULT 0, - gross_amount DECIMAL(10,2) NOT NULL DEFAULT 0, - method VARCHAR(32) NOT NULL DEFAULT '', - account_info TEXT DEFAULT NULL, - status VARCHAR(20) NOT NULL DEFAULT 'pending', - operation_key VARCHAR(191) DEFAULT NULL, - ledger_transaction_id BIGINT UNSIGNED DEFAULT NULL, - created_at DATETIME NOT NULL, - processed_at DATETIME DEFAULT NULL, - note TEXT DEFAULT NULL, - PRIMARY KEY (id), - UNIQUE KEY uniq_withdrawal_number (withdrawal_number), - UNIQUE KEY uniq_withdrawal_operation (operation_key), - UNIQUE KEY uniq_ledger_transaction (ledger_transaction_id), - KEY idx_user (user_id), - KEY idx_user_created (user_id, created_at), - KEY idx_status (status), - KEY idx_created (created_at) - ) ENGINE=InnoDB {$charset_collate};"; - dbDelta($sql_withdrawal); - - // ---------- 11. 举报工单 ---------- - $sql_report = "CREATE TABLE {$prefix}aksrt_report ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - reporter_id BIGINT UNSIGNED NOT NULL, - target_type VARCHAR(20) NOT NULL, - target_id BIGINT UNSIGNED NOT NULL, - reason TEXT NOT NULL, - category VARCHAR(40) NOT NULL DEFAULT '', - status VARCHAR(20) NOT NULL DEFAULT 'pending', - created_at DATETIME NOT NULL, - updated_at DATETIME DEFAULT NULL, - notes_json TEXT DEFAULT NULL, - PRIMARY KEY (id), - KEY idx_status (status), - KEY idx_target (target_type, target_id), - KEY idx_reporter (reporter_id), - KEY idx_created (created_at) - ) ENGINE=InnoDB {$charset_collate};"; - dbDelta($sql_report); - - // ---------- 12. 用户限制/封禁 ---------- - $sql_restriction = "CREATE TABLE {$prefix}aksrt_user_restriction ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - user_id BIGINT UNSIGNED NOT NULL, - type VARCHAR(20) NOT NULL DEFAULT 'warn', - reason TEXT NOT NULL, - scope VARCHAR(20) NOT NULL DEFAULT 'global', - expires_at DATETIME DEFAULT NULL, - created_by BIGINT UNSIGNED NOT NULL DEFAULT 0, - created_at DATETIME NOT NULL, - PRIMARY KEY (id), - KEY idx_user (user_id), - KEY idx_type (type), - KEY idx_scope (scope), - KEY idx_expires (expires_at), - KEY idx_created (created_at) - ) ENGINE=InnoDB {$charset_collate};"; - dbDelta($sql_restriction); - - // ── follow ── - $sql_follow = "CREATE TABLE {$prefix}aksrt_follow ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - follower_id BIGINT UNSIGNED NOT NULL, - following_id BIGINT UNSIGNED NOT NULL, - created_at DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00', - PRIMARY KEY (id), - UNIQUE KEY idx_pair (follower_id, following_id), - KEY idx_following (following_id), - KEY idx_created (created_at) - ) ENGINE=InnoDB {$charset_collate};"; - dbDelta($sql_follow); - - // ── coupon ── - $sql_coupon = "CREATE TABLE {$prefix}aksrt_coupon ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - code VARCHAR(60) NOT NULL DEFAULT '', - type VARCHAR(20) NOT NULL DEFAULT 'fixed', - value DECIMAL(10,2) NOT NULL DEFAULT 0, - min_amount DECIMAL(10,2) NOT NULL DEFAULT 0, - usage_limit INT UNSIGNED NOT NULL DEFAULT 0, - used_count INT UNSIGNED NOT NULL DEFAULT 0, - expires_at DATETIME DEFAULT NULL, - per_user_limit INT UNSIGNED NOT NULL DEFAULT 0, - product_scope TEXT DEFAULT NULL, - max_discount DECIMAL(10,2) NOT NULL DEFAULT 0, - status VARCHAR(20) NOT NULL DEFAULT 'active', - created_at DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00', - PRIMARY KEY (id), - UNIQUE KEY idx_code (code), - KEY idx_status (status), - KEY idx_expires (expires_at) - ) ENGINE=InnoDB {$charset_collate};"; - dbDelta($sql_coupon); - - // ── coupon_usage (R68) ── - $sql_coupon_usage = "CREATE TABLE {$prefix}aksrt_coupon_usage ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - coupon_id BIGINT UNSIGNED NOT NULL DEFAULT 0, - user_id BIGINT UNSIGNED NOT NULL DEFAULT 0, - order_number VARCHAR(32) NOT NULL DEFAULT '', - discount DECIMAL(10,2) NOT NULL DEFAULT 0, - created_at DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00', - PRIMARY KEY (id), - KEY idx_coupon (coupon_id), - KEY idx_user (user_id), - UNIQUE KEY uniq_coupon_usage_order (order_number) - ) ENGINE=InnoDB {$charset_collate};"; - dbDelta($sql_coupon_usage); - - // ── typed charge cards ── - $sql_charge_card = "CREATE TABLE {$prefix}aksrt_charge_card ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - card VARCHAR(60) NOT NULL DEFAULT '', - password VARCHAR(80) NOT NULL DEFAULT '', - type VARCHAR(20) NOT NULL DEFAULT 'balance', - amount DECIMAL(10,2) NOT NULL DEFAULT 0, - balance DECIMAL(10,2) NOT NULL DEFAULT 0, - status VARCHAR(20) NOT NULL DEFAULT 'active', - meta LONGTEXT DEFAULT NULL, - order_number VARCHAR(32) NOT NULL DEFAULT '', - used_by BIGINT UNSIGNED DEFAULT NULL, - used_at DATETIME DEFAULT NULL, - expires_at DATETIME DEFAULT NULL, - created_at DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00', - note TEXT DEFAULT NULL, - PRIMARY KEY (id), - UNIQUE KEY idx_password (password), - KEY idx_card (card), - KEY idx_type (type), - KEY idx_status (status), - KEY idx_used_by (used_by), - KEY idx_order_number (order_number), - KEY idx_expires (expires_at) - ) ENGINE=InnoDB {$charset_collate};"; - dbDelta($sql_charge_card); - - // ── one-time invite codes (registration gate + per-batch rewards) ── - $sql_invite_code = "CREATE TABLE {$prefix}aksrt_invite_code ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - code VARCHAR(32) NOT NULL, - batch_id VARCHAR(32) NOT NULL DEFAULT '', - remark VARCHAR(191) NOT NULL DEFAULT '', - rewards TEXT DEFAULT NULL, - max_uses INT UNSIGNED NOT NULL DEFAULT 1, - used_count INT UNSIGNED NOT NULL DEFAULT 0, - status VARCHAR(20) NOT NULL DEFAULT 'active', - expires_at DATETIME DEFAULT NULL, - created_by BIGINT UNSIGNED NOT NULL DEFAULT 0, - created_at DATETIME NOT NULL, - updated_at DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00', - PRIMARY KEY (id), - UNIQUE KEY uniq_invite_code (code), - KEY idx_batch (batch_id), - KEY idx_status (status), - KEY idx_expires (expires_at) - ) ENGINE=InnoDB {$charset_collate};"; - dbDelta($sql_invite_code); - - // ── invite-code usage log (who consumed which code) ── - $sql_invite_code_usage = "CREATE TABLE {$prefix}aksrt_invite_code_usage ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - code_id BIGINT UNSIGNED NOT NULL, - user_id BIGINT UNSIGNED NOT NULL, - used_at DATETIME NOT NULL, - PRIMARY KEY (id), - UNIQUE KEY uniq_invite_usage (code_id, user_id), - KEY idx_usage_user (user_id), - KEY idx_usage_code (code_id) - ) ENGINE=InnoDB {$charset_collate};"; - dbDelta($sql_invite_code_usage); - - // ── canonical Ref attribution and permanent user binding ── - $sql_affiliate_relation = "CREATE TABLE {$prefix}aksrt_affiliate_relation ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - referral_code VARCHAR(32) NOT NULL, - promoter_id BIGINT UNSIGNED NOT NULL, - invitee_id BIGINT UNSIGNED DEFAULT NULL, - first_touch_at DATETIME NOT NULL, - bound_at DATETIME DEFAULT NULL, - status VARCHAR(20) NOT NULL DEFAULT 'captured', - reward_status VARCHAR(20) NOT NULL DEFAULT 'skipped', - rewarded_at DATETIME DEFAULT NULL, - registration_ip VARCHAR(64) DEFAULT NULL, - created_at DATETIME NOT NULL, - PRIMARY KEY (id), - KEY idx_referral_code (referral_code), - UNIQUE KEY uniq_invitee (invitee_id), - KEY idx_promoter (promoter_id), - KEY idx_status_touch (status, first_touch_at), - KEY idx_reward_status (reward_status), - KEY idx_reward_ip (registration_ip, rewarded_at) - ) ENGINE=InnoDB {$charset_collate};"; - dbDelta($sql_affiliate_relation); - - // ── immutable commission ledger ── - // 佣金即时入账(无冻结/结算期):下单支付即 credit 到 COMMISSION 资金池, - // 本表仅作佣金明细账 + 退款冲正的溯源记录。status: credited | cancelled。 - $sql_commission_ledger = "CREATE TABLE {$prefix}aksrt_commission_ledger ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - promoter_id BIGINT UNSIGNED NOT NULL, - source_user_id BIGINT UNSIGNED NOT NULL DEFAULT 0, - order_number VARCHAR(64) NOT NULL, - order_type VARCHAR(32) NOT NULL, - eligible_base DECIMAL(18,2) NOT NULL DEFAULT 0, - ratio DECIMAL(8,4) NOT NULL DEFAULT 0, - original_amount DECIMAL(18,2) NOT NULL DEFAULT 0, - adjustment_total DECIMAL(18,2) NOT NULL DEFAULT 0, - status VARCHAR(20) NOT NULL DEFAULT 'credited', - idempotency_key VARCHAR(128) NOT NULL, - cancel_operation_key VARCHAR(191) DEFAULT NULL, - cancel_payload_hash CHAR(64) DEFAULT NULL, - created_at DATETIME NOT NULL, - note TEXT DEFAULT NULL, - PRIMARY KEY (id), - UNIQUE KEY uniq_commission_idempotency (idempotency_key), - UNIQUE KEY uniq_commission_cancel_operation (cancel_operation_key), - KEY idx_promoter_status (promoter_id, status), - KEY idx_order_type (order_number, order_type), - KEY idx_created (created_at) - ) ENGINE=InnoDB {$charset_collate};"; - dbDelta($sql_commission_ledger); - - // 佣金欠款挂账:退款全额收回后,若佣金池不足产生的差额记为欠款, - // 后续新增佣金先抵扣欠款再入池。每用户一行。 - $sql_commission_debt = "CREATE TABLE {$prefix}aksrt_commission_debt ( - user_id BIGINT UNSIGNED NOT NULL, - debt_minor BIGINT UNSIGNED NOT NULL DEFAULT 0, - updated_at DATETIME NOT NULL, - PRIMARY KEY (user_id) - ) ENGINE=InnoDB {$charset_collate};"; - dbDelta($sql_commission_debt); - - // ── immutable commission adjustments (refund/debt/manual) ── - $sql_commission_adjustment = "CREATE TABLE {$prefix}aksrt_commission_adjustment ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - ledger_id BIGINT UNSIGNED NOT NULL, - promoter_id BIGINT UNSIGNED NOT NULL, - amount DECIMAL(18,2) NOT NULL DEFAULT 0, - reason VARCHAR(32) NOT NULL, - source_key VARCHAR(128) NOT NULL, - operator_id BIGINT UNSIGNED NOT NULL DEFAULT 0, - created_at DATETIME NOT NULL, - note TEXT DEFAULT NULL, - PRIMARY KEY (id), - UNIQUE KEY uniq_adjustment_source (source_key), - KEY idx_ledger (ledger_id), - KEY idx_promoter (promoter_id) - ) ENGINE=InnoDB {$charset_collate};"; - dbDelta($sql_commission_adjustment); - - // ── payout account snapshots ── - $sql_payout_account = "CREATE TABLE {$prefix}aksrt_payout_account ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - user_id BIGINT UNSIGNED NOT NULL, - channel VARCHAR(20) NOT NULL, - attachment_id BIGINT UNSIGNED NOT NULL DEFAULT 0, - account_hash CHAR(64) NOT NULL DEFAULT '', - status VARCHAR(20) NOT NULL DEFAULT 'active', - created_at DATETIME NOT NULL, - updated_at DATETIME NOT NULL, - PRIMARY KEY (id), - UNIQUE KEY uniq_user_channel (user_id, channel), - KEY idx_user_status (user_id, status) - ) ENGINE=InnoDB {$charset_collate};"; - dbDelta($sql_payout_account); - - // ── order_items (§6.5 多商品多商家) ── - $sql_order_items = "CREATE TABLE {$prefix}aksrt_order_item ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - order_number VARCHAR(32) NOT NULL DEFAULT '', - product_id BIGINT UNSIGNED NOT NULL DEFAULT 0, - product_title VARCHAR(255) NOT NULL DEFAULT '', - options_key VARCHAR(255) NOT NULL DEFAULT '', - options_label VARCHAR(255) NOT NULL DEFAULT '', - quantity INT UNSIGNED NOT NULL DEFAULT 1, - unit_price DECIMAL(10,2) NOT NULL DEFAULT 0, - total_price DECIMAL(10,2) NOT NULL DEFAULT 0, - discount_amount DECIMAL(10,2) NOT NULL DEFAULT 0, - shipping_fee DECIMAL(10,2) NOT NULL DEFAULT 0, - pay_price DECIMAL(10,2) NOT NULL DEFAULT 0, - shipping_type VARCHAR(20) NOT NULL DEFAULT 'express', - auto_delivery_content TEXT DEFAULT NULL, - discount_hit TEXT DEFAULT NULL, - gift_data TEXT DEFAULT NULL, - image VARCHAR(512) NOT NULL DEFAULT '', - PRIMARY KEY (id), - KEY idx_order (order_number), - KEY idx_product (product_id) - ) ENGINE=InnoDB {$charset_collate};"; - dbDelta($sql_order_items); - - self::migrate_identity_tables($prefix, $charset_collate); - self::migrate_vip_tables($prefix, $charset_collate); - } -} +get_charset_collate(); + $prefix = $wpdb->prefix; + + // ---------- 1. 统一资产账户与不可变账本 ---------- + $sql_asset_account = "CREATE TABLE {$prefix}aksrt_asset_account ( + user_id BIGINT UNSIGNED NOT NULL, + asset VARCHAR(16) NOT NULL, + balance_minor BIGINT UNSIGNED NOT NULL DEFAULT 0, + version BIGINT UNSIGNED NOT NULL DEFAULT 0, + updated_at DATETIME(6) NOT NULL, + PRIMARY KEY (user_id, asset), + KEY idx_asset_balance (asset, balance_minor) + ) ENGINE=InnoDB {$charset_collate};"; + dbDelta($sql_asset_account); + + $sql_asset_transaction = "CREATE TABLE {$prefix}aksrt_asset_transaction ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + operation_key VARCHAR(191) NOT NULL, + payload_hash CHAR(64) NOT NULL, + reason VARCHAR(64) NOT NULL, + ref_type VARCHAR(32) DEFAULT NULL, + ref_key VARCHAR(191) DEFAULT NULL, + actor_user_id BIGINT UNSIGNED NOT NULL DEFAULT 0, + metadata LONGTEXT DEFAULT NULL, + ip VARCHAR(64) DEFAULT NULL, + ua VARCHAR(255) DEFAULT NULL, + created_at DATETIME(6) NOT NULL, + PRIMARY KEY (id), + UNIQUE KEY uniq_operation_key (operation_key), + KEY idx_reason_time (reason, created_at), + KEY idx_reference (ref_type, ref_key) + ) ENGINE=InnoDB {$charset_collate};"; + dbDelta($sql_asset_transaction); + + $sql_asset_entry = "CREATE TABLE {$prefix}aksrt_asset_entry ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + transaction_id BIGINT UNSIGNED NOT NULL, + line_no SMALLINT UNSIGNED NOT NULL, + user_id BIGINT UNSIGNED NOT NULL, + asset VARCHAR(16) NOT NULL, + entry_type VARCHAR(32) NOT NULL, + delta_minor BIGINT NOT NULL, + balance_after_minor BIGINT UNSIGNED NOT NULL, + counterparty_user_id BIGINT UNSIGNED DEFAULT NULL, + remark VARCHAR(255) DEFAULT NULL, + created_at DATETIME(6) NOT NULL, + PRIMARY KEY (id), + UNIQUE KEY uniq_transaction_line (transaction_id, line_no), + KEY idx_user_asset_time (user_id, asset, created_at, id), + KEY idx_transaction (transaction_id) + ) ENGINE=InnoDB {$charset_collate};"; + dbDelta($sql_asset_entry); + + // 后台「资金流水」页的默认查询是 WHERE 1=1 ORDER BY created_at DESC, id DESC LIMIT n + // 外加同条件的 COUNT(*),且界面上的日期区间筛选也只有 created_at 可用。 + // 索引必须能以 created_at 为前导列,否则每次翻页都是全表扫描 + filesort, + // 而 idx_flow_event_time 的前导列是 event_type(筛选时可空),救不了这两条查询。 + $sql_financial_flow = "CREATE TABLE {$prefix}aksrt_financial_flow ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + flow_number VARCHAR(32) NOT NULL, + operation_key VARCHAR(191) NOT NULL, + event_type VARCHAR(64) NOT NULL, + ref_type VARCHAR(32) DEFAULT NULL, + ref_key VARCHAR(191) DEFAULT NULL, + order_number VARCHAR(32) DEFAULT NULL, + withdrawal_number VARCHAR(32) DEFAULT NULL, + ledger_transaction_id BIGINT UNSIGNED DEFAULT NULL, + provider_transaction_id VARCHAR(128) DEFAULT NULL, + metadata LONGTEXT DEFAULT NULL, + created_at DATETIME(6) NOT NULL, + PRIMARY KEY (id), + UNIQUE KEY uniq_flow_number (flow_number), + UNIQUE KEY uniq_flow_operation (operation_key), + UNIQUE KEY uniq_flow_ledger_transaction (ledger_transaction_id), + KEY idx_flow_created (created_at), + KEY idx_flow_event_time (event_type, created_at), + KEY idx_flow_order (order_number), + KEY idx_flow_withdrawal (withdrawal_number), + KEY idx_flow_reference (ref_type, ref_key), + KEY idx_flow_provider_transaction (provider_transaction_id) + ) ENGINE=InnoDB {$charset_collate};"; + dbDelta($sql_financial_flow); + + $sql_financial_flow_entry = "CREATE TABLE {$prefix}aksrt_financial_flow_entry ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + flow_id BIGINT UNSIGNED NOT NULL, + line_no SMALLINT UNSIGNED NOT NULL, + user_id BIGINT UNSIGNED NOT NULL DEFAULT 0, + asset VARCHAR(16) NOT NULL, + direction VARCHAR(8) NOT NULL, + entry_type VARCHAR(32) NOT NULL, + delta_minor BIGINT NOT NULL, + balance_after_minor BIGINT UNSIGNED DEFAULT NULL, + counterparty_user_id BIGINT UNSIGNED DEFAULT NULL, + remark VARCHAR(255) DEFAULT NULL, + PRIMARY KEY (id), + UNIQUE KEY uniq_flow_line (flow_id, line_no), + KEY idx_flow_entry_user (user_id, flow_id), + KEY idx_flow_entry_asset (asset, flow_id), + KEY idx_flow_entry_direction (direction, flow_id), + KEY idx_flow_entry_counterparty (counterparty_user_id, flow_id) + ) ENGINE=InnoDB {$charset_collate};"; + dbDelta($sql_financial_flow_entry); + + // ---------- 2. 签到 ---------- + $sql_signin = "CREATE TABLE {$prefix}aksrt_signin_log ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + user_id BIGINT UNSIGNED NOT NULL, + signin_date DATE NOT NULL, + streak INT NOT NULL DEFAULT 1, + reward INT NOT NULL DEFAULT 0, + PRIMARY KEY (id), + UNIQUE KEY uniq_user_date (user_id, signin_date), + KEY idx_user (user_id) + ) ENGINE=InnoDB {$charset_collate};"; + dbDelta($sql_signin); + + // ---------- 3. 下载 token ---------- + $sql_dl_token = "CREATE TABLE {$prefix}aksrt_dl_token ( + token CHAR(48) NOT NULL, + user_id BIGINT UNSIGNED NOT NULL, + post_id BIGINT UNSIGNED NOT NULL, + source_index INT NOT NULL DEFAULT 0, + cost INT NOT NULL DEFAULT 0, + ip_bind VARCHAR(64) DEFAULT NULL, + ua_hash CHAR(40) DEFAULT NULL, + used_count INT NOT NULL DEFAULT 0, + max_uses INT NOT NULL DEFAULT 1, + expires_at DATETIME NOT NULL, + created_at DATETIME NOT NULL, + PRIMARY KEY (token), + KEY idx_expires (expires_at), + KEY idx_user (user_id), + KEY idx_post (post_id) + ) ENGINE=InnoDB {$charset_collate};"; + dbDelta($sql_dl_token); + + // ---------- 5. 用户通知收件箱 ---------- + $sql_notification = "CREATE TABLE {$prefix}aksrt_notification ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + user_id BIGINT UNSIGNED NOT NULL, + type VARCHAR(32) NOT NULL, + title VARCHAR(191) NOT NULL, + description TEXT DEFAULT NULL, + target_url TEXT DEFAULT NULL, + is_read TINYINT(1) NOT NULL DEFAULT 0, + created_at DATETIME NOT NULL, + read_at DATETIME DEFAULT NULL, + PRIMARY KEY (id), + KEY idx_user_created (user_id, created_at), + KEY idx_user_read (user_id, is_read) + ) ENGINE=InnoDB {$charset_collate};"; + dbDelta($sql_notification); + + // ---------- 8. 订单表 ---------- + $sql_order = "CREATE TABLE {$prefix}aksrt_order ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + order_number VARCHAR(32) NOT NULL, + request_key VARCHAR(191) DEFAULT NULL, + user_id BIGINT UNSIGNED NOT NULL, + product_type VARCHAR(32) NOT NULL DEFAULT '', + product_id BIGINT UNSIGNED NOT NULL DEFAULT 0, + amount DECIMAL(10,2) NOT NULL DEFAULT 0, + currency VARCHAR(8) NOT NULL DEFAULT 'CNY', + status VARCHAR(20) NOT NULL DEFAULT 'pending', + shipping_status TINYINT NOT NULL DEFAULT 0, + payment_method VARCHAR(32) NOT NULL DEFAULT '', + payment_transaction_id VARCHAR(128) NOT NULL DEFAULT '', + created_at DATETIME NOT NULL, + paid_at DATETIME DEFAULT NULL, + delivered_at DATETIME DEFAULT NULL, + completed_at DATETIME DEFAULT NULL, + cancelled_at DATETIME DEFAULT NULL, + refunded_at DATETIME DEFAULT NULL, + expires_at DATETIME DEFAULT NULL, + note TEXT DEFAULT NULL, + PRIMARY KEY (id), + UNIQUE KEY uniq_order_number (order_number), + UNIQUE KEY uniq_request_key (request_key), + KEY idx_user (user_id), + KEY idx_user_status_created (user_id, status, created_at), + KEY idx_status (status), + KEY idx_created (created_at), + KEY idx_product (product_type, product_id) + ) ENGINE=InnoDB {$charset_collate};"; + dbDelta($sql_order); + + // ---------- order after-sales ---------- + $sql_order_aftersale = "CREATE TABLE {$prefix}aksrt_order_aftersale ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + order_number VARCHAR(32) NOT NULL, + user_id BIGINT UNSIGNED NOT NULL, + aftersale_type VARCHAR(24) NOT NULL DEFAULT 'refund', + reason TEXT NOT NULL, + apply_form LONGTEXT DEFAULT NULL, + images LONGTEXT DEFAULT NULL, + refund_amount VARCHAR(32) NOT NULL DEFAULT '0', + status VARCHAR(20) NOT NULL DEFAULT 'pending', + admin_note TEXT DEFAULT NULL, + gateway_request_id VARCHAR(128) DEFAULT NULL, + gateway_refund_id VARCHAR(128) DEFAULT NULL, + gateway_result LONGTEXT DEFAULT NULL, + created_at DATETIME NOT NULL, + reviewed_at DATETIME DEFAULT NULL, + completed_at DATETIME DEFAULT NULL, + PRIMARY KEY (id), + KEY idx_order_status (order_number, status), + KEY idx_user_created (user_id, created_at), + KEY idx_status_created (status, created_at) + ) ENGINE=InnoDB {$charset_collate};"; + dbDelta($sql_order_aftersale); + + // ---------- 9. 作者收益台账 ---------- + $sql_revenue = "CREATE TABLE {$prefix}aksrt_revenue ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + author_id BIGINT UNSIGNED NOT NULL DEFAULT 0, + order_number VARCHAR(32) NOT NULL DEFAULT '', + post_id BIGINT UNSIGNED NOT NULL DEFAULT 0, + buyer_id BIGINT UNSIGNED NOT NULL DEFAULT 0, + currency VARCHAR(8) NOT NULL DEFAULT 'CNY', + original_amount DECIMAL(18,2) NOT NULL DEFAULT 0, + share_ratio DECIMAL(5,2) NOT NULL DEFAULT 0, + author_level TINYINT UNSIGNED NOT NULL DEFAULT 0, + amount DECIMAL(18,2) NOT NULL DEFAULT 0, + type VARCHAR(32) NOT NULL DEFAULT 'order', + status VARCHAR(20) NOT NULL DEFAULT 'pending', + reversal_amount DECIMAL(18,2) NOT NULL DEFAULT 0, + error_message TEXT DEFAULT NULL, + audit_json LONGTEXT DEFAULT NULL, + created_at DATETIME NOT NULL, + settled_at DATETIME DEFAULT NULL, + reversed_at DATETIME DEFAULT NULL, + PRIMARY KEY (id), + UNIQUE KEY uniq_creator_order (author_id, order_number), + KEY idx_author (author_id), + KEY idx_order (order_number), + KEY idx_post (post_id), + KEY idx_buyer (buyer_id), + KEY idx_currency (currency), + KEY idx_status (status), + KEY idx_created (created_at) + ) ENGINE=InnoDB {$charset_collate};"; + dbDelta($sql_revenue); + + // ---------- 10. 提现台账 ---------- + $sql_withdrawal = "CREATE TABLE {$prefix}aksrt_withdrawal ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + withdrawal_number VARCHAR(32) NOT NULL, + user_id BIGINT UNSIGNED NOT NULL, + amount DECIMAL(10,2) NOT NULL DEFAULT 0, + gross_amount DECIMAL(10,2) NOT NULL DEFAULT 0, + method VARCHAR(32) NOT NULL DEFAULT '', + account_info TEXT DEFAULT NULL, + status VARCHAR(20) NOT NULL DEFAULT 'pending', + operation_key VARCHAR(191) DEFAULT NULL, + ledger_transaction_id BIGINT UNSIGNED DEFAULT NULL, + created_at DATETIME NOT NULL, + processed_at DATETIME DEFAULT NULL, + note TEXT DEFAULT NULL, + PRIMARY KEY (id), + UNIQUE KEY uniq_withdrawal_number (withdrawal_number), + UNIQUE KEY uniq_withdrawal_operation (operation_key), + UNIQUE KEY uniq_ledger_transaction (ledger_transaction_id), + KEY idx_user (user_id), + KEY idx_user_created (user_id, created_at), + KEY idx_status (status), + KEY idx_created (created_at) + ) ENGINE=InnoDB {$charset_collate};"; + dbDelta($sql_withdrawal); + + // ---------- 11. 举报工单 ---------- + $sql_report = "CREATE TABLE {$prefix}aksrt_report ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + reporter_id BIGINT UNSIGNED NOT NULL, + target_type VARCHAR(20) NOT NULL, + target_id BIGINT UNSIGNED NOT NULL, + reason TEXT NOT NULL, + category VARCHAR(40) NOT NULL DEFAULT '', + status VARCHAR(20) NOT NULL DEFAULT 'pending', + created_at DATETIME NOT NULL, + updated_at DATETIME DEFAULT NULL, + notes_json TEXT DEFAULT NULL, + PRIMARY KEY (id), + KEY idx_status (status), + KEY idx_target (target_type, target_id), + KEY idx_reporter (reporter_id), + KEY idx_created (created_at) + ) ENGINE=InnoDB {$charset_collate};"; + dbDelta($sql_report); + + // ---------- 12. 用户限制/封禁 ---------- + $sql_restriction = "CREATE TABLE {$prefix}aksrt_user_restriction ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + user_id BIGINT UNSIGNED NOT NULL, + type VARCHAR(20) NOT NULL DEFAULT 'warn', + reason TEXT NOT NULL, + scope VARCHAR(20) NOT NULL DEFAULT 'global', + expires_at DATETIME DEFAULT NULL, + created_by BIGINT UNSIGNED NOT NULL DEFAULT 0, + created_at DATETIME NOT NULL, + PRIMARY KEY (id), + KEY idx_user (user_id), + KEY idx_type (type), + KEY idx_scope (scope), + KEY idx_expires (expires_at), + KEY idx_created (created_at) + ) ENGINE=InnoDB {$charset_collate};"; + dbDelta($sql_restriction); + + // ── follow ── + $sql_follow = "CREATE TABLE {$prefix}aksrt_follow ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + follower_id BIGINT UNSIGNED NOT NULL, + following_id BIGINT UNSIGNED NOT NULL, + created_at DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00', + PRIMARY KEY (id), + UNIQUE KEY idx_pair (follower_id, following_id), + KEY idx_following (following_id), + KEY idx_created (created_at) + ) ENGINE=InnoDB {$charset_collate};"; + dbDelta($sql_follow); + + // ── coupon ── + $sql_coupon = "CREATE TABLE {$prefix}aksrt_coupon ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + code VARCHAR(60) NOT NULL DEFAULT '', + type VARCHAR(20) NOT NULL DEFAULT 'fixed', + value DECIMAL(10,2) NOT NULL DEFAULT 0, + min_amount DECIMAL(10,2) NOT NULL DEFAULT 0, + usage_limit INT UNSIGNED NOT NULL DEFAULT 0, + used_count INT UNSIGNED NOT NULL DEFAULT 0, + expires_at DATETIME DEFAULT NULL, + per_user_limit INT UNSIGNED NOT NULL DEFAULT 0, + product_scope TEXT DEFAULT NULL, + max_discount DECIMAL(10,2) NOT NULL DEFAULT 0, + status VARCHAR(20) NOT NULL DEFAULT 'active', + created_at DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00', + PRIMARY KEY (id), + UNIQUE KEY idx_code (code), + KEY idx_status (status), + KEY idx_expires (expires_at) + ) ENGINE=InnoDB {$charset_collate};"; + dbDelta($sql_coupon); + + // ── coupon_usage (R68) ── + $sql_coupon_usage = "CREATE TABLE {$prefix}aksrt_coupon_usage ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + coupon_id BIGINT UNSIGNED NOT NULL DEFAULT 0, + user_id BIGINT UNSIGNED NOT NULL DEFAULT 0, + order_number VARCHAR(32) NOT NULL DEFAULT '', + discount DECIMAL(10,2) NOT NULL DEFAULT 0, + created_at DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00', + PRIMARY KEY (id), + KEY idx_coupon (coupon_id), + KEY idx_user (user_id), + UNIQUE KEY uniq_coupon_usage_order (order_number) + ) ENGINE=InnoDB {$charset_collate};"; + dbDelta($sql_coupon_usage); + + // ── typed charge cards ── + $sql_charge_card = "CREATE TABLE {$prefix}aksrt_charge_card ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + card VARCHAR(60) NOT NULL DEFAULT '', + password VARCHAR(80) NOT NULL DEFAULT '', + type VARCHAR(20) NOT NULL DEFAULT 'balance', + amount DECIMAL(10,2) NOT NULL DEFAULT 0, + balance DECIMAL(10,2) NOT NULL DEFAULT 0, + status VARCHAR(20) NOT NULL DEFAULT 'active', + meta LONGTEXT DEFAULT NULL, + order_number VARCHAR(32) NOT NULL DEFAULT '', + used_by BIGINT UNSIGNED DEFAULT NULL, + used_at DATETIME DEFAULT NULL, + expires_at DATETIME DEFAULT NULL, + created_at DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00', + note TEXT DEFAULT NULL, + PRIMARY KEY (id), + UNIQUE KEY idx_password (password), + KEY idx_card (card), + KEY idx_type (type), + KEY idx_status (status), + KEY idx_used_by (used_by), + KEY idx_order_number (order_number), + KEY idx_expires (expires_at) + ) ENGINE=InnoDB {$charset_collate};"; + dbDelta($sql_charge_card); + + // ── one-time invite codes (registration gate + per-batch rewards) ── + $sql_invite_code = "CREATE TABLE {$prefix}aksrt_invite_code ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + code VARCHAR(32) NOT NULL, + batch_id VARCHAR(32) NOT NULL DEFAULT '', + remark VARCHAR(191) NOT NULL DEFAULT '', + rewards TEXT DEFAULT NULL, + max_uses INT UNSIGNED NOT NULL DEFAULT 1, + used_count INT UNSIGNED NOT NULL DEFAULT 0, + status VARCHAR(20) NOT NULL DEFAULT 'active', + expires_at DATETIME DEFAULT NULL, + created_by BIGINT UNSIGNED NOT NULL DEFAULT 0, + created_at DATETIME NOT NULL, + updated_at DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00', + PRIMARY KEY (id), + UNIQUE KEY uniq_invite_code (code), + KEY idx_batch (batch_id), + KEY idx_status (status), + KEY idx_expires (expires_at) + ) ENGINE=InnoDB {$charset_collate};"; + dbDelta($sql_invite_code); + + // ── invite-code usage log (who consumed which code) ── + $sql_invite_code_usage = "CREATE TABLE {$prefix}aksrt_invite_code_usage ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + code_id BIGINT UNSIGNED NOT NULL, + user_id BIGINT UNSIGNED NOT NULL, + used_at DATETIME NOT NULL, + PRIMARY KEY (id), + UNIQUE KEY uniq_invite_usage (code_id, user_id), + KEY idx_usage_user (user_id), + KEY idx_usage_code (code_id) + ) ENGINE=InnoDB {$charset_collate};"; + dbDelta($sql_invite_code_usage); + + // ── canonical Ref attribution and permanent user binding ── + $sql_affiliate_relation = "CREATE TABLE {$prefix}aksrt_affiliate_relation ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + referral_code VARCHAR(32) NOT NULL, + promoter_id BIGINT UNSIGNED NOT NULL, + invitee_id BIGINT UNSIGNED DEFAULT NULL, + first_touch_at DATETIME NOT NULL, + bound_at DATETIME DEFAULT NULL, + status VARCHAR(20) NOT NULL DEFAULT 'captured', + reward_status VARCHAR(20) NOT NULL DEFAULT 'skipped', + rewarded_at DATETIME DEFAULT NULL, + registration_ip VARCHAR(64) DEFAULT NULL, + created_at DATETIME NOT NULL, + PRIMARY KEY (id), + KEY idx_referral_code (referral_code), + UNIQUE KEY uniq_invitee (invitee_id), + KEY idx_promoter (promoter_id), + KEY idx_status_touch (status, first_touch_at), + KEY idx_reward_status (reward_status), + KEY idx_reward_ip (registration_ip, rewarded_at) + ) ENGINE=InnoDB {$charset_collate};"; + dbDelta($sql_affiliate_relation); + + // ── immutable commission ledger ── + // 佣金即时入账(无冻结/结算期):下单支付即 credit 到 COMMISSION 资金池, + // 本表仅作佣金明细账 + 退款冲正的溯源记录。status: credited | cancelled。 + $sql_commission_ledger = "CREATE TABLE {$prefix}aksrt_commission_ledger ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + promoter_id BIGINT UNSIGNED NOT NULL, + source_user_id BIGINT UNSIGNED NOT NULL DEFAULT 0, + order_number VARCHAR(64) NOT NULL, + order_type VARCHAR(32) NOT NULL, + eligible_base DECIMAL(18,2) NOT NULL DEFAULT 0, + ratio DECIMAL(8,4) NOT NULL DEFAULT 0, + original_amount DECIMAL(18,2) NOT NULL DEFAULT 0, + adjustment_total DECIMAL(18,2) NOT NULL DEFAULT 0, + status VARCHAR(20) NOT NULL DEFAULT 'credited', + idempotency_key VARCHAR(128) NOT NULL, + cancel_operation_key VARCHAR(191) DEFAULT NULL, + cancel_payload_hash CHAR(64) DEFAULT NULL, + created_at DATETIME NOT NULL, + note TEXT DEFAULT NULL, + PRIMARY KEY (id), + UNIQUE KEY uniq_commission_idempotency (idempotency_key), + UNIQUE KEY uniq_commission_cancel_operation (cancel_operation_key), + KEY idx_promoter_status (promoter_id, status), + KEY idx_order_type (order_number, order_type), + KEY idx_created (created_at) + ) ENGINE=InnoDB {$charset_collate};"; + dbDelta($sql_commission_ledger); + + // 佣金欠款挂账:退款全额收回后,若佣金池不足产生的差额记为欠款, + // 后续新增佣金先抵扣欠款再入池。每用户一行。 + $sql_commission_debt = "CREATE TABLE {$prefix}aksrt_commission_debt ( + user_id BIGINT UNSIGNED NOT NULL, + debt_minor BIGINT UNSIGNED NOT NULL DEFAULT 0, + updated_at DATETIME NOT NULL, + PRIMARY KEY (user_id) + ) ENGINE=InnoDB {$charset_collate};"; + dbDelta($sql_commission_debt); + + // ── immutable commission adjustments (refund/debt/manual) ── + $sql_commission_adjustment = "CREATE TABLE {$prefix}aksrt_commission_adjustment ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + ledger_id BIGINT UNSIGNED NOT NULL, + promoter_id BIGINT UNSIGNED NOT NULL, + amount DECIMAL(18,2) NOT NULL DEFAULT 0, + reason VARCHAR(32) NOT NULL, + source_key VARCHAR(128) NOT NULL, + operator_id BIGINT UNSIGNED NOT NULL DEFAULT 0, + created_at DATETIME NOT NULL, + note TEXT DEFAULT NULL, + PRIMARY KEY (id), + UNIQUE KEY uniq_adjustment_source (source_key), + KEY idx_ledger (ledger_id), + KEY idx_promoter (promoter_id) + ) ENGINE=InnoDB {$charset_collate};"; + dbDelta($sql_commission_adjustment); + + // ── payout account snapshots ── + $sql_payout_account = "CREATE TABLE {$prefix}aksrt_payout_account ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + user_id BIGINT UNSIGNED NOT NULL, + channel VARCHAR(20) NOT NULL, + attachment_id BIGINT UNSIGNED NOT NULL DEFAULT 0, + account_hash CHAR(64) NOT NULL DEFAULT '', + status VARCHAR(20) NOT NULL DEFAULT 'active', + created_at DATETIME NOT NULL, + updated_at DATETIME NOT NULL, + PRIMARY KEY (id), + UNIQUE KEY uniq_user_channel (user_id, channel), + KEY idx_user_status (user_id, status) + ) ENGINE=InnoDB {$charset_collate};"; + dbDelta($sql_payout_account); + + // ── order_items (§6.5 多商品多商家) ── + $sql_order_items = "CREATE TABLE {$prefix}aksrt_order_item ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + order_number VARCHAR(32) NOT NULL DEFAULT '', + product_id BIGINT UNSIGNED NOT NULL DEFAULT 0, + product_title VARCHAR(255) NOT NULL DEFAULT '', + options_key VARCHAR(255) NOT NULL DEFAULT '', + options_label VARCHAR(255) NOT NULL DEFAULT '', + quantity INT UNSIGNED NOT NULL DEFAULT 1, + unit_price DECIMAL(10,2) NOT NULL DEFAULT 0, + total_price DECIMAL(10,2) NOT NULL DEFAULT 0, + discount_amount DECIMAL(10,2) NOT NULL DEFAULT 0, + shipping_fee DECIMAL(10,2) NOT NULL DEFAULT 0, + pay_price DECIMAL(10,2) NOT NULL DEFAULT 0, + shipping_type VARCHAR(20) NOT NULL DEFAULT 'express', + auto_delivery_content TEXT DEFAULT NULL, + discount_hit TEXT DEFAULT NULL, + gift_data TEXT DEFAULT NULL, + image VARCHAR(512) NOT NULL DEFAULT '', + PRIMARY KEY (id), + KEY idx_order (order_number), + KEY idx_product (product_id) + ) ENGINE=InnoDB {$charset_collate};"; + dbDelta($sql_order_items); + + self::migrate_identity_tables($prefix, $charset_collate); + self::migrate_vip_tables($prefix, $charset_collate); + } +} diff --git a/src/Payment/AfterSaleService.php b/src/Payment/AfterSaleService.php index 7ad2509c..473bc530 100644 --- a/src/Payment/AfterSaleService.php +++ b/src/Payment/AfterSaleService.php @@ -1,680 +1,680 @@ -[0-9]{20}|[0-9]{22})'; - register_rest_route('aksrt/v1', '/orders/' . $number . '/after-sale', [ - ['methods' => \WP_REST_Server::CREATABLE, 'callback' => [$this, 'rest_submit'], 'permission_callback' => [$this, 'require_user'], 'args' => [ - 'order_number' => ['required' => true, 'sanitize_callback' => 'sanitize_text_field'], - 'reason' => ['required' => true, 'sanitize_callback' => 'sanitize_textarea_field'], - 'reason_label' => ['required' => false, 'sanitize_callback' => 'sanitize_text_field'], - 'materials' => ['required' => false, 'type' => 'array', 'validate_callback' => static function ($value): bool { - if (!is_array($value) || count($value) > 10) return false; - foreach ($value as $item) { - if (!is_array($item)) return false; - foreach (['label', 'value'] as $key) { - if (isset($item[$key]) && !is_string($item[$key])) return false; - } - } - return true; - }], - 'description' => ['required' => false, 'sanitize_callback' => 'sanitize_textarea_field'], - 'images' => ['required' => false, 'type' => 'array', 'validate_callback' => static function ($value): bool { - return is_array($value) && count($value) <= 3; - }], - ]], - ['methods' => \WP_REST_Server::READABLE, 'callback' => [$this, 'rest_list'], 'permission_callback' => [$this, 'require_user'], 'args' => [ - 'order_number' => ['required' => true, 'sanitize_callback' => 'sanitize_text_field'], - ]], - ['methods' => 'DELETE', 'callback' => [$this, 'rest_cancel'], 'permission_callback' => [$this, 'require_user'], 'args' => [ - 'order_number' => ['required' => true, 'sanitize_callback' => 'sanitize_text_field'], - ]], - ]); - // 售后举证图片上传:复用评论图片上传链路(大小 / 类型 / 内容审核同一套校验)。 - register_rest_route('aksrt/v1', '/orders/' . $number . '/after-sale/upload-image', [ - ['methods' => \WP_REST_Server::CREATABLE, 'callback' => [$this, 'rest_upload_image'], 'permission_callback' => [$this, 'require_after_sale_image']], - ]); - } - - /** 售后举证图片上传权限:登录 + nonce + 售后开启 + 图片开关开启 + 订单归属本人。 */ - public function require_after_sale_image(\WP_REST_Request $request) { - $user = $this->require_user($request); - if (is_wp_error($user)) { - return $user; - } - if (!class_exists('\AKSRT\Settings\PaymentRuntime')) { - return new \WP_Error('aksrt_after_sale_forbidden', __('售后功能暂未开启', 'aksrt'), ['status' => 403]); - } - $settings = \AKSRT\Settings\PaymentRuntime::get_after_sale_settings(); - if (empty($settings['enabled']) || empty($settings['img_enabled'])) { - return new \WP_Error('aksrt_after_sale_forbidden', __('售后图片上传未开启', 'aksrt'), ['status' => 403]); - } - $number = sanitize_text_field((string) $request->get_param('order_number')); - if (!OrderRepository::get_instance()->get_user_order(get_current_user_id(), $number)) { - return new \WP_Error('aksrt_order_missing', __('订单不存在', 'aksrt'), ['status' => 404]); - } - return true; - } - - public function rest_upload_image(\WP_REST_Request $request) { - if (!class_exists('\AKSRT\Admin\UserController')) { - return new \WP_Error('aksrt_after_sale_upload', __('上传服务不可用', 'aksrt'), ['status' => 500]); - } - return \AKSRT\Admin\UserController::get_instance()->rest_upload_comment_image($request); - } - - public function require_user(\WP_REST_Request $request) { - if (!is_user_logged_in()) return new \WP_Error('aksrt_need_login', __('请先登录', 'aksrt'), ['status' => 401]); - $nonce = (string) $request->get_header('X-WP-Nonce'); - return $nonce !== '' && wp_verify_nonce($nonce, 'wp_rest') ? true : new \WP_Error('aksrt_rest_nonce', __('安全验证失败,请刷新页面后重试', 'aksrt'), ['status' => 403]); - } - - public function rest_submit(\WP_REST_Request $request) { - $images = $request->get_param('images'); - $materials = $request->get_param('materials'); - $result = $this->create( - get_current_user_id(), - (string) $request->get_param('order_number'), - (string) $request->get_param('reason'), - is_array($images) ? $images : [], - [ - 'reason_label' => (string) $request->get_param('reason_label'), - 'materials' => is_array($materials) ? $materials : [], - 'description' => (string) $request->get_param('description'), - ] - ); - return is_wp_error($result) ? $result : rest_ensure_response(['success' => true, 'data' => $result]); - } - - public function rest_list(\WP_REST_Request $request) { - $number = sanitize_text_field((string) $request->get_param('order_number')); - if (!OrderRepository::get_instance()->get_user_order(get_current_user_id(), $number)) return new \WP_Error('aksrt_order_missing', __('订单不存在', 'aksrt'), ['status' => 404]); - return rest_ensure_response(['success' => true, 'data' => $this->list_for_order($number)]); - } - - public function rest_cancel(\WP_REST_Request $request) { - $result = $this->cancel_for_user(get_current_user_id(), (string) $request->get_param('order_number')); - return is_wp_error($result) ? $result : rest_ensure_response(['success' => true, 'data' => $result]); - } - - /** @param array