From 0868a71576f0221e3cb13c9849ae7ab8f64379f5 Mon Sep 17 00:00:00 2001 From: Jason Date: Thu, 16 Oct 2025 21:40:42 +0800 Subject: [PATCH] refactor: split ProviderForm into smaller focused components - Created ProviderPresetSelector component (80 lines) - Created BasicFormFields component (60 lines) - Created ClaudeFormFields component (272 lines) - Created CodexFormFields component (131 lines) - Reduced ProviderForm from 866 to 544 lines (37% reduction) Each component now has a clear single responsibility: - ProviderPresetSelector: Handles preset selection UI - BasicFormFields: Name and website URL inputs - ClaudeFormFields: All Claude-specific form fields - CodexFormFields: All Codex-specific form fields - ProviderForm: Orchestrates hooks and component composition Benefits: - Better code organization and maintainability - Easier to test individual components - Clearer separation of concerns - More reusable components --- .../providers/forms/BasicFormFields.tsx | 60 +++ .../providers/forms/ClaudeFormFields.tsx | 272 +++++++++++ .../providers/forms/CodexFormFields.tsx | 131 ++++++ .../providers/forms/ProviderForm.tsx | 438 +++--------------- .../forms/ProviderPresetSelector.tsx | 80 ++++ 5 files changed, 601 insertions(+), 380 deletions(-) create mode 100644 src/components/providers/forms/BasicFormFields.tsx create mode 100644 src/components/providers/forms/ClaudeFormFields.tsx create mode 100644 src/components/providers/forms/CodexFormFields.tsx create mode 100644 src/components/providers/forms/ProviderPresetSelector.tsx diff --git a/src/components/providers/forms/BasicFormFields.tsx b/src/components/providers/forms/BasicFormFields.tsx new file mode 100644 index 0000000..6b0da5c --- /dev/null +++ b/src/components/providers/forms/BasicFormFields.tsx @@ -0,0 +1,60 @@ +import { useTranslation } from "react-i18next"; +import { + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import type { UseFormReturn } from "react-hook-form"; +import type { ProviderFormData } from "@/lib/schemas/provider"; + +interface BasicFormFieldsProps { + form: UseFormReturn; +} + +export function BasicFormFields({ form }: BasicFormFieldsProps) { + const { t } = useTranslation(); + + return ( + <> + ( + + + {t("provider.name", { defaultValue: "供应商名称" })} + + + + + + + )} + /> + + ( + + + {t("provider.websiteUrl", { defaultValue: "官网链接" })} + + + + + + + )} + /> + + ); +} diff --git a/src/components/providers/forms/ClaudeFormFields.tsx b/src/components/providers/forms/ClaudeFormFields.tsx new file mode 100644 index 0000000..f01b3d6 --- /dev/null +++ b/src/components/providers/forms/ClaudeFormFields.tsx @@ -0,0 +1,272 @@ +import { useTranslation } from "react-i18next"; +import { FormLabel } from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import ApiKeyInput from "@/components/ProviderForm/ApiKeyInput"; +import EndpointSpeedTest from "@/components/ProviderForm/EndpointSpeedTest"; +import KimiModelSelector from "@/components/ProviderForm/KimiModelSelector"; +import { Zap } from "lucide-react"; +import type { ProviderCategory } from "@/types"; +import type { TemplateValueConfig } from "@/config/providerPresets"; + +interface ClaudeFormFieldsProps { + // API Key + shouldShowApiKey: boolean; + apiKey: string; + onApiKeyChange: (key: string) => void; + category?: ProviderCategory; + shouldShowApiKeyLink: boolean; + websiteUrl: string; + + // Template Values + templateValueEntries: Array<[string, TemplateValueConfig]>; + templateValues: Record; + templatePresetName: string; + onTemplateValueChange: (key: string, value: string) => void; + + // Base URL + shouldShowSpeedTest: boolean; + baseUrl: string; + onBaseUrlChange: (url: string) => void; + isEndpointModalOpen: boolean; + onEndpointModalToggle: (open: boolean) => void; + onCustomEndpointsChange: (endpoints: string[]) => void; + + // Model Selector + shouldShowKimiSelector: boolean; + shouldShowModelSelector: boolean; + claudeModel: string; + claudeSmallFastModel: string; + onModelChange: ( + field: "ANTHROPIC_MODEL" | "ANTHROPIC_SMALL_FAST_MODEL", + value: string + ) => void; + + // Kimi Model Selector + kimiAnthropicModel: string; + kimiAnthropicSmallFastModel: string; + onKimiModelChange: ( + field: "ANTHROPIC_MODEL" | "ANTHROPIC_SMALL_FAST_MODEL", + value: string + ) => void; +} + +export function ClaudeFormFields({ + shouldShowApiKey, + apiKey, + onApiKeyChange, + category, + shouldShowApiKeyLink, + websiteUrl, + templateValueEntries, + templateValues, + templatePresetName, + onTemplateValueChange, + shouldShowSpeedTest, + baseUrl, + onBaseUrlChange, + isEndpointModalOpen, + onEndpointModalToggle, + onCustomEndpointsChange, + shouldShowKimiSelector, + shouldShowModelSelector, + claudeModel, + claudeSmallFastModel, + onModelChange, + kimiAnthropicModel, + kimiAnthropicSmallFastModel, + onKimiModelChange, +}: ClaudeFormFieldsProps) { + const { t } = useTranslation(); + + return ( + <> + {/* API Key 输入框 */} + {shouldShowApiKey && ( +
+ + {/* API Key 获取链接 */} + {shouldShowApiKeyLink && websiteUrl && ( + + )} +
+ )} + + {/* 模板变量输入 */} + {templateValueEntries.length > 0 && ( +
+ + {t("providerForm.parameterConfig", { + name: templatePresetName, + defaultValue: `${templatePresetName} 参数配置`, + })} + +
+ {templateValueEntries.map(([key, config]) => ( +
+ + {config.label} + + onTemplateValueChange(key, e.target.value)} + placeholder={config.placeholder || config.label} + autoComplete="off" + /> +
+ ))} +
+
+ )} + + {/* Base URL 输入框 */} + {shouldShowSpeedTest && ( +
+
+ + {t("providerForm.apiEndpoint", { defaultValue: "API 端点" })} + + +
+ onBaseUrlChange(e.target.value)} + placeholder={t("providerForm.apiEndpointPlaceholder", { + defaultValue: "https://api.example.com", + })} + autoComplete="off" + /> +
+

+ {t("providerForm.apiHint", { + defaultValue: "API 端点地址用于连接服务器", + })} +

+
+
+ )} + + {/* 端点测速弹窗 */} + {shouldShowSpeedTest && isEndpointModalOpen && ( + onEndpointModalToggle(false)} + onCustomEndpointsChange={onCustomEndpointsChange} + /> + )} + + {/* 模型选择器 */} + {shouldShowModelSelector && ( +
+
+ {/* ANTHROPIC_MODEL */} +
+ + {t("providerForm.anthropicModel", { + defaultValue: "主模型", + })} + + + onModelChange("ANTHROPIC_MODEL", e.target.value) + } + placeholder={t("providerForm.modelPlaceholder", { + defaultValue: "claude-3-7-sonnet-20250219", + })} + autoComplete="off" + /> +
+ + {/* ANTHROPIC_SMALL_FAST_MODEL */} +
+ + {t("providerForm.anthropicSmallFastModel", { + defaultValue: "快速模型", + })} + + + onModelChange("ANTHROPIC_SMALL_FAST_MODEL", e.target.value) + } + placeholder={t("providerForm.smallModelPlaceholder", { + defaultValue: "claude-3-5-haiku-20241022", + })} + autoComplete="off" + /> +
+
+

