import { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Form, Input, InputNumber, Modal, Select, Switch, message } from 'antd'; import { SyncOutlined } from '@ant-design/icons'; import dayjs from 'dayjs'; import type { Dayjs } from 'dayjs'; import { HttpUtil, RandomUtil, SizeFormatter } from '@/utils'; import { TLS_FLOW_CONTROL } from '@/schemas/primitives'; import DateTimePicker from '@/components/DateTimePicker'; import type { InboundOption } from '@/hooks/useClients'; import { ClientBulkAddFormSchema, type ClientBulkAddFormValues } from '@/schemas/client'; const FLOW_OPTIONS = Object.values(TLS_FLOW_CONTROL); const JSON_HEADERS = { headers: { 'Content-Type': 'application/json' } } as const; const MULTI_CLIENT_PROTOCOLS = new Set([ 'shadowsocks', 'vless', 'vmess', 'trojan', 'hysteria', 'hysteria2', ]); interface ClientBulkAddModalProps { open: boolean; inbounds: InboundOption[]; ipLimitEnable?: boolean; onOpenChange: (open: boolean) => void; onSaved?: () => void; } type FormState = ClientBulkAddFormValues; function emptyForm(): FormState { return { emailMethod: 0, firstNum: 1, lastNum: 1, emailPrefix: '', emailPostfix: '', quantity: 1, subId: '', comment: '', flow: '', limitIp: 0, totalGB: 0, expiryTime: 0, inboundIds: [], }; } export default function ClientBulkAddModal({ open, inbounds, ipLimitEnable = false, onOpenChange, onSaved, }: ClientBulkAddModalProps) { const { t } = useTranslation(); const [messageApi, messageContextHolder] = message.useMessage(); const [form, setForm] = useState(emptyForm); const [delayedStart, setDelayedStart] = useState(false); const [saving, setSaving] = useState(false); useEffect(() => { if (!open) return; setForm(emptyForm()); setDelayedStart(false); }, [open]); function update(key: K, value: FormState[K]) { setForm((prev) => ({ ...prev, [key]: value })); } const flowCapableIds = useMemo(() => { const ids = new Set(); for (const row of inbounds || []) { if (row?.tlsFlowCapable) ids.add(row.id); } return ids; }, [inbounds]); const showFlow = useMemo( () => (form.inboundIds || []).some((id) => flowCapableIds.has(id)), [form.inboundIds, flowCapableIds], ); useEffect(() => { if (!showFlow && form.flow) { update('flow', ''); } }, [showFlow, form.flow]); const inboundOptions = useMemo( () => (inbounds || []) .filter((ib) => MULTI_CLIENT_PROTOCOLS.has(ib.protocol || '')) .map((ib) => ({ label: `${ib.remark || `#${ib.id}`} · ${ib.protocol}:${ib.port}`, value: ib.id, })), [inbounds], ); const expiryDate = useMemo( () => (form.expiryTime > 0 ? dayjs(form.expiryTime) : null), [form.expiryTime], ); const delayedExpireDays = form.expiryTime < 0 ? form.expiryTime / -86400000 : 0; function buildEmails(): string[] { const method = form.emailMethod; const out: string[] = []; let start: number; let end: number; if (method > 1) { start = form.firstNum; end = form.lastNum + 1; } else { start = 0; end = form.quantity; } const prefix = method > 0 && form.emailPrefix.length > 0 ? form.emailPrefix : ''; const useNum = method > 1; const postfix = method > 2 && form.emailPostfix.length > 0 ? form.emailPostfix : ''; for (let i = start; i < end; i++) { let email = ''; if (method !== 4) email = RandomUtil.randomLowerAndNum(6); email += useNum ? prefix + String(i) + postfix : prefix + postfix; out.push(email); } return out; } async function submit() { const validated = ClientBulkAddFormSchema.safeParse(form); if (!validated.success) { messageApi.error(t(validated.error.issues[0]?.message ?? 'somethingWentWrong')); return; } const emails = buildEmails(); if (emails.length === 0) return; setSaving(true); const silentJsonOpts = { ...JSON_HEADERS, silent: true }; try { const results = await Promise.all(emails.map((email) => { const client = { email, subId: form.subId || RandomUtil.randomLowerAndNum(16), id: RandomUtil.randomUUID(), password: RandomUtil.randomLowerAndNum(16), auth: RandomUtil.randomLowerAndNum(16), flow: showFlow ? (form.flow || '') : '', totalGB: Math.round((form.totalGB || 0) * SizeFormatter.ONE_GB), expiryTime: form.expiryTime, limitIp: Number(form.limitIp) || 0, comment: form.comment, enable: true, }; const payload = { client, inboundIds: form.inboundIds }; return HttpUtil.post('/panel/api/clients/add', payload, silentJsonOpts); })); let ok = 0; let failed = 0; let firstError = ''; for (const msg of results) { if (msg?.success) ok++; else { failed++; if (!firstError && msg?.msg) firstError = msg.msg; } } if (failed === 0) { messageApi.success(t('pages.clients.toasts.bulkCreated', { count: ok })); } else { messageApi.warning(firstError ? `${t('pages.clients.toasts.bulkCreatedMixed', { ok, failed })} — ${firstError}` : t('pages.clients.toasts.bulkCreatedMixed', { ok, failed })); } onSaved?.(); onOpenChange(false); } finally { setSaving(false); } } return ( <> {messageContextHolder} onOpenChange(false)} >
update('emailMethod', v)} options={[ { value: 0, label: 'Random' }, { value: 1, label: 'Random + Prefix' }, { value: 2, label: 'Random + Prefix + Num' }, { value: 3, label: 'Random + Prefix + Num + Postfix' }, { value: 4, label: 'Prefix + Num + Postfix' }, ]} /> {form.emailMethod > 1 && ( <> update('firstNum', Number(v) || 1)} /> update('lastNum', Number(v) || 1)} /> )} {form.emailMethod > 0 && ( update('emailPrefix', e.target.value)} /> )} {form.emailMethod > 2 && ( update('emailPostfix', e.target.value)} /> )} {form.emailMethod < 2 && ( update('quantity', Number(v) || 1)} /> )} {t('subscription.title')} update('subId', RandomUtil.randomLowerAndNum(16))} /> }> update('subId', e.target.value)} /> update('comment', e.target.value)} /> {showFlow && (