diff --git a/activities/part-substitution-assessment/__tests__/activity.test.ts b/activities/part-substitution-assessment/__tests__/activity.test.ts index 903b685..b64510c 100644 --- a/activities/part-substitution-assessment/__tests__/activity.test.ts +++ b/activities/part-substitution-assessment/__tests__/activity.test.ts @@ -132,8 +132,16 @@ describe('台账声明', () => { ].sort()); }); + /* + * Task 7b(2026-08-29):`type_id` 声明是 `text` 不是 `integer` —— 台账是 JSONB,不存在 + * 物理列类型这回事,这条纯粹是逻辑类型;改 text 是因为它是本仓唯一「auto:true 又是 + * 整数型」的主键,integer 会踩两个坑(`resolveRowKey` 的续号步骤抢在 `autoKeys()` 前面 + * 把它按草稿行数续成 1、撞种子的 `type_id=1`;`autoKeys()` 补零的字符串 `'03'` 经引用列的 + * `coerceToColumn(_, 'integer')` 会被 `Number()` 削掉前导零,`refersTo`/查表按字符串比对 + * 全部失配)。查证过程见 Task 7b 报告。 + */ it('类型字典:type_id 是自增主键 —— 人在件上新建类型时留空,由基座续号', () => { - expect(colOf('parts.chip_type', 'type_id')).toMatchObject({ primaryKey: true, type: 'integer', auto: true }); + expect(colOf('parts.chip_type', 'type_id')).toMatchObject({ primaryKey: true, type: 'text', auto: true }); expect(colOf('parts.chip_type', 'type_name')?.required).toBe(true); }); @@ -145,7 +153,7 @@ describe('台账声明', () => { }); it('P-08:类型必填并引用字典;createOnly 撤掉 —— 在库老料要能补上类型这一格', () => { - expect(colOf('parts.profile', 'chip_type_id')).toMatchObject({ required: true, type: 'integer', refersTo: 'parts.chip_type.type_id' }); + expect(colOf('parts.profile', 'chip_type_id')).toMatchObject({ required: true, type: 'text', refersTo: 'parts.chip_type.type_id' }); expect(ledgerOf('parts.profile')?.createOnly).toBeUndefined(); /* 封装与工艺本来就是「核心参数」—— 它们该以类型字段的身份出现在字典里,不在主记录上另开一列 */ for (const gone of ['package', 'process_technology', 'eol_pcn_status', 'application_scenarios']) { @@ -376,9 +384,9 @@ describe('报告模版:编辑面与只读面各就各位', () => { expect(viewHtml('landing-confirm')).not.toContain('data-edit-add='); }); - it('每个可编辑格子的 data-col 都在对应台账对象的声明里(input-supplement 一屏三张表,三张台账对象的声明并起来看;difference-review 一张表)', () => { + it('每个可编辑格子的 data-col 都在对应台账对象的声明里(input-supplement 一屏四张表,四张台账对象的声明并起来看;difference-review 一张表)', () => { const cases: Array<[string, Array<[string, string]>]> = [ - ['input-supplement', [['parts', 'profile'], ['parts', 'type_param'], ['parts', 'parameter']]], + ['input-supplement', [['parts', 'profile'], ['parts', 'chip_type'], ['parts', 'type_param'], ['parts', 'parameter']]], ['difference-review', [['substitution', 'difference_item']]], ]; for (const [view, objects] of cases) { @@ -425,9 +433,10 @@ describe('报告模版:编辑面与只读面各就各位', () => { }); /* - * 件① 评估输入补全(2026-08-29,Task 7):S1 的配方缺什么就发一张数据工单,`view: 'input-supplement'` - * 指向它。三块竖排——要评估什么(两颗料的存在性 + 主数据)/ 芯片类型(选型 + 新建类型时的核心参数 - * 字段清单)/ 参数值(逐条实测/规格值);顶部一条红带摆配方算好的 `gate_reason`。 + * 件① 评估输入补全(2026-08-29,Task 7;Task 7b 补②a 块):S1 的配方缺什么就发一张数据工单, + * `view: 'input-supplement'` 指向它。四块竖排——要评估什么(两颗料的存在性 + 主数据)/ + * 芯片类型(选型 + 新建类型)/ 核心参数字段清单(新建类型时要填)/ 参数值(逐条实测/规格值); + * 顶部一条红带摆配方算好的 `gate_reason`。 * 复用文件顶部已有的 `viewHtml` 短写(与 brief 草稿里的 `viewOf` 同一件事,不重复定义一个)。 */ describe('件① 评估输入补全', () => { @@ -439,18 +448,63 @@ describe('件① 评估输入补全', () => { expect(steps.find((s) => s.item === 'S1_inputs')?.view).toBe('input-supplement'); }); - it('三块都在,行都绑本单草稿(不是整表)', () => { + it('四块都在,行都绑本单草稿(不是整表)', () => { expect(html).toContain('data-rows="mine:sub_eval.part"'); 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"'); }); - it('芯片类型是从台账现读的下拉 —— 写死等于「只能选出包那天就有的那几种」', () => { + it('芯片类型是从台账现读的下拉 —— 写死等于「只能选出包那天就有的那几种」(块①的 chip_type_id 与块②b 的 type_id 都是)', () => { 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 下拉 */ + 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); + }); + + /* + * Task 7b:件① 新建类型(`parts.chip_type`)与新建字段(`parts.type_param`)两张表开局都是 + * 零行,`ViewContainer.addRow` 靠克隆 `tr:last-of-type` 添行,零行时找不到样子可克隆 + * (查证见 Task 7b 报告)。补一条**不受 `data-rows` 管辖的静态种子行**——两张表各一条, + * 字面量手写 `data-col` 输入格,不经 `cell()`/`data-slot`,行数再少也在。 + */ + it('**零行也加得出第一行**:两张表各带一条静态种子行(class="jc-row-static"),不靠 data-rows 生成', () => { + const staticRows = html.match(//g) ?? []; + expect(staticRows).toHaveLength(2); + /* 种子行里的输入格是手写的,不经 twinInputs —— 直接断言 data-col 存在即可 */ + expect(html).toContain(''); + expect(html).toContain(''); + expect(html).toContain(''); + expect(html).toContain(' { + expect(html).toContain('系统生成'); + // 「系统生成」只出现在 chip_type 那条种子行里 —— 精确抓出那一行,断言里面没有 data-col="type_id" + const chipTypeStaticRow = /(?:(?!<\/tr>)[\s\S])*?系统生成(?:(?!<\/tr>)[\s\S])*?<\/tr>/.exec(html)?.[0] ?? ''; + expect(chipTypeStaticRow).not.toBe(''); + expect(chipTypeStaticRow).not.toContain('data-col="type_id"'); + expect(chipTypeStaticRow).toContain('data-col="type_name"'); + expect(chipTypeStaticRow).toContain('data-col="description"'); }); it('**主键列必挂 `data-col`** —— 它不是编辑面,是行的钥匙(漏了会把人改的行错配到别行上)', () => { @@ -458,12 +512,26 @@ describe('件① 评估输入补全', () => { expect(html).toContain('data-col="param_name"'); }); - it('可改的三张表各有一道编辑闸', () => { - for (const address of ['parts.profile', 'parts.type_param', 'parts.parameter']) { + it('可改的四张表各有一道编辑闸', () => { + for (const address of ['parts.profile', 'parts.chip_type', 'parts.type_param', 'parts.parameter']) { expect(html).toContain(`data-edit="${address}"`); } }); + /* + * Task 7b:`data-edit="parts.chip_type"`(与 `parts.type_param` 同款)两处都写 —— + * 外层 `` 一处(给 `ViewContainer.collect()`/`.on` 切换看,它按 + * `document.querySelector` 找唯一宿主,要同时收到已有行与种子行两处的输入)、 + * 内层 `data-rows` 那个 `` 一处(给 `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); + } + }); + /* * 2026-08-29 评审 Minor:brief 原话是「红带显隐用 data-class-of」,但 gate_reason 是 * 中文自由文本 —— classToken 只留 [a-z0-9-],中文字符全被剥空,不论空串还是有内容的 @@ -489,14 +557,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*\}/); - // 这张件的三张表都走 .jc-tbl-wrap(没有 .tbl-wrap)—— 补丁真的接在会用到的类上 + // 这张件的四张表都走 .jc-tbl-wrap(没有 .tbl-wrap)—— 补丁真的接在会用到的类上(Task 7b 新增 parts.chip_type 这一张,3→4) expect(html).not.toContain('class="tbl-wrap"'); const jcTblWrapCount = html.match(/class="jc-tbl-wrap"/g)?.length ?? 0; - expect(jcTblWrapCount).toBe(3); + expect(jcTblWrapCount).toBe(4); }); }); @@ -736,30 +804,40 @@ describe.skip('徽标别名与值域相关的坑(2026-08-27 实测账,回归网) * 与 `sub_eval.input_gap` 两张随重写整个退休,不再有对应条目。这两条 Critical 回归网继续钉在 * 新件上:主键(internal_pn / type_id+param_name)都挂了 `data-col`,必填列都有可填的控件。 */ -describe('可编辑件的主键与必填列(2026-08-27 复审:主键丢钥匙 / 必填填不了,两条 Critical 回归)', () => { - /** input-supplement 一屏三张真能编辑的表与它们各自绑定的台账对象 */ +describe('可编辑件的主键与必填列(2026-08-27 复审:主键丢钥匙 / 必填填不了,两条 Critical 回归;Task 7b 补 parts.chip_type)', () => { + /** input-supplement 一屏四张真能编辑的表与它们各自绑定的台账对象(Task 7b 加入 parts.chip_type) */ 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' }, ]; - it('每一个主键列都有 data-col —— 它不是编辑面,是行的钥匙,丢了就整行保存不下去', () => { + it('每一个非豁免的主键列都有 data-col —— 它不是编辑面,是行的钥匙,丢了就整行保存不下去', () => { for (const { view, domain, object } of EDITABLE) { const table = byAddress(domain, object); /* - * `caseKey` 声明的那一列(如 `eval_ref`)是例外:它是「一单一号,全表当然一致」的 - * 案号型主键(与 I01 的 `round_ref` 同款),件上压根不摆,服务端 `resolveRowKey` - * 按「全表一致」自动补上(`apps/api/src/app/routes/desk.ts` 注释里点名的那条路)—— - * 摆出来给人填反而是错的(那会让人以为能按行改案号)。真正逐行不同、必须挂 - * `data-col` 的,是主键里除 caseKey 之外的那几列(如 `item_key`)。 + * 两类主键列豁免,不必挂 `data-col`: + * ① `caseKey` 声明的那一列(如 `eval_ref`)—— 「一单一号,全表当然一致」的案号型 + * 主键(与 I01 的 `round_ref` 同款),件上压根不摆,服务端 `resolveRowKey` 按 + * 「全表一致」自动补上(`apps/api/src/app/routes/desk.ts` 注释里点名的那条路)—— + * 摆出来给人填反而是错的(那会让人以为能按行改案号)。 + * ② `auto:true` 的那一列(如 `parts.chip_type.type_id`,Task 7b 补)—— 自增列不问人 + * (与联系人编码 `contact_code` 同一条纪律),值由 `writeEditedRows` 的 + * `autoKeys()` 在写入那一刻续号,件上给它挂 `data-col` 反而是在问人一个他答不上 + * 来的号(该表种子行只带 `type_name`/`description` 两格,不带 `type_id`)。 + * 真正逐行不同、必须挂 `data-col` 的,是主键里除这两类之外的那几列。 */ - const pk = (table?.fields.filter((f) => f.primaryKey).map((f) => f.name) ?? []).filter((name) => name !== table?.caseKey); - expect(pk.length, `${domain}.${object} 没有非 caseKey 的主键列`).toBeGreaterThan(0); + const pk = (table?.fields.filter((f) => f.primaryKey).map((f) => f.name) ?? []).filter( + (name) => name !== table?.caseKey && table?.fields.find((f) => f.name === name)?.auto !== true, + ); const html = viewHtml(view); for (const col of pk) { expect(html, `${view}: 主键列 ${domain}.${object}.${col} 缺 data-col`).toContain(`data-col="${col}"`); } + // 至少一类主键真存在(caseKey/auto 全豁免的表,如 chip_type,pk 允许为空数组——上面循环体自然跳过) + const allPk = table?.fields.filter((f) => f.primaryKey).map((f) => f.name) ?? []; + expect(allPk.length, `${domain}.${object} 没有任何主键列`).toBeGreaterThan(0); } }); diff --git a/activities/part-substitution-assessment/src/manifest.ts b/activities/part-substitution-assessment/src/manifest.ts index b676688..e06cd16 100644 --- a/activities/part-substitution-assessment/src/manifest.ts +++ b/activities/part-substitution-assessment/src/manifest.ts @@ -54,7 +54,7 @@ export const manifest: ActivityManifest = { activityId: ACTIVITY_ID, name: 'ACT-WORK-05 · 差异评估', /* 重写不是改 —— 同版重装是 no-op(静默丢失,踩过) */ - packageVersion: '2.0.0', + packageVersion: '2.1.0', category: '需求分析与方案匹配', sdkVersion: '0.1.0-m5', checksum: 'dev-checksum-part-substitution-assessment', @@ -219,7 +219,29 @@ export const manifest: ActivityManifest = { kind: 'master', label: '芯片类型字典', fields: [ - col({ name: 'type_id', type: 'integer', label: '类型编号', primaryKey: true, auto: true }), + /* + * **`type_id` 声明成 `text` 而不是 `integer`**(Task 7b,2026-08-29,件① 补「新建类型」 + * 逼出来的两处平台级发现,记在这里免得下一个人重踩): + * ①`resolveRowKey`(`apps/api/src/app/routes/desk.ts`)的「③ 续号」只认 `isSeq` + * (declared type 是 integer/number),不看 `auto` —— 这个类型碰巧是**唯一**一个 + * `auto:true` 又是 `integer` 的主键,于是它抢在 `writeEditedRows` 的 `autoKeys()` + * (真正的续号器,数official+草稿叠加、不会撞号)前面,自己按「本单草稿有几行」 + * 铸了个从 1 起的号——种子已经占了 `type_id=1/2`,人第一次新建类型就会撞车, + * 把种子的「DC-DC 降压稳压器」悄悄改名。②即便①不发生,`autoKeys()` 的续号是 + * 两位补零字符串(`'03'`),而**引用它的那几列**(`parts.profile.chip_type_id` 等) + * 若声明成 `integer`,写入时会被 `coerceToColumn` 按 `Number()` 解析掉前导零—— + * `'03'` 存成 `parts.chip_type.type_id`、`3` 存成 `parts.profile.chip_type_id`, + * `refersTo`/`data-by` 那几处按 `String(...)` 比对的查找会全部失配(`'03' !== '3'`), + * 写入当场被 `refersTo` 拒、或下拉/查表显示不出人刚建的类型。 + * **两处病根都是「integer 类型的自增列」这个此前从未出现过的组合触发的**——本仓 + * 唯一先例 `contact_code` 一直是 `text`。改成 `text`(此列与下面 5 处同引用列一起改, + * 保持同一条身份链路全程一种类型)对存储没有影响(台账是 JSONB,没有物理列类型), + * 只是把逻辑类型改得诚实;S-FEEL 那几处只做等值/去重比较,不做数值运算,不受影响 + * (已逐处核对:distinct 计数 / lookup·take / refersTo 皆按字符串比,`>` 只用在 + * `distinct_chip_type_id` 这个计数列上,不用在值本身)。查证过程详见 + * Task 7b 报告(`.superpowers/sdd/2026-08-29-act-work-05-rewrite/task-7b-report.md`)。 + */ + col({ name: 'type_id', type: 'text', label: '类型编号', primaryKey: true, auto: true }), col({ name: 'type_name', type: 'text', label: '类型名称', required: true }), col({ name: 'description', type: 'text', label: '类型说明', expect: true }), ], @@ -230,7 +252,8 @@ export const manifest: ActivityManifest = { kind: 'master', label: '芯片类型核心参数字段', fields: [ - col({ name: 'type_id', type: 'integer', label: '所属类型', primaryKey: true, refersTo: 'parts.chip_type.type_id' }), + /* 与 `parts.chip_type.type_id` 同一条身份链路,同一个理由改成 text——见上面那条大注释 */ + col({ name: 'type_id', type: 'text', label: '所属类型', primaryKey: true, refersTo: 'parts.chip_type.type_id' }), col({ name: 'param_name', type: 'text', label: '参数名', primaryKey: true }), /* AI 分析时读它 —— 这一格说清「这个参数是什么意思」,是 S2 唯一一次 analyze 出场的入参之一 */ col({ name: 'param_desc', type: 'text', label: '参数说明', required: true }), @@ -254,7 +277,8 @@ export const manifest: ActivityManifest = { label: '芯片主数据(P-08)', fields: [ col({ name: 'internal_pn', type: 'text', label: '内部料号', primaryKey: true }), - col({ name: 'chip_type_id', type: 'integer', label: '芯片类型', required: true, refersTo: 'parts.chip_type.type_id' }), + /* 与 `parts.chip_type.type_id` 同一条身份链路,同一个理由改成 text —— 见该字段的大注释 */ + col({ name: 'chip_type_id', type: 'text', label: '芯片类型', required: true, refersTo: 'parts.chip_type.type_id' }), col({ name: 'brand_name', type: 'text', label: '品牌名称', required: true }), col({ name: 'lifecycle_status', type: 'text', label: '生命周期状态', required: true, tally: ['active', 'nrnd', 'eol', 'discontinued', 'risk'] }), col({ name: 'specification_description', type: 'text', label: '规格描述', expect: true }), @@ -314,7 +338,8 @@ export const manifest: ActivityManifest = { col({ name: 'exists_status', type: 'text', label: '在库状态', required: true, tally: ['found', 'absent'] }), col({ name: 'chip_type_id', - type: 'integer', + /* 与 `parts.chip_type.type_id` 同一条身份链路,同一个理由改成 text —— 见该字段的大注释 */ + type: 'text', label: '芯片类型', required: true, /* @@ -334,7 +359,8 @@ export const manifest: ActivityManifest = { label: '本单案卷', fields: [ col({ name: 'eval_ref', type: 'text', label: '评估案号', primaryKey: true }), - col({ name: 'chip_type_id', type: 'integer', label: '本单锁定的芯片类型' }), + /* 与 `parts.chip_type.type_id` 同一条身份链路,同一个理由改成 text —— 见该字段的大注释 */ + col({ name: 'chip_type_id', type: 'text', label: '本单锁定的芯片类型' }), col({ name: 'gate_reason', type: 'text', label: '入参未就绪原因(件上摆的就是它)' }), col({ name: 'summary', type: 'text', label: '结论摘要' }), ], @@ -354,7 +380,8 @@ export const manifest: ActivityManifest = { col({ name: 'eval_ref', type: 'text', label: '评估案号', required: true }), col({ name: 'original_internal_pn', type: 'text', label: '原内部料号', required: true, refersTo: 'parts.profile.internal_pn' }), col({ name: 'candidate_internal_pn', type: 'text', label: '候选内部料号', required: true, refersTo: 'parts.profile.internal_pn' }), - col({ name: 'chip_type_id', type: 'integer', label: '芯片类型', required: true }), + /* 与 `parts.chip_type.type_id` 同一条身份链路,同一个理由改成 text —— 见该字段的大注释 */ + col({ name: 'chip_type_id', type: 'text', label: '芯片类型', required: true }), col({ name: 'recommendation_level', type: 'text', label: '推荐等级', required: true, tally: ['recommended', 'conditional', 'not_recommended'] }), col({ name: 'recommendation_reason', type: 'text', label: '推荐理由', required: true }), col({ name: 'difference_summary', type: 'text', label: '差异摘要', required: true }), diff --git a/activities/part-substitution-assessment/src/views/report.ts b/activities/part-substitution-assessment/src/views/report.ts index 3d14c99..9392b64 100644 --- a/activities/part-substitution-assessment/src/views/report.ts +++ b/activities/part-substitution-assessment/src/views/report.ts @@ -5,10 +5,11 @@ * 差异评估结论审核 / 评审放行与落账确认)按 spec §8 全部作废,S1 那一步不再分成 * 「先核对存在性 → 分别派 P-08/P-10 数据工单 → 再摆评估输入清单」三个局面 —— * 新的四步案例模型只有一张数据工单卡(`variant: 'data'`),这里对应**一张**新局面 - * `input-supplement`(评估输入补全):**三块竖排**(spec §6.1)—— + * `input-supplement`(评估输入补全):**四块竖排**(spec §6.1,Task 7b 补第②a 块)—— * ① 要评估什么(两颗料的存在性 + 主数据,`sub_eval.part` 铺行 / `parts.profile` 承载编辑); - * ② 芯片类型(每颗料各自的类型选择,选项从 `parts.chip_type` 现读; - * 下面跟着新建类型时要填的核心参数字段清单 `parts.type_param`,供加行); + * ②a 芯片类型(每颗料各自的类型选择,选项从 `parts.chip_type` 现读;类型下拉里没有合适的 + * 就在这里现新建一个,`parts.chip_type` 承载编辑); + * ②b 核心参数字段清单(新建类型时要填的字段清单,`parts.type_param` 承载编辑,供加行); * ③ 参数值(逐条核心参数的实测 / 规格值,一字段一行,`parts.parameter`)。 * 顶部一条红带摆 S1 配方算好的 `gate_reason`(「还差什么」的一句话,配方 §see case-model.ts * 的 step④),够不够齐、该不该出现由样式表判(§5「模版不许算」)。 @@ -17,6 +18,17 @@ * 结论审核」一节);`landing-confirm`(件③ 落账确认)已由 Task 9 重做并交付(见文件尾 * 「局面:落账确认」一节)—— 三张件此刻齐了。 * + * **Task 7b(2026-08-29)补②a 块**:件① 交付时「新建类型」只有一句 guide 文字,没有可 + * 落笔的地方(类型下拉只能挑**已有**的)。零平台改动补齐(查证与三条候选路见 Task 7b 报告): + * `parts.chip_type` 与 `parts.type_param` 两张表新开局都是零行,`ViewContainer.addRow` + * 靠克隆最后一行添新行、零行时无样可克隆——两张表各加一个**不受 `data-rows` 管辖的 + * 静态种子行**(不经 `cell()`/`data-slot`,字面量手写 `data-col` 输入格,详见 + * `CHIP_TYPE_TABLE`/`TYPE_PARAM_TABLE` 头注)。查证过程中额外揪出并顺手治死两处平台级 + * 潜伏坑(`resolveRowKey` 的续号步骤抢在 `autoKeys()` 续号器前面 / 引用列声明成 `integer` + * 会把补零字符串的前导零削掉致使 `refersTo` 失配)——两者的共同根因是`parts.chip_type.type_id` + * 是本仓**唯一**「auto:true 又是 integer」的主键,改声明成 `text`(与其六处同一身份链路的 + * 引用列一起改)后两处坑自然绕开,详见 manifest.ts 该字段的大注释。 + * * 版面语言仍是「搬,不要照着做」(《HTML报告生成规范》§7):`P08_CSS`(逐字节抽自 * `docs/SS02-ACT-WORK-05-html/ACT-WORK-05-数据工单-01-芯片主数据创建.html`, * 见 `./target-style.ts` 头注)是**唯一**的样式来源 —— 三块内容原来分属 P-08 数据工单 / @@ -104,6 +116,15 @@ html.editable .jc-gapbar.v-0 { display: none; } * 用 id 选择器把计数器重新点亮(id 特异度压过 P08_CSS 的类选择器)。 */ #tbl1.on td.rn .jc-rn-num, #tbl3.on td.rn .jc-rn-num { display: inline; } +/* + * **新建行(Task 7b,件① 补「新建芯片类型」)**:parts.chip_type / parts.type_param + * 两张表新增一条**字面量静态行**(不走 data-rows,不受行数所限,ViewContainer.addRow + * 找不到 tr:last-of-type 可克隆时也照常在——这一条从来不指望被克隆出来,它本身就是种子)。 + * 只在编辑态露面:读的时候看见一行「系统生成 / 占位提示」的空壳子是界面撒谎的另一种形状。 + * table.on(与其余 .jc-cell/.jc-vcell 同一个开关)已经个别管住了里面每个输入格的显隐, + * 这条补的是**行本身**(#、静态提示文字这类不挂 .jc-cell 的内容不受那条规则约束)。 + */ +table:not(.on) tr.jc-row-static { display: none; } `; /* @@ -307,9 +328,66 @@ const PART_TABLE = `
`; /** - * 块② 芯片类型的核心参数字段清单 —— `parts.type_param`,复合主键 `type_id + param_name`。 - * **供新建类型时加字段行**(唯一带「+加一行」/删行的一张):类型下拉里没有合适的, - * 人就在这里把新类型的核心参数字段一条条定下来(名字 / 说明 / 硬软约束 / 单位)。 + * 块②a 新建芯片类型 —— `parts.chip_type`(Task 7b,2026-08-29 补齐)。类型下拉(块①)里 + * 没有合适的,人在这里现造一个:类型名称(必填)+ 说明(建议)。`type_id` 是自增主键 + * (`auto:true`,声明改 `text` 的理由见 manifest 那条大注释)—— **不进表单**,自增列 + * 不问人(与联系人编码 `contact_code` 同一条纪律,2026-08-02 立);只读态摆 `ro()`, + * 连孪生输入框都不给。 + * + * **零行照样加得出第一行**(这是这一块存在的全部理由):`mine:parts.chip_type` 每单 + * 开局是零行,`ViewContainer.addRow` 靠克隆 `tr:last-of-type` 添新行,零行时找不到可 + * 克隆的样子(查证见 Task 7b 报告)。这里**多一个不受 `data-rows` 管辖的静态 ``**, + * 里面字面量摆一行输入格(不经 `cell()`/`data-slot`,直接手写 ``)—— + * 它不是数据行,不受「零行」影响,永远在那儿;填了按保存,`writeEditedRows` 认得出 + * 「主键全空 + 别的列有值」这个形状(`mintable`),转给 `autoKeys()` 续号,和别处的 + * `contact_code` 走的是同一条路。填过一次之后 `mine:parts.chip_type` 就有行了, + * 上面那张动态表本身的「+加一行」(参见 addrow('parts.chip_type', …))也就跟着能用。 + * + * `data-edit="parts.chip_type"` **两处都写**——外层 `` 一处,内层 `data-rows` + * 那个 `` 一处:`renderRows` 判「这组行可编辑吗」只认与 `data-rows` **同一个标签** + * 自带的属性(不看祖先),内层那份是留给它看的;`ViewContainer.collect()`/`.on` 切换按 + * `document.querySelector('[data-edit="…"]')` 找**唯一**一个宿主,要同时收到「已有行」 + * 与「新建那一行」两处的输入,宿主必须是两者共同的祖先,外层那份是留给它看的。 + * `.on` 落点用 `host.closest('table')`——外层就是 `
` 自身,`closest` 命中自己, + * 落的还是同一张 `
`,与从前只在 `` 上挂 `data-edit` 时完全一致,CSS 不必改。 + */ +const CHIP_TYPE_TABLE = `
+ + + + + + + + + + + + + + + + + + +
#编号类型名称类型说明
${ro('type_id', 'mono')}${cell('type_name')}${cell('description')}
系统生成
`; + +/** + * 块②b 芯片类型的核心参数字段清单 —— `parts.type_param`,复合主键 `type_id + param_name`。 + * **供新建类型时加字段行**:块②a 新建的类型(或已有类型)在这里把核心参数字段一条条 + * 定下来(名字 / 说明 / 硬软约束 / 单位)。 + * + * **`type_id` 改成下拉(Task 7b)**:从前是裸文本输入(`cell('type_id','mono')`),等于 + * 要人记住/照抄一个此刻还不存在、要等保存那一刻才由 `autoKeys()` 铸出来的自增号—— + * 那是在问人一件他答不上来的事。改成与块①同款的 `optionSelect`(选项从 `parts.chip_type` + * 现读,人挑的是**类型名称**):块②a 新建的类型一旦保存,这份下拉的选项就会带着它 + * (`data-options` 现读,与写死选项那条老毛病同一课)——**先建类型、保存,新类型立刻 + * 出现在这份下拉里,不必谁记住那个号**,「新建类型 → 定它的字段」这条链路因此不需要 + * 同一次保存里就把父行的自增键传给子行(那条路要基座补一层「跨行自增键联动」, + * 查证过没有必要:换个问法——选下拉而不是记数字——链路一样打通,且零平台改动)。 + * + * 新增一行零行也加得出**的静态种子行**(与块②a 同一套手法,详细道理见那边的大注释)—— + * 这张表本就是**唯一带「+加一行」/删行的一张**,零行时那颗按钮同样够不到岸,种子行同样补。 * * **不挂 `id="tbl2"`**:P08_CSS 的 `#tbl2 th:nth-child(n)` 那几条列宽规则是按位置 * (nth-child)写死给旧版 `parts.parameter` 表(所属料号/参数名/值/单位/约束/说明,7 列)调的, @@ -317,10 +395,10 @@ const PART_TABLE = `
* 位置 5、6 恰好被那两条老规则套反(约束套上「单位」的宽度、参考单位套上「约束」的宽度)。 * 不挂 id 就不会继承这几条位置写死的规则,退回普通 `table`/`th`/`td` 基础样式。 */ -const TYPE_PARAM_TABLE = `
+const TYPE_PARAM_TABLE = `
- + @@ -328,12 +406,23 @@ const TYPE_PARAM_TABLE = `
#所属类型编号所属类型 参数名 参数说明 约束(硬/软)
- + + + + + + + + +
${cell('type_id', 'mono')} + + ${optionSelect('type_id', 'parts.chip_type', 'type_id', 'type_name', true)} + ${cell('param_name')} ${cell('param_desc')} ${badge('ct', 'constraint_type', true)}${select('constraint_type', [['hard', '硬约束'], ['soft', '软约束']], true)} ${cell('param_unit')}
${optionSelect('type_id', 'parts.chip_type', 'type_id', 'type_name', true)}
`; /** @@ -390,18 +479,23 @@ export const INPUT_SUPPLEMENT = doc({ '模板定位:本单是「数据工单」——目标台账 = 芯片主数据(P-08 主记录 + 标准化参数子表),核心参数字段清单挂在芯片类型字典上。', '触发原因:下面【要评估什么】里两行是本次评估要比对的原 PN 与候选 PN —— 至少有一颗不在库、两颗归的类型不一致、或必填格还空着,下面这条红带才会出现。', '已绑定:两颗料的内部料号已按启动参数铺进下表,不必你重填料号本身,只需核对 / 补齐其余各格;在库的料现值已回填,只是查漏补缺。', - '纪律:两颗料必须归到同一个芯片类型下才谈得上逐项比对;类型下拉里没有合适的就新建一个,并把它的核心参数字段一并定下来;查不到的参数值留空(会被红带点名),不要编一个值顶替。', + '纪律:两颗料必须归到同一个芯片类型下才谈得上逐项比对;类型下拉里没有合适的就在【芯片类型】区新建一个,并把它的核心参数字段一并定下来;查不到的参数值留空(会被红带点名),不要编一个值顶替。', ]) + gateBar() + zone('要评估什么', '两颗料的存在性与主数据(每行一颗,人不加行,*为必填列)', ZIC_TABLE) + editGate('parts.profile', '两颗料的主数据核对 / 补齐,点「编辑」开表') + gapbar('parts_profile', '必填') + PART_TABLE + - zone('芯片类型', '两颗料所归类型的核心参数字段清单(供新建类型时补齐,*为必填列)', ZIC_TABLE) + - editGate('parts.type_param', '字段名字 / 说明 / 硬软约束,点「编辑」核对;新建类型用「加一行」补字段') + + zone('芯片类型', '上面下拉选不到合适的类型,就在这里新建一个(选填,*为必填列)', ZIC_TABLE) + + editGate('parts.chip_type', '新建一个类型:表尾那一行填类型名称与说明,保存后立刻出现在上面的下拉里') + + gapbar('parts_chip_type', '必填') + + CHIP_TYPE_TABLE + + addrow('parts.chip_type', '+ 再建一个类型', '两颗料若都要新建、且类型不同,先各自保存,「加一行」能再建一个') + + zone('核心参数字段', '这个类型逐条比对时要看哪些参数(新建类型时必填,*为必填列)', ZIC_TABLE) + + editGate('parts.type_param', '字段名字 / 说明 / 硬软约束,点「编辑」核对;表尾那一行 / 「加一行」补字段') + gapbar('parts_type_param', '必填') + TYPE_PARAM_TABLE + - addrow('parts.type_param', '+ 加一行(补一个核心参数字段)', '所属类型编号 / 参数名 / 参数说明 / 约束类型必须写;参考单位选填') + + addrow('parts.type_param', '+ 加一行(补一个核心参数字段)', '所属类型 / 参数名 / 参数说明 / 约束类型必须写;参考单位选填') + zone('参数值', '逐条核心参数的实测 / 规格值(一字段一行,*为必填列;单位由字典定,这里不改)', ZIC_TABLE) + editGate('parts.parameter', '逐条参数值核对 / 补齐,点「编辑」开表') + gapbar('parts_parameter', '必填') +