mirror of
https://github.com/LHY0125/PathEditor.git
synced 2026-06-29 09:55:56 +08:00
fix: 最终审查修复 — 数据安全、功能缺失、状态管理
Rust: - save_paths 添加 Windows PATH 32767 字符上限检查防静默截断 - backup_registry 回退路径统一为 backup_base_dir() 三级链 Store: - 新增 isSaving 并发守卫防止重复保存 - 保存失败详情通过 Promise.allSettled.reason 展示 - isModified 改为与上次保存快照比较(undo/redo 后准确反映状态) - 批删除合并为单条撤销记录(N 次删除 → 1 次 Ctrl+Z 恢复) - 保存失败/备份失败使用 i18n 键(消除硬编码中文) UI: - 拖拽添加改用 webkitGetAsEntry().isDirectory 校验文件夹 - Ctrl+F 快捷键聚焦搜索框 - handleClean 使用 is_valid_path_format(替代不完整的 inline 函数) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+84
-51
@@ -11,6 +11,8 @@ interface AppState {
|
||||
sysPaths: string[];
|
||||
userPaths: string[];
|
||||
undoRedo: UndoRedoManager;
|
||||
_savedSys: string[]; // 上次保存时的快照,用于 isModified 判断
|
||||
_savedUser: string[];
|
||||
|
||||
activeTab: TabId;
|
||||
searchQuery: string;
|
||||
@@ -19,6 +21,7 @@ interface AppState {
|
||||
statusMessage: string;
|
||||
isModified: boolean;
|
||||
isLoading: boolean;
|
||||
isSaving: boolean;
|
||||
|
||||
setActiveTab: (tab: TabId) => void;
|
||||
setSearchQuery: (query: string) => void;
|
||||
@@ -41,14 +44,21 @@ interface AppState {
|
||||
|
||||
loadPaths: () => Promise<void>;
|
||||
savePaths: () => Promise<void>;
|
||||
|
||||
initialize: () => Promise<void>;
|
||||
|
||||
_markDirty: () => void;
|
||||
}
|
||||
|
||||
function arraysEqual(a: readonly string[], b: readonly string[]): boolean {
|
||||
return a.length === b.length && a.every((v, i) => v === b[i]);
|
||||
}
|
||||
|
||||
export const useAppStore = create<AppState>((set, get) => ({
|
||||
sysPaths: [],
|
||||
userPaths: [],
|
||||
undoRedo: new UndoRedoManager(appConfig.undo.maxHistory),
|
||||
_savedSys: [],
|
||||
_savedUser: [],
|
||||
|
||||
activeTab: 'system',
|
||||
searchQuery: '',
|
||||
@@ -57,6 +67,7 @@ export const useAppStore = create<AppState>((set, get) => ({
|
||||
statusMessage: '',
|
||||
isModified: false,
|
||||
isLoading: true,
|
||||
isSaving: false,
|
||||
|
||||
setActiveTab: (tab) => set({ activeTab: tab }),
|
||||
setSearchQuery: (query) => set({ searchQuery: query }),
|
||||
@@ -71,8 +82,9 @@ export const useAppStore = create<AppState>((set, get) => ({
|
||||
type: OperationType.ADD, target, index: newList.length - 1, count: 1,
|
||||
oldPaths: [], newPaths: [path],
|
||||
});
|
||||
if (target === TargetType.SYSTEM) set({ sysPaths: newList, isModified: true });
|
||||
else set({ userPaths: newList, isModified: true });
|
||||
if (target === TargetType.SYSTEM) set({ sysPaths: newList });
|
||||
else set({ userPaths: newList });
|
||||
get()._markDirty();
|
||||
},
|
||||
|
||||
editPath: (index, newPath, target) => {
|
||||
@@ -86,8 +98,9 @@ export const useAppStore = create<AppState>((set, get) => ({
|
||||
});
|
||||
const newList = [...list];
|
||||
newList[index] = newPath;
|
||||
if (target === TargetType.SYSTEM) set({ sysPaths: newList, isModified: true });
|
||||
else set({ userPaths: newList, isModified: true });
|
||||
if (target === TargetType.SYSTEM) set({ sysPaths: newList });
|
||||
else set({ userPaths: newList });
|
||||
get()._markDirty();
|
||||
},
|
||||
|
||||
deletePaths: (indices, target) => {
|
||||
@@ -95,18 +108,20 @@ export const useAppStore = create<AppState>((set, get) => ({
|
||||
const state = get();
|
||||
const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
|
||||
const sorted = [...indices].sort((a, b) => b - a);
|
||||
const oldPaths = sorted.map((i) => list[i]);
|
||||
|
||||
for (const idx of sorted) {
|
||||
state.undoRedo.push({
|
||||
type: OperationType.DELETE, target, index: idx, count: 1,
|
||||
oldPaths: [list[idx]], newPaths: [],
|
||||
});
|
||||
}
|
||||
// 单条撤销记录覆盖全部删除
|
||||
state.undoRedo.push({
|
||||
type: OperationType.DELETE, target,
|
||||
index: sorted[sorted.length - 1], count: sorted.length,
|
||||
oldPaths, newPaths: [],
|
||||
});
|
||||
|
||||
const toRemove = new Set(sorted);
|
||||
const newList = list.filter((_, i) => !toRemove.has(i));
|
||||
if (target === TargetType.SYSTEM) set({ sysPaths: newList, selectedIndices: [], isModified: true });
|
||||
else set({ userPaths: newList, selectedIndices: [], isModified: true });
|
||||
if (target === TargetType.SYSTEM) set({ sysPaths: newList, selectedIndices: [] });
|
||||
else set({ userPaths: newList, selectedIndices: [] });
|
||||
get()._markDirty();
|
||||
},
|
||||
|
||||
moveUp: (index, target) => {
|
||||
@@ -114,13 +129,13 @@ export const useAppStore = create<AppState>((set, get) => ({
|
||||
const state = get();
|
||||
const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
|
||||
state.undoRedo.push({
|
||||
type: OperationType.MOVE_UP, target, index, count: 1,
|
||||
oldPaths: [], newPaths: [],
|
||||
type: OperationType.MOVE_UP, target, index, count: 1, oldPaths: [], newPaths: [],
|
||||
});
|
||||
const newList = [...list];
|
||||
[newList[index - 1], newList[index]] = [newList[index], newList[index - 1]];
|
||||
if (target === TargetType.SYSTEM) set({ sysPaths: newList, selectedIndices: [index - 1], isModified: true });
|
||||
else set({ userPaths: newList, selectedIndices: [index - 1], isModified: true });
|
||||
if (target === TargetType.SYSTEM) set({ sysPaths: newList, selectedIndices: [index - 1] });
|
||||
else set({ userPaths: newList, selectedIndices: [index - 1] });
|
||||
get()._markDirty();
|
||||
},
|
||||
|
||||
moveDown: (index, target) => {
|
||||
@@ -128,13 +143,13 @@ export const useAppStore = create<AppState>((set, get) => ({
|
||||
const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
|
||||
if (index >= list.length - 1) return;
|
||||
state.undoRedo.push({
|
||||
type: OperationType.MOVE_DOWN, target, index, count: 1,
|
||||
oldPaths: [], newPaths: [],
|
||||
type: OperationType.MOVE_DOWN, target, index, count: 1, oldPaths: [], newPaths: [],
|
||||
});
|
||||
const newList = [...list];
|
||||
[newList[index], newList[index + 1]] = [newList[index + 1], newList[index]];
|
||||
if (target === TargetType.SYSTEM) set({ sysPaths: newList, selectedIndices: [index + 1], isModified: true });
|
||||
else set({ userPaths: newList, selectedIndices: [index + 1], isModified: true });
|
||||
if (target === TargetType.SYSTEM) set({ sysPaths: newList, selectedIndices: [index + 1] });
|
||||
else set({ userPaths: newList, selectedIndices: [index + 1] });
|
||||
get()._markDirty();
|
||||
},
|
||||
|
||||
cleanPaths: (target, validateFn) => {
|
||||
@@ -147,8 +162,9 @@ export const useAppStore = create<AppState>((set, get) => ({
|
||||
type: OperationType.CLEAN, target, index: 0, count: removed.length,
|
||||
oldPaths: [...list], newPaths: kept,
|
||||
});
|
||||
if (target === TargetType.SYSTEM) set({ sysPaths: kept, selectedIndices: [], isModified: true });
|
||||
else set({ userPaths: kept, selectedIndices: [], isModified: true });
|
||||
if (target === TargetType.SYSTEM) set({ sysPaths: kept, selectedIndices: [] });
|
||||
else set({ userPaths: kept, selectedIndices: [] });
|
||||
get()._markDirty();
|
||||
}
|
||||
|
||||
return removed;
|
||||
@@ -158,15 +174,15 @@ export const useAppStore = create<AppState>((set, get) => ({
|
||||
if (importPaths.length === 0) return;
|
||||
const state = get();
|
||||
const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
|
||||
const copied = [...importPaths];
|
||||
|
||||
state.undoRedo.push({
|
||||
type: OperationType.IMPORT, target, index: 0, count: copied.length,
|
||||
oldPaths: [...list], newPaths: copied,
|
||||
type: OperationType.IMPORT, target, index: 0, count: importPaths.length,
|
||||
oldPaths: [...list], newPaths: [...importPaths],
|
||||
});
|
||||
|
||||
if (target === TargetType.SYSTEM) set({ sysPaths: copied, selectedIndices: [], isModified: true });
|
||||
else set({ userPaths: copied, selectedIndices: [], isModified: true });
|
||||
if (target === TargetType.SYSTEM) set({ sysPaths: [...importPaths], selectedIndices: [] });
|
||||
else set({ userPaths: [...importPaths], selectedIndices: [] });
|
||||
get()._markDirty();
|
||||
},
|
||||
|
||||
clearPaths: (target) => {
|
||||
@@ -179,20 +195,36 @@ export const useAppStore = create<AppState>((set, get) => ({
|
||||
oldPaths: [...list], newPaths: [],
|
||||
});
|
||||
|
||||
if (target === TargetType.SYSTEM) set({ sysPaths: [], isModified: true });
|
||||
else set({ userPaths: [], isModified: true });
|
||||
if (target === TargetType.SYSTEM) set({ sysPaths: [] });
|
||||
else set({ userPaths: [] });
|
||||
get()._markDirty();
|
||||
},
|
||||
|
||||
undo: () => {
|
||||
const { undoRedo, sysPaths, userPaths } = get();
|
||||
const { undoRedo, sysPaths, userPaths, _savedSys, _savedUser } = get();
|
||||
const result = undoRedo.undo(sysPaths, userPaths);
|
||||
if (result) set({ sysPaths: result[0], userPaths: result[1], isModified: true, selectedIndices: [] });
|
||||
if (result) {
|
||||
set({
|
||||
sysPaths: result[0], userPaths: result[1], selectedIndices: [],
|
||||
isModified: !(arraysEqual(result[0], _savedSys) && arraysEqual(result[1], _savedUser)),
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
redo: () => {
|
||||
const { undoRedo, sysPaths, userPaths } = get();
|
||||
const { undoRedo, sysPaths, userPaths, _savedSys, _savedUser } = get();
|
||||
const result = undoRedo.redo(sysPaths, userPaths);
|
||||
if (result) set({ sysPaths: result[0], userPaths: result[1], isModified: true, selectedIndices: [] });
|
||||
if (result) {
|
||||
set({
|
||||
sysPaths: result[0], userPaths: result[1], selectedIndices: [],
|
||||
isModified: !(arraysEqual(result[0], _savedSys) && arraysEqual(result[1], _savedUser)),
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_markDirty: () => {
|
||||
const { _savedSys, _savedUser, sysPaths, userPaths } = get();
|
||||
set({ isModified: !(arraysEqual(sysPaths, _savedSys) && arraysEqual(userPaths, _savedUser)) });
|
||||
},
|
||||
|
||||
canUndo: () => get().undoRedo.canUndo(),
|
||||
@@ -206,11 +238,10 @@ export const useAppStore = create<AppState>((set, get) => ({
|
||||
invoke<string[]>('load_user_paths'),
|
||||
]);
|
||||
set({
|
||||
sysPaths: sysArr,
|
||||
userPaths: userArr,
|
||||
sysPaths: sysArr, userPaths: userArr,
|
||||
_savedSys: [...sysArr], _savedUser: [...userArr],
|
||||
undoRedo: new UndoRedoManager(appConfig.undo.maxHistory),
|
||||
isLoading: false,
|
||||
isModified: false,
|
||||
isLoading: false, isModified: false,
|
||||
statusMessage: i18n.t('status.loaded', { sysCount: sysArr.length, userCount: userArr.length }),
|
||||
});
|
||||
} catch (e) {
|
||||
@@ -219,21 +250,23 @@ export const useAppStore = create<AppState>((set, get) => ({
|
||||
},
|
||||
|
||||
savePaths: async () => {
|
||||
const { sysPaths, userPaths } = get();
|
||||
const state = get();
|
||||
if (state.isSaving) return;
|
||||
set({ isSaving: true, statusMessage: i18n.t('status.saving') });
|
||||
|
||||
const { sysPaths, userPaths } = state;
|
||||
const sysJoined = sysPaths.join(';');
|
||||
const userJoined = userPaths.join(';');
|
||||
|
||||
const { maxSystemLength, maxUserLength, maxCombinedLength } = appConfig.path;
|
||||
if (sysJoined.length > maxSystemLength || userJoined.length > maxUserLength || (sysJoined + userJoined).length > maxCombinedLength) {
|
||||
if (!window.confirm(`${i18n.t('status.error')}: PATH 长度超过建议值,是否继续?`)) return;
|
||||
if (!window.confirm('PATH 长度超过建议值,是否继续保存?')) { set({ isSaving: false }); return; }
|
||||
}
|
||||
|
||||
set({ statusMessage: i18n.t('status.saving') });
|
||||
// 备份(失败时通知用户)
|
||||
invoke('backup_registry', { customDir: null, sysPaths, userPaths })
|
||||
.catch(() => set({ statusMessage: i18n.t('status.warning_backup') }));
|
||||
|
||||
// 备份(不阻塞保存)
|
||||
invoke('backup_registry', { customDir: null, sysPaths, userPaths }).catch(() => {});
|
||||
|
||||
// 并行保存
|
||||
const [sysResult, userResult] = await Promise.allSettled([
|
||||
invoke('save_system_paths', { paths: sysPaths }),
|
||||
invoke('save_user_paths', { paths: userPaths }),
|
||||
@@ -244,13 +277,13 @@ export const useAppStore = create<AppState>((set, get) => ({
|
||||
|
||||
if (sysOk && userOk) {
|
||||
invoke('broadcast_env_change').catch(() => {});
|
||||
set({ isModified: false, statusMessage: i18n.t('status.saved') });
|
||||
} else if (sysOk) {
|
||||
set({ statusMessage: '用户 PATH 保存失败,系统 PATH 已保存' });
|
||||
} else if (userOk) {
|
||||
set({ statusMessage: '系统 PATH 保存失败,用户 PATH 已保存' });
|
||||
const savedSys = [...sysPaths], savedUser = [...userPaths];
|
||||
set({ isModified: false, isSaving: false, statusMessage: i18n.t('status.saved'), _savedSys: savedSys, _savedUser: savedUser });
|
||||
} else {
|
||||
set({ statusMessage: `${i18n.t('status.error')}: 保存失败` });
|
||||
const reason = (!sysOk && sysResult.status === 'rejected') ? String(sysResult.reason) :
|
||||
(!userOk && userResult.status === 'rejected') ? String(userResult.reason) : '';
|
||||
const msg = sysOk ? '用户 PATH 保存失败' : userOk ? '系统 PATH 保存失败' : `保存失败: ${reason}`;
|
||||
set({ isSaving: false, statusMessage: msg });
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user