From 670e271ff6b6865554843997587efaf214344c1e Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 17 Sep 2026 11:41:05 +0800 Subject: [PATCH] =?UTF-8?q?fix(payment):=20=E6=B7=BB=E5=8A=A0=E5=8D=A1?= =?UTF-8?q?=E5=AF=86=E9=A1=B5=E6=8F=90=E7=A4=BA=E5=9D=97=E8=87=AA=E5=B8=A6?= =?UTF-8?q?=E4=BE=9D=E8=B5=96=E7=8A=B6=E6=80=81=EF=BC=8C=E8=84=B1=E7=A6=BB?= =?UTF-8?q?=E6=89=80=E5=9C=A8=E8=A1=8C=E4=B9=9F=E4=B8=8D=E5=86=8D=E9=93=BA?= =?UTF-8?q?=E5=BC=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户复报「四条导入提示仍然全部铺开」。其 DevTools 实测给出关键判据: 同一页面上 formClass=1 / submsg=4 / depRows=17 与线上一致,但四条提示的 父级 为 null、computed display 为 block —— 也就是说提示块虽在 DOM 中, 却不在任何表格行内。此时依赖状态只挂在 上,隐藏与切换都无处落地, 于是四条提示永久可见、切换卡密类型/添加方式也没有反应。 修复:把 data-dep 与初始隐藏状态同时挂到内层提示块上,求值器改为 「谁带 data-dep 就管谁」,行与提示块各自落自己的 display。 变更(src/Admin/Native/PayChargeCards.php): - submessage_row(): 与内层
共用同一份 data-dep;初始隐藏状态合并进提示块原有的 margin/padding 内联样式, 不产生重复的 style 属性。原「只给 加 style」的写法保留给行本身。 - evalDeps():选择器由 '.aksrt-cardpass-form tr[data-dep]' 放宽为 '.aksrt-cardpass-form [data-dep]',并改用 el.style.display 落值。 测试(tests/admin/charge-cards-dependency.test.cjs): - DOM 桩按真实结构为每条提示建「行 + 提示块」两个元素,新增用例 「提示块与所在行同层落 display」断言两层始终同状态(默认 8 个元素全隐藏、 命中依赖时仅该提示的两层可见、切回默认两层同时收起)。 - 契约用例新增三条静态断言:提示块必须自带 data-dep、必须自带初始隐藏状态、 求值选择器必须同时覆盖行与提示块。 - 负向验证:把提示块的 data-dep 去掉后跑测试 → pass 5 / fail 1,护栏生效。 验证: - node --test 两个 admin 契约测试 10/10 通过;php -l 无语法错误; - 线上抓取:tr_total=19 / tr_with_dep=17 / div_with_dep=4 / display_none=18; 四条提示的 div 均带 data-dep="…" 与 display:none。 - 真实浏览器模拟「提示块被搬出所在行」(foster-parent 到 之前)后: 默认态四条全部 none;切到「会员兑换 + 导入卡密」时,只有已脱离行的 「导入会员兑换卡密」变为可见(ownStyle 中的 display:none 被移除), 其余三条维持 none;切回默认四条全部收 none。 --- src/Admin/Native/PayChargeCards.php | 17 +++++-- tests/admin/charge-cards-dependency.test.cjs | 51 +++++++++++++++++--- 2 files changed, 56 insertions(+), 12 deletions(-) diff --git a/src/Admin/Native/PayChargeCards.php b/src/Admin/Native/PayChargeCards.php index 018b993c..6cd0a6d2 100644 --- a/src/Admin/Native/PayChargeCards.php +++ b/src/Admin/Native/PayChargeCards.php @@ -853,8 +853,13 @@ class PayChargeCards { * @param array $deps */ private function submessage_row(array $deps, string $content): void { - $hidden = $this->deps_visible($deps) ? '' : ' style="display:none;"'; - echo ''; // phpcs:ignore WordPress.Security.EscapeOutput -- 文案已转义。 + $dep_attr = ' data-dep="' . esc_attr((string) wp_json_encode($deps)) . '"'; + $hidden = $this->deps_visible($deps) ? '' : 'display:none;'; + // data-dep 与初始隐藏状态必须同时挂在 和内层提示块上,不能只依赖行: + // 提示块一旦脱离所在行(表格上下文里出现不被允许的节点时,HTML 解析器会把它 + // foster-parent 到
' . $content . '
之前),只有 带状态会让四条提示永久铺开、 + // 且切换卡密类型/添加方式时求值结果无处落地。自带状态后两种结构都成立。 + echo ''; // phpcs:ignore WordPress.Security.EscapeOutput -- 文案已转义。 } /** @@ -1075,9 +1080,11 @@ class PayChargeCards { return els[0].value; } function evalDeps() { - document.querySelectorAll('.aksrt-cardpass-form tr[data-dep]').forEach(function (tr) { + // 选择器同时匹配行元素与提示块自身:提示块可能脱离所在行, + // 因此每层都各自带 data-dep 并各自落 display,谁在 DOM 里就管谁。 + document.querySelectorAll('.aksrt-cardpass-form [data-dep]').forEach(function (el) { try { - var deps = JSON.parse(tr.getAttribute('data-dep')); + var deps = JSON.parse(el.getAttribute('data-dep')); var show = deps.every(function (d) { var v = formVal(d.key); switch (d.op) { @@ -1089,7 +1096,7 @@ class PayChargeCards { } return true; }); - tr.style.display = show ? '' : 'none'; + el.style.display = show ? '' : 'none'; } catch (e) { /* noop */ } }); } diff --git a/tests/admin/charge-cards-dependency.test.cjs b/tests/admin/charge-cards-dependency.test.cjs index 95efacc7..23e1f304 100644 --- a/tests/admin/charge-cards-dependency.test.cjs +++ b/tests/admin/charge-cards-dependency.test.cjs @@ -120,17 +120,22 @@ function createForm() { addInput('only_pass_s', 'checkbox', '1', false); addInput('vip_exchange[time]', 'text', '', false); - const rows = addRows.map((row) => { + // 每个依赖行对应一个 ;提示行(submessage_row)还会把同一份 data-dep 与隐藏状态 + // 额外挂在
' . $content . '
内的提示块上,因此这里也为它建第二个元素(模拟提示块脱离行的情况)。 + const rows = []; + for (const row of addRows) { const json = JSON.stringify(row.deps); - return { label: row.label, deps: row.deps, el: { style: {}, getAttribute: (n) => (n === 'data-dep' ? json : null) } }; - }); + const makeEl = () => ({ style: {}, getAttribute: (n) => (n === 'data-dep' ? json : null) }); + rows.push({ label: row.label, deps: row.deps, el: makeEl() }); + if (HINTS.includes(row.label)) rows.push({ label: row.label, deps: row.deps, el: makeEl() }); + } const handlers = {}; const document = { readyState: 'loading', querySelectorAll(sel) { if (sel === '.aksrt-cardpass-form [name]') return inputs; - if (sel === '.aksrt-cardpass-form tr[data-dep]') return rows.map((row) => row.el); + if (sel === '.aksrt-cardpass-form [data-dep]') return rows.map((row) => row.el); return []; }, addEventListener(type, fn) { (handlers[type] = handlers[type] || []).push(fn); }, @@ -147,10 +152,14 @@ function createForm() { }); fire('DOMContentLoaded'); - const visible = () => rows.filter((row) => row.el.style.display !== 'none').map((row) => row.label); + const visible = () => [...new Set(rows.filter((row) => row.el.style.display !== 'none').map((row) => row.label))]; + /** 提示的「行 + 提示块」两层各自的显示状态,用于验证两层不会分叉。 */ + const hintLayers = () => rows + .filter((row) => HINTS.includes(row.label)) + .map((row) => row.label + ':' + (row.el.style.display === 'none' ? 'off' : 'on')); return { - setRadio, setChecked, visible, + setRadio, setChecked, visible, hintLayers, change() { fire('change', { target: { closest: () => ({}) } }); }, hintVisible: () => visible().filter((label) => HINTS.includes(label)), count: rows.length, @@ -194,6 +203,11 @@ test('两种行渲染都接入服务端预隐藏,避免首帧铺开', () => { assert.match(page, /private function field_row\([\s\S]*?\$hidden\s*=\s*\(\$deps && !\$this->deps_visible\(\$deps\)\)/); assert.match(page, /private function submessage_row\([\s\S]*?\$hidden\s*=\s*\$this->deps_visible\(\$deps\)/); assert.match(script, /document\.addEventListener\('DOMContentLoaded', evalDeps\)/); + // 提示块必须自带 data-dep 与初始隐藏状态:脱离所在行时(表格解析器把非法节点 + // foster-parent 到 之前)仍能被求值并保持隐藏。 + assert.match(page, /