'use client'; import { UserDetail } from '@/app/dashboard/user/user-detail'; import { IpLink } from '@/components/ip-link'; import { ProTable, ProTableActions } from '@/components/pro-table'; import { getUserSubscribeById } from '@/services/admin/user'; import { useQuery } from '@tanstack/react-query'; import { Badge } from '@workspace/ui/components/badge'; import { Button } from '@workspace/ui/components/button'; import { Card, CardContent } from '@workspace/ui/components/card'; import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger, } from '@workspace/ui/components/sheet'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from '@workspace/ui/components/tooltip'; import { ConfirmButton } from '@workspace/ui/custom-components/confirm-button'; import { cn } from '@workspace/ui/lib/utils'; import { useTranslations } from 'next-intl'; import { useRef, useState } from 'react'; import { toast } from 'sonner'; import ServerConfig from './server-config'; import ServerForm from './server-form'; type ProtocolName = 'shadowsocks' | 'vmess' | 'vless' | 'trojan' | 'hysteria2' | 'tuic' | 'anytls'; type ProtocolEntry = { protocol: ProtocolName; enabled: boolean; config: Record }; interface ServerFormFields { name: string; server_addr: string; country?: string; city?: string; protocols: ProtocolEntry[]; } type ServerStatus = { online?: unknown; cpu?: number; mem?: number; disk?: number; updated_at?: number; }; type ServerItem = ServerFormFields & { id: number; status?: ServerStatus; [key: string]: unknown }; const mockList: ServerItem[] = [ { id: 1, name: 'Server A', server_addr: '1.1.1.1', country: 'US', city: 'SFO', protocols: [ { protocol: 'shadowsocks', enabled: true, config: { method: 'aes-128-gcm', port: 443, server_key: null }, }, { protocol: 'trojan', enabled: true, config: { port: 8443, transport: 'tcp', security: 'tls' }, }, { protocol: 'vmess', enabled: false, config: { port: 1443, transport: 'websocket', transport_config: { path: '/ws', host: 'example.com' }, security: 'tls', }, }, ], status: { online: { 1001: ['1.2.3.4'], 1002: ['5.6.7.8', '9.9.9.9'] }, cpu: 34, mem: 62, disk: 48, updated_at: Date.now() / 1000, }, }, { id: 2, name: 'Server B', server_addr: '2.2.2.2', country: 'JP', city: 'Tokyo', protocols: [ { protocol: 'vmess', enabled: true, config: { port: 2443, transport: 'tcp', security: 'none' }, }, { protocol: 'hysteria2', enabled: true, config: { port: 3443, hop_ports: '443,8443,10443', hop_interval: 15, security: 'tls' }, }, { protocol: 'tuic', enabled: false, config: { port: 4443 } }, ], status: { online: { 2001: ['10.0.0.1'] }, cpu: 72, mem: 81, disk: 67, updated_at: Date.now() / 1000, }, }, { id: 3, name: 'Server C', server_addr: '3.3.3.3', country: 'DE', city: 'FRA', protocols: [ { protocol: 'anytls', enabled: true, config: { port: 80 } }, { protocol: 'shadowsocks', enabled: false, config: { method: 'chacha20-ietf-poly1305', port: 8080 }, }, ], status: { online: {}, cpu: 0, mem: 0, disk: 0, updated_at: 0 }, }, ]; let mockData: ServerItem[] = [...mockList]; const getServerList = async () => ({ list: mockData, total: mockData.length }); const createServer = async (values: Omit) => { mockData.push({ id: Date.now(), name: '', server_addr: '', protocols: [], ...values, }); return true; }; const updateServer = async (id: number, values: Omit) => { mockData = mockData.map((i) => (i.id === id ? { ...i, ...values } : i)); return true; }; const deleteServer = async (id: number) => { mockData = mockData.filter((i) => i.id !== id); return true; }; const PROTOCOL_COLORS: Record = { shadowsocks: 'bg-green-500', vmess: 'bg-rose-500', vless: 'bg-blue-500', trojan: 'bg-yellow-500', hysteria2: 'bg-purple-500', tuic: 'bg-cyan-500', anytls: 'bg-gray-500', }; function getEnabledProtocols(p: ServerItem['protocols']) { return Array.isArray(p) ? p.filter((x) => x.enabled) : []; } function ProtocolBadge({ item, t, }: { item: ServerItem['protocols'][number]; t: (key: string) => string; }) { const color = PROTOCOL_COLORS[item.protocol]; const port = (item?.config as any)?.port as number | undefined; const extra: string[] = []; if ((item.config as any)?.transport) extra.push(String((item.config as any).transport)); if ((item.config as any)?.security && (item.config as any).security !== 'none') extra.push(String((item.config as any).security)); const label = `${item.protocol}${port ? ` (${port})` : ''}`; const tipParts = [label, extra.length ? `ยท ${extra.join(' / ')}` : ''].filter(Boolean); const tooltip = tipParts.join(' '); return ( {label} {tooltip || t('notAvailable')} ); } function PctBar({ value }: { value: number }) { const v = Math.max(0, Math.min(100, Math.round(value))); return (
{v}%
); } function RegionIpCell({ country, city, ip, t, }: { country?: string; city?: string; ip: string; t: (key: string) => string; }) { const region = [country, city].filter(Boolean).join(' / ') || t('notAvailable'); return (
{region} {ip}
); } function UserSubscribeInfo({ userId, type, t, }: { userId: number; type: 'account' | 'subscribeName' | 'subscribeId' | 'traffic' | 'expireTime'; t: (key: string) => string; }) { const { data } = useQuery({ enabled: userId !== 0, queryKey: ['getUserSubscribeById', userId], queryFn: async () => { const { data } = await getUserSubscribeById({ id: userId }); return data.data; }, }); if (!data) return --; if (type === 'account') return data.user_id ? ( ) : ( -- ); if (type === 'subscribeName') return data.subscribe?.name ? ( {data.subscribe.name} ) : ( -- ); if (type === 'subscribeId') return data.id ? ( {data.id} ) : ( -- ); if (type === 'traffic') { const used = (data.upload || 0) + (data.download || 0); const total = data.traffic || 0; return (
{`${(used / 1024 ** 3).toFixed(2)} GB / ${total > 0 ? (total / 1024 ** 3).toFixed(2) + ' GB' : t('unlimited')}`}
); } if (type === 'expireTime') { if (!data.expire_time) return --; const expired = data.expire_time < Date.now() / 1000; return (
{new Date((data.expire_time || 0) * 1000).toLocaleString()} {expired && ( {t('expired')} )}
); } return --; } function normalizeOnlineMap(online: unknown): { uid: string; ips: string[] }[] { if (!online || typeof online !== 'object' || Array.isArray(online)) return []; const m = online as Record; const rows = Object.entries(m).map(([uid, ips]) => { if (Array.isArray(ips)) return { uid, ips: (ips as unknown[]).map(String) }; if (typeof ips === 'string') return { uid, ips: [ips] }; const o = ips as Record; if (Array.isArray(o?.ips)) return { uid, ips: (o.ips as unknown[]).map(String) }; return { uid, ips: [] }; }); return rows.filter((r) => r.ips.length > 0); } function OnlineUsersCell({ status, t }: { status?: ServerStatus; t: (key: string) => string }) { const [open, setOpen] = useState(false); const rows = normalizeOnlineMap(status?.online); const count = rows.length; return ( {t('onlineUsers')}
> header={{ hidden: true }} columns={[ { accessorKey: 'ips', header: t('ipAddresses'), cell: ({ row }) => { const ips = row.original.ips; return (
{ips.map((ip, i) => (
{i === 0 ? ( ) : ( )}
))}
); }, }, { accessorKey: 'user', header: t('user'), cell: ({ row }) => ( ), }, { accessorKey: 'subscription', header: t('subscription'), cell: ({ row }) => ( ), }, { accessorKey: 'subscribeId', header: t('subscribeId'), cell: ({ row }) => ( ), }, { accessorKey: 'traffic', header: t('traffic'), cell: ({ row }) => ( ), }, { accessorKey: 'expireTime', header: t('expireTime'), cell: ({ row }) => ( ), }, ]} request={async () => ({ list: rows, total: rows.length })} />
); } export default function ServersPage() { const t = useTranslations('servers'); const [loading, setLoading] = useState(false); const ref = useRef(null); return (
action={ref} header={{ title: t('pageTitle'), toolbar: ( { setLoading(true); await createServer(values as any); toast.success(t('created')); ref.current?.refresh(); setLoading(false); return true; }} /> ), }} columns={[ { accessorKey: 'id', header: t('id'), cell: ({ row }) => {row.getValue('id')}, }, { accessorKey: 'name', header: t('name') }, { id: 'region_ip', header: t('serverAddress'), cell: ({ row }) => ( ), }, { accessorKey: 'protocols', header: t('protocols'), cell: ({ row }) => { const enabled = getEnabledProtocols(row.original.protocols); if (!enabled.length) return t('noData'); return (
{enabled.map((p, idx) => ( ))}
); }, }, { id: 'status', header: t('status'), cell: ({ row }) => { const s = (row.original.status ?? {}) as ServerStatus; const on = !!( s.online && typeof s.online === 'object' && !Array.isArray(s.online) && Object.keys(s.online as Record).length ); return (
{on ? t('online') : t('offline')}
); }, }, { id: 'cpu', header: t('cpu'), cell: ({ row }) => , }, { id: 'mem', header: t('memory'), cell: ({ row }) => , }, { id: 'disk', header: t('disk'), cell: ({ row }) => , }, { id: 'online_users', header: t('onlineUsers'), cell: ({ row }) => ( ), }, ]} params={[{ key: 'search' }]} request={async (_pagination, filter) => { const { list } = await getServerList(); const keyword = (filter?.search || '').toLowerCase().trim(); const filtered = keyword ? list.filter((item) => [item.name, item.server_addr, item.country, item.city] .filter(Boolean) .some((v) => String(v).toLowerCase().includes(keyword)), ) : list; return { list: filtered, total: filtered.length }; }} actions={{ render: (row) => [ { setLoading(true); await updateServer(row.id as number, values as any); toast.success(t('updated')); ref.current?.refresh(); setLoading(false); return true; }} />, {t('delete')}} title={t('confirmDeleteTitle')} description={t('confirmDeleteDesc')} onConfirm={async () => { await deleteServer(row.id as number); toast.success(t('deleted')); ref.current?.refresh(); }} cancelText={t('cancel')} confirmText={t('confirm')} />, , ], }} />
); }