refactor: 全面代码质量提升 — StringList→string[], strict 模式, 死代码清理

架构重构:
- StringList 类替换为不可变 string[](消除 dataVersion hack,Zustand 自然检测变化)
- UndoRedoManager.undo/redo 返回新数组而非原地修改
- 删除 dataVersion 字段和 _bumpVersion()
- 启用 TypeScript strict 模式

死代码清理:
- 删除 string-list.ts, string-list.test.ts, use-path-validation.ts
- Rust AppError 保留供未来使用

功能修复:
- importFromJson 添加 try/catch
- handleClean 使用真实格式验证替代 () => true
- savePaths 保存前调用 backup_registry,处理部分保存失败
- importFromJson 校验非 object 类型输入

i18n 完善:
- MergePreview/StatusBar 硬编码中文 → t() 调用
- 新增 merge.* 和 status.* 翻译键

Rust 改进:
- registry.rs 抽取 load_paths/save_paths 通用函数,消除重复
- registry 新增 6 个单元测试(split/join/roundtrip)
- backup.rs 时间戳加毫秒防覆盖,回退路径改为 home_dir

元数据:
- package.json 名称→patheditor, 版本→4.0.0
- 新增 CHANGELOG.md
- 移除 UndoRedoButtons 废弃注释
- tsconfig 添加 strict:true

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-26 00:26:27 +08:00
parent 2ceec54790
commit bfd114d80f
21 changed files with 410 additions and 836 deletions
+5 -5
View File
@@ -49,7 +49,7 @@ export function AppShell() {
const list = target === TargetType.SYSTEM
? useAppStore.getState().sysPaths
: useAppStore.getState().userPaths;
const value = list.get(idx);
const value = list[idx];
if (value) {
setEditDialog({ open: true, index: idx, value, target });
}
@@ -88,7 +88,7 @@ export function AppShell() {
const handleClean = useCallback(() => {
const removed = useAppStore.getState().cleanPaths(
getCurrentTarget(),
() => true, // 简化版,全有效
(p) => p.includes('%') || p.includes('\\') || p.includes('/') || /^[a-zA-Z]:[/\\]/.test(p),
);
if (removed.length > 0) {
useAppStore.getState().setStatusMessage(
@@ -120,7 +120,7 @@ export function AppShell() {
const handleExport = useCallback(() => {
const state = useAppStore.getState();
const data = { system: state.sysPaths.toArray(), user: state.userPaths.toArray() };
const data = { system: state.sysPaths, user: state.userPaths };
const content = exportToJson(data);
const mime = 'application/json';
@@ -137,8 +137,8 @@ export function AppShell() {
const handleSave = useCallback(() => {
const state = useAppStore.getState();
const sysJoined = state.sysPaths.toArray().join(';');
const userJoined = state.userPaths.toArray().join(';');
const sysJoined = state.sysPaths.join(';');
const userJoined = state.userPaths.join(';');
const combined = sysJoined + ';' + userJoined;
const warnings: string[] = [];
+6 -4
View File
@@ -1,7 +1,9 @@
import { useAppStore } from '@/store/app-store';
import { useThemeStore } from '@/store/theme-store';
import { useTranslation } from 'react-i18next';
export function StatusBar() {
const { t } = useTranslation();
const statusMessage = useAppStore((s) => s.statusMessage);
const isLoading = useAppStore((s) => s.isLoading);
const isAdmin = useAppStore((s) => s.isAdmin);
@@ -17,11 +19,11 @@ export function StatusBar() {
color: 'var(--app-fg)',
}}
>
<span>{isLoading ? '加载中...' : statusMessage}</span>
<span>{isLoading ? t('status.loading') : statusMessage}</span>
<div className="flex gap-3">
{isModified && <span className="text-yellow-500"> </span>}
{!isAdmin && <span className="text-yellow-500"></span>}
<span style={{ opacity: 0.5 }}>{isDark ? '深色' : '浅色'}</span>
{isModified && <span className="text-yellow-500"> {t('status.modified')}</span>}
{!isAdmin && <span className="text-yellow-500">{t('status.readonly_label')}</span>}
<span style={{ opacity: 0.5 }}>{isDark ? t('status.dark') : t('status.light')}</span>
</div>
</footer>
);
+8 -8
View File
@@ -1,22 +1,22 @@
import { useMemo } from 'react';
import { useAppStore } from '@/store/app-store';
import { useTranslation } from 'react-i18next';
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);
const { t } = useTranslation();
const allPaths = useMemo(() => {
const result: { path: string; source: '系统' | '用户'; index: number }[] = [];
sysPaths.all.forEach((p, i) => result.push({ path: p, source: '系统' as const, index: i }));
userPaths.all.forEach((p, i) => result.push({ path: p, source: '用户' as const, index: i }));
const result: { path: string; source: string; index: number }[] = [];
sysPaths.forEach((p, i) => result.push({ path: p, source: t('merge.system'), index: i }));
userPaths.forEach((p, i) => result.push({ path: p, source: t('merge.user'), index: i }));
if (!searchQuery) return result;
const q = searchQuery.toLowerCase();
return result.filter((r) => r.path.toLowerCase().includes(q));
}, [sysPaths, userPaths, searchQuery]);
}, [sysPaths, userPaths, searchQuery, t]);
return (
<div className="flex-1 overflow-auto">
@@ -27,8 +27,8 @@ export function MergePreview() {
style={{ backgroundColor: 'var(--app-list-alt)', color: 'var(--app-fg)' }}
>
<th className="w-10 px-2 py-1">#</th>
<th className="px-2 py-1"></th>
<th className="w-16 px-2 py-1"></th>
<th className="px-2 py-1">{t('dialog.pathLabel')}</th>
<th className="w-16 px-2 py-1">{t('merge.source')}</th>
</tr>
</thead>
<tbody>
+5 -7
View File
@@ -12,8 +12,6 @@ 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);
@@ -31,11 +29,11 @@ export function PathTable({ tabId }: PathTableProps) {
// 过滤搜索
const filtered = useMemo<PathRow[]>(() => {
if (!searchQuery) return paths.all.map((p, i) => ({ path: p, index: i }));
if (!searchQuery) return paths.map((p, i) => ({ path: p, index: i }));
const q = searchQuery.toLowerCase();
const result: PathRow[] = [];
for (let i = 0; i < paths.length; i++) {
const p = paths.get(i)!;
const p = paths[i];
if (p.toLowerCase().includes(q)) result.push({ path: p, index: i });
}
return result;
@@ -44,7 +42,7 @@ export function PathTable({ tabId }: PathTableProps) {
// 异步验证未缓存的路径
useEffect(() => {
let cancelled = false;
const allPaths = paths.all;
const allPaths = paths;
// 找出未缓存的路径
const toValidate = allPaths.filter((p) => !validationCache.has(p));
@@ -81,7 +79,7 @@ export function PathTable({ tabId }: PathTableProps) {
// 异步展开环境变量(用于 tooltip)
useEffect(() => {
let cancelled = false;
const toExpand = paths.all.filter(
const toExpand = paths.filter(
(p) => p.includes('%') && !expandedCache.has(p),
);
if (toExpand.length === 0) return;
@@ -146,7 +144,7 @@ export function PathTable({ tabId }: PathTableProps) {
if (!isActive) return;
window.dispatchEvent(
new CustomEvent('path-dblclick', {
detail: { index: realIndex, path: paths.get(realIndex) },
detail: { index: realIndex, path: paths[realIndex] },
}),
);
},
@@ -16,9 +16,6 @@ export function UndoRedoButtons() {
borderColor: 'var(--app-border)',
};
// 订阅状态更新(canUndo/canRedo 不会触发 re-render,用 setTimeout 简单轮询不优雅,但 Zustand 的 subscribe 可以)
// 这里简化为每次渲染时检查(因为 undo/redo 会修改列表触发重渲染)
return (
<div className="flex gap-1">
<button