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 -19
View File
@@ -22,8 +22,7 @@ export function Editor({ darkMode }: EditorProps) {
getScrollTop,
setScrollTop,
getSelection,
setSelection,
setLineAtTop
setSelection
} = useCodeMirror({
content: activeTab?.content ?? '',
onChange: useCallback((value: string) => {
@@ -31,25 +30,9 @@ export function Editor({ darkMode }: EditorProps) {
updateTabContent(activeTabId, value)
setModified(activeTabId, true)
}, [activeTabId, updateTabContent, setModified]),
darkMode,
onScroll: useCallback((line: number) => {
// 派发行号给预览侧做滚动同步
window.dispatchEvent(new CustomEvent('editor-scroll', { detail: { line } }))
}, [])
darkMode
})
// 反向同步:监听预览滚动事件
useEffect(() => {
const handlePreviewScroll = (e: Event) => {
const line = (e as CustomEvent).detail?.line
if (typeof line === 'number') {
setLineAtTop(Math.floor(line) + 1) // setLineAtTop 是 1-indexed
}
}
window.addEventListener('preview-scroll', handlePreviewScroll)
return () => window.removeEventListener('preview-scroll', handlePreviewScroll)
}, [setLineAtTop])
// 切换标签时加载内容
useEffect(() => {
if (!activeTab) return
@@ -11,10 +11,9 @@ interface UseCodeMirrorOptions {
content: string
onChange: (value: string) => void
darkMode: boolean
onScroll?: (line: number) => void
}
export function useCodeMirror({ content, onChange, darkMode, onScroll }: UseCodeMirrorOptions) {
export function useCodeMirror({ content, onChange, darkMode }: UseCodeMirrorOptions) {
const containerRef = useRef<HTMLDivElement>(null)
const viewRef = useRef<EditorView | null>(null)
const isExternalUpdate = useRef(false)
@@ -81,23 +80,7 @@ export function useCodeMirror({ content, onChange, darkMode, onScroll }: UseCode
viewRef.current = view
// 滚动事件监听:派发行号(支持小数表示行内偏移)
const handleScroll = () => {
if (onScroll) {
const dom = view.scrollDOM
const scrollTop = dom.scrollTop
const block = view.lineBlockAtHeight(scrollTop)
const line = view.state.doc.lineAt(block.from)
const fraction = block.height > 0
? (scrollTop - block.top) / block.height
: 0
onScroll(line.number - 1 + Math.max(0, fraction)) // 0-indexed + 行内比例
}
}
view.scrollDOM.addEventListener('scroll', handleScroll)
return () => {
view.scrollDOM.removeEventListener('scroll', handleScroll)
view.destroy()
viewRef.current = null
}
@@ -116,12 +99,6 @@ export function useCodeMirror({ content, onChange, darkMode, onScroll }: UseCode
isExternalUpdate.current = false
}
}, [])
// 获取当前内容
const getContent = useCallback(() => {
return viewRef.current?.state.doc.toString() ?? ''
}, [])
// 获取/设置滚动位置
const getScrollTop = useCallback(() => {
return viewRef.current?.scrollDOM.scrollTop ?? 0
@@ -147,35 +124,13 @@ export function useCodeMirror({ content, onChange, darkMode, onScroll }: UseCode
view.dispatch({ selection: { anchor: from, head: to } })
view.focus()
}, [])
// 滚动到指定行
const scrollTo = useCallback((pos: number) => {
const view = viewRef.current
if (!view) return
view.dispatch({ effects: EditorView.scrollIntoView(pos, { y: 'center' }) })
}, [])
// 滚动使指定行出现在编辑器顶部(反向同步用)
const setLineAtTop = useCallback((lineNumber: number) => {
const view = viewRef.current
if (!view) return
const line = Math.max(1, Math.min(lineNumber, view.state.doc.lines))
const lineInfo = view.state.doc.line(line)
const block = view.lineBlockAt(lineInfo.from)
const currentTop = view.lineBlockAtHeight(view.scrollDOM.scrollTop).top
view.scrollDOM.scrollTop += block.top - currentTop
}, [])
return {
containerRef,
viewRef,
setContent,
getContent,
getScrollTop,
setScrollTop,
getSelection,
setSelection,
scrollTo,
setLineAtTop
setSelection
}
}