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