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'; import { RuleFormSchema, type RuleFormValues } from '@/schemas/xray'; 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; } type FormState = RuleFormValues; 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 validated = RuleFormSchema.safeParse(form); if (!validated.success) return; const v = validated.data; const built: Record = { type: 'field', domain: csv(v.domain), ip: csv(v.ip), port: v.port, sourcePort: v.sourcePort, vlessRoute: v.vlessRoute, network: v.network, sourceIP: csv(v.sourceIP), user: csv(v.user), inboundTag: v.inboundTag, protocol: v.protocol, attrs: Object.fromEntries(v.attrs.filter(([k]) => k)), outboundTag: v.outboundTag === '' ? undefined : v.outboundTag, balancerTag: v.balancerTag === '' ? undefined : v.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 }))} />