本文档供 Claude Code 阅读,用于指导从 Electron + React 到 Tauri + React 的完整重构。
Desk Manager 是一个 Windows 桌面小组件应用,包含文件管理和待办事项两大功能,以紧凑窗口形式常驻桌面。
| 需求 | 详细说明 |
|---|---|
| 无边框窗口 | 不使用系统标题栏,自定义 TopBar(标题 + 锁定/设置/关闭按钮) |
| 透明背景 | 窗口背景 rgba(255,255,255, dynamic),用户可调节透明度 0-100% |
| 窗口锁定 | 锁定后禁止拖动和缩放,边框变为蓝色发光 |
| 边缘吸附自动隐藏 | 靠近屏幕边缘(8px)后自动缩成 6px 细条;鼠标进入 60px 触发区展开 |
| 居中启动 | 每次启动窗口在主屏幕居中,不保存位置 |
| 尺寸约束 | 解锁时最小 350×500,最大为屏幕宽度的 50% × 全屏高度 |
| 尺寸持久化 | 窗口大小保存到磁盘(500ms 防抖),启动时恢复 |
| 需求 | 详细说明 |
|---|---|
| 拖拽添加 | 从 Windows 桌面/资源管理器拖拽文件或文件夹到窗口 |
| 文件图标 | 显示 Windows 系统原生图标(当前使用 Electron app.getFileIcon()) |
| 文件夹图标 | 固定显示 📁 emoji |
| 双击打开 | 双击文件卡片,用系统默认程序打开 |
| 右键菜单 | 右键文件卡片显示「删除」选项 |
| 卡片布局 | 纵向列表排列,每项 40px 高,图标 32×32 + 文件名 |
| 需求 | 详细说明 |
|---|---|
| 创建任务 | 输入标题(必填)和描述(可选),自动记录当前时间 |
| 编辑任务 | 点击标题进入编辑模式,可修改标题和描述 |
| 标记完成 | 点击 checkbox 标记完成,标题划线 + 降低透明度 |
| 软删除 | 右键「删除」设置 deleted: true,可在已删除分类中恢复 |
| 三分类标签页 | 待办(未完成未删除)/ 已完成(completed=true)/ 已删除(deleted=true) |
| 拖拽排序 | 待办标签页中可拖拽调整任务顺序(交换 order 值) |
| 时间提醒 | 每 60 秒检查一次,距截止时间 ≤5 分钟时发送系统通知 |
| 紧急高亮 | 距截止时间 ≤1 小时时,卡片显示橙色边框和暖色背景 |
| 需求 | 详细说明 |
|---|---|
| 可调节分隔 | 文件区和待办区之间有可拖拽分隔线(6px),范围 20%-80% |
| 设置面板 | 覆盖层显示透明度滑块(0-100) |
| 数据持久化 | 所有状态自动保存到 JSON 文件:透明度、锁定状态、容器列表、待办事项、分隔比例、窗口大小 |
- 整体圆角 12px,卡片/面板 8-10px,按钮 4-6px
- 主题色:蓝色
rgba(100,150,255,*)、绿色#4CAF50、橙色rgba(255,150,50,*)、红色#d32f2f - 半透明毛玻璃效果:TopBar 使用
backdrop-filter: blur(10px) - 过渡动画统一
0.2s ease - 字体:系统字体栈
-apple-system, 'Segoe UI', ... - 自定义滚动条:6px 宽,圆角 3px
Electron 的 app.getFileIcon() 获取 Windows 系统图标并以 base64 data URL 传递到 React 渲染的方案,经过多次修复仍无法正常工作:
| 次数 | 方法 | 结果 |
|---|---|---|
| 1 | 移除 display: none 条件,改用 emoji fallback |
img onLoad 能触发了,但图标仍有异常 |
| 2 | nativeImage.toDataURL() 替代手动 base64 拼接 |
不可行,图标仍无法正常显示 |
已知问题链路:
app.getFileIcon()→ 可能返回空 buffer 或透明图标(已知 Electron bug)- 图标通过 IPC 以 base64 字符串传输 → 中间环节多,出错难排查
- React
<img src="data:...">渲染 → imageLoaded 状态与实际显示不同步 - 文件不存在时直接 return null,跳过 emoji 降级
Electron 打包后安装包 ~85MB(捆绑完整 Chromium + Node.js),对于一个桌面小工具来说过于臃肿。
Electron 应用空闲时占 200-300MB 内存,不适合「常驻桌面」的小工具场景。
| Electron (当前) | Tauri | C# WPF | |
|---|---|---|---|
| 安装包大小 | ~85 MB | ~2-5 MB | ~2-3 MB (需系统 .NET) |
| 内存占用 | 200-300 MB | 30-40 MB | 20-30 MB |
| 启动速度 | 1-2 秒 | < 0.5 秒 | < 0.5 秒 |
| 前端代码复用 | - | 可直接复用 React + TS | 需 XAML 全部重写 |
| Windows 图标获取 | app.getFileIcon() 不稳定 |
Rust windows crate 直接调用 SHGetFileInfo |
ExtractAssociatedIcon() 一行搞定 |
| 渲染引擎 | 捆绑 Chromium | 系统 WebView2 (Win10/11 自带) | 原生 WPF 渲染 |
| 学习成本 | 已掌握 | 需学 Rust(后端部分) | 需学 C# + XAML |
- 安装包缩小 ~95%:从 85MB 降到 2-5MB,用系统自带的 WebView2 而不是捆绑 Chromium
- 前端代码可大量复用:现有的 React 组件、CSS 样式、TypeScript 类型定义可以直接迁移
- Rust 后端直接调用 Windows API:通过
windowscrate 调用SHGetFileInfo获取系统图标,不再依赖 Electron 的封装层 - 内存占用降低 ~85%:从 200-300MB 降到 30-40MB,适合常驻桌面小工具
- Tauri v2 功能完备:支持无边框窗口、透明背景、系统托盘、自定义协议等项目需要的所有特性
方案:自定义协议 file-icon:// + Rust 直接调用 Windows API
渲染进程: <img src="file-icon:///C:/path/to/file.txt">
↓ (浏览器原生请求,无 IPC)
Tauri 主进程: protocol handler
↓ (Rust 直接调用)
Windows API: SHGetFileInfo → HICON → PNG bytes
↓ (二进制响应,无 base64)
渲染进程: 浏览器原生图片渲染
相比 Electron 方案的优势:
- 无 IPC base64 传输开销
- 无 NativeImage 中间转换
- Rust 直接操作 Windows 内存,无 Node.js GC 干扰
- 浏览器原生处理图片加载和缓存
以下 React 组件和样式文件可以基本不改地迁移到 Tauri 项目:
| 文件 | 说明 | 迁移改动 |
|---|---|---|
src/components/TodoList.tsx |
待办事项列表(标签页、拖拽排序) | 无需改动 |
src/components/TodoItem.tsx |
待办事项条目(显示、编辑、右键菜单) | 无需改动 |
src/components/TodoForm.tsx |
任务输入表单 | 无需改动 |
src/components/SettingsPanel.tsx |
设置面板(透明度滑块) | 无需改动 |
src/components/TopBar.tsx |
顶部栏(锁定/设置/关闭按钮) | 替换 window.electronAPI 为 Tauri invoke |
src/components/FileManager.tsx |
文件管理器容器 | 无需改动 |
src/components/FileCard.tsx |
文件卡片 | 图标 src 改为 file-icon:// 协议 URL |
src/App.tsx |
主状态管理 | 替换 window.electronAPI 为 Tauri invoke |
src/App.css |
主布局样式 | 无需改动 |
src/index.css |
全局样式 | 无需改动 |
所有 *.css 组件样式 |
视觉设计 | 无需改动 |
| 文件 | 改动内容 |
|---|---|
src/App.tsx |
window.electronAPI.xxx() → Tauri invoke('xxx', {...}) |
src/components/TopBar.tsx |
electronAPI.closeApp() → Tauri appWindow.close() |
src/components/FileCard.tsx |
data URL 图标 → file-icon:// 协议 URL;移除 imageLoaded 状态 |
src/components/CardContainer.tsx |
1. 拖拽获取文件路径方式适配 Tauri;2. 图标 URL 改为 file-icon://;3. cards 状态提升到 App.tsx 做持久化 |
src/components/ResizeFrame.tsx |
Electron 的无边框缩放改为 Tauri 的 startDragging() / startResizing() API |
| 模块 | Electron 原实现 | Tauri Rust 实现 |
|---|---|---|
| 窗口管理 | electron/main.ts BrowserWindow 配置 |
tauri.conf.json + Rust WindowBuilder |
| 边缘吸附 & 自动隐藏 | main.ts 中 JS 实现(~150 行) | Rust 实现,调用 GetCursorPos / SetWindowPos Windows API |
| 文件图标获取 | app.getFileIcon() → base64 |
Rust SHGetFileInfo → PNG → 自定义协议二进制响应 |
| 数据持久化 | fs.readFileSync/writeFileSync JSON |
Tauri tauri-plugin-store 或 Rust serde_json + fs |
| 系统通知 | Electron Notification |
Tauri tauri-plugin-notification |
| 打开文件 | shell.openPath() |
Tauri tauri-plugin-shell 的 open() |
| 窗口透明度 | mainWindow.setOpacity() |
Tauri WebviewWindow::set_opacity() |
| 窗口锁定 | setResizable(false) + setMovable(false) |
Tauri set_resizable(false) + 自定义逻辑 |
| 检查文件夹 | fs.promises.stat().isDirectory() |
Rust std::fs::metadata().is_dir() |
desk-manager/
├── src/ # 前端代码(从 Electron 版迁移)
│ ├── components/ # React 组件(大部分直接复用)
│ │ ├── TopBar.tsx
│ │ ├── FileManager.tsx
│ │ ├── FileCard.tsx # 改用 file-icon:// 协议
│ │ ├── CardContainer.tsx # 适配 Tauri 拖拽 + 状态提升
│ │ ├── TodoList.tsx # 直接复用
│ │ ├── TodoItem.tsx # 直接复用
│ │ ├── TodoForm.tsx # 直接复用
│ │ ├── SettingsPanel.tsx # 直接复用
│ │ └── ResizeFrame.tsx # 适配 Tauri 窗口 API
│ ├── App.tsx # 适配 Tauri invoke
│ ├── App.css # 直接复用
│ ├── main.tsx # 直接复用
│ └── index.css # 直接复用
├── src-tauri/ # Tauri Rust 后端
│ ├── src/
│ │ ├── main.rs # 应用入口
│ │ ├── lib.rs # Tauri setup、插件注册、IPC commands
│ │ ├── commands/
│ │ │ ├── mod.rs
│ │ │ ├── file_icon.rs # Windows SHGetFileInfo 图标获取
│ │ │ ├── file_ops.rs # is_directory、open_path
│ │ │ ├── app_data.rs # 数据持久化(load/save)
│ │ │ └── window.rs # 锁定、透明度、边缘吸附
│ │ └── protocol.rs # file-icon:// 自定义协议处理
│ ├── Cargo.toml # Rust 依赖
│ ├── tauri.conf.json # Tauri 窗口配置
│ └── icons/ # 应用图标
├── package.json
├── vite.config.ts
└── tsconfig.json
interface AppData {
transparency: number // 0-100
isLocked: boolean
containers: CardContainer[] // 文件容器列表
todos: Todo[] // 所有待办事项
fileManagerHeight: number // 20-80 (百分比)
windowSize: { width: number; height: number }
}interface Todo {
id: string
title: string
time: string // ISO datetime "YYYY-MM-DDTHH:mm"
description: string
completed: boolean
deleted: boolean
order: number // 仅待办标签页有效
}// 当前结构(仅存 id 和 name,cards 在组件本地状态,不持久化)
interface CardContainer {
id: string
name: string
}
// 重构后(cards 纳入持久化)
interface CardContainer {
id: string
name: string
cards: FileCard[]
}
interface FileCard {
id: string
name: string
type: 'file' | 'folder'
path: string
}
// 注意:iconPath 不再存储,由 file-icon:// 协议根据 path 实时获取使用 windows crate 调用 SHGetFileInfoW:
use windows::Win32::UI::Shell::{SHGetFileInfoW, SHFILEINFOW, SHGFI_ICON, SHGFI_LARGEICON};
use windows::Win32::UI::WindowsAndMessaging::DestroyIcon;
// 1. 调用 SHGetFileInfoW 获取 HICON
// 2. 将 HICON 转为 BITMAP(GetIconInfo + GetDIBits)
// 3. 将 BGRA 像素数据编码为 PNG(image crate)
// 4. 通过自定义协议以 image/png 二进制响应
// 5. 调用 DestroyIcon 释放资源在 tauri::Builder 中注册:
.register_asynchronous_uri_scheme_protocol("file-icon", |_ctx, request, responder| {
// 解析路径 → 获取图标 → 返回 PNG 二进制 Response
})前端使用:
<img src="file-icon://localhost/C:/path/to/file.txt" />Tauri 的窗口 API + Windows GetCursorPos:
// 定时检查窗口位置和鼠标位置
// 窗口靠近屏幕边缘 → 记录原始位置 → 缩小到 6px
// 鼠标进入触发区 → 恢复到原始位置和大小[dependencies]
tauri = { version = "2", features = ["tray-icon"] }
tauri-plugin-shell = "2"
tauri-plugin-notification = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
image = "0.25" # PNG 编码
windows = { version = "0.58", features = [
"Win32_UI_Shell", # SHGetFileInfo
"Win32_UI_WindowsAndMessaging", # Icon/Cursor API
"Win32_Graphics_Gdi", # Bitmap 转换
] }- 初始化 Tauri v2 项目(
npm create tauri-app),保留 React + Vite 前端配置 - 复制前端代码:将
src/目录下所有组件和样式迁移到新项目 - 实现 Rust 后端命令:按优先级依次实现
- 数据持久化(load/save)→ 前端状态能正常恢复
- 文件图标自定义协议 → 解决核心痛点
- 文件操作(is_directory、open_path)
- 窗口管理(锁定、透明度)
- 边缘吸附自动隐藏 → 最复杂,最后实现
- 适配前端调用:将
window.electronAPI.xxx替换为invoke('xxx') - 修复 CardContainer 状态提升:将 cards 纳入 App.tsx 管理并持久化
- 测试打包:
npm run tauri build验证安装包大小和功能完整性