mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-05-15 10:36:30 +00:00
feat(frontend): rebuild xray DNS section to match main branch
DnsTab now exposes every field the legacy panel did — top-level toggles (tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback strategy, client subnet), the servers table with per-row strategy and domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new DnsServerModal covers the full add/edit flow and collapses to a bare string when the user only sets an address — matching the wire shape the legacy form emits for plain DNS entries like "8.8.8.8". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
36e75143fa
commit
d8721093e4
2 changed files with 504 additions and 74 deletions
168
frontend/src/pages/xray/DnsServerModal.vue
Normal file
168
frontend/src/pages/xray/DnsServerModal.vue
Normal file
|
|
@ -0,0 +1,168 @@
|
||||||
|
<script setup>
|
||||||
|
import { computed, reactive, ref, watch } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { PlusOutlined, MinusOutlined } from '@ant-design/icons-vue';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
// DNS server add/edit modal — mirrors web/html/modals/xray_dns_modal.html.
|
||||||
|
// The legacy panel allowed both string-form ("8.8.8.8") and object-form
|
||||||
|
// servers; we always edit as an object and the parent can decide
|
||||||
|
// whether to collapse to a string when nothing besides address is set.
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
open: { type: Boolean, default: false },
|
||||||
|
server: { type: [Object, String, null], default: null },
|
||||||
|
isEdit: { type: Boolean, default: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:open', 'confirm']);
|
||||||
|
|
||||||
|
const DEFAULT_SERVER = () => ({
|
||||||
|
address: 'localhost',
|
||||||
|
port: 53,
|
||||||
|
domains: [],
|
||||||
|
expectIPs: [],
|
||||||
|
unexpectedIPs: [],
|
||||||
|
queryStrategy: 'UseIP',
|
||||||
|
skipFallback: true,
|
||||||
|
disableCache: false,
|
||||||
|
finalQuery: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const STRATEGIES = ['UseSystem', 'UseIP', 'UseIPv4', 'UseIPv6'];
|
||||||
|
|
||||||
|
const form = reactive(DEFAULT_SERVER());
|
||||||
|
|
||||||
|
watch(() => props.open, (next) => {
|
||||||
|
if (!next) return;
|
||||||
|
Object.assign(form, DEFAULT_SERVER());
|
||||||
|
if (props.server == null) return;
|
||||||
|
if (typeof props.server === 'string') {
|
||||||
|
form.address = props.server;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Object — copy fields, defaulting missing arrays to empty.
|
||||||
|
Object.assign(form, {
|
||||||
|
...DEFAULT_SERVER(),
|
||||||
|
...props.server,
|
||||||
|
domains: [...(props.server.domains || [])],
|
||||||
|
expectIPs: [...(props.server.expectIPs || [])],
|
||||||
|
unexpectedIPs: [...(props.server.unexpectedIPs || [])],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function close() { emit('update:open', false); }
|
||||||
|
|
||||||
|
function onOk() {
|
||||||
|
// If the user only set an address (everything else default), emit a
|
||||||
|
// bare string — that's the wire shape the legacy panel uses for
|
||||||
|
// servers like "8.8.8.8" and keeps the JSON tidy.
|
||||||
|
const isPlain = form.domains.length === 0
|
||||||
|
&& form.expectIPs.length === 0
|
||||||
|
&& form.unexpectedIPs.length === 0
|
||||||
|
&& form.port === 53
|
||||||
|
&& form.queryStrategy === 'UseIP'
|
||||||
|
&& form.skipFallback === true
|
||||||
|
&& form.disableCache === false
|
||||||
|
&& form.finalQuery === false;
|
||||||
|
if (isPlain) {
|
||||||
|
emit('confirm', form.address);
|
||||||
|
} else {
|
||||||
|
emit('confirm', {
|
||||||
|
address: form.address,
|
||||||
|
port: form.port,
|
||||||
|
domains: [...form.domains].filter(Boolean),
|
||||||
|
expectIPs: [...form.expectIPs].filter(Boolean),
|
||||||
|
unexpectedIPs: [...form.unexpectedIPs].filter(Boolean),
|
||||||
|
queryStrategy: form.queryStrategy,
|
||||||
|
skipFallback: form.skipFallback,
|
||||||
|
disableCache: form.disableCache,
|
||||||
|
finalQuery: form.finalQuery,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = computed(() =>
|
||||||
|
props.isEdit ? t('pages.xray.dns.edit') : t('pages.xray.dns.add'),
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<a-modal
|
||||||
|
:open="open"
|
||||||
|
:title="title"
|
||||||
|
:ok-text="t('confirm')"
|
||||||
|
:cancel-text="t('close')"
|
||||||
|
:mask-closable="false"
|
||||||
|
@ok="onOk"
|
||||||
|
@cancel="close"
|
||||||
|
>
|
||||||
|
<a-form :colon="false" :label-col="{ md: { span: 8 } }" :wrapper-col="{ md: { span: 14 } }">
|
||||||
|
<a-form-item :label="t('pages.inbounds.address')">
|
||||||
|
<a-input v-model:value="form.address" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item :label="t('pages.inbounds.port')">
|
||||||
|
<a-input-number v-model:value="form.port" :min="1" :max="65535" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item :label="t('pages.xray.dns.strategy')">
|
||||||
|
<a-select v-model:value="form.queryStrategy" :style="{ width: '100%' }">
|
||||||
|
<a-select-option v-for="s in STRATEGIES" :key="s" :value="s">{{ s }}</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-divider :style="{ margin: '5px 0' }" />
|
||||||
|
|
||||||
|
<a-form-item :label="t('pages.xray.dns.domains')">
|
||||||
|
<a-button size="small" type="primary" @click="form.domains.push('')">
|
||||||
|
<template #icon><PlusOutlined /></template>
|
||||||
|
</a-button>
|
||||||
|
<template v-for="(_, idx) in form.domains" :key="`d${idx}`">
|
||||||
|
<a-input v-model:value="form.domains[idx]" :style="{ marginTop: '4px' }">
|
||||||
|
<template #addonAfter>
|
||||||
|
<MinusOutlined @click="form.domains.splice(idx, 1)" />
|
||||||
|
</template>
|
||||||
|
</a-input>
|
||||||
|
</template>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item :label="t('pages.xray.dns.expectIPs')">
|
||||||
|
<a-button size="small" type="primary" @click="form.expectIPs.push('')">
|
||||||
|
<template #icon><PlusOutlined /></template>
|
||||||
|
</a-button>
|
||||||
|
<template v-for="(_, idx) in form.expectIPs" :key="`e${idx}`">
|
||||||
|
<a-input v-model:value="form.expectIPs[idx]" :style="{ marginTop: '4px' }">
|
||||||
|
<template #addonAfter>
|
||||||
|
<MinusOutlined @click="form.expectIPs.splice(idx, 1)" />
|
||||||
|
</template>
|
||||||
|
</a-input>
|
||||||
|
</template>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item :label="t('pages.xray.dns.unexpectIPs')">
|
||||||
|
<a-button size="small" type="primary" @click="form.unexpectedIPs.push('')">
|
||||||
|
<template #icon><PlusOutlined /></template>
|
||||||
|
</a-button>
|
||||||
|
<template v-for="(_, idx) in form.unexpectedIPs" :key="`u${idx}`">
|
||||||
|
<a-input v-model:value="form.unexpectedIPs[idx]" :style="{ marginTop: '4px' }">
|
||||||
|
<template #addonAfter>
|
||||||
|
<MinusOutlined @click="form.unexpectedIPs.splice(idx, 1)" />
|
||||||
|
</template>
|
||||||
|
</a-input>
|
||||||
|
</template>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-divider :style="{ margin: '5px 0' }" />
|
||||||
|
|
||||||
|
<a-form-item label="Skip fallback">
|
||||||
|
<a-switch v-model:checked="form.skipFallback" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item :label="t('pages.xray.dns.disableCache')">
|
||||||
|
<a-switch v-model:checked="form.disableCache" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="Final query">
|
||||||
|
<a-switch v-model:checked="form.finalQuery" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</a-modal>
|
||||||
|
</template>
|
||||||
|
|
@ -1,27 +1,46 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import {
|
||||||
|
PlusOutlined,
|
||||||
|
MoreOutlined,
|
||||||
|
EditOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
} from '@ant-design/icons-vue';
|
||||||
|
|
||||||
// Compact DNS editor — a master enable switch plus a JSON textarea
|
import SettingListItem from '@/components/SettingListItem.vue';
|
||||||
// for the full dns + fakedns trees. The legacy panel had a
|
import DnsServerModal from './DnsServerModal.vue';
|
||||||
// dedicated DNS-server modal + fakedns row editor; both are large
|
|
||||||
// enough to deserve their own commits. For now this gives users a
|
const { t } = useI18n();
|
||||||
// working path to edit DNS settings without leaving the structured
|
|
||||||
// page.
|
// Structured DNS editor — mirrors web/html/settings/xray/dns.html.
|
||||||
|
// Master enable switch + general DNS options + per-server table with
|
||||||
|
// add/edit/delete (modal flow), plus a Fake DNS table. Both lists
|
||||||
|
// flow through templateSettings.dns / .fakedns reactively so the
|
||||||
|
// useXraySetting composable picks every edit up via its deep watch.
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
templateSettings: { type: Object, default: null },
|
templateSettings: { type: Object, default: null },
|
||||||
});
|
});
|
||||||
|
|
||||||
const enableDns = computed({
|
const STRATEGIES = ['UseSystem', 'UseIP', 'UseIPv4', 'UseIPv6'];
|
||||||
|
|
||||||
|
// ============== Master toggle ==============
|
||||||
|
const enableDNS = computed({
|
||||||
get: () => !!props.templateSettings?.dns,
|
get: () => !!props.templateSettings?.dns,
|
||||||
set: (next) => {
|
set: (next) => {
|
||||||
if (!props.templateSettings) return;
|
if (!props.templateSettings) return;
|
||||||
if (next) {
|
if (next) {
|
||||||
props.templateSettings.dns = {
|
props.templateSettings.dns = {
|
||||||
servers: [],
|
|
||||||
queryStrategy: 'UseIP',
|
|
||||||
tag: 'dns_inbound',
|
tag: 'dns_inbound',
|
||||||
|
clientIp: '',
|
||||||
|
queryStrategy: 'UseIP',
|
||||||
|
disableCache: false,
|
||||||
|
disableFallback: false,
|
||||||
|
disableFallbackIfMatch: false,
|
||||||
|
useSystemHosts: false,
|
||||||
enableParallelQuery: false,
|
enableParallelQuery: false,
|
||||||
|
servers: [],
|
||||||
};
|
};
|
||||||
props.templateSettings.fakedns = null;
|
props.templateSettings.fakedns = null;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -31,81 +50,324 @@ const enableDns = computed({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const dnsJson = computed({
|
// ============== Field bridges ==============
|
||||||
get: () => {
|
function dnsField(field, fallback) {
|
||||||
if (!props.templateSettings?.dns) return '';
|
return computed({
|
||||||
try { return JSON.stringify(props.templateSettings.dns, null, 2); }
|
get: () => props.templateSettings?.dns?.[field] ?? fallback,
|
||||||
catch (_e) { return ''; }
|
set: (v) => {
|
||||||
|
if (props.templateSettings?.dns) props.templateSettings.dns[field] = v;
|
||||||
},
|
},
|
||||||
set: (next) => {
|
});
|
||||||
if (!props.templateSettings) return;
|
|
||||||
try {
|
|
||||||
const parsed = next.trim() ? JSON.parse(next) : null;
|
|
||||||
props.templateSettings.dns = parsed;
|
|
||||||
} catch (_e) {
|
|
||||||
// wait for valid JSON — leaves the previous value untouched
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
const dnsTag = dnsField('tag', 'dns_inbound');
|
||||||
|
const dnsClientIp = dnsField('clientIp', '');
|
||||||
|
const dnsStrategy = dnsField('queryStrategy', 'UseIP');
|
||||||
|
const dnsDisableCache = dnsField('disableCache', false);
|
||||||
|
const dnsDisableFallback = dnsField('disableFallback', false);
|
||||||
|
const dnsDisableFallbackIfMatch = dnsField('disableFallbackIfMatch', false);
|
||||||
|
const dnsEnableParallelQuery = dnsField('enableParallelQuery', false);
|
||||||
|
const dnsUseSystemHosts = dnsField('useSystemHosts', false);
|
||||||
|
|
||||||
|
// ============== DNS server table ==============
|
||||||
|
const dnsServers = computed(() => {
|
||||||
|
const list = props.templateSettings?.dns?.servers || [];
|
||||||
|
return list.map((s, idx) => ({ key: idx, server: s }));
|
||||||
});
|
});
|
||||||
|
|
||||||
const fakednsJson = computed({
|
const dnsColumns = computed(() => [
|
||||||
get: () => {
|
{ title: '#', key: 'action', align: 'center', width: 60 },
|
||||||
if (!props.templateSettings?.fakedns) return '';
|
{ title: t('pages.inbounds.address'), key: 'address', align: 'left' },
|
||||||
try { return JSON.stringify(props.templateSettings.fakedns, null, 2); }
|
{ title: t('pages.xray.dns.domains'), key: 'domains', align: 'left' },
|
||||||
catch (_e) { return ''; }
|
{ title: t('pages.xray.dns.expectIPs'), key: 'expectIPs', align: 'left' },
|
||||||
},
|
]);
|
||||||
set: (next) => {
|
|
||||||
if (!props.templateSettings) return;
|
function addrFor(server) {
|
||||||
try {
|
return typeof server === 'string' ? server : server?.address || '';
|
||||||
const parsed = next.trim() ? JSON.parse(next) : null;
|
}
|
||||||
if (parsed) props.templateSettings.fakedns = parsed;
|
function domainsFor(server) {
|
||||||
else delete props.templateSettings.fakedns;
|
return typeof server === 'object' ? (server.domains || []).join(',') : '';
|
||||||
} catch (_e) { /* wait for valid JSON */ }
|
}
|
||||||
},
|
function expectIPsFor(server) {
|
||||||
|
return typeof server === 'object' ? (server.expectIPs || []).join(',') : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============== Server modal ==============
|
||||||
|
const serverModalOpen = ref(false);
|
||||||
|
const editingServer = ref(null);
|
||||||
|
const editingIndex = ref(null);
|
||||||
|
|
||||||
|
function openAddServer() {
|
||||||
|
editingServer.value = null;
|
||||||
|
editingIndex.value = null;
|
||||||
|
serverModalOpen.value = true;
|
||||||
|
}
|
||||||
|
function openEditServer(idx) {
|
||||||
|
editingServer.value = props.templateSettings.dns.servers[idx];
|
||||||
|
editingIndex.value = idx;
|
||||||
|
serverModalOpen.value = true;
|
||||||
|
}
|
||||||
|
function onServerConfirm(value) {
|
||||||
|
if (!props.templateSettings?.dns) return;
|
||||||
|
if (!Array.isArray(props.templateSettings.dns.servers)) {
|
||||||
|
props.templateSettings.dns.servers = [];
|
||||||
|
}
|
||||||
|
if (editingIndex.value == null) {
|
||||||
|
props.templateSettings.dns.servers.push(value);
|
||||||
|
} else {
|
||||||
|
props.templateSettings.dns.servers[editingIndex.value] = value;
|
||||||
|
}
|
||||||
|
serverModalOpen.value = false;
|
||||||
|
}
|
||||||
|
function deleteServer(idx) {
|
||||||
|
props.templateSettings.dns.servers.splice(idx, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============== Fake DNS table ==============
|
||||||
|
const DEFAULT_FAKEDNS = () => ({ ipPool: '198.18.0.0/15', poolSize: 65535 });
|
||||||
|
|
||||||
|
const fakeDnsList = computed(() => {
|
||||||
|
const list = Array.isArray(props.templateSettings?.fakedns)
|
||||||
|
? props.templateSettings.fakedns
|
||||||
|
: [];
|
||||||
|
return list.map((entry, idx) => ({ key: idx, ...entry }));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const fakednsColumns = computed(() => [
|
||||||
|
{ title: '#', key: 'action', align: 'center', width: 60 },
|
||||||
|
{ title: 'IP pool', dataIndex: 'ipPool', key: 'ipPool', align: 'left' },
|
||||||
|
{ title: 'Pool size', dataIndex: 'poolSize', key: 'poolSize', align: 'right', width: 120 },
|
||||||
|
]);
|
||||||
|
|
||||||
|
function addFakedns() {
|
||||||
|
if (!props.templateSettings) return;
|
||||||
|
if (!Array.isArray(props.templateSettings.fakedns)) {
|
||||||
|
props.templateSettings.fakedns = [];
|
||||||
|
}
|
||||||
|
props.templateSettings.fakedns.push(DEFAULT_FAKEDNS());
|
||||||
|
}
|
||||||
|
function deleteFakedns(idx) {
|
||||||
|
props.templateSettings.fakedns.splice(idx, 1);
|
||||||
|
if (props.templateSettings.fakedns.length === 0) {
|
||||||
|
props.templateSettings.fakedns = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function updateFakednsField(idx, field, value) {
|
||||||
|
if (!props.templateSettings.fakedns?.[idx]) return;
|
||||||
|
props.templateSettings.fakedns[idx] = {
|
||||||
|
...props.templateSettings.fakedns[idx],
|
||||||
|
[field]: value,
|
||||||
|
};
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<a-space direction="vertical" size="middle" :style="{ width: '100%' }">
|
<a-collapse default-active-key="1">
|
||||||
<a-form layout="vertical">
|
<!-- ============== General DNS settings ============== -->
|
||||||
<a-form-item label="Enable DNS">
|
<a-collapse-panel key="1" :header="t('pages.xray.generalConfigs')">
|
||||||
<a-switch v-model:checked="enableDns" />
|
<SettingListItem paddings="small">
|
||||||
</a-form-item>
|
<template #title>{{ t('pages.xray.dns.enable') }}</template>
|
||||||
|
<template #description>{{ t('pages.xray.dns.enableDesc') }}</template>
|
||||||
<template v-if="enableDns">
|
<template #control>
|
||||||
<a-alert
|
<a-switch v-model:checked="enableDNS" />
|
||||||
type="info"
|
|
||||||
show-icon
|
|
||||||
message="The full DNS tree is editable here. A dedicated server-by-server editor is coming in a future commit."
|
|
||||||
class="mb-12"
|
|
||||||
/>
|
|
||||||
<a-form-item label="dns (JSON)">
|
|
||||||
<a-textarea
|
|
||||||
v-model:value="dnsJson"
|
|
||||||
:auto-size="{ minRows: 12, maxRows: 28 }"
|
|
||||||
spellcheck="false"
|
|
||||||
class="json-editor"
|
|
||||||
/>
|
|
||||||
</a-form-item>
|
|
||||||
|
|
||||||
<a-form-item label="fakedns (JSON, optional)">
|
|
||||||
<a-textarea
|
|
||||||
v-model:value="fakednsJson"
|
|
||||||
:auto-size="{ minRows: 6, maxRows: 18 }"
|
|
||||||
spellcheck="false"
|
|
||||||
class="json-editor"
|
|
||||||
placeholder="Leave empty to omit fakedns."
|
|
||||||
/>
|
|
||||||
</a-form-item>
|
|
||||||
</template>
|
</template>
|
||||||
</a-form>
|
</SettingListItem>
|
||||||
|
|
||||||
|
<template v-if="enableDNS">
|
||||||
|
<SettingListItem paddings="small">
|
||||||
|
<template #title>{{ t('pages.xray.dns.tag') }}</template>
|
||||||
|
<template #description>{{ t('pages.xray.dns.tagDesc') }}</template>
|
||||||
|
<template #control>
|
||||||
|
<a-input v-model:value="dnsTag" />
|
||||||
|
</template>
|
||||||
|
</SettingListItem>
|
||||||
|
|
||||||
|
<SettingListItem paddings="small">
|
||||||
|
<template #title>{{ t('pages.xray.dns.clientIp') }}</template>
|
||||||
|
<template #description>{{ t('pages.xray.dns.clientIpDesc') }}</template>
|
||||||
|
<template #control>
|
||||||
|
<a-input v-model:value="dnsClientIp" />
|
||||||
|
</template>
|
||||||
|
</SettingListItem>
|
||||||
|
|
||||||
|
<SettingListItem paddings="small">
|
||||||
|
<template #title>{{ t('pages.xray.dns.strategy') }}</template>
|
||||||
|
<template #description>{{ t('pages.xray.dns.strategyDesc') }}</template>
|
||||||
|
<template #control>
|
||||||
|
<a-select v-model:value="dnsStrategy" :style="{ width: '100%' }">
|
||||||
|
<a-select-option v-for="s in STRATEGIES" :key="s" :value="s">{{ s }}</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</template>
|
||||||
|
</SettingListItem>
|
||||||
|
|
||||||
|
<SettingListItem paddings="small">
|
||||||
|
<template #title>{{ t('pages.xray.dns.disableCache') }}</template>
|
||||||
|
<template #description>{{ t('pages.xray.dns.disableCacheDesc') }}</template>
|
||||||
|
<template #control>
|
||||||
|
<a-switch v-model:checked="dnsDisableCache" />
|
||||||
|
</template>
|
||||||
|
</SettingListItem>
|
||||||
|
|
||||||
|
<SettingListItem paddings="small">
|
||||||
|
<template #title>{{ t('pages.xray.dns.disableFallback') }}</template>
|
||||||
|
<template #description>{{ t('pages.xray.dns.disableFallbackDesc') }}</template>
|
||||||
|
<template #control>
|
||||||
|
<a-switch v-model:checked="dnsDisableFallback" />
|
||||||
|
</template>
|
||||||
|
</SettingListItem>
|
||||||
|
|
||||||
|
<SettingListItem paddings="small">
|
||||||
|
<template #title>{{ t('pages.xray.dns.disableFallbackIfMatch') }}</template>
|
||||||
|
<template #description>{{ t('pages.xray.dns.disableFallbackIfMatchDesc') }}</template>
|
||||||
|
<template #control>
|
||||||
|
<a-switch v-model:checked="dnsDisableFallbackIfMatch" />
|
||||||
|
</template>
|
||||||
|
</SettingListItem>
|
||||||
|
|
||||||
|
<SettingListItem paddings="small">
|
||||||
|
<template #title>{{ t('pages.xray.dns.enableParallelQuery') }}</template>
|
||||||
|
<template #description>{{ t('pages.xray.dns.enableParallelQueryDesc') }}</template>
|
||||||
|
<template #control>
|
||||||
|
<a-switch v-model:checked="dnsEnableParallelQuery" />
|
||||||
|
</template>
|
||||||
|
</SettingListItem>
|
||||||
|
|
||||||
|
<SettingListItem paddings="small">
|
||||||
|
<template #title>{{ t('pages.xray.dns.useSystemHosts') }}</template>
|
||||||
|
<template #description>{{ t('pages.xray.dns.useSystemHostsDesc') }}</template>
|
||||||
|
<template #control>
|
||||||
|
<a-switch v-model:checked="dnsUseSystemHosts" />
|
||||||
|
</template>
|
||||||
|
</SettingListItem>
|
||||||
|
</template>
|
||||||
|
</a-collapse-panel>
|
||||||
|
|
||||||
|
<!-- ============== DNS servers ============== -->
|
||||||
|
<a-collapse-panel v-if="enableDNS" key="2" header="DNS">
|
||||||
|
<a-empty v-if="dnsServers.length === 0" :description="t('emptyDnsDesc')">
|
||||||
|
<a-button type="primary" @click="openAddServer">
|
||||||
|
<template #icon><PlusOutlined /></template>
|
||||||
|
{{ t('pages.xray.dns.add') }}
|
||||||
|
</a-button>
|
||||||
|
</a-empty>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<a-space direction="vertical" size="middle" :style="{ width: '100%' }">
|
||||||
|
<a-button type="primary" @click="openAddServer">
|
||||||
|
<template #icon><PlusOutlined /></template>
|
||||||
|
{{ t('pages.xray.dns.add') }}
|
||||||
|
</a-button>
|
||||||
|
<a-table
|
||||||
|
:columns="dnsColumns"
|
||||||
|
:data-source="dnsServers"
|
||||||
|
:row-key="(r) => r.key"
|
||||||
|
:pagination="false"
|
||||||
|
size="small"
|
||||||
|
bordered
|
||||||
|
>
|
||||||
|
<template #bodyCell="{ column, record, index }">
|
||||||
|
<template v-if="column.key === 'action'">
|
||||||
|
<a-space :size="6">
|
||||||
|
<span class="row-index">{{ index + 1 }}</span>
|
||||||
|
<a-dropdown :trigger="['click']">
|
||||||
|
<a-button shape="circle" size="small">
|
||||||
|
<MoreOutlined />
|
||||||
|
</a-button>
|
||||||
|
<template #overlay>
|
||||||
|
<a-menu>
|
||||||
|
<a-menu-item @click="openEditServer(index)">
|
||||||
|
<EditOutlined /> {{ t('edit') }}
|
||||||
|
</a-menu-item>
|
||||||
|
<a-menu-item class="danger" @click="deleteServer(index)">
|
||||||
|
<DeleteOutlined /> {{ t('delete') }}
|
||||||
|
</a-menu-item>
|
||||||
|
</a-menu>
|
||||||
|
</template>
|
||||||
|
</a-dropdown>
|
||||||
</a-space>
|
</a-space>
|
||||||
</template>
|
</template>
|
||||||
|
<template v-else-if="column.key === 'address'">
|
||||||
|
{{ addrFor(record.server) }}
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'domains'">
|
||||||
|
<span class="muted">{{ domainsFor(record.server) }}</span>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'expectIPs'">
|
||||||
|
<span class="muted">{{ expectIPsFor(record.server) }}</span>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</a-table>
|
||||||
|
</a-space>
|
||||||
|
</template>
|
||||||
|
</a-collapse-panel>
|
||||||
|
|
||||||
|
<!-- ============== Fake DNS ============== -->
|
||||||
|
<a-collapse-panel v-if="enableDNS" key="3" header="Fake DNS">
|
||||||
|
<a-empty v-if="fakeDnsList.length === 0" :description="t('emptyFakeDnsDesc')">
|
||||||
|
<a-button type="primary" @click="addFakedns">
|
||||||
|
<template #icon><PlusOutlined /></template>
|
||||||
|
{{ t('pages.xray.fakedns.add') }}
|
||||||
|
</a-button>
|
||||||
|
</a-empty>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<a-space direction="vertical" size="middle" :style="{ width: '100%' }">
|
||||||
|
<a-button type="primary" @click="addFakedns">
|
||||||
|
<template #icon><PlusOutlined /></template>
|
||||||
|
{{ t('pages.xray.fakedns.add') }}
|
||||||
|
</a-button>
|
||||||
|
<a-table
|
||||||
|
:columns="fakednsColumns"
|
||||||
|
:data-source="fakeDnsList"
|
||||||
|
:row-key="(r) => r.key"
|
||||||
|
:pagination="false"
|
||||||
|
size="small"
|
||||||
|
bordered
|
||||||
|
>
|
||||||
|
<template #bodyCell="{ column, record, index }">
|
||||||
|
<template v-if="column.key === 'action'">
|
||||||
|
<a-space :size="6">
|
||||||
|
<span class="row-index">{{ index + 1 }}</span>
|
||||||
|
<a-button shape="circle" size="small" danger @click="deleteFakedns(index)">
|
||||||
|
<DeleteOutlined />
|
||||||
|
</a-button>
|
||||||
|
</a-space>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'ipPool'">
|
||||||
|
<a-input
|
||||||
|
:value="record.ipPool"
|
||||||
|
size="small"
|
||||||
|
@change="(e) => updateFakednsField(index, 'ipPool', e.target.value)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'poolSize'">
|
||||||
|
<a-input-number
|
||||||
|
:value="record.poolSize"
|
||||||
|
:min="1"
|
||||||
|
size="small"
|
||||||
|
@change="(v) => updateFakednsField(index, 'poolSize', v)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</a-table>
|
||||||
|
</a-space>
|
||||||
|
</template>
|
||||||
|
</a-collapse-panel>
|
||||||
|
</a-collapse>
|
||||||
|
|
||||||
|
<DnsServerModal
|
||||||
|
v-model:open="serverModalOpen"
|
||||||
|
:server="editingServer"
|
||||||
|
:is-edit="editingIndex != null"
|
||||||
|
@confirm="onServerConfirm"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.mb-12 { margin-bottom: 12px; }
|
.row-index {
|
||||||
.json-editor {
|
font-weight: 500;
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
opacity: 0.7;
|
||||||
font-size: 12px;
|
|
||||||
}
|
}
|
||||||
|
.muted { opacity: 0.7; word-break: break-all; }
|
||||||
|
.danger { color: #ff4d4f; }
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue