fix: 全面审查修复 14 个 bug,新增 Rust 单元测试

CRITICAL:
- PathTable/MergePreview 操作后不重新渲染(加 dataVersion 版本号机制)
- moveUp/moveDown 后 selectedIndices 过时(更新到新位置)

HIGH:
- ImportDialog 显示 "true" 而非路径数量(改为 number 类型)
- F1 快捷键无效果(添加 onHelp 回调)
- useKeyboard 每次渲染重复注册事件(改用 ref 模式)
- batch delete 撤销顺序错误(拆分为独立记录)
- importPaths 存储数组引用而非副本
- StringList.all 暴露内部数组(改为返回副本)
- expand_env_vars 静默吞 API 错误(加 log::warn)
- join_path 写入前未修剪路径(加 trim 避免注册表污染)

MEDIUM:
- handleClean 总传 () => true 不验证无效路径
- HelpDialog/ImportDialog 缺 Escape 关闭
- initDarkMode 不同步 Zustand store
- 多处硬编码中文改为 i18n.t()
- Rust unsafe 块补全 SAFETY 注释

新增 Rust 测试: system.rs 4 个单元测试

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-25 23:14:26 +08:00
parent b1acb3690c
commit 2ceec54790
11 changed files with 173 additions and 93 deletions
+7 -2
View File
@@ -77,7 +77,12 @@ fn split_path(raw: &str) -> Vec<String> {
.collect() .collect()
} }
/// 用分号连接路径列表 /// 用分号连接路径列表(去除首尾空格避免污染注册表)
fn join_path(paths: &[String]) -> String { fn join_path(paths: &[String]) -> String {
paths.join(";") paths
.iter()
.map(|p| p.trim())
.filter(|p| !p.is_empty())
.collect::<Vec<_>>()
.join(";")
} }
+41 -3
View File
@@ -29,28 +29,32 @@ pub fn expand_env_vars(path: &str) -> String {
return path.to_string(); return path.to_string();
} }
// 转为 UTF-16 宽字符串 // 转为 UTF-16 宽字符串(以 null 结尾)
let wide_path: Vec<u16> = path let wide_path: Vec<u16> = path
.encode_utf16() .encode_utf16()
.chain(std::iter::once(0)) .chain(std::iter::once(0))
.collect(); .collect();
// 先查询需要的缓冲区大小 (lpDst=NULL) // SAFETY: wide_path 是以 null 结尾的 UTF-16 字符串,lpDst 为 null 且 nSize 为 0
// 根据 MSDN 文档此时 API 只查询所需缓冲区大小而不写入数据
let required = unsafe { let required = unsafe {
ExpandEnvironmentStringsW(wide_path.as_ptr(), std::ptr::null_mut(), 0) ExpandEnvironmentStringsW(wide_path.as_ptr(), std::ptr::null_mut(), 0)
}; };
if required == 0 { if required == 0 {
log::warn!("expand_env_vars: API 查询缓冲区失败, 返回原始路径: {path}");
return path.to_string(); return path.to_string();
} }
// 实际展开 // SAFETY: buffer 容量为 requiredAPI 返回的精确大小),wide_path 以 null 结尾,
// 且两个指针指向不同的内存区域,不存在重叠
let mut buffer: Vec<u16> = vec![0; required as usize]; let mut buffer: Vec<u16> = vec![0; required as usize];
let result = unsafe { let result = unsafe {
ExpandEnvironmentStringsW(wide_path.as_ptr(), buffer.as_mut_ptr(), required) ExpandEnvironmentStringsW(wide_path.as_ptr(), buffer.as_mut_ptr(), required)
}; };
if result == 0 { if result == 0 {
log::warn!("expand_env_vars: 展开失败, 返回原始路径: {path}");
return path.to_string(); return path.to_string();
} }
@@ -66,8 +70,11 @@ pub fn broadcast_env_change() {
const WM_SETTINGCHANGE: u32 = 0x001A; const WM_SETTINGCHANGE: u32 = 0x001A;
const SMTO_ABORTIFHUNG: u32 = 0x0002; const SMTO_ABORTIFHUNG: u32 = 0x0002;
// SAFETY: env_str 是以 null 结尾的 UTF-16 字符串,所有指针和常量均遵循 Win32 API 约定
let env_str: Vec<u16> = "Environment\0".encode_utf16().collect(); let env_str: Vec<u16> = "Environment\0".encode_utf16().collect();
// SAFETY: env_str.as_ptr() 指向以 null 结尾的字符串,HWND_BROADCAST 是合法句柄,
// lpdwResult 为 null 表示不需要返回值,其他参数均为常量
let result = unsafe { let result = unsafe {
SendMessageTimeoutW( SendMessageTimeoutW(
HWND_BROADCAST, HWND_BROADCAST,
@@ -108,3 +115,34 @@ extern "system" {
lpdwResult: *mut usize, lpdwResult: *mut usize,
) -> isize; ) -> isize;
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn validate_path_env_var_always_valid() {
assert!(validate_path("%JAVA_HOME%\\bin"));
}
#[test]
fn expand_env_vars_no_percent_returns_original() {
let result = expand_env_vars("C:\\Windows");
assert_eq!(result, "C:\\Windows");
}
#[test]
fn expand_env_vars_with_invalid_var_returns_original() {
// 展开不存在的变量可能会回归原始值或产生部分展开;测试是否不会崩溃
let result = expand_env_vars("%__NONEXISTENT_VAR__%");
// 至少不应为空白
assert!(!result.is_empty());
}
#[test]
fn check_admin_returns_bool() {
let result = check_admin();
// 在任意机器上应返回 true 或 false,不应 panic
assert!((result == true) || (result == false));
}
}
+10
View File
@@ -1,3 +1,4 @@
import { useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
interface HelpDialogProps { interface HelpDialogProps {
@@ -8,6 +9,15 @@ interface HelpDialogProps {
export function HelpDialog({ open, onClose }: HelpDialogProps) { export function HelpDialog({ open, onClose }: HelpDialogProps) {
const { t } = useTranslation(); const { t } = useTranslation();
useEffect(() => {
if (!open) return;
const handler = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
window.addEventListener('keydown', handler);
return () => window.removeEventListener('keydown', handler);
}, [open, onClose]);
if (!open) return null; if (!open) return null;
return ( return (
+20 -10
View File
@@ -1,22 +1,32 @@
import { useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
interface ImportDialogProps { interface ImportDialogProps {
open: boolean; open: boolean;
hasSystem: boolean; systemCount: number;
hasUser: boolean; userCount: number;
onSelect: (target: 'system' | 'user' | 'both') => void; onSelect: (target: 'system' | 'user' | 'both') => void;
onCancel: () => void; onCancel: () => void;
} }
export function ImportDialog({ export function ImportDialog({
open, open,
hasSystem, systemCount,
hasUser, userCount,
onSelect, onSelect,
onCancel, onCancel,
}: ImportDialogProps) { }: ImportDialogProps) {
const { t } = useTranslation(); const { t } = useTranslation();
useEffect(() => {
if (!open) return;
const handler = (e: KeyboardEvent) => {
if (e.key === 'Escape') onCancel();
};
window.addEventListener('keydown', handler);
return () => window.removeEventListener('keydown', handler);
}, [open, onCancel]);
if (!open) return null; if (!open) return null;
return ( return (
@@ -32,12 +42,12 @@ export function ImportDialog({
> >
<h2 className="text-lg font-semibold mb-4">{t('dialog.importTarget')}</h2> <h2 className="text-lg font-semibold mb-4">{t('dialog.importTarget')}</h2>
<p className="text-sm mb-4 opacity-70"> <p className="text-sm mb-4 opacity-70">
{hasSystem && `系统变量: ${hasSystem}`} {systemCount > 0 && `系统变量: ${systemCount}`}
{hasSystem && hasUser && ' | '} {systemCount > 0 && userCount > 0 && ' | '}
{hasUser && `用户变量: ${hasUser}`} {userCount > 0 && `用户变量: ${userCount}`}
</p> </p>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
{hasSystem && ( {systemCount > 0 && (
<button <button
className="px-4 py-2 text-sm rounded border text-left" className="px-4 py-2 text-sm rounded border text-left"
style={{ borderColor: 'var(--app-border)' }} style={{ borderColor: 'var(--app-border)' }}
@@ -46,7 +56,7 @@ export function ImportDialog({
{t('dialog.importSystem')} {t('dialog.importSystem')}
</button> </button>
)} )}
{hasUser && ( {userCount > 0 && (
<button <button
className="px-4 py-2 text-sm rounded border text-left" className="px-4 py-2 text-sm rounded border text-left"
style={{ borderColor: 'var(--app-border)' }} style={{ borderColor: 'var(--app-border)' }}
@@ -55,7 +65,7 @@ export function ImportDialog({
{t('dialog.importUser')} {t('dialog.importUser')}
</button> </button>
)} )}
{hasSystem && hasUser && ( {systemCount > 0 && userCount > 0 && (
<button <button
className="px-4 py-2 text-sm rounded border text-left" className="px-4 py-2 text-sm rounded border text-left"
style={{ borderColor: 'var(--app-border)' }} style={{ borderColor: 'var(--app-border)' }}
+3 -2
View File
@@ -168,6 +168,7 @@ export function AppShell() {
onDelete: handleDelete, onDelete: handleDelete,
onUndo: () => useAppStore.getState().undo(), onUndo: () => useAppStore.getState().undo(),
onRedo: () => useAppStore.getState().redo(), onRedo: () => useAppStore.getState().redo(),
onHelp: () => setHelpOpen(true),
}); });
// ── 双击编辑监听 ── // ── 双击编辑监听 ──
@@ -318,8 +319,8 @@ export function AppShell() {
<ImportDialog <ImportDialog
open={importDialog.open} open={importDialog.open}
hasSystem={importDialog.system.length > 0} systemCount={importDialog.system.length}
hasUser={importDialog.user.length > 0} userCount={importDialog.user.length}
onSelect={handleImportSelect} onSelect={handleImportSelect}
onCancel={() => setImportDialog({ open: false, system: [], user: [] })} onCancel={() => setImportDialog({ open: false, system: [], user: [] })}
/> />
@@ -2,6 +2,8 @@ import { useMemo } from 'react';
import { useAppStore } from '@/store/app-store'; import { useAppStore } from '@/store/app-store';
export function MergePreview() { export function MergePreview() {
const dataVersion = useAppStore((s) => s.dataVersion);
void dataVersion; // 订阅版本号强制重渲染
const sysPaths = useAppStore((s) => s.sysPaths); const sysPaths = useAppStore((s) => s.sysPaths);
const userPaths = useAppStore((s) => s.userPaths); const userPaths = useAppStore((s) => s.userPaths);
const searchQuery = useAppStore((s) => s.searchQuery); const searchQuery = useAppStore((s) => s.searchQuery);
+2
View File
@@ -12,6 +12,8 @@ interface PathRow {
} }
export function PathTable({ tabId }: PathTableProps) { export function PathTable({ tabId }: PathTableProps) {
const dataVersion = useAppStore((s) => s.dataVersion);
void dataVersion; // 订阅版本号强制重渲染
const sysPaths = useAppStore((s) => s.sysPaths); const sysPaths = useAppStore((s) => s.sysPaths);
const userPaths = useAppStore((s) => s.userPaths); const userPaths = useAppStore((s) => s.userPaths);
const searchQuery = useAppStore((s) => s.searchQuery); const searchQuery = useAppStore((s) => s.searchQuery);
+1 -1
View File
@@ -79,6 +79,6 @@ export class StringList {
/** 只读数组 */ /** 只读数组 */
get all(): readonly string[] { get all(): readonly string[] {
return this.items; return [...this.items];
} }
} }
+19 -12
View File
@@ -1,4 +1,4 @@
import { useEffect } from 'react'; import { useEffect, useRef } from 'react';
import { useAppStore } from '@/store/app-store'; import { useAppStore } from '@/store/app-store';
interface KeyboardActions { interface KeyboardActions {
@@ -7,18 +7,21 @@ interface KeyboardActions {
onDelete: () => void; onDelete: () => void;
onUndo: () => void; onUndo: () => void;
onRedo: () => void; onRedo: () => void;
onHelp: () => void;
} }
/** /**
* 全局键盘快捷键 * 全局键盘快捷键
* Ctrl+N 新建, Ctrl+S 保存, Ctrl+Z 撤销, Ctrl+Y 重做, Delete 删除 * Ctrl+N 新建, Ctrl+S 保存, Ctrl+Z 撤销, Ctrl+Y 重做, Delete 删除, F1 帮助
* 使用 ref 避免因 actions 对象每次渲染都是新引用而重复注册事件
*/ */
export function useKeyboard(actions: KeyboardActions) { export function useKeyboard(actions: KeyboardActions) {
const isAdmin = useAppStore((s) => s.isAdmin); const isAdmin = useAppStore((s) => s.isAdmin);
const actionsRef = useRef(actions);
actionsRef.current = actions;
useEffect(() => { useEffect(() => {
const handler = (e: KeyboardEvent) => { const handler = (e: KeyboardEvent) => {
// 如果焦点在输入框中,只响应 Escape
const tag = (e.target as HTMLElement)?.tagName; const tag = (e.target as HTMLElement)?.tagName;
const isInput = tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT'; const isInput = tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT';
@@ -29,32 +32,36 @@ export function useKeyboard(actions: KeyboardActions) {
return; return;
} }
if (!isAdmin) return; const a = actionsRef.current;
const ctrl = e.ctrlKey || e.metaKey; const ctrl = e.ctrlKey || e.metaKey;
if (ctrl && e.key === 'z') { if (ctrl && e.key === 'z') {
if (!isAdmin) return;
e.preventDefault(); e.preventDefault();
actions.onUndo(); a.onUndo();
} else if (ctrl && e.key === 'y') { } else if (ctrl && e.key === 'y') {
if (!isAdmin) return;
e.preventDefault(); e.preventDefault();
actions.onRedo(); a.onRedo();
} else if (ctrl && e.key === 'n') { } else if (ctrl && e.key === 'n') {
if (!isAdmin) return;
e.preventDefault(); e.preventDefault();
actions.onNew(); a.onNew();
} else if (ctrl && e.key === 's') { } else if (ctrl && e.key === 's') {
if (!isAdmin) return;
e.preventDefault(); e.preventDefault();
actions.onSave(); a.onSave();
} else if (e.key === 'Delete' || e.key === 'Backspace') { } else if (e.key === 'Delete' || e.key === 'Backspace') {
if (!isAdmin) return;
e.preventDefault(); e.preventDefault();
actions.onDelete(); a.onDelete();
} else if (e.key === 'F1') { } else if (e.key === 'F1') {
e.preventDefault(); e.preventDefault();
// 帮助由 AppShell 处理 a.onHelp();
} }
}; };
window.addEventListener('keydown', handler); window.addEventListener('keydown', handler);
return () => window.removeEventListener('keydown', handler); return () => window.removeEventListener('keydown', handler);
}, [isAdmin, actions]); }, [isAdmin]); // 只依赖 isAdminactions 通过 ref 读取
} }
+56 -59
View File
@@ -12,12 +12,11 @@ import { pathClean } from '@/core/path-manager';
export type TabId = 'system' | 'user' | 'merged'; export type TabId = 'system' | 'user' | 'merged';
interface AppState { interface AppState {
// 数据(用 StringList 而非 string[] 以支持 undo/redo
sysPaths: StringList; sysPaths: StringList;
userPaths: StringList; userPaths: StringList;
undoRedo: UndoRedoManager; undoRedo: UndoRedoManager;
dataVersion: number;
// UI 状态
activeTab: TabId; activeTab: TabId;
searchQuery: string; searchQuery: string;
selectedIndices: number[]; selectedIndices: number[];
@@ -26,13 +25,11 @@ interface AppState {
isModified: boolean; isModified: boolean;
isLoading: boolean; isLoading: boolean;
// 基本操作
setActiveTab: (tab: TabId) => void; setActiveTab: (tab: TabId) => void;
setSearchQuery: (query: string) => void; setSearchQuery: (query: string) => void;
setSelectedIndices: (indices: number[]) => void; setSelectedIndices: (indices: number[]) => void;
setStatusMessage: (msg: string) => void; setStatusMessage: (msg: string) => void;
// CRUD(带撤销/重做)
addPath: (path: string, target: TargetType) => void; addPath: (path: string, target: TargetType) => void;
editPath: (index: number, newPath: string, target: TargetType) => void; editPath: (index: number, newPath: string, target: TargetType) => void;
deletePaths: (indices: number[], target: TargetType) => void; deletePaths: (indices: number[], target: TargetType) => void;
@@ -42,29 +39,26 @@ interface AppState {
importPaths: (target: TargetType, importPaths: string[]) => void; importPaths: (target: TargetType, importPaths: string[]) => void;
clearPaths: (target: TargetType) => void; clearPaths: (target: TargetType) => void;
// 撤销/重做
undo: () => void; undo: () => void;
redo: () => void; redo: () => void;
canUndo: () => boolean; canUndo: () => boolean;
canRedo: () => boolean; canRedo: () => boolean;
// 数据加载/保存
loadPaths: () => Promise<void>; loadPaths: () => Promise<void>;
savePaths: () => Promise<void>; savePaths: () => Promise<void>;
loadFromStringLists: (sys: string[], user: string[]) => void; loadFromStringLists: (sys: string[], user: string[]) => void;
// 初始化
initialize: () => Promise<void>; initialize: () => Promise<void>;
// 内部辅助
_getTargetList: (target: TargetType) => StringList; _getTargetList: (target: TargetType) => StringList;
_markModified: () => void; _bumpVersion: () => void;
} }
export const useAppStore = create<AppState>((set, get) => ({ export const useAppStore = create<AppState>((set, get) => ({
sysPaths: new StringList(), sysPaths: new StringList(),
userPaths: new StringList(), userPaths: new StringList(),
undoRedo: new UndoRedoManager(50), undoRedo: new UndoRedoManager(50),
dataVersion: 0,
activeTab: 'system', activeTab: 'system',
searchQuery: '', searchQuery: '',
@@ -84,7 +78,7 @@ export const useAppStore = create<AppState>((set, get) => ({
return target === TargetType.SYSTEM ? sysPaths : userPaths; return target === TargetType.SYSTEM ? sysPaths : userPaths;
}, },
_markModified: () => set({ isModified: true }), _bumpVersion: () => set((s) => ({ isModified: true, dataVersion: s.dataVersion + 1 })),
// ── CRUD ── // ── CRUD ──
@@ -99,13 +93,13 @@ export const useAppStore = create<AppState>((set, get) => ({
oldPaths: [], oldPaths: [],
newPaths: [path], newPaths: [path],
}); });
get()._markModified(); get()._bumpVersion();
}, },
editPath: (index, newPath, target) => { editPath: (index, newPath, target) => {
const list = get()._getTargetList(target); const list = get()._getTargetList(target);
const oldPath = list.get(index); const oldPath = list.get(index);
if (!oldPath) return; if (oldPath === undefined) return;
get().undoRedo.push({ get().undoRedo.push({
type: OperationType.EDIT, type: OperationType.EDIT,
@@ -116,41 +110,32 @@ export const useAppStore = create<AppState>((set, get) => ({
newPaths: [newPath], newPaths: [newPath],
}); });
list.set(index, newPath); list.set(index, newPath);
get()._markModified(); get()._bumpVersion();
}, },
deletePaths: (indices, target) => { deletePaths: (indices, target) => {
const list = get()._getTargetList(target); const list = get()._getTargetList(target);
// 从大到小排 if (indices.length === 0) return;
// 从大到小排序
const sorted = [...indices].sort((a, b) => b - a); const sorted = [...indices].sort((a, b) => b - a);
const oldPaths = sorted.map((i) => list.get(i)!);
// 记录单个 DELETE(合并多个删除为一条记录) // 每个删除独立记录,保证撤销时顺序正确
if (sorted.length === 1) { for (const idx of sorted) {
const oldPath = list.get(idx)!;
get().undoRedo.push({ get().undoRedo.push({
type: OperationType.DELETE, type: OperationType.DELETE,
target, target,
index: sorted[0], index: idx,
count: 1, count: 1,
oldPaths, oldPaths: [oldPath],
newPaths: [],
});
} else {
get().undoRedo.push({
type: OperationType.DELETE,
target,
index: sorted[sorted.length - 1],
count: sorted.length,
oldPaths,
newPaths: [], newPaths: [],
}); });
list.removeAt(idx);
} }
for (const i of sorted) {
list.removeAt(i);
}
set({ selectedIndices: [] }); set({ selectedIndices: [] });
get()._markModified(); get()._bumpVersion();
}, },
moveUp: (index, target) => { moveUp: (index, target) => {
@@ -165,7 +150,8 @@ export const useAppStore = create<AppState>((set, get) => ({
newPaths: [], newPaths: [],
}); });
list.swap(index, index - 1); list.swap(index, index - 1);
get()._markModified(); set({ selectedIndices: [index - 1] });
get()._bumpVersion();
}, },
moveDown: (index, target) => { moveDown: (index, target) => {
@@ -180,7 +166,8 @@ export const useAppStore = create<AppState>((set, get) => ({
newPaths: [], newPaths: [],
}); });
list.swap(index, index + 1); list.swap(index, index + 1);
get()._markModified(); set({ selectedIndices: [index + 1] });
get()._bumpVersion();
}, },
cleanPaths: (target, validateFn) => { cleanPaths: (target, validateFn) => {
@@ -197,7 +184,8 @@ export const useAppStore = create<AppState>((set, get) => ({
oldPaths, oldPaths,
newPaths: list.toArray(), newPaths: list.toArray(),
}); });
get()._markModified(); set({ selectedIndices: [] });
get()._bumpVersion();
} }
return removed; return removed;
@@ -207,22 +195,23 @@ export const useAppStore = create<AppState>((set, get) => ({
if (importPaths.length === 0) return; if (importPaths.length === 0) return;
const list = get()._getTargetList(target); const list = get()._getTargetList(target);
const oldPaths = list.toArray(); const oldPaths = list.toArray();
const copied = [...importPaths];
get().undoRedo.push({ get().undoRedo.push({
type: OperationType.IMPORT, type: OperationType.IMPORT,
target, target,
index: 0, index: 0,
count: importPaths.length, count: copied.length,
oldPaths, oldPaths,
newPaths: importPaths, newPaths: copied,
}); });
// 覆盖为导入的路径
list.clear(); list.clear();
for (const p of importPaths) { for (const p of copied) {
list.add(p); list.add(p);
} }
get()._markModified(); set({ selectedIndices: [] });
get()._bumpVersion();
}, },
clearPaths: (target) => { clearPaths: (target) => {
@@ -239,7 +228,7 @@ export const useAppStore = create<AppState>((set, get) => ({
newPaths: [], newPaths: [],
}); });
list.clear(); list.clear();
get()._markModified(); get()._bumpVersion();
}, },
// ── 撤销/重做 ── // ── 撤销/重做 ──
@@ -248,6 +237,7 @@ export const useAppStore = create<AppState>((set, get) => ({
const { undoRedo, sysPaths, userPaths } = get(); const { undoRedo, sysPaths, userPaths } = get();
if (undoRedo.undo(sysPaths, userPaths)) { if (undoRedo.undo(sysPaths, userPaths)) {
set({ isModified: true, selectedIndices: [] }); set({ isModified: true, selectedIndices: [] });
get()._bumpVersion();
} }
}, },
@@ -255,6 +245,7 @@ export const useAppStore = create<AppState>((set, get) => ({
const { undoRedo, sysPaths, userPaths } = get(); const { undoRedo, sysPaths, userPaths } = get();
if (undoRedo.redo(sysPaths, userPaths)) { if (undoRedo.redo(sysPaths, userPaths)) {
set({ isModified: true, selectedIndices: [] }); set({ isModified: true, selectedIndices: [] });
get()._bumpVersion();
} }
}, },
@@ -264,9 +255,14 @@ export const useAppStore = create<AppState>((set, get) => ({
// ── 数据加载/保存 ── // ── 数据加载/保存 ──
loadFromStringLists: (sys: string[], user: string[]) => { loadFromStringLists: (sys: string[], user: string[]) => {
const sysPaths = StringList.fromArray(sys); set({
const userPaths = StringList.fromArray(user); sysPaths: StringList.fromArray(sys),
set({ sysPaths, userPaths, isModified: false, isLoading: false }); userPaths: StringList.fromArray(user),
undoRedo: new UndoRedoManager(50),
isModified: false,
isLoading: false,
dataVersion: get().dataVersion + 1,
});
}, },
loadPaths: async () => { loadPaths: async () => {
@@ -277,44 +273,45 @@ export const useAppStore = create<AppState>((set, get) => ({
invoke<string[]>('load_user_paths'), invoke<string[]>('load_user_paths'),
]); ]);
const sysPaths = StringList.fromArray(sysArr);
const userPaths = StringList.fromArray(userArr);
const undoRedo = new UndoRedoManager(50);
set({ set({
sysPaths, sysPaths: StringList.fromArray(sysArr),
userPaths, userPaths: StringList.fromArray(userArr),
undoRedo, undoRedo: new UndoRedoManager(50),
isLoading: false, isLoading: false,
isModified: false, isModified: false,
dataVersion: get().dataVersion + 1,
statusMessage: i18n.t('status.loaded', { statusMessage: i18n.t('status.loaded', {
sysCount: sysArr.length, sysCount: sysArr.length,
userCount: userArr.length, userCount: userArr.length,
}), }),
}); });
} catch (e) { } catch (e) {
set({ isLoading: false, statusMessage: `加载失败: ${e}` }); set({ isLoading: false, statusMessage: `${i18n.t('status.error')}: ${e}` });
} }
}, },
savePaths: async () => { savePaths: async () => {
const { sysPaths, userPaths } = get(); const { sysPaths, userPaths } = get();
set({ statusMessage: '正在保存...' }); set({ statusMessage: i18n.t('status.saving') });
try { try {
await invoke('save_system_paths', { paths: sysPaths.toArray() }); await invoke('save_system_paths', { paths: sysPaths.toArray() });
await invoke('save_user_paths', { paths: userPaths.toArray() }); await invoke('save_user_paths', { paths: userPaths.toArray() });
await invoke('broadcast_env_change'); await invoke('broadcast_env_change');
set({ isModified: false, statusMessage: '保存成功' }); set({ isModified: false, statusMessage: i18n.t('status.saved') });
} catch (e) { } catch (e) {
set({ statusMessage: `保存失败: ${e}` }); set({ statusMessage: `${i18n.t('status.error')}: ${e}` });
} }
}, },
initialize: async () => { initialize: async () => {
const isAdmin: boolean = await invoke('check_admin'); try {
set({ isAdmin }); const isAdmin: boolean = await invoke('check_admin');
if (!isAdmin) { set({ isAdmin });
set({ statusMessage: '只读模式 — 需要管理员权限才能编辑' }); if (!isAdmin) {
set({ statusMessage: i18n.t('status.readonly') });
}
} catch {
set({ isAdmin: false, statusMessage: i18n.t('status.readonly') });
} }
await get().loadPaths(); await get().loadPaths();
}, },
+12 -4
View File
@@ -5,8 +5,16 @@ interface ThemeState {
toggle: () => void; toggle: () => void;
} }
function getSavedDarkMode(): boolean {
try {
return localStorage.getItem('darkMode') === '1';
} catch {
return false;
}
}
export const useThemeStore = create<ThemeState>((set) => ({ export const useThemeStore = create<ThemeState>((set) => ({
isDark: false, isDark: getSavedDarkMode(),
toggle: () => toggle: () =>
set((state) => { set((state) => {
const next = !state.isDark; const next = !state.isDark;
@@ -21,10 +29,10 @@ export const useThemeStore = create<ThemeState>((set) => ({
}), }),
})); }));
/** 初始化深色模式状态(从 localStorage 读取 */ /** 初始化深色模式DOM 类名 + store 状态 */
export function initDarkMode(): void { export function initDarkMode(): void {
const saved = localStorage.getItem('darkMode'); if (getSavedDarkMode()) {
if (saved === '1') {
document.documentElement.classList.add('dark'); document.documentElement.classList.add('dark');
useThemeStore.setState({ isDark: true });
} }
} }