三面拆分⑳:属性卡图纸化重排 —— 检验单判据、索引栏拣选、明细表映射(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
+96 -24
View File
@@ -557,36 +557,108 @@
.g-ver-act { border: 1px solid var(--cline); background: #fff; border-radius: 8px; padding: 3px 9px; font: inherit; font-size: 11px; font-weight: 600; color: var(--ink-2); cursor: pointer; } .g-ver-act { border: 1px solid var(--cline); background: #fff; border-radius: 8px; padding: 3px 9px; font: inherit; font-size: 11px; font-weight: 600; color: var(--ink-2); cursor: pointer; }
.g-ver-act:hover { border-color: #c9d6e6; } .g-ver-act:hover { border-color: #c9d6e6; }
.g-ver-act.t-go { color: var(--ok); border-color: color-mix(in srgb, var(--ok) 35%, var(--cline)); } .g-ver-act.t-go { color: var(--ok); border-color: color-mix(in srgb, var(--ok) 35%, var(--cline)); }
/* 异常看板:与版本卡同风(图纸式),行是红左线的方框;点一条定位到节点属性 */ /* ══ 下半区重排(2026-07-28):判据卡 = 检验单,属性卡 = 索引栏 + 明细区 ══
两卡同排 560 定高(行高由属性卡定,判据卡 grid 拉伸补齐),仍是图纸语言;
检验章(判据面全清的合格章)是唯一的戏眼,其余一律细线纪律。 */
/* ── 激活 · 双判据 → 检验单:上下两格判据面均分卡高,面头一行、面体列违规 ── */
.g-exboard { min-height: 0; }
.g-exboard .g-vercard-h { height: auto; padding: 13px 18px 10px; } .g-exboard .g-vercard-h { height: auto; padding: 13px 18px 10px; }
.g-vercard-list.g-exsheet { padding: 0; gap: 0; overflow: hidden; }
.g-exface { flex: 1 1 0; min-height: 0; display: flex; flex-direction: column; }
.g-exface + .g-exface { border-top: 1px solid var(--sh-line); }
.g-exfacet { flex: none; display: flex; align-items: center; gap: 7px; padding: 10px 16px 9px; font: 600 9.5px/1 var(--sh-mono, var(--mono)); letter-spacing: 0.14em; text-transform: uppercase; color: var(--sh-steel, #46586a); border-bottom: 1px dashed #e4e9ef; }
.g-exfacet.ok { color: #2e7d4f; }
.g-exfacet .st { margin-left: auto; letter-spacing: .1em; text-align: right; }
.g-exfacet:not(.ok) .st { color: #b0413e; }
.g-exfacet:not(.ok) svg { color: #b0413e; }
.g-exface-b { flex: 1; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: 7px; padding: 10px 14px 12px; }
/* 违规行:方角细线 + 红左线(压回全局 .g-ver 的圆角白卡版);点一条定位到节点属性 */
.g-exface-b .g-ver { border-radius: 0; border: 1px solid var(--sh-line); border-left: 3px solid #b0413e; padding: 7px 10px; font-family: var(--sh-mono); box-shadow: none; }
.g-exface-b .g-ver:hover { border-color: #aeb9c6; border-left-color: #b0413e; }
.g-exface-b .g-ver:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }
.g-ex { cursor: pointer; border-left-color: #b0413e; } .g-ex { cursor: pointer; border-left-color: #b0413e; }
.g-ex > b { font-size: 11px; } .g-ex > b { font-size: 11px; }
.g-ex .msg { flex: 1; min-width: 0; font-size: 10.5px; line-height: 1.55; letter-spacing: 0; text-transform: none; color: #3a4452; } .g-ex .msg { flex: 1; min-width: 0; font-size: 10.5px; line-height: 1.55; letter-spacing: 0; text-transform: none; color: #3a4452; }
.g-exfacet { display: flex; align-items: center; gap: 6px; padding: 6px 2px 2px; font: 600 9.5px/1 var(--mono); letter-spacing: 0.14em; text-transform: uppercase; color: var(--sh-steel, #46586a); } /* 检验章:该面清零即盖 —— 双线方框微旋,检验单的通过凭证;半透边线是印泥感 */
.g-exfacet.ok { color: #2e7d4f; } .g-exstamp { margin: auto; display: flex; flex-direction: column; align-items: center; gap: 5px; padding: 11px 20px 10px; border: 1.5px solid rgba(46, 125, 79, .55); outline: 1px solid rgba(46, 125, 79, .3); outline-offset: 3px; color: #2e7d4f; transform: rotate(-4deg); font: 700 12.5px/1.2 var(--sh-mono); letter-spacing: .34em; text-indent: .34em; }
.g-maprow { display: flex; align-items: center; gap: 6px; flex-wrap: nowrap; } .g-exstamp small { font-size: 7.5px; font-weight: 600; letter-spacing: .18em; text-indent: .18em; opacity: .7; }
.g-maprow .g-input { flex: 1 1 0; min-width: 0; } /* 第二面的章反向微旋 —— 两枚章不该是同一手势盖出来的 */
.g-maprow .g-select { flex: 0 0 112px; min-width: 0; } .g-exface + .g-exface .g-exstamp { transform: rotate(2.5deg); }
.g-maprow .g-btn { flex: none; padding: 4px 7px; } /* 裁定带:两面全清才出 —— 黄铜是「可生效」的颜色 */
.g-maparr { color: var(--sh-steel, #46586a); font-weight: 700; } .g-exverdict { flex: none; padding: 9px 16px 9px 13px; border-top: 1px solid var(--sh-line); border-left: 3px solid var(--sh-brass); background: var(--sh-brass-wash); font-family: var(--sh-mono); font-size: 10px; line-height: 1.8; color: var(--sh-brass); }
.g-exboard-empty { padding: 16px 18px; font-family: var(--sh-mono); font-size: 10px; line-height: 1.9; color: var(--sh-muted); } .g-exboard-empty { padding: 16px 18px; font-family: var(--sh-mono); font-size: 10px; line-height: 1.9; color: var(--sh-muted); }
/* 激活判据卡与节点属性卡**同排等高**(560px 定高):行高由属性卡定,看板拉伸补齐; /* ── 节点 · 属性 → 索引栏(固定)+ 明细区(滚动)────────────────────── */
清单超高在卡内滚,不把页面撑长 —— 定高卡片才是台面件,不是随内容乱长的段落 */
.g-exboard { min-height: 0; }
/* 节点属性卡:图纸卡(外壳复用 g-vercard),与流程图同宽;题头右侧是状态字 + 保存芯片 */
.g-nodecard { height: 560px; min-height: 0; } .g-nodecard { height: 560px; min-height: 0; }
.g-nodecard .g-vercard-h { height: auto; padding: 13px 18px 10px; } .g-nodecard .g-vercard-h { height: auto; padding: 13px 18px 10px; }
.g-nodecard .g-vercard-h .n { margin-left: auto; } .g-nodecard .g-vercard-h .n { margin-left: auto; letter-spacing: .1em; }
.g-nodecard .g-vercard-h .n.dirty { color: var(--sh-brass); }
.g-nodecard .g-vercard-h .g-ver-new { margin-left: 10px; } .g-nodecard .g-vercard-h .g-ver-new { margin-left: 10px; }
/* 内部三段(提示/节点条/属性格)统一 12px 呼吸 —— 从前是裸兄弟,三处贴死 */ .g-nodecard-b { flex: 1; min-height: 0; display: flex; flex-direction: column; gap: 0; padding: 0; overflow: hidden; }
.g-nodecard-b { flex: 1; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: 12px; padding: 14px 18px 16px; } .g-nodecard-b > .g-note { flex: none; margin: 10px 14px 0; }
/* 小卡与节点条也入图纸语言:方角细线;等高 250,超高的内容自己滚 */
.g-nodecard .g-pick { border-radius: 0; border-color: var(--sh-line); } /* 索引栏 = 图框分度带:浅底压一层,行首竖签(节点/边 + 计数),芯片缩成分度块 */
.g-nodecard .g-props { grid-template-columns: repeat(2, minmax(0, 1fr)); align-items: stretch; gap: 12px; } .g-pickrail { flex: none; background: #f9fafc; border-bottom: 1px solid var(--sh-line); }
/* 边属性:整宽单列 —— 条件与映射各占满一行,映射行才有地方一行排开 */ .g-pickrow { display: flex; align-items: flex-start; gap: 10px; padding: 8px 14px; }
.g-nodecard .g-props.g-eprops { grid-template-columns: minmax(0, 1fr); } .g-pickrow + .g-pickrow { border-top: 1px dashed #e4e9ef; }
.g-nodecard .g-prop { height: 250px; display: flex; flex-direction: column; overflow: hidden; border: 1px solid var(--sh-line); border-radius: 0; box-shadow: none; } .g-picklbl { flex: none; display: inline-flex; align-items: baseline; gap: 4px; min-width: 42px; padding-top: 6px; font: 600 9px/1 var(--sh-mono); letter-spacing: .14em; text-transform: uppercase; color: var(--sh-steel); }
.g-nodecard .g-prop-h { font-family: var(--sh-mono); font-size: 9.5px; font-weight: 600; letter-spacing: .12em; text-transform: uppercase; color: var(--sh-steel); border-bottom: 1px solid var(--sh-line); } .g-picklbl b { font-weight: 600; font-size: 9px; color: var(--sh-muted); }
.g-nodecard .g-prop-b { flex: 1; min-height: 0; overflow-y: auto; } .g-pickrow .g-picker { gap: 4px; }
.g-nodecard .g-pick { height: 22px; padding: 0 8px; gap: 5px; border-radius: 0; border-color: var(--sh-line); background: #fff; font-size: 11px; color: #3a4452; }
.g-nodecard .g-pick b { font-size: 9px; font-family: var(--sh-mono); }
.g-nodecard .g-pick:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }
/* 明细区:卡内唯一的滚动层 —— 索引在上钉住,拉表单不丢定位 */
.g-propsheet { flex: 1; min-height: 0; overflow-y: auto; padding: 12px 14px 14px; }
/* 空态 = 图纸空页:居中十字规两行说明,不再是一条飘着的灰带 */
.g-sheetempty { box-sizing: border-box; min-height: 100%; max-width: 480px; margin: 0 auto; padding: 20px 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 9px; text-align: center; }
.g-sheetempty svg { color: var(--sh-steel); opacity: .55; }
.g-sheetempty b { font-size: 12px; font-weight: 700; color: var(--sh-steel); }
.g-sheetempty p { margin: 0; font-size: 11px; line-height: 1.9; color: var(--sh-muted); }
/* 属性小卡:两列手排、按内容自高 —— 空白不再被 250 定高摊出来,长卡也不内滚;
左列 = 身份与数据口,右列 = 类型与装载(JSX 里 .g-propcol 手排,语义成组、高度可控) */
.g-nodecard .g-props { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); align-items: start; gap: 12px; }
.g-propcol { min-width: 0; display: flex; flex-direction: column; gap: 12px; }
.g-nodecard .g-props.g-eprops { display: flex; flex-direction: column; align-items: stretch; gap: 12px; }
.g-nodecard .g-prop { border: 1px solid var(--sh-line); border-radius: 0; box-shadow: none; }
.g-nodecard .g-prop-h { padding: 8px 12px; background: #fff; font: 600 9px/1.5 var(--sh-mono); letter-spacing: .12em; text-transform: uppercase; color: var(--sh-steel); border-bottom: 1px solid var(--sh-line); }
.g-nodecard .g-prop-h svg { color: var(--sh-steel); opacity: .6; }
.g-nodecard .g-prop-h .x { font-size: 8.5px; font-weight: 600; letter-spacing: .04em; color: var(--sh-muted); text-align: right; }
.g-nodecard .g-prop-b { padding: 11px 12px 13px; gap: 11px; }
/* 字段 = 图签小格:微墨签(mono 大写)+ 纸上填线(输入只留底线,聚焦底线转钢蓝) */
.g-nodecard .g-field { gap: 3px; }
.g-nodecard .g-field > em { font: 600 9px/1.6 var(--sh-mono); letter-spacing: .12em; text-transform: uppercase; color: var(--sh-steel); }
.g-nodecard .g-field > em i { font-family: var(--sans); font-size: 10px; font-weight: 400; letter-spacing: 0; text-transform: none; color: var(--sh-muted); }
.g-nodecard .g-input, .g-nodecard .g-select, .g-nodecard .g-ta { border: 0; border-bottom: 1px solid var(--sh-line); border-radius: 0; background-color: transparent; padding: 5px 2px; }
.g-nodecard .g-input:focus, .g-nodecard .g-select:focus, .g-nodecard .g-ta:focus { outline: 0; border-color: #4e7daf; box-shadow: 0 1px 0 0 #4e7daf; }
.g-nodecard .g-input[readonly] { background-color: transparent; color: var(--sh-muted); border-bottom-style: dashed; }
.g-nodecard .g-select { padding-right: 22px; background-position: right 8px center, right 3px center; }
.g-nodecard .g-seg { border-radius: 0; padding: 2px; background: #eef1f4; }
.g-nodecard .g-seg-b { border-radius: 0; height: 24px; font-size: 11px; }
.g-nodecard .g-seg-b.on { color: var(--sh-steel); box-shadow: 0 0 0 1px var(--sh-line); }
.g-nodecard .g-note { border-radius: 0; border: 1px solid var(--sh-line); border-left-width: 3px; border-left-color: var(--sh-steel); background: #fafbfd; font-size: 11px; line-height: 1.8; color: var(--sh-muted); }
.g-nodecard .g-note svg { color: var(--sh-steel); }
.g-nodecard .g-note.warn { border-color: #ecd9ab; border-left-color: var(--sh-brass); background: var(--sh-brass-wash); color: var(--sh-brass); }
.g-nodecard .g-note.warn svg { color: var(--sh-brass); }
.g-nodecard .g-chip { border-radius: 0; }
.g-nodecard .g-btn { border-radius: 0; border-color: var(--sh-line); color: var(--sh-steel); }
.g-nodecard .g-btn:hover { border-color: #9fb0c0; background: #f4f6f9; }
.g-nodecard .g-le-x { border-radius: 0; border-color: var(--sh-line); }
.g-nodecard .g-le-add { border-radius: 0; border-color: var(--sh-line); color: var(--sh-steel); }
.g-nodecard .g-le-add:hover { color: var(--sh-brass); border-color: var(--sh-brass); }
.g-nodecard .g-le-e { font: 600 9px/1.6 var(--sh-mono); letter-spacing: .12em; text-transform: uppercase; color: var(--sh-muted); }
/* 映射 = 明细表:mono 列头一行,行与列头同一 6 列栅格;
非「上游输出口」的来源没有独立口名 —— 值输入在口/路径两列上合并(.wide) */
.g-maphead, .g-maprow { display: grid; grid-template-columns: minmax(0, 1fr) 16px 104px minmax(0, .72fr) minmax(0, 1fr) 26px; gap: 6px; align-items: center; }
.g-maphead { font: 600 8.5px/1 var(--sh-mono); letter-spacing: .12em; text-transform: uppercase; color: var(--sh-muted); padding-bottom: 5px; border-bottom: 1px solid var(--sh-line); margin-bottom: -4px; }
.g-maphead .a { text-align: center; }
.g-maprow .g-input, .g-maprow .g-select { min-width: 0; }
.g-maprow .wide { grid-column: 4 / 6; }
.g-maprow > .g-chip { grid-column: 3 / 6; justify-self: start; }
.g-maprow > .g-btn { grid-column: 6; justify-self: stretch; padding: 4px 0; display: inline-flex; align-items: center; justify-content: center; }
.g-maparr { color: var(--sh-steel, #46586a); font-weight: 700; text-align: center; }
@@ -105,7 +105,16 @@ function EdgeProps({ edge, onPatch }: { edge: FlowEdge; onPatch: (f: (e: FlowEdg
/> />
</Field> </Field>
</Prop> </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) => { {rows.map((r, i) => {
const known = SOURCE_KINDS.some((k) => k.k === r.source.from); const known = SOURCE_KINDS.some((k) => k.k === r.source.from);
return ( return (
@@ -161,7 +170,7 @@ function EdgeProps({ edge, onPatch }: { edge: FlowEdge; onPatch: (f: (e: FlowEdg
)} )}
{r.source.from === 'initiation' && ( {r.source.from === 'initiation' && (
<input <input
className="g-input mono" className="g-input mono wide"
value={String(r.source['path'] ?? '')} value={String(r.source['path'] ?? '')}
placeholder="起单字段路径" placeholder="起单字段路径"
onChange={(e) => patchRow(i, (x) => ({ ...x, source: { ...x.source, path: e.target.value } }))} 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' && ( {r.source.from === 'constant' && (
<input <input
className="g-input mono" className="g-input mono wide"
value={String(r.source['value'] ?? '')} value={String(r.source['value'] ?? '')}
placeholder="常量值" placeholder="常量值"
onChange={(e) => patchRow(i, (x) => ({ ...x, source: { ...x.source, value: e.target.value } }))} 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"> <aside className="g-vercard g-exboard">
<div className="g-vercard-h">激活 · 双判据</div> <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> <div className="g-exboard-empty">校验中…</div>
) : ( </div>
(() => { ) : (
/* 能不能激活看两面:①图编排闭合完善 ②活动装载。unknown-activity 归装载,其余归编排。 */ (() => {
const loading = activeReport.violations.filter((v) => v.kind === 'unknown-activity'); /* 能不能激活看两面:①图编排闭合完善 ②活动装载。unknown-activity 归装载,其余归编排。 */
const drawing = activeReport.violations.filter((v) => v.kind !== 'unknown-activity'); const loading = activeReport.violations.filter((v) => v.kind === 'unknown-activity');
const row = (v: (typeof activeReport.violations)[number], i: number) => { const drawing = activeReport.violations.filter((v) => v.kind !== 'unknown-activity');
const nid = v.nodeId ?? pkg.edges.find((e) => e.id === v.edgeId)?.to; const row = (v: (typeof activeReport.violations)[number], i: number) => {
return ( const nid = v.nodeId ?? pkg.edges.find((e) => e.id === v.edgeId)?.to;
<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 ( 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' : '')}> <div className={'g-exfacet' + (drawing.length === 0 ? ' ok' : '')}>
<Ico name={drawing.length === 0 ? 'circle-check' : 'triangle-alert'} s={12} /> <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> </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' : '')}> <div className={'g-exfacet' + (loading.length === 0 ? ' ok' : '')}>
<Ico name={loading.length === 0 ? 'circle-check' : 'triangle-alert'} s={12} /> <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> </div>
{loading.map(row)} <div className="g-exface-b">
{activeReport.violations.length === 0 && ( {loading.length === 0 ? (
<div className="g-exboard-empty"> <div className="g-exstamp">
两面都齐 —— v{pkg.version} 可运行。 装载齐备<small>CHK·2 — IMPL LINKED</small>
{pkg.version === activeVersion ? '这一版正在生效。' : '版本行的「激活」已亮,点它即可上线。'} </div>
</div> ) : (
)} loading.map(row)
</> )}
); </div>
})() </section>
)} {activeReport.violations.length === 0 && (
</div> <div className="g-exverdict">
两面都齐 —— v{pkg.version} 可运行。
{pkg.version === activeVersion ? '这一版正在生效。' : '版本行的「激活」已亮,点它即可上线。'}
</div>
)}
</div>
);
})()
)}
<div className="g-vercard-f">点一条定位到属性去补;两面清零,激活才亮。</div> <div className="g-vercard-f">点一条定位到属性去补;两面清零,激活才亮。</div>
</aside> </aside>
@@ -895,7 +927,7 @@ function FlowLab({
<div className="g-vercard g-nodecard"> <div className="g-vercard g-nodecard">
<div className="g-vercard-h"> <div className="g-vercard-h">
节点 · 属性 节点 · 属性
<span className="n">{dirty ? '有改动待保存' : '点节点就地填写'}</span> <span className={'n' + (dirty ? ' dirty' : '')}>{dirty ? '有改动待保存' : '点节点就地填写'}</span>
<button <button
className="g-ver-new" className="g-ver-new"
disabled={!dirty || save.isPending} disabled={!dirty || save.isPending}
@@ -913,38 +945,57 @@ function FlowLab({
{saveErr} {saveErr}
</Note> </Note>
)} )}
<div className="g-picker"> {/* 索引栏 = 图框分度带:节点行 + 边行,行首竖签;固定不滚,属性区在下面自己滚 */}
{nodes <div className="g-pickrail">
.filter((n) => n.kind !== 'start' && n.kind !== 'end' && n.kind !== 'end-bad') <div className="g-pickrow">
.map((n) => ( <span className="g-picklbl">
<button 节点<b>{nodes.filter((n) => n.kind !== 'start' && n.kind !== 'end' && n.kind !== 'end-bad').length}</b>
key={n.id} </span>
className={`g-pick t-${kindTone(n)}` + (sel === n.id ? ' on' : '')} <div className="g-picker">
onClick={() => { setSel(sel === n.id ? null : n.id); setSelEdge(null); }} {nodes
> .filter((n) => n.kind !== 'start' && n.kind !== 'end' && n.kind !== 'end-bad')
<b className="g-mono">{n.id}</b> .map((n) => (
{n.label} <button
</button> key={n.id}
))} className={`g-pick t-${kindTone(n)}` + (sel === n.id ? ' on' : '')}
</div> onClick={() => { setSel(sel === n.id ? null : n.id); setSelEdge(null); }}
{/* 边拣选条:边号立牌(E1、E2…),条件住属性面不上按钮 —— 图只定顺序,契约在属性里填 */} >
<div className="g-picker"> <b className="g-mono">{n.id}</b>
{(graph?.edges ?? []).map((e, i) => ( {n.label}
<button </button>
key={`${e.from}→${e.to}-${i}`} ))}
className={'g-pick t-mute' + (selEdge === i ? ' on' : '')} </div>
onClick={() => { setSelEdge(selEdge === i ? null : i); setSel(null); }} </div>
> {/* 边行:边号立牌(E1、E2…),条件住属性面不上按钮 —— 图只定顺序,契约在属性里填 */}
<b className="g-mono">{e.code ?? `边${i + 1}`}</b> <div className="g-pickrow">
{e.label ?? ''} <span className="g-picklbl">
</button> 边<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> </div>
{/* key 随选中对象换 —— 换一件看,明细区滚回顶,不带上一件的滚动位 */}
<div className="g-propsheet" key={node !== null ? `n:${node.id}` : selEdge !== null ? `e:${selEdge}` : 'blank'}>
{node === null && selEdge === null ? ( {node === null && selEdge === null ? (
<Note> <div className="g-sheetempty">
点画布或上面的拣选条选一件 —— 节点填岗位与链接实现,边填传递契约(条件 + 映射)。 <Ico name="target" s={20} />
审批裁决(verdict)与放行选口(outcome)由系统注入判据,不用你声明。 <b>点画布或拣选条,选一个节点或一条边</b>
</Note> <p>
节点填名称与链接实现;边填传递契约(条件 + 映射)。负责人运营时分配,不在这儿。
审批裁决(verdict)与放行选口(outcome)由系统注入判据,不用你声明。
</p>
</div>
) : node === null && selEdge !== null && graph !== null ? ( ) : node === null && selEdge !== null && graph !== null ? (
<EdgeProps <EdgeProps
edge={graph.edges[selEdge]!} edge={graph.edges[selEdge]!}
@@ -957,7 +1008,10 @@ function FlowLab({
/> />
) : node === null ? null : ( ) : node === null ? null : (
<> <>
{/* 两列手排(不用自动砌体):左列 = 身份与数据口,右列 = 类型与装载 ——
卡数随节点种类变(审批点无装载卡),手排保证两列始终语义成组、高度可控 */}
<div className="g-props"> <div className="g-props">
<div className="g-propcol">
<Prop ico="tag" title="身份与命名" extra={kindLabel(node)}> <Prop ico="tag" title="身份与命名" extra={kindLabel(node)}>
<Field k="节点 ID" hint="编排内唯一,边映射按它寻址"> <Field k="节点 ID" hint="编排内唯一,边映射按它寻址">
<input className="g-input mono" value={node.id} readOnly /> <input className="g-input mono" value={node.id} readOnly />
@@ -984,6 +1038,27 @@ function FlowLab({
)} )}
</Prop> </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 <Prop
ico="user-check" ico="user-check"
title="类型与责任" title="类型与责任"
@@ -1022,24 +1097,6 @@ function FlowLab({
)} )}
</Prop> </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' && ( {node.kind === 'activity' && node.flavor !== 'approval' && (
<Prop ico="boxes" title="链接活动实现" extra="装载面:编排不含实现,链接决定可运行"> <Prop ico="boxes" title="链接活动实现" extra="装载面:编排不含实现,链接决定可运行">
<Field k="实现" hint="活动住库;链好了这节点才算装载(激活判据之二)"> <Field k="实现" hint="活动住库;链好了这节点才算装载(激活判据之二)">
@@ -1083,9 +1140,11 @@ function FlowLab({
)} )}
</Prop> </Prop>
)} )}
</div>
</div> </div>
</> </>
)} )}
</div>
</div> </div>
</div> </div>
</div> </div>