mirror of
https://github.com/LHY0125/PathEditor.git
synced 2026-06-29 01:45:54 +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:
@@ -2,16 +2,18 @@ use chrono::Local;
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
/// 获取 APPDATA 路径下的备份目录
|
fn backup_base_dir() -> PathBuf {
|
||||||
#[tauri::command]
|
|
||||||
pub fn get_appdata_dir() -> String {
|
|
||||||
dirs::data_dir()
|
dirs::data_dir()
|
||||||
.or_else(dirs::home_dir)
|
.or_else(dirs::home_dir)
|
||||||
.unwrap_or_else(|| PathBuf::from("."))
|
.unwrap_or_else(|| PathBuf::from("."))
|
||||||
.join("PathEditor")
|
.join("PathEditor")
|
||||||
.join("backups")
|
.join("backups")
|
||||||
.to_string_lossy()
|
}
|
||||||
.to_string()
|
|
||||||
|
/// 获取 APPDATA 路径下的备份目录
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_appdata_dir() -> String {
|
||||||
|
backup_base_dir().to_string_lossy().to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 备份当前注册表中的系统 PATH 和用户 PATH
|
/// 备份当前注册表中的系统 PATH 和用户 PATH
|
||||||
@@ -21,12 +23,7 @@ pub fn backup_registry(custom_dir: Option<String>, sys_paths: Vec<String>, user_
|
|||||||
// 确定备份目录
|
// 确定备份目录
|
||||||
let backup_dir = match custom_dir {
|
let backup_dir = match custom_dir {
|
||||||
Some(ref dir) if !dir.is_empty() => PathBuf::from(dir),
|
Some(ref dir) if !dir.is_empty() => PathBuf::from(dir),
|
||||||
_ => {
|
_ => backup_base_dir(),
|
||||||
dirs::data_dir()
|
|
||||||
.unwrap_or_else(|| PathBuf::from("C:\\"))
|
|
||||||
.join("PathEditor")
|
|
||||||
.join("backups")
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 创建目录
|
// 创建目录
|
||||||
|
|||||||
@@ -19,12 +19,22 @@ fn load_paths(root: winreg::HKEY, sub_path: &str, label: &str) -> Result<Vec<Str
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn save_paths(root: winreg::HKEY, sub_path: &str, label: &str, paths: &[String]) -> Result<(), String> {
|
fn save_paths(root: winreg::HKEY, sub_path: &str, label: &str, paths: &[String]) -> Result<(), String> {
|
||||||
|
let value = join_path(paths);
|
||||||
|
|
||||||
|
// Windows 注册表 REG_EXPAND_SZ 上限 32767 字符
|
||||||
|
const MAX_PATH_LEN: usize = 32767;
|
||||||
|
if value.len() > MAX_PATH_LEN {
|
||||||
|
return Err(format!(
|
||||||
|
"{} PATH 总长度 {} 超出 Windows 限制 {} 字符,请移除部分路径后再保存",
|
||||||
|
label, value.len(), MAX_PATH_LEN
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
let key = RegKey::predef(root);
|
let key = RegKey::predef(root);
|
||||||
let env_key = key
|
let env_key = key
|
||||||
.open_subkey_with_flags(sub_path, KEY_WRITE)
|
.open_subkey_with_flags(sub_path, KEY_WRITE)
|
||||||
.map_err(|e| format!("无法写入{}注册表(需要管理员权限): {}", label, e))?;
|
.map_err(|e| format!("无法写入{}注册表(需要管理员权限): {}", label, e))?;
|
||||||
|
|
||||||
let value = join_path(paths);
|
|
||||||
env_key
|
env_key
|
||||||
.set_value(PATH_VALUE, &value)
|
.set_value(PATH_VALUE, &value)
|
||||||
.map_err(|e| format!("无法写入{} PATH: {}", label, e))?;
|
.map_err(|e| format!("无法写入{} PATH: {}", label, e))?;
|
||||||
|
|||||||
@@ -89,10 +89,13 @@ export function AppShell() {
|
|||||||
onDrop={(e) => {
|
onDrop={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (activeTab === 'merged') return;
|
if (activeTab === 'merged') return;
|
||||||
for (let i = 0; i < e.dataTransfer.files.length; i++) {
|
for (let i = 0; i < e.dataTransfer.items.length; i++) {
|
||||||
|
const entry = e.dataTransfer.items[i].webkitGetAsEntry();
|
||||||
|
if (entry?.isDirectory) {
|
||||||
const path = (e.dataTransfer.files[i] as any).path;
|
const path = (e.dataTransfer.files[i] as any).path;
|
||||||
if (path) useAppStore.getState().addPath(path, activeTab === 'user' ? TargetType.USER : TargetType.SYSTEM);
|
if (path) useAppStore.getState().addPath(path, activeTab === 'user' ? TargetType.USER : TargetType.SYSTEM);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{activeTab === 'merged' ? <MergePreview /> : <PathTable tabId={activeTab as 'system' | 'user'} />}
|
{activeTab === 'merged' ? <MergePreview /> : <PathTable tabId={activeTab as 'system' | 'user'} />}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useAppStore } from '@/store/app-store';
|
|||||||
import { TargetType } from '@/core/undo-redo';
|
import { TargetType } from '@/core/undo-redo';
|
||||||
import { open } from '@tauri-apps/plugin-dialog';
|
import { open } from '@tauri-apps/plugin-dialog';
|
||||||
import { importFromContent, exportToJson, flattenImportResult } from '@/core/import-export';
|
import { importFromContent, exportToJson, flattenImportResult } from '@/core/import-export';
|
||||||
|
import { is_valid_path_format } from '@/core/validation';
|
||||||
import { useKeyboard } from './use-keyboard';
|
import { useKeyboard } from './use-keyboard';
|
||||||
import i18n from '@/i18n';
|
import i18n from '@/i18n';
|
||||||
import type { TabId } from '@/store/app-store';
|
import type { TabId } from '@/store/app-store';
|
||||||
@@ -68,7 +69,7 @@ export function useAppActions(activeTab: TabId, dialogs: DialogState) {
|
|||||||
const handleClean = useCallback(() => {
|
const handleClean = useCallback(() => {
|
||||||
const removed = useAppStore.getState().cleanPaths(
|
const removed = useAppStore.getState().cleanPaths(
|
||||||
getCurrentTarget(),
|
getCurrentTarget(),
|
||||||
(p) => p.includes('%') || p.includes('\\') || p.includes('/') || /^[a-zA-Z]:[/\\]/.test(p),
|
is_valid_path_format,
|
||||||
);
|
);
|
||||||
if (removed.length > 0) {
|
if (removed.length > 0) {
|
||||||
useAppStore.getState().setStatusMessage(
|
useAppStore.getState().setStatusMessage(
|
||||||
|
|||||||
@@ -55,6 +55,11 @@ export function useKeyboard(actions: KeyboardActions) {
|
|||||||
if (!isAdmin) return;
|
if (!isAdmin) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
a.onDelete();
|
a.onDelete();
|
||||||
|
} else if (ctrl && e.key === 'f') {
|
||||||
|
e.preventDefault();
|
||||||
|
const searchInput = document.querySelector<HTMLInputElement>('input[placeholder]');
|
||||||
|
searchInput?.focus();
|
||||||
|
searchInput?.select();
|
||||||
} else if (e.key === 'F1') {
|
} else if (e.key === 'F1') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
a.onHelp();
|
a.onHelp();
|
||||||
|
|||||||
@@ -39,6 +39,7 @@
|
|||||||
"saving": "Saving...",
|
"saving": "Saving...",
|
||||||
"saved": "Saved successfully",
|
"saved": "Saved successfully",
|
||||||
"error": "Operation failed",
|
"error": "Operation failed",
|
||||||
|
"warning_backup": "Backup creation failed, save will proceed without backup",
|
||||||
"deleted": "Deleted {{count}} path(s)",
|
"deleted": "Deleted {{count}} path(s)",
|
||||||
"loaded": "Loaded {{sysCount}} system and {{userCount}} user paths",
|
"loaded": "Loaded {{sysCount}} system and {{userCount}} user paths",
|
||||||
"dragFolderOnly": "Only folders can be dropped",
|
"dragFolderOnly": "Only folders can be dropped",
|
||||||
|
|||||||
@@ -39,6 +39,7 @@
|
|||||||
"saving": "正在保存...",
|
"saving": "正在保存...",
|
||||||
"saved": "保存成功",
|
"saved": "保存成功",
|
||||||
"error": "操作失败",
|
"error": "操作失败",
|
||||||
|
"warning_backup": "备份创建失败,保存将继续但不生成备份",
|
||||||
"deleted": "已删除 {{count}} 个路径",
|
"deleted": "已删除 {{count}} 个路径",
|
||||||
"loaded": "已加载 {{sysCount}} 个系统路径和 {{userCount}} 个用户路径",
|
"loaded": "已加载 {{sysCount}} 个系统路径和 {{userCount}} 个用户路径",
|
||||||
"dragFolderOnly": "只能拖拽文件夹",
|
"dragFolderOnly": "只能拖拽文件夹",
|
||||||
|
|||||||
+82
-49
@@ -11,6 +11,8 @@ interface AppState {
|
|||||||
sysPaths: string[];
|
sysPaths: string[];
|
||||||
userPaths: string[];
|
userPaths: string[];
|
||||||
undoRedo: UndoRedoManager;
|
undoRedo: UndoRedoManager;
|
||||||
|
_savedSys: string[]; // 上次保存时的快照,用于 isModified 判断
|
||||||
|
_savedUser: string[];
|
||||||
|
|
||||||
activeTab: TabId;
|
activeTab: TabId;
|
||||||
searchQuery: string;
|
searchQuery: string;
|
||||||
@@ -19,6 +21,7 @@ interface AppState {
|
|||||||
statusMessage: string;
|
statusMessage: string;
|
||||||
isModified: boolean;
|
isModified: boolean;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
isSaving: boolean;
|
||||||
|
|
||||||
setActiveTab: (tab: TabId) => void;
|
setActiveTab: (tab: TabId) => void;
|
||||||
setSearchQuery: (query: string) => void;
|
setSearchQuery: (query: string) => void;
|
||||||
@@ -41,14 +44,21 @@ interface AppState {
|
|||||||
|
|
||||||
loadPaths: () => Promise<void>;
|
loadPaths: () => Promise<void>;
|
||||||
savePaths: () => Promise<void>;
|
savePaths: () => Promise<void>;
|
||||||
|
|
||||||
initialize: () => 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) => ({
|
export const useAppStore = create<AppState>((set, get) => ({
|
||||||
sysPaths: [],
|
sysPaths: [],
|
||||||
userPaths: [],
|
userPaths: [],
|
||||||
undoRedo: new UndoRedoManager(appConfig.undo.maxHistory),
|
undoRedo: new UndoRedoManager(appConfig.undo.maxHistory),
|
||||||
|
_savedSys: [],
|
||||||
|
_savedUser: [],
|
||||||
|
|
||||||
activeTab: 'system',
|
activeTab: 'system',
|
||||||
searchQuery: '',
|
searchQuery: '',
|
||||||
@@ -57,6 +67,7 @@ export const useAppStore = create<AppState>((set, get) => ({
|
|||||||
statusMessage: '',
|
statusMessage: '',
|
||||||
isModified: false,
|
isModified: false,
|
||||||
isLoading: true,
|
isLoading: true,
|
||||||
|
isSaving: false,
|
||||||
|
|
||||||
setActiveTab: (tab) => set({ activeTab: tab }),
|
setActiveTab: (tab) => set({ activeTab: tab }),
|
||||||
setSearchQuery: (query) => set({ searchQuery: query }),
|
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,
|
type: OperationType.ADD, target, index: newList.length - 1, count: 1,
|
||||||
oldPaths: [], newPaths: [path],
|
oldPaths: [], newPaths: [path],
|
||||||
});
|
});
|
||||||
if (target === TargetType.SYSTEM) set({ sysPaths: newList, isModified: true });
|
if (target === TargetType.SYSTEM) set({ sysPaths: newList });
|
||||||
else set({ userPaths: newList, isModified: true });
|
else set({ userPaths: newList });
|
||||||
|
get()._markDirty();
|
||||||
},
|
},
|
||||||
|
|
||||||
editPath: (index, newPath, target) => {
|
editPath: (index, newPath, target) => {
|
||||||
@@ -86,8 +98,9 @@ export const useAppStore = create<AppState>((set, get) => ({
|
|||||||
});
|
});
|
||||||
const newList = [...list];
|
const newList = [...list];
|
||||||
newList[index] = newPath;
|
newList[index] = newPath;
|
||||||
if (target === TargetType.SYSTEM) set({ sysPaths: newList, isModified: true });
|
if (target === TargetType.SYSTEM) set({ sysPaths: newList });
|
||||||
else set({ userPaths: newList, isModified: true });
|
else set({ userPaths: newList });
|
||||||
|
get()._markDirty();
|
||||||
},
|
},
|
||||||
|
|
||||||
deletePaths: (indices, target) => {
|
deletePaths: (indices, target) => {
|
||||||
@@ -95,18 +108,20 @@ export const useAppStore = create<AppState>((set, get) => ({
|
|||||||
const state = get();
|
const state = get();
|
||||||
const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
|
const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
|
||||||
const sorted = [...indices].sort((a, b) => b - a);
|
const sorted = [...indices].sort((a, b) => b - a);
|
||||||
|
const oldPaths = sorted.map((i) => list[i]);
|
||||||
|
|
||||||
for (const idx of sorted) {
|
// 单条撤销记录覆盖全部删除
|
||||||
state.undoRedo.push({
|
state.undoRedo.push({
|
||||||
type: OperationType.DELETE, target, index: idx, count: 1,
|
type: OperationType.DELETE, target,
|
||||||
oldPaths: [list[idx]], newPaths: [],
|
index: sorted[sorted.length - 1], count: sorted.length,
|
||||||
|
oldPaths, newPaths: [],
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
const toRemove = new Set(sorted);
|
const toRemove = new Set(sorted);
|
||||||
const newList = list.filter((_, i) => !toRemove.has(i));
|
const newList = list.filter((_, i) => !toRemove.has(i));
|
||||||
if (target === TargetType.SYSTEM) set({ sysPaths: newList, selectedIndices: [], isModified: true });
|
if (target === TargetType.SYSTEM) set({ sysPaths: newList, selectedIndices: [] });
|
||||||
else set({ userPaths: newList, selectedIndices: [], isModified: true });
|
else set({ userPaths: newList, selectedIndices: [] });
|
||||||
|
get()._markDirty();
|
||||||
},
|
},
|
||||||
|
|
||||||
moveUp: (index, target) => {
|
moveUp: (index, target) => {
|
||||||
@@ -114,13 +129,13 @@ export const useAppStore = create<AppState>((set, get) => ({
|
|||||||
const state = get();
|
const state = get();
|
||||||
const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
|
const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
|
||||||
state.undoRedo.push({
|
state.undoRedo.push({
|
||||||
type: OperationType.MOVE_UP, target, index, count: 1,
|
type: OperationType.MOVE_UP, target, index, count: 1, oldPaths: [], newPaths: [],
|
||||||
oldPaths: [], newPaths: [],
|
|
||||||
});
|
});
|
||||||
const newList = [...list];
|
const newList = [...list];
|
||||||
[newList[index - 1], newList[index]] = [newList[index], newList[index - 1]];
|
[newList[index - 1], newList[index]] = [newList[index], newList[index - 1]];
|
||||||
if (target === TargetType.SYSTEM) set({ sysPaths: newList, selectedIndices: [index - 1], isModified: true });
|
if (target === TargetType.SYSTEM) set({ sysPaths: newList, selectedIndices: [index - 1] });
|
||||||
else set({ userPaths: newList, selectedIndices: [index - 1], isModified: true });
|
else set({ userPaths: newList, selectedIndices: [index - 1] });
|
||||||
|
get()._markDirty();
|
||||||
},
|
},
|
||||||
|
|
||||||
moveDown: (index, target) => {
|
moveDown: (index, target) => {
|
||||||
@@ -128,13 +143,13 @@ export const useAppStore = create<AppState>((set, get) => ({
|
|||||||
const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
|
const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
|
||||||
if (index >= list.length - 1) return;
|
if (index >= list.length - 1) return;
|
||||||
state.undoRedo.push({
|
state.undoRedo.push({
|
||||||
type: OperationType.MOVE_DOWN, target, index, count: 1,
|
type: OperationType.MOVE_DOWN, target, index, count: 1, oldPaths: [], newPaths: [],
|
||||||
oldPaths: [], newPaths: [],
|
|
||||||
});
|
});
|
||||||
const newList = [...list];
|
const newList = [...list];
|
||||||
[newList[index], newList[index + 1]] = [newList[index + 1], newList[index]];
|
[newList[index], newList[index + 1]] = [newList[index + 1], newList[index]];
|
||||||
if (target === TargetType.SYSTEM) set({ sysPaths: newList, selectedIndices: [index + 1], isModified: true });
|
if (target === TargetType.SYSTEM) set({ sysPaths: newList, selectedIndices: [index + 1] });
|
||||||
else set({ userPaths: newList, selectedIndices: [index + 1], isModified: true });
|
else set({ userPaths: newList, selectedIndices: [index + 1] });
|
||||||
|
get()._markDirty();
|
||||||
},
|
},
|
||||||
|
|
||||||
cleanPaths: (target, validateFn) => {
|
cleanPaths: (target, validateFn) => {
|
||||||
@@ -147,8 +162,9 @@ export const useAppStore = create<AppState>((set, get) => ({
|
|||||||
type: OperationType.CLEAN, target, index: 0, count: removed.length,
|
type: OperationType.CLEAN, target, index: 0, count: removed.length,
|
||||||
oldPaths: [...list], newPaths: kept,
|
oldPaths: [...list], newPaths: kept,
|
||||||
});
|
});
|
||||||
if (target === TargetType.SYSTEM) set({ sysPaths: kept, selectedIndices: [], isModified: true });
|
if (target === TargetType.SYSTEM) set({ sysPaths: kept, selectedIndices: [] });
|
||||||
else set({ userPaths: kept, selectedIndices: [], isModified: true });
|
else set({ userPaths: kept, selectedIndices: [] });
|
||||||
|
get()._markDirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
return removed;
|
return removed;
|
||||||
@@ -158,15 +174,15 @@ export const useAppStore = create<AppState>((set, get) => ({
|
|||||||
if (importPaths.length === 0) return;
|
if (importPaths.length === 0) return;
|
||||||
const state = get();
|
const state = get();
|
||||||
const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
|
const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
|
||||||
const copied = [...importPaths];
|
|
||||||
|
|
||||||
state.undoRedo.push({
|
state.undoRedo.push({
|
||||||
type: OperationType.IMPORT, target, index: 0, count: copied.length,
|
type: OperationType.IMPORT, target, index: 0, count: importPaths.length,
|
||||||
oldPaths: [...list], newPaths: copied,
|
oldPaths: [...list], newPaths: [...importPaths],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (target === TargetType.SYSTEM) set({ sysPaths: copied, selectedIndices: [], isModified: true });
|
if (target === TargetType.SYSTEM) set({ sysPaths: [...importPaths], selectedIndices: [] });
|
||||||
else set({ userPaths: copied, selectedIndices: [], isModified: true });
|
else set({ userPaths: [...importPaths], selectedIndices: [] });
|
||||||
|
get()._markDirty();
|
||||||
},
|
},
|
||||||
|
|
||||||
clearPaths: (target) => {
|
clearPaths: (target) => {
|
||||||
@@ -179,20 +195,36 @@ export const useAppStore = create<AppState>((set, get) => ({
|
|||||||
oldPaths: [...list], newPaths: [],
|
oldPaths: [...list], newPaths: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (target === TargetType.SYSTEM) set({ sysPaths: [], isModified: true });
|
if (target === TargetType.SYSTEM) set({ sysPaths: [] });
|
||||||
else set({ userPaths: [], isModified: true });
|
else set({ userPaths: [] });
|
||||||
|
get()._markDirty();
|
||||||
},
|
},
|
||||||
|
|
||||||
undo: () => {
|
undo: () => {
|
||||||
const { undoRedo, sysPaths, userPaths } = get();
|
const { undoRedo, sysPaths, userPaths, _savedSys, _savedUser } = get();
|
||||||
const result = undoRedo.undo(sysPaths, userPaths);
|
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: () => {
|
redo: () => {
|
||||||
const { undoRedo, sysPaths, userPaths } = get();
|
const { undoRedo, sysPaths, userPaths, _savedSys, _savedUser } = get();
|
||||||
const result = undoRedo.redo(sysPaths, userPaths);
|
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(),
|
canUndo: () => get().undoRedo.canUndo(),
|
||||||
@@ -206,11 +238,10 @@ export const useAppStore = create<AppState>((set, get) => ({
|
|||||||
invoke<string[]>('load_user_paths'),
|
invoke<string[]>('load_user_paths'),
|
||||||
]);
|
]);
|
||||||
set({
|
set({
|
||||||
sysPaths: sysArr,
|
sysPaths: sysArr, userPaths: userArr,
|
||||||
userPaths: userArr,
|
_savedSys: [...sysArr], _savedUser: [...userArr],
|
||||||
undoRedo: new UndoRedoManager(appConfig.undo.maxHistory),
|
undoRedo: new UndoRedoManager(appConfig.undo.maxHistory),
|
||||||
isLoading: false,
|
isLoading: false, isModified: false,
|
||||||
isModified: false,
|
|
||||||
statusMessage: i18n.t('status.loaded', { sysCount: sysArr.length, userCount: userArr.length }),
|
statusMessage: i18n.t('status.loaded', { sysCount: sysArr.length, userCount: userArr.length }),
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -219,21 +250,23 @@ export const useAppStore = create<AppState>((set, get) => ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
savePaths: async () => {
|
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 sysJoined = sysPaths.join(';');
|
||||||
const userJoined = userPaths.join(';');
|
const userJoined = userPaths.join(';');
|
||||||
|
|
||||||
const { maxSystemLength, maxUserLength, maxCombinedLength } = appConfig.path;
|
const { maxSystemLength, maxUserLength, maxCombinedLength } = appConfig.path;
|
||||||
if (sysJoined.length > maxSystemLength || userJoined.length > maxUserLength || (sysJoined + userJoined).length > maxCombinedLength) {
|
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([
|
const [sysResult, userResult] = await Promise.allSettled([
|
||||||
invoke('save_system_paths', { paths: sysPaths }),
|
invoke('save_system_paths', { paths: sysPaths }),
|
||||||
invoke('save_user_paths', { paths: userPaths }),
|
invoke('save_user_paths', { paths: userPaths }),
|
||||||
@@ -244,13 +277,13 @@ export const useAppStore = create<AppState>((set, get) => ({
|
|||||||
|
|
||||||
if (sysOk && userOk) {
|
if (sysOk && userOk) {
|
||||||
invoke('broadcast_env_change').catch(() => {});
|
invoke('broadcast_env_change').catch(() => {});
|
||||||
set({ isModified: false, statusMessage: i18n.t('status.saved') });
|
const savedSys = [...sysPaths], savedUser = [...userPaths];
|
||||||
} else if (sysOk) {
|
set({ isModified: false, isSaving: false, statusMessage: i18n.t('status.saved'), _savedSys: savedSys, _savedUser: savedUser });
|
||||||
set({ statusMessage: '用户 PATH 保存失败,系统 PATH 已保存' });
|
|
||||||
} else if (userOk) {
|
|
||||||
set({ statusMessage: '系统 PATH 保存失败,用户 PATH 已保存' });
|
|
||||||
} else {
|
} 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