Compare commits

..

4 Commits

Author SHA1 Message Date
Serendipity 41008e9282 chore: 从仓库移除 CLAUDE.md 和 .claude/ 内部配置文件
这些是 Claude Code 工具的内部配置文件,对开源项目用户无意义。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 08:27:16 +08:00
Serendipity 732b2aabaa docs: 重写 README.md 为开源项目风格
- 添加 badges(版本/Tauri/React/Rust/测试数)
- 按功能模块分组(路径管理/验证/撤销/导入导出/安全/界面)
- 补充安装章节(Releases + 源码构建)
- 补充贡献指南(开发环境、代码规范)
- 完整项目结构和快捷键表

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 08:22:35 +08:00
Serendipity 19bdb3078a docs: 重写 CLAUDE.md 反映最新项目状态
- 更新架构图(移除已删除的 string-list.ts,补充 Modal/buttons/ErrorBoundary)
- 新增数据模型章节(不可变 string[] + snapshot 比较 isModified)
- 新增撤销/重做 API 说明
- 新增错误处理章节(前端 + Rust 完整覆盖表)
- 补充 TypeScript strict 模式、Rust SAFETY 注释等质量约束

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 08:20:15 +08:00
Serendipity bdbb399ddc refactor: 清理 LOW 问题 — 样式去重、死代码删除、命名修正
- 抽取 buttons.ts 共享按钮样式,消除 3 个组件的重复定义
- store 删除未调用的 canUndo/canRedo 方法
- importFromContent 变量 ext→lower 修正确性
- CSV 导出修复 BOM 重复(exportToCsv 自带 BOM)
- Rust error.rs 添加 allow(dead_code) 消除编译警告

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 08:13:17 +08:00
12 changed files with 165 additions and 231 deletions
-28
View File
@@ -1,28 +0,0 @@
{
"permissions": {
"allow": [
"Bash(cmake --build build)",
"Bash(npm --version)",
"Bash(npm create *)",
"Bash(npm install *)",
"Bash(npx tauri *)",
"Bash(rm -f src/App.css src/index.css)",
"Bash(rm -rf src/assets)",
"Bash(npm run *)",
"Bash(npx tsc *)",
"Bash(npx vite *)",
"Bash(cargo check *)",
"Bash(rustup show *)",
"Bash(rustup toolchain *)",
"Bash(npx vitest *)",
"Bash(cargo build *)",
"Bash(where link.exe)",
"Bash(dir \"C:\\\\Program Files\\\\Microsoft Visual Studio\")",
"Bash(dir \"C:\\\\Program Files \\(x86\\)\\\\Microsoft Visual Studio\")",
"Bash(rustup override *)",
"Bash(dir *)",
"Bash(cargo clean *)",
"Bash(\"D:/settings/Language/Rust/mingw64/bin/gcc.exe\" --version)"
]
}
}
+2 -1
View File
@@ -22,4 +22,5 @@ dist-ssr
*.njsproj
*.sln
*.sw?
.claude/worktrees/
.claude/
CLAUDE.md
-95
View File
@@ -1,95 +0,0 @@
# CLAUDE.md
## 项目概述
PathEditor v4.0 — Windows 系统环境变量 (PATH) 编辑器,使用 Tauri 2.x + React 19 + TypeScript + Rust 构建。
## 构建命令
```bash
# 安装前端依赖
npm install
# 开发模式(热更新)
npx tauri dev
# 仅前端
npm run dev
# 前端测试
npm test
npm run test:watch
# 构建生产版本
npm run build
# Rust 后端检查
cd src-tauri && cargo check
# Rust 后端测试
cd src-tauri && cargo test
# 完整构建(安装包)
npx tauri build
```
## 架构
前后端分离,通过 Tauri IPC 通信:
```
src/ # React 前端 (TypeScript)
├── core/ # 纯逻辑 — 零 React 依赖
│ ├── string-list.ts # StringList 数据结构
│ ├── undo-redo.ts # 撤销/重做管理器(8 种操作类型)
│ ├── path-manager.ts # 路径增删移清理
│ ├── import-export.ts # JSON/CSV/TXT 导入导出
│ └── validation.ts # 路径格式验证
├── store/ # Zustand 状态管理
│ ├── app-store.ts # 主状态(路径、撤销、CRUD、加载/保存)
│ └── theme-store.ts # 深色/浅色模式
├── components/ # React UI 组件
│ ├── layout/ # AppShell、TitleBar、StatusBar
│ ├── path-list/ # PathTable、MergePreview
│ ├── toolbar/ # ToolBar、ActionButtons、UndoRedoButtons、SearchInput
│ └── dialogs/ # PathEditDialog、HelpDialog、ImportDialog
├── hooks/ # use-keyboard、use-path-validation
├── i18n/ # i18next 中英文翻译
└── config/ # default.json UI 参数配置
src-tauri/ # Tauri Rust 后端
├── src/
│ ├── commands/
│ │ ├── registry.rs # 注册表读写(load/save system & user paths
│ │ ├── system.rs # check_admin、validate_path、expand_env_vars、broadcast
│ │ └── backup.rs # backup_registry、get_appdata_dir
│ ├── error.rs
│ └── lib.rs # 注册所有 IPC commands
├── Cargo.toml
└── tauri.conf.json
tests/unit/ # Vitest 前端单元测试
```
## IPC 接口(Rust → Frontend
| Command | 功能 |
|---------|------|
| `load_system_paths` | 从 HKLM 注册表读取系统 PATH |
| `load_user_paths` | 从 HKCU 注册表读取用户 PATH |
| `save_system_paths` | 保存系统 PATH 到注册表 |
| `save_user_paths` | 保存用户 PATH 到注册表 |
| `check_admin` | 检测管理员权限 |
| `validate_path` | 验证路径目录是否存在 |
| `expand_env_vars` | 展开 %VAR% 环境变量 |
| `broadcast_env_change` | 广播 WM_SETTINGCHANGE |
| `backup_registry` | 备份注册表 PATH 到文件 |
| `get_appdata_dir` | 获取备份目录路径 |
## 关键约束
- Rust 工具链:`stable-x86_64-pc-windows-gnu`(项目已设 override
- `.cargo/config.toml` 添加了 `-lmcfgthread` 兼容 GCC 15.2.0 MinGW
- 移除 `cdylib` crate-type 避免 DLL 导出序数溢出
- 运行需要管理员权限才能编辑系统 PATH
- `cargo test` 需要 MinGW bin 在 PATH 中(GCC 15.2.0 运行时依赖 `libmcfgthread-2.dll`),开发模式下可用 `npx tauri dev` 替代
+136 -62
View File
@@ -1,88 +1,94 @@
# PathEditor v4.0
<p align="center">
<h1>PathEditor</h1>
<p>Windows 系统环境变量 (PATH) 编辑器</p>
</p>
Windows 系统环境变量 (PATH) 编辑器,基于 Tauri 2.x + React 19 + TypeScript + Rust 构建。
<p align="center">
<img src="https://img.shields.io/badge/version-4.0.0-blue" alt="version">
<img src="https://img.shields.io/badge/tauri-2.x-ffa03a" alt="tauri">
<img src="https://img.shields.io/badge/react-19-61dafb" alt="react">
<img src="https://img.shields.io/badge/rust-1.95-000000" alt="rust">
<img src="https://img.shields.io/badge/typescript-strict-blue" alt="typescript">
<img src="https://img.shields.io/badge/license-MIT-green" alt="license">
<img src="https://img.shields.io/badge/tests-55%20passed-brightgreen" alt="tests">
</p>
---
## 简介
PathEditor 是 Windows PATH 环境变量的可视化管理工具。支持系统变量和用户变量的增删改查、拖拽排序、一键清理无效路径、导入导出以及完整的撤销/重做。
v4.0 使用 **Tauri 2.x + React 19 + TypeScript + Rust** 完全重写,替代了原有的 C + IUP GUI。
## 截图
_[待补充]_
## 功能
- 查看和编辑系统/用户 PATH 环境变量
### 路径管理
- 查看和编辑 **系统 PATH**HKLM)和 **用户 PATH**HKCU
- 新建、编辑、删除、上移、下移路径条目
- 一键清理无效和重复路径
- 完整撤销/重做支持(最多 50 步)
- 导入/导出 JSON、CSV、TXT 三种格式
- 深色模式 / 浅色模式切换
- 中英文界面切换
- 合并预览(同时查看系统 + 用户路径)
- 搜索过滤
- 多选批量删除
- 实时搜索过滤
- 合并预览(系统 + 用户路径并列显示)
- 文件夹拖拽添加
- 注册表备份
## 运行
### 路径验证
- **红色**标记:路径在文件系统中不存在
- **橙色**标记:路径在列表中重复出现
- 环境变量路径(含 `%VAR%`)悬浮展开预览
需要管理员权限才能编辑系统 PATH(非管理员自动进入只读模式)。
### 撤销/重做
- 支持 8 种操作类型,最多 50 步历史
- 新增、删除、编辑、移动、清理、清空、导入均可撤销
### 导入/导出
- **JSON**:结构化导出,含版本和时间戳
- **CSV**UTF-8 BOM 编码,兼容 Excel
- **TXT**:纯文本,每行一个路径
### 安全
- 保存前自动备份注册表到 `%APPDATA%/PathEditor/backups/`
- PATH 长度检查(Windows 单变量上限 32767 字符)
- 非管理员自动进入**只读模式**
- 保存中途失败精确提示哪个注册表 hive 出错
### 界面
- 深色模式 / 浅色模式
- 中文 / English 界面切换
- 全局键盘快捷键
- 修改状态指示(黄点)+ 未保存退出确认
## 安装
从 [Releases](https://github.com/LHY0125/PathEditor/releases) 下载最新版 `PathEditor_4.0.0_x64-setup.exe` 安装。
或从源码构建:
```bash
# 安装依赖
npm install
# 开发模式(热更新)
npx tauri dev
# 构建安装包
npx tauri build
```
## 技术栈
| 层 | 技术 |
|---|---|
| 前端框架 | React 19 + TypeScript |
| UI 样式 | Tailwind CSS 4 |
| 状态管理 | Zustand |
| 国际化 | i18next |
| 桌面框架 | Tauri 2.x |
| 后端语言 | Rust |
| 测试 | Vitest (前端) |
| 构建 | Vite |
## 架构
```
src/ # React 前端
├── core/ # 纯逻辑(StringList、撤销/重做、路径管理、导入导出)
├── store/ # Zustand 状态管理
├── components/ # UI 组件(列表、工具栏、对话框)
├── hooks/ # 自定义 Hooks(键盘快捷键、路径验证)
├── i18n/ # 中英文翻译
└── config/ # UI 参数配置
src-tauri/ # Rust 后端
└── src/commands/
├── registry.rs # 注册表读写
├── system.rs # 权限检测、路径验证、环境变量展开、系统广播
└── backup.rs # 注册表备份
```
## 快捷键
| 快捷键 | 功能 |
|--------|------|
| Ctrl+N | 新建路径 |
| Ctrl+S | 保存 |
| Ctrl+Z | 撤销 |
| Ctrl+Y | 重做 |
| Ctrl+F | 搜索 |
| Delete | 删除选中 |
| F1 | 帮助 |
> **要求**Windows 10+(自带 WebView2),管理员权限才能编辑系统 PATH。
## 开发
```bash
# 开发模式(热更新)
npx tauri dev
# 仅前端
npm run dev
# 前端测试
npm test
# 前端测试(监听模式)
npm run test:watch
# Rust 后端检查
cd src-tauri && cargo check
@@ -90,10 +96,78 @@ cd src-tauri && cargo check
cd src-tauri && cargo test
```
### 技术栈
| 层 | 技术 |
|---|---|
| 前端框架 | React 19 + TypeScript (strict) |
| UI 样式 | Tailwind CSS 4 |
| 状态管理 | Zustand |
| 国际化 | i18next |
| 桌面框架 | Tauri 2.x |
| 后端 | Rust (winreg + windows-rs FFI) |
| 前端测试 | Vitest (45 个测试) |
| Rust 测试 | cargo test (10 个测试) |
| 构建 | Vite |
| 打包 | NSIS |
### 项目结构
```
src/ # React 前端
├── core/ # 纯逻辑 — 零框架依赖、零平台依赖
├── store/ # Zustand 状态管理
├── components/
│ ├── layout/ # AppShell、TitleBar、StatusBar、ErrorBoundary
│ ├── path-list/ # PathTable、MergePreview
│ ├── toolbar/ # ToolBar、ActionButtons、UndoRedoButtons、SearchInput
│ ├── dialogs/ # PathEditDialog、HelpDialog、ImportDialog
│ └── ui/ # Modal、buttons(共享组件)
├── hooks/ # useAppActions、useKeyboard
├── i18n/ # zh-CN / en
└── config/ # default.json
src-tauri/ # Rust 后端
└── src/commands/
├── registry.rs # 注册表读写
├── system.rs # 权限检测、路径验证、环境变量展开
└── backup.rs # 注册表备份
tests/unit/ # 前端单元测试
```
## 快捷键
| 快捷键 | 功能 |
|--------|------|
| `Ctrl+N` | 新建路径 |
| `Ctrl+S` | 保存 |
| `Ctrl+Z` | 撤销 |
| `Ctrl+Y` | 重做 |
| `Ctrl+F` | 搜索 |
| `Delete` | 删除选中 |
| `F1` | 帮助 |
## 贡献
欢迎提交 Issue 和 Pull Request。在开始大改动前,建议先开 Issue 讨论。
### 本地开发环境
- Node.js 22+
- Rust 1.95+ (stable-x86_64-pc-windows-gnu)
- MinGW-w64 (GCC 15.x 需配置 `-lmcfgthread` 链接标志)
### 代码规范
- TypeScript `strict: true`,零编译错误
- 所有 Rust `unsafe` 块必须有 `// SAFETY:` 注释
- 前端核心逻辑在 `src/core/`,纯函数,零依赖,可独立测试
## 许可证
MIT License
## 作者
刘航宇 — [GitHub](https://github.com/LHY0125/PathEditor)
[刘航宇](https://github.com/LHY0125) — 河南理工大学人工智能协会
+2 -1
View File
@@ -1,6 +1,7 @@
use serde::Serialize;
/// 传给前端的统一错误类型(保留供未来使用,当前命令返回 Result<T, String>
/// 传给前端的统一错误类型(保留供未来迁移使用,届时所有命令改为返回 Result<T, AppError>
#[allow(dead_code)]
#[derive(Debug, Serialize)]
pub struct AppError {
pub message: String,
+1 -8
View File
@@ -1,5 +1,6 @@
import { useTranslation } from 'react-i18next';
import { useAppStore } from '@/store/app-store';
import { btnClass, btnStyle } from '@/components/ui/buttons';
interface ActionButtonsProps {
onNew: () => void;
@@ -24,14 +25,6 @@ export function ActionButtons({
const isAdmin = useAppStore((s) => s.isAdmin);
const disabled = !isAdmin;
const btnClass =
'px-3 py-1 text-sm rounded border transition-colors disabled:opacity-40 disabled:cursor-not-allowed';
const btnStyle = {
backgroundColor: 'var(--app-bg)',
color: 'var(--app-fg)',
borderColor: 'var(--app-border)',
};
return (
<div className="flex gap-1 flex-wrap">
<button className={btnClass} style={btnStyle} disabled={disabled} onClick={onNew}>
+12 -19
View File
@@ -1,5 +1,6 @@
import { useTranslation } from 'react-i18next';
import { useAppStore } from '@/store/app-store';
import { btnClass, btnStyle } from '@/components/ui/buttons';
import { SearchInput } from './SearchInput';
import { ActionButtons } from './ActionButtons';
import { UndoRedoButtons } from './UndoRedoButtons';
@@ -26,14 +27,6 @@ export function ToolBar(props: ToolBarProps) {
const isAdmin = useAppStore((s) => s.isAdmin);
const isModified = useAppStore((s) => s.isModified);
const sysBtnClass =
'px-3 py-1 text-sm rounded border transition-colors disabled:opacity-40 disabled:cursor-not-allowed';
const sysBtnStyle = {
backgroundColor: 'var(--app-bg)',
color: 'var(--app-fg)',
borderColor: 'var(--app-border)',
};
return (
<div className="space-y-2 pb-2 border-b" style={{ borderColor: 'var(--app-border)' }}>
{/* 第一行: 搜索 + 系统按钮 */}
@@ -42,38 +35,38 @@ export function ToolBar(props: ToolBarProps) {
<div className="flex-1" />
<UndoRedoButtons />
<button
className={sysBtnClass}
style={sysBtnStyle}
className={btnClass}
style={btnStyle}
disabled={!isAdmin}
onClick={props.onImport}
>
{t('button.import')}
</button>
<button className={sysBtnClass} style={sysBtnStyle} onClick={props.onExport}>
<button className={btnClass} style={btnStyle} onClick={props.onExport}>
{t('button.export')}
</button>
<button
className={sysBtnClass}
className={btnClass}
style={{
...sysBtnStyle,
backgroundColor: isModified ? '#2563eb' : sysBtnStyle.backgroundColor,
color: isModified ? '#fff' : sysBtnStyle.color,
...btnStyle,
backgroundColor: isModified ? '#2563eb' : btnStyle.backgroundColor,
color: isModified ? '#fff' : btnStyle.color,
}}
disabled={!isAdmin}
onClick={props.onSave}
>
{t('button.save')}
</button>
<button className={sysBtnClass} style={sysBtnStyle} onClick={props.onCancel}>
<button className={btnClass} style={btnStyle} onClick={props.onCancel}>
{t('button.cancel')}
</button>
<button className={sysBtnClass} style={sysBtnStyle} onClick={props.onHelp}>
<button className={btnClass} style={btnStyle} onClick={props.onHelp}>
{t('button.help')}
</button>
<button className={sysBtnClass} style={sysBtnStyle} onClick={props.onLanguage}>
<button className={btnClass} style={btnStyle} onClick={props.onLanguage}>
{t('button.language')}
</button>
<button className={sysBtnClass} style={sysBtnStyle} onClick={props.onDarkMode}>
<button className={btnClass} style={btnStyle} onClick={props.onDarkMode}>
{t('button.darkMode')}
</button>
</div>
+1 -8
View File
@@ -1,5 +1,6 @@
import { useTranslation } from 'react-i18next';
import { useAppStore } from '@/store/app-store';
import { btnClass, btnStyle } from '@/components/ui/buttons';
export function UndoRedoButtons() {
const { t } = useTranslation();
@@ -8,14 +9,6 @@ export function UndoRedoButtons() {
const undo = useAppStore((s) => s.undo);
const redo = useAppStore((s) => s.redo);
const btnClass =
'px-3 py-1 text-sm rounded border transition-colors disabled:opacity-40 disabled:cursor-not-allowed';
const btnStyle = {
backgroundColor: 'var(--app-bg)',
color: 'var(--app-fg)',
borderColor: 'var(--app-border)',
};
return (
<div className="flex gap-1">
<button
+7
View File
@@ -0,0 +1,7 @@
export const btnClass = 'px-3 py-1 text-sm rounded border transition-colors disabled:opacity-40 disabled:cursor-not-allowed';
export const btnStyle: React.CSSProperties = {
backgroundColor: 'var(--app-bg)',
color: 'var(--app-fg)',
borderColor: 'var(--app-border)',
};
+3 -3
View File
@@ -193,10 +193,10 @@ export function importFromContent(
content: string,
filepath: string,
): ImportResult {
const ext = filepath.toLowerCase();
if (ext.endsWith('.csv')) {
const lower = filepath.toLowerCase();
if (lower.endsWith('.csv')) {
return importFromCsv(content);
} else if (ext.endsWith('.json')) {
} else if (lower.endsWith('.json')) {
return importFromJson(content);
} else {
// TXT 文件:所有路径放入 system(用户后续可选择目标)
+1 -1
View File
@@ -108,7 +108,7 @@ export function useAppActions(activeTab: TabId, dialogs: DialogState) {
const content = isCsv ? exportToCsv(data) : exportToJson(data);
const mime = isCsv ? 'text/csv' : 'application/json';
const ext = isCsv ? '.csv' : '.json';
const blob = new Blob([isCsv ? '' : '', content], { type: mime });
const blob = new Blob([content], { type: mime });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
-5
View File
@@ -39,8 +39,6 @@ interface AppState {
undo: () => void;
redo: () => void;
canUndo: () => boolean;
canRedo: () => boolean;
loadPaths: () => Promise<void>;
savePaths: () => Promise<void>;
@@ -227,9 +225,6 @@ export const useAppStore = create<AppState>((set, get) => ({
set({ isModified: !(arraysEqual(sysPaths, _savedSys) && arraysEqual(userPaths, _savedUser)) });
},
canUndo: () => get().undoRedo.canUndo(),
canRedo: () => get().undoRedo.canRedo(),
loadPaths: async () => {
try {
set({ isLoading: true });