Archived
feat(w05): 数据工单去表格化 —— 翻面芯片卡(背面参数就地改)+ 基准瓷贴卡;平台多宿主收值
2026-08-31 用户第三轮:「界面上东西太多,做成几个区域;类型与核心参数字段做成和芯片卡
差不多的卡片;芯片卡与它的核心参数值合并 —— 点击翻转,背面写核心参数,悬浮要看得出能翻;
这个界面都不再需要表格来展示数据了」。
视图(主画面零表格,表格只活在新建类型抽屉里 —— 表单要加行,加行机制认 tr):
· 比对基准卡:类型丸 + 描述 + 字段瓷贴(名/硬软徽/单位/说明),与芯片卡同语言;
· 芯片卡翻面:正面主数据、背面本颗芯片的核心参数瓷贴(值必填红 / 说明建议橘)。
翻转纯 CSS(checkbox + :has,零脚本;checkbox 无 data-col,collect 不认它),
悬浮出「点击翻面 · 核心参数」提示丸 + 侧倾预告;主数据编辑锁正面、参数编辑自动
翻背面(reduced-motion 下瞬时切换);说明四条收进折叠。
平台(ui-kit ViewContainer,多宿主收值):「参数值印在每张卡背、就地可改」= 同一张表
的行分居两张卡 —— 一个地址一个宿主装不下。hostsOf(querySelectorAll)+ 收值/开合/
镜像/快照全部按宿主清单走;**格子只归最近的 data-edit 祖先**(不然卡背参数会被主数据
的闸收走并进主数据行 —— collect-nested 钉过的静默丢数据换脸回来)。嵌套 data-rows
不带 data-edit 时行为原样(两条旧钉测不动);addRow 仍单宿主(会加行的表都是单宿主)。
途中踩了三脚都逮回:CSS 注释里写了「data-col=」原文把 ②③ 的「全只读」字面断言踩翻
(选择器与注释都不许出现);.flip-cue i 特异度压过 .cue-back{display:none} 两枚提示
丸同时冒出;host 计数断言错把渲染后的数当模版源码的数。
测:ui-kit multihost 3 条(排除规则变异验证)+ 包内 110(翻面/无表格/瓷贴新锚)+
2528 全仓四门绿;2.9.0 装库 SQL 核过(flip/back_host/ftiles/cue/主画面表数=2);
浏览器实测:点卡真翻(背面 5 瓷贴)、参数编辑自动翻背双卡成表单、LM 全值回显。
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
This commit is contained in:
@@ -570,7 +570,7 @@ describe('件① 评估输入补全', () => {
|
||||
expect(html).toContain('data-rows="mine:parts.profile" data-edit="parts.profile"');
|
||||
expect(html).not.toContain('data-rows="mine:sub_eval.part" data-edit="parts.profile"');
|
||||
const nested = [...html.matchAll(/<span data-rows="mine:sub_eval\.part" data-by="internal_pn=internal_pn">((?:(?!<\/span>)[\s\S])*?)<\/span>/g)];
|
||||
expect(nested.length).toBeGreaterThanOrEqual(2);
|
||||
expect(nested.length).toBeGreaterThanOrEqual(1);
|
||||
for (const m of nested) expect(m[1] ?? '').not.toContain('data-col');
|
||||
});
|
||||
|
||||
@@ -597,20 +597,50 @@ describe('件① 评估输入补全', () => {
|
||||
/* 全只读:选类型在档案卡,建类型在抽屉 —— 基准卡上一个编辑面都不许有 */
|
||||
expect(basis).not.toContain('data-col');
|
||||
expect(basis).not.toContain('data-edit');
|
||||
/* 抽屉存在,且两张编辑表都住在里面 */
|
||||
/* 基准卡不再有表格:类型 + 字段全是瓷贴 */
|
||||
expect(basis).not.toContain('<table');
|
||||
expect(basis).toContain('class="ftiles"');
|
||||
/* 抽屉存在,且两张编辑表都住在里面(说明折叠用的是 lab lab-brief,不会被认成这一个) */
|
||||
const labAt = html.indexOf('<details class="lab">');
|
||||
const labEnd = html.indexOf('</details>');
|
||||
const labEnd = html.indexOf('</details>', labAt);
|
||||
expect(labAt).toBeGreaterThanOrEqual(0);
|
||||
expect(html.indexOf('<table data-edit="parts.chip_type">')).toBeGreaterThan(labAt);
|
||||
expect(html.indexOf('<table data-edit="parts.type_param">')).toBeGreaterThan(labAt);
|
||||
expect(labEnd).toBeGreaterThan(html.indexOf('<table data-edit="parts.type_param">'));
|
||||
});
|
||||
|
||||
it('三级占位:必填红 / 建议橘 —— 空格子直说要不要填(档案卡与参数值表两处)', () => {
|
||||
/*
|
||||
* 2026-08-31 用户第三轮:「芯片卡片需要和该芯片的核心参数值合并…点击后卡片翻转,背面写
|
||||
* 核心参数,悬浮要看得出可以翻;这个界面都不再需要表格来展示数据」。
|
||||
* 背面就地可改 = 两张卡背各住一个 parts.parameter 宿主 —— 多宿主收值是同日的平台扩面
|
||||
* (ViewContainer hostsOf + 格子只归最近的 data-edit 祖先,ui-kit multihost 测试钉着)。
|
||||
*/
|
||||
it('翻面卡:背面住着参数宿主、按料过滤(渲染时随行复制成每卡一个 —— 多宿主收值由 ui-kit multihost 测试钉);翻转是无 data-col 的 checkbox;编辑态锁面', () => {
|
||||
expect((html.match(/data-edit="parts\.parameter"/g) ?? []).length).toBe(1);
|
||||
expect(html).toContain('data-rows="mine:parts.parameter" data-by="internal_pn=internal_pn" data-edit="parts.parameter"');
|
||||
const flip = /<input class="flip-tg"[^>]*>/.exec(html)?.[0] ?? '';
|
||||
expect(flip).not.toBe('');
|
||||
expect(flip).toContain('type="checkbox"');
|
||||
expect(flip).not.toContain('data-col');
|
||||
expect(html).toContain('class="face front"');
|
||||
expect(html).toContain('class="face back"');
|
||||
/* 编辑锁面:主数据编辑停正面,参数编辑翻背面 —— 判断全在样式表 */
|
||||
expect(html).toContain('.dossiers.on .dossier .flip-in{transform:rotateY(0deg)}');
|
||||
expect(html).toContain('.dossier:has(.pback.on) .flip-in{transform:rotateY(180deg)}');
|
||||
/* 悬浮提示丸:看得出点击可翻 */
|
||||
expect(html).toContain('点击翻面');
|
||||
});
|
||||
|
||||
it('主画面无表格:说明折叠之前的正文一张 <table 都没有 —— 表格只活在抽屉里(表单要加行,加行机制认 tr)', () => {
|
||||
const beforeDrawer = html.slice(0, html.indexOf('<details class="lab">'));
|
||||
expect(beforeDrawer).not.toContain('<table');
|
||||
});
|
||||
|
||||
it('三级占位:必填红 / 建议橘 —— 空格子直说要不要填(档案卡正面与卡背参数瓷贴两处)', () => {
|
||||
expect(html).toContain('.fcell.req .jc-vcell:empty::after{content:"必填"');
|
||||
expect(html).toContain('.fcell.sug .jc-vcell:empty::after{content:"建议"');
|
||||
expect(html).toContain('td.pv .jc-vcell:empty::after{content:"必填"');
|
||||
expect(html).toContain('td.sg .jc-vcell:empty::after{content:"建议"');
|
||||
expect(html).toContain('.ptile .pv .jc-vcell:empty::after{content:"必填"');
|
||||
expect(html).toContain('.ptile .sg .jc-vcell:empty::after{content:"建议"');
|
||||
});
|
||||
|
||||
it('芯片类型是从台账现读的下拉 —— 写死等于「只能选出包那天就有的那几种」(块①的 chip_type_id 与块②b 的 type_id 都是)', () => {
|
||||
@@ -769,14 +799,14 @@ describe('件① 评估输入补全', () => {
|
||||
* 只读态与编辑态下「#」列逐行都是空白。钉死 `.jc-tbl-wrap` 现在也接了这五条规则,
|
||||
* 且这张件确实只用 `.jc-tbl-wrap`(没有表在用没打这条补丁就会数不出来的 `.tbl-wrap`)。
|
||||
*/
|
||||
it('四张表的序号列真的会数:.jc-tbl-wrap 也接了 CSS 计数器(不再只认 .tbl-wrap)', () => {
|
||||
it('抽屉里两张表的序号列真的会数:.jc-tbl-wrap 也接了 CSS 计数器(不再只认 .tbl-wrap)', () => {
|
||||
expect(html).toMatch(/\.jc-tbl-wrap\s+table\s*,\s*\.tbl-wrap\s+table\s*\{\s*counter-reset:\s*rn;?\s*\}/);
|
||||
expect(html).toMatch(/\.jc-tbl-wrap\s+table\s+td\.rn\s*,\s*\.tbl-wrap\s+table\s+td\.rn\s*\{\s*counter-increment:\s*rn;?\s*\}/);
|
||||
expect(html).toMatch(/\.jc-tbl-wrap\s+table\s+td\.rn\s+\.jc-rn-num::before\s*,\s*\.tbl-wrap\s+table\s+td\.rn\s+\.jc-rn-num::before\s*\{\s*content:\s*counter\(rn\);?\s*\}/);
|
||||
// 四张:比对基准卡的字段表 + 抽屉里两张编辑表 + 参数值表(2026-08-31 类型/字段并成基准卡后的账)
|
||||
// 只剩抽屉里两张(2026-08-31 第三轮:基准卡瓷贴化、参数值上卡背 —— 主画面零表格)
|
||||
expect(html).not.toContain('class="tbl-wrap"');
|
||||
const jcTblWrapCount = html.match(/class="jc-tbl-wrap"/g)?.length ?? 0;
|
||||
expect(jcTblWrapCount).toBe(4);
|
||||
expect(jcTblWrapCount).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ export const manifest: ActivityManifest = {
|
||||
activityId: ACTIVITY_ID,
|
||||
name: 'ACT-WORK-05 · 差异评估',
|
||||
/* 重写不是改 —— 同版重装是 no-op(静默丢失,踩过) */
|
||||
packageVersion: '2.8.1',
|
||||
packageVersion: '2.9.0',
|
||||
category: '需求分析与方案匹配',
|
||||
sdkVersion: '0.1.0-m5',
|
||||
checksum: 'dev-checksum-part-substitution-assessment',
|
||||
|
||||
@@ -178,10 +178,21 @@ ${badgeCss('.rl', [
|
||||
* `option[selected]`(行作用域求值,不受外层回落污染):这一行真选上了才摆名,
|
||||
* 没选摆「待选择」胶囊 —— 判断全在样式表(与被退回的换版面那版同一招,机制经过实测)。
|
||||
*
|
||||
* 人不加行(要评估哪两颗是启动参数定死的),没有「+加一行」;整卡一个宿主一道闸
|
||||
* (`bodyOf` 是单 querySelector),`.on` 落在 `data-edit` 元素自身(非表格容器)。
|
||||
* 人不加行(要评估哪两颗是启动参数定死的),没有「+加一行」。
|
||||
*
|
||||
* **翻面(2026-08-31 用户定)**:每张卡正面主数据、背面这颗芯片的核心参数值 ——
|
||||
* 点击卡面翻转(纯 CSS:`.flip-tg` checkbox + :has,零脚本;checkbox 不带 `data-col`,
|
||||
* collect 不认它),悬浮出「点击翻面 · 核心参数」提示丸 + 轻微侧倾预告可翻。
|
||||
* **背面的参数就地可改**:两张卡背各住一个 `data-edit="parts.parameter"` 宿主 ——
|
||||
* 多宿主收值是 2026-08-31 平台扩面(ViewContainer `hostsOf` + 「格子只归最近的
|
||||
* data-edit 祖先」,packages/ui-kit/__tests__/view-container-multihost.test.ts 钉着);
|
||||
* 开哪面的编辑,卡就停在哪面(主数据编辑锁正面、参数编辑自动翻背面,CSS 兜底)。
|
||||
*/
|
||||
const DOSSIERS = `<section class="board"><article class="dossiers" data-rows="mine:parts.profile" data-edit="parts.profile"><section class="dossier">
|
||||
<label class="flip-hit"><input class="flip-tg" type="checkbox" aria-label="翻转这张卡"></label>
|
||||
<span class="flip-cue"><i class="cue-front">点击翻面 · 核心参数</i><i class="cue-back">点击翻回 · 主数据</i></span>
|
||||
<div class="flip-in">
|
||||
<div class="face front">
|
||||
<header class="dz-top">
|
||||
${CHIP}
|
||||
<span class="dz-id">
|
||||
@@ -202,6 +213,17 @@ const DOSSIERS = `<section class="board"><article class="dossiers" data-rows="mi
|
||||
<label class="fcell sug wide"><i>规格描述</i>${cell('specification_description')}</label>
|
||||
<label class="fcell sug wide"><i>Datasheet 引用</i>${cell('datasheet_ref', 'mono')}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="face back">
|
||||
<header class="pb-hd"><b class="mono" data-slot="internal_pn" data-blank></b><span class="pb-lb">核心参数值</span></header>
|
||||
<section class="pback" data-rows="mine:parts.parameter" data-by="internal_pn=internal_pn" data-edit="parts.parameter"><div class="ptile">
|
||||
<span class="pt-hd"><span class="pt-name">${cell('param_name')}</span><i class="pt-unit" data-slot="param_unit" data-blank></i></span>
|
||||
<span class="pt-val pv">${cell('param_value', 'mono')}</span>
|
||||
<span class="pt-note sg">${cell('param_note')}</span>
|
||||
<span class="rowkey">${cell('internal_pn', 'mono')}</span>
|
||||
</div></section>
|
||||
</div>
|
||||
</div>
|
||||
</section></article></section>`;
|
||||
|
||||
/**
|
||||
@@ -220,25 +242,16 @@ 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">
|
||||
<header class="basis-hd">
|
||||
<span class="basis-lb">比对基准</span>
|
||||
${CHIP}
|
||||
<span class="basis-id">
|
||||
<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>
|
||||
</header>
|
||||
<div class="jc-tbl-wrap"><table>
|
||||
<thead><tr>
|
||||
<th style="width:38px">#</th>
|
||||
<th data-h="参数名">参数名</th>
|
||||
<th data-h="参数说明">参数说明</th>
|
||||
<th data-h="约束类型">约束(硬/软)</th>
|
||||
<th data-h="参考单位">参考单位</th>
|
||||
</tr></thead>
|
||||
<tbody data-rows="parts.type_param" data-by="type_id=chip_type_id"><tr>
|
||||
<td class="rn"><span class="jc-rn-num"></span></td>
|
||||
<td class="cust" data-slot="param_name" data-blank></td>
|
||||
<td class="desc" data-slot="param_desc" data-blank></td>
|
||||
<td><span class="ct" data-class-of="constraint_type"></span></td>
|
||||
<td data-slot="param_unit" data-blank></td>
|
||||
</tr></tbody>
|
||||
</table></div>
|
||||
<section class="ftiles" data-rows="parts.type_param" data-by="type_id=chip_type_id"><div class="ftile">
|
||||
<span class="ft-hd"><b data-slot="param_name" data-blank></b><span class="ct" data-class-of="constraint_type"></span><i class="ft-unit" data-slot="param_unit" data-blank></i></span>
|
||||
<p class="ft-desc" data-slot="param_desc" data-blank></p>
|
||||
</div></section>
|
||||
</section></article>`;
|
||||
|
||||
/**
|
||||
@@ -338,35 +351,6 @@ const TYPE_PARAM_TABLE = `<div class="jc-tbl-wrap"><table data-edit="parts.type_
|
||||
</tr></thead>
|
||||
</table></div>`;
|
||||
|
||||
/**
|
||||
* 块③ 参数值 —— `parts.parameter`,一字段一行(行由 S1 配方按 `sub_eval.part ×
|
||||
* parts.type_param` 铺,`prune:true` 会把不属于这个组合的孤行清掉,故这张表也**人不加行**)。
|
||||
*
|
||||
* `internal_pn` / `param_name` 也走 `cell()`:**主键格必挂 `data-col`**是本仓既定约定
|
||||
* (它不是编辑面,是行的钥匙 —— 漏了,保存时这一行凑不出主键,人改的值可能被错配到
|
||||
* 别的行上,2026-08-02 踩过)。业务上真正要人填的只有值与说明。
|
||||
* `param_unit` 是唯一真正只读、连孪生输入框都不给的一列:配方每拍从字典直接复制这一格,
|
||||
* 手改了也会被字典值覆盖回去,给它可编辑的孪生件只会让人误以为改了算数。
|
||||
*/
|
||||
const PARAMETER_TABLE = `<div class="jc-tbl-wrap"><table>
|
||||
<thead><tr>
|
||||
<th style="width:38px">#</th>
|
||||
<th data-h="所属料号">所属料号</th>
|
||||
<th data-h="参数名">参数名</th>
|
||||
<th class="req" data-h="值">值</th>
|
||||
<th data-h="单位">单位</th>
|
||||
<th data-h="说明">说明</th>
|
||||
</tr></thead>
|
||||
<tbody data-rows="mine:parts.parameter" data-edit="parts.parameter"><tr>
|
||||
<td class="rn"><span class="jc-rn-num"></span></td>
|
||||
<td><span class="jc-vcell ro pkrole"><span data-rows="mine:${D}.part" data-by="internal_pn=internal_pn"><i class="rl" data-class-of="role"></i></span></span>${cell('internal_pn', 'mono')}</td>
|
||||
<td>${cell('param_name')}</td>
|
||||
<td class="pv">${cell('param_value', 'mono')}</td>
|
||||
<td>${ro('param_unit')}</td>
|
||||
<td class="sg">${cell('param_note')}</td>
|
||||
</tr></tbody>
|
||||
</table></div>`;
|
||||
|
||||
export const INPUT_SUPPLEMENT = doc({
|
||||
accent: ACCENT_DATA,
|
||||
alias: ALIAS_INPUT,
|
||||
@@ -378,19 +362,22 @@ export const INPUT_SUPPLEMENT = doc({
|
||||
footTag: '工单类型:数据',
|
||||
body:
|
||||
gateBar() +
|
||||
zone('做什么', '核对两颗料在不在库、归没归同一个芯片类型、参数值齐不齐;缺什么就在下面补齐', ZIC_TASK) +
|
||||
`<details class="lab lab-brief"><summary class="lab-sm"><b>【做什么】</b>核对两颗料在不在库、归没归同一个芯片类型、参数值齐不齐 —— 展开看说明与纪律<span class="lab-caret"></span></summary>` +
|
||||
note([
|
||||
'<span class="need">模板定位</span>:本单是「数据工单」——目标台账 = 芯片主数据(P-08 主记录 + 标准化参数子表),核心参数字段清单挂在芯片类型字典上。',
|
||||
'<span class="need">触发原因</span>:下面电路板上两张档案卡就是本次要比对的原 PN 与候选 PN —— 至少有一颗不在库、两颗归的类型不一致、或必填格还空着,顶部那条红带才会出现。',
|
||||
'<span class="done">已绑定</span>:两颗料的内部料号已按启动参数铺上档案卡,不必你重填料号本身,只需核对 / 补齐其余各格;在库的料现值已回填,只是查漏补缺。',
|
||||
'<span class="need">纪律</span>:两颗料必须归到同一个芯片类型下才谈得上逐项比对;类型下拉里没有合适的就在【芯片类型】区新建一个,并把它的核心参数字段一并定下来;查不到的参数值留空(会被红带点名),不要编一个值顶替。',
|
||||
'<span class="need">纪律</span>:两颗料必须归到同一个芯片类型下才谈得上逐项比对;类型下拉里没有合适的就在下面的抽屉里新建一个,并把它的核心参数字段一并定下来;查不到的参数值留空(会被红带点名),不要编一个值顶替。',
|
||||
]) +
|
||||
zone('要评估什么', '两颗料各一张档案卡,直接在卡上核对 / 补齐(人不加行,*为必填)', ZIC_TABLE) +
|
||||
editGate('parts.profile', '两张档案卡上的主数据核对 / 补齐,点「编辑」开卡') +
|
||||
gapbar('parts_profile', '必填') +
|
||||
DOSSIERS +
|
||||
zone('比对基准 · 芯片类型与核心参数字段', '两颗料归到哪个类型,就按哪个类型的字段逐项比 —— 档案卡上改选,这张卡当场跟着换', ZIC_TABLE) +
|
||||
`</details>` +
|
||||
zone('比对基准', '芯片类型 + 它的核心参数字段 —— 两颗料归到哪个类型,就按这些字段逐项比;档案卡上改选,这张卡当场跟着换', ZIC_TABLE) +
|
||||
BASIS_CARD +
|
||||
zone('芯片档案', '一颗芯片一张卡:正面主数据、背面核心参数值 —— 点卡翻面看另一面', ZIC_TABLE) +
|
||||
editGate('parts.profile', '正面主数据核对 / 补齐 —— 开编辑后卡停在正面') +
|
||||
editGate('parts.parameter', '背面参数值核对 / 补齐 —— 开编辑后两张卡自动翻到背面') +
|
||||
gapbar('parts_profile', '必填') +
|
||||
gapbar('parts_parameter', '必填') +
|
||||
DOSSIERS +
|
||||
gapbar('parts_chip_type', '必填') +
|
||||
gapbar('parts_type_param', '必填') +
|
||||
`<details class="lab"><summary class="lab-sm"><b>类型库里没有合适的?</b>展开新建一个类型,并把它的核心参数字段定下来 —— 定完回到上面的档案卡挑它<span class="lab-caret"></span></summary><div class="lab-bd">` +
|
||||
@@ -402,10 +389,6 @@ export const INPUT_SUPPLEMENT = doc({
|
||||
TYPE_PARAM_TABLE +
|
||||
addrow('parts.type_param', '+ 加一行(补一个核心参数字段)', '所属类型 / 参数名 / 参数说明 / 约束类型必须写;参考单位选填') +
|
||||
`</div></details>` +
|
||||
zone('参数值', '逐条核心参数的实测 / 规格值(一字段一行,*为必填列;单位由字典定,这里不改)', ZIC_TABLE) +
|
||||
editGate('parts.parameter', '逐条参数值核对 / 补齐,点「编辑」开表') +
|
||||
gapbar('parts_parameter', '必填') +
|
||||
PARAMETER_TABLE +
|
||||
flow('<b>提交 / 确认:</b>本工单只承载内容;确认请在对话区的【执行卡】中进行。确认后活动自动从「等待数据」唤醒。'),
|
||||
});
|
||||
|
||||
|
||||
@@ -288,28 +288,84 @@ table.on tr.jc-row-static td{background:var(--acc-wash);border-top:1px dashed va
|
||||
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.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 元素自身) */
|
||||
.dossiers.on .jc-vcell{display:none}
|
||||
.dossiers.on .jc-vcell.ro{display:block}
|
||||
.dossiers.on .jc-cell{display:block}
|
||||
/* 档案卡的编辑态 —— **只点亮正面**(.on 落在 data-edit 元素自身;卡背住着另一地址的宿主,
|
||||
不圈 .front 的话参数输入会被主数据的闸顺手点亮) */
|
||||
.dossiers.on .front .jc-vcell{display:none}
|
||||
.dossiers.on .front .jc-vcell.ro{display:block}
|
||||
.dossiers.on .front .jc-cell{display:block}
|
||||
.dossiers.on .front .jc-vcell:empty::after{content:none}
|
||||
.dossiers.on .dz-role{display:flex}
|
||||
/* 参数值表:所属料号列的角色徽 + 值/说明列的三级占位 */
|
||||
.pkrole{margin-bottom:2px}
|
||||
.jc-tbl-wrap td.pv .jc-vcell:empty::after{content:"必填";font-size:10.5px;font-weight:800;color:var(--danger);background:var(--danger-soft);border-radius:999px;padding:0 9px}
|
||||
.jc-tbl-wrap td.sg .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}
|
||||
|
||||
/* ── 翻面(纯 CSS:checkbox + :has,零脚本;reduced-motion 下瞬时切换)── */
|
||||
.dossier{position:relative;perspective:1200px;padding:0;background:none;border:0;box-shadow:none}
|
||||
.dossier:hover{transform:none;box-shadow:none}
|
||||
.flip-in{display:grid;transform-style:preserve-3d;transition:transform .55s cubic-bezier(.3,.7,.3,1)}
|
||||
.face{grid-area:1 / 1;display:flex;flex-direction:column;gap:11px;background:#fff;border:1px solid var(--line-soft);border-radius:16px;padding:15px 16px 14px;box-shadow:0 8px 22px rgba(18,38,63,.09);backface-visibility:hidden;min-width:0}
|
||||
.face.back{transform:rotateY(180deg)}
|
||||
.flip-tg{position:absolute;opacity:0;pointer-events:none}
|
||||
.flip-hit{position:absolute;inset:0;z-index:3;cursor:pointer}
|
||||
.flip-cue{position:absolute;z-index:4;bottom:10px;right:10px;pointer-events:none;opacity:.6;transition:opacity .16s,transform .16s}
|
||||
.flip-cue i{font-style:normal;display:inline-block;font-size:10.5px;font-weight:800;color:var(--acc-deep);background:var(--acc-soft);border:1px solid var(--acc-line);border-radius:999px;padding:2px 10px}
|
||||
.dossier:hover .flip-cue{opacity:1;transform:translateY(-2px)}
|
||||
/* 两句提示按面切换 —— 选择器都写到 .flip-cue i 之下的同层级,别被基座那条特异度压回来(踩过) */
|
||||
.flip-cue .cue-back{display:none}
|
||||
/* 翻转三态,后写的赢:人点的 < 主数据编辑锁正面 < 参数编辑翻背面 */
|
||||
.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-back{display:inline-block}
|
||||
.dossiers.on .dossier .flip-in{transform:rotateY(0deg)}
|
||||
.dossier:has(.pback.on) .flip-in{transform:rotateY(180deg)}
|
||||
/* 编辑中不许翻(哪面在编辑锁哪面),盖满卡面的点击层与提示丸一起收 */
|
||||
.dossiers.on .flip-hit,.dossier:has(.pback.on) .flip-hit{display:none}
|
||||
.dossiers.on .flip-cue,.dossier:has(.pback.on) .flip-cue{display:none}
|
||||
/* 悬浮侧倾:预告「这张卡点了会翻」—— 只在读态、只在真有悬浮的设备、且不嫌动画的人那里 */
|
||||
@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)}
|
||||
.dossiers:not(.on) .dossier:hover:has(.flip-tg:checked):not(:has(.pback.on)) .flip-in{transform:rotateY(168deg)}
|
||||
}
|
||||
|
||||
/* ── 卡背:参数瓷贴 ── */
|
||||
.pb-hd{display:flex;align-items:baseline;gap:9px;flex-wrap:wrap}
|
||||
.pb-hd b{font-size:14px;font-weight:800;color:var(--ink)}
|
||||
.pb-lb{font-size:10.5px;font-weight:800;letter-spacing:.08em;color:var(--ink-4)}
|
||||
.pback{display:grid;grid-template-columns:1fr 1fr;gap:8px}
|
||||
.ptile{display:flex;flex-direction:column;gap:2px;border:1px solid var(--line-soft);border-radius:10px;background:var(--soft);padding:8px 10px;min-width:0}
|
||||
.pt-hd{display:flex;align-items:baseline;gap:6px;min-width:0}
|
||||
.pt-hd .jc-vcell{font-size:11px;font-weight:800;color:var(--ink-3)}
|
||||
.pt-unit{font-style:normal;font-size:10px;color:var(--ink-4);font-family:var(--mono)}
|
||||
.pt-val .jc-vcell{font-size:14px;font-weight:800;color:var(--ink)}
|
||||
.pt-note .jc-vcell{font-size:10.5px;color:var(--ink-3)}
|
||||
/* 三级占位:值必填红 / 说明建议橘 */
|
||||
.ptile .pv .jc-vcell:empty::after{content:"必填";font-size:10.5px;font-weight:800;color:var(--danger);background:var(--danger-soft);border-radius:999px;padding:0 9px}
|
||||
.ptile .sg .jc-vcell:empty::after{content:"建议";font-size:10px;font-weight:800;color:var(--amber);background:var(--amber-soft);border-radius:999px;padding:0 8px}
|
||||
/* 参数编辑态:值与说明换输入,参数名照旧当标签摆着;两枚主键孪生件藏死(钥匙不是编辑面)。
|
||||
藏钥匙用包装类(.pt-name / .rowkey),不用「data-col 属性选择器」—— 那段选择器文本会原样
|
||||
出现在共享样式表里,把「落账确认全只读」那类字面断言踩翻(2026-08-31 踩过,连注释也不许写)。 */
|
||||
.pback.on .pv > .jc-vcell,.pback.on .sg > .jc-vcell{display:none}
|
||||
.pback.on .pv > .jc-cell,.pback.on .sg > .jc-cell{display:block}
|
||||
.pback .pt-name .jc-cell{display:none}
|
||||
.pt-name{display:inline-flex;min-width:0}
|
||||
|
||||
/* ================= 比对基准卡(类型 + 字段合一,只读、跟手)================= */
|
||||
.basis{display:block;margin:0 0 10px}
|
||||
.basis:empty{border:1.5px dashed var(--line);border-radius:12px;padding:16px;display:block}
|
||||
.basis:empty::after{content:"还没定比对基准 —— 先在上面任一张档案卡上挑一个芯片类型,或展开下面的抽屉新建一个。";font-size:12.5px;color:var(--ink-3)}
|
||||
.basis-in{display:flex;flex-direction:column;gap:10px;border:1px solid var(--acc-line);border-radius:12px;background:var(--acc-wash);padding:12px 14px 14px}
|
||||
.basis-hd{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap}
|
||||
.basis-hd{display:flex;align-items:center;gap:12px;flex-wrap:wrap}
|
||||
.basis-lb{flex:none;font-size:10.5px;font-weight:800;letter-spacing:.08em;color:var(--acc-deep)}
|
||||
.basis-chip b{display:inline-block;font-size:13px;font-weight:800;color:#fff;background:var(--acc);border-radius:999px;padding:2px 14px;margin-right:8px}
|
||||
.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-in:has(tbody:empty) .jc-tbl-wrap{display:none}
|
||||
.basis-in .jc-tbl-wrap{background:#fff}
|
||||
.basis-hd .csvg{width:56px;flex:none}
|
||||
.basis-id{display:flex;flex-direction:column;gap:5px;min-width:0;flex:1 1 auto}
|
||||
.basis-in:has(.ftiles:empty) .ftiles{display:none}
|
||||
.ftiles{display:grid;grid-template-columns:1fr 1fr;gap:8px}
|
||||
.ftile{display:flex;flex-direction:column;gap:3px;border:1px solid var(--line-soft);border-radius:10px;background:#fff;padding:9px 11px;min-width:0}
|
||||
.ft-hd{display:flex;align-items:baseline;gap:7px;flex-wrap:wrap}
|
||||
.ft-hd b{font-size:12.5px;font-weight:800;color:var(--ink)}
|
||||
.ft-unit{font-style:normal;font-size:10px;color:var(--ink-4);font-family:var(--mono)}
|
||||
.ft-desc{margin:0;font-size:11px;line-height:1.6;color:var(--ink-3)}
|
||||
.ft-desc:empty{display:none}
|
||||
|
||||
/* ================= 新建类型工作抽屉(details,零脚本)================= */
|
||||
.lab{margin:0 0 6px;border:1.5px dashed var(--line);border-radius:12px;background:var(--soft)}
|
||||
@@ -322,11 +378,15 @@ table.on tr.jc-row-static td{background:var(--acc-wash);border-top:1px dashed va
|
||||
.lab[open] .lab-caret{transform:rotate(-135deg)}
|
||||
.lab-bd{padding:2px 14px 14px}
|
||||
.lab-sub{margin:16px 0 8px;font-size:11.5px;color:var(--ink-3)}
|
||||
.lab-brief{margin:0 0 16px}
|
||||
.lab-brief .brief{margin:2px 14px 14px;border-left-width:3px}
|
||||
|
||||
/* ================= 窄屏 ================= */
|
||||
@media(max-width:620px){
|
||||
.card-head{padding:16px 14px 18px}
|
||||
.dz-grid{grid-template-columns:1fr}
|
||||
.pback{grid-template-columns:1fr}
|
||||
.ftiles{grid-template-columns:1fr}
|
||||
.card-bd{padding:16px 14px 6px}
|
||||
.hero-title{font-size:19.5px}
|
||||
.stage{gap:14px}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* **多宿主收值 + 嵌套异地址排除**(2026-08-31,W05 芯片卡翻面逼出的平台扩面)。
|
||||
*
|
||||
* 版面要求「参数值印在每张芯片卡的**背面**、就地可改」:参数表的行天然分居两张卡,
|
||||
* 一个宿主装不下 —— `hostsOf` 把同一地址的全部宿主都数进来,每个宿主各自归行。
|
||||
*
|
||||
* 更要紧的是**嵌套异地址排除**:卡背的参数宿主(`data-edit="parts.parameter"`)住在
|
||||
* 芯片卡宿主(`data-edit="parts.profile"`)的行里 —— 外层收值若把里层宿主的格子也
|
||||
* 收走,参数值就会被当成主数据的列并进主数据的行(`view-container-collect-nested.test.ts`
|
||||
* 钉过的静默丢数据,换一副面孔回来)。规则:**格子只归它最近的 `data-edit` 祖先**。
|
||||
* 嵌套的 `data-rows` 不带 `data-edit` 时行为原样(照旧并行 —— 那两条钉测不动)。
|
||||
*/
|
||||
// @vitest-environment jsdom
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { collect, setOpenIn } from '../src/ViewContainer.tsx';
|
||||
|
||||
/** 两张芯片卡:正面是主数据格(profile),背面各住一个参数宿主(parameter) */
|
||||
const FLIP_CARDS =
|
||||
'<article data-edit="parts.profile">' +
|
||||
'<section class="dossier">' +
|
||||
'<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="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="3"/></div>' +
|
||||
'</section></div>' +
|
||||
'</section>' +
|
||||
'<section class="dossier">' +
|
||||
'<div class="front"><input data-col="internal_pn" value="XL2596S-5.0"/><input data-col="brand_name" value="XLSEMI"/></div>' +
|
||||
'<div class="back"><section class="pback" data-edit="parts.parameter">' +
|
||||
'<div class="ptile"><input data-col="internal_pn" value="XL2596S-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="XL2596S-5.0"/><input data-col="param_name" value="输出电流"/><input data-col="param_value" value="2.5"/></div>' +
|
||||
'</section></div>' +
|
||||
'</section>' +
|
||||
'</article>' +
|
||||
'<div class="jc-editgate" data-edit-for="parts.parameter"></div>';
|
||||
|
||||
function docOf(html: string): Document {
|
||||
const doc = document.implementation.createHTMLDocument('');
|
||||
doc.body.innerHTML = html;
|
||||
return doc;
|
||||
}
|
||||
|
||||
describe('多宿主 collect():同一地址的宿主都数进来,格子只归最近的 data-edit 祖先', () => {
|
||||
it('parts.parameter 分居两张卡背的两个宿主 —— 4 个瓷贴收到 4 行,主键各自带对', () => {
|
||||
const got = collect(docOf(FLIP_CARDS), 'parts.parameter');
|
||||
expect(got).toHaveLength(4);
|
||||
const xl = got.filter((r) => r['internal_pn'] === 'XL2596S-5.0');
|
||||
expect(xl).toHaveLength(2);
|
||||
expect(new Map(xl.map((r) => [r['param_name'], r['param_value']])).get('输出电流')).toBe('2.5');
|
||||
});
|
||||
|
||||
it('parts.profile 收值**不含**卡背参数宿主的格子 —— 两行主数据,一列参数都不许混进来', () => {
|
||||
const got = collect(docOf(FLIP_CARDS), 'parts.profile');
|
||||
expect(got).toHaveLength(2);
|
||||
for (const row of got) {
|
||||
expect(row['brand_name']).toBeDefined();
|
||||
/* 静默丢数据的那副面孔:param_value 若出现在主数据行里,就是里层宿主被外层吞了 */
|
||||
expect(row['param_value']).toBeUndefined();
|
||||
expect(row['param_name']).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
it('setOpenIn:一把闸开合同一地址的**全部**宿主(非表格容器,.on 落在宿主自身)', () => {
|
||||
const doc = docOf(FLIP_CARDS);
|
||||
setOpenIn(doc, 'parts.parameter', true);
|
||||
const hosts = Array.from(doc.querySelectorAll('.pback'));
|
||||
expect(hosts).toHaveLength(2);
|
||||
expect(hosts.every((h) => h.classList.contains('on'))).toBe(true);
|
||||
expect(doc.querySelector('[data-edit-for="parts.parameter"]')?.classList.contains('on')).toBe(true);
|
||||
/* 外层 profile 宿主不许被顺手点亮 —— 两把闸各管各的 */
|
||||
expect(doc.querySelector('[data-edit="parts.profile"]')?.classList.contains('on')).toBe(false);
|
||||
setOpenIn(doc, 'parts.parameter', false);
|
||||
expect(hosts.every((h) => !h.classList.contains('on'))).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -161,9 +161,33 @@ function replaceInPlace(doc: Document, html: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
/** 一张可改的表的容器(tbody)—— 按 `data-edit` 地址找 */
|
||||
/** 一张可改的表的容器(tbody)—— 按 `data-edit` 地址找(多宿主时取文档序第一个,老调用方语义不变) */
|
||||
export function bodyOf(doc: Document, address: string): HTMLElement | null {
|
||||
return doc.querySelector<HTMLElement>(`[data-edit="${CSS.escape(address)}"]`);
|
||||
return hostsOf(doc, address)[0] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* **同一个地址的全部宿主**(2026-08-31,W05 芯片卡翻面逼出的多宿主收值)。
|
||||
*
|
||||
* 从前一个地址恰一个宿主(`bodyOf` 单 querySelector)——「参数值印在每张芯片卡的背面、
|
||||
* 就地可改」这种版面下,同一张表(`parts.parameter`)的行天然分居两张卡,一个宿主装不下。
|
||||
* 现在收值 / 开合 / 镜像都按**宿主清单**走:每个宿主各自归行(行仍是宿主的直接子元素),
|
||||
* 清单合起来就是这张表。加行(`addRow`)仍只认第一个宿主 —— 会加行的表(类型字典 /
|
||||
* 字段清单)从来都是单宿主,多宿主的表行由配方铺、人不加行。
|
||||
*/
|
||||
export function hostsOf(doc: Document, address: string): HTMLElement[] {
|
||||
return Array.from(doc.querySelectorAll<HTMLElement>(`[data-edit="${CSS.escape(address)}"]`));
|
||||
}
|
||||
|
||||
/**
|
||||
* 这个格子归这个宿主管吗 —— **认最近的 `data-edit` 祖先**。
|
||||
* 宿主可以嵌套(芯片卡是 `parts.profile` 的行,卡背里又住着 `parts.parameter` 的宿主):
|
||||
* 外层收值时不认里层宿主的格子,不然参数值会被当成主数据的列并进主数据的行
|
||||
* (`view-container-collect-nested.test.ts` 钉过的那种静默丢数据,换一副面孔回来)。
|
||||
* 嵌套的 `data-rows` **不带** `data-edit` 时行为原样(照旧并行 —— 那两条钉测不动)。
|
||||
*/
|
||||
function ownsCell(host: HTMLElement, cell: Element): boolean {
|
||||
return cell.closest('[data-edit]') === host;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -186,11 +210,11 @@ type EditCell = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
|
||||
|
||||
/** 一张表的输入格子;`gone` 的行(人删掉的)不算在内。下拉与多行文本(textarea)与文本输入同源同待 */
|
||||
function cellsOf(doc: Document, address: string): EditCell[] {
|
||||
const host = bodyOf(doc, address);
|
||||
if (host === null) return [];
|
||||
return Array.from(
|
||||
host.querySelectorAll<EditCell>('input[data-col], select[data-col], textarea[data-col]'),
|
||||
).filter((i) => i.closest('.jc-row-gone') === null);
|
||||
return hostsOf(doc, address).flatMap((host) =>
|
||||
Array.from(host.querySelectorAll<EditCell>('input[data-col], select[data-col], textarea[data-col]')).filter(
|
||||
(i) => ownsCell(host, i) && i.closest('.jc-row-gone') === null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -212,10 +236,12 @@ export function unanswered(cell: EditCell): boolean {
|
||||
* 认行元素则怎么增删都对得上:同一个 `<tr>` 里的格子就是同一行。
|
||||
*/
|
||||
export function collect(doc: Document, address: string): Array<Record<string, string>> {
|
||||
const host = bodyOf(doc, address);
|
||||
if (host === null) return [];
|
||||
const byRow = new Map<Element, Record<string, string>>();
|
||||
for (const cell of cellsOf(doc, address)) {
|
||||
for (const host of hostsOf(doc, address)) {
|
||||
for (const cell of Array.from(
|
||||
host.querySelectorAll<EditCell>('input[data-col], select[data-col], textarea[data-col]'),
|
||||
)) {
|
||||
if (!ownsCell(host, cell) || cell.closest('.jc-row-gone') !== null) continue;
|
||||
const col = cell.dataset['col'];
|
||||
const row = rowOf(host, cell);
|
||||
if (col === undefined || row == null) continue;
|
||||
@@ -230,6 +256,7 @@ export function collect(doc: Document, address: string): Array<Record<string, st
|
||||
one[col] = cell.value;
|
||||
byRow.set(row, one);
|
||||
}
|
||||
}
|
||||
return [...byRow.values()];
|
||||
}
|
||||
|
||||
@@ -265,16 +292,18 @@ function mirrorTwins(doc: Document, address: string): void {
|
||||
|
||||
/** 人删掉的那几行 —— 只交主键那几格够认行的值(整行都交也无妨,服务端只看主键) */
|
||||
function collectGone(doc: Document, address: string): Array<Record<string, string>> {
|
||||
const host = bodyOf(doc, address);
|
||||
if (host === null) return [];
|
||||
return Array.from(host.querySelectorAll<HTMLElement>('.jc-row-gone')).map((row) => {
|
||||
return hostsOf(doc, address).flatMap((host) =>
|
||||
Array.from(host.querySelectorAll<HTMLElement>('.jc-row-gone'))
|
||||
.filter((row) => row.closest('[data-edit]') === host)
|
||||
.map((row) => {
|
||||
const one: Record<string, string> = {};
|
||||
for (const cell of Array.from(row.querySelectorAll<HTMLInputElement>('input[data-col]'))) {
|
||||
const col = cell.dataset['col'];
|
||||
if (col !== undefined) one[col] = cell.value;
|
||||
}
|
||||
return one;
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -322,6 +351,16 @@ export function addRow(doc: Document, address: string): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 那块内容与它的闸一起开合 —— **同一地址的全部宿主一起**(多宿主时一把闸管一整张表)。
|
||||
* `.on` 落在哪:行容器是表格的落在 `<table>`(老件,样式按 `table.on` 写);
|
||||
* 不是表格的落在 `data-edit` 元素自身(2026-08-02 A2 对决卡立的规矩)。
|
||||
*/
|
||||
export function setOpenIn(doc: Document, address: string, on: boolean): void {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* `data-file="域.对象/主键值"` → 取文件要的那两样。
|
||||
*
|
||||
@@ -409,7 +448,7 @@ export function ViewContainer({ html, title = '活动视图', editable = false,
|
||||
const fileSink = useRef(onOpenFile);
|
||||
fileSink.current = onOpenFile;
|
||||
/** 进编辑态那一刻的样子 —— 「取消」要还原成它(值 + 那时有几行) */
|
||||
const snapshot = useRef(new Map<string, { values: string[]; rows: number }>());
|
||||
const snapshot = useRef(new Map<string, { values: string[]; rows: number[] }>());
|
||||
|
||||
/*
|
||||
* 挂事件 + 开关闸 —— 都在**父文档这一侧**做,iframe 里没有脚本。
|
||||
@@ -462,11 +501,7 @@ export function ViewContainer({ html, title = '活动视图', editable = false,
|
||||
* 容器是 `<section>`,从前这里写死 `closest('table')`,非表格容器永远拿不到
|
||||
* `.on`,编辑态整个打不开)。
|
||||
*/
|
||||
const setOpen = (address: string, on: boolean): void => {
|
||||
doc.querySelector<HTMLElement>(`[data-edit-for="${CSS.escape(address)}"]`)?.classList.toggle('on', on);
|
||||
const host = doc.querySelector<HTMLElement>(`[data-edit="${CSS.escape(address)}"]`);
|
||||
(host?.closest('table') ?? host)?.classList.toggle('on', on);
|
||||
};
|
||||
const setOpen = (address: string, on: boolean): void => setOpenIn(doc, address, on);
|
||||
|
||||
const open = hit.dataset['editOpen'];
|
||||
const cancel = hit.dataset['editCancel'];
|
||||
@@ -479,7 +514,8 @@ export function ViewContainer({ html, title = '活动视图', editable = false,
|
||||
// (radio 的 value 是恒定的选项值,快照与还原认的是 checked)
|
||||
snapshot.current.set(open, {
|
||||
values: cellsOf(doc, open).map((c) => (c.type === 'radio' ? ((c as HTMLInputElement).checked ? '1' : '') : c.value)),
|
||||
rows: Array.from(bodyOf(doc, open)?.children ?? []).length,
|
||||
/* 行数逐宿主记 —— 多宿主时「取消」要把每个宿主各自加出来的行裁回去 */
|
||||
rows: hostsOf(doc, open).map((h) => h.children.length),
|
||||
});
|
||||
setOpen(open, true);
|
||||
return;
|
||||
@@ -488,11 +524,11 @@ export function ViewContainer({ html, title = '活动视图', editable = false,
|
||||
const was = snapshot.current.get(cancel);
|
||||
if (was !== undefined) {
|
||||
// 加出来的那几行整个撤掉,删掉的标记摘掉,再把值放回去 —— 三样一起才叫还原
|
||||
const host = bodyOf(doc, cancel);
|
||||
if (host !== null) {
|
||||
while (host.children.length > was.rows) host.lastElementChild?.remove();
|
||||
hostsOf(doc, cancel).forEach((host, i) => {
|
||||
const keep = was.rows[i];
|
||||
if (keep !== undefined) while (host.children.length > keep) host.lastElementChild?.remove();
|
||||
for (const row of Array.from(host.querySelectorAll('.jc-row-gone'))) row.classList.remove('jc-row-gone');
|
||||
}
|
||||
});
|
||||
cellsOf(doc, cancel).forEach((c, i) => {
|
||||
const saved = was.values[i];
|
||||
if (saved === undefined) return;
|
||||
|
||||
Reference in New Issue
Block a user