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
+208
View File
@@ -0,0 +1,208 @@
import React, { useState, useCallback, useEffect, useRef } from 'react'
import { useTabStore } from './stores/tabStore'
import { useEditorStore } from './stores/editorStore'
import { useSearchStore } from './stores/searchStore'
import { useTheme } from './hooks/useTheme'
import { useSettings } from './hooks/useSettings'
import { useKeyboard } from './hooks/useKeyboard'
import { useUnsavedWarning } from './hooks/useUnsavedWarning'
import { useFileWatch } from './hooks/useFileWatch'
import { Toolbar } from './components/Toolbar/Toolbar'
import { TabBar } from './components/TabBar/TabBar'
import { Editor } from './components/Editor/Editor'
import { Preview } from './components/Preview/Preview'
import { Sidebar } from './components/Sidebar/Sidebar'
import { SearchBar } from './components/SearchBar/SearchBar'
import { StatusBar } from './components/StatusBar/StatusBar'
import { WelcomeScreen } from './components/WelcomeScreen/WelcomeScreen'
import { Toast } from './components/Toast/Toast'
import { ModifiedBanner } from './components/ModifiedBanner/ModifiedBanner'
import { DropOverlay } from './components/DropOverlay/DropOverlay'
import { useDragDrop } from './hooks/useDragDrop'
export default function App() {
const tabs = useTabStore(s => s.tabs)
const activeTabId = useTabStore(s => s.activeTabId)
const getActiveTab = useTabStore(s => s.getActiveTab)
const createTab = useTabStore(s => s.createTab)
const setModified = useTabStore(s => s.setModified)
const updateTabContent = useTabStore(s => s.updateTabContent)
const viewMode = useEditorStore(s => s.viewMode)
const splitRatio = useEditorStore(s => s.splitRatio)
const { darkMode, toggleDarkMode } = useTheme()
const { saveViewMode, saveSplitRatio } = useSettings()
const [showModifiedBanner, setShowModifiedBanner] = useState(false)
const [modifiedFilePath, setModifiedFilePath] = useState<string | null>(null)
const [toastMessage, setToastMessage] = useState<string | null>(null)
const showToast = useCallback((msg: string) => {
setToastMessage(msg)
setTimeout(() => setToastMessage(null), 3000)
}, [])
// 拖拽打开
useDragDrop(showToast)
// 文件修改检测
useFileWatch()
// 未保存提醒
useUnsavedWarning(() => tabs.some(t => t.isModified))
// 文件外部修改事件
useEffect(() => {
const handler = (e: Event) => {
const filePath = (e as CustomEvent).detail
const tab = getActiveTab()
if (tab && tab.filePath === filePath) {
setShowModifiedBanner(true)
setModifiedFilePath(filePath)
}
}
window.addEventListener('file-externally-modified', handler)
return () => window.removeEventListener('file-externally-modified', handler)
}, [getActiveTab])
// 打开文件
const handleOpenFile = useCallback(async () => {
if (window.electronAPI) {
const result = await window.electronAPI.openFile()
if (result && 'filePath' in result) {
createTab(result.filePath, result.content)
}
}
}, [createTab])
// 保存文件
const handleSave = useCallback(async () => {
const tab = getActiveTab()
if (!tab) return
if (window.electronAPI) {
const result = await window.electronAPI.saveFile({
filePath: tab.filePath,
content: tab.content
})
if (result.success) {
useTabStore.getState().setModified(tab.id, false)
showToast('已保存')
}
}
}, [getActiveTab, showToast])
// 另存为
const handleSaveAs = useCallback(async () => {
const tab = getActiveTab()
if (!tab) return
if (window.electronAPI) {
const result = await window.electronAPI.saveFileAs({ content: tab.content })
if (result.success) {
useTabStore.getState().setModified(tab.id, false)
showToast('已保存')
}
}
}, [getActiveTab, showToast])
// 快捷键
useKeyboard(handleOpenFile, handleSave, handleSaveAs)
// 视图模式切换
const handleViewModeChange = useCallback((mode: 'split' | 'editor' | 'preview') => {
saveViewMode(mode)
}, [saveViewMode])
// 主进程事件
useEffect(() => {
if (!window.electronAPI) return
window.electronAPI.removeAllListeners('file:openInTab')
window.electronAPI.removeAllListeners('menu:save')
window.electronAPI.removeAllListeners('menu:saveAs')
window.electronAPI.onFileOpenInTab((data: { filePath: string; content: string }) => {
createTab(data.filePath, data.content)
})
window.electronAPI.onMenuSave(() => handleSave())
window.electronAPI.onMenuSaveAs(() => handleSaveAs())
}, [createTab, handleSave, handleSaveAs])
const activeTab = getActiveTab()
const hasTabs = tabs.length > 0
// 分屏样式
const editorStyle: React.CSSProperties = viewMode === 'split'
? { flex: `0 0 ${splitRatio}%` }
: {}
const previewStyle: React.CSSProperties = viewMode === 'split'
? { flex: `0 0 ${100 - splitRatio}%` }
: {}
return (
<div id="app" className={`mode-${viewMode}`}>
<Toolbar
onOpen={handleOpenFile}
onSave={handleSave}
viewMode={viewMode}
onViewModeChange={handleViewModeChange}
darkMode={darkMode}
onToggleDark={toggleDarkMode}
/>
<div id="workspace">
<Sidebar />
<div id="main-content">
<TabBar />
{showModifiedBanner && (
<ModifiedBanner
onReload={() => {
if (window.electronAPI && modifiedFilePath) {
window.electronAPI.reloadFile().then(result => {
if (result.success && result.content) {
const tab = getActiveTab()
if (tab) {
updateTabContent(tab.id, result.content)
setModified(tab.id, false)
}
}
setShowModifiedBanner(false)
})
}
}}
onDismiss={() => setShowModifiedBanner(false)}
/>
)}
{hasTabs ? (
<div id="content-wrapper">
{viewMode !== 'preview' && (
<div id="editor-panel" style={editorStyle}>
<SearchBar />
<Editor />
</div>
)}
{viewMode !== 'editor' && <div id="resizer" />}
{viewMode !== 'editor' && (
<div id="preview-panel" style={previewStyle}>
<Preview />
</div>
)}
</div>
) : (
<WelcomeScreen
onOpen={handleOpenFile}
onNew={() => createTab(null, '')}
/>
)}
</div>
</div>
<StatusBar />
{toastMessage && <Toast message={toastMessage} />}
<DropOverlay />
</div>
)
}