Archived
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:
@@ -450,7 +450,7 @@ describe('报告模版:编辑面与只读面各就各位', () => {
|
|||||||
|
|
||||||
it('每个可编辑格子的 data-col 都在对应台账对象的声明里(input-supplement 一屏四张表,四张台账对象的声明并起来看;difference-review 一张表)', () => {
|
it('每个可编辑格子的 data-col 都在对应台账对象的声明里(input-supplement 一屏四张表,四张台账对象的声明并起来看;difference-review 一张表)', () => {
|
||||||
const cases: Array<[string, Array<[string, string]>]> = [
|
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 */
|
/* 件② 2026-08-31 起多一张:收口四格可就地改,写向 substitution.relation */
|
||||||
['difference-review', [['substitution', 'difference_item'], ['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');
|
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:sub_eval.part"');
|
||||||
expect(html).toContain('data-rows="mine:parts.profile"');
|
expect(html).toContain('data-rows="mine:parts.profile"');
|
||||||
expect(html).toContain('data-rows="mine:parts.parameter"');
|
expect(html).toContain('data-rows="mine:parts.parameter"');
|
||||||
expect(html).toContain('data-rows="mine:parts.chip_type"');
|
expect(html).not.toContain('data-rows="mine:parts.chip_type"');
|
||||||
expect(html).toContain('data-rows="mine:parts.type_param"');
|
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-col');
|
||||||
expect(basis).not.toContain('data-edit');
|
expect(basis).not.toContain('data-edit');
|
||||||
/* 基准卡不再有表格:类型 + 字段全是瓷贴 */
|
/* 基准卡不再有表格:类型 + 字段全是瓷贴;说明藏进悬浮气泡(降噪,2026-08-31) */
|
||||||
expect(basis).not.toContain('<table');
|
expect(basis).not.toContain('<table');
|
||||||
expect(basis).toContain('class="ftiles"');
|
expect(basis).toContain('class="ftiles"');
|
||||||
/* 抽屉存在,且两张编辑表都住在里面(说明折叠用的是 lab lab-brief,不会被认成这一个) */
|
expect(html).toContain('.ftile:hover .ft-desc');
|
||||||
const labAt = html.indexOf('<details class="lab">');
|
/* 新建类型抽屉已随「只选不建」退役(2026-08-31 用户定)—— 只剩说明折叠那一只 details */
|
||||||
const labEnd = html.indexOf('</details>', labAt);
|
expect(html).not.toContain('<details class="lab">');
|
||||||
expect(labAt).toBeGreaterThanOrEqual(0);
|
expect(html).not.toContain('data-edit="parts.chip_type"');
|
||||||
expect(html.indexOf('<table data-edit="parts.chip_type">')).toBeGreaterThan(labAt);
|
expect(html).not.toContain('data-edit="parts.type_param"');
|
||||||
expect(html.indexOf('<table data-edit="parts.type_param">')).toBeGreaterThan(labAt);
|
|
||||||
expect(labEnd).toBeGreaterThan(html.indexOf('<table data-edit="parts.type_param">'));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -631,34 +629,28 @@ describe('件① 评估输入补全', () => {
|
|||||||
expect(html).toContain('点击翻面');
|
expect(html).toContain('点击翻面');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('主画面无表格:说明折叠之前的正文一张 <table 都没有 —— 表格只活在抽屉里(表单要加行,加行机制认 tr)', () => {
|
it('整件零表格(2026-08-31 用户:「这个界面都不再需要表格来展示数据了」;抽屉连表带功能一并退役)', () => {
|
||||||
const beforeDrawer = html.slice(0, html.indexOf('<details class="lab">'));
|
expect(html).not.toContain('<table');
|
||||||
expect(beforeDrawer).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.req .jc-vcell:empty::after{content:"必填"');
|
||||||
expect(html).toContain('.fcell.sug .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 .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-options="parts.chip_type"');
|
||||||
expect(html).toContain('data-option-value="type_id"');
|
expect(html).toContain('data-option-value="type_id"');
|
||||||
expect(html).toContain('data-option-label="type_name"');
|
expect(html).toContain('data-option-label="type_name"');
|
||||||
/* data-options 与 data-slot 不能共存(渲染时 data-slot 会被剥掉)—— 当下值由 data-col 定 */
|
/* data-options 与 data-slot 不能共存(渲染时 data-slot 会被剥掉)—— 当下值由 data-col 定 */
|
||||||
expect(/data-options="[^"]*"[^>]*data-slot=/.test(html)).toBe(false);
|
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"');
|
expect(html).toContain('data-col="chip_type_id" required data-options="parts.chip_type"');
|
||||||
/*
|
expect(html).not.toContain('data-col="type_id"');
|
||||||
* 块②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);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -667,18 +659,14 @@ describe('件① 评估输入补全', () => {
|
|||||||
* (查证见 Task 7b 报告)。补一条**不受 `data-rows` 管辖的静态种子行**——两张表各一条,
|
* (查证见 Task 7b 报告)。补一条**不受 `data-rows` 管辖的静态种子行**——两张表各一条,
|
||||||
* 字面量手写 `data-col` 输入格,不经 `cell()`/`data-slot`,行数再少也在。
|
* 字面量手写 `data-col` 输入格,不经 `cell()`/`data-slot`,行数再少也在。
|
||||||
*/
|
*/
|
||||||
it('**零行也加得出第一行**:两张表各带一条静态种子行(class="jc-row-static"),不靠 data-rows 生成', () => {
|
/*
|
||||||
const staticRows = html.match(/<tr class="jc-row-static">/g) ?? [];
|
* SKIP(2026-08-31 用户定「把添加芯片类型的功能从这个活动中移除,只能选择已有类型」):
|
||||||
expect(staticRows).toHaveLength(2);
|
* 新建类型抽屉连同两张编辑表(静态种子行 / type_id 禁用 data-col 的 Critical-1 修法 /
|
||||||
/* 种子行里的输入格是手写的,不经 twinInputs —— 直接断言 data-col 存在即可 */
|
* thead 后置回归网 / data-edit 双写)整体退役 —— 这些钉的是已移除的创建路径,不是被放松的
|
||||||
expect(html).toContain('<input class="jc-cell" data-col="type_name" placeholder="新类型名称">');
|
* 断言。平台侧机制钉测(view-render-addrow-thead / view-table-edit)不随本活动退役。
|
||||||
expect(html).toContain('<input class="jc-cell" data-col="description"');
|
*/
|
||||||
expect(html).toContain('<input class="jc-cell" data-col="param_name" placeholder="参数名">');
|
it.skip('**零行也加得出第一行**:两张表各带一条静态种子行(class="jc-row-static"),不靠 data-rows 生成', () => {
|
||||||
expect(html).toContain('<input class="jc-cell" data-col="param_desc"');
|
expect(true).toBe(true);
|
||||||
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; }');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -696,17 +684,14 @@ describe('件① 评估输入补全', () => {
|
|||||||
* 钉住修法:`type_id` 必须挂 `data-col`,且必须是禁用的 —— 人看得见「由系统生成」
|
* 钉住修法:`type_id` 必须挂 `data-col`,且必须是禁用的 —— 人看得见「由系统生成」
|
||||||
* 这句占位,但怎么点都改不了它的值。
|
* 这句占位,但怎么点都改不了它的值。
|
||||||
*/
|
*/
|
||||||
it('**新建类型的种子行:type_id 挂 data-col、但禁用恒空**(2026-08-30 修 Critical 1 —— 漏挂会让 resolveRowKey 的"全表一致"把新建的类型误判成既有那一行,静默覆盖它)', () => {
|
/*
|
||||||
expect(html).toContain('由系统生成');
|
* SKIP(2026-08-31 用户定「把添加芯片类型的功能从这个活动中移除,只能选择已有类型」):
|
||||||
// 「由系统生成」只出现在 chip_type 那条种子行里 —— 精确抓出那一行
|
* 新建类型抽屉连同两张编辑表(静态种子行 / type_id 禁用 data-col 的 Critical-1 修法 /
|
||||||
const chipTypeStaticRow = /<tr class="jc-row-static">(?:(?!<\/tr>)[\s\S])*?由系统生成(?:(?!<\/tr>)[\s\S])*?<\/tr>/.exec(html)?.[0] ?? '';
|
* thead 后置回归网 / data-edit 双写)整体退役 —— 这些钉的是已移除的创建路径,不是被放松的
|
||||||
expect(chipTypeStaticRow).not.toBe('');
|
* 断言。平台侧机制钉测(view-render-addrow-thead / view-table-edit)不随本活动退役。
|
||||||
/* 必须是一个真的 <input data-col>(collect() 只认 input/select/textarea[data-col]),
|
*/
|
||||||
不能是纯文本 <span> —— 否则这一格永远不会出现在回传的 cells 里,barred 的保护无从谈起 */
|
it.skip('**新建类型的种子行:type_id 挂 data-col、但禁用恒空**(2026-08-30 修 Critical 1 —— 漏挂会让 resolveRowKey 的"全表一致"把新建的类型误判成既有那一行,静默覆盖它)', () => {
|
||||||
expect(chipTypeStaticRow).toMatch(/<input[^>]*data-col="type_id"[^>]*disabled/);
|
expect(true).toBe(true);
|
||||||
/* disabled 意味着人怎么点都改不了它的值 —— 保存时这一格恒是空串,才谈得上「barred」 */
|
|
||||||
expect(chipTypeStaticRow).toContain('data-col="type_name"');
|
|
||||||
expect(chipTypeStaticRow).toContain('data-col="description"');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('**主键列必挂 `data-col`** —— 它不是编辑面,是行的钥匙(漏了会把人改的行错配到别行上)', () => {
|
it('**主键列必挂 `data-col`** —— 它不是编辑面,是行的钥匙(漏了会把人改的行错配到别行上)', () => {
|
||||||
@@ -714,10 +699,28 @@ describe('件① 评估输入补全', () => {
|
|||||||
expect(html).toContain('data-col="param_name"');
|
expect(html).toContain('data-col="param_name"');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('可改的三张表 + 档案卡各有一道编辑闸', () => {
|
/*
|
||||||
for (const address of ['parts.profile', 'parts.chip_type', 'parts.type_param', 'parts.parameter']) {
|
* 2026-08-31 用户定:两行编辑闸退役,换成卡右上角的笔↔软盘 —— 笔开当前这一面的编辑
|
||||||
expect(html).toContain(`data-edit="${address}"`);
|
* (正面=主数据 / 背面=参数值),软盘保存,叉放弃。按钮就是平台认的 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` 那个 `<tbody>` 一处(给 `renderRows` 的 `editable` 检测看,
|
||||||
* 它只认与 `data-rows` 同一个标签自带的属性,不看祖先)。
|
* 它只认与 `data-rows` 同一个标签自带的属性,不看祖先)。
|
||||||
*/
|
*/
|
||||||
it('新建类型 / 新建字段两张表的 data-edit 各出现两次(外层 table + 内层 data-rows tbody)', () => {
|
/*
|
||||||
for (const address of ['parts.chip_type', 'parts.type_param']) {
|
* SKIP(2026-08-31 用户定「把添加芯片类型的功能从这个活动中移除,只能选择已有类型」):
|
||||||
const count = (html.match(new RegExp(`data-edit="${address}"`, 'g')) ?? []).length;
|
* 新建类型抽屉连同两张编辑表(静态种子行 / type_id 禁用 data-col 的 Critical-1 修法 /
|
||||||
expect(count, address).toBe(2);
|
* 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>` 之前(哪怕只改一张表),这条测试立刻标红,
|
* 谁若把 `<thead>` 挪回两个 `<tbody>` 之前(哪怕只改一张表),这条测试立刻标红,
|
||||||
* 提醒去核对 `view-render-addrow-thead.test.ts` 是否也需要同步。
|
* 提醒去核对 `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']) {
|
* SKIP(2026-08-31 用户定「把添加芯片类型的功能从这个活动中移除,只能选择已有类型」):
|
||||||
const tableStart = html.indexOf(`<table data-edit="${address}">`);
|
* 新建类型抽屉连同两张编辑表(静态种子行 / type_id 禁用 data-col 的 Critical-1 修法 /
|
||||||
expect(tableStart, address).toBeGreaterThanOrEqual(0);
|
* thead 后置回归网 / data-edit 双写)整体退役 —— 这些钉的是已移除的创建路径,不是被放松的
|
||||||
const tableEnd = html.indexOf('</table>', tableStart);
|
* 断言。平台侧机制钉测(view-render-addrow-thead / view-table-edit)不随本活动退役。
|
||||||
const tableHtml = html.slice(tableStart, tableEnd);
|
*/
|
||||||
const theadAt = tableHtml.indexOf('<thead>');
|
it.skip('**回归安全网**:两张新表的 <thead> 都排在两个 <tbody> 之后(不是最前面)—— 与 view-render-addrow-thead.test.ts 的假设锁在一起', () => {
|
||||||
const tbodyPositions = [...tableHtml.matchAll(/<tbody/g)].map((m) => m.index ?? -1);
|
expect(true).toBe(true);
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -799,14 +799,14 @@ describe('件① 评估输入补全', () => {
|
|||||||
* 只读态与编辑态下「#」列逐行都是空白。钉死 `.jc-tbl-wrap` 现在也接了这五条规则,
|
* 只读态与编辑态下「#」列逐行都是空白。钉死 `.jc-tbl-wrap` 现在也接了这五条规则,
|
||||||
* 且这张件确实只用 `.jc-tbl-wrap`(没有表在用没打这条补丁就会数不出来的 `.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*\}/);
|
* SKIP(2026-08-31 用户定「把添加芯片类型的功能从这个活动中移除,只能选择已有类型」):
|
||||||
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*\}/);
|
* 新建类型抽屉连同两张编辑表(静态种子行 / type_id 禁用 data-col 的 Critical-1 修法 /
|
||||||
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*\}/);
|
* thead 后置回归网 / data-edit 双写)整体退役 —— 这些钉的是已移除的创建路径,不是被放松的
|
||||||
// 只剩抽屉里两张(2026-08-31 第三轮:基准卡瓷贴化、参数值上卡背 —— 主画面零表格)
|
* 断言。平台侧机制钉测(view-render-addrow-thead / view-table-edit)不随本活动退役。
|
||||||
expect(html).not.toContain('class="tbl-wrap"');
|
*/
|
||||||
const jcTblWrapCount = html.match(/class="jc-tbl-wrap"/g)?.length ?? 0;
|
it.skip('抽屉里两张表的序号列真的会数:.jc-tbl-wrap 也接了 CSS 计数器(不再只认 .tbl-wrap)', () => {
|
||||||
expect(jcTblWrapCount).toBe(2);
|
expect(true).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1068,10 +1068,9 @@ describe.skip('徽标别名与值域相关的坑(2026-08-27 实测账,回归网)
|
|||||||
*/
|
*/
|
||||||
describe('可编辑件的主键与必填列(2026-08-27 复审:主键丢钥匙 / 必填填不了,两条 Critical 回归;Task 7b 补 parts.chip_type)', () => {
|
describe('可编辑件的主键与必填列(2026-08-27 复审:主键丢钥匙 / 必填填不了,两条 Critical 回归;Task 7b 补 parts.chip_type)', () => {
|
||||||
/** input-supplement 一屏四张真能编辑的表与它们各自绑定的台账对象(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 }> = [
|
const EDITABLE: ReadonlyArray<{ view: string; domain: string; object: string }> = [
|
||||||
{ view: 'input-supplement', domain: 'parts', object: 'profile' },
|
{ 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' },
|
{ view: 'input-supplement', domain: 'parts', object: 'parameter' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -146,10 +146,10 @@ describe('S1 配方', () => {
|
|||||||
* 变成空字符串)见 `apps/api/src/modules/loop-engine/__tests__/w05-gate-reason-order.test.ts`
|
* 变成空字符串)见 `apps/api/src/modules/loop-engine/__tests__/w05-gate-reason-order.test.ts`
|
||||||
* ——这份文件只做结构断言,不跑引擎(活动包不许 import apps/*)。
|
* ——这份文件只做结构断言,不跑引擎(活动包不许 import apps/*)。
|
||||||
*/
|
*/
|
||||||
it('八笔,次序是:两颗料 → 类型字典自投 → 字段清单自投 → 主数据骨架 → 参数骨架 → 案卷 → 卡 → 交代', () => {
|
it('六笔,次序是:两颗料 → 主数据骨架 → 参数骨架 → 案卷 → 卡 → 交代(类型字典/字段清单两笔自投影已随「只选不建」退役)', () => {
|
||||||
expect(kinds).toEqual(['project', 'project', 'project', 'project', 'project', 'project', 'card', 'say']);
|
expect(kinds).toEqual(['project', 'project', 'project', 'project', 'card', 'say']);
|
||||||
expect(acts.map((a) => (a.kind === 'project' ? a.into : undefined)).filter(Boolean)).toEqual([
|
expect(acts.map((a) => (a.kind === 'project' ? a.into : undefined)).filter(Boolean)).toEqual([
|
||||||
'sub_eval.part', 'parts.chip_type', 'parts.type_param', 'parts.profile', 'parts.parameter', 'sub_eval.case',
|
'sub_eval.part', 'parts.profile', 'parts.parameter', 'sub_eval.case',
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -165,21 +165,13 @@ describe('S1 配方', () => {
|
|||||||
* 跳过已落地的行(loop-engine/port.ts),prune 回来:没落地的陈旧选型照撤,
|
* 跳过已落地的行(loop-engine/port.ts),prune 回来:没落地的陈旧选型照撤,
|
||||||
* 已落地的原地留下 —— 案例 2 不再炸,用户那条路清得干净。
|
* 已落地的原地留下 —— 案例 2 不再炸,用户那条路清得干净。
|
||||||
*/
|
*/
|
||||||
it('②③ 两笔自投影都读 sub_eval.part 的 chip_type_id 去查字典/字段清单,原样抄回自己(带 prune,已落地由基座跳过)', () => {
|
/*
|
||||||
const chipType = acts[1];
|
* SKIP(2026-08-31 用户定「只选不建」):②③ 两笔自投影连同 gateLedgers 上那两张表
|
||||||
expect(chipType?.kind === 'project' && chipType.into).toBe('parts.chip_type');
|
* 一并退役 —— 类型只许从字典里选,选中的行本来就在正式表里,镜像与落地都失去了对象。
|
||||||
expect(chipType?.kind === 'project' && chipType.prune).toBe(true);
|
* 这条钉的机制(prune + skipLanded)仍活在 auto-runner 测试里,不随本活动退役。
|
||||||
expect(chipType?.kind === 'project' && chipType.from).toMatchObject({
|
*/
|
||||||
each: 'mine:sub_eval.part',
|
it.skip('②③ 两笔自投影都读 sub_eval.part 的 chip_type_id 去查字典/字段清单,原样抄回自己(带 prune,已落地由基座跳过)', () => {
|
||||||
then: { each: 'parts.chip_type', where: { type_id: { outer: 'chip_type_id' } } },
|
expect(true).toBe(true);
|
||||||
});
|
|
||||||
const typeParam = acts[2];
|
|
||||||
expect(typeParam?.kind === 'project' && typeParam.into).toBe('parts.type_param');
|
|
||||||
expect(typeParam?.kind === 'project' && typeParam.prune).toBe(true);
|
|
||||||
expect(typeParam?.kind === 'project' && typeParam.from).toMatchObject({
|
|
||||||
each: 'mine:sub_eval.part',
|
|
||||||
then: { each: 'parts.type_param', where: { type_id: { outer: 'chip_type_id' } } },
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('案号由 let 铸,只从本单开案时刻取材', () => {
|
it('案号由 let 铸,只从本单开案时刻取材', () => {
|
||||||
@@ -203,7 +195,7 @@ describe('S1 配方', () => {
|
|||||||
* 「加一行」),必须换成说实情、给真出路的话:没法比出差异,终止重开。
|
* 「加一行」),必须换成说实情、给真出路的话:没法比出差异,终止重开。
|
||||||
*/
|
*/
|
||||||
it('gate_reason 第一档:两参必填之后只剩"填成同一个号"这条残余路径,文案不再指向不存在的"补两格"', () => {
|
it('gate_reason 第一档:两参必填之后只剩"填成同一个号"这条残余路径,文案不再指向不存在的"补两格"', () => {
|
||||||
const kase = acts[5];
|
const kase = acts[3];
|
||||||
const fields = kase?.kind === 'project' ? kase.fields : undefined;
|
const fields = kase?.kind === 'project' ? kase.fields : undefined;
|
||||||
const gateReason = fields?.['gate_reason'] as { when?: string; then?: string } | undefined;
|
const gateReason = fields?.['gate_reason'] as { when?: string; then?: string } | undefined;
|
||||||
expect(gateReason?.when).toBe('sub_eval_part_rows < 2');
|
expect(gateReason?.when).toBe('sub_eval_part_rows < 2');
|
||||||
@@ -213,7 +205,7 @@ describe('S1 配方', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('参数骨架:两颗料都铺、值回填现值 —— 「在库但当年参数没录全」也要被拦住', () => {
|
it('参数骨架:两颗料都铺、值回填现值 —— 「在库但当年参数没录全」也要被拦住', () => {
|
||||||
const params = acts[4];
|
const params = acts[2];
|
||||||
/* 'live':人加不了行的表,quiet 拍也撤 —— 改类型后旧参数行跟手消失(2026-08-31 用户点名「只会变多不会变少」) */
|
/* 'live':人加不了行的表,quiet 拍也撤 —— 改类型后旧参数行跟手消失(2026-08-31 用户点名「只会变多不会变少」) */
|
||||||
expect(params?.kind === 'project' && params.prune).toBe('live');
|
expect(params?.kind === 'project' && params.prune).toBe('live');
|
||||||
const from = params?.kind === 'project' ? params.from : undefined;
|
const from = params?.kind === 'project' ? params.from : undefined;
|
||||||
@@ -224,7 +216,7 @@ describe('S1 配方', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('**发卡的条件全是基座读数** —— 不许拿 `gate_reason != ""` 当条件', () => {
|
it('**发卡的条件全是基座读数** —— 不许拿 `gate_reason != ""` 当条件', () => {
|
||||||
const card = acts[6];
|
const card = acts[4];
|
||||||
const when = card?.kind === 'card' ? (card.when ?? '') : '';
|
const when = card?.kind === 'card' ? (card.when ?? '') : '';
|
||||||
/*
|
/*
|
||||||
* S-FEEL 里**未绑定的名字 `!= ""` 求值为 true**,而 `gate_reason` 为空串时那一列压根不写、
|
* S-FEEL 里**未绑定的名字 `!= ""` 求值为 true**,而 `gate_reason` 为空串时那一列压根不写、
|
||||||
@@ -245,16 +237,16 @@ describe('S1 配方', () => {
|
|||||||
* (2026-08-30 真跑 e2e 撞见)。现在四张表都在闸上、也都会一起落地。
|
* (2026-08-30 真跑 e2e 撞见)。现在四张表都在闸上、也都会一起落地。
|
||||||
*/
|
*/
|
||||||
it('卡:数据工单、四张表都在闸上(含 parts.chip_type/parts.type_param,Ruling H35)、且有 basis(基座拒发 basis 为空的工单)', () => {
|
it('卡:数据工单、四张表都在闸上(含 parts.chip_type/parts.type_param,Ruling H35)、且有 basis(基座拒发 basis 为空的工单)', () => {
|
||||||
const card = acts[6];
|
const card = acts[4];
|
||||||
expect(card?.kind === 'card' && card.as).toBe('execution');
|
expect(card?.kind === 'card' && card.as).toBe('execution');
|
||||||
expect(card?.kind === 'card' && card.variant).toBe('data');
|
expect(card?.kind === 'card' && card.variant).toBe('data');
|
||||||
expect(card?.kind === 'card' && card.gateLedgers).toEqual(['parts.chip_type', 'parts.type_param', 'parts.profile', 'parts.parameter']);
|
expect(card?.kind === 'card' && card.gateLedgers).toEqual(['parts.profile', 'parts.parameter']);
|
||||||
expect((card?.kind === 'card' ? card.basis ?? [] : []).length).toBeGreaterThan(0);
|
expect((card?.kind === 'card' ? card.basis ?? [] : []).length).toBeGreaterThan(0);
|
||||||
expect((card?.kind === 'card' ? card.guide ?? [] : []).length).toBeGreaterThanOrEqual(3);
|
expect((card?.kind === 'card' ? card.guide ?? [] : []).length).toBeGreaterThanOrEqual(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('发卡条件与 doneWhen 是同一件事的正反面', () => {
|
it('发卡条件与 doneWhen 是同一件事的正反面', () => {
|
||||||
const when = acts[6]?.kind === 'card' ? (acts[6].when ?? '') : '';
|
const when = acts[4]?.kind === 'card' ? (acts[4].when ?? '') : '';
|
||||||
for (const reading of [
|
for (const reading of [
|
||||||
'sub_eval_part_exists_status_found', 'sub_eval_part_missing',
|
'sub_eval_part_exists_status_found', 'sub_eval_part_missing',
|
||||||
'sub_eval_part_distinct_chip_type_id', 'parts_profile_missing', 'parts_parameter_missing',
|
'sub_eval_part_distinct_chip_type_id', 'parts_profile_missing', 'parts_parameter_missing',
|
||||||
|
|||||||
@@ -339,7 +339,7 @@ export const ASSESSMENT_STEPS = [
|
|||||||
与 auto.do 第 ⑥ 笔 `card` 的 `gateLedgers` 同源(parts.chip_type / parts.type_param /
|
与 auto.do 第 ⑥ 笔 `card` 的 `gateLedgers` 同源(parts.chip_type / parts.type_param /
|
||||||
parts.profile / parts.parameter —— 四张表都在闸上,理由见那笔 `card` 定义处的注释)。 */
|
parts.profile / parts.parameter —— 四张表都在闸上,理由见那笔 `card` 定义处的注释)。 */
|
||||||
view: 'input-supplement',
|
view: 'input-supplement',
|
||||||
writes: ['sub_eval.part', 'sub_eval.case', 'parts.chip_type', 'parts.type_param', 'parts.profile', 'parts.parameter'],
|
writes: ['sub_eval.part', 'sub_eval.case', 'parts.profile', 'parts.parameter'],
|
||||||
reads: ['originalInternalPn', 'candidateInternalPn', 'case:openedAt', 'parts.profile', 'parts.parameter', 'parts.chip_type', 'parts.type_param'],
|
reads: ['originalInternalPn', 'candidateInternalPn', 'case:openedAt', 'parts.profile', 'parts.parameter', 'parts.chip_type', 'parts.type_param'],
|
||||||
tools: [],
|
tools: [],
|
||||||
doneWhen:
|
doneWhen:
|
||||||
@@ -378,82 +378,12 @@ export const ASSESSMENT_STEPS = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
/*
|
/*
|
||||||
* ② **选中的那一行类型字典,原样投一遍**(2026-08-30,Ruling H35 —— 真跑 e2e 撞见的
|
* ~~② 类型字典自投影 / ③ 字段清单自投影~~(2026-08-31 用户定「本活动只选类型、
|
||||||
* 平台侧连带问题:「数据工单确认即入库」按 `gateLedgers` 整表并入正式表,而
|
* 不新增类型」后退役):这两笔当年(Ruling H35)是为「人新建的类型/字段要随数据
|
||||||
* `parts.chip_type` 从 Task 7 起就没列在 `gateLedgers` 里 —— 人一旦真的新建一个类型,
|
* 工单确认一起落地、且 mine: 不为空好让闸不误拦」而生 —— 类型只许从字典里选之后,
|
||||||
* `parts.profile`/`parts.type_param` 引用着它一起落地,**字典行本身**却只留在这一单
|
* 选中的类型本来就在正式表里,镜像与落地都失去了对象;`gateLedgers` 同批收窄到
|
||||||
* 草稿里、从没落地过。下一个任务读到同一颗料,查这个 `chip_type_id` 时字典里空空如也,
|
* `parts.profile` / `parts.parameter` 两张真被人编辑的表。历史见 git(2.9.0 及更早)。
|
||||||
* `refersTo` 当场拒——是一条悬空引用,不是「查无此值」那种输入错误)。
|
|
||||||
*
|
|
||||||
* **这一笔不看「新建还是已有」,统统投一遍**(不选型的选也一样、新建的选也一样):
|
|
||||||
* 读的是 `sub_eval.part` 每一行当下的 `chip_type_id`(空串时下面这层 `each` 天然查
|
|
||||||
* 不到、不产生任何行 —— 人还没选类型的那颗不会被提前铺出假行),按它去
|
|
||||||
* `parts.chip_type` 里找到那一行、原样把 `type_name`/`description` 抄回自己身上。
|
|
||||||
* 值没变,`draftWrite` 同 PK 覆盖静默跳过(幂等,不产生多余的写入痕迹)——
|
|
||||||
* 但**这一单的草稿层从此真的有这一行**,`mine:parts.chip_type` 不再天然为空。
|
|
||||||
*
|
|
||||||
* 这条把 `parts.chip_type` 重新放回下面 ⑥ 的 `gateLedgers` 铺平了路:闸不会再拿
|
|
||||||
* 「零行」的旧理由误拦(那个前提不成立了),而选中的类型(新建的也好、已有的也好)
|
|
||||||
* 会随「数据工单确认入库」一起落进正式表 —— 不再留悬空引用。
|
|
||||||
*/
|
*/
|
||||||
{
|
|
||||||
kind: 'project' as const,
|
|
||||||
into: 'parts.chip_type',
|
|
||||||
/*
|
|
||||||
* **带 `prune`,已落地的行由基座跳过**(2026-08-31 改回)。这一笔曾特意不带
|
|
||||||
* `prune`(2026-08-30 e2e 案例 2:类型行随「数据工单确认即入库」落地后,人改了
|
|
||||||
* 类型选择,`prune` 想撤已落地的旧行,`draftDrop` 硬拒、配方整单炸)—— 当时的
|
|
||||||
* 代价是**没落地的**陈旧行也跟着不清:人在件上把候选从 LDO 改回 DC-DC,LDO 的
|
|
||||||
* 字典行与字段行积在屏上,「下面还是显示两个类型」(2026-08-31 用户实测点名)。
|
|
||||||
* 病根在撤行机制一刀切,不在 prune 本身 —— 现在 `draftDrop` 对配方这一路带
|
|
||||||
* `skipLanded`(见 loop-engine/port.ts):已落地的行原地留下(它已是世界的一部分,
|
|
||||||
* 字典行本就该积累),**没落地的陈旧选型照撤** —— 案例 2 那条路不再炸,
|
|
||||||
* 用户点名的那条路清得干净。撤行只在非 quiet 拍跑(人正编辑时不动他的行,F3)。
|
|
||||||
*/
|
|
||||||
prune: true,
|
|
||||||
from: {
|
|
||||||
each: 'mine:sub_eval.part',
|
|
||||||
then: { each: 'parts.chip_type', where: { type_id: { outer: 'chip_type_id' } } },
|
|
||||||
},
|
|
||||||
fields: {
|
|
||||||
type_id: { col: 'type_id' },
|
|
||||||
type_name: { col: 'type_name' },
|
|
||||||
description: { col: 'description' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
/*
|
|
||||||
* ③ **选中类型的核心参数字段清单,同一套道理原样投一遍**(2026-08-30,与 ② 同一次
|
|
||||||
* 发现、同一个技法):`parts.type_param` 只有人在件上「新建类型时加字段」才会写它 ——
|
|
||||||
* 挑一个已有类型时,这张表天然一行都没有,而它此刻仍在 ⑥ 的 `gateLedgers` 上,
|
|
||||||
* 「零行 = 没干完」的闸对这条路一样是误判(2026-08-30 真跑 e2e 五案例实测坐实:
|
|
||||||
* 「归到已有类型」的候选缺库 / 类型不一致 / 原PN缺库 / 两颗都缺库四例,全部卡死在
|
|
||||||
* 同一条「芯片类型核心参数字段 这一单还一行都没有」)。
|
|
||||||
*
|
|
||||||
* 修法与 ② 完全同构:按当下选中的 `chip_type_id` 把那个类型的核心参数字段清单
|
|
||||||
* 整套原样抄回自己身上(值没变,幂等,不产生多余写入)——**新建的类型也走这条路**:
|
|
||||||
* 人在件上新增的字段行本就已经在 `mine:parts.type_param` 里,这一笔重新投一遍
|
|
||||||
* 原样命中、照样幂等跳过,不会覆盖人刚填的内容。
|
|
||||||
*
|
|
||||||
* **同样带 `prune`(已落地的由基座 `skipLanded` 跳过)**—— 与 ② 同一次改回、
|
|
||||||
* 同一个理由,见 ② 头上的大注释:改选类型后,旧类型**没落地**的字段行照撤,
|
|
||||||
* 已随数据工单确认落地的原地留下,H35 案例 2 的死锁不再触发。
|
|
||||||
*/
|
|
||||||
{
|
|
||||||
kind: 'project' as const,
|
|
||||||
into: 'parts.type_param',
|
|
||||||
prune: true,
|
|
||||||
from: {
|
|
||||||
each: 'mine:sub_eval.part',
|
|
||||||
then: { each: 'parts.type_param', where: { type_id: { outer: 'chip_type_id' } } },
|
|
||||||
},
|
|
||||||
fields: {
|
|
||||||
type_id: { col: 'type_id' },
|
|
||||||
param_name: { col: 'param_name' },
|
|
||||||
param_desc: { col: 'param_desc' },
|
|
||||||
constraint_type: { col: 'constraint_type' },
|
|
||||||
param_unit: { col: 'param_unit' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
/*
|
/*
|
||||||
* ④ 主数据骨架:**两颗料都铺、值回填现值**。
|
* ④ 主数据骨架:**两颗料都铺、值回填现值**。
|
||||||
* 在库那颗回填出它此刻的样子(人不必重填);要建那颗全空等人填。
|
* 在库那颗回填出它此刻的样子(人不必重填);要建那颗全空等人填。
|
||||||
@@ -566,7 +496,7 @@ export const ASSESSMENT_STEPS = [
|
|||||||
then: '有料号库里查不到 —— 在下面把它的主数据建起来。',
|
then: '有料号库里查不到 —— 在下面把它的主数据建起来。',
|
||||||
else: {
|
else: {
|
||||||
when: 'sub_eval_part_missing > 0',
|
when: 'sub_eval_part_missing > 0',
|
||||||
then: '有料号还没归芯片类型 —— 在下面给它选一个;没有合适的就新建一个,并把它的核心参数字段一并定下来。',
|
then: '有料号还没归芯片类型 —— 在档案卡上给它选一个;字典里没有合适的,先请数据管理员补进类型字典(本活动只选不建)。',
|
||||||
else: {
|
else: {
|
||||||
when: 'sub_eval_part_distinct_chip_type_id > 1',
|
when: 'sub_eval_part_distinct_chip_type_id > 1',
|
||||||
then: '两颗料不是同一个芯片类型,逐项比对无从谈起 —— 换一颗料,或者把归错的那颗类型改对。',
|
then: '两颗料不是同一个芯片类型,逐项比对无从谈起 —— 换一颗料,或者把归错的那颗类型改对。',
|
||||||
@@ -601,29 +531,18 @@ export const ASSESSMENT_STEPS = [
|
|||||||
],
|
],
|
||||||
guide: [
|
guide: [
|
||||||
'两颗料必须归到**同一个芯片类型**下 —— 要比的核心参数字段由类型定,类型不同就没有可比的字段。',
|
'两颗料必须归到**同一个芯片类型**下 —— 要比的核心参数字段由类型定,类型不同就没有可比的字段。',
|
||||||
'类型下拉里没有合适的,就新建一个,并把它的**核心参数字段**一并定下来:每个字段要有名字、一句说明、以及硬约束还是软约束。',
|
'类型**只从字典里选,本活动不新增类型**(2026-08-31 用户定)—— 字典里没有合适的,先请数据管理员补进字典,再回来选。',
|
||||||
'说明写「这个参数为什么要紧」——**它是后面判定差异时唯一的解释来源**,写成参数名的复述等于没写。',
|
'硬约束 = 不满足就不能用(烧芯片、超温域、装不上);软约束 = 影响表现但可权衡(功耗、噪声)—— 字段的硬软在字典上定死,件上「比对基准」卡可查。',
|
||||||
'硬约束 = 不满足就不能用(烧芯片、超温域、装不上);软约束 = 影响表现但可权衡(功耗、噪声)。',
|
|
||||||
'参数值填**原厂规格书上的值**,查不到就留空 —— 留空会被红带点名,但**编一个值比留空坏得多**。',
|
'参数值填**原厂规格书上的值**,查不到就留空 —— 留空会被红带点名,但**编一个值比留空坏得多**。',
|
||||||
],
|
],
|
||||||
standard: '红带消失、提交钮变绿:两颗料都在库、归了同一个类型、每个核心参数字段都有值。',
|
standard: '红带消失、提交钮变绿:两颗料都在库、归了同一个类型、每个核心参数字段都有值。',
|
||||||
/*
|
/*
|
||||||
* **`parts.chip_type` 与 `parts.type_param` 都在闸上**(2026-08-30 改,Ruling H35)——
|
* **闸只盯人真编辑的两张表**(2026-08-31 随「只选不建」收窄):从前 `parts.chip_type`/
|
||||||
* 从前 `parts.chip_type` 被永久排除在外,理由是「它没有编辑手段,`mine:` 天然为空,
|
* `parts.type_param` 也在名单上 —— 那是给「人新建的类型要随确认一起落地」铺的路
|
||||||
* 列了就永久卡死」;那条理由**从 ②③ 两笔自我投影加上之后就不再成立**——
|
* (Ruling H35,史见 git)。类型只许从字典里选之后,字典行本来就在正式表里,
|
||||||
* `mine:parts.chip_type`/`mine:parts.type_param` 现在恒不为空(只要 `sub_eval.part`
|
* 落地无对象、mine: 恒为空 —— 留在名单上反而会被「零行 = 没干完」的闸误拦死。
|
||||||
* 里已经选定了类型),闸不会再误判「一行都没有」。
|
|
||||||
*
|
|
||||||
* **既然要落地就必须在名单里,这不是碰运气**:「数据工单确认即入库」把 `gateLedgers`
|
|
||||||
* 列出的表整批并入正式表;`parts.chip_type` 若不在这份名单里,人新建的类型字典行
|
|
||||||
* 就只留在草稿层,而引用它的 `parts.profile`/`parts.type_param` 却随名单一起落地 ——
|
|
||||||
* 落地那一刻起,正式表里就有一行指着字典里查无此号的悬空引用(2026-08-30 真跑
|
|
||||||
* e2e 撞见的平台侧连带发现:范围放行没有对 `refersTo` 做传递闭包,并一张表却不
|
|
||||||
* 并它引用的表,必然留下悬空引用;那条更普遍的平台缺陷已记 `docs/debt.md`,
|
|
||||||
* 这里只在活动自己的声明层堵住这一处具体触发点)。四张表都在闸上、也都会一起
|
|
||||||
* 落地,才是「选类型」这件事完整、闭环的样子。
|
|
||||||
*/
|
*/
|
||||||
gateLedgers: ['parts.chip_type', 'parts.type_param', 'parts.profile', 'parts.parameter'],
|
gateLedgers: ['parts.profile', 'parts.parameter'],
|
||||||
},
|
},
|
||||||
{ kind: 'say' as const, text: '已核对两颗料的在库状态与参数齐备情况。' },
|
{ kind: 'say' as const, text: '已核对两颗料的在库状态与参数齐备情况。' },
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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.9.0',
|
packageVersion: '2.10.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',
|
||||||
@@ -568,7 +568,7 @@ export const manifest: ActivityManifest = {
|
|||||||
* LM2596S-3.3 刻意不在库。
|
* LM2596S-3.3 刻意不在库。
|
||||||
*/
|
*/
|
||||||
samples: [
|
samples: [
|
||||||
{ name: '候选缺库·新建类型', input: { originalInternalPn: 'LM2596S-5.0', candidateInternalPn: 'XL2596S-5.0' }, expect: { ports: ['assessment-released'] } },
|
{ name: '候选缺库·建档定型', input: { originalInternalPn: 'LM2596S-5.0', candidateInternalPn: 'XL2596S-5.0' }, expect: { ports: ['assessment-released'] } },
|
||||||
{ name: '都在库·零卡直分析', input: { originalInternalPn: 'LM2596S-5.0', candidateInternalPn: 'LM2596S-ADJ' }, expect: { ports: ['assessment-released'] } },
|
{ name: '都在库·零卡直分析', input: { originalInternalPn: 'LM2596S-5.0', candidateInternalPn: 'LM2596S-ADJ' }, expect: { ports: ['assessment-released'] } },
|
||||||
{ name: '类型不一致·硬拦', input: { originalInternalPn: 'LM2596S-5.0', candidateInternalPn: 'AMS1117-3.3' }, expect: { ports: ['assessment-released'] } },
|
{ name: '类型不一致·硬拦', input: { originalInternalPn: 'LM2596S-5.0', candidateInternalPn: 'AMS1117-3.3' }, expect: { ports: ['assessment-released'] } },
|
||||||
{ name: '原PN缺库', input: { originalInternalPn: 'LM2596S-3.3', candidateInternalPn: 'LM2596S-ADJ' }, expect: { ports: ['assessment-released'] } },
|
{ name: '原PN缺库', input: { originalInternalPn: 'LM2596S-3.3', candidateInternalPn: 'LM2596S-ADJ' }, expect: { ports: ['assessment-released'] } },
|
||||||
|
|||||||
@@ -70,16 +70,12 @@ const editGate = (address: string, label: string): string => `<div class="jc-edi
|
|||||||
<button class="jc-egbtn eg-save" type="button" data-edit-save="${address}">保存</button>
|
<button class="jc-egbtn eg-save" type="button" data-edit-save="${address}">保存</button>
|
||||||
</div>`;
|
</div>`;
|
||||||
const gapbar = (object: string, what: string): string => `<div class="jc-gapbar" data-class-of="${object}_missing">
|
const gapbar = (object: string, what: string): string => `<div class="jc-gapbar" data-class-of="${object}_missing">
|
||||||
<span><b>还差 <span data-slot="${object}_missing"></span> 行${what}没填</b> —— <span data-slot="${object}_missing_keys"></span>。按「编辑」打开,带 * 的必填格填上,补齐了才确认得了。</span>
|
<span><b>还差 <span data-slot="${object}_missing"></span> 行${what}没填</b> —— <span data-slot="${object}_missing_keys"></span>。打开编辑,带 * 的必填格填上,补齐了才确认得了。</span>
|
||||||
</div>`;
|
</div>`;
|
||||||
const addrow = (address: string, label: string, hint: string): string => `<div class="jc-addrow">
|
/** 可改格:只读那一份留空,渲染器自动配孪生 `<input>`(手写 input 会不填值,人一点编辑就是白框)。
|
||||||
<button type="button" data-edit-add="${address}">${label}</button>
|
* `ph` 落成孪生件的 placeholder(渲染器认 `data-ph`,2026-08-31)—— 短短一句说清这格填什么。 */
|
||||||
<em>${hint}</em>
|
const cell = (col: string, cls = '', ph = ''): string =>
|
||||||
</div>`;
|
`<span class="jc-vcell${cls === '' ? '' : ` ${cls}`}" data-slot="${col}" data-col="${col}"${ph === '' ? '' : ` data-ph="${ph}"`} data-blank></span>`;
|
||||||
/** 可改格:只读那一份留空,渲染器自动配孪生 `<input>`(手写 input 会不填值,人一点编辑就是白框) */
|
|
||||||
const cell = (col: string, cls = ''): string => `<span class="jc-vcell${cls === '' ? '' : ` ${cls}`}" data-slot="${col}" data-col="${col}" data-blank></span>`;
|
|
||||||
/** 只读格:即便挂在可改的表里也不许被 `table.on` 藏起来 */
|
|
||||||
const ro = (col: string, cls = ''): string => `<span class="jc-vcell ro${cls === '' ? '' : ` ${cls}`}" data-slot="${col}" data-blank></span>`;
|
|
||||||
const select = (col: string, options: ReadonlyArray<readonly [string, string]>, required = false): string =>
|
const select = (col: string, options: ReadonlyArray<readonly [string, string]>, required = false): string =>
|
||||||
`<select class="jc-cell" data-col="${col}"${required ? ' required' : ''}><option value=""></option>${options.map(([v, l]) => `<option value="${v}">${l}</option>`).join('')}</select>`;
|
`<select class="jc-cell" data-col="${col}"${required ? ' required' : ''}><option value=""></option>${options.map(([v, l]) => `<option value="${v}">${l}</option>`).join('')}</select>`;
|
||||||
/**
|
/**
|
||||||
@@ -111,9 +107,9 @@ const gateBar = (): string => `<div class="jc-gate-bar">
|
|||||||
* 收件人 / 关联任务两格由引擎注入;工单编号即本次评估案号 `eval_ref`(页脚)。
|
* 收件人 / 关联任务两格由引擎注入;工单编号即本次评估案号 `eval_ref`(页脚)。
|
||||||
* `stage` 是每张件自己的 hero 场景(SVG 舞台),`accent` 决定这张件的强调色。
|
* `stage` 是每张件自己的 hero 场景(SVG 舞台),`accent` 决定这张件的强调色。
|
||||||
*/
|
*/
|
||||||
const doc = (input: { accent: string; alias: string; tag: string; kicker: string; title: string; step: string; stage: string; body: string; footTag: string }): string =>
|
const doc = (input: { accent: string; alias: string; tag: string; kicker: string; title: string; step: string; stage: string; body: string; footTag: string; headTone?: 'light' }): string =>
|
||||||
`<style>${SHELL_CSS}${input.accent}${input.alias}</style>
|
`<style>${SHELL_CSS}${input.accent}${input.alias}</style>
|
||||||
<div class="page"><div class="card">
|
<div class="page"><div class="card${input.headTone === 'light' ? ' hd-light' : ''}">
|
||||||
<header class="card-head">
|
<header class="card-head">
|
||||||
<div class="hero-top">
|
<div class="hero-top">
|
||||||
<span class="hero-sys">${input.kicker}</span>
|
<span class="hero-sys">${input.kicker}</span>
|
||||||
@@ -130,6 +126,14 @@ const doc = (input: { accent: string; alias: string; tag: string; kicker: string
|
|||||||
</footer>
|
</footer>
|
||||||
</div></div>`;
|
</div></div>`;
|
||||||
|
|
||||||
|
/** 卡角编辑钮的三枚图标:笔(开编辑)/ 软盘(保存)/ 叉(放弃)—— 2026-08-31 用户定的笔↔软盘交互 */
|
||||||
|
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>';
|
||||||
|
const DISK_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="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2Z"/><path d="M17 21v-8H7v8"/><path d="M7 3v5h8"/></svg>';
|
||||||
|
const X_SVG =
|
||||||
|
'<svg class="fic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg>';
|
||||||
|
|
||||||
/* ==================== 局面:评估输入补全(input-supplement,S1 的数据工单) ==================== */
|
/* ==================== 局面:评估输入补全(input-supplement,S1 的数据工单) ==================== */
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -191,6 +195,14 @@ ${badgeCss('.rl', [
|
|||||||
const DOSSIERS = `<section class="board"><article class="dossiers" data-rows="mine:parts.profile" data-edit="parts.profile"><section class="dossier">
|
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>
|
<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>
|
<span class="flip-cue"><i class="cue-front">点击翻面 · 核心参数</i><i class="cue-back">点击翻回 · 主数据</i></span>
|
||||||
|
<span class="fabs">
|
||||||
|
<button class="fab xbtn x-f" type="button" data-edit-cancel="parts.profile" title="放弃这一面的修改">${X_SVG}</button>
|
||||||
|
<button class="fab xbtn x-b" type="button" data-edit-cancel="parts.parameter" title="放弃这一面的修改">${X_SVG}</button>
|
||||||
|
<button class="fab pen pen-f" type="button" data-edit-open="parts.profile" title="编辑这一面(主数据)">${PEN_SVG}</button>
|
||||||
|
<button class="fab pen pen-b" type="button" data-edit-open="parts.parameter" title="编辑这一面(核心参数值)">${PEN_SVG}</button>
|
||||||
|
<button class="fab disk disk-f" type="button" data-edit-save="parts.profile" title="保存这一面">${DISK_SVG}</button>
|
||||||
|
<button class="fab disk disk-b" type="button" data-edit-save="parts.parameter" title="保存这一面">${DISK_SVG}</button>
|
||||||
|
</span>
|
||||||
<div class="flip-in">
|
<div class="flip-in">
|
||||||
<div class="face front">
|
<div class="face front">
|
||||||
<header class="dz-top">
|
<header class="dz-top">
|
||||||
@@ -218,8 +230,8 @@ const DOSSIERS = `<section class="board"><article class="dossiers" data-rows="mi
|
|||||||
<header class="pb-hd"><b class="mono" data-slot="internal_pn" data-blank></b><span class="pb-lb">核心参数值</span></header>
|
<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">
|
<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-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-val pv">${cell('param_value', 'mono', '规格书上的值 · 查不到留空')}</span>
|
||||||
<span class="pt-note sg">${cell('param_note')}</span>
|
<span class="pt-note sg">${cell('param_note', '', '补充说明 · 选填')}</span>
|
||||||
<span class="rowkey">${cell('internal_pn', 'mono')}</span>
|
<span class="rowkey">${cell('internal_pn', 'mono')}</span>
|
||||||
</div></section>
|
</div></section>
|
||||||
</div>
|
</div>
|
||||||
@@ -254,102 +266,13 @@ const BASIS_CARD = `<article class="basis" data-rows="mine:${D}.case"><section c
|
|||||||
</div></section>
|
</div></section>
|
||||||
</section></article>`;
|
</section></article>`;
|
||||||
|
|
||||||
/**
|
/*
|
||||||
* 块②a 新建芯片类型 —— `parts.chip_type`。类型下拉(块①)里没有合适的,人在这里现造一个。
|
* ~~块②a 新建芯片类型 / 块②b 核心参数字段清单~~(2026-08-31 用户定:「把添加芯片类型的
|
||||||
* `type_id` 是自增主键 —— **人不填它**,自增列不问人;已有行只读态摆 `ro()`。
|
* 功能从这个活动中移除,不能在这个活动中新增芯片类型,只能选择已有的芯片类型」)。
|
||||||
*
|
* 两张编辑表(静态种子行、加删行、thead 后置、type_id 禁用 data-col 那整套 Critical-1
|
||||||
* **种子行里 `type_id` 必须挂一个真实的(禁用、恒空)`data-col`,不能干脆不给 ——
|
* 修法)与「新建类型」抽屉一并退役 —— 类型字典只在治理侧维护,本活动只读它。
|
||||||
* 这是 2026-08-30 终审 Critical 1 的病灶与修法**:第一版把这一格写成纯文本 `<span>`,
|
* 机制史与钉测见 git(2.9.0 及更早)与 view-render-addrow-thead.test.ts(平台侧仍在)。
|
||||||
* 不带 `data-col` —— 保存时 `collect()` 只认 `input[data-col]/select[data-col]/
|
|
||||||
* textarea[data-col]`,`type_id` 压根不出现在回传的 `cells` 里;而 `desk.ts` 的
|
|
||||||
* `resolveRowKey` 用 `k in cells` 判「这一列人碰过没有」,漏挂在它眼里等同「这张表没有
|
|
||||||
* 这一列」,专为「auto 列件上摆着却空着」设计的 `barred` 保护从未触发,退回「全表一致」:
|
|
||||||
* `parts.chip_type` 恰好一行时,人新建的类型被当场判成「就是那唯一一行」,一次保存就把
|
|
||||||
* 种子字典行**静默改名** —— `autoKeys()` 续号器永远轮不到。修法:真的
|
|
||||||
* `<input data-col="type_id" disabled>`(值恒空,只显示「由系统生成」占位)。
|
|
||||||
* 回归钉在 `__tests__/activity.test.ts` 与 `apps/api/src/app/__tests__/view-table-edit.test.ts`。
|
|
||||||
*
|
|
||||||
* **零行照样加得出第一行**(这一块存在的全部理由):`mine:parts.chip_type` 每单开局是
|
|
||||||
* 零行,`ViewContainer.addRow` 靠克隆 `tr:last-of-type` 添新行,零行时无样可克隆 ——
|
|
||||||
* 多一个不受 `data-rows` 管辖的静态 `<tbody>`,字面量摆一行输入格(不经 `cell()`),
|
|
||||||
* 它不是数据行,永远在;填了按保存,`writeEditedRows` 认得出「主键全空 + 别的列有值」
|
|
||||||
* (`mintable`),转给 `autoKeys()` 续号。
|
|
||||||
*
|
|
||||||
* `data-edit="parts.chip_type"` **两处都写** —— 外层 `<table>` 一处(给
|
|
||||||
* `ViewContainer.collect()` 与 `.on` 切换看,要同时收到已有行与种子行两处的输入,宿主
|
|
||||||
* 必须是两者共同的祖先),内层 `data-rows` 那个 `<tbody>` 一处(`renderRows` 判「这组行
|
|
||||||
* 可编辑吗」只认与 `data-rows` 同一个标签自带的属性)。
|
|
||||||
*
|
|
||||||
* **`<thead>` 排在两个 `<tbody>` 之后 —— 评审逮到的 Critical 回归修法,不是随手换的顺序**
|
|
||||||
* (2026-08-30):`bodyOf()` 解析到外层 `<table>` 后,`addRow()` 在这个 host 里找
|
|
||||||
* `tr:last-of-type` —— `:last-of-type` 按各自父元素判定,`<thead>` 里那个 `<tr>` 必然
|
|
||||||
* 「最后」,若 `<thead>` 排前面会被文档序优先命中:克隆出来的是表头文字。CSS 的
|
|
||||||
* `table-header-group` 排版规则保证 `<thead>` 视觉上永远在最前,不看 DOM 序 —— 呈现
|
|
||||||
* 一字不变,而选择器认文档序。证据与变异验证见
|
|
||||||
* `apps/api/src/modules/interaction/__tests__/view-render-addrow-thead.test.ts`。
|
|
||||||
*/
|
*/
|
||||||
const CHIP_TYPE_TABLE = `<div class="jc-tbl-wrap"><table data-edit="parts.chip_type">
|
|
||||||
<tbody data-rows="mine:parts.chip_type" data-edit="parts.chip_type"><tr>
|
|
||||||
<td class="rn"><span class="jc-rn-num"></span></td>
|
|
||||||
<td>${ro('type_id', 'mono')}</td>
|
|
||||||
<td>${cell('type_name')}</td>
|
|
||||||
<td>${cell('description')}</td>
|
|
||||||
</tr></tbody>
|
|
||||||
<tbody><tr class="jc-row-static">
|
|
||||||
<td class="rn"><span class="jc-rn-num"></span></td>
|
|
||||||
<td><input class="jc-cell mono" data-col="type_id" placeholder="由系统生成" disabled></td>
|
|
||||||
<td><input class="jc-cell" data-col="type_name" placeholder="新类型名称"></td>
|
|
||||||
<td><input class="jc-cell" data-col="description" placeholder="这个类型是什么(供选型参考)"></td>
|
|
||||||
</tr></tbody>
|
|
||||||
<thead><tr>
|
|
||||||
<th style="width:38px">#</th>
|
|
||||||
<th data-h="类型编号">编号</th>
|
|
||||||
<th class="req" data-h="类型名称">类型名称</th>
|
|
||||||
<th data-h="类型说明">类型说明</th>
|
|
||||||
</tr></thead>
|
|
||||||
</table></div>`;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 块②b 芯片类型的核心参数字段清单 —— `parts.type_param`,复合主键 `type_id + param_name`。
|
|
||||||
* 新建的类型在这里把核心参数字段一条条定下来(名字 / 说明 / 硬软约束 / 单位)。
|
|
||||||
*
|
|
||||||
* `type_id` 是与块①同款的 `optionSelect`(选项从 `parts.chip_type` 现读,人挑的是
|
|
||||||
* **类型名称**):块②a 新建的类型一旦保存,这份下拉立刻带上它 —— 不必谁记住那个号。
|
|
||||||
* 静态种子行与块②a 同一套手法;这张表是唯一带「+加一行」/删行的一张,零行时那颗按钮
|
|
||||||
* 同样够不到岸,种子行同样补。
|
|
||||||
*
|
|
||||||
* **`<thead>` 排在两个 `<tbody>` 之后,同 `CHIP_TYPE_TABLE` 那条大注释** —— 这张表更
|
|
||||||
* 要紧:它的「加一行」在外层 `data-edit` 挂上之前本来是好的,是一次真回归。
|
|
||||||
*/
|
|
||||||
const TYPE_PARAM_TABLE = `<div class="jc-tbl-wrap"><table data-edit="parts.type_param">
|
|
||||||
<tbody data-rows="mine:parts.type_param" data-edit="parts.type_param"><tr>
|
|
||||||
<td class="rn"><span class="jc-rn-num"></span><button class="jc-rowdel" type="button" title="删除此行" data-edit-del="parts.type_param">×</button></td>
|
|
||||||
<td>
|
|
||||||
<span class="jc-vcell"><span data-rows="parts.chip_type" data-by="type_id=type_id"><i data-slot="type_name" data-blank></i></span></span>
|
|
||||||
${optionSelect('type_id', 'parts.chip_type', 'type_id', 'type_name', true)}
|
|
||||||
</td>
|
|
||||||
<td>${cell('param_name')}</td>
|
|
||||||
<td>${cell('param_desc')}</td>
|
|
||||||
<td>${badge('ct', 'constraint_type', true)}${select('constraint_type', [['hard', '硬约束'], ['soft', '软约束']], true)}</td>
|
|
||||||
<td>${cell('param_unit')}</td>
|
|
||||||
</tr></tbody>
|
|
||||||
<tbody><tr class="jc-row-static">
|
|
||||||
<td class="rn"><span class="jc-rn-num"></span></td>
|
|
||||||
<td>${optionSelect('type_id', 'parts.chip_type', 'type_id', 'type_name', true)}</td>
|
|
||||||
<td><input class="jc-cell" data-col="param_name" placeholder="参数名"></td>
|
|
||||||
<td><input class="jc-cell" data-col="param_desc" placeholder="这个参数为什么要紧"></td>
|
|
||||||
<td><select class="jc-cell" data-col="constraint_type"><option value=""></option><option value="hard">硬约束</option><option value="soft">软约束</option></select></td>
|
|
||||||
<td><input class="jc-cell" data-col="param_unit" placeholder="单位(选填)"></td>
|
|
||||||
</tr></tbody>
|
|
||||||
<thead><tr>
|
|
||||||
<th style="width:38px">#</th>
|
|
||||||
<th class="req" data-h="所属类型">所属类型</th>
|
|
||||||
<th class="req" data-h="参数名">参数名</th>
|
|
||||||
<th class="req" data-h="参数说明">参数说明</th>
|
|
||||||
<th class="req" data-h="约束类型">约束(硬/软)</th>
|
|
||||||
<th data-h="参考单位">参考单位</th>
|
|
||||||
</tr></thead>
|
|
||||||
</table></div>`;
|
|
||||||
|
|
||||||
export const INPUT_SUPPLEMENT = doc({
|
export const INPUT_SUPPLEMENT = doc({
|
||||||
accent: ACCENT_DATA,
|
accent: ACCENT_DATA,
|
||||||
@@ -359,6 +282,7 @@ export const INPUT_SUPPLEMENT = doc({
|
|||||||
title: '差异评估的输入还差几格',
|
title: '差异评估的输入还差几格',
|
||||||
step: '步骤 1/4 · 校验入参与补数据',
|
step: '步骤 1/4 · 校验入参与补数据',
|
||||||
stage: '',
|
stage: '',
|
||||||
|
headTone: 'light',
|
||||||
footTag: '工单类型:数据',
|
footTag: '工单类型:数据',
|
||||||
body:
|
body:
|
||||||
gateBar() +
|
gateBar() +
|
||||||
@@ -367,28 +291,15 @@ export const INPUT_SUPPLEMENT = doc({
|
|||||||
'<span class="need">模板定位</span>:本单是「数据工单」——目标台账 = 芯片主数据(P-08 主记录 + 标准化参数子表),核心参数字段清单挂在芯片类型字典上。',
|
'<span class="need">模板定位</span>:本单是「数据工单」——目标台账 = 芯片主数据(P-08 主记录 + 标准化参数子表),核心参数字段清单挂在芯片类型字典上。',
|
||||||
'<span class="need">触发原因</span>:下面电路板上两张档案卡就是本次要比对的原 PN 与候选 PN —— 至少有一颗不在库、两颗归的类型不一致、或必填格还空着,顶部那条红带才会出现。',
|
'<span class="need">触发原因</span>:下面电路板上两张档案卡就是本次要比对的原 PN 与候选 PN —— 至少有一颗不在库、两颗归的类型不一致、或必填格还空着,顶部那条红带才会出现。',
|
||||||
'<span class="done">已绑定</span>:两颗料的内部料号已按启动参数铺上档案卡,不必你重填料号本身,只需核对 / 补齐其余各格;在库的料现值已回填,只是查漏补缺。',
|
'<span class="done">已绑定</span>:两颗料的内部料号已按启动参数铺上档案卡,不必你重填料号本身,只需核对 / 补齐其余各格;在库的料现值已回填,只是查漏补缺。',
|
||||||
'<span class="need">纪律</span>:两颗料必须归到同一个芯片类型下才谈得上逐项比对;类型下拉里没有合适的就在下面的抽屉里新建一个,并把它的核心参数字段一并定下来;查不到的参数值留空(会被红带点名),不要编一个值顶替。',
|
'<span class="need">纪律</span>:两颗料必须归到同一个芯片类型下才谈得上逐项比对;类型只从字典里选,本活动不新增类型 —— 字典里没有合适的,先请数据管理员补进字典;查不到的参数值留空(会被红带点名),不要编一个值顶替。',
|
||||||
]) +
|
]) +
|
||||||
`</details>` +
|
`</details>` +
|
||||||
zone('比对基准', '芯片类型 + 它的核心参数字段 —— 两颗料归到哪个类型,就按这些字段逐项比;档案卡上改选,这张卡当场跟着换', ZIC_TABLE) +
|
zone('芯片档案', '一颗芯片一张卡:正面主数据、背面核心参数值 —— 点卡翻面看另一面;右上角的笔 = 编辑当前这一面,软盘 = 保存', ZIC_TABLE) +
|
||||||
BASIS_CARD +
|
|
||||||
zone('芯片档案', '一颗芯片一张卡:正面主数据、背面核心参数值 —— 点卡翻面看另一面', ZIC_TABLE) +
|
|
||||||
editGate('parts.profile', '正面主数据核对 / 补齐 —— 开编辑后卡停在正面') +
|
|
||||||
editGate('parts.parameter', '背面参数值核对 / 补齐 —— 开编辑后两张卡自动翻到背面') +
|
|
||||||
gapbar('parts_profile', '必填') +
|
gapbar('parts_profile', '必填') +
|
||||||
gapbar('parts_parameter', '必填') +
|
gapbar('parts_parameter', '必填') +
|
||||||
DOSSIERS +
|
DOSSIERS +
|
||||||
gapbar('parts_chip_type', '必填') +
|
zone('比对基准', '芯片类型 + 它的核心参数字段 —— 两颗料归到哪个类型,就按这些字段逐项比;字段说明悬浮即看', ZIC_TABLE) +
|
||||||
gapbar('parts_type_param', '必填') +
|
BASIS_CARD +
|
||||||
`<details class="lab"><summary class="lab-sm"><b>类型库里没有合适的?</b>展开新建一个类型,并把它的核心参数字段定下来 —— 定完回到上面的档案卡挑它<span class="lab-caret"></span></summary><div class="lab-bd">` +
|
|
||||||
editGate('parts.chip_type', '新建一个类型:表尾那一行填类型名称与说明,保存后立刻出现在档案卡的下拉里') +
|
|
||||||
CHIP_TYPE_TABLE +
|
|
||||||
addrow('parts.chip_type', '+ 再建一个类型', '两颗料若都要新建、且类型不同,先各自保存,「加一行」能再建一个') +
|
|
||||||
`<p class="lab-sub">这个类型逐条比对时要看哪些参数 —— 名字 / 说明 / 硬软约束(*为必填列)</p>` +
|
|
||||||
editGate('parts.type_param', '字段名字 / 说明 / 硬软约束,点「编辑」核对;表尾那一行 / 「加一行」补字段') +
|
|
||||||
TYPE_PARAM_TABLE +
|
|
||||||
addrow('parts.type_param', '+ 加一行(补一个核心参数字段)', '所属类型 / 参数名 / 参数说明 / 约束类型必须写;参考单位选填') +
|
|
||||||
`</div></details>` +
|
|
||||||
flow('<b>提交 / 确认:</b>本工单只承载内容;确认请在对话区的【执行卡】中进行。确认后活动自动从「等待数据」唤醒。'),
|
flow('<b>提交 / 确认:</b>本工单只承载内容;确认请在对话区的【执行卡】中进行。确认后活动自动从「等待数据」唤醒。'),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -110,6 +110,15 @@ i{font-style:normal}
|
|||||||
.seal-sum{flex:1 1 240px;min-width:200px;max-width:420px;margin:0;font-size:13px;line-height:1.85;color:var(--hero-dim)}
|
.seal-sum{flex:1 1 240px;min-width:200px;max-width:420px;margin:0;font-size:13px;line-height:1.85;color:var(--hero-dim)}
|
||||||
.seal-sum:empty::after{content:"—"}
|
.seal-sum:empty::after{content:"—"}
|
||||||
|
|
||||||
|
/* ── 浅色题头变体(件① 2026-08-31 用户:「标题背景还是改成之前的那种蓝色渐变」——
|
||||||
|
渐变取自原目标件 card-head 那一条,深色电路夜景只留给验收两件)── */
|
||||||
|
.hd-light .card-head{background:linear-gradient(120deg,#f0f5ff 0%,#fff 55%,#eef4ff 100%);color:var(--ink);border-bottom:2px solid var(--ink)}
|
||||||
|
.hd-light .hero-sys{color:var(--ink-4)}
|
||||||
|
.hd-light .hero-title{color:var(--ink)}
|
||||||
|
.hd-light .hero-step{color:var(--ink-3);border-color:var(--line)}
|
||||||
|
.hd-light .hero-meta{color:var(--ink-3)}
|
||||||
|
.hd-light .hero-meta b{color:var(--ink)}
|
||||||
|
|
||||||
/* ================= 正文分区 ================= */
|
/* ================= 正文分区 ================= */
|
||||||
.card-bd{padding:20px 22px 8px}
|
.card-bd{padding:20px 22px 8px}
|
||||||
.sec{display:flex;align-items:baseline;gap:9px;margin:26px 0 10px;flex-wrap:wrap}
|
.sec{display:flex;align-items:baseline;gap:9px;margin:26px 0 10px;flex-wrap:wrap}
|
||||||
@@ -324,6 +333,23 @@ table.on tr.jc-row-static td{background:var(--acc-wash);border-top:1px dashed va
|
|||||||
.dossiers:not(.on) .dossier:hover:has(.flip-tg:checked):not(:has(.pback.on)) .flip-in{transform:rotateY(168deg)}
|
.dossiers:not(.on) .dossier:hover:has(.flip-tg:checked):not(:has(.pback.on)) .flip-in{transform:rotateY(168deg)}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── 卡角编辑钮:笔=编辑当前这一面,软盘=保存,叉=放弃(2026-08-31 用户定,顶掉两行编辑闸)──
|
||||||
|
笔只在待处理态露面(html.editable,与从前的闸同一条纪律);软盘/叉只在对应面的编辑态露面。 */
|
||||||
|
.fabs{position:absolute;top:10px;right:10px;z-index:5;display:flex;gap:6px}
|
||||||
|
.fab{display:none;width:32px;height:32px;border-radius:999px;border:1px solid var(--acc-line);background:#fff;color:var(--acc-deep);cursor:pointer;place-items:center;padding:0;box-shadow:0 4px 12px rgba(18,38,63,.14);transition:border-color .16s,color .16s,box-shadow .16s}
|
||||||
|
.fab:hover{border-color:var(--acc);box-shadow:0 6px 16px rgba(18,38,63,.22)}
|
||||||
|
.fab:active{transform:scale(.94)}
|
||||||
|
.fic{width:15px;height:15px;display:block;margin:auto}
|
||||||
|
.fab.pen{color:var(--acc-deep)}
|
||||||
|
.fab.disk{background:var(--acc);border-color:var(--acc);color:#fff}
|
||||||
|
.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}
|
||||||
|
.dossiers.on .disk-f{display:grid}
|
||||||
|
.dossiers.on .x-f{display:grid}
|
||||||
|
.dossier:has(.pback.on) .disk-b{display:grid}
|
||||||
|
.dossier:has(.pback.on) .x-b{display:grid}
|
||||||
|
|
||||||
/* ── 卡背:参数瓷贴 ── */
|
/* ── 卡背:参数瓷贴 ── */
|
||||||
.pb-hd{display:flex;align-items:baseline;gap:9px;flex-wrap:wrap}
|
.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-hd b{font-size:14px;font-weight:800;color:var(--ink)}
|
||||||
@@ -337,7 +363,8 @@ table.on tr.jc-row-static td{background:var(--acc-wash);border-top:1px dashed va
|
|||||||
.pt-note .jc-vcell{font-size:10.5px;color:var(--ink-3)}
|
.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 .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}
|
/* 说明空着**什么都不摆**(2026-08-31 用户:「没写就什么都不要展示」)—— 连兜底破折号一起收 */
|
||||||
|
.ptile .sg .jc-vcell:empty{display:none}
|
||||||
/* 参数编辑态:值与说明换输入,参数名照旧当标签摆着;两枚主键孪生件藏死(钥匙不是编辑面)。
|
/* 参数编辑态:值与说明换输入,参数名照旧当标签摆着;两枚主键孪生件藏死(钥匙不是编辑面)。
|
||||||
藏钥匙用包装类(.pt-name / .rowkey),不用「data-col 属性选择器」—— 那段选择器文本会原样
|
藏钥匙用包装类(.pt-name / .rowkey),不用「data-col 属性选择器」—— 那段选择器文本会原样
|
||||||
出现在共享样式表里,把「落账确认全只读」那类字面断言踩翻(2026-08-31 踩过,连注释也不许写)。 */
|
出现在共享样式表里,把「落账确认全只读」那类字面断言踩翻(2026-08-31 踩过,连注释也不许写)。 */
|
||||||
@@ -359,13 +386,19 @@ table.on tr.jc-row-static td{background:var(--acc-wash);border-top:1px dashed va
|
|||||||
.basis-hd .csvg{width:56px;flex:none}
|
.basis-hd .csvg{width:56px;flex:none}
|
||||||
.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}
|
||||||
.ftiles{display:grid;grid-template-columns:1fr 1fr;gap:8px}
|
/* 降噪(2026-08-31 用户:「乱糟糟…没必要显示全就不要显示完全,鼠标指上去后再显示」):
|
||||||
.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}
|
.ftiles{display:flex;flex-wrap:wrap;gap:8px}
|
||||||
.ft-hd b{font-size:12.5px;font-weight:800;color:var(--ink)}
|
.ftile{position:relative;display:flex;flex-direction:row;align-items:center;gap:7px;border:1px solid var(--line-soft);border-radius:999px;background:#fff;padding:6px 13px;min-width:0;transition:border-color .16s,box-shadow .16s}
|
||||||
|
.ftile:hover{border-color:var(--acc-line);box-shadow:0 4px 12px rgba(18,38,63,.1)}
|
||||||
|
.ft-hd{display:flex;align-items:baseline;gap:7px;white-space:nowrap}
|
||||||
|
.ft-hd b{font-size:12px;font-weight:800;color:var(--ink)}
|
||||||
.ft-unit{font-style:normal;font-size:10px;color:var(--ink-4);font-family:var(--mono)}
|
.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{display:none;position:absolute;z-index:6;top:calc(100% + 7px);left:0;width:max-content;max-width:280px;margin:0;font-size:11px;line-height:1.65;color:var(--ink-2);background:#fff;border:1px solid var(--line);border-radius:9px;padding:8px 11px;box-shadow:0 10px 24px rgba(18,38,63,.16)}
|
||||||
.ft-desc:empty{display:none}
|
.ftile:hover .ft-desc:not(:empty){display:block}
|
||||||
|
/* 类型说明:默认一行截断,悬浮题头才放开 */
|
||||||
|
.basis-desc{display:block;max-width:100%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||||
|
.basis-hd:hover .basis-desc{white-space:normal}
|
||||||
|
|
||||||
/* ================= 新建类型工作抽屉(details,零脚本)================= */
|
/* ================= 新建类型工作抽屉(details,零脚本)================= */
|
||||||
.lab{margin:0 0 6px;border:1.5px dashed var(--line);border-radius:12px;background:var(--soft)}
|
.lab{margin:0 0 6px;border:1.5px dashed var(--line);border-radius:12px;background:var(--soft)}
|
||||||
@@ -377,7 +410,6 @@ table.on tr.jc-row-static td{background:var(--acc-wash);border-top:1px dashed va
|
|||||||
.lab-caret{margin-left:auto;flex:none;width:8px;height:8px;border-right:2px solid var(--ink-4);border-bottom:2px solid var(--ink-4);transform:rotate(45deg);transition:transform .16s}
|
.lab-caret{margin-left:auto;flex:none;width:8px;height:8px;border-right:2px solid var(--ink-4);border-bottom:2px solid var(--ink-4);transform:rotate(45deg);transition:transform .16s}
|
||||||
.lab[open] .lab-caret{transform:rotate(-135deg)}
|
.lab[open] .lab-caret{transform:rotate(-135deg)}
|
||||||
.lab-bd{padding:2px 14px 14px}
|
.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{margin:0 0 16px}
|
||||||
.lab-brief .brief{margin:2px 14px 14px;border-left-width:3px}
|
.lab-brief .brief{margin:2px 14px 14px;border-left-width:3px}
|
||||||
|
|
||||||
|
|||||||
@@ -542,3 +542,17 @@ describe('data-options:下拉的选项从台账现读', () => {
|
|||||||
expect(out).toContain('<option value="2" selected>LDO 线性稳压器</option>');
|
expect(out).toContain('<option value="2" selected>LDO 线性稳压器</option>');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('孪生件占位提示:data-ph 落成 input 的 placeholder(2026-08-31,W05 参数瓷贴)', () => {
|
||||||
|
it('带 data-ph 的可改格,孪生 input 有 placeholder;不带的没有', () => {
|
||||||
|
const html =
|
||||||
|
'<div data-rows="mine:parts.parameter" data-edit="parts.parameter"><p>' +
|
||||||
|
'<span class="jc-vcell" data-slot="param_value" data-col="param_value" data-ph="规格书上的值,查不到留空" data-blank></span>' +
|
||||||
|
'<span class="jc-vcell" data-slot="param_note" data-col="param_note" data-blank></span>' +
|
||||||
|
'</p></div>';
|
||||||
|
const out = renderView(html, { 'mine:parts.parameter': [{ param_value: '', param_note: '' }] });
|
||||||
|
expect(out).toContain('placeholder="规格书上的值,查不到留空"');
|
||||||
|
const notePart = out.slice(out.indexOf('data-col="param_note"'));
|
||||||
|
expect(notePart.slice(0, notePart.indexOf('/>'))).not.toContain('placeholder=');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -263,7 +263,9 @@ function twinInputs(inner: string, rowIndex: number): string {
|
|||||||
.split(/\s+/)
|
.split(/\s+/)
|
||||||
.filter((c) => c === 'mono' || c === 'num')
|
.filter((c) => c === 'mono' || c === 'num')
|
||||||
.join(' ');
|
.join(' ');
|
||||||
const input = `<input class="jc-cell${style === '' ? '' : ` ${style}`}" data-col="${col}" data-row="${rowIndex}" data-slot="${slot}" data-value/>`;
|
/* `data-ph`:模版给这一格的输入孪生件配一句占位提示(2026-08-31,W05 参数瓷贴要「短短一句说清这格填什么」) */
|
||||||
|
const ph = /\bdata-ph\s*=\s*"([^"]*)"/.exec(attrs)?.[1];
|
||||||
|
const input = `<input class="jc-cell${style === '' ? '' : ` ${style}`}" data-col="${col}" data-row="${rowIndex}" data-slot="${slot}"${ph === undefined || ph === '' ? '' : ` placeholder="${ph}"`} data-value/>`;
|
||||||
// 只读那一份把 `data-col` 交出去 —— 它的活已经干完(告诉这里是哪一列),留着只是噪音
|
// 只读那一份把 `data-col` 交出去 —— 它的活已经干完(告诉这里是哪一列),留着只是噪音
|
||||||
return `${all.replace(/\s*\bdata-col\s*=\s*"[^"]*"/, '')}${input}`;
|
return `${all.replace(/\s*\bdata-col\s*=\s*"[^"]*"/, '')}${input}`;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1051,7 +1051,8 @@ function ExecForm({ order, onReplyOrder, onLocate, orderValues, onField, face =
|
|||||||
</button>
|
</button>
|
||||||
<p className="jc-fc-q">{order.ask}</p>
|
<p className="jc-fc-q">{order.ask}</p>
|
||||||
<Guide steps={order.steps} standard={order.standard} />
|
<Guide steps={order.steps} standard={order.standard} />
|
||||||
{order.basis.length > 0 && <Kv items={order.basis} />}
|
{/* 参考信息不再上卡(2026-08-31 用户定:「派发的工单参考信息这个板块不要显示」)——
|
||||||
|
basis 数据照旧随卡走(工单详情 / 归档件仍有),只是对话区这张卡不摆 */}
|
||||||
{order.fields.map((f) => (
|
{order.fields.map((f) => (
|
||||||
<label key={f.key} className="jc-fc-fld">
|
<label key={f.key} className="jc-fc-fld">
|
||||||
<span>
|
<span>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
*/
|
*/
|
||||||
// @vitest-environment jsdom
|
// @vitest-environment jsdom
|
||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import { collect, setOpenIn } from '../src/ViewContainer.tsx';
|
import { collect, mirrorTwins, setOpenIn } from '../src/ViewContainer.tsx';
|
||||||
|
|
||||||
/** 两张芯片卡:正面是主数据格(profile),背面各住一个参数宿主(parameter) */
|
/** 两张芯片卡:正面是主数据格(profile),背面各住一个参数宿主(parameter) */
|
||||||
const FLIP_CARDS =
|
const FLIP_CARDS =
|
||||||
@@ -73,3 +73,31 @@ describe('多宿主 collect():同一地址的宿主都数进来,格子只归最
|
|||||||
expect(hosts.every((h) => !h.classList.contains('on'))).toBe(true);
|
expect(hosts.every((h) => !h.classList.contains('on'))).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('mirrorTwins:下拉镜像的是选项文案,不是 value(2026-08-31「保存一瞬间变数字」)', () => {
|
||||||
|
const SEL =
|
||||||
|
'<div data-edit="parts.profile"><div class="row">' +
|
||||||
|
'<span class="jc-vcell tsel">旧名</span>' +
|
||||||
|
'<select class="jc-cell" data-col="chip_type_id">' +
|
||||||
|
'<option value=""></option><option value="1">DC-DC 降压稳压器</option><option value="2">LDO 线性稳压器</option>' +
|
||||||
|
'</select>' +
|
||||||
|
'</div></div>';
|
||||||
|
|
||||||
|
it('镜进孪生件的是「DC-DC 降压稳压器」,不是「1」;selected 属性拨到人选的那一项(守卫 CSS 认它)', () => {
|
||||||
|
const doc = document.implementation.createHTMLDocument('');
|
||||||
|
doc.body.innerHTML = SEL;
|
||||||
|
const sel = doc.querySelector('select') as HTMLSelectElement;
|
||||||
|
sel.value = '1';
|
||||||
|
mirrorTwins(doc, 'parts.profile');
|
||||||
|
expect(doc.querySelector('.tsel')?.textContent).toBe('DC-DC 降压稳压器');
|
||||||
|
expect(doc.querySelector('option[value="1"]')?.hasAttribute('selected')).toBe(true);
|
||||||
|
expect(doc.querySelector('option[value="2"]')?.hasAttribute('selected')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('没选(空占位)= 没答:孪生件一个字不动', () => {
|
||||||
|
const doc = document.implementation.createHTMLDocument('');
|
||||||
|
doc.body.innerHTML = SEL;
|
||||||
|
mirrorTwins(doc, 'parts.profile');
|
||||||
|
expect(doc.querySelector('.tsel')?.textContent).toBe('旧名');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -280,12 +280,25 @@ export function collect(doc: Document, address: string): Array<Record<string, st
|
|||||||
* 而 `twinInputs` 是把 input **紧接着**只读那份写出来的,前一个兄弟就是它的孪生件。
|
* 而 `twinInputs` 是把 input **紧接着**只读那份写出来的,前一个兄弟就是它的孪生件。
|
||||||
* 这条是在真单的 DOM 上查出来的:合成 fixture 里我两份都写了 data-slot,于是照样绿。
|
* 这条是在真单的 DOM 上查出来的:合成 fixture 里我两份都写了 data-slot,于是照样绿。
|
||||||
*/
|
*/
|
||||||
function mirrorTwins(doc: Document, address: string): void {
|
export function mirrorTwins(doc: Document, address: string): void {
|
||||||
for (const cell of cellsOf(doc, address)) {
|
for (const cell of cellsOf(doc, address)) {
|
||||||
// 与 collect 同一条规矩:没答的不算话(「没点/没选」= 不改)
|
// 与 collect 同一条规矩:没答的不算话(「没点/没选」= 不改)
|
||||||
if (unanswered(cell)) continue;
|
if (unanswered(cell)) continue;
|
||||||
const twin = cell.previousElementSibling;
|
const twin = cell.previousElementSibling;
|
||||||
if (twin === null || twin.tagName === 'INPUT' || twin.tagName === 'SELECT' || twin.tagName === 'TEXTAREA') continue;
|
if (twin === null || twin.tagName === 'INPUT' || twin.tagName === 'SELECT' || twin.tagName === 'TEXTAREA') continue;
|
||||||
|
/*
|
||||||
|
* **下拉镜像的是选项文案,不是 value**(2026-08-31 用户:「保存的一瞬间芯片类型
|
||||||
|
* 下拉框的值会变成数字」)—— 下拉的 value 是机器码(type_id、tally 值),
|
||||||
|
* 摆给人看的向来是人读名;顺手把 selected 属性拨到人选的那一项:守卫类 CSS
|
||||||
|
* (:has(option[selected]))与「取消后再编辑」的回显都认它,不拨的话保存后到
|
||||||
|
* 重推回来那一段,屏上仍按旧选择说话。
|
||||||
|
*/
|
||||||
|
if (cell.tagName === 'SELECT') {
|
||||||
|
const sel = cell as HTMLSelectElement;
|
||||||
|
for (const o of Array.from(sel.options)) o.toggleAttribute('selected', o.value === sel.value && sel.value !== '');
|
||||||
|
twin.textContent = sel.selectedOptions[0]?.textContent ?? sel.value;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
twin.textContent = cell.value;
|
twin.textContent = cell.value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-49
@@ -604,22 +604,9 @@ const SUITES = {
|
|||||||
edits: [],
|
edits: [],
|
||||||
extraTables: [
|
extraTables: [
|
||||||
/*
|
/*
|
||||||
* `parts.chip_type` **没有任何 `data-edit` 绑定**(manifest.ts / case-model.ts
|
* 2026-08-31「只选不建」:parts.chip_type / parts.type_param 两条随功能退役 ——
|
||||||
* 头注明写:新建类型时人在 `parts.type_param` 那张表加行,`parts.chip_type` 那
|
* S1 的写授权面(writes)已收窄,再喂这两张表会被 400 拒。字典只在治理侧维护。
|
||||||
* 张字典表本身没有编辑面 —— `ViewContainer.addRow` 靠克隆末行实现「加一行」,
|
|
||||||
* 而它在任何一单里都是从零行起,没有可克隆的样子)。写的授权面是**台账声明**
|
|
||||||
* 不是「当下卡上有没有绑定」(`writesOf` 只认 `declarations.ledger`),所以这条
|
|
||||||
* 写入走得通 —— 只是脚本代脚本走 API,不是真人代替点了一个并不存在的按钮。
|
|
||||||
* 只有样例 0(候选缺库·新建类型)会喂 `edits`,其余案例这里留空、这张表不写。
|
|
||||||
*/
|
*/
|
||||||
{ address: 'parts.chip_type', rowKey: 'type_id', blankCol: 'type_name', edits: [] },
|
|
||||||
{
|
|
||||||
// 复合主键 (type_id, param_name) —— 同一类型两个字段不能靠单列区分。
|
|
||||||
address: 'parts.type_param',
|
|
||||||
rowKey: ['type_id', 'param_name'],
|
|
||||||
blankCol: 'param_desc',
|
|
||||||
edits: [],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
// 复合主键 (internal_pn, param_name) —— 两颗料同名参数(如都有「工作温度」)
|
// 复合主键 (internal_pn, param_name) —— 两颗料同名参数(如都有「工作温度」)
|
||||||
// 不这样区分,rows.find 会永远命中第一条同名行、把不同料的值互相顶掉。
|
// 不这样区分,rows.find 会永远命中第一条同名行、把不同料的值互相顶掉。
|
||||||
@@ -634,8 +621,8 @@ const SUITES = {
|
|||||||
cases: [
|
cases: [
|
||||||
{
|
{
|
||||||
sample: 0,
|
sample: 0,
|
||||||
name: '候选缺库 · XL2596S-5.0 新建芯片类型(自增 type_id,字段清单沿用原型口径)入库,'
|
name: '候选缺库 · XL2596S-5.0 建档并选已有类型(type_1,2026-08-31 起本活动只选不建)'
|
||||||
+ '原 PN 一并转挂新类型才够得上 distinct=1 · 审核先点名意见驳回再通过',
|
+ '· 审核先点名意见驳回再通过',
|
||||||
decideSeq: ['custom:使能阈值那条 UNKNOWN 的去向请写明验证任务号与测试条件', 'approve'],
|
decideSeq: ['custom:使能阈值那条 UNKNOWN 的去向请写明验证任务号与测试条件', 'approve'],
|
||||||
expectDecision: true,
|
expectDecision: true,
|
||||||
expectPort: 'assessment-released',
|
expectPort: 'assessment-released',
|
||||||
@@ -660,39 +647,13 @@ const SUITES = {
|
|||||||
* `type_id`,只是绕开这一条平台坑去验证后续链路,不代表浏览器里的真人也这样能走通。**
|
* `type_id`,只是绕开这一条平台坑去验证后续链路,不代表浏览器里的真人也这样能走通。**
|
||||||
*/
|
*/
|
||||||
customFill: async (taskId) => {
|
customFill: async (taskId) => {
|
||||||
const typeId = '90';
|
/* 2026-08-31「只选不建」:两颗料都挂到既有的 type_1(DC-DC)—— 新建类型那条路
|
||||||
// ① 新建芯片类型:显式给 type_id(见上方大注释,不走 autoKeys 续号)
|
(显式 type_id=90 绕全表一致坑的那整套)随功能移除,史见 git 2.9.0 及更早。 */
|
||||||
|
const typeId = '1';
|
||||||
await api(`/api/desk/tasks/${taskId}/table`, {
|
await api(`/api/desk/tasks/${taskId}/table`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
tables: {
|
tables: {
|
||||||
'parts.chip_type': [
|
|
||||||
{
|
|
||||||
type_id: typeId,
|
|
||||||
type_name: 'DC-DC 降压稳压器(评估自定义)',
|
|
||||||
description: '本次评估自定义的降压转换器类型,核心参数字段清单沿用原类型口径,供原 PN 与候选料共用比对。',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
console.log(` 新建类型已保存,显式指定 type_id=${typeId}(绕开全表一致平台坑,已报未改)`);
|
|
||||||
// ③ 核心参数字段清单(逐字照抄 type_id=1 的字典行,见 seed-mvp-data.ts)+
|
|
||||||
// 两颗料都转挂新类型(distinct 要求同一个值)+ 候选料的参数值。
|
|
||||||
// LM2596S-5.0 的既有参数值按同名 lookup 自动带过来,不必在这里重填。
|
|
||||||
await api(`/api/desk/tasks/${taskId}/table`, {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({
|
|
||||||
tables: {
|
|
||||||
'parts.type_param': [
|
|
||||||
{ type_id: typeId, param_name: '输入电压范围', param_desc: '芯片允许的输入直流电压区间。候选料的上限低于应用输入上限就是硬伤,烧的是芯片。', constraint_type: 'hard', param_unit: 'V' },
|
|
||||||
{ type_id: typeId, param_name: '输出电压', param_desc: '固定版给定值;可调版给可调区间,需外围反馈电阻网络设定。固定换可调要动外围。', constraint_type: 'hard', param_unit: 'V' },
|
|
||||||
{ type_id: typeId, param_name: '输出电流', param_desc: '满载持续输出能力。低于原料即降额,负载重时会掉压或过热保护。', constraint_type: 'hard', param_unit: 'A' },
|
|
||||||
{ type_id: typeId, param_name: '开关频率', param_desc: '决定外围电感与输出电容的取值。频率变了外围多半要跟着换,不是换颗料就完。', constraint_type: 'soft', param_unit: 'kHz' },
|
|
||||||
{ type_id: typeId, param_name: '静态电流', param_desc: '空载待机时自身消耗。电池供电场景敏感,市电场景通常不敏感。', constraint_type: 'soft', param_unit: 'mA' },
|
|
||||||
{ type_id: typeId, param_name: '工作温度', param_desc: '保证参数指标的环境温度区间。工业温域窄于应用要求就是硬伤。', constraint_type: 'hard', param_unit: '℃' },
|
|
||||||
{ type_id: typeId, param_name: '使能阈值', param_desc: 'ON/OFF 引脚的开关门限。上游用 MCU 电平直驱时,阈值对不上会关不掉或开不起来。', constraint_type: 'hard', param_unit: 'V' },
|
|
||||||
],
|
|
||||||
'parts.profile': [
|
'parts.profile': [
|
||||||
{ internal_pn: 'LM2596S-5.0', chip_type_id: typeId },
|
{ internal_pn: 'LM2596S-5.0', chip_type_id: typeId },
|
||||||
{
|
{
|
||||||
@@ -768,10 +729,8 @@ const SUITES = {
|
|||||||
await db.query("delete from ledger.validation__record where validation_internal_pn = 'XL2596S-5.0'");
|
await db.query("delete from ledger.validation__record where validation_internal_pn = 'XL2596S-5.0'");
|
||||||
await db.query("delete from ledger.parts__parameter where internal_pn = 'XL2596S-5.0'");
|
await db.query("delete from ledger.parts__parameter where internal_pn = 'XL2596S-5.0'");
|
||||||
await db.query("delete from ledger.parts__profile where internal_pn = 'XL2596S-5.0'");
|
await db.query("delete from ledger.parts__profile where internal_pn = 'XL2596S-5.0'");
|
||||||
await db.query("delete from ledger.parts__type_param where type_id = '90'");
|
|
||||||
await db.query("delete from ledger.parts__chip_type where type_id = '90'");
|
|
||||||
});
|
});
|
||||||
console.log(' 跨案例隔离:LM2596S-5.0 已转回 type_1;这一例全新建的 XL2596S-5.0(候选料本身、评估结果、type_id=90 字典行)已整体清空');
|
console.log(' 跨案例隔离:XL2596S-5.0(这一例建档的候选料与它的评估结果)已整体清空;LM2596S-5.0 类型兜底归位 type_1');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user