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:
2026-01-30 07:40:35 +08:00
commit f4448bbef2
106 changed files with 19282 additions and 0 deletions

View File

@@ -0,0 +1,182 @@
import { useState, useMemo } from 'react'
import { CheckSquare, Square, MinusSquare } from 'lucide-react'
import type { CheckedAccount } from '../../types'
import {
Table,
TableHeader,
TableBody,
TableRow,
TableHead,
TableCell,
StatusBadge,
Button,
} from '../common'
import { maskEmail, maskToken } from '../../utils/format'
interface AccountTableProps {
accounts: CheckedAccount[]
selectedIds: number[]
onSelectionChange: (ids: number[]) => void
disabled?: boolean
}
export default function AccountTable({
accounts,
selectedIds,
onSelectionChange,
disabled = false,
}: AccountTableProps) {
const [showTokens, setShowTokens] = useState(false)
const activeAccounts = useMemo(
() => accounts.filter((acc) => acc.status === 'active'),
[accounts]
)
const allSelected = selectedIds.length === accounts.length && accounts.length > 0
const someSelected = selectedIds.length > 0 && selectedIds.length < accounts.length
const activeSelected = activeAccounts.every((acc) => selectedIds.includes(acc.id))
const handleSelectAll = () => {
if (allSelected) {
onSelectionChange([])
} else {
onSelectionChange(accounts.map((acc) => acc.id))
}
}
const handleSelectActive = () => {
onSelectionChange(activeAccounts.map((acc) => acc.id))
}
const handleSelectNone = () => {
onSelectionChange([])
}
const handleToggle = (id: number) => {
if (selectedIds.includes(id)) {
onSelectionChange(selectedIds.filter((i) => i !== id))
} else {
onSelectionChange([...selectedIds, id])
}
}
if (accounts.length === 0) {
return null
}
return (
<div className="space-y-3">
{/* Selection controls */}
<div className="flex flex-wrap items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={handleSelectActive}
disabled={disabled || activeAccounts.length === 0}
>
({activeAccounts.length})
</Button>
<Button variant="outline" size="sm" onClick={handleSelectAll} disabled={disabled}>
({accounts.length})
</Button>
<Button
variant="outline"
size="sm"
onClick={handleSelectNone}
disabled={disabled || selectedIds.length === 0}
>
</Button>
<div className="flex-1" />
<Button variant="ghost" size="sm" onClick={() => setShowTokens(!showTokens)}>
{showTokens ? '隐藏 Token' : '显示 Token'}
</Button>
</div>
{/* Selection summary */}
<div className="text-sm text-slate-600 dark:text-slate-400">
{' '}
<span className="font-medium text-slate-900 dark:text-slate-100">{selectedIds.length}</span>{' '}
{activeSelected && activeAccounts.length > 0 && (
<span className="ml-2 text-green-600 dark:text-green-400">
( {activeAccounts.length} )
</span>
)}
</div>
{/* Table */}
<div className="border border-slate-200 dark:border-slate-700 rounded-lg overflow-hidden">
<Table>
<TableHeader>
<TableRow hoverable={false}>
<TableHead className="w-12">
<button
onClick={handleSelectAll}
disabled={disabled}
className="p-1 hover:bg-slate-200 dark:hover:bg-slate-600 rounded disabled:opacity-50"
>
{allSelected ? (
<CheckSquare className="h-4 w-4 text-blue-600 dark:text-blue-400" />
) : someSelected ? (
<MinusSquare className="h-4 w-4 text-blue-600 dark:text-blue-400" />
) : (
<Square className="h-4 w-4 text-slate-400" />
)}
</button>
</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead>Account ID</TableHead>
<TableHead>Plan Type</TableHead>
{showTokens && <TableHead>Token</TableHead>}
</TableRow>
</TableHeader>
<TableBody>
{accounts.map((account) => (
<TableRow key={account.id}>
<TableCell>
<button
onClick={() => handleToggle(account.id)}
disabled={disabled}
className="p-1 hover:bg-slate-100 dark:hover:bg-slate-700 rounded disabled:opacity-50"
>
{selectedIds.includes(account.id) ? (
<CheckSquare className="h-4 w-4 text-blue-600 dark:text-blue-400" />
) : (
<Square className="h-4 w-4 text-slate-400" />
)}
</button>
</TableCell>
<TableCell>
<span className="font-mono text-sm">{maskEmail(account.account)}</span>
</TableCell>
<TableCell>
<StatusBadge status={account.status} />
</TableCell>
<TableCell>
<span className="font-mono text-xs text-slate-500 dark:text-slate-400">
{account.accountId || '-'}
</span>
</TableCell>
<TableCell>
<span className="text-sm text-slate-600 dark:text-slate-400">
{account.planType || '-'}
</span>
</TableCell>
{showTokens && (
<TableCell>
<span className="font-mono text-xs text-slate-500 dark:text-slate-400">
{maskToken(account.token, 12)}
</span>
</TableCell>
)}
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
)
}