mirror of
https://github.com/LHY0125/PathEditor.git
synced 2026-06-29 01:45:54 +08:00
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:
@@ -1,11 +1,9 @@
|
|||||||
import { useState, useCallback, useEffect } from 'react';
|
import { useState } from 'react';
|
||||||
import { useAppStore, type TabId } from '@/store/app-store';
|
import { useAppStore, type TabId } from '@/store/app-store';
|
||||||
import { useThemeStore } from '@/store/theme-store';
|
import { useThemeStore } from '@/store/theme-store';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import i18n from '@/i18n';
|
import i18n from '@/i18n';
|
||||||
import { TargetType } from '@/core/undo-redo';
|
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 { StatusBar } from './StatusBar';
|
||||||
import { TitleBar } from './TitleBar';
|
import { TitleBar } from './TitleBar';
|
||||||
import { ToolBar } from '@/components/toolbar/ToolBar';
|
import { ToolBar } from '@/components/toolbar/ToolBar';
|
||||||
@@ -14,177 +12,28 @@ import { MergePreview } from '@/components/path-list/MergePreview';
|
|||||||
import { PathEditDialog } from '@/components/dialogs/PathEditDialog';
|
import { PathEditDialog } from '@/components/dialogs/PathEditDialog';
|
||||||
import { HelpDialog } from '@/components/dialogs/HelpDialog';
|
import { HelpDialog } from '@/components/dialogs/HelpDialog';
|
||||||
import { ImportDialog } from '@/components/dialogs/ImportDialog';
|
import { ImportDialog } from '@/components/dialogs/ImportDialog';
|
||||||
import { useKeyboard } from '@/hooks/use-keyboard';
|
import { useAppActions, type DialogState } from '@/hooks/use-app-actions';
|
||||||
|
|
||||||
export function AppShell() {
|
export function AppShell() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const activeTab = useAppStore((s) => s.activeTab);
|
const activeTab = useAppStore((s) => s.activeTab);
|
||||||
const setActiveTab = useAppStore((s) => s.setActiveTab);
|
const setActiveTab = useAppStore((s) => s.setActiveTab);
|
||||||
const selectedIndices = useAppStore((s) => s.selectedIndices);
|
|
||||||
const setSelectedIndices = useAppStore((s) => s.setSelectedIndices);
|
const setSelectedIndices = useAppStore((s) => s.setSelectedIndices);
|
||||||
|
|
||||||
// 对话弹窗状态
|
const [editDialog, setEditDialog] = useState<DialogState['editDialog']>({
|
||||||
const [editDialog, setEditDialog] = useState<{ open: boolean; index: number; value: string; target: TargetType }>({
|
|
||||||
open: false, index: -1, value: '', target: TargetType.SYSTEM,
|
open: false, index: -1, value: '', target: TargetType.SYSTEM,
|
||||||
});
|
});
|
||||||
const [newDialog, setNewDialog] = useState(false);
|
const [newDialog, setNewDialog] = useState(false);
|
||||||
const [helpOpen, setHelpOpen] = useState(false);
|
const [helpOpen, setHelpOpen] = useState(false);
|
||||||
const [importDialog, setImportDialog] = useState<{ open: boolean; system: string[]; user: string[] }>({
|
const [importDialog, setImportDialog] = useState<DialogState['importDialog']>({
|
||||||
open: false, system: [], user: [],
|
open: false, system: [], user: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── 操作处理 ──
|
const actions = useAppActions(activeTab, {
|
||||||
|
editDialog, newDialog, helpOpen, importDialog,
|
||||||
const getCurrentTarget = useCallback((): TargetType => {
|
setEditDialog, setNewDialog, setHelpOpen, setImportDialog,
|
||||||
return activeTab === 'user' ? TargetType.USER : TargetType.SYSTEM;
|
|
||||||
}, [activeTab]);
|
|
||||||
|
|
||||||
const handleNew = useCallback(() => {
|
|
||||||
setNewDialog(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleEdit = useCallback(() => {
|
|
||||||
if (selectedIndices.length !== 1) return;
|
|
||||||
const idx = selectedIndices[0];
|
|
||||||
const target = getCurrentTarget();
|
|
||||||
const list = target === TargetType.SYSTEM
|
|
||||||
? useAppStore.getState().sysPaths
|
|
||||||
: useAppStore.getState().userPaths;
|
|
||||||
const value = list[idx];
|
|
||||||
if (value) {
|
|
||||||
setEditDialog({ open: true, index: idx, value, target });
|
|
||||||
}
|
|
||||||
}, [selectedIndices, getCurrentTarget]);
|
|
||||||
|
|
||||||
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(() => {
|
|
||||||
if (selectedIndices.length === 0) return;
|
|
||||||
useAppStore.getState().deletePaths(selectedIndices, getCurrentTarget());
|
|
||||||
}, [selectedIndices, getCurrentTarget]);
|
|
||||||
|
|
||||||
const handleMoveUp = useCallback(() => {
|
|
||||||
if (selectedIndices.length !== 1) return;
|
|
||||||
useAppStore.getState().moveUp(selectedIndices[0], getCurrentTarget());
|
|
||||||
}, [selectedIndices, getCurrentTarget]);
|
|
||||||
|
|
||||||
const handleMoveDown = useCallback(() => {
|
|
||||||
if (selectedIndices.length !== 1) return;
|
|
||||||
useAppStore.getState().moveDown(selectedIndices[0], getCurrentTarget());
|
|
||||||
}, [selectedIndices, 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(
|
|
||||||
t('status.deleted', { count: removed.length }),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, [getCurrentTarget, t]);
|
|
||||||
|
|
||||||
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();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleExport = useCallback(() => {
|
|
||||||
const state = useAppStore.getState();
|
|
||||||
const data = { system: state.sysPaths, user: state.userPaths };
|
|
||||||
|
|
||||||
const content = exportToJson(data);
|
|
||||||
const mime = 'application/json';
|
|
||||||
const ext = '.json';
|
|
||||||
|
|
||||||
const blob = new Blob([content], { type: mime });
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = url;
|
|
||||||
a.download = `patheditor_export${ext}`;
|
|
||||||
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]);
|
|
||||||
|
|
||||||
const handleNewConfirm = useCallback((value: string) => {
|
|
||||||
setNewDialog(false);
|
|
||||||
if (value.trim()) {
|
|
||||||
useAppStore.getState().addPath(value.trim(), getCurrentTarget());
|
|
||||||
}
|
|
||||||
}, [getCurrentTarget]);
|
|
||||||
|
|
||||||
const handleEditConfirm = useCallback((value: string) => {
|
|
||||||
setEditDialog({ open: false, index: -1, value: '', target: TargetType.SYSTEM });
|
|
||||||
if (value.trim()) {
|
|
||||||
useAppStore.getState().editPath(editDialog.index, value.trim(), editDialog.target);
|
|
||||||
}
|
|
||||||
}, [editDialog]);
|
|
||||||
|
|
||||||
const handleImportSelect = useCallback((target: 'system' | 'user' | 'both') => {
|
|
||||||
const { system, user } = 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: [] });
|
|
||||||
}, [importDialog]);
|
|
||||||
|
|
||||||
// Tab 切换
|
|
||||||
const tabConfig: { id: TabId; label: string }[] = [
|
const tabConfig: { id: TabId; label: string }[] = [
|
||||||
{ id: 'system', label: t('tab.system') },
|
{ id: 'system', label: t('tab.system') },
|
||||||
{ id: 'user', label: t('tab.user') },
|
{ id: 'user', label: t('tab.user') },
|
||||||
@@ -192,27 +41,15 @@ export function AppShell() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="flex flex-col h-screen" style={{ backgroundColor: 'var(--app-bg)', color: 'var(--app-fg)' }}>
|
||||||
className="flex flex-col h-screen"
|
|
||||||
style={{ backgroundColor: 'var(--app-bg)', color: 'var(--app-fg)' }}
|
|
||||||
>
|
|
||||||
<TitleBar />
|
<TitleBar />
|
||||||
|
|
||||||
{/* Tab 栏 */}
|
<div className="flex border-b px-4" style={{ borderColor: 'var(--app-border)' }}>
|
||||||
<div
|
|
||||||
className="flex border-b px-4"
|
|
||||||
style={{ borderColor: 'var(--app-border)' }}
|
|
||||||
>
|
|
||||||
{tabConfig.map((tab) => (
|
{tabConfig.map((tab) => (
|
||||||
<button
|
<button
|
||||||
key={tab.id}
|
key={tab.id}
|
||||||
onClick={() => {
|
onClick={() => { setActiveTab(tab.id); setSelectedIndices([]); }}
|
||||||
setActiveTab(tab.id);
|
className={`px-4 py-1.5 text-sm font-medium transition-colors ${activeTab === tab.id ? 'tab-active' : 'opacity-60'}`}
|
||||||
setSelectedIndices([]);
|
|
||||||
}}
|
|
||||||
className={`px-4 py-1.5 text-sm font-medium transition-colors ${
|
|
||||||
activeTab === tab.id ? 'tab-active' : 'opacity-60'
|
|
||||||
}`}
|
|
||||||
style={{ color: activeTab === tab.id ? '#3b82f6' : 'var(--app-fg)' }}
|
style={{ color: activeTab === tab.id ? '#3b82f6' : 'var(--app-fg)' }}
|
||||||
>
|
>
|
||||||
{tab.label}
|
{tab.label}
|
||||||
@@ -220,19 +57,18 @@ export function AppShell() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 工具栏 */}
|
|
||||||
<div className="px-4 py-2">
|
<div className="px-4 py-2">
|
||||||
<ToolBar
|
<ToolBar
|
||||||
onNew={handleNew}
|
onNew={actions.handleNew}
|
||||||
onEdit={handleEdit}
|
onEdit={actions.handleEdit}
|
||||||
onBrowse={handleBrowse}
|
onBrowse={actions.handleBrowse}
|
||||||
onDelete={handleDelete}
|
onDelete={actions.handleDelete}
|
||||||
onMoveUp={handleMoveUp}
|
onMoveUp={actions.handleMoveUp}
|
||||||
onMoveDown={handleMoveDown}
|
onMoveDown={actions.handleMoveDown}
|
||||||
onClean={handleClean}
|
onClean={actions.handleClean}
|
||||||
onImport={handleImport}
|
onImport={actions.handleImport}
|
||||||
onExport={handleExport}
|
onExport={actions.handleExport}
|
||||||
onSave={handleSave}
|
onSave={actions.handleSave}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
const state = useAppStore.getState();
|
const state = useAppStore.getState();
|
||||||
if (state.isModified && !window.confirm('有未保存的修改,确定退出吗?')) return;
|
if (state.isModified && !window.confirm('有未保存的修改,确定退出吗?')) return;
|
||||||
@@ -247,60 +83,27 @@ export function AppShell() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 路径列表(支持拖拽文件夹) */}
|
|
||||||
<div
|
<div
|
||||||
className="flex-1 overflow-hidden"
|
className="flex-1 overflow-hidden"
|
||||||
onDragOver={(e) => {
|
onDragOver={(e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'link'; }}
|
||||||
e.preventDefault();
|
|
||||||
e.dataTransfer.dropEffect = 'link';
|
|
||||||
}}
|
|
||||||
onDrop={(e) => {
|
onDrop={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (activeTab === 'merged') return;
|
if (activeTab === 'merged') return;
|
||||||
const files = e.dataTransfer.files;
|
for (let i = 0; i < e.dataTransfer.files.length; i++) {
|
||||||
for (let i = 0; i < files.length; i++) {
|
const path = (e.dataTransfer.files[i] as any).path;
|
||||||
const path = (files[i] as any).path;
|
if (path) useAppStore.getState().addPath(path, activeTab === 'user' ? TargetType.USER : TargetType.SYSTEM);
|
||||||
if (path) {
|
|
||||||
useAppStore.getState().addPath(path, getCurrentTarget());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{activeTab === 'merged' ? (
|
{activeTab === 'merged' ? <MergePreview /> : <PathTable tabId={activeTab as 'system' | 'user'} />}
|
||||||
<MergePreview />
|
|
||||||
) : (
|
|
||||||
<PathTable tabId={activeTab as 'system' | 'user'} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<StatusBar />
|
<StatusBar />
|
||||||
|
|
||||||
{/* 弹窗 */}
|
<PathEditDialog open={newDialog} title={t('dialog.newPath')} initialValue="" onConfirm={actions.handleNewConfirm} onCancel={() => setNewDialog(false)} />
|
||||||
<PathEditDialog
|
<PathEditDialog open={editDialog.open} title={t('dialog.editPath')} initialValue={editDialog.value} onConfirm={actions.handleEditConfirm} onCancel={() => setEditDialog({ open: false, index: -1, value: '', target: TargetType.SYSTEM })} />
|
||||||
open={newDialog}
|
|
||||||
title={t('dialog.newPath')}
|
|
||||||
initialValue=""
|
|
||||||
onConfirm={handleNewConfirm}
|
|
||||||
onCancel={() => setNewDialog(false)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<PathEditDialog
|
|
||||||
open={editDialog.open}
|
|
||||||
title={t('dialog.editPath')}
|
|
||||||
initialValue={editDialog.value}
|
|
||||||
onConfirm={handleEditConfirm}
|
|
||||||
onCancel={() => setEditDialog({ open: false, index: -1, value: '', target: TargetType.SYSTEM })}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<HelpDialog open={helpOpen} onClose={() => setHelpOpen(false)} />
|
<HelpDialog open={helpOpen} onClose={() => setHelpOpen(false)} />
|
||||||
|
<ImportDialog open={importDialog.open} systemCount={importDialog.system.length} userCount={importDialog.user.length} onSelect={actions.handleImportSelect} onCancel={() => setImportDialog({ open: false, system: [], user: [] })} />
|
||||||
<ImportDialog
|
|
||||||
open={importDialog.open}
|
|
||||||
systemCount={importDialog.system.length}
|
|
||||||
userCount={importDialog.user.length}
|
|
||||||
onSelect={handleImportSelect}
|
|
||||||
onCancel={() => setImportDialog({ open: false, system: [], user: [] })}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
+11
-6
@@ -230,15 +230,20 @@ export const useAppStore = create<AppState>((set, get) => ({
|
|||||||
|
|
||||||
set({ statusMessage: i18n.t('status.saving') });
|
set({ statusMessage: i18n.t('status.saving') });
|
||||||
|
|
||||||
// 保存前备份
|
// 备份(不阻塞保存)
|
||||||
try { await invoke('backup_registry', { customDir: null, sysPaths, userPaths }); } catch { /* 备份失败不阻止保存 */ }
|
invoke('backup_registry', { customDir: null, sysPaths, userPaths }).catch(() => {});
|
||||||
|
|
||||||
let sysOk = true, userOk = true;
|
// 并行保存
|
||||||
try { await invoke('save_system_paths', { paths: sysPaths }); } catch { sysOk = false; }
|
const [sysResult, userResult] = await Promise.allSettled([
|
||||||
try { await invoke('save_user_paths', { paths: userPaths }); } catch { userOk = false; }
|
invoke('save_system_paths', { paths: sysPaths }),
|
||||||
|
invoke('save_user_paths', { paths: userPaths }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const sysOk = sysResult.status === 'fulfilled';
|
||||||
|
const userOk = userResult.status === 'fulfilled';
|
||||||
|
|
||||||
if (sysOk && userOk) {
|
if (sysOk && userOk) {
|
||||||
try { await invoke('broadcast_env_change'); } catch { /* 广播失败不阻止 */ }
|
invoke('broadcast_env_change').catch(() => {});
|
||||||
set({ isModified: false, statusMessage: i18n.t('status.saved') });
|
set({ isModified: false, statusMessage: i18n.t('status.saved') });
|
||||||
} else if (sysOk) {
|
} else if (sysOk) {
|
||||||
set({ statusMessage: '用户 PATH 保存失败,系统 PATH 已保存' });
|
set({ statusMessage: '用户 PATH 保存失败,系统 PATH 已保存' });
|
||||||
|
|||||||
Reference in New Issue
Block a user