附录 B:NoteFlow 完整源码
💡 进群学习加 wx: agentupdate
(申请发送: agentupdate)
(申请发送: agentupdate)
跟完教程 12 章后,NoteFlow 项目的完整代码结构。供参考对照。
项目结构
noteflow/
├── public/
├── src/
│ ├── components/
│ │ ├── NoteList.jsx — 笔记列表
│ │ ├── NoteList.test.jsx
│ │ ├── NoteList.css
│ │ ├── NoteEditor.jsx — 笔记编辑器
│ │ ├── NoteEditor.test.jsx
│ │ ├── NoteEditor.css
│ │ ├── SearchBar.jsx — 搜索栏
│ │ ├── SearchBar.test.jsx
│ │ ├── SearchBar.css
│ │ ├── TagSelector.jsx — 分类标签选择
│ │ ├── TagSelector.test.jsx
│ │ └── TagSelector.css
│ ├── hooks/
│ │ ├── useNotes.js — 笔记数据管理
│ │ ├── useNotes.test.js
│ │ ├── useSearch.js — 搜索功能
│ │ └── useSearch.test.js
│ ├── utils/
│ │ ├── storage.js — localStorage 封装
│ │ ├── storage.test.js
│ │ ├── search.js — 搜索和高亮
│ │ ├── search.test.js
│ │ ├── migration.js — 数据迁移
│ │ └── migration.test.js
│ ├── styles/
│ │ └── design-system.css — 设计系统 CSS 变量
│ ├── App.jsx — 主应用
│ ├── App.test.jsx — 集成测试
│ ├── App.css
│ └── main.jsx
├── docs/
│ ├── notes/
│ │ └── requirement-review.md — 需求文档(第 2 章产出)
│ ├── design/
│ │ └── design-system.md — 设计系统(第 3 章产出)
│ └── superpowers/
│ └── plans/
│ ├── noteflow-p0.md — P0 实施计划(第 4 章产出)
│ └── noteflow-p1.md — P1 实施计划(第 7 章产出)
├── CLAUDE.md — 项目说明(第 1、10 章产出)
├── package.json
├── vite.config.js
└── vitest.config.js
核心代码
storage.js
const STORAGE_KEY = 'noteflow-notes';
const MAX_STORAGE_MB = 5;
export function saveNotes(notes) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(notes));
}
export function loadNotes() {
const data = localStorage.getItem(STORAGE_KEY);
return data ? JSON.parse(data) : [];
}
export function getStorageUsage() {
const data = localStorage.getItem(STORAGE_KEY) || '';
const usedBytes = new Blob([data]).size;
const maxBytes = MAX_STORAGE_MB * 1024 * 1024;
return (usedBytes / maxBytes) * 100;
}
useNotes.js
import { useState, useCallback } from 'react';
import { loadNotes, saveNotes } from '../utils/storage';
import { migrateNotes } from '../utils/migration';
export function useNotes() {
const [notes, setNotes] = useState(() => migrateNotes(loadNotes()));
const [storageWarning, setStorageWarning] = useState(null);
const persist = useCallback((updatedNotes) => {
try {
saveNotes(updatedNotes);
setStorageWarning(null);
} catch (e) {
setStorageWarning('存储空间不足,建议导出备份后清理');
}
}, []);
const addNote = useCallback((note) => {
setNotes(prev => {
const updated = [...prev, {
...note,
id: crypto.randomUUID(),
createdAt: Date.now(),
updatedAt: Date.now(),
tags: note.tags || [],
}];
persist(updated);
return updated;
});
}, [persist]);
const updateNote = useCallback((id, changes) => {
setNotes(prev => {
const updated = prev.map(note =>
note.id === id
? { ...note, ...changes, updatedAt: Date.now() }
: note
);
persist(updated);
return updated;
});
}, [persist]);
const deleteNote = useCallback((id) => {
setNotes(prev => {
const updated = prev.filter(note => note.id !== id);
persist(updated);
return updated;
});
}, [persist]);
return { notes, addNote, updateNote, deleteNote, storageWarning };
}
useSearch.js
import { useMemo } from 'react';
import { filterNotes } from '../utils/search';
export function useSearch(notes, query) {
return useMemo(() => filterNotes(notes, query), [notes, query]);
}
search.js
export function filterNotes(notes, query) {
if (!query.trim()) return notes;
const lowerQuery = query.toLowerCase();
return notes.filter(note =>
note.title.toLowerCase().includes(lowerQuery) ||
note.content.toLowerCase().includes(lowerQuery)
);
}
export function highlightText(text, query) {
if (!query.trim()) return [{ type: 'text', text }];
const regex = new RegExp(`(${escapeRegex(query)})`, 'gi');
const parts = text.split(regex);
return parts.map((part, index) =>
part.toLowerCase() === query.toLowerCase()
? { type: 'highlight', text: part, key: index }
: { type: 'text', text: part, key: index }
);
}
function escapeRegex(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
App.jsx
import { useState } from 'react';
import { useNotes } from './hooks/useNotes';
import { useSearch } from './hooks/useSearch';
import { NoteList } from './components/NoteList';
import { NoteEditor } from './components/NoteEditor';
import { SearchBar } from './components/SearchBar';
import { TagSelector } from './components/TagSelector';
import './styles/design-system.css';
export default function App() {
const { notes, addNote, updateNote, deleteNote, storageWarning } = useNotes();
const [selectedId, setSelectedId] = useState(null);
const [searchQuery, setSearchQuery] = useState('');
const [selectedTag, setSelectedTag] = useState(null);
const filteredByTag = selectedTag
? notes.filter(note => note.tags?.includes(selectedTag))
: notes;
const searchResults = useSearch(filteredByTag, searchQuery);
const selectedNote = notes.find(note => note.id === selectedId);
return (
<div className="app">
{storageWarning && (
<div className="storage-warning">{storageWarning}</div>
)}
<aside className="sidebar">
<SearchBar query={searchQuery} onChange={setSearchQuery} />
<TagSelector
selected={selectedTag}
onSelect={setSelectedTag}
/>
<NoteList
notes={searchResults}
searchQuery={searchQuery}
selectedId={selectedId}
onSelect={setSelectedId}
onAdd={() => {
const note = addNote({ title: '未命名笔记', content: '' });
setSelectedId(note.id);
}}
onDelete={(id) => {
deleteNote(id);
if (selectedId === id) setSelectedId(null);
}}
/>
</aside>
<main className="editor">
{selectedNote ? (
<NoteEditor
note={selectedNote}
onChange={(changes) => updateNote(selectedId, changes)}
/>
) : (
<div className="empty-state">
<p>选择一条笔记开始编辑</p>
<p className="empty-hint">或点击左侧 + 创建新笔记</p>
</div>
)}
</main>
</div>
);
}
测试统计
| 模块 | 测试文件 | 测试用例数 | 覆盖率 |
|---|---|---|---|
| storage | storage.test.js | 3 | 100% |
| search | search.test.js | 5 | 95% |
| migration | migration.test.js | 2 | 100% |
| useNotes | useNotes.test.js | 6 | 90% |
| useSearch | useSearch.test.js | 4 | 95% |
| NoteList | NoteList.test.jsx | 3 | 75% |
| NoteEditor | NoteEditor.test.jsx | 3 | 70% |
| SearchBar | SearchBar.test.jsx | 2 | 80% |
| TagSelector | TagSelector.test.jsx | 2 | 80% |
| App | App.test.jsx | 3 | 65% |
| 总计 | 10 个文件 | 33 | 87% |