Skip to content

Commit 7dd8af2

Browse files
committed
feat(tasks): REQ-294 周回顾批裁决 UI + REQ-301 封存显隐
1 parent c513442 commit 7dd8af2

4 files changed

Lines changed: 146 additions & 5 deletions

File tree

‎app/src/components/ActionCenterOverlay.tsx‎

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,50 @@ export default function ActionCenterOverlay({ onClose, onChanged }: Props) {
107107
const [sopStart, setSopStart] = useState("0");
108108
const [sopEnd, setSopEnd] = useState("0");
109109
const [suggestions, setSuggestions] = useState<Record<number, string[]>>({});
110+
// v0.20.3(REQ-294):批量周回顾(选中执行/放弃——batch_weekly_resolve 单事务)
111+
const [batchMode, setBatchMode] = useState(false);
112+
const [checked, setChecked] = useState<Set<number>>(new Set());
113+
const [batchReason, setBatchReason] = useState("");
114+
115+
const toggleChecked = (id: number) => {
116+
setChecked((prev) => {
117+
const next = new Set(prev);
118+
if (next.has(id)) next.delete(id); else next.add(id);
119+
return next;
120+
});
121+
};
122+
123+
const runBatch = async (action: "done" | "abandon") => {
124+
const candidates = [...overdue, ...planned, ...rows];
125+
const decisions = candidates
126+
.filter((r) => checked.has(r.id) && !r.unrefined)
127+
.map((r) => ({ rowId: r.id, action, reason: action === "abandon" ? batchReason.trim() || null : null }));
128+
if (decisions.length === 0) {
129+
setErr("请先勾选要批决议的任务行");
130+
return;
131+
}
132+
try {
133+
const view = await invoke<{ done: number; abandoned: number; failed: string[] }>(
134+
"batch_weekly_resolve",
135+
{ decisions },
136+
);
137+
setMsg(`⚖ 周回顾批提交完成:执行 ${view.done} · 放弃 ${view.abandoned}${view.failed.length > 0 ? ` · 失败 ${view.failed.length} 条(${view.failed[0]})` : ""}`);
138+
setChecked(new Set());
139+
setBatchReason("");
140+
setBatchMode(false);
141+
onChanged?.();
142+
const [o, p, s, u, h] = await Promise.all([
143+
fetchTab("overdue"),
144+
fetchTab("planned"),
145+
fetchTab("someday"),
146+
fetchTab("unrefined"),
147+
invoke<HistoryRow[]>("completion_history_list", { eventType: null, limit: 150 }),
148+
]);
149+
setOverdue(o); setPlanned(p); setRows(s); setUnrefined(u); setHistory(h);
150+
} catch (e) {
151+
setErr(String(e));
152+
}
153+
};
110154
// 裁决队列(四分区同拉全量行,前端按 tab 分区展示——逾期标红/计划/搁置/提炼)
111155
const [rows, setRows] = useState<ActionRow[]>([]);
112156
const [history, setHistory] = useState<HistoryRow[]>([]);
@@ -366,6 +410,39 @@ export default function ActionCenterOverlay({ onClose, onChanged }: Props) {
366410

367411
{tab === "queue" ? (
368412
<>
413+
{/* v0.20.3(REQ-294):周回顾批裁决(不留死尸——裁决机制批量面) */}
414+
<div style={{ display: "flex", gap: 6, alignItems: "center", marginBottom: 8, flexWrap: "wrap" }}>
415+
<button style={ghostBtn} onClick={() => setBatchMode((m) => !m)} data-testid="weekly-batch-toggle">
416+
{batchMode ? "退出批量(⚖ 周回顾)" : "⚖ 批量裁决(周回顾)"}
417+
</button>
418+
{batchMode && (
419+
<>
420+
<input value={batchReason} onChange={(e) => setBatchReason(e.target.value)} placeholder="放弃原因(批量放弃共用,可空)" style={{ fontSize: 12, width: 200, border: "1px solid #e5e7eb", borderRadius: 4, padding: "2px 6px" }} />
421+
<button style={okBtn} disabled={checked.size === 0} onClick={() => void runBatch("done")}>
422+
⚖ 执行选中({checked.size})
423+
</button>
424+
<button style={{ ...ghostBtn, color: "#dc2626" }} disabled={checked.size === 0} onClick={() => void runBatch("abandon")}>
425+
✗ 放弃选中(留因)
426+
</button>
427+
</>
428+
)}
429+
</div>
430+
{batchMode && (
431+
<div style={{ border: "1px dashed #c4b5fd", borderRadius: 8, padding: 6, marginBottom: 8, display: "flex", flexDirection: "column", gap: 4 }}>
432+
{[...overdue, ...planned, ...rows].filter((r) => !r.unrefined).length === 0 && (
433+
<p style={{ margin: 0, fontSize: 12, color: "#9ca3af" }}>当前无可批决议的 todo 行</p>
434+
)}
435+
{[...overdue, ...planned, ...rows]
436+
.filter((r) => !r.unrefined)
437+
.map((r) => (
438+
<label key={r.id} style={{ display: "flex", gap: 6, alignItems: "center", fontSize: 12, cursor: "pointer", padding: "2px 4px", borderRadius: 4, background: checked.has(r.id) ? "#f5f3ff" : "transparent" }}>
439+
<input type="checkbox" checked={checked.has(r.id)} onChange={() => toggleChecked(r.id)} />
440+
<span style={{ color: "#111827" }}>{r.text}</span>
441+
<span style={{ fontSize: 10.5, color: "#9ca3af", marginLeft: "auto" }}>@{r.note_title}</span>
442+
</label>
443+
))}
444+
</div>
445+
)}
369446
<h4 style={{ fontSize: 12.5, margin: "6px 0 4px", color: "#dc2626" }}>逾期({overdue.length})</h4>
370447
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
371448
{overdue.length === 0 ? empty("无逾期——裁决是机制不是自动清理") : overdue.map((r) => renderRow(r, true))}

‎app/src/components/SealToggle.tsx‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/**
2+
* SealToggle — SE 情绪 tag(#树洞 类)显隐开关(v0.20.3 / REQ-301)。
3+
*
4+
* @ai-context: 封存默认排除(列表默认不可见——SE 内容不进学习主流程);开启后
5+
* 设置可显(默认关=保持排除,feature_flags.sealed_tags_visible)。
6+
*/
7+
import { useEffect, useState } from "react";
8+
import { invoke } from "@tauri-apps/api/core";
9+
10+
export function SealToggle() {
11+
const [on, setOn] = useState(false);
12+
const [msg, setMsg] = useState("");
13+
14+
useEffect(() => {
15+
void invoke<{ sealedTagsVisible?: boolean }>("get_feature_flags")
16+
.then((f) => setOn(!!f.sealedTagsVisible))
17+
.catch(() => undefined);
18+
}, []);
19+
20+
const toggle = async (next: boolean) => {
21+
try {
22+
await invoke("set_feature_flag", { name: "sealed_tags_visible", value: next });
23+
setOn(next);
24+
setMsg(next ? "已显示 #树洞 类封存笔记(默认排除态解除)" : "已恢复默认:封存(#树洞)笔记在列表隐藏");
25+
} catch (e) {
26+
setMsg(`设置失败: ${e}`);
27+
}
28+
};
29+
30+
return (
31+
<div style={{ fontSize: 12, color: "#374151", display: "flex", alignItems: "center", gap: 8 }}>
32+
<b>🧊 封存显隐(#树洞)</b>
33+
<input type="checkbox" checked={on} onChange={(e) => void toggle(e.target.checked)} style={{ cursor: "pointer" }} />
34+
<span style={{ fontSize: 11, color: "#9ca3af" }}>{on ? "显示封存笔记" : "默认排除(推荐——情绪内容不进学习主流程)"}</span>
35+
{msg && <span style={{ fontSize: 11, color: msg.startsWith("设置失败") ? "#dc2626" : "#047857" }}>{msg}</span>}
36+
</div>
37+
);
38+
}
39+
40+
export default SealToggle;

‎app/src/pages/NotesPage.tsx‎

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -373,11 +373,30 @@ export default function NotesPage({ focusNoteId, focusNoteSearch, focusGroupId,
373373
return Array.from(set).sort();
374374
}, [notes]);
375375

376+
// v0.20.3(REQ-301):SE 情绪 tag(#树洞)默认排除(设置可显)
377+
const [sealedVisible, setSealedVisible] = useState(false);
378+
useEffect(() => {
379+
void invoke<{ sealedTagsVisible?: boolean }>("get_feature_flags")
380+
.then((f) => setSealedVisible(!!f.sealedTagsVisible))
381+
.catch(() => setSealedVisible(false));
382+
}, [refreshToken]);
383+
376384
// v0.11.0:组过滤在客户端生效(列表已全量加载;组切换零请求)
377-
const visibleNotes = useMemo(
378-
() => (groupFilter === null ? notes : notes.filter((n) => n.group_id === groupFilter)),
379-
[notes, groupFilter],
380-
);
385+
// v0.20.3(REQ-301):SE 封存默认不可见——#树洞 类 tag 笔记从默认列表排除
386+
const visibleNotes = useMemo(() => {
387+
let list = groupFilter === null ? notes : notes.filter((n) => n.group_id === groupFilter);
388+
if (!sealedVisible && tagFilter == null) {
389+
list = list.filter((n) => {
390+
try {
391+
const tags = JSON.parse(n.tags ?? "[]") as unknown[];
392+
return !tags.some((t) => String(t).includes("树洞"));
393+
} catch {
394+
return true; // tags 非 JSON(旧数据)→ 不误滤
395+
}
396+
});
397+
}
398+
return list;
399+
}, [notes, groupFilter, sealedVisible, tagFilter]);
381400

382401
// v0.14 B:组映射(noteId → 组,resolveNoteColor 组继承档用)
383402
const groupMap = useMemo(() => new Map(groups.map((g) => [g.id, g])), [groups]);

‎app/src/pages/SettingsPage.tsx‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import LearningLibraryPanel from "../components/LearningLibraryPanel";
2525
import AiProviderSettings from "../components/AiProviderSettings";
2626
import AiTaskPanel from "../components/AiTaskPanel";
2727
import FeatureFlagSetting from "../components/FeatureFlagSetting";
28+
// v0.20.3(REQ-301):SE 情绪 tag 封存显隐
29+
import SealToggle from "../components/SealToggle";
2830
// v0.20.2(REQ-269):ASR 混淆画像闭环面板(候选确认制/规则管理)
2931
import AsrConfusionPanel from "../components/AsrConfusionPanel";
3032
// v0.20.2(REQ-270):可选 LLM 文本校对开关(默认关双闸门之二)
@@ -125,7 +127,10 @@ export default function SettingsPage({ active = true }: Props) {
125127
{/* ── 功能预览(默认关的新能力开关,v4 §11.3)── */}
126128
<GroupTitle>功能预览</GroupTitle>
127129
<div style={{ ...panel, marginBottom: 4 }}>
128-
<FeatureFlagSetting />
130+
<SealToggle />
131+
<div style={{ marginTop: 8 }}>
132+
<FeatureFlagSetting />
133+
</div>
129134
</div>
130135
</div>
131136
</div>

0 commit comments

Comments
 (0)