feat: Implement initial full-stack application structure including frontend pages, components, hooks, API integration, and backend services for account pooling and management.
This commit is contained in:
263
frontend/src/pages/Accounts.tsx
Normal file
263
frontend/src/pages/Accounts.tsx
Normal file
@@ -0,0 +1,263 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { RefreshCw, Search, Settings, ChevronLeft, ChevronRight } from 'lucide-react'
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardContent,
|
||||
Button,
|
||||
Input,
|
||||
Select,
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableRow,
|
||||
TableHead,
|
||||
TableCell,
|
||||
StatusBadge,
|
||||
} from '../components/common'
|
||||
import { useS2AApi } from '../hooks/useS2AApi'
|
||||
import { useConfig } from '../hooks/useConfig'
|
||||
import type { S2AAccount, AccountListParams } from '../types'
|
||||
import { formatDateTime } from '../utils/format'
|
||||
|
||||
export default function Accounts() {
|
||||
const { config } = useConfig()
|
||||
const { getAccounts, loading, error } = useS2AApi()
|
||||
|
||||
const [accounts, setAccounts] = useState<S2AAccount[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
const [page, setPage] = useState(1)
|
||||
const [pageSize] = useState(20)
|
||||
const [search, setSearch] = useState('')
|
||||
const [statusFilter, setStatusFilter] = useState<string>('')
|
||||
const [refreshing, setRefreshing] = useState(false)
|
||||
|
||||
const hasConfig = config.s2a.apiBase && config.s2a.adminKey
|
||||
|
||||
const fetchAccounts = useCallback(async () => {
|
||||
if (!hasConfig) return
|
||||
|
||||
setRefreshing(true)
|
||||
const params: AccountListParams = {
|
||||
page,
|
||||
page_size: pageSize,
|
||||
platform: 'openai',
|
||||
}
|
||||
|
||||
if (search) params.search = search
|
||||
if (statusFilter) params.status = statusFilter as 'active' | 'inactive' | 'error'
|
||||
|
||||
const result = await getAccounts(params)
|
||||
if (result) {
|
||||
setAccounts(result.data)
|
||||
setTotal(result.total)
|
||||
}
|
||||
setRefreshing(false)
|
||||
}, [hasConfig, page, pageSize, search, statusFilter, getAccounts])
|
||||
|
||||
useEffect(() => {
|
||||
fetchAccounts()
|
||||
}, [fetchAccounts])
|
||||
|
||||
const handleSearch = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setPage(1)
|
||||
fetchAccounts()
|
||||
}
|
||||
|
||||
const totalPages = Math.ceil(total / pageSize)
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-900 dark:text-slate-100">号池账号</h1>
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400">查看 S2A 号池中的账号列表</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={fetchAccounts}
|
||||
disabled={!hasConfig || refreshing}
|
||||
icon={<RefreshCw className={`h-4 w-4 ${refreshing ? 'animate-spin' : ''}`} />}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Connection warning */}
|
||||
{!hasConfig && (
|
||||
<div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-xl p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<Settings className="h-5 w-5 text-yellow-600 dark:text-yellow-400 mt-0.5" />
|
||||
<div>
|
||||
<p className="font-medium text-yellow-800 dark:text-yellow-200">请先配置 S2A 连接</p>
|
||||
<p className="mt-1 text-sm text-yellow-700 dark:text-yellow-300">
|
||||
查看账号列表需要配置 S2A API 连接信息
|
||||
</p>
|
||||
<Link to="/config" className="mt-3 inline-block">
|
||||
<Button size="sm" variant="outline">
|
||||
前往配置
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Filters */}
|
||||
{hasConfig && (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<form onSubmit={handleSearch} className="flex flex-wrap items-end gap-4">
|
||||
<div className="flex-1 min-w-[200px]">
|
||||
<Input
|
||||
placeholder="搜索账号名称..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-40">
|
||||
<Select
|
||||
value={statusFilter}
|
||||
onChange={(e) => {
|
||||
setStatusFilter(e.target.value)
|
||||
setPage(1)
|
||||
}}
|
||||
options={[
|
||||
{ value: '', label: '全部状态' },
|
||||
{ value: 'active', label: '正常' },
|
||||
{ value: 'inactive', label: '停用' },
|
||||
{ value: 'error', label: '错误' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit" icon={<Search className="h-4 w-4" />}>
|
||||
搜索
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Error */}
|
||||
{error && (
|
||||
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-xl p-4">
|
||||
<p className="text-red-700 dark:text-red-400">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Account List */}
|
||||
{hasConfig && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>账号列表</CardTitle>
|
||||
<span className="text-sm text-slate-500 dark:text-slate-400">共 {total} 个账号</span>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{loading && accounts.length === 0 ? (
|
||||
<div className="space-y-3">
|
||||
{[1, 2, 3, 4, 5].map((i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="h-16 bg-slate-100 dark:bg-slate-700 rounded-lg animate-pulse"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : accounts.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-slate-500 dark:text-slate-400">暂无账号</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="border border-slate-200 dark:border-slate-700 rounded-lg overflow-hidden">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow hoverable={false}>
|
||||
<TableHead>ID</TableHead>
|
||||
<TableHead>名称</TableHead>
|
||||
<TableHead>类型</TableHead>
|
||||
<TableHead>状态</TableHead>
|
||||
<TableHead className="text-right">并发</TableHead>
|
||||
<TableHead className="text-right">优先级</TableHead>
|
||||
<TableHead>创建时间</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{accounts.map((account) => (
|
||||
<TableRow key={account.id}>
|
||||
<TableCell>
|
||||
<span className="font-mono text-sm">{account.id}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="font-medium">{account.name}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-sm text-slate-500 dark:text-slate-400">
|
||||
{account.type}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<StatusBadge status={account.status} />
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{account.current_concurrency !== undefined ? (
|
||||
<span>
|
||||
{account.current_concurrency}/{account.concurrency}
|
||||
</span>
|
||||
) : (
|
||||
account.concurrency
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">{account.priority}</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-sm text-slate-500 dark:text-slate-400">
|
||||
{formatDateTime(account.created_at)}
|
||||
</span>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
{totalPages > 1 && (
|
||||
<div className="flex items-center justify-between mt-4">
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400">
|
||||
第 {page} 页,共 {totalPages} 页
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
||||
disabled={page === 1}
|
||||
icon={<ChevronLeft className="h-4 w-4" />}
|
||||
>
|
||||
上一页
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
|
||||
disabled={page === totalPages}
|
||||
icon={<ChevronRight className="h-4 w-4" />}
|
||||
>
|
||||
下一页
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user