Archived
两处 UI 重做:节点属性 = 图签 + 画布同款节点芯片;邮件输入 = 台面上的一张纸信
**流程包 · 节点属性**:泛用表单换成**图签**(title block)——
· 卡顶一枚从画布上「拎下来」的节点芯片:活动蓝 E8F1FB/4E7DAF、审批琥珀
FBF0DC/B98432,左上角标同画布 —— 属性改的是哪一件,一眼对上,不用回图上找。
· 字段收成图签行:76px 标签格(mono 竖签 + 格线)+ 值格,行间细实线 ——
表格,不是表单;值格输入无框,格子本身就是边界。
· 出边收成路由小表:带题一行(共用提示住右角:活动点「条件读本节点的输出字段」/
审批点「条件读审批裁决 verdict」),每行 E号 → 去向 + 去向节点名,条件在值位;
从前三条重复的长提示删成一条,空条件的语义进 placeholder(留空 = 默认兜底)。
**测试台 · 邮件输入**:表单换成**一张纸信压在暗台面上** —— 整台仪器里唯一的浅色面,
把「机器说 mono、信是外界」推到视觉结论:
· 暖白纸面(FBFAF6)+ 纸边细线 + 投影浮起;信上全是无衬线,标签是小号暖灰。
· 解剖照邮件写:信头三行(发件人 / 收件箱▾ / 主题加粗)→ 正文(纸上的一片空,
不描边不描线 —— 写信不是填表,resize 手柄也摘了)→ 附件签条(虚线隔开,
纸质小签 + 「加一个 BOM」虚框钮)。
· 聚焦色在纸上转琥珀(纸是暖的,磷光绿留给机器侧)。
实测:改坏主题 → 预检 0/2,改回 → 2/2 —— 灯对纸上的字照样实时。
旧 .tb-ml / .g-att 两组样式退役。五闸绿。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
@@ -852,37 +852,49 @@ function FlowLab({
|
||||
* 也不再摆数据契约声明:数据契约**归活动自己**,要看去活动库看(下面那枚钮直达)。
|
||||
*/
|
||||
<div className="g-nodeform">
|
||||
<Field k="节点 ID" hint="编排内唯一,边按它连、判据按它寻址">
|
||||
<input className="g-input mono" value={node.id} readOnly />
|
||||
</Field>
|
||||
<Field k="名称">
|
||||
{/*
|
||||
* 你点的就是它:选中的节点以**画布同款皮肤**摆在卡顶(活动蓝 / 审批琥珀,
|
||||
* 左上角标同画布)—— 属性改的是哪一件,一眼对上,不用回图上找。
|
||||
*/}
|
||||
<div className={'g-nchip' + (node.flavor === 'approval' ? ' apr' : '')}>
|
||||
<i>{node.id}</i>
|
||||
<b>{node.label || '(未命名)'}</b>
|
||||
<em>{node.flavor === 'approval' ? '审批点' : '活动点'}</em>
|
||||
</div>
|
||||
|
||||
{/* 图签行:字段名住左格(mono 竖签),值住右格 —— 与图框的图签同一形制 */}
|
||||
<div className="g-tbrow">
|
||||
<em>ID</em>
|
||||
<span className="v mono">{node.id}</span>
|
||||
</div>
|
||||
<div className="g-tbrow">
|
||||
<em>名称</em>
|
||||
<input
|
||||
className="g-input"
|
||||
className="g-tbin"
|
||||
value={node.label}
|
||||
placeholder="节点名称"
|
||||
onChange={(e) => patch((n) => ({ ...n, label: e.target.value }))}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
{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>
|
||||
</>
|
||||
<div className="g-tbrow">
|
||||
<em>类型</em>
|
||||
<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 }))}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{node.kind === 'activity' && node.flavor !== 'approval' && (
|
||||
<>
|
||||
<Field k="链接实现" hint="活动住库;链好了这节点才算装载(激活判据之二)">
|
||||
<div className="g-tbrow">
|
||||
<em>实现</em>
|
||||
<span className="v col">
|
||||
<select
|
||||
className="g-select"
|
||||
className="g-tbin sel"
|
||||
value={refAct?.activityId ?? ''}
|
||||
onChange={(e) => patch((n) => ({ ...n, act: e.target.value === '' ? undefined : e.target.value }))}
|
||||
>
|
||||
@@ -893,47 +905,63 @@ function FlowLab({
|
||||
</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>
|
||||
)}
|
||||
</>
|
||||
{refAct !== undefined ? (
|
||||
/* 数据契约归活动自己 —— 要看它吃什么吐什么,去活动库看 */
|
||||
<button
|
||||
className="g-tblink"
|
||||
onClick={() => nav.jump('library', 'shelf', { k: 'act', id: refAct.activityId })}
|
||||
>
|
||||
去活动库看契约 <Ico name="arrow-right" s={11} />
|
||||
</button>
|
||||
) : (
|
||||
<small className="warn">图可以照画照存,激活前链上即可</small>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{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;留空即默认兜底'
|
||||
}
|
||||
>
|
||||
<input
|
||||
className="g-input mono"
|
||||
value={edge.condition ?? ''}
|
||||
placeholder={outEdges.length === 1 ? '无条件' : '留空 = 默认兜底'}
|
||||
onChange={(e) => patchEdge(index, (x) => ({ ...x, condition: e.target.value }))}
|
||||
/>
|
||||
</Field>
|
||||
))}
|
||||
{outEdges.length > 0 && (
|
||||
<>
|
||||
{/*
|
||||
* 出边 = 路由小表:**分支归发起它的这个节点管**(Mastra 的
|
||||
* `.branch([[条件, 下一步], …])` 挂在上一步上)。三条重复的长提示收成一句;
|
||||
* 条件留空即默认兜底(placeholder 说),边本身只是画布上的一条线。
|
||||
*/}
|
||||
<div className="g-tbband">
|
||||
出边 · {outEdges.length === 1 ? '直连' : '命中即走'}
|
||||
<span className="x">
|
||||
{node.kind === 'activity' && node.flavor === 'approval'
|
||||
? '条件读审批裁决 verdict'
|
||||
: '条件读本节点的输出字段'}
|
||||
</span>
|
||||
</div>
|
||||
{outEdges.map(({ edge, index }) => {
|
||||
const target = nodes.find((n) => n.id === edge.to);
|
||||
return (
|
||||
<div key={edge.code ?? index} className="g-tbedge">
|
||||
<span className="rt">
|
||||
<b>{edge.code ?? `边${index + 1}`}</b>
|
||||
<Ico name="arrow-right" s={11} />
|
||||
<span className="to">{edge.to}</span>
|
||||
{target !== undefined && target.label !== '' && <span className="nm">{target.label}</span>}
|
||||
</span>
|
||||
<input
|
||||
className="g-tbin mono"
|
||||
value={edge.condition ?? ''}
|
||||
placeholder={
|
||||
outEdges.length === 1
|
||||
? '无条件直连'
|
||||
: node.kind === 'activity' && node.flavor === 'approval'
|
||||
? 'verdict = "pass" · 留空 = 默认兜底'
|
||||
: 'needAlternative = true · 留空 = 默认兜底'
|
||||
}
|
||||
onChange={(e) => patchEdge(index, (x) => ({ ...x, condition: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -419,59 +419,65 @@ function LetterForm({
|
||||
}): ReactNode {
|
||||
const set = <K extends keyof MailDraft>(k: K, v: MailDraft[K]): void => onChange({ ...draft, [k]: v });
|
||||
return (
|
||||
<div className="tb-ml">
|
||||
<label className="tb-ml-r">
|
||||
<em>发件人</em>
|
||||
<input className="tb-ml-i" value={draft.from} onChange={(e) => set('from', e.target.value)} />
|
||||
</label>
|
||||
<label className="tb-ml-r">
|
||||
<em>收件箱</em>
|
||||
{/* 投给哪个信箱决定用哪套抽键规则 —— 选,不手打:地址错一位,预检整屏熄灯还不知道为什么 */}
|
||||
<select className="tb-ml-i sel" value={channel?.address ?? ''} onChange={(e) => set('to', e.target.value)}>
|
||||
{channels.map((ch) => (
|
||||
<option key={ch.id} value={ch.address}>
|
||||
{ch.address} —— {ch.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="tb-ml-r">
|
||||
<em>主题</em>
|
||||
<input className="tb-ml-i" value={draft.subject} onChange={(e) => set('subject', e.target.value)} />
|
||||
</label>
|
||||
<label className="tb-ml-r tall">
|
||||
<em>正文</em>
|
||||
<textarea className="tb-ml-i" rows={4} value={draft.body} onChange={(e) => set('body', e.target.value)} />
|
||||
</label>
|
||||
<div className="tb-ml-r tall">
|
||||
<em>附件</em>
|
||||
<div className="tb-ml-at">
|
||||
{draft.attachments.map((a, i) => (
|
||||
<span key={a.name} className="g-att">
|
||||
<Ico name="paperclip" s={11} />
|
||||
{a.name}
|
||||
<i>{(a.size / 1024).toFixed(0)} KB</i>
|
||||
<button
|
||||
aria-label={`移除 ${a.name}`}
|
||||
onClick={() => set('attachments', draft.attachments.filter((_, j) => j !== i))}
|
||||
>
|
||||
<Ico name="x" s={10} />
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
<button
|
||||
className="g-att-add"
|
||||
onClick={() =>
|
||||
set('attachments', [
|
||||
...draft.attachments,
|
||||
{ name: `BOM-${draft.attachments.length + 1}.xlsx`, contentType: XLSX, size: 20_480 },
|
||||
])
|
||||
}
|
||||
>
|
||||
<Ico name="plus" s={11} />
|
||||
加一个 BOM
|
||||
</button>
|
||||
</div>
|
||||
/*
|
||||
* 一张**纸信**压在暗台面上 —— 整台仪器里唯一的浅色面。
|
||||
* 「机器说 mono、信是外界」推到视觉结论:外界伸进仪器的那件东西,就长得像张纸。
|
||||
* 解剖照邮件写:信头三行(发件人 / 收件箱 / 主题)→ 正文 → 附件签条。
|
||||
* 收件箱是**选**的不是打的:投给哪个信箱决定用哪套抽键规则,地址错一位,
|
||||
* 左边预检整屏熄灯还不知道为什么。
|
||||
*/
|
||||
<div className="tb-paper">
|
||||
<div className="tb-pp-h">
|
||||
<label className="tb-pp-r">
|
||||
<em>发件人</em>
|
||||
<input className="tb-pp-i" value={draft.from} onChange={(e) => set('from', e.target.value)} />
|
||||
</label>
|
||||
<label className="tb-pp-r">
|
||||
<em>收件箱</em>
|
||||
<select className="tb-pp-i sel" value={channel?.address ?? ''} onChange={(e) => set('to', e.target.value)}>
|
||||
{channels.map((ch) => (
|
||||
<option key={ch.id} value={ch.address}>
|
||||
{ch.address} —— {ch.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="tb-pp-r">
|
||||
<em>主题</em>
|
||||
<input className="tb-pp-i strong" value={draft.subject} onChange={(e) => set('subject', e.target.value)} />
|
||||
</label>
|
||||
</div>
|
||||
<textarea
|
||||
className="tb-pp-body"
|
||||
rows={5}
|
||||
value={draft.body}
|
||||
placeholder="正文…"
|
||||
aria-label="正文"
|
||||
onChange={(e) => set('body', e.target.value)}
|
||||
/>
|
||||
<div className="tb-pp-att">
|
||||
{draft.attachments.map((a, i) => (
|
||||
<span key={a.name} className="tb-att">
|
||||
<Ico name="paperclip" s={11} />
|
||||
{a.name}
|
||||
<i>{(a.size / 1024).toFixed(0)} KB</i>
|
||||
<button aria-label={`移除 ${a.name}`} onClick={() => set('attachments', draft.attachments.filter((_, j) => j !== i))}>
|
||||
<Ico name="x" s={10} />
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
<button
|
||||
className="tb-att-add"
|
||||
onClick={() =>
|
||||
set('attachments', [
|
||||
...draft.attachments,
|
||||
{ name: `BOM-${draft.attachments.length + 1}.xlsx`, contentType: XLSX, size: 20_480 },
|
||||
])
|
||||
}
|
||||
>
|
||||
<Ico name="plus" s={11} />
|
||||
加一个 BOM
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user