两处 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:
2026-07-29 18:11:44 +08:00
co-authored by Claude Opus 5
parent 6cff6ab34c
commit f8f208e945
3 changed files with 217 additions and 141 deletions
+69 -27
View File
@@ -630,13 +630,48 @@
/* 两面都齐:一枚小检验章,别再占一整张卡 */ /* 两面都齐:一枚小检验章,别再占一整张卡 */
.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; } .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; }
/* 节点属性**平铺**(2026-07-30):节点只决定几件事 —— 分类卡把四五个字段摊成半屏, /* ── 节点属性 = 图签(title block):字段名住左格(mono 竖签 + 细分隔线),值住右格 ──
撤掉;一列字段直排,字段之间一条细线断句就够。 */ 与图框右下角的图签、左栏的版本条同一形制;卡顶一枚**画布同款皮肤**的节点芯片,
「你点的就是它」不用回图上找。 */
.g-nodeform { display: flex; flex-direction: column; gap: 0; } .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; } /* 节点芯片:画布皮肤原样(活动蓝 E8F1FB/4E7DAF · 审批琥珀 FBF0DC/B98432),
.g-nodeform > .g-note { margin: 11px 0 0; } 左上角标 = 画布的 code 角标同款 mono 小字 */
.g-nodeform > .g-btn { align-self: flex-start; margin-top: 11px; } .g-nchip { position: relative; display: flex; align-items: center; gap: 8px; margin: 2px 0 12px; padding: 13px 12px 12px 14px; border: 1.5px solid #4e7daf; border-radius: 7px; background: #e8f1fb; }
.g-nchip > i { position: absolute; left: 7px; top: 3px; font: 600 8.5px/1 var(--sh-mono); font-style: normal; color: #8a93a0; }
.g-nchip > b { font-size: 13px; font-weight: 700; color: var(--sh-ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.g-nchip > em { margin-left: auto; font: 600 8.5px/1 var(--sh-mono); font-style: normal; letter-spacing: .12em; color: #4e7daf; }
.g-nchip.apr { border-color: #b98432; background: #fbf0dc; }
.g-nchip.apr > em { color: #b98432; }
/* 图签行:76px 标签格(右缘细线即「格线」)+ 值格;行间细实线 —— 表格,不是表单 */
.g-tbrow { display: grid; grid-template-columns: 76px minmax(0, 1fr); align-items: center; gap: 0 12px; padding: 9px 0; border-bottom: 1px solid var(--sh-grid, #edf1f5); }
.g-tbrow > em { align-self: start; padding-top: 2px; border-right: 1px solid var(--sh-line); font: 600 9px/1.7 var(--sh-mono); font-style: normal; letter-spacing: .14em; text-transform: uppercase; color: var(--sh-steel); }
.g-tbrow > .v { min-width: 0; font-size: 12.5px; color: var(--sh-ink); }
.g-tbrow > .v.mono { font-family: var(--sh-mono); font-weight: 700; font-size: 12px; }
.g-tbrow > .v.col { display: flex; flex-direction: column; align-items: flex-start; gap: 6px; }
.g-tbrow > .v small.warn { font-size: 10px; color: var(--sh-brass); }
/* 值格输入:无框,聚焦时行底线转钢蓝 —— 格子本身就是边界,不再套一层输入框 */
.g-tbin { width: 100%; box-sizing: border-box; border: 0; background: transparent; padding: 1px 0; color: var(--sh-ink); font: inherit; font-size: 12.5px; }
.g-tbin.mono { font-family: var(--sh-mono); font-size: 11.5px; }
.g-tbin::placeholder { color: #a8b2be; }
.g-tbin:focus { outline: 0; }
.g-tbrow:focus-within, .g-tbedge:focus-within { border-bottom-color: #4e7daf; }
.g-tbin.sel { appearance: none; padding-right: 18px; background: transparent url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="10" height="6" viewBox="0 0 10 6"><path d="M1 1l4 4 4-4" fill="none" stroke="%2346586A" stroke-width="1.5"/></svg>') no-repeat right 2px center; cursor: pointer; }
.g-tblink { display: inline-flex; align-items: center; gap: 4px; border: 0; background: transparent; padding: 0; font-size: 11px; color: #23507e; cursor: pointer; }
.g-tblink:hover { text-decoration: underline; }
.g-tbrow .g-seg { align-self: center; }
/* 出边带 + 路由行:E号 → 去向(名),条件在下一行的值位 —— 一条共用提示住带右角 */
.g-tbband { display: flex; align-items: baseline; gap: 8px; margin-top: 14px; padding: 7px 0 6px; border-bottom: 1px solid var(--sh-line); font: 600 9.5px/1 var(--sh-mono); letter-spacing: .14em; text-transform: uppercase; color: var(--sh-steel); }
.g-tbband .x { margin-left: auto; font-size: 8.5px; letter-spacing: .04em; text-transform: none; color: var(--sh-muted); }
.g-tbedge { display: flex; flex-direction: column; gap: 3px; padding: 8px 0 9px; border-bottom: 1px solid var(--sh-grid, #edf1f5); }
.g-tbedge .rt { display: flex; align-items: baseline; gap: 6px; font-family: var(--sh-mono); font-size: 10px; color: var(--sh-muted); }
.g-tbedge .rt b { font-size: 11px; font-weight: 700; color: var(--sh-ink); }
.g-tbedge .rt svg { align-self: center; color: var(--sh-muted); }
.g-tbedge .rt .to { font-weight: 700; color: var(--sh-steel); }
.g-tbedge .rt .nm { font-family: var(--sans); font-size: 11px; color: var(--sh-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.g-tbedge .g-tbin { padding-left: 0; }
/* 字段 = 图签小格:微墨签(mono 大写)+ 纸上填线(输入只留底线,聚焦底线转钢蓝) */ /* 字段 = 图签小格:微墨签(mono 大写)+ 纸上填线(输入只留底线,聚焦底线转钢蓝) */
.g-nodecard .g-field { gap: 3px; } .g-nodecard .g-field { gap: 3px; }
@@ -772,27 +807,34 @@
.tb-well-none { margin: 0; padding: 10px 12px; font-size: 11px; line-height: 1.8; color: var(--sh-muted); } .tb-well-none { margin: 0; padding: 10px 12px; font-size: 11px; line-height: 1.8; color: var(--sh-muted); }
.tb-well-t { margin: 0; padding: 0 12px 11px; font-size: 10px; line-height: 1.7; color: var(--sh-muted); } .tb-well-t { margin: 0; padding: 0 12px 11px; font-size: 10px; line-height: 1.7; color: var(--sh-muted); }
/* ── ① 一封信:外界的东西 —— 全屏唯一的无衬线区,标签在左、底线在下 ── */ /* ── ① 一封信 = 一张**纸**压在暗台面上:整台仪器里唯一的浅色面 ──
.tb-ml { display: flex; flex-direction: column; padding: 2px 14px 12px; } 「机器说 mono、信是外界」推到视觉结论 —— 外界伸进仪器的那件东西,长得像张纸。
.tb-ml-r { display: grid; grid-template-columns: 56px minmax(0, 1fr); align-items: center; gap: 12px; padding: 8px 0; border-bottom: 1px solid var(--sh-line); } 暖白纸面、纸边细线、投影浮起;信上全是无衬线,只有标签是小号暖灰。 */
.tb-ml-r.tall { align-items: start; } .tb-paper { margin: 12px 14px 14px; border: 1px solid #d9d3c4; border-radius: 5px; background: #fbfaf6; box-shadow: 0 2px 10px rgba(0, 0, 0, .38), 0 8px 26px -10px rgba(0, 0, 0, .5); }
.tb-ml-r:last-child { border-bottom: 0; } .tb-pp-h { padding: 4px 16px 0; }
.tb-ml-r > em { font: 600 9px/1.7 var(--sh-mono); letter-spacing: .14em; text-transform: uppercase; font-style: normal; color: var(--sh-muted); padding-top: 1px; } .tb-pp-r { display: grid; grid-template-columns: 52px minmax(0, 1fr); align-items: center; gap: 10px; padding: 8px 0 7px; border-bottom: 1px solid #ece7da; }
.tb-ml-i { width: 100%; box-sizing: border-box; border: 0; background: transparent; padding: 3px 0; color: var(--sh-ink); font: 400 13px/1.7 var(--sans); resize: vertical; } .tb-pp-r > em { font: 600 10px/1.6 var(--sans); font-style: normal; color: #8b8674; }
.tb-ml-i::placeholder { color: var(--sh-muted); } .tb-pp-i { width: 100%; box-sizing: border-box; border: 0; background: transparent; padding: 0; color: #2b3040; font: 400 13px/1.7 var(--sans); }
.tb-ml-i:focus { outline: 0; } .tb-pp-i.strong { font-weight: 600; }
.tb-ml-r:focus-within { border-bottom-color: var(--sh-live); } .tb-pp-i::placeholder { color: #b3ad9c; }
.tb-ml-i.sel { appearance: none; padding-right: 20px; background: transparent url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="10" height="6" viewBox="0 0 10 6"><path d="M1 1l4 4 4-4" fill="none" stroke="%2392a8c9" stroke-width="1.5"/></svg>') no-repeat right 4px center; cursor: pointer; } .tb-pp-i:focus { outline: 0; }
.tb-ml-i.sel option { background: var(--sh-panel); color: var(--sh-ink); } .tb-pp-r:focus-within { border-bottom-color: #b98432; }
.tb-ml-at { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; padding: 2px 0; } .tb-pp-i.sel { appearance: none; padding-right: 20px; background: transparent url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="10" height="6" viewBox="0 0 10 6"><path d="M1 1l4 4 4-4" fill="none" stroke="%238b8674" stroke-width="1.5"/></svg>') no-repeat right 4px center; cursor: pointer; }
.g-att { display: inline-flex; align-items: center; gap: 5px; padding: 4px 6px 4px 8px; border: 1px solid var(--sh-line); border-radius: 6px; background: var(--sh-panel-2); font-size: 11px; color: var(--sh-ink); } .tb-pp-i.sel option { background: #fbfaf6; color: #2b3040; }
.g-att svg { color: var(--sh-steel); } /* 正文:纸上的一片空,不描边不描线 —— 写信不是填表 */
.g-att i { font-style: normal; font-family: var(--sh-mono); font-size: 9.5px; color: var(--sh-muted); } .tb-pp-body { display: block; width: 100%; box-sizing: border-box; border: 0; background: transparent; padding: 12px 16px 6px; color: #2b3040; font: 400 13px/1.9 var(--sans); resize: none; min-height: 110px; }
.g-att button { display: inline-flex; padding: 2px; border: 0; background: transparent; color: var(--sh-muted); cursor: pointer; } .tb-pp-body::placeholder { color: #b3ad9c; }
.g-att button:hover { color: var(--sh-alarm); } .tb-pp-body:focus { outline: 0; }
.g-att-add { display: inline-flex; align-items: center; gap: 5px; padding: 5px 9px; border: 1px dashed var(--sh-lineb); border-radius: 6px; background: transparent; color: var(--sh-steel); font: inherit; font-size: 11px; cursor: pointer; } /* 附件签条:纸的下沿,一条虚线隔开 —— 夹在信末的东西 */
.g-att-add:hover { border-color: var(--sh-steel); color: var(--sh-ink); } .tb-pp-att { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; padding: 9px 16px 12px; border-top: 1px dashed #ddd7c8; }
.g-att-add:focus-visible, .g-att button:focus-visible { outline: 2px solid var(--sh-live); outline-offset: 1px; } .tb-att { display: inline-flex; align-items: center; gap: 5px; padding: 3px 6px 3px 8px; border: 1px solid #ddd7c8; border-radius: 4px; background: #f4f1e8; font: 500 11px/1.5 var(--sans); color: #4a4536; }
.tb-att svg { color: #8b8674; }
.tb-att i { font-style: normal; font-family: var(--sh-mono); font-size: 9px; color: #8b8674; }
.tb-att button { display: inline-flex; padding: 2px; border: 0; background: transparent; color: #8b8674; cursor: pointer; }
.tb-att button:hover { color: #b0413e; }
.tb-att-add { display: inline-flex; align-items: center; gap: 5px; padding: 4px 9px; border: 1px dashed #c9c2ae; border-radius: 4px; background: transparent; color: #8b8674; font: 500 11px/1.5 var(--sans); cursor: pointer; }
.tb-att-add:hover { border-color: #8b8674; color: #4a4536; }
.tb-att-add:focus-visible, .tb-att button:focus-visible, .tb-pp-i:focus-visible { outline: 2px solid #b98432; outline-offset: 1px; }
/* ── ② 节点指派:一条竖轨挂着所有节点,右端是插座 —— 插了人点琥珀 ── */ /* ── ② 节点指派:一条竖轨挂着所有节点,右端是插座 —— 插了人点琥珀 ── */
.tb-asn { display: flex; flex-direction: column; padding: 6px 14px 10px; } .tb-asn { display: flex; flex-direction: column; padding: 6px 14px 10px; }
@@ -852,37 +852,49 @@ function FlowLab({
* 也不再摆数据契约声明:数据契约**归活动自己**,要看去活动库看(下面那枚钮直达)。 * 也不再摆数据契约声明:数据契约**归活动自己**,要看去活动库看(下面那枚钮直达)。
*/ */
<div className="g-nodeform"> <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 <input
className="g-input" className="g-tbin"
value={node.label} value={node.label}
placeholder="节点名称" placeholder="节点名称"
onChange={(e) => patch((n) => ({ ...n, label: e.target.value }))} onChange={(e) => patch((n) => ({ ...n, label: e.target.value }))}
/> />
</Field> </div>
{node.kind === 'activity' && ( {node.kind === 'activity' && (
<> <div className="g-tbrow">
<Field k="节点类型" hint="只分两种;审批点是系统内置要素,不链活动"> <em>类型</em>
<Seg <Seg
value={node.flavor === 'approval' ? 'approval' : 'act'} value={node.flavor === 'approval' ? 'approval' : 'act'}
options={[ options={[
{ k: 'act', label: '活动点' }, { k: 'act', label: '活动点' },
{ k: 'approval', label: '审批点' }, { k: 'approval', label: '审批点' },
]} ]}
onPick={(k) => patch((n) => ({ ...n, flavor: k === 'approval' ? ('approval' as const) : undefined }))} onPick={(k) => patch((n) => ({ ...n, flavor: k === 'approval' ? ('approval' as const) : undefined }))}
/> />
</Field> </div>
</>
)} )}
{node.kind === 'activity' && node.flavor !== 'approval' && ( {node.kind === 'activity' && node.flavor !== 'approval' && (
<> <div className="g-tbrow">
<Field k="链接实现" hint="活动住库;链好了这节点才算装载(激活判据之二)"> <em>实现</em>
<span className="v col">
<select <select
className="g-select" className="g-tbin sel"
value={refAct?.activityId ?? ''} value={refAct?.activityId ?? ''}
onChange={(e) => patch((n) => ({ ...n, act: e.target.value === '' ? undefined : e.target.value }))} onChange={(e) => patch((n) => ({ ...n, act: e.target.value === '' ? undefined : e.target.value }))}
> >
@@ -893,47 +905,63 @@ function FlowLab({
</option> </option>
))} ))}
</select> </select>
</Field> {refAct !== undefined ? (
{refAct === undefined ? ( /* 数据契约归活动自己 —— 要看它吃什么吐什么,去活动库看 */
<Note tone="warn">还没链接活动库里已安装的实现 —— 图可以照画照存,激活前链上即可。</Note> <button
) : ( className="g-tblink"
/* 数据契约归活动自己 —— 要看它吃什么吐什么,去活动库看 */ onClick={() => nav.jump('library', 'shelf', { k: 'act', id: refAct.activityId })}
<button >
className="g-btn" 去活动库看契约 <Ico name="arrow-right" s={11} />
onClick={() => nav.jump('library', 'shelf', { k: 'act', id: refAct.activityId })} </button>
> ) : (
<Ico name="arrow-right" s={13} /> <small className="warn">图可以照画照存,激活前链上即可</small>
去活动库看 {refAct.name} 的契约 )}
</button> </span>
)} </div>
</>
)} )}
{outEdges.map(({ edge, index }) => ( {outEdges.length > 0 && (
/* <>
* 出边条件:**分支归发起它的这个节点管**(Mastra 的 `.branch([[条件, 下一步], …])` {/*
* 就挂在上一步上)—— 边本身不是可编辑对象,它只是画布上的一条线。 * 出边 = 路由小表:**分支归发起它的这个节点管**(Mastra 的
*/ * `.branch([[条件, 下一步], …])` 挂在上一步上)。三条重复的长提示收成一句;
<Field * 条件留空即默认兜底(placeholder 说),边本身只是画布上的一条线。
key={edge.code ?? index} */}
k={`出边 ${edge.code ?? index + 1} → ${edge.to}`} <div className="g-tbband">
hint={ 出边 · {outEdges.length === 1 ? '直连' : '命中即走'}
outEdges.length === 1 <span className="x">
? '只有一条出边,无条件直连' {node.kind === 'activity' && node.flavor === 'approval'
: node.kind === 'activity' && node.flavor === 'approval' ? '条件读审批裁决 verdict'
? // 审批点的判据是**系统注入**的裁决,不是它自己的输出字段 : '条件读本节点的输出字段'}
'读审批裁决:verdict = "pass" / "reject";留空即默认兜底' </span>
: '读本节点的输出字段,如 needAlternative = true;留空即默认兜底' </div>
} {outEdges.map(({ edge, index }) => {
> const target = nodes.find((n) => n.id === edge.to);
<input return (
className="g-input mono" <div key={edge.code ?? index} className="g-tbedge">
value={edge.condition ?? ''} <span className="rt">
placeholder={outEdges.length === 1 ? '无条件' : '留空 = 默认兜底'} <b>{edge.code ?? `边${index + 1}`}</b>
onChange={(e) => patchEdge(index, (x) => ({ ...x, condition: e.target.value }))} <Ico name="arrow-right" s={11} />
/> <span className="to">{edge.to}</span>
</Field> {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>
)} )}
</div> </div>
@@ -419,59 +419,65 @@ function LetterForm({
}): ReactNode { }): ReactNode {
const set = <K extends keyof MailDraft>(k: K, v: MailDraft[K]): void => onChange({ ...draft, [k]: v }); const set = <K extends keyof MailDraft>(k: K, v: MailDraft[K]): void => onChange({ ...draft, [k]: v });
return ( return (
<div className="tb-ml"> /*
<label className="tb-ml-r"> * 一张**纸信**压在暗台面上 —— 整台仪器里唯一的浅色面。
<em>发件人</em> * 「机器说 mono、信是外界」推到视觉结论:外界伸进仪器的那件东西,就长得像张纸。
<input className="tb-ml-i" value={draft.from} onChange={(e) => set('from', e.target.value)} /> * 解剖照邮件写:信头三行(发件人 / 收件箱 / 主题)→ 正文 → 附件签条。
</label> * 收件箱是**选**的不是打的:投给哪个信箱决定用哪套抽键规则,地址错一位,
<label className="tb-ml-r"> * 左边预检整屏熄灯还不知道为什么。
<em>收件箱</em> */
{/* 投给哪个信箱决定用哪套抽键规则 —— 选,不手打:地址错一位,预检整屏熄灯还不知道为什么 */} <div className="tb-paper">
<select className="tb-ml-i sel" value={channel?.address ?? ''} onChange={(e) => set('to', e.target.value)}> <div className="tb-pp-h">
{channels.map((ch) => ( <label className="tb-pp-r">
<option key={ch.id} value={ch.address}> <em>发件人</em>
{ch.address} —— {ch.name} <input className="tb-pp-i" value={draft.from} onChange={(e) => set('from', e.target.value)} />
</option> </label>
))} <label className="tb-pp-r">
</select> <em>收件箱</em>
</label> <select className="tb-pp-i sel" value={channel?.address ?? ''} onChange={(e) => set('to', e.target.value)}>
<label className="tb-ml-r"> {channels.map((ch) => (
<em>主题</em> <option key={ch.id} value={ch.address}>
<input className="tb-ml-i" value={draft.subject} onChange={(e) => set('subject', e.target.value)} /> {ch.address} —— {ch.name}
</label> </option>
<label className="tb-ml-r tall"> ))}
<em>正文</em> </select>
<textarea className="tb-ml-i" rows={4} value={draft.body} onChange={(e) => set('body', e.target.value)} /> </label>
</label> <label className="tb-pp-r">
<div className="tb-ml-r tall"> <em>主题</em>
<em>附件</em> <input className="tb-pp-i strong" value={draft.subject} onChange={(e) => set('subject', e.target.value)} />
<div className="tb-ml-at"> </label>
{draft.attachments.map((a, i) => ( </div>
<span key={a.name} className="g-att"> <textarea
<Ico name="paperclip" s={11} /> className="tb-pp-body"
{a.name} rows={5}
<i>{(a.size / 1024).toFixed(0)} KB</i> value={draft.body}
<button placeholder="正文…"
aria-label={`移除 ${a.name}`} aria-label="正文"
onClick={() => set('attachments', draft.attachments.filter((_, j) => j !== i))} onChange={(e) => set('body', e.target.value)}
> />
<Ico name="x" s={10} /> <div className="tb-pp-att">
</button> {draft.attachments.map((a, i) => (
</span> <span key={a.name} className="tb-att">
))} <Ico name="paperclip" s={11} />
<button {a.name}
className="g-att-add" <i>{(a.size / 1024).toFixed(0)} KB</i>
onClick={() => <button aria-label={`移除 ${a.name}`} onClick={() => set('attachments', draft.attachments.filter((_, j) => j !== i))}>
set('attachments', [ <Ico name="x" s={10} />
...draft.attachments, </button>
{ name: `BOM-${draft.attachments.length + 1}.xlsx`, contentType: XLSX, size: 20_480 }, </span>
]) ))}
} <button
> className="tb-att-add"
<Ico name="plus" s={11} /> onClick={() =>
加一个 BOM set('attachments', [
</button> ...draft.attachments,
</div> { name: `BOM-${draft.attachments.length + 1}.xlsx`, contentType: XLSX, size: 20_480 },
])
}
>
<Ico name="plus" s={11} />
加一个 BOM
</button>
</div> </div>
</div> </div>
); );