import { useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { Button, Form, Input, Modal, Select, Space, Switch, Tooltip } from 'antd'; import { PlusOutlined, MinusOutlined, QuestionCircleOutlined } from '@ant-design/icons'; import { FormProvider, useForm, useWatch } from 'react-hook-form'; import { InputAddon } from '@/components/ui'; import { FormField } from '@/components/form/rhf'; import { useInboundOptions } from '@/api/queries/useInboundOptions'; import { RuleFormSchema, type RuleFormValues } from '@/schemas/xray'; import { buildRemarkByTag, formatInboundTag, isApiRule } from './helpers'; export interface RoutingRule { enabled?: boolean; 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; } const initialForm = (): RuleFormValues => ({ enabled: true, 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 methods = useForm({ defaultValues: initialForm() }); const isEdit = rule != null; const { data: inboundOptions } = useInboundOptions(); const remarkByTag = useMemo(() => buildRemarkByTag(inboundOptions || []), [inboundOptions]); useEffect(() => { if (!open) return; if (rule) { methods.reset({ enabled: rule.enabled !== false, 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 { methods.reset(initialForm()); } }, [open, rule, methods]); const attrs = useWatch({ control: methods.control, name: 'attrs' }) ?? []; function submit() { const validated = RuleFormSchema.safeParse(methods.getValues()); if (!validated.success) return; const v = validated.data; const built: Record = { type: 'field', enabled: v.enabled, 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 managedKeys = new Set(Object.keys(built)); const out: Record = {}; if (rule) { for (const [key, value] of Object.entries(rule)) { if (!managedKeys.has(key) && value !== undefined) out[key] = value; } } 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 (
{t('pages.xray.ruleForm.sourceIps')} {t('pages.xray.ruleForm.sourcePort')} {t('pages.xray.ruleForm.vlessRoute')} ({ value: p, label: p }))} />