+ {t("providerForm.modelHelper", { + defaultValue: + "可选:指定默认使用的 Claude 模型,留空则使用系统默认。", + })} +

+
+ )} + + {/* Kimi 模型选择器 */} + {shouldShowKimiSelector && ( + + )} + + ); +} diff --git a/src/components/providers/forms/CodexFormFields.tsx b/src/components/providers/forms/CodexFormFields.tsx new file mode 100644 index 0000000..01f9cf1 --- /dev/null +++ b/src/components/providers/forms/CodexFormFields.tsx @@ -0,0 +1,131 @@ +import { useTranslation } from "react-i18next"; +import { FormLabel } from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import ApiKeyInput from "@/components/ProviderForm/ApiKeyInput"; +import EndpointSpeedTest from "@/components/ProviderForm/EndpointSpeedTest"; +import { Zap } from "lucide-react"; +import type { ProviderCategory } from "@/types"; + +interface CodexFormFieldsProps { + // API Key + codexApiKey: string; + onApiKeyChange: (key: string) => void; + category?: ProviderCategory; + shouldShowApiKeyLink: boolean; + websiteUrl: string; + + // Base URL + shouldShowSpeedTest: boolean; + codexBaseUrl: string; + onBaseUrlChange: (url: string) => void; + isEndpointModalOpen: boolean; + onEndpointModalToggle: (open: boolean) => void; + onCustomEndpointsChange: (endpoints: string[]) => void; +} + +export function CodexFormFields({ + codexApiKey, + onApiKeyChange, + category, + shouldShowApiKeyLink, + websiteUrl, + shouldShowSpeedTest, + codexBaseUrl, + onBaseUrlChange, + isEndpointModalOpen, + onEndpointModalToggle, + onCustomEndpointsChange, +}: CodexFormFieldsProps) { + const { t } = useTranslation(); + + return ( + <> + {/* Codex API Key 输入框 */} +
+ + {/* Codex API Key 获取链接 */} + {shouldShowApiKeyLink && websiteUrl && ( + + )} +
+ + {/* Codex Base URL 输入框 */} + {shouldShowSpeedTest && ( +
+
+ + {t("codexConfig.apiUrlLabel", { defaultValue: "API 端点" })} + + +
+ onBaseUrlChange(e.target.value)} + placeholder={t("providerForm.codexApiEndpointPlaceholder", { + defaultValue: "https://api.example.com/v1", + })} + autoComplete="off" + /> +
+

+ {t("providerForm.codexApiHint", { + defaultValue: "Codex API 端点地址", + })} +

+
+
+ )} + + {/* 端点测速弹窗 - Codex */} + {shouldShowSpeedTest && isEndpointModalOpen && ( + onEndpointModalToggle(false)} + onCustomEndpointsChange={onCustomEndpointsChange} + /> + )} + + ); +} diff --git a/src/components/providers/forms/ProviderForm.tsx b/src/components/providers/forms/ProviderForm.tsx index 63da05c..b4d8b22 100644 --- a/src/components/providers/forms/ProviderForm.tsx +++ b/src/components/providers/forms/ProviderForm.tsx @@ -3,15 +3,7 @@ import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { useTranslation } from "react-i18next"; import { Button } from "@/components/ui/button"; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; +import { Form } from "@/components/ui/form"; import { providerSchema, type ProviderFormData } from "@/lib/schemas/provider"; import type { AppType } from "@/lib/api"; import type { ProviderCategory, CustomEndpoint } from "@/types"; @@ -21,12 +13,12 @@ import { type CodexProviderPreset, } from "@/config/codexProviderPresets"; import { applyTemplateValues } from "@/utils/providerConfigUtils"; -import ApiKeyInput from "@/components/ProviderForm/ApiKeyInput"; -import EndpointSpeedTest from "@/components/ProviderForm/EndpointSpeedTest"; import CodexConfigEditor from "@/components/ProviderForm/CodexConfigEditor"; -import KimiModelSelector from "@/components/ProviderForm/KimiModelSelector"; import { CommonConfigEditor } from "./CommonConfigEditor"; -import { Zap } from "lucide-react"; +import { ProviderPresetSelector } from "./ProviderPresetSelector"; +import { BasicFormFields } from "./BasicFormFields"; +import { ClaudeFormFields } from "./ClaudeFormFields"; +import { CodexFormFields } from "./CodexFormFields"; import { useProviderCategory, useApiKeyState, @@ -436,382 +428,68 @@ export function ProviderForm({
{/* 预设供应商选择(仅新增模式显示) */} {!initialData && ( -
- - {t("providerPreset.label", { defaultValue: "预设供应商" })} - -
- {/* 自定义按钮 */} - - - {/* 预设按钮 */} - {categoryKeys.map((category) => { - const entries = groupedPresets[category]; - if (!entries || entries.length === 0) return null; - return entries.map((entry) => ( - - )); - })} -
-

- {t("providerPreset.helper", { - defaultValue: "选择预设后可继续调整下方字段。", - })} -

-
+ )} - ( - - - {t("provider.name", { defaultValue: "供应商名称" })} - - - - - - - )} - /> + {/* 基础字段 */} + - ( - - - {t("provider.websiteUrl", { defaultValue: "官网链接" })} - - - - - - - )} - /> - - {/* API Key 输入框(仅 Claude 且非编辑模式显示) */} - {appType === "claude" && - shouldShowApiKey(form.watch("settingsConfig"), isEditMode) && ( -
- - {/* API Key 获取链接 */} - {shouldShowClaudeApiKeyLink && claudeWebsiteUrl && ( - - )} -
- )} - - {/* 模板变量输入(仅 Claude 且有模板变量时显示) */} - {appType === "claude" && templateValueEntries.length > 0 && ( -
- - {t("providerForm.parameterConfig", { - name: templatePreset?.name || "", - defaultValue: `${templatePreset?.name || ""} 参数配置`, - })} - -
- {templateValueEntries.map(([key, config]) => ( -
- - {config.label} - - - handleTemplateValueChange(key, e.target.value) - } - placeholder={config.placeholder || config.label} - autoComplete="off" - /> -
- ))} -
-
+ {/* Claude 专属字段 */} + {appType === "claude" && ( + )} - {/* Base URL 输入框(仅 Claude 第三方/自定义显示) */} - {appType === "claude" && shouldShowSpeedTest && ( -
-
- - {t("providerForm.apiEndpoint", { defaultValue: "API 端点" })} - - -
- handleClaudeBaseUrlChange(e.target.value)} - placeholder={t("providerForm.apiEndpointPlaceholder", { - defaultValue: "https://api.example.com", - })} - autoComplete="off" - /> -
-

- {t("providerForm.apiHint", { - defaultValue: "API 端点地址用于连接服务器", - })} -

-
-
- )} - - {/* 端点测速弹窗 - Claude */} - {appType === "claude" && shouldShowSpeedTest && isEndpointModalOpen && ( - setIsEndpointModalOpen(false)} + {/* Codex 专属字段 */} + {appType === "codex" && !isEditMode && ( + )} - {/* 模型选择器(仅 Claude 非官方且非 Kimi 供应商显示) */} - {appType === "claude" && - category !== "official" && - !shouldShowKimiSelector && ( -
-
- {/* ANTHROPIC_MODEL */} -
- - {t("providerForm.anthropicModel", { - defaultValue: "主模型", - })} - - - handleModelChange("ANTHROPIC_MODEL", e.target.value) - } - placeholder={t("providerForm.modelPlaceholder", { - defaultValue: "claude-3-7-sonnet-20250219", - })} - autoComplete="off" - /> -
- - {/* ANTHROPIC_SMALL_FAST_MODEL */} -
- - {t("providerForm.anthropicSmallFastModel", { - defaultValue: "快速模型", - })} - - - handleModelChange( - "ANTHROPIC_SMALL_FAST_MODEL", - e.target.value - ) - } - placeholder={t("providerForm.smallModelPlaceholder", { - defaultValue: "claude-3-5-haiku-20241022", - })} - autoComplete="off" - /> -
-
-

- {t("providerForm.modelHelper", { - defaultValue: - "可选:指定默认使用的 Claude 模型,留空则使用系统默认。", - })} -

-
- )} - - {/* Kimi 模型选择器(仅 Claude 且是 Kimi 供应商时显示) */} - {appType === "claude" && shouldShowKimiSelector && ( - - )} - - {/* Codex API Key 输入框 */} - {appType === "codex" && !isEditMode && ( -
- - {/* Codex API Key 获取链接 */} - {shouldShowCodexApiKeyLink && codexWebsiteUrl && ( - - )} -
- )} - - {/* Codex Base URL 输入框 */} - {appType === "codex" && shouldShowSpeedTest && ( -
-
- - {t("codexConfig.apiUrlLabel", { defaultValue: "API 端点" })} - - -
- handleCodexBaseUrlChange(e.target.value)} - placeholder={t("providerForm.codexApiEndpointPlaceholder", { - defaultValue: "https://api.example.com/v1", - })} - autoComplete="off" - /> -
-

- {t("providerForm.codexApiHint", { - defaultValue: "Codex API 端点地址", - })} -

-
-
- )} - - {/* 端点测速弹窗 - Codex */} - {appType === "codex" && - shouldShowSpeedTest && - isCodexEndpointModalOpen && ( - setIsCodexEndpointModalOpen(false)} - onCustomEndpointsChange={setDraftCustomEndpoints} - /> - )} - {/* 配置编辑器:Claude 使用通用配置编辑器,Codex 使用专用编辑器 */} {appType === "codex" ? ( ; + categoryKeys: string[]; + presetCategoryLabels: Record; + onPresetChange: (value: string) => void; +} + +export function ProviderPresetSelector({ + selectedPresetId, + groupedPresets, + categoryKeys, + presetCategoryLabels, + onPresetChange, +}: ProviderPresetSelectorProps) { + const { t } = useTranslation(); + + return ( +
+ + {t("providerPreset.label", { defaultValue: "预设供应商" })} + +
+ {/* 自定义按钮 */} + + + {/* 预设按钮 */} + {categoryKeys.map((category) => { + const entries = groupedPresets[category]; + if (!entries || entries.length === 0) return null; + return entries.map((entry) => ( + + )); + })} +
+

+ {t("providerPreset.helper", { + defaultValue: "选择预设后可继续调整下方字段。", + })} +

+
+ ); +}