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:
182
frontend/src/components/upload/AccountTable.tsx
Normal file
182
frontend/src/components/upload/AccountTable.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user