diff --git a/apps/web/src/app/styles/console.css b/apps/web/src/app/styles/console.css index 43b1937..4857112 100644 --- a/apps/web/src/app/styles/console.css +++ b/apps/web/src/app/styles/console.css @@ -340,7 +340,12 @@ .g-canvas { display: flex; flex-direction: column; min-height: 0; } /* 图自身按 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-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 复刻件未改):命中层描边 = 选中框 */ .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:hover { border-color: #aeb9c6; } .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:hover { border-color: #c9d6e6; } .g-ver.on { border-color: var(--brand); box-shadow: 0 0 0 2px var(--brand-wash); } diff --git a/apps/web/src/features/gov-process/internal/panel.tsx b/apps/web/src/features/gov-process/internal/panel.tsx index b60acbe..8ef8954 100644 --- a/apps/web/src/features/gov-process/internal/panel.tsx +++ b/apps/web/src/features/gov-process/internal/panel.tsx @@ -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 ( -
- - - onPatch((x) => ({ ...x, condition: e.target.value }))} - /> - - - - onPatch((x) => { - const { label: _drop, ...rest } = x; - return e.target.value === '' ? rest : { ...rest, label: e.target.value }; - }) - } - /> - - -
- ); -} - /* ---------------- 台面:总览 · 流程包 · 触发器 ---------------- */ /** @@ -387,8 +348,6 @@ function FlowLab({ /** 画布工作副本:属性卡与手绘编辑都改它;保存时合并回流程包 */ const [graph, setGraph] = useState(() => packageToGraph(pkg, [])); const [sel, setSel] = useState(null); - /** 边选中态:graph.edges 的下标(与节点选中互斥 —— 属性卡一次编辑一件事) */ - const [selEdge, setSelEdge] = useState(null); const [dirty, setDirty] = useState(false); const [report, setReport] = useState(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 && {String(nid)}} @@ -901,7 +868,7 @@ function FlowLab({ - ))} - - {/* key 随选中对象换 —— 换一件看,明细区滚回顶,不带上一件的滚动位 */} -
- {node === null && selEdge === null ? ( +
+ {node === null ? (
- 点画布或拣选条,选一个节点或一条边 + 点画布或拣选条,选一个节点

- 节点填名称与链接实现;边填传递契约(条件 + 映射)。负责人运营时分配,不在这儿。 + 节点填名称、链接实现、往哪走(出边条件)。边不是可编辑对象 —— 它只表达顺序; + 点画布上的线会选中发起它的那个节点。负责人运营时分配,不在这儿; 审批裁决(verdict)与放行选口(outcome)由系统注入判据,不用你声明。

- ) : node === null && selEdge !== null && graph !== null ? ( - { - setGraph((g) => - g === null ? g : { ...g, edges: g.edges.map((e, i) => (i === selEdge ? f(e) : e)) }, - ); - setDirty(true); - }} - /> - ) : node === null ? null : ( + ) : ( <> {/* 两列手排(不用自动砌体):左列 = 身份与数据口,右列 = 类型与装载 —— 卡数随节点种类变(审批点无装载卡),手排保证两列始终语义成组、高度可控 */}
- + @@ -1111,6 +1051,42 @@ function FlowLab({ )} )} + + {outEdges.length > 0 && ( + /* + * 出边 · 分支(2026-07-30):**分支归发起它的这个节点管**(Mastra 的 + * `.branch([[条件, 下一步], …])` 就挂在上一步上)—— 所以条件填在这里, + * 边本身不再是可编辑对象:它只是画布上的一条线,表达顺序。 + * 条件读的是这个节点的输出字段;留空即默认兜底(至多一条)。 + */ + + {outEdges.map(({ edge, index }) => ( + + patchEdge(index, (x) => ({ ...x, condition: e.target.value }))} + /> + + ))} + + )}