Skip to content

Commit 96dc41c

Browse files
committed
refactor(ui): 拆 SessionListPanel 第 4 步 — 搜索三件套
1 parent e918a96 commit 96dc41c

7 files changed

Lines changed: 375 additions & 192 deletions

File tree

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/**
2+
* NoteListToolbar — 笔记列表顶栏(标题栏 + 搜索/排序/标签区;批 0-C2·Task 2 自 NoteListView 抽出)。
3+
*
4+
* @ai-context: 展示适配器——只做「透传 + 一层组合」,**不含业务逻辑**(选择模式态在
5+
* hooks/useNoteListSelection,过滤/排序态在父层 pages/NotesPage)。
6+
* 返回 **fragment**:两个区块仍是列表外壳(width/borderRight/flex 列)的
7+
* 直接子元素,不新增包裹层(包裹层会改变 flex 布局)。
8+
* @ai-context: 文案与 data-testid 是测试契约(`batch-mode-toggle`、
9+
* 「选择模式(N)」「选择」「+ 新建」…测试用 getByText 精确匹配),
10+
* **逐字保留**(含全角括号与省略号)。样式全内联,**不换 className**。
11+
* @ai-context: `ghostBtn` 由本文件导出并被 NoteListBatchMenu 复用——同一份按钮基底,
12+
* 避免两处样式漂移;`btn` 仅作为其基底,保持模块私有。
13+
* @ai-context: 边界——`SortMode` 以 **type-only import** 自 NoteListView 取(编译期
14+
* 擦除,无运行时循环依赖);公共 API(`export type SortMode`)仍归主文件。
15+
*/
16+
import type { SortMode } from "./NoteListView";
17+
18+
const btn: React.CSSProperties = { padding: "5px 10px", cursor: "pointer", fontSize: 12 };
19+
/** 幽灵按钮基底(顶栏「选择」+ 批量栏/选集菜单 6 处共用) */
20+
export const ghostBtn: React.CSSProperties = { ...btn, fontSize: 11, borderRadius: 6, border: "1px solid #d1d5db", background: "#fff" };
21+
22+
interface Props {
23+
/** 批量选择模式(单击行=勾选) */
24+
selectionMode: boolean;
25+
/** 选集大小(模式徽标「选择模式(N)」) */
26+
selectionCount: number;
27+
/** 「选择 / 退出选择模式」切换(父层裁决进入/退出) */
28+
onToggleBatchMode: () => void;
29+
onCreate: () => void;
30+
onCollapse?: () => void;
31+
keyword: string;
32+
onKeywordChange: (kw: string) => void;
33+
onRefresh: () => void;
34+
sortMode: SortMode;
35+
onSortModeChange: (mode: SortMode) => void;
36+
allTags: string[];
37+
tagFilter: string | null;
38+
onTagFilterChange: (tag: string | null) => void;
39+
}
40+
41+
export default function NoteListToolbar({
42+
selectionMode, selectionCount, onToggleBatchMode, onCreate, onCollapse,
43+
keyword, onKeywordChange, onRefresh, sortMode, onSortModeChange,
44+
allTags, tagFilter, onTagFilterChange,
45+
}: Props) {
46+
return (
47+
<>
48+
<div style={{ padding: "10px 14px", borderBottom: "1px solid #e5e7eb", fontWeight: 600, display: "flex", alignItems: "center", gap: 8 }}>
49+
<span>📝 笔记</span>
50+
{selectionMode && (
51+
<span style={{ fontSize: 10.5, color: "#4f46e5", border: "1px solid #c7d2fe", borderRadius: 10, padding: "0 6px", background: "#eef2ff", lineHeight: "16px" }}>
52+
选择模式{selectionCount > 0 ? `(${selectionCount})` : ""}
53+
</span>
54+
)}
55+
<button
56+
data-testid="batch-mode-toggle"
57+
onClick={onToggleBatchMode}
58+
style={{
59+
...ghostBtn,
60+
marginLeft: "auto",
61+
borderColor: selectionMode ? "#4f46e5" : undefined,
62+
color: selectionMode ? "#3730a3" : "#4b5563",
63+
}}
64+
title={selectionMode ? "退出选择模式(Esc)" : "进入选择模式:单击笔记=勾选(可多选后批量操作)"}
65+
>
66+
选择
67+
</button>
68+
<button onClick={onCreate} style={{ fontSize: 12, cursor: "pointer", padding: "2px 8px", borderRadius: 4, border: "1px solid #d1d5db", background: "#f9fafb" }} title="新建笔记">+ 新建</button>
69+
<button onClick={onCollapse} style={{ fontSize: 12, cursor: "pointer", border: "none", background: "none", color: "#9ca3af" }} title="折叠列表">⟨</button>
70+
</div>
71+
72+
<div style={{ padding: 10, borderBottom: "1px solid #f3f4f6", display: "flex", flexDirection: "column", gap: 6 }}>
73+
<div style={{ display: "flex", gap: 6 }}>
74+
<input value={keyword} onChange={(e) => onKeywordChange(e.target.value)} placeholder="搜索标题/正文…" style={{ flex: 1, padding: "6px 8px", fontSize: 13, border: "1px solid #e5e7eb", borderRadius: 6, minWidth: 0 }} />
75+
<button onClick={onRefresh} style={{ fontSize: 13, cursor: "pointer" }}>⟳</button>
76+
</div>
77+
<select value={sortMode} onChange={(e) => onSortModeChange(e.target.value as SortMode)} style={{ fontSize: 12, padding: "3px 6px", border: "1px solid #e5e7eb", borderRadius: 4 }}>
78+
<option value="updated-desc">按更新时间</option>
79+
<option value="pin-first">置顶优先</option>
80+
<option value="created-desc">按创建时间</option>
81+
</select>
82+
{allTags.length > 0 && (
83+
<div style={{ display: "flex", flexWrap: "wrap", gap: 4 }}>
84+
{tagFilter && (
85+
<span onClick={() => onTagFilterChange(null)} style={{ fontSize: 11, color: "#6b7280", cursor: "pointer", border: "1px solid #d1d5db", borderRadius: 10, padding: "1px 6px", background: "#f3f4f6" }}>清除过滤 ✕</span>
86+
)}
87+
{allTags.map((t) => (
88+
<span key={t} onClick={() => onTagFilterChange(t)} style={{ fontSize: 11, cursor: "pointer", border: `1px solid ${tagFilter === t ? "#0d9488" : "#e5e7eb"}`, borderRadius: 10, padding: "1px 6px", background: tagFilter === t ? "#f0fdfa" : "#f9fafb", color: tagFilter === t ? "#0d9488" : "#6b7280" }}>{t}</span>
89+
))}
90+
</div>
91+
)}
92+
</div>
93+
</>
94+
);
95+
}

‎app/src/components/NoteListView.tsx‎

Lines changed: 16 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,14 @@ import { useNoteListSelection } from "../hooks/useNoteListSelection";
3030
import { useNoteMarquee } from "../hooks/useNoteMarquee";
3131
// 批 0-C2:展示件(列表体 / 顶栏 / 批量栏与选集菜单)——纯透传适配器,逻辑在 hook
3232
import NoteListBody from "./NoteListBody";
33+
import NoteListToolbar, { ghostBtn } from "./NoteListToolbar";
3334
import NoteRowContextMenu from "./NoteRowContextMenu";
3435

3536
// 兼容既有导入面(NoteReadingView/NotesPage/parseTags.test 从此解析——v0.15 移厝 utils)
3637
export { parseTags, fmtDate } from "../utils/noteHelpers";
3738

3839
export type SortMode = "updated-desc" | "pin-first" | "created-desc";
3940

40-
const btn: React.CSSProperties = { padding: "5px 10px", cursor: "pointer", fontSize: 12 };
41-
const ghostBtn: React.CSSProperties = { ...btn, fontSize: 11, borderRadius: 6, border: "1px solid #d1d5db", background: "#fff" };
42-
4341
interface Props {
4442
width?: number;
4543
notes: Note[];
@@ -136,51 +134,21 @@ export default function NoteListView({
136134

137135
return (
138136
<div style={{ width, flexShrink: 0, borderRight: "1px solid #e5e7eb", display: "flex", flexDirection: "column", minWidth: 0 }}>
139-
<div style={{ padding: "10px 14px", borderBottom: "1px solid #e5e7eb", fontWeight: 600, display: "flex", alignItems: "center", gap: 8 }}>
140-
<span>📝 笔记</span>
141-
{selectionMode && (
142-
<span style={{ fontSize: 10.5, color: "#4f46e5", border: "1px solid #c7d2fe", borderRadius: 10, padding: "0 6px", background: "#eef2ff", lineHeight: "16px" }}>
143-
选择模式{selection.size > 0 ? `(${selection.size})` : ""}
144-
</span>
145-
)}
146-
<button
147-
data-testid="batch-mode-toggle"
148-
onClick={() => (selectionMode ? exitBatch() : setSelectionMode(true))}
149-
style={{
150-
...ghostBtn,
151-
marginLeft: "auto",
152-
borderColor: selectionMode ? "#4f46e5" : undefined,
153-
color: selectionMode ? "#3730a3" : "#4b5563",
154-
}}
155-
title={selectionMode ? "退出选择模式(Esc)" : "进入选择模式:单击笔记=勾选(可多选后批量操作)"}
156-
>
157-
选择
158-
</button>
159-
<button onClick={onCreate} style={{ fontSize: 12, cursor: "pointer", padding: "2px 8px", borderRadius: 4, border: "1px solid #d1d5db", background: "#f9fafb" }} title="新建笔记">+ 新建</button>
160-
<button onClick={onCollapse} style={{ fontSize: 12, cursor: "pointer", border: "none", background: "none", color: "#9ca3af" }} title="折叠列表">⟨</button>
161-
</div>
162-
163-
<div style={{ padding: 10, borderBottom: "1px solid #f3f4f6", display: "flex", flexDirection: "column", gap: 6 }}>
164-
<div style={{ display: "flex", gap: 6 }}>
165-
<input value={keyword} onChange={(e) => onKeywordChange(e.target.value)} placeholder="搜索标题/正文…" style={{ flex: 1, padding: "6px 8px", fontSize: 13, border: "1px solid #e5e7eb", borderRadius: 6, minWidth: 0 }} />
166-
<button onClick={onRefresh} style={{ fontSize: 13, cursor: "pointer" }}>⟳</button>
167-
</div>
168-
<select value={sortMode} onChange={(e) => onSortModeChange(e.target.value as SortMode)} style={{ fontSize: 12, padding: "3px 6px", border: "1px solid #e5e7eb", borderRadius: 4 }}>
169-
<option value="updated-desc">按更新时间</option>
170-
<option value="pin-first">置顶优先</option>
171-
<option value="created-desc">按创建时间</option>
172-
</select>
173-
{allTags.length > 0 && (
174-
<div style={{ display: "flex", flexWrap: "wrap", gap: 4 }}>
175-
{tagFilter && (
176-
<span onClick={() => onTagFilterChange(null)} style={{ fontSize: 11, color: "#6b7280", cursor: "pointer", border: "1px solid #d1d5db", borderRadius: 10, padding: "1px 6px", background: "#f3f4f6" }}>清除过滤 ✕</span>
177-
)}
178-
{allTags.map((t) => (
179-
<span key={t} onClick={() => onTagFilterChange(t)} style={{ fontSize: 11, cursor: "pointer", border: `1px solid ${tagFilter === t ? "#0d9488" : "#e5e7eb"}`, borderRadius: 10, padding: "1px 6px", background: tagFilter === t ? "#f0fdfa" : "#f9fafb", color: tagFilter === t ? "#0d9488" : "#6b7280" }}>{t}</span>
180-
))}
181-
</div>
182-
)}
183-
</div>
137+
<NoteListToolbar
138+
selectionMode={selectionMode}
139+
selectionCount={selection.size}
140+
onToggleBatchMode={() => (selectionMode ? exitBatch() : setSelectionMode(true))}
141+
onCreate={onCreate}
142+
onCollapse={onCollapse}
143+
keyword={keyword}
144+
onKeywordChange={onKeywordChange}
145+
onRefresh={onRefresh}
146+
sortMode={sortMode}
147+
onSortModeChange={onSortModeChange}
148+
allTags={allTags}
149+
tagFilter={tagFilter}
150+
onTagFilterChange={onTagFilterChange}
151+
/>
184152

185153
<NoteListBody
186154
sections={sections}

0 commit comments

Comments
 (0)