import { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Button, Divider, Modal, Tag, Tooltip, message } from 'antd'; import { CopyOutlined } from '@ant-design/icons'; import { ClipboardManager, HttpUtil, IntlUtil, SizeFormatter } from '@/utils'; import { useDatepicker } from '@/hooks/useDatepicker'; import type { ClientRecord, InboundOption } from '@/hooks/useClients'; import './ClientInfoModal.css'; interface SubSettings { enable: boolean; subURI: string; subJsonURI: string; subJsonEnable: boolean; } interface ClientInfoModalProps { open: boolean; client: ClientRecord | null; inboundsById: Record; isOnline: boolean; subSettings?: SubSettings; onOpenChange: (open: boolean) => void; } interface ApiMsg { success?: boolean; obj?: T; } const DEFAULT_SUB: SubSettings = { enable: false, subURI: '', subJsonURI: '', subJsonEnable: false }; export default function ClientInfoModal({ open, client, inboundsById, isOnline, subSettings = DEFAULT_SUB, onOpenChange, }: ClientInfoModalProps) { const { datepicker } = useDatepicker(); const { t } = useTranslation(); const expiryLabel = (ts?: number) => { if (!ts) return '∞'; if (ts < 0) { const days = Math.round(ts / -86400000); return `${t('pages.clients.delayedStart')}: ${days}d`; } return IntlUtil.formatDate(ts, datepicker); }; const dateLabel = (ts?: number) => (!ts || ts <= 0 ? '-' : IntlUtil.formatDate(ts, datepicker)); const [messageApi, messageContextHolder] = message.useMessage(); const [links, setLinks] = useState([]); useEffect(() => { if (!open) { setLinks([]); return; } if (!client?.subId) return; let cancelled = false; (async () => { const msg = await HttpUtil.get( `/panel/api/clients/subLinks/${encodeURIComponent(client.subId!)}`, ) as ApiMsg; if (cancelled) return; setLinks(msg?.success && Array.isArray(msg.obj) ? msg.obj : []); })(); return () => { cancelled = true; }; }, [open, client?.subId]); const traffic = client?.traffic || null; const totalBytes = client?.totalGB || 0; const used = (traffic?.up || 0) + (traffic?.down || 0); const remaining = useMemo(() => { if (totalBytes <= 0) return -1; const r = totalBytes - used; return r > 0 ? r : 0; }, [totalBytes, used]); const subLink = useMemo(() => { if (!client?.subId || !subSettings?.subURI) return ''; return subSettings.subURI + client.subId; }, [client?.subId, subSettings?.subURI]); const subJsonLink = useMemo(() => { if (!client?.subId) return ''; if (!subSettings?.subJsonEnable || !subSettings?.subJsonURI) return ''; return subSettings.subJsonURI + client.subId; }, [client?.subId, subSettings?.subJsonEnable, subSettings?.subJsonURI]); const showSubscription = !!(subSettings?.enable && client?.subId); async function copyValue(text: string) { if (!text) return; const ok = await ClipboardManager.copyText(String(text)); if (ok) messageApi.success(t('copied')); } return ( <> {messageContextHolder} onOpenChange(false)} > {client && ( <> {client.uuid && ( )} {client.password && ( )} {client.auth && ( )} {client.comment && ( )}
{t('pages.clients.online')} {client.enable && isOnline ? {t('pages.clients.online')} : {t('pages.clients.offline')}} {t('lastOnline')}: {dateLabel(traffic?.lastOnline)}
{t('status')} {client.enable ? t('enabled') : t('disabled')}
{t('pages.clients.email')} {client.email ? {client.email} : {t('none')}}
{t('pages.clients.subId')} {client.subId || '-'} {client.subId && (
{t('pages.clients.uuid')} {client.uuid}
{t('password')} {client.password}
{t('pages.clients.auth')} {client.auth}
{t('pages.clients.flow')} {client.flow ? {client.flow} : {t('none')}}
{t('pages.inbounds.traffic')} ↑ {SizeFormatter.sizeFormat(traffic?.up || 0)} {' '}/ ↓ {SizeFormatter.sizeFormat(traffic?.down || 0)} {SizeFormatter.sizeFormat(used)} / {totalBytes > 0 ? SizeFormatter.sizeFormat(totalBytes) : '∞'}
{t('remained')} {remaining < 0 ? : 0 ? '' : 'red'}>{SizeFormatter.sizeFormat(remaining)}}
{t('pages.inbounds.expireDate')} {!client.expiryTime ? : {expiryLabel(client.expiryTime)}} {(client.expiryTime ?? 0) > 0 && ( {IntlUtil.formatRelativeTime(client.expiryTime)} )}
{t('pages.clients.ipLimit')} {!client.limitIp ? : {client.limitIp}}
{t('pages.inbounds.createdAt')} {dateLabel(client.createdAt)}
{t('pages.inbounds.updatedAt')} {dateLabel(client.updatedAt)}
{t('pages.clients.comment')} {client.comment}
{t('pages.clients.attachedInbounds')}
{(client.inboundIds || []).map((id) => { const ib = inboundsById[id]; return ( {ib ? `${ib.remark || `#${id}`} (${ib.protocol}:${ib.port})` : `#${id}`} ); })} {(!client.inboundIds || client.inboundIds.length === 0) && ( )}
{links.length > 0 && ( <> {t('pages.inbounds.copyLink')} {links.map((link, idx) => (
{`${t('pages.clients.link')} ${idx + 1}`}
{link}
))} )} {showSubscription && subLink && ( <> {t('subscription.title')}
{t('subscription.title')}
{subLink}
{subJsonLink && (
JSON
{subJsonLink}
)} )} )}
); }