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,353 @@
import { useCallback, useMemo, useState } from 'react'
import type { TestConnectionResult } from '@/lib/config-api'
import { AlertCircle, CheckCircle2, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Loader2, Pencil, Search, Trash2, XCircle, Zap } from 'lucide-react'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { ProviderCard } from './ProviderCard'
import type { APIProvider } from './types'
interface ProviderListProps {
providers: APIProvider[]
testingProviders: Set<string>
testResults: Map<string, TestConnectionResult>
selectedProviders: Set<number>
onEdit: (provider: APIProvider, index: number) => void
onDelete: (index: number) => void
onTest: (name: string) => void
onToggleSelect: (index: number) => void
onToggleSelectAll: () => void
}
export function ProviderList({
providers,
testingProviders,
testResults,
selectedProviders,
onEdit,
onDelete,
onTest,
onToggleSelect,
onToggleSelectAll,
}: ProviderListProps) {
const [searchQuery, setSearchQuery] = useState('')
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(20)
const [jumpToPage, setJumpToPage] = useState('')
const filteredProviders = useMemo(() => {
if (!searchQuery) return providers
const query = searchQuery.toLowerCase()
return providers.filter((provider) => (
provider.name.toLowerCase().includes(query) ||
provider.base_url.toLowerCase().includes(query) ||
provider.client_type.toLowerCase().includes(query)
))
}, [providers, searchQuery])
const { totalPages, paginatedProviders } = useMemo(() => {
const total = Math.ceil(filteredProviders.length / pageSize)
const paginated = filteredProviders.slice(
(page - 1) * pageSize,
page * pageSize
)
return { totalPages: total, paginatedProviders: paginated }
}, [filteredProviders, page, pageSize])
const handleJumpToPage = useCallback(() => {
const targetPage = parseInt(jumpToPage)
if (targetPage >= 1 && targetPage <= totalPages) {
setPage(targetPage)
setJumpToPage('')
}
}, [jumpToPage, totalPages])
const renderTestStatus = (providerName: string) => {
const isTesting = testingProviders.has(providerName)
const result = testResults.get(providerName)
if (isTesting) {
return (
<Badge variant="secondary" className="gap-1">
<Loader2 className="h-3 w-3 animate-spin" />
</Badge>
)
}
if (!result) {
return (
<Badge variant="outline" className="text-muted-foreground">
</Badge>
)
}
if (result.network_ok) {
if (result.api_key_valid === true) {
return (
<Badge className="gap-1 bg-green-600 hover:bg-green-700">
<CheckCircle2 className="h-3 w-3" />
</Badge>
)
} else if (result.api_key_valid === false) {
return (
<Badge variant="destructive" className="gap-1">
<AlertCircle className="h-3 w-3" />
Key无效
</Badge>
)
} else {
return (
<Badge className="gap-1 bg-blue-600 hover:bg-blue-700">
<CheckCircle2 className="h-3 w-3" />
访
</Badge>
)
}
} else {
return (
<Badge variant="destructive" className="gap-1">
<XCircle className="h-3 w-3" />
线
</Badge>
)
}
}
return (
<>
{/* 搜索框 */}
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-2 mb-4">
<div className="relative w-full sm:flex-1 sm:max-w-sm">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="搜索提供商名称、URL 或类型..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9"
/>
</div>
{searchQuery && (
<p className="text-sm text-muted-foreground whitespace-nowrap">
{filteredProviders.length}
</p>
)}
</div>
{/* 移动端卡片视图 */}
<div className="md:hidden space-y-3">
{filteredProviders.length === 0 ? (
<div className="text-center text-muted-foreground py-8 rounded-lg border bg-card">
{searchQuery ? '未找到匹配的提供商' : '暂无提供商配置,点击"添加提供商"开始配置'}
</div>
) : (
paginatedProviders.map((provider, displayIndex) => {
const actualIndex = providers.findIndex(p => p === provider)
return (
<ProviderCard
key={displayIndex}
provider={provider}
actualIndex={actualIndex}
testingProviders={testingProviders}
testResults={testResults}
onEdit={onEdit}
onDelete={onDelete}
onTest={onTest}
/>
)
})
)}
</div>
{/* 桌面端表格视图 */}
<div className="hidden md:block rounded-lg border bg-card overflow-hidden">
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-12">
<Checkbox
checked={selectedProviders.size === filteredProviders.length && filteredProviders.length > 0}
onCheckedChange={onToggleSelectAll}
/>
</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead>URL</TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
<TableHead className="text-right">()</TableHead>
<TableHead className="text-right">()</TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{paginatedProviders.length === 0 ? (
<TableRow>
<TableCell colSpan={9} className="text-center text-muted-foreground py-8">
{searchQuery ? '未找到匹配的提供商' : '暂无提供商配置,点击"添加提供商"开始配置'}
</TableCell>
</TableRow>
) : (
paginatedProviders.map((provider, displayIndex) => {
const actualIndex = providers.findIndex(p => p === provider)
return (
<TableRow key={displayIndex}>
<TableCell>
<Checkbox
checked={selectedProviders.has(actualIndex)}
onCheckedChange={() => onToggleSelect(actualIndex)}
/>
</TableCell>
<TableCell>
{renderTestStatus(provider.name)}
</TableCell>
<TableCell className="font-medium">{provider.name}</TableCell>
<TableCell className="max-w-xs truncate" title={provider.base_url}>
{provider.base_url}
</TableCell>
<TableCell>{provider.client_type}</TableCell>
<TableCell className="text-right">{provider.max_retry}</TableCell>
<TableCell className="text-right">{provider.timeout}</TableCell>
<TableCell className="text-right">{provider.retry_interval}</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-2">
<Button
variant="outline"
size="sm"
onClick={() => onTest(provider.name)}
disabled={testingProviders.has(provider.name)}
title="测试连接"
>
{testingProviders.has(provider.name) ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Zap className="h-4 w-4" />
)}
</Button>
<Button
variant="default"
size="sm"
onClick={() => onEdit(provider, actualIndex)}
>
<Pencil className="h-4 w-4 mr-1" strokeWidth={2} fill="none" />
</Button>
<Button
size="sm"
onClick={() => onDelete(actualIndex)}
className="bg-red-600 hover:bg-red-700 text-white"
>
<Trash2 className="h-4 w-4 mr-1" strokeWidth={2} fill="none" />
</Button>
</div>
</TableCell>
</TableRow>
)
})
)}
</TableBody>
</Table>
</div>
</div>
{/* 分页 */}
{filteredProviders.length > 0 && (
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 mt-4">
<div className="flex items-center gap-2">
<Label htmlFor="page-size-provider" className="text-sm whitespace-nowrap"></Label>
<Select
value={pageSize.toString()}
onValueChange={(value) => {
setPageSize(parseInt(value))
setPage(1)
}}
>
<SelectTrigger id="page-size-provider" className="w-20">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="10">10</SelectItem>
<SelectItem value="20">20</SelectItem>
<SelectItem value="50">50</SelectItem>
<SelectItem value="100">100</SelectItem>
</SelectContent>
</Select>
<span className="text-sm text-muted-foreground">
{(page - 1) * pageSize + 1} {' '}
{Math.min(page * pageSize, filteredProviders.length)} {filteredProviders.length}
</span>
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setPage(1)}
disabled={page === 1}
className="hidden sm:flex"
>
<ChevronsLeft className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setPage((p) => Math.max(1, p - 1))}
disabled={page === 1}
>
<ChevronLeft className="h-4 w-4 sm:mr-1" />
<span className="hidden sm:inline"></span>
</Button>
<div className="flex items-center gap-2">
<Input
type="number"
value={jumpToPage}
onChange={(e) => setJumpToPage(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleJumpToPage()}
placeholder={page.toString()}
className="w-16 h-8 text-center"
min={1}
max={totalPages}
/>
<Button
variant="outline"
size="sm"
onClick={handleJumpToPage}
disabled={!jumpToPage}
className="h-8"
>
</Button>
</div>
<Button
variant="outline"
size="sm"
onClick={() => setPage((p) => p + 1)}
disabled={page >= totalPages}
>
<span className="hidden sm:inline"></span>
<ChevronRight className="h-4 w-4 sm:ml-1" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setPage(totalPages)}
disabled={page >= totalPages}
className="hidden sm:flex"
>
<ChevronsRight className="h-4 w-4" />
</Button>
</div>
</div>
)}
</>
)
}