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>
|
||||
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
|
||||
// for the full dns + fakedns trees. The legacy panel had a
|
||||
// dedicated DNS-server modal + fakedns row editor; both are large
|
||||
// enough to deserve their own commits. For now this gives users a
|
||||
// working path to edit DNS settings without leaving the structured
|
||||
// page.
|
||||
import SettingListItem from '@/components/SettingListItem.vue';
|
||||
import DnsServerModal from './DnsServerModal.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
// 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({
|
||||
templateSettings: { type: Object, default: null },
|
||||
});
|
||||
|
||||
const enableDns = computed({
|
||||
const STRATEGIES = ['UseSystem', 'UseIP', 'UseIPv4', 'UseIPv6'];
|
||||
|
||||
// ============== Master toggle ==============
|
||||
const enableDNS = computed({
|
||||
get: () => !!props.templateSettings?.dns,
|
||||
set: (next) => {
|
||||
if (!props.templateSettings) return;
|
||||
if (next) {
|
||||
props.templateSettings.dns = {
|
||||
servers: [],
|
||||
queryStrategy: 'UseIP',
|
||||
tag: 'dns_inbound',
|
||||
clientIp: '',
|
||||
queryStrategy: 'UseIP',
|
||||
disableCache: false,
|
||||
disableFallback: false,
|
||||
disableFallbackIfMatch: false,
|
||||
useSystemHosts: false,
|
||||
enableParallelQuery: false,
|
||||
servers: [],
|
||||
};
|
||||
props.templateSettings.fakedns = null;
|
||||
} else {
|
||||
|
|
@ -31,81 +50,324 @@ const enableDns = computed({
|
|||
},
|
||||
});
|
||||
|
||||
const dnsJson = computed({
|
||||
get: () => {
|
||||
if (!props.templateSettings?.dns) return '';
|
||||
try { return JSON.stringify(props.templateSettings.dns, null, 2); }
|
||||
catch (_e) { return ''; }
|
||||
// ============== Field bridges ==============
|
||||
function dnsField(field, fallback) {
|
||||
return computed({
|
||||
get: () => props.templateSettings?.dns?.[field] ?? fallback,
|
||||
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({
|
||||
get: () => {
|
||||
if (!props.templateSettings?.fakedns) return '';
|
||||
try { return JSON.stringify(props.templateSettings.fakedns, null, 2); }
|
||||
catch (_e) { return ''; }
|
||||
},
|
||||
set: (next) => {
|
||||
if (!props.templateSettings) return;
|
||||
try {
|
||||
const parsed = next.trim() ? JSON.parse(next) : null;
|
||||
if (parsed) props.templateSettings.fakedns = parsed;
|
||||
else delete props.templateSettings.fakedns;
|
||||
} catch (_e) { /* wait for valid JSON */ }
|
||||
},
|
||||
const dnsColumns = computed(() => [
|
||||
{ title: '#', key: 'action', align: 'center', width: 60 },
|
||||
{ title: t('pages.inbounds.address'), key: 'address', align: 'left' },
|
||||
{ title: t('pages.xray.dns.domains'), key: 'domains', align: 'left' },
|
||||
{ title: t('pages.xray.dns.expectIPs'), key: 'expectIPs', align: 'left' },
|
||||
]);
|
||||
|
||||
function addrFor(server) {
|
||||
return typeof server === 'string' ? server : server?.address || '';
|
||||
}
|
||||
function domainsFor(server) {
|
||||
return typeof server === 'object' ? (server.domains || []).join(',') : '';
|
||||
}
|
||||
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>
|
||||
|
||||
<template>
|
||||
<a-space direction="vertical" size="middle" :style="{ width: '100%' }">
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="Enable DNS">
|
||||
<a-switch v-model:checked="enableDns" />
|
||||
</a-form-item>
|
||||
|
||||
<template v-if="enableDns">
|
||||
<a-alert
|
||||
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>
|
||||
<a-collapse default-active-key="1">
|
||||
<!-- ============== General DNS settings ============== -->
|
||||
<a-collapse-panel key="1" :header="t('pages.xray.generalConfigs')">
|
||||
<SettingListItem paddings="small">
|
||||
<template #title>{{ t('pages.xray.dns.enable') }}</template>
|
||||
<template #description>{{ t('pages.xray.dns.enableDesc') }}</template>
|
||||
<template #control>
|
||||
<a-switch v-model:checked="enableDNS" />
|
||||
</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>
|
||||
</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>
|
||||
.mb-12 { margin-bottom: 12px; }
|
||||
.json-editor {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
font-size: 12px;
|
||||
.row-index {
|
||||
font-weight: 500;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.muted { opacity: 0.7; word-break: break-all; }
|
||||
.danger { color: #ff4d4f; }
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Reference in a new issue