Files
accounts-manager-web/components/accounts/account-table.tsx
cloud370 43a828a2c6 feat: 增强表格交互体验和数据展示
- 新增每页数量localStorage缓存,刷新后保持用户选择
- 增加表格显示高度从600px到800px,显示更多数据
- 支持点击表格行进行选择,提升操作便利性
- 格式化创建时间显示为详细时间(年/月/日 时:分)
- 优化复选框和操作按钮的点击事件冲突

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-24 05:36:37 +08:00

238 lines
9.8 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 } 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;
onView?: (account: Account) => void;
onEdit?: (account: Account) => void;
onDelete?: (account: Account) => void;
}
export function AccountTable({
accounts,
loading,
selectedIds,
pagination,
onSelectAll,
onSelectOne,
onPageChange,
onPageSizeChange,
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>;
case 'exported':
return <Badge variant="outline" className="bg-blue-50 text-blue-700 border-blue-200"></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="10000">10000</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()}
{/* 表格区域 - 增加高度以显示更多数据 */}
<div className="border rounded-lg max-h-[800px] 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]"></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 cursor-pointer"
onClick={() => onSelectOne(account.id, !selectedIds.includes(account.id))}
>
<td className="p-2 align-middle w-12" onClick={(e) => e.stopPropagation()}>
<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).toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
})}
</td>
<td className="p-2 align-middle w-[120px]" onClick={(e) => e.stopPropagation()}>
<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>
);
}