chore: 同步 v5.0 基础设施完善到 v5.1
CI / 前端检查 (格式 + 类型 + Lint + 测试 + 覆盖率) (push) Has been cancelled
CI / Rust 检查 (格式 + Check + Clippy + Test) (push) Has been cancelled

从 v5.0 cherry-pick 的开源项目基础设施改进:

新增配置文件:
- .editorconfig, .gitattributes, .prettierrc, .markdownlint.json
- commitlint.config.js

新增 GitHub 社区文件:
- .github/dependabot.yml — 依赖自动更新
- .github/CODEOWNERS — 自动 PR 审查分配
- .github/FUNDING.yml — 开源赞助入口

新增文档:
- ROADMAP.md — 路线图
- SUPPORT.md — 帮助指南
- docs/screenshots/ — 应用截图

新增 Git Hooks:
- .husky/pre-commit — lint-staged 自动格式化+修复
- .husky/commit-msg — commitlint 校验

CI 强化:
- 新增 Prettier 格式检查
- 新增 Vitest 覆盖率 + Codecov 上报
- 保留 v5.1 已有的 rust-cache + jsdom 全局环境

修复:
- index.html 标题 v4.0 → v5.1
- PathEditDialog set-state-in-effect 改用 useRef prevOpen 守卫
- merge-preview.test.tsx no-explicit-any 修复
- 所有 TS/TSX 文件 Prettier 格式化统一

v5.1 保留特性:
- @tanstack/react-virtual 虚拟滚动
- jsdom 全局测试环境
- Swatinem/rust-cache CI 加速
- 105 测试全部通过
This commit is contained in:
2026-06-19 19:24:03 +08:00
parent 60de924b08
commit 9453006310
52 changed files with 2783 additions and 619 deletions
+389 -318
View File
@@ -54,11 +54,12 @@ interface AppState {
loadPaths: () => Promise<void>;
savePaths: (force?: boolean) => Promise<SaveResult>;
initialize: () => Promise<void>;
}
function arraysEqual(a: readonly PathEntry[], b: readonly PathEntry[]): boolean {
return a.length === b.length && a.every((v, i) => v.path === b[i].path && v.enabled === b[i].enabled);
return (
a.length === b.length && a.every((v, i) => v.path === b[i].path && v.enabled === b[i].enabled)
);
}
export const useAppStore = create<AppState>((set, get) => {
@@ -69,335 +70,405 @@ export const useAppStore = create<AppState>((set, get) => {
return {
sysPaths: [],
userPaths: [],
undoRedo: new UndoRedoManager(appConfig.undo.maxHistory),
_savedSys: [],
_savedUser: [],
userPaths: [],
undoRedo: new UndoRedoManager(appConfig.undo.maxHistory),
_savedSys: [],
_savedUser: [],
activeTab: 'system',
searchQuery: '',
selectedIndices: [],
isAdmin: false,
statusMessage: '',
isModified: false,
isLoading: true,
isSaving: false,
activeTab: 'system',
searchQuery: '',
selectedIndices: [],
isAdmin: false,
statusMessage: '',
isModified: false,
isLoading: true,
isSaving: false,
setActiveTab: (tab) => set({ activeTab: tab }),
setSearchQuery: (query) => set({ searchQuery: query }),
setSelectedIndices: (indices) => set({ selectedIndices: indices }),
setStatusMessage: (msg) => set({ statusMessage: msg }),
setActiveTab: (tab) => set({ activeTab: tab }),
setSearchQuery: (query) => set({ searchQuery: query }),
setSelectedIndices: (indices) => set({ selectedIndices: indices }),
setStatusMessage: (msg) => set({ statusMessage: msg }),
addPath: (path, target) => {
const state = get();
const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
const entry: PathEntry = { path, enabled: true };
const newList = [...list, entry];
state.undoRedo.push({
type: OperationType.ADD, target, index: newList.length - 1, count: 1,
oldPaths: [], newPaths: [entry],
});
if (target === TargetType.SYSTEM) set({ sysPaths: newList });
else set({ userPaths: newList });
markDirty();
},
editPath: (index, newPath, target) => {
const state = get();
const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
const oldEntry = list[index];
if (!oldEntry) return;
const newEntry: PathEntry = { path: newPath, enabled: oldEntry.enabled };
state.undoRedo.push({
type: OperationType.EDIT, target, index, count: 1,
oldPaths: [oldEntry], newPaths: [newEntry],
});
const newList = [...list];
newList[index] = newEntry;
if (target === TargetType.SYSTEM) set({ sysPaths: newList });
else set({ userPaths: newList });
markDirty();
},
deletePaths: (indices, target) => {
if (indices.length === 0) return;
const state = get();
const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
const sortedDesc = [...indices].sort((a, b) => b - a);
const sortedAsc = [...indices].sort((a, b) => a - b);
const oldPaths = sortedAsc.map((i) => list[i]);
state.undoRedo.push({
type: OperationType.DELETE, target,
index: sortedAsc[0], count: sortedAsc.length,
oldPaths, newPaths: [],
indices: sortedAsc,
});
const toRemove = new Set(sortedDesc);
const newList = list.filter((_, i) => !toRemove.has(i));
if (target === TargetType.SYSTEM) set({ sysPaths: newList, selectedIndices: [] });
else set({ userPaths: newList, selectedIndices: [] });
markDirty();
},
moveUp: (index, target) => {
if (index <= 0) return;
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: [],
});
const newList = [...list];
[newList[index - 1], newList[index]] = [newList[index], newList[index - 1]];
if (target === TargetType.SYSTEM) set({ sysPaths: newList, selectedIndices: [index - 1] });
else set({ userPaths: newList, selectedIndices: [index - 1] });
markDirty();
},
moveDown: (index, target) => {
const state = 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: [],
});
const newList = [...list];
[newList[index], newList[index + 1]] = [newList[index + 1], newList[index]];
if (target === TargetType.SYSTEM) set({ sysPaths: newList, selectedIndices: [index + 1] });
else set({ userPaths: newList, selectedIndices: [index + 1] });
markDirty();
},
cleanPaths: (target, validateFn) => {
const state = get();
const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
const [kept, removed] = pathClean(list, validateFn);
if (removed.length > 0) {
addPath: (path, target) => {
const state = get();
const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
const entry: PathEntry = { path, enabled: true };
const newList = [...list, entry];
state.undoRedo.push({
type: OperationType.CLEAN, target, index: 0, count: removed.length,
oldPaths: [...list], newPaths: kept,
type: OperationType.ADD,
target,
index: newList.length - 1,
count: 1,
oldPaths: [],
newPaths: [entry],
});
if (target === TargetType.SYSTEM) set({ sysPaths: kept, selectedIndices: [] });
else set({ userPaths: kept, selectedIndices: [] });
if (target === TargetType.SYSTEM) set({ sysPaths: newList });
else set({ userPaths: newList });
markDirty();
}
},
return removed.map(e => e.path);
},
replacePaths: (target, newPaths) => {
if (newPaths.length === 0) return;
const state = get();
const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
const entries: PathEntry[] = newPaths.map(p => ({ path: p, enabled: true }));
state.undoRedo.push({
type: OperationType.IMPORT, target, index: 0, count: entries.length,
oldPaths: [...list], newPaths: [...entries],
});
if (target === TargetType.SYSTEM) set({ sysPaths: [...entries], selectedIndices: [] });
else set({ userPaths: [...entries], selectedIndices: [] });
markDirty();
},
replaceBothPaths: (sysPaths, userPaths) => {
const state = get();
const sysEntries: PathEntry[] = sysPaths.map(p => ({ path: p, enabled: true }));
const usrEntries: PathEntry[] = userPaths.map(p => ({ path: p, enabled: true }));
state.undoRedo.push({
type: OperationType.IMPORT_BOTH, target: TargetType.SYSTEM, index: 0,
count: sysEntries.length + usrEntries.length,
oldPaths: [...state.sysPaths], newPaths: [...sysEntries],
oldPathsOther: [...state.userPaths], newPathsOther: [...usrEntries],
});
set({ sysPaths: [...sysEntries], userPaths: [...usrEntries], selectedIndices: [] });
markDirty();
},
clearPaths: (target) => {
const state = get();
const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
if (list.length === 0) return;
state.undoRedo.push({
type: OperationType.CLEAR, target, index: 0, count: list.length,
oldPaths: [...list], newPaths: [],
});
if (target === TargetType.SYSTEM) set({ sysPaths: [] });
else set({ userPaths: [] });
markDirty();
},
togglePath: (index, target) => {
const state = get();
const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
const oldEntry = list[index];
if (!oldEntry) return;
const newEntry: PathEntry = { path: oldEntry.path, enabled: !oldEntry.enabled };
state.undoRedo.push({
type: OperationType.TOGGLE, target, index, count: 1,
oldPaths: [oldEntry], newPaths: [newEntry],
});
const newList = [...list];
newList[index] = newEntry;
if (target === TargetType.SYSTEM) set({ sysPaths: newList });
else set({ userPaths: newList });
markDirty();
// 即时保存禁用状态
const { sysPaths: sys, userPaths: usr } = get();
const sysDisabled = sys.filter(e => !e.enabled).map(e => e.path);
const usrDisabled = usr.filter(e => !e.enabled).map(e => e.path);
invoke('save_disabled_state', { system: sysDisabled, user: usrDisabled })
.catch((e) => console.warn('保存禁用状态失败:', e));
},
undo: () => {
const { undoRedo, sysPaths, userPaths, _savedSys, _savedUser } = get();
const result = undoRedo.undo(sysPaths, userPaths);
if (result) {
set({
sysPaths: result[0], userPaths: result[1], selectedIndices: [],
// 内联计算 isModified 而非调用 markDirty(),避免两次 set() 导致额外渲染
isModified: !(arraysEqual(result[0], _savedSys) && arraysEqual(result[1], _savedUser)),
editPath: (index, newPath, target) => {
const state = get();
const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
const oldEntry = list[index];
if (!oldEntry) return;
const newEntry: PathEntry = { path: newPath, enabled: oldEntry.enabled };
state.undoRedo.push({
type: OperationType.EDIT,
target,
index,
count: 1,
oldPaths: [oldEntry],
newPaths: [newEntry],
});
// 同步持久化 disabled 状态,与 togglePath 保持一致
invoke('save_disabled_state', {
system: result[0].filter(e => !e.enabled).map(e => e.path),
user: result[1].filter(e => !e.enabled).map(e => e.path),
}).catch((e) => console.warn('保存禁用状态失败:', e));
}
},
const newList = [...list];
newList[index] = newEntry;
if (target === TargetType.SYSTEM) set({ sysPaths: newList });
else set({ userPaths: newList });
markDirty();
},
redo: () => {
const { undoRedo, sysPaths, userPaths, _savedSys, _savedUser } = get();
const result = undoRedo.redo(sysPaths, userPaths);
if (result) {
set({
sysPaths: result[0], userPaths: result[1], selectedIndices: [],
// 内联计算 isModified 而非调用 markDirty(),避免两次 set() 导致额外渲染
isModified: !(arraysEqual(result[0], _savedSys) && arraysEqual(result[1], _savedUser)),
deletePaths: (indices, target) => {
if (indices.length === 0) return;
const state = get();
const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
const sortedDesc = [...indices].sort((a, b) => b - a);
const sortedAsc = [...indices].sort((a, b) => a - b);
const oldPaths = sortedAsc.map((i) => list[i]);
state.undoRedo.push({
type: OperationType.DELETE,
target,
index: sortedAsc[0],
count: sortedAsc.length,
oldPaths,
newPaths: [],
indices: sortedAsc,
});
// 同步持久化 disabled 状态,与 togglePath 保持一致
invoke('save_disabled_state', {
system: result[0].filter(e => !e.enabled).map(e => e.path),
user: result[1].filter(e => !e.enabled).map(e => e.path),
}).catch((e) => console.warn('保存禁用状态失败:', e));
}
},
loadPaths: async () => {
try {
set({ isLoading: true });
const [sysArr, userArr] = await Promise.all([
invoke<string[]>('load_system_paths'),
invoke<string[]>('load_user_paths'),
const toRemove = new Set(sortedDesc);
const newList = list.filter((_, i) => !toRemove.has(i));
if (target === TargetType.SYSTEM) set({ sysPaths: newList, selectedIndices: [] });
else set({ userPaths: newList, selectedIndices: [] });
markDirty();
},
moveUp: (index, target) => {
if (index <= 0) return;
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: [],
});
const newList = [...list];
[newList[index - 1], newList[index]] = [newList[index], newList[index - 1]];
if (target === TargetType.SYSTEM) set({ sysPaths: newList, selectedIndices: [index - 1] });
else set({ userPaths: newList, selectedIndices: [index - 1] });
markDirty();
},
moveDown: (index, target) => {
const state = 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: [],
});
const newList = [...list];
[newList[index], newList[index + 1]] = [newList[index + 1], newList[index]];
if (target === TargetType.SYSTEM) set({ sysPaths: newList, selectedIndices: [index + 1] });
else set({ userPaths: newList, selectedIndices: [index + 1] });
markDirty();
},
cleanPaths: (target, validateFn) => {
const state = get();
const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
const [kept, removed] = pathClean(list, validateFn);
if (removed.length > 0) {
state.undoRedo.push({
type: OperationType.CLEAN,
target,
index: 0,
count: removed.length,
oldPaths: [...list],
newPaths: kept,
});
if (target === TargetType.SYSTEM) set({ sysPaths: kept, selectedIndices: [] });
else set({ userPaths: kept, selectedIndices: [] });
markDirty();
}
return removed.map((e) => e.path);
},
replacePaths: (target, newPaths) => {
if (newPaths.length === 0) return;
const state = get();
const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
const entries: PathEntry[] = newPaths.map((p) => ({ path: p, enabled: true }));
state.undoRedo.push({
type: OperationType.IMPORT,
target,
index: 0,
count: entries.length,
oldPaths: [...list],
newPaths: [...entries],
});
if (target === TargetType.SYSTEM) set({ sysPaths: [...entries], selectedIndices: [] });
else set({ userPaths: [...entries], selectedIndices: [] });
markDirty();
},
replaceBothPaths: (sysPaths, userPaths) => {
const state = get();
const sysEntries: PathEntry[] = sysPaths.map((p) => ({ path: p, enabled: true }));
const usrEntries: PathEntry[] = userPaths.map((p) => ({ path: p, enabled: true }));
state.undoRedo.push({
type: OperationType.IMPORT_BOTH,
target: TargetType.SYSTEM,
index: 0,
count: sysEntries.length + usrEntries.length,
oldPaths: [...state.sysPaths],
newPaths: [...sysEntries],
oldPathsOther: [...state.userPaths],
newPathsOther: [...usrEntries],
});
set({ sysPaths: [...sysEntries], userPaths: [...usrEntries], selectedIndices: [] });
markDirty();
},
clearPaths: (target) => {
const state = get();
const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
if (list.length === 0) return;
state.undoRedo.push({
type: OperationType.CLEAR,
target,
index: 0,
count: list.length,
oldPaths: [...list],
newPaths: [],
});
if (target === TargetType.SYSTEM) set({ sysPaths: [] });
else set({ userPaths: [] });
markDirty();
},
togglePath: (index, target) => {
const state = get();
const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
const oldEntry = list[index];
if (!oldEntry) return;
const newEntry: PathEntry = { path: oldEntry.path, enabled: !oldEntry.enabled };
state.undoRedo.push({
type: OperationType.TOGGLE,
target,
index,
count: 1,
oldPaths: [oldEntry],
newPaths: [newEntry],
});
const newList = [...list];
newList[index] = newEntry;
if (target === TargetType.SYSTEM) set({ sysPaths: newList });
else set({ userPaths: newList });
markDirty();
// 即时保存禁用状态
const { sysPaths: sys, userPaths: usr } = get();
const sysDisabled = sys.filter((e) => !e.enabled).map((e) => e.path);
const usrDisabled = usr.filter((e) => !e.enabled).map((e) => e.path);
invoke('save_disabled_state', { system: sysDisabled, user: usrDisabled }).catch((e) =>
console.warn('保存禁用状态失败:', e),
);
},
undo: () => {
const { undoRedo, sysPaths, userPaths, _savedSys, _savedUser } = get();
const result = undoRedo.undo(sysPaths, userPaths);
if (result) {
set({
sysPaths: result[0],
userPaths: result[1],
selectedIndices: [],
// 内联计算 isModified 而非调用 markDirty(),避免两次 set() 导致额外渲染
isModified: !(arraysEqual(result[0], _savedSys) && arraysEqual(result[1], _savedUser)),
});
// 同步持久化 disabled 状态,与 togglePath 保持一致
invoke('save_disabled_state', {
system: result[0].filter((e) => !e.enabled).map((e) => e.path),
user: result[1].filter((e) => !e.enabled).map((e) => e.path),
}).catch((e) => console.warn('保存禁用状态失败:', e));
}
},
redo: () => {
const { undoRedo, sysPaths, userPaths, _savedSys, _savedUser } = get();
const result = undoRedo.redo(sysPaths, userPaths);
if (result) {
set({
sysPaths: result[0],
userPaths: result[1],
selectedIndices: [],
// 内联计算 isModified 而非调用 markDirty(),避免两次 set() 导致额外渲染
isModified: !(arraysEqual(result[0], _savedSys) && arraysEqual(result[1], _savedUser)),
});
// 同步持久化 disabled 状态,与 togglePath 保持一致
invoke('save_disabled_state', {
system: result[0].filter((e) => !e.enabled).map((e) => e.path),
user: result[1].filter((e) => !e.enabled).map((e) => e.path),
}).catch((e) => console.warn('保存禁用状态失败:', e));
}
},
loadPaths: async () => {
try {
set({ isLoading: true });
const [sysArr, userArr] = await Promise.all([
invoke<string[]>('load_system_paths'),
invoke<string[]>('load_user_paths'),
]);
// 加载禁用状态(文件不存在时返回空)
let sysDisabled: string[] = [];
let usrDisabled: string[] = [];
try {
const result = await invoke<[string[], string[]]>('load_disabled_state');
sysDisabled = result[0];
usrDisabled = result[1];
} catch {
// 文件不存在或损坏,忽略
}
const sysSet = new Set(sysDisabled);
const usrSet = new Set(usrDisabled);
const sysEntries: PathEntry[] = sysArr.map((p) => ({ path: p, enabled: !sysSet.has(p) }));
const usrEntries: PathEntry[] = userArr.map((p) => ({ path: p, enabled: !usrSet.has(p) }));
set({
sysPaths: sysEntries,
userPaths: usrEntries,
_savedSys: [...sysEntries],
_savedUser: [...usrEntries],
undoRedo: new UndoRedoManager(appConfig.undo.maxHistory),
isLoading: false,
isModified: false,
statusMessage: i18n.t('status.loaded', {
sysCount: sysArr.length,
userCount: userArr.length,
}),
});
} catch (e) {
set({ isLoading: false, statusMessage: `${i18n.t('status.error')}: ${String(e)}` });
}
},
savePaths: async (force?: boolean) => {
const state = get();
if (state.isSaving) return { kind: 'blocked' };
set({ isSaving: true, statusMessage: i18n.t('status.saving') });
// 只保存 enabled 的路径到注册表
const sysPaths = state.sysPaths.filter((e) => e.enabled).map((e) => e.path);
const userPaths = state.userPaths.filter((e) => e.enabled).map((e) => e.path);
const sysJoined = sysPaths.join(';');
const userJoined = userPaths.join(';');
// 长度检查:非强制模式下返回警告,由 UI 层确认
const { maxSystemLength, maxUserLength, maxCombinedLength } = appConfig.path;
if (
!force &&
(sysJoined.length > maxSystemLength ||
userJoined.length > maxUserLength ||
(sysJoined + userJoined).length > maxCombinedLength)
) {
set({ isSaving: false, statusMessage: i18n.t('status.saveWarningLongPaths') });
return { kind: 'warning', reason: 'lengthExceeded' };
}
// 备份当前注册表(保存前备份旧值,失败仅警告不中断)
let backupFailed = false;
await invoke('backup_registry', { customDir: null }).catch(() => {
backupFailed = true;
});
const origSys = state._savedSys.filter((e) => e.enabled).map((e) => e.path);
const origUser = state._savedUser.filter((e) => e.enabled).map((e) => e.path);
const [sysResult, userResult] = await Promise.allSettled([
invoke('save_system_paths', { paths: sysPaths, original: origSys }),
invoke('save_user_paths', { paths: userPaths, original: origUser }),
]);
// 加载禁用状态(文件不存在时返回空)
let sysDisabled: string[] = [];
let usrDisabled: string[] = [];
try {
const result = await invoke<[string[], string[]]>('load_disabled_state');
sysDisabled = result[0];
usrDisabled = result[1];
} catch {
// 文件不存在或损坏,忽略
}
const sysOk = sysResult.status === 'fulfilled';
const userOk = userResult.status === 'fulfilled';
const sysSet = new Set(sysDisabled);
const usrSet = new Set(usrDisabled);
const sysEntries: PathEntry[] = sysArr.map(p => ({ path: p, enabled: !sysSet.has(p) }));
const usrEntries: PathEntry[] = userArr.map(p => ({ path: p, enabled: !usrSet.has(p) }));
set({
sysPaths: sysEntries, userPaths: usrEntries,
_savedSys: [...sysEntries], _savedUser: [...usrEntries],
undoRedo: new UndoRedoManager(appConfig.undo.maxHistory),
isLoading: false, isModified: false,
statusMessage: i18n.t('status.loaded', { sysCount: sysArr.length, userCount: userArr.length }),
});
} catch (e) {
set({ isLoading: false, statusMessage: `${i18n.t('status.error')}: ${String(e)}` });
}
},
savePaths: async (force?: boolean) => {
const state = get();
if (state.isSaving) return { kind: 'blocked' };
set({ isSaving: true, statusMessage: i18n.t('status.saving') });
// 只保存 enabled 的路径到注册表
const sysPaths = state.sysPaths.filter(e => e.enabled).map(e => e.path);
const userPaths = state.userPaths.filter(e => e.enabled).map(e => e.path);
const sysJoined = sysPaths.join(';');
const userJoined = userPaths.join(';');
// 长度检查:非强制模式下返回警告,由 UI 层确认
const { maxSystemLength, maxUserLength, maxCombinedLength } = appConfig.path;
if (!force && (sysJoined.length > maxSystemLength || userJoined.length > maxUserLength || (sysJoined + userJoined).length > maxCombinedLength)) {
set({ isSaving: false, statusMessage: i18n.t('status.saveWarningLongPaths') });
return { kind: 'warning', reason: 'lengthExceeded' };
}
// 备份当前注册表(保存前备份旧值,失败仅警告不中断)
let backupFailed = false;
await invoke('backup_registry', { customDir: null })
.catch(() => { backupFailed = true; });
const origSys = state._savedSys.filter(e => e.enabled).map(e => e.path);
const origUser = state._savedUser.filter(e => e.enabled).map(e => e.path);
const [sysResult, userResult] = await Promise.allSettled([
invoke('save_system_paths', { paths: sysPaths, original: origSys }),
invoke('save_user_paths', { paths: userPaths, original: origUser }),
]);
const sysOk = sysResult.status === 'fulfilled';
const userOk = userResult.status === 'fulfilled';
if (sysOk && userOk) {
invoke('broadcast_env_change').catch(() => {});
const savedSys = [...state.sysPaths], savedUser = [...state.userPaths];
set({ isModified: false, isSaving: false,
statusMessage: backupFailed ? i18n.t('status.saved_without_backup') : i18n.t('status.saved'),
_savedSys: savedSys, _savedUser: savedUser });
return { kind: 'success' };
} else {
const sysErr = (!sysOk && sysResult.status === 'rejected') ? String(sysResult.reason) : '';
const usrErr = (!userOk && userResult.status === 'rejected') ? String(userResult.reason) : '';
const parts = [sysErr, usrErr].filter(Boolean);
const msg = sysOk ? `用户 PATH 保存失败: ${usrErr}` : userOk ? `系统 PATH 保存失败: ${sysErr}` : `保存失败: ${parts.join('; ')}`;
if (sysOk || userOk) {
// partial success
set({ isSaving: false });
await get().loadPaths(); // reload to avoid state drift
set({ statusMessage: msg }); // restore the error message overwritten by loadPaths
return { kind: 'partial', message: msg };
if (sysOk && userOk) {
invoke('broadcast_env_change').catch(() => {});
const savedSys = [...state.sysPaths],
savedUser = [...state.userPaths];
set({
isModified: false,
isSaving: false,
statusMessage: backupFailed
? i18n.t('status.saved_without_backup')
: i18n.t('status.saved'),
_savedSys: savedSys,
_savedUser: savedUser,
});
return { kind: 'success' };
} else {
set({ isSaving: false, statusMessage: msg });
return { kind: 'failure', message: msg };
}
}
},
const sysErr = !sysOk && sysResult.status === 'rejected' ? String(sysResult.reason) : '';
const usrErr = !userOk && userResult.status === 'rejected' ? String(userResult.reason) : '';
const parts = [sysErr, usrErr].filter(Boolean);
initialize: async () => {
try {
const isAdmin: boolean = await invoke('check_admin');
set({ isAdmin });
if (!isAdmin) set({ statusMessage: i18n.t('status.readonly') });
} catch {
set({ isAdmin: false, statusMessage: i18n.t('status.readonly') });
}
await get().loadPaths();
},
};});
const msg = sysOk
? `用户 PATH 保存失败: ${usrErr}`
: userOk
? `系统 PATH 保存失败: ${sysErr}`
: `保存失败: ${parts.join('; ')}`;
if (sysOk || userOk) {
// partial success
set({ isSaving: false });
await get().loadPaths(); // reload to avoid state drift
set({ statusMessage: msg }); // restore the error message overwritten by loadPaths
return { kind: 'partial', message: msg };
} else {
set({ isSaving: false, statusMessage: msg });
return { kind: 'failure', message: msg };
}
}
},
initialize: async () => {
try {
const isAdmin: boolean = await invoke('check_admin');
set({ isAdmin });
if (!isAdmin) set({ statusMessage: i18n.t('status.readonly') });
} catch {
set({ isAdmin: false, statusMessage: i18n.t('status.readonly') });
}
await get().loadPaths();
},
};
});