From d28861ff9cae255705bfbf3bfb88f90c92e8c3d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E8=88=AA=E5=AE=87?= <3364451258@qq.com> Date: Tue, 26 May 2026 00:51:32 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=8A=BD=E5=8F=96=20Modal=20?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=E3=80=81=E6=94=AF=E6=8C=81=20JSON/CSV=20?= =?UTF-8?q?=E5=AF=BC=E5=87=BA=E3=80=81=E6=B8=85=E7=90=86=E5=86=97=E4=BD=99?= =?UTF-8?q?=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 Modal 组件,消除 3 个 Dialog 中重复的遮罩层/Escape/stopPropagation 代码 - PathEditDialog/HelpDialog/ImportDialog 改用 Modal 包裹 - handleExport 支持 JSON/CSV 两种格式(CSV 导出代码之前存在但从未接线) - App.tsx 移除冗余的 initDarkMode 后重复设 store 的逻辑 - ErrorBoundary 添加 componentDidCatch 日志和 console.error Co-Authored-By: Claude Opus 4.7 --- src/App.tsx | 6 +- src/components/dialogs/HelpDialog.tsx | 50 +++---------- src/components/dialogs/ImportDialog.tsx | 87 ++++------------------- src/components/dialogs/PathEditDialog.tsx | 73 ++++++------------- src/components/layout/ErrorBoundary.tsx | 5 ++ src/components/ui/Modal.tsx | 34 +++++++++ src/hooks/use-app-actions.ts | 14 ++-- 7 files changed, 94 insertions(+), 175 deletions(-) create mode 100644 src/components/ui/Modal.tsx diff --git a/src/App.tsx b/src/App.tsx index 8a3e3d8..71ab478 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,6 @@ import { useEffect } from 'react'; import { useAppStore } from '@/store/app-store'; -import { initDarkMode, useThemeStore } from '@/store/theme-store'; +import { initDarkMode } from '@/store/theme-store'; import { AppShell } from '@/components/layout/AppShell'; import { ErrorBoundary } from '@/components/layout/ErrorBoundary'; @@ -9,10 +9,6 @@ export default function App() { useEffect(() => { initDarkMode(); - const saved = localStorage.getItem('darkMode'); - if (saved === '1') { - useThemeStore.setState({ isDark: true }); - } initialize(); }, [initialize]); diff --git a/src/components/dialogs/HelpDialog.tsx b/src/components/dialogs/HelpDialog.tsx index 8327385..7428d6e 100644 --- a/src/components/dialogs/HelpDialog.tsx +++ b/src/components/dialogs/HelpDialog.tsx @@ -1,50 +1,20 @@ -import { useEffect } from 'react'; import { useTranslation } from 'react-i18next'; +import { Modal } from '@/components/ui/Modal'; -interface HelpDialogProps { - open: boolean; - onClose: () => void; -} +interface HelpDialogProps { open: boolean; onClose: () => void; } export function HelpDialog({ open, onClose }: HelpDialogProps) { const { t } = useTranslation(); - useEffect(() => { - if (!open) return; - const handler = (e: KeyboardEvent) => { - if (e.key === 'Escape') onClose(); - }; - window.addEventListener('keydown', handler); - return () => window.removeEventListener('keydown', handler); - }, [open, onClose]); - - if (!open) return null; - return ( -
-
e.stopPropagation()} - > -

{t('dialog.helpTitle')}

-
-          {t('help.content')}
-        
-
- -
+ +

{t('dialog.helpTitle')}

+
{t('help.content')}
+
+
-
+ ); } diff --git a/src/components/dialogs/ImportDialog.tsx b/src/components/dialogs/ImportDialog.tsx index 47978db..7c07259 100644 --- a/src/components/dialogs/ImportDialog.tsx +++ b/src/components/dialogs/ImportDialog.tsx @@ -1,5 +1,5 @@ -import { useEffect } from 'react'; import { useTranslation } from 'react-i18next'; +import { Modal } from '@/components/ui/Modal'; interface ImportDialogProps { open: boolean; @@ -9,80 +9,23 @@ interface ImportDialogProps { onCancel: () => void; } -export function ImportDialog({ - open, - systemCount, - userCount, - onSelect, - onCancel, -}: ImportDialogProps) { +export function ImportDialog({ open, systemCount, userCount, onSelect, onCancel }: ImportDialogProps) { const { t } = useTranslation(); - useEffect(() => { - if (!open) return; - const handler = (e: KeyboardEvent) => { - if (e.key === 'Escape') onCancel(); - }; - window.addEventListener('keydown', handler); - return () => window.removeEventListener('keydown', handler); - }, [open, onCancel]); - - if (!open) return null; - return ( -
-
e.stopPropagation()} - > -

{t('dialog.importTarget')}

-

- {systemCount > 0 && `系统变量: ${systemCount} 条`} - {systemCount > 0 && userCount > 0 && ' | '} - {userCount > 0 && `用户变量: ${userCount} 条`} -

-
- {systemCount > 0 && ( - - )} - {userCount > 0 && ( - - )} - {systemCount > 0 && userCount > 0 && ( - - )} - -
+ +

{t('dialog.importTarget')}

+

+ {systemCount > 0 && `系统变量: ${systemCount} 条`} + {systemCount > 0 && userCount > 0 && ' | '} + {userCount > 0 && `用户变量: ${userCount} 条`} +

+
+ {systemCount > 0 && } + {userCount > 0 && } + {systemCount > 0 && userCount > 0 && } +
-
+ ); } diff --git a/src/components/dialogs/PathEditDialog.tsx b/src/components/dialogs/PathEditDialog.tsx index e8f4326..7fdbffb 100644 --- a/src/components/dialogs/PathEditDialog.tsx +++ b/src/components/dialogs/PathEditDialog.tsx @@ -1,5 +1,6 @@ import { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; +import { Modal } from '@/components/ui/Modal'; interface PathEditDialogProps { open: boolean; @@ -13,61 +14,27 @@ export function PathEditDialog({ open, title, initialValue, onConfirm, onCancel const { t } = useTranslation(); const [value, setValue] = useState(initialValue); - // 每次打开时同步 initialValue(解决 React 复用实例导致空白的问题) - useEffect(() => { - if (open) { - setValue(initialValue); - } - }, [open, initialValue]); - - if (!open) return null; + useEffect(() => { if (open) setValue(initialValue); }, [open, initialValue]); return ( -
-
e.stopPropagation()} - > -

{title}

- - setValue(e.target.value)} - onKeyDown={(e) => { - if (e.key === 'Enter') onConfirm(value); - if (e.key === 'Escape') onCancel(); - }} - className="w-full px-3 py-2 rounded border text-sm outline-none" - style={{ - backgroundColor: 'var(--app-list-bg)', - color: 'var(--app-fg)', - borderColor: 'var(--app-border)', - }} - /> -
- - -
+ +

{title}

+ + setValue(e.target.value)} + onKeyDown={(e) => { if (e.key === 'Enter') onConfirm(value); }} + className="w-full min-w-[400px] px-3 py-2 rounded border text-sm outline-none" + style={{ backgroundColor: 'var(--app-list-bg)', color: 'var(--app-fg)', borderColor: 'var(--app-border)' }} + /> +
+ +
-
+ ); } diff --git a/src/components/layout/ErrorBoundary.tsx b/src/components/layout/ErrorBoundary.tsx index f5503fb..3aa9a0f 100644 --- a/src/components/layout/ErrorBoundary.tsx +++ b/src/components/layout/ErrorBoundary.tsx @@ -7,9 +7,14 @@ export class ErrorBoundary extends Component { state: State = { hasError: false, error: '' }; static getDerivedStateFromError(e: Error): State { + console.error('[ErrorBoundary]', e); return { hasError: true, error: e.message }; } + componentDidCatch(_e: Error, info: React.ErrorInfo) { + console.error('[ErrorBoundary] componentStack:', info.componentStack); + } + render() { if (this.state.hasError) { return ( diff --git a/src/components/ui/Modal.tsx b/src/components/ui/Modal.tsx new file mode 100644 index 0000000..ab4321c --- /dev/null +++ b/src/components/ui/Modal.tsx @@ -0,0 +1,34 @@ +import { useEffect, type ReactNode } from 'react'; + +interface ModalProps { + open: boolean; + onClose: () => void; + children: ReactNode; +} + +export function Modal({ open, onClose, children }: ModalProps) { + useEffect(() => { + if (!open) return; + const handler = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); }; + window.addEventListener('keydown', handler); + return () => window.removeEventListener('keydown', handler); + }, [open, onClose]); + + if (!open) return null; + + return ( +
+
e.stopPropagation()} + > + {children} +
+
+ ); +} diff --git a/src/hooks/use-app-actions.ts b/src/hooks/use-app-actions.ts index 8c33d2d..7dd2bdf 100644 --- a/src/hooks/use-app-actions.ts +++ b/src/hooks/use-app-actions.ts @@ -2,7 +2,7 @@ import { useCallback, useEffect } from 'react'; import { useAppStore } from '@/store/app-store'; import { TargetType } from '@/core/undo-redo'; import { open } from '@tauri-apps/plugin-dialog'; -import { importFromContent, exportToJson, flattenImportResult } from '@/core/import-export'; +import { importFromContent, exportToJson, exportToCsv, flattenImportResult } from '@/core/import-export'; import { is_valid_path_format } from '@/core/validation'; import { useKeyboard } from './use-keyboard'; import i18n from '@/i18n'; @@ -101,14 +101,18 @@ export function useAppActions(activeTab: TabId, dialogs: DialogState) { input.click(); }, [setImportDialog]); - const handleExport = useCallback(() => { + const handleExport = useCallback((format: 'json' | 'csv' = 'json') => { const state = useAppStore.getState(); - const content = exportToJson({ system: state.sysPaths, user: state.userPaths }); - const blob = new Blob([content], { type: 'application/json' }); + const data = { system: state.sysPaths, user: state.userPaths }; + const isCsv = format === 'csv'; + const content = isCsv ? exportToCsv(data) : exportToJson(data); + const mime = isCsv ? 'text/csv' : 'application/json'; + const ext = isCsv ? '.csv' : '.json'; + const blob = new Blob([isCsv ? '' : '', content], { type: mime }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; - a.download = 'patheditor_export.json'; + a.download = `patheditor_export${ext}`; a.click(); URL.revokeObjectURL(url); }, []);