mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-30 15:07:14 +00:00
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:
@@ -362,7 +362,7 @@ const title = computed(() =>
|
||||
<a-form-item v-else>
|
||||
<template #label>
|
||||
<a-tooltip :title="t('pages.inbounds.leaveBlankToNeverExpire')">{{ t('pages.inbounds.expireDate')
|
||||
}}</a-tooltip>
|
||||
}}</a-tooltip>
|
||||
</template>
|
||||
<DateTimePicker v-model:value="expiryDate" />
|
||||
<a-tag v-if="mode === 'edit' && isExpired" color="red">{{ t('depleted') }}</a-tag>
|
||||
|
||||
@@ -291,8 +291,8 @@ function rowKey(client) {
|
||||
{{ IntlUtil.formatRelativeTime(client.expiryTime) }}
|
||||
</a-tag>
|
||||
</a-popover>
|
||||
<a-tag v-else :color="ColorUtils.userExpiryColor(expireDiff, client, isDarkTheme)"
|
||||
:style="{ border: 'none' }" class="infinite-tag">
|
||||
<a-tag v-else :color="ColorUtils.userExpiryColor(expireDiff, client, isDarkTheme)" :style="{ border: 'none' }"
|
||||
class="infinite-tag">
|
||||
<InfinityIcon />
|
||||
</a-tag>
|
||||
</div>
|
||||
@@ -373,7 +373,9 @@ function rowKey(client) {
|
||||
<a-tag v-else-if="client.expiryTime < 0" color="green">
|
||||
{{ -client.expiryTime / 86400000 }}d ({{ t('pages.client.delayedStart') }})
|
||||
</a-tag>
|
||||
<a-tag v-else color="purple"><InfinityIcon /></a-tag>
|
||||
<a-tag v-else color="purple">
|
||||
<InfinityIcon />
|
||||
</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -561,6 +563,7 @@ function rowKey(client) {
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
:global(body.dark) .client-card {
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
@@ -571,6 +574,7 @@ function rowKey(client) {
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.client-card-head .client-email {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
@@ -580,6 +584,7 @@ function rowKey(client) {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.client-card-actions {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
@@ -587,6 +592,7 @@ function rowKey(client) {
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.client-card-actions .row-icon {
|
||||
font-size: 20px;
|
||||
padding: 4px;
|
||||
@@ -605,12 +611,14 @@ function rowKey(client) {
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.client-card-foot .stat-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.client-card-foot .stat-label {
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
@@ -619,6 +627,7 @@ function rowKey(client) {
|
||||
min-width: 96px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.client-card-foot :deep(.ant-tag) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -560,19 +560,11 @@ watch(
|
||||
<a-input v-model:value="dbForm.remark" />
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('pages.inbounds.deployTo')">
|
||||
<a-select
|
||||
v-model:value="dbForm.nodeId"
|
||||
:disabled="mode === 'edit'"
|
||||
:placeholder="t('pages.inbounds.localPanel')"
|
||||
allow-clear
|
||||
>
|
||||
<a-select v-model:value="dbForm.nodeId" :disabled="mode === 'edit'"
|
||||
:placeholder="t('pages.inbounds.localPanel')" allow-clear>
|
||||
<a-select-option :value="null">{{ t('pages.inbounds.localPanel') }}</a-select-option>
|
||||
<a-select-option
|
||||
v-for="n in selectableNodes"
|
||||
:key="n.id"
|
||||
:value="n.id"
|
||||
:disabled="n.status === 'offline'"
|
||||
>
|
||||
<a-select-option v-for="n in selectableNodes" :key="n.id" :value="n.id"
|
||||
:disabled="n.status === 'offline'">
|
||||
{{ n.name }}{{ n.status === 'offline' ? ' (offline)' : '' }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
|
||||
@@ -327,14 +327,16 @@ const showSubscriptionTab = computed(
|
||||
<tr>
|
||||
<td>{{ t('pages.inbounds.createdAt') }}</td>
|
||||
<td>
|
||||
<a-tag v-if="clientSettings.created_at">{{ IntlUtil.formatDate(clientSettings.created_at, datepicker) }}</a-tag>
|
||||
<a-tag v-if="clientSettings.created_at">{{ IntlUtil.formatDate(clientSettings.created_at, datepicker)
|
||||
}}</a-tag>
|
||||
<a-tag v-else>-</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ t('pages.inbounds.updatedAt') }}</td>
|
||||
<td>
|
||||
<a-tag v-if="clientSettings.updated_at">{{ IntlUtil.formatDate(clientSettings.updated_at, datepicker) }}</a-tag>
|
||||
<a-tag v-if="clientSettings.updated_at">{{ IntlUtil.formatDate(clientSettings.updated_at, datepicker)
|
||||
}}</a-tag>
|
||||
<a-tag v-else>-</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -356,7 +358,7 @@ const showSubscriptionTab = computed(
|
||||
<div class="ip-log">
|
||||
<div v-if="clientIpsArray.length > 0">
|
||||
<a-tag v-for="(item, idx) in clientIpsArray" :key="idx" color="blue" class="ip-log-row">{{ item
|
||||
}}</a-tag>
|
||||
}}</a-tag>
|
||||
</div>
|
||||
<a-tag v-else>{{ clientIpsText || t('tgbot.noIpRecord') }}</a-tag>
|
||||
</div>
|
||||
@@ -472,7 +474,7 @@ const showSubscriptionTab = computed(
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<a :href="subJsonLink" target="_blank" rel="noopener noreferrer" class="link-panel-anchor">{{ subJsonLink
|
||||
}}</a>
|
||||
}}</a>
|
||||
</div>
|
||||
</template>
|
||||
</a-tab-pane>
|
||||
@@ -627,11 +629,7 @@ const showSubscriptionTab = computed(
|
||||
<dd><a-tag class="value-tag">{{ inbound.settings.ip }}</a-tag></dd>
|
||||
</div>
|
||||
<template v-if="inbound.settings.auth === 'password' && inbound.settings.accounts?.length">
|
||||
<div
|
||||
v-for="(account, idx) in inbound.settings.accounts"
|
||||
:key="idx"
|
||||
class="info-row"
|
||||
>
|
||||
<div v-for="(account, idx) in inbound.settings.accounts" :key="idx" class="info-row">
|
||||
<dt>{{ t('username') }} #{{ idx + 1 }}</dt>
|
||||
<dd class="account-row">
|
||||
<a-tag color="green" class="value-tag">{{ account.user }}</a-tag>
|
||||
@@ -651,11 +649,7 @@ const showSubscriptionTab = computed(
|
||||
|
||||
<!-- HTTP accounts -->
|
||||
<dl v-if="dbInbound.isHTTP && inbound.settings.accounts?.length" class="info-list info-list-block">
|
||||
<div
|
||||
v-for="(account, idx) in inbound.settings.accounts"
|
||||
:key="idx"
|
||||
class="info-row"
|
||||
>
|
||||
<div v-for="(account, idx) in inbound.settings.accounts" :key="idx" class="info-row">
|
||||
<dt>{{ t('username') }} #{{ idx + 1 }}</dt>
|
||||
<dd class="account-row">
|
||||
<a-tag color="green" class="value-tag">{{ account.user }}</a-tag>
|
||||
|
||||
@@ -276,8 +276,7 @@ function showQrCodeMenu(dbInbound) {
|
||||
<span class="card-id">#{{ record.id }}</span>
|
||||
<span class="tag-name">{{ record.remark }}</span>
|
||||
<div class="card-actions" @click.stop>
|
||||
<a-switch :checked="record.enable" size="small"
|
||||
@change="(next) => onSwitchEnable(record, next)" />
|
||||
<a-switch :checked="record.enable" size="small" @change="(next) => onSwitchEnable(record, next)" />
|
||||
<a-dropdown :trigger="['click']" placement="bottomRight">
|
||||
<MoreOutlined class="row-action-trigger" @click.prevent />
|
||||
<template #overlay>
|
||||
@@ -391,17 +390,17 @@ function showQrCodeMenu(dbInbound) {
|
||||
:color="ColorUtils.usageColor(Date.now(), expireDiff, record._expiryTime)">
|
||||
{{ IntlUtil.formatRelativeTime(record.expiryTime) }}
|
||||
</a-tag>
|
||||
<a-tag v-else color="purple"><InfinityIcon /></a-tag>
|
||||
<a-tag v-else color="purple">
|
||||
<InfinityIcon />
|
||||
</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Expanded client list (multi-user only) -->
|
||||
<div v-if="record.isMultiUser() && isExpanded(record.id)" class="card-clients">
|
||||
<ClientRowTable :db-inbound="record" :is-mobile="true"
|
||||
:traffic-diff="trafficDiff" :expire-diff="expireDiff" :online-clients="onlineClients"
|
||||
:last-online-map="lastOnlineMap" :is-dark-theme="isDarkTheme"
|
||||
@edit-client="(p) => emit('edit-client', p)"
|
||||
@qrcode-client="(p) => emit('qrcode-client', p)"
|
||||
<ClientRowTable :db-inbound="record" :is-mobile="true" :traffic-diff="trafficDiff" :expire-diff="expireDiff"
|
||||
:online-clients="onlineClients" :last-online-map="lastOnlineMap" :is-dark-theme="isDarkTheme"
|
||||
@edit-client="(p) => emit('edit-client', p)" @qrcode-client="(p) => emit('qrcode-client', p)"
|
||||
@info-client="(p) => emit('info-client', p)"
|
||||
@reset-traffic-client="(p) => emit('reset-traffic-client', p)"
|
||||
@delete-client="(p) => emit('delete-client', p)"
|
||||
@@ -412,8 +411,7 @@ function showQrCodeMenu(dbInbound) {
|
||||
|
||||
<!-- ====================== Desktop: a-table ======================== -->
|
||||
<a-table v-else :columns="columns" :data-source="visibleInbounds" :row-key="(r) => r.id"
|
||||
:pagination="paginationFor(visibleInbounds)" :scroll="{ x: 1000 }"
|
||||
:style="{ marginTop: '10px' }" size="small"
|
||||
:pagination="paginationFor(visibleInbounds)" :scroll="{ x: 1000 }" :style="{ marginTop: '10px' }" size="small"
|
||||
:row-class-name="(r) => (r.isMultiUser() ? '' : 'hide-expand-icon')">
|
||||
<!-- Per-inbound client list, expanded by clicking the row's
|
||||
default expand chevron. Hidden via row-class-name for
|
||||
@@ -697,6 +695,7 @@ function showQrCodeMenu(dbInbound) {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
:global(body.dark) .inbound-card {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
@@ -709,10 +708,12 @@ function showQrCodeMenu(dbInbound) {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.card-id {
|
||||
font-size: 11px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.tag-name {
|
||||
font-weight: 600;
|
||||
flex: 1;
|
||||
@@ -721,18 +722,21 @@ function showQrCodeMenu(dbInbound) {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.card-expand {
|
||||
font-size: 12px;
|
||||
opacity: 0.6;
|
||||
transition: transform 150ms ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.card-expand.is-expanded {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
@@ -742,12 +746,14 @@ function showQrCodeMenu(dbInbound) {
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.stat-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
@@ -756,6 +762,7 @@ function showQrCodeMenu(dbInbound) {
|
||||
min-width: 96px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.card-stats :deep(.ant-tag) {
|
||||
margin: 0;
|
||||
}
|
||||
@@ -777,10 +784,12 @@ function showQrCodeMenu(dbInbound) {
|
||||
padding: 0 12px;
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
:deep(.ant-card-head-title),
|
||||
:deep(.ant-card-extra) {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
:deep(.ant-card-body) {
|
||||
padding: 8px;
|
||||
}
|
||||
@@ -790,7 +799,8 @@ function showQrCodeMenu(dbInbound) {
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
.filter-bar.mobile > * {
|
||||
|
||||
.filter-bar.mobile>* {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -608,11 +608,11 @@ function onRowAction({ key, dbInbound }) {
|
||||
<!-- Inbound list — toolbar, search/filter, columns, row actions -->
|
||||
<a-col :span="24">
|
||||
<InboundList :db-inbounds="dbInbounds" :client-count="clientCount" :online-clients="onlineClients"
|
||||
:last-online-map="lastOnlineMap" :is-dark-theme="themeState.isDark"
|
||||
:expire-diff="expireDiff" :traffic-diff="trafficDiff" :page-size="pageSize" :is-mobile="isMobile"
|
||||
:sub-enable="subSettings.enable" :nodes-by-id="nodesById" @refresh="refresh" @add-inbound="onAddInbound"
|
||||
@general-action="onGeneralAction" @row-action="onRowAction" @edit-client="onEditClient"
|
||||
@qrcode-client="onQrcodeClient" @info-client="onInfoClient"
|
||||
:last-online-map="lastOnlineMap" :is-dark-theme="themeState.isDark" :expire-diff="expireDiff"
|
||||
:traffic-diff="trafficDiff" :page-size="pageSize" :is-mobile="isMobile"
|
||||
:sub-enable="subSettings.enable" :nodes-by-id="nodesById" @refresh="refresh"
|
||||
@add-inbound="onAddInbound" @general-action="onGeneralAction" @row-action="onRowAction"
|
||||
@edit-client="onEditClient" @qrcode-client="onQrcodeClient" @info-client="onInfoClient"
|
||||
@reset-traffic-client="onResetTrafficClient" @delete-client="onDeleteClient"
|
||||
@toggle-enable-client="onToggleEnableClient" />
|
||||
</a-col>
|
||||
|
||||
@@ -107,11 +107,7 @@ function download() {
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<div v-if="showQr" class="qr-panel-canvas">
|
||||
<canvas
|
||||
ref="canvas"
|
||||
:style="{ width: `${size}px`, height: `${size}px` }"
|
||||
@click="copy"
|
||||
/>
|
||||
<canvas ref="canvas" :style="{ width: `${size}px`, height: `${size}px` }" @click="copy" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -154,5 +150,4 @@ function download() {
|
||||
image-rendering: pixelated;
|
||||
image-rendering: crisp-edges;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user