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>
This commit is contained in:
241
components/accounts/account-table.tsx
Normal file
241
components/accounts/account-table.tsx
Normal file
@@ -0,0 +1,241 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user