'use client'; import { useEffect, useState } from 'react'; import { Loader2, Unlock, Lock, Ban, Pencil, Trash2 } from 'lucide-react'; export interface ProxyItem { id: number; pool_name: string; proto: string; ip: string; port: number; username?: string | null; password?: string | null; next_use_time?: string | null; status?: string | null; created_at?: string; updated_at?: string; } interface ProxyTableProps { data: ProxyItem[]; loading: boolean; selectedIds: number[]; onToggleSelect: (proxyId: number) => void; onToggleSelectAll: () => void; onEdit: (proxy: ProxyItem) => void; onDelete: (proxy: ProxyItem) => void; } function parseUtcDateTime(value?: string | null) { if (!value) return null; const normalized = value.trim().replace(' ', 'T'); const hasTimezone = /[zZ]|[+-]\d{2}:\d{2}$/.test(normalized); const utcValue = hasTimezone ? normalized : `${normalized}Z`; const timestamp = new Date(utcValue).getTime(); return Number.isNaN(timestamp) ? null : timestamp; } export default function ProxyTable({ data, loading, selectedIds, onToggleSelect, onToggleSelectAll, onEdit, onDelete }: ProxyTableProps) { const [now, setNow] = useState(() => Date.now()); useEffect(() => { const timer = window.setInterval(() => { setNow(Date.now()); }, 1000); return () => window.clearInterval(timer); }, []); const getRemainingTime = (nextUseTime?: string | null): string | null => { if (!nextUseTime) return null; const untilMs = parseUtcDateTime(nextUseTime); if (untilMs === null || untilMs <= now) return null; const diffSeconds = Math.floor((untilMs - now) / 1000); const days = Math.floor(diffSeconds / (3600 * 24)); const hours = Math.floor((diffSeconds % (3600 * 24)) / 3600); const minutes = Math.floor((diffSeconds % 3600) / 60); const seconds = diffSeconds % 60; if (days > 0) return `${days}d ${hours}h`; if (hours > 0) return `${hours}h ${minutes}m`; if (minutes > 0) return `${minutes}m ${seconds}s`; return `${seconds}s`; }; const renderStatusBadge = (isDisabled: boolean, isLocked: boolean) => { if (isDisabled) { return ( Disabled ); } if (isLocked) { return ( Locked ); } return ( Active ); }; if (loading) { return (
加载数据中...
); } if (data.length === 0) { return (
暂无数据
); } const allSelected = data.length > 0 && data.every((item) => selectedIds.includes(item.id)); return (
{data.map((item) => { const remainingTime = getRemainingTime(item.next_use_time); const isLocked = remainingTime !== null; const isDisabled = item.status?.toLowerCase() === 'disable'; return ( ); })}
ID 代理池 协议 地址 鉴权 状态 剩余时间 操作
onToggleSelect(item.id)} className="h-4 w-4 rounded border-slate-300 text-blue-600 focus:ring-blue-500" aria-label={`选择代理 ${item.ip}:${item.port}`} /> #{item.id} {item.pool_name} {item.proto} {item.ip}:{item.port} {item.username ? ( {item.username}{item.password ? ' / ******' : ''} ) : ( N/A )} {renderStatusBadge(isDisabled, isLocked)} {isLocked ? (
{remainingTime}
) : ( - )}
{data.map((item) => { const remainingTime = getRemainingTime(item.next_use_time); const isLocked = remainingTime !== null; const isDisabled = item.status?.toLowerCase() === 'disable'; return (
onToggleSelect(item.id)} className="mt-1 h-4 w-4 rounded border-slate-300 text-blue-600 focus:ring-blue-500" aria-label={`选择代理 ${item.ip}:${item.port}`} />
#{item.id}
{item.pool_name}
{renderStatusBadge(isDisabled, isLocked)}
协议 {item.proto}
地址 {item.ip}:{item.port}
鉴权 {item.username ? ( {item.username}{item.password ? ' / ******' : ''} ) : ( N/A )}
剩余时间 {isLocked ? (
{remainingTime}
) : ( - )}
); })}
); }