'use strict' /** * 「售后策略」功能契约: * 1) 后台 schema(PHP SchemaPayment after_sale 分区)与 SPA JS schema 的键、顺序、 * 类型、默认值 1:1 对齐,且 schema 与侧边页均已注册(/payment/after-sale 可达); * 2) 运行时(PaymentRuntime)缺键回退 schema 默认、HTML 走 kses、时效边界正确; * 3) 售后工单:images 列、create 图片清洗与上限、上传路由权限、latest_for_order; * 4) 订单视图模型输出 after_sale 状态(按钮矩阵的唯一数据源); * 5) 前台:申请/超时效/售后详细三个独立弹窗、modal-mini + 蓝色炫彩头、placeholder 与 * 图片上传配置化、不可再申请提示原文、订单详情弹窗不再内嵌售后表单(死代码清理)。 */ const assert = require('node:assert/strict') const fs = require('node:fs') const path = require('node:path') const { spawnSync } = require('node:child_process') const test = require('node:test') const root = path.resolve(__dirname, '..', '..') const read = (file) => fs.readFileSync(path.join(root, file), 'utf8') const FIELD_ORDER = [ 'after_sale_s', 'after_sale_time_limit', 'after_sale_expired_text', 'after_sale_reasons', 'after_sale_desc', 'after_sale_materials', 'after_sale_reason_placeholder', 'after_sale_img_s', ] /** Extract the balanced [...] block assigned to a PHP array key inside a section body. */ function phpSection(src, sectionKey) { const start = src.indexOf(`'${sectionKey}' => [`) assert.notEqual(start, -1, `PHP schema has a ${sectionKey} section`) const open = src.indexOf('[', start) let depth = 0 for (let i = open; i < src.length; i++) { const ch = src[i] if (ch === "'" || ch === '"') { const quote = ch i++ while (i < src.length) { if (src[i] === '\\') { i += 2; continue } if (src[i] === quote) break i++ } continue } if (ch === '[') depth++ else if (ch === ']') { depth-- if (depth === 0) return src.slice(open, i + 1) } } throw new Error(`unbalanced PHP section: ${sectionKey}`) } test('after-sale schema is 1:1 between PHP and SPA', () => { const php = phpSection(read('src/Admin/Schema/SchemaPayment.php'), 'after_sale') const js = read('assets/admin/src/schema/sections/payment/afterSale.js') assert.match(php, /'key'\s*=>\s*'after_sale'/) assert.match(php, /'title'\s*=>\s*'售后策略'/) assert.match(php, /'option_key'\s*=>\s*'aksrt_settings'/) // 字段键、顺序、类型一致(JS 侧 defaults 与 groups 同序,按首次出现去重) const phpKeys = [...php.matchAll(/'key'\s*=>\s*'([a-z0-9_]+)'/g)].map((m) => m[1]) const jsKeys = [...js.matchAll(/key:\s*'([a-z0-9_]+)'/g)].map((m) => m[1]) const sectionKeys = (keys) => [...new Set(keys.filter((key) => FIELD_ORDER.includes(key)))] assert.deepEqual(sectionKeys(phpKeys), FIELD_ORDER) assert.deepEqual(sectionKeys(jsKeys), FIELD_ORDER) for (const [phpType, jsType] of [ ['switch', 'switch'], ['number', 'number'], ['textarea', 'textarea'], ['group', 'group'], ['input', 'input'], ]) { assert.ok(php.includes(`'type' => '${phpType}'`), `missing PHP field type ${phpType}`) assert.ok(js.includes(`type: '${jsType}'`), `missing JS field type ${jsType}`) } // 默认值一致(开关/时效/占位文案/预置原因/所需材料) assert.match(php, /'after_sale_s', 'label' => '售后功能', 'type' => 'switch', 'default' => true/) assert.match(js, /after_sale_s:\s*true/) assert.match(js, /key: 'after_sale_s', label: '售后功能', type: 'switch', default: true/) assert.match(php, /'after_sale_time_limit', 'label' => '申请售后时效\(天\)', 'type' => 'number', 'default' => 7, 'min' => 0, 'max' => 3650/) assert.match(js, /key: 'after_sale_time_limit', label: '申请售后时效\(天\)', type: 'number', default: 7, min: 0, max: 3650/) assert.match(php, /'after_sale_reason_placeholder', 'label' => '申请说明占位文案', 'type' => 'input', 'default' => '必填,请您详细填写申请原因'/) assert.match(js, /key: 'after_sale_reason_placeholder', label: '申请说明占位文案', type: 'input', default: '必填,请您详细填写申请原因'/) assert.match(php, /'after_sale_img_s', 'label' => '申请售后可上传图片', 'type' => 'switch', 'default' => true/) assert.match(js, /key: 'after_sale_img_s', label: '申请售后可上传图片', type: 'switch', default: true/) for (const reason of ['质量问题', '假冒品牌', '其他原因']) { assert.ok(php.includes(`'t' => '${reason}'`), `missing PHP preset reason ${reason}`) assert.ok(js.includes(`{ t: '${reason}' }`), `missing JS preset reason ${reason}`) } for (const material of ['联系邮箱', '联系电话']) { assert.ok(php.includes(`'t' => '${material}'`), `missing PHP material ${material}`) assert.ok(js.includes(`{ t: '${material}' }`), `missing JS material ${material}`) } // HTML 配置项(超时效提示 / 申请说明)走 kses 通道 assert.match(php, /'after_sale_expired_text', 'label' => '超过时效提示', 'type' => 'textarea', 'sanitize_type' => 'html'/) assert.match(php, /'after_sale_desc', 'label' => '申请售后说明', 'type' => 'textarea', 'sanitize_type' => 'html'/) assert.match(js, /key: 'after_sale_expired_text', label: '超过时效提示', type: 'textarea', sanitize_type: 'html'/) assert.match(js, /key: 'after_sale_desc', label: '申请售后说明', type: 'textarea', sanitize_type: 'html'/) }) test('after-sale schema and sidebar page are registered under 支付&付费', () => { const schemaIndex = read('assets/admin/src/schema/sections/payment/index.js') assert.match(schemaIndex, /import \{ afterSaleSchema \} from '\.\/afterSale\.js'/) assert.match(schemaIndex, /after_sale:\s*afterSaleSchema/) const section = read('assets/admin/src/constants/sections/payment.js') assert.match(section, /key: 'payment-after-sale'/) assert.match(section, /name: 'PaymentAfterSale'/) assert.match(section, /label: '售后策略'/) assert.match(section, /route: '\/payment\/after-sale'/) assert.match(section, /canonicalDomain: 'payment'/) assert.match(section, /section: 'after_sale'/) // 侧栏条目位于提现策略之后(支付&付费末位) assert.ok(section.indexOf("key: 'payment-after-sale'") > section.indexOf("key: 'payment-withdrawal'")) const schemaRegistry = read('assets/admin/src/schema/index.js') assert.match(schemaRegistry, /paymentSchemas/) }) test('after-sale runtime resolves defaults, overrides and the expiry boundary matrix', () => { const result = spawnSync(process.env.PHP_BINARY || 'php', [path.join(__dirname, 'after-sale-policy-harness.php')], { cwd: root, encoding: 'utf8', }) assert.equal(result.status, 0, result.stderr || result.stdout) const payload = JSON.parse(result.stdout) assert.deepEqual(payload.defaults, { enabled: true, time_limit: 7, expired_text: '
抱歉,您的订单已超过售后申请时效
', reasons: [ { slug: 'c440cdf71825', name: '质量问题' }, { slug: '2ead35a456a0', name: '假冒品牌' }, { slug: 'fba26eab6b2b', name: '其他原因' }, ], materials: ['联系邮箱', '联系电话'], desc: '', img_enabled: true, reason_placeholder: '必填,请您详细填写申请原因', }) assert.equal(payload.overridden.enabled, false) assert.equal(payload.overridden.time_limit, 30) assert.equal(payload.overridden.img_enabled, false) assert.equal(payload.overridden.reason_placeholder, '请描述问题') assert.deepEqual(payload.overridden.reasons, [ { slug: 'c440cdf71825', name: '质量问题' }, { slug: '327e0ab68567', name: '发错货' }, ]) assert.deepEqual(payload.overridden.materials, ['联系邮箱']) // HTML 配置项剥离 script,保留 div assert.equal(payload.overridden.expired_text, '
已超过时效
') assert.equal(payload.overridden.desc, '
请保持商品完好
') assert.deepEqual(payload.expired_ok, { within: true, exact_boundary: true, just_expired: true, long_expired: true, no_limit: true, missing_paid: true, }) }) test('after-sale tickets persist evidence images and expose the latest row', () => { const schema = read('src/Core/DBMigratorPart2.php') assert.match(schema, /CREATE TABLE \{\$prefix\}aksrt_order_aftersale[\s\S]*?images LONGTEXT DEFAULT NULL[\s\S]*?refund_amount/) // images 列随 schema 2.5.1 引入:dbDelta 只在版本变化时重跑,列变更必须同步升版本, // 否则老安装不会 ALTER TABLE,写入直接报 Unknown column 'images'。 const migrator = read('src/Core/DBMigrator.php') const version = migrator.match(/const SCHEMA_VERSION = '(\d+)\.(\d+)\.(\d+)'/) assert.ok(version, 'DBMigrator exposes a numeric SCHEMA_VERSION') const [major, minor, patch] = version.slice(1).map(Number) assert.ok( major > 2 || (major === 2 && minor > 5) || (major === 2 && minor === 5 && patch >= 1), `SCHEMA_VERSION must be at least 2.5.1 after adding order_aftersale.images, got ${major}.${minor}.${patch}` ) const service = read('src/Payment/AfterSaleService.php') assert.match(service, /public function create\(int \$user_id, string \$order_number, string \$reason, array \$images = \[\], array \$apply_form = \[\]\)/) // 结构化申请表单:预置原因 / 申请资料 / 申请说明独立入库(reason 融合串保持原样) assert.match(service, /\$apply_form = \$this->sanitize_apply_form\(\$apply_form\);/) assert.match(service, /private function sanitize_apply_form\(array \$form\): array/) assert.match(service, /'apply_form' => \$apply_form === \[\] \? null : wp_json_encode\(\$apply_form/) assert.match(service, /\$images = \$this->sanitize_images\(\$images\);/) assert.match(service, /'images' => wp_json_encode\(\$images\)/) assert.match(service, /private function sanitize_images\(array \$images\): array \{[\s\S]*?count\(\$clean\) >= 3[\s\S]*?esc_url_raw/) assert.match(service, /public function latest_for_order\(string \$order_number\): \?array \{[\s\S]*?ORDER BY id DESC LIMIT 1/) // normalize 输出 images 数组(坏 JSON / 空串回落空数组) assert.match(service, /json_decode\(\(string\) \(\$row\['images'\] \?\? ''\), true\)/) // 图片上传路由:登录 + nonce + 售后/图片开关 + 订单归属;回调委托评论图片上传 assert.match(service, /\/orders\/' \. \$number \. '\/after-sale\/upload-image'/) assert.match(service, /public function require_after_sale_image\(\\WP_REST_Request \$request\)/) assert.match(service, /get_after_sale_settings\(\)[\s\S]*?empty\(\$settings\['enabled'\]\)[\s\S]*?empty\(\$settings\['img_enabled'\]\)/) assert.match(service, /get_user_order\(get_current_user_id\(\), \$number\)/) assert.match(service, /UserController::get_instance\(\)->rest_upload_comment_image\(\$request\)/) assert.match(service, /'images' => \['required' => false, 'type' => 'array'[\s\S]*?count\(\$value\) <= 3/) // 提交 REST 透传 images // rest_submit 同步上报结构化申请表单(reason_label / materials / description) assert.match(service, /'reason_label' => \(string\) \$request->get_param\('reason_label'\),/) assert.match(service, /'materials' +\=> is_array\(\$materials\) \? \$materials : \[\],/) assert.match(service, /'description' +\=> \(string\) \$request->get_param\('description'\),/) // 既有售后资产契约(锁 / 退款 / 完成)不受影响 assert.match(service, /GET_LOCK\(%s, 5\)/) assert.match(service, /Order::STATUS_PAID/) }) test('order view model exposes the after-sale button matrix', () => { const userOrders = read('src/Payment/UserOrders.php') assert.match(userOrders, /private function after_sale_state\(string \$group, string \$order_number, string \$paid_at, \?AfterSaleService \$service, array \$records = \[\]\): array \{/) assert.match(userOrders, /'after_sale' => \$after_sale_state,/) assert.match(userOrders, /'enabled'\s*=> \$enabled,/) assert.match(userOrders, /'expired'\s*=> \(bool\) \$expired,/) assert.match(userOrders, /'has_application' => \$has_application,/) assert.match(userOrders, /'open'\s*=> \$open,/) // can_apply 只问「有没有进行中的工单」:completed 不挡,能否再售后由订单是否已退款决定 assert.match(userOrders, /'can_apply'\s*=> \$enabled && !\$expired && !\$open,/) // 可再次申请售后按工单状态推理:rejected / cancelled / completed 都允许再次申请; // pending / processing / approved(流转中)不允许。订单已退款时 enabled 直接为 false, // 上面那条自然挡死,不需要工单状态再判一次。 assert.match(userOrders, /\$reapplyable_statuses = \[\s*AfterSaleService::STATUS_REJECTED,\s*AfterSaleService::STATUS_CANCELLED,\s*AfterSaleService::STATUS_COMPLETED,\s*\];/) assert.ok(!userOrders.includes('$reapplyable ='), 'can_apply must not re-check the ticket status') assert.match(userOrders, /'can_reapply'\s*=> \$enabled && !\$expired && \$has_application && in_array\(\$latest_status, \$reapplyable_statuses, true\),/) assert.match(userOrders, /'latest'\s*=> \$has_application \? \[/) assert.match(userOrders, /'apply_form'\s*=> is_array\(\$latest\['apply_form'\] \?\? null\) \? \$latest\['apply_form'\] : null,/) // 处理记录取代 reviewed_at / completed_at / admin_note 三个单点字段 assert.match(userOrders, /'handling_log'\s*=> array_values\(\(array\) \(\$latest\['handling_log'\] \?\? \[\]\)\),/) // 仅已支付订单开放;开关关闭整体不处理售后 assert.match(userOrders, /\$enabled = \$group === OrderCenterQuery::STATUS_PAID && !empty\(\$settings\['enabled'\]\);/) // 时效以支付时间为准 assert.match(userOrders, /\$this->after_sale_state\(\$group, \$order_number, \(string\) \(\$order\['paid_at'\] \?\? ''\), \$after_sale_service, \$after_sales\)/) // 列表模式单行查询,详情模式复用全量工单 assert.match(userOrders, /\$after_sales = \$detail && \$after_sale_service \? \$after_sale_service->list_for_order\(\$order_number\) : \[\];/) assert.match(userOrders, /\$latest = \$service->latest_for_order\(\$order_number\);/) assert.match(userOrders, /\$view\['after_sales'\] = \$after_sales;/) // 旧 can_after_sale 双路已移除 assert.ok(!userOrders.includes('can_after_sale'), 'can_after_sale must be replaced by the after_sale state') }) test('frontend after-sale modals are standalone and config-driven', () => { const js = read('assets/js/order-after-sale.js') // 四个独立弹窗 + modal-mini 几何 + 蓝色炫彩标题区 assert.match(js, /className: 'aksrt-after-sale-apply-modal'/) assert.match(js, /className: 'aksrt-after-sale-detail-modal'/) assert.match(js, /className: 'aksrt-after-sale-expired-modal'/) assert.match(js, /className: 'aksrt-after-sale-withdraw-modal'/) assert.match(js, /modal-colorful-header colorful-bg jb-blue/) assert.match(js, /AksrtModalRuntime\.open\(\{/) // 滚动容器与禁封申诉弹窗同口径:no-scrollbar 完全隐藏滚动条(含细滚动条), // 避免 modal-mini 弹窗边缘出现可见滚动栏。 assert.match(js, /'
'/) assert.match(js, /'
'/) assert.ok(!js.includes('mini-scrollbar'), 'after-sale modals must not render a visible scrollbar') // 申请表单:预置原因徽章、所需资料、必填申请说明(placeholder 取后台配置) assert.match(js, /reasonOptions\(\)/) assert.match(js, /materialOptions\(\)/) assert.match(js, /data-aksrt-after-sale-reason=/) assert.match(js, /data-aksrt-after-sale-material=/) assert.match(js, /var placeholder = String\(cfg\.reasonPlaceholder \|\| '必填,请您详细填写申请原因'\)/) assert.match(js, /name="reason"/) assert.match(js, /notyf\('请填写申请说明', 'danger'\)/) assert.match(js, /notyf\('请选择售后原因', 'danger'\)/) // 图片上传:开关控制渲染,最多 3 张,上传/删除/提交 assert.match(js, /if \(cfg\.imgEnabled\) \{/) assert.match(js, /MAX_IMAGES = 3/) assert.match(js, /\/after-sale\/upload-image'/) assert.match(js, /data-aksrt-after-sale-image-remove/) assert.match(js, /images: collectImages\(shell\)/) assert.match(js, /reason_label: selectedReasonLabel\(shell\)/) assert.match(js, /materials: collectMaterials\(shell\)/) assert.match(js, /description: reasonText,/) // 申请说明(HTML)+ 超时效提示(HTML)由后台配置直出 assert.match(js, /var desc = String\(cfg\.desc \|\| ''\)/) assert.match(js, /var text = String\(settings\(\)\.expiredText \|\| '抱歉,您的订单已超过售后申请时效'\)/) // 售后详细:状态徽章 / 明细行 / 图片缩略图;不可再申请提示原文 + 我知道了 // 申请表单分行展示:原因 / 申请资料 / 申请说明各自独立成行(与申请时间同格式); // 老数据无 apply_form 时兜底展示融合串 assert.match(js, /detailRow\('原因', String\(applyForm\.reason_label \|\| ''\)\)/) assert.match(js, /detailRow\('申请说明', String\(applyForm\.description \|\| ''\)\)/) assert.match(js, /STATUS_LABELS = \{[\s\S]*?pending: '待审核'[\s\S]*?completed: '已完成',/) // 不可再申请:不再展示阻断文案,仅保留关闭入口;可再申请:仅保留单个主按钮(无「关闭」) assert.ok(!js.includes('暂不支持再次申请售后'), 'blocked notice copy must be removed') assert.ok(!js.includes('REAPPLY_BLOCKED_TEXT'), 'blocked text constant must be removed') assert.ok(!js.includes('>关闭'), 'detail modal must not render a 关闭 button') assert.match(js, /data-aksrt-modal-close>我知道了 \.but,[\s\S]{0,260}min-height:\s*62px/) assert.match(modalCss, /-apply-modal \.but-average\.modal-buts > \.but \+ \.but,\s*\n\s*\.aksrt-after-sale-detail-modal \.but-average\.modal-buts > \.but \+ \.but,\s*\n\s*\.aksrt-after-sale-expired-modal \.but-average\.modal-buts > \.but \+ \.but,\s*\n\s*\.aksrt-after-sale-withdraw-modal \.but-average\.modal-buts > \.but \+ \.but\s*\{[^}]*border-left:\s*1px solid var\(--modal-line-soft/) assert.match(modalCss, /-apply-modal \.but-average\.modal-buts > \.but:hover,\s*\n\s*\.aksrt-after-sale-detail-modal \.but-average\.modal-buts > \.but:hover,\s*\n\s*\.aksrt-after-sale-expired-modal \.but-average\.modal-buts > \.but:hover,\s*\n\s*\.aksrt-after-sale-withdraw-modal \.but-average\.modal-buts > \.but:hover\s*\{[^}]*background:\s*color-mix\(in srgb, var\(--modal-ink/) assert.match(modalCss, /-apply-modal \.but-average\.modal-buts > \.but\.c-blue:hover,\s*\n\s*\.aksrt-after-sale-detail-modal \.but-average\.modal-buts > \.but\.c-blue:hover,\s*\n\s*\.aksrt-after-sale-expired-modal \.but-average\.modal-buts > \.but\.c-blue:hover,\s*\n\s*\.aksrt-after-sale-withdraw-modal \.but-average\.modal-buts > \.but\.c-blue:hover\s*\{[^}]*color:\s*color-mix/) // 撤销售后:入口仅随待审核状态渲染,点击先过二次确认弹窗,确认后 DELETE 工单 assert.match(js, /status === 'pending'[\s\S]*?data-aksrt-after-sale-withdraw="' \+ escapeHtml\(\(order && order\.order_number\) \|\| ''\)/) assert.match(js, /buildWithdrawHtml\(orderNumber\)/) assert.match(js, /className: 'aksrt-after-sale-withdraw-modal'/) assert.match(js, /data-aksrt-after-sale-withdraw-confirm="' \+ escapeHtml\(orderNumber\) \+ '"/) assert.match(js, /method: 'DELETE'/) assert.match(js, /AksrtModalRuntime\.close\('withdraw'\)/) assert.match(js, /notyf\('售后申请已撤销', 'success'\)/) // 人机验证:提交与撤销前先过验证(场景未启用时挂载点缺失,run 直接放行) assert.match(js, /function runCaptcha\(scene\)/) assert.match(js, /runCaptcha\('after_sale_apply'\)/) assert.match(js, /runCaptcha\('after_sale_cancel'\)/) assert.match(js, /AKSRTCaptcha\.run\(scene\)/) // 提交后关闭弹窗并刷新订单列表 assert.match(js, /AKSRTOrderCenter\.refresh\(orderNumber\)/) assert.match(js, /window\.AksrtModalRuntime\.close\('submit'\)/) // 入口委托:申请 / 售后详细 assert.match(js, /data-aksrt-after-sale-apply/) assert.match(js, /data-aksrt-after-sale-detail/) assert.match(js, /auto_fun\.aksrt-after-sale/) }) test('order center drops the embedded after-sale form and exposes refresh()', () => { const js = read('assets/js/order-center.js') // 内嵌售后表单与两个 submit 处理器已彻底移除(死代码清理) assert.ok(!js.includes('data-aksrt-after-sale-form'), 'embedded after-sale form must be removed') assert.ok(!js.includes('提交售后申请'), 'embedded after-sale submit copy must be removed') assert.ok(!js.includes('onDocumentSubmit'), 'document submit handler for the after-sale form must be removed') // 列表卡片与订单详情弹窗底部共用新的售后按钮 assert.match(js, /function afterSaleButtons\(order\)/) assert.match(js, /function afterSalePayload\(order\)/) assert.match(js, /data-aksrt-after-sale-detail="' \+ payload \+ '"/) assert.match(js, /data-aksrt-after-sale-apply="' \+ payload \+ '"/) assert.match(js, /if \(afterSale\.enabled\) html \+= afterSaleButtons\(order\);/) assert.ok(!js.includes('\n html += afterSaleButtons(order);\n'), 'detail modal footer must not render the shared after-sale buttons') // 底部按钮与禁封申诉弹窗同式(but-average modal-buts);「售后详细」入口收拢到记录卡片 assert.match(js, /aksrt-order-detail__actions but-average modal-buts/) assert.match(js, /data-aksrt-after-sale-apply="' \+ afterSalePayload\(order\) \+ '"/) // 底部「关闭」与售后详细「我知道了」同款(but c-blue,平铺观感由 orders.css 呈现) assert.match(js, /class="but c-blue" data-aksrt-modal-close>关闭'), 'record card has a view-detail entry') assert.match(js, /data-aksrt-after-sale-detail="' \+ recordPayload \+ '"/) // 记录卡片:原因 / 申请理由分行(新数据走 apply_form,老数据兜底融合串) assert.ok(js.includes('aksrt-order-detail__after-sale-field">原因:'), 'record card shows 原因:') assert.ok(js.includes('aksrt-order-detail__after-sale-field">申请理由:'), 'record card shows 申请理由:') assert.match(js, /data-aksrt-after-sale-detail="/) assert.match(js, /data-aksrt-after-sale-apply="/) // 订单详情弹窗 CSS:记录卡片「查看详细」foot 右对齐;底部按钮与售后弹窗家族 // 同款平铺观感(modal-runtime 同款规则镜像到吸底 footer),单按钮占满整行。 const ordersCss = read('assets/css/routes/orders.css') assert.match(ordersCss, /\.aksrt-order-detail__after-sale-foot\s*\{[^}]*justify-content:\s*flex-end/) assert.match(ordersCss, /\.aksrt-order-detail-modal \.aksrt-order-detail__actions\.but-average\.modal-buts > \.but\s*\{[^}]*background:\s*transparent/) assert.match(ordersCss, /\.aksrt-order-detail-modal \.aksrt-order-detail__actions\.but-average\.modal-buts > \.but\.c-blue\s*\{[^}]*color:\s*var\(--modal-primary/) assert.match(ordersCss, /\.aksrt-order-detail-modal \.aksrt-order-detail__actions\.but-average\.modal-buts > \.but:only-child\s*\{[^}]*grid-column:\s*1 \/ -1/) // 分界线与悬停阴影与售后弹窗家族同款(分隔色用本弹窗 footer 同源的 --detail-line); // footer 零垂直内边距、按钮满高(min-height 64 = 13+38+13),按钮盒=整个底部区域,安全区留 footer 底部 assert.match(ordersCss, /\.aksrt-order-detail-modal \.aksrt-order-detail__actions\s*\{[^}]*padding:\s*0 0 env\(safe-area-inset-bottom/) assert.match(ordersCss, /\.aksrt-order-detail-modal \.aksrt-order-detail__actions\.but-average\.modal-buts > \.but\s*\{[^}]*min-height:\s*64px/) assert.match(ordersCss, /\.aksrt-order-detail-modal \.aksrt-order-detail__actions\.but-average\.modal-buts > \.but \+ \.but\s*\{[^}]*border-left:\s*1px solid var\(--detail-line\)/) assert.match(ordersCss, /\.aksrt-order-detail-modal \.aksrt-order-detail__actions\.but-average\.modal-buts > \.but:hover\s*\{[^}]*background:\s*color-mix\(in srgb, var\(--modal-ink/) assert.match(ordersCss, /\.aksrt-order-detail-modal \.aksrt-order-detail__actions\.but-average\.modal-buts > \.but\.c-blue:hover\s*\{[^}]*color:\s*color-mix/) // 售后记录区保留在订单详情弹窗:记录卡片带申请时间,「查看详细」逐条入口; // 取消申请按钮与处理器已移除(撤销入口移入售后详细弹窗)。 assert.match(js, /售后记录/) assert.match(js, /