import { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Button, Empty, Input, InputNumber, Modal, Select, Space, Switch, Table, Tabs } from 'antd'; import { DatabaseOutlined, DeleteOutlined, ExperimentOutlined, MenuOutlined, PlusOutlined, ProfileOutlined, SettingOutlined, } from '@ant-design/icons'; import { SettingListItem } from '@/components/ui'; import { useMediaQuery } from '@/hooks/useMediaQuery'; import { catTabLabel } from '@/pages/settings/catTabLabel'; import DnsServerModal from './DnsServerModal'; import type { DnsServerValue } from './DnsServerModal'; import DnsPresetsModal from './DnsPresetsModal'; import type { XraySettingsValue, SetTemplate } from '@/hooks/useXraySetting'; import './DnsTab.css'; import { STRATEGIES, DEFAULT_FAKEDNS } from './helpers'; import type { DnsConfig, HostRow, FakednsRow } from './types'; import { useDnsServerColumns, useFakednsColumns } from './useDnsColumns'; interface DnsTabProps { templateSettings: XraySettingsValue | null; setTemplateSettings: SetTemplate; } export default function DnsTab({ templateSettings, setTemplateSettings }: DnsTabProps) { const { t } = useTranslation(); const { isMobile } = useMediaQuery(); const [modal, modalContextHolder] = Modal.useModal(); const [hostsList, setHostsList] = useState([]); const [serverModalOpen, setServerModalOpen] = useState(false); const [editingServer, setEditingServer] = useState(null); const [editingIndex, setEditingIndex] = useState(null); const [presetsModalOpen, setPresetsModalOpen] = useState(false); const dns = (templateSettings?.dns as DnsConfig | undefined) ?? null; const dnsEnabled = !!dns; const mutate = useCallback( (mutator: (next: XraySettingsValue) => void) => { setTemplateSettings((prev) => { if (!prev) return prev; const clone = JSON.parse(JSON.stringify(prev)) as XraySettingsValue; mutator(clone); return clone; }); }, [setTemplateSettings], ); function toggleDNS(enabled: boolean) { mutate((next) => { if (enabled) { (next as { dns?: DnsConfig }).dns = { tag: 'dns_inbound', queryStrategy: 'UseIP', disableCache: false, disableFallback: false, disableFallbackIfMatch: false, useSystemHosts: false, enableParallelQuery: false, serveStale: false, serveExpiredTTL: 0, hosts: {}, servers: [], }; next.fakedns = null; } else { delete next.dns; delete next.fakedns; } }); } useEffect(() => { if (!dns) { setHostsList([]); return; } const src = dns.hosts || {}; setHostsList( Object.entries(src).map(([domain, val]) => ({ domain, values: Array.isArray(val) ? [...val] : [String(val)], })), ); // eslint-disable-next-line react-hooks/exhaustive-deps }, [dnsEnabled]); function syncHosts(next: HostRow[]) { setHostsList(next); mutate((tt) => { if (!tt.dns) return; const obj: Record = {}; for (const row of next) { if (!row.domain) continue; const vals = (row.values || []).filter(Boolean); if (vals.length === 0) continue; obj[row.domain] = vals.length === 1 ? vals[0] : vals; } if (Object.keys(obj).length > 0) { (tt.dns as DnsConfig).hosts = obj; } else if ('hosts' in (tt.dns as DnsConfig)) { delete (tt.dns as DnsConfig).hosts; } }); } function setDnsField(key: K, value: DnsConfig[K], omit = false) { mutate((tt) => { if (!tt.dns) return; if (omit && (value == null || (typeof value === 'string' && value.trim() === ''))) { delete (tt.dns as Record)[key as string]; } else { (tt.dns as Record)[key as string] = value; } }); } const dnsServers = useMemo(() => { const list = dns?.servers || []; return list.map((server, idx) => ({ key: idx, server })); }, [dns?.servers]); const dnsColumns = useDnsServerColumns({ openEditServer, deleteServer }); function openAddServer() { setEditingServer(null); setEditingIndex(null); setServerModalOpen(true); } function openEditServer(idx: number) { setEditingServer((dns?.servers || [])[idx] || null); setEditingIndex(idx); setServerModalOpen(true); } function onServerConfirm(value: DnsServerValue) { mutate((tt) => { if (!tt.dns) return; const cfg = tt.dns as DnsConfig; if (!Array.isArray(cfg.servers)) cfg.servers = []; if (editingIndex == null) cfg.servers.push(value); else cfg.servers[editingIndex] = value; }); setServerModalOpen(false); } function deleteServer(idx: number) { mutate((tt) => { const cfg = tt.dns as DnsConfig | undefined; if (cfg?.servers) cfg.servers.splice(idx, 1); }); } function clearAllServers() { modal.confirm({ title: t('pages.xray.dns.clearAllTitle'), content: t('pages.xray.dns.clearAllConfirm'), okText: t('delete'), okButtonProps: { danger: true }, cancelText: t('cancel'), onOk: () => mutate((tt) => { if (tt.dns) (tt.dns as DnsConfig).servers = []; }), }); } function onPresetInstall(servers: string[]) { mutate((tt) => { if (tt.dns) (tt.dns as DnsConfig).servers = servers; }); setPresetsModalOpen(false); } const fakeDnsList = useMemo<{ key: number; ipPool: string; poolSize: number }[]>(() => { const list = Array.isArray(templateSettings?.fakedns) ? (templateSettings?.fakedns as FakednsRow[]) : []; return list.map((entry, idx) => ({ key: idx, ...entry })); }, [templateSettings?.fakedns]); const fakednsColumns = useFakednsColumns({ deleteFakedns, updateFakednsField }); function addFakedns() { mutate((tt) => { if (!Array.isArray(tt.fakedns)) tt.fakedns = []; (tt.fakedns as FakednsRow[]).push(DEFAULT_FAKEDNS()); }); } function deleteFakedns(idx: number) { mutate((tt) => { const list = tt.fakedns as FakednsRow[] | undefined; if (!list) return; list.splice(idx, 1); if (list.length === 0) tt.fakedns = null; }); } function updateFakednsField(idx: number, field: 'ipPool' | 'poolSize', value: string | number) { mutate((tt) => { const list = tt.fakedns as FakednsRow[] | undefined; if (!list?.[idx]) return; (list[idx] as unknown as Record)[field] = value; }); } const items = useMemo(() => { const out = [ { key: '1', label: catTabLabel(, t('pages.xray.generalConfigs'), isMobile), children: ( <> } /> {dnsEnabled && ( <> setDnsField('tag', e.target.value)} /> } /> setDnsField('clientIp', e.target.value, true)} /> } /> ({ value: s, label: s }))} onChange={(v) => setDnsField('queryStrategy', v)} /> } /> {( [ ['disableCache', 'pages.xray.dns.disableCache', 'pages.xray.dns.disableCacheDesc'], ['disableFallback', 'pages.xray.dns.disableFallback', 'pages.xray.dns.disableFallbackDesc'], ['disableFallbackIfMatch', 'pages.xray.dns.disableFallbackIfMatch', 'pages.xray.dns.disableFallbackIfMatchDesc'], ['enableParallelQuery', 'pages.xray.dns.enableParallelQuery', 'pages.xray.dns.enableParallelQueryDesc'], ['useSystemHosts', 'pages.xray.dns.useSystemHosts', 'pages.xray.dns.useSystemHostsDesc'], ['serveStale', 'pages.xray.dns.serveStale', 'pages.xray.dns.serveStaleDesc'], ] as const ).map(([field, titleKey, descKey]) => ( setDnsField(field as keyof DnsConfig, v as never)} /> } /> ))} setDnsField('serveExpiredTTL', Number(v) || 0)} /> } /> )} ), }, ]; if (dnsEnabled) { out.push({ key: 'hosts', label: catTabLabel(, t('pages.xray.dns.hosts'), isMobile), children: hostsList.length === 0 ? ( ) : ( {hostsList.map((row, idx) => (
{ const next = hostsList.map((r, i) => (i === idx ? { ...r, domain: e.target.value } : r)); syncHosts(next); }} />