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>
1680 lines
47 KiB
Markdown
1680 lines
47 KiB
Markdown
# 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<Record<string, Provider>>({})
|
||
const [currentProviderId, setCurrentProviderId] = useState<string>("")
|
||
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 (
|
||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||
<DialogContent className="max-w-2xl max-h-[80vh]">
|
||
<DialogHeader>
|
||
<DialogTitle>设置</DialogTitle>
|
||
</DialogHeader>
|
||
|
||
<Tabs defaultValue="general">
|
||
<TabsList className="grid w-full grid-cols-3">
|
||
<TabsTrigger value="general">通用</TabsTrigger>
|
||
<TabsTrigger value="advanced">高级</TabsTrigger>
|
||
<TabsTrigger value="about">关于</TabsTrigger>
|
||
</TabsList>
|
||
|
||
<TabsContent value="general" className="space-y-4">
|
||
<LanguageSettings
|
||
value={settings.language}
|
||
onChange={(lang) => updateSettings({ language: lang })}
|
||
/>
|
||
<WindowSettings settings={settings} onChange={updateSettings} />
|
||
<ConfigPathDisplay />
|
||
</TabsContent>
|
||
|
||
<TabsContent value="advanced" className="space-y-4">
|
||
<DirectorySettings settings={settings} onChange={updateSettings} />
|
||
<ImportExportSection />
|
||
</TabsContent>
|
||
|
||
<TabsContent value="about">
|
||
<AboutSection />
|
||
</TabsContent>
|
||
</Tabs>
|
||
|
||
<DialogFooter>
|
||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||
取消
|
||
</Button>
|
||
<Button onClick={saveSettings} disabled={isPending}>
|
||
{isPending ? '保存中...' : '保存'}
|
||
</Button>
|
||
</DialogFooter>
|
||
</DialogContent>
|
||
</Dialog>
|
||
)
|
||
}
|
||
```
|
||
|
||
**2. LanguageSettings.tsx (~40行)**
|
||
|
||
职责: 语言切换 UI
|
||
|
||
```typescript
|
||
interface LanguageSettingsProps {
|
||
value: 'zh' | 'en'
|
||
onChange: (lang: 'zh' | 'en') => void
|
||
}
|
||
|
||
export function LanguageSettings({ value, onChange }: LanguageSettingsProps) {
|
||
return (
|
||
<div>
|
||
<h3 className="text-sm font-medium mb-3">语言设置</h3>
|
||
<div className="inline-flex p-0.5 bg-gray-100 dark:bg-gray-800 rounded-lg">
|
||
<Button
|
||
variant={value === 'zh' ? 'default' : 'ghost'}
|
||
size="sm"
|
||
onClick={() => onChange('zh')}
|
||
>
|
||
中文
|
||
</Button>
|
||
<Button
|
||
variant={value === 'en' ? 'default' : 'ghost'}
|
||
size="sm"
|
||
onClick={() => onChange('en')}
|
||
>
|
||
English
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
```
|
||
|
||
**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 (
|
||
<div>
|
||
<Label className="text-xs">{label}</Label>
|
||
{description && <p className="text-xs text-muted-foreground mb-1">{description}</p>}
|
||
<div className="flex gap-2">
|
||
<Input
|
||
value={value || ''}
|
||
onChange={(e) => onChange(e.target.value)}
|
||
className="flex-1 font-mono text-xs"
|
||
/>
|
||
<Button variant="outline" size="icon" onClick={handleBrowse}>
|
||
<FolderSearch className="h-4 w-4" />
|
||
</Button>
|
||
<Button variant="outline" size="icon" onClick={handleReset}>
|
||
<Undo2 className="h-4 w-4" />
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
```
|
||
|
||
**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<Settings | null>(null);
|
||
const currentSettings = localSettings || settings || {};
|
||
|
||
return {
|
||
settings: currentSettings,
|
||
updateSettings: (updates: Partial<Settings>) => {
|
||
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 <ProviderEmptyState />
|
||
}
|
||
|
||
return (
|
||
<DndContext
|
||
sensors={sensors}
|
||
collisionDetection={closestCenter}
|
||
onDragEnd={handleDragEnd}
|
||
>
|
||
<SortableContext
|
||
items={sortedProviders.map(p => p.id)}
|
||
strategy={verticalListSortingStrategy}
|
||
>
|
||
<div className="space-y-3">
|
||
{sortedProviders.map(provider => (
|
||
<ProviderCard
|
||
key={provider.id}
|
||
provider={provider}
|
||
isCurrent={provider.id === currentProviderId}
|
||
appType={appType}
|
||
/>
|
||
))}
|
||
</div>
|
||
</SortableContext>
|
||
</DndContext>
|
||
)
|
||
}
|
||
```
|
||
|
||
**useDragSort.ts (逻辑提取)**
|
||
|
||
```typescript
|
||
export function useDragSort(
|
||
providers: Record<string, Provider>,
|
||
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<HTMLButtonElement>,
|
||
VariantProps<typeof buttonVariants> {
|
||
asChild?: boolean
|
||
}
|
||
|
||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||
const Comp = asChild ? Slot : "button"
|
||
return (
|
||
<Comp
|
||
className={cn(buttonVariants({ variant, size, className }))}
|
||
ref={ref}
|
||
{...props}
|
||
/>
|
||
)
|
||
}
|
||
)
|
||
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<Record<string, Provider>> => {
|
||
return await invoke("get_providers", { app_type: appType, app: appType });
|
||
},
|
||
|
||
getCurrent: async (appType: AppType): Promise<string> => {
|
||
return await invoke("get_current_provider", {
|
||
app_type: appType,
|
||
app: appType,
|
||
});
|
||
},
|
||
|
||
add: async (provider: Provider, appType: AppType): Promise<boolean> => {
|
||
return await invoke("add_provider", {
|
||
provider,
|
||
app_type: appType,
|
||
app: appType,
|
||
});
|
||
},
|
||
|
||
update: async (provider: Provider, appType: AppType): Promise<boolean> => {
|
||
return await invoke("update_provider", {
|
||
provider,
|
||
app_type: appType,
|
||
app: appType,
|
||
});
|
||
},
|
||
|
||
delete: async (id: string, appType: AppType): Promise<boolean> => {
|
||
return await invoke("delete_provider", {
|
||
id,
|
||
app_type: appType,
|
||
app: appType,
|
||
});
|
||
},
|
||
|
||
switch: async (id: string, appType: AppType): Promise<boolean> => {
|
||
return await invoke("switch_provider", {
|
||
id,
|
||
app_type: appType,
|
||
app: appType,
|
||
});
|
||
},
|
||
|
||
importDefault: async (appType: AppType): Promise<boolean> => {
|
||
return await invoke("import_default_config", {
|
||
app_type: appType,
|
||
app: appType,
|
||
});
|
||
},
|
||
|
||
updateTrayMenu: async (): Promise<boolean> => {
|
||
return await invoke("update_tray_menu");
|
||
},
|
||
|
||
updateSortOrder: async (
|
||
updates: Array<{ id: string; sortIndex: number }>,
|
||
appType: AppType
|
||
): Promise<boolean> => {
|
||
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<string, Provider>
|
||
): Record<string, Provider> => {
|
||
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<string, Provider> = {};
|
||
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<Provider, "id">) => {
|
||
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<typeof providerSchema>;
|
||
```
|
||
|
||
---
|
||
|
||
### 阶段 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(
|
||
<React.StrictMode>
|
||
<QueryClientProvider client={queryClient}>
|
||
<ThemeProvider defaultTheme="system" storageKey="cc-switch-theme">
|
||
<UpdateProvider>
|
||
<App />
|
||
<Toaster />
|
||
</UpdateProvider>
|
||
</ThemeProvider>
|
||
</QueryClientProvider>
|
||
</React.StrictMode>
|
||
)
|
||
```
|
||
|
||
**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<ThemeProviderState>({
|
||
theme: 'system',
|
||
setTheme: () => null,
|
||
})
|
||
|
||
export function ThemeProvider({
|
||
children,
|
||
defaultTheme = 'system',
|
||
storageKey = 'ui-theme',
|
||
...props
|
||
}: ThemeProviderProps) {
|
||
const [theme, setTheme] = useState<Theme>(
|
||
() => (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 (
|
||
<ThemeProviderContext.Provider {...props} value={value}>
|
||
{children}
|
||
</ThemeProviderContext.Provider>
|
||
)
|
||
}
|
||
|
||
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 <commit-range>
|
||
```
|
||
|
||
### 用户数据保护
|
||
|
||
在重构前自动备份配置:
|
||
|
||
```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. **沟通协作**: 定期同步进度
|
||
|
||
---
|
||
|
||
**祝重构顺利! 🚀**
|