feat(xray/dns): align DNS settings with Xray docs + UI polish

- DNS server modal: rename expectIPs -> expectedIPs (per docs); add
  per-server tag, clientIP, serveStale, serveExpiredTTL, timeoutMs;
  flip skipFallback default to false; hydration still accepts legacy
  expectIPs for back-compat.
- DNS tab: add hosts editor (domain -> IP/array), serveStale +
  serveExpiredTTL controls, "Use Preset" button bringing back the
  legacy preset gallery (Google / Cloudflare / AdGuard + Family
  variants — fixed AdGuard Family IPs that were wrong in legacy),
  and a "Delete All" button to wipe the server list at once.
- i18n: add 15 new dns.* keys across all 13 locales.
- Frontend-wide formatter pass on Vue components (whitespace and
  attribute layout only, no behavior changes).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
MHSanaei
2026-05-10 17:03:11 +02:00
parent 8e7d215b4a
commit a96612f595
50 changed files with 1203 additions and 886 deletions
+38 -34
View File
@@ -163,11 +163,8 @@ const themeClass = computed(() => ({
<ThemeSwitchLogin />
<span>{{ t('pages.settings.language') }}</span>
<a-select v-model:value="lang" class="lang-select" @change="onLangChange">
<a-select-option
v-for="l in LanguageManager.supportedLanguages"
:key="l.value"
:value="l.value"
>
<a-select-option v-for="l in LanguageManager.supportedLanguages" :key="l.value"
:value="l.value">
<span :aria-label="l.name">{{ l.icon }}</span>
&nbsp;&nbsp;<span>{{ l.name }}</span>
</a-select-option>
@@ -175,7 +172,9 @@ const themeClass = computed(() => ({
</a-space>
</template>
<a-button shape="circle">
<template #icon><SettingOutlined /></template>
<template #icon>
<SettingOutlined />
</template>
</a-button>
</a-popover>
</template>
@@ -185,12 +184,7 @@ const themeClass = computed(() => ({
<a-col :xs="24" :sm="subJsonUrl || subClashUrl ? 12 : 24" class="qr-col">
<div class="qr-box">
<a-tag color="purple" class="qr-tag">{{ t('pages.settings.subSettings') }}</a-tag>
<canvas
ref="subQr"
class="qr-canvas"
:title="t('copy')"
@click="copy(subUrl)"
/>
<canvas ref="subQr" class="qr-canvas" :title="t('copy')" @click="copy(subUrl)" />
</div>
</a-col>
<a-col v-if="subJsonUrl" :xs="24" :sm="12" class="qr-col">
@@ -198,23 +192,13 @@ const themeClass = computed(() => ({
<a-tag color="purple" class="qr-tag">
{{ t('pages.settings.subSettings') }} JSON
</a-tag>
<canvas
ref="subJsonQr"
class="qr-canvas"
:title="t('copy')"
@click="copy(subJsonUrl)"
/>
<canvas ref="subJsonQr" class="qr-canvas" :title="t('copy')" @click="copy(subJsonUrl)" />
</div>
</a-col>
<a-col v-if="subClashUrl" :xs="24" :sm="12" class="qr-col">
<div class="qr-box">
<a-tag color="purple" class="qr-tag">Clash / Mihomo</a-tag>
<canvas
ref="subClashQr"
class="qr-canvas"
:title="t('copy')"
@click="copy(subClashUrl)"
/>
<canvas ref="subClashQr" class="qr-canvas" :title="t('copy')" @click="copy(subClashUrl)" />
</div>
</a-col>
</a-row>
@@ -248,12 +232,7 @@ const themeClass = computed(() => ({
<!-- ============== Individual links ============== -->
<div v-if="links.length" class="links-section">
<div
v-for="(link, idx) in links"
:key="link"
class="link-row"
@click="copy(link)"
>
<div v-for="(link, idx) in links" :key="link" class="link-row" @click="copy(link)">
<a-tag color="purple" class="link-tag">{{ linkName(link, idx) }}</a-tag>
<div class="link-box">
<CopyOutlined class="link-copy-icon" />
@@ -267,12 +246,15 @@ const themeClass = computed(() => ({
<a-col :xs="24" :sm="12" class="app-col">
<a-dropdown :trigger="['click']">
<a-button :block="isMobile" size="large" type="primary">
<AndroidOutlined /> Android <DownOutlined />
<AndroidOutlined /> Android
<DownOutlined />
</a-button>
<template #overlay>
<a-menu>
<a-menu-item key="android-v2box" @click="open(`v2box://install-sub?url=${encodeURIComponent(subUrl)}&name=${encodeURIComponent(sId)}`)">V2Box</a-menu-item>
<a-menu-item key="android-v2rayng" @click="open(`v2rayng://install-config?url=${encodeURIComponent(subUrl)}`)">V2RayNG</a-menu-item>
<a-menu-item key="android-v2box"
@click="open(`v2box://install-sub?url=${encodeURIComponent(subUrl)}&name=${encodeURIComponent(sId)}`)">V2Box</a-menu-item>
<a-menu-item key="android-v2rayng"
@click="open(`v2rayng://install-config?url=${encodeURIComponent(subUrl)}`)">V2RayNG</a-menu-item>
<a-menu-item key="android-singbox" @click="copy(subUrl)">Sing-box</a-menu-item>
<a-menu-item key="android-v2raytun" @click="copy(subUrl)">V2RayTun</a-menu-item>
<a-menu-item key="android-npvtunnel" @click="copy(subUrl)">NPV Tunnel</a-menu-item>
@@ -284,7 +266,8 @@ const themeClass = computed(() => ({
<a-col :xs="24" :sm="12" class="app-col">
<a-dropdown :trigger="['click']">
<a-button :block="isMobile" size="large" type="primary">
<AppleOutlined /> iOS <DownOutlined />
<AppleOutlined /> iOS
<DownOutlined />
</a-button>
<template #overlay>
<a-menu>
@@ -314,14 +297,17 @@ const themeClass = computed(() => ({
min-height: 100vh;
background: var(--bg-page);
}
.subscription-page.is-dark {
--bg-page: #0a1222;
--bg-card: #151f31;
}
.subscription-page.is-dark.is-ultra {
--bg-page: #050505;
--bg-card: #0c0e12;
}
.subscription-page :deep(.ant-layout),
.subscription-page :deep(.ant-layout-content) {
background: transparent;
@@ -339,10 +325,12 @@ const themeClass = computed(() => ({
.qr-row {
margin-bottom: 12px;
}
.qr-col {
display: flex;
justify-content: center;
}
.qr-box {
display: inline-flex;
flex-direction: column;
@@ -350,11 +338,13 @@ const themeClass = computed(() => ({
gap: 4px;
width: 220px;
}
.qr-tag {
width: 100%;
text-align: center;
margin: 0;
}
.qr-canvas {
cursor: pointer;
background: #fff;
@@ -370,16 +360,19 @@ const themeClass = computed(() => ({
.info-table {
margin-top: 12px;
}
.info-table :deep(.ant-descriptions-view),
.info-table :deep(.ant-descriptions-view) table,
.info-table :deep(.ant-descriptions-view) th,
.info-table :deep(.ant-descriptions-view) td {
border-color: rgba(0, 0, 0, 0.18) !important;
}
.info-table :deep(tbody > tr > th),
.info-table :deep(tbody > tr > td) {
border-bottom: 1px solid rgba(0, 0, 0, 0.18) !important;
}
.info-table :deep(tbody > tr:last-child > th),
.info-table :deep(tbody > tr:last-child > td) {
border-bottom: none !important;
@@ -391,10 +384,12 @@ const themeClass = computed(() => ({
.is-dark .info-table :deep(.ant-descriptions-view) td {
border-color: rgba(255, 255, 255, 0.18) !important;
}
.is-dark .info-table :deep(tbody > tr > th),
.is-dark .info-table :deep(tbody > tr > td) {
border-bottom: 1px solid rgba(255, 255, 255, 0.18) !important;
}
.is-dark .info-table :deep(tbody > tr:last-child > th),
.is-dark .info-table :deep(tbody > tr:last-child > td) {
border-bottom: none !important;
@@ -404,17 +399,20 @@ const themeClass = computed(() => ({
.links-section {
margin-top: 16px;
}
.link-row {
position: relative;
margin-bottom: 16px;
text-align: center;
}
.link-tag {
margin-bottom: -10px;
position: relative;
z-index: 2;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.link-box {
cursor: pointer;
border-radius: 12px;
@@ -430,19 +428,23 @@ const themeClass = computed(() => ({
background: rgba(0, 0, 0, 0.03);
border: 1px solid rgba(0, 0, 0, 0.08);
}
.link-box:hover {
background: rgba(0, 0, 0, 0.05);
border-color: rgba(0, 0, 0, 0.14);
}
.link-copy-icon {
margin-right: 6px;
opacity: 0.6;
}
.is-dark .link-box {
background: rgba(0, 0, 0, 0.2);
border-color: rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.85);
}
.is-dark .link-box:hover {
background: rgba(0, 0, 0, 0.3);
border-color: rgba(255, 255, 255, 0.2);
@@ -452,6 +454,7 @@ const themeClass = computed(() => ({
.apps-row {
margin-top: 24px;
}
.app-col {
text-align: center;
}
@@ -459,6 +462,7 @@ const themeClass = computed(() => ({
.settings-popover {
min-width: 220px;
}
.lang-select {
width: 100%;
}