refactor: AppShell 拆分 + savePaths 并行化

- 抽取 useAppActions hook(~160行),AppShell 从 306 行精简至 105 行
- AppShell 现在只负责布局和渲染,操作逻辑全部可单独测试
- savePaths 改为 Promise.allSettled 并行保存 + 并行备份
- useKeyboard 通过 hook 内部集成,不再暴露给 AppShell

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-26 00:36:59 +08:00
parent 3a21891f84
commit e6a2416271
3 changed files with 211 additions and 232 deletions
+171
View File
@@ -0,0 +1,171 @@
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 { useKeyboard } from './use-keyboard';
import i18n from '@/i18n';
import type { TabId } from '@/store/app-store';
export interface DialogState {
editDialog: { open: boolean; index: number; value: string; target: TargetType };
newDialog: boolean;
helpOpen: boolean;
importDialog: { open: boolean; system: string[]; user: string[] };
setEditDialog: (v: DialogState['editDialog']) => void;
setNewDialog: (v: boolean) => void;
setHelpOpen: (v: boolean) => void;
setImportDialog: (v: DialogState['importDialog']) => void;
}
export function useAppActions(activeTab: TabId, dialogs: DialogState) {
const { setEditDialog, setNewDialog, setHelpOpen, setImportDialog } = dialogs;
const getCurrentTarget = useCallback((): TargetType => {
return activeTab === 'user' ? TargetType.USER : TargetType.SYSTEM;
}, [activeTab]);
// ── CRUD ──
const handleNew = useCallback(() => setNewDialog(true), [setNewDialog]);
const handleEdit = useCallback(() => {
const idx = useAppStore.getState().selectedIndices[0];
if (idx === undefined) return;
const target = activeTab === 'user' ? TargetType.USER : TargetType.SYSTEM;
const list = target === TargetType.SYSTEM
? useAppStore.getState().sysPaths
: useAppStore.getState().userPaths;
const value = list[idx];
if (value) setEditDialog({ open: true, index: idx, value, target });
}, [activeTab, setEditDialog]);
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(() => {
const idx = useAppStore.getState().selectedIndices;
if (idx.length === 0) return;
useAppStore.getState().deletePaths(idx, getCurrentTarget());
}, [getCurrentTarget]);
const handleMoveUp = useCallback(() => {
const idx = useAppStore.getState().selectedIndices[0];
if (idx === undefined) return;
useAppStore.getState().moveUp(idx, getCurrentTarget());
}, [getCurrentTarget]);
const handleMoveDown = useCallback(() => {
const idx = useAppStore.getState().selectedIndices[0];
if (idx === undefined) return;
useAppStore.getState().moveDown(idx, getCurrentTarget());
}, [getCurrentTarget]);
const handleClean = useCallback(() => {
const removed = useAppStore.getState().cleanPaths(
getCurrentTarget(),
(p) => p.includes('%') || p.includes('\\') || p.includes('/') || /^[a-zA-Z]:[/\\]/.test(p),
);
if (removed.length > 0) {
useAppStore.getState().setStatusMessage(
i18n.t('status.deleted', { count: removed.length }),
);
}
}, [getCurrentTarget]);
// ── 导入导出 ──
const handleImport = useCallback(() => {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json,.csv,.txt';
input.onchange = async (e) => {
const file = (e.target as HTMLInputElement).files?.[0];
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 });
} else if (result.system.length > 0) {
useAppStore.getState().importPaths(TargetType.SYSTEM, result.system);
} else if (result.user.length > 0) {
useAppStore.getState().importPaths(TargetType.USER, result.user);
}
};
input.click();
}, [setImportDialog]);
const handleExport = useCallback(() => {
const state = useAppStore.getState();
const content = exportToJson({ system: state.sysPaths, user: state.userPaths });
const blob = new Blob([content], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'patheditor_export.json';
a.click();
URL.revokeObjectURL(url);
}, []);
const handleSave = useCallback(() => {
useAppStore.getState().savePaths();
}, []);
// ── 键盘 ──
useKeyboard({
onNew: handleNew,
onSave: handleSave,
onDelete: handleDelete,
onUndo: () => useAppStore.getState().undo(),
onRedo: () => useAppStore.getState().redo(),
onHelp: () => setHelpOpen(true),
});
// ── 双击编辑 ──
useEffect(() => {
const handler = (e: Event) => {
const detail = (e as CustomEvent).detail;
if (detail && typeof detail.index === 'number') {
const target = getCurrentTarget();
setEditDialog({ open: true, index: detail.index, value: detail.path, target });
}
};
window.addEventListener('path-dblclick', handler);
return () => window.removeEventListener('path-dblclick', handler);
}, [getCurrentTarget, setEditDialog]);
// ── 弹窗确认 ──
const handleNewConfirm = useCallback((value: string) => {
setNewDialog(false);
if (value.trim()) useAppStore.getState().addPath(value.trim(), getCurrentTarget());
}, [getCurrentTarget, setNewDialog]);
const handleEditConfirm = useCallback((value: string) => {
const d = dialogs.editDialog;
setEditDialog({ open: false, index: -1, value: '', target: TargetType.SYSTEM });
if (value.trim()) useAppStore.getState().editPath(d.index, value.trim(), d.target);
}, [dialogs.editDialog, setEditDialog]);
const handleImportSelect = useCallback((target: 'system' | 'user' | 'both') => {
const { system, user } = dialogs.importDialog;
const flat = flattenImportResult({ system, user }, target);
if (flat.system.length > 0) useAppStore.getState().importPaths(TargetType.SYSTEM, flat.system);
if (flat.user.length > 0) useAppStore.getState().importPaths(TargetType.USER, flat.user);
setImportDialog({ open: false, system: [], user: [] });
}, [dialogs.importDialog, setImportDialog]);
return {
handleNew, handleEdit, handleBrowse, handleDelete,
handleMoveUp, handleMoveDown, handleClean,
handleImport, handleExport, handleSave,
handleNewConfirm, handleEditConfirm, handleImportSelect,
};
}