diff --git a/CLAUDE.md b/CLAUDE.md index b984467..54457f6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -92,3 +92,4 @@ tests/unit/ # Vitest 前端单元测试 - `.cargo/config.toml` 添加了 `-lmcfgthread` 兼容 GCC 15.2.0 MinGW - 移除 `cdylib` crate-type 避免 DLL 导出序数溢出 - 运行需要管理员权限才能编辑系统 PATH +- `cargo test` 需要 MinGW bin 在 PATH 中(GCC 15.2.0 运行时依赖 `libmcfgthread-2.dll`),开发模式下可用 `npx tauri dev` 替代 diff --git a/src-tauri/src/error.rs b/src-tauri/src/error.rs index 59e8272..ec8950c 100644 --- a/src-tauri/src/error.rs +++ b/src-tauri/src/error.rs @@ -1,6 +1,6 @@ use serde::Serialize; -/// 传给前端的统一错误类型 +/// 传给前端的统一错误类型(保留供未来使用,当前命令返回 Result) #[derive(Debug, Serialize)] pub struct AppError { pub message: String, diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index ad892a2..d2468e2 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -4,6 +4,7 @@ mod error; #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() + .plugin(tauri_plugin_dialog::init()) .setup(|app| { if cfg!(debug_assertions) { app.handle().plugin( diff --git a/src/App.tsx b/src/App.tsx index a80bf11..8a3e3d8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,6 +2,7 @@ import { useEffect } from 'react'; import { useAppStore } from '@/store/app-store'; import { initDarkMode, useThemeStore } from '@/store/theme-store'; import { AppShell } from '@/components/layout/AppShell'; +import { ErrorBoundary } from '@/components/layout/ErrorBoundary'; export default function App() { const initialize = useAppStore((s) => s.initialize); @@ -15,5 +16,9 @@ export default function App() { initialize(); }, [initialize]); - return ; + return ( + + + + ); } diff --git a/src/components/layout/AppShell.tsx b/src/components/layout/AppShell.tsx index 231fd6e..d9e8169 100644 --- a/src/components/layout/AppShell.tsx +++ b/src/components/layout/AppShell.tsx @@ -4,6 +4,7 @@ import { useThemeStore } from '@/store/theme-store'; import { useTranslation } from 'react-i18next'; import i18n from '@/i18n'; import { TargetType } from '@/core/undo-redo'; +import { open } from '@tauri-apps/plugin-dialog'; import { importFromContent, exportToJson, flattenImportResult } from '@/core/import-export'; import { StatusBar } from './StatusBar'; import { TitleBar } from './TitleBar'; @@ -55,19 +56,11 @@ export function AppShell() { } }, [selectedIndices, getCurrentTarget]); - const handleBrowse = useCallback(() => { - // Tauri native dialog (简化版 — 后续可增强) - const input = document.createElement('input'); - input.type = 'file'; - input.webkitdirectory = true; - input.onchange = (e) => { - const files = (e.target as HTMLInputElement).files; - if (files && files.length > 0) { - const path = (files[0] as any).path || files[0].name; - useAppStore.getState().addPath(path, getCurrentTarget()); - } - }; - input.click(); + const handleBrowse = useCallback(async () => { + const selected = await open({ directory: true, multiple: false }); + if (selected && typeof selected === 'string') { + useAppStore.getState().addPath(selected, getCurrentTarget()); + } }, [getCurrentTarget]); const handleDelete = useCallback(() => { @@ -103,9 +96,10 @@ export function AppShell() { input.accept = '.json,.csv,.txt'; input.onchange = async (e) => { const file = (e.target as HTMLInputElement).files?.[0]; - if (!file) return; + if (!file) { input.remove(); return; } const content = await file.text(); const result = importFromContent(content, file.name); + input.remove(); if (result.system.length > 0 && result.user.length > 0) { setImportDialog({ open: true, system: result.system, user: result.user }); @@ -136,28 +130,7 @@ export function AppShell() { }, []); const handleSave = useCallback(() => { - const state = useAppStore.getState(); - const sysJoined = state.sysPaths.join(';'); - const userJoined = state.userPaths.join(';'); - const combined = sysJoined + ';' + userJoined; - - const warnings: string[] = []; - if (sysJoined.length > 2048) { - warnings.push(`系统 PATH 长度 ${sysJoined.length} 超过建议值 2048`); - } - if (userJoined.length > 2048) { - warnings.push(`用户 PATH 长度 ${userJoined.length} 超过建议值 2048`); - } - if (combined.length > 8191) { - warnings.push(`合并 PATH 长度 ${combined.length} 超过命令行安全限制 8191`); - } - - if (warnings.length > 0) { - const msg = warnings.join('\n') + '\n\n是否继续保存?'; - if (!window.confirm(msg)) return; - } - - state.savePaths(); + useAppStore.getState().savePaths(); }, []); // ── 键盘快捷键 ── @@ -260,7 +233,11 @@ export function AppShell() { onImport={handleImport} onExport={handleExport} onSave={handleSave} - onCancel={() => window.close()} + onCancel={() => { + const state = useAppStore.getState(); + if (state.isModified && !window.confirm('有未保存的修改,确定退出吗?')) return; + window.close(); + }} onHelp={() => setHelpOpen(true)} onLanguage={() => { const current = localStorage.getItem('i18nextLng') || 'zh-CN'; diff --git a/src/components/layout/ErrorBoundary.tsx b/src/components/layout/ErrorBoundary.tsx new file mode 100644 index 0000000..f5503fb --- /dev/null +++ b/src/components/layout/ErrorBoundary.tsx @@ -0,0 +1,33 @@ +import { Component, type ReactNode } from 'react'; + +interface Props { children: ReactNode; } +interface State { hasError: boolean; error: string; } + +export class ErrorBoundary extends Component { + state: State = { hasError: false, error: '' }; + + static getDerivedStateFromError(e: Error): State { + return { hasError: true, error: e.message }; + } + + render() { + if (this.state.hasError) { + return ( +
+
+

应用出错

+

{this.state.error}

+ +
+
+ ); + } + return this.props.children; + } +} diff --git a/src/components/layout/StatusBar.tsx b/src/components/layout/StatusBar.tsx index 2f39847..8f230e8 100644 --- a/src/components/layout/StatusBar.tsx +++ b/src/components/layout/StatusBar.tsx @@ -9,6 +9,7 @@ export function StatusBar() { const isAdmin = useAppStore((s) => s.isAdmin); const isModified = useAppStore((s) => s.isModified); const isDark = useThemeStore((s) => s.isDark); + const hasError = statusMessage.includes(t('status.error')); return (