refactor: v2.0 全量重构 — TypeScript + React + Zustand + IndexedDB

技术栈升级:
- JavaScript → TypeScript 5.6(全量类型安全)
- 原生 DOM → React 18(函数组件 + Hooks)
- 全局变量 → Zustand 5(轻量状态管理)
- localStorage → IndexedDB / Dexie.js 4(大容量、异步、索引)
- marked.js → unified / remark / rehype(插件化渲染管线)
- 无打包 → electron-vite 3(HMR 热更新)
- 纯 CSS → CSS Variables + CSS Modules

新增功能:
- 标签页状态 IndexedDB 持久化(关闭后可恢复)
- 最近打开文件列表
- 大文件虚拟化行号(>2000 行)
- 搜索高亮二分查找优化 O(log N)
- rehype-sanitize HTML 安全过滤

文件结构:
- 7 个源文件 → 51 个模块化文件
- src/main/     主进程(6 文件)
- src/preload/  预加载(1 文件)
- src/renderer/ 渲染进程(42 文件:组件/hooks/stores/lib/db/types/styles)
- src/shared/   共享类型(2 文件)

构建验证:
- TypeScript 检查零错误
- electron-vite build 成功
- 产物:main 15kB + preload 2kB + renderer 1.5MB
This commit is contained in:
thzxx
2026-05-27 20:29:23 +08:00
parent c2cdba546b
commit 5e1c89d280
59 changed files with 4674 additions and 314 deletions
+13
View File
@@ -0,0 +1,13 @@
export interface FileNode {
name: string
path: string
type: 'file' | 'dir'
children?: FileNode[]
}
export interface DirTreeResult {
success: boolean
tree?: FileNode[]
rootPath?: string
error?: string
}
+5
View File
@@ -0,0 +1,5 @@
export type { Tab } from './tab'
export type { FileNode, DirTreeResult } from './file'
export type { ViewMode, Settings } from './settings'
export type { SearchMatch, SearchOptions } from './search'
export type { ElectronAPI, IpcInvokeMap } from './ipc'
+59
View File
@@ -0,0 +1,59 @@
import type {
OpenFileResponse,
ReadFileResult,
SaveFilePayload,
SaveFileResult,
SaveAsPayload,
ReloadFileResult,
FileStatsResult,
ReadDirTreeResult
} from '../../shared/types'
export interface IpcInvokeMap {
'dialog:openFile': [void, OpenFileResponse]
'file:read': [string, ReadFileResult]
'file:save': [SaveFilePayload, SaveFileResult]
'file:saveAs': [SaveAsPayload, SaveFileResult]
'file:getCurrentPath': [void, string | null]
'file:stats': [string, FileStatsResult]
'file:reload': [void, ReloadFileResult]
'tab:switched': [string | null, void]
'window:forceClose': [void, void]
'window:cancelClose': [void, void]
'dir:readTree': [string, ReadDirTreeResult]
'dir:openDialog': [void, string | null]
'dir:watch': [string, void]
'dir:unwatch': [void, void]
}
export interface ElectronAPI {
openFile: () => Promise<OpenFileResponse>
readFile: (filePath: string) => Promise<ReadFileResult>
saveFile: (data: SaveFilePayload) => Promise<SaveFileResult>
saveFileAs: (data: SaveAsPayload) => Promise<SaveFileResult>
getCurrentPath: () => Promise<string | null>
getFileStats: (filePath: string) => Promise<FileStatsResult>
reloadFile: () => Promise<ReloadFileResult>
tabSwitched: (filePath: string | null) => Promise<void>
forceClose: () => Promise<void>
cancelClose: () => Promise<void>
openExternal: (url: string) => void
readDirTree: (dirPath: string) => Promise<ReadDirTreeResult>
openFolderDialog: () => Promise<string | null>
watchDir: (dirPath: string) => Promise<void>
unwatchDir: () => Promise<void>
onFileOpenInTab: (callback: (data: { filePath: string; content: string }) => void) => void
onMenuSave: (callback: () => void) => void
onMenuSaveAs: (callback: () => void) => void
onViewModeChange: (callback: (mode: string) => void) => void
onExternalModification: (callback: (filePath: string) => void) => void
onDirChanged: (callback: () => void) => void
onConfirmClose: (callback: () => void) => void
removeAllListeners: (channel: string) => void
}
declare global {
interface Window {
electronAPI?: ElectronAPI
}
}
+9
View File
@@ -0,0 +1,9 @@
export interface SearchMatch {
start: number
end: number
}
export interface SearchOptions {
caseSensitive: boolean
useRegex: boolean
}
+17
View File
@@ -0,0 +1,17 @@
export type ViewMode = 'split' | 'editor' | 'preview'
export interface Settings {
darkMode: boolean
viewMode: ViewMode
splitRatio: number
sidebarCollapsed: boolean
sidebarWidth: number
}
export const DEFAULT_SETTINGS: Settings = {
darkMode: false,
viewMode: 'split',
splitRatio: 50,
sidebarCollapsed: false,
sidebarWidth: 240
}
+11
View File
@@ -0,0 +1,11 @@
export interface Tab {
id: string
filePath: string | null
content: string
isModified: boolean
scrollTop: number
scrollLeft: number
selectionStart: number
selectionEnd: number
previewScrollTop: number
}