refactor(config): split modelProvider.tsx into modular directory

- 拆分为 7 个文件:index.ts (barrel), types.ts, utils.ts, 3 个组件, index.tsx (主页面 895行)
- 所有子组件 < 500 行
- 构建零错误
- 功能完全等价
This commit is contained in:
DrSmoothl
2026-03-01 19:58:18 +08:00
parent b800011ed7
commit e1f9936561
6 changed files with 1844 additions and 1815 deletions

View File

@@ -0,0 +1,458 @@
import { useCallback, useMemo, useState } from 'react'
import { Check, ChevronsUpDown, Copy, Eye, EyeOff } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command'
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { HelpTooltip } from '@/components/ui/help-tooltip'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { useToast } from '@/hooks/use-toast'
import { PROVIDER_TEMPLATES } from '../providerTemplates'
import type { APIProvider, FormErrors } from './types'
import { validateProvider } from './utils'
interface ProviderFormProps {
open: boolean
onOpenChange: (open: boolean) => void
editingProvider: APIProvider | null
editingIndex: number | null
providers: APIProvider[]
onSave: (provider: APIProvider, index: number | null) => void
tourState: { isRunning: boolean }
}
export function ProviderForm({
open,
onOpenChange,
editingProvider,
editingIndex,
providers,
onSave,
tourState,
}: ProviderFormProps) {
const [formErrors, setFormErrors] = useState<FormErrors>({})
const [selectedTemplate, setSelectedTemplate] = useState<string>('custom')
const [templateComboboxOpen, setTemplateComboboxOpen] = useState(false)
const [showApiKey, setShowApiKey] = useState(false)
const [localProvider, setLocalProvider] = useState<APIProvider | null>(editingProvider)
const { toast } = useToast()
// 同步外部状态到本地
if (editingProvider !== localProvider && open) {
setLocalProvider(editingProvider)
setFormErrors({})
setShowApiKey(false)
// 检测匹配的模板
if (editingProvider) {
const matchedTemplate = PROVIDER_TEMPLATES.find(
t => t.base_url === editingProvider.base_url && t.client_type === editingProvider.client_type
)
setSelectedTemplate(matchedTemplate?.id || 'custom')
} else {
setSelectedTemplate('custom')
}
}
const isUsingTemplate = useMemo(() => selectedTemplate !== 'custom', [selectedTemplate])
const handleTemplateChange = useCallback((templateId: string) => {
setSelectedTemplate(templateId)
setTemplateComboboxOpen(false)
const template = PROVIDER_TEMPLATES.find(t => t.id === templateId)
if (template && template.id !== 'custom') {
setLocalProvider(prev => ({
...prev!,
name: template.name,
base_url: template.base_url,
client_type: template.client_type,
}))
} else if (template?.id === 'custom') {
setLocalProvider(prev => ({
...prev!,
name: '',
base_url: '',
client_type: 'openai',
}))
}
}, [])
const copyApiKey = useCallback(async () => {
if (!localProvider?.api_key) return
try {
await navigator.clipboard.writeText(localProvider.api_key)
toast({
title: '复制成功',
description: 'API Key 已复制到剪贴板',
})
} catch {
toast({
title: '复制失败',
description: '无法访问剪贴板',
variant: 'destructive',
})
}
}, [localProvider?.api_key, toast])
const handleSaveEdit = () => {
if (!localProvider) return
const { isValid, errors } = validateProvider(localProvider, providers, editingIndex)
if (!isValid) {
setFormErrors(errors)
return
}
setFormErrors({})
onSave(localProvider, editingIndex)
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
className="max-w-[95vw] sm:max-w-2xl max-h-[90vh] overflow-y-auto"
data-tour="provider-dialog"
preventOutsideClose={tourState.isRunning}
>
<DialogHeader>
<DialogTitle>
{editingIndex !== null ? '编辑提供商' : '添加提供商'}
</DialogTitle>
<DialogDescription>
API
</DialogDescription>
</DialogHeader>
<form onSubmit={(e) => { e.preventDefault(); handleSaveEdit(); }} autoComplete="off">
<div className="grid gap-4 py-4">
<div className="grid gap-2" data-tour="provider-template-select">
<Label htmlFor="template"></Label>
<Popover open={templateComboboxOpen} onOpenChange={setTemplateComboboxOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={templateComboboxOpen}
className="w-full justify-between"
>
{selectedTemplate
? PROVIDER_TEMPLATES.find((template) => template.id === selectedTemplate)?.display_name
: "选择提供商模板..."}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="p-0" align="start" style={{ width: 'var(--radix-popover-trigger-width)' }}>
<Command>
<CommandInput placeholder="搜索提供商模板..." />
<ScrollArea className="h-[300px]">
<CommandList className="max-h-none overflow-visible">
<CommandEmpty></CommandEmpty>
<CommandGroup>
{PROVIDER_TEMPLATES.map((template) => (
<CommandItem
key={template.id}
value={template.display_name}
onSelect={() => handleTemplateChange(template.id)}
>
<Check
className={`mr-2 h-4 w-4 ${
selectedTemplate === template.id ? "opacity-100" : "opacity-0"
}`}
/>
{template.display_name}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</ScrollArea>
</Command>
</PopoverContent>
</Popover>
<p className="text-xs text-muted-foreground">
URL ,
</p>
</div>
<div className="grid gap-2" data-tour="provider-name-input">
<div className="flex items-center gap-1.5">
<Label htmlFor="name" className={formErrors.name ? 'text-destructive' : ''}> *</Label>
<HelpTooltip
content={
<div className="space-y-2">
<p className="font-medium"></p>
<p> API 便</p>
<ul className="list-disc list-inside space-y-1 text-xs">
<li>使 DeepSeekOpenAI</li>
<li></li>
</ul>
</div>
}
side="right"
maxWidth="350px"
/>
</div>
<Input
id="name"
value={localProvider?.name || ''}
onChange={(e) => {
setLocalProvider((prev) =>
prev ? { ...prev, name: e.target.value } : null
)
if (formErrors.name) {
setFormErrors((prev) => ({ ...prev, name: undefined }))
}
}}
placeholder="例如: DeepSeek, SiliconFlow"
className={formErrors.name ? 'border-destructive focus-visible:ring-destructive' : ''}
/>
{formErrors.name && (
<p className="text-xs text-destructive">{formErrors.name}</p>
)}
</div>
<div className="grid gap-2" data-tour="provider-url-input">
<div className="flex items-center gap-1.5">
<Label htmlFor="base_url" className={formErrors.base_url ? 'text-destructive' : ''}> URL *</Label>
<HelpTooltip
content={
<div className="space-y-2">
<p className="font-medium">API </p>
<p> API URL /v1 </p>
<ul className="list-disc list-inside space-y-1 text-xs">
<li><strong>OpenAI </strong>https://api.openai.com/v1</li>
<li><strong>DeepSeek</strong>https://api.deepseek.com</li>
<li><strong></strong>https://api.siliconflow.cn/v1</li>
<li> URL</li>
</ul>
</div>
}
side="right"
maxWidth="400px"
/>
</div>
<Input
id="base_url"
value={localProvider?.base_url || ''}
onChange={(e) => {
setLocalProvider((prev) =>
prev ? { ...prev, base_url: e.target.value } : null
)
if (formErrors.base_url) {
setFormErrors((prev) => ({ ...prev, base_url: undefined }))
}
}}
placeholder="https://api.example.com/v1"
disabled={isUsingTemplate}
className={`${isUsingTemplate ? 'bg-muted cursor-not-allowed' : ''} ${formErrors.base_url ? 'border-destructive focus-visible:ring-destructive' : ''}`}
/>
{formErrors.base_url && (
<p className="text-xs text-destructive">{formErrors.base_url}</p>
)}
{isUsingTemplate && !formErrors.base_url && (
<p className="text-xs text-muted-foreground">
使 URL ,"自定义"
</p>
)}
</div>
<div className="grid gap-2" data-tour="provider-apikey-input">
<div className="flex items-center gap-1.5">
<Label htmlFor="api_key" className={formErrors.api_key ? 'text-destructive' : ''}>API Key *</Label>
<HelpTooltip
content={
<div className="space-y-2">
<p className="font-medium">API </p>
<p></p>
<ul className="list-disc list-inside space-y-1 text-xs">
<li> <code>sk-</code> </li>
<li></li>
<li>/</li>
<li></li>
</ul>
</div>
}
side="right"
maxWidth="350px"
/>
</div>
<div className="flex gap-2">
<Input
id="api_key"
type={showApiKey ? 'text' : 'password'}
value={localProvider?.api_key || ''}
onChange={(e) => {
setLocalProvider((prev) =>
prev ? { ...prev, api_key: e.target.value } : null
)
if (formErrors.api_key) {
setFormErrors((prev) => ({ ...prev, api_key: undefined }))
}
}}
placeholder="sk-..."
className={`flex-1 ${formErrors.api_key ? 'border-destructive focus-visible:ring-destructive' : ''}`}
/>
<Button
type="button"
variant="outline"
size="icon"
onClick={() => setShowApiKey(!showApiKey)}
title={showApiKey ? '隐藏密钥' : '显示密钥'}
>
{showApiKey ? (
<EyeOff className="h-4 w-4" />
) : (
<Eye className="h-4 w-4" />
)}
</Button>
<Button
type="button"
variant="outline"
size="icon"
onClick={copyApiKey}
title="复制密钥"
>
<Copy className="h-4 w-4" />
</Button>
</div>
{formErrors.api_key && (
<p className="text-xs text-destructive">{formErrors.api_key}</p>
)}
</div>
<div className="grid gap-2">
<div className="flex items-center gap-1.5">
<Label htmlFor="client_type"></Label>
<HelpTooltip
content={
<div className="space-y-2">
<p className="font-medium">API </p>
<p>使 API </p>
<ul className="list-disc list-inside space-y-1 text-xs">
<li><strong>OpenAI</strong> OpenAI API </li>
<li><strong>Gemini</strong>Google Gemini </li>
<li> OpenAI </li>
</ul>
</div>
}
side="right"
maxWidth="350px"
/>
</div>
<Select
value={localProvider?.client_type || 'openai'}
onValueChange={(value) =>
setLocalProvider((prev) =>
prev ? { ...prev, client_type: value } : null
)
}
disabled={isUsingTemplate}
>
<SelectTrigger id="client_type" className={isUsingTemplate ? 'bg-muted cursor-not-allowed' : ''}>
<SelectValue placeholder="选择客户端类型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="openai">OpenAI</SelectItem>
<SelectItem value="gemini">Gemini</SelectItem>
</SelectContent>
</Select>
{isUsingTemplate && (
<p className="text-xs text-muted-foreground">
使,"自定义"
</p>
)}
</div>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div className="grid gap-2">
<div className="flex items-center gap-1.5">
<Label htmlFor="max_retry"></Label>
<HelpTooltip
content="API 请求失败时的最大重试次数。设置为 0 表示不重试。默认值2"
side="top"
maxWidth="250px"
/>
</div>
<Input
id="max_retry"
type="number"
min="0"
value={localProvider?.max_retry ?? ''}
onChange={(e) => {
const val = e.target.value === '' ? null : parseInt(e.target.value)
setLocalProvider((prev) =>
prev ? { ...prev, max_retry: val } : null
)
}}
placeholder="默认: 2"
/>
</div>
<div className="grid gap-2">
<div className="flex items-center gap-1.5">
<Label htmlFor="timeout">()</Label>
<HelpTooltip
content="单次 API 请求的超时时间。超时后会触发重试或报错。默认值30 秒"
side="top"
maxWidth="250px"
/>
</div>
<Input
id="timeout"
type="number"
min="1"
value={localProvider?.timeout ?? ''}
onChange={(e) => {
const val = e.target.value === '' ? null : parseInt(e.target.value)
setLocalProvider((prev) =>
prev ? { ...prev, timeout: val } : null
)
}}
placeholder="默认: 30"
/>
</div>
<div className="grid gap-2">
<div className="flex items-center gap-1.5">
<Label htmlFor="retry_interval">()</Label>
<HelpTooltip
content="两次重试之间的等待时间(秒)。适当的间隔可以避免触发 API 限流。默认值10 秒"
side="top"
maxWidth="250px"
/>
</div>
<Input
id="retry_interval"
type="number"
min="1"
value={localProvider?.retry_interval ?? ''}
onChange={(e) => {
const val = e.target.value === '' ? null : parseInt(e.target.value)
setLocalProvider((prev) =>
prev
? { ...prev, retry_interval: val }
: null
)
}}
placeholder="默认: 10"
/>
</div>
</div>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => onOpenChange(false)} data-tour="provider-cancel-button">
</Button>
<Button type="submit" data-tour="provider-save-button"></Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
)
}