Skip to content

Commit e4a5a3b

Browse files
committed
refactor(notes): NotesPage 1299 行拆 22 文件(7 hooks + 14 组件 + 1 lib),主文件 293 行
1 parent 67cc1a3 commit e4a5a3b

23 files changed

Lines changed: 2303 additions & 1299 deletions
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/**
2+
* @ai-context: 批量删除笔记确认弹窗(真删除不可撤销)。
3+
* 自 NotesPage.tsx 原样拆出;选中数量与取消/确认动作经 props 注入。
4+
* @ai-context: Batch-delete confirmation dialog extracted verbatim from
5+
* NotesPage.tsx. Selected count and cancel/confirm actions are injected via props.
6+
*/
7+
import { Trash2 } from 'lucide-react';
8+
import { Modal, Button } from '@/components/ui';
9+
10+
interface BatchDeleteDialogProps {
11+
/** 是否打开 */
12+
open: boolean;
13+
/** 批量选中数量 */
14+
count: number;
15+
/** 取消删除 */
16+
onCancel: () => void;
17+
/** 确认删除 */
18+
onConfirm: () => void;
19+
}
20+
21+
export default function BatchDeleteDialog({ open, count, onCancel, onConfirm }: BatchDeleteDialogProps) {
22+
return (
23+
<Modal
24+
open={open}
25+
onClose={onCancel}
26+
title="批量删除笔记"
27+
description={`确定要删除选中的 ${count} 篇笔记吗?此操作不可撤销。`}
28+
footer={
29+
<>
30+
<Button variant="secondary" onClick={onCancel}>取消</Button>
31+
<Button variant="danger" icon={<Trash2 className="w-4 h-4" strokeWidth={1.5} />} onClick={onConfirm}>删除 {count} 篇</Button>
32+
</>
33+
}
34+
>
35+
<div />
36+
</Modal>
37+
);
38+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/**
2+
* @ai-context: 剪藏弹窗:URL 网页剪藏 + PDF 导入为笔记。
3+
* 自 NotesPage.tsx 原样拆出;URL 输入、剪藏状态与三个动作(URL 剪藏/PDF 导入/关闭)经 props 注入。
4+
* @ai-context: Clip-import dialog (web URL + PDF into notes) extracted verbatim
5+
* from NotesPage.tsx. URL input, loading state and actions are injected via props.
6+
*/
7+
import { Modal, Button } from '@/components/ui';
8+
import { Input } from '@/components/ui/Input';
9+
10+
interface ClipImportDialogProps {
11+
/** 是否打开 */
12+
open: boolean;
13+
/** 网页 URL 输入值 */
14+
clipUrl: string;
15+
/** 剪藏进行中(禁用按钮) */
16+
clipLoading: boolean;
17+
/** URL 输入变化 */
18+
onUrlChange: (v: string) => void;
19+
/** 剪藏当前 URL */
20+
onClipUrl: () => void;
21+
/** 选择 PDF 文件导入 */
22+
onClipPdf: () => void;
23+
/** 关闭弹窗 */
24+
onClose: () => void;
25+
}
26+
27+
export default function ClipImportDialog({
28+
open,
29+
clipUrl,
30+
clipLoading,
31+
onUrlChange,
32+
onClipUrl,
33+
onClipPdf,
34+
onClose,
35+
}: ClipImportDialogProps) {
36+
return (
37+
<Modal
38+
open={open}
39+
onClose={onClose}
40+
title="剪藏"
41+
description="将网页内容或 PDF 文件导入为笔记"
42+
size="sm"
43+
>
44+
<div className="flex flex-col gap-4">
45+
<div className="flex flex-col gap-2">
46+
<label className="text-c1 text-text-tertiary">网页 URL</label>
47+
<div className="flex gap-2">
48+
<Input
49+
placeholder="输入网页链接…"
50+
value={clipUrl}
51+
onChange={(e) => onUrlChange(e.target.value)}
52+
onKeyDown={(e) => { if (e.key === 'Enter') onClipUrl(); }}
53+
className="flex-1"
54+
/>
55+
<Button size="sm" onClick={onClipUrl} disabled={clipLoading || !clipUrl.trim()}>
56+
{clipLoading ? '剪藏中…' : '剪藏'}
57+
</Button>
58+
</div>
59+
</div>
60+
<div className="flex items-center gap-2">
61+
<span className="flex-1 h-px bg-border/30" />
62+
<span className="text-c1 text-text-tertiary">或</span>
63+
<span className="flex-1 h-px bg-border/30" />
64+
</div>
65+
<Button
66+
variant="secondary"
67+
size="sm"
68+
onClick={() => { onClipPdf(); onClose(); }}
69+
disabled={clipLoading}
70+
className="w-full"
71+
>
72+
选择 PDF 文件导入
73+
</Button>
74+
</div>
75+
</Modal>
76+
);
77+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/**
2+
* @ai-context: 删除分组确认弹窗:组内笔记移回根目录,可选同时删除组内全部笔记(含子孙分组)。
3+
* 自 NotesPage.tsx 原样拆出;分组名、递归笔记数、复选状态与动作经 props 注入。
4+
* @ai-context: Delete-folder confirmation dialog extracted verbatim from
5+
* NotesPage.tsx. Folder name, recursive note count, checkbox state and actions
6+
* are injected via props.
7+
*/
8+
import { Trash2 } from 'lucide-react';
9+
import { Modal, Button } from '@/components/ui';
10+
11+
interface DeleteFolderDialogProps {
12+
/** 是否打开(deleteFolderTarget 非空) */
13+
open: boolean;
14+
/** 待删除分组名(用于文案) */
15+
folderName: string;
16+
/** 分组树(含子孙分组)下的笔记数 */
17+
noteCount: number;
18+
/** 是否勾选「同时删除组内全部笔记」 */
19+
withNotesChecked: boolean;
20+
/** 勾选状态变化 */
21+
onWithNotesChange: (checked: boolean) => void;
22+
/** 取消删除(同时重置复选) */
23+
onCancel: () => void;
24+
/** 确认删除 */
25+
onConfirm: () => void;
26+
}
27+
28+
export default function DeleteFolderDialog({
29+
open,
30+
folderName,
31+
noteCount,
32+
withNotesChecked,
33+
onWithNotesChange,
34+
onCancel,
35+
onConfirm,
36+
}: DeleteFolderDialogProps) {
37+
return (
38+
<Modal
39+
open={open}
40+
onClose={onCancel}
41+
title="删除分组"
42+
description={folderName
43+
? `确定要删除分组「${folderName}」吗?${noteCount > 0 ? `组内 ${noteCount} 篇笔记将移至「全部笔记」,` : ''}笔记内容不会被删除。`
44+
: ''}
45+
footer={
46+
<>
47+
<Button variant="secondary" onClick={onCancel}>取消</Button>
48+
<Button
49+
variant="danger"
50+
icon={<Trash2 className="w-4 h-4" strokeWidth={1.5} />}
51+
onClick={onConfirm}
52+
>
53+
{withNotesChecked ? `删除分组与 ${noteCount} 篇笔记` : '删除分组'}
54+
</Button>
55+
</>
56+
}
57+
>
58+
<div className="flex flex-col gap-3">
59+
{/* 附加选项:同时删除组内全部笔记(含子孙分组,不可撤销) */}
60+
{noteCount > 0 && (
61+
<label className="flex items-start gap-2 p-2.5 rounded-kb-md border border-semantic-error/25 bg-semantic-error/5 cursor-pointer">
62+
<input
63+
type="checkbox"
64+
checked={withNotesChecked}
65+
onChange={(e) => onWithNotesChange(e.target.checked)}
66+
className="mt-0.5 accent-[var(--kb-brand-500)]"
67+
/>
68+
<span className="text-b3 text-text-secondary">
69+
同时删除组内全部笔记(含子分组,共 <b className="text-semantic-error">{noteCount}</b> 篇)
70+
<span className="block text-c1 text-semantic-error mt-0.5">笔记内容将永久丢失,此操作不可撤销</span>
71+
</span>
72+
</label>
73+
)}
74+
</div>
75+
</Modal>
76+
);
77+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/**
2+
* @ai-context: 单条笔记删除确认弹窗(真删除不可撤销)。
3+
* 自 NotesPage.tsx 原样拆出;打开态与取消/确认动作经 props 注入。
4+
* @ai-context: Single-note delete confirmation dialog extracted verbatim from
5+
* NotesPage.tsx. Open state and cancel/confirm actions are injected via props.
6+
*/
7+
import { Trash2 } from 'lucide-react';
8+
import { Modal, Button } from '@/components/ui';
9+
10+
interface DeleteNoteDialogProps {
11+
/** 是否打开(deleteTargetId 非空) */
12+
open: boolean;
13+
/** 取消删除 */
14+
onCancel: () => void;
15+
/** 确认删除 */
16+
onConfirm: () => void;
17+
}
18+
19+
export default function DeleteNoteDialog({ open, onCancel, onConfirm }: DeleteNoteDialogProps) {
20+
return (
21+
<Modal
22+
open={open}
23+
onClose={onCancel}
24+
title="确认删除"
25+
description="确定要删除这条笔记吗?此操作不可撤销。"
26+
footer={
27+
<>
28+
<Button variant="secondary" onClick={onCancel}>取消</Button>
29+
<Button variant="danger" icon={<Trash2 className="w-4 h-4" strokeWidth={1.5} />} onClick={onConfirm}>删除</Button>
30+
</>
31+
}
32+
>
33+
<div />
34+
</Modal>
35+
);
36+
}
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/**
2+
* @ai-context: 沉浸 3D 视图(双主题双形态:深色=沉降深渊 / 浅色=穹顶石窟):
3+
* AnimatePresence 形态切换 + AbyssView2D/GrottoView3D + ReefDiverConsole 控制盘。
4+
* 自 NotesPage.tsx 原样拆出;节点投影(reefNotes)与搜索高亮(highlightIds)的
5+
* useMemo 一并内聚于此(仅本视图使用),其余状态/动作全部经 props 注入。
6+
* @ai-context: Immersive 3D view (dark=abyss / light=grotto) with morph transition
7+
* and diver console, extracted verbatim from NotesPage.tsx. The reef projection
8+
* and search-highlight memos live here (used only by this view); other state/actions
9+
* are injected via props.
10+
*/
11+
import { useMemo } from 'react';
12+
import { AnimatePresence, motion } from 'framer-motion';
13+
import type { Note, NoteFolder } from '@/types/models';
14+
import { AbyssView2D } from '../components/reef/AbyssView2D';
15+
import { GrottoView3D } from '../components/reef/GrottoView3D';
16+
import { ReefDiverConsole } from '../components/reef/ReefDiverConsole';
17+
import type { ReefMorph, ReefNote } from '../components/reef/reefTypes';
18+
19+
interface Immersive3DViewProps {
20+
/** 过滤后的笔记列表(本视图内部投影为无 content 的 ReefNote) */
21+
notes: Note[];
22+
/** 当前选中笔记 id */
23+
selectedId: string | null;
24+
/** 当前 hover 笔记 id */
25+
hoveredId: string | null;
26+
/** 当前文件夹筛选(dim 未命中笔记) */
27+
focusFolderId: string | null;
28+
/** 主题 → 3D 形态:深色=abyss / 浅色=grotto */
29+
morph: ReefMorph;
30+
/** 文件夹列表(控制盘筛选) */
31+
folders: NoteFolder[];
32+
/** 搜索词(控制盘声呐 + 声呐点亮高亮) */
33+
searchQuery: string;
34+
/** hover 变化回调 */
35+
onHover: (id: string | null) => void;
36+
/** 选中笔记回调 */
37+
onSelect: (id: string) => void;
38+
/** 打开笔记回调 */
39+
onOpen: (id: string) => void;
40+
/** 退出沉浸视图回调 */
41+
onExit: () => void;
42+
/** 选择文件夹回调 */
43+
onSelectFolder: (id: string | null) => void;
44+
/** 搜索词变化回调 */
45+
onSearchChange: (q: string) => void;
46+
/** 新建笔记回调(打开模板选择器) */
47+
onNewNote: () => void;
48+
/** 剪藏回调(打开剪藏弹窗) */
49+
onClip: () => void;
50+
/** 打开笔记图谱回调 */
51+
onGraph: () => void;
52+
/** 进入批量模式回调(先退出 3D 视图) */
53+
onBatch: () => void;
54+
/** 进入折纸视图回调(先退出 3D 视图) */
55+
onOrigami: () => void;
56+
}
57+
58+
export default function Immersive3DView({
59+
notes,
60+
selectedId,
61+
hoveredId,
62+
focusFolderId,
63+
morph,
64+
folders,
65+
searchQuery,
66+
onHover,
67+
onSelect,
68+
onOpen,
69+
onExit,
70+
onSelectFolder,
71+
onSearchChange,
72+
onNewNote,
73+
onClip,
74+
onGraph,
75+
onBatch,
76+
onOrigami,
77+
}: Immersive3DViewProps) {
78+
// 3D 视图节点投影(无 content 全文,与 P1-1 惰性加载兼容)
79+
const reefNotes = useMemo<ReefNote[]>(() => notes.map((n) => ({
80+
id: n.id,
81+
title: n.title,
82+
template: n.template,
83+
wordCount: n.wordCount ?? 0,
84+
updatedAt: n.updatedAt,
85+
folderId: n.folderId,
86+
tags: n.tags,
87+
pinned: n.pinned,
88+
})), [notes]);
89+
90+
// 搜索高亮:声呐点亮(标题/标签匹配;空搜索=全亮)
91+
const highlightIds = useMemo<ReadonlySet<string> | null>(() => {
92+
const q = searchQuery.trim().toLowerCase();
93+
if (!q) return null;
94+
const set = new Set<string>();
95+
for (const n of notes) {
96+
if (
97+
n.title.toLowerCase().includes(q)
98+
|| n.tags.some((t) => t.toLowerCase().includes(q))
99+
) {
100+
set.add(n.id);
101+
}
102+
}
103+
return set;
104+
}, [notes, searchQuery]);
105+
106+
return (
107+
<div className="relative flex-1 min-w-0 h-full">
108+
{/* 主题切换时形态淡入淡出(mode="wait":旧 Canvas 卸载后再挂载新形态,避免双 Canvas 共存) */}
109+
<AnimatePresence mode="wait">
110+
<motion.div
111+
key={morph}
112+
className="absolute inset-0"
113+
initial={{ opacity: 0 }}
114+
animate={{ opacity: 1 }}
115+
exit={{ opacity: 0 }}
116+
transition={{ duration: 0.3 }}
117+
>
118+
{morph === 'abyss' ? (
119+
<AbyssView2D
120+
notes={reefNotes}
121+
selectedId={selectedId}
122+
hoveredId={hoveredId}
123+
highlightIds={highlightIds}
124+
focusFolderId={focusFolderId}
125+
onHover={onHover}
126+
onSelect={onSelect}
127+
onOpen={onOpen}
128+
onExit={onExit}
129+
/>
130+
) : (
131+
<GrottoView3D
132+
notes={reefNotes}
133+
selectedId={selectedId}
134+
hoveredId={hoveredId}
135+
highlightIds={highlightIds}
136+
focusFolderId={focusFolderId}
137+
onHover={onHover}
138+
onSelect={onSelect}
139+
onOpen={onOpen}
140+
onExit={onExit}
141+
/>
142+
)}
143+
</motion.div>
144+
</AnimatePresence>
145+
146+
<ReefDiverConsole
147+
morph={morph}
148+
folders={folders}
149+
selectedFolderId={focusFolderId}
150+
onSelectFolder={onSelectFolder}
151+
searchQuery={searchQuery}
152+
onSearchChange={onSearchChange}
153+
onNewNote={onNewNote}
154+
onClip={onClip}
155+
onGraph={onGraph}
156+
onBatch={onBatch}
157+
onOrigami={onOrigami}
158+
onExit={onExit}
159+
/>
160+
</div>
161+
);
162+
}

0 commit comments

Comments
 (0)