 0b95ca36f1
			
		
	
	0b95ca36f1
	
	
	
		
			
			- 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>
		
			
				
	
	
		
			241 lines
		
	
	
		
			9.7 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			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>
 | |
|   );
 | |
| } |