节点属性平铺 —— 不分类,也不写数据契约声明

分类卡(身份与命名 / 数据接口 / 类型与责任 / 链接活动实现 / 出边)全撤:
节点本来只决定这么几件事,分成五张小卡反而把四五个字段摊成半屏。
现在是一列字段直排,字段间一条细线断句:
  节点 ID(只读)· 名称 · 说明 · 节点类型 · 链接实现 · 出边条件(每条一格)。

「数据接口 · 声明」整节删掉 —— 数据契约**归活动自己**,编排面不复述它。
链好实现后只留一枚直达钮:「去活动库看 X 的契约」。

顺带清掉两条一直漏网的说教横幅(前一轮 ⓘ 清扫时因为文案里夹了 <b> 没匹配上):
「编排不带岗位 ——…」与「审批点是系统内置要素 ——…」。前者的意思已收进
「节点类型」那行的提示,后者本就是常识性交代,不必占一整块。

CSS 里 .g-props/.g-propcol/.g-eprops/.g-prop* 与 .g-dif* 两组样式随之退役,
换成一条 .g-nodeform。

五闸绿。

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 17:29:11 +08:00
co-authored by Claude Opus 5
parent 5414e99778
commit 28623cd1cb
2 changed files with 99 additions and 215 deletions
+7 -23
View File
@@ -630,16 +630,13 @@
/* 两面都齐:一枚小检验章,别再占一整张卡 */
.g-okstamp { display: inline-flex; align-items: center; gap: 6px; margin-top: 4px; padding: 5px 11px; border: 1px solid rgba(46, 125, 79, .45); font: 600 10px/1 var(--sh-mono); letter-spacing: .08em; color: #2e7d4f; }
/* 属性小卡:三栏改版后右栏是**窄轨**(≈335),两列摊不开 —— 改单列竖排,
顺序即阅读次序:身份 → 数据接口 → 类型与责任 → 装载 → 出边。长了在明细区自己滚。 */
.g-nodecard .g-props { display: flex; flex-direction: column; align-items: stretch; gap: 12px; }
.g-propcol { min-width: 0; display: flex; flex-direction: column; gap: 12px; }
.g-nodecard .g-props.g-eprops { display: flex; flex-direction: column; align-items: stretch; gap: 12px; }
.g-nodecard .g-prop { border: 1px solid var(--sh-line); border-radius: 0; box-shadow: none; }
.g-nodecard .g-prop-h { padding: 8px 12px; background: #fff; font: 600 9px/1.5 var(--sh-mono); letter-spacing: .12em; text-transform: uppercase; color: var(--sh-steel); border-bottom: 1px solid var(--sh-line); }
.g-nodecard .g-prop-h svg { color: var(--sh-steel); opacity: .6; }
.g-nodecard .g-prop-h .x { font-size: 8.5px; font-weight: 600; letter-spacing: .04em; color: var(--sh-muted); text-align: right; }
.g-nodecard .g-prop-b { padding: 11px 12px 13px; gap: 11px; }
/* 节点属性**平铺**(2026-07-30):节点只决定几件事 —— 分类卡把四五个字段摊成半屏,
撤掉;一列字段直排,字段之间一条细线断句就够。 */
.g-nodeform { display: flex; flex-direction: column; gap: 0; }
.g-nodeform > .g-field { padding: 11px 2px; border-bottom: 1px dashed var(--line-soft); }
.g-nodeform > .g-field:last-child { border-bottom: 0; }
.g-nodeform > .g-note { margin: 11px 0 0; }
.g-nodeform > .g-btn { align-self: flex-start; margin-top: 11px; }
/* 字段 = 图签小格:微墨签(mono 大写)+ 纸上填线(输入只留底线,聚焦底线转钢蓝) */
.g-nodecard .g-field { gap: 3px; }
@@ -675,19 +672,6 @@
.g-maprow > .g-btn { grid-column: 6; justify-self: stretch; padding: 4px 0; display: inline-flex; align-items: center; justify-content: center; }
.g-maparr { color: var(--sh-steel, #46586a); font-weight: 700; text-align: center; }
/* ── 节点属性 · 数据接口:契约即管道 —— 输入字段(名/型/来源)+ 输出口 ── */
.g-dif { display: flex; flex-direction: column; gap: 2px; }
.g-dif + .g-dif { margin-top: 8px; }
.g-dif > em { font: 600 9px/1.6 var(--mono); font-style: normal; letter-spacing: .1em; text-transform: uppercase; color: var(--ink-4); padding-bottom: 3px; border-bottom: 1px solid var(--line-soft); }
.g-dif-r { display: flex; align-items: baseline; flex-wrap: wrap; gap: 4px 10px; padding: 4px 0; border-bottom: 1px dashed var(--line-soft); font-size: 11.5px; }
.g-dif-r:last-child { border-bottom: 0; }
.g-dif-r .k { font-family: var(--mono); font-weight: 700; color: var(--ink); }
.g-dif-r .k i { font-style: normal; font-family: var(--mono); font-size: 8.5px; font-weight: 600; letter-spacing: .06em; color: #8a5f1c; margin-left: 5px; padding: 1px 4px; border: 1px solid #e3d1a8; border-radius: 3px; }
.g-dif-r .t { font-family: var(--mono); font-size: 10px; color: var(--ink-4); }
.g-dif-r .s { margin-left: auto; font-size: 11px; color: var(--ink-3); }
.g-dif-r .s.ask { color: #8a5f1c; }
.g-dif-r .f { min-width: 0; font-family: var(--mono); font-size: 10.5px; color: var(--ink-3); overflow-wrap: anywhere; }
/* ── 运营中心 · 工作流 · 运行配置:一包一卡,里面两节(触发器 / 节点负责人)── */
.g-runconf { display: flex; flex-direction: column; gap: 4px; padding: 10px 14px 12px; }
.g-runconf + .g-runconf { border-top: 1px dashed var(--line-soft); }
@@ -3,7 +3,9 @@
*
* 一区一内容:三枚计数摆在货架顶端,底下就是包货架(触发器已独立成区)。
* 货架一流程一卡(不按版本平铺);进详情是**三栏**:左窄轨=版本(选中那版才出动作:
* 激活/停用 · 改名 · 删除)、中=流程图、右=节点属性(点图选节点,内容长了卡内滚,不拉长图)。
* 激活/停用 · 改名 · 删除)、中=流程图、右=节点属性 —— **平铺一列字段,不分类**:
* 节点只决定 ID/名称/说明 · 类型 · 链接哪个活动 · 出边条件;数据契约归活动自己,
* 这里不写声明,要看去活动库看(点图选节点,内容长了卡内滚,不拉长图)。
* 「激活判据」不再单占一卡 —— 没选节点时,属性栏就地列出还差哪几条,点一条定位过去。
*
* 版本是**运行时开关**,不是发布流程:
@@ -35,7 +37,6 @@ import {
Kpi,
Note,
PageHead,
Prop,
QueryGate,
Sec,
Seg,
@@ -401,8 +402,6 @@ function FlowLab({
setDirty(true);
};
const kindLabel = (n: FlowNode) =>
n.kind === 'activity' ? (n.flavor === 'approval' ? '审批点' : '活动点') : n.kind === 'start' ? '调用接口' : '显式终点';
// 画布上的选中态:diagram-js 给每个形状挂了 data-element-id,外挂一个类,不改画布件
useEffect(() => {
@@ -847,202 +846,103 @@ function FlowLab({
)}
</div>
) : (
<>
{/* 两列手排(不用自动砌体):左列 = 身份与数据口,右列 = 类型与装载 ——
卡数随节点种类变(审批点无装载卡),手排保证两列始终语义成组、高度可控 */}
<div className="g-props">
<div className="g-propcol">
<Prop ico="tag" title="身份与命名" extra={kindLabel(node)}>
<Field k="节点 ID" hint="编排内唯一,边按它连、判据按它寻址">
<input className="g-input mono" value={node.id} readOnly />
</Field>
<Field k="名称">
/*
* 节点属性**平铺**(2026-07-30 用户定「不用分类了」):不再分「身份/类型/装载/出边」
* 几张小卡 —— 节点本来只决定这么几件,分类反而把四五个字段摊成半屏卡片。
* 也不再摆数据契约声明:数据契约**归活动自己**,要看去活动库看(下面那枚钮直达)。
*/
<div className="g-nodeform">
<Field k="节点 ID" hint="编排内唯一,边按它连、判据按它寻址">
<input className="g-input mono" value={node.id} readOnly />
</Field>
<Field k="名称">
<input
className="g-input"
value={node.label}
placeholder="节点名称"
onChange={(e) => patch((n) => ({ ...n, label: e.target.value }))}
/>
</Field>
{node.kind === 'activity' && (
<>
<Field k="说明" hint="画布上的第二行小字">
<input
className="g-input"
value={node.label}
placeholder="节点名称"
onChange={(e) => patch((n) => ({ ...n, label: e.target.value }))}
value={node.sub ?? ''}
placeholder="如:照谱执行 / 临场演绎 / 人裁节点"
onChange={(e) => patch((n) => ({ ...n, sub: e.target.value === '' ? undefined : e.target.value }))}
/>
</Field>
{node.kind === 'activity' && (
<Field k="说明" hint="画布上的第二行小字">
<input
className="g-input"
value={node.sub ?? ''}
placeholder="如:照谱执行 / 临场演绎 / 人裁节点"
onChange={(e) =>
patch((n) => ({ ...n, sub: e.target.value === '' ? undefined : e.target.value }))
}
/>
</Field>
<Field k="节点类型" hint="只分两种;审批点是系统内置要素,不链活动">
<Seg
value={node.flavor === 'approval' ? 'approval' : 'act'}
options={[
{ k: 'act', label: '活动点' },
{ k: 'approval', label: '审批点' },
]}
onPick={(k) => patch((n) => ({ ...n, flavor: k === 'approval' ? ('approval' as const) : undefined }))}
/>
</Field>
</>
)}
{node.kind === 'activity' && node.flavor !== 'approval' && (
<>
<Field k="链接实现" hint="活动住库;链好了这节点才算装载(激活判据之二)">
<select
className="g-select"
value={refAct?.activityId ?? ''}
onChange={(e) => patch((n) => ({ ...n, act: e.target.value === '' ? undefined : e.target.value }))}
>
<option value="">未链接 —— 激活会被「活动装载」拦下</option>
{activities.map((a) => (
<option key={a.activityId} value={a.activityId}>
{a.name} · {a.activityId}
</option>
))}
</select>
</Field>
{refAct === undefined ? (
<Note tone="warn">还没链接活动库里已安装的实现 —— 图可以照画照存,激活前链上即可。</Note>
) : (
/* 数据契约归活动自己 —— 要看它吃什么吐什么,去活动库看 */
<button
className="g-btn"
onClick={() => nav.jump('library', 'shelf', { k: 'act', id: refAct.activityId })}
>
<Ico name="arrow-right" s={13} />
去活动库看 {refAct.name} 的契约
</button>
)}
</Prop>
</>
)}
{node.kind === 'activity' && node.flavor !== 'approval' && (
/*
* 数据接口 —— **只声明,不做任何要求**(2026-07-30 用户定):
* 这里照着活动契约把「吃什么、吐什么」列出来,不标必填、不推来源、不校验类型。
* 字段够不够、缺了怎么补,是**活动内实现**的事,不在编排层现形。
* 数组照实写成 `T[]` —— 声明得看得出是一个还是一串。
*/
<Prop
ico="git-merge"
title="数据接口 · 声明"
extra={refAct === undefined ? '先链接活动实现' : '只声明,不做任何要求'}
>
{refAct === undefined ? (
<Note>链接活动实现后,这里照它的契约列出吃什么、吐什么。</Note>
) : (
<>
<div className="g-dif">
<em>输入 · {refAct.contract.inputFields.length} 字段</em>
{refAct.contract.inputFields.map((f) => (
<div key={f.name} className="g-dif-r">
<span className="k">{f.name}</span>
<span className="t">{f.type}</span>
</div>
))}
</div>
<div className="g-dif">
<em>输出 · {refAct.contract.outputPorts.length} 口</em>
{refAct.contract.outputPorts.map((port) => (
<div key={port.name} className="g-dif-r">
<span className="k">{port.name}</span>
<span className="f">
{port.fields.length > 0
? port.fields.map((f) => `${f.name} ${f.type}`).join(' · ')
: '(整包事实)'}
</span>
</div>
))}
</div>
</>
)}
</Prop>
)}
</div>
<div className="g-propcol">
<Prop
ico="user-check"
title="类型与责任"
extra={
node.kind === 'activity'
? node.flavor === 'approval'
? '运行时产出审批单'
: '运行时产出任务'
: undefined
{outEdges.map(({ edge, index }) => (
/*
* 出边条件:**分支归发起它的这个节点管**(Mastra 的 `.branch([[条件, 下一步], …])`
* 就挂在上一步上)—— 边本身不是可编辑对象,它只是画布上的一条线。
*/
<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;留空即默认兜底'
}
>
{node.kind === 'activity' && (
<Field k="节点类型" hint="节点只分两种 —— 实现方式是活动库的事,这里不选">
<Seg
value={node.flavor === 'approval' ? 'approval' : 'act'}
options={[
{ k: 'act', label: '活动点' },
{ k: 'approval', label: '审批点' },
]}
onPick={(k) =>
patch((n) => ({ ...n, flavor: k === 'approval' ? ('approval' as const) : undefined }))
}
/>
</Field>
)}
<Note>
编排<b>不带岗位</b> —— 负责人是运营时分配的:工作流绑了员工组按组轮值,
没绑组全员轮值(最闲者先)。审批点与活动点同一口径。
</Note>
{node.kind === 'activity' && node.flavor === 'approval' && (
<Note>
<b>审批点是系统内置要素</b>,不是活动 —— 声明即用、零开发、不进活动库(一个流程包里需要开发的
只有活动)。功能属性四件:审批岗位(复用负责人四策略解析)· 输入装配映射 · 驳回走向
(声明式:回指定节点重开新实例 / 终止)· 催办阶梯。运行时它产出<b>审批单</b>而非任务。
</Note>
)}
</Prop>
{node.kind === 'activity' && node.flavor !== 'approval' && (
<Prop ico="boxes" title="链接活动实现" extra="装载面:编排不含实现,链接决定可运行">
<Field k="实现" hint="活动住库;链好了这节点才算装载(激活判据之二)">
<select
className="g-select"
value={refAct?.activityId ?? ''}
onChange={(e) =>
patch((n) => ({ ...n, act: e.target.value === '' ? undefined : e.target.value }))
}
>
<option value="">未链接 —— 激活会被「活动装载」拦下</option>
{activities.map((a) => (
<option key={a.activityId} value={a.activityId}>
{a.name} · {a.activityId}
</option>
))}
</select>
</Field>
{refAct !== undefined ? (
<>
{/* 实现方式(范式/版本)是装载信息,只读展示 —— 编排面不决定它 */}
<div className="g-prop-ref">
<Chip tone={refAct.paradigm === 'loop' ? 'collab' : 'auto'} ico="git-commit">
{refAct.paradigm === 'loop' ? '实现:案例 loop' : '实现:标准 flow'}
</Chip>
<Chip tone="mute">
v{refAct.packageVersions[0]?.version ?? '—'} × a
{refAct.assetVersions.find((x) => x.effective)?.version ?? '—'}
</Chip>
</div>
<button
className="g-btn"
onClick={() => nav.jump('library', 'shelf', { k: 'act', id: refAct.activityId })}
>
<Ico name="arrow-right" s={13} />
去活动库看契约
</button>
</>
) : (
<Note tone="warn">还没链接活动库里已安装的实现 —— 图可以照画照存,激活前链上即可。</Note>
)}
</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>
</>
<input
className="g-input mono"
value={edge.condition ?? ''}
placeholder={outEdges.length === 1 ? '无条件' : '留空 = 默认兜底'}
onChange={(e) => patchEdge(index, (x) => ({ ...x, condition: e.target.value }))}
/>
</Field>
))}
</div>
)}
</div>
</div>