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);
}, []);