# 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/ # 废纸篓(已删除笔记的临时存放) │ │ ├── .md │ │ └── ... │ ├── versions/ # 本地版本快照(Git-like 内容寻址) │ │ ├── # 文件内容的 SHA-256 哈希 │ │ ├── │ │ └── ... │ └── 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 存储元数据: ```markdown --- 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/架构图.png) ├── 笔记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` | #### 引用方式 笔记中引用附件使用**相对于自身**的路径: ```markdown ![架构图](笔记1.assets/架构图.png) [需求文档](笔记1.assets/需求文档.pdf) ``` #### 移动笔记时的附件处理 这是本设计的核心优势——**移动笔记零成本**: ``` 移动前: 笔记本A/ ├── 会议纪要.md └── 会议纪要.assets/ └── 白板照片.jpg 移动后(拖拽到笔记本B): 笔记本B/ ├── 会议纪要.md # 引用路径不变:![照片](会议纪要.assets/白板照片.jpg) └── 会议纪要.assets/ └── 白板照片.jpg 笔记本A/ └── (干净,无残留) ``` **只需要移动两个东西**: 1. `会议纪要.md` 2. `会议纪要.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 设计原则 1. **数据库不同步**:`index.sqlite` 纯本地,同步后在本地重建 2. **版本快照不同步**:`.tunji/versions/` 纯本地 3. **配置可选同步**:`.tunji/config.json` 可选是否同步 4. **只同步用户内容**:`.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 同步 ```javascript // 策略:自动 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 同步(坚果云等) ```javascript // config 示例: { type: 'webdav', serverUrl: 'https://dav.jianguoyun.com/dav/', username: 'user@example.com', password: 'app-specific-password', // 应用专用密码 remotePath: '/tunji-notes/', // 远程目录路径 syncInterval: 300 // 自动同步间隔(秒) } ``` #### S3 同步 ```javascript // 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 为什么需要本地数据库 1. **全文搜索**:纯文件遍历搜索太慢,需要倒排索引 2. **元数据查询**:按标签、创建时间、更新时间等维度快速筛选 3. **排序和分页**:大量笔记的列表展示需要高效查询 4. **跨笔记本搜索**:模糊搜索需要全局索引 ### 5.2 数据库选型 **better-sqlite3**(Electron 主进程) - 纯本地,无需网络 - 同步到工作目录 `.tunji/index.sqlite` - **不同步到远程** ### 5.3 数据库 Schema ```sql -- 笔记索引表(从 .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/ 读取内容 → 预览差异(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=` | 笔记列表(过滤) | 指定笔记本下的笔记 | | `/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 通信接口 ```javascript // 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。 ```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:完善与优化 - [ ] 版本历史 / 回退 - [ ] 废纸篓 - [ ] 暗色模式 - [ ] 插件系统 - [ ] 性能优化 - [ ] 打包发布