版本栏三改:显示完整 · 动作统一住顶部 · 「实现」明确为链接自活动库

· 栏宽 132 → 196–224:一版一行排满「v4 生效 10 节点 · 14 边」,不再挤成三行。
  · 激活/停用 · 改名 · 删除收成**顶部动作条**(右端注明作用对象 v几,
    换版看,条上的钮跟着换语义);行里只剩事实,不在行内展开小钮。
    改名输入就地在动作条下方展开,不再吊在卡底。
  · 「实现」行改口:标签「实现 · 链接自活动库」,空选项写
    「从活动库链接一个活动 —— 未链接,激活会被拦」—— 节点不含实现,
    链接才算装载,这层语义直接写在控件上。

五闸绿。

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-29 18:48:25 +08:00
co-authored by Claude Opus 5
parent 9c947950ce
commit 541b1c816e
2 changed files with 73 additions and 65 deletions
+17 -14
View File
@@ -521,7 +521,7 @@
属性区从右侧窄栏挪到图下整行(g-wide),声明式契约细节再往下。 */ 属性区从右侧窄栏挪到图下整行(g-wide),声明式契约细节再往下。 */
/* 三栏(2026-07-30):左**窄**版本条 · 中流程图 · 右节点属性。 /* 三栏(2026-07-30):左**窄**版本条 · 中流程图 · 右节点属性。
右栏用包壳绝对定位 —— 行高由中栏的图定,属性长了在卡内滚,不把图拉长。 */ 右栏用包壳绝对定位 —— 行高由中栏的图定,属性长了在卡内滚,不把图拉长。 */
.g-flowlab { display: grid; grid-template-columns: 132px minmax(0, 2.5fr) minmax(320px, 1.25fr); gap: 12px; align-items: stretch; } .g-flowlab { display: grid; grid-template-columns: minmax(196px, 224px) minmax(0, 2.5fr) minmax(320px, 1.25fr); gap: 12px; align-items: stretch; }
.g-nodewrap { position: relative; min-height: 0; } .g-nodewrap { position: relative; min-height: 0; }
.g-nodewrap > .g-nodecard { position: absolute; inset: 0; height: auto; } .g-nodewrap > .g-nodecard { position: absolute; inset: 0; height: auto; }
/* 版本卡:与右侧的图**等高**(grid stretch),版本多了在卡内滚,不把页面撑长。 /* 版本卡:与右侧的图**等高**(grid stretch),版本多了在卡内滚,不把页面撑长。
@@ -545,24 +545,27 @@
.g-ver-new:disabled { opacity: .5; cursor: default; box-shadow: none; } .g-ver-new:disabled { opacity: .5; cursor: default; box-shadow: none; }
.g-vercard-list { flex: 1; min-height: 0; overflow-y: auto; padding: 12px 14px; display: flex; flex-direction: column; gap: 8px; } .g-vercard-list { flex: 1; min-height: 0; overflow-y: auto; padding: 12px 14px; display: flex; flex-direction: column; gap: 8px; }
/* ── 窄版本条:一版一行(v几 + 状态 + 计数),动作只在**看着的那一版**上展开 ── */ /* ── 版本条:**动作统一住顶部**(作用于正看的这一版),行里只放事实、单行排满 ── */
.g-verslim .g-vercard-h { padding: 30px 10px 16px; gap: 4px; } .g-verslim .g-vercard-h { padding: 30px 14px 16px; gap: 4px; }
.g-verslim .g-vercard-list { padding: 10px 8px; gap: 6px; } .g-verslim .g-vercard-list { padding: 10px 12px; gap: 6px; }
.g-verrow { display: flex; flex-direction: column; border: 1px solid var(--sh-line); border-left: 3px solid var(--sh-steel); background: #fff; } /* 动作条:一排小钮 + 右端注明作用对象(v几)—— 换版看,条上的钮跟着换语义 */
.g-veracts { flex: none; display: flex; align-items: center; gap: 5px; padding: 9px 14px; border-bottom: 1px solid var(--sh-line); background: #f9fafc; }
.g-veracts .g-ver-act { padding: 4px 10px; letter-spacing: .06em; }
.g-veracts .who { margin-left: auto; font: 600 9.5px/1 var(--sh-mono); letter-spacing: .1em; color: var(--sh-muted); }
.g-verrow { display: flex; align-items: baseline; gap: 8px; width: 100%; padding: 9px 11px; border: 1px solid var(--sh-line); border-left: 3px solid var(--sh-steel); background: #fff; font-family: var(--sh-mono); font-size: 10px; color: #3a4452; text-align: left; cursor: pointer; transition: border-color var(--t-fast); }
.g-verrow:hover { border-color: #aeb9c6; }
.g-verrow.on { border-color: #9fb0c0; background: #f2f5f8; } .g-verrow.on { border-color: #9fb0c0; background: #f2f5f8; }
.g-verrow.live { border-left-color: var(--sh-brass); background: var(--sh-brass-wash); } .g-verrow.live { border-left-color: var(--sh-brass); background: var(--sh-brass-wash); }
.g-verrow.live.on { border-color: #cbb98d; } .g-verrow.live.on { border-color: #cbb98d; }
.g-verrow-h { display: flex; flex-wrap: wrap; align-items: baseline; gap: 3px 6px; padding: 7px 8px; border: 0; background: transparent; font-family: var(--sh-mono); font-size: 10px; color: #3a4452; text-align: left; cursor: pointer; } .g-verrow:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }
.g-verrow-h > b { font-size: 12.5px; font-weight: 700; color: var(--sh-ink); } .g-verrow > b { font-size: 13px; font-weight: 700; color: var(--sh-ink); }
.g-verrow-h .st { font-size: 9px; letter-spacing: .1em; color: var(--sh-muted); } .g-verrow .st { font-size: 9px; letter-spacing: .12em; color: var(--sh-muted); }
.g-verrow-h .st.live { color: var(--sh-brass); font-weight: 700; } .g-verrow .st.live { color: var(--sh-brass); font-weight: 700; }
.g-verrow-h .cnt { flex-basis: 100%; font-size: 9px; color: var(--sh-muted); } .g-verrow .cnt { margin-left: auto; font-size: 9.5px; color: var(--sh-muted); white-space: nowrap; }
.g-verrow-a { display: flex; flex-wrap: wrap; gap: 4px; padding: 0 8px 8px; }
.g-verrow-a .g-ver-act { flex: 1 1 auto; text-align: center; padding: 3px 6px; letter-spacing: .06em; }
.g-ver-act.t-bad { color: #b0413e; border-color: #e0bcba; } .g-ver-act.t-bad { color: #b0413e; border-color: #e0bcba; }
.g-ver-act:disabled { opacity: .45; cursor: default; } .g-ver-act:disabled { opacity: .45; cursor: default; }
/* 改名条:名字改的是整条流,所以落在卡底、不挂在某一行上 */ /* 改名条:紧贴动作条下方(点「改名」就地展开) */
.g-verrename { flex: none; display: flex; flex-direction: column; gap: 5px; padding: 9px 10px; border-top: 1px solid var(--sh-line); background: #f9fafc; } .g-verrename { flex: none; display: flex; flex-direction: column; gap: 5px; padding: 9px 14px; border-bottom: 1px solid var(--sh-line); background: #f9fafc; }
.g-verrename .g-input { height: 26px; font-size: 12px; } .g-verrename .g-input { height: 26px; font-size: 12px; }
.g-verrename .t { font-family: var(--sh-mono); font-size: 8.5px; line-height: 1.7; color: var(--sh-muted); } .g-verrename .t { font-family: var(--sh-mono); font-size: 8.5px; line-height: 1.7; color: var(--sh-muted); }
.g-ver { display: flex; align-items: center; gap: 9px; padding: 9px 11px; border: 1px solid var(--sh-line); border-left: 3px solid var(--sh-steel); background: #fff; cursor: pointer; font-family: var(--sh-mono); font-size: 11px; color: #3a4452; } .g-ver { display: flex; align-items: center; gap: 9px; padding: 9px 11px; border: 1px solid var(--sh-line); border-left: 3px solid var(--sh-steel); background: #fff; cursor: pointer; font-family: var(--sh-mono); font-size: 11px; color: #3a4452; }
@@ -661,61 +661,46 @@ function FlowLab({
{/* 编排工作区:左 1/3 版本列表 + 右 2/3 画布 —— 版本收在流程内部,不平铺台面 */} {/* 编排工作区:左 1/3 版本列表 + 右 2/3 画布 —— 版本收在流程内部,不平铺台面 */}
<Sec ico="route" title="编排工作区"> <Sec ico="route" title="编排工作区">
<div className="g-flowlab"> <div className="g-flowlab">
{/* 左栏窄:一版一行只放「v几 + 状态」;动作(激活/停用/改名/删除)只在选中行展开 */} {/*
* 左栏:版本。**动作统一住顶部**(作用于正看的这一版),行里只放事实
* (v几 · 状态 · 点边计数)—— 完整显示,不在行内再展开一排小钮。
*/}
<aside className="g-vercard g-verslim"> <aside className="g-vercard g-verslim">
<div className="g-vercard-h"> <div className="g-vercard-h">
版本 版本
<button className="g-ver-new" onClick={askClone} disabled={cloneVersion.isPending} title="以当前版为底克隆一版"> <button className="g-ver-new" onClick={askClone} disabled={cloneVersion.isPending} title="以当前版为底克隆一版">
<Ico name="plus" s={11} /> <Ico name="plus" s={11} /> 新建
</button> </button>
</div> </div>
<div className="g-vercard-list"> {/* 动作条:激活/停用 · 改名 · 删除 —— 全部作用于下面列表里选中的那一版 */}
{versions.map((v) => { <div className="g-veracts">
const isActive = v.version === activeVersion; {pkg.version === activeVersion ? (
const isShown = v.version === pkg.version; <button className="g-ver-act" onClick={() => askRetire(pkg)}>
return ( 停用
<div key={v.version} className={'g-verrow' + (isShown ? ' on' : '') + (isActive ? ' live' : '')}> </button>
<button className="g-verrow-h" onClick={() => onPick(v.version)}> ) : (
<b>v{v.version}</b> <button
<span className={'st' + (isActive ? ' live' : '')}>{isActive ? '生效' : '停用'}</span> className="g-ver-act t-go"
<span className="cnt"> /* 不闭合点不了:结构闭合 + 活动装载两面清零才亮 */
{v.nodes.length}点·{v.edges.length}边 disabled={closedOf(pkg.version) !== true}
</span> title={closedOf(pkg.version) === true ? `激活 v${pkg.version}` : '还不闭合 —— 属性面会列出差哪几条'}
</button> onClick={() => askActivate(pkg)}
{isShown && ( >
/* 动作只在**看着的那一版**上出现:窄栏放不下每行一排钮,也不该让人误点别版 */ 激活
<div className="g-verrow-a"> </button>
{isActive ? ( )}
<button className="g-ver-act" onClick={() => askRetire(v)}> <button className="g-ver-act" onClick={() => setRenaming(true)} title="改这条流的名字(所有版本同名)">
停用 改名
</button> </button>
) : ( <button
<button className="g-ver-act t-bad"
className="g-ver-act t-go" disabled={pkg.version === activeVersion || dropVersion.isPending}
/* 不闭合点不了:结构闭合 + 活动装载两面清零才亮 */ title={pkg.version === activeVersion ? '生效版删不掉 —— 先停用' : `删掉 v${pkg.version}`}
disabled={closedOf(v.version) !== true} onClick={() => askDrop(pkg)}
title={closedOf(v.version) === true ? `激活 v${v.version}` : '还不闭合 —— 属性面会列出差哪几条'} >
onClick={() => askActivate(v)} 删除
> </button>
激活 <span className="who">v{pkg.version}</span>
</button>
)}
<button className="g-ver-act" onClick={() => setRenaming(true)} title="改这条流的名字(所有版本同名)">
改名
</button>
<button
className="g-ver-act t-bad"
disabled={isActive || dropVersion.isPending}
title={isActive ? '生效版删不掉 —— 先停用' : `删掉 v${v.version}`}
onClick={() => askDrop(v)}
>
删除
</button>
</div>
)}
</div>
);
})}
</div> </div>
{renaming && ( {renaming && (
<div className="g-verrename"> <div className="g-verrename">
@@ -732,6 +717,25 @@ function FlowLab({
<span className="t">回车改名 · Esc 取消 —— 名字是这条流的身份,各版本同名</span> <span className="t">回车改名 · Esc 取消 —— 名字是这条流的身份,各版本同名</span>
</div> </div>
)} )}
<div className="g-vercard-list">
{versions.map((v) => {
const isActive = v.version === activeVersion;
const isShown = v.version === pkg.version;
return (
<button
key={v.version}
className={'g-verrow' + (isShown ? ' on' : '') + (isActive ? ' live' : '')}
onClick={() => onPick(v.version)}
>
<b>v{v.version}</b>
<span className={'st' + (isActive ? ' live' : '')}>{isActive ? '生效' : '停用'}</span>
<span className="cnt">
{v.nodes.length} 节点 · {v.edges.length} 边
</span>
</button>
);
})}
</div>
</aside> </aside>
<div className="g-canvas" ref={canvasRef}> <div className="g-canvas" ref={canvasRef}>
@@ -897,14 +901,15 @@ function FlowLab({
)} )}
{node.kind === 'activity' && node.flavor !== 'approval' && ( {node.kind === 'activity' && node.flavor !== 'approval' && (
<div className="g-tbrow"> <div className="g-tbrow">
<em>实现</em> {/* 实现 = **从活动库链接一个活动**:节点不含实现,链接才算装载(激活判据之二) */}
<em>实现 · 链接自活动库</em>
<span className="v col"> <span className="v col">
<select <select
className="g-tbin sel" className="g-tbin sel"
value={refAct?.activityId ?? ''} value={refAct?.activityId ?? ''}
onChange={(e) => patch((n) => ({ ...n, act: e.target.value === '' ? undefined : e.target.value }))} onChange={(e) => patch((n) => ({ ...n, act: e.target.value === '' ? undefined : e.target.value }))}
> >
<option value="">未链接 —— 激活会被「活动装载」拦下</option> <option value="">从活动库链接一个活动 —— 未链接,激活会被拦</option>
{activities.map((a) => ( {activities.map((a) => (
<option key={a.activityId} value={a.activityId}> <option key={a.activityId} value={a.activityId}>
{a.name} · {a.activityId} {a.name} · {a.activityId}