Files
codexautopool/frontend/src/pages/Accounts.tsx

264 lines
9.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
)
}