import { useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { Form, Input, InputNumber, Modal, Select, Switch, message } from 'antd'; import { Controller, FormProvider, useForm, useWatch } from 'react-hook-form'; import { FormField, rhfZodValidate } from '@/components/form/rhf'; import SelectAllClearButtons from '@/components/form/SelectAllClearButtons'; import { useInboundOptions } from '@/api/queries/useInboundOptions'; import { formatInboundLabel } from '@/lib/inbounds/label'; import { SubBalancerFormSchema, SubBalancerStrategySchema, type SubBalancer, type SubBalancerFormValues, type SubBalancerStrategy, } from '@/schemas/subBalancer'; // The JSON subscription only builds proxy outbounds for these protocols; // mtproto has no proxy-outbound case, so it is excluded from balancer members. const MULTI_CLIENT_PROTOCOLS = new Set([ 'shadowsocks', 'vless', 'vmess', 'trojan', 'hysteria', 'wireguard', ]); const STRATEGY_LABEL_KEYS: Record = { leastLoad: 'pages.settings.subBalancers.strategyLeastLoad', leastPing: 'pages.settings.subBalancers.strategyLeastPing', random: 'pages.settings.subBalancers.strategyRandom', roundRobin: 'pages.settings.subBalancers.strategyRoundRobin', }; function initialState(balancer: SubBalancer | null): SubBalancerFormValues { return { remark: balancer?.remark ?? '', strategy: balancer?.strategy ?? 'random', inboundIds: [...(balancer?.inboundIds ?? [])], memberWeights: balancer?.memberWeights ? { ...balancer.memberWeights } : undefined, sortOrder: balancer?.sortOrder ?? 1, enabled: balancer?.enabled ?? true, }; } interface SubBalancerFormModalProps { open: boolean; balancer: SubBalancer | null; onClose: () => void; onConfirm: (values: SubBalancerFormValues) => void; } export default function SubBalancerFormModal({ open, balancer, onClose, onConfirm, }: SubBalancerFormModalProps) { const { t } = useTranslation(); const [messageApi, messageContextHolder] = message.useMessage(); const methods = useForm({ defaultValues: initialState(balancer) }); const isEdit = balancer != null; useEffect(() => { if (open) methods.reset(initialState(balancer)); }, [open, balancer, methods]); const inboundIds = useWatch({ control: methods.control, name: 'inboundIds' }); const strategy = useWatch({ control: methods.control, name: 'strategy' }); // Weights only make sense for leastLoad; the fields hide but keep their // values so an accidental toggle away and back loses nothing until submit. const showWeights = strategy === 'leastLoad'; const { data: inboundOptionsRaw } = useInboundOptions(); const inboundOptions = useMemo( () => (inboundOptionsRaw ?? []) .filter((ib) => MULTI_CLIENT_PROTOCOLS.has(ib.protocol || '')) .filter((ib) => ib.enable || (inboundIds || []).includes(ib.id)) .map((ib) => ({ label: formatInboundLabel(ib.tag, ib.remark), value: ib.id, title: formatInboundLabel(ib.tag, ib.remark), })), [inboundOptionsRaw, inboundIds], ); function onFinish(values: SubBalancerFormValues) { const candidate: SubBalancerFormValues = { ...values }; if (candidate.memberWeights) { const cleaned = Object.fromEntries( Object.entries(candidate.memberWeights).filter( ([, v]) => typeof v === 'number' && Number.isFinite(v) && v > 0, ), ); candidate.memberWeights = Object.keys(cleaned).length > 0 ? cleaned : undefined; } // xray ignores costs on every strategy but leastLoad — never send them. if (candidate.strategy !== 'leastLoad') { delete candidate.memberWeights; } const parsed = SubBalancerFormSchema.safeParse(candidate); if (!parsed.success) { messageApi.error( t(parsed.error.issues[0]?.message ?? 'pages.settings.subBalancers.errRemarkRequired'), ); return; } onConfirm(parsed.data); } const strategies = SubBalancerStrategySchema.options.map((value) => ({ value, label: t(STRATEGY_LABEL_KEYS[value]), })); return ( {messageContextHolder}
methods.setValue('inboundIds', v, { shouldDirty: true })} /> {showWeights && (inboundIds ?? []).length > 0 && (
{(inboundIds ?? []).map((id) => { const option = inboundOptions.find((o) => o.value === id); return (
{option?.label ?? `#${id}`} ( field.onChange(typeof v === 'number' ? v : undefined)} /> )} />
); })}
)}
); }