Tunglies b8fbabae04
fix: frontend memory leaks — Monaco dispose, TQ cache eviction, useEffect cleanup
- Dispose Monaco editor instances on dialog close to prevent cycle leak
- Replace gcTime: Infinity with finite TTLs and evict orphaned subscription queryKeys
- Add missing useEffect cleanup for timers, move setTimeout out of useMemo
2026-04-05 23:10:45 +08:00

1098 lines
34 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import MonacoEditor from '@monaco-editor/react'
import { RestartAltRounded } from '@mui/icons-material'
import {
Box,
Button,
FormControl,
List,
ListItem,
ListItemText,
MenuItem,
Select,
styled,
TextField,
Typography,
} from '@mui/material'
import { invoke } from '@tauri-apps/api/core'
import { useLockFn } from 'ahooks'
import yaml from 'js-yaml'
import type { editor } from 'monaco-editor'
import type { Ref } from 'react'
import {
useCallback,
useEffect,
useImperativeHandle,
useReducer,
useRef,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import { BaseDialog, DialogRef, Switch } from '@/components/base'
import { useClash } from '@/hooks/use-clash'
import { showNotice } from '@/services/notice-service'
import { useThemeMode } from '@/services/states'
import { debugLog } from '@/utils/debug'
import getSystem from '@/utils/get-system'
const Item = styled(ListItem)(() => ({
padding: '5px 2px',
'& textarea': {
lineHeight: 1.5,
fontSize: 14,
resize: 'vertical',
},
}))
type NameserverPolicy = Record<string, any>
function parseNameserverPolicy(str: string): NameserverPolicy {
const result: NameserverPolicy = {}
if (!str) return result
const ruleRegex = /\s*([^=]+?)\s*=\s*([^,]+)(?:,|$)/g
let match: RegExpExecArray | null
while ((match = ruleRegex.exec(str)) !== null) {
const [, domainsPart, serversPart] = match
const domains = [domainsPart.trim()]
const servers = serversPart.split(';').map((s) => s.trim())
domains.forEach((domain) => {
result[domain] = servers
})
}
return result
}
function formatNameserverPolicy(policy: unknown): string {
if (!policy || typeof policy !== 'object') return ''
return Object.entries(policy as Record<string, unknown>)
.map(([domain, servers]) => {
const serversStr = Array.isArray(servers) ? servers.join(';') : servers
return `${domain}=${serversStr}`
})
.join(', ')
}
function formatHosts(hosts: unknown): string {
if (!hosts || typeof hosts !== 'object') return ''
const result: string[] = []
Object.entries(hosts as Record<string, unknown>).forEach(
([domain, value]) => {
if (Array.isArray(value)) {
const ipsStr = value.join(';')
result.push(`${domain}=${ipsStr}`)
} else {
result.push(`${domain}=${value}`)
}
},
)
return result.join(', ')
}
function parseHosts(str: string): NameserverPolicy {
const result: NameserverPolicy = {}
if (!str) return result
str.split(',').forEach((item) => {
const parts = item.trim().split('=')
if (parts.length < 2) return
const domain = parts[0].trim()
const valueStr = parts.slice(1).join('=').trim()
if (valueStr.includes(';')) {
result[domain] = valueStr
.split(';')
.map((s) => s.trim())
.filter(Boolean)
} else {
result[domain] = valueStr
}
})
return result
}
function parseList(str: string): string[] {
if (!str?.trim()) return []
return str
.split(',')
.map((item) => item.trim())
.filter(Boolean)
}
// 默认DNS配置
const DEFAULT_DNS_CONFIG = {
enable: true,
listen: ':53',
'enhanced-mode': 'fake-ip' as 'fake-ip' | 'redir-host',
'fake-ip-range': '198.18.0.1/16',
'fake-ip-filter-mode': 'blacklist' as 'blacklist' | 'whitelist',
'prefer-h3': false,
'respect-rules': false,
'use-hosts': false,
'use-system-hosts': false,
ipv6: true,
'fake-ip-filter': [
'*.lan',
'*.local',
'*.arpa',
'time.*.com',
'ntp.*.com',
'time.*.com',
'+.market.xiaomi.com',
'localhost.ptlogin2.qq.com',
'*.msftncsi.com',
'www.msftconnecttest.com',
],
'default-nameserver': [
'system',
'223.6.6.6',
'8.8.8.8',
'2400:3200::1',
'2001:4860:4860::8888',
],
nameserver: [
'8.8.8.8',
'https://doh.pub/dns-query',
'https://dns.alidns.com/dns-query',
],
fallback: [],
'nameserver-policy': {},
'proxy-server-nameserver': [
'https://doh.pub/dns-query',
'https://dns.alidns.com/dns-query',
'tls://223.5.5.5',
],
'direct-nameserver': [],
'direct-nameserver-follow-policy': false,
'fallback-filter': {
geoip: true,
'geoip-code': 'CN',
ipcidr: ['240.0.0.0/4', '0.0.0.0/32'],
domain: ['+.google.com', '+.facebook.com', '+.youtube.com'],
},
}
export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
const { t } = useTranslation()
const { clash, mutateClash } = useClash()
const themeMode = useThemeMode()
const [open, setOpen] = useState(false)
const [visualization, setVisualization] = useState(true)
const skipYamlSyncRef = useRef(false)
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null)
const [values, setValues] = useState<{
enable: boolean
listen: string
enhancedMode: 'fake-ip' | 'redir-host'
fakeIpRange: string
fakeIpFilterMode: 'blacklist' | 'whitelist'
preferH3: boolean
respectRules: boolean
useHosts: boolean
useSystemHosts: boolean
ipv6: boolean
fakeIpFilter: string
nameserver: string
fallback: string
defaultNameserver: string
proxyServerNameserver: string
directNameserver: string
directNameserverFollowPolicy: boolean
fallbackGeoip: boolean
fallbackGeoipCode: string
fallbackIpcidr: string
fallbackDomain: string
nameserverPolicy: string
hosts: string // hosts设置独立于dns
}>({
enable: DEFAULT_DNS_CONFIG.enable,
listen: DEFAULT_DNS_CONFIG.listen,
enhancedMode: DEFAULT_DNS_CONFIG['enhanced-mode'],
fakeIpRange: DEFAULT_DNS_CONFIG['fake-ip-range'],
fakeIpFilterMode: DEFAULT_DNS_CONFIG['fake-ip-filter-mode'],
preferH3: DEFAULT_DNS_CONFIG['prefer-h3'],
respectRules: DEFAULT_DNS_CONFIG['respect-rules'],
useHosts: DEFAULT_DNS_CONFIG['use-hosts'],
useSystemHosts: DEFAULT_DNS_CONFIG['use-system-hosts'],
ipv6: DEFAULT_DNS_CONFIG.ipv6,
fakeIpFilter: DEFAULT_DNS_CONFIG['fake-ip-filter'].join(', '),
defaultNameserver: DEFAULT_DNS_CONFIG['default-nameserver'].join(', '),
nameserver: DEFAULT_DNS_CONFIG.nameserver.join(', '),
fallback: DEFAULT_DNS_CONFIG.fallback.join(', '),
proxyServerNameserver:
DEFAULT_DNS_CONFIG['proxy-server-nameserver']?.join(', ') || '',
directNameserver: DEFAULT_DNS_CONFIG['direct-nameserver']?.join(', ') || '',
directNameserverFollowPolicy:
DEFAULT_DNS_CONFIG['direct-nameserver-follow-policy'] || false,
fallbackGeoip: DEFAULT_DNS_CONFIG['fallback-filter'].geoip,
fallbackGeoipCode: DEFAULT_DNS_CONFIG['fallback-filter']['geoip-code'],
fallbackIpcidr:
DEFAULT_DNS_CONFIG['fallback-filter'].ipcidr?.join(', ') || '',
fallbackDomain:
DEFAULT_DNS_CONFIG['fallback-filter'].domain?.join(', ') || '',
nameserverPolicy: '',
hosts: '',
})
// 用于YAML编辑模式
const [yamlContent, setYamlContent] = useReducer(
(_: string, next: string) => next,
'',
)
// 从配置对象更新表单值
const updateValuesFromConfig = useCallback(
(config: any) => {
if (!config) return
const dnsConfig = config.dns || {}
const hostsConfig = config.hosts || {}
const enhancedMode =
dnsConfig['enhanced-mode'] || DEFAULT_DNS_CONFIG['enhanced-mode']
const validEnhancedMode =
enhancedMode === 'fake-ip' || enhancedMode === 'redir-host'
? enhancedMode
: DEFAULT_DNS_CONFIG['enhanced-mode']
const fakeIpFilterMode =
dnsConfig['fake-ip-filter-mode'] ||
DEFAULT_DNS_CONFIG['fake-ip-filter-mode']
const validFakeIpFilterMode =
fakeIpFilterMode === 'blacklist' || fakeIpFilterMode === 'whitelist'
? fakeIpFilterMode
: DEFAULT_DNS_CONFIG['fake-ip-filter-mode']
setValues({
enable: dnsConfig.enable ?? DEFAULT_DNS_CONFIG.enable,
listen: dnsConfig.listen ?? DEFAULT_DNS_CONFIG.listen,
enhancedMode: validEnhancedMode,
fakeIpRange:
dnsConfig['fake-ip-range'] ?? DEFAULT_DNS_CONFIG['fake-ip-range'],
fakeIpFilterMode: validFakeIpFilterMode,
preferH3: dnsConfig['prefer-h3'] ?? DEFAULT_DNS_CONFIG['prefer-h3'],
respectRules:
dnsConfig['respect-rules'] ?? DEFAULT_DNS_CONFIG['respect-rules'],
useHosts: dnsConfig['use-hosts'] ?? DEFAULT_DNS_CONFIG['use-hosts'],
useSystemHosts:
dnsConfig['use-system-hosts'] ??
DEFAULT_DNS_CONFIG['use-system-hosts'],
ipv6: dnsConfig.ipv6 ?? DEFAULT_DNS_CONFIG.ipv6,
fakeIpFilter:
dnsConfig['fake-ip-filter']?.join(', ') ??
DEFAULT_DNS_CONFIG['fake-ip-filter'].join(', '),
nameserver:
dnsConfig.nameserver?.join(', ') ??
DEFAULT_DNS_CONFIG.nameserver.join(', '),
fallback:
dnsConfig.fallback?.join(', ') ??
DEFAULT_DNS_CONFIG.fallback.join(', '),
defaultNameserver:
dnsConfig['default-nameserver']?.join(', ') ??
DEFAULT_DNS_CONFIG['default-nameserver'].join(', '),
proxyServerNameserver:
dnsConfig['proxy-server-nameserver']?.join(', ') ??
(DEFAULT_DNS_CONFIG['proxy-server-nameserver']?.join(', ') || ''),
directNameserver:
dnsConfig['direct-nameserver']?.join(', ') ??
(DEFAULT_DNS_CONFIG['direct-nameserver']?.join(', ') || ''),
directNameserverFollowPolicy:
dnsConfig['direct-nameserver-follow-policy'] ??
DEFAULT_DNS_CONFIG['direct-nameserver-follow-policy'],
fallbackGeoip:
dnsConfig['fallback-filter']?.geoip ??
DEFAULT_DNS_CONFIG['fallback-filter'].geoip,
fallbackGeoipCode:
dnsConfig['fallback-filter']?.['geoip-code'] ??
DEFAULT_DNS_CONFIG['fallback-filter']['geoip-code'],
fallbackIpcidr:
dnsConfig['fallback-filter']?.ipcidr?.join(', ') ??
DEFAULT_DNS_CONFIG['fallback-filter'].ipcidr.join(', '),
fallbackDomain:
dnsConfig['fallback-filter']?.domain?.join(', ') ??
DEFAULT_DNS_CONFIG['fallback-filter'].domain.join(', '),
nameserverPolicy:
formatNameserverPolicy(dnsConfig['nameserver-policy']) || '',
hosts: formatHosts(hostsConfig) || '',
})
},
[setValues],
)
const generateDnsConfig = useCallback(() => {
const dnsConfig: any = {
enable: values.enable,
listen: values.listen,
'enhanced-mode': values.enhancedMode,
'fake-ip-range': values.fakeIpRange,
'fake-ip-filter-mode': values.fakeIpFilterMode,
'prefer-h3': values.preferH3,
'respect-rules': values.respectRules,
'use-hosts': values.useHosts,
'use-system-hosts': values.useSystemHosts,
ipv6: values.ipv6,
'fake-ip-filter': parseList(values.fakeIpFilter),
'default-nameserver': parseList(values.defaultNameserver),
nameserver: parseList(values.nameserver),
'direct-nameserver-follow-policy': values.directNameserverFollowPolicy,
'fallback-filter': {
geoip: values.fallbackGeoip,
'geoip-code': values.fallbackGeoipCode,
ipcidr: parseList(values.fallbackIpcidr),
domain: parseList(values.fallbackDomain),
},
fallback: parseList(values.fallback),
'proxy-server-nameserver': parseList(values.proxyServerNameserver),
'direct-nameserver': parseList(values.directNameserver),
}
const policy = parseNameserverPolicy(values.nameserverPolicy)
if (Object.keys(policy).length > 0) {
dnsConfig['nameserver-policy'] = policy
}
return dnsConfig
}, [values])
const updateYamlFromValues = useCallback(() => {
const config: Record<string, any> = {}
const dnsConfig = generateDnsConfig()
if (Object.keys(dnsConfig).length > 0) {
config.dns = dnsConfig
}
const hosts = parseHosts(values.hosts)
if (Object.keys(hosts).length > 0) {
config.hosts = hosts
}
setYamlContent(yaml.dump(config, { forceQuotes: true }))
}, [generateDnsConfig, setYamlContent, values.hosts])
// 重置为默认值
const resetToDefaults = useCallback(() => {
setValues({
enable: DEFAULT_DNS_CONFIG.enable,
listen: DEFAULT_DNS_CONFIG.listen,
enhancedMode: DEFAULT_DNS_CONFIG['enhanced-mode'],
fakeIpRange: DEFAULT_DNS_CONFIG['fake-ip-range'],
fakeIpFilterMode: DEFAULT_DNS_CONFIG['fake-ip-filter-mode'],
preferH3: DEFAULT_DNS_CONFIG['prefer-h3'],
respectRules: DEFAULT_DNS_CONFIG['respect-rules'],
useHosts: DEFAULT_DNS_CONFIG['use-hosts'],
useSystemHosts: DEFAULT_DNS_CONFIG['use-system-hosts'],
ipv6: DEFAULT_DNS_CONFIG.ipv6,
fakeIpFilter: DEFAULT_DNS_CONFIG['fake-ip-filter'].join(', '),
defaultNameserver: DEFAULT_DNS_CONFIG['default-nameserver'].join(', '),
nameserver: DEFAULT_DNS_CONFIG.nameserver.join(', '),
fallback: DEFAULT_DNS_CONFIG.fallback.join(', '),
proxyServerNameserver:
DEFAULT_DNS_CONFIG['proxy-server-nameserver']?.join(', ') || '',
directNameserver:
DEFAULT_DNS_CONFIG['direct-nameserver']?.join(', ') || '',
directNameserverFollowPolicy:
DEFAULT_DNS_CONFIG['direct-nameserver-follow-policy'] || false,
fallbackGeoip: DEFAULT_DNS_CONFIG['fallback-filter'].geoip,
fallbackGeoipCode: DEFAULT_DNS_CONFIG['fallback-filter']['geoip-code'],
fallbackIpcidr:
DEFAULT_DNS_CONFIG['fallback-filter'].ipcidr?.join(', ') || '',
fallbackDomain:
DEFAULT_DNS_CONFIG['fallback-filter'].domain?.join(', ') || '',
nameserverPolicy: '',
hosts: '',
})
updateYamlFromValues()
}, [setValues, updateYamlFromValues])
// 从YAML更新表单值
const updateValuesFromYaml = useCallback(() => {
try {
const parsedYaml = yaml.load(yamlContent) as any
if (!parsedYaml) return
skipYamlSyncRef.current = true
updateValuesFromConfig(parsedYaml)
} catch {
showNotice.error('settings.modals.dns.errors.invalidYaml')
}
}, [yamlContent, updateValuesFromConfig])
useEffect(() => {
if (skipYamlSyncRef.current) {
skipYamlSyncRef.current = false
return
}
updateYamlFromValues()
}, [updateYamlFromValues])
const latestUpdateValuesFromYamlRef = useRef(updateValuesFromYaml)
const latestUpdateYamlFromValuesRef = useRef(updateYamlFromValues)
useEffect(() => {
latestUpdateValuesFromYamlRef.current = updateValuesFromYaml
latestUpdateYamlFromValuesRef.current = updateYamlFromValues
}, [updateValuesFromYaml, updateYamlFromValues])
useEffect(() => {
if (visualization) {
latestUpdateValuesFromYamlRef.current()
} else {
latestUpdateYamlFromValuesRef.current()
}
}, [visualization])
useEffect(() => {
return () => {
editorRef.current?.dispose()
editorRef.current = null
}
}, [])
const initDnsConfig = useCallback(async () => {
try {
const dnsConfigExists = await invoke<boolean>(
'check_dns_config_exists',
{},
)
if (dnsConfigExists) {
const dnsConfig = await invoke<string>('get_dns_config_content', {})
const config = yaml.load(dnsConfig) as any
updateValuesFromConfig(config)
setYamlContent(dnsConfig)
} else {
resetToDefaults()
}
} catch (err) {
console.error('Failed to initialize DNS config', err)
resetToDefaults()
}
}, [resetToDefaults, setYamlContent, updateValuesFromConfig])
useImperativeHandle(
ref,
() => ({
open: () => {
setOpen(true)
void initDnsConfig()
},
close: () => setOpen(false),
}),
[initDnsConfig],
)
// 生成DNS配置对象
// 处理保存操作
const onSave = useLockFn(async () => {
try {
let config: Record<string, any>
if (visualization) {
// 使用表单值生成配置
config = {}
const dnsConfig = generateDnsConfig()
if (Object.keys(dnsConfig).length > 0) {
config.dns = dnsConfig
}
const hosts = parseHosts(values.hosts)
if (Object.keys(hosts).length > 0) {
config.hosts = hosts
}
} else {
// 使用YAML编辑器的值
const parsedConfig = yaml.load(yamlContent)
if (typeof parsedConfig !== 'object' || parsedConfig === null) {
throw new Error(t('settings.modals.dns.errors.invalid'))
}
config = parsedConfig as Record<string, any>
}
// 保存配置
await invoke('save_dns_config', { dnsConfig: config })
// 验证配置
const [isValid, errorMsg] = await invoke<[boolean, string]>(
'validate_dns_config',
{},
)
if (!isValid) {
let cleanErrorMsg = errorMsg
// 提取关键错误信息
if (errorMsg.includes('level=error')) {
const errorLines = errorMsg
.split('\n')
.filter(
(line) =>
line.includes('level=error') ||
line.includes('level=fatal') ||
line.includes('failed'),
)
if (errorLines.length > 0) {
cleanErrorMsg = errorLines
.map((line) => {
const msgMatch = line.match(/msg="([^"]+)"/)
return msgMatch ? msgMatch[1] : line
})
.join(', ')
}
}
showNotice.error(
'settings.modals.dns.messages.configError',
cleanErrorMsg,
)
return
}
// 如果DNS开关当前是打开的则需要应用新的DNS配置
if (clash?.dns?.enable) {
await invoke('apply_dns_config', { apply: true })
mutateClash()
}
setOpen(false)
showNotice.success('settings.modals.dns.messages.saved')
} catch (err) {
showNotice.error(err)
}
})
// YAML编辑器内容变更处理
const handleYamlChange = (value?: string) => {
setYamlContent(value || '')
// 允许YAML编辑后立即分析和更新表单值
try {
const config = yaml.load(value || '') as any
if (config && typeof config === 'object') {
setTimeout(() => {
updateValuesFromConfig(config)
}, 300)
}
} catch (err) {
debugLog('YAML解析错误忽略自动更新', err)
}
}
// 处理表单值变化
const handleChange = (field: string) => (event: any) => {
const value =
event.target.type === 'checkbox'
? event.target.checked
: event.target.value
setValues((prev) => {
const newValues = {
...prev,
[field]: value,
}
// 当可视化编辑模式下的值变化时自动更新YAML
if (visualization) {
setTimeout(() => {
updateYamlFromValues()
}, 0)
}
return newValues
})
}
return (
<BaseDialog
open={open}
disableEnforceFocus={!visualization}
title={
<Box display="flex" justifyContent="space-between" alignItems="center">
{t('settings.modals.dns.dialog.title')}
<Box display="flex" alignItems="center" gap={1}>
<Button
variant="outlined"
size="small"
color="warning"
startIcon={<RestartAltRounded />}
onClick={resetToDefaults}
>
{t('shared.actions.resetToDefault')}
</Button>
<Button
variant="contained"
size="small"
onClick={() => {
setVisualization((prev) => !prev)
}}
>
{visualization
? t('shared.editorModes.advanced')
: t('shared.editorModes.visualization')}
</Button>
</Box>
</Box>
}
contentSx={{
width: 550,
overflow: 'auto',
...(visualization
? {}
: { padding: '0 24px', display: 'flex', flexDirection: 'column' }),
}}
okBtn={t('shared.actions.save')}
cancelBtn={t('shared.actions.cancel')}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
onOk={onSave}
>
{/* Warning message */}
<Typography
variant="body2"
color="warning.main"
sx={{ mb: 2, mt: 0, fontStyle: 'italic' }}
>
{t('settings.modals.dns.dialog.warning')}
</Typography>
{visualization ? (
<List>
<Typography
variant="subtitle1"
sx={{ mt: 1, mb: 1, fontWeight: 'bold' }}
>
{t('settings.modals.dns.sections.general')}
</Typography>
<Item>
<ListItemText primary={t('settings.modals.dns.fields.enable')} />
<Switch
edge="end"
checked={values.enable}
onChange={handleChange('enable')}
/>
</Item>
<Item>
<ListItemText primary={t('settings.modals.dns.fields.listen')} />
<TextField
size="small"
autoComplete="off"
spellCheck="false"
value={values.listen}
onChange={handleChange('listen')}
placeholder=":53"
sx={{ width: 150 }}
/>
</Item>
<Item>
<ListItemText
primary={t('settings.modals.dns.fields.enhancedMode')}
/>
<FormControl size="small" sx={{ width: 150 }}>
<Select
value={values.enhancedMode}
onChange={handleChange('enhancedMode')}
>
<MenuItem value="fake-ip">fake-ip</MenuItem>
<MenuItem value="redir-host">redir-host</MenuItem>
</Select>
</FormControl>
</Item>
<Item>
<ListItemText
primary={t('settings.modals.dns.fields.fakeIpRange')}
/>
<TextField
size="small"
autoComplete="off"
spellCheck="false"
value={values.fakeIpRange}
onChange={handleChange('fakeIpRange')}
placeholder="198.18.0.1/16"
sx={{ width: 150 }}
/>
</Item>
<Item>
<ListItemText
primary={t('settings.modals.dns.fields.fakeIpFilterMode')}
/>
<FormControl size="small" sx={{ width: 150 }}>
<Select
value={values.fakeIpFilterMode}
onChange={handleChange('fakeIpFilterMode')}
>
<MenuItem value="blacklist">blacklist</MenuItem>
<MenuItem value="whitelist">whitelist</MenuItem>
</Select>
</FormControl>
</Item>
<Item>
<ListItemText
primary={t('settings.modals.dns.fields.ipv6.label')}
secondary={t('settings.modals.dns.fields.ipv6.description')}
/>
<Switch
edge="end"
checked={values.ipv6}
onChange={handleChange('ipv6')}
/>
</Item>
<Item>
<ListItemText
primary={t('settings.modals.dns.fields.preferH3.label')}
secondary={t('settings.modals.dns.fields.preferH3.description')}
/>
<Switch
edge="end"
checked={values.preferH3}
onChange={handleChange('preferH3')}
/>
</Item>
<Item>
<ListItemText
primary={t('settings.modals.dns.fields.respectRules.label')}
secondary={t(
'settings.modals.dns.fields.respectRules.description',
)}
/>
<Switch
edge="end"
checked={values.respectRules}
onChange={handleChange('respectRules')}
/>
</Item>
<Item>
<ListItemText
primary={t('settings.modals.dns.fields.useHosts.label')}
secondary={t('settings.modals.dns.fields.useHosts.description')}
/>
<Switch
edge="end"
checked={values.useHosts}
onChange={handleChange('useHosts')}
/>
</Item>
<Item>
<ListItemText
primary={t('settings.modals.dns.fields.useSystemHosts.label')}
secondary={t(
'settings.modals.dns.fields.useSystemHosts.description',
)}
/>
<Switch
edge="end"
checked={values.useSystemHosts}
onChange={handleChange('useSystemHosts')}
/>
</Item>
<Item>
<ListItemText
primary={t('settings.modals.dns.fields.directPolicy.label')}
secondary={t(
'settings.modals.dns.fields.directPolicy.description',
)}
/>
<Switch
edge="end"
checked={values.directNameserverFollowPolicy}
onChange={handleChange('directNameserverFollowPolicy')}
/>
</Item>
<Item sx={{ flexDirection: 'column', alignItems: 'flex-start' }}>
<ListItemText
primary={t('settings.modals.dns.fields.defaultNameserver.label')}
secondary={t(
'settings.modals.dns.fields.defaultNameserver.description',
)}
/>
<TextField
fullWidth
multiline
minRows={2}
maxRows={3}
size="small"
spellCheck="false"
value={values.defaultNameserver}
onChange={handleChange('defaultNameserver')}
placeholder="system,223.6.6.6, 8.8.8.8, 2400:3200::1, 2001:4860:4860::8888"
/>
</Item>
<Item sx={{ flexDirection: 'column', alignItems: 'flex-start' }}>
<ListItemText
primary={t('settings.modals.dns.fields.nameserver.label')}
secondary={t('settings.modals.dns.fields.nameserver.description')}
/>
<TextField
fullWidth
multiline
minRows={2}
maxRows={4}
size="small"
spellCheck="false"
value={values.nameserver}
onChange={handleChange('nameserver')}
placeholder="8.8.8.8, https://doh.pub/dns-query, https://dns.alidns.com/dns-query"
/>
</Item>
<Item sx={{ flexDirection: 'column', alignItems: 'flex-start' }}>
<ListItemText
primary={t('settings.modals.dns.fields.fallback.label')}
secondary={t('settings.modals.dns.fields.fallback.description')}
/>
<TextField
fullWidth
multiline
minRows={2}
maxRows={4}
size="small"
spellCheck="false"
value={values.fallback}
onChange={handleChange('fallback')}
placeholder="https://dns.alidns.com/dns-query, https://dns.google/dns-query, https://cloudflare-dns.com/dns-query"
/>
</Item>
<Item sx={{ flexDirection: 'column', alignItems: 'flex-start' }}>
<ListItemText
primary={t('settings.modals.dns.fields.proxy.label')}
secondary={t('settings.modals.dns.fields.proxy.description')}
/>
<TextField
fullWidth
multiline
minRows={2}
maxRows={3}
size="small"
spellCheck="false"
value={values.proxyServerNameserver}
onChange={handleChange('proxyServerNameserver')}
placeholder="https://doh.pub/dns-query, https://dns.alidns.com/dns-query"
/>
</Item>
<Item sx={{ flexDirection: 'column', alignItems: 'flex-start' }}>
<ListItemText
primary={t('settings.modals.dns.fields.directNameserver.label')}
secondary={t(
'settings.modals.dns.fields.directNameserver.description',
)}
/>
<TextField
fullWidth
multiline
minRows={2}
maxRows={3}
size="small"
spellCheck="false"
value={values.directNameserver}
onChange={handleChange('directNameserver')}
placeholder="system, 223.6.6.6"
/>
</Item>
<Item sx={{ flexDirection: 'column', alignItems: 'flex-start' }}>
<ListItemText
primary={t('settings.modals.dns.fields.fakeIpFilter.label')}
secondary={t(
'settings.modals.dns.fields.fakeIpFilter.description',
)}
/>
<TextField
fullWidth
multiline
minRows={2}
maxRows={4}
size="small"
spellCheck="false"
value={values.fakeIpFilter}
onChange={handleChange('fakeIpFilter')}
placeholder="*.lan, *.local, localhost.ptlogin2.qq.com"
/>
</Item>
<Item sx={{ flexDirection: 'column', alignItems: 'flex-start' }}>
<ListItemText
primary={t('settings.modals.dns.fields.nameserverPolicy.label')}
secondary={t(
'settings.modals.dns.fields.nameserverPolicy.description',
)}
/>
<TextField
fullWidth
multiline
minRows={2}
maxRows={4}
size="small"
spellCheck="false"
value={values.nameserverPolicy}
onChange={handleChange('nameserverPolicy')}
placeholder="+.arpa=10.0.0.1, rule-set:cn=https://doh.pub/dns-query;https://dns.alidns.com/dns-query"
/>
</Item>
<Typography
variant="subtitle2"
sx={{ mt: 2, mb: 1, fontWeight: 'bold' }}
>
{t('settings.modals.dns.sections.fallbackFilter')}
</Typography>
<Item>
<ListItemText
primary={t('settings.modals.dns.fields.geoipFiltering.label')}
secondary={t(
'settings.modals.dns.fields.geoipFiltering.description',
)}
/>
<Switch
edge="end"
checked={values.fallbackGeoip}
onChange={handleChange('fallbackGeoip')}
/>
</Item>
<Item>
<ListItemText primary={t('settings.modals.dns.fields.geoipCode')} />
<TextField
size="small"
autoComplete="off"
spellCheck="false"
value={values.fallbackGeoipCode}
onChange={handleChange('fallbackGeoipCode')}
placeholder="CN"
sx={{ width: 100 }}
/>
</Item>
<Item sx={{ flexDirection: 'column', alignItems: 'flex-start' }}>
<ListItemText
primary={t('settings.modals.dns.fields.fallbackIpCidr.label')}
secondary={t(
'settings.modals.dns.fields.fallbackIpCidr.description',
)}
/>
<TextField
fullWidth
multiline
minRows={2}
maxRows={3}
size="small"
spellCheck="false"
value={values.fallbackIpcidr}
onChange={handleChange('fallbackIpcidr')}
placeholder="240.0.0.0/4, 127.0.0.1/8"
/>
</Item>
<Item sx={{ flexDirection: 'column', alignItems: 'flex-start' }}>
<ListItemText
primary={t('settings.modals.dns.fields.fallbackDomain.label')}
secondary={t(
'settings.modals.dns.fields.fallbackDomain.description',
)}
/>
<TextField
fullWidth
multiline
minRows={2}
maxRows={3}
size="small"
spellCheck="false"
value={values.fallbackDomain}
onChange={handleChange('fallbackDomain')}
placeholder="+.google.com, +.facebook.com, +.youtube.com"
/>
</Item>
{/* Hosts 配置部分 */}
<Typography
variant="subtitle1"
sx={{ mt: 3, mb: 0, fontWeight: 'bold' }}
>
{t('settings.modals.dns.sections.hosts')}
</Typography>
<Item sx={{ flexDirection: 'column', alignItems: 'flex-start' }}>
<ListItemText
primary={t('settings.modals.dns.fields.hosts.label')}
secondary={t('settings.modals.dns.fields.hosts.description')}
/>
<TextField
fullWidth
multiline
minRows={2}
maxRows={4}
size="small"
spellCheck="false"
value={values.hosts}
onChange={handleChange('hosts')}
placeholder="*.clash.dev=127.0.0.1, alpha.clash.dev=::1, test.com=1.1.1.1;2.2.2.2, baidu.com=google.com"
/>
</Item>
</List>
) : (
<MonacoEditor
height="100vh"
language="yaml"
value={yamlContent}
theme={themeMode === 'light' ? 'light' : 'vs-dark'}
className="flex-grow"
onMount={(editorInstance) => {
editorRef.current = editorInstance
}}
options={{
tabSize: 2,
minimap: {
enabled: document.documentElement.clientWidth >= 1500,
},
mouseWheelZoom: true,
quickSuggestions: {
strings: true,
comments: true,
other: true,
},
padding: {
top: 33,
},
fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${
getSystem() === 'windows' ? ', twemoji mozilla' : ''
}`,
fontLigatures: false,
smoothScrolling: true,
}}
onChange={handleYamlChange}
/>
)}
</BaseDialog>
)
}