| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287 |
- '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 (
- <span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-bold bg-red-100 text-red-700 border border-red-200">
- <Ban size={12} /> Disabled
- </span>
- );
- }
- if (isLocked) {
- return (
- <span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-bold bg-orange-100 text-orange-700 border border-orange-200">
- <Lock size={12} /> Locked
- </span>
- );
- }
- return (
- <span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-bold bg-green-100 text-green-700 border border-green-200">
- <Unlock size={12} /> Active
- </span>
- );
- };
- if (loading) {
- return (
- <div className="bg-white border border-slate-200 rounded-xl shadow-sm p-20 flex justify-center items-center">
- <div className="flex flex-col items-center text-slate-500">
- <Loader2 className="w-8 h-8 animate-spin mb-2 text-blue-600" />
- <span className="text-sm">加载数据中...</span>
- </div>
- </div>
- );
- }
- if (data.length === 0) {
- return (
- <div className="bg-white border border-slate-200 rounded-xl shadow-sm p-20 flex justify-center items-center">
- <span className="text-slate-500 text-sm">暂无数据</span>
- </div>
- );
- }
- const allSelected = data.length > 0 && data.every((item) => selectedIds.includes(item.id));
- return (
- <div className="space-y-4">
- <div className="hidden md:block bg-white border border-slate-200 rounded-xl shadow-sm overflow-hidden">
- <div className="overflow-x-auto w-full">
- <table className="w-full text-left text-sm whitespace-nowrap">
- <thead className="bg-slate-50 border-b border-slate-200">
- <tr>
- <th className="px-4 py-4 font-semibold text-slate-700">
- <input
- type="checkbox"
- checked={allSelected}
- onChange={onToggleSelectAll}
- className="h-4 w-4 rounded border-slate-300 text-blue-600 focus:ring-blue-500"
- aria-label="全选代理"
- />
- </th>
- <th className="px-6 py-4 font-semibold text-slate-700">ID</th>
- <th className="px-6 py-4 font-semibold text-slate-700">代理池</th>
- <th className="px-6 py-4 font-semibold text-slate-700">协议</th>
- <th className="px-6 py-4 font-semibold text-slate-700">地址</th>
- <th className="px-6 py-4 font-semibold text-slate-700">鉴权</th>
- <th className="px-6 py-4 font-semibold text-slate-700">状态</th>
- <th className="px-6 py-4 font-semibold text-slate-700">剩余时间</th>
- <th className="px-6 py-4 font-semibold text-slate-700 text-right">操作</th>
- </tr>
- </thead>
- <tbody className="divide-y divide-slate-100">
- {data.map((item) => {
- const remainingTime = getRemainingTime(item.next_use_time);
- const isLocked = remainingTime !== null;
- const isDisabled = item.status?.toLowerCase() === 'disable';
- return (
- <tr key={item.id} className="hover:bg-slate-50/50 transition-colors">
- <td className="px-4 py-4">
- <input
- type="checkbox"
- checked={selectedIds.includes(item.id)}
- onChange={() => 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}`}
- />
- </td>
- <td className="px-6 py-4 text-slate-500 font-mono">#{item.id}</td>
- <td className="px-6 py-4 font-medium text-slate-900">
- <span className="bg-slate-100 px-2 py-1 rounded text-xs border border-slate-200">
- {item.pool_name}
- </span>
- </td>
- <td className="px-6 py-4 uppercase font-medium text-slate-700">{item.proto}</td>
- <td className="px-6 py-4 font-mono text-slate-700">{item.ip}:{item.port}</td>
- <td className="px-6 py-4 text-slate-600">
- {item.username ? (
- <span className="font-mono text-xs">{item.username}{item.password ? ' / ******' : ''}</span>
- ) : (
- <span className="text-slate-300 italic">N/A</span>
- )}
- </td>
- <td className="px-6 py-4">{renderStatusBadge(isDisabled, isLocked)}</td>
- <td className="px-6 py-4">
- {isLocked ? (
- <div className="inline-flex items-center gap-1.5 text-orange-600 font-mono font-bold bg-orange-50 px-2 py-1 rounded border border-orange-100 text-xs">
- <Lock size={12} />
- {remainingTime}
- </div>
- ) : (
- <span className="text-slate-300">-</span>
- )}
- </td>
- <td className="px-6 py-4 text-right">
- <div className="flex items-center justify-end gap-2">
- <button
- onClick={() => onEdit(item)}
- className="p-2 text-slate-500 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition"
- title="编辑代理"
- >
- <Pencil size={16} />
- </button>
- <button
- onClick={() => onDelete(item)}
- className="p-2 text-slate-500 hover:text-red-700 hover:bg-red-50 rounded-lg transition"
- title="删除代理"
- >
- <Trash2 size={16} />
- </button>
- </div>
- </td>
- </tr>
- );
- })}
- </tbody>
- </table>
- </div>
- </div>
- <div className="md:hidden space-y-4">
- {data.map((item) => {
- const remainingTime = getRemainingTime(item.next_use_time);
- const isLocked = remainingTime !== null;
- const isDisabled = item.status?.toLowerCase() === 'disable';
- return (
- <div key={item.id} className="bg-white p-4 rounded-xl shadow-sm border border-slate-200">
- <div className="flex items-start justify-between gap-3 mb-4">
- <div className="flex items-start gap-3">
- <input
- type="checkbox"
- checked={selectedIds.includes(item.id)}
- onChange={() => 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}`}
- />
- <div>
- <div className="text-xs font-mono text-slate-400">#{item.id}</div>
- <div className="mt-1 inline-flex items-center bg-slate-100 px-2 py-1 rounded text-xs border border-slate-200 text-slate-700">
- {item.pool_name}
- </div>
- </div>
- </div>
- <div>{renderStatusBadge(isDisabled, isLocked)}</div>
- </div>
- <div className="space-y-3 bg-slate-50 p-3 rounded-lg border border-slate-100 mb-4">
- <div className="flex items-center justify-between gap-3">
- <span className="text-xs text-slate-500">协议</span>
- <span className="text-sm font-medium uppercase text-slate-800">{item.proto}</span>
- </div>
- <div className="flex items-center justify-between gap-3">
- <span className="text-xs text-slate-500">地址</span>
- <span className="text-sm font-mono text-slate-800 text-right break-all">{item.ip}:{item.port}</span>
- </div>
- <div className="flex items-center justify-between gap-3">
- <span className="text-xs text-slate-500">鉴权</span>
- {item.username ? (
- <span className="text-sm font-mono text-slate-800 text-right break-all">
- {item.username}{item.password ? ' / ******' : ''}
- </span>
- ) : (
- <span className="text-slate-300 italic text-sm">N/A</span>
- )}
- </div>
- <div className="flex items-center justify-between gap-3">
- <span className="text-xs text-slate-500">剩余时间</span>
- {isLocked ? (
- <div className="inline-flex items-center gap-1.5 text-orange-600 font-mono font-bold bg-orange-50 px-2 py-1 rounded border border-orange-100 text-xs">
- <Lock size={12} />
- {remainingTime}
- </div>
- ) : (
- <span className="text-slate-300 text-sm">-</span>
- )}
- </div>
- </div>
- <div className="grid grid-cols-2 gap-2">
- <button
- onClick={() => onEdit(item)}
- className="flex items-center justify-center gap-2 py-2.5 bg-blue-50 text-blue-600 rounded-lg text-sm font-medium border border-blue-100"
- >
- <Pencil size={14} /> 编辑
- </button>
- <button
- onClick={() => onDelete(item)}
- className="flex items-center justify-center gap-2 py-2.5 bg-red-50 text-red-600 rounded-lg text-sm font-medium border border-red-100"
- >
- <Trash2 size={14} /> 删除
- </button>
- </div>
- </div>
- );
- })}
- </div>
- </div>
- );
- }
|