import { useCallback, useEffect, useRef, useState } from 'react'; import { HttpUtil } from '@/utils'; const JSON_HEADERS = { headers: { 'Content-Type': 'application/json' } } as const; export interface ClientTraffic { up?: number; down?: number; total?: number; expiryTime?: number; enable?: boolean; lastOnline?: number; } export interface ClientRecord { email: string; subId?: string; uuid?: string; password?: string; auth?: string; flow?: string; totalGB?: number; expiryTime?: number; limitIp?: number; tgId?: number | string; comment?: string; enable?: boolean; inboundIds?: number[]; traffic?: ClientTraffic; reverse?: { tag?: string }; createdAt?: number; updatedAt?: number; [key: string]: unknown; } export interface InboundOption { id: number; remark?: string; protocol?: string; port?: number; tlsFlowCapable?: boolean; } interface ApiMsg { success?: boolean; msg?: string; obj?: T; } interface SubSettings { enable: boolean; subURI: string; subJsonURI: string; subJsonEnable: boolean; } export function useClients() { const [clients, setClients] = useState([]); const [inbounds, setInbounds] = useState([]); const [onlines, setOnlines] = useState([]); const [loading, setLoading] = useState(false); const [fetched, setFetched] = useState(false); const [subSettings, setSubSettings] = useState({ enable: false, subURI: '', subJsonURI: '', subJsonEnable: false, }); const [ipLimitEnable, setIpLimitEnable] = useState(false); const [tgBotEnable, setTgBotEnable] = useState(false); const [expireDiff, setExpireDiff] = useState(0); const [trafficDiff, setTrafficDiff] = useState(0); const [pageSize, setPageSize] = useState(0); const clientsRef = useRef([]); const invalidateTimerRef = useRef(null); useEffect(() => { clientsRef.current = clients; }, [clients]); const refresh = useCallback(async () => { setLoading(true); try { const [clientsMsg, inboundsMsg] = await Promise.all([ HttpUtil.get('/panel/api/clients/list') as Promise>, HttpUtil.get('/panel/api/inbounds/options') as Promise>, ]); if (clientsMsg?.success) { setClients(Array.isArray(clientsMsg.obj) ? clientsMsg.obj : []); } if (inboundsMsg?.success) { setInbounds(Array.isArray(inboundsMsg.obj) ? inboundsMsg.obj : []); } setFetched(true); } finally { setLoading(false); } }, []); const fetchSubSettings = useCallback(async () => { const msg = await HttpUtil.post('/panel/setting/defaultSettings') as ApiMsg>; if (!msg?.success) return; const s = msg.obj || {}; setSubSettings({ enable: !!s.subEnable, subURI: (s.subURI as string) || '', subJsonURI: (s.subJsonURI as string) || '', subJsonEnable: !!s.subJsonEnable, }); setIpLimitEnable(!!s.ipLimitEnable); setTgBotEnable(!!s.tgBotEnable); setExpireDiff(((s.expireDiff as number) ?? 0) * 86400000); setTrafficDiff(((s.trafficDiff as number) ?? 0) * 1073741824); setPageSize((s.pageSize as number) ?? 0); }, []); const create = useCallback(async (payload: unknown) => { const msg = await HttpUtil.post('/panel/api/clients/add', payload, JSON_HEADERS) as ApiMsg; if (msg?.success) await refresh(); return msg; }, [refresh]); const update = useCallback(async (email: string, client: unknown) => { if (!email) return null; const encoded = encodeURIComponent(email); const msg = await HttpUtil.post(`/panel/api/clients/update/${encoded}`, client, JSON_HEADERS) as ApiMsg; if (msg?.success) await refresh(); return msg; }, [refresh]); const remove = useCallback(async (email: string, keepTraffic = false) => { if (!email) return null; const encoded = encodeURIComponent(email); const url = keepTraffic ? `/panel/api/clients/del/${encoded}?keepTraffic=1` : `/panel/api/clients/del/${encoded}`; const msg = await HttpUtil.post(url) as ApiMsg; if (msg?.success) await refresh(); return msg; }, [refresh]); const removeMany = useCallback(async (emails: string[], keepTraffic = false) => { if (!Array.isArray(emails) || emails.length === 0) return []; const suffix = keepTraffic ? '?keepTraffic=1' : ''; const results = await Promise.all(emails.map((email) => { const url = `/panel/api/clients/del/${encodeURIComponent(email)}${suffix}`; return HttpUtil.post(url, undefined, { silent: true }) as Promise; })); await refresh(); return results; }, [refresh]); const attach = useCallback(async (email: string, inboundIds: number[]) => { if (!email) return null; const encoded = encodeURIComponent(email); const msg = await HttpUtil.post(`/panel/api/clients/${encoded}/attach`, { inboundIds }, JSON_HEADERS) as ApiMsg; if (msg?.success) await refresh(); return msg; }, [refresh]); const detach = useCallback(async (email: string, inboundIds: number[]) => { if (!email) return null; const encoded = encodeURIComponent(email); const msg = await HttpUtil.post(`/panel/api/clients/${encoded}/detach`, { inboundIds }, JSON_HEADERS) as ApiMsg; if (msg?.success) await refresh(); return msg; }, [refresh]); const resetTraffic = useCallback(async (client: ClientRecord) => { if (!client?.email) return null; const url = `/panel/api/clients/resetTraffic/${encodeURIComponent(client.email)}`; const msg = await HttpUtil.post(url) as ApiMsg; if (msg?.success) await refresh(); return msg; }, [refresh]); const resetAllTraffics = useCallback(async () => { const msg = await HttpUtil.post('/panel/api/clients/resetAllTraffics') as ApiMsg; if (msg?.success) await refresh(); return msg; }, [refresh]); const delDepleted = useCallback(async () => { const msg = await HttpUtil.post('/panel/api/clients/delDepleted') as ApiMsg<{ deleted?: number }>; if (msg?.success) await refresh(); return msg; }, [refresh]); const setEnable = useCallback(async (client: ClientRecord, enable: boolean) => { if (!client?.email) return null; const payload = { email: client.email, subId: client.subId, id: client.uuid, password: client.password, auth: client.auth, totalGB: client.totalGB || 0, expiryTime: client.expiryTime || 0, limitIp: client.limitIp || 0, comment: client.comment || '', enable: !!enable, }; return update(client.email, payload); }, [update]); const applyTrafficEvent = useCallback((payload: unknown) => { if (!payload || typeof payload !== 'object') return; const p = payload as { onlineClients?: string[] }; if (Array.isArray(p.onlineClients)) { setOnlines(p.onlineClients); } }, []); const applyClientStatsEvent = useCallback((payload: unknown) => { if (!payload || typeof payload !== 'object') return; const p = payload as { clients?: ClientTraffic[] & { email?: string }[] }; if (!Array.isArray(p.clients) || p.clients.length === 0) return; const byEmail = new Map(); for (const row of p.clients as (ClientTraffic & { email?: string })[]) { if (row && row.email) byEmail.set(row.email, row); } const cur = clientsRef.current || []; let touched = false; const next = cur.slice(); for (let i = 0; i < next.length; i++) { const row = next[i]; const upd = byEmail.get(row?.email); if (!upd) continue; const merged: ClientTraffic = { ...(row.traffic || {}) }; if (typeof upd.up === 'number') merged.up = upd.up; if (typeof upd.down === 'number') merged.down = upd.down; if (typeof upd.total === 'number') merged.total = upd.total; if (typeof upd.expiryTime === 'number') merged.expiryTime = upd.expiryTime; if (typeof upd.enable === 'boolean') merged.enable = upd.enable; if (typeof upd.lastOnline === 'number') merged.lastOnline = upd.lastOnline; next[i] = { ...row, traffic: merged }; touched = true; } if (touched) setClients(next); }, []); const applyInvalidate = useCallback((payload: unknown) => { if (!payload || typeof payload !== 'object') return; const p = payload as { type?: string }; if (p.type !== 'inbounds' && p.type !== 'clients') return; if (invalidateTimerRef.current != null) clearTimeout(invalidateTimerRef.current); invalidateTimerRef.current = window.setTimeout(() => { invalidateTimerRef.current = null; refresh(); }, 200); }, [refresh]); useEffect(() => { Promise.all([refresh(), fetchSubSettings()]); }, [refresh, fetchSubSettings]); return { clients, inbounds, onlines, loading, fetched, subSettings, ipLimitEnable, tgBotEnable, expireDiff, trafficDiff, pageSize, refresh, create, update, remove, removeMany, attach, detach, resetTraffic, resetAllTraffics, delDepleted, setEnable, applyTrafficEvent, applyClientStatsEvent, applyInvalidate, }; }