refactor: 删除分屏功能 + 代码审计修复 + 安全加固

## 删除分屏功能
- 删除 ViewMode 'split' 类型,仅保留 editor/preview
- 删除 splitRatio 状态和 Resizer 组件
- 删除滚动同步模块 (scrollSync.ts, rehypeSourceLine.ts)
- 更新快捷键: Ctrl+1 编辑, Ctrl+2 预览

## Bug 修复
- 修复 Toast setTimeout 内存泄漏 (App.tsx)
- 修复 ModifiedBanner reload 更新错误标签 (改用 modifiedFilePath 匹配)
- 修复 FileWatcher error 未重置 isSelfWriting (file-watcher.ts)
- 修复另存为时未 stop watcher (ipc-handlers.ts)

## 死代码清理 (10 项)
- 删除 main/ipc-channels.ts (与 shared/ 重复)
- 删除 main/file-system.ts 未使用的 formatBytes
- 删除 constants.ts 6 个未使用常量
- 删除 fileUtils.ts 未使用的 formatBytes
- 删除 Icons.tsx 5 个未使用图标 (SplitView/ChevronUp/ChevronDown/X/ArrowUp/ArrowDown)
- 删除 useCodeMirror 未使用的 getContent/scrollTo
- 删除 TabBar 未使用的 menuRef
- 删除 Tab.scrollLeft/previewScrollTop 字段
- 删除 tabStore 未使用的 getTabIndex
- 删除 Toast/ModifiedBanner 多余 React import

## 安全加固
- ipc-handlers: 添加路径遍历防护 (validatePath 函数)
- preload: openExternal 仅允许 http/https 协议
- window-manager: 启用 sandbox: true
- preload: removeAllListeners 改为精确取消订阅 (返回 Unsubscribe 函数)

## 状态持久化
- activeTabId 持久化到 IndexedDB (刷新后恢复正确标签)
- sidebar 状态持久化到 IndexedDB (isVisible/sidebarWidth)

## 代码优化
- preload 使用 IPC_CHANNELS 常量替代硬编码字符串
- ipc-handlers _event 类型改为 IpcMainInvokeEvent
- settingsRepository 删除 splitRatio 字段
This commit is contained in:
thzxx
2026-05-28 13:42:11 +08:00
parent 3c6e4ac5ce
commit 9c92dcfa9d
37 changed files with 242 additions and 655 deletions
+2 -4
View File
@@ -7,15 +7,13 @@ export function useFileWatch() {
useEffect(() => {
if (!window.electronAPI) return
window.electronAPI.onExternalModification((filePath: string) => {
const unsubscribe = window.electronAPI.onExternalModification((filePath: string) => {
const tab = getActiveTab()
if (tab && tab.filePath === filePath) {
window.dispatchEvent(new CustomEvent('file-externally-modified', { detail: filePath }))
}
})
return () => {
window.electronAPI?.removeAllListeners('file:externallyModified')
}
return unsubscribe
}, [getActiveTab])
}
+2 -3
View File
@@ -12,9 +12,8 @@ export function useKeyboard(handleOpenFile: () => void, handleSave: () => void,
if (isCtrl && e.key === 'o') { e.preventDefault(); handleOpenFile(); return }
if (isCtrl && e.key === 's' && !e.shiftKey) { e.preventDefault(); handleSave(); return }
if (isCtrl && e.shiftKey && e.key === 'S') { e.preventDefault(); handleSaveAs(); return }
if (isCtrl && e.key === '1') { e.preventDefault(); setViewMode('split'); return }
if (isCtrl && e.key === '2') { e.preventDefault(); setViewMode('editor'); return }
if (isCtrl && e.key === '3') { e.preventDefault(); setViewMode('preview'); return }
if (isCtrl && e.key === '1') { e.preventDefault(); setViewMode('editor'); return }
if (isCtrl && e.key === '2') { e.preventDefault(); setViewMode('preview'); return }
const tabState = useTabStore.getState()
if (isCtrl && e.key === 't') { e.preventDefault(); tabState.createTab(null, ''); return }
+3 -11
View File
@@ -5,17 +5,14 @@ import type { ViewMode } from '../types/settings'
export function useSettings() {
const viewMode = useEditorStore(s => s.viewMode)
const splitRatio = useEditorStore(s => s.splitRatio)
const setViewMode = useEditorStore(s => s.setViewMode)
const setSplitRatio = useEditorStore(s => s.setSplitRatio)
// 初始化
useEffect(() => {
settingsRepository.load().then(settings => {
setViewMode(settings.viewMode ?? 'split')
setSplitRatio(settings.splitRatio ?? 50)
setViewMode(settings.viewMode ?? 'editor')
})
}, [setViewMode, setSplitRatio])
}, [setViewMode])
// M-10: 使用 useCallback 稳定函数引用
const saveViewMode = useCallback((mode: ViewMode) => {
@@ -23,10 +20,5 @@ export function useSettings() {
settingsRepository.save({ viewMode: mode })
}, [setViewMode])
const saveSplitRatio = useCallback((ratio: number) => {
setSplitRatio(ratio)
settingsRepository.save({ splitRatio: ratio })
}, [setSplitRatio])
return { viewMode, splitRatio, saveViewMode, saveSplitRatio }
return { viewMode, saveViewMode }
}
+2 -4
View File
@@ -15,7 +15,7 @@ export function useUnsavedWarning(hasUnsaved: () => boolean) {
const api = window.electronAPI
api.onConfirmClose(() => {
const unsubscribe = api.onConfirmClose(() => {
if (!hasUnsaved()) {
api.forceClose()
return
@@ -28,8 +28,6 @@ export function useUnsavedWarning(hasUnsaved: () => boolean) {
}
})
return () => {
api.removeAllListeners('window:confirmClose')
}
return unsubscribe
}, [hasUnsaved])
}