From 95e2d846555516eea1c892f6631ad7b647764329 Mon Sep 17 00:00:00 2001 From: Jason Date: Thu, 16 Oct 2025 09:38:41 +0800 Subject: [PATCH] docs: add comprehensive refactoring documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docs/REFACTORING_CHECKLIST.md | 489 +++++++++ docs/REFACTORING_MASTER_PLAN.md | 1679 +++++++++++++++++++++++++++++++ docs/REFACTORING_REFERENCE.md | 834 +++++++++++++++ 3 files changed, 3002 insertions(+) create mode 100644 docs/REFACTORING_CHECKLIST.md create mode 100644 docs/REFACTORING_MASTER_PLAN.md create mode 100644 docs/REFACTORING_REFERENCE.md diff --git a/docs/REFACTORING_CHECKLIST.md b/docs/REFACTORING_CHECKLIST.md new file mode 100644 index 0000000..dbdafce --- /dev/null +++ b/docs/REFACTORING_CHECKLIST.md @@ -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 +``` + +--- + +## 📝 总结报告 + +### 成功指标 + +- [ ] 所有现有功能正常工作 +- [ ] 代码量减少 40%+ +- [ ] 无用户数据丢失 +- [ ] 性能未下降 + +### 经验教训 + +**遇到的主要挑战**: +1. ___________ +2. ___________ +3. ___________ + +**解决方案**: +1. ___________ +2. ___________ +3. ___________ + +**未来改进**: +1. ___________ +2. ___________ +3. ___________ + +--- + +**重构完成日期**: ___________ +**总耗时**: _____ 天 +**参与人员**: ___________ diff --git a/docs/REFACTORING_MASTER_PLAN.md b/docs/REFACTORING_MASTER_PLAN.md new file mode 100644 index 0000000..e2af742 --- /dev/null +++ b/docs/REFACTORING_MASTER_PLAN.md @@ -0,0 +1,1679 @@ +# CC Switch 现代化重构完整方案 + +## 📋 目录 + +- [第一部分: 战略规划](#第一部分-战略规划) + - [重构背景与目标](#重构背景与目标) + - [当前问题全面分析](#当前问题全面分析) + - [技术选型与理由](#技术选型与理由) +- [第二部分: 架构设计](#第二部分-架构设计) + - [新的目录结构](#新的目录结构) + - [数据流架构](#数据流架构) + - [组件拆分详细方案](#组件拆分详细方案) +- [第三部分: 实施计划](#第三部分-实施计划) + - [分阶段实施路线图](#分阶段实施路线图) + - [详细实施步骤](#详细实施步骤) +- [第四部分: 质量保障](#第四部分-质量保障) + - [测试策略](#测试策略) + - [风险控制](#风险控制) + - [回滚方案](#回滚方案) + +--- + +# 第一部分: 战略规划 + +## 🎯 重构背景与目标 + +### 为什么要重构? + +当前代码库存在以下核心问题: + +1. **状态管理混乱** + - 手动管理 20+ `useState` + - 大量复杂的 `useEffect` 依赖链 + - 数据同步逻辑分散 + +2. **组件过于臃肿** + - `SettingsModal.tsx`: **1046 行** 😱 + - `ProviderList.tsx`: **418 行** + - `ProviderForm.tsx`: **271 行** + +3. **代码重复严重** + - 相似的数据获取逻辑在多个组件重复 + - 表单验证逻辑手动编写 + - 错误处理不统一 + +4. **UI 缺乏统一性** + - 自定义样式分散 + - 缺乏设计系统 + - 响应式支持不足 + +5. **可维护性差** + - 组件职责不清晰 + - 耦合度高 + - 难以测试 + +### 重构目标 + +| 维度 | 目标 | 衡量标准 | +| -------------- | -------------------- | -------------- | +| **代码质量** | 减少 40-60% 样板代码 | 代码行数统计 | +| **开发效率** | 提升 50%+ 开发速度 | 新功能开发时间 | +| **用户体验** | 统一设计系统 | UI 一致性检查 | +| **可维护性** | 清晰的架构分层 | 代码审查时间 | +| **功能完整性** | 100% 功能无回归 | 全量测试通过 | + +--- + +## 🔍 当前问题全面分析 + +### 问题 1: App.tsx - 状态管理混乱 (412行) + +**现状**: + +```typescript +// 10+ 个 useState,状态管理混乱 +const [providers, setProviders] = useState>({}) +const [currentProviderId, setCurrentProviderId] = useState("") +const [notification, setNotification] = useState<{...} | null>(null) +const [isNotificationVisible, setIsNotificationVisible] = useState(false) +const [confirmDialog, setConfirmDialog] = useState<{...} | null>(null) +const [isSettingsOpen, setIsSettingsOpen] = useState(false) +const [isMcpOpen, setIsMcpOpen] = useState(false) +// ... 更多 + +// 手动数据加载,缺少 loading/error 状态 +const loadProviders = async () => { + const loadedProviders = await window.api.getProviders(activeApp) + const currentId = await window.api.getCurrentProvider(activeApp) + setProviders(loadedProviders) + setCurrentProviderId(currentId) +} + +// 复杂的 useEffect 依赖 +useEffect(() => { + loadProviders() +}, [activeApp]) +``` + +**核心问题**: + +- ❌ 状态同步困难 +- ❌ 没有 loading/error 处理 +- ❌ 错误处理不统一 +- ❌ 组件责任过重 + +**目标**: + +```typescript +// React Query: 3 行搞定 +const { data, isLoading, error } = useProvidersQuery(activeApp); +const providers = data?.providers || {}; +const currentProviderId = data?.currentProviderId || ""; +``` + +--- + +### 问题 2: SettingsModal.tsx - 超级巨无霸组件 (1046行) + +**现状结构**: + +``` +SettingsModal.tsx (1046 行) +├── 20+ useState (settings, configPath, version, isChecking...) +├── 15+ 处理函数 +│ ├── loadSettings() +│ ├── saveSettings() +│ ├── handleLanguageChange() +│ ├── handleCheckUpdate() +│ ├── handleExportConfig() +│ ├── handleImportConfig() +│ ├── handleBrowseConfigDir() +│ └── ... 更多 +├── 语言设置 UI +├── 窗口行为设置 UI +├── 配置文件位置 UI +├── 配置目录覆盖 UI (3个输入框) +├── 导入导出 UI +├── 关于和更新 UI +└── 2个子对话框 (ImportProgress, RestartConfirm) +``` + +**核心问题**: + +- ❌ 单个文件超过 1000 行 +- ❌ 多种职责混杂 +- ❌ 难以理解和维护 +- ❌ 无法并行开发 +- ❌ 难以测试 + +**目标**: 拆分为 **7 个小组件** (~470 行总计) + +--- + +### 问题 3: ProviderList.tsx - 内嵌组件和逻辑混杂 (418行) + +**现状结构**: + +``` +ProviderList.tsx (418 行) +├── SortableProviderItem (内嵌子组件, ~100行) +├── 拖拽排序逻辑 +├── 用量配置逻辑 +├── URL 处理逻辑 +├── Claude 插件同步逻辑 +└── 空状态 UI +``` + +**核心问题**: + +- ❌ 内嵌组件导致代码难读 +- ❌ 拖拽逻辑和 UI 混在一起 +- ❌ 业务逻辑分散 + +**目标**: 拆分为 **4 个独立组件** + **1 个自定义 Hook** + +--- + +### 问题 4: tauri-api.ts - 全局污染 (712行) + +**现状**: + +```typescript +// 问题 1: 污染全局命名空间 +if (typeof window !== "undefined") { + (window as any).api = tauriAPI; +} + +// 问题 2: 无缓存机制 +getProviders: async (app?: AppType) => { + try { + return await invoke("get_providers", { app_type: app, app }); + } catch (error) { + console.error("获取供应商列表失败:", error); + return {}; // 错误被吞掉 + } +}; +``` + +**核心问题**: + +- ❌ 全局 `window.api` 污染命名空间 +- ❌ 无缓存,重复请求 +- ❌ 无自动重试 +- ❌ 错误处理不统一 + +**目标**: + +- 封装为 API 层 (`lib/api/`) +- React Query 管理缓存和状态 + +--- + +### 问题 5: 表单验证 - 手动编写 (ProviderForm.tsx) + +**现状**: + +```typescript +const [name, setName] = useState(""); +const [nameError, setNameError] = useState(""); +const [apiKey, setApiKey] = useState(""); +const [apiKeyError, setApiKeyError] = useState(""); + +const validate = () => { + let valid = true; + if (!name) { + setNameError("请填写名称"); + valid = false; + } else { + setNameError(""); + } + if (!apiKey) { + setApiKeyError("请填写 API Key"); + valid = false; + } else if (apiKey.length < 10) { + setApiKeyError("API Key 长度不足"); + valid = false; + } else { + setApiKeyError(""); + } + return valid; +}; +``` + +**核心问题**: + +- ❌ 每个字段需要 2 个 state (值 + 错误) +- ❌ 验证逻辑手动编写 +- ❌ 代码冗长 + +**目标**: 使用 `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) }); +``` + +--- + +## 🛠 技术选型与理由 + +### 核心技术栈 + +| 技术 | 版本 | 用途 | 替代方案 | 为何选它? | +| ------------------------- | ------- | -------------- | --------------- | -------------------- | +| **@tanstack/react-query** | ^5.90.2 | 服务端状态管理 | SWR, RTK Query | 功能最全,生态最好 | +| **react-hook-form** | ^7.63.0 | 表单管理 | Formik | 性能更好,API 更简洁 | +| **zod** | ^4.1.11 | 运行时类型验证 | yup, joi | TypeScript 原生支持 | +| **shadcn/ui** | latest | UI 组件库 | Radix UI 原生 | 可定制,代码归属权 | +| **sonner** | ^2.0.7 | Toast 通知 | react-hot-toast | 更现代,动画更好 | +| **next-themes** | ^0.4.6 | 主题管理 | 自定义实现 | 开箱即用,SSR 友好 | + +--- + +# 第二部分: 架构设计 + +## 📁 新的目录结构 + +### 完整目录树 + +``` +src/ +├── components/ +│ ├── ui/ # shadcn/ui 基础组件 (由 CLI 生成) +│ │ ├── button.tsx +│ │ ├── dialog.tsx +│ │ ├── input.tsx +│ │ ├── label.tsx +│ │ ├── form.tsx +│ │ ├── select.tsx +│ │ ├── switch.tsx +│ │ ├── tabs.tsx +│ │ ├── card.tsx +│ │ ├── badge.tsx +│ │ └── sonner.tsx # Toast 组件 +│ │ +│ ├── providers/ # 供应商管理模块 +│ │ ├── ProviderList.tsx # 列表容器 (~100行) +│ │ ├── ProviderCard.tsx # 供应商卡片 (~120行) +│ │ ├── ProviderActions.tsx # 操作按钮组 (~80行) +│ │ ├── ProviderEmptyState.tsx # 空状态 (~30行) +│ │ ├── AddProviderDialog.tsx # 添加对话框 (~60行) +│ │ ├── EditProviderDialog.tsx # 编辑对话框 (~60行) +│ │ └── forms/ # 表单子模块 +│ │ ├── ProviderForm.tsx # 主表单 (~150行) +│ │ ├── PresetSelector.tsx # 预设选择器 (~60行) +│ │ ├── ApiKeyInput.tsx # API Key 输入 (~40行) +│ │ ├── ConfigEditor.tsx # 配置编辑器 (~80行) +│ │ └── KimiModelSelector.tsx # Kimi 模型选择器 (~40行) +│ │ +│ ├── settings/ # 设置管理模块 (拆分自 SettingsModal) +│ │ ├── SettingsDialog.tsx # 设置对话框容器 (~80行) +│ │ ├── LanguageSettings.tsx # 语言设置 (~40行) +│ │ ├── WindowSettings.tsx # 窗口行为设置 (~50行) +│ │ ├── ConfigPathDisplay.tsx # 配置路径显示 (~40行) +│ │ ├── DirectorySettings/ # 目录设置子模块 +│ │ │ ├── index.tsx # 目录设置容器 (~60行) +│ │ │ └── DirectoryInput.tsx # 单个目录输入组件 (~50行) +│ │ ├── ImportExportSection.tsx # 导入导出 (~120行) +│ │ ├── AboutSection.tsx # 关于和更新 (~100行) +│ │ └── RestartDialog.tsx # 重启确认对话框 (~40行) +│ │ +│ ├── usage/ # 用量查询模块 +│ │ ├── UsageFooter.tsx # 用量信息展示 +│ │ ├── UsageScriptModal.tsx # 用量脚本配置 +│ │ └── UsageEditor.tsx # 脚本编辑器 +│ │ +│ ├── mcp/ # MCP 管理模块 +│ │ ├── McpPanel.tsx # MCP 管理面板 +│ │ ├── McpList.tsx # MCP 列表 +│ │ ├── McpForm.tsx # MCP 表单 +│ │ └── McpTemplates.tsx # MCP 模板选择 +│ │ +│ ├── shared/ # 共享组件 +│ │ ├── AppSwitcher.tsx # Claude/Codex 切换器 +│ │ ├── ConfirmDialog.tsx # 确认对话框 +│ │ ├── UpdateBadge.tsx # 更新徽章 +│ │ ├── JsonEditor.tsx # JSON 编辑器 +│ │ ├── BrandIcons.tsx # 品牌图标 +│ │ └── ImportProgressModal.tsx # 导入进度 +│ │ +│ ├── theme-provider.tsx # 主题 Provider +│ └── mode-toggle.tsx # 主题切换按钮 +│ +├── hooks/ # 自定义 Hooks (业务逻辑层) +│ ├── useSettings.ts # 设置管理逻辑 +│ ├── useImportExport.ts # 导入导出逻辑 +│ ├── useDragSort.ts # 拖拽排序逻辑 +│ ├── useProviderActions.ts # 供应商操作 (可选) +│ ├── useVSCodeSync.ts # VS Code 同步 +│ ├── useClaudePlugin.ts # Claude 插件管理 +│ └── useAppVersion.ts # 版本信息 +│ +├── lib/ +│ ├── query/ # React Query 层 +│ │ ├── index.ts # 导出所有 hooks +│ │ ├── queryClient.ts # QueryClient 配置 +│ │ ├── queries.ts # 所有查询 hooks +│ │ └── mutations.ts # 所有变更 hooks +│ │ +│ ├── api/ # API 调用层 (封装 Tauri invoke) +│ │ ├── providers.ts # 供应商 API +│ │ ├── settings.ts # 设置 API +│ │ ├── mcp.ts # MCP API +│ │ ├── usage.ts # 用量查询 API +│ │ ├── vscode.ts # VS Code API +│ │ └── index.ts # 聚合导出 +│ │ +│ ├── schemas/ # Zod 验证 Schemas +│ │ ├── provider.ts # 供应商验证规则 +│ │ ├── settings.ts # 设置验证规则 +│ │ └── mcp.ts # MCP 验证规则 +│ │ +│ ├── utils/ # 工具函数 +│ │ ├── errorHandling.ts # 错误处理 +│ │ ├── providerUtils.ts # 供应商工具 +│ │ └── configUtils.ts # 配置工具 +│ │ +│ └── utils.ts # shadcn/ui 工具函数 (cn) +│ +├── types/ # TypeScript 类型定义 +│ └── index.ts +│ +├── contexts/ # React Contexts (保留现有) +│ └── UpdateContext.tsx # 更新管理 Context +│ +├── i18n/ # 国际化 (保留现有) +│ ├── index.ts +│ └── locales/ +│ +├── App.tsx # 主应用组件 (简化到 ~100行) +├── main.tsx # 入口文件 (添加 Providers) +└── index.css # 全局样式 +``` + +### 目录结构设计原则 + +1. **按功能模块分组** (providers/, settings/, mcp/) +2. **按技术层次分层** (components/, hooks/, lib/) +3. **UI 组件独立** (ui/ 目录) +4. **业务逻辑提取** (hooks/ 目录) +5. **数据层封装** (api/ 目录) + +--- + +## 🏗 数据流架构 + +### 分层架构图 + +``` +┌─────────────────────────────────────────┐ +│ UI 层 (Components) │ +│ ProviderList, SettingsDialog, etc. │ +└────────────────┬────────────────────────┘ + │ 使用 + ↓ +┌─────────────────────────────────────────┐ +│ 业务逻辑层 (Custom Hooks) │ +│ useSettings, useDragSort, etc. │ +└────────────────┬────────────────────────┘ + │ 调用 + ↓ +┌─────────────────────────────────────────┐ +│ 数据管理层 (React Query Hooks) │ +│ useProvidersQuery, useMutation, etc. │ +└────────────────┬────────────────────────┘ + │ 调用 + ↓ +┌─────────────────────────────────────────┐ +│ API 层 (API Functions) │ +│ providersApi, settingsApi, etc. │ +└────────────────┬────────────────────────┘ + │ invoke + ↓ +┌─────────────────────────────────────────┐ +│ Tauri Backend (Rust) │ +│ Commands, State, File System │ +└─────────────────────────────────────────┘ +``` + +### 数据流示例 + +**场景**: 切换供应商 + +``` +1. 用户点击按钮 + ↓ +2. ProviderCard 调用 onClick={() => switchMutation.mutate(id)} + ↓ +3. useSwitchProviderMutation (lib/query/mutations.ts) + - mutationFn: 调用 providersApi.switch(id, appType) + ↓ +4. providersApi.switch (lib/api/providers.ts) + - 调用 invoke('switch_provider', { id, app_type }) + ↓ +5. Tauri Backend (Rust) + - 执行切换逻辑 + - 更新配置文件 + - 返回结果 + ↓ +6. useSwitchProviderMutation + - onSuccess: invalidateQueries(['providers', appType]) + - onSuccess: updateTrayMenu() + - onSuccess: toast.success('切换成功') + ↓ +7. useProvidersQuery 自动重新获取数据 + ↓ +8. UI 自动更新 +``` + +### 关键设计原则 + +1. **单一职责**: 每层只做一件事 +2. **依赖倒置**: UI 依赖抽象 (hooks),不依赖具体实现 +3. **开闭原则**: 易于扩展,无需修改现有代码 +4. **状态分离**: + - 服务端状态 → React Query + - 客户端 UI 状态 → useState + - 全局状态 → Context + +--- + +## 🔧 组件拆分详细方案 + +### 拆分策略: SettingsModal (1046行 → 7个组件) + +#### 拆分前后对比 + +``` +┌───────────────────────────────────┐ +│ SettingsModal.tsx (1046 行) │ ❌ 过于臃肿 +│ │ +│ - 20+ useState │ +│ - 15+ 函数 │ +│ - 600+ 行 JSX │ +│ - 难以理解和维护 │ +└───────────────────────────────────┘ + + ↓ 重构 + +┌─────────────────────────────────────────────────┐ +│ settings/ 模块 (7个组件, ~470行) │ +│ │ +│ ├── SettingsDialog.tsx (容器, ~80行) │ +│ │ └── 使用 useSettings hook │ +│ │ │ +│ ├── LanguageSettings.tsx (~40行) │ +│ ├── WindowSettings.tsx (~50行) │ +│ ├── ConfigPathDisplay.tsx (~40行) │ +│ ├── DirectorySettings/ (~110行) │ +│ │ ├── index.tsx (~60行) │ +│ │ └── DirectoryInput.tsx (~50行) │ +│ ├── ImportExportSection.tsx (~120行) │ +│ │ └── 使用 useImportExport hook │ +│ └── AboutSection.tsx (~100行) │ +│ └── 使用 useAppVersion, useUpdate hooks │ +└─────────────────────────────────────────────────┘ + +✅ 每个组件 30-120 行 +✅ 职责清晰 +✅ 易于测试 +✅ 可独立开发 +``` + +#### 拆分详细方案 + +**1. SettingsDialog.tsx (容器组件, ~80行)** + +职责: 组织整体布局,协调子组件 + +```typescript +import { LanguageSettings } from './LanguageSettings' +import { WindowSettings } from './WindowSettings' +import { DirectorySettings } from './DirectorySettings' +import { ImportExportSection } from './ImportExportSection' +import { AboutSection } from './AboutSection' +import { useSettings } from '@/hooks/useSettings' + +export function SettingsDialog({ open, onOpenChange }) { + const { settings, updateSettings, saveSettings, isPending } = useSettings() + + return ( + + + + 设置 + + + + + 通用 + 高级 + 关于 + + + + updateSettings({ language: lang })} + /> + + + + + + + + + + + + + + + + + + + + + ) +} +``` + +**2. LanguageSettings.tsx (~40行)** + +职责: 语言切换 UI + +```typescript +interface LanguageSettingsProps { + value: 'zh' | 'en' + onChange: (lang: 'zh' | 'en') => void +} + +export function LanguageSettings({ value, onChange }: LanguageSettingsProps) { + return ( +
+

语言设置

+
+ + +
+
+ ) +} +``` + +**3. DirectoryInput.tsx (~50行)** + +职责: 可复用的目录选择输入框 + +```typescript +import { FolderSearch, Undo2 } from 'lucide-react' + +interface DirectoryInputProps { + label: string + description?: string + value?: string + onChange: (value: string | undefined) => void + type: 'app' | 'claude' | 'codex' +} + +export function DirectoryInput({ label, description, value, onChange }: DirectoryInputProps) { + const handleBrowse = async () => { + const selected = await window.api.selectConfigDirectory(value) + if (selected) onChange(selected) + } + + const handleReset = () => { + onChange(undefined) + } + + return ( +
+ + {description &&

{description}

} +
+ onChange(e.target.value)} + className="flex-1 font-mono text-xs" + /> + + +
+
+ ) +} +``` + +**4. useSettings Hook (业务逻辑提取)** + +```typescript +export function useSettings() { + const queryClient = useQueryClient(); + + // 获取设置 + const { data: settings, isLoading } = useQuery({ + queryKey: ["settings"], + queryFn: async () => await settingsApi.get(), + }); + + // 保存设置 + const saveMutation = useMutation({ + mutationFn: async (newSettings: Settings) => + await settingsApi.save(newSettings), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["settings"] }); + toast.success("设置已保存"); + }, + }); + + // 本地临时状态 (保存前) + const [localSettings, setLocalSettings] = useState(null); + const currentSettings = localSettings || settings || {}; + + return { + settings: currentSettings, + updateSettings: (updates: Partial) => { + setLocalSettings((prev) => ({ ...prev, ...updates })); + }, + saveSettings: () => { + if (localSettings) saveMutation.mutate(localSettings); + }, + resetSettings: () => setLocalSettings(null), + isPending: saveMutation.isPending, + isLoading, + }; +} +``` + +--- + +### 拆分策略: ProviderList (418行 → 4个组件 + 1个Hook) + +#### 拆分方案 + +``` +ProviderList.tsx (418 行) ❌ 内嵌组件、逻辑混杂 + + ↓ 重构 + +providers/ 模块 (4个组件 + 1个Hook, ~330行) + +├── ProviderList.tsx (容器, ~100行) +│ └── 使用 useDragSort hook +│ +├── ProviderCard.tsx (~120行) +│ └── 显示单个供应商信息 +│ +├── ProviderActions.tsx (~80行) +│ └── 操作按钮组 (switch, edit, delete, usage) +│ +├── ProviderEmptyState.tsx (~30行) +│ └── 空状态提示 +│ +└── hooks/useDragSort.ts (~100行) + └── 拖拽排序逻辑 +``` + +#### 代码示例 + +**ProviderList.tsx (容器)** + +```typescript +import { ProviderCard } from './ProviderCard' +import { ProviderEmptyState } from './ProviderEmptyState' +import { useDragSort } from '@/hooks/useDragSort' + +export function ProviderList({ providers, currentProviderId, appType }) { + const { sortedProviders, handleDragEnd, sensors } = useDragSort(providers, appType) + + if (sortedProviders.length === 0) { + return + } + + return ( + + p.id)} + strategy={verticalListSortingStrategy} + > +
+ {sortedProviders.map(provider => ( + + ))} +
+
+
+ ) +} +``` + +**useDragSort.ts (逻辑提取)** + +```typescript +export function useDragSort( + providers: Record, + appType: AppType +) { + const queryClient = useQueryClient(); + const { t } = useTranslation(); + + // 排序逻辑 + const sortedProviders = useMemo(() => { + return Object.values(providers).sort((a, b) => { + if (a.sortIndex !== undefined && b.sortIndex !== undefined) { + return a.sortIndex - b.sortIndex; + } + const timeA = a.createdAt || 0; + const timeB = b.createdAt || 0; + if (timeA === 0 && timeB === 0) { + return a.name.localeCompare(b.name, "zh-CN"); + } + return timeA === 0 ? -1 : timeB === 0 ? 1 : timeA - timeB; + }); + }, [providers]); + + // 拖拽传感器 + const sensors = useSensors( + useSensor(PointerSensor, { activationConstraint: { distance: 8 } }), + useSensor(KeyboardSensor) + ); + + // 拖拽结束处理 + const handleDragEnd = useCallback( + async (event: DragEndEvent) => { + const { active, over } = event; + if (!over || active.id === over.id) return; + + const oldIndex = sortedProviders.findIndex((p) => p.id === active.id); + const newIndex = sortedProviders.findIndex((p) => p.id === over.id); + + const reordered = arrayMove(sortedProviders, oldIndex, newIndex); + const updates = reordered.map((p, i) => ({ id: p.id, sortIndex: i })); + + try { + await providersApi.updateSortOrder(updates, appType); + queryClient.invalidateQueries({ queryKey: ["providers", appType] }); + toast.success(t("provider.sortUpdated")); + } catch (error) { + toast.error(t("provider.sortUpdateFailed")); + } + }, + [sortedProviders, appType, queryClient, t] + ); + + return { sortedProviders, sensors, handleDragEnd }; +} +``` + +--- + +### 代码量对比总结 + +| 组件 | 重构前 | 重构后 | 变化 | +| -------------------- | ---------- | ---------------- | -------- | +| **SettingsModal** | 1046 行 | 7个组件 ~470行 | **-55%** | +| **ProviderList** | 418 行 | 4个组件 ~330行 | **-21%** | +| **业务逻辑 (Hooks)** | 混在组件中 | 5个 hooks ~400行 | 提取独立 | +| **总计** | 1464 行 | ~1200 行 | **-18%** | + +**注意**: 代码总量略有减少,但**可维护性大幅提升**: + +- ✅ 每个文件 30-120 行,易于理解 +- ✅ 关注点分离,职责清晰 +- ✅ 业务逻辑可复用 +- ✅ 易于测试和调试 + +--- + +# 第三部分: 实施计划 + +## 📅 分阶段实施路线图 + +### 总览 + +| 阶段 | 目标 | 工期 | 产出 | +| ---------- | -------------- | ------------ | ---------------------------- | +| **阶段 0** | 准备环境 | 1 天 | 依赖安装、配置完成 | +| **阶段 1** | 搭建基础设施 | 2-3 天 | API 层、Query Hooks 完成 | +| **阶段 2** | 重构核心功能 | 3-4 天 | App.tsx、ProviderList 完成 | +| **阶段 3** | 重构设置和辅助 | 2-3 天 | SettingsDialog、通知系统完成 | +| **阶段 4** | 清理和优化 | 1-2 天 | 旧代码删除、优化完成 | +| **阶段 5** | 测试和修复 | 2-3 天 | 测试通过、Bug 修复 | +| **总计** | - | **11-16 天** | v4.0.0 发布 | + +--- + +### 阶段 0: 准备阶段 (1天) + +**目标**: 环境准备和依赖安装 + +#### 任务清单 + +- [ ] 创建新分支 `refactor/modernization` +- [ ] 创建备份标签 `git tag backup-before-refactor` +- [ ] 安装核心依赖 +- [ ] 配置 shadcn/ui +- [ ] 配置 TypeScript 路径别名 +- [ ] 配置 Vite 路径解析 +- [ ] 验证开发服务器启动 + +#### 详细步骤 + +**1. 创建分支和备份** + +```bash +# 创建新分支 +git checkout -b refactor/modernization + +# 创建备份标签 +git tag backup-before-refactor + +# 推送标签到远程 (可选) +git push origin backup-before-refactor +``` + +**2. 安装依赖** + +```bash +# 核心依赖 +pnpm add @tanstack/react-query +pnpm add react-hook-form @hookform/resolvers +pnpm add zod +pnpm add sonner +pnpm add next-themes + +# Radix UI 组件 (shadcn/ui 依赖) +pnpm add @radix-ui/react-dialog +pnpm add @radix-ui/react-dropdown-menu +pnpm add @radix-ui/react-label +pnpm add @radix-ui/react-select +pnpm add @radix-ui/react-slot +pnpm add @radix-ui/react-switch +pnpm add @radix-ui/react-tabs +pnpm add @radix-ui/react-checkbox + +# 样式工具 +pnpm add class-variance-authority +pnpm add clsx +pnpm add tailwind-merge +``` + +**3. 创建 `components.json`** + +```json +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "src/index.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + } +} +``` + +**4. 更新 `tsconfig.json`** + +```json +{ + "compilerOptions": { + // ... 现有配置 + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + } +} +``` + +**5. 更新 `vite.config.mts`** + +```typescript +import path from "path"; +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, +}); +``` + +**6. 验证** + +```bash +pnpm dev # 确保开发服务器正常启动 +pnpm typecheck # 确保类型检查通过 +``` + +--- + +### 阶段 1: 基础设施 (2-3天) + +**目标**: 搭建新架构的基础层 + +#### 任务清单 + +- [ ] 创建工具函数 (`lib/utils.ts`) +- [ ] 添加基础 UI 组件 (Button, Dialog, Input, Form 等) +- [ ] 创建 Query Client 配置 +- [ ] 封装 API 层 (providers, settings, mcp) +- [ ] 创建 Query Hooks (queries, mutations) +- [ ] 创建 Zod Schemas + +#### 详细步骤 + +**Step 1.1: 创建 `src/lib/utils.ts`** + +```typescript +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} +``` + +**Step 1.2: 添加 shadcn/ui 基础组件** + +创建 `src/components/ui/button.tsx`: + +```typescript +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground shadow hover:bg-primary/90", + destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", + outline: "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", + secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2", + sm: "h-8 rounded-md px-3 text-xs", + lg: "h-10 rounded-md px-8", + icon: "h-9 w-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" + +export { Button, buttonVariants } +``` + +类似地创建: + +- `dialog.tsx` +- `input.tsx` +- `label.tsx` +- `form.tsx` +- `select.tsx` +- `switch.tsx` +- `tabs.tsx` +- `textarea.tsx` +- `sonner.tsx` + +**参考**: https://ui.shadcn.com/docs/components + +**Step 1.3: 创建 Query Client** + +`src/lib/query/queryClient.ts`: + +```typescript +import { QueryClient } from "@tanstack/react-query"; + +export const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: 1, + refetchOnWindowFocus: false, + staleTime: 1000 * 60 * 5, // 5 分钟 + }, + mutations: { + retry: false, + }, + }, +}); +``` + +**Step 1.4: 封装 API 层** + +`src/lib/api/providers.ts`: + +```typescript +import { invoke } from "@tauri-apps/api/core"; +import { Provider } from "@/types"; + +export type AppType = "claude" | "codex"; + +export const providersApi = { + getAll: async (appType: AppType): Promise> => { + return await invoke("get_providers", { app_type: appType, app: appType }); + }, + + getCurrent: async (appType: AppType): Promise => { + return await invoke("get_current_provider", { + app_type: appType, + app: appType, + }); + }, + + add: async (provider: Provider, appType: AppType): Promise => { + return await invoke("add_provider", { + provider, + app_type: appType, + app: appType, + }); + }, + + update: async (provider: Provider, appType: AppType): Promise => { + return await invoke("update_provider", { + provider, + app_type: appType, + app: appType, + }); + }, + + delete: async (id: string, appType: AppType): Promise => { + return await invoke("delete_provider", { + id, + app_type: appType, + app: appType, + }); + }, + + switch: async (id: string, appType: AppType): Promise => { + return await invoke("switch_provider", { + id, + app_type: appType, + app: appType, + }); + }, + + importDefault: async (appType: AppType): Promise => { + return await invoke("import_default_config", { + app_type: appType, + app: appType, + }); + }, + + updateTrayMenu: async (): Promise => { + return await invoke("update_tray_menu"); + }, + + updateSortOrder: async ( + updates: Array<{ id: string; sortIndex: number }>, + appType: AppType + ): Promise => { + return await invoke("update_providers_sort_order", { + updates, + app_type: appType, + app: appType, + }); + }, +}; +``` + +类似地创建: + +- `src/lib/api/settings.ts` +- `src/lib/api/mcp.ts` +- `src/lib/api/index.ts` (聚合导出) + +**Step 1.5: 创建 Query Hooks** + +`src/lib/query/queries.ts`: + +```typescript +import { useQuery } from "@tanstack/react-query"; +import { providersApi, AppType } from "@/lib/api"; +import { Provider } from "@/types"; + +// 排序辅助函数 +const sortProviders = ( + providers: Record +): Record => { + return Object.fromEntries( + Object.values(providers) + .sort((a, b) => { + const timeA = a.createdAt || 0; + const timeB = b.createdAt || 0; + if (timeA === 0 && timeB === 0) { + return a.name.localeCompare(b.name, "zh-CN"); + } + if (timeA === 0) return -1; + if (timeB === 0) return 1; + return timeA - timeB; + }) + .map((provider) => [provider.id, provider]) + ); +}; + +export const useProvidersQuery = (appType: AppType) => { + return useQuery({ + queryKey: ["providers", appType], + queryFn: async () => { + let providers: Record = {}; + let currentProviderId = ""; + + try { + providers = await providersApi.getAll(appType); + } catch (error) { + console.error("获取供应商列表失败:", error); + } + + try { + currentProviderId = await providersApi.getCurrent(appType); + } catch (error) { + console.error("获取当前供应商失败:", error); + } + + // 自动导入默认配置 + if (Object.keys(providers).length === 0) { + try { + const success = await providersApi.importDefault(appType); + if (success) { + providers = await providersApi.getAll(appType); + currentProviderId = await providersApi.getCurrent(appType); + } + } catch (error) { + console.error("导入默认配置失败:", error); + } + } + + return { providers: sortProviders(providers), currentProviderId }; + }, + }); +}; +``` + +`src/lib/query/mutations.ts`: + +```typescript +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { providersApi, AppType } from "@/lib/api"; +import { Provider } from "@/types"; +import { toast } from "sonner"; +import { useTranslation } from "react-i18next"; + +export const useAddProviderMutation = (appType: AppType) => { + const queryClient = useQueryClient(); + const { t } = useTranslation(); + + return useMutation({ + mutationFn: async (provider: Omit) => { + const newProvider: Provider = { + ...provider, + id: crypto.randomUUID(), + createdAt: Date.now(), + }; + await providersApi.add(newProvider, appType); + return newProvider; + }, + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: ["providers", appType] }); + await providersApi.updateTrayMenu(); + toast.success(t("notifications.providerAdded")); + }, + onError: (error: Error) => { + toast.error(t("notifications.addFailed", { error: error.message })); + }, + }); +}; + +export const useSwitchProviderMutation = (appType: AppType) => { + const queryClient = useQueryClient(); + const { t } = useTranslation(); + + return useMutation({ + mutationFn: async (providerId: string) => { + return await providersApi.switch(providerId, appType); + }, + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: ["providers", appType] }); + await providersApi.updateTrayMenu(); + toast.success( + t("notifications.switchSuccess", { appName: t(`apps.${appType}`) }) + ); + }, + onError: (error: Error) => { + toast.error(t("notifications.switchFailed") + ": " + error.message); + }, + }); +}; + +// 类似地创建: useDeleteProviderMutation, useUpdateProviderMutation +``` + +**Step 1.6: 创建 Zod Schemas** + +`src/lib/schemas/provider.ts`: + +```typescript +import { z } from "zod"; + +export const providerSchema = z.object({ + name: z.string().min(1, "请填写供应商名称"), + websiteUrl: z.string().url("请输入有效的网址").optional().or(z.literal("")), + settingsConfig: z + .string() + .min(1, "请填写配置内容") + .refine( + (val) => { + try { + JSON.parse(val); + return true; + } catch { + return false; + } + }, + { message: "配置 JSON 格式错误" } + ), +}); + +export type ProviderFormData = z.infer; +``` + +--- + +### 阶段 2: 核心功能重构 (3-4天) + +**目标**: 重构 App.tsx 和供应商管理 + +#### 任务清单 + +- [ ] 更新 `main.tsx` (添加 Providers) +- [ ] 创建主题 Provider +- [ ] 重构 `App.tsx` (412行 → ~100行) +- [ ] 拆分 ProviderList (4个组件) +- [ ] 创建 `useDragSort` Hook +- [ ] 重构表单组件 (使用 react-hook-form) +- [ ] 创建 AddProvider / EditProvider Dialog + +#### 详细步骤 + +**Step 2.1: 更新 `main.tsx`** + +```typescript +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App' +import { UpdateProvider } from './contexts/UpdateContext' +import './index.css' +import './i18n' +import { QueryClientProvider } from '@tanstack/react-query' +import { queryClient } from '@/lib/query' +import { ThemeProvider } from '@/components/theme-provider' +import { Toaster } from '@/components/ui/sonner' + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + + + + + + + + +) +``` + +**Step 2.2: 创建 `theme-provider.tsx`** + +```typescript +import { createContext, useContext, useEffect, useState } from 'react' + +type Theme = 'dark' | 'light' | 'system' + +type ThemeProviderProps = { + children: React.ReactNode + defaultTheme?: Theme + storageKey?: string +} + +type ThemeProviderState = { + theme: Theme + setTheme: (theme: Theme) => void +} + +const ThemeProviderContext = createContext({ + theme: 'system', + setTheme: () => null, +}) + +export function ThemeProvider({ + children, + defaultTheme = 'system', + storageKey = 'ui-theme', + ...props +}: ThemeProviderProps) { + const [theme, setTheme] = useState( + () => (localStorage.getItem(storageKey) as Theme) || defaultTheme + ) + + useEffect(() => { + const root = window.document.documentElement + root.classList.remove('light', 'dark') + + if (theme === 'system') { + const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches + ? 'dark' + : 'light' + root.classList.add(systemTheme) + return + } + + root.classList.add(theme) + }, [theme]) + + const value = { + theme, + setTheme: (theme: Theme) => { + localStorage.setItem(storageKey, theme) + setTheme(theme) + }, + } + + return ( + + {children} + + ) +} + +export const useTheme = () => { + const context = useContext(ThemeProviderContext) + if (context === undefined) { + throw new Error('useTheme must be used within a ThemeProvider') + } + return context +} +``` + +**Step 2.3: 重构 `App.tsx`** + +(参考前面的代码示例,从 412 行简化到 ~100 行) + +**Step 2.4-2.7: 拆分 ProviderList** + +(参考前面的组件拆分详细方案) + +--- + +### 阶段 3: 设置和辅助功能 (2-3天) + +**目标**: 重构 SettingsModal 和通知系统 + +#### 任务清单 + +- [ ] 拆分 SettingsDialog (7个组件) +- [ ] 创建 `useSettings` Hook +- [ ] 创建 `useImportExport` Hook +- [ ] 替换通知系统为 Sonner +- [ ] 重构 ConfirmDialog + +#### 详细步骤 + +(参考前面的组件拆分详细方案) + +--- + +### 阶段 4: 清理和优化 (1-2天) + +**目标**: 清理旧代码,优化性能 + +#### 任务清单 + +- [ ] 删除 `lib/styles.ts` +- [ ] 删除旧的 Modal 组件 +- [ ] 移除 `window.api` 全局绑定 +- [ ] 清理无用的 state 和函数 +- [ ] 更新类型定义 +- [ ] 代码格式化 +- [ ] TypeScript 检查 + +--- + +### 阶段 5: 测试和修复 (2-3天) + +**目标**: 全面测试,修复 Bug + +#### 功能测试清单 + +- [ ] 添加供应商 (Claude/Codex) +- [ ] 编辑供应商 +- [ ] 删除供应商 +- [ ] 切换供应商 +- [ ] 拖拽排序 +- [ ] 设置保存 +- [ ] 导入导出配置 +- [ ] 主题切换 +- [ ] MCP 管理 +- [ ] 用量查询 +- [ ] 托盘菜单同步 + +#### 边界情况测试 + +- [ ] 空供应商列表 +- [ ] 网络错误 +- [ ] 表单验证 +- [ ] 并发操作 +- [ ] 大量数据 (100+ 供应商) + +--- + +# 第四部分: 质量保障 + +## 🧪 测试策略 + +### 手动测试 + +每完成一个阶段后进行全量功能测试。 + +### 自动化测试 (可选) + +可以考虑添加: + +- Vitest 单元测试 (hooks, utils) +- Testing Library 组件测试 + +--- + +## 🚨 风险控制 + +### 潜在风险 + +1. **功能回归**: 重构可能引入 bug +2. **用户数据丢失**: 配置文件操作失败 +3. **性能下降**: 新架构可能影响性能 +4. **兼容性问题**: 依赖库平台兼容性 + +### 缓解措施 + +1. **逐步重构**: 按阶段进行,每阶段后测试 +2. **保留备份**: Git tag + 配置文件备份 +3. **Beta 测试**: 先发布 beta 版本 +4. **回滚方案**: 准备快速回滚机制 + +--- + +## ⏪ 回滚方案 + +### 如果需要回滚 + +```bash +# 方案 1: 回到重构前 +git reset --hard backup-before-refactor + +# 方案 2: 创建回滚分支 +git checkout -b rollback-refactor +git revert +``` + +### 用户数据保护 + +在重构前自动备份配置: + +```rust +// Rust 后端 +fn backup_config_before_refactor() -> Result<()> { + let config_path = get_app_config_path()?; + let backup_path = config_path.with_extension("backup.json"); + fs::copy(config_path, backup_path)?; + Ok(()) +} +``` + +--- + +## 🎯 成功标准 + +### 必须达成 (Must Have) + +- ✅ 所有现有功能正常工作 +- ✅ 无用户数据丢失 +- ✅ 性能不下降 +- ✅ TypeScript 检查通过 + +### 期望达成 (Should Have) + +- ✅ 代码量减少 40%+ +- ✅ 用户反馈积极 +- ✅ 开发体验提升明显 + +### 可选达成 (Nice to Have) + +- ⭕ 添加自动化测试 +- ⭕ 性能优化 20%+ + +--- + +## 📊 预期成果 + +### 代码质量 + +- **代码行数**: 减少 40-60% +- **文件数量**: UI 组件增加,但单文件更小 +- **可维护性**: 大幅提升 + +### 开发效率 + +- **新功能开发**: 提升 50%+ +- **Bug 修复**: 提升 30%+ +- **代码审查**: 提升 40%+ + +### 用户体验 + +- **界面一致性**: 统一的设计语言 +- **响应速度**: 更好的加载反馈 +- **错误提示**: 更友好的错误信息 + +--- + +## 📚 参考资料 + +- [TanStack Query 文档](https://tanstack.com/query/latest) +- [react-hook-form 文档](https://react-hook-form.com/) +- [shadcn/ui 文档](https://ui.shadcn.com/) +- [Zod 文档](https://zod.dev/) +- [原始 PR #76](https://github.com/farion1231/cc-switch/pull/76) + +--- + +## 📝 注意事项 + +1. **分支管理**: 在新分支进行,不要直接在 main 上修改 +2. **提交粒度**: 每完成一小步就提交,便于回滚 +3. **文档更新**: 同步更新 CLAUDE.md +4. **依赖锁定**: 锁定依赖版本 +5. **沟通协作**: 定期同步进度 + +--- + +**祝重构顺利! 🚀** diff --git a/docs/REFACTORING_REFERENCE.md b/docs/REFACTORING_REFERENCE.md new file mode 100644 index 0000000..ee6c876 --- /dev/null +++ b/docs/REFACTORING_REFERENCE.md @@ -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
Loading...
+ if (error) return
Error: {error.message}
+ + return
{/* 使用 data */}
+} +``` + +### 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 ( + + ) +} +``` + +### 乐观更新 + +```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 + +function MyForm() { + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: { + name: '', + email: '', + age: 0, + }, + }) + + const onSubmit = (data: FormData) => { + console.log(data) + } + + return ( +
+ + {form.formState.errors.name && ( + {form.formState.errors.name.message} + )} + + +
+ ) +} +``` + +### 使用 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({ + resolver: zodResolver(schema), + }) + + return ( +
+ + ( + + 名称 + + + + + + )} + /> + + + + + ) +} +``` + +### 动态表单验证 + +```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() + + const handleBlur = async () => { + // 验证单个字段 + await form.trigger('name') + + // 验证多个字段 + await form.trigger(['name', 'email']) + + // 验证所有字段 + const isValid = await form.trigger() + } + + return
...
+} +``` + +--- + +## 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 ( + + + + 标题 + 描述信息 + + + {/* 内容 */} +
对话框内容
+ + + + + +
+
+ ) +} +``` + +### Select (选择器) + +```typescript +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' + +function MySelect() { + const [value, setValue] = useState('') + + return ( + + ) +} +``` + +### Tabs (标签页) + +```typescript +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' + +function MyTabs() { + return ( + + + 标签1 + 标签2 + 标签3 + + + +
标签1的内容
+
+ + +
标签2的内容
+
+ + +
标签3的内容
+
+
+ ) +} +``` + +### 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>({}) +const [currentProviderId, setCurrentProviderId] = useState('') +const [loading, setLoading] = useState(false) +const [error, setError] = useState(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 ( +
+ setName(e.target.value)} /> + {nameError && {nameError}} + + setApiKey(e.target.value)} /> + {apiKeyError && {apiKeyError}} + + +
+) +``` + +**新代码** (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 ( +
+ + ( + + + + + + + )} + /> + + ( + + + + + + + )} + /> + + + + +) +``` + +**减少**: 从 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 && ( +
+ {notification.message} +
+ )} + {/* 其他内容 */} + +) +``` + +**新代码** (Sonner): + +```typescript +import { toast } from 'sonner' + +// 在需要的地方直接调用 +toast.success('操作成功') +toast.error('操作失败') + +// 在 main.tsx 中只需添加一次 +import { Toaster } from '@/components/ui/sonner' + + +``` + +**减少**: 从 20+ 行到 1 行调用 + +--- + +### 示例 4: 对话框迁移 + +**旧代码** (自定义 Modal): + +```typescript +const [isOpen, setIsOpen] = useState(false) + +return ( + <> + + + {isOpen && ( +
setIsOpen(false)}> +
e.stopPropagation()}> +
+

标题

+ +
+
+ {/* 内容 */} +
+
+ + +
+
+
+ )} + +) +``` + +**新代码** (shadcn/ui Dialog): + +```typescript +import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' + +const [isOpen, setIsOpen] = useState(false) + +return ( + <> + + + + + + 标题 + + {/* 内容 */} + + + + + + + +) +``` + +**优势**: +- 无需自定义样式 +- 内置无障碍支持 +- 自动管理焦点和 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' + + + + + +``` + +### 查看表单状态 + +```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', // 提交时验证(最快) +}) +``` + +--- + +**提示**: 将此文档保存在浏览器书签或编辑器中,方便随时查阅!