feat(w05): 按卡独立编辑 + 缺项点名并进顶条 + 区序换回 + 类型层叠图

2026-08-31 用户四连:
① 「各自编辑各自的部分,不要做联动」—— 平台新增 scopedTargets(ui-kit):开合作用域
  跟着触发钮走,三档 —— 钮在本地址宿主的行里→只这一行(A 卡的笔只开 A 卡);钮在别的
  宿主的行里→行内属于本地址的宿主(这张卡的卡背);外部闸行→全宿主(老行为,② 的闸
  不受影响)。收值与保存不随作用域收窄(collect 仍按地址全量,「只写真改了的行」在
  服务端把关)。编辑态/锁面/笔盘显隐 CSS 全部改成按卡(.dossier.on);同地址同时只开
  一张卡(笔在别卡编辑中自动隐去,快照键按地址,双开会互相踩)。2 测 + 变异验证。
② 逐块「还差 N 行必填」红带撤下,点名并进顶部「还差什么」—— 同一批基座读数
  (_missing/_missing_keys)在顶条出两行子行,数值 0 自己消失;gate_reason 改口。
③ 比对基准与芯片档案区序换回(基准在前)。
④ 比对基准换「层叠双封装」SVG(后面那枚淡出成剪影 —— 一类而非一颗),不再与芯片卡
  共用同一枚封装图。

测:ui-kit 35(scoped 2 新,变异红过)+ 包内 105 + 2528 全仓;2.11.0 装库五探针核过
(合并条/无旧红带/类型图/按卡 CSS/基准在前);浏览器实测 XL 卡独自翻背进编辑、LM 卡纹丝不动。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
This commit is contained in:
xcs
2026-08-31 17:01:06 +08:00
co-authored by Claude Fable 5
parent 05ebecb541
commit 26db6c685b
7 changed files with 130 additions and 32 deletions
@@ -623,7 +623,8 @@ describe('件① 评估输入补全', () => {
expect(html).toContain('class="face front"'); expect(html).toContain('class="face front"');
expect(html).toContain('class="face back"'); expect(html).toContain('class="face back"');
/* 编辑锁面:主数据编辑停正面,参数编辑翻背面 —— 判断全在样式表 */ /* 编辑锁面:主数据编辑停正面,参数编辑翻背面 —— 判断全在样式表 */
expect(html).toContain('.dossiers.on .dossier .flip-in{transform:rotateY(0deg)}'); /* 按卡锁面(2026-08-31「不要做联动」):.on 由 scopedTargets 落在这张卡的行上 */
expect(html).toContain('.dossier.on .flip-in{transform:rotateY(0deg)}');
expect(html).toContain('.dossier:has(.pback.on) .flip-in{transform:rotateY(180deg)}'); expect(html).toContain('.dossier:has(.pback.on) .flip-in{transform:rotateY(180deg)}');
/* 悬浮提示丸:看得出点击可翻 */ /* 悬浮提示丸:看得出点击可翻 */
expect(html).toContain('点击翻面'); expect(html).toContain('点击翻面');
@@ -713,7 +714,7 @@ describe('件① 评估输入补全', () => {
/* 老的闸行不再出现在正文里(样式表里的基座规则给 ② 用,不算) */ /* 老的闸行不再出现在正文里(样式表里的基座规则给 ② 用,不算) */
expect(html.slice(html.indexOf('</style>'))).not.toContain('jc-editgate'); expect(html.slice(html.indexOf('</style>'))).not.toContain('jc-editgate');
/* 笔↔软盘的按面显隐由样式表判 */ /* 笔↔软盘的按面显隐由样式表判 */
expect(html).toContain('.dossiers.on .disk-f{display:grid}'); expect(html).toContain('.dossier.on .disk-f{display:grid}');
expect(html).toContain(".dossier:has(.pback.on) .disk-b{display:grid}"); expect(html).toContain(".dossier:has(.pback.on) .disk-b{display:grid}");
/* 参数瓷贴的输入孪生件带占位提示(渲染器认 data-ph) */ /* 参数瓷贴的输入孪生件带占位提示(渲染器认 data-ph) */
expect(html).toContain('data-ph="规格书上的值 · 查不到留空"'); expect(html).toContain('data-ph="规格书上的值 · 查不到留空"');
@@ -788,6 +789,19 @@ describe('件① 评估输入补全', () => {
expect(html).toContain('.jc-gate-bar:has(.jc-gate-text:empty)'); expect(html).toContain('.jc-gate-bar:has(.jc-gate-text:empty)');
}); });
/*
* 2026-08-31 用户:「页面上所有的问题都合并到最上面的还差什么,不要在某一个板块显示
* 类似还差 1 行必填没填这种提示」—— 逐块 gapbar 撤下,点名并进顶条(同一批基座读数,
* 数值 0 的那行由样式表自己收起来)。
*/
it('缺项点名全并进顶条「还差什么」,正文不再有逐块红带', () => {
expect(html).toContain('class="jc-gate-sub" data-class-of="parts_profile_missing"');
expect(html).toContain('data-slot="parts_profile_missing_keys"');
expect(html).toContain('data-slot="parts_parameter_missing_keys"');
expect(html).toContain('html.editable .jc-gate-sub.v-0{display:none}');
expect(html.slice(html.indexOf('</style>'))).not.toContain('jc-gapbar');
});
it('表宽超出要能横着滑 —— 不许写成 overflow:hidden(E04/I01 抄错过一次)', () => { it('表宽超出要能横着滑 —— 不许写成 overflow:hidden(E04/I01 抄错过一次)', () => {
expect(html).not.toMatch(/\.jc-tbl-wrap\s*\{[^}]*overflow\s*:\s*hidden/); expect(html).not.toMatch(/\.jc-tbl-wrap\s*\{[^}]*overflow\s*:\s*hidden/);
}); });
@@ -502,7 +502,7 @@ export const ASSESSMENT_STEPS = [
then: '两颗料不是同一个芯片类型,逐项比对无从谈起 —— 换一颗料,或者把归错的那颗类型改对。', then: '两颗料不是同一个芯片类型,逐项比对无从谈起 —— 换一颗料,或者把归错的那颗类型改对。',
else: { else: {
when: 'parts_profile_missing + parts_parameter_missing > 0', when: 'parts_profile_missing + parts_parameter_missing > 0',
then: '还有必填格空着 —— 红带点名的那几格填上就能提交。', then: '还有必填格空着 —— 缺哪几格就点名在下面两行,填上就能提交。',
else: '', else: '',
}, },
}, },
@@ -54,7 +54,7 @@ export const manifest: ActivityManifest = {
activityId: ACTIVITY_ID, activityId: ACTIVITY_ID,
name: 'ACT-WORK-05 · 差异评估', name: 'ACT-WORK-05 · 差异评估',
/* 重写不是改 —— 同版重装是 no-op(静默丢失,踩过) */ /* 重写不是改 —— 同版重装是 no-op(静默丢失,踩过) */
packageVersion: '2.10.0', packageVersion: '2.11.0',
category: '需求分析与方案匹配', category: '需求分析与方案匹配',
sdkVersion: '0.1.0-m5', sdkVersion: '0.1.0-m5',
checksum: 'dev-checksum-part-substitution-assessment', checksum: 'dev-checksum-part-substitution-assessment',
@@ -100,6 +100,8 @@ const optionSelect = (col: string, address: string, valueCol: string, labelCol:
*/ */
const gateBar = (): string => `<div class="jc-gate-bar"> const gateBar = (): string => `<div class="jc-gate-bar">
<span><b>还差什么</b> —— <span class="jc-gate-text" data-slot="gate_reason" data-blank></span></span> <span><b>还差什么</b> —— <span class="jc-gate-text" data-slot="gate_reason" data-blank></span></span>
<span class="jc-gate-sub" data-class-of="parts_profile_missing">主数据还差 <b data-slot="parts_profile_missing"></b> 行必填 —— <span data-slot="parts_profile_missing_keys"></span></span>
<span class="jc-gate-sub" data-class-of="parts_parameter_missing">参数值还差 <b data-slot="parts_parameter_missing"></b> 行必填 —— <span data-slot="parts_parameter_missing_keys"></span></span>
</div>`; </div>`;
/** /**
@@ -126,6 +128,20 @@ const doc = (input: { accent: string; alias: string; tag: string; kicker: string
</footer> </footer>
</div></div>`; </div></div>`;
/**
* 芯片**类型**的图形 —— 层叠双封装(一类而非一颗),别与档案卡的单颗封装共用一枚
* (2026-08-31 用户:「芯片类型能不能换一个更贴近芯片类型的 svg,不要和芯片使用同一个图片」)。
*/
const TYPE_SVG =
'<svg class="tsvg" viewBox="0 0 96 72" aria-hidden="true">' +
'<g class="t-ghost"><rect class="c-pin" x="13" y="7" width="10" height="4" rx="1.4"/><rect class="c-pin" x="13" y="18" width="10" height="4" rx="1.4"/><rect class="c-pin" x="13" y="29" width="10" height="4" rx="1.4"/><rect class="c-pin" x="79" y="7" width="10" height="4" rx="1.4"/><rect class="c-pin" x="79" y="18" width="10" height="4" rx="1.4"/><rect class="c-pin" x="79" y="29" width="10" height="4" rx="1.4"/><rect class="c-body" x="22" y="2" width="58" height="42" rx="7"/></g>' +
'<rect class="c-pin" x="3" y="27" width="11" height="5" rx="1.6"/><rect class="c-pin" x="3" y="39" width="11" height="5" rx="1.6"/><rect class="c-pin" x="3" y="51" width="11" height="5" rx="1.6"/>' +
'<rect class="c-pin" x="70" y="27" width="11" height="5" rx="1.6"/><rect class="c-pin" x="70" y="39" width="11" height="5" rx="1.6"/><rect class="c-pin" x="70" y="51" width="11" height="5" rx="1.6"/>' +
'<rect class="c-body" x="13" y="22" width="58" height="44" rx="8"/>' +
'<rect class="c-die" x="31" y="36" width="22" height="18" rx="3.5"/>' +
'<circle class="c-dot" cx="23" cy="30" r="2.3"/>' +
'</svg>';
/** 卡角编辑钮的三枚图标:笔(开编辑)/ 软盘(保存)/ 叉(放弃)—— 2026-08-31 用户定的笔↔软盘交互 */ /** 卡角编辑钮的三枚图标:笔(开编辑)/ 软盘(保存)/ 叉(放弃)—— 2026-08-31 用户定的笔↔软盘交互 */
const PEN_SVG = const PEN_SVG =
'<svg class="fic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z"/></svg>'; '<svg class="fic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z"/></svg>';
@@ -254,7 +270,7 @@ const DOSSIERS = `<section class="board"><article class="dossiers" data-rows="mi
*/ */
const BASIS_CARD = `<article class="basis" data-rows="mine:${D}.case"><section class="basis-in"> const BASIS_CARD = `<article class="basis" data-rows="mine:${D}.case"><section class="basis-in">
<header class="basis-hd"> <header class="basis-hd">
${CHIP} ${TYPE_SVG}
<span class="basis-id"> <span class="basis-id">
<span class="basis-lb">比对基准 · 芯片类型</span> <span class="basis-lb">比对基准 · 芯片类型</span>
<span class="basis-chip" data-rows="parts.chip_type" data-by="type_id=chip_type_id"><b data-slot="type_name" data-blank></b><i class="basis-desc" data-slot="description" data-blank></i></span> <span class="basis-chip" data-rows="parts.chip_type" data-by="type_id=chip_type_id"><b data-slot="type_name" data-blank></b><i class="basis-desc" data-slot="description" data-blank></i></span>
@@ -294,12 +310,10 @@ export const INPUT_SUPPLEMENT = doc({
'<span class="need">纪律</span>:两颗料必须归到同一个芯片类型下才谈得上逐项比对;类型只从字典里选,本活动不新增类型 —— 字典里没有合适的,先请数据管理员补进字典;查不到的参数值留空(会被红带点名),不要编一个值顶替。', '<span class="need">纪律</span>:两颗料必须归到同一个芯片类型下才谈得上逐项比对;类型只从字典里选,本活动不新增类型 —— 字典里没有合适的,先请数据管理员补进字典;查不到的参数值留空(会被红带点名),不要编一个值顶替。',
]) + ]) +
`</details>` + `</details>` +
zone('芯片档案', '一颗芯片一张卡:正面主数据、背面核心参数值 —— 点卡翻面看另一面;右上角的笔 = 编辑当前这一面,软盘 = 保存', ZIC_TABLE) +
gapbar('parts_profile', '必填') +
gapbar('parts_parameter', '必填') +
DOSSIERS +
zone('比对基准', '芯片类型 + 它的核心参数字段 —— 两颗料归到哪个类型,就按这些字段逐项比;字段说明悬浮即看', ZIC_TABLE) + zone('比对基准', '芯片类型 + 它的核心参数字段 —— 两颗料归到哪个类型,就按这些字段逐项比;字段说明悬浮即看', ZIC_TABLE) +
BASIS_CARD + BASIS_CARD +
zone('芯片档案', '一颗芯片一张卡:正面主数据、背面核心参数值 —— 点卡翻面看另一面;右上角的笔 = 只编辑这张卡当前这一面,软盘 = 保存', ZIC_TABLE) +
DOSSIERS +
flow('<b>提交 / 确认:</b>本工单只承载内容;确认请在对话区的【执行卡】中进行。确认后活动自动从「等待数据」唤醒。'), flow('<b>提交 / 确认:</b>本工单只承载内容;确认请在对话区的【执行卡】中进行。确认后活动自动从「等待数据」唤醒。'),
}); });
@@ -146,9 +146,14 @@ i{font-style:normal}
.foot-r{letter-spacing:.04em} .foot-r{letter-spacing:.04em}
/* ================= 顶部「还差什么」红带(机制:显隐认 data-blank 的空文本节点) ================= */ /* ================= 顶部「还差什么」红带(机制:显隐认 data-blank 的空文本节点) ================= */
.jc-gate-bar{display:flex;align-items:flex-start;gap:8px;margin:0 0 16px;padding:11px 14px;border-radius:10px;background:var(--danger-soft);border:1px solid #f5c0ba;color:var(--danger);font-size:12.5px;line-height:1.7} .jc-gate-bar{display:flex;flex-direction:column;gap:5px;margin:0 0 16px;padding:11px 14px;border-radius:10px;background:var(--danger-soft);border:1px solid #f5c0ba;color:var(--danger);font-size:12.5px;line-height:1.7}
.jc-gate-bar b{font-weight:800} .jc-gate-bar b{font-weight:800}
.jc-gate-bar:has(.jc-gate-text:empty){display:none} .jc-gate-bar:has(.jc-gate-text:empty){display:none}
/* 逐块的缺项点名并进这一条(2026-08-31 用户:「页面上所有的问题都合并到最上面的还差什么」)——
与提交闸同源的基座读数,数值为 0 的那一行自己消失,只在待处理态出现 */
.jc-gate-sub{display:none;font-size:12px;color:#a03d34;padding-left:2px}
html.editable .jc-gate-sub{display:block}
html.editable .jc-gate-sub.v-0{display:none}
/* 缺项红带 —— 与提交闸同源的必填提示,只在待处理态出现 */ /* 缺项红带 —— 与提交闸同源的必填提示,只在待处理态出现 */
.jc-gapbar{display:none} .jc-gapbar{display:none}
@@ -297,13 +302,14 @@ table.on tr.jc-row-static td{background:var(--acc-wash);border-top:1px dashed va
wrapper 不为 :empty,上一条够不着 —— 认裸 class 补同一枚胶囊(单层 :has) */ wrapper 不为 :empty,上一条够不着 —— 认裸 class 补同一枚胶囊(单层 :has) */
.fcell.req .jc-vcell:has(> [class="stx"])::after{content:"必填";font-size:10.5px;font-weight:800;color:var(--danger);background:var(--danger-soft);border-radius:999px;padding:0 9px} .fcell.req .jc-vcell:has(> [class="stx"])::after{content:"必填";font-size:10.5px;font-weight:800;color:var(--danger);background:var(--danger-soft);border-radius:999px;padding:0 9px}
.fcell.sug .jc-vcell:empty::after{content:"建议";font-size:10.5px;font-weight:800;color:var(--amber);background:var(--amber-soft);border-radius:999px;padding:0 9px} .fcell.sug .jc-vcell:empty::after{content:"建议";font-size:10.5px;font-weight:800;color:var(--amber);background:var(--amber-soft);border-radius:999px;padding:0 9px}
/* 档案卡的编辑态 —— **只点亮正面**(.on 落在 data-edit 元素自身;卡背住着另一地址的宿主, /* 档案卡的编辑态 —— **按卡开,只点亮这张卡的正面**(2026-08-31 用户:「各自编辑各自的部分,
不圈 .front 的话参数输入会被主数据的闸顺手点亮) */ 不要做联动」:.on 由 scopedTargets 落在被点那张卡的行 section 上,不再落整个宿主);
.dossiers.on .front .jc-vcell{display:none} 只圈 .front 是因为卡背住着另一地址的宿主,不圈的话参数输入会被主数据的编辑顺手点亮 */
.dossiers.on .front .jc-vcell.ro{display:block} .dossier.on .front .jc-vcell{display:none}
.dossiers.on .front .jc-cell{display:block} .dossier.on .front .jc-vcell.ro{display:block}
.dossiers.on .front .jc-vcell:empty::after{content:none} .dossier.on .front .jc-cell{display:block}
.dossiers.on .dz-role{display:flex} .dossier.on .front .jc-vcell:empty::after{content:none}
.dossier.on .dz-role{display:flex}
/* ── 翻面(纯 CSS:checkbox + :has,零脚本;reduced-motion 下瞬时切换)── */ /* ── 翻面(纯 CSS:checkbox + :has,零脚本;reduced-motion 下瞬时切换)── */
.dossier{position:relative;perspective:1200px;padding:0;background:none;border:0;box-shadow:none} .dossier{position:relative;perspective:1200px;padding:0;background:none;border:0;box-shadow:none}
@@ -322,15 +328,15 @@ table.on tr.jc-row-static td{background:var(--acc-wash);border-top:1px dashed va
.dossier:has(.flip-tg:checked) .flip-in{transform:rotateY(180deg)} .dossier:has(.flip-tg:checked) .flip-in{transform:rotateY(180deg)}
.dossier:has(.flip-tg:checked) .flip-cue .cue-front{display:none} .dossier:has(.flip-tg:checked) .flip-cue .cue-front{display:none}
.dossier:has(.flip-tg:checked) .flip-cue .cue-back{display:inline-block} .dossier:has(.flip-tg:checked) .flip-cue .cue-back{display:inline-block}
.dossiers.on .dossier .flip-in{transform:rotateY(0deg)} .dossier.on .flip-in{transform:rotateY(0deg)}
.dossier:has(.pback.on) .flip-in{transform:rotateY(180deg)} .dossier:has(.pback.on) .flip-in{transform:rotateY(180deg)}
/* 编辑中不许翻(哪面在编辑锁哪面),盖满卡面的点击层与提示丸一起收 */ /* 编辑中的**这张卡**不许翻(哪面在编辑锁哪面),点击层与提示丸一起收;另一张卡不受影响 */
.dossiers.on .flip-hit,.dossier:has(.pback.on) .flip-hit{display:none} .dossier.on .flip-hit,.dossier:has(.pback.on) .flip-hit{display:none}
.dossiers.on .flip-cue,.dossier:has(.pback.on) .flip-cue{display:none} .dossier.on .flip-cue,.dossier:has(.pback.on) .flip-cue{display:none}
/* 悬浮侧倾:预告「这张卡点了会翻」—— 只在读态、只在真有悬浮的设备、且不嫌动画的人那里 */ /* 悬浮侧倾:预告「这张卡点了会翻」—— 只在读态、只在真有悬浮的设备、且不嫌动画的人那里 */
@media (hover:hover) and (prefers-reduced-motion: no-preference){ @media (hover:hover) and (prefers-reduced-motion: no-preference){
.dossiers:not(.on) .dossier:hover:not(:has(.flip-tg:checked)):not(:has(.pback.on)) .flip-in{transform:rotateY(-12deg)} .dossier:hover:not(.on):not(:has(.flip-tg:checked)):not(:has(.pback.on)) .flip-in{transform:rotateY(-12deg)}
.dossiers:not(.on) .dossier:hover:has(.flip-tg:checked):not(:has(.pback.on)) .flip-in{transform:rotateY(168deg)} .dossier:hover:not(.on):has(.flip-tg:checked):not(:has(.pback.on)) .flip-in{transform:rotateY(168deg)}
} }
/* ── 卡角编辑钮:笔=编辑当前这一面,软盘=保存,叉=放弃(2026-08-31 用户定,顶掉两行编辑闸)── /* ── 卡角编辑钮:笔=编辑当前这一面,软盘=保存,叉=放弃(2026-08-31 用户定,顶掉两行编辑闸)──
@@ -343,10 +349,11 @@ table.on tr.jc-row-static td{background:var(--acc-wash);border-top:1px dashed va
.fab.pen{color:var(--acc-deep)} .fab.pen{color:var(--acc-deep)}
.fab.disk{background:var(--acc);border-color:var(--acc);color:#fff} .fab.disk{background:var(--acc);border-color:var(--acc);color:#fff}
.fab.xbtn{color:var(--ink-3)} .fab.xbtn{color:var(--ink-3)}
html.editable .dossiers:not(.on) .dossier:not(:has(.pback.on)):not(:has(.flip-tg:checked)) .pen-f{display:grid} /* 笔:这张卡当下这一面、且这张卡没在编辑 —— 各卡各开,互不联动 */
html.editable .dossiers:not(.on) .dossier:has(.flip-tg:checked):not(:has(.pback.on)) .pen-b{display:grid} html.editable .dossier:not(.on):not(:has(.pback.on)):not(:has(.flip-tg:checked)) .pen-f{display:grid}
.dossiers.on .disk-f{display:grid} html.editable .dossier:not(.on):not(:has(.pback.on)):has(.flip-tg:checked) .pen-b{display:grid}
.dossiers.on .x-f{display:grid} .dossier.on .disk-f{display:grid}
.dossier.on .x-f{display:grid}
.dossier:has(.pback.on) .disk-b{display:grid} .dossier:has(.pback.on) .disk-b{display:grid}
.dossier:has(.pback.on) .x-b{display:grid} .dossier:has(.pback.on) .x-b{display:grid}
@@ -384,6 +391,10 @@ html.editable .dossiers:not(.on) .dossier:has(.flip-tg:checked):not(:has(.pback.
.basis-desc{font-style:normal;font-size:11.5px;color:var(--ink-3)} .basis-desc{font-style:normal;font-size:11.5px;color:var(--ink-3)}
.basis-hd:has(.basis-chip:empty)::after{content:"待选择";font-size:11px;font-weight:700;color:var(--amber);background:var(--amber-soft);border:1px dashed #eeda9e;border-radius:999px;padding:1px 10px} .basis-hd:has(.basis-chip:empty)::after{content:"待选择";font-size:11px;font-weight:700;color:var(--amber);background:var(--amber-soft);border:1px dashed #eeda9e;border-radius:999px;padding:1px 10px}
.basis-hd .csvg{width:56px;flex:none} .basis-hd .csvg{width:56px;flex:none}
/* 类型图:层叠双封装 —— 后面那枚是「同类的别的芯片」,淡出成剪影 */
.basis-hd .tsvg{width:60px;flex:none}
.tsvg{display:block}
.t-ghost{opacity:.32}
.basis-id{display:flex;flex-direction:column;gap:5px;min-width:0;flex:1 1 auto} .basis-id{display:flex;flex-direction:column;gap:5px;min-width:0;flex:1 1 auto}
.basis-in:has(.ftiles:empty) .ftiles{display:none} .basis-in:has(.ftiles:empty) .ftiles{display:none}
/* 降噪(2026-08-31 用户:「乱糟糟…没必要显示全就不要显示完全,鼠标指上去后再显示」): /* 降噪(2026-08-31 用户:「乱糟糟…没必要显示全就不要显示完全,鼠标指上去后再显示」):
@@ -12,12 +12,13 @@
*/ */
// @vitest-environment jsdom // @vitest-environment jsdom
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import { collect, mirrorTwins, setOpenIn } from '../src/ViewContainer.tsx'; import { collect, mirrorTwins, scopedTargets, setOpenIn } from '../src/ViewContainer.tsx';
/** 两张芯片卡:正面是主数据格(profile),背面各住一个参数宿主(parameter) */ /** 两张芯片卡:正面是主数据格(profile),背面各住一个参数宿主(parameter) */
const FLIP_CARDS = const FLIP_CARDS =
'<article data-edit="parts.profile">' + '<article data-edit="parts.profile">' +
'<section class="dossier">' + '<section class="dossier">' +
'<button class="pen-a" data-edit-open="parts.profile"></button>' +
'<div class="front"><input data-col="internal_pn" value="LM2596S-5.0"/><input data-col="brand_name" value="TI"/></div>' + '<div class="front"><input data-col="internal_pn" value="LM2596S-5.0"/><input data-col="brand_name" value="TI"/></div>' +
'<div class="back"><section class="pback" data-edit="parts.parameter">' + '<div class="back"><section class="pback" data-edit="parts.parameter">' +
'<div class="ptile"><input data-col="internal_pn" value="LM2596S-5.0"/><input data-col="param_name" value="输出电压"/><input data-col="param_value" value="5.0"/></div>' + '<div class="ptile"><input data-col="internal_pn" value="LM2596S-5.0"/><input data-col="param_name" value="输出电压"/><input data-col="param_value" value="5.0"/></div>' +
@@ -101,3 +102,28 @@ describe('mirrorTwins:下拉镜像的是选项文案,不是 value(2026-08-31「
expect(doc.querySelector('.tsel')?.textContent).toBe('旧名'); expect(doc.querySelector('.tsel')?.textContent).toBe('旧名');
}); });
}); });
describe('scopedTargets:开合作用域跟着触发钮走(2026-08-31「不要做联动,各自编辑各自的部分」)', () => {
it('钮在本地址宿主的行里 → 只这一行(A 卡的笔只开 A 卡);钮对别的地址 → 只这张卡里那个宿主', () => {
const doc = docOf(FLIP_CARDS);
const pen = doc.querySelector('.pen-a') as HTMLElement;
// ① profile:钮在 profile 宿主里 → 落到它所在的那一行(第一张卡的 section)
const t1 = scopedTargets(doc, 'parts.profile', pen);
expect(t1).toHaveLength(1);
expect(t1[0]?.className).toBe('dossier');
expect(t1[0]?.contains(pen)).toBe(true);
// ② parameter:钮不在任何 parameter 宿主里,但在 profile 的行里 → 行内那一个 pback,不是两个
const t2 = scopedTargets(doc, 'parts.parameter', pen);
expect(t2).toHaveLength(1);
expect(t2[0]?.classList.contains('pback')).toBe(true);
expect(t1[0]?.contains(t2[0] ?? null)).toBe(true);
});
it('外部闸(不在任何宿主/行里)→ 全部宿主,老行为', () => {
const doc = docOf(FLIP_CARDS);
const gate = doc.querySelector('[data-edit-for="parts.parameter"]') as HTMLElement;
const t = scopedTargets(doc, 'parts.parameter', gate);
expect(t).toHaveLength(2);
expect(t.every((h) => h.classList.contains('pback'))).toBe(true);
});
});
+37 -4
View File
@@ -365,15 +365,45 @@ export function addRow(doc: Document, address: string): boolean {
} }
/** /**
* 那块内容与它的闸一起开合 —— **同一地址的全部宿主一起**(多宿主时一把闸管一整张表)。 * 那块内容与它的闸一起开合。`.on` 落在哪:行容器是表格的落在 `<table>`(老件,样式按
* `.on` 落在哪:行容器是表格的落在 `<table>`(老件,样式按 `table.on` 写); * `table.on` 写);不是表格的落在 `data-edit` 元素自身(2026-08-02 A2 对决卡立的规矩)。
* 不是表格的落在 `data-edit` 元素自身(2026-08-02 A2 对决卡立的规矩)。 * 这一版是**全宿主**开合(外部闸行用它);按了卡上钮的走 `scopedTargets`(见下)。
*/ */
export function setOpenIn(doc: Document, address: string, on: boolean): void { export function setOpenIn(doc: Document, address: string, on: boolean): void {
doc.querySelector<HTMLElement>(`[data-edit-for="${CSS.escape(address)}"]`)?.classList.toggle('on', on); doc.querySelector<HTMLElement>(`[data-edit-for="${CSS.escape(address)}"]`)?.classList.toggle('on', on);
for (const host of hostsOf(doc, address)) (host.closest('table') ?? host).classList.toggle('on', on); for (const host of hostsOf(doc, address)) (host.closest('table') ?? host).classList.toggle('on', on);
} }
/**
* **开合的作用域跟着触发钮走**(2026-08-31 用户:「芯片档案编辑就不能各自编辑各自的部分吗,
* 不要做联动」)。三档,从近到远:
* ① 钮长在这张表**自己的宿主**里 —— 落到钮所在的**那一行**(宿主的直接子元素;
* W05 芯片卡的笔落在这张卡的行上,只这张卡进编辑);行认不出就整个宿主;
* ② 钮长在**别的**宿主的某一行里 —— 落到**这一行内部**属于本地址的宿主们
* (W05 卡背的参数宿主住在主数据宿主的行里:点这张卡的笔,只这张卡的卡背开);
* ③ 都不是(外部闸行,如差异审核件的编辑闸)—— 全部宿主一起,老行为。
* 收值与保存**不随作用域收窄**(collect 仍按地址全量;「只写真改了的行」在服务端把关,
* 没动的行是 no-op)—— 作用域只管「屏上哪一块进编辑态」这一件事。
*/
export function scopedTargets(doc: Document, address: string, trigger: Element): HTMLElement[] {
const hosts = hostsOf(doc, address);
for (const host of hosts) {
if (host.contains(trigger)) {
const row = rowOf(host, trigger);
return [(row as HTMLElement | null) ?? host];
}
}
/* 钮在别的宿主的行里:找出包住钮的那一行(任意 data-edit 宿主的直接子元素),取行内的本地址宿主 */
for (let n: Element | null = trigger; n !== null; n = n.parentElement) {
const parent = n.parentElement;
if (parent !== null && parent.hasAttribute('data-edit')) {
const inner = hosts.filter((h) => n !== null && n.contains(h));
if (inner.length > 0) return inner;
}
}
return hosts.map((h) => (h.closest('table') ?? h) as HTMLElement);
}
/** /**
* `data-file="域.对象/主键值"` → 取文件要的那两样。 * `data-file="域.对象/主键值"` → 取文件要的那两样。
* *
@@ -514,7 +544,10 @@ export function ViewContainer({ html, title = '活动视图', editable = false,
* 容器是 `<section>`,从前这里写死 `closest('table')`,非表格容器永远拿不到 * 容器是 `<section>`,从前这里写死 `closest('table')`,非表格容器永远拿不到
* `.on`,编辑态整个打不开)。 * `.on`,编辑态整个打不开)。
*/ */
const setOpen = (address: string, on: boolean): void => setOpenIn(doc, address, on); const setOpen = (address: string, on: boolean): void => {
doc.querySelector<HTMLElement>(`[data-edit-for="${CSS.escape(address)}"]`)?.classList.toggle('on', on);
for (const target of scopedTargets(doc, address, hit)) (target.closest('table') ?? target).classList.toggle('on', on);
};
const open = hit.dataset['editOpen']; const open = hit.dataset['editOpen'];
const cancel = hit.dataset['editCancel']; const cancel = hit.dataset['editCancel'];