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
+77 -38
View File
@@ -3,7 +3,12 @@ import { useAppStore } from '@/store/app-store';
import { TargetType } from '@/core/undo-redo';
import { open } from '@tauri-apps/plugin-dialog';
import { invoke } from '@tauri-apps/api/core';
import { importFromContent, exportToJson, exportToCsv, flattenImportResult } from '@/core/import-export';
import {
importFromContent,
exportToJson,
exportToCsv,
flattenImportResult,
} from '@/core/import-export';
import type { PathEntry } from '@/core/path-entry';
import { is_valid_path_format } from '@/core/validation';
import { useKeyboard } from './use-keyboard';
@@ -38,9 +43,10 @@ export function useAppActions(activeTab: TabId, dialogs: DialogState) {
const idx = useAppStore.getState().selectedIndices[0];
if (idx === undefined) return;
const target = activeTab === 'user' ? TargetType.USER : TargetType.SYSTEM;
const list = target === TargetType.SYSTEM
? useAppStore.getState().sysPaths
: useAppStore.getState().userPaths;
const list =
target === TargetType.SYSTEM
? useAppStore.getState().sysPaths
: useAppStore.getState().userPaths;
const entry = list[idx];
if (entry) setEditDialog({ open: true, index: idx, value: entry.path, target });
}, [activeTab, setEditDialog]);
@@ -71,14 +77,9 @@ export function useAppActions(activeTab: TabId, dialogs: DialogState) {
}, [getCurrentTarget]);
const handleClean = useCallback(() => {
const removed = useAppStore.getState().cleanPaths(
getCurrentTarget(),
is_valid_path_format,
);
const removed = useAppStore.getState().cleanPaths(getCurrentTarget(), is_valid_path_format);
if (removed.length > 0) {
useAppStore.getState().setStatusMessage(
i18n.t('status.deleted', { count: removed.length }),
);
useAppStore.getState().setStatusMessage(i18n.t('status.deleted', { count: removed.length }));
}
}, [getCurrentTarget]);
@@ -95,9 +96,15 @@ export function useAppActions(activeTab: TabId, dialogs: DialogState) {
if (result.system.length > 0 && result.user.length > 0) {
setImportDialog({ open: true, system: result.system, user: result.user });
} else if (result.system.length > 0) {
useAppStore.getState().replacePaths(TargetType.SYSTEM, result.system.map(e => e.path));
useAppStore.getState().replacePaths(
TargetType.SYSTEM,
result.system.map((e) => e.path),
);
} else if (result.user.length > 0) {
useAppStore.getState().replacePaths(TargetType.USER, result.user.map(e => e.path));
useAppStore.getState().replacePaths(
TargetType.USER,
result.user.map((e) => e.path),
);
}
}, [setImportDialog]);
@@ -122,7 +129,10 @@ export function useAppActions(activeTab: TabId, dialogs: DialogState) {
if (result.kind === 'warning') {
// 长度超限,需要用户确认
const { ask } = await import('@tauri-apps/plugin-dialog');
const confirmed = await ask(i18n.t('status.saveWarningLongPaths'), { title: i18n.t('dialog.backupTitle'), kind: 'warning' });
const confirmed = await ask(i18n.t('status.saveWarningLongPaths'), {
title: i18n.t('dialog.backupTitle'),
kind: 'warning',
});
if (confirmed) {
await useAppStore.getState().savePaths(true);
}
@@ -156,33 +166,62 @@ export function useAppActions(activeTab: TabId, dialogs: DialogState) {
// ── 弹窗确认 ──
const handleNewConfirm = useCallback((value: string) => {
setNewDialog(false);
if (value.trim()) useAppStore.getState().addPath(value.trim(), getCurrentTarget());
}, [getCurrentTarget, setNewDialog]);
const handleNewConfirm = useCallback(
(value: string) => {
setNewDialog(false);
if (value.trim()) useAppStore.getState().addPath(value.trim(), getCurrentTarget());
},
[getCurrentTarget, setNewDialog],
);
const handleEditConfirm = useCallback((value: string) => {
const d = dialogs.editDialog;
setEditDialog({ open: false, index: -1, value: '', target: TargetType.SYSTEM });
if (value.trim()) useAppStore.getState().editPath(d.index, value.trim(), d.target);
}, [dialogs.editDialog, setEditDialog]);
const handleEditConfirm = useCallback(
(value: string) => {
const d = dialogs.editDialog;
setEditDialog({ open: false, index: -1, value: '', target: TargetType.SYSTEM });
if (value.trim()) useAppStore.getState().editPath(d.index, value.trim(), d.target);
},
[dialogs.editDialog, setEditDialog],
);
const handleImportSelect = useCallback((target: 'system' | 'user' | 'both') => {
const { system, user } = dialogs.importDialog;
const flat = flattenImportResult({ system, user }, target);
if (target === 'both' && flat.system.length > 0 && flat.user.length > 0) {
useAppStore.getState().replaceBothPaths(flat.system.map(e => e.path), flat.user.map(e => e.path));
} else {
if (flat.system.length > 0) useAppStore.getState().replacePaths(TargetType.SYSTEM, flat.system.map(e => e.path));
if (flat.user.length > 0) useAppStore.getState().replacePaths(TargetType.USER, flat.user.map(e => e.path));
}
setImportDialog({ open: false, system: [], user: [] });
}, [dialogs.importDialog, setImportDialog]);
const handleImportSelect = useCallback(
(target: 'system' | 'user' | 'both') => {
const { system, user } = dialogs.importDialog;
const flat = flattenImportResult({ system, user }, target);
if (target === 'both' && flat.system.length > 0 && flat.user.length > 0) {
useAppStore.getState().replaceBothPaths(
flat.system.map((e) => e.path),
flat.user.map((e) => e.path),
);
} else {
if (flat.system.length > 0)
useAppStore.getState().replacePaths(
TargetType.SYSTEM,
flat.system.map((e) => e.path),
);
if (flat.user.length > 0)
useAppStore.getState().replacePaths(
TargetType.USER,
flat.user.map((e) => e.path),
);
}
setImportDialog({ open: false, system: [], user: [] });
},
[dialogs.importDialog, setImportDialog],
);
return {
handleNew, handleEdit, handleBrowse, handleDelete,
handleMoveUp, handleMoveDown, handleClean,
handleImport, handleExport, handleSave,
handleNewConfirm, handleEditConfirm, handleImportSelect,
handleNew,
handleEdit,
handleBrowse,
handleDelete,
handleMoveUp,
handleMoveDown,
handleClean,
handleImport,
handleExport,
handleSave,
handleNewConfirm,
handleEditConfirm,
handleImportSelect,
};
}