import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Button, Form, Input, Modal, Select, Space, Tooltip } from 'antd'; import { PlusOutlined, MinusOutlined, QuestionCircleOutlined } from '@ant-design/icons'; import InputAddon from '@/components/InputAddon'; export interface RoutingRule { type?: string; domain?: string | string[]; ip?: string | string[]; port?: string; sourcePort?: string; vlessRoute?: string; network?: string; sourceIP?: string | string[]; user?: string | string[]; inboundTag?: string[]; protocol?: string[]; attrs?: Record; outboundTag?: string; balancerTag?: string; [key: string]: unknown; } interface RuleFormModalProps { open: boolean; rule: RoutingRule | null; inboundTags: string[]; outboundTags: string[]; balancerTags: string[]; onClose: () => void; onConfirm: (rule: Record) => void; } interface FormState { domain: string; ip: string; port: string; sourcePort: string; vlessRoute: string; network: string; sourceIP: string; user: string; inboundTag: string[]; protocol: string[]; attrs: [string, string][]; outboundTag: string; balancerTag: string; } const initialForm = (): FormState => ({ domain: '', ip: '', port: '', sourcePort: '', vlessRoute: '', network: '', sourceIP: '', user: '', inboundTag: [], protocol: [], attrs: [], outboundTag: '', balancerTag: '', }); const NETWORKS = ['', 'TCP', 'UDP', 'TCP,UDP']; const PROTOCOLS = ['http', 'tls', 'bittorrent', 'quic']; function csv(value: string): string[] { if (!value) return []; return value.split(',').map((s) => s.trim()).filter(Boolean); } export default function RuleFormModal({ open, rule, inboundTags, outboundTags, balancerTags, onClose, onConfirm, }: RuleFormModalProps) { const { t } = useTranslation(); const [form, setForm] = useState(initialForm); const isEdit = rule != null; useEffect(() => { if (!open) return; if (rule) { setForm({ domain: Array.isArray(rule.domain) ? rule.domain.join(',') : rule.domain || '', ip: Array.isArray(rule.ip) ? rule.ip.join(',') : rule.ip || '', port: rule.port || '', sourcePort: rule.sourcePort || '', vlessRoute: rule.vlessRoute || '', network: rule.network || '', sourceIP: Array.isArray(rule.sourceIP) ? rule.sourceIP.join(',') : rule.sourceIP || '', user: Array.isArray(rule.user) ? rule.user.join(',') : rule.user || '', inboundTag: rule.inboundTag || [], protocol: rule.protocol || [], attrs: rule.attrs ? Object.entries(rule.attrs) : [], outboundTag: rule.outboundTag || '', balancerTag: rule.balancerTag || '', }); } else { setForm(initialForm()); } }, [open, rule]); const update = (key: K, value: FormState[K]) => setForm((prev) => ({ ...prev, [key]: value })); function submit() { const built: Record = { type: 'field', domain: csv(form.domain), ip: csv(form.ip), port: form.port, sourcePort: form.sourcePort, vlessRoute: form.vlessRoute, network: form.network, sourceIP: csv(form.sourceIP), user: csv(form.user), inboundTag: form.inboundTag, protocol: form.protocol, attrs: Object.fromEntries(form.attrs.filter(([k]) => k)), outboundTag: form.outboundTag === '' ? undefined : form.outboundTag, balancerTag: form.balancerTag === '' ? undefined : form.balancerTag, }; const out: Record = {}; for (const [k, v] of Object.entries(built)) { if (v == null) continue; if (Array.isArray(v) && v.length === 0) continue; if (typeof v === 'object' && !Array.isArray(v) && Object.keys(v).length === 0) continue; if (v === '') continue; out[k] = v; } onConfirm(out); } const title = isEdit ? `${t('edit')} ${t('pages.xray.Routings')}` : `+ ${t('pages.xray.Routings')}`; const okText = isEdit ? t('pages.clients.submitEdit') : t('create'); return (
Source IPs } > update('sourceIP', e.target.value)} placeholder="0.0.0.0/8, fc00::/7, geoip:ir" /> Source port } > update('sourcePort', e.target.value)} placeholder="53,443,1000-2000" /> VLESS route } > update('vlessRoute', e.target.value)} placeholder="53,443,1000-2000" /> update('protocol', v)} options={PROTOCOLS.map((p) => ({ value: p, label: p }))} />