DnsTab.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  1. <script setup>
  2. import { computed, ref } from 'vue';
  3. import { useI18n } from 'vue-i18n';
  4. import {
  5. PlusOutlined,
  6. MoreOutlined,
  7. EditOutlined,
  8. DeleteOutlined,
  9. } from '@ant-design/icons-vue';
  10. import SettingListItem from '@/components/SettingListItem.vue';
  11. import DnsServerModal from './DnsServerModal.vue';
  12. const { t } = useI18n();
  13. // Structured DNS editor — mirrors web/html/settings/xray/dns.html.
  14. // Master enable switch + general DNS options + per-server table with
  15. // add/edit/delete (modal flow), plus a Fake DNS table. Both lists
  16. // flow through templateSettings.dns / .fakedns reactively so the
  17. // useXraySetting composable picks every edit up via its deep watch.
  18. const props = defineProps({
  19. templateSettings: { type: Object, default: null },
  20. });
  21. const STRATEGIES = ['UseSystem', 'UseIP', 'UseIPv4', 'UseIPv6'];
  22. // ============== Master toggle ==============
  23. const enableDNS = computed({
  24. get: () => !!props.templateSettings?.dns,
  25. set: (next) => {
  26. if (!props.templateSettings) return;
  27. if (next) {
  28. props.templateSettings.dns = {
  29. tag: 'dns_inbound',
  30. clientIp: '',
  31. queryStrategy: 'UseIP',
  32. disableCache: false,
  33. disableFallback: false,
  34. disableFallbackIfMatch: false,
  35. useSystemHosts: false,
  36. enableParallelQuery: false,
  37. servers: [],
  38. };
  39. props.templateSettings.fakedns = null;
  40. } else {
  41. delete props.templateSettings.dns;
  42. delete props.templateSettings.fakedns;
  43. }
  44. },
  45. });
  46. // ============== Field bridges ==============
  47. function dnsField(field, fallback) {
  48. return computed({
  49. get: () => props.templateSettings?.dns?.[field] ?? fallback,
  50. set: (v) => {
  51. if (props.templateSettings?.dns) props.templateSettings.dns[field] = v;
  52. },
  53. });
  54. }
  55. const dnsTag = dnsField('tag', 'dns_inbound');
  56. const dnsClientIp = dnsField('clientIp', '');
  57. const dnsStrategy = dnsField('queryStrategy', 'UseIP');
  58. const dnsDisableCache = dnsField('disableCache', false);
  59. const dnsDisableFallback = dnsField('disableFallback', false);
  60. const dnsDisableFallbackIfMatch = dnsField('disableFallbackIfMatch', false);
  61. const dnsEnableParallelQuery = dnsField('enableParallelQuery', false);
  62. const dnsUseSystemHosts = dnsField('useSystemHosts', false);
  63. // ============== DNS server table ==============
  64. const dnsServers = computed(() => {
  65. const list = props.templateSettings?.dns?.servers || [];
  66. return list.map((s, idx) => ({ key: idx, server: s }));
  67. });
  68. const dnsColumns = computed(() => [
  69. { title: '#', key: 'action', align: 'center', width: 60 },
  70. { title: t('pages.inbounds.address'), key: 'address', align: 'left' },
  71. { title: t('pages.xray.dns.domains'), key: 'domains', align: 'left' },
  72. { title: t('pages.xray.dns.expectIPs'), key: 'expectIPs', align: 'left' },
  73. ]);
  74. function addrFor(server) {
  75. return typeof server === 'string' ? server : server?.address || '';
  76. }
  77. function domainsFor(server) {
  78. return typeof server === 'object' ? (server.domains || []).join(',') : '';
  79. }
  80. function expectIPsFor(server) {
  81. return typeof server === 'object' ? (server.expectIPs || []).join(',') : '';
  82. }
  83. // ============== Server modal ==============
  84. const serverModalOpen = ref(false);
  85. const editingServer = ref(null);
  86. const editingIndex = ref(null);
  87. function openAddServer() {
  88. editingServer.value = null;
  89. editingIndex.value = null;
  90. serverModalOpen.value = true;
  91. }
  92. function openEditServer(idx) {
  93. editingServer.value = props.templateSettings.dns.servers[idx];
  94. editingIndex.value = idx;
  95. serverModalOpen.value = true;
  96. }
  97. function onServerConfirm(value) {
  98. if (!props.templateSettings?.dns) return;
  99. if (!Array.isArray(props.templateSettings.dns.servers)) {
  100. props.templateSettings.dns.servers = [];
  101. }
  102. if (editingIndex.value == null) {
  103. props.templateSettings.dns.servers.push(value);
  104. } else {
  105. props.templateSettings.dns.servers[editingIndex.value] = value;
  106. }
  107. serverModalOpen.value = false;
  108. }
  109. function deleteServer(idx) {
  110. props.templateSettings.dns.servers.splice(idx, 1);
  111. }
  112. // ============== Fake DNS table ==============
  113. const DEFAULT_FAKEDNS = () => ({ ipPool: '198.18.0.0/15', poolSize: 65535 });
  114. const fakeDnsList = computed(() => {
  115. const list = Array.isArray(props.templateSettings?.fakedns)
  116. ? props.templateSettings.fakedns
  117. : [];
  118. return list.map((entry, idx) => ({ key: idx, ...entry }));
  119. });
  120. const fakednsColumns = computed(() => [
  121. { title: '#', key: 'action', align: 'center', width: 60 },
  122. { title: 'IP pool', dataIndex: 'ipPool', key: 'ipPool', align: 'left' },
  123. { title: 'Pool size', dataIndex: 'poolSize', key: 'poolSize', align: 'right', width: 120 },
  124. ]);
  125. function addFakedns() {
  126. if (!props.templateSettings) return;
  127. if (!Array.isArray(props.templateSettings.fakedns)) {
  128. props.templateSettings.fakedns = [];
  129. }
  130. props.templateSettings.fakedns.push(DEFAULT_FAKEDNS());
  131. }
  132. function deleteFakedns(idx) {
  133. props.templateSettings.fakedns.splice(idx, 1);
  134. if (props.templateSettings.fakedns.length === 0) {
  135. props.templateSettings.fakedns = null;
  136. }
  137. }
  138. function updateFakednsField(idx, field, value) {
  139. if (!props.templateSettings.fakedns?.[idx]) return;
  140. props.templateSettings.fakedns[idx] = {
  141. ...props.templateSettings.fakedns[idx],
  142. [field]: value,
  143. };
  144. }
  145. </script>
  146. <template>
  147. <a-collapse default-active-key="1">
  148. <!-- ============== General DNS settings ============== -->
  149. <a-collapse-panel key="1" :header="t('pages.xray.generalConfigs')">
  150. <SettingListItem paddings="small">
  151. <template #title>{{ t('pages.xray.dns.enable') }}</template>
  152. <template #description>{{ t('pages.xray.dns.enableDesc') }}</template>
  153. <template #control>
  154. <a-switch v-model:checked="enableDNS" />
  155. </template>
  156. </SettingListItem>
  157. <template v-if="enableDNS">
  158. <SettingListItem paddings="small">
  159. <template #title>{{ t('pages.xray.dns.tag') }}</template>
  160. <template #description>{{ t('pages.xray.dns.tagDesc') }}</template>
  161. <template #control>
  162. <a-input v-model:value="dnsTag" />
  163. </template>
  164. </SettingListItem>
  165. <SettingListItem paddings="small">
  166. <template #title>{{ t('pages.xray.dns.clientIp') }}</template>
  167. <template #description>{{ t('pages.xray.dns.clientIpDesc') }}</template>
  168. <template #control>
  169. <a-input v-model:value="dnsClientIp" />
  170. </template>
  171. </SettingListItem>
  172. <SettingListItem paddings="small">
  173. <template #title>{{ t('pages.xray.dns.strategy') }}</template>
  174. <template #description>{{ t('pages.xray.dns.strategyDesc') }}</template>
  175. <template #control>
  176. <a-select v-model:value="dnsStrategy" :style="{ width: '100%' }">
  177. <a-select-option v-for="s in STRATEGIES" :key="s" :value="s">{{ s }}</a-select-option>
  178. </a-select>
  179. </template>
  180. </SettingListItem>
  181. <SettingListItem paddings="small">
  182. <template #title>{{ t('pages.xray.dns.disableCache') }}</template>
  183. <template #description>{{ t('pages.xray.dns.disableCacheDesc') }}</template>
  184. <template #control>
  185. <a-switch v-model:checked="dnsDisableCache" />
  186. </template>
  187. </SettingListItem>
  188. <SettingListItem paddings="small">
  189. <template #title>{{ t('pages.xray.dns.disableFallback') }}</template>
  190. <template #description>{{ t('pages.xray.dns.disableFallbackDesc') }}</template>
  191. <template #control>
  192. <a-switch v-model:checked="dnsDisableFallback" />
  193. </template>
  194. </SettingListItem>
  195. <SettingListItem paddings="small">
  196. <template #title>{{ t('pages.xray.dns.disableFallbackIfMatch') }}</template>
  197. <template #description>{{ t('pages.xray.dns.disableFallbackIfMatchDesc') }}</template>
  198. <template #control>
  199. <a-switch v-model:checked="dnsDisableFallbackIfMatch" />
  200. </template>
  201. </SettingListItem>
  202. <SettingListItem paddings="small">
  203. <template #title>{{ t('pages.xray.dns.enableParallelQuery') }}</template>
  204. <template #description>{{ t('pages.xray.dns.enableParallelQueryDesc') }}</template>
  205. <template #control>
  206. <a-switch v-model:checked="dnsEnableParallelQuery" />
  207. </template>
  208. </SettingListItem>
  209. <SettingListItem paddings="small">
  210. <template #title>{{ t('pages.xray.dns.useSystemHosts') }}</template>
  211. <template #description>{{ t('pages.xray.dns.useSystemHostsDesc') }}</template>
  212. <template #control>
  213. <a-switch v-model:checked="dnsUseSystemHosts" />
  214. </template>
  215. </SettingListItem>
  216. </template>
  217. </a-collapse-panel>
  218. <!-- ============== DNS servers ============== -->
  219. <a-collapse-panel v-if="enableDNS" key="2" header="DNS">
  220. <a-empty v-if="dnsServers.length === 0" :description="t('emptyDnsDesc')">
  221. <a-button type="primary" @click="openAddServer">
  222. <template #icon><PlusOutlined /></template>
  223. {{ t('pages.xray.dns.add') }}
  224. </a-button>
  225. </a-empty>
  226. <template v-else>
  227. <a-space direction="vertical" size="middle" :style="{ width: '100%' }">
  228. <a-button type="primary" @click="openAddServer">
  229. <template #icon><PlusOutlined /></template>
  230. {{ t('pages.xray.dns.add') }}
  231. </a-button>
  232. <a-table
  233. :columns="dnsColumns"
  234. :data-source="dnsServers"
  235. :row-key="(r) => r.key"
  236. :pagination="false"
  237. size="small"
  238. bordered
  239. >
  240. <template #bodyCell="{ column, record, index }">
  241. <template v-if="column.key === 'action'">
  242. <a-space :size="6">
  243. <span class="row-index">{{ index + 1 }}</span>
  244. <a-dropdown :trigger="['click']">
  245. <a-button shape="circle" size="small">
  246. <MoreOutlined />
  247. </a-button>
  248. <template #overlay>
  249. <a-menu>
  250. <a-menu-item @click="openEditServer(index)">
  251. <EditOutlined /> {{ t('edit') }}
  252. </a-menu-item>
  253. <a-menu-item class="danger" @click="deleteServer(index)">
  254. <DeleteOutlined /> {{ t('delete') }}
  255. </a-menu-item>
  256. </a-menu>
  257. </template>
  258. </a-dropdown>
  259. </a-space>
  260. </template>
  261. <template v-else-if="column.key === 'address'">
  262. {{ addrFor(record.server) }}
  263. </template>
  264. <template v-else-if="column.key === 'domains'">
  265. <span class="muted">{{ domainsFor(record.server) }}</span>
  266. </template>
  267. <template v-else-if="column.key === 'expectIPs'">
  268. <span class="muted">{{ expectIPsFor(record.server) }}</span>
  269. </template>
  270. </template>
  271. </a-table>
  272. </a-space>
  273. </template>
  274. </a-collapse-panel>
  275. <!-- ============== Fake DNS ============== -->
  276. <a-collapse-panel v-if="enableDNS" key="3" header="Fake DNS">
  277. <a-empty v-if="fakeDnsList.length === 0" :description="t('emptyFakeDnsDesc')">
  278. <a-button type="primary" @click="addFakedns">
  279. <template #icon><PlusOutlined /></template>
  280. {{ t('pages.xray.fakedns.add') }}
  281. </a-button>
  282. </a-empty>
  283. <template v-else>
  284. <a-space direction="vertical" size="middle" :style="{ width: '100%' }">
  285. <a-button type="primary" @click="addFakedns">
  286. <template #icon><PlusOutlined /></template>
  287. {{ t('pages.xray.fakedns.add') }}
  288. </a-button>
  289. <a-table
  290. :columns="fakednsColumns"
  291. :data-source="fakeDnsList"
  292. :row-key="(r) => r.key"
  293. :pagination="false"
  294. size="small"
  295. bordered
  296. >
  297. <template #bodyCell="{ column, record, index }">
  298. <template v-if="column.key === 'action'">
  299. <a-space :size="6">
  300. <span class="row-index">{{ index + 1 }}</span>
  301. <a-button shape="circle" size="small" danger @click="deleteFakedns(index)">
  302. <DeleteOutlined />
  303. </a-button>
  304. </a-space>
  305. </template>
  306. <template v-else-if="column.key === 'ipPool'">
  307. <a-input
  308. :value="record.ipPool"
  309. size="small"
  310. @change="(e) => updateFakednsField(index, 'ipPool', e.target.value)"
  311. />
  312. </template>
  313. <template v-else-if="column.key === 'poolSize'">
  314. <a-input-number
  315. :value="record.poolSize"
  316. :min="1"
  317. size="small"
  318. @change="(v) => updateFakednsField(index, 'poolSize', v)"
  319. />
  320. </template>
  321. </template>
  322. </a-table>
  323. </a-space>
  324. </template>
  325. </a-collapse-panel>
  326. </a-collapse>
  327. <DnsServerModal
  328. v-model:open="serverModalOpen"
  329. :server="editingServer"
  330. :is-edit="editingIndex != null"
  331. @confirm="onServerConfirm"
  332. />
  333. </template>
  334. <style scoped>
  335. .row-index {
  336. font-weight: 500;
  337. opacity: 0.7;
  338. }
  339. .muted { opacity: 0.7; word-break: break-all; }
  340. .danger { color: #ff4d4f; }
  341. </style>