From 695bc9192f67f1b4055c682a921c8046f8250c56 Mon Sep 17 00:00:00 2001 From: CieanYau Date: Fri, 6 Nov 2026 01:42:56 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=95=9C=E5=83=8F=E8=AE=A2?= =?UTF-8?q?=E5=8D=95=E9=87=8D=E8=AF=95=E4=B8=8E=E7=BA=A7=E8=81=94=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E5=8F=AF=E8=A7=81=E6=80=A7=E4=B8=80=E8=87=B4=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/config/CommonConfig.vue | 95 ++-- .../instance/ImagePurchaseDialog.vue | 19 +- tests/review-purchase.e2e.test.mjs | 430 ++++++++++++++++++ 3 files changed, 497 insertions(+), 47 deletions(-) create mode 100644 tests/review-purchase.e2e.test.mjs diff --git a/app/components/config/CommonConfig.vue b/app/components/config/CommonConfig.vue index f041f40..2fd189b 100644 --- a/app/components/config/CommonConfig.vue +++ b/app/components/config/CommonConfig.vue @@ -59,6 +59,33 @@ var ce = { "24:month": `biennially`, "36:month": `triennially`, }; +// 默认值、购物车回填和界面共用可见叶子集合;隐藏祖先会隐藏整个子树。 +// 保留后端遍历顺序供默认值使用,显示排序仅由界面处理。 +function cascadeItems(option) { + const items = []; + function visit(nodes, path) { + for (const node of nodes) { + if (!node || typeof node !== `object` || Array.isArray(node)) continue; + if (Number(node.hidden) === 1) continue; + const name = String(node.item_name ?? ``).trim(); + const labels = name ? [...path, name] : path; + const children = Array.isArray(node.children) ? node.children : []; + const id = Number(node.item_id); + if ( + (Number(node.is_leaf) === 1 || children.length === 0) && + Number.isInteger(id) && + id > 0 + ) + items.push({ item_id: id, label: labels.join(` / `) || `选项 ${id}` }); + else if (children.length) visit(children, labels); + } + } + visit(option.tree ?? [], []); + return items.filter( + (item, index) => + items.findIndex((other) => other.item_id === item.item_id) === index, + ); +} function R(e, t = {}) { // 业务状态按普通配置 u、级联 d、重复数量 p 分开存储,再按现有后端契约组装。 let n = useApi(), @@ -218,7 +245,7 @@ function R(e, t = {}) { (p.value[String(n.id)] = [Number(e ?? n.qty_min ?? 1)]), n.option_type === `cascade`) ) { - let e = fe(n)[0]; + let e = cascadeItems(n)[0]; e && (d.value[String(n.id)] = { item_id: e.item_id, @@ -268,15 +295,11 @@ function R(e, t = {}) { e.id ); }); - function fe(e) { - let t = (e) => e.flatMap((e) => (e.children?.length ? t(e.children) : [e])); - return t(e.tree ?? []).filter((e) => Number(e.hidden) !== 1); - } function pe(e, t, n = 1) { let r = l.value?.configoptions.find((t) => t.id === e); r && r.option_type === `cascade` && - fe(r).some((e) => e.item_id === t) && + cascadeItems(r).some((e) => e.item_id === t) && ((d.value[String(e)] = { item_id: t, quantity: Math.max(1, Number(n) || 1), @@ -373,17 +396,25 @@ function R(e, t = {}) { let n = t, r = Number(n.item_id), i = Number(n.quantity ?? 1); - return Number.isFinite(r) - ? [ - [ - e, - { - item_id: r, - quantity: Math.max(1, Number.isFinite(i) ? i : 1), - }, - ], - ] - : []; + const option = l.value?.configoptions.find( + (option) => String(option.id) === e, + ); + // 已保存的级联项不可静默替换成另一资源,也不能继续提交已隐藏的节点。 + if ( + !option || + option.option_type !== `cascade` || + !cascadeItems(option).some((item) => item.item_id === r) + ) + throw Error(`商品级联配置已调整,请返回购物车删除该项后重新配置`); + return [ + [ + e, + { + item_id: r, + quantity: Math.max(1, Number.isFinite(i) ? i : 1), + }, + ], + ]; }), )); let i = t.repeat_configoption; @@ -773,34 +804,8 @@ var le = { ? configuration_display_r(t) : t; function Z(e) { - let t = [], - n = (e, r) => { - for (let i of e) { - if (!i || typeof i != `object` || Array.isArray(i)) continue; - let e = i; - if (Number(e.hidden) === 1) continue; - let a = String(e.item_name ?? ``).trim(), - o = a ? [...r, a] : r, - s = Array.isArray(e.children) ? e.children : [], - c = Number(e.item_id); - (Number(e.is_leaf) === 1 || s.length === 0) && - Number.isInteger(c) && - c > 0 - ? t.push({ - item_id: c, - label: o.join(` / `) || `选项 ${c}`, - }) - : s.length && n(s, o); - } - }; - return ( - n(e.tree ?? [], []), - t - .sort((e, t) => e.label.localeCompare(t.label, `zh-Hans-CN`)) - .filter( - (e, t, n) => - n.findIndex((t) => t.item_id === e.item_id) === t, - ) + return cascadeItems(e).sort((e, t) => + e.label.localeCompare(t.label, `zh-Hans-CN`), ); } function Re(e) { diff --git a/app/components/instance/ImagePurchaseDialog.vue b/app/components/instance/ImagePurchaseDialog.vue index 2bf4dea..7cfa44b 100644 --- a/app/components/instance/ImagePurchaseDialog.vue +++ b/app/components/instance/ImagePurchaseDialog.vue @@ -1,5 +1,5 @@