三面拆分⑯:流程包详情改三栏 · 字段只声明 · 版本可删可改名
**字段只声明,不做任何要求**(用户定):编排层不再碰字段 ——
· 激活闸只剩两面:结构闭合(dangling-node / multi-default-edge)与活动装载
(unknown-activity);字段类的检查(同名不同型、容忍分支被必填依赖)全部退役。
· 节点属性卡的「数据接口」从「推来源」改成**纯声明**:照活动契约列出吃什么、吐什么,
不标必填、不推来源、不提补全 —— 缺了怎么补是**活动内实现**的事,不在这一层现形。
· 字段类型支持数组:统一写成 `T[]`(如 `any[]`),出口口的字段也带上类型
(原来只有字段名)。实测 A2 的 benchmark 口显示「rows any[] · needSourcing boolean」。
**详情页改三栏**:左窄轨版本(132px) | 中流程图 | 右节点属性。
· 版本条一版一行(v几 · 状态 · 点/边计数),动作只在**看着的那一版**上展开 ——
窄栏放不下每行一排钮,也不该让人误点别版。
· 删掉「激活 · 双判据」整卡:没选节点时,属性栏就地列出还差哪几条(点一条定位过去);
两面都齐时盖一枚小检验章,不再占一整张卡。
· 删掉节点拣选行 —— 直接在图上点节点。
· 属性卡用包壳绝对定位:行高由中栏的图定,属性长了在卡内滚,不把图拉长
(实测卡高 467、明细内容 1039、可滚,图框不受影响)。窄轨里属性小卡改单列竖排。
· 顺带校正:上一轮那条「图框下沿对齐左列底栏顶线」的 20px 留白,
三栏改版后左栏已无底栏,没有对齐对象了 —— 改回四边等距(18px)。
**版本可删可改名**(新):
· 删:生效版不可删(先停用,ActiveVersionError → 409)、在途钉版不可删
(那些单要按它跑完,PinnedVersionError → 409);删掉正看的那版会落到剩下的最新版,
一版不剩就回货架。
· 改名:名字是**这条流的身份**,一次改及于所有版本 —— 版本列表上只显示 v1/v2…,
若只改一版的名,列表上什么也看不见,人会以为没生效。
· 5 条单测守两道闸与改名口径;实测走查:改名落库 → 改回;生效版删 409、停用版删 200。
process CONTRACT.md 与产品设计.md 同步。五闸绿。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
@@ -2,8 +2,9 @@
|
||||
* feature `gov-process` —— 流程包(设计态):**一条流一个包,版本收在包内部**。
|
||||
*
|
||||
* 一区一内容:三枚计数摆在货架顶端,底下就是包货架(触发器已独立成区)。
|
||||
* 货架一流程一卡(不按版本平铺);进详情后左 1/3 是版本列表、右 2/3 是流程图,
|
||||
* 图下是节点属性(身份 · 数据接口 · 装载 · 出边条件),再往下是调用接口与触发器。
|
||||
* 货架一流程一卡(不按版本平铺);进详情是**三栏**:左窄轨=版本(选中那版才出动作:
|
||||
* 激活/停用 · 改名 · 删除)、中=流程图、右=节点属性(点图选节点,内容长了卡内滚,不拉长图)。
|
||||
* 「激活判据」不再单占一卡 —— 没选节点时,属性栏就地列出还差哪几条,点一条定位过去。
|
||||
*
|
||||
* 版本是**运行时开关**,不是发布流程:
|
||||
* · 编辑即其身:保存**写回当前版本**(号不变);新版本只出自「新建」
|
||||
@@ -14,7 +15,14 @@
|
||||
import { useRef, useState, useEffect } from 'react';
|
||||
import { useMutation, useQueryClient, useQuery } from '@tanstack/react-query';
|
||||
import type { ClosureReport, ProcessPackage, ProcessNode } from '@contracts';
|
||||
import { processPackageSchema, saveProcessRoute, setProcessStateRoute, validateProcessRoute } from '@contracts';
|
||||
import {
|
||||
deleteProcessVersionRoute,
|
||||
processPackageSchema,
|
||||
renameProcessRoute,
|
||||
saveProcessRoute,
|
||||
setProcessStateRoute,
|
||||
validateProcessRoute,
|
||||
} from '@contracts';
|
||||
import { call } from '@web-shared/typed-fetch.ts';
|
||||
import { Ico } from '@web-shared/Ico.tsx';
|
||||
import type { AskSpec } from '@web-shared/gov/primitives.tsx';
|
||||
@@ -351,6 +359,8 @@ function FlowLab({
|
||||
const [dirty, setDirty] = useState(false);
|
||||
const [report, setReport] = useState<ClosureReport | undefined>(undefined);
|
||||
const [ask, setAsk] = useState<{ spec: AskSpec; danger?: boolean } | undefined>(undefined);
|
||||
/** 改名就地开一个输入框(名字改的是整条流,不是某一版) */
|
||||
const [renaming, setRenaming] = useState(false);
|
||||
const canvasRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const nodes = graph?.nodes ?? [];
|
||||
@@ -365,49 +375,6 @@ function FlowLab({
|
||||
(node.act === undefined && semanticNode?.kind === 'activity' && a.activityId === semanticNode.activityId),
|
||||
);
|
||||
|
||||
/*
|
||||
* 数据接口的来源推导(对齐 Mastra 名流动):这个节点的每个输入字段,运行时会从哪来 ——
|
||||
* ① 起单入参里有同名字段 → 起单带进;② 某个上游(沿边回溯)的输出口发同名字段 → 上游发;
|
||||
* ③ 都没有 → 受理归一时派补全单找人收。设计期看得见,不必等跑起来猜。
|
||||
*/
|
||||
const fieldSources = (() => {
|
||||
if (node === null) return new Map<string, string>();
|
||||
const initFields = new Set(
|
||||
Object.keys(((pkg.callInterface.input as { properties?: Record<string, unknown> }).properties ?? {})),
|
||||
);
|
||||
// 祖先集:沿边回溯(含回边,访问集防环)
|
||||
const ancestors = new Set<string>();
|
||||
const queue = [node.id];
|
||||
while (queue.length > 0) {
|
||||
const cur = queue.pop()!;
|
||||
for (const e of graph?.edges ?? []) {
|
||||
if (e.to !== cur || ancestors.has(e.from)) continue;
|
||||
ancestors.add(e.from);
|
||||
queue.push(e.from);
|
||||
}
|
||||
}
|
||||
const actOf = (nid: string): string | undefined => {
|
||||
const g = nodes.find((x) => x.id === nid);
|
||||
if (g?.act !== undefined) return g.act;
|
||||
const sem = pkg.nodes.find((x) => x.id === nid);
|
||||
return sem?.kind === 'activity' ? sem.activityId : undefined;
|
||||
};
|
||||
const out = new Map<string, string>();
|
||||
for (const f of refAct?.contract.inputFields ?? []) {
|
||||
if (initFields.has(f.name)) {
|
||||
out.set(f.name, '起单入参');
|
||||
continue;
|
||||
}
|
||||
const from = [...ancestors].find((nid) =>
|
||||
activities
|
||||
.find((a) => a.activityId === actOf(nid))
|
||||
?.contract.outputPorts.some((port) => port.fields.includes(f.name)),
|
||||
);
|
||||
out.set(f.name, from !== undefined ? `上游 ${from}` : '补全单');
|
||||
}
|
||||
return out;
|
||||
})();
|
||||
|
||||
// 每次进页问一次闭合 —— 画图即校验(与后端裁决同源);落笔保存后用返回的报告
|
||||
const liveReport = useQuery({
|
||||
queryKey: ['gov', 'process-report', pkg.id, pkg.version],
|
||||
@@ -436,7 +403,6 @@ function FlowLab({
|
||||
|
||||
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');
|
||||
|
||||
// 画布上的选中态:diagram-js 给每个形状挂了 data-element-id,外挂一个类,不改画布件
|
||||
useEffect(() => {
|
||||
@@ -477,6 +443,34 @@ function FlowLab({
|
||||
},
|
||||
});
|
||||
|
||||
/** 删一版:生效版与在途钉版都会被服务端挡回来(409),判词照抄 */
|
||||
const dropVersion = useMutation({
|
||||
mutationFn: (version: number) =>
|
||||
call<{ packages: ProcessPackage[] }>(deleteProcessVersionRoute, {
|
||||
params: { id: pkg.id, version: String(version) },
|
||||
}),
|
||||
onSuccess: (out) => {
|
||||
void queryClient.invalidateQueries({ queryKey: ['gov', 'process'] });
|
||||
setAsk(undefined);
|
||||
// 删掉的正是在看的这一版 → 落到还剩的最新一版;一版不剩就回货架
|
||||
const rest = out.packages;
|
||||
if (rest.length === 0) nav.jump('workflows', 'pkgs');
|
||||
else if (rest[0] !== undefined) onSaved(rest[0].version);
|
||||
},
|
||||
onError: (e: unknown) => setSaveErr((e as { message?: string }).message ?? '删不掉'),
|
||||
});
|
||||
|
||||
/** 改名:名字是这条流的身份 —— 一次改及于所有版本 */
|
||||
const rename = useMutation({
|
||||
mutationFn: (name: string) =>
|
||||
call<{ packages: ProcessPackage[] }>(renameProcessRoute, { params: { id: pkg.id }, body: { name } }),
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({ queryKey: ['gov', 'process'] });
|
||||
setRenaming(false);
|
||||
},
|
||||
onError: (e: unknown) => setSaveErr((e as { message?: string }).message ?? '改名失败'),
|
||||
});
|
||||
|
||||
/**
|
||||
* 新建版本 = **以当前看的版本为底克隆一版**(服务端定版号,**不生效**)。
|
||||
* 与「激活」分工明确:激活只是把生效指针切过去,不产生新版本;
|
||||
@@ -538,6 +532,23 @@ function FlowLab({
|
||||
},
|
||||
});
|
||||
|
||||
const askDrop = (v: ProcessPackage) =>
|
||||
setAsk({
|
||||
danger: true,
|
||||
spec: {
|
||||
title: `删除 v${v.version}:${pkg.name}`,
|
||||
note: (
|
||||
<Note tone="warn">
|
||||
版本删了**不可撤销**。生效版与**在途钉版**删不掉 —— 后者那些单要按这一版跑完,
|
||||
定义没了就再也推不动。
|
||||
</Note>
|
||||
),
|
||||
impact: { who: '这一版的定义不再存在', undo: '不可撤销', when: '确认即删' },
|
||||
okLabel: `删掉 v${v.version}`,
|
||||
onOk: () => dropVersion.mutate(v.version),
|
||||
},
|
||||
});
|
||||
|
||||
const askRetire = (v: ProcessPackage) =>
|
||||
setAsk({
|
||||
danger: true,
|
||||
@@ -652,12 +663,12 @@ function FlowLab({
|
||||
{/* 编排工作区:左 1/3 版本列表 + 右 2/3 画布 —— 版本收在流程内部,不平铺台面 */}
|
||||
<Sec ico="route" title="编排工作区">
|
||||
<div className="g-flowlab">
|
||||
<aside className="g-vercard">
|
||||
{/* 题头与右侧图题成对:「流程包 · 版本」对「流程图 · 顺序」;新建与编辑同款芯片 */}
|
||||
{/* 左栏窄:一版一行只放「v几 + 状态」;动作(激活/停用/改名/删除)只在选中行展开 */}
|
||||
<aside className="g-vercard g-verslim">
|
||||
<div className="g-vercard-h">
|
||||
流程包 · 版本
|
||||
<button className="g-ver-new" onClick={askClone} disabled={cloneVersion.isPending}>
|
||||
<Ico name="plus" s={11} /> 新建
|
||||
版本
|
||||
<button className="g-ver-new" onClick={askClone} disabled={cloneVersion.isPending} title="以当前版为底克隆一版">
|
||||
<Ico name="plus" s={11} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="g-vercard-list">
|
||||
@@ -665,52 +676,64 @@ function FlowLab({
|
||||
const isActive = v.version === activeVersion;
|
||||
const isShown = v.version === pkg.version;
|
||||
return (
|
||||
<div
|
||||
key={v.version}
|
||||
className={'g-ver' + (isShown ? ' on' : '') + (isActive ? ' live' : '')}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => onPick(v.version)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') onPick(v.version);
|
||||
}}
|
||||
>
|
||||
<b>v{v.version}</b>
|
||||
<span className={'st' + (isActive ? ' live' : '')}>{isActive ? '生效中' : '停用'}</span>
|
||||
<span className="m">
|
||||
<div key={v.version} className={'g-verrow' + (isShown ? ' on' : '') + (isActive ? ' live' : '')}>
|
||||
<button className="g-verrow-h" 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}边
|
||||
{v.nodes.length}点·{v.edges.length}边
|
||||
</span>
|
||||
{isActive ? (
|
||||
<button
|
||||
className="g-ver-act"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
askRetire(v);
|
||||
}}
|
||||
>
|
||||
停用
|
||||
</button>
|
||||
{isShown && (
|
||||
/* 动作只在**看着的那一版**上出现:窄栏放不下每行一排钮,也不该让人误点别版 */
|
||||
<div className="g-verrow-a">
|
||||
{isActive ? (
|
||||
<button className="g-ver-act" onClick={() => askRetire(v)}>
|
||||
停用
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className="g-ver-act t-go"
|
||||
/* 不闭合点不了:结构闭合 + 活动装载两面清零才亮 */
|
||||
disabled={closedOf(v.version) !== true}
|
||||
title={closedOf(v.version) === true ? `激活 v${v.version}` : '还不闭合 —— 属性面会列出差哪几条'}
|
||||
onClick={() => askActivate(v)}
|
||||
>
|
||||
激活
|
||||
</button>
|
||||
)}
|
||||
<button className="g-ver-act" onClick={() => setRenaming(true)} title="改这条流的名字(所有版本同名)">
|
||||
改名
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className="g-ver-act t-go"
|
||||
/* 不闭合点不了:异常清零才亮 —— 完整性闸在激活,不在画图 */
|
||||
disabled={closedOf(v.version) !== true}
|
||||
title={closedOf(v.version) === true ? `激活 v${v.version}` : '有异常,先看异常看板'}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
askActivate(v);
|
||||
}}
|
||||
className="g-ver-act t-bad"
|
||||
disabled={isActive || dropVersion.isPending}
|
||||
title={isActive ? '生效版删不掉 —— 先停用' : `删掉 v${v.version}`}
|
||||
onClick={() => askDrop(v)}
|
||||
>
|
||||
激活
|
||||
删除
|
||||
</button>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="g-vercard-f">恰一个生效版本;在途工作流按启动时的版本跑完,启停与新建都不影响它们。</div>
|
||||
{renaming && (
|
||||
<div className="g-verrename">
|
||||
<input
|
||||
className="g-input"
|
||||
autoFocus
|
||||
defaultValue={pkg.name}
|
||||
placeholder="流程名"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') rename.mutate((e.target as HTMLInputElement).value.trim());
|
||||
if (e.key === 'Escape') setRenaming(false);
|
||||
}}
|
||||
/>
|
||||
<span className="t">回车改名 · Esc 取消 —— 名字是这条流的身份,各版本同名</span>
|
||||
</div>
|
||||
)}
|
||||
</aside>
|
||||
|
||||
<div className="g-canvas" ref={canvasRef}>
|
||||
@@ -750,90 +773,10 @@ function FlowLab({
|
||||
</div>
|
||||
|
||||
{/*
|
||||
* 左下:异常看板 —— 与版本卡同列同风(图纸式)。
|
||||
* 画图不检查,这里**替**你盯着:哪个节点缺什么,点一条定位到属性去补;
|
||||
* 清零了激活按钮才亮 —— 完整性闸在激活,不在画图。
|
||||
* 右栏:节点属性。外面这层包壳是**为了不撑高整行** —— 卡绝对定位撑满行高,
|
||||
* 行高由中栏的图定;属性长了在卡内滚,不把图拉长(用户要的「卡片高度跟图一致」)。
|
||||
*/}
|
||||
<aside className="g-vercard g-exboard">
|
||||
<div className="g-vercard-h">激活 · 双判据</div>
|
||||
{activeReport === undefined ? (
|
||||
<div className="g-vercard-list g-exsheet">
|
||||
<div className="g-exboard-empty">校验中…</div>
|
||||
</div>
|
||||
) : (
|
||||
(() => {
|
||||
/* 能不能激活看两面:①图编排闭合完善 ②活动装载。unknown-activity 归装载,其余归编排。 */
|
||||
const loading = activeReport.violations.filter((v) => v.kind === 'unknown-activity');
|
||||
const drawing = activeReport.violations.filter((v) => v.kind !== 'unknown-activity');
|
||||
const row = (v: (typeof activeReport.violations)[number], i: number) => {
|
||||
const nid = v.nodeId ?? pkg.edges.find((e) => e.id === v.edgeId)?.to;
|
||||
return (
|
||||
<div
|
||||
key={`${v.kind}-${i}`}
|
||||
className="g-ver g-ex"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => {
|
||||
if (nid !== undefined) setSel(String(nid));
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && nid !== undefined) setSel(String(nid));
|
||||
}}
|
||||
>
|
||||
{nid !== undefined && <b>{String(nid)}</b>}
|
||||
<span className="msg">{v.message}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
/* 检验单式:两格判据面均分卡高,面体列违规行;全清的面盖检验章,两面全清出裁定带 */
|
||||
return (
|
||||
<div className="g-vercard-list g-exsheet">
|
||||
<section className="g-exface">
|
||||
<div className={'g-exfacet' + (drawing.length === 0 ? ' ok' : '')}>
|
||||
<Ico name={drawing.length === 0 ? 'circle-check' : 'triangle-alert'} s={12} />
|
||||
判据一 · 图编排
|
||||
<span className="st">{drawing.length === 0 ? '闭合' : `${drawing.length} 项待补`}</span>
|
||||
</div>
|
||||
<div className="g-exface-b">
|
||||
{drawing.length === 0 ? (
|
||||
<div className="g-exstamp">
|
||||
编排闭合<small>CHK·1 — GRAPH CLOSED</small>
|
||||
</div>
|
||||
) : (
|
||||
drawing.map(row)
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
<section className="g-exface">
|
||||
<div className={'g-exfacet' + (loading.length === 0 ? ' ok' : '')}>
|
||||
<Ico name={loading.length === 0 ? 'circle-check' : 'triangle-alert'} s={12} />
|
||||
判据二 · 活动装载
|
||||
<span className="st">{loading.length === 0 ? '齐' : `${loading.length} 个未链接实现`}</span>
|
||||
</div>
|
||||
<div className="g-exface-b">
|
||||
{loading.length === 0 ? (
|
||||
<div className="g-exstamp">
|
||||
装载齐备<small>CHK·2 — IMPL LINKED</small>
|
||||
</div>
|
||||
) : (
|
||||
loading.map(row)
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
{activeReport.violations.length === 0 && (
|
||||
<div className="g-exverdict">
|
||||
两面都齐 —— v{pkg.version} 可运行。
|
||||
{pkg.version === activeVersion ? '这一版正在生效。' : '版本行的「激活」已亮,点它即可上线。'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})()
|
||||
)}
|
||||
<div className="g-vercard-f">点一条定位到属性去补;两面清零,激活才亮。</div>
|
||||
</aside>
|
||||
|
||||
{/* 右下:节点属性(图纸卡,与流程图同宽)—— 闭环(岗位/判据/映射)在这里决定 */}
|
||||
<div className="g-nodewrap">
|
||||
<div className="g-vercard g-nodecard">
|
||||
<div className="g-vercard-h">
|
||||
节点 · 属性
|
||||
@@ -855,39 +798,53 @@ function FlowLab({
|
||||
{saveErr}
|
||||
</Note>
|
||||
)}
|
||||
{/* 索引栏 = 图框分度带:节点行 + 边行,行首竖签;固定不滚,属性区在下面自己滚 */}
|
||||
<div className="g-pickrail">
|
||||
<div className="g-pickrow">
|
||||
<span className="g-picklbl">
|
||||
节点<b>{nodes.filter((n) => n.kind !== 'start' && n.kind !== 'end' && n.kind !== 'end-bad').length}</b>
|
||||
</span>
|
||||
<div className="g-picker">
|
||||
{nodes
|
||||
.filter((n) => n.kind !== 'start' && n.kind !== 'end' && n.kind !== 'end-bad')
|
||||
.map((n) => (
|
||||
<button
|
||||
key={n.id}
|
||||
className={`g-pick t-${kindTone(n)}` + (sel === n.id ? ' on' : '')}
|
||||
onClick={() => setSel(sel === n.id ? null : n.id)}
|
||||
>
|
||||
<b className="g-mono">{n.id}</b>
|
||||
{n.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* key 随选中对象换 —— 换一件看,明细区滚回顶,不带上一件的滚动位 */}
|
||||
<div className="g-propsheet" key={node !== null ? `n:${node.id}` : 'blank'}>
|
||||
{node === null ? (
|
||||
/*
|
||||
* 没选节点时,这块地方替原来的「激活判据」看板:还差哪几条就列在这儿,点一条定位过去。
|
||||
* 激活闸只剩两面 —— **结构闭合**与**活动装载**;字段一概不查(只声明,不要求)。
|
||||
*/
|
||||
<div className="g-sheetempty">
|
||||
<Ico name="target" s={20} />
|
||||
<b>点画布或拣选条,选一个节点</b>
|
||||
<p>
|
||||
节点填名称、链接实现、往哪走(出边条件)。边不是可编辑对象 —— 它只表达顺序;
|
||||
点画布上的线会选中发起它的那个节点。负责人运营时分配,不在这儿;
|
||||
审批裁决(verdict)与放行选口(outcome)由系统注入判据,不用你声明。
|
||||
</p>
|
||||
{(activeReport?.violations.length ?? 0) === 0 ? (
|
||||
<>
|
||||
<Ico name="target" s={20} />
|
||||
<b>在图上点一个节点</b>
|
||||
<p>
|
||||
节点填名称、链接实现、往哪走(出边条件)。边不是可编辑对象 —— 它只表达顺序;
|
||||
点线会选中发起它的那个节点。负责人运营时分配,不在这儿。
|
||||
</p>
|
||||
{activeReport !== undefined && (
|
||||
<span className="g-okstamp">
|
||||
<Ico name="circle-check" s={13} />
|
||||
结构闭合 · 活动装载齐备 —— v{pkg.version} 可运行
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="g-lack">
|
||||
<div className="g-lack-h">
|
||||
<Ico name="triangle-alert" s={13} />
|
||||
还差 {activeReport!.violations.length} 条才能激活
|
||||
</div>
|
||||
{activeReport!.violations.map((v, i) => {
|
||||
const nid = v.nodeId ?? pkg.edges.find((e) => e.id === v.edgeId)?.to;
|
||||
return (
|
||||
<button
|
||||
key={`${v.kind}-${i}`}
|
||||
className="g-lack-r"
|
||||
disabled={nid === undefined}
|
||||
onClick={() => {
|
||||
if (nid !== undefined) setSel(String(nid));
|
||||
}}
|
||||
>
|
||||
{nid !== undefined && <b>{String(nid)}</b>}
|
||||
<span>{v.message}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
@@ -923,43 +880,39 @@ function FlowLab({
|
||||
|
||||
{node.kind === 'activity' && node.flavor !== 'approval' && (
|
||||
/*
|
||||
* 数据接口(对齐 Mastra):边不传数据,约束力全在输入/输出字段 ——
|
||||
* 这里不填写、只**看清**:每个输入字段运行时从哪来(起单 / 上游同名 / 补全单),
|
||||
* 每个输出口发什么名。要改字段,改的是活动契约,不是这张卡。
|
||||
* 数据接口 —— **只声明,不做任何要求**(2026-07-30 用户定):
|
||||
* 这里照着活动契约把「吃什么、吐什么」列出来,不标必填、不推来源、不校验类型。
|
||||
* 字段够不够、缺了怎么补,是**活动内实现**的事,不在编排层现形。
|
||||
* 数组照实写成 `T[]` —— 声明得看得出是一个还是一串。
|
||||
*/
|
||||
<Prop
|
||||
ico="git-merge"
|
||||
title="数据接口 · 按字段名流动"
|
||||
extra={refAct === undefined ? '先链接活动实现' : '契约即管道:名字对上,数据就到'}
|
||||
title="数据接口 · 声明"
|
||||
extra={refAct === undefined ? '先链接活动实现' : '只声明,不做任何要求'}
|
||||
>
|
||||
{refAct === undefined ? (
|
||||
<Note>链接活动实现后,这里列出它吃什么、吐什么、每个字段从哪来。</Note>
|
||||
<Note>链接活动实现后,这里照它的契约列出吃什么、吐什么。</Note>
|
||||
) : (
|
||||
<>
|
||||
<div className="g-dif">
|
||||
<em>输入 · {refAct.contract.inputFields.length} 字段</em>
|
||||
{refAct.contract.inputFields.map((f) => {
|
||||
const src = fieldSources.get(f.name) ?? '补全单';
|
||||
return (
|
||||
<div key={f.name} className="g-dif-r">
|
||||
<span className="k">
|
||||
{f.name}
|
||||
{f.required && <i>必填</i>}
|
||||
</span>
|
||||
<span className="t">{f.type}</span>
|
||||
<span className={'s' + (src === '补全单' ? ' ask' : '')}>
|
||||
{src === '补全单' ? '← 补全单(受理归一找人收)' : `← ${src}`}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{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.join(' · ') : '(整包事实)'}</span>
|
||||
<span className="f">
|
||||
{port.fields.length > 0
|
||||
? port.fields.map((f) => `${f.name} ${f.type}`).join(' · ')
|
||||
: '(整包事实)'}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -1094,6 +1047,7 @@ function FlowLab({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Sec>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user