三面拆分⑳:属性卡图纸化重排 —— 检验单判据、索引栏拣选、明细表映射(SubAgent 设计)

frontend-design 技能专项重排流程包详情下半区三卡(仅呈现,零逻辑改动):

- 激活双判据卡 → **检验单**:判据一·图编排 / 判据二·活动装载两个检验格,
  各带面头状态字;全清盖检验章(编排闭合 CHK·1 / 装载齐备 CHK·2,反向微旋),
  两面全清出黄铜裁定带「两面都齐 —— vN 可运行」;违规行红左线方角压回
- 节点属性卡 → **索引栏 + 明细区**:节点/边两行分度带(行首 mono 竖签带计数,
  22px 方角芯片,行间虚线),下方唯一滚动层;空态改居中图纸空页
- 节点态两列手排(语义成组):身份与命名+输入+输出口 | 类型与责任+链接活动实现;
  小卡按内容自高(去 250 一刀切),卡头 mono 微签,字段改**底线填写式**
  (mono 大写微签标签、readonly 虚线底、聚焦转钢蓝)
- 边态映射卡加 mono 列头(下游字段/←/来源/口名/路径·值),
  与行共用 6 列 grid,起单载荷/常量的值输入跨列合并 —— BOM 式明细表
- 钩子类名(g-pick/g-ver/g-exfacet/g-props/g-maprow)全保留;
  顺手清一句过时空态文案(「节点填岗位」→ 负责人运营时分配)

三态截图核对(空态检验章/A1 两列/E7 明细表)+ 五闸退出码全过。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
2026-07-28 22:22:02 +08:00
co-authored by Claude Fable 5
parent f145bde875
commit 967627d753
2 changed files with 250 additions and 119 deletions
@@ -105,7 +105,16 @@ function EdgeProps({ edge, onPatch }: { edge: FlowEdge; onPatch: (f: (e: FlowEdg
/>
</Field>
</Prop>
<Prop ico="git-merge" title="映射 · 载荷装配" extra="下游字段 ← 来源;必填映射齐了这条边才闭合">
<Prop ico="git-merge" title="映射 · 载荷装配" extra="必填映射齐了这条边才闭合">
{/* 明细表列头:行与列头同一 6 列栅格;非「上游输出口」的来源在口/路径两列上合并 */}
<div className="g-maphead" aria-hidden="true">
<span>下游字段</span>
<span className="a">←</span>
<span>来源</span>
<span>口名</span>
<span>路径 · 值</span>
<span />
</div>
{rows.map((r, i) => {
const known = SOURCE_KINDS.some((k) => k.k === r.source.from);
return (
@@ -161,7 +170,7 @@ function EdgeProps({ edge, onPatch }: { edge: FlowEdge; onPatch: (f: (e: FlowEdg
)}
{r.source.from === 'initiation' && (
<input
className="g-input mono"
className="g-input mono wide"
value={String(r.source['path'] ?? '')}
placeholder="起单字段路径"
onChange={(e) => patchRow(i, (x) => ({ ...x, source: { ...x.source, path: e.target.value } }))}
@@ -169,7 +178,7 @@ function EdgeProps({ edge, onPatch }: { edge: FlowEdge; onPatch: (f: (e: FlowEdg
)}
{r.source.from === 'constant' && (
<input
className="g-input mono"
className="g-input mono wide"
value={String(r.source['value'] ?? '')}
placeholder="常量值"
onChange={(e) => patchRow(i, (x) => ({ ...x, source: { ...x.source, value: e.target.value } }))}
@@ -837,57 +846,80 @@ function FlowLab({
*/}
<aside className="g-vercard g-exboard">
<div className="g-vercard-h">激活 · 双判据</div>
<div className="g-vercard-list">
{activeReport === undefined ? (
{activeReport === undefined ? (
<div className="g-vercard-list g-exsheet">
<div className="g-exboard-empty">校验中…</div>
) : (
(() => {
/* 能不能激活看两面:①图编排闭合完善 ②活动装载。unknown-activity 归装载,其余归编排。 */
const loading = activeReport.violations.filter((v) => v.kind === 'unknown-activity');
const drawing = activeReport.violations.filter((v) => v.kind !== 'unknown-activity');
const row = (v: (typeof activeReport.violations)[number], i: number) => {
const nid = v.nodeId ?? pkg.edges.find((e) => e.id === v.edgeId)?.to;
return (
<div
key={`${v.kind}-${i}`}
className="g-ver g-ex"
role="button"
tabIndex={0}
onClick={() => {
if (nid !== undefined) { setSel(String(nid)); setSelEdge(null); }
}}
onKeyDown={(e) => {
if (e.key === 'Enter' && nid !== undefined) { setSel(String(nid)); setSelEdge(null); }
}}
>
{nid !== undefined && <b>{String(nid)}</b>}
<span className="msg">{v.message}</span>
</div>
);
};
</div>
) : (
(() => {
/* 能不能激活看两面:①图编排闭合完善 ②活动装载。unknown-activity 归装载,其余归编排。 */
const loading = activeReport.violations.filter((v) => v.kind === 'unknown-activity');
const drawing = activeReport.violations.filter((v) => v.kind !== 'unknown-activity');
const row = (v: (typeof activeReport.violations)[number], i: number) => {
const nid = v.nodeId ?? pkg.edges.find((e) => e.id === v.edgeId)?.to;
return (
<>
<div
key={`${v.kind}-${i}`}
className="g-ver g-ex"
role="button"
tabIndex={0}
onClick={() => {
if (nid !== undefined) { setSel(String(nid)); setSelEdge(null); }
}}
onKeyDown={(e) => {
if (e.key === 'Enter' && nid !== undefined) { setSel(String(nid)); setSelEdge(null); }
}}
>
{nid !== undefined && <b>{String(nid)}</b>}
<span className="msg">{v.message}</span>
</div>
);
};
/* 检验单式:两格判据面均分卡高,面体列违规行;全清的面盖检验章,两面全清出裁定带 */
return (
<div className="g-vercard-list g-exsheet">
<section className="g-exface">
<div className={'g-exfacet' + (drawing.length === 0 ? ' ok' : '')}>
<Ico name={drawing.length === 0 ? 'circle-check' : 'triangle-alert'} s={12} />
图编排 {drawing.length === 0 ? '闭合' : `${drawing.length} 项待补`}
判据一 · 图编排
<span className="st">{drawing.length === 0 ? '闭合' : `${drawing.length} 项待补`}</span>
</div>
{drawing.map(row)}
<div className="g-exface-b">
{drawing.length === 0 ? (
<div className="g-exstamp">
编排闭合<small>CHK·1 — GRAPH CLOSED</small>
</div>
) : (
drawing.map(row)
)}
</div>
</section>
<section className="g-exface">
<div className={'g-exfacet' + (loading.length === 0 ? ' ok' : '')}>
<Ico name={loading.length === 0 ? 'circle-check' : 'triangle-alert'} s={12} />
活动装载 {loading.length === 0 ? '齐' : `${loading.length} 个节点未链接实现`}
判据二 · 活动装载
<span className="st">{loading.length === 0 ? '齐' : `${loading.length} 个未链接实现`}</span>
</div>
{loading.map(row)}
{activeReport.violations.length === 0 && (
<div className="g-exboard-empty">
两面都齐 —— v{pkg.version} 可运行。
{pkg.version === activeVersion ? '这一版正在生效。' : '版本行的「激活」已亮,点它即可上线。'}
</div>
)}
</>
);
})()
)}
</div>
<div className="g-exface-b">
{loading.length === 0 ? (
<div className="g-exstamp">
装载齐备<small>CHK·2 — IMPL LINKED</small>
</div>
) : (
loading.map(row)
)}
</div>
</section>
{activeReport.violations.length === 0 && (
<div className="g-exverdict">
两面都齐 —— v{pkg.version} 可运行。
{pkg.version === activeVersion ? '这一版正在生效。' : '版本行的「激活」已亮,点它即可上线。'}
</div>
)}
</div>
);
})()
)}
<div className="g-vercard-f">点一条定位到属性去补;两面清零,激活才亮。</div>
</aside>
@@ -895,7 +927,7 @@ function FlowLab({
<div className="g-vercard g-nodecard">
<div className="g-vercard-h">
节点 · 属性
<span className="n">{dirty ? '有改动待保存' : '点节点就地填写'}</span>
<span className={'n' + (dirty ? ' dirty' : '')}>{dirty ? '有改动待保存' : '点节点就地填写'}</span>
<button
className="g-ver-new"
disabled={!dirty || save.isPending}
@@ -913,38 +945,57 @@ function FlowLab({
{saveErr}
</Note>
)}
<div className="g-picker">
{nodes
.filter((n) => n.kind !== 'start' && n.kind !== 'end' && n.kind !== 'end-bad')
.map((n) => (
<button
key={n.id}
className={`g-pick t-${kindTone(n)}` + (sel === n.id ? ' on' : '')}
onClick={() => { setSel(sel === n.id ? null : n.id); setSelEdge(null); }}
>
<b className="g-mono">{n.id}</b>
{n.label}
</button>
))}
</div>
{/* 边拣选条:边号立牌(E1、E2…),条件住属性面不上按钮 —— 图只定顺序,契约在属性里填 */}
<div className="g-picker">
{(graph?.edges ?? []).map((e, i) => (
<button
key={`${e.from}→${e.to}-${i}`}
className={'g-pick t-mute' + (selEdge === i ? ' on' : '')}
onClick={() => { setSelEdge(selEdge === i ? null : i); setSel(null); }}
>
<b className="g-mono">{e.code ?? `边${i + 1}`}</b>
{e.label ?? ''}
</button>
))}
{/* 索引栏 = 图框分度带:节点行 + 边行,行首竖签;固定不滚,属性区在下面自己滚 */}
<div className="g-pickrail">
<div className="g-pickrow">
<span className="g-picklbl">
节点<b>{nodes.filter((n) => n.kind !== 'start' && n.kind !== 'end' && n.kind !== 'end-bad').length}</b>
</span>
<div className="g-picker">
{nodes
.filter((n) => n.kind !== 'start' && n.kind !== 'end' && n.kind !== 'end-bad')
.map((n) => (
<button
key={n.id}
className={`g-pick t-${kindTone(n)}` + (sel === n.id ? ' on' : '')}
onClick={() => { setSel(sel === n.id ? null : n.id); setSelEdge(null); }}
>
<b className="g-mono">{n.id}</b>
{n.label}
</button>
))}
</div>
</div>
{/* 边行:边号立牌(E1、E2…),条件住属性面不上按钮 —— 图只定顺序,契约在属性里填 */}
<div className="g-pickrow">
<span className="g-picklbl">
边<b>{(graph?.edges ?? []).length}</b>
</span>
<div className="g-picker">
{(graph?.edges ?? []).map((e, i) => (
<button
key={`${e.from}→${e.to}-${i}`}
className={'g-pick t-mute' + (selEdge === i ? ' on' : '')}
onClick={() => { setSelEdge(selEdge === i ? null : i); setSel(null); }}
>
<b className="g-mono">{e.code ?? `边${i + 1}`}</b>
{e.label ?? ''}
</button>
))}
</div>
</div>
</div>
{/* key 随选中对象换 —— 换一件看,明细区滚回顶,不带上一件的滚动位 */}
<div className="g-propsheet" key={node !== null ? `n:${node.id}` : selEdge !== null ? `e:${selEdge}` : 'blank'}>
{node === null && selEdge === null ? (
<Note>
点画布或上面的拣选条选一件 —— 节点填岗位与链接实现,边填传递契约(条件 + 映射)。
审批裁决(verdict)与放行选口(outcome)由系统注入判据,不用你声明。
</Note>
<div className="g-sheetempty">
<Ico name="target" s={20} />
<b>点画布或拣选条,选一个节点或一条边</b>
<p>
节点填名称与链接实现;边填传递契约(条件 + 映射)。负责人运营时分配,不在这儿。
审批裁决(verdict)与放行选口(outcome)由系统注入判据,不用你声明。
</p>
</div>
) : node === null && selEdge !== null && graph !== null ? (
<EdgeProps
edge={graph.edges[selEdge]!}
@@ -957,7 +1008,10 @@ function FlowLab({
/>
) : node === null ? null : (
<>
{/* 两列手排(不用自动砌体):左列 = 身份与数据口,右列 = 类型与装载 ——
卡数随节点种类变(审批点无装载卡),手排保证两列始终语义成组、高度可控 */}
<div className="g-props">
<div className="g-propcol">
<Prop ico="tag" title="身份与命名" extra={kindLabel(node)}>
<Field k="节点 ID" hint="编排内唯一,边映射按它寻址">
<input className="g-input mono" value={node.id} readOnly />
@@ -984,6 +1038,27 @@ function FlowLab({
)}
</Prop>
{node.kind === 'activity' && (
<Prop ico="git-merge" title="输入" extra="上游边映射的落点">
<ListEdit
items={node.in ?? []}
placeholder="如:客户邮件"
onChange={(v) => patch((n) => ({ ...n, in: v.length > 0 ? [...v] : undefined }))}
/>
</Prop>
)}
{node.kind === 'activity' && (
<Prop ico="upload" title="输出口" extra="下游边映射的源">
<ListEdit
items={node.out ?? []}
placeholder="如:受理记录"
onChange={(v) => patch((n) => ({ ...n, out: v.length > 0 ? [...v] : undefined }))}
/>
</Prop>
)}
</div>
<div className="g-propcol">
<Prop
ico="user-check"
title="类型与责任"
@@ -1022,24 +1097,6 @@ function FlowLab({
)}
</Prop>
{node.kind === 'activity' && (
<Prop ico="git-merge" title="输入" extra="上游边映射的落点">
<ListEdit
items={node.in ?? []}
placeholder="如:客户邮件"
onChange={(v) => patch((n) => ({ ...n, in: v.length > 0 ? [...v] : undefined }))}
/>
</Prop>
)}
{node.kind === 'activity' && (
<Prop ico="upload" title="输出口" extra="下游边映射的源">
<ListEdit
items={node.out ?? []}
placeholder="如:受理记录"
onChange={(v) => patch((n) => ({ ...n, out: v.length > 0 ? [...v] : undefined }))}
/>
</Prop>
)}
{node.kind === 'activity' && node.flavor !== 'approval' && (
<Prop ico="boxes" title="链接活动实现" extra="装载面:编排不含实现,链接决定可运行">
<Field k="实现" hint="活动住库;链好了这节点才算装载(激活判据之二)">
@@ -1083,9 +1140,11 @@ function FlowLab({
)}
</Prop>
)}
</div>
</div>
</>
)}
</div>
</div>
</div>
</div>