Files
tunji2/docs/DESIGN.md
2026-06-01 17:28:00 +08:00

914 lines
35 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 存储元数据:
```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/<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 通信接口
```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完善与优化
- [ ] 版本历史 / 回退
- [ ] 废纸篓
- [ ] 暗色模式
- [ ] 插件系统
- [ ] 性能优化
- [ ] 打包发布