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(null) const [toastMessage, setToastMessage] = useState(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]) // M-11: 主进程事件注册(使用 ref 保持最新回调引用 + cleanup 清理) const handleSaveRef = useRef(handleSave) const handleSaveAsRef = useRef(handleSaveAs) handleSaveRef.current = handleSave handleSaveAsRef.current = handleSaveAs useEffect(() => { if (!window.electronAPI) return const api = window.electronAPI const onOpen = (data: { filePath: string; content: string }) => { createTab(data.filePath, data.content) } const onSave = () => handleSaveRef.current() const onSaveAs = () => handleSaveAsRef.current() api.onFileOpenInTab(onOpen) api.onMenuSave(onSave) api.onMenuSaveAs(onSaveAs) return () => { api.removeAllListeners('file:openInTab') api.removeAllListeners('menu:save') api.removeAllListeners('menu:saveAs') } }, [createTab]) 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 (
{showModifiedBanner && ( { 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 ? (
{viewMode !== 'preview' && (
)} {viewMode !== 'editor' && } {viewMode !== 'editor' && (
)}
) : ( createTab(null, '')} /> )}
{toastMessage && }
) } // B-08: 分屏调节器组件 function Resizer({ onResize }: { onResize: (ratio: number) => void }) { const handleMouseDown = useCallback((e: React.MouseEvent) => { e.preventDefault() document.body.style.cursor = 'col-resize' document.body.style.userSelect = 'none' const handleMouseMove = (ev: MouseEvent) => { const wrapper = document.getElementById('content-wrapper') if (!wrapper) return const rect = wrapper.getBoundingClientRect() const pct = Math.max(20, Math.min(80, ((ev.clientX - rect.left) / rect.width) * 100)) const editorPanel = document.getElementById('editor-panel') const previewPanel = document.getElementById('preview-panel') if (editorPanel) editorPanel.style.flex = `0 0 ${pct}%` if (previewPanel) previewPanel.style.flex = `0 0 ${100 - pct}%` } const handleMouseUp = (ev: MouseEvent) => { document.removeEventListener('mousemove', handleMouseMove) document.removeEventListener('mouseup', handleMouseUp) document.body.style.cursor = '' document.body.style.userSelect = '' // 保存最终比例 const wrapper = document.getElementById('content-wrapper') if (wrapper) { const rect = wrapper.getBoundingClientRect() const pct = Math.max(20, Math.min(80, ((ev.clientX - rect.left) / rect.width) * 100)) onResize(pct) } } document.addEventListener('mousemove', handleMouseMove) document.addEventListener('mouseup', handleMouseUp) }, [onResize]) return
}