编排页两处收口:图框下沿对齐左列底栏;边不再是可编辑对象

**对齐**:图框下边原本停在纸面内距上方,与左列版本卡底栏的顶线差 16px,
两条本该连成一线的横线错开。改法不是拿魔数把图框往下推 ——
纸面改成 flex 列、图框 flex:1 吃满(被栅格拉高时跟着长,不会又浮回上面),
下方留出**恰好一条底栏的高度**:20 = 38(底栏净高) − 18(纸面下内距)。
底栏那侧加 min-height:38 锁住基线,两边注释互指。实测差 0。

**边不再是可编辑对象**:上一轮边已瘦成「顺序 + 条件」,再给它一张属性卡、
一条 E1…E14 拣选行、一套与节点互斥的选中态,就是为一个条件养一整套机制。
按 Mastra 的语义,分支本来就归**发起它的那一步**管
(`.branch([[条件, 下一步], …])` 挂在上一步上)—— 于是:
  · 删掉边属性卡、边拣选行、selEdge 选中态;
  · 节点属性卡新增「出边 · 往哪走」:列出本节点的每条出边(E号 → 去向)与条件,
    单出边显示「无条件直连」,留空即默认兜底;
  · 审批点的提示单写一版 —— 它的判据是**系统注入**的 verdict,不是自己的输出字段;
  · 点画布上的线不再打开边卡,而是选中发起它的那个节点。

顺带清掉三处失真文案:头注与「节点 ID」提示里的「边映射」、空态里的
「边填传递契约(条件 + 映射)」。

五闸绿。

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 15:13:55 +08:00
co-authored by Claude Opus 5
parent 4cbfea5b46
commit f03f23f2ca
2 changed files with 73 additions and 89 deletions
@@ -3,7 +3,7 @@
*
* 一区一内容:三枚计数摆在货架顶端,底下就是包货架(触发器已独立成区)。
* 货架一流程一卡(不按版本平铺);进详情后左 1/3 是版本列表、右 2/3 是流程图,
* 图下是节点属性,再往下才是声明式的契约细节(调用接口 · 边映射 · 触发器)。
* 图下是节点属性(身份 · 数据接口 · 装载 · 出边条件),再往下是调用接口与触发器。
*
* 版本是**运行时开关**,不是发布流程:
* · 编辑即其身:保存**写回当前版本**(号不变);新版本只出自「新建」
@@ -64,45 +64,6 @@ function flowsOf(packages: readonly ProcessPackage[]) {
});
}
/* ── 边属性:只剩顺序与条件(2026-07-30 对齐 Mastra)────────────────────
Mastra 的边不传数据 —— 约束力全在两端活动的输入/输出字段(schema 咬合)。
数据按字段名流动:上游发的名就是下游收的名;边属性卡因此只剩条件与标签,
「映射 · 载荷装配」整节退役 —— 下游拿什么去**节点属性**的数据接口看。 */
function EdgeProps({ edge, onPatch }: { edge: FlowEdge; onPatch: (f: (e: FlowEdge) => FlowEdge) => void }) {
return (
<div className="g-props g-eprops">
<Prop
ico="git-branch"
title={`${edge.code ?? '边'} · ${edge.from} → ${edge.to}`}
extra="边只表达顺序与条件 —— 数据按字段名自动流动"
>
<Field k="S-FEEL 条件" hint="读上游节点的输出字段,如 needSourcing = true;留空即无条件(默认边,至多一条)">
<input
className="g-input mono"
value={edge.condition ?? ''}
placeholder="留空 = 默认兜底"
onChange={(e) => onPatch((x) => ({ ...x, condition: e.target.value }))}
/>
</Field>
<Field k="边标签" hint="画在线旁的注记,辅助阅读,不参与判断">
<input
className="g-input"
value={edge.label ?? ''}
placeholder="如:需要选型"
onChange={(e) =>
onPatch((x) => {
const { label: _drop, ...rest } = x;
return e.target.value === '' ? rest : { ...rest, label: e.target.value };
})
}
/>
</Field>
</Prop>
</div>
);
}
/* ---------------- 台面:总览 · 流程包 · 触发器 ---------------- */
/**
@@ -387,8 +348,6 @@ function FlowLab({
/** 画布工作副本:属性卡与手绘编辑都改它;保存时合并回流程包 */
const [graph, setGraph] = useState<FlowGraph | null>(() => packageToGraph(pkg, []));
const [sel, setSel] = useState<string | null>(null);
/** 边选中态:graph.edges 的下标(与节点选中互斥 —— 属性卡一次编辑一件事) */
const [selEdge, setSelEdge] = useState<number | null>(null);
const [dirty, setDirty] = useState(false);
const [report, setReport] = useState<ClosureReport | undefined>(undefined);
const [ask, setAsk] = useState<{ spec: AskSpec; danger?: boolean } | undefined>(undefined);
@@ -463,6 +422,18 @@ function FlowLab({
setDirty(true);
};
/*
* 出边**归发起它的节点**(Mastra:`.branch([[条件, 下一步], …])` 挂在上一步上)——
* 边不再是可编辑对象,分支条件在源节点的属性卡里填,所以这里按 `from` 取它的出边。
*/
const outEdges = (graph?.edges ?? [])
.map((edge, index) => ({ edge, index }))
.filter(({ edge }) => sel !== null && edge.from === sel);
const patchEdge = (index: number, f: (e: FlowEdge) => FlowEdge) => {
setGraph((g) => (g === null ? g : { ...g, edges: g.edges.map((e, i) => (i === index ? f(e) : e)) }));
setDirty(true);
};
const kindLabel = (n: FlowNode) =>
n.kind === 'activity' ? (n.flavor === 'approval' ? '审批点' : '活动点') : n.kind === 'start' ? '调用接口' : '显式终点';
const kindTone = (n: FlowNode) => (n.kind === 'activity' ? (n.flavor === 'approval' ? 'warn' : 'auto') : 'mute');
@@ -755,16 +726,12 @@ function FlowLab({
/*
* 画布只画**编排逻辑**:节点的名字、连线的顺序;边上的字是辅助阅读的注记。
* 图上真正生效可编辑的只有**名字** —— 不传 positions,双击就不出岗位一栏。
* 岗位、判据、映射这些闭环要素在**节点属性**里决定;分配到人是运营的事,这里不碰。
* 岗位、判据这些闭环要素在**节点属性**里决定;分配到人是运营的事,这里不碰。
*/
canEdit
onNodeClick={(nid) => { setSel(nid); setSelEdge(null); }}
onEdgeClick={(edge) => {
const i = (graph?.edges ?? []).findIndex(
(x) => x.from === edge.from && x.to === edge.to && x.code === edge.code,
);
if (i >= 0) { setSelEdge(i); setSel(null); }
}}
onNodeClick={(nid) => setSel(nid)}
/* 边不是可编辑对象 —— 点一条线,选中**发起它的那个节点**(分支归它管) */
onEdgeClick={(edge) => setSel(edge.from)}
onEditNodeText={async (nid, p2) => {
setGraph((g) =>
g === null
@@ -807,10 +774,10 @@ function FlowLab({
role="button"
tabIndex={0}
onClick={() => {
if (nid !== undefined) { setSel(String(nid)); setSelEdge(null); }
if (nid !== undefined) setSel(String(nid));
}}
onKeyDown={(e) => {
if (e.key === 'Enter' && nid !== undefined) { setSel(String(nid)); setSelEdge(null); }
if (e.key === 'Enter' && nid !== undefined) setSel(String(nid));
}}
>
{nid !== undefined && <b>{String(nid)}</b>}
@@ -901,7 +868,7 @@ function FlowLab({
<button
key={n.id}
className={`g-pick t-${kindTone(n)}` + (sel === n.id ? ' on' : '')}
onClick={() => { setSel(sel === n.id ? null : n.id); setSelEdge(null); }}
onClick={() => setSel(sel === n.id ? null : n.id)}
>
<b className="g-mono">{n.id}</b>
{n.label}
@@ -909,54 +876,27 @@ function FlowLab({
))}
</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 ? (
<div className="g-propsheet" key={node !== null ? `n:${node.id}` : 'blank'}>
{node === null ? (
<div className="g-sheetempty">
<Ico name="target" s={20} />
<b>点画布或拣选条,选一个节点或一条边</b>
<b>点画布或拣选条,选一个节点</b>
<p>
节点填名称与链接实现;边填传递契约(条件 + 映射)。负责人运营时分配,不在这儿。
节点填名称、链接实现、往哪走(出边条件)。边不是可编辑对象 —— 它只表达顺序;
点画布上的线会选中发起它的那个节点。负责人运营时分配,不在这儿;
审批裁决(verdict)与放行选口(outcome)由系统注入判据,不用你声明。
</p>
</div>
) : node === null && selEdge !== null && graph !== null ? (
<EdgeProps
edge={graph.edges[selEdge]!}
onPatch={(f) => {
setGraph((g) =>
g === null ? g : { ...g, edges: g.edges.map((e, i) => (i === selEdge ? f(e) : e)) },
);
setDirty(true);
}}
/>
) : node === null ? null : (
) : (
<>
{/* 两列手排(不用自动砌体):左列 = 身份与数据口,右列 = 类型与装载 ——
卡数随节点种类变(审批点无装载卡),手排保证两列始终语义成组、高度可控 */}
<div className="g-props">
<div className="g-propcol">
<Prop ico="tag" title="身份与命名" extra={kindLabel(node)}>
<Field k="节点 ID" hint="编排内唯一,边映射按它寻址">
<Field k="节点 ID" hint="编排内唯一,边按它连、判据按它寻址">
<input className="g-input mono" value={node.id} readOnly />
</Field>
<Field k="名称">
@@ -1111,6 +1051,42 @@ function FlowLab({
)}
</Prop>
)}
{outEdges.length > 0 && (
/*
* 出边 · 分支(2026-07-30):**分支归发起它的这个节点管**(Mastra 的
* `.branch([[条件, 下一步], …])` 就挂在上一步上)—— 所以条件填在这里,
* 边本身不再是可编辑对象:它只是画布上的一条线,表达顺序。
* 条件读的是这个节点的输出字段;留空即默认兜底(至多一条)。
*/
<Prop
ico="git-branch"
title="出边 · 往哪走"
extra={outEdges.length === 1 ? '单出边 · 直连' : `${outEdges.length} 条 · 命中即走`}
>
{outEdges.map(({ edge, index }) => (
<Field
key={edge.code ?? index}
k={`${edge.code ?? `边${index + 1}`} → ${edge.to}`}
hint={
outEdges.length === 1
? '只有一条出边,无条件直连'
: node.kind === 'activity' && node.flavor === 'approval'
? // 审批点的判据是**系统注入**的裁决,不是它自己的输出字段
'读审批裁决:verdict = "pass" / "reject";留空即默认兜底'
: '读本节点的输出字段,如 needAlternative = true;留空即默认兜底'
}
>
<input
className="g-input mono"
value={edge.condition ?? ''}
placeholder={outEdges.length === 1 ? '无条件' : '留空 = 默认兜底'}
onChange={(e) => patchEdge(index, (x) => ({ ...x, condition: e.target.value }))}
/>
</Field>
))}
</Prop>
)}
</div>
</div>
</>