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