Archived
编排页两处收口:图框下沿对齐左列底栏;边不再是可编辑对象
**对齐**:图框下边原本停在纸面内距上方,与左列版本卡底栏的顶线差 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:
@@ -340,7 +340,12 @@
|
|||||||
.g-canvas { display: flex; flex-direction: column; min-height: 0; }
|
.g-canvas { display: flex; flex-direction: column; min-height: 0; }
|
||||||
/* 图自身按 aspect-ratio 缩放,整张一定看得全——纸面不再限高、不再内部滚动 */
|
/* 图自身按 aspect-ratio 缩放,整张一定看得全——纸面不再限高、不再内部滚动 */
|
||||||
.g-canvas .process-desk { flex: 1; min-height: 0; height: auto; max-height: none; overflow: visible; padding: 0; background: transparent; display: flex; flex-direction: column; }
|
.g-canvas .process-desk { flex: 1; min-height: 0; height: auto; max-height: none; overflow: visible; padding: 0; background: transparent; display: flex; flex-direction: column; }
|
||||||
.g-canvas .process-doc { flex: 1; min-height: 0; box-sizing: border-box; max-width: none; padding: 14px 18px 18px; border-radius: 10px; box-shadow: var(--cshadow); }
|
.g-canvas .process-doc { flex: 1; min-height: 0; box-sizing: border-box; max-width: none; padding: 14px 18px 18px; border-radius: 10px; box-shadow: var(--cshadow); display: flex; flex-direction: column; }
|
||||||
|
/* 图框吃满纸面,并在下方空出**恰好一条底栏的高度** —— 图框下沿与左列版本卡底栏
|
||||||
|
(.g-vercard-f,净高 38)的上沿连成同一条横线。20 = 38(底栏净高) − 18(纸面下内距)。
|
||||||
|
纸面被栅格拉高时(版本多、左列更长)图框跟着长,才不会又浮回上面去。 */
|
||||||
|
.g-canvas .process-doc > .hero { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; margin-bottom: 20px; }
|
||||||
|
.g-canvas .process-doc > .hero > .fig-body { flex: 1 1 auto; min-height: 0; }
|
||||||
/* 画布上的选中态(外挂类,_flow 复刻件未改):命中层描边 = 选中框 */
|
/* 画布上的选中态(外挂类,_flow 复刻件未改):命中层描边 = 选中框 */
|
||||||
.g-canvas .flow-node-sel .djs-hit { fill: rgba(51, 112, 255, .09) !important; stroke: #3370ff !important; stroke-width: 2.5px !important; stroke-opacity: 1 !important; rx: 7px; ry: 7px; }
|
.g-canvas .flow-node-sel .djs-hit { fill: rgba(51, 112, 255, .09) !important; stroke: #3370ff !important; stroke-width: 2.5px !important; stroke-opacity: 1 !important; rx: 7px; ry: 7px; }
|
||||||
|
|
||||||
@@ -549,7 +554,10 @@
|
|||||||
.g-ver-act { border: 1px solid var(--sh-line); background: #fff; padding: 2px 8px; font-family: inherit; font-size: 9.5px; letter-spacing: .1em; color: #3a4452; cursor: pointer; }
|
.g-ver-act { border: 1px solid var(--sh-line); background: #fff; padding: 2px 8px; font-family: inherit; font-size: 9.5px; letter-spacing: .1em; color: #3a4452; cursor: pointer; }
|
||||||
.g-ver-act:hover { border-color: #aeb9c6; }
|
.g-ver-act:hover { border-color: #aeb9c6; }
|
||||||
.g-ver-act.t-go { color: var(--sh-brass); border-color: #d9c9a4; }
|
.g-ver-act.t-go { color: var(--sh-brass); border-color: #d9c9a4; }
|
||||||
.g-vercard-f { flex: none; padding: 10px 16px; border-top: 1px solid var(--sh-line); font-family: var(--sh-mono); font-size: 9.5px; line-height: 1.8; color: var(--sh-muted); }
|
/* 卡底栏 —— 它的净高(1 顶线 + 10 + 17 行 + 10 = 38)是**两列共用的基线**:
|
||||||
|
右侧画布的图框下沿要落在这条顶线上(见上面 .g-canvas .process-doc > .hero 的下外距)。
|
||||||
|
文案改长到换行、或改了内距,那边的 20px 要跟着算。 */
|
||||||
|
.g-vercard-f { flex: none; min-height: 38px; box-sizing: border-box; padding: 10px 16px; border-top: 1px solid var(--sh-line); font-family: var(--sh-mono); font-size: 9.5px; line-height: 1.8; color: var(--sh-muted); }
|
||||||
.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); }
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
*
|
*
|
||||||
* 一区一内容:三枚计数摆在货架顶端,底下就是包货架(触发器已独立成区)。
|
* 一区一内容:三枚计数摆在货架顶端,底下就是包货架(触发器已独立成区)。
|
||||||
* 货架一流程一卡(不按版本平铺);进详情后左 1/3 是版本列表、右 2/3 是流程图,
|
* 货架一流程一卡(不按版本平铺);进详情后左 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 [graph, setGraph] = useState<FlowGraph | null>(() => packageToGraph(pkg, []));
|
||||||
const [sel, setSel] = useState<string | null>(null);
|
const [sel, setSel] = useState<string | null>(null);
|
||||||
/** 边选中态:graph.edges 的下标(与节点选中互斥 —— 属性卡一次编辑一件事) */
|
|
||||||
const [selEdge, setSelEdge] = useState<number | null>(null);
|
|
||||||
const [dirty, setDirty] = useState(false);
|
const [dirty, setDirty] = useState(false);
|
||||||
const [report, setReport] = useState<ClosureReport | undefined>(undefined);
|
const [report, setReport] = useState<ClosureReport | undefined>(undefined);
|
||||||
const [ask, setAsk] = useState<{ spec: AskSpec; danger?: boolean } | undefined>(undefined);
|
const [ask, setAsk] = useState<{ spec: AskSpec; danger?: boolean } | undefined>(undefined);
|
||||||
@@ -463,6 +422,18 @@ function FlowLab({
|
|||||||
setDirty(true);
|
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) =>
|
const kindLabel = (n: FlowNode) =>
|
||||||
n.kind === 'activity' ? (n.flavor === 'approval' ? '审批点' : '活动点') : n.kind === 'start' ? '调用接口' : '显式终点';
|
n.kind === 'activity' ? (n.flavor === 'approval' ? '审批点' : '活动点') : n.kind === 'start' ? '调用接口' : '显式终点';
|
||||||
const kindTone = (n: FlowNode) => (n.kind === 'activity' ? (n.flavor === 'approval' ? 'warn' : 'auto') : 'mute');
|
const kindTone = (n: FlowNode) => (n.kind === 'activity' ? (n.flavor === 'approval' ? 'warn' : 'auto') : 'mute');
|
||||||
@@ -755,16 +726,12 @@ function FlowLab({
|
|||||||
/*
|
/*
|
||||||
* 画布只画**编排逻辑**:节点的名字、连线的顺序;边上的字是辅助阅读的注记。
|
* 画布只画**编排逻辑**:节点的名字、连线的顺序;边上的字是辅助阅读的注记。
|
||||||
* 图上真正生效可编辑的只有**名字** —— 不传 positions,双击就不出岗位一栏。
|
* 图上真正生效可编辑的只有**名字** —— 不传 positions,双击就不出岗位一栏。
|
||||||
* 岗位、判据、映射这些闭环要素在**节点属性**里决定;分配到人是运营的事,这里不碰。
|
* 岗位、判据这些闭环要素在**节点属性**里决定;分配到人是运营的事,这里不碰。
|
||||||
*/
|
*/
|
||||||
canEdit
|
canEdit
|
||||||
onNodeClick={(nid) => { setSel(nid); setSelEdge(null); }}
|
onNodeClick={(nid) => setSel(nid)}
|
||||||
onEdgeClick={(edge) => {
|
/* 边不是可编辑对象 —— 点一条线,选中**发起它的那个节点**(分支归它管) */
|
||||||
const i = (graph?.edges ?? []).findIndex(
|
onEdgeClick={(edge) => setSel(edge.from)}
|
||||||
(x) => x.from === edge.from && x.to === edge.to && x.code === edge.code,
|
|
||||||
);
|
|
||||||
if (i >= 0) { setSelEdge(i); setSel(null); }
|
|
||||||
}}
|
|
||||||
onEditNodeText={async (nid, p2) => {
|
onEditNodeText={async (nid, p2) => {
|
||||||
setGraph((g) =>
|
setGraph((g) =>
|
||||||
g === null
|
g === null
|
||||||
@@ -807,10 +774,10 @@ function FlowLab({
|
|||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (nid !== undefined) { setSel(String(nid)); setSelEdge(null); }
|
if (nid !== undefined) setSel(String(nid));
|
||||||
}}
|
}}
|
||||||
onKeyDown={(e) => {
|
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>}
|
{nid !== undefined && <b>{String(nid)}</b>}
|
||||||
@@ -901,7 +868,7 @@ function FlowLab({
|
|||||||
<button
|
<button
|
||||||
key={n.id}
|
key={n.id}
|
||||||
className={`g-pick t-${kindTone(n)}` + (sel === n.id ? ' on' : '')}
|
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>
|
<b className="g-mono">{n.id}</b>
|
||||||
{n.label}
|
{n.label}
|
||||||
@@ -909,54 +876,27 @@ function FlowLab({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
{/* key 随选中对象换 —— 换一件看,明细区滚回顶,不带上一件的滚动位 */}
|
{/* key 随选中对象换 —— 换一件看,明细区滚回顶,不带上一件的滚动位 */}
|
||||||
<div className="g-propsheet" key={node !== null ? `n:${node.id}` : selEdge !== null ? `e:${selEdge}` : 'blank'}>
|
<div className="g-propsheet" key={node !== null ? `n:${node.id}` : 'blank'}>
|
||||||
{node === null && selEdge === null ? (
|
{node === null ? (
|
||||||
<div className="g-sheetempty">
|
<div className="g-sheetempty">
|
||||||
<Ico name="target" s={20} />
|
<Ico name="target" s={20} />
|
||||||
<b>点画布或拣选条,选一个节点或一条边</b>
|
<b>点画布或拣选条,选一个节点</b>
|
||||||
<p>
|
<p>
|
||||||
节点填名称与链接实现;边填传递契约(条件 + 映射)。负责人运营时分配,不在这儿。
|
节点填名称、链接实现、往哪走(出边条件)。边不是可编辑对象 —— 它只表达顺序;
|
||||||
|
点画布上的线会选中发起它的那个节点。负责人运营时分配,不在这儿;
|
||||||
审批裁决(verdict)与放行选口(outcome)由系统注入判据,不用你声明。
|
审批裁决(verdict)与放行选口(outcome)由系统注入判据,不用你声明。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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-props">
|
||||||
<div className="g-propcol">
|
<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 />
|
||||||
</Field>
|
</Field>
|
||||||
<Field k="名称">
|
<Field k="名称">
|
||||||
@@ -1111,6 +1051,42 @@ function FlowLab({
|
|||||||
)}
|
)}
|
||||||
</Prop>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
Reference in New Issue
Block a user