Files
accounts-manager-web/components/accounts/account-table.tsx
Your Name 0b95ca36f1 Initial project setup with Next.js accounts manager
- Set up Next.js project structure
- Added UI components and styling
- Configured package dependencies
- Added feature documentation

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-23 01:40:14 +08:00

241 lines
9.7 KiB
TypeScript

"use client";
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { ChevronLeft, ChevronRight, Eye, Edit, Trash2, RefreshCw } from 'lucide-react';
import { Account } from '@/lib/types';
interface AccountTableProps {
accounts: Account[];
loading: boolean;
selectedIds: number[];
pagination: {
page: number;
pageSize: number;
total: number;
totalPages: number;
};
onSelectAll: (checked: boolean) => void;
onSelectOne: (id: number, checked: boolean) => void;
onPageChange: (page: number) => void;
onPageSizeChange: (pageSize: number) => void;
onRefresh: () => void;
onView?: (account: Account) => void;
onEdit?: (account: Account) => void;
onDelete?: (account: Account) => void;
}
export function AccountTable({
accounts,
loading,
selectedIds,
pagination,
onSelectAll,
onSelectOne,
onPageChange,
onPageSizeChange,
onRefresh,
onView,
onEdit,
onDelete
}: AccountTableProps) {
const getStatusBadge = (status: string) => {
switch (status) {
case 'available':
return <Badge variant="default" className="bg-green-500"></Badge>;
case 'locked':
return <Badge variant="secondary"></Badge>;
case 'banned':
return <Badge variant="destructive"></Badge>;
default:
return <Badge variant="outline">{status}</Badge>;
}
};
const renderPagination = () => {
return (
<div className="flex items-center justify-between mb-4">
<div className="flex items-center space-x-4">
<div className="text-sm text-muted-foreground">
{(pagination.page - 1) * pagination.pageSize + 1} - {Math.min(pagination.page * pagination.pageSize, pagination.total)} {pagination.total}
</div>
<div className="flex items-center space-x-2">
<span className="text-sm text-muted-foreground"></span>
<Select value={pagination.pageSize.toString()} onValueChange={(value) => onPageSizeChange(Number(value))}>
<SelectTrigger className="w-26">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="20">20</SelectItem>
<SelectItem value="100">100</SelectItem>
<SelectItem value="1000">1000</SelectItem>
<SelectItem value="100000">100000</SelectItem>
</SelectContent>
</Select>
<span className="text-sm text-muted-foreground"></span>
</div>
</div>
{pagination.totalPages > 1 && (
<div className="flex items-center space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => onPageChange(pagination.page - 1)}
disabled={pagination.page <= 1}
>
<ChevronLeft className="h-4 w-4" />
</Button>
<div className="flex items-center space-x-1">
{Array.from({ length: Math.min(5, pagination.totalPages) }, (_, i) => {
let pageNum;
if (pagination.totalPages <= 5) {
pageNum = i + 1;
} else if (pagination.page <= 3) {
pageNum = i + 1;
} else if (pagination.page >= pagination.totalPages - 2) {
pageNum = pagination.totalPages - 4 + i;
} else {
pageNum = pagination.page - 2 + i;
}
return (
<Button
key={pageNum}
variant={pageNum === pagination.page ? "default" : "outline"}
size="sm"
onClick={() => onPageChange(pageNum)}
>
{pageNum}
</Button>
);
})}
</div>
<Button
variant="outline"
size="sm"
onClick={() => onPageChange(pagination.page + 1)}
disabled={pagination.page >= pagination.totalPages}
>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
)}
</div>
);
};
return (
<div className="flex flex-col">
{/* 分页器 - 移到顶部 */}
{renderPagination()}
{/* 表格区域 - 直接使用原生table确保sticky正常工作 */}
<div className="border rounded-lg max-h-[600px] overflow-auto">
<table className="w-full caption-bottom text-sm">
<thead className="sticky top-0 bg-background z-20 border-b shadow-sm">
<tr className="hover:bg-muted/50 border-b transition-colors">
<th className="text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap w-12">
<Checkbox
checked={selectedIds.length === accounts.length && accounts.length > 0}
onCheckedChange={onSelectAll}
/>
</th>
<th className="text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap">ID</th>
<th className="text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap"></th>
<th className="text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap">ID</th>
<th className="text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap"></th>
<th className="text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap"></th>
<th className="text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap"></th>
<th className="text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap"></th>
<th className="text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap w-[120px]">
<div className="flex items-center justify-between">
<span></span>
<Button
variant="ghost"
size="sm"
onClick={onRefresh}
disabled={loading}
className="h-6 w-6 p-0"
>
<RefreshCw className={`h-3 w-3 ${loading ? 'animate-spin' : ''}`} />
</Button>
</div>
</th>
</tr>
</thead>
<tbody>
{loading ? (
<tr className="hover:bg-muted/50 border-b transition-colors">
<td colSpan={9} className="p-2 align-middle text-center py-8">
...
</td>
</tr>
) : accounts.length === 0 ? (
<tr className="hover:bg-muted/50 border-b transition-colors">
<td colSpan={9} className="p-2 align-middle text-center py-8">
</td>
</tr>
) : (
accounts.map((account) => (
<tr key={account.id} className="hover:bg-muted/50 border-b transition-colors">
<td className="p-2 align-middle w-12">
<Checkbox
checked={selectedIds.includes(account.id)}
onCheckedChange={(checked) => onSelectOne(account.id, checked as boolean)}
/>
</td>
<td className="p-2 align-middle font-mono">{account.id}</td>
<td className="p-2 align-middle">{account.platform}</td>
<td className="p-2 align-middle font-mono max-w-[150px] truncate">{account.customId}</td>
<td className="p-2 align-middle">{account.ownerId}</td>
<td className="p-2 align-middle">{getStatusBadge(account.status)}</td>
<td className="p-2 align-middle max-w-[200px] truncate">{account.notes || '-'}</td>
<td className="p-2 align-middle text-sm text-muted-foreground">
{new Date(account.createdAt).toLocaleDateString('zh-CN')}
</td>
<td className="p-2 align-middle w-[120px]">
<div className="flex items-center space-x-1">
{onView && (
<Button
variant="ghost"
size="sm"
onClick={() => onView(account)}
>
<Eye className="h-3 w-3" />
</Button>
)}
{onEdit && (
<Button
variant="ghost"
size="sm"
onClick={() => onEdit(account)}
>
<Edit className="h-3 w-3" />
</Button>
)}
{onDelete && (
<Button
variant="ghost"
size="sm"
onClick={() => onDelete(account)}
>
<Trash2 className="h-3 w-3" />
</Button>
)}
</div>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
);
}