mirror of
https://github.com/LHY0125/PathEditor.git
synced 2026-06-30 18:45:55 +08:00
Compare commits
4 Commits
d28861ff9c
..
v4.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 41008e9282 | |||
| 732b2aabaa | |||
| 19bdb3078a | |||
| bdbb399ddc |
@@ -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
@@ -22,4 +22,5 @@ dist-ssr
|
|||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
.claude/worktrees/
|
.claude/
|
||||||
|
CLAUDE.md
|
||||||
|
|||||||
@@ -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` 替代
|
|
||||||
@@ -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
|
```bash
|
||||||
# 安装依赖
|
# 安装依赖
|
||||||
npm install
|
npm install
|
||||||
|
|
||||||
# 开发模式(热更新)
|
|
||||||
npx tauri dev
|
|
||||||
|
|
||||||
# 构建安装包
|
# 构建安装包
|
||||||
npx tauri build
|
npx tauri build
|
||||||
```
|
```
|
||||||
|
|
||||||
## 技术栈
|
> **要求**:Windows 10+(自带 WebView2),管理员权限才能编辑系统 PATH。
|
||||||
|
|
||||||
| 层 | 技术 |
|
|
||||||
|---|---|
|
|
||||||
| 前端框架 | 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 | 帮助 |
|
|
||||||
|
|
||||||
## 开发
|
## 开发
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# 开发模式(热更新)
|
||||||
|
npx tauri dev
|
||||||
|
|
||||||
|
# 仅前端
|
||||||
|
npm run dev
|
||||||
|
|
||||||
# 前端测试
|
# 前端测试
|
||||||
npm test
|
npm test
|
||||||
|
|
||||||
# 前端测试(监听模式)
|
|
||||||
npm run test:watch
|
|
||||||
|
|
||||||
# Rust 后端检查
|
# Rust 后端检查
|
||||||
cd src-tauri && cargo check
|
cd src-tauri && cargo check
|
||||||
|
|
||||||
@@ -90,10 +96,78 @@ cd src-tauri && cargo check
|
|||||||
cd src-tauri && cargo test
|
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
|
MIT License
|
||||||
|
|
||||||
## 作者
|
## 作者
|
||||||
|
|
||||||
刘航宇 — [GitHub](https://github.com/LHY0125/PathEditor)
|
[刘航宇](https://github.com/LHY0125) — 河南理工大学人工智能协会
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
/// 传给前端的统一错误类型(保留供未来使用,当前命令返回 Result<T, String>)
|
/// 传给前端的统一错误类型(保留供未来迁移使用,届时所有命令改为返回 Result<T, AppError>)
|
||||||
|
#[allow(dead_code)]
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
pub struct AppError {
|
pub struct AppError {
|
||||||
pub message: String,
|
pub message: String,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useAppStore } from '@/store/app-store';
|
import { useAppStore } from '@/store/app-store';
|
||||||
|
import { btnClass, btnStyle } from '@/components/ui/buttons';
|
||||||
|
|
||||||
interface ActionButtonsProps {
|
interface ActionButtonsProps {
|
||||||
onNew: () => void;
|
onNew: () => void;
|
||||||
@@ -24,14 +25,6 @@ export function ActionButtons({
|
|||||||
const isAdmin = useAppStore((s) => s.isAdmin);
|
const isAdmin = useAppStore((s) => s.isAdmin);
|
||||||
const disabled = !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 (
|
return (
|
||||||
<div className="flex gap-1 flex-wrap">
|
<div className="flex gap-1 flex-wrap">
|
||||||
<button className={btnClass} style={btnStyle} disabled={disabled} onClick={onNew}>
|
<button className={btnClass} style={btnStyle} disabled={disabled} onClick={onNew}>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useAppStore } from '@/store/app-store';
|
import { useAppStore } from '@/store/app-store';
|
||||||
|
import { btnClass, btnStyle } from '@/components/ui/buttons';
|
||||||
import { SearchInput } from './SearchInput';
|
import { SearchInput } from './SearchInput';
|
||||||
import { ActionButtons } from './ActionButtons';
|
import { ActionButtons } from './ActionButtons';
|
||||||
import { UndoRedoButtons } from './UndoRedoButtons';
|
import { UndoRedoButtons } from './UndoRedoButtons';
|
||||||
@@ -26,14 +27,6 @@ export function ToolBar(props: ToolBarProps) {
|
|||||||
const isAdmin = useAppStore((s) => s.isAdmin);
|
const isAdmin = useAppStore((s) => s.isAdmin);
|
||||||
const isModified = useAppStore((s) => s.isModified);
|
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 (
|
return (
|
||||||
<div className="space-y-2 pb-2 border-b" style={{ borderColor: 'var(--app-border)' }}>
|
<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" />
|
<div className="flex-1" />
|
||||||
<UndoRedoButtons />
|
<UndoRedoButtons />
|
||||||
<button
|
<button
|
||||||
className={sysBtnClass}
|
className={btnClass}
|
||||||
style={sysBtnStyle}
|
style={btnStyle}
|
||||||
disabled={!isAdmin}
|
disabled={!isAdmin}
|
||||||
onClick={props.onImport}
|
onClick={props.onImport}
|
||||||
>
|
>
|
||||||
{t('button.import')}
|
{t('button.import')}
|
||||||
</button>
|
</button>
|
||||||
<button className={sysBtnClass} style={sysBtnStyle} onClick={props.onExport}>
|
<button className={btnClass} style={btnStyle} onClick={props.onExport}>
|
||||||
{t('button.export')}
|
{t('button.export')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={sysBtnClass}
|
className={btnClass}
|
||||||
style={{
|
style={{
|
||||||
...sysBtnStyle,
|
...btnStyle,
|
||||||
backgroundColor: isModified ? '#2563eb' : sysBtnStyle.backgroundColor,
|
backgroundColor: isModified ? '#2563eb' : btnStyle.backgroundColor,
|
||||||
color: isModified ? '#fff' : sysBtnStyle.color,
|
color: isModified ? '#fff' : btnStyle.color,
|
||||||
}}
|
}}
|
||||||
disabled={!isAdmin}
|
disabled={!isAdmin}
|
||||||
onClick={props.onSave}
|
onClick={props.onSave}
|
||||||
>
|
>
|
||||||
{t('button.save')}
|
{t('button.save')}
|
||||||
</button>
|
</button>
|
||||||
<button className={sysBtnClass} style={sysBtnStyle} onClick={props.onCancel}>
|
<button className={btnClass} style={btnStyle} onClick={props.onCancel}>
|
||||||
{t('button.cancel')}
|
{t('button.cancel')}
|
||||||
</button>
|
</button>
|
||||||
<button className={sysBtnClass} style={sysBtnStyle} onClick={props.onHelp}>
|
<button className={btnClass} style={btnStyle} onClick={props.onHelp}>
|
||||||
{t('button.help')}
|
{t('button.help')}
|
||||||
</button>
|
</button>
|
||||||
<button className={sysBtnClass} style={sysBtnStyle} onClick={props.onLanguage}>
|
<button className={btnClass} style={btnStyle} onClick={props.onLanguage}>
|
||||||
{t('button.language')}
|
{t('button.language')}
|
||||||
</button>
|
</button>
|
||||||
<button className={sysBtnClass} style={sysBtnStyle} onClick={props.onDarkMode}>
|
<button className={btnClass} style={btnStyle} onClick={props.onDarkMode}>
|
||||||
{t('button.darkMode')}
|
{t('button.darkMode')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useAppStore } from '@/store/app-store';
|
import { useAppStore } from '@/store/app-store';
|
||||||
|
import { btnClass, btnStyle } from '@/components/ui/buttons';
|
||||||
|
|
||||||
export function UndoRedoButtons() {
|
export function UndoRedoButtons() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -8,14 +9,6 @@ export function UndoRedoButtons() {
|
|||||||
const undo = useAppStore((s) => s.undo);
|
const undo = useAppStore((s) => s.undo);
|
||||||
const redo = useAppStore((s) => s.redo);
|
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 (
|
return (
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -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)',
|
||||||
|
};
|
||||||
@@ -193,10 +193,10 @@ export function importFromContent(
|
|||||||
content: string,
|
content: string,
|
||||||
filepath: string,
|
filepath: string,
|
||||||
): ImportResult {
|
): ImportResult {
|
||||||
const ext = filepath.toLowerCase();
|
const lower = filepath.toLowerCase();
|
||||||
if (ext.endsWith('.csv')) {
|
if (lower.endsWith('.csv')) {
|
||||||
return importFromCsv(content);
|
return importFromCsv(content);
|
||||||
} else if (ext.endsWith('.json')) {
|
} else if (lower.endsWith('.json')) {
|
||||||
return importFromJson(content);
|
return importFromJson(content);
|
||||||
} else {
|
} else {
|
||||||
// TXT 文件:所有路径放入 system(用户后续可选择目标)
|
// TXT 文件:所有路径放入 system(用户后续可选择目标)
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ export function useAppActions(activeTab: TabId, dialogs: DialogState) {
|
|||||||
const content = isCsv ? exportToCsv(data) : exportToJson(data);
|
const content = isCsv ? exportToCsv(data) : exportToJson(data);
|
||||||
const mime = isCsv ? 'text/csv' : 'application/json';
|
const mime = isCsv ? 'text/csv' : 'application/json';
|
||||||
const ext = isCsv ? '.csv' : '.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 url = URL.createObjectURL(blob);
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = url;
|
a.href = url;
|
||||||
|
|||||||
@@ -39,8 +39,6 @@ interface AppState {
|
|||||||
|
|
||||||
undo: () => void;
|
undo: () => void;
|
||||||
redo: () => void;
|
redo: () => void;
|
||||||
canUndo: () => boolean;
|
|
||||||
canRedo: () => boolean;
|
|
||||||
|
|
||||||
loadPaths: () => Promise<void>;
|
loadPaths: () => Promise<void>;
|
||||||
savePaths: () => Promise<void>;
|
savePaths: () => Promise<void>;
|
||||||
@@ -227,9 +225,6 @@ export const useAppStore = create<AppState>((set, get) => ({
|
|||||||
set({ isModified: !(arraysEqual(sysPaths, _savedSys) && arraysEqual(userPaths, _savedUser)) });
|
set({ isModified: !(arraysEqual(sysPaths, _savedSys) && arraysEqual(userPaths, _savedUser)) });
|
||||||
},
|
},
|
||||||
|
|
||||||
canUndo: () => get().undoRedo.canUndo(),
|
|
||||||
canRedo: () => get().undoRedo.canRedo(),
|
|
||||||
|
|
||||||
loadPaths: async () => {
|
loadPaths: async () => {
|
||||||
try {
|
try {
|
||||||
set({ isLoading: true });
|
set({ isLoading: true });
|
||||||
|
|||||||
Reference in New Issue
Block a user