feat(w05): 数据工单第四轮九连改 —— 笔↔软盘卡角编辑、只选不建、浅蓝题头、镜像修数字

2026-08-31 用户一次点齐九件,逐条落:
① 两行编辑闸退役 → 卡右上角笔(开当前这一面的编辑)↔ 软盘(保存)+ 叉(放弃);
  按钮就是平台的 data-edit-open/save/cancel 触发器,快照/取消机制原样,按面显隐全在样式表。
② 保存一瞬间下拉变数字 → ui-kit mirrorTwins:下拉镜像**选项文案**不镜 value,并把
  selected 属性拨到人选的那一项(守卫 CSS 与取消回显都认它)。2 测。
③ 参数输入框占位提示 → 渲染器 twinInputs 认 data-ph 落成 placeholder(平台小扩面,1 测);
  值格「规格书上的值 · 查不到留空」,说明格「补充说明 · 选填」。
④ 参数说明空着不再摆「建议」→ 什么都不摆(连兜底破折号一起收)。
⑤ 区序对调:芯片档案在前,比对基准在后。
⑥ **新建芯片类型整条路移除**(只能选已有类型):抽屉与两张编辑表(种子行/Critical-1
  修法/thead 回归网)退役,S1 配方 ②③ 自投影删除(八笔→六笔),gateLedgers 收窄到
  profile+parameter,writes 同步,gate_reason 与指导书改口「只选不建」;样例改名
  「候选缺库·建档定型」;e2e 案例 0 改走选既有 type_1(type_id=90 那套绕坑法随路退役)。
  退役面的七条钉测按仓例 it.skip 留墓志铭(钉的是已移除的路径,不是放松断言)。
⑦ 题头回浅蓝渐变(原目标件那条 120°),深色夜景只留验收两件。
⑧ 对话区执行卡不再摆「参考信息」板块(basis 数据照旧随卡走,详情/归档仍有)。
⑨ 比对基准降噪:字段收成丸(名+硬软徽+单位),说明藏进悬浮气泡指上即看;
  类型说明一行截断、悬浮展开。

测:ui-kit 33(mirror 2 新)+ 渲染器 61(ph 1 新)+ 包内 104(38 steps 重排)+ 2525 全仓
四门绿;2.10.0 装库 SQL 六探针全中(hd-light/软盘/data-ph/零表格/无 chip_type 编辑面/
gateLedgers 两张);:8787 已重启载入 data-ph 渲染器。

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 16:41:14 +08:00
co-authored by Claude Fable 5
parent f6c8dccb4f
commit 51914292dc
12 changed files with 259 additions and 389 deletions
@@ -450,7 +450,7 @@ describe('报告模版:编辑面与只读面各就各位', () => {
it('每个可编辑格子的 data-col 都在对应台账对象的声明里(input-supplement 一屏四张表,四张台账对象的声明并起来看;difference-review 一张表)', () => {
const cases: Array<[string, Array<[string, string]>]> = [
['input-supplement', [['parts', 'profile'], ['parts', 'chip_type'], ['parts', 'type_param'], ['parts', 'parameter']]],
['input-supplement', [['parts', 'profile'], ['parts', 'parameter']]],
/* 件② 2026-08-31 起多一张:收口四格可就地改,写向 substitution.relation */
['difference-review', [['substitution', 'difference_item'], ['substitution', 'relation']]],
];
@@ -548,12 +548,12 @@ describe('件① 评估输入补全', () => {
expect(steps.find((s) => s.item === 'S1_inputs')?.view).toBe('input-supplement');
});
it('四块都在,行都绑本单草稿(不是整表);档案卡行源是 parts.profile 本身', () => {
it('三块都在,行都绑本单草稿(不是整表);档案卡行源是 parts.profile 本身(类型字典只读整表,不再有 mine: 镜像)', () => {
expect(html).toContain('data-rows="mine:sub_eval.part"');
expect(html).toContain('data-rows="mine:parts.profile"');
expect(html).toContain('data-rows="mine:parts.parameter"');
expect(html).toContain('data-rows="mine:parts.chip_type"');
expect(html).toContain('data-rows="mine:parts.type_param"');
expect(html).not.toContain('data-rows="mine:parts.chip_type"');
expect(html).not.toContain('data-rows="mine:parts.type_param"');
});
/*
@@ -597,16 +597,14 @@ describe('件① 评估输入补全', () => {
/* 全只读:选类型在档案卡,建类型在抽屉 —— 基准卡上一个编辑面都不许有 */
expect(basis).not.toContain('data-col');
expect(basis).not.toContain('data-edit');
/* 基准卡不再有表格:类型 + 字段全是瓷贴 */
/* 基准卡不再有表格:类型 + 字段全是瓷贴;说明藏进悬浮气泡(降噪,2026-08-31) */
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>', 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">'));
expect(html).toContain('.ftile:hover .ft-desc');
/* 新建类型抽屉已随「只选不建」退役(2026-08-31 用户定)—— 只剩说明折叠那一只 details */
expect(html).not.toContain('<details class="lab">');
expect(html).not.toContain('data-edit="parts.chip_type"');
expect(html).not.toContain('data-edit="parts.type_param"');
});
/*
@@ -631,34 +629,28 @@ describe('件① 评估输入补全', () => {
expect(html).toContain('点击翻面');
});
it('主画面无表格:说明折叠之前的正文一张 <table 都没有 —— 表格只活在抽屉里(表单要加行,加行机制认 tr)', () => {
const beforeDrawer = html.slice(0, html.indexOf('<details class="lab">'));
expect(beforeDrawer).not.toContain('<table');
it('整件零表格(2026-08-31 用户:「这个界面都不再需要表格来展示数据了」;抽屉连表带功能一并退役)', () => {
expect(html).not.toContain('<table');
expect(html).not.toContain('class="jc-tbl-wrap"');
});
it('三级占位:必填红 / 建议橘 —— 空格子直说要不要填(档案卡正面与卡背参数瓷贴两处)', () => {
it('占位口径:正面必填红/建议橘;卡背值必填红,说明空着**什么都不摆**(2026-08-31 用户:「没写就什么都不要展示」)', () => {
expect(html).toContain('.fcell.req .jc-vcell:empty::after{content:"必填"');
expect(html).toContain('.fcell.sug .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:"建议"');
expect(html).toContain('.ptile .sg .jc-vcell:empty{display:none}');
expect(html).not.toContain('.ptile .sg .jc-vcell:empty::after');
});
it('芯片类型是从台账现读的下拉 —— 写死等于「只能选出包那天就有的那几种」(块①的 chip_type_id 与块②b 的 type_id 都是)', () => {
it('芯片类型是从台账现读的下拉 —— 写死等于「只能选出包那天就有的那几种」', () => {
expect(html).toContain('data-options="parts.chip_type"');
expect(html).toContain('data-option-value="type_id"');
expect(html).toContain('data-option-label="type_name"');
/* data-options 与 data-slot 不能共存(渲染时 data-slot 会被剥掉)—— 当下值由 data-col 定 */
expect(/data-options="[^"]*"[^>]*data-slot=/.test(html)).toBe(false);
/* 块①(sub_eval.part 行模版,只有一份行模版,被 data-rows 迭代)绑 chip_type_id 下拉 */
/* 档案卡正面绑 chip_type_id 下拉 —— 只选不建(2026-08-31 用户定),type_id 的写入下拉已随抽屉退役 */
expect(html).toContain('data-col="chip_type_id" required data-options="parts.chip_type"');
/*
* 块②b(type_param)绑 type_id 下拉的地方**该出现两次**——已有行的行模版(data-rows
* 迭代)一份、种子行(字面量静态行,不受行数所限)一份;只数子串存在会被「只剩其中一份」
* 这种退化蒙混过去(已用变异验证逮到:只把动态行模版那份换回裸文本输入,这条子串断言
* 仍然为真,因为种子行那份还在——按出现次数数才钉得住两处都在)。
*/
const typeIdSelectCount = (html.match(/data-col="type_id" required data-options="parts\.chip_type"/g) ?? []).length;
expect(typeIdSelectCount).toBe(2);
expect(html).not.toContain('data-col="type_id"');
});
/*
@@ -667,18 +659,14 @@ describe('件① 评估输入补全', () => {
* (查证见 Task 7b 报告)。补一条**不受 `data-rows` 管辖的静态种子行**——两张表各一条,
* 字面量手写 `data-col` 输入格,不经 `cell()`/`data-slot`,行数再少也在。
*/
it('**零行也加得出第一行**:两张表各带一条静态种子行(class="jc-row-static"),不靠 data-rows 生成', () => {
const staticRows = html.match(/<tr class="jc-row-static">/g) ?? [];
expect(staticRows).toHaveLength(2);
/* 种子行里的输入格是手写的,不经 twinInputs —— 直接断言 data-col 存在即可 */
expect(html).toContain('<input class="jc-cell" data-col="type_name" placeholder="新类型名称">');
expect(html).toContain('<input class="jc-cell" data-col="description"');
expect(html).toContain('<input class="jc-cell" data-col="param_name" placeholder="参数名">');
expect(html).toContain('<input class="jc-cell" data-col="param_desc"');
expect(html).toContain('<select class="jc-cell" data-col="constraint_type">');
expect(html).toContain('<input class="jc-cell" data-col="param_unit"');
/* 种子行只在编辑态露面 —— 读的时候不该看见一行空壳子(界面不撒谎) */
expect(html).toContain('table:not(.on) tr.jc-row-static { display: none; }');
/*
* SKIP(2026-08-31 用户定「把添加芯片类型的功能从这个活动中移除,只能选择已有类型」):
* 新建类型抽屉连同两张编辑表(静态种子行 / type_id 禁用 data-col 的 Critical-1 修法 /
* thead 后置回归网 / data-edit 双写)整体退役 —— 这些钉的是已移除的创建路径,不是被放松的
* 断言。平台侧机制钉测(view-render-addrow-thead / view-table-edit)不随本活动退役。
*/
it.skip('**零行也加得出第一行**:两张表各带一条静态种子行(class="jc-row-static"),不靠 data-rows 生成', () => {
expect(true).toBe(true);
});
/*
@@ -696,17 +684,14 @@ describe('件① 评估输入补全', () => {
* 钉住修法:`type_id` 必须挂 `data-col`,且必须是禁用的 —— 人看得见「由系统生成」
* 这句占位,但怎么点都改不了它的值。
*/
it('**新建类型的种子行:type_id 挂 data-col、但禁用恒空**(2026-08-30 修 Critical 1 —— 漏挂会让 resolveRowKey 的"全表一致"把新建的类型误判成既有那一行,静默覆盖它)', () => {
expect(html).toContain('由系统生成');
// 「由系统生成」只出现在 chip_type 那条种子行里 —— 精确抓出那一行
const chipTypeStaticRow = /<tr class="jc-row-static">(?:(?!<\/tr>)[\s\S])*?由系统生成(?:(?!<\/tr>)[\s\S])*?<\/tr>/.exec(html)?.[0] ?? '';
expect(chipTypeStaticRow).not.toBe('');
/* 必须是一个真的 <input data-col>(collect() 只认 input/select/textarea[data-col]),
不能是纯文本 <span> —— 否则这一格永远不会出现在回传的 cells 里,barred 的保护无从谈起 */
expect(chipTypeStaticRow).toMatch(/<input[^>]*data-col="type_id"[^>]*disabled/);
/* disabled 意味着人怎么点都改不了它的值 —— 保存时这一格恒是空串,才谈得上「barred」 */
expect(chipTypeStaticRow).toContain('data-col="type_name"');
expect(chipTypeStaticRow).toContain('data-col="description"');
/*
* SKIP(2026-08-31 用户定「把添加芯片类型的功能从这个活动中移除,只能选择已有类型」):
* 新建类型抽屉连同两张编辑表(静态种子行 / type_id 禁用 data-col 的 Critical-1 修法 /
* thead 后置回归网 / data-edit 双写)整体退役 —— 这些钉的是已移除的创建路径,不是被放松的
* 断言。平台侧机制钉测(view-render-addrow-thead / view-table-edit)不随本活动退役。
*/
it.skip('**新建类型的种子行:type_id 挂 data-col、但禁用恒空**(2026-08-30 修 Critical 1 —— 漏挂会让 resolveRowKey 的"全表一致"把新建的类型误判成既有那一行,静默覆盖它)', () => {
expect(true).toBe(true);
});
it('**主键列必挂 `data-col`** —— 它不是编辑面,是行的钥匙(漏了会把人改的行错配到别行上)', () => {
@@ -714,10 +699,28 @@ describe('件① 评估输入补全', () => {
expect(html).toContain('data-col="param_name"');
});
it('可改的三张表 + 档案卡各有一道编辑闸', () => {
for (const address of ['parts.profile', 'parts.chip_type', 'parts.type_param', 'parts.parameter']) {
expect(html).toContain(`data-edit="${address}"`);
/*
* 2026-08-31 用户定:两行编辑闸退役,换成卡右上角的笔↔软盘 —— 笔开当前这一面的编辑
* (正面=主数据 / 背面=参数值),软盘保存,叉放弃。按钮就是平台认的 data-edit-open /
* data-edit-save / data-edit-cancel 触发器,快照与取消机制原样。
*/
it('编辑收进卡角:笔(open)/软盘(save)/叉(cancel)正反两面各一套;地址只剩 profile 与 parameter', () => {
for (const address of ['parts.profile', 'parts.parameter']) {
expect(html).toContain(`data-edit-open="${address}"`);
expect(html).toContain(`data-edit-save="${address}"`);
expect(html).toContain(`data-edit-cancel="${address}"`);
}
/* 老的闸行不再出现在正文里(样式表里的基座规则给 ② 用,不算) */
expect(html.slice(html.indexOf('</style>'))).not.toContain('jc-editgate');
/* 笔↔软盘的按面显隐由样式表判 */
expect(html).toContain('.dossiers.on .disk-f{display:grid}');
expect(html).toContain(".dossier:has(.pback.on) .disk-b{display:grid}");
/* 参数瓷贴的输入孪生件带占位提示(渲染器认 data-ph) */
expect(html).toContain('data-ph="规格书上的值 · 查不到留空"');
expect(html).toContain('data-ph="补充说明 · 选填"');
/* 题头回浅蓝渐变(2026-08-31 用户:「标题背景还是改成之前的那种蓝色渐变」) */
expect(html).toContain('class="card hd-light"');
expect(html).toContain('linear-gradient(120deg,#f0f5ff 0%,#fff 55%,#eef4ff 100%)');
});
/*
@@ -727,11 +730,14 @@ describe('件① 评估输入补全', () => {
* 内层 `data-rows` 那个 `<tbody>` 一处(给 `renderRows` 的 `editable` 检测看,
* 它只认与 `data-rows` 同一个标签自带的属性,不看祖先)。
*/
it('新建类型 / 新建字段两张表的 data-edit 各出现两次(外层 table + 内层 data-rows tbody)', () => {
for (const address of ['parts.chip_type', 'parts.type_param']) {
const count = (html.match(new RegExp(`data-edit="${address}"`, 'g')) ?? []).length;
expect(count, address).toBe(2);
}
/*
* SKIP(2026-08-31 用户定「把添加芯片类型的功能从这个活动中移除,只能选择已有类型」):
* 新建类型抽屉连同两张编辑表(静态种子行 / type_id 禁用 data-col 的 Critical-1 修法 /
* thead 后置回归网 / data-edit 双写)整体退役 —— 这些钉的是已移除的创建路径,不是被放松的
* 断言。平台侧机制钉测(view-render-addrow-thead / view-table-edit)不随本活动退役。
*/
it.skip('新建类型 / 新建字段两张表的 data-edit 各出现两次(外层 table + 内层 data-rows tbody)', () => {
expect(true).toBe(true);
});
/*
@@ -758,20 +764,14 @@ describe('件① 评估输入补全', () => {
* 谁若把 `<thead>` 挪回两个 `<tbody>` 之前(哪怕只改一张表),这条测试立刻标红,
* 提醒去核对 `view-render-addrow-thead.test.ts` 是否也需要同步。
*/
it('**回归安全网**:两张新表的 <thead> 都排在两个 <tbody> 之后(不是最前面)—— 与 view-render-addrow-thead.test.ts 的假设锁在一起', () => {
for (const address of ['parts.chip_type', 'parts.type_param']) {
const tableStart = html.indexOf(`<table data-edit="${address}">`);
expect(tableStart, address).toBeGreaterThanOrEqual(0);
const tableEnd = html.indexOf('</table>', tableStart);
const tableHtml = html.slice(tableStart, tableEnd);
const theadAt = tableHtml.indexOf('<thead>');
const tbodyPositions = [...tableHtml.matchAll(/<tbody/g)].map((m) => m.index ?? -1);
expect(theadAt, `${address}: 找不到 <thead>`).toBeGreaterThanOrEqual(0);
expect(tbodyPositions.length, `${address}: 应有两个 <tbody>`).toBe(2);
// <thead> 必须排在两个 <tbody> 的开标签**之后**(源码序;视觉呈现由 CSS 的
// table-header-group 保证仍在最前,与源码序无关)
for (const pos of tbodyPositions) expect(theadAt, address).toBeGreaterThan(pos);
}
/*
* SKIP(2026-08-31 用户定「把添加芯片类型的功能从这个活动中移除,只能选择已有类型」):
* 新建类型抽屉连同两张编辑表(静态种子行 / type_id 禁用 data-col 的 Critical-1 修法 /
* thead 后置回归网 / data-edit 双写)整体退役 —— 这些钉的是已移除的创建路径,不是被放松的
* 断言。平台侧机制钉测(view-render-addrow-thead / view-table-edit)不随本活动退役。
*/
it.skip('**回归安全网**:两张新表的 <thead> 都排在两个 <tbody> 之后(不是最前面)—— 与 view-render-addrow-thead.test.ts 的假设锁在一起', () => {
expect(true).toBe(true);
});
/*
@@ -799,14 +799,14 @@ describe('件① 评估输入补全', () => {
* 只读态与编辑态下「#」列逐行都是空白。钉死 `.jc-tbl-wrap` 现在也接了这五条规则,
* 且这张件确实只用 `.jc-tbl-wrap`(没有表在用没打这条补丁就会数不出来的 `.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 第三轮:基准卡瓷贴化、参数值上卡背 —— 主画面零表格)
expect(html).not.toContain('class="tbl-wrap"');
const jcTblWrapCount = html.match(/class="jc-tbl-wrap"/g)?.length ?? 0;
expect(jcTblWrapCount).toBe(2);
/*
* SKIP(2026-08-31 用户定「把添加芯片类型的功能从这个活动中移除,只能选择已有类型」):
* 新建类型抽屉连同两张编辑表(静态种子行 / type_id 禁用 data-col 的 Critical-1 修法 /
* thead 后置回归网 / data-edit 双写)整体退役 —— 这些钉的是已移除的创建路径,不是被放松的
* 断言。平台侧机制钉测(view-render-addrow-thead / view-table-edit)不随本活动退役。
*/
it.skip('抽屉里两张表的序号列真的会数:.jc-tbl-wrap 也接了 CSS 计数器(不再只认 .tbl-wrap)', () => {
expect(true).toBe(true);
});
});
@@ -1068,10 +1068,9 @@ describe.skip('徽标别名与值域相关的坑(2026-08-27 实测账,回归网)
*/
describe('可编辑件的主键与必填列(2026-08-27 复审:主键丢钥匙 / 必填填不了,两条 Critical 回归;Task 7b 补 parts.chip_type)', () => {
/** input-supplement 一屏四张真能编辑的表与它们各自绑定的台账对象(Task 7b 加入 parts.chip_type) */
/* 2026-08-31「只选不建」:chip_type / type_param 不再是可编辑面,从名单撤下 */
const EDITABLE: ReadonlyArray<{ view: string; domain: string; object: string }> = [
{ view: 'input-supplement', domain: 'parts', object: 'profile' },
{ view: 'input-supplement', domain: 'parts', object: 'chip_type' },
{ view: 'input-supplement', domain: 'parts', object: 'type_param' },
{ view: 'input-supplement', domain: 'parts', object: 'parameter' },
];