docs: 更新 README.md 和 DESIGN.md,删除 REFACTOR-PLAN.md

- README.md: 更新版本号 v0.1.0,添加 CodeMirror 6 说明,添加关于/Gitee按钮说明
- DESIGN.md: 重写为当前架构,移除重构计划相关内容,精简为实际代码结构
- 删除 REFACTOR-PLAN.md(重构已完成)
This commit is contained in:
thzxx
2026-05-28 15:53:14 +08:00
parent 03a71bdf94
commit 4a1c06320f
3 changed files with 144 additions and 1234 deletions
+101 -287
View File
@@ -1,44 +1,44 @@
# MarkLite v2.0 — 架构设计文档 # MarkLite v0.1.0 — 架构设计文档
## 1. 项目概述 ## 1. 项目概述
MarkLite 是一款轻量级的 Windows 本地 Markdown 编辑器桌面应用程序。基于 Electron + React + TypeScript 构建,采用 Zustand 状态管理、IndexedDB 持久化、unified/rehype Markdown 渲染管线,提供类型安全、模块化、可测试的现代化架构 MarkLite 是一款轻量级的 Windows 本地 Markdown 编辑器桌面应用程序。基于 Electron + React + TypeScript 构建,采用 CodeMirror 6 编辑器、Zustand 状态管理、IndexedDB 持久化、unified/rehype Markdown 渲染管线。
### 1.1 核心原则 ### 1.1 核心原则
1. **类型安全** — 全量 TypeScript,所有 IPC 通信、状态、接口均有类型定义 1. **类型安全** — 全量 TypeScript,所有 IPC 通信、状态、接口均有类型定义
2. **模块化**51 个源文件按职责分层:主进程 / 预加载 / 渲染进程(组件 / stores / hooks / lib / db / types 2. **模块化** — 源文件按职责分层:主进程 / 预加载 / 渲染进程(组件 / stores / hooks / lib / db / types
3. **功能 100% 兼容** — 重构不丢失任何现有功能 3. **安全隔离** — contextIsolation + nodeIntegration:false + CSP + rehype-sanitize
4. **可测试性** — 业务逻辑(lib/)与 UIcomponents/)解耦stores 和 lib 可独立测试 4. **可测试性** — 业务逻辑(lib/)与 UIcomponents/)解耦
## 2. 技术架构 ## 2. 技术架构
### 2.1 技术栈 ### 2.1 技术栈
| 组件 | 技术选型 | 版本 | 说明 | | 组件 | 技术 | 版本 | 说明 |
|------|----------|------|------| |------|------|------|------|
| 桌面框架 | Electron | v28 | 跨平台桌面应用框架 | | 桌面框架 | Electron | v28 | 跨平台桌面应用框架 |
| 前端框架 | React | v18 | 函数组件 + Hooks | | 前端框架 | React | v18 | 函数组件 + Hooks |
| 类型系统 | TypeScript | v5.6 | 全量类型安全 | | 类型系统 | TypeScript | v5.6 | 全量类型安全 |
| 状态管理 | Zustand | v5 | 轻量级状态管理,selector 优化 | | 编辑器 | CodeMirror | v6 | 现代化代码编辑器 |
| 状态管理 | Zustand | v5 | 轻量级状态管理 |
| 持久化 | Dexie.js (IndexedDB) | v4 | 标签页状态 / 用户设置 / 最近文件 | | 持久化 | Dexie.js (IndexedDB) | v4 | 标签页状态 / 用户设置 / 最近文件 |
| Markdown 解析 | unified / remark / rehype | v11 | 插件化渲染管线 | | Markdown 解析 | unified / remark / rehype | v11 | 插件化渲染管线 |
| 代码高亮 | rehype-highlight | v7 | 基于 highlight.js 的语法高亮 | | 代码高亮 | rehype-highlight | v7 | 基于 highlight.js |
| 构建工具 | electron-vite | v3 | Electron + Vite 集成HMR 热更新 | | 构建工具 | electron-vite | v3 | Electron + ViteHMR 热更新 |
| 打包工具 | electron-builder | v25 | 生成 Windows NSIS 安装包 | | 打包工具 | electron-builder | v25 | Windows NSIS 安装包 |
| 样式 | CSS Variables + CSS Modules | — | 主题驱动,样式隔离 | | 样式 | CSS Variables | — | 主题驱动,亮色/暗色 |
### 2.2 进程架构 ### 2.2 进程架构
``` ```
┌──────────────────────────────────────────────────────────────┐ ┌──────────────────────────────────────────────────────────────┐
│ Main Process (src/main/) 6 文件 │ │ Main Process (src/main/) 5 文件 │
│ index.ts 入口:窗口创建、app 生命周期、单实例锁 │ │ index.ts 入口:窗口创建、app 生命周期、单实例锁 │
│ ipc-handlers.ts 所有 ipcMain.handle 注册 │ │ ipc-handlers.ts 所有 ipcMain.handle 注册 │
│ file-system.ts 文件读写、目录树构建、BOM 剥离 │ │ file-system.ts 文件读写、目录树构建、BOM 剥离 │
│ file-watcher.ts fs.watch 封装(单文件 + 目录监听) │ │ file-watcher.ts fs.watch 封装(单文件 + 目录监听) │
│ window-manager.ts 窗口创建、关闭拦截、单实例锁 │ │ window-manager.ts 窗口创建、关闭拦截、单实例锁 │
│ ipc-channels.ts IPC 通道名常量 │
└───────────────────────┬──────────────────────────────────────┘ └───────────────────────┬──────────────────────────────────────┘
│ contextBridge (安全隔离) │ contextBridge (安全隔离)
┌───────────────────────▼──────────────────────────────────────┐ ┌───────────────────────▼──────────────────────────────────────┐
@@ -48,40 +48,25 @@ MarkLite 是一款轻量级的 Windows 本地 Markdown 编辑器桌面应用程
└───────────────────────┬──────────────────────────────────────┘ └───────────────────────┬──────────────────────────────────────┘
┌───────────────────────▼──────────────────────────────────────┐ ┌───────────────────────▼──────────────────────────────────────┐
│ Renderer Process (src/renderer/) 42 文件 React 18 │ │ Renderer Process (src/renderer/) React 18
│ │ │ │
┌─ components/ (11) ──────────────────────────────────────┐ │ components/ (11) Toolbar · TabBar · Editor · Preview
│ Toolbar · TabBar · Editor · Preview · Sidebar Sidebar · StatusBar · WelcomeScreen
SearchBar · StatusBar · WelcomeScreen · Toast Toast · ModifiedBanner · DropOverlay
ModifiedBanner · DropOverlay │
└──────────────────────────────────────────────────────────┘ stores/ (3) tabStore · editorStore · sidebarStore
┌─ stores/ (4) ───────────────────────────────────────────┐ hooks/ (6) useTheme · useSettings · useKeyboard
tabStore · editorStore · sidebarStore · searchStore │ useDragDrop · useFileWatch · useUnsaved
└──────────────────────────────────────────────────────────┘ lib/ (3) markdown · fileUtils · constants
┌─ hooks/ (6) ────────────────────────────────────────────┐ db/ (4) schema · tab · settings · recentFiles
│ useTheme · useSettings · useKeyboard · useDragDrop types/ (6) tab · file · settings · ipc · index
│ useFileWatch · useUnsavedWarning styles/ (3) variables · global · markdown-body
│ └──────────────────────────────────────────────────────────┘ │
│ ┌─ lib/ (5) ──────────────────────────────────────────────┐ │
│ │ markdown · scrollSync · searchEngine · fileUtils │ │
│ │ constants │ │
│ └──────────────────────────────────────────────────────────┘ │
│ ┌─ db/ (4) ───────────────────────────────────────────────┐ │
│ │ schema · tabRepository · settingsRepository │ │
│ │ recentFilesRepository │ │
│ └──────────────────────────────────────────────────────────┘ │
│ ┌─ types/ (6) ────────────────────────────────────────────┐ │
│ │ tab · file · settings · search · ipc · index │ │
│ └──────────────────────────────────────────────────────────┘ │
│ ┌─ styles/ (3) ───────────────────────────────────────────┐ │
│ │ variables.css · global.css · markdown-body.css │ │
│ └──────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘ └──────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐ ┌──────────────────────────────────────────────────────────────┐
│ Shared (src/shared/) 2 文件 │ │ Shared (src/shared/) 2 文件 │
│ ipc-channels.ts IPC 通道名常量(主进程/渲染进程共用) │ ipc-channels.ts IPC 通道名常量
│ types.ts 共享类型定义FileNode, 结果类型等) │ types.ts 共享类型定义
└──────────────────────────────────────────────────────────────┘ └──────────────────────────────────────────────────────────────┘
``` ```
@@ -92,9 +77,10 @@ MarkLite 是一款轻量级的 Windows 本地 Markdown 编辑器桌面应用程
| Electron | `contextIsolation: true` | 渲染进程与主进程隔离 | | Electron | `contextIsolation: true` | 渲染进程与主进程隔离 |
| Electron | `nodeIntegration: false` | 渲染进程无法访问 Node.js API | | Electron | `nodeIntegration: false` | 渲染进程无法访问 Node.js API |
| IPC | `contextBridge.exposeInMainWorld` | 仅暴露 22 个类型安全方法/事件 | | IPC | `contextBridge.exposeInMainWorld` | 仅暴露 22 个类型安全方法/事件 |
| CSP | `default-src 'self'; script-src 'self'` | 阻断内联脚本、外部资源加载 | | CSP | `default-src 'self'; script-src 'self'` | 阻断内联脚本、外部资源 |
| HTML | `rehype-sanitize` | 渲染 Markdown 时过滤危险标签/属性/JS 协议 | | HTML | `rehype-sanitize` | 渲染 Markdown 时过滤危险标签/属性 |
| 链接 | 协议白名单 | 仅允许 `http:` / `https:` / `#` 锚点 | | 链接 | 协议白名单 | 仅允许 `http:` / `https:` / `#` 锚点 |
| 路径 | `validatePath()` | 防止路径遍历攻击 |
## 3. 状态管理架构 ## 3. 状态管理架构
@@ -108,16 +94,16 @@ MarkLite 是一款轻量级的 Windows 本地 Markdown 编辑器桌面应用程
│ │ │ │ (CM6) │ │ │ │ │ │ (CM6) │ │
├─────────┴──────────┴──────────┴──────────┴──────────────┤ ├─────────┴──────────┴──────────┴──────────┴──────────────┤
│ Zustand Stores │ │ Zustand Stores │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ │ tabStore │ │editorStore│ │sidebarStore│ │searchStore│ │ │ tabStore │ │editorStore│ │sidebarStore│
│ │ - tabs │ │- viewMode│ │- tree │ │- matches │ │ │ - tabs │ │- viewMode│ │- tree │
│ │- activeId│ │- darkMode│ │- expanded │ │- index │ │ │- activeId│ │- darkMode│ │- expanded │
│ │ - mru │ │- viewMode│ │- rootPath │ │- options │ │ - mru │ │ │ │- rootPath │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └──────────┘ │ └────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │ │ │ │ │ │ │ │
│ ┌────▼────────────▼────────────▼────────────────────┐ │ │ ┌────▼────────────▼────────────▼────────────────────┐ │
│ │ IndexedDB (Dexie.js) │ │ │ │ IndexedDB (Dexie.js) │ │
│ │ tabSnapshots │ settings │ recentFiles │ │ │ │ tabSnapshots │ settings │ recentFiles │ activeTab │ │
│ └───────────────────────────────────────────────────┘ │ │ └───────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────┘
``` ```
@@ -132,11 +118,13 @@ interface TabState {
createTab(filePath?, content?) // 创建标签(同文件不重复打开) createTab(filePath?, content?) // 创建标签(同文件不重复打开)
closeTab(tabId) // 关闭标签(自动切换到相邻标签) closeTab(tabId) // 关闭标签(自动切换到相邻标签)
switchToTab(tabId) // 切换标签(保存当前状态 → MRU 记录 → 加载目标) switchToTab(tabId) // 切换标签
updateTabContent(tabId, content) // 更新内容(标记修改) updateTabContent(tabId, content) // 更新内容(标记修改)
setModified(tabId, modified) // 设置修改状态 setModified(tabId, modified) // 设置修改状态
getActiveTab() // 获取当前标签 getActiveTab() // 获取当前标签
updateTabScroll(tabId, scroll) // 更新滚动/光标位置 updateTabScroll(tabId, scroll) // 更新滚动/光标位置
saveToDB() // 保存到 IndexedDB
loadFromDB() // 从 IndexedDB 加载
} }
``` ```
@@ -145,6 +133,7 @@ interface TabState {
```typescript ```typescript
interface EditorState { interface EditorState {
viewMode: 'editor' | 'preview' // 视图模式 viewMode: 'editor' | 'preview' // 视图模式
darkMode: boolean // 暗色主题
} }
``` ```
@@ -160,34 +149,20 @@ interface SidebarState {
} }
``` ```
### 3.5 searchStore — 搜索状态
```typescript
interface SearchState {
isVisible: boolean // 搜索栏是否可见
showReplace: boolean // 替换行是否展开
searchText: string // 搜索文本
replaceText: string // 替换文本
matches: SearchMatch[] // 所有匹配项
currentIndex: number // 当前匹配索引
options: SearchOptions // { caseSensitive, useRegex }
}
```
## 4. 数据持久化 — IndexedDB ## 4. 数据持久化 — IndexedDB
通过 Dexie.js 封装 IndexedDB,替代 localStorage 通过 Dexie.js 封装 IndexedDB
### 4.1 数据库 Schema ### 4.1 数据库 Schema
```typescript ```typescript
// db/schema.ts
const db = new Dexie('MarkLite') const db = new Dexie('MarkLite')
db.version(1).stores({ db.version(1).stores({
tabSnapshots: 'id, filePath, updatedAt', // 标签页快照 tabSnapshots: 'id, filePath, updatedAt', // 标签页快照
settings: 'id', // 用户设置 settings: 'id', // 用户设置
recentFiles: '++id, filePath, lastOpened' // 最近打开文件 recentFiles: '++id, filePath, lastOpened', // 最近打开文件
activeTab: 'id' // 当前活动标签
}) })
``` ```
@@ -195,19 +170,10 @@ db.version(1).stores({
| Store | 字段 | 说明 | | Store | 字段 | 说明 |
|-------|------|------| |-------|------|------|
| `tabSnapshots` | id, filePath, content, scrollTop, scrollLeft, selectionStart, selectionEnd, previewScrollTop, isModified, updatedAt | 标签页状态快照,关闭时保存,启动时恢复 | | `tabSnapshots` | id, filePath, content, scrollTop, selectionStart, selectionEnd, isModified, updatedAt | 标签页状态快照 |
| `settings` | id(固定'default'), darkMode, viewMode, sidebarCollapsed, sidebarWidth | 用户偏好设置 | | `settings` | id, darkMode, viewMode, sidebarCollapsed, sidebarWidth | 用户偏好设置 |
| `recentFiles` | ++id, filePath, lastOpened | 最近打开文件列表 | | `recentFiles` | ++id, filePath, lastOpened | 最近打开文件列表 |
| `activeTab` | id, activeTabId | 当前活动标签 ID |
### 4.3 IndexedDB 优势(vs localStorage
| 维度 | localStorage | IndexedDB |
|------|-------------|-----------|
| 容量 | ~5-10MB | 数百 MB |
| API | 同步 | 异步,不阻塞 UI |
| 索引 | 无 | 支持索引查询 |
| 大文件 | 无法缓存 20MB 文件 | 可缓存任意大小文件 |
| 标签恢复 | 关闭后丢失 | 可持久化恢复所有标签 |
## 5. IPC 通信设计 ## 5. IPC 通信设计
@@ -216,18 +182,18 @@ db.version(1).stores({
| 通道 | 参数 | 返回值 | 说明 | | 通道 | 参数 | 返回值 | 说明 |
|------|------|--------|------| |------|------|--------|------|
| `dialog:openFile` | 无 | `OpenFileResponse` | 打开文件对话框 | | `dialog:openFile` | 无 | `OpenFileResponse` | 打开文件对话框 |
| `file:read` | `filePath: string` | `ReadFileResult` | 读取文件内容 | | `file:read` | `filePath` | `ReadFileResult` | 读取文件内容 |
| `file:save` | `{ filePath, content }` | `SaveFileResult` | 保存文件 | | `file:save` | `{ filePath, content }` | `SaveFileResult` | 保存文件 |
| `file:saveAs` | `{ content }` | `SaveFileResult` | 另存为 | | `file:saveAs` | `{ content }` | `SaveFileResult` | 另存为 |
| `file:getCurrentPath` | 无 | `string \| null` | 获取当前文件路径 | | `file:getCurrentPath` | 无 | `string \| null` | 获取当前文件路径 |
| `file:stats` | `filePath: string` | `FileStatsResult` | 获取文件元信息 | | `file:stats` | `filePath` | `FileStatsResult` | 获取文件元信息 |
| `file:reload` | 无 | `ReloadFileResult` | 重新加载当前文件 | | `file:reload` | 无 | `ReloadFileResult` | 重新加载当前文件 |
| `tab:switched` | `filePath: string \| null` | `void` | 通知主进程切换活动文件 | | `tab:switched` | `filePath \| null` | `void` | 通知主进程切换活动文件 |
| `window:forceClose` | 无 | `void` | 强制关闭窗口 | | `window:forceClose` | 无 | `void` | 强制关闭窗口 |
| `window:cancelClose` | 无 | `void` | 取消关闭 | | `window:cancelClose` | 无 | `void` | 取消关闭 |
| `dir:readTree` | `dirPath: string` | `ReadDirTreeResult` | 递归读取目录树 | | `dir:readTree` | `dirPath` | `ReadDirTreeResult` | 递归读取目录树 |
| `dir:openDialog` | 无 | `string \| null` | 打开文件夹选择对话框 | | `dir:openDialog` | 无 | `string \| null` | 打开文件夹选择对话框 |
| `dir:watch` | `dirPath: string` | `void` | 监听目录变化 | | `dir:watch` | `dirPath` | `void` | 监听目录变化 |
| `dir:unwatch` | 无 | `void` | 停止监听目录变化 | | `dir:unwatch` | 无 | `void` | 停止监听目录变化 |
### 5.2 主进程 → 渲染进程(send) ### 5.2 主进程 → 渲染进程(send)
@@ -235,148 +201,66 @@ db.version(1).stores({
| 通道 | 数据 | 说明 | | 通道 | 数据 | 说明 |
|------|------|------| |------|------|------|
| `file:openInTab` | `{ filePath, content }` | 在新标签中打开文件 | | `file:openInTab` | `{ filePath, content }` | 在新标签中打开文件 |
| `file:externallyModified` | `filePath: string` | 文件被外部修改 | | `file:externallyModified` | `filePath` | 文件被外部修改 |
| `window:confirmClose` | 无 | 请求确认关闭 | | `window:confirmClose` | 无 | 请求确认关闭 |
| `sidebar:dirChanged` | 无 | 目录结构变化,通知刷新树 | | `sidebar:dirChanged` | 无 | 目录结构变化 |
### 5.3 类型安全 ## 6. 编辑器架构 — CodeMirror 6
所有 IPC 通道通过 `src/shared/types.ts``src/renderer/types/ipc.ts` 定义类型: ### 6.1 功能特性
```typescript - Markdown 语法高亮
// src/renderer/types/ipc.ts - 行号显示
export interface ElectronAPI { - 代码折叠 (foldGutter)
openFile: () => Promise<OpenFileResponse> - 括号匹配 (bracketMatching)
readFile: (filePath: string) => Promise<ReadFileResult> - 搜索替换 (search) — 中文本地化
saveFile: (data: SaveFilePayload) => Promise<SaveFileResult> - 历史记录 (history) — 支持 undo/redo
// ... 共 22 个方法/事件 - Tab 缩进 (indentWithTab)
} - 自动换行 (lineWrapping)
``` - 暗色主题 (oneDark)
## 6. 标签页数据模型 ### 6.2 滚动与选区持久化
```typescript 切换标签时自动保存/恢复:
// src/renderer/types/tab.ts - 滚动位置 (`scrollTop`)
export interface Tab { - 光标选区 (`selectionStart`, `selectionEnd`)
id: string // 唯一标识(自增计数器)
filePath: string | null // 文件路径(未命名标签为 null)
content: string // 编辑器内容
isModified: boolean // 是否已修改
scrollTop: number // 编辑器滚动位置
scrollLeft: number
selectionStart: number // 光标选区
selectionEnd: number
previewScrollTop: number // 预览面板滚动位置
}
```
切换标签时自动保存当前状态、恢复目标状态。使用 `execCommand('insertText')` 替换内容以保留 undo/redo 历史。
## 7. Markdown 渲染管线 ## 7. Markdown 渲染管线
### 7.1 渲染流程
``` ```
Markdown 文本 Markdown 文本
remark-parse 解析为 MDASTMarkdown AST remark-parse 解析为 MDAST
remark-gfm 扩展 GFM 语法(表格、任务列表、删除线) remark-gfm 扩展 GFM 语法
remark-rehype 转换为 HASTHTML AST remark-rehype 转换为 HAST
rehype-raw 解析内联 HTML rehype-raw 解析内联 HTML
rehype-sanitize 安全过滤(移除 script/iframe/on* 事件/javascript: 协议) rehype-sanitize 安全过滤
rehype-highlight 代码块语法高亮(180+ 语言) rehype-fixImages 相对路径图片转 file:// URL
rehype-stringify 序列化为 HTML 字符串 rehype-highlight 代码语法高亮
dangerouslySetInnerHTML 渲染到 React DOM rehype-stringify 序列化为 HTML
dangerouslySetInnerHTML 渲染到 DOM
``` ```
### 7.2 支持的语法 ## 8. UI 设计
- 标题(h1-h6)、段落、换行 ### 8.1 色彩方案
- **粗体**、*斜体*、~~删除线~~
- 有序/无序列表、任务列表 `- [x]`
- 代码块(围栏式 + 语法高亮,180+ 语言)、行内代码
- 链接、图片(支持相对路径转 file:// URL
- 表格、引用块、水平线
- HTML 内联元素
- GFMGitHub Flavored Markdown
## 8. 滚动同步算法
基于**块级元素 DOM 位置映射**的编辑器-预览滚动同步:
```
1. 识别编辑器中的块级元素起始行
- 标题 (#)、列表 (-/*/+)、引用 (>)、表格 (|)
- 围栏代码块 (```)、水平线 (---)、段落分隔(连续空行)
2. 获取预览面板中每个 DOM 子元素的 offsetTop
3. 将块起始行号映射到预览 DOM 位置
- 块起始行数 : 预览 DOM 元素数 = 线性对应
4. 线性插值填充所有中间行
- 对于非块起始行,找到前后最近的已映射行
- 按行号比例插值计算预览位置
5. 滚动时通过映射表直接查找目标位置
- O(1) 查找,无需遍历
```
## 9. 搜索替换引擎
### 9.1 匹配算法
```
普通文本搜索:
- 大小写不敏感时:haystack.toLowerCase().indexOf(needle)
- 大小写敏感时:haystack.indexOf(needle)
- O(N) 线性扫描
正则表达式搜索:
- new RegExp(text, flags)
- 循环 exec() 收集所有匹配
- 空匹配保护:regex.lastIndex++
```
### 9.2 高亮渲染
```
字符位置 → 像素坐标转换:
1. 构建 lineStarts 缓存(每行首字符的偏移量)
2. 二分查找 O(log N):字符位置 → 行号
3. 列号 = 字符位置 - lineStarts[行号]
4. top = 行号 × 行高 + paddingTop
5. left = 列号 × 字符宽度 + paddingLeft
搜索高亮 overlay
- 绝对定位 div,覆盖在 textarea 上方
- pointer-events: none 不拦截编辑器交互
- 滚动时同步更新 overlay top 偏移
```
### 9.3 替换策略
- **替换当前**:使用 `execCommand('insertText')` 保留 undo 历史
- **全部替换**:从后往前拼接,一次 `execCommand('insertText')` 原子操作,Ctrl+Z 可一次性撤销
## 10. UI 设计
### 10.1 色彩方案
#### 亮色主题 #### 亮色主题
@@ -385,10 +269,7 @@ dangerouslySetInnerHTML 渲染到 React DOM
| 主色调 | `--primary` | `#1a73e8` | | 主色调 | `--primary` | `#1a73e8` |
| 背景色 | `--bg` | `#ffffff` | | 背景色 | `--bg` | `#ffffff` |
| 次级背景 | `--bg-secondary` | `#f8f9fa` | | 次级背景 | `--bg-secondary` | `#f8f9fa` |
| 三级背景 | `--bg-tertiary` | `#f1f3f4` |
| 文字色 | `--text` | `#333333` | | 文字色 | `--text` | `#333333` |
| 次级文字 | `--text-secondary` | `#5f6368` |
| 代码块背景 | `--code-bg` | `#f6f8fa` |
| 边框色 | `--border` | `#e1e4e8` | | 边框色 | `--border` | `#e1e4e8` |
#### 暗色主题 #### 暗色主题
@@ -398,100 +279,45 @@ dangerouslySetInnerHTML 渲染到 React DOM
| 主色调 | `--primary` | `#8ab4f8` | | 主色调 | `--primary` | `#8ab4f8` |
| 背景色 | `--bg` | `#1e1e1e` | | 背景色 | `--bg` | `#1e1e1e` |
| 次级背景 | `--bg-secondary` | `#252526` | | 次级背景 | `--bg-secondary` | `#252526` |
| 三级背景 | `--bg-tertiary` | `#2d2d2d` |
| 文字色 | `--text` | `#d4d4d4` | | 文字色 | `--text` | `#d4d4d4` |
| 次级文字 | `--text-secondary` | `#9e9e9e` |
| 代码块背景 | `--code-bg` | `#2d2d2d` |
| 边框色 | `--border` | `#3e3e3e` | | 边框色 | `--border` | `#3e3e3e` |
### 10.2 字体 ### 8.2 布局
- **UI 字体**: `system-ui, -apple-system, "Segoe UI", Roboto, sans-serif`
- **编辑器字体**: `"Cascadia Code", "Fira Code", "JetBrains Mono", Consolas, monospace`
- **预览字体**: 同 UI 字体
### 10.3 布局
``` ```
┌──────────────────────────────────────────────────────────────────────────┐ ┌──────────────────────────────────────────────────────────────────────────┐
│ MarkLite - filename.md ─ □ ✕ │ │ MarkLite - filename.md ─ □ ✕ │
├──────────────────────────────────────────────────────────────────────────┤ ├──────────────────────────────────────────────────────────────────────────┤
│ 📁 打开 │ 💾 保存 │ ✏️ 编辑 │ 👁 预览 │ 🌙 │ 📁 打开 │ 💾 保存 │ ✏️ 编辑 │ 👁 预览 │ 🌙 🔗
├──────────────────────────────────────────────────────────────────────────┤ ├──────────────────────────────────────────────────────────────────────────┤
│ [file1.md] [file2.md] [未命名] [+] │ │ [file1.md] [file2.md] [未命名] [+] │
├──────────────────────────────────────────────────────────────────────────┤ ├───────────────────────────────────────────────────────────────────────┤
⚠️ 文件已被外部程序修改 [重新加载] [忽略] 资源管理器 │
├──────────┬─────────────────────────┬─────────────────────────────────────┤ │ ▼ project │ 1 # Title │ Title │
资源管理器 📁 src 2───────
▼ project │ 🔍 查找... 2/5 Title 📄 file1│ 3 content... │ content...
📁 src │ [替换... ] [替换][全部]│ ─────── 📄 file2│ │
│ 📄 file1│ │ │ ├──────────┴─────────────────────────────────────────────────────────────┤
📄 file2│ 1 # Title Title │ filename.md UTF-8 Markdown
│ 📁 lib │ 2 │ ─────── │ └────────────────────────────────────────────────────────────────────────┘
│ │ 3 content... │ content... │
│ │ 4 │ │
├──────────┴─────────────────────────┴─────────────────────────────────────┤
│ filename.md │ UTF-8 │ Markdown │ 1.2 KB │ 行 3, 列 1 │
└──────────────────────────────────────────────────────────────────────────┘
``` ```
## 11. 快捷键 ## 9. 构建与发布
| 快捷键 | 功能 | ### 9.1 开发模式
|:-------|:-----|
| `Ctrl + T` | 新建标签页 |
| `Ctrl + W` | 关闭当前标签页 |
| `Ctrl + Tab` | 切换到下一个标签页(MRU 顺序) |
| `Ctrl + Shift + Tab` | 切换到上一个标签页 |
| `Ctrl + O` | 打开文件 |
| `Ctrl + S` | 保存文件 |
| `Ctrl + Shift + S` | 另存为 |
| `Ctrl + 1` | 编辑模式 |
| `Ctrl + 2` | 预览模式 |
| `Ctrl + F` | 搜索 |
| `Ctrl + H` | 搜索并替换 |
| `Enter` / `Shift+Enter` | 下一个 / 上一个匹配 |
| `Alt + C` | 切换区分大小写 |
| `Alt + R` | 切换正则表达式 |
| `Ctrl + Shift + G` | 替换当前匹配 |
| `Ctrl + Shift + H` | 全部替换 |
| `Esc` | 关闭搜索栏 |
## 12. 构建与发布
### 12.1 开发模式
```bash ```bash
npm run dev # electron-vite devHMR 热更新) npm run dev # electron-vite devHMR 热更新)
``` ```
### 12.2 生产构建 ### 9.2 生产构建
```bash ```bash
npm run build # electron-vite build + electron-builder --win npm run build # electron-vite build + electron-builder --win
npm run build:portable # 便携版(免安装) npm run build:portable # 便携版(免安装)
``` ```
### 12.3 构建产物 ### 9.3 打包配置
```
dist/
├── main/index.js 15 kB 主进程(TypeScript 编译)
├── preload/index.js 2 kB 预加载脚本
└── renderer/
├── index.html 入口 HTML
├── assets/index-*.css 18 kB 样式
└── assets/index-*.js 1.4 MB React 应用
```
### 12.4 代码检查
```bash
npm run typecheck # TypeScript 类型检查
npm run lint # ESLint 代码检查
```
### 12.5 打包配置
- 输出格式:NSIS 安装包(.exe) - 输出格式:NSIS 安装包(.exe)
- 目标平台:Windows x64 - 目标平台:Windows x64
@@ -499,26 +325,18 @@ npm run lint # ESLint 代码检查
- 文件关联:`.md` / `.markdown` / `.txt` - 文件关联:`.md` / `.markdown` / `.txt`
- 支持自定义安装目录、桌面/开始菜单快捷方式 - 支持自定义安装目录、桌面/开始菜单快捷方式
详见 [DEVSETUP.md](DEVSETUP.md)。 ## 10. 依赖清单
## 13. 依赖清单
### 运行时依赖 ### 运行时依赖
| 包名 | 版本 | 用途 | | 包名 | 版本 | 用途 |
|------|------|------| |------|------|------|
| react | ^18.3 | UI 框架 | | react / react-dom | ^18.3 | UI 框架 |
| react-dom | ^18.3 | React DOM 渲染 |
| zustand | ^5.0 | 状态管理 | | zustand | ^5.0 | 状态管理 |
| dexie | ^4.0 | IndexedDB 封装 | | dexie | ^4.0 | IndexedDB 封装 |
| nanoid | ^5.0 | 唯一 ID 生成 | | nanoid | ^5.0 | 唯一 ID 生成 |
| unified | ^11.0 | Markdown 处理管线 | | @codemirror/* | ^6.x | 代码编辑器 |
| remark-parse | ^11.0 | Markdown 解析器 | | unified / remark / rehype | ^11.0 | Markdown 渲染管线 |
| remark-gfm | ^4.0 | GFM 扩展 |
| remark-rehype | ^11.1 | MDAST → HAST 转换 |
| rehype-raw | ^7.0 | 内联 HTML 解析 |
| rehype-sanitize | ^6.0 | HTML 安全过滤 |
| rehype-stringify | ^10.0 | HAST → HTML 序列化 |
| rehype-highlight | ^7.0 | 代码语法高亮 | | rehype-highlight | ^7.0 | 代码语法高亮 |
### 开发依赖 ### 开发依赖
@@ -528,9 +346,5 @@ npm run lint # ESLint 代码检查
| electron | ^28.0 | 桌面框架 | | electron | ^28.0 | 桌面框架 |
| electron-builder | ^25.0 | 打包工具 | | electron-builder | ^25.0 | 打包工具 |
| electron-vite | ^3.0 | 构建工具 | | electron-vite | ^3.0 | 构建工具 |
| @vitejs/plugin-react | ^4.3 | Vite React 插件 |
| typescript | ^5.6 | 类型系统 | | typescript | ^5.6 | 类型系统 |
| @types/react | ^18.3 | React 类型 |
| @types/react-dom | ^18.3 | React DOM 类型 |
| eslint | ^9.0 | 代码检查 | | eslint | ^9.0 | 代码检查 |
| @typescript-eslint/eslint-plugin | ^8.0 | TypeScript ESLint 规则 |
+43 -122
View File
@@ -14,7 +14,7 @@
<img src="https://img.shields.io/badge/TypeScript-5.6-3178C6?style=flat-square&logo=typescript" alt="TypeScript"> <img src="https://img.shields.io/badge/TypeScript-5.6-3178C6?style=flat-square&logo=typescript" alt="TypeScript">
<img src="https://img.shields.io/badge/React-18-61DAFB?style=flat-square&logo=react" alt="React"> <img src="https://img.shields.io/badge/React-18-61DAFB?style=flat-square&logo=react" alt="React">
<img src="https://img.shields.io/badge/License-MIT-green?style=flat-square" alt="License"> <img src="https://img.shields.io/badge/License-MIT-green?style=flat-square" alt="License">
<img src="https://img.shields.io/badge/Version-2.0.0-orange?style=flat-square" alt="Version"> <img src="https://img.shields.io/badge/Version-v0.1.0-orange?style=flat-square" alt="Version">
</p> </p>
<p align="center"> <p align="center">
@@ -30,7 +30,7 @@
|------|------| |------|------|
| 📑 **多标签页** | 同时打开多个文件,Ctrl+T 新建、Ctrl+W 关闭、Ctrl+Tab MRU 切换 | | 📑 **多标签页** | 同时打开多个文件,Ctrl+T 新建、Ctrl+W 关闭、Ctrl+Tab MRU 切换 |
| 📂 **文件打开** | 按钮打开 / 拖拽打开 / 文件关联(双击 .md) / 命令行参数 | | 📂 **文件打开** | 按钮打开 / 拖拽打开 / 文件关联(双击 .md) / 命令行参数 |
| ✏️ **实时编辑** | 左侧编辑器,支持 Tab 缩进、行号显示、光标位置、大文件虚拟化行号 | | ✏️ **实时编辑** | 左侧编辑器CodeMirror 6,支持 Tab 缩进、行号显示、折叠、括号匹配 |
| 👁 **实时预览** | 右侧预览面板,基于 unified/rehype 管线渲染,编辑即更新 | | 👁 **实时预览** | 右侧预览面板,基于 unified/rehype 管线渲染,编辑即更新 |
| 🔤 **代码高亮** | 基于 rehype-highlight,支持 180+ 种编程语言语法高亮 | | 🔤 **代码高亮** | 基于 rehype-highlight,支持 180+ 种编程语言语法高亮 |
| 🎨 **两种视图** | 编辑模式 / 预览模式,自由切换 | | 🎨 **两种视图** | 编辑模式 / 预览模式,自由切换 |
@@ -43,32 +43,6 @@
| ⌨️ **快捷键** | 完整的键盘快捷键支持,操作高效 | | ⌨️ **快捷键** | 完整的键盘快捷键支持,操作高效 |
| 📦 **NSIS 安装包** | 一键打包为 Windows exe 安装程序 / 便携版 | | 📦 **NSIS 安装包** | 一键打包为 Windows exe 安装程序 / 便携版 |
## 📸 界面预览
```
┌──────────────────────────────────────────────────────────────────────────┐
│ MarkLite - README.md ─ □ ✕ │
├──────────────────────────────────────────────────────────────────────────┤
│ 📁 打开 │ 💾 保存 │ ✏️ 编辑 │ 👁 预览 │ 🌙 │
├──────────────────────────────────────────────────────────────────────────┤
│ [README.md] [DESIGN.md] [main.ts] [+] │
├──────────┬─────────────────────────┬─────────────────────────────────────┤
│ 资源管理器│ │ │
│ ▼ MarkLite│ 🔍 查找... 3/12 │ MarkLite │
│ 📁 src │ [替换... ] [替换][全部]│ ───────────── │
│ 📄 README│ │ │
│ 📄 DESIGN│ 1 # MarkLite │ 一款轻量级的 Windows 本地 │
│ 📁 lib │ 2 │ Markdown 编辑器桌面应用... │
│ 📁 assets│ 3 一款轻量级的... │ │
│ │ 4 │ ■ 实时编辑 │
│ │ 5 ## 功能特性 │ ■ 实时预览 │
│ │ 6 │ ■ 代码高亮 │
│ │ 7 - 📝 实时编辑 │ │
├──────────┴─────────────────────────┴─────────────────────────────────────┤
│ README.md │ UTF-8 │ Markdown │ 2.1 KB │ 行 9, 列 12 │
└──────────────────────────────────────────────────────────────────────────┘
```
## 🚀 快速开始 ## 🚀 快速开始
### 环境要求 ### 环境要求
@@ -128,11 +102,9 @@ npm run lint
| `Ctrl + 2` | 预览模式 | | `Ctrl + 2` | 预览模式 |
| `Ctrl + F` | 搜索 | | `Ctrl + F` | 搜索 |
| `Ctrl + H` | 搜索并替换 | | `Ctrl + H` | 搜索并替换 |
| `Ctrl + B` | 粗体 |
| `Ctrl + I` | 斜体 |
| `Enter` / `Shift+Enter` | 下一个 / 上一个匹配 | | `Enter` / `Shift+Enter` | 下一个 / 上一个匹配 |
| `Alt + C` | 切换区分大小写 |
| `Alt + R` | 切换正则表达式 |
| `Ctrl + Shift + G` | 替换当前匹配 |
| `Ctrl + Shift + H` | 全部替换 |
| `Esc` | 关闭搜索栏 | | `Esc` | 关闭搜索栏 |
## 🛠️ 技术栈 ## 🛠️ 技术栈
@@ -142,13 +114,14 @@ npm run lint
| 桌面框架 | [Electron](https://www.electronjs.org/) v28 | 跨平台桌面应用框架 | | 桌面框架 | [Electron](https://www.electronjs.org/) v28 | 跨平台桌面应用框架 |
| 前端框架 | [React](https://react.dev/) v18 | 函数组件 + Hooks | | 前端框架 | [React](https://react.dev/) v18 | 函数组件 + Hooks |
| 类型系统 | [TypeScript](https://www.typescriptlang.org/) v5.6 | 全量类型安全 | | 类型系统 | [TypeScript](https://www.typescriptlang.org/) v5.6 | 全量类型安全 |
| 编辑器 | [CodeMirror](https://codemirror.net/) v6 | 现代化代码编辑器 |
| 状态管理 | [Zustand](https://zustand-demo.pmnd.rs/) v5 | 轻量级状态管理 | | 状态管理 | [Zustand](https://zustand-demo.pmnd.rs/) v5 | 轻量级状态管理 |
| 持久化 | [Dexie.js](https://dexie.org/) v4 (IndexedDB) | 标签页状态 & 用户设置持久化 | | 持久化 | [Dexie.js](https://dexie.org/) v4 (IndexedDB) | 标签页状态 & 用户设置持久化 |
| Markdown 解析 | [unified](https://unifiedjs.com/) / [remark](https://remark.js.org/) / [rehype](https://rehype.js.org/) | 插件化 Markdown 渲染管线 | | Markdown 解析 | [unified](https://unifiedjs.com/) / [remark](https://remark.js.org/) / [rehype](https://rehype.js.org/) | 插件化 Markdown 渲染管线 |
| 代码高亮 | [rehype-highlight](https://github.com/rehypejs/rehype-highlight) | 基于 highlight.js 的语法高亮 | | 代码高亮 | [rehype-highlight](https://github.com/rehypejs/rehype-highlight) | 基于 highlight.js 的语法高亮 |
| 构建工具 | [electron-vite](https://electron-vite.org/) v3 | Electron + Vite 集成,HMR 热更新 | | 构建工具 | [electron-vite](https://electron-vite.org/) v3 | Electron + Vite 集成,HMR 热更新 |
| 打包工具 | [electron-builder](https://www.electron.build/) | 生成 exe 安装包 | | 打包工具 | [electron-builder](https://www.electron.build/) | 生成 exe 安装包 |
| 样式 | CSS Variables + CSS Modules | 主题驱动,样式隔离 | | 样式 | CSS Variables | 主题驱动,亮色/暗色切换 |
## 📁 项目结构 ## 📁 项目结构
@@ -161,125 +134,73 @@ MarkLite/
├── .eslintrc.cjs # ESLint + TypeScript 规则 ├── .eslintrc.cjs # ESLint + TypeScript 规则
├── src/ ├── src/
│ ├── main/ # ===== 主进程 (Node.js) ===== │ ├── main/ # 主进程 (Node.js)
│ │ ├── index.ts # 入口:窗口创建、app 生命周期、单实例锁 │ │ ├── index.ts # 入口:窗口创建、app 生命周期、单实例锁
│ │ ├── ipc-handlers.ts # 所有 ipcMain.handle 注册 │ │ ├── ipc-handlers.ts # 所有 ipcMain.handle 注册
│ │ ├── file-system.ts # 文件读写、目录树构建、BOM 剥离 │ │ ├── file-system.ts # 文件读写、目录树构建、BOM 剥离
│ │ ├── file-watcher.ts # fs.watch 封装(单文件 + 目录监听) │ │ ├── file-watcher.ts # fs.watch 封装(单文件 + 目录监听)
│ │ ── window-manager.ts # 窗口创建、关闭拦截、单实例锁 │ │ ── window-manager.ts # 窗口创建、关闭拦截、单实例锁
│ │ └── ipc-channels.ts # IPC 通道名常量
│ │ │ │
│ ├── preload/ # ===== 预加载脚本 ===== │ ├── preload/ # 预加载脚本
│ │ └── index.ts # contextBridge 类型安全暴露 │ │ └── index.ts # contextBridge 类型安全暴露
│ │ │ │
│ ├── renderer/ # ===== 渲染进程 (React) ===== │ ├── renderer/ # 渲染进程 (React 18)
│ │ ├── index.html # 入口 HTML(含 CSP 策略) │ │ ├── index.html # 入口 HTML(含 CSP 策略)
│ │ ├── main.tsx # React 入口 (createRoot) │ │ ├── main.tsx # React 入口
│ │ ├── App.tsx # 根组件:布局编排、全局事件 │ │ ├── App.tsx # 根组件:布局编排、全局事件
│ │ │ │ │ │
│ │ ├── components/ # ----- UI 组件 ----- │ │ ├── components/ # UI 组件
│ │ │ ├── Toolbar/ # 工具栏(打开、保存、视图切换、暗色主题) │ │ │ ├── Toolbar/ # 工具栏
│ │ │ ├── TabBar/ # 标签页栏(多标签、关闭、MRU 切换) │ │ │ ├── TabBar/ # 标签页栏
│ │ │ ├── Editor/ # 编辑器(行号、大文件虚拟化、Tab 缩进) │ │ │ ├── Editor/ # CodeMirror 6 编辑器
│ │ │ ├── Preview/ # Markdown 预览面板unified/rehype 渲染) │ │ │ ├── Preview/ # Markdown 预览面板
│ │ │ ├── Sidebar/ # 侧边栏文件树(递归、展开折叠、独立文件区) │ │ │ ├── Sidebar/ # 侧边栏文件树
│ │ │ ├── SearchBar/ # 搜索替换栏(高亮、正则、大小写) │ │ │ ├── StatusBar/ # 状态栏
│ │ │ ├── StatusBar/ # 状态栏(文件名、编码、语言)
│ │ │ ├── WelcomeScreen/ # 欢迎屏幕 │ │ │ ├── WelcomeScreen/ # 欢迎屏幕
│ │ │ ├── Toast/ # Toast 通知 │ │ │ ├── Toast/ # Toast 通知
│ │ │ ├── ModifiedBanner/ # 文件外部修改提示横幅 │ │ │ ├── ModifiedBanner/ # 文件外部修改提示
│ │ │ ── DropOverlay/ # 拖拽文件覆盖层 │ │ │ ── DropOverlay/ # 拖拽文件覆盖层
│ │ │ └── Icons.tsx # SVG 图标库
│ │ │ │ │ │
│ │ ├── stores/ # ----- Zustand 状态管理 ----- │ │ ├── stores/ # Zustand 状态管理
│ │ │ ├── tabStore.ts # 标签页状态tabs、activeTabId、mruStack │ │ │ ├── tabStore.ts # 标签页状态
│ │ │ ├── editorStore.ts # 编辑器状态viewMode、darkMode │ │ │ ├── editorStore.ts # 编辑器状态
│ │ │ ── sidebarStore.ts # 侧边栏状态tree、expandedDirs、rootPath │ │ │ ── sidebarStore.ts # 侧边栏状态
│ │ │ └── searchStore.ts # 搜索替换状态(matches、currentIndex、options
│ │ │ │ │ │
│ │ ├── hooks/ # ----- 自定义 Hooks ----- │ │ ├── hooks/ # 自定义 Hooks
│ │ │ ├── useTheme.ts # 暗色/亮色主题IndexedDB 持久化) │ │ │ ├── useTheme.ts # 暗色/亮色主题
│ │ │ ├── useSettings.ts # 用户设置读写 │ │ │ ├── useSettings.ts # 用户设置
│ │ │ ├── useKeyboard.ts # 全局快捷键 │ │ │ ├── useKeyboard.ts # 全局快捷键
│ │ │ ├── useDragDrop.ts # 拖拽打开文件 │ │ │ ├── useDragDrop.ts # 拖拽打开
│ │ │ ├── useFileWatch.ts # 外部文件修改监听 │ │ │ ├── useFileWatch.ts # 外部修改监听
│ │ │ └── useUnsavedWarning.ts # 未保存提醒Electron + 浏览器兼容) │ │ │ └── useUnsavedWarning.ts # 未保存提醒
│ │ │ │ │ │
│ │ ├── lib/ # ----- 工具库 ----- │ │ ├── lib/ # 工具库
│ │ │ ├── markdown.ts # Markdown 渲染管线unified → remark → rehype │ │ │ ├── markdown.ts # Markdown 渲染管线
│ │ │ ├── scrollSync.ts # 滚动同步算法(块级元素 DOM 位置映射) │ │ │ ├── fileUtils.ts # 文件工具函数
│ │ │ ├── searchEngine.ts # 搜索匹配引擎(普通/正则/大小写/二分查找)
│ │ │ ├── fileUtils.ts # 文件大小格式化、扩展名检查
│ │ │ └── constants.ts # 常量定义 │ │ │ └── constants.ts # 常量定义
│ │ │ │ │ │
│ │ ├── db/ # ----- IndexedDB 持久化层 ----- │ │ ├── db/ # IndexedDB 持久化层
│ │ │ ├── schema.ts # Dexie 数据库定义 + 类型(tabSnapshots/settings/recentFiles │ │ │ ├── schema.ts # Dexie 数据库定义
│ │ │ ├── tabRepository.ts # 标签页状态 CRUD │ │ │ ├── tabRepository.ts # 标签页 CRUD
│ │ │ ├── settingsRepository.ts # 用户设置 CRUD │ │ │ ├── settingsRepository.ts # 设置 CRUD
│ │ │ └── recentFilesRepository.ts # 最近打开文件 │ │ │ └── recentFilesRepository.ts # 最近文件
│ │ │ │ │ │
│ │ ├── types/ # ----- TypeScript 类型 ----- │ │ ├── types/ # TypeScript 类型
│ │ │ ├── tab.ts # Tab 接口 │ │ └── styles/ # 全局样式
│ │ │ ├── file.ts # FileNode, DirTree 等
│ │ │ ├── settings.ts # Settings 接口 + 默认值
│ │ │ ├── search.ts # SearchMatch, SearchOptions
│ │ │ ├── ipc.ts # ElectronAPI 接口 + IPC 通道类型映射
│ │ │ └── index.ts # 类型统一导出
│ │ │
│ │ └── styles/ # ----- 全局样式 -----
│ │ ├── variables.css # CSS 变量(亮色/暗色主题)
│ │ ├── global.css # 全局 reset + 布局 + 组件样式
│ │ └── markdown-body.css # Markdown 预览正文样式
│ │ │ │
│ └── shared/ # ===== 主进程/渲染进程共享 ===== │ └── shared/ # 主进程/渲染进程共享
│ ├── ipc-channels.ts # IPC 通道名常量 │ ├── ipc-channels.ts # IPC 通道名常量
│ └── types.ts # 共享类型定义 │ └── types.ts # 共享类型定义
├── lib/ # 第三方离线库(marked.js, highlight.js
├── assets/ ├── assets/
│ └── icon.ico # 应用图标(多尺寸) │ └── icon.ico # 应用图标
├── DESIGN.md # 架构设计文档 ├── DESIGN.md # 架构设计文档
├── DEVSETUP.md # 开发环境配置指南 ├── DEVSETUP.md # 开发环境配置指南
├── REFACTOR-PLAN.md # 重构方案文档
├── LICENSE # MIT 许可证 ├── LICENSE # MIT 许可证
└── README.md # 本文件 └── README.md # 本文件
``` ```
## 🏗️ 架构设计
```
┌─────────────────────────────────────────────────────────────┐
│ Main Process (src/main/) │
│ 窗口管理 · 文件系统 · 文件监听 · IPC · 目录树 · 单实例锁 │
└──────────────────────┬──────────────────────────────────────┘
│ contextBridge (安全隔离)
┌──────────────────────▼──────────────────────────────────────┐
│ Preload Script (src/preload/) │
│ 类型安全 API 桥接层 │
└──────────────────────┬──────────────────────────────────────┘
┌──────────────────────▼──────────────────────────────────────┐
│ Renderer Process (src/renderer/) — React 18 │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Components (11 个) │ │
│ │ Toolbar · TabBar · Editor · Preview · Sidebar │ │
│ │ SearchBar · StatusBar · WelcomeScreen · Toast │ │
│ │ ModifiedBanner · DropOverlay │ │
│ └─────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Zustand Stores (4 个) │ │
│ │ tabStore · editorStore · sidebarStore · searchStore │ │
│ └─────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ IndexedDB (Dexie.js) │ │
│ │ tabSnapshots · settings · recentFiles │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
安全策略:contextIsolation: true + nodeIntegration: false + CSP
渲染进程无法直接访问 Node.js API
```
## 📝 支持的 Markdown 语法 ## 📝 支持的 Markdown 语法
- ✅ 标题(h1 ~ h6 - ✅ 标题(h1 ~ h6
-825
View File
@@ -1,825 +0,0 @@
# MarkLite 重构方案:TypeScript + React + IndexedDB
## 一、重构目标与原则
| 维度 | 现状 | 目标 |
|------|------|------|
| 语言 | JavaScript (无类型) | **TypeScript 5.x** (全量类型) |
| UI 框架 | 原生 DOM 操作 | **React 18** (函数组件 + Hooks) |
| 状态管理 | 全局变量 + 闭包 | **Zustand** (轻量状态管理) |
| 持久化 | localStorage | **IndexedDB** (via Dexie.js) |
| 样式 | 纯 CSS | **CSS Modules** + CSS 变量 |
| 构建 | 无打包 | **Vite** + electron-vite |
| Markdown | marked.min.js (离线) | **unified/remark/rehype** 生态 |
| 代码高亮 | highlight.js (离线) | **Shiki****rehype-highlight** |
| 编辑器 | `<textarea>` | **CodeMirror 6** (专业编辑器) |
**核心原则**
1. **功能 100% 兼容** — 不丢失任何现有功能
2. **渐进式重构** — 主进程/预加载脚本改动最小,重点重构渲染进程
3. **类型安全** — 所有接口、IPC 通信、状态都有 TypeScript 类型
4. **可测试性** — 业务逻辑与 UI 解耦,便于单元测试
---
## 二、新项目结构
```
MarkLite/
├── package.json
├── tsconfig.json # TypeScript 配置
├── electron.vite.config.ts # electron-vite 构建配置
├── .eslintrc.cjs # ESLint + TS 规则
├── src/
│ ├── main/ # ===== 主进程 =====
│ │ ├── index.ts # 入口:窗口创建、app 生命周期
│ │ ├── ipc-handlers.ts # 所有 ipcMain.handle 注册
│ │ ├── file-system.ts # 文件读写、目录树构建
│ │ ├── file-watcher.ts # fs.watch 封装(单文件 + 目录)
│ │ ├── window-manager.ts # 窗口创建、关闭拦截、单实例锁
│ │ └── menu.ts # 原生菜单(可选)
│ │
│ ├── preload/ # ===== 预加载 =====
│ │ └── index.ts # contextBridge 类型安全暴露
│ │
│ ├── renderer/ # ===== 渲染进程 (React) =====
│ │ ├── index.html # 入口 HTML
│ │ ├── main.tsx # React 入口 (createRoot)
│ │ ├── App.tsx # 根组件:布局编排
│ │ │
│ │ ├── components/ # ===== UI 组件 =====
│ │ │ ├── Toolbar/
│ │ │ │ ├── Toolbar.tsx
│ │ │ │ └── Toolbar.module.css
│ │ │ ├── TabBar/
│ │ │ │ ├── TabBar.tsx
│ │ │ │ ├── TabItem.tsx
│ │ │ │ └── TabBar.module.css
│ │ │ ├── Editor/
│ │ │ │ ├── Editor.tsx # CodeMirror 6 封装
│ │ │ │ ├── EditorToolbar.tsx # 格式化工具栏(加粗/斜体/链接等)
│ │ │ │ ├── useCodeMirror.ts # CodeMirror 初始化 Hook
│ │ │ │ └── Editor.module.css
│ │ │ ├── Preview/
│ │ │ │ ├── Preview.tsx # Markdown 渲染面板
│ │ │ │ ├── MarkdownRenderer.tsx # rehype/remark 处理
│ │ │ │ ├── useScrollSync.ts # 滚动同步 Hook
│ │ │ │ └── Preview.module.css
│ │ │ ├── Sidebar/
│ │ │ │ ├── Sidebar.tsx
│ │ │ │ ├── FileTree.tsx # 文件树递归组件
│ │ │ │ ├── FileTreeItem.tsx
│ │ │ │ ├── IndependentFiles.tsx
│ │ │ │ └── Sidebar.module.css
│ │ │ ├── SearchBar/
│ │ │ │ ├── SearchBar.tsx
│ │ │ │ └── SearchBar.module.css
│ │ │ ├── StatusBar/
│ │ │ │ ├── StatusBar.tsx
│ │ │ │ └── StatusBar.module.css
│ │ │ ├── WelcomeScreen/
│ │ │ │ ├── WelcomeScreen.tsx
│ │ │ │ └── WelcomeScreen.module.css
│ │ │ ├── ModifiedBanner/
│ │ │ │ └── ModifiedBanner.tsx
│ │ │ ├── DropOverlay/
│ │ │ │ └── DropOverlay.tsx
│ │ │ └── Toast/
│ │ │ ├── Toast.tsx
│ │ │ └── Toast.module.css
│ │ │
│ │ ├── stores/ # ===== 状态管理 (Zustand) =====
│ │ │ ├── tabStore.ts # 标签页状态
│ │ │ ├── editorStore.ts # 编辑器状态(视图模式、暗色主题等)
│ │ │ ├── sidebarStore.ts # 侧边栏状态
│ │ │ └── searchStore.ts # 搜索替换状态
│ │ │
│ │ ├── hooks/ # ===== 自定义 Hooks =====
│ │ │ ├── useTheme.ts # 暗色/亮色主题
│ │ │ ├── useKeyboard.ts # 全局快捷键
│ │ │ ├── useDragDrop.ts # 拖拽打开
│ │ │ ├── useFileWatch.ts # 外部修改监听
│ │ │ ├── useUnsavedWarning.ts # 未保存提醒
│ │ │ └── useSettings.ts # 用户设置读写
│ │ │
│ │ ├── lib/ # ===== 工具库 =====
│ │ │ ├── markdown.ts # Markdown 渲染管线(remark + rehype
│ │ │ ├── sanitize.ts # HTML 安全过滤
│ │ │ ├── scrollSync.ts # 滚动同步算法(行号→DOM 映射)
│ │ │ ├── searchEngine.ts # 搜索匹配引擎(普通/正则/大小写)
│ │ │ ├── fileUtils.ts # 文件大小格式化、扩展名检查等
│ │ │ └── constants.ts # 常量定义(文件大小限制、扩展名列表等)
│ │ │
│ │ ├── db/ # ===== IndexedDB 层 =====
│ │ │ ├── schema.ts # Dexie 数据库定义 + 类型
│ │ │ ├── tabRepository.ts # 标签页状态 CRUD
│ │ │ ├── settingsRepository.ts # 用户设置 CRUD
│ │ │ └── recentFilesRepository.ts # 最近打开文件
│ │ │
│ │ ├── types/ # ===== 全局类型 =====
│ │ │ ├── tab.ts # Tab 接口
│ │ │ ├── file.ts # FileNode, DirTree 等
│ │ │ ├── settings.ts # Settings 接口
│ │ │ ├── search.ts # SearchMatch, SearchOptions 等
│ │ │ └── ipc.ts # IPC 通道类型映射
│ │ │
│ │ └── styles/ # ===== 全局样式 =====
│ │ ├── variables.css # CSS 变量(亮色/暗色)
│ │ ├── global.css # 全局 reset + 通用样式
│ │ ├── markdown-body.css # Markdown 预览样式
│ │ └── code-theme.css # 代码高亮主题
│ │
│ └── shared/ # ===== 主进程/渲染进程共享 =====
│ ├── ipc-channels.ts # IPC 通道名常量
│ └── types.ts # 共享类型定义
├── assets/
│ └── icon.ico
├── DESIGN.md
├── DEVSETUP.md
├── README.md
├── LICENSE
└── REFACTOR-PLAN.md # 本文件
```
---
## 三、技术选型详解
### 1. TypeScript — 类型安全
```typescript
// src/renderer/types/tab.ts
export interface Tab {
id: string; // nanoid 生成
filePath: string | null;
content: string;
isModified: boolean;
scrollTop: number;
scrollLeft: number;
selectionStart: number;
selectionEnd: number;
previewScrollTop: number;
}
// src/renderer/types/ipc.ts — IPC 通道类型映射
export interface IpcInvokeMap {
'dialog:openFile': [void, OpenFileResult | null];
'file:read': [string, ReadFileResult];
'file:save': [SaveFilePayload, SaveFileResult];
'file:saveAs': [SaveAsPayload, SaveFileResult];
'file:reload': [void, ReloadFileResult];
'dir:readTree': [string, ReadDirTreeResult];
'dir:openDialog': [void, string | null];
'dir:watch': [string, void];
'tab:switched': [string | null, void];
'window:forceClose': [void, void];
}
// 类型安全的 IPC 调用封装
export function invoke<K extends keyof IpcInvokeMap>(
channel: K,
...args: IpcInvokeMap[K][0] extends void ? [] : [IpcInvokeMap[K][0]]
): Promise<IpcInvokeMap[K][1]> {
return window.electronAPI.invoke(channel, ...args);
}
```
### 2. Zustand — 状态管理
```typescript
// src/renderer/stores/tabStore.ts
import { create } from 'zustand';
import { nanoid } from 'nanoid';
import type { Tab } from '../types/tab';
interface TabState {
tabs: Tab[];
activeTabId: string | null;
mruStack: string[];
// Actions
createTab: (filePath?: string | null, content?: string) => Tab;
closeTab: (tabId: string) => void;
switchToTab: (tabId: string) => void;
updateTabContent: (tabId: string, content: string) => void;
setModified: (tabId: string, modified: boolean) => void;
getActiveTab: () => Tab | null;
}
export const useTabStore = create<TabState>((set, get) => ({
tabs: [],
activeTabId: null,
mruStack: [],
createTab: (filePath = null, content = '') => {
// 检查是否已打开
if (filePath) {
const existing = get().tabs.find(t => t.filePath === filePath);
if (existing) {
get().switchToTab(existing.id);
return existing;
}
}
const tab: Tab = {
id: nanoid(),
filePath,
content,
isModified: false,
scrollTop: 0,
scrollLeft: 0,
selectionStart: 0,
selectionEnd: 0,
previewScrollTop: 0,
};
set(state => ({
tabs: [...state.tabs, tab],
activeTabId: tab.id,
}));
return tab;
},
closeTab: (tabId) => {
set(state => {
const index = state.tabs.findIndex(t => t.id === tabId);
if (index === -1) return state;
const newTabs = state.tabs.filter(t => t.id !== tabId);
const newMru = state.mruStack.filter(id => id !== tabId);
let newActiveId = state.activeTabId;
if (state.activeTabId === tabId) {
if (newTabs.length === 0) {
newActiveId = null;
} else {
const newIndex = Math.min(index, newTabs.length - 1);
newActiveId = newTabs[newIndex].id;
}
}
return {
tabs: newTabs,
activeTabId: newActiveId,
mruStack: newMru,
};
});
},
switchToTab: (tabId) => {
set(state => {
if (state.activeTabId === tabId) return state;
return {
activeTabId: tabId,
mruStack: [
state.activeTabId!,
...state.mruStack.filter(id => id !== state.activeTabId),
],
};
});
},
updateTabContent: (tabId, content) => {
set(state => ({
tabs: state.tabs.map(t =>
t.id === tabId ? { ...t, content, isModified: true } : t
),
}));
},
setModified: (tabId, modified) => {
set(state => ({
tabs: state.tabs.map(t =>
t.id === tabId ? { ...t, isModified: modified } : t
),
}));
},
getActiveTab: () => {
const { tabs, activeTabId } = get();
return tabs.find(t => t.id === activeTabId) ?? null;
},
}));
```
### 3. IndexedDB (Dexie.js) — 持久化
```typescript
// src/renderer/db/schema.ts
import Dexie, { type EntityTable } from 'dexie';
interface TabSnapshot {
id: string; // 与 Tab.id 对应
filePath: string | null;
content: string;
scrollTop: number;
scrollLeft: number;
selectionStart: number;
selectionEnd: number;
previewScrollTop: number;
isModified: boolean;
updatedAt: number; // 时间戳
}
interface Settings {
id: string; // 固定为 'default'
darkMode: boolean;
viewMode: 'split' | 'editor' | 'preview';
splitRatio: number;
sidebarCollapsed: boolean;
sidebarWidth: number;
}
interface RecentFile {
id?: number; // 自增主键
filePath: string;
lastOpened: number;
}
const db = new Dexie('MarkLite') as Dexie & {
tabSnapshots: EntityTable<TabSnapshot, 'id'>;
settings: EntityTable<Settings, 'id'>;
recentFiles: EntityTable<RecentFile, 'id'>;
};
db.version(1).stores({
tabSnapshots: 'id, filePath, updatedAt',
settings: 'id',
recentFiles: '++id, filePath, lastOpened',
});
export { db };
export type { TabSnapshot, Settings, RecentFile };
```
```typescript
// src/renderer/db/tabRepository.ts
import { db, type TabSnapshot } from './schema';
export const tabRepository = {
/** 保存所有标签快照(关闭时/定期) */
async saveAll(tabs: TabSnapshot[]): Promise<void> {
await db.transaction('rw', db.tabSnapshots, async () => {
await db.tabSnapshots.clear();
await db.tabSnapshots.bulkAdd(tabs);
});
},
/** 加载上次的标签快照 */
async loadAll(): Promise<TabSnapshot[]> {
return db.tabSnapshots.orderBy('updatedAt').toArray();
},
/** 清除所有快照(用户主动清除时) */
async clearAll(): Promise<void> {
await db.tabSnapshots.clear();
},
};
```
### 4. CodeMirror 6 — 编辑器
```typescript
// src/renderer/components/Editor/useCodeMirror.ts
import { useEffect, useRef, useCallback } from 'react';
import { EditorState, type Extension } from '@codemirror/state';
import { EditorView, keymap, lineNumbers, highlightActiveLine } from '@codemirror/view';
import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
import { syntaxHighlighting, defaultHighlightStyle } from '@codemirror/language';
import { oneDark } from '@codemirror/theme-one-dark';
interface UseCodeMirrorOptions {
content: string;
onChange: (value: string) => void;
darkMode: boolean;
}
export function useCodeMirror({ content, onChange, darkMode }: UseCodeMirrorOptions) {
const containerRef = useRef<HTMLDivElement>(null);
const viewRef = useRef<EditorView | null>(null);
// 初始化
useEffect(() => {
if (!containerRef.current) return;
const extensions: Extension[] = [
lineNumbers(),
highlightActiveLine(),
history(),
keymap.of([...defaultKeymap, ...historyKeymap]),
markdown({ base: markdownLanguage }),
syntaxHighlighting(defaultHighlightStyle),
EditorView.updateListener.of(update => {
if (update.docChanged) {
onChange(update.state.doc.toString());
}
}),
EditorView.lineWrapping,
];
if (darkMode) extensions.push(oneDark);
const state = EditorState.create({
doc: content,
extensions,
});
const view = new EditorView({
state,
parent: containerRef.current,
});
viewRef.current = view;
return () => {
view.destroy();
viewRef.current = null;
};
}, [darkMode]); // darkMode 变化时重建
// 外部内容更新(切换标签时)
const setContent = useCallback((newContent: string) => {
const view = viewRef.current;
if (!view) return;
const current = view.state.doc.toString();
if (current !== newContent) {
view.dispatch({
changes: { from: 0, to: current.length, insert: newContent },
});
}
}, []);
return { containerRef, viewRef, setContent };
}
```
### 5. Markdown 渲染管线
```typescript
// src/renderer/lib/markdown.ts
import { unified } from 'unified';
import remarkParse from 'remark-parse';
import remarkGfm from 'remark-gfm';
import remarkRehype from 'remark-rehype';
import rehypeRaw from 'rehype-raw';
import rehypeSanitize, { defaultSchema } from 'rehype-sanitize';
import rehypeStringify from 'rehype-stringify';
import rehypeHighlight from 'rehype-highlight';
const processor = unified()
.use(remarkParse)
.use(remarkGfm)
.use(remarkRehype, { allowDangerousHtml: true })
.use(rehypeRaw)
.use(rehypeSanitize, {
...defaultSchema,
attributes: {
...defaultSchema.attributes,
img: [...(defaultSchema.attributes?.img ?? []), ['src']],
},
})
.use(rehypeHighlight)
.use(rehypeStringify);
export async function renderMarkdown(content: string): Promise<string> {
const result = await processor.process(content);
return String(result);
}
```
---
## 四、IndexedDB 替代 localStorage 的数据模型
| 原 localStorage Key | IndexedDB Store | 字段 | 说明 |
|---------------------|-----------------|------|------|
| `marklite-settings` | `settings` | darkMode, viewMode, splitRatio, sidebarCollapsed, sidebarWidth | 用户偏好 |
| *(无,标签状态丢失)* | `tabSnapshots` | id, filePath, content, scrollTop, ... | **新增**:标签页状态持久化 |
| *(无)* | `recentFiles` | filePath, lastOpened | **新增**:最近打开文件列表 |
**IndexedDB 优势**
- 存储容量大(localStorage 约 5-10MBIndexedDB 可达数百 MB
- 支持大文件内容缓存(当前 20MB 限制的文件也能存)
- 支持索引查询(按 filePath、updatedAt 查询)
- 异步 API,不阻塞 UI
- 标签页状态可持久化(关闭窗口后恢复所有标签)
---
## 五、状态管理架构
```
┌─────────────────────────────────────────────────────────┐
│ App.tsx (根组件) │
├─────────┬──────────┬──────────┬──────────┬──────────────┤
│Toolbar │ TabBar │ Sidebar │ Editor │ Preview │
│ │ │ │ (CM6) │ │
├─────────┴──────────┴──────────┴──────────┴──────────────┤
│ Zustand Stores │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ tabStore │ │editorStore│ │sidebarStore│ │searchStore│ │
│ │ - tabs │ │- viewMode│ │- tree │ │- matches │ │
│ │- activeId│ │- darkMode│ │- expanded │ │- index │ │
│ │ - mru │ │- splitPct│ │- rootPath │ │- options │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └──────────┘ │
│ │ │ │ │
│ ┌────▼────────────▼────────────▼────────────────────┐ │
│ │ IndexedDB (Dexie.js) │ │
│ │ tabSnapshots │ settings │ recentFiles │ │
│ └───────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
```
---
## 六、主进程重构(改动最小)
主进程保持 Electron 原生,仅将 JS → TS,逻辑不变:
```typescript
// src/main/file-system.ts — 类型安全的文件操作
import { readFile, writeFile, stat, readdir } from 'fs/promises';
import { join, extname } from 'path';
import type { FileNode, ReadFileResult, SaveFileResult } from '../shared/types';
const ALLOWED_EXTENSIONS = ['.md', '.markdown', '.txt'] as const;
const MAX_FILE_SIZE = 20 * 1024 * 1024; // 20MB
export async function readFileContent(filePath: string): Promise<ReadFileResult> {
const stats = await stat(filePath);
if (stats.size > MAX_FILE_SIZE) {
return { success: false, error: `文件过大 (${formatBytes(stats.size)})` };
}
let content = await readFile(filePath, 'utf-8');
// 去除 BOM
if (content.charCodeAt(0) === 0xFEFF) content = content.slice(1);
return { success: true, content };
}
export async function buildDirTree(dirPath: string): Promise<FileNode[]> {
const entries = await readdir(dirPath, { withFileTypes: true });
const result: FileNode[] = [];
for (const entry of entries) {
if (shouldSkip(entry.name)) continue;
const fullPath = join(dirPath, entry.name);
if (entry.isDirectory()) {
result.push({
name: entry.name,
path: fullPath,
type: 'dir',
children: await buildDirTree(fullPath),
});
} else if (ALLOWED_EXTENSIONS.includes(extname(entry.name).toLowerCase() as any)) {
result.push({ name: entry.name, path: fullPath, type: 'file' });
}
}
return result.sort((a, b) => {
if (a.type !== b.type) return a.type === 'dir' ? -1 : 1;
return a.name.localeCompare(b.name);
});
}
```
---
## 七、滚动同步算法(保留现有逻辑)
现有基于**块级元素 DOM 位置映射**的算法非常优秀,直接用 TypeScript 重写:
```typescript
// src/renderer/lib/scrollSync.ts
interface ScrollMap {
lineToPreviewMap: number[];
}
export function buildScrollMap(
markdown: string,
previewContainer: HTMLElement
): ScrollMap {
const lines = markdown.split('\n');
const totalLines = lines.length;
const lineToPreviewMap = new Array(totalLines).fill(0);
// 1. 识别块级元素起始行
const blockStarts = identifyBlockStarts(lines);
// 2. 获取预览 DOM 元素位置
const previewPositions = getPreviewPositions(previewContainer);
// 3. 块起始行 → 预览位置映射
// 4. 线性插值填充中间行
interpolateBlockPositions(blockStarts, previewPositions, lineToPreviewMap, totalLines);
return { lineToPreviewMap };
}
function identifyBlockStarts(lines: string[]): Set<number> {
const starts = new Set<number>();
let inCodeBlock = false;
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (line.match(/^```/)) {
if (!inCodeBlock) starts.add(i);
inCodeBlock = !inCodeBlock;
continue;
}
if (inCodeBlock) continue;
if (line.match(/^#{1,6}\s/) || line.match(/^[-*+]\s/) || line.match(/^\d+\.\s/) ||
line.match(/^>/) || line.match(/^\|/) || line.match(/^---/) || line === '') {
starts.add(i);
}
}
return starts;
}
```
---
## 八、CSS Modules + CSS 变量
```css
/* src/renderer/styles/variables.css */
:root {
/* 亮色主题 */
--color-primary: #1a73e8;
--color-primary-dark: #1557b0;
--color-primary-light: rgba(26, 115, 232, 0.1);
--color-bg: #ffffff;
--color-bg-secondary: #f8f9fa;
--color-bg-tertiary: #f1f3f4;
--color-text: #333333;
--color-text-secondary: #5f6368;
--color-text-tertiary: #80868b;
--color-border: #e1e4e8;
--color-border-light: #f0f0f0;
--color-code-bg: #f6f8fa;
/* 布局 */
--toolbar-height: 36px;
--tabbar-height: 34px;
--statusbar-height: 24px;
--sidebar-width: 220px;
/* 字体 */
--font-ui: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
--font-mono: "Cascadia Code", "Fira Code", "JetBrains Mono", Consolas, monospace;
--font-size: 14px;
/* 圆角/阴影 */
--radius: 6px;
--shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
--shadow-lg: 0 4px 12px rgba(0,0,0,0.15);
}
:root.dark {
--color-primary: #8ab4f8;
--color-primary-dark: #669df6;
--color-primary-light: rgba(138, 180, 248, 0.15);
--color-bg: #1e1e1e;
--color-bg-secondary: #252526;
--color-bg-tertiary: #2d2d2d;
--color-text: #d4d4d4;
--color-text-secondary: #9e9e9e;
--color-text-tertiary: #6e6e6e;
--color-border: #3e3e3e;
--color-border-light: #333333;
--color-code-bg: #2d2d2d;
}
```
---
## 九、构建配置
```typescript
// electron.vite.config.ts
import { defineConfig, externalizeDepsPlugin } from 'electron-vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
main: {
plugins: [externalizeDepsPlugin()],
build: {
outDir: 'dist/main',
rollupOptions: {
input: { index: 'src/main/index.ts' },
},
},
},
preload: {
plugins: [externalizeDepsPlugin()],
build: {
outDir: 'dist/preload',
rollupOptions: {
input: { index: 'src/preload/index.ts' },
},
},
},
renderer: {
plugins: [react()],
root: 'src/renderer',
build: {
outDir: 'dist/renderer',
rollupOptions: {
input: { index: 'src/renderer/index.html' },
},
},
},
});
```
```json
// package.json (关键 scripts)
{
"scripts": {
"dev": "electron-vite dev",
"build": "electron-vite build && electron-builder",
"build:portable": "electron-vite build && electron-builder --win portable",
"lint": "eslint src --ext .ts,.tsx",
"typecheck": "tsc --noEmit"
}
}
```
---
## 十、依赖清单
```json
{
"dependencies": {
"react": "^18.3",
"react-dom": "^18.3",
"zustand": "^5.0",
"dexie": "^4.0",
"nanoid": "^5.0",
"@codemirror/view": "^6.35",
"@codemirror/state": "^6.5",
"@codemirror/lang-markdown": "^6.3",
"@codemirror/theme-one-dark": "^6.1",
"@codemirror/commands": "^6.7",
"@codemirror/language": "^6.10",
"unified": "^11.0",
"remark-parse": "^11.0",
"remark-gfm": "^4.0",
"remark-rehype": "^11.1",
"rehype-raw": "^7.0",
"rehype-sanitize": "^6.0",
"rehype-stringify": "^10.0",
"rehype-highlight": "^7.0"
},
"devDependencies": {
"electron": "^28.0",
"electron-builder": "^25.0",
"electron-vite": "^3.0",
"@vitejs/plugin-react": "^4.3",
"typescript": "^5.6",
"@types/react": "^18.3",
"@types/react-dom": "^18.3",
"eslint": "^9.0",
"@typescript-eslint/eslint-plugin": "^8.0"
}
}
```
---
## 十一、分阶段实施计划
| 阶段 | 内容 | 预估工时 |
|------|------|----------|
| **Phase 0** | 脚手架搭建:electron-vite + React + TypeScript + ESLint | 0.5 天 |
| **Phase 1** | 主进程 TypeScript 化(文件系统、IPC、窗口管理) | 1 天 |
| **Phase 2** | IndexedDB 层 + Zustand stores + 设置持久化 | 1 天 |
| **Phase 3** | 基础 UIApp 布局 + Toolbar + TabBar + StatusBar | 1 天 |
| **Phase 4** | CodeMirror 6 编辑器集成 + 暗色主题 | 1 天 |
| **Phase 5** | Markdown 预览(unified/rehype 管线)+ 滚动同步 | 1.5 天 |
| **Phase 6** | 侧边栏文件树 + 独立文件区 | 1 天 |
| **Phase 7** | 搜索替换(引擎 + 高亮 + 快捷键) | 1 天 |
| **Phase 8** | 拖拽打开 + 文件监听 + 未保存提醒 + Toast | 0.5 天 |
| **Phase 9** | 测试 + 修复 + 打包验证 | 1 天 |
| **总计** | | **约 9.5 天** |
---
## 十二、关键决策说明
| 决策 | 理由 |
|------|------|
| **CodeMirror 6 替代 textarea** | 原生 undo/redo、语法高亮、行号内置、大文件性能优、扩展性强 |
| **Zustand 替代全局变量** | React 原生状态管理,比 Redux 轻量 10 倍,支持 selector 优化 |
| **Dexie.js 替代 localStorage** | 异步 API、支持索引、容量大、支持复杂查询 |
| **unified/rehype 替代 marked** | 插件化架构、GFM 原生支持、安全过滤内置、社区活跃 |
| **electron-vite 替代手动配置** | 开箱即用的 Electron + Vite 集成,HMR 热更新 |
| **CSS Modules 替代纯 CSS** | 样式隔离、TypeScript 类型提示、避免类名冲突 |
| **主进程保持 Electron 原生** | 主进程改动最小化,降低重构风险 |