35 KiB
Tunji - 笔记应用设计文档
类印象笔记 + Typora 的桌面笔记应用,基于 Electron + Vue 3 + Tailwind CSS + PrimeVue (Unstyled) 构建
一、技术栈
| 层级 | 技术 | 说明 |
|---|---|---|
| 桌面框架 | Electron | 跨平台桌面应用 |
| 前端框架 | Vue 3 (Composition API) | 响应式 UI |
| 路由 | Vue Router 4 | SPA 路由管理 |
| 状态管理 | Pinia | 全局状态管理 |
| 样式 | Tailwind CSS 4 | 原子化 CSS |
| 组件库 | PrimeVue (Unstyled Mode) | 无样式组件,配合 Tailwind 自定义样式 |
| 图标 | lucide-vue-next | 轻量级图标库 |
| Markdown 处理 | unified (remark + rehype) | Markdown 解析 / 渲染 / 编辑 |
| 编辑器引擎 | ProseMirror / Tiptap | 富文本块编辑(Typora 风格) |
| 本地数据库 | better-sqlite3 (主进程) | 搜索索引、元数据缓存 |
| 文件监控 | chokidar | 工作目录文件变更监听 |
| 同步引擎 | 自研抽象层 | 支持 Git / WebDAV / S3 / 坚果云 |
二、主题色系统
参考 PrimeVue Lara 主题设计,采用蓝色为主色调,搭配中性灰色系。
2.1 主色 (Primary)
| 变量名 | 色值 | 用途 |
|---|---|---|
--p-primary-50 |
#eff6ff |
最浅背景 |
--p-primary-100 |
#dbeafe |
浅色背景 / hover 背景 |
--p-primary-200 |
#bfdbfe |
边框 / 分割线 |
--p-primary-300 |
#93c5fd |
禁用状态 |
--p-primary-400 |
#60a5fa |
hover 边框 |
--p-primary-500 |
#3b82f6 |
默认按钮 / 链接 |
--p-primary-600 |
#2563eb |
按钮 hover |
--p-primary-700 |
#1d4ed8 |
按钮 active / pressed |
--p-primary-800 |
#1e40af |
深色文字 |
--p-primary-900 |
#1e3a8a |
最深色 |
2.2 表面色 (Surface)
| 变量名 | 色值 | 用途 |
|---|---|---|
--p-surface-0 |
#ffffff |
页面背景 / 卡片背景 |
--p-surface-50 |
#f8fafc |
左侧栏背景 |
--p-surface-100 |
#f1f5f9 |
次级背景 / hover 背景 |
--p-surface-200 |
#e2e8f0 |
分割线 / 边框 |
--p-surface-300 |
#cbd5e1 |
禁用边框 |
--p-surface-400 |
#94a3b8 |
占位符文字 |
--p-surface-500 |
#64748b |
次要文字 |
--p-surface-600 |
#475569 |
正文文字 |
--p-surface-700 |
#334155 |
标题文字 |
--p-surface-800 |
#1e293b |
主标题文字 |
--p-surface-900 |
#0f172a |
最深文字 |
2.3 语义色
| 语义 | 色值 | 说明 |
|---|---|---|
--p-success-500 |
#22c55e |
成功 / 新建 |
--p-success-600 |
#16a34a |
成功 hover |
--p-warn-500 |
#f59e0b |
警告 |
--p-warn-600 |
#d97706 |
警告 hover |
--p-danger-500 |
#ef4444 |
危险 / 删除 |
--p-danger-600 |
#dc2626 |
危险 hover |
--p-info-500 |
#06b6d4 |
信息提示 |
2.4 暗色模式
暗色模式下 surface 反转,primary 色保持不变但降低饱和度:
| 变量名 | 色值 | 用途 |
|---|---|---|
--p-surface-0 (dark) |
#0f172a |
页面背景 |
--p-surface-50 (dark) |
#1e293b |
左侧栏背景 |
--p-surface-100 (dark) |
#334155 |
卡片背景 |
--p-surface-200 (dark) |
#475569 |
边框 / 分割线 |
--p-surface-700 (dark) |
#e2e8f0 |
标题文字 |
--p-surface-800 (dark) |
#f1f5f9 |
主标题文字 |
三、工作目录设计
3.1 核心概念
参考 Obsidian 的 Vault 模型:用户首次启动时必须选择一个工作目录,所有数据(笔记、笔记本、标签、配置、插件)都在该目录下管理。
3.2 目录结构
<用户选择的工作目录>/
├── .tunji/ # 应用元数据目录(隐藏)
│ ├── config.json # 应用配置(主题、编辑器设置等)
│ ├── tags.json # 标签定义(全局标签库)
│ ├── notebooks.json # 笔记本元数据(排序、颜色等)
│ ├── sync.json # 同步配置(WebDAV/S3/Git 凭据引用)
│ ├── plugins/ # 插件目录(未来扩展)
│ │ └── ...
│ ├── trash/ # 废纸篓(已删除笔记的临时存放)
│ │ ├── <note-id>.md
│ │ └── ...
│ ├── versions/ # 本地版本快照(Git-like 内容寻址)
│ │ ├── <hash1> # 文件内容的 SHA-256 哈希
│ │ ├── <hash2>
│ │ └── ...
│ └── index.sqlite # 本地搜索索引(不同步!)
│
├── 笔记本A/ # 用户的笔记本(即文件夹)
│ ├── 笔记1.md # 笔记文件
│ ├── 笔记1.assets/ # 笔记1 的专属附件目录(同名配对)
│ │ ├── 架构图.png
│ │ └── 需求文档.pdf
│ ├── 笔记2.md
│ ├── 笔记2.assets/ # 笔记2 的专属附件目录
│ │ └── screenshot.jpg
│ ├── 子笔记本C/ # 笔记本支持嵌套
│ │ ├── 笔记3.md
│ │ ├── 笔记3.assets/
│ │ │ └── 流程图.png
│ │ └── ...
│ └── ...
│
├── 笔记本B/
│ ├── 笔记4.md
│ ├── 笔记4.assets/
│ │ └── 演示视频.mp4
│ └── ...
│
└── 无归属笔记.md # 根目录下不属于任何笔记本的笔记
└── 无归属笔记.assets/
└── logo.png
3.3 笔记文件格式
每个 .md 文件使用 YAML Front Matter 存储元数据:
---
id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
title: "笔记标题"
created: "2026-06-01T10:00:00.000Z"
updated: "2026-06-01T12:30:00.000Z"
tags: ["工作", "会议", "项目A"]
notebook: "笔记本A"
pinned: false
favorited: false
---
# 笔记标题
这是笔记正文内容...
3.4 附件存储设计
设计原则
每个笔记文件拥有一个同名的 .assets/ 目录,附件与笔记一一配对。这是 VS Code Markdown 预览和 Typora 验证过的成熟模式。
命名规则:笔记名.md ↔ 笔记名.assets/
存储方式
笔记本A/
├── 笔记1.md # 引用: 
├── 笔记1.assets/ # 笔记1 的专属附件
│ ├── 架构图.png # 保留用户原始文件名
│ ├── 架构图(1).png # 同名文件自动编号
│ └── 需求文档.pdf
├── 笔记2.md
├── 笔记2.assets/
│ └── screenshot.jpg
└── 子笔记本C/
├── 笔记3.md
├── 笔记3.assets/
│ └── 流程图.png
└── ...
文件名策略(不用 UUID)
| 场景 | 处理方式 | 示例 |
|---|---|---|
| 首次保存 | 保留原始文件名 | 架构图.png |
| 同名冲突 | 自动追加数字后缀 | 架构图(1).png、架构图(2).png |
| 粘贴/截图 | 使用时间戳命名 | image-20260601-143022.png |
| 剪贴板图片 | 使用时间戳命名 | clipboard-20260601-143022.png |
引用方式
笔记中引用附件使用相对于自身的路径:

[需求文档](笔记1.assets/需求文档.pdf)
移动笔记时的附件处理
这是本设计的核心优势——移动笔记零成本:
移动前:
笔记本A/
├── 会议纪要.md
└── 会议纪要.assets/
└── 白板照片.jpg
移动后(拖拽到笔记本B):
笔记本B/
├── 会议纪要.md # 引用路径不变:
└── 会议纪要.assets/
└── 白板照片.jpg
笔记本A/
└── (干净,无残留)
只需要移动两个东西:
会议纪要.md会议纪要.assets/目录
引用路径 会议纪要.assets/白板照片.jpg 是相对于 .md 文件的,所以完全不需要修改笔记内容。
重命名笔记时的附件处理
重命名笔记时自动重命名对应的 .assets/ 目录:
重命名前:笔记1.md + 笔记1.assets/
重命名后:项目方案.md + 项目方案.assets/
同时批量替换笔记内所有 笔记1.assets/ → 项目方案.assets/ 的引用。
删除笔记时的附件处理
- 删除笔记 →
.md文件 + 同名.assets/目录一起移入废纸篓 - 恢复笔记 → 一起恢复
- 彻底删除 → 一起彻底删除
- 不存在孤儿附件文件
对比其他方案
| 方案 | 移动笔记 | 重命名笔记 | 删除笔记 | 用户可读性 | 同步冲突 |
|---|---|---|---|---|---|
| 全局附件 + UUID | 需迁移附件+改引用 | 需改引用 | 需扫描孤儿文件 | 差(UUID) | 高(大目录) |
| 每笔记本 assets | 需迁移附件+改引用 | 需改引用 | 需扫描孤儿文件 | 好 | 中 |
| 每笔记 .assets(推荐) | 移动两个文件/目录即可 | 改目录名+批量替换引用 | 自动跟随 | 好 | 低(分散) |
3.5 笔记本 vs 文件夹
- 笔记本 = 文件夹:直接映射,用户在文件管理器中看到的结构就是应用中的结构
- 支持无限层级嵌套
.tunji/目录下的notebooks.json存储笔记本的额外元数据(颜色、图标、排序)
四、同步架构设计
4.1 设计原则
- 数据库不同步:
index.sqlite纯本地,同步后在本地重建 - 版本快照不同步:
.tunji/versions/纯本地 - 配置可选同步:
.tunji/config.json可选是否同步 - 只同步用户内容:
.md文件 + 同名.assets/目录 +.tunji/tags.json+.tunji/notebooks.json
4.2 同步抽象层
┌─────────────────────────────────────────┐
│ SyncManager │
│ (事件驱动,统一接口) │
├─────────────────────────────────────────┤
│ - sync() 触发全量同步 │
│ - pull() 拉取远程变更 │
│ - push() 推送本地变更 │
│ - getConflicts() 获取冲突列表 │
│ - resolve() 解决冲突 │
│ - getStatus() 同步状态 │
├──────────┬──────────┬──────────┬─────────┤
│ GitSync │WebDavSync│ S3Sync │坚果云Sync│
│ (libgit2)│(webdav) │(aws-sdk) │(webdav) │
└──────────┴──────────┴──────────┴─────────┘
4.3 同步策略
Git 同步
// 策略:自动 commit + push,冲突时提示用户
// config 示例:
{
type: 'git',
repoUrl: 'https://github.com/user/notes.git',
branch: 'main', // 默认 main
autoCommit: true, // 每次变更自动 commit
autoPush: true, // 每次 commit 后自动 push
commitMessage: '{{date}} {{action}}', // 支持模板
sshKeyPath: '', // SSH 私钥路径(可选)
token: '' // HTTPS token(可选)
}
WebDAV 同步(坚果云等)
// config 示例:
{
type: 'webdav',
serverUrl: 'https://dav.jianguoyun.com/dav/',
username: 'user@example.com',
password: 'app-specific-password', // 应用专用密码
remotePath: '/tunji-notes/', // 远程目录路径
syncInterval: 300 // 自动同步间隔(秒)
}
S3 同步
// config 示例:
{
type: 's3',
endpoint: 'https://s3.amazonaws.com', // S3 兼容端点
bucket: 'my-notes',
accessKey: 'AKIAIOSFODNN7EXAMPLE',
secretKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
region: 'us-east-1',
prefix: 'tunji/' // 对象前缀
}
4.4 冲突解决策略
| 场景 | 策略 |
|---|---|
| 仅本地修改 | 直接推送 |
| 仅远程修改 | 直接拉取 |
| 双方都修改 | 冲突:生成冲突副本 笔记名(冲突-本地).md + 笔记名(冲突-远程).md,提示用户手动合并 |
| 本地删除 vs 远程修改 | 提示用户确认:恢复 or 确认删除 |
| 远程删除 vs 本地修改 | 提示用户确认:重新推送 or 确认删除 |
4.5 同步状态机
IDLE → SYNCING → SUCCESS
→ CONFLICT (需用户介入)
→ ERROR (网络错误等,自动重试)
五、本地数据库设计
5.1 为什么需要本地数据库
- 全文搜索:纯文件遍历搜索太慢,需要倒排索引
- 元数据查询:按标签、创建时间、更新时间等维度快速筛选
- 排序和分页:大量笔记的列表展示需要高效查询
- 跨笔记本搜索:模糊搜索需要全局索引
5.2 数据库选型
better-sqlite3(Electron 主进程)
- 纯本地,无需网络
- 同步到工作目录
.tunji/index.sqlite - 不同步到远程
5.3 数据库 Schema
-- 笔记索引表(从 .md 文件解析的元数据)
CREATE TABLE notes (
id TEXT PRIMARY KEY, -- UUID
title TEXT NOT NULL,
file_path TEXT NOT NULL UNIQUE, -- 相对于工作目录的路径
notebook_path TEXT, -- 所属笔记本路径
created_at TEXT, -- ISO 8601
updated_at TEXT,
pinned INTEGER DEFAULT 0,
favorited INTEGER DEFAULT 0,
word_count INTEGER DEFAULT 0,
is_deleted INTEGER DEFAULT 0 -- 软删除(废纸篓)
);
-- 标签表
CREATE TABLE tags (
id TEXT PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
color TEXT -- 标签颜色
);
-- 笔记-标签关联表
CREATE TABLE note_tags (
note_id TEXT NOT NULL,
tag_id TEXT NOT NULL,
PRIMARY KEY (note_id, tag_id),
FOREIGN KEY (note_id) REFERENCES notes(id),
FOREIGN KEY (tag_id) REFERENCES tags(id)
);
-- 全文搜索虚拟表(FTS5)
CREATE VIRTUAL TABLE notes_fts USING fts5(
title,
content, -- 笔记正文(不含 front matter)
note_id UNINDEXED,
tokenize='unicode61'
);
-- 笔记本元数据表
CREATE TABLE notebooks (
path TEXT PRIMARY KEY, -- 相对于工作目录的路径
name TEXT NOT NULL,
parent_path TEXT,
color TEXT,
icon TEXT,
sort_order INTEGER DEFAULT 0
);
-- 同步状态表
CREATE TABLE sync_state (
key TEXT PRIMARY KEY,
value TEXT
);
5.4 索引重建流程
当用户在其他设备同步后打开应用,或删除了 index.sqlite:
1. 扫描工作目录下所有 .md 文件
2. 解析 front matter 元数据
3. 提取正文内容
4. 批量写入 notes 表
5. 批量写入 notes_fts 全文索引
6. 解析 tags.json,写入 tags 表
7. 建立 note_tags 关联
8. 完成,进入正常模式
六、版本控制与文件防丢失
6.1 本地版本快照(不依赖 Git)
原理:每次保存笔记时,将旧内容的 SHA-256 哈希作为文件名,存入 .tunji/versions/ 目录。
.tunji/versions/
├── a1b2c3d4... # 笔记某次修改的内容快照
├── e5f6g7h8...
└── ...
.tunji/versions-manifest.json # 版本清单
{
"note-id-1": [
{ "hash": "a1b2c3d4...", "timestamp": "2026-06-01T10:00:00Z", "message": "自动保存" },
{ "hash": "e5f6g7h8...", "timestamp": "2026-06-01T09:30:00Z", "message": "自动保存" }
]
}
6.2 版本回退机制
用户点击"版本历史"
→ 读取 versions-manifest.json 获取该笔记的所有版本
→ 按时间倒序展示版本列表
→ 用户选择某个版本
→ 从 versions/<hash> 读取内容
→ 预览差异(diff view)
→ 确认回退
→ 将当前版本存为新快照(可撤销)
→ 将选中版本内容写回 .md 文件
→ 更新数据库索引
6.3 防丢失策略
| 策略 | 说明 |
|---|---|
| 自动保存 | 编辑器内容每 5 秒自动保存到 .md 文件 |
| 版本快照 | 每次保存前,旧版本自动存入 versions/ |
| 废纸篓 | 删除笔记不直接删除,移入 .tunji/trash/,保留 30 天 |
| 崩溃恢复 | Electron 主进程监听 crash 事件,编辑器状态暂存 localStorage |
| 同步前备份 | 同步拉取远程变更前,先备份本地当前版本 |
6.4 版本快照清理策略
- 保留最近 50 个版本
- 超过 30 天的版本,仅保留每天第一个
- 超过 90 天的版本,仅保留每周第一个
- 用户可手动清理所有历史版本
七、页面布局设计
7.1 整体布局
┌──────────────────────────────────────────────────────────┐
│ Electron Title Bar │
├──────────┬───────────────────────────────────────────────┤
│ │ │
│ LEFT │ RIGHT AREA │
│ AREA │ (Router View) │
│ │ │
│ 240- │ ┌─────────────────────────────────────────┐ │
│ 480px │ │ Notes View / Tags View / Note Detail │ │
│ 可拖拽 │ │ │ │
│ │ │ │ │
│ │ │ │ │
│ │ │ │ │
│ │ └─────────────────────────────────────────┘ │
│ │ │
└──────────┴───────────────────────────────────────────────┘
7.2 左侧栏 (LeftArea) 详细设计
┌────────────────────────────┐
│ 👤 用户名称 [◀] │ ← 第一行:头像 + 名称 + 折叠按钮
├────────────────────────────┤
│ 🔍 搜索笔记... [+] │ ← 第二行:搜索框 + 新建按钮
├────────────────────────────┤
│ 📝 笔记 │ ← 第三行:Menu Item - 跳转笔记列表
├────────────────────────────┤
│ 🏷️ 标签 │ ← 第四行:Menu Item - 跳转标签页
├────────────────────────────┤
│ 📚 笔记本 │ ← 第五行:Menu Label
│ ├─ 📂 笔记本A │ ← PanelMenu:可展开/折叠
│ │ ├─ 📄 笔记1 │
│ │ ├─ 📄 笔记2 │
│ │ └─ 📂 子笔记本C │
│ │ └─ 📄 笔记3 │
│ └─ 📂 笔记本B │
│ └─ 📄 笔记4 │
│ │
│ │
│ │
├────────────────────────────┤
│ ⚙️ 设置 🗑️ 废纸篓│ ← 底部固定:设置 + 废纸篓
└────────────────────────────┘
交互细节
- 折叠按钮:点击后左侧栏收起为 0px 或 48px(仅显示图标),右侧区域全屏
- 搜索框:实时搜索(debounce 300ms),结果在右侧区域展示
- 新建按钮:点击后在当前选中的笔记本下创建新笔记(若未选中笔记本则创建在根目录)
- 笔记本 PanelMenu:点击笔记本切换展开/折叠;点击笔记在右侧打开详情
- 拖拽调整宽度:左侧栏右边框可拖拽,范围 240px ~ 480px
7.3 右侧区域 (RightArea) 路由设计
| 路由 | 视图 | 说明 |
|---|---|---|
/ |
首页 / 欢迎页 | 首次打开或未选中笔记时展示 |
/notes |
笔记列表 | 工作目录下所有笔记,支持列表/卡片视图切换 |
/notes?notebook=<path> |
笔记列表(过滤) | 指定笔记本下的笔记 |
/tags |
标签列表 | 展示所有标签,点击标签查看关联笔记 |
/tags/:tagId |
标签详情 | 某个标签下的所有笔记,支持列表/卡片视图 |
/note/:id |
笔记详情 | 笔记编辑/预览/双栏视图 |
/search?q=xxx |
搜索结果 | 全局搜索结果 |
/settings |
设置页面 | 应用设置 |
/trash |
废纸篓 | 已删除笔记列表 |
7.4 笔记详情页 (NoteDetail) 详细设计
┌──────────────────────────────────────────────────────────┐
│ 笔记标题(可编辑) │
├──────────────────────────────────────────────────────────┤
│ [编辑] [预览] [双栏] │ 🏷️ 标签 │ ⋯ 更多操作 │
├──────────────────────────────────────────────────────────┤
│ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 笔记内容区域 │ │
│ │ │ │
│ │ 支持: │ │
│ │ - Markdown 实时渲染 │ │
│ │ - 块级编辑(Typora 风格) │ │
│ │ - 拖拽移动块 │ │
│ │ - 图片粘贴 / 拖入 │ │
│ │ - 代码块高亮 │ │
│ │ - 表格编辑 │ │
│ │ - 数学公式 (KaTeX) │ │
│ │ - Mermaid 图表 │ │
│ │ │ │
│ └────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────┘
三种视图模式
| 模式 | 说明 |
|---|---|
| 编辑模式 | 纯 Markdown 编辑(类似 VS Code) |
| 预览模式 | 纯渲染预览(只读) |
| 双栏模式 | 左侧 Markdown 源码,右侧实时预览(类似 Typora 的源码模式) |
注:后期实现块编辑后,默认进入"所见即所得"模式,编辑/预览/双栏作为切换选项
八、组件架构
8.1 核心组件树
App.vue
├── MainLayout
│ ├── LeftArea
│ │ ├── UserProfile # 头像 + 用户名
│ │ ├── CollapseButton # 折叠按钮
│ │ ├── SearchInput # 搜索框
│ │ ├── AddNoteButton # 新建笔记按钮
│ │ ├── MenuItem (笔记) # 路由跳转
│ │ ├── MenuItem (标签) # 路由跳转
│ │ ├── NotebookTree # 笔记本树(PanelMenu)
│ │ │ ├── NotebookNode # 笔记本节点(可展开)
│ │ │ │ ├── NoteItem # 笔记条目
│ │ │ │ └── NotebookNode # 递归嵌套
│ │ ├── SettingsLink # 设置入口
│ │ └── TrashLink # 废纸篓入口
│ │
│ ├── ResizeHandle # 拖拽调整宽度
│ │
│ └── RightArea (RouterView)
│ ├── WelcomeView # 欢迎页
│ ├── NotesListView # 笔记列表
│ │ ├── NoteCard # 卡片模式
│ │ └── NoteListItem # 列表模式
│ ├── TagsView # 标签列表
│ ├── TagDetailView # 标签详情
│ ├── NoteDetailView # 笔记详情
│ │ ├── NoteTitle # 标题编辑
│ │ ├── NoteToolbar # 操作栏
│ │ ├── NoteEditor # 编辑器
│ │ │ ├── TiptapEditor # Tiptap 编辑器实例
│ │ │ └── BlockMenu # 块操作菜单
│ │ └── NotePreview # 渲染预览
│ ├── SearchView # 搜索结果
│ ├── SettingsView # 设置页面
│ └── TrashView # 废纸篓
│
└── StatusBar # 底部状态栏(可选)
8.2 Electron 进程架构
┌─────────────────────────────────────────────────────────┐
│ Main Process │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ FileManager│ │ Database │ │ SyncEngine│ │
│ │ (chokidar)│ │(sqlite3) │ │ (各后端) │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │ │ │ │
│ └──────────────┼──────────────┘ │
│ │ IPC Bridge │
├───────────────────────┼─────────────────────────────────┤
│ │ │
│ Renderer Process (Vue 3) │
│ │ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ fileStore │ │ noteStore │ │ syncStore │ │
│ │ (Pinia) │ │ (Pinia) │ │ (Pinia) │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
8.3 IPC 通信接口
// preload.js 暴露到渲染进程的 API
// 通过 contextBridge 暴露,渲染进程通过 window.tunjiAPI 调用
// 文件操作
window.tunjiAPI.file.read(filePath) // → string
window.tunjiAPI.file.write(filePath, content) // → void
window.tunjiAPI.file.delete(filePath) // → void
window.tunjiAPI.file.rename(oldPath, newPath) // → void
window.tunjiAPI.file.list(dirPath) // → FileEntry[]
window.tunjiAPI.file.watch(callback) // 监听文件变更事件
// 数据库操作
window.tunjiAPI.db.searchNotes(query) // → NoteSearchResult[]
window.tunjiAPI.db.getNotes(filter) // → NoteMeta[]
window.tunjiAPI.db.getNote(id) // → NoteMeta
window.tunjiAPI.db.updateNoteMeta(id, meta) // → void
window.tunjiAPI.db.getTags() // → Tag[]
window.tunjiAPI.db.getNotebooks() // → Notebook[]
window.tunjiAPI.db.rebuildIndex() // → void
// 同步操作
window.tunjiAPI.sync.getStatus() // → SyncStatus
window.tunjiAPI.sync.start() // → void
window.tunjiAPI.sync.configure(config) // → void
window.tunjiAPI.sync.resolveConflict(noteId, resolution) // resolution: 'local'|'remote'|'merge'
// 工作目录
window.tunjiAPI.workspace.select() // → string|null 打开目录选择对话框
window.tunjiAPI.workspace.get() // → string|null 获取当前工作目录
window.tunjiAPI.workspace.set(path) // → void 设置工作目录
// 系统
window.tunjiAPI.system.getAppVersion() // → string
window.tunjiAPI.system.getPlatform() // → string
window.tunjiAPI.system.openExternal(url) // → void
window.tunjiAPI.system.showItemInFolder(path) // → void
九、Pinia Store 设计
所有 Store 使用 Vue 3 Composition API 风格(setup 函数),纯 JavaScript。
// stores/workspace.js - 工作目录状态
// state: { path, isReady }
// actions: selectWorkspace(), initWorkspace(path)
// stores/notes.js - 笔记状态
// state: { notes, currentNote, viewMode, filter }
// actions: fetchNotes(filter), openNote(id), createNote(notebookPath),
// deleteNote(id), saveNote(id, content)
// stores/notebooks.js - 笔记本状态
// state: { notebooks, expandedPaths }
// actions: fetchNotebooks(), toggleExpand(path), createNotebook(name, parentPath),
// renameNotebook(path, newName), deleteNotebook(path)
// stores/tags.js - 标签状态
// state: { tags }
// actions: fetchTags(), createTag(name, color), deleteTag(id),
// addTagToNote(noteId, tagId), removeTagFromNote(noteId, tagId)
// stores/sync.js - 同步状态
// state: { status, config, conflicts }
// actions: startSync(), configureSync(config), resolveConflict(noteId, resolution)
// stores/ui.js - UI 状态
// state: { leftCollapsed, leftWidth, theme }
// actions: toggleLeft(), setLeftWidth(width), setTheme(theme)
十、项目结构
tunji/
├── docs/
│ └── DESIGN.md # 本文档
│
├── electron/
│ ├── main.js # Electron 主进程入口
│ ├── preload.js # 预加载脚本(IPC 桥接)
│ ├── ipc/
│ │ ├── file.ipc.js # 文件操作 IPC 处理
│ │ ├── db.ipc.js # 数据库 IPC 处理
│ │ ├── sync.ipc.js # 同步 IPC 处理
│ │ └── workspace.ipc.js # 工作目录 IPC 处理
│ ├── services/
│ │ ├── file-manager.js # 文件管理服务
│ │ ├── database.js # SQLite 数据库服务
│ │ ├── index-builder.js # 索引构建服务
│ │ ├── version-manager.js # 版本管理服务
│ │ └── sync/
│ │ ├── sync-manager.js # 同步管理器
│ │ ├── git-sync.js # Git 同步实现
│ │ ├── webdav-sync.js # WebDAV 同步实现
│ │ └── s3-sync.js # S3 同步实现
│ └── utils/
│ ├── markdown-parser.js # Front matter 解析
│ └── file-hash.js # 文件哈希计算
│
├── src/
│ ├── App.vue # 根组件
│ ├── main.js # Vue 入口
│ ├── router/
│ │ └── index.js # 路由配置
│ ├── stores/ # Pinia 状态管理
│ │ ├── workspace.js
│ │ ├── notes.js
│ │ ├── notebooks.js
│ │ ├── tags.js
│ │ ├── sync.js
│ │ └── ui.js
│ ├── layouts/
│ │ └── MainLayout.vue # 主布局
│ ├── components/
│ │ ├── left-area/
│ │ │ ├── LeftArea.vue
│ │ │ ├── UserProfile.vue
│ │ │ ├── SearchInput.vue
│ │ │ ├── AddNoteButton.vue
│ │ │ ├── MenuItem.vue
│ │ │ ├── NotebookTree.vue
│ │ │ ├── NotebookNode.vue
│ │ │ └── NoteItem.vue
│ │ ├── right-area/
│ │ │ ├── NotesListView.vue
│ │ │ ├── NoteCard.vue
│ │ │ ├── NoteListItem.vue
│ │ │ ├── TagsView.vue
│ │ │ ├── TagDetailView.vue
│ │ │ ├── NoteDetailView.vue
│ │ │ ├── SearchView.vue
│ │ │ ├── SettingsView.vue
│ │ │ └── TrashView.vue
│ │ ├── editor/
│ │ │ ├── NoteEditor.vue
│ │ │ ├── NotePreview.vue
│ │ │ ├── TiptapEditor.vue
│ │ │ └── BlockMenu.vue
│ │ └── common/
│ │ ├── ResizeHandle.vue
│ │ ├── ViewToggle.vue # 列表/卡片视图切换
│ │ └── EmptyState.vue
│ ├── composables/
│ │ ├── useDragResize.js # 拖拽调整宽度
│ │ ├── useSearch.js # 搜索逻辑
│ │ ├── useAutoSave.js # 自动保存
│ │ └── useVersionHistory.js # 版本历史
│ └── styles/
│ ├── theme.css # 主题变量定义
│ └── tailwind.css # Tailwind 入口
│
├── index.html # HTML 入口
├── package.json
├── vite.config.js
├── tailwind.config.js
├── electron-builder.json # Electron 打包配置
└── README.md
十一、开发路线图
Phase 1:基础骨架(MVP)
- Electron + Vue 3 + Vite 项目初始化
- Tailwind CSS + PrimeVue (Unstyled) 集成
- 主题色系统实现
- 工作目录选择功能
- 两栏布局 + 拖拽调整宽度
- 左侧栏基础组件
- 路由基础配置
Phase 2:核心笔记功能
- Markdown 文件读写
- Front matter 解析
- 笔记创建 / 编辑 / 删除
- 笔记本(文件夹)管理
- Tiptap 编辑器集成
- Markdown 实时预览
Phase 3:索引与搜索
- SQLite 数据库集成
- 笔记索引构建
- 全文搜索
- 标签系统
Phase 4:同步功能
- 同步抽象层
- Git 同步
- WebDAV 同步
- S3 同步
- 冲突解决
Phase 5:高级编辑
- Typora 风格块编辑
- 拖拽排序
- 图片管理
- 代码高亮
- 数学公式
- Mermaid 图表
Phase 6:完善与优化
- 版本历史 / 回退
- 废纸篓
- 暗色模式
- 插件系统
- 性能优化
- 打包发布