流程详情:版本卡与图平齐 + 显式「新建版本」+ 节点属性成卡

三处走查反馈:

① 版本列改成**固定高度卡片,与右侧的图等高**(grid stretch):
   头部 = 标题 + 版本数 + 「新建版本」;中段版本行,多了在卡内滚,不把页面撑长;
   卡尾一句常驻说明(恰一个生效;在途按启动版本跑完)。实测两侧 591px 对 591px,顶对齐。

② 版本从此有**显式的新建入口** —— 之前只能靠改图保存来隐式长版本。
   「新建版本」= 以当前看的版本为底克隆一版(服务端定版号并即刻生效)。
   与「激活」分工明确:激活只是切生效指针,不长版本;新建才长 ——
   把旧版本的画法复活成最新版、或在现状上另起一版接着画,都走这儿。
   画布有未保存改动时确认卡会明说「克隆不带未保存的改动」。
   实测:以 v3 为底建 v4 → v4 生效、v3 自动停用、列表实时刷新。

③ 节点属性从裸块改成**卡片**(与页面其它卡同一形制),标题右侧带状态
   (只读 / 有改动待保存 / 点节点就地填写)。

typecheck ✓ lint ✓ depcruise ✓ api:check ✓ 498 测试 ✓

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
2026-07-28 14:50:08 +08:00
co-authored by Claude Opus 5
parent 710946e729
commit ff114a2246
2 changed files with 121 additions and 67 deletions
+9 -2
View File
@@ -507,8 +507,15 @@
/* ── 流程详情:左版本列 + 右画布(补充)────────────────────────────── /* ── 流程详情:左版本列 + 右画布(补充)──────────────────────────────
版本收在流程内部:左列是这条流的全部版本(恰一个生效),点谁看谁的图; 版本收在流程内部:左列是这条流的全部版本(恰一个生效),点谁看谁的图;
属性区从右侧窄栏挪到图下整行(g-wide),声明式契约细节再往下。 */ 属性区从右侧窄栏挪到图下整行(g-wide),声明式契约细节再往下。 */
.g-flowlab { display: grid; grid-template-columns: minmax(220px, 1fr) minmax(0, 2.2fr); gap: 12px; align-items: start; } .g-flowlab { display: grid; grid-template-columns: minmax(220px, 1fr) minmax(0, 2.2fr); gap: 12px; align-items: stretch; }
.g-vers { position: sticky; top: 2px; display: flex; flex-direction: column; gap: 8px; } /* 版本卡:与右侧的图**等高**(grid stretch),版本多了在卡内滚,不把页面撑长 */
.g-vercard { display: flex; flex-direction: column; min-height: 0; background: var(--card); border: 1px solid var(--cline); border-radius: 13px; box-shadow: var(--cshadow); overflow: hidden; }
.g-vercard-h { flex: none; display: flex; align-items: center; gap: 8px; padding: 11px 13px; border-bottom: 1px solid var(--line-soft); font-size: 12.5px; font-weight: 800; color: var(--ink-2); }
.g-vercard-h svg { color: var(--ink-4); }
.g-vercard-h .n { font-family: var(--mono); font-size: 11px; font-weight: 700; color: var(--ink-4); }
.g-vercard-h .g-ver-act { margin-left: auto; display: inline-flex; align-items: center; gap: 4px; }
.g-vercard-list { flex: 1; min-height: 0; overflow-y: auto; padding: 10px; display: flex; flex-direction: column; gap: 8px; }
.g-vercard-f { flex: none; padding: 9px 13px; border-top: 1px dashed var(--line-soft); font-size: 11px; line-height: 1.6; color: var(--ink-3); }
.g-ver { display: flex; align-items: center; gap: 8px; padding: 10px 12px; background: var(--card); border: 1px solid var(--cline); border-radius: 11px; cursor: pointer; font: inherit; text-align: left; transition: border-color var(--t-fast), box-shadow var(--t-fast); } .g-ver { display: flex; align-items: center; gap: 8px; padding: 10px 12px; background: var(--card); border: 1px solid var(--cline); border-radius: 11px; cursor: pointer; font: inherit; text-align: left; transition: border-color var(--t-fast), box-shadow var(--t-fast); }
.g-ver:hover { border-color: #c9d6e6; } .g-ver:hover { border-color: #c9d6e6; }
.g-ver.on { border-color: var(--brand); box-shadow: 0 0 0 2px var(--brand-wash); } .g-ver.on { border-color: var(--brand); box-shadow: 0 0 0 2px var(--brand-wash); }
@@ -435,6 +435,48 @@ function FlowLab({
}, },
}); });
/**
* 新建版本 = **以当前看的版本为底克隆一版**(服务端定版号并即刻生效)。
* 与「激活」分工明确:激活只是把生效指针切过去,不产生新版本;
* 新建版本才长一版 —— 把旧定义复活成最新版、或在现状上另起一版接着画,都走这儿。
*/
const cloneVersion = useMutation({
mutationFn: () => call<{ pkg: ProcessPackage; report: ClosureReport }>(saveProcessRoute, { body: { pkg } }),
onSuccess: (out) => {
setReport(out.report);
void queryClient.invalidateQueries({ queryKey: ['gov', 'process'] });
void queryClient.invalidateQueries({ queryKey: ['gov', 'process-reports'] });
setAsk(undefined);
onSaved();
},
});
const askClone = () =>
setAsk({
spec: {
title: `新建版本:以 v${pkg.version} 为底 → v${nextVersion}`,
note:
dirty && !readOnly ? (
<Note tone="warn">
画布上有<b>未保存的改动</b>,新建版本不会带上它们(克隆的是已存的 v{pkg.version})。
想连改动一起走,用下面的「保存并生效」。
</Note>
) : (
<Note>
把 v{pkg.version} 的定义原样复制成 v{nextVersion} 并即刻生效
{readOnly ? ' —— 这就是「把旧版本的画法复活成最新版」的路' : ',再在新版上接着画'}。
</Note>
),
impact: {
who: `此后新起的工作流走 v${nextVersion}`,
undo: '版本并存,可把生效切回任一版',
when: '确认即建并生效',
},
okLabel: `建 v${nextVersion} 并生效`,
onOk: () => cloneVersion.mutate(),
},
});
const askActivate = (v: ProcessPackage) => const askActivate = (v: ProcessPackage) =>
setAsk({ setAsk({
spec: { spec: {
@@ -588,69 +630,73 @@ function FlowLab({
} }
> >
<div className="g-flowlab"> <div className="g-flowlab">
<aside className="g-vers"> <aside className="g-vercard">
<div className="g-side-h"> <div className="g-vercard-h">
<Ico name="history" s={13} /> <Ico name="history" s={13} />
版本 <b>版本</b>
<span className="x">恰一个生效 · 切换不动在途</span> <span className="n">{versions.length}</span>
<button className="g-ver-act t-go" onClick={askClone} disabled={cloneVersion.isPending}>
<Ico name="plus" s={12} /> 新建版本
</button>
</div> </div>
{versions.map((v) => { <div className="g-vercard-list">
const isActive = v.version === activeVersion; {versions.map((v) => {
const isShown = v.version === pkg.version; const isActive = v.version === activeVersion;
return ( const isShown = v.version === pkg.version;
<div return (
key={v.version} <div
className={'g-ver' + (isShown ? ' on' : '') + (isActive ? ' t-live' : '')} key={v.version}
role="button" className={'g-ver' + (isShown ? ' on' : '')}
tabIndex={0} role="button"
onClick={() => onPick(v.version)} tabIndex={0}
onKeyDown={(e) => { onClick={() => onPick(v.version)}
if (e.key === 'Enter') onPick(v.version); onKeyDown={(e) => {
}} if (e.key === 'Enter') onPick(v.version);
> }}
<b>v{v.version}</b> >
{isActive ? ( <b>v{v.version}</b>
<St tone="ok" ico="circle-check">
生效中
</St>
) : (
<St tone="mute" ico="pause">
停用
</St>
)}
<span className="m">
<span className="cnt">
{v.nodes.length} 节点 · {v.edges.length} 边
</span>
{isActive ? ( {isActive ? (
<button <St tone="ok" ico="circle-check">
className="g-ver-act" 生效中
onClick={(e) => { </St>
e.stopPropagation();
askRetire(v);
}}
>
停用
</button>
) : ( ) : (
<button <St tone="mute" ico="pause">
className="g-ver-act t-go" 停用
onClick={(e) => { </St>
e.stopPropagation();
askActivate(v);
}}
>
激活
</button>
)} )}
</span> <span className="m">
</div> <span className="cnt">
); {v.nodes.length} 节点 · {v.edges.length} 边
})} </span>
<Note> {isActive ? (
保存恒存成新版本并即刻生效;旧版本原样留着,在途工作流按启动时的版本跑完。 <button
把生效切回旧版即是回退 —— 不用改图。 className="g-ver-act"
</Note> onClick={(e) => {
e.stopPropagation();
askRetire(v);
}}
>
停用
</button>
) : (
<button
className="g-ver-act t-go"
onClick={(e) => {
e.stopPropagation();
askActivate(v);
}}
>
激活
</button>
)}
</span>
</div>
);
})}
</div>
<div className="g-vercard-f">
恰一个生效版本;在途工作流按启动时的版本跑完,启停与新建都不影响它们。
</div>
</aside> </aside>
<div className="g-canvas" ref={canvasRef}> <div className="g-canvas" ref={canvasRef}>
@@ -690,13 +736,13 @@ function FlowLab({
</div> </div>
</div> </div>
{/* 图下:节点属性 —— 点画布或点节点条,属性就地填 */} {/* 图下:节点属性卡 —— 点画布或点节点条,属性就地填 */}
<div className="g-side g-wide"> <div className="g-wide">
<div className="g-side-h"> <Card
<Ico name="sliders-horizontal" s={13} /> ico="sliders-horizontal"
节点属性 title="节点属性"
<span className="x">{readOnly ? `v${pkg.version} 只读` : dirty ? '有改动待保存' : '点节点就地填写'}</span> extra={readOnly ? `v${pkg.version} 只读` : dirty ? '有改动待保存' : '点节点就地填写'}
</div> >
<div className="g-picker"> <div className="g-picker">
{nodes {nodes
.filter((n) => n.kind !== 'start' && n.kind !== 'end' && n.kind !== 'end-bad') .filter((n) => n.kind !== 'start' && n.kind !== 'end' && n.kind !== 'end-bad')
@@ -936,6 +982,7 @@ function FlowLab({
/> />
</> </>
)} )}
</Card>
</div> </div>
</Sec> </Sec>