|
7 | 7 | * 轨迹统计 + 保鲜 diff("笔记有出入?")+ 修订建议(纯本地聚合)。 |
8 | 8 | * 证据=可选相对路径输入(notes-images/ 前缀白名单——命令层校验; |
9 | 9 | * 图片三入口落盘的完整上传流登记后置)。 |
| 10 | + * @ai-context: 批 4 T8 迁移:自建遮罩/居中几何(原 640 px 面板 → `Modal` 的 `l` 档)与自绘头部 |
| 11 | + * (标题 + 关闭钮)交给 `Modal`(barrel 导入,B5)——遮罩、ESC、焦点陷阱、层级、 |
| 12 | + * body 滚动锁都是它的独占职责(ADR-033 §7)。开合态仍由父层持有 |
| 13 | + * (`ActionCenterPanel` 的条件挂载)⇒ `open` 恒为 `true`,160ms 退场相位不触发。 |
10 | 14 | */ |
11 | 15 | import { useEffect, useState } from "react"; |
12 | | -import { zIndex } from "../ui/zIndex"; |
| 16 | +import { Modal } from "../ui/primitives"; |
13 | 17 | import { invoke } from "@tauri-apps/api/core"; |
14 | 18 |
|
15 | 19 | /** 响应结构(SopTemplate/SopRunStep/SopRun/SopRunDetail 均 serde camelCase——字段须 camel 读取) */ |
@@ -56,25 +60,6 @@ interface Props { |
56 | 60 | onChanged?: () => void; |
57 | 61 | } |
58 | 62 |
|
59 | | -const overlayStyle: React.CSSProperties = { |
60 | | - position: "fixed", |
61 | | - inset: 0, |
62 | | - background: "rgba(0,0,0,.45)", |
63 | | - zIndex: zIndex("modal"), |
64 | | - display: "flex", |
65 | | - alignItems: "center", |
66 | | - justifyContent: "center", |
67 | | -}; |
68 | | -const cardStyle: React.CSSProperties = { |
69 | | - background: "#fff", |
70 | | - borderRadius: 12, |
71 | | - width: 640, |
72 | | - maxWidth: "92vw", |
73 | | - maxHeight: "84vh", |
74 | | - overflow: "auto", |
75 | | - padding: 16, |
76 | | - fontSize: 13, |
77 | | -}; |
78 | 63 | const btn: React.CSSProperties = { padding: "4px 10px", cursor: "pointer", fontSize: 12, borderRadius: 6 }; |
79 | 64 | const okBtn: React.CSSProperties = { ...btn, background: "#0d9488", color: "#fff", border: "none" }; |
80 | 65 | const ghostBtn: React.CSSProperties = { ...btn, background: "#fff", border: "1px solid #e5e7eb", color: "#374151" }; |
@@ -181,111 +166,107 @@ export default function SopRunOverlay({ template, onClose, onChanged }: Props) { |
181 | 166 | const modeLabel = confirmMode ? "总览核对(DO-CONFIRM)" : "逐步引导(READ-DO)"; |
182 | 167 |
|
183 | 168 | return ( |
184 | | - <div style={overlayStyle} onClick={onClose}> |
185 | | - <div style={cardStyle} onClick={(e) => e.stopPropagation()}> |
186 | | - <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 10 }}> |
187 | | - <h3 style={{ margin: 0, fontSize: 15 }}>🧭 {template.name}</h3> |
188 | | - <span style={{ fontSize: 11, color: "#6b7280" }}>@{template.noteTitle}</span> |
189 | | - <span style={{ fontSize: 11, color: "#0f766e", background: "#f0fdfa", border: "1px solid #99f6e4", borderRadius: 10, padding: "1px 8px" }}> |
190 | | - {modeLabel} |
191 | | - </span> |
192 | | - <button style={{ ...ghostBtn, marginLeft: "auto" }} onClick={onClose}> |
193 | | - 关闭 |
194 | | - </button> |
| 169 | + <Modal open onClose={onClose} title={template.name} size="l" testId="sop-run-overlay"> |
| 170 | + {/* 自绘头部已删:标题(去纯装饰 emoji 🧭)与关闭钮归 `Modal` 的 head;备注名与模式徽标 |
| 171 | + 逐字保留为正文首行(字号/色值/边框原样) */} |
| 172 | + <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 10 }}> |
| 173 | + <span style={{ fontSize: 11, color: "#6b7280" }}>@{template.noteTitle}</span> |
| 174 | + <span style={{ fontSize: 11, color: "#0f766e", background: "#f0fdfa", border: "1px solid #99f6e4", borderRadius: 10, padding: "1px 8px" }}> |
| 175 | + {modeLabel} |
| 176 | + </span> |
| 177 | + </div> |
| 178 | + |
| 179 | + {err && <div style={{ fontSize: 12, color: "#dc2626", background: "#fef2f2", border: "1px solid #fca5a5", borderRadius: 6, padding: "6px 10px", marginBottom: 8 }}>{err}</div>} |
| 180 | + {detail?.freshnessChanged && stage === "run" && ( |
| 181 | + <div style={{ fontSize: 12, color: "#b45309", background: "#fffbeb", border: "1px solid #f59e0b", borderRadius: 6, padding: "6px 10px", marginBottom: 8 }}> |
| 182 | + ⚠ 笔记正文与启动快照已有出入——结算时请对比修订模板(执行即保鲜) |
195 | 183 | </div> |
| 184 | + )} |
196 | 185 |
|
197 | | - {err && <div style={{ fontSize: 12, color: "#dc2626", background: "#fef2f2", border: "1px solid #fca5a5", borderRadius: 6, padding: "6px 10px", marginBottom: 8 }}>{err}</div>} |
198 | | - {detail?.freshnessChanged && stage === "run" && ( |
199 | | - <div style={{ fontSize: 12, color: "#b45309", background: "#fffbeb", border: "1px solid #f59e0b", borderRadius: 6, padding: "6px 10px", marginBottom: 8 }}> |
200 | | - ⚠ 笔记正文与启动快照已有出入——结算时请对比修订模板(执行即保鲜) |
| 186 | + {stage === "run" && detail && ( |
| 187 | + <> |
| 188 | + <div style={{ display: "flex", gap: 6, alignItems: "center", marginBottom: 8 }}> |
| 189 | + <button style={ghostBtn} onClick={() => setConfirmMode((m) => !m)} title="两种执行模式可随时切换"> |
| 190 | + {confirmMode ? "切到逐步引导" : "切到总览核对"} |
| 191 | + </button> |
| 192 | + <span style={{ fontSize: 11, color: "#9ca3af", marginLeft: "auto" }}> |
| 193 | + 进行中 {detail.stats.done + detail.stats.failed + detail.stats.skipped}/{detail.stats.total} · 待办 {pending} |
| 194 | + </span> |
201 | 195 | </div> |
202 | | - )} |
203 | | - |
204 | | - {stage === "run" && detail && ( |
205 | | - <> |
206 | | - <div style={{ display: "flex", gap: 6, alignItems: "center", marginBottom: 8 }}> |
207 | | - <button style={ghostBtn} onClick={() => setConfirmMode((m) => !m)} title="两种执行模式可随时切换"> |
208 | | - {confirmMode ? "切到逐步引导" : "切到总览核对"} |
209 | | - </button> |
210 | | - <span style={{ fontSize: 11, color: "#9ca3af", marginLeft: "auto" }}> |
211 | | - 进行中 {detail.stats.done + detail.stats.failed + detail.stats.skipped}/{detail.stats.total} · 待办 {pending} |
212 | | - </span> |
213 | | - </div> |
214 | | - <div style={{ display: "flex", flexDirection: "column", gap: 8 }}> |
215 | | - {detail.steps.map((s) => { |
216 | | - const done = s.status === "done"; |
217 | | - const failed = s.status === "failed"; |
218 | | - const skipped = s.status === "skipped"; |
219 | | - return ( |
220 | | - <div key={s.id} style={{ border: "1px solid #e5e7eb", borderRadius: 8, padding: "8px 10px", background: done ? "#ecfdf5" : failed ? "#fef2f2" : "#fff" }}> |
221 | | - <div style={{ display: "flex", gap: 6 }}> |
222 | | - <span style={{ fontSize: 11, color: "#9ca3af", flexShrink: 0 }}>步骤 {s.stepNo}</span> |
223 | | - <span style={{ fontSize: 13, color: "#111827", flex: 1 }}>{s.textSnapshot}</span> |
224 | | - <span style={{ fontSize: 11, color: "#9ca3af", flexShrink: 0 }}> |
225 | | - {done ? "✓" : failed ? "✗" : skipped ? "⏭" : s.status} |
226 | | - </span> |
227 | | - </div> |
228 | | - {!done && !failed && !skipped && ( |
229 | | - <> |
230 | | - <div style={{ display: "flex", gap: 4, marginTop: 6, flexWrap: "wrap" }}> |
231 | | - <button style={okBtn} disabled={busy} onClick={() => void stepAction(s.stepNo, "done")}>✓ 完成</button> |
232 | | - <button style={ghostBtn} disabled={busy} onClick={() => void stepAction(s.stepNo, "skipped")}>⏭ 跳过</button> |
233 | | - <button style={{ ...ghostBtn, color: "#dc2626" }} disabled={busy} onClick={() => { setFailOpen(failOpen === s.stepNo ? null : s.stepNo); setFailNote(""); }}>✗ 失败</button> |
234 | | - </div> |
235 | | - {failOpen === s.stepNo && ( |
236 | | - <div style={{ display: "flex", gap: 4, marginTop: 6 }}> |
237 | | - <input autoFocus placeholder="失败原因(建议记录可观测信号差在哪)" value={failNote} onChange={(e) => setFailNote(e.target.value)} style={{ flex: 1, fontSize: 12, border: "1px solid #e5e7eb", borderRadius: 4, padding: "2px 6px" }} /> |
238 | | - <button style={okBtn} disabled={busy} onClick={() => void stepAction(s.stepNo, "failed")}>记录失败</button> |
239 | | - </div> |
240 | | - )} |
241 | | - {s.status === "todo" && ( |
242 | | - <div style={{ display: "flex", gap: 4, marginTop: 4, alignItems: "center" }}> |
243 | | - <span style={{ fontSize: 11, color: "#9ca3af" }}>证据路径(notes-images/…,可选):</span> |
244 | | - <input |
245 | | - value={evidenceByStep[s.stepNo] ?? ""} |
246 | | - onChange={(e) => setEvidenceByStep((m) => ({ ...m, [s.stepNo]: e.target.value }))} |
247 | | - placeholder="notes-images/xxx.png" |
248 | | - style={{ flex: 1, fontSize: 12, border: "1px solid #e5e7eb", borderRadius: 4, padding: "2px 6px" }} |
249 | | - /> |
250 | | - </div> |
251 | | - )} |
252 | | - </> |
253 | | - )} |
| 196 | + <div style={{ display: "flex", flexDirection: "column", gap: 8 }}> |
| 197 | + {detail.steps.map((s) => { |
| 198 | + const done = s.status === "done"; |
| 199 | + const failed = s.status === "failed"; |
| 200 | + const skipped = s.status === "skipped"; |
| 201 | + return ( |
| 202 | + <div key={s.id} style={{ border: "1px solid #e5e7eb", borderRadius: 8, padding: "8px 10px", background: done ? "#ecfdf5" : failed ? "#fef2f2" : "#fff" }}> |
| 203 | + <div style={{ display: "flex", gap: 6 }}> |
| 204 | + <span style={{ fontSize: 11, color: "#9ca3af", flexShrink: 0 }}>步骤 {s.stepNo}</span> |
| 205 | + <span style={{ fontSize: 13, color: "#111827", flex: 1 }}>{s.textSnapshot}</span> |
| 206 | + <span style={{ fontSize: 11, color: "#9ca3af", flexShrink: 0 }}> |
| 207 | + {done ? "✓" : failed ? "✗" : skipped ? "⏭" : s.status} |
| 208 | + </span> |
254 | 209 | </div> |
255 | | - ); |
256 | | - })} |
257 | | - </div> |
258 | | - <div style={{ display: "flex", gap: 6, marginTop: 10 }}> |
259 | | - <button style={okBtn} disabled={busy || pending > 0} onClick={() => void finish("done")}> |
260 | | - ✅ 结算(完成) |
261 | | - </button> |
262 | | - <button style={{ ...ghostBtn, color: "#b45309" }} disabled={busy} onClick={() => void finish("aborted")}> |
263 | | - ⏹ 中止并归档 |
264 | | - </button> |
265 | | - {pending > 0 && <span style={{ fontSize: 11, color: "#b45309" }}>还有 {pending} 步未处理(可先处理或中止)</span>} |
266 | | - </div> |
267 | | - </> |
268 | | - )} |
| 210 | + {!done && !failed && !skipped && ( |
| 211 | + <> |
| 212 | + <div style={{ display: "flex", gap: 4, marginTop: 6, flexWrap: "wrap" }}> |
| 213 | + <button style={okBtn} disabled={busy} onClick={() => void stepAction(s.stepNo, "done")}>✓ 完成</button> |
| 214 | + <button style={ghostBtn} disabled={busy} onClick={() => void stepAction(s.stepNo, "skipped")}>⏭ 跳过</button> |
| 215 | + <button style={{ ...ghostBtn, color: "#dc2626" }} disabled={busy} onClick={() => { setFailOpen(failOpen === s.stepNo ? null : s.stepNo); setFailNote(""); }}>✗ 失败</button> |
| 216 | + </div> |
| 217 | + {failOpen === s.stepNo && ( |
| 218 | + <div style={{ display: "flex", gap: 4, marginTop: 6 }}> |
| 219 | + <input autoFocus placeholder="失败原因(建议记录可观测信号差在哪)" value={failNote} onChange={(e) => setFailNote(e.target.value)} style={{ flex: 1, fontSize: 12, border: "1px solid #e5e7eb", borderRadius: 4, padding: "2px 6px" }} /> |
| 220 | + <button style={okBtn} disabled={busy} onClick={() => void stepAction(s.stepNo, "failed")}>记录失败</button> |
| 221 | + </div> |
| 222 | + )} |
| 223 | + {s.status === "todo" && ( |
| 224 | + <div style={{ display: "flex", gap: 4, marginTop: 4, alignItems: "center" }}> |
| 225 | + <span style={{ fontSize: 11, color: "#9ca3af" }}>证据路径(notes-images/…,可选):</span> |
| 226 | + <input |
| 227 | + value={evidenceByStep[s.stepNo] ?? ""} |
| 228 | + onChange={(e) => setEvidenceByStep((m) => ({ ...m, [s.stepNo]: e.target.value }))} |
| 229 | + placeholder="notes-images/xxx.png" |
| 230 | + style={{ flex: 1, fontSize: 12, border: "1px solid #e5e7eb", borderRadius: 4, padding: "2px 6px" }} |
| 231 | + /> |
| 232 | + </div> |
| 233 | + )} |
| 234 | + </> |
| 235 | + )} |
| 236 | + </div> |
| 237 | + ); |
| 238 | + })} |
| 239 | + </div> |
| 240 | + <div style={{ display: "flex", gap: 6, marginTop: 10 }}> |
| 241 | + <button style={okBtn} disabled={busy || pending > 0} onClick={() => void finish("done")}> |
| 242 | + ✅ 结算(完成) |
| 243 | + </button> |
| 244 | + <button style={{ ...ghostBtn, color: "#b45309" }} disabled={busy} onClick={() => void finish("aborted")}> |
| 245 | + ⏹ 中止并归档 |
| 246 | + </button> |
| 247 | + {pending > 0 && <span style={{ fontSize: 11, color: "#b45309" }}>还有 {pending} 步未处理(可先处理或中止)</span>} |
| 248 | + </div> |
| 249 | + </> |
| 250 | + )} |
269 | 251 |
|
270 | | - {stage === "settle" && detail && ( |
271 | | - <div> |
272 | | - <div style={{ fontSize: 14, fontWeight: 600, marginBottom: 8 }}> |
273 | | - {detail.run.status === "done" ? "✅ run 已完成并归档入史" : "⏹ run 已中止归档"} |
274 | | - </div> |
275 | | - <div style={{ fontSize: 12, color: "#374151", marginBottom: 6 }}> |
276 | | - 轨迹:✓完成 {detail.stats.done} · ⏭跳过 {detail.stats.skipped} · ✗失败 {detail.stats.failed}(共 {detail.stats.total} 步) |
277 | | - </div> |
278 | | - {detail.freshnessChanged && ( |
279 | | - <div style={{ fontSize: 12, color: "#b45309", marginBottom: 6 }}> |
280 | | - 📝 笔记正文有出入——可对比快照修订模板段落(编辑即模板,无双写) |
281 | | - </div> |
282 | | - )} |
283 | | - <div style={{ display: "flex", gap: 6, marginTop: 8 }}> |
284 | | - <button style={okBtn} onClick={onClose}>返回</button> |
| 252 | + {stage === "settle" && detail && ( |
| 253 | + <div> |
| 254 | + <div style={{ fontSize: 14, fontWeight: 600, marginBottom: 8 }}> |
| 255 | + {detail.run.status === "done" ? "✅ run 已完成并归档入史" : "⏹ run 已中止归档"} |
| 256 | + </div> |
| 257 | + <div style={{ fontSize: 12, color: "#374151", marginBottom: 6 }}> |
| 258 | + 轨迹:✓完成 {detail.stats.done} · ⏭跳过 {detail.stats.skipped} · ✗失败 {detail.stats.failed}(共 {detail.stats.total} 步) |
| 259 | + </div> |
| 260 | + {detail.freshnessChanged && ( |
| 261 | + <div style={{ fontSize: 12, color: "#b45309", marginBottom: 6 }}> |
| 262 | + 📝 笔记正文有出入——可对比快照修订模板段落(编辑即模板,无双写) |
285 | 263 | </div> |
| 264 | + )} |
| 265 | + <div style={{ display: "flex", gap: 6, marginTop: 8 }}> |
| 266 | + <button style={okBtn} onClick={onClose}>返回</button> |
286 | 267 | </div> |
287 | | - )} |
288 | | - </div> |
289 | | - </div> |
| 268 | + </div> |
| 269 | + )} |
| 270 | + </Modal> |
290 | 271 | ); |
291 | 272 | } |
0 commit comments