56 KiB
万物收藏 — 产品需求文档 & 开发计划
版本: v1.0.0
技术栈: Nuxt 4 · SQLite · Drizzle ORM
文档日期: 2026-05-17
状态: 待评审
目录
1. 产品概述
1.1 产品定位
万物收藏是一款面向个人用户的知识收藏与管理工具,核心理念是打通「收藏 → 整理 → 深读 → 创作」的完整闭环,解决以下核心问题:
- 收藏内容分散在浏览器书签、手机备忘录、各类 App,无法统一管理
- 收藏了但忘了,找不到,形成信息黑洞
- 收藏只是「消费」,缺乏将其转化为个人输出的工具支持
1.2 产品愿景
让每一条收藏都能产生价值,成为用户的第二大脑。
1.3 核心功能概览
| 模块 | 功能 |
|---|---|
| 收藏录入 | 支持网页/文本/图片/文件/视频等多种类型,多入口采集 |
| 分类体系 | 树状分类(文件夹)+ 扁平标签,双轨管理 |
| 检索系统 | 全文搜索 + 多维过滤 + AI 语义搜索 |
| 批注系统 | 文字高亮、段落批注、整体备注、评分 |
| 文章创作 | Markdown 编辑器,引用收藏,导出与分享 |
| AI 辅助 | 摘要生成、标签推荐、相关推荐、对话检索 |
2. 用户角色与场景
2.1 目标用户
主要用户: 知识工作者(设计师、工程师、研究员、内容创作者)
次要用户: 有阅读习惯的普通用户
2.2 典型使用场景
场景 A — 碎片化收藏
用户在浏览器中看到好文章,通过浏览器插件一键保存,添加标签,收藏进入收件箱,稍后整理。
场景 B — 深度整理
用户每周花 20 分钟整理收件箱,给每条收藏补充分类、标签,读一遍后在重要段落高亮批注。
场景 C — 创作输出
用户要写一篇关于「AI 设计工具」的文章,搜索相关收藏,将多条内容引用到编辑器,形成文章,导出 Markdown 或分享链接。
场景 D — 快速检索
用户想找上周看到的一篇关于「设计系统」的文章,在搜索框输入关键词,1 秒内找到目标。
3. 功能需求详述
3.1 收件箱(Inbox)
功能描述: 所有新收藏的默认落点,未整理的收藏集中于此。
需求明细:
| ID | 需求 | 优先级 |
|---|---|---|
| INB-01 | 收件箱显示所有未分配分类的收藏,按时间倒序排列 | P0 |
| INB-02 | 顶部显示待处理数量(红色角标) | P0 |
| INB-03 | 支持在收件箱直接操作:分配分类、打标签、删除 | P0 |
| INB-04 | 支持批量选择并批量操作(批量移动分类、批量删除) | P1 |
| INB-05 | 收件箱内容可以「稍后处理」标记,区别于已整理内容 | P2 |
交互逻辑:
- 收藏录入时,若未指定分类,自动归入收件箱
- 分配分类后,从收件箱消失,移入对应分类
- 收件箱数量 =
items WHERE category_id IS NULL
3.2 收藏录入
3.2.1 支持的内容类型
| 类型 | 标识 | 说明 |
|---|---|---|
| 网页 | web |
URL 链接,自动抓取标题、描述、OG 图片、favicon |
| 文本 | text |
任意文字、语录、代码片段、想法 |
| 图片 | image |
本地上传,支持 jpg/png/webp/gif,最大 10MB |
| 文件 | file |
PDF、Word、PPT 等文档,最大 50MB |
| 视频 | video |
YouTube/B站等外链,抓取封面和标题 |
3.2.2 录入入口
| ID | 需求 | 优先级 |
|---|---|---|
| ADD-01 | 网页端「新增收藏」按钮,弹出 Modal 表单 | P0 |
| ADD-02 | 浏览器扩展(Chrome/Edge),一键保存当前页面 | P1 |
| ADD-03 | 系统分享菜单(iOS/Android App,后续计划) | P3 |
| ADD-04 | 剪贴板监听:复制 URL 后,页面弹出快捷保存提示 | P2 |
| ADD-05 | 快捷键 Cmd/Ctrl + K 打开快速收藏输入框 |
P1 |
3.2.3 网页类型自动抓取
当用户输入 URL 后,后端自动:
- 抓取页面
<title>、<meta name="description">、og:image、favicon - 提取正文内容(使用
@extractus/article-extractor) - 如为视频链接(YouTube/bilibili),调用 oEmbed API 获取元数据
- AI 自动生成摘要(异步,完成后更新记录)
| ID | 需求 | 优先级 |
|---|---|---|
| FETCH-01 | URL 输入后,2 秒内自动填充标题、描述、封面图预览 | P0 |
| FETCH-02 | 抓取失败时,给出友好提示,允许手动填写标题 | P0 |
| FETCH-03 | 支持 User-Agent 伪装,绕过简单的反爬机制 | P1 |
| FETCH-04 | 抓取结果缓存 7 天,同一 URL 不重复抓取 | P1 |
| FETCH-05 | 抓取正文内容并存储,支持全文搜索 | P1 |
3.2.4 录入表单字段
| 字段 | 类型 | 是否必填 | 说明 |
|---|---|---|---|
type |
枚举 | 是 | web/text/image/file/video |
url |
string | web/video 必填 | 原始链接 |
title |
string | 是 | 标题,自动抓取可覆盖 |
description |
text | 否 | 描述或摘录内容 |
cover_url |
string | 否 | 封面图 URL |
category_id |
integer | 否 | 分类 ID,空则入收件箱 |
tags |
string[] | 否 | 标签名数组 |
note |
text | 否 | 个人备注 |
rating |
integer | 否 | 1-5 评分,默认不评 |
3.3 分类管理
功能描述: 树状文件夹结构,最多 3 级嵌套。
| ID | 需求 | 优先级 |
|---|---|---|
| CAT-01 | 创建分类,设置名称和图标(emoji) | P0 |
| CAT-02 | 编辑分类名称、图标 | P0 |
| CAT-03 | 删除分类(提示:其下收藏将移入收件箱) | P0 |
| CAT-04 | 拖拽排序分类 | P1 |
| CAT-05 | 分类支持颜色主题(6 种预设色) | P2 |
| CAT-06 | 分类支持移动(改变父级) | P1 |
| CAT-07 | 侧边栏显示每个分类的收藏数量 | P0 |
| CAT-08 | 最多支持 3 级嵌套,第 4 级创建时提示限制 | P0 |
分类图标:从预设 Emoji 集合选择,或直接输入 Emoji 字符。
3.4 标签管理
功能描述: 扁平化多维标记,一条内容可关联多个标签。
| ID | 需求 | 优先级 |
|---|---|---|
| TAG-01 | 录入时直接输入标签名,自动创建 | P0 |
| TAG-02 | 标签输入支持 autocomplete(已有标签下拉提示) | P0 |
| TAG-03 | 标签管理页:列出所有标签及数量,支持重命名、删除、合并 | P1 |
| TAG-04 | 标签支持自定义颜色(从 8 种预设色中选) | P1 |
| TAG-05 | AI 智能推荐标签(基于内容分析,收藏时弹出建议) | P2 |
| TAG-06 | 侧边栏显示常用标签(按使用频率,最多显示 10 个) | P0 |
| TAG-07 | 点击标签名,过滤显示所有含该标签的收藏 | P0 |
3.5 检索系统
3.5.1 全文搜索
| ID | 需求 | 优先级 |
|---|---|---|
| SEARCH-01 | 支持对 title、description、note、content(正文)的全文搜索 |
P0 |
| SEARCH-02 | 搜索结果高亮匹配关键词 | P0 |
| SEARCH-03 | 搜索响应时间 < 500ms(本地 SQLite FTS5) | P0 |
| SEARCH-04 | 支持短语搜索(加引号)和排除搜索(-keyword) | P1 |
| SEARCH-05 | 搜索历史记录,最近 20 条,可清除 | P2 |
实现方案: SQLite FTS5 全文索引,建立 items_fts 虚拟表,对 title + description + content + note 建联合索引。
3.5.2 多维过滤
支持以下维度组合过滤,多条件为 AND 关系:
| 过滤维度 | 可选值 |
|---|---|
| 内容类型 | web / text / image / file / video |
| 分类 | 选中分类(含子分类) |
| 标签 | 选中标签(多选 AND/OR 切换) |
| 时间范围 | 今天 / 本周 / 本月 / 自定义范围 |
| 星标 | 是 / 否 |
| 评分 | ≥ N 星 |
| 排序 | 收藏时间倒序 / 更新时间倒序 / 评分倒序 |
| ID | 需求 | 优先级 |
|---|---|---|
| FILTER-01 | 顶部过滤栏支持快速按类型切换 | P0 |
| FILTER-02 | 高级过滤面板(抽屉),组合多维度 | P1 |
| FILTER-03 | 过滤条件 URL 参数化,支持分享过滤状态 | P2 |
| FILTER-04 | 过滤条件组合可保存为「智能分类」 | P3 |
3.6 批注系统
功能描述: 在收藏内容上进行个人标注,沉淀思考。
| ID | 需求 | 优先级 |
|---|---|---|
| ANN-01 | 整体备注:在收藏详情页写自由文本备注(Markdown 支持) | P0 |
| ANN-02 | 评分:1-5 星,可修改 | P0 |
| ANN-03 | 文字高亮:选中网页正文中的文字,标记颜色高亮(4 种色) | P1 |
| ANN-04 | 段落批注:在高亮文字处添加批注评论 | P2 |
| ANN-05 | 批注列表:详情页显示所有高亮和批注的汇总 | P2 |
| ANN-06 | 高亮内容支持导出(复制为 Markdown 格式) | P2 |
高亮存储格式:
{
"highlights": [
{
"id": "uuid",
"text": "被高亮的原文",
"color": "yellow",
"start_offset": 120,
"end_offset": 180,
"note": "批注内容(可为空)",
"created_at": "2026-05-17T10:00:00Z"
}
]
}
3.7 文章创作
功能描述: 以收藏为素材,进行深度写作创作的工具。
3.7.1 编辑器功能
| ID | 需求 | 优先级 |
|---|---|---|
| ART-01 | Markdown 编辑器,支持实时预览(左右分栏) | P0 |
| ART-02 | 编辑器工具栏:标题/加粗/斜体/链接/图片/代码块/引用 | P0 |
| ART-03 | 自动保存(debounce 1 秒)并显示保存状态 | P0 |
| ART-04 | 支持插入收藏引用:输入 [[ 触发收藏搜索面板 |
P0 |
| ART-05 | 引用块显示:收藏卡片形式(标题 + 来源 + 摘录) | P1 |
| ART-06 | 文章封面图设置 | P1 |
| ART-07 | 文章字数统计、阅读时间预估 | P1 |
| ART-08 | 版本历史:每次保存自动记录快照,可恢复历史版本 | P2 |
3.7.2 文章关联
| ID | 需求 | 优先级 |
|---|---|---|
| ART-REL-01 | 文章与收藏的关联关系:一篇文章可关联多条收藏 | P0 |
| ART-REL-02 | 收藏详情页显示「被 N 篇文章引用」,点击可跳转 | P1 |
| ART-REL-03 | 关联关系在文章发布时自动建立(解析 [[ 引用块) |
P0 |
3.7.3 发布与导出
| ID | 需求 | 优先级 |
|---|---|---|
| ART-PUB-01 | 导出为 Markdown 文件(.md) | P0 |
| ART-PUB-02 | 导出为 PDF | P1 |
| ART-PUB-03 | 生成只读分享链接(公开访问,无需登录) | P1 |
| ART-PUB-04 | 分享链接可设置有效期(7 天 / 30 天 / 永久) | P2 |
| ART-PUB-05 | 分享页支持评论(访客可留言) | P3 |
3.8 AI 辅助功能
| ID | 功能 | 触发方式 | 优先级 |
|---|---|---|---|
| AI-01 | 收藏摘要生成 | 收藏录入后异步生成 | P1 |
| AI-02 | 标签智能推荐 | 录入时提示,可一键采纳 | P1 |
| AI-03 | 相关收藏推荐 | 详情页侧栏展示 | P2 |
| AI-04 | 对话式检索「问我的收藏」 | 底部 AI 对话框 | P2 |
| AI-05 | 写作辅助(续写/润色) | 文章编辑器内 AI 按钮 | P3 |
| AI-06 | 每周回顾生成 | 定时任务,每周一 | P3 |
AI 接入方案: 通过 API Key 方式接入,优先支持 OpenAI / Anthropic / 本地 Ollama,在设置页配置。摘要生成使用流式输出,避免等待时间过长。
3.9 用户与账户
初版为单用户本地应用,不需要注册登录。后续版本规划云同步和多账户。
| ID | 需求 | 优先级 |
|---|---|---|
| USER-01 | 设置页:修改显示名称和头像 | P1 |
| USER-02 | 设置页:配置 AI API Key 和 Provider | P1 |
| USER-03 | 设置页:导出全量数据(JSON + 附件 ZIP) | P1 |
| USER-04 | 设置页:导入数据(从 JSON 恢复) | P2 |
| USER-05 | 设置页:配置浏览器插件 Token | P1 |
4. 数据库设计
数据库: SQLite(文件路径:./data/wanwu.db)
ORM: Drizzle ORM
迁移管理: Drizzle Kit
4.1 完整 Schema 定义
// db/schema.ts
import { sqliteTable, text, integer, real, blob } from 'drizzle-orm/sqlite-core'
import { sql } from 'drizzle-orm'
// ─── 分类表 ───────────────────────────────────────────
export const categories = sqliteTable('categories', {
id: integer('id').primaryKey({ autoIncrement: true }),
name: text('name').notNull(),
icon: text('icon').notNull().default('📁'), // emoji
color: text('color').default('#6B7280'), // hex 颜色
parentId: integer('parent_id').references(() => categories.id, { onDelete: 'set null' }),
sortOrder: integer('sort_order').notNull().default(0),
createdAt: text('created_at').notNull().default(sql`(datetime('now'))`),
updatedAt: text('updated_at').notNull().default(sql`(datetime('now'))`),
})
// ─── 标签表 ───────────────────────────────────────────
export const tags = sqliteTable('tags', {
id: integer('id').primaryKey({ autoIncrement: true }),
name: text('name').notNull().unique(),
color: text('color').default('#6B7280'),
createdAt: text('created_at').notNull().default(sql`(datetime('now'))`),
})
// ─── 收藏主表 ─────────────────────────────────────────
export const items = sqliteTable('items', {
id: integer('id').primaryKey({ autoIncrement: true }),
type: text('type', { enum: ['web', 'text', 'image', 'file', 'video'] }).notNull(),
title: text('title').notNull(),
description: text('description'), // 简短描述/摘录
content: text('content'), // 完整正文(用于 FTS)
url: text('url'), // 原始链接
coverUrl: text('cover_url'), // 封面图 URL
faviconUrl: text('favicon_url'), // 网站图标
sourceHost: text('source_host'), // 来源域名
filePath: text('file_path'), // 本地文件路径(image/file 类型)
fileSize: integer('file_size'), // 文件大小 bytes
fileMime: text('file_mime'), // MIME type
// 组织
categoryId: integer('category_id').references(() => categories.id, { onDelete: 'set null' }),
// 个人标记
note: text('note'), // 个人备注(支持 Markdown)
rating: integer('rating'), // 1-5,null 表示未评
starred: integer('starred', { mode: 'boolean' }).notNull().default(false),
highlights: text('highlights', { mode: 'json' }), // 高亮批注 JSON
// AI 生成
aiSummary: text('ai_summary'), // AI 摘要
aiKeywords: text('ai_keywords', { mode: 'json' }), // AI 关键词数组
// 状态
isArchived: integer('is_archived', { mode: 'boolean' }).notNull().default(false),
// 时间戳
publishedAt: text('published_at'), // 原文发布时间
createdAt: text('created_at').notNull().default(sql`(datetime('now'))`),
updatedAt: text('updated_at').notNull().default(sql`(datetime('now'))`),
})
// ─── 收藏-标签 多对多关联表 ───────────────────────────
export const itemTags = sqliteTable('item_tags', {
itemId: integer('item_id').notNull().references(() => items.id, { onDelete: 'cascade' }),
tagId: integer('tag_id').notNull().references(() => tags.id, { onDelete: 'cascade' }),
})
// ─── 文章表 ───────────────────────────────────────────
export const articles = sqliteTable('articles', {
id: integer('id').primaryKey({ autoIncrement: true }),
title: text('title').notNull().default('无标题文章'),
content: text('content').notNull().default(''), // Markdown 原文
coverUrl: text('cover_url'),
status: text('status', { enum: ['draft', 'published'] }).notNull().default('draft'),
wordCount: integer('word_count').notNull().default(0),
// 分享
shareToken: text('share_token').unique(), // 公开分享 token
shareExpiresAt: text('share_expires_at'),
createdAt: text('created_at').notNull().default(sql`(datetime('now'))`),
updatedAt: text('updated_at').notNull().default(sql`(datetime('now'))`),
})
// ─── 文章-收藏 关联表 ─────────────────────────────────
export const articleItems = sqliteTable('article_items', {
articleId: integer('article_id').notNull().references(() => articles.id, { onDelete: 'cascade' }),
itemId: integer('item_id').notNull().references(() => items.id, { onDelete: 'cascade' }),
sortOrder: integer('sort_order').notNull().default(0),
})
// ─── 文章版本历史表 ───────────────────────────────────
export const articleVersions = sqliteTable('article_versions', {
id: integer('id').primaryKey({ autoIncrement: true }),
articleId: integer('article_id').notNull().references(() => articles.id, { onDelete: 'cascade' }),
content: text('content').notNull(),
wordCount: integer('word_count').notNull().default(0),
createdAt: text('created_at').notNull().default(sql`(datetime('now'))`),
})
// ─── FTS 虚拟表(全文搜索)────────────────────────────
// 使用 SQLite FTS5,通过迁移脚本手动创建:
// CREATE VIRTUAL TABLE items_fts USING fts5(
// id UNINDEXED,
// title,
// description,
// content,
// note,
// content=items,
// content_rowid=id,
// tokenize='unicode61 remove_diacritics 1'
// );
//
// 同步触发器:
// CREATE TRIGGER items_ai AFTER INSERT ON items BEGIN
// INSERT INTO items_fts(rowid, title, description, content, note)
// VALUES (new.id, new.title, new.description, new.content, new.note);
// END;
// CREATE TRIGGER items_au AFTER UPDATE ON items BEGIN
// INSERT INTO items_fts(items_fts, rowid, title, description, content, note)
// VALUES ('delete', old.id, old.title, old.description, old.content, old.note);
// INSERT INTO items_fts(rowid, title, description, content, note)
// VALUES (new.id, new.title, new.description, new.content, new.note);
// END;
// CREATE TRIGGER items_ad AFTER DELETE ON items BEGIN
// INSERT INTO items_fts(items_fts, rowid, title, description, content, note)
// VALUES ('delete', old.id, old.title, old.description, old.content, old.note);
// END;
// ─── 用户设置表 ───────────────────────────────────────
export const settings = sqliteTable('settings', {
key: text('key').primaryKey(),
value: text('value', { mode: 'json' }),
updatedAt: text('updated_at').notNull().default(sql`(datetime('now'))`),
})
// 预设 settings key:
// display_name : string
// avatar_url : string
// ai_provider : 'openai' | 'anthropic' | 'ollama'
// ai_api_key : string (加密存储)
// ai_model : string
// ai_base_url : string (Ollama 自定义地址)
// plugin_token : string (浏览器插件认证 token)
// theme : 'dark' | 'light' | 'system'
4.2 索引设计
-- 高频查询索引
CREATE INDEX idx_items_category ON items(category_id);
CREATE INDEX idx_items_created ON items(created_at DESC);
CREATE INDEX idx_items_starred ON items(starred) WHERE starred = 1;
CREATE INDEX idx_items_type ON items(type);
CREATE INDEX idx_items_rating ON items(rating);
CREATE INDEX idx_item_tags_item ON item_tags(item_id);
CREATE INDEX idx_item_tags_tag ON item_tags(tag_id);
CREATE INDEX idx_article_items ON article_items(article_id);
CREATE UNIQUE INDEX idx_item_tags_unique ON item_tags(item_id, tag_id);
4.3 Drizzle 配置
// drizzle.config.ts
import { defineConfig } from 'drizzle-kit'
export default defineConfig({
schema: './db/schema.ts',
out: './db/migrations',
dialect: 'sqlite',
dbCredentials: {
url: process.env.DATABASE_URL ?? './data/wanwu.db',
},
verbose: true,
strict: true,
})
5. API 接口设计
所有接口基于 Nuxt 4 的 server/api/ 目录,使用 H3 框架,格式遵循 REST 约定。
通用响应格式:
// 成功
{ data: T, total?: number, page?: number }
// 失败
{ error: { code: string, message: string } }
通用错误码:
| Code | HTTP Status | 说明 |
|---|---|---|
VALIDATION_ERROR |
400 | 参数校验失败 |
NOT_FOUND |
404 | 资源不存在 |
CONFLICT |
409 | 资源冲突(如标签名重复) |
INTERNAL_ERROR |
500 | 服务器内部错误 |
5.1 收藏接口
GET /api/items
获取收藏列表,支持分页和过滤。
Query 参数:
| 参数 | 类型 | 说明 |
|---|---|---|
page |
number | 页码,默认 1 |
limit |
number | 每页条数,默认 20,最大 100 |
q |
string | 搜索关键词(全文搜索) |
type |
string | 内容类型过滤 |
categoryId |
number | 'inbox' | 分类 ID,inbox 表示无分类 |
tagIds |
string | 标签 ID 逗号分隔 |
tagMode |
'and' | 'or' | 多标签关系,默认 and |
starred |
boolean | 星标过滤 |
ratingGte |
number | 评分下限 |
dateFrom |
string | 开始时间 ISO 8601 |
dateTo |
string | 结束时间 ISO 8601 |
sort |
string | created_at_desc / updated_at_desc / rating_desc |
archived |
boolean | 是否已归档,默认 false |
响应:
{
data: Item[],
total: number,
page: number,
limit: number,
hasMore: boolean
}
POST /api/items
创建新收藏。
Request Body:
{
type: 'web' | 'text' | 'image' | 'file' | 'video'
url?: string
title: string
description?: string
categoryId?: number
tagNames?: string[] // 标签名数组,自动创建不存在的标签
note?: string
rating?: number
starred?: boolean
}
响应: { data: Item }
副作用:
- 若 type=web,触发异步 URL 抓取任务(Server-Sent Events 通知进度)
- 触发异步 AI 摘要生成
GET /api/items/:id
获取单条收藏详情,包含完整字段(含 content、highlights)。
PATCH /api/items/:id
更新收藏。支持部分更新,仅传入需要修改的字段。
可更新字段: title description note rating starred categoryId isArchived highlights coverUrl
DELETE /api/items/:id
删除收藏。若有关联文章引用,保留关联记录但收藏标记为已删除。
POST /api/items/:id/tags
为收藏添加标签。
Body: { tagName: string }
响应: { data: { itemId, tagId, tagName } }
DELETE /api/items/:id/tags/:tagId
移除收藏的某个标签。
POST /api/items/fetch-url
预抓取 URL 元数据(用于录入表单实时预览)。
Body: { url: string }
响应:
{
data: {
title: string
description: string
coverUrl: string
faviconUrl: string
sourceHost: string
type: 'web' | 'video'
}
}
POST /api/items/batch
批量操作。
Body:
{
ids: number[]
action: 'move' | 'delete' | 'star' | 'unstar' | 'archive'
categoryId?: number // action=move 时必填
}
5.2 分类接口
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/categories |
获取分类树(嵌套结构) |
| POST | /api/categories |
创建分类 |
| PATCH | /api/categories/:id |
更新分类(名称/图标/颜色/父级/排序) |
| DELETE | /api/categories/:id |
删除分类 |
| POST | /api/categories/reorder |
批量更新排序 { ids: number[] } |
GET /api/categories 响应:
{
data: Array<{
id: number
name: string
icon: string
color: string
itemCount: number
children: Category[] // 递归
}>
}
5.3 标签接口
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/tags |
获取所有标签及使用数量 |
| POST | /api/tags |
创建标签 |
| PATCH | /api/tags/:id |
更新标签名称/颜色 |
| DELETE | /api/tags/:id |
删除标签(解除所有关联) |
| POST | /api/tags/:id/merge |
合并标签 { targetTagId: number } |
5.4 文章接口
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/articles |
获取文章列表(分页) |
| POST | /api/articles |
创建新文章 |
| GET | /api/articles/:id |
获取文章详情(含关联收藏) |
| PATCH | /api/articles/:id |
更新文章内容 |
| DELETE | /api/articles/:id |
删除文章 |
| POST | /api/articles/:id/publish |
发布文章 |
| POST | /api/articles/:id/share |
生成分享链接 |
| DELETE | /api/articles/:id/share |
撤销分享 |
| GET | /api/articles/:id/versions |
获取版本历史 |
| POST | /api/articles/:id/restore/:versionId |
恢复历史版本 |
| GET | /api/articles/:id/export |
导出文章(query: format=md/pdf) |
| GET | /api/share/:token |
公开分享页数据(无需认证) |
5.5 搜索接口
GET /api/search
Query: q (必填), limit (默认 10)
响应:
{
data: {
items: Array<Item & { highlight: string }> // highlight 为带 <mark> 标签的匹配片段
articles: Array<Article & { highlight: string }>
}
}
GET /api/search/suggest
搜索建议(用于输入框 autocomplete)。
Query: q, limit (默认 5)
响应: { data: string[] } — 建议关键词数组
5.6 AI 接口
POST /api/ai/summarize
手动触发 AI 摘要(流式响应)。
Body: { itemId: number }
响应: text/event-stream,逐 token 返回
POST /api/ai/suggest-tags
获取 AI 标签建议。
Body: { title: string, description?: string, content?: string }
响应: { data: string[] } — 建议标签名数组
POST /api/ai/chat
对话式检索「问我的收藏」。
Body:
{
message: string
history?: Array<{ role: 'user' | 'assistant', content: string }>
}
响应: text/event-stream
5.7 设置接口
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/settings |
获取所有设置 |
| PATCH | /api/settings |
批量更新设置 { [key]: value } |
| GET | /api/settings/export |
导出全量数据 (ZIP) |
| POST | /api/settings/import |
导入数据 |
6. 前端页面与交互
6.1 页面路由结构
/ → 重定向到 /inbox
/inbox → 收件箱
/items → 全部收藏
/items/starred → 星标收藏
/items?categoryId=N → 分类视图
/items?tagId=N → 标签视图
/articles → 文章列表
/articles/new → 新建文章
/articles/:id → 文章编辑器
/share/:token → 公开分享页(无需认证,独立 layout)
/settings → 设置页
/settings/tags → 标签管理
6.2 全局布局(Layout)
三栏结构:
┌──────────────────────────────────────────────────────────────┐
│ Topbar(高度 48px):Logo | 搜索框 | 新增按钮 | 头像 │
├───────────┬──────────────────────────────────┬───────────────┤
│ │ │ │
│ Sidebar │ 主内容区(卡片/列表) │ 详情面板 │
│ 220px │ │ 380px │
│ │ │ (可收起) │
│ │ AI 对话条(固定底部) │ │
└───────────┴──────────────────────────────────┴───────────────┘
Sidebar 区块:
- 视图导航(收件箱、全部、最近、星标、文章)
- 分类树(可展开/折叠,带拖拽排序)
- 常用标签(最多 10 个)
- 底部:设置入口
详情面板(抽屉):
- 点击卡片时从右侧滑入(400px 宽)
- 不影响主列表区域(overlay 模式在移动端,push 模式在桌面端)
- 内容:封面/预览、标题、来源、AI 摘要、标签、备注编辑、评分、相关推荐、操作按钮
6.3 主内容区
视图模式
| 模式 | 说明 |
|---|---|
| 卡片(Grid) | 2-3 列瀑布流,每卡含封面缩略图 |
| 列表(List) | 单列,含标题/来源/标签/时间,密度更高 |
卡片组件规范
网页类型卡片:
┌─────────────────────────────┐
│ [封面图 / 120px 高] │
│ ┌类型badge┐ ⭐收藏按钮 │
├─────────────────────────────┤
│ favicon 来源域名 │
│ 标题文字(最多 2 行) │
│ 描述(最多 2 行,灰色) │
│ #标签1 #标签2 05-16 │
└─────────────────────────────┘
文本类型卡片(无封面):
- 顶部区域显示文本前 3 行内容(背景渐变)
排序与分组
- 支持按时间/评分/标题排序
- 支持按日期分组(今天 / 本周 / 更早)
6.4 搜索体验
- 聚焦搜索框,展示最近搜索记录
- 输入时实时 autocomplete(debounce 150ms)
- 回车执行全文搜索,结果页显示匹配高亮
- 搜索词在结果列表中用
<mark>高亮 - 搜索结果分区:收藏 / 文章
6.5 新增收藏 Modal
┌──────────────────────────────────────────┐
│ 新增收藏 ✕ │
├──────────────────────────────────────────┤
│ 类型:[🌐 网页] [📝 文本] [🖼️ 图片] │
│ [🎬 视频] [📄 文件] │
│ │
│ 链接或内容 │
│ ┌────────────────────────────────────┐ │
│ │ https://... │ │
│ └────────────────────────────────────┘ │
│ │
│ [正在抓取元数据...] ← URL 输入后自动触发 │
│ │
│ 标题(已自动填充) │
│ ┌────────────────────────────────────┐ │
│ │ Vercel 设计系统 2024 更新 │ │
│ └────────────────────────────────────┘ │
│ │
│ 分类 标签 │
│ ┌──────────────┐ ┌──────────────────┐│
│ │ 📁 设计 ▾ │ │ #设计 #参考... + ││
│ └──────────────┘ └──────────────────┘│
│ │
│ [AI 建议标签:设计系统 Vercel 工具 +] │
│ │
│ 备注(可选) │
│ ┌────────────────────────────────────┐ │
│ │ │ │
│ └────────────────────────────────────┘ │
│ │
│ [取消] [保存收藏 →] │
└──────────────────────────────────────────┘
6.6 文章编辑器页面
布局:
┌─────────────────────────────────────────────────────────────┐
│ ← 返回 | 文章标题(inline 编辑) | 草稿已保存 发布 ▾ │
├──────────────────────────┬──────────────────────────────────┤
│ │ │
│ Markdown 编辑区 │ 实时预览区 │
│ (左半) │ (右半) │
│ │ │
│ 输入 [[ 触发 │ │
│ 收藏搜索面板 │ │
│ │ │
└──────────────────────────┴──────────────────────────────────┘
│ 字数:1,234 阅读时间:约 5 分钟 | 版本历史 导出 分享 │
└─────────────────────────────────────────────────────────────┘
[[ 收藏引用面板:
- 弹出 Popover,输入搜索词实时过滤收藏
- 选中后插入引用块:
> [!cite] Vercel 设计系统 2024 更新 > 来源:vercel.com · 2026-05-16 > Vercel 团队分享了最新的设计系统更新...
7. 技术架构
7.1 技术栈详情
| 层级 | 技术 | 版本 | 用途 |
|---|---|---|---|
| 框架 | Nuxt 4 | 4.x | 全栈框架,SSR/SPA |
| 运行时 | Node.js | 20 LTS | 服务端运行时 |
| 数据库 | SQLite | 3.x | 本地持久化存储 |
| ORM | Drizzle ORM | latest | 类型安全数据库操作 |
| 数据库驱动 | better-sqlite3 | latest | SQLite Node.js 绑定 |
| UI 组件 | Nuxt UI v3 | 3.x | 基础 UI 组件库 |
| CSS | Tailwind CSS | v4 | 样式 |
| 状态管理 | Pinia | latest | 客户端状态 |
| 编辑器 | CodeMirror 6 | 6.x | Markdown 编辑器 |
| MD 渲染 | markdown-it | latest | Markdown → HTML |
| 图标 | Nuxt Icon / Lucide | latest | 图标库 |
| 验证 | Zod | latest | API 参数校验 |
| 测试 | Vitest | latest | 单元/集成测试 |
7.2 项目目录结构
wanwu/
├── app/
│ ├── components/
│ │ ├── item/
│ │ │ ├── ItemCard.vue # 收藏卡片
│ │ │ ├── ItemCardList.vue # 列表视图行
│ │ │ ├── ItemDetail.vue # 详情面板
│ │ │ ├── ItemAddModal.vue # 新增 Modal
│ │ │ └── ItemHighlight.vue # 高亮批注组件
│ │ ├── category/
│ │ │ ├── CategoryTree.vue # 分类树
│ │ │ └── CategoryModal.vue # 分类编辑
│ │ ├── article/
│ │ │ ├── ArticleEditor.vue # 文章编辑器
│ │ │ ├── ArticleCard.vue # 文章卡片
│ │ │ └── ArticleCitePanel.vue # 引用收藏面板
│ │ ├── search/
│ │ │ ├── SearchBar.vue
│ │ │ └── SearchResults.vue
│ │ ├── common/
│ │ │ ├── TagBadge.vue
│ │ │ ├── StarRating.vue
│ │ │ ├── FilterBar.vue
│ │ │ └── AiChatStrip.vue
│ │ └── layout/
│ │ ├── AppSidebar.vue
│ │ └── AppTopbar.vue
│ ├── composables/
│ │ ├── useItems.ts # 收藏数据操作
│ │ ├── useCategories.ts
│ │ ├── useTags.ts
│ │ ├── useArticles.ts
│ │ ├── useSearch.ts
│ │ ├── useAi.ts
│ │ └── useInfiniteScroll.ts
│ ├── layouts/
│ │ ├── default.vue # 主布局(三栏)
│ │ └── share.vue # 分享页布局
│ ├── pages/
│ │ ├── index.vue # → redirect /inbox
│ │ ├── inbox.vue
│ │ ├── items.vue # 全部收藏 / 分类 / 标签视图
│ │ ├── articles/
│ │ │ ├── index.vue
│ │ │ ├── new.vue
│ │ │ └── [id].vue
│ │ ├── share/
│ │ │ └── [token].vue
│ │ └── settings/
│ │ ├── index.vue
│ │ └── tags.vue
│ └── stores/
│ ├── ui.ts # UI 状态(侧栏收起、详情面板等)
│ ├── items.ts
│ ├── categories.ts
│ └── search.ts
├── server/
│ ├── api/
│ │ ├── items/
│ │ │ ├── index.get.ts
│ │ │ ├── index.post.ts
│ │ │ ├── [id].get.ts
│ │ │ ├── [id].patch.ts
│ │ │ ├── [id].delete.ts
│ │ │ ├── [id]/tags.post.ts
│ │ │ ├── [id]/tags/[tagId].delete.ts
│ │ │ ├── fetch-url.post.ts
│ │ │ └── batch.post.ts
│ │ ├── categories/
│ │ │ ├── index.get.ts
│ │ │ ├── index.post.ts
│ │ │ ├── [id].patch.ts
│ │ │ ├── [id].delete.ts
│ │ │ └── reorder.post.ts
│ │ ├── tags/
│ │ │ ├── index.get.ts
│ │ │ ├── index.post.ts
│ │ │ ├── [id].patch.ts
│ │ │ ├── [id].delete.ts
│ │ │ └── [id]/merge.post.ts
│ │ ├── articles/
│ │ │ ├── index.get.ts
│ │ │ ├── index.post.ts
│ │ │ ├── [id].get.ts
│ │ │ ├── [id].patch.ts
│ │ │ ├── [id].delete.ts
│ │ │ ├── [id]/publish.post.ts
│ │ │ ├── [id]/share.post.ts
│ │ │ ├── [id]/export.get.ts
│ │ │ └── [id]/versions/
│ │ ├── search/
│ │ │ ├── index.get.ts
│ │ │ └── suggest.get.ts
│ │ ├── ai/
│ │ │ ├── summarize.post.ts
│ │ │ ├── suggest-tags.post.ts
│ │ │ └── chat.post.ts
│ │ ├── settings/
│ │ │ ├── index.get.ts
│ │ │ ├── index.patch.ts
│ │ │ ├── export.get.ts
│ │ │ └── import.post.ts
│ │ └── share/
│ │ └── [token].get.ts
│ ├── services/
│ │ ├── fetcher.ts # URL 抓取服务
│ │ ├── ai.ts # AI 接入层
│ │ ├── search.ts # FTS 搜索逻辑
│ │ └── exporter.ts # 导出服务
│ ├── utils/
│ │ ├── db.ts # Drizzle 实例(单例)
│ │ └── validate.ts # Zod schema 集合
│ └── plugins/
│ └── db-init.ts # 数据库初始化(运行迁移)
├── db/
│ ├── schema.ts
│ ├── migrations/ # Drizzle Kit 生成
│ └── seed.ts # 测试数据
├── public/
│ └── uploads/ # 本地上传文件(.gitignore)
├── data/ # SQLite 数据库文件(.gitignore)
├── drizzle.config.ts
├── nuxt.config.ts
├── package.json
└── .env.example
7.3 关键实现细节
数据库连接(单例模式)
// server/utils/db.ts
import { drizzle } from 'drizzle-orm/better-sqlite3'
import Database from 'better-sqlite3'
import * as schema from '~/db/schema'
let db: ReturnType<typeof drizzle>
export function useDB() {
if (!db) {
const sqlite = new Database(process.env.DATABASE_URL ?? './data/wanwu.db')
sqlite.pragma('journal_mode = WAL') // 提高并发性能
sqlite.pragma('foreign_keys = ON') // 启用外键约束
sqlite.pragma('synchronous = NORMAL') // 平衡安全与性能
db = drizzle(sqlite, { schema })
}
return db
}
URL 抓取服务
// server/services/fetcher.ts
import { extract } from '@extractus/article-extractor'
import { load } from 'cheerio'
export async function fetchUrlMeta(url: string) {
const res = await fetch(url, {
headers: { 'User-Agent': 'Mozilla/5.0 ...' },
signal: AbortSignal.timeout(8000),
})
const html = await res.text()
const $ = load(html)
const meta = {
title: $('meta[property="og:title"]').attr('content') || $('title').text(),
description: $('meta[property="og:description"]').attr('content')
|| $('meta[name="description"]').attr('content'),
coverUrl: $('meta[property="og:image"]').attr('content'),
faviconUrl: resolveFavicon(url, $),
sourceHost: new URL(url).hostname,
}
// 提取正文
const article = await extract(url)
return { ...meta, content: article?.content }
}
FTS5 搜索
// server/services/search.ts
export async function fullTextSearch(db: DB, query: string, limit = 20) {
const sanitized = query.replace(/['"*]/g, ' ').trim()
const ftsQuery = sanitized.split(/\s+/).map(t => `"${t}"`).join(' AND ')
return db.all(sql`
SELECT
i.*,
snippet(items_fts, 1, '<mark>', '</mark>', '...', 20) AS highlight
FROM items_fts
JOIN items i ON i.id = items_fts.rowid
WHERE items_fts MATCH ${ftsQuery}
AND i.is_archived = 0
ORDER BY rank
LIMIT ${limit}
`)
}
7.4 nuxt.config.ts
export default defineNuxtConfig({
compatibilityDate: '2025-01-01',
future: { compatibilityVersion: 4 },
modules: [
'@nuxt/ui',
'@pinia/nuxt',
'@nuxt/icon',
'nuxt-file-storage',
],
nitro: {
experimental: { asyncContext: true },
storage: {
uploads: { driver: 'fs', base: './public/uploads' }
}
},
css: ['~/assets/css/main.css'],
runtimeConfig: {
databaseUrl: './data/wanwu.db',
aiProvider: '',
aiApiKey: '',
aiModel: '',
public: {}
}
})
8. 开发计划
8.1 里程碑总览
| 里程碑 | 内容 | 工期 | 目标完成日 |
|---|---|---|---|
| M0 | 项目初始化与基础架构 | 3 天 | Week 1 |
| M1 | 数据库 + 核心 API | 5 天 | Week 2 |
| M2 | 收藏管理前端(增删改查) | 5 天 | Week 3 |
| M3 | 分类 + 标签系统 | 3 天 | Week 3-4 |
| M4 | 搜索与过滤系统 | 3 天 | Week 4 |
| M5 | 详情面板与批注 | 4 天 | Week 5 |
| M6 | 文章编辑器 | 5 天 | Week 6 |
| M7 | AI 功能接入 | 4 天 | Week 7 |
| M8 | 设置页 + 导入导出 | 3 天 | Week 8 |
| M9 | 浏览器扩展 | 4 天 | Week 9 |
| M10 | 测试 + 打磨 + 发布 | 5 天 | Week 10 |
总工期: 约 10 周(单人全职开发)
8.2 M0 — 项目初始化(Week 1,第 1-3 天)
任务清单:
npx nuxi init wanwu --template ui初始化项目- 配置 Nuxt 4 兼容模式(
future.compatibilityVersion: 4) - 安装依赖:
better-sqlite3,drizzle-orm,drizzle-kit,zod,@extractus/article-extractor,cheerio - 创建
db/schema.ts,定义所有表结构 - 运行
drizzle-kit generate生成迁移文件 - 实现
server/utils/db.ts单例 + 数据库初始化插件 - 配置
.env.example,列出所有环境变量 - 设置 ESLint + Prettier + husky pre-commit
- 配置 Tailwind CSS 主题色(暗色系 + 琥珀色 accent)
- 建立全局 Layout(Sidebar + Topbar + 主内容区占位)
- 编写
db/seed.ts,插入 10 条测试收藏数据 - 验证:
npm run dev启动无报错,数据库文件正常创建
交付物: 可运行的开发环境,数据库结构已建立
8.3 M1 — 核心 API(Week 2,第 4-8 天)
任务清单(按接口优先级):
收藏 CRUD(第 4-5 天):
GET /api/items— 列表查询(含分页、类型过滤、排序)POST /api/items— 创建收藏(Zod 校验)GET /api/items/:id— 单条详情PATCH /api/items/:id— 部分更新DELETE /api/items/:id— 删除
分类 + 标签(第 6 天):
GET/POST/PATCH/DELETE /api/categories— 分类 CRUD(含递归树查询)GET/POST/PATCH/DELETE /api/tags— 标签 CRUDPOST /api/items/:id/tags、DELETE /api/items/:id/tags/:tagId
URL 抓取(第 7 天):
POST /api/items/fetch-url— URL 元数据抓取- 实现
server/services/fetcher.ts - 错误处理:超时 / 抓取失败 / 非 HTML 响应
FTS 搜索(第 8 天):
- 在迁移脚本中创建 FTS5 虚拟表和同步触发器
GET /api/search— 全文搜索,返回带高亮片段GET /api/search/suggest— 搜索建议
验证: 用 Postman/Hoppscotch 测试所有接口,全部通过
8.4 M2 — 收藏管理前端(Week 2-3,第 6-10 天)
任务清单:
基础列表(第 6-7 天):
pages/items.vue— 收藏列表页,接入GET /api/itemscomponents/item/ItemCard.vue— 卡片组件(含 hover 效果、封面图、类型 badge)components/item/ItemCardList.vue— 列表视图行- 卡片/列表视图切换
- 无限滚动加载(Intersection Observer +
useInfiniteScrollcomposable)
新增收藏(第 8 天):
components/item/ItemAddModal.vue- 类型切换(5 种)
- URL 输入 + 自动抓取(loading 态)
- 标题、分类选择、标签输入(autocomplete)
- AI 标签推荐展示(后续接入)
- 接入
POST /api/items - 成功后刷新列表(乐观更新)
Pinia Store(第 9 天):
stores/items.ts— 收藏状态管理(列表、分页、加载状态)stores/categories.tsstores/ui.ts— 面板状态、视图模式
快捷键(第 10 天):
Cmd/Ctrl + K打开快速收藏输入框Esc关闭 Modal / 详情面板
8.5 M3 — 分类 + 标签系统(Week 3-4,第 11-13 天)
任务清单:
分类树(第 11 天):
components/category/CategoryTree.vue— 递归树形组件,展开/折叠- 分类创建/编辑 Modal(名称、图标 Emoji、颜色)
- 拖拽排序(使用
vue-draggable-plus) - 接入分类 CRUD API
标签系统(第 12 天):
components/common/TagBadge.vue— 标签组件(含颜色、点击过滤)pages/settings/tags.vue— 标签管理页(列表、编辑、删除、合并)- 侧边栏常用标签显示
收件箱(第 13 天):
pages/inbox.vue— 收件箱视图(仅显示categoryId IS NULL)- 顶部 Topbar 收件箱数量角标
- 收件箱批量分类操作
8.6 M4 — 搜索与过滤(Week 4,第 14-16 天)
任务清单:
搜索(第 14 天):
components/search/SearchBar.vue— 顶部搜索框,输入 autocomplete- 搜索结果页,含高亮显示
- 搜索历史(localStorage 存储)
过滤系统(第 15-16 天):
components/common/FilterBar.vue— 类型快速切换- 高级过滤面板(抽屉 Drawer):时间范围、评分、星标
- 多标签 AND/OR 模式切换
- 过滤状态 URL 参数化(使用
useRoute+navigateTo)
8.7 M5 — 详情面板与批注(Week 5,第 17-20 天)
任务清单:
详情面板(第 17-18 天):
components/item/ItemDetail.vue— 右侧详情面板- 封面大图
- 标题(inline 编辑)
- 来源信息、发布时间
- AI 摘要(含 loading skeleton)
- 标签展示 + 添加标签
- 备注编辑区(Markdown 支持,自动保存)
- 星标评分
- 相关推荐(3 条)
- 操作按钮:写文章 / 复制链接 / 编辑 / 删除
- 面板打开/关闭动画(CSS transition)
- 移动端改为全屏 Drawer
批注功能(第 19-20 天):
components/item/ItemHighlight.vue— 网页正文展示 + 高亮选择- 选中文字触发颜色选择气泡
- 批注 CRUD(存储到
items.highlightsJSON 字段) - 批注汇总列表
8.8 M6 — 文章编辑器(Week 6,第 21-25 天)
任务清单:
编辑器核心(第 21-22 天):
- 集成 CodeMirror 6(Markdown 语法高亮、vi/emacs 键位可选)
- 实时预览(markdown-it 渲染)
- 工具栏(标题/加粗/斜体/链接/图片/代码块/引用)
- 自动保存(debounce 1s,显示保存状态)
- 字数统计、阅读时间预估
收藏引用(第 23 天):
components/article/ArticleCitePanel.vue—[[触发的收藏搜索面板- 选中收藏后插入引用 Markdown 块
- 预览区渲染引用块为卡片样式
- 解析引用关系,保存到
article_items
发布与导出(第 24 天):
- 文章状态切换(草稿 → 发布)
- 导出 Markdown(直接下载)
- 生成分享链接(生成 token,写入 DB)
- 分享页
pages/share/[token].vue
版本历史(第 25 天):
- 每次保存记录快照到
article_versions - 版本历史侧边栏(时间 + 字数)
- 点击恢复某版本
8.9 M7 — AI 功能(Week 7,第 26-29 天)
任务清单:
AI 基础接入(第 26 天):
server/services/ai.ts— 统一 AI 接入层- 支持 OpenAI / Anthropic / Ollama
- 流式输出封装
- 设置页:AI 配置(Provider / API Key / Model)
- 测试连接功能
摘要生成(第 27 天):
- 收藏录入后异步触发摘要(Server-Sent Events)
- 详情面板显示摘要 + loading 骨架
- 手动重新生成摘要按钮
标签推荐(第 28 天):
- 新增 Modal 中:URL 抓取完成后自动调用 AI 建议标签
- 建议标签一键添加
- 接口:
POST /api/ai/suggest-tags
对话检索(第 29 天):
components/common/AiChatStrip.vue— 底部 AI 对话条POST /api/ai/chat— RAG 检索:先 FTS 搜索相关收藏,再送入 AI 生成回答- 对话历史(session 级)
- 引用相关收藏卡片在回答中展示
8.10 M8 — 设置页 + 导入导出(Week 8,第 30-32 天)
任务清单:
pages/settings/index.vue— 设置主页- 个人资料(名称、头像)
- AI 配置
- 浏览器扩展 Token 管理
- 主题切换(暗/亮/系统)
- 危险区域(清空数据)
- 全量数据导出(JSON + 附件 ZIP)
- 数据导入与校验
GET /api/settings/export实现
8.11 M9 — 浏览器扩展(Week 9,第 33-36 天)
任务清单:
- 创建 Chrome Extension(Manifest V3)项目
extension/ - Popup 页面:
- 显示当前页面标题/封面
- 选择分类、输入标签
- 「保存到万物收藏」按钮
- Background Service Worker:
- 调用本地
http://localhost:3000/api/items(或配置的 Host) - Token 认证
- 调用本地
- 右键菜单:「保存链接到万物收藏」
- 选中文字右键:「保存文字片段到万物收藏」
- 测试扩展在 Chrome / Edge 中正常工作
8.12 M10 — 测试与发布(Week 10,第 37-41 天)
任务清单:
测试(第 37-38 天):
- 核心 API 单元测试(Vitest):items / categories / tags CRUD
- 搜索功能测试
- 边界情况:超长标题、特殊字符、大文件上传
- 多浏览器兼容测试(Chrome / Firefox / Safari)
性能优化(第 39 天):
- 图片懒加载
- 虚拟滚动(收藏列表超过 500 条时)
- 数据库慢查询排查(EXPLAIN QUERY PLAN)
- Nuxt build 产物分析(
nuxt analyze)
打磨(第 40 天):
- 空状态设计(收件箱为空、搜索无结果等)
- 错误状态设计(网络失败、抓取失败等)
- Loading skeleton 补全
- 响应式适配(iPad 布局)
发布(第 41 天):
- 编写 README(安装、配置、启动指南)
npm run build验证生产构建- Docker 镜像打包(
Dockerfile+docker-compose.yml) - GitHub Release v1.0.0
9. 非功能需求
9.1 性能要求
| 指标 | 目标值 |
|---|---|
| 收藏列表首屏加载 | < 1s(本地 SQLite) |
| 全文搜索响应 | < 500ms |
| URL 元数据抓取 | < 3s(含 timeout) |
| 页面路由切换 | < 200ms(客户端导航) |
| 数据库查询(单次) | < 100ms |
9.2 可靠性
- SQLite WAL 模式,防止写入崩溃数据损坏
- 文章自动保存,最小化数据丢失风险
- 导出功能作为数据备份手段
- URL 抓取失败不影响收藏创建
9.3 安全性
- AI API Key 不明文存储(本地应用采用系统 Keychain 或环境变量)
- 文件上传类型白名单校验(不允许可执行文件)
- 文件上传大小限制(图片 10MB,文档 50MB)
- 分享链接使用 UUID v4 Token,无法枚举
- 所有 API 输入通过 Zod 校验,防止注入
9.4 数据持久化
- 数据库文件:
./data/wanwu.db - 上传文件:
./public/uploads/(按日期分目录) - 建议用户定期使用「导出功能」备份数据
- 数据库迁移向后兼容,升级不丢数据
10. 验收标准
10.1 P0 功能验收(必须完成)
| 功能 | 验收标准 |
|---|---|
| 新增网页收藏 | 输入 URL → 自动填充标题和描述 → 保存成功 → 列表显示 |
| 新增文本收藏 | 输入文字 → 保存 → 列表显示 |
| 分类创建 | 创建分类 → 侧边栏显示 → 收藏可分配该分类 |
| 标签打标 | 录入时添加标签 → 卡片显示 → 点击标签过滤有效 |
| 全文搜索 | 输入关键词 → 500ms 内返回结果 → 关键词高亮 |
| 编辑备注 | 详情面板编辑备注 → 1s 内自动保存 → 刷新后仍存在 |
| 写文章 | 新建文章 → 使用 [[ 引用收藏 → 导出 Markdown 正确 |
| 删除收藏 | 删除后列表消失,数据库记录删除 |
| 收件箱 | 无分类收藏显示在收件箱,分配分类后消失 |
10.2 P1 功能验收(重要)
| 功能 | 验收标准 |
|---|---|
| AI 摘要 | 保存收藏后,详情面板摘要在 10s 内显示 |
| 标签推荐 | 新增收藏时展示 AI 建议标签,点击可采纳 |
| 批注高亮 | 在正文中选中文字 → 选色 → 高亮持久化 |
| 文章分享 | 生成链接 → 无需登录访问 → 内容正确显示 |
| 浏览器扩展 | 扩展 Popup 可保存当前页面到收藏 |
| 数据导出 | 导出 ZIP 包含 JSON 数据和所有附件 |
10.3 已知限制(v1.0 不做)
- 不支持多用户 / 账户系统
- 不支持云同步(本地优先)
- 不支持移动 App(Web 响应式适配为主)
- AI 对话无跨 session 记忆
- 不支持 PDF 正文全文搜索(仅标题和备注)
文档结束 — 万物收藏 PRD v1.0.0