docs: add comprehensive refactoring documentation
Add three key documents to guide the project restructure: - REFACTORING_MASTER_PLAN.md: Complete refactoring roadmap with 6 stages - REFACTORING_CHECKLIST.md: Detailed task checklist for tracking progress - REFACTORING_REFERENCE.md: Technical reference and implementation guide This refactoring aims to modernize the codebase with React Query, react-hook-form, zod validation, and shadcn/ui components while maintaining the current Tailwind CSS 4.x stack. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
489
docs/REFACTORING_CHECKLIST.md
Normal file
489
docs/REFACTORING_CHECKLIST.md
Normal file
@@ -0,0 +1,489 @@
|
|||||||
|
# CC Switch 重构实施清单
|
||||||
|
|
||||||
|
> 用于跟踪重构进度的详细检查清单
|
||||||
|
|
||||||
|
**开始日期**: ___________
|
||||||
|
**预计完成**: ___________
|
||||||
|
**当前阶段**: ___________
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 阶段 0: 准备阶段 (预计 1 天)
|
||||||
|
|
||||||
|
### 环境准备
|
||||||
|
|
||||||
|
- [ ] 创建新分支 `refactor/modernization`
|
||||||
|
- [ ] 创建备份标签 `git tag backup-before-refactor`
|
||||||
|
- [ ] 备份用户配置文件 `~/.cc-switch/config.json`
|
||||||
|
- [ ] 通知团队成员重构开始
|
||||||
|
|
||||||
|
### 依赖安装
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm add @tanstack/react-query
|
||||||
|
pnpm add react-hook-form @hookform/resolvers
|
||||||
|
pnpm add zod
|
||||||
|
pnpm add sonner
|
||||||
|
pnpm add next-themes
|
||||||
|
pnpm add @radix-ui/react-dialog @radix-ui/react-dropdown-menu
|
||||||
|
pnpm add @radix-ui/react-label @radix-ui/react-select
|
||||||
|
pnpm add @radix-ui/react-slot @radix-ui/react-switch @radix-ui/react-tabs
|
||||||
|
pnpm add class-variance-authority clsx tailwind-merge tailwindcss-animate
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] 安装核心依赖 (上述命令)
|
||||||
|
- [ ] 验证依赖安装成功 `pnpm install`
|
||||||
|
- [ ] 验证编译通过 `pnpm typecheck`
|
||||||
|
|
||||||
|
### 配置文件
|
||||||
|
|
||||||
|
- [ ] 创建 `components.json`
|
||||||
|
- [ ] 更新 `tsconfig.json` 添加路径别名
|
||||||
|
- [ ] 更新 `vite.config.mts` 添加路径解析
|
||||||
|
- [ ] 验证开发服务器启动 `pnpm dev`
|
||||||
|
|
||||||
|
**完成时间**: ___________
|
||||||
|
**遇到的问题**: ___________
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 阶段 1: 基础设施 (预计 2-3 天)
|
||||||
|
|
||||||
|
### 1.1 工具函数和基础组件
|
||||||
|
|
||||||
|
- [ ] 创建 `src/lib/utils.ts` (cn 函数)
|
||||||
|
- [ ] 创建 `src/components/ui/button.tsx`
|
||||||
|
- [ ] 创建 `src/components/ui/dialog.tsx`
|
||||||
|
- [ ] 创建 `src/components/ui/input.tsx`
|
||||||
|
- [ ] 创建 `src/components/ui/label.tsx`
|
||||||
|
- [ ] 创建 `src/components/ui/textarea.tsx`
|
||||||
|
- [ ] 创建 `src/components/ui/select.tsx`
|
||||||
|
- [ ] 创建 `src/components/ui/switch.tsx`
|
||||||
|
- [ ] 创建 `src/components/ui/tabs.tsx`
|
||||||
|
- [ ] 创建 `src/components/ui/sonner.tsx`
|
||||||
|
- [ ] 创建 `src/components/ui/form.tsx`
|
||||||
|
|
||||||
|
**测试**:
|
||||||
|
- [ ] 验证所有 UI 组件可以正常导入
|
||||||
|
- [ ] 创建一个测试页面验证组件样式
|
||||||
|
|
||||||
|
### 1.2 Query Client 设置
|
||||||
|
|
||||||
|
- [ ] 创建 `src/lib/query/queryClient.ts`
|
||||||
|
- [ ] 配置默认选项 (retry, staleTime 等)
|
||||||
|
- [ ] 导出 queryClient 实例
|
||||||
|
|
||||||
|
### 1.3 API 层
|
||||||
|
|
||||||
|
- [ ] 创建 `src/lib/api/providers.ts`
|
||||||
|
- [ ] getAll
|
||||||
|
- [ ] getCurrent
|
||||||
|
- [ ] add
|
||||||
|
- [ ] update
|
||||||
|
- [ ] delete
|
||||||
|
- [ ] switch
|
||||||
|
- [ ] importDefault
|
||||||
|
- [ ] updateTrayMenu
|
||||||
|
|
||||||
|
- [ ] 创建 `src/lib/api/settings.ts`
|
||||||
|
- [ ] get
|
||||||
|
- [ ] save
|
||||||
|
|
||||||
|
- [ ] 创建 `src/lib/api/mcp.ts`
|
||||||
|
- [ ] getConfig
|
||||||
|
- [ ] upsertServer
|
||||||
|
- [ ] deleteServer
|
||||||
|
|
||||||
|
- [ ] 创建 `src/lib/api/index.ts` (聚合导出)
|
||||||
|
|
||||||
|
**测试**:
|
||||||
|
- [ ] 验证 API 调用不会出现运行时错误
|
||||||
|
- [ ] 确认类型定义正确
|
||||||
|
|
||||||
|
### 1.4 Query Hooks
|
||||||
|
|
||||||
|
- [ ] 创建 `src/lib/query/queries.ts`
|
||||||
|
- [ ] useProvidersQuery
|
||||||
|
- [ ] useSettingsQuery
|
||||||
|
- [ ] useMcpConfigQuery
|
||||||
|
|
||||||
|
- [ ] 创建 `src/lib/query/mutations.ts`
|
||||||
|
- [ ] useAddProviderMutation
|
||||||
|
- [ ] useSwitchProviderMutation
|
||||||
|
- [ ] useDeleteProviderMutation
|
||||||
|
- [ ] useUpdateProviderMutation
|
||||||
|
- [ ] useSaveSettingsMutation
|
||||||
|
|
||||||
|
- [ ] 创建 `src/lib/query/index.ts` (聚合导出)
|
||||||
|
|
||||||
|
**测试**:
|
||||||
|
- [ ] 在临时组件中测试每个 hook
|
||||||
|
- [ ] 验证 loading/error 状态正确
|
||||||
|
- [ ] 验证缓存和自动刷新工作
|
||||||
|
|
||||||
|
**完成时间**: ___________
|
||||||
|
**遇到的问题**: ___________
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 阶段 2: 核心功能重构 (预计 3-4 天)
|
||||||
|
|
||||||
|
### 2.1 主题系统
|
||||||
|
|
||||||
|
- [ ] 创建 `src/components/theme-provider.tsx`
|
||||||
|
- [ ] 创建 `src/components/mode-toggle.tsx`
|
||||||
|
- [ ] 更新 `src/index.css` 添加主题变量
|
||||||
|
- [ ] 删除 `src/hooks/useDarkMode.ts`
|
||||||
|
- [ ] 更新所有组件使用新的主题系统
|
||||||
|
|
||||||
|
**测试**:
|
||||||
|
- [ ] 验证主题切换正常工作
|
||||||
|
- [ ] 验证系统主题跟随功能
|
||||||
|
- [ ] 验证主题持久化
|
||||||
|
|
||||||
|
### 2.2 更新 main.tsx
|
||||||
|
|
||||||
|
- [ ] 引入 QueryClientProvider
|
||||||
|
- [ ] 引入 ThemeProvider
|
||||||
|
- [ ] 添加 Toaster 组件
|
||||||
|
- [ ] 移除旧的 API 导入
|
||||||
|
|
||||||
|
**测试**:
|
||||||
|
- [ ] 验证应用可以正常启动
|
||||||
|
- [ ] 验证 Context 正确传递
|
||||||
|
|
||||||
|
### 2.3 重构 App.tsx
|
||||||
|
|
||||||
|
- [ ] 使用 useProvidersQuery 替代手动状态管理
|
||||||
|
- [ ] 移除所有 loadProviders 相关代码
|
||||||
|
- [ ] 移除手动 notification 状态
|
||||||
|
- [ ] 简化事件监听逻辑
|
||||||
|
- [ ] 更新对话框为新的 Dialog 组件
|
||||||
|
|
||||||
|
**目标**: 将 412 行代码减少到 ~100 行
|
||||||
|
|
||||||
|
**测试**:
|
||||||
|
- [ ] 验证供应商列表正常加载
|
||||||
|
- [ ] 验证切换 Claude/Codex 正常工作
|
||||||
|
- [ ] 验证事件监听正常工作
|
||||||
|
|
||||||
|
### 2.4 重构 ProviderList
|
||||||
|
|
||||||
|
- [ ] 创建 `src/components/providers/ProviderList.tsx`
|
||||||
|
- [ ] 使用 mutation hooks 处理操作
|
||||||
|
- [ ] 移除 onNotify prop
|
||||||
|
- [ ] 移除手动状态管理
|
||||||
|
|
||||||
|
**测试**:
|
||||||
|
- [ ] 验证供应商列表渲染
|
||||||
|
- [ ] 验证切换操作
|
||||||
|
- [ ] 验证删除操作
|
||||||
|
|
||||||
|
### 2.5 重构表单系统
|
||||||
|
|
||||||
|
- [ ] 创建 `src/lib/schemas/provider.ts` (Zod schema)
|
||||||
|
- [ ] 创建 `src/components/providers/ProviderForm.tsx`
|
||||||
|
- [ ] 使用 react-hook-form
|
||||||
|
- [ ] 使用 zodResolver
|
||||||
|
- [ ] 字段级验证
|
||||||
|
|
||||||
|
- [ ] 创建 `src/components/providers/AddProviderDialog.tsx`
|
||||||
|
- [ ] 使用新的 Dialog 组件
|
||||||
|
- [ ] 集成 ProviderForm
|
||||||
|
- [ ] 使用 useAddProviderMutation
|
||||||
|
|
||||||
|
- [ ] 创建 `src/components/providers/EditProviderDialog.tsx`
|
||||||
|
- [ ] 使用新的 Dialog 组件
|
||||||
|
- [ ] 集成 ProviderForm
|
||||||
|
- [ ] 使用 useUpdateProviderMutation
|
||||||
|
|
||||||
|
**测试**:
|
||||||
|
- [ ] 验证表单验证正常工作
|
||||||
|
- [ ] 验证错误提示显示正确
|
||||||
|
- [ ] 验证提交操作成功
|
||||||
|
- [ ] 验证表单重置功能
|
||||||
|
|
||||||
|
### 2.6 清理旧组件
|
||||||
|
|
||||||
|
- [ ] 删除 `src/components/AddProviderModal.tsx`
|
||||||
|
- [ ] 删除 `src/components/EditProviderModal.tsx`
|
||||||
|
- [ ] 更新所有引用这些组件的地方
|
||||||
|
|
||||||
|
**完成时间**: ___________
|
||||||
|
**遇到的问题**: ___________
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 阶段 3: 设置和辅助功能 (预计 2-3 天)
|
||||||
|
|
||||||
|
### 3.1 重构 SettingsDialog
|
||||||
|
|
||||||
|
- [ ] 创建 `src/components/settings/SettingsDialog.tsx`
|
||||||
|
- [ ] 使用 Tabs 组件
|
||||||
|
- [ ] 集成各个设置子组件
|
||||||
|
|
||||||
|
- [ ] 创建 `src/components/settings/GeneralSettings.tsx`
|
||||||
|
- [ ] 语言设置
|
||||||
|
- [ ] 配置目录设置
|
||||||
|
- [ ] 其他通用设置
|
||||||
|
|
||||||
|
- [ ] 创建 `src/components/settings/AboutSection.tsx`
|
||||||
|
- [ ] 版本信息
|
||||||
|
- [ ] 更新检查
|
||||||
|
- [ ] 链接
|
||||||
|
|
||||||
|
- [ ] 创建 `src/components/settings/ImportExportSection.tsx`
|
||||||
|
- [ ] 导入功能
|
||||||
|
- [ ] 导出功能
|
||||||
|
|
||||||
|
**目标**: 将 643 行拆分为 4-5 个小组件,每个 100-150 行
|
||||||
|
|
||||||
|
**测试**:
|
||||||
|
- [ ] 验证设置保存功能
|
||||||
|
- [ ] 验证导入导出功能
|
||||||
|
- [ ] 验证更新检查功能
|
||||||
|
|
||||||
|
### 3.2 重构通知系统
|
||||||
|
|
||||||
|
- [ ] 在所有 mutations 中使用 `toast` 替代 `showNotification`
|
||||||
|
- [ ] 移除 App.tsx 中的 notification 状态
|
||||||
|
- [ ] 移除自定义通知组件
|
||||||
|
|
||||||
|
**测试**:
|
||||||
|
- [ ] 验证成功通知显示
|
||||||
|
- [ ] 验证错误通知显示
|
||||||
|
- [ ] 验证通知自动消失
|
||||||
|
|
||||||
|
### 3.3 重构确认对话框
|
||||||
|
|
||||||
|
- [ ] 更新 `src/components/ConfirmDialog.tsx` 使用新的 Dialog
|
||||||
|
- [ ] 或者直接使用 shadcn/ui 的 AlertDialog
|
||||||
|
|
||||||
|
**测试**:
|
||||||
|
- [ ] 验证删除确认对话框
|
||||||
|
- [ ] 验证其他确认场景
|
||||||
|
|
||||||
|
**完成时间**: ___________
|
||||||
|
**遇到的问题**: ___________
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 阶段 4: 清理和优化 (预计 1-2 天)
|
||||||
|
|
||||||
|
### 4.1 移除旧代码
|
||||||
|
|
||||||
|
- [ ] 删除 `src/lib/styles.ts`
|
||||||
|
- [ ] 从 `src/lib/tauri-api.ts` 移除 `window.api` 绑定
|
||||||
|
- [ ] 精简 `src/lib/tauri-api.ts`,只保留事件监听相关
|
||||||
|
- [ ] 删除或更新 `src/vite-env.d.ts` 中的过时类型
|
||||||
|
|
||||||
|
### 4.2 代码审查
|
||||||
|
|
||||||
|
- [ ] 检查所有 TODO 注释
|
||||||
|
- [ ] 检查是否还有 `window.api` 调用
|
||||||
|
- [ ] 检查是否还有手动状态管理
|
||||||
|
- [ ] 统一代码风格
|
||||||
|
|
||||||
|
### 4.3 类型检查
|
||||||
|
|
||||||
|
- [ ] 运行 `pnpm typecheck` 确保无错误
|
||||||
|
- [ ] 修复所有类型错误
|
||||||
|
- [ ] 更新类型定义
|
||||||
|
|
||||||
|
### 4.4 性能优化
|
||||||
|
|
||||||
|
- [ ] 检查是否有不必要的重渲染
|
||||||
|
- [ ] 添加必要的 React.memo
|
||||||
|
- [ ] 优化 Query 缓存配置
|
||||||
|
|
||||||
|
**完成时间**: ___________
|
||||||
|
**遇到的问题**: ___________
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 阶段 5: 测试和修复 (预计 2-3 天)
|
||||||
|
|
||||||
|
### 5.1 功能测试
|
||||||
|
|
||||||
|
#### 供应商管理
|
||||||
|
- [ ] 添加供应商 (Claude)
|
||||||
|
- [ ] 添加供应商 (Codex)
|
||||||
|
- [ ] 编辑供应商
|
||||||
|
- [ ] 删除供应商
|
||||||
|
- [ ] 切换供应商
|
||||||
|
- [ ] 导入默认配置
|
||||||
|
|
||||||
|
#### 应用切换
|
||||||
|
- [ ] Claude <-> Codex 切换
|
||||||
|
- [ ] 切换后数据正确加载
|
||||||
|
- [ ] 切换后托盘菜单更新
|
||||||
|
|
||||||
|
#### 设置
|
||||||
|
- [ ] 保存通用设置
|
||||||
|
- [ ] 切换语言
|
||||||
|
- [ ] 配置目录选择
|
||||||
|
- [ ] 导入配置
|
||||||
|
- [ ] 导出配置
|
||||||
|
|
||||||
|
#### UI 交互
|
||||||
|
- [ ] 主题切换 (亮色/暗色)
|
||||||
|
- [ ] 对话框打开/关闭
|
||||||
|
- [ ] 表单验证
|
||||||
|
- [ ] Toast 通知
|
||||||
|
|
||||||
|
#### MCP 管理
|
||||||
|
- [ ] 列表显示
|
||||||
|
- [ ] 添加 MCP
|
||||||
|
- [ ] 编辑 MCP
|
||||||
|
- [ ] 删除 MCP
|
||||||
|
- [ ] 启用/禁用 MCP
|
||||||
|
|
||||||
|
### 5.2 边界情况测试
|
||||||
|
|
||||||
|
- [ ] 空供应商列表
|
||||||
|
- [ ] 无效配置文件
|
||||||
|
- [ ] 网络错误
|
||||||
|
- [ ] 后端错误响应
|
||||||
|
- [ ] 并发操作
|
||||||
|
- [ ] 表单输入边界值
|
||||||
|
|
||||||
|
### 5.3 兼容性测试
|
||||||
|
|
||||||
|
- [ ] Windows 测试
|
||||||
|
- [ ] macOS 测试
|
||||||
|
- [ ] Linux 测试
|
||||||
|
|
||||||
|
### 5.4 性能测试
|
||||||
|
|
||||||
|
- [ ] 100+ 供应商加载速度
|
||||||
|
- [ ] 快速切换供应商
|
||||||
|
- [ ] 内存使用情况
|
||||||
|
- [ ] CPU 使用情况
|
||||||
|
|
||||||
|
### 5.5 Bug 修复
|
||||||
|
|
||||||
|
**Bug 列表** (发现后记录):
|
||||||
|
|
||||||
|
1. ___________
|
||||||
|
- [ ] 已修复
|
||||||
|
- [ ] 已验证
|
||||||
|
|
||||||
|
2. ___________
|
||||||
|
- [ ] 已修复
|
||||||
|
- [ ] 已验证
|
||||||
|
|
||||||
|
**完成时间**: ___________
|
||||||
|
**遇到的问题**: ___________
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 最终检查
|
||||||
|
|
||||||
|
### 代码质量
|
||||||
|
|
||||||
|
- [ ] 所有 TypeScript 错误已修复
|
||||||
|
- [ ] 运行 `pnpm format` 格式化代码
|
||||||
|
- [ ] 运行 `pnpm typecheck` 通过
|
||||||
|
- [ ] 代码审查完成
|
||||||
|
|
||||||
|
### 文档更新
|
||||||
|
|
||||||
|
- [ ] 更新 `CLAUDE.md` 反映新架构
|
||||||
|
- [ ] 更新 `README.md` (如有必要)
|
||||||
|
- [ ] 添加 Migration Guide (可选)
|
||||||
|
|
||||||
|
### 性能基准
|
||||||
|
|
||||||
|
记录性能数据:
|
||||||
|
|
||||||
|
**旧版本**:
|
||||||
|
- 启动时间: _____ms
|
||||||
|
- 供应商加载: _____ms
|
||||||
|
- 内存占用: _____MB
|
||||||
|
|
||||||
|
**新版本**:
|
||||||
|
- 启动时间: _____ms
|
||||||
|
- 供应商加载: _____ms
|
||||||
|
- 内存占用: _____MB
|
||||||
|
|
||||||
|
### 代码统计
|
||||||
|
|
||||||
|
**代码行数对比**:
|
||||||
|
|
||||||
|
| 文件 | 旧版本 | 新版本 | 减少 |
|
||||||
|
|------|--------|--------|------|
|
||||||
|
| App.tsx | 412 | ~100 | -76% |
|
||||||
|
| tauri-api.ts | 712 | ~50 | -93% |
|
||||||
|
| ProviderForm.tsx | 271 | ~150 | -45% |
|
||||||
|
| SettingsModal.tsx | 643 | ~400 (拆分) | -38% |
|
||||||
|
| **总计** | 2038 | ~700 | **-66%** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 发布准备
|
||||||
|
|
||||||
|
### Pre-release 测试
|
||||||
|
|
||||||
|
- [ ] 创建 beta 版本 `v4.0.0-beta.1`
|
||||||
|
- [ ] 在测试环境验证
|
||||||
|
- [ ] 收集用户反馈
|
||||||
|
|
||||||
|
### 正式发布
|
||||||
|
|
||||||
|
- [ ] 合并到 main 分支
|
||||||
|
- [ ] 创建 Release Tag `v4.0.0`
|
||||||
|
- [ ] 更新 Changelog
|
||||||
|
- [ ] 发布 GitHub Release
|
||||||
|
- [ ] 通知用户更新
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚨 回滚触发条件
|
||||||
|
|
||||||
|
如果出现以下情况,考虑回滚:
|
||||||
|
|
||||||
|
- [ ] 重大功能无法使用
|
||||||
|
- [ ] 用户数据丢失
|
||||||
|
- [ ] 严重性能问题
|
||||||
|
- [ ] 无法修复的兼容性问题
|
||||||
|
|
||||||
|
**回滚命令**:
|
||||||
|
```bash
|
||||||
|
git reset --hard backup-before-refactor
|
||||||
|
# 或
|
||||||
|
git revert <commit-range>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 总结报告
|
||||||
|
|
||||||
|
### 成功指标
|
||||||
|
|
||||||
|
- [ ] 所有现有功能正常工作
|
||||||
|
- [ ] 代码量减少 40%+
|
||||||
|
- [ ] 无用户数据丢失
|
||||||
|
- [ ] 性能未下降
|
||||||
|
|
||||||
|
### 经验教训
|
||||||
|
|
||||||
|
**遇到的主要挑战**:
|
||||||
|
1. ___________
|
||||||
|
2. ___________
|
||||||
|
3. ___________
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
1. ___________
|
||||||
|
2. ___________
|
||||||
|
3. ___________
|
||||||
|
|
||||||
|
**未来改进**:
|
||||||
|
1. ___________
|
||||||
|
2. ___________
|
||||||
|
3. ___________
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**重构完成日期**: ___________
|
||||||
|
**总耗时**: _____ 天
|
||||||
|
**参与人员**: ___________
|
||||||
1679
docs/REFACTORING_MASTER_PLAN.md
Normal file
1679
docs/REFACTORING_MASTER_PLAN.md
Normal file
File diff suppressed because it is too large
Load Diff
834
docs/REFACTORING_REFERENCE.md
Normal file
834
docs/REFACTORING_REFERENCE.md
Normal file
@@ -0,0 +1,834 @@
|
|||||||
|
# 重构快速参考指南
|
||||||
|
|
||||||
|
> 常见模式和代码示例的速查表
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📑 目录
|
||||||
|
|
||||||
|
1. [React Query 使用](#react-query-使用)
|
||||||
|
2. [react-hook-form 使用](#react-hook-form-使用)
|
||||||
|
3. [shadcn/ui 组件使用](#shadcnui-组件使用)
|
||||||
|
4. [代码迁移示例](#代码迁移示例)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## React Query 使用
|
||||||
|
|
||||||
|
### 基础查询
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 定义查询 Hook
|
||||||
|
export const useProvidersQuery = (appType: AppType) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['providers', appType],
|
||||||
|
queryFn: async () => {
|
||||||
|
const data = await providersApi.getAll(appType)
|
||||||
|
return data
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在组件中使用
|
||||||
|
function MyComponent() {
|
||||||
|
const { data, isLoading, error } = useProvidersQuery('claude')
|
||||||
|
|
||||||
|
if (isLoading) return <div>Loading...</div>
|
||||||
|
if (error) return <div>Error: {error.message}</div>
|
||||||
|
|
||||||
|
return <div>{/* 使用 data */}</div>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mutation (变更操作)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 定义 Mutation Hook
|
||||||
|
export const useAddProviderMutation = (appType: AppType) => {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (provider: Provider) => {
|
||||||
|
return await providersApi.add(provider, appType)
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
// 重新获取数据
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['providers', appType] })
|
||||||
|
toast.success('添加成功')
|
||||||
|
},
|
||||||
|
onError: (error: Error) => {
|
||||||
|
toast.error(`添加失败: ${error.message}`)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在组件中使用
|
||||||
|
function AddProviderDialog() {
|
||||||
|
const mutation = useAddProviderMutation('claude')
|
||||||
|
|
||||||
|
const handleSubmit = (data: Provider) => {
|
||||||
|
mutation.mutate(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={() => handleSubmit(formData)}
|
||||||
|
disabled={mutation.isPending}
|
||||||
|
>
|
||||||
|
{mutation.isPending ? '添加中...' : '添加'}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 乐观更新
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const useSwitchProviderMutation = (appType: AppType) => {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (providerId: string) => {
|
||||||
|
return await providersApi.switch(providerId, appType)
|
||||||
|
},
|
||||||
|
// 乐观更新: 在请求发送前立即更新 UI
|
||||||
|
onMutate: async (providerId) => {
|
||||||
|
// 取消正在进行的查询
|
||||||
|
await queryClient.cancelQueries({ queryKey: ['providers', appType] })
|
||||||
|
|
||||||
|
// 保存当前数据(以便回滚)
|
||||||
|
const previousData = queryClient.getQueryData(['providers', appType])
|
||||||
|
|
||||||
|
// 乐观更新
|
||||||
|
queryClient.setQueryData(['providers', appType], (old: any) => ({
|
||||||
|
...old,
|
||||||
|
currentProviderId: providerId,
|
||||||
|
}))
|
||||||
|
|
||||||
|
return { previousData }
|
||||||
|
},
|
||||||
|
// 如果失败,回滚
|
||||||
|
onError: (err, providerId, context) => {
|
||||||
|
queryClient.setQueryData(['providers', appType], context?.previousData)
|
||||||
|
toast.error('切换失败')
|
||||||
|
},
|
||||||
|
// 无论成功失败,都重新获取数据
|
||||||
|
onSettled: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['providers', appType] })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 依赖查询
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 第二个查询依赖第一个查询的结果
|
||||||
|
const { data: providers } = useProvidersQuery(appType)
|
||||||
|
const currentProviderId = providers?.currentProviderId
|
||||||
|
|
||||||
|
const { data: currentProvider } = useQuery({
|
||||||
|
queryKey: ['provider', currentProviderId],
|
||||||
|
queryFn: () => providersApi.getById(currentProviderId!),
|
||||||
|
enabled: !!currentProviderId, // 只有当 ID 存在时才执行
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## react-hook-form 使用
|
||||||
|
|
||||||
|
### 基础表单
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useForm } from 'react-hook-form'
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod'
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
// 定义验证 schema
|
||||||
|
const schema = z.object({
|
||||||
|
name: z.string().min(1, '请输入名称'),
|
||||||
|
email: z.string().email('邮箱格式不正确'),
|
||||||
|
age: z.number().min(18, '年龄必须大于18'),
|
||||||
|
})
|
||||||
|
|
||||||
|
type FormData = z.infer<typeof schema>
|
||||||
|
|
||||||
|
function MyForm() {
|
||||||
|
const form = useForm<FormData>({
|
||||||
|
resolver: zodResolver(schema),
|
||||||
|
defaultValues: {
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
age: 0,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const onSubmit = (data: FormData) => {
|
||||||
|
console.log(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||||
|
<input {...form.register('name')} />
|
||||||
|
{form.formState.errors.name && (
|
||||||
|
<span>{form.formState.errors.name.message}</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button type="submit">提交</button>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 使用 shadcn/ui Form 组件
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useForm } from 'react-hook-form'
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod'
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from '@/components/ui/form'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
|
||||||
|
function MyForm() {
|
||||||
|
const form = useForm<FormData>({
|
||||||
|
resolver: zodResolver(schema),
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>名称</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="请输入名称" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button type="submit">提交</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 动态表单验证
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 根据条件动态验证
|
||||||
|
const schema = z.object({
|
||||||
|
type: z.enum(['official', 'custom']),
|
||||||
|
apiKey: z.string().optional(),
|
||||||
|
baseUrl: z.string().optional(),
|
||||||
|
}).refine(
|
||||||
|
(data) => {
|
||||||
|
// 如果是自定义供应商,必须填写 baseUrl
|
||||||
|
if (data.type === 'custom') {
|
||||||
|
return !!data.baseUrl
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: '自定义供应商必须填写 Base URL',
|
||||||
|
path: ['baseUrl'],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 手动触发验证
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function MyForm() {
|
||||||
|
const form = useForm<FormData>()
|
||||||
|
|
||||||
|
const handleBlur = async () => {
|
||||||
|
// 验证单个字段
|
||||||
|
await form.trigger('name')
|
||||||
|
|
||||||
|
// 验证多个字段
|
||||||
|
await form.trigger(['name', 'email'])
|
||||||
|
|
||||||
|
// 验证所有字段
|
||||||
|
const isValid = await form.trigger()
|
||||||
|
}
|
||||||
|
|
||||||
|
return <form>...</form>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## shadcn/ui 组件使用
|
||||||
|
|
||||||
|
### Dialog (对话框)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
} from '@/components/ui/dialog'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
|
||||||
|
function MyDialog() {
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>标题</DialogTitle>
|
||||||
|
<DialogDescription>描述信息</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
{/* 内容 */}
|
||||||
|
<div>对话框内容</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setOpen(false)}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleConfirm}>确认</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Select (选择器)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select'
|
||||||
|
|
||||||
|
function MySelect() {
|
||||||
|
const [value, setValue] = useState('')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select value={value} onValueChange={setValue}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="请选择" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="option1">选项1</SelectItem>
|
||||||
|
<SelectItem value="option2">选项2</SelectItem>
|
||||||
|
<SelectItem value="option3">选项3</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tabs (标签页)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||||
|
|
||||||
|
function MyTabs() {
|
||||||
|
return (
|
||||||
|
<Tabs defaultValue="tab1">
|
||||||
|
<TabsList>
|
||||||
|
<TabsTrigger value="tab1">标签1</TabsTrigger>
|
||||||
|
<TabsTrigger value="tab2">标签2</TabsTrigger>
|
||||||
|
<TabsTrigger value="tab3">标签3</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value="tab1">
|
||||||
|
<div>标签1的内容</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="tab2">
|
||||||
|
<div>标签2的内容</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="tab3">
|
||||||
|
<div>标签3的内容</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Toast 通知 (Sonner)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
|
// 成功通知
|
||||||
|
toast.success('操作成功')
|
||||||
|
|
||||||
|
// 错误通知
|
||||||
|
toast.error('操作失败')
|
||||||
|
|
||||||
|
// 加载中
|
||||||
|
const toastId = toast.loading('处理中...')
|
||||||
|
// 完成后更新
|
||||||
|
toast.success('处理完成', { id: toastId })
|
||||||
|
// 或
|
||||||
|
toast.dismiss(toastId)
|
||||||
|
|
||||||
|
// 自定义持续时间
|
||||||
|
toast.success('消息', { duration: 5000 })
|
||||||
|
|
||||||
|
// 带操作按钮
|
||||||
|
toast('确认删除?', {
|
||||||
|
action: {
|
||||||
|
label: '删除',
|
||||||
|
onClick: () => handleDelete(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 代码迁移示例
|
||||||
|
|
||||||
|
### 示例 1: 状态管理迁移
|
||||||
|
|
||||||
|
**旧代码** (手动状态管理):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const [providers, setProviders] = useState<Record<string, Provider>>({})
|
||||||
|
const [currentProviderId, setCurrentProviderId] = useState('')
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [error, setError] = useState<Error | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const load = async () => {
|
||||||
|
setLoading(true)
|
||||||
|
setError(null)
|
||||||
|
try {
|
||||||
|
const data = await window.api.getProviders(appType)
|
||||||
|
const currentId = await window.api.getCurrentProvider(appType)
|
||||||
|
setProviders(data)
|
||||||
|
setCurrentProviderId(currentId)
|
||||||
|
} catch (err) {
|
||||||
|
setError(err as Error)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
load()
|
||||||
|
}, [appType])
|
||||||
|
```
|
||||||
|
|
||||||
|
**新代码** (React Query):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { data, isLoading, error } = useProvidersQuery(appType)
|
||||||
|
const providers = data?.providers || {}
|
||||||
|
const currentProviderId = data?.currentProviderId || ''
|
||||||
|
```
|
||||||
|
|
||||||
|
**减少**: 从 20+ 行到 3 行
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 示例 2: 表单验证迁移
|
||||||
|
|
||||||
|
**旧代码** (手动验证):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const [name, setName] = useState('')
|
||||||
|
const [nameError, setNameError] = useState('')
|
||||||
|
const [apiKey, setApiKey] = useState('')
|
||||||
|
const [apiKeyError, setApiKeyError] = useState('')
|
||||||
|
|
||||||
|
const validate = () => {
|
||||||
|
let valid = true
|
||||||
|
|
||||||
|
if (!name.trim()) {
|
||||||
|
setNameError('请输入名称')
|
||||||
|
valid = false
|
||||||
|
} else {
|
||||||
|
setNameError('')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!apiKey.trim()) {
|
||||||
|
setApiKeyError('请输入 API Key')
|
||||||
|
valid = false
|
||||||
|
} else if (apiKey.length < 10) {
|
||||||
|
setApiKeyError('API Key 长度不足')
|
||||||
|
valid = false
|
||||||
|
} else {
|
||||||
|
setApiKeyError('')
|
||||||
|
}
|
||||||
|
|
||||||
|
return valid
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
if (validate()) {
|
||||||
|
// 提交
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form>
|
||||||
|
<input value={name} onChange={e => setName(e.target.value)} />
|
||||||
|
{nameError && <span>{nameError}</span>}
|
||||||
|
|
||||||
|
<input value={apiKey} onChange={e => setApiKey(e.target.value)} />
|
||||||
|
{apiKeyError && <span>{apiKeyError}</span>}
|
||||||
|
|
||||||
|
<button onClick={handleSubmit}>提交</button>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**新代码** (react-hook-form + zod):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const schema = z.object({
|
||||||
|
name: z.string().min(1, '请输入名称'),
|
||||||
|
apiKey: z.string().min(10, 'API Key 长度不足'),
|
||||||
|
})
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
resolver: zodResolver(schema),
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="apiKey"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button type="submit">提交</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**减少**: 从 40+ 行到 30 行,且更健壮
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 示例 3: 通知系统迁移
|
||||||
|
|
||||||
|
**旧代码** (自定义通知):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const [notification, setNotification] = useState<{
|
||||||
|
message: string
|
||||||
|
type: 'success' | 'error'
|
||||||
|
} | null>(null)
|
||||||
|
const [isVisible, setIsVisible] = useState(false)
|
||||||
|
|
||||||
|
const showNotification = (message: string, type: 'success' | 'error') => {
|
||||||
|
setNotification({ message, type })
|
||||||
|
setIsVisible(true)
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsVisible(false)
|
||||||
|
setTimeout(() => setNotification(null), 300)
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{notification && (
|
||||||
|
<div className={`notification ${isVisible ? 'visible' : ''} ${notification.type}`}>
|
||||||
|
{notification.message}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/* 其他内容 */}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**新代码** (Sonner):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
|
// 在需要的地方直接调用
|
||||||
|
toast.success('操作成功')
|
||||||
|
toast.error('操作失败')
|
||||||
|
|
||||||
|
// 在 main.tsx 中只需添加一次
|
||||||
|
import { Toaster } from '@/components/ui/sonner'
|
||||||
|
|
||||||
|
<Toaster />
|
||||||
|
```
|
||||||
|
|
||||||
|
**减少**: 从 20+ 行到 1 行调用
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 示例 4: 对话框迁移
|
||||||
|
|
||||||
|
**旧代码** (自定义 Modal):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button onClick={() => setIsOpen(true)}>打开</button>
|
||||||
|
|
||||||
|
{isOpen && (
|
||||||
|
<div className="modal-backdrop" onClick={() => setIsOpen(false)}>
|
||||||
|
<div className="modal-content" onClick={e => e.stopPropagation()}>
|
||||||
|
<div className="modal-header">
|
||||||
|
<h2>标题</h2>
|
||||||
|
<button onClick={() => setIsOpen(false)}>×</button>
|
||||||
|
</div>
|
||||||
|
<div className="modal-body">
|
||||||
|
{/* 内容 */}
|
||||||
|
</div>
|
||||||
|
<div className="modal-footer">
|
||||||
|
<button onClick={() => setIsOpen(false)}>取消</button>
|
||||||
|
<button onClick={handleConfirm}>确认</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**新代码** (shadcn/ui Dialog):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||||
|
|
||||||
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button onClick={() => setIsOpen(true)}>打开</Button>
|
||||||
|
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>标题</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
{/* 内容 */}
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setIsOpen(false)}>取消</Button>
|
||||||
|
<Button onClick={handleConfirm}>确认</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**优势**:
|
||||||
|
- 无需自定义样式
|
||||||
|
- 内置无障碍支持
|
||||||
|
- 自动管理焦点和 ESC 键
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 示例 5: API 调用迁移
|
||||||
|
|
||||||
|
**旧代码** (window.api):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 添加供应商
|
||||||
|
const handleAdd = async (provider: Provider) => {
|
||||||
|
try {
|
||||||
|
await window.api.addProvider(provider, appType)
|
||||||
|
await loadProviders()
|
||||||
|
showNotification('添加成功', 'success')
|
||||||
|
} catch (error) {
|
||||||
|
showNotification('添加失败', 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**新代码** (React Query Mutation):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 在组件中
|
||||||
|
const addMutation = useAddProviderMutation(appType)
|
||||||
|
|
||||||
|
const handleAdd = (provider: Provider) => {
|
||||||
|
addMutation.mutate(provider)
|
||||||
|
// 成功和错误处理已在 mutation 定义中处理
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**优势**:
|
||||||
|
- 自动处理 loading 状态
|
||||||
|
- 统一的错误处理
|
||||||
|
- 自动刷新数据
|
||||||
|
- 更少的样板代码
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### Q: 如何在 mutation 成功后关闭对话框?
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const mutation = useAddProviderMutation(appType)
|
||||||
|
|
||||||
|
const handleSubmit = (data: Provider) => {
|
||||||
|
mutation.mutate(data, {
|
||||||
|
onSuccess: () => {
|
||||||
|
setIsOpen(false) // 关闭对话框
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Q: 如何在表单中使用异步验证?
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const schema = z.object({
|
||||||
|
name: z.string().refine(
|
||||||
|
async (name) => {
|
||||||
|
// 检查名称是否已存在
|
||||||
|
const exists = await checkNameExists(name)
|
||||||
|
return !exists
|
||||||
|
},
|
||||||
|
{ message: '名称已存在' }
|
||||||
|
),
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Q: 如何手动刷新 Query 数据?
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
// 方式1: 使缓存失效,触发重新获取
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['providers', appType] })
|
||||||
|
|
||||||
|
// 方式2: 直接刷新
|
||||||
|
queryClient.refetchQueries({ queryKey: ['providers', appType] })
|
||||||
|
|
||||||
|
// 方式3: 更新缓存数据
|
||||||
|
queryClient.setQueryData(['providers', appType], newData)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Q: 如何在组件外部使用 toast?
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 直接导入并使用即可
|
||||||
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
|
export const someUtil = () => {
|
||||||
|
toast.success('工具函数中的通知')
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 调试技巧
|
||||||
|
|
||||||
|
### React Query DevTools
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 在 main.tsx 中添加
|
||||||
|
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
|
||||||
|
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<App />
|
||||||
|
<ReactQueryDevtools initialIsOpen={false} />
|
||||||
|
</QueryClientProvider>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 查看表单状态
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const form = useForm()
|
||||||
|
|
||||||
|
// 在开发模式下打印表单状态
|
||||||
|
console.log('Form values:', form.watch())
|
||||||
|
console.log('Form errors:', form.formState.errors)
|
||||||
|
console.log('Is valid:', form.formState.isValid)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 性能优化建议
|
||||||
|
|
||||||
|
### 1. 避免不必要的重渲染
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 使用 React.memo
|
||||||
|
export const ProviderCard = React.memo(({ provider, onEdit }: Props) => {
|
||||||
|
// ...
|
||||||
|
})
|
||||||
|
|
||||||
|
// 或使用 useMemo
|
||||||
|
const sortedProviders = useMemo(
|
||||||
|
() => Object.values(providers).sort(...),
|
||||||
|
[providers]
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Query 配置优化
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { data } = useQuery({
|
||||||
|
queryKey: ['providers', appType],
|
||||||
|
queryFn: fetchProviders,
|
||||||
|
staleTime: 1000 * 60 * 5, // 5分钟内不重新获取
|
||||||
|
gcTime: 1000 * 60 * 10, // 10分钟后清除缓存
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 表单性能优化
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 使用 mode 控制验证时机
|
||||||
|
const form = useForm({
|
||||||
|
mode: 'onBlur', // 失去焦点时验证
|
||||||
|
// mode: 'onChange', // 每次输入都验证(较慢)
|
||||||
|
// mode: 'onSubmit', // 提交时验证(最快)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**提示**: 将此文档保存在浏览器书签或编辑器中,方便随时查阅!
|
||||||
Reference in New Issue
Block a user