import { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Alert, Button, Col, Form, Input, InputNumber, Modal, Row, Select, Switch, message, } from 'antd'; import type { NodeRecord } from '@/hooks/useNodes'; import './NodeFormModal.css'; type Mode = 'add' | 'edit'; interface ApiMsg { success?: boolean; msg?: string; obj?: T; } interface NodeFormModalProps { open: boolean; mode: Mode; node: NodeRecord | null; testConnection: (payload: Partial) => Promise>; save: (payload: Partial) => Promise; onOpenChange: (open: boolean) => void; } interface FormState { id: number; name: string; remark: string; scheme: 'http' | 'https'; address: string; port: number; basePath: string; apiToken: string; enable: boolean; allowPrivateAddress: boolean; } function defaultForm(): FormState { return { id: 0, name: '', remark: '', scheme: 'https', address: '', port: 2053, basePath: '/', apiToken: '', enable: true, allowPrivateAddress: false, }; } export default function NodeFormModal({ open, mode, node, testConnection, save, onOpenChange, }: NodeFormModalProps) { const { t } = useTranslation(); const [messageApi, messageContextHolder] = message.useMessage(); const [form, setForm] = useState(defaultForm); const [submitting, setSubmitting] = useState(false); const [testing, setTesting] = useState(false); const [testResult, setTestResult] = useState<{ status: string; latencyMs?: number; xrayVersion?: string; error?: string; } | null>(null); useEffect(() => { if (!open) return; const base = defaultForm(); const next: FormState = mode === 'edit' && node ? { ...base, ...(node as unknown as Partial), id: node.id, scheme: (node.scheme as 'http' | 'https') || base.scheme, } : base; setForm(next); setTestResult(null); }, [open, mode, node]); const title = useMemo( () => (mode === 'edit' ? t('pages.nodes.editNode') : t('pages.nodes.addNode')), [mode, t], ); function buildPayload(): Partial { return { id: form.id || 0, name: form.name?.trim() || '', remark: form.remark?.trim() || '', scheme: form.scheme || 'https', address: form.address?.trim() || '', port: Number(form.port) || 0, basePath: form.basePath?.trim() || '/', apiToken: form.apiToken?.trim() || '', enable: !!form.enable, allowPrivateAddress: !!form.allowPrivateAddress, }; } function update(key: K, value: FormState[K]) { setForm((prev) => ({ ...prev, [key]: value })); } async function onTest() { setTesting(true); setTestResult(null); try { const payload = buildPayload(); if (!payload.address || !payload.port) { messageApi.error(t('pages.nodes.toasts.fillRequired')); return; } const msg = await testConnection(payload); if (msg?.success && msg.obj) { setTestResult(msg.obj); } else { setTestResult({ status: 'offline', error: msg?.msg || 'unknown error' }); } } finally { setTesting(false); } } async function onSave() { const payload = buildPayload(); if (!payload.name || !payload.address || !payload.port) { messageApi.error(t('pages.nodes.toasts.fillRequired')); return; } setSubmitting(true); try { const msg = await save(payload); if (msg?.success) { onOpenChange(false); } } finally { setSubmitting(false); } } function close() { if (!submitting) onOpenChange(false); } return ( <> {messageContextHolder}
update('name', e.target.value)} /> update('remark', e.target.value)} /> update('address', e.target.value)} /> update('port', Number(v) || 0)} /> update('basePath', e.target.value)} /> update('enable', v)} /> update('allowPrivateAddress', v)} />
{t('pages.nodes.allowPrivateAddressHint')}
update('apiToken', e.target.value)} />
{t('pages.nodes.apiTokenHint')}
{testResult && (
{testResult.status === 'online' ? ( ) : ( )}
)}
); }