Skip to content

Commit 4ef7e85

Browse files
committed
feat(notes): 笔记列表级批量删除(与会话管理台同操作逻辑)
用户要求:笔记删除按钮需先点开笔记——对齐会话管理台:列表行勾选 + 底部批量操作栏(全选三态/计数/批量删除/取消);confirm 确认后逐条 delete_note,取消保留勾选;删除成功清空右栏选中并刷新。右栏单条删除保留。版本 0.12.8;tsc 零错误、vitest 92 通过。
1 parent 25982bc commit 4ef7e85

7 files changed

Lines changed: 133 additions & 3 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55

66
## [Unreleased] - 2026-08-23
77

8+
### v0.12.8 笔记列表级批量删除(2026-08-23 交付,详见 [docs/versions/v0.12.8.md](docs/versions/v0.12.8.md))
9+
10+
- **笔记删除与会话同操作逻辑(用户要求)**:列表行新增勾选 + 底部批量操作栏(全选三态 / 已选计数 / 批量删除 / 取消)——无需先打开笔记;确认框说明后果,取消确认保留勾选;删除后右栏选中态同步清空并刷新列表;右栏单条删除按钮保留
11+
- 验证:`tsc --noEmit` 零错误;vitest 92 全绿
12+
813
### v0.12.7 会话内部 ID 不外显(2026-08-23 交付,详见 [docs/versions/v0.12.7.md](docs/versions/v0.12.7.md))
914

1015
- **会话内部 id 不再外显(用户要求)**:实时捕获开始/停止状态、课堂助手实时捕获提示、融合完成直达卡片、图文采集开始/采集中/已保存提示、视频导入完成提示、AI 任务中心 refine 任务行——全部去掉「会话 #N」;仅保留会话列表显示序号(display_no,与内部 id 解耦)

‎app/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "app",
33
"private": true,
4-
"version": "0.12.7",
4+
"version": "0.12.8",
55
"type": "module",
66
"scripts": {
77
"dev": "vite",

‎app/src-tauri/tauri.conf.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://schema.tauri.app/config/2",
33
"productName": "Entropydecrease",
4-
"version": "0.12.7",
4+
"version": "0.12.8",
55
"identifier": "com.entropydecrease.app",
66
"build": {
77
"beforeDevCommand": "npm run dev",

‎app/src/components/NoteListView.tsx‎

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,16 @@
44
* @ai-context: 纯展示/受控组件——数据与状态全部由 NotesPage 编排(参照
55
* SessionsPage → SessionListPanel/SessionDetailPanel 既有模式)。
66
* 行 id=`note-row-{id}` 供 focusNoteId 跨页直达滚动定位。
7+
* @ai-context: v0.12.7 勾选批量删除(与会话管理台同逻辑:行内勾选 +
8+
* 底部批量操作栏;副作用经 onBatchDelete 上抛父层确认/invoke)。
79
*/
10+
import { useState } from "react";
811
import type { Note } from "../types";
912

1013
export type SortMode = "updated-desc" | "pin-first" | "created-desc";
1114

15+
const btn: React.CSSProperties = { padding: "5px 10px", cursor: "pointer", fontSize: 12 };
16+
1217
/** 解析 tags JSON 为字符串数组(损坏 JSON 回退空数组——防御性) */
1318
export function parseTags(note: Note): string[] {
1419
try {
@@ -39,12 +44,29 @@ interface Props {
3944
onCreate: () => void;
4045
onRefresh: () => void;
4146
onOpenSession: (sessionId: number) => void;
47+
/** 批量删除(父层负责确认/invoke/刷新;resolve=true 表示已执行删除——
48+
* 勾选集合在删除执行后才清空,取消确认时保留勾选——与会话管理台一致) */
49+
onBatchDelete: (ids: number[]) => Promise<boolean>;
4250
}
4351

4452
export default function NoteListView({
4553
notes, keyword, tagFilter, sortMode, allTags, selectedId, status,
46-
onKeywordChange, onTagFilterChange, onSortModeChange, onSelect, onCreate, onRefresh, onOpenSession,
54+
onKeywordChange, onTagFilterChange, onSortModeChange, onSelect, onCreate, onRefresh, onOpenSession, onBatchDelete,
4755
}: Props) {
56+
// v0.12.7:勾选集合(面板本地状态——结构与会话管理台 selected 一致)
57+
const [selected, setSelected] = useState<Set<number>>(new Set());
58+
59+
const toggleSelect = (id: number) => {
60+
setSelected((cur) => {
61+
const next = new Set(cur);
62+
if (next.has(id)) next.delete(id);
63+
else next.add(id);
64+
return next;
65+
});
66+
};
67+
68+
const clearSelection = () => setSelected(new Set());
69+
4870
return (
4971
<div style={{ width: 320, flexShrink: 0, borderRight: "1px solid #e5e7eb", display: "flex", flexDirection: "column" }}>
5072
<div style={{ padding: "10px 14px", borderBottom: "1px solid #e5e7eb", fontWeight: 600, display: "flex", alignItems: "center", gap: 8 }}>
@@ -125,6 +147,14 @@ export default function NoteListView({
125147
}}
126148
>
127149
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
150+
<input
151+
type="checkbox"
152+
checked={selected.has(n.id)}
153+
onClick={(e) => e.stopPropagation()}
154+
onChange={() => toggleSelect(n.id)}
155+
style={{ cursor: "pointer", flexShrink: 0 }}
156+
title="勾选后可批量删除"
157+
/>
128158
{n.pin ? <span style={{ fontSize: 11, color: "#b45309" }}>📌</span> : null}
129159
<div style={{ fontSize: 13, fontWeight: 500, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", flex: 1 }}>
130160
{n.title}
@@ -150,6 +180,40 @@ export default function NoteListView({
150180
);
151181
})}
152182
</div>
183+
{/* v0.12.7:勾选批量删除栏(与会话管理台同模式——全选三态 + 计数 + 批量删除) */}
184+
{selected.size > 0 && (
185+
<div style={{ borderTop: "1px solid #e5e7eb", padding: 8, display: "flex", gap: 6, alignItems: "center", background: "#fff" }}>
186+
<input
187+
type="checkbox"
188+
ref={(el) => {
189+
if (el) el.indeterminate = selected.size > 0 && selected.size < notes.length;
190+
}}
191+
checked={selected.size === notes.length && notes.length > 0}
192+
onChange={() => {
193+
if (selected.size === notes.length && notes.length > 0) {
194+
clearSelection();
195+
} else {
196+
setSelected(new Set(notes.map((n) => n.id)));
197+
}
198+
}}
199+
style={{ cursor: "pointer", flexShrink: 0 }}
200+
title="全选当前列表的笔记"
201+
/>
202+
<span style={{ fontSize: 12, color: "#374151" }}>已选 {selected.size} 个</span>
203+
<button
204+
style={{ ...btn, fontSize: 11, borderRadius: 6, border: "1px solid #fca5a5", color: "#dc2626", background: "#fff" }}
205+
onClick={async () => {
206+
const ids = [...selected];
207+
if (await onBatchDelete(ids)) clearSelection();
208+
}}
209+
>
210+
批量删除
211+
</button>
212+
<button style={{ ...btn, marginLeft: "auto", fontSize: 11 }} onClick={clearSelection}>
213+
取消
214+
</button>
215+
</div>
216+
)}
153217
{status && <p style={{ padding: 8, fontSize: 12, color: "#dc2626" }}>{status}</p>}
154218
</div>
155219
);

‎app/src/pages/NotesPage.tsx‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
*/
1414
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
1515
import { invoke } from "@tauri-apps/api/core";
16+
import { confirm } from "@tauri-apps/plugin-dialog";
1617
import type { Note } from "../types";
1718
import NoteEditView from "../components/NoteEditView";
1819
import NoteListView, { parseTags } from "../components/NoteListView";
@@ -155,6 +156,31 @@ export default function NotesPage({ focusNoteId, onOpenSessions }: Props) {
155156
}
156157
};
157158

159+
/**
160+
* v0.12.7:列表级批量删除(用户要求:与「会话」管理台同操作逻辑——勾选 +
161+
* 确认 + 逐条 invoke,无需先打开笔记;删除选中笔记同步清空右栏选中态)。
162+
* 返回是否执行了删除(取消确认返回 false——父面板据此保留/清空勾选)。
163+
*/
164+
const runBatchDelete = async (ids: number[]): Promise<boolean> => {
165+
const ok = await confirm(`确定删除选中的 ${ids.length} 个笔记?删除后不可恢复。`, {
166+
title: "熵减",
167+
kind: "warning",
168+
});
169+
if (!ok) return false;
170+
let failed = 0;
171+
for (const id of ids) {
172+
try {
173+
await invoke<boolean>("delete_note", { id });
174+
if (selected?.id === id) setSelected(null);
175+
} catch {
176+
failed += 1;
177+
}
178+
}
179+
setStatus(failed > 0 ? `已删除 ${ids.length - failed} 个,${failed} 个失败` : "");
180+
void load(keyword, tagFilter, sortMode);
181+
return true;
182+
};
183+
158184
const runPinToggle = async (note: Note) => {
159185
try {
160186
const newPin = note.pin ? 0 : 1;
@@ -283,6 +309,7 @@ export default function NotesPage({ focusNoteId, onOpenSessions }: Props) {
283309
onCreate={handleCreate}
284310
onRefresh={() => void load(keyword, tagFilter, sortMode)}
285311
onOpenSession={(id) => onOpenSessions?.(id)}
312+
onBatchDelete={runBatchDelete}
286313
/>
287314
)}
288315

‎docs/versions/README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
| v0.12.5 | [v0.12.5.md](./v0.12.5.md) — AI 精修线路修复:工作台采纳前右侧恒空(refine_workbench 三级数据源 + 未采纳任务持久化兜底)+ 线路优化(完成即自动打开工作台 / 采纳回传 taskId / 重新生成走父级管线) | 版本文档(代码已交付 2026-08-23,真机验收待执行) |
3838
| v0.12.6 | [v0.12.6.md](./v0.12.6.md) — 采集浮窗显隐链路修复:启动自动显示(预创建参数传反)+ 浮窗出现→主窗隐藏/收起→回显 + 锁定自解锁(ADR-025:全局快捷键 Ctrl+Shift+F,float_toggle 单一语义)+ 浮窗窗口级 CSS 重置(两侧透明条/滚条) | 版本文档(代码已交付 2026-08-23,真机验收待执行) |
3939
| v0.12.7 | [v0.12.7.md](./v0.12.7.md) — 会话内部 ID 不外显:采集/导入/融合/任务中心等用户可见文案全部去掉「会话 #N」,仅保留显示序号 display_no(与内部 id 解耦) | 版本文档(代码已交付 2026-08-23) |
40+
| v0.12.8 | [v0.12.8.md](./v0.12.8.md) — 笔记列表级批量删除:行内勾选 + 底部批量操作栏(全选三态/计数/批量删除),与会话管理台同操作逻辑,无需先打开笔记 | 版本文档(代码已交付 2026-08-23) |
4041
| v0.13 | [v0.13.md](./v0.13.md) — 大目标版本系列:知识体系层(v0.13.0 理念修订批/ADR-024 / v0.13.1 体系基建 / v0.13.2 概念双面体 / v0.13.3 决策与应用 / v0.13.4 审计与整合;方案 B:全局+领域双层、组仍唯一容器、REQ-202~212) | 系列文档(已立项 2026-08-23,开发未启动,REQ-146 门控) |
4142

4243
## 沉淀规则

‎docs/versions/v0.12.8.md‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# v0.12.8:笔记列表级批量删除
2+
3+
> 状态:代码已交付(2026-08-23)
4+
> 依据:用户指令「修改笔记的删除逻辑:当前删除按钮需点击笔记后才可以,想要类似会话那样的操作逻辑」(2026-08-23)
5+
6+
## 一句话
7+
8+
笔记列表行新增勾选(与会话管理台同模式)——行内勾选后出现底部批量操作栏
9+
(全选三态 / 已选计数 / 批量删除 / 取消),**无需先打开笔记**即可删除。
10+
11+
## 背景
12+
13+
此前笔记删除只能走右栏阅读视图的「删除」按钮(先点开笔记 → 再删除);
14+
会话管理台(SessionListPanel)则是「行内勾选 + 批量操作栏」的列表级删除。
15+
本次让笔记删除与会话对齐。
16+
17+
## 实现
18+
19+
| 位置 | 改动 |
20+
|------|------|
21+
| NoteListView | 行内勾选框(点击 stopPropagation,不触发选中打开)+ 底部批量操作栏(全选三态与已选计数,结构同 SessionListPanel);勾选为面板本地状态 |
22+
| NoteListView → NotesPage | 新 prop `onBatchDelete: (ids) => Promise<boolean>`——删除执行成功后清空勾选;取消确认返回 false 时保留勾选 |
23+
| NotesPage.runBatchDelete | confirm 确认(kind: warning)→ 逐条 `delete_note` → 失败计数回显 → 刷新列表;被删中选中笔记同步清空右栏 |
24+
25+
## 边界(不改)
26+
27+
- 右栏阅读视图单条「删除」按钮:保留原逻辑(点击即在读笔记上删除)
28+
- 会话/碎片删除逻辑未动
29+
- 删除确认文案:删除后不可恢复(版本记录级联删除)
30+
31+
## 验证
32+
33+
- `tsc --noEmit` 零错误;vitest 92 全绿(现有用例未覆盖该交互,行为对齐既有会话面板模式)

0 commit comments

Comments
 (0)