Skip to content

Commit 6b8a112

Browse files
committed
feat(tasks): REQ-296 SOP 执行器与行动中心 SOP 库页签
1 parent fabadde commit 6b8a112

2 files changed

Lines changed: 401 additions & 3 deletions

File tree

‎app/src/components/ActionCenterOverlay.tsx‎

Lines changed: 136 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,17 @@
1111
*/
1212
import { useCallback, useEffect, useRef, useState } from "react";
1313
import { invoke } from "@tauri-apps/api/core";
14+
import SopRunOverlay from "./SopRunOverlay";
15+
16+
interface SopTemplateView {
17+
id: number;
18+
note_id: number;
19+
name: string;
20+
start_line: number;
21+
end_line: number;
22+
mode: string;
23+
note_title: string;
24+
}
1425

1526
interface ActionRow {
1627
id: number;
@@ -64,6 +75,7 @@ const cardStyle: React.CSSProperties = {
6475
const btn: React.CSSProperties = { padding: "4px 10px", cursor: "pointer", fontSize: 12, borderRadius: 6, border: "1px solid #e5e7eb", background: "#fff", color: "#374151" };
6576
const okBtn: React.CSSProperties = { ...btn, background: "#0d9488", color: "#fff", border: "none" };
6677
const dangerBtn: React.CSSProperties = { ...btn, color: "#dc2626" };
78+
const ghostBtn: React.CSSProperties = { ...btn };
6779

6880
const TYPE_LABEL: Record<string, string> = {
6981
done: "✓ 完成",
@@ -81,7 +93,16 @@ function fmtDate(ts: number): string {
8193
}
8294

8395
export default function ActionCenterOverlay({ onClose, onChanged }: Props) {
84-
const [tab, setTab] = useState<"queue" | "history">("queue");
96+
const [tab, setTab] = useState<"queue" | "history" | "sop">("queue");
97+
// SOP 库(模板/新建/执行——REQ-296)
98+
const [templates, setTemplates] = useState<SopTemplateView[]>([]);
99+
const [notes, setNotes] = useState<{ id: number; title: string }[]>([]);
100+
const [activeTemplate, setActiveTemplate] = useState<SopTemplateView | null>(null);
101+
const [sopNoteId, setSopNoteId] = useState<number | null>(null);
102+
const [sopName, setSopName] = useState("");
103+
const [sopStart, setSopStart] = useState("0");
104+
const [sopEnd, setSopEnd] = useState("0");
105+
const [suggestions, setSuggestions] = useState<Record<number, string[]>>({});
85106
// 裁决队列(四分区同拉全量行,前端按 tab 分区展示——逾期标红/计划/搁置/提炼)
86107
const [rows, setRows] = useState<ActionRow[]>([]);
87108
const [history, setHistory] = useState<HistoryRow[]>([]);
@@ -134,8 +155,62 @@ export default function ActionCenterOverlay({ onClose, onChanged }: Props) {
134155
setUnrefined(u);
135156
})();
136157
void reload();
158+
void loadSop();
159+
// eslint-disable-next-line react-hooks/exhaustive-deps
137160
}, [fetchTab, reload]);
138161

162+
const loadSop = useCallback(async () => {
163+
try {
164+
const [t, n] = await Promise.all([
165+
invoke<SopTemplateView[]>("sop_template_list", { noteId: null }),
166+
invoke<{ id: number; title: string }[]>("list_notes"),
167+
]);
168+
setTemplates(t);
169+
setNotes(n);
170+
if (sopNoteId == null && n.length > 0) setSopNoteId(n[0].id);
171+
} catch (e) {
172+
setErr(`SOP 库加载失败: ${e}`);
173+
}
174+
}, [sopNoteId]);
175+
176+
const createTemplate = async () => {
177+
const start = Number.parseInt(sopStart, 10);
178+
const end = Number.parseInt(sopEnd, 10);
179+
if (sopNoteId == null) { setErr("请先选择笔记"); return; }
180+
if (!sopName.trim()) { setErr("请输入模板名"); return; }
181+
if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end < start) {
182+
setErr("行范围非法(0 起整数,end ≥ start)");
183+
return;
184+
}
185+
try {
186+
await invoke("sop_template_create", { noteId: sopNoteId, name: sopName.trim(), startLine: start, endLine: end, mode: null });
187+
setSopName("");
188+
setMsg(`✓ 模板「${sopName.trim()}」已创建(段落行引用,编辑正文即编辑模板)`);
189+
await loadSop();
190+
} catch (e) {
191+
setErr(String(e));
192+
}
193+
};
194+
195+
const fetchSuggestions = async (templateId: number) => {
196+
try {
197+
const list = await invoke<string[]>("sop_revision_suggestions", { templateId });
198+
setSuggestions((m) => ({ ...m, [templateId]: list }));
199+
} catch (e) {
200+
setErr(String(e));
201+
}
202+
};
203+
204+
const deleteTemplate = async (t: SopTemplateView) => {
205+
try {
206+
await invoke("sop_template_delete", { templateId: t.id });
207+
setMsg(`已删除模板「${t.name}」(正文未动)`);
208+
await loadSop();
209+
} catch (e) {
210+
setErr(String(e));
211+
}
212+
};
213+
139214
const afterMutate = async (info?: string) => {
140215
if (info) setMsg(info);
141216
onChanged?.();
@@ -253,6 +328,7 @@ export default function ActionCenterOverlay({ onClose, onChanged }: Props) {
253328
[
254329
["queue", `裁决队列(${overdue.length + planned.length + rows.length + unrefined.length})`],
255330
["history", `完成史(${history.length})`],
331+
["sop", `SOP 库(${templates.length})`],
256332
] as const
257333
).map(([k, label]) => (
258334
<button
@@ -301,7 +377,7 @@ export default function ActionCenterOverlay({ onClose, onChanged }: Props) {
301377
))}
302378
</div>
303379
</>
304-
) : (
380+
) : tab === "history" ? (
305381
<div style={{ display: "flex", flexDirection: "column", gap: 5 }}>
306382
{history.length === 0 ? empty("暂无完成记录——完成即证据,周回顾原料在此沉淀") : history.map((h) => (
307383
<div key={h.id} style={{ display: "flex", gap: 8, fontSize: 12, borderBottom: "1px solid #f3f4f6", padding: "4px 2px" }}>
@@ -314,10 +390,67 @@ export default function ActionCenterOverlay({ onClose, onChanged }: Props) {
314390
</div>
315391
))}
316392
</div>
393+
) : (
394+
<>
395+
{/* SOP 库:模板行范围引用 + 执行(REQ-296) */}
396+
<div style={{ fontSize: 12, color: "#374151", marginBottom: 8 }}>
397+
<b>SOP 模板({templates.length})</b>{" "}
398+
<span style={{ color: "#9ca3af", fontSize: 11 }}>模板=笔记段落行范围引用(编辑正文即编辑模板,无双写);执行=步骤快照跑 run</span>
399+
</div>
400+
<div style={{ border: "1px solid #e5e7eb", borderRadius: 8, padding: 8, marginBottom: 10 }}>
401+
<div style={{ display: "flex", gap: 4, flexWrap: "wrap", alignItems: "center" }}>
402+
<span style={{ fontSize: 11, color: "#6b7280" }}>新建:</span>
403+
<select value={sopNoteId ?? undefined} onChange={(e) => setSopNoteId(Number(e.target.value))} style={{ fontSize: 12, maxWidth: 180 }}>
404+
{notes.map((n) => <option key={n.id} value={n.id}>{n.title.slice(0, 18)}</option>)}
405+
</select>
406+
<input value={sopName} onChange={(e) => setSopName(e.target.value)} placeholder="模板名" style={{ fontSize: 12, width: 110, border: "1px solid #e5e7eb", borderRadius: 4, padding: "2px 6px" }} />
407+
<span style={{ fontSize: 11, color: "#9ca3af" }}>行</span>
408+
<input value={sopStart} onChange={(e) => setSopStart(e.target.value)} style={{ fontSize: 12, width: 44, border: "1px solid #e5e7eb", borderRadius: 4, padding: "2px 6px" }} />
409+
<span style={{ fontSize: 11, color: "#9ca3af" }}>–</span>
410+
<input value={sopEnd} onChange={(e) => setSopEnd(e.target.value)} style={{ fontSize: 12, width: 44, border: "1px solid #e5e7eb", borderRadius: 4, padding: "2px 6px" }} />
411+
<button style={okBtn} onClick={() => void createTemplate()}>创建</button>
412+
</div>
413+
<div style={{ fontSize: 11, color: "#9ca3af", marginTop: 4 }}>
414+
行号 0 起(标题=0);空行自动跳过;超 50 步拒绝。编辑器内选中段落生成入口在笔记工具栏接线(同款命令)。
415+
</div>
416+
</div>
417+
{templates.length === 0 ? (
418+
<p style={{ fontSize: 12, color: "#9ca3af" }}>暂无 SOP 模板——选中笔记步骤段落(行范围)即可创建</p>
419+
) : (
420+
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
421+
{templates.map((t) => (
422+
<div key={t.id} style={{ border: "1px solid #e5e7eb", borderRadius: 8, padding: "6px 10px" }}>
423+
<div style={{ display: "flex", gap: 6, alignItems: "center" }}>
424+
<span style={{ fontSize: 12.5, fontWeight: 600, color: "#111827" }}>{t.name}</span>
425+
<span style={{ fontSize: 11, color: "#9ca3af" }}>@{t.note_title} · 行 {t.start_line}–{t.end_line} · {t.mode === "confirm" ? "总览核对" : "逐步引导"}</span>
426+
<span style={{ marginLeft: "auto", display: "flex", gap: 4 }}>
427+
<button style={okBtn} onClick={() => setActiveTemplate(t)}>▶ 执行</button>
428+
<button style={ghostBtn} onClick={() => void fetchSuggestions(t.id)}>💡 修订建议</button>
429+
<button style={{ ...ghostBtn, color: "#dc2626" }} onClick={() => void deleteTemplate(t)}>删除</button>
430+
</span>
431+
</div>
432+
{(suggestions[t.id]?.length ?? 0) > 0 && (
433+
<div style={{ fontSize: 11, color: "#b45309", marginTop: 4 }}>
434+
{suggestions[t.id].map((s, i) => <div key={i}>💡 {s}</div>)}
435+
</div>
436+
)}
437+
</div>
438+
))}
439+
</div>
440+
)}
441+
</>
317442
)}
318443
<p style={{ fontSize: 11, color: "#9ca3af", marginTop: 10 }}>
319-
勾选任务行即被自动收录;产物 ☑️ 行可在上方「待提炼」区一键转标准任务行;🎴 转卡为规划中出口(G7 预留)。
444+
勾选任务行即被自动收录;产物 ☑️ 行可在上方「待提炼」区一键转标准任务行;🎴 转卡为规划中出口(G7 预留)。SOP run 完成自动入完成史。
320445
</p>
446+
{/* v0.20.3(REQ-296):SOP 执行器(嵌套 Overlay——覆盖本面板) */}
447+
{activeTemplate && (
448+
<SopRunOverlay
449+
template={activeTemplate}
450+
onClose={() => setActiveTemplate(null)}
451+
onChanged={() => { void loadSop(); onChanged?.(); }}
452+
/>
453+
)}
321454
</div>
322455
</div>
323456
);

0 commit comments

Comments
 (0)