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'];