refactor(inbounds): reorder Inbound's Data tabs (client first, sub inline)
Some checks are pending
Release 3X-UI / build (386) (push) Waiting to run
Release 3X-UI / build (amd64) (push) Waiting to run
Release 3X-UI / build (arm64) (push) Waiting to run
Release 3X-UI / build (armv5) (push) Waiting to run
Release 3X-UI / build (armv6) (push) Waiting to run
Release 3X-UI / build (armv7) (push) Waiting to run
Release 3X-UI / build (s390x) (push) Waiting to run
Release 3X-UI / Build for Windows (push) Waiting to run

Show the per-client pane as the default tab and fold the subscription
URLs into its bottom (under a divider) so the modal has two tabs
instead of three. Inbound details move to the second tab and remain
the default fallback for protocol-only entries (HTTP/Mixed/Tunnel/
WireGuard) that have no per-client view.
This commit is contained in:
MHSanaei 2026-05-10 01:59:02 +02:00
parent 5ac88271af
commit 267fb1c866
No known key found for this signature in database
GPG key ID: 7E4060F2FBE5AB7A

View file

@ -180,8 +180,7 @@ function downloadText(content, filename) {
FileManager.downloadTextFile(content, filename); FileManager.downloadTextFile(content, filename);
} }
// Active tab in the 3-pane layout. Reset on each open below. const activeTab = ref('client');
const activeTab = ref('inbound');
// === Build state on open =========================================== // === Build state on open ===========================================
function genSubLink(subId) { function genSubLink(subId) {
@ -195,7 +194,7 @@ watch(() => props.open, (next) => {
if (!next) return; if (!next) return;
if (!props.dbInbound) return; if (!props.dbInbound) return;
activeTab.value = 'inbound'; activeTab.value = props.dbInbound.toInbound().clients?.length ? 'client' : 'inbound';
dbInbound.value = props.dbInbound; dbInbound.value = props.dbInbound;
inbound.value = props.dbInbound.toInbound(); inbound.value = props.dbInbound.toInbound();
@ -272,7 +271,214 @@ const showSubscriptionTab = computed(
<template v-if="dbInbound && inbound"> <template v-if="dbInbound && inbound">
<a-tabs v-model:active-key="activeTab"> <a-tabs v-model:active-key="activeTab">
<!-- ============================================================ <!-- ============================================================
TAB 1 Inbound: protocol, transport, security, per-protocol TAB 1 Client: per-client info + share links + subscription
(subscription is folded in here so users don't need a third
tab the sub URLs are per-client anyway).
============================================================== -->
<a-tab-pane v-if="showClientTab" key="client" :tab="t('pages.inbounds.client')">
<table class="info-table block">
<tbody>
<tr>
<td>{{ t('pages.inbounds.email') }}</td>
<td>
<a-tag v-if="clientSettings.email" color="green">{{ clientSettings.email }}</a-tag>
<a-tag v-else color="red">{{ t('none') }}</a-tag>
</td>
</tr>
<tr v-if="clientSettings.id">
<td>ID</td>
<td><a-tag>{{ clientSettings.id }}</a-tag></td>
</tr>
<tr v-if="dbInbound.isVMess">
<td>{{ t('security') }}</td>
<td><a-tag>{{ clientSettings.security }}</a-tag></td>
</tr>
<tr v-if="inbound.canEnableTlsFlow()">
<td>Flow</td>
<td>
<a-tag v-if="clientSettings.flow">{{ clientSettings.flow }}</a-tag>
<a-tag v-else color="orange">{{ t('none') }}</a-tag>
</td>
</tr>
<tr v-if="clientSettings.password">
<td>{{ t('password') }}</td>
<td><a-tag class="info-large-tag">{{ clientSettings.password }}</a-tag></td>
</tr>
<tr>
<td>{{ t('status') }}</td>
<td>
<a-tag v-if="isDepleted" color="red">{{ t('depleted') }}</a-tag>
<a-tag v-else-if="isEnable" color="green">{{ t('enabled') }}</a-tag>
<a-tag v-else>{{ t('disabled') }}</a-tag>
</td>
</tr>
<tr v-if="clientStats">
<td>{{ t('usage') }}</td>
<td>
<a-tag color="green">
{{ SizeFormatter.sizeFormat(clientStats.up + clientStats.down) }}
</a-tag>
<a-tag>
{{ SizeFormatter.sizeFormat(clientStats.up) }} /
{{ SizeFormatter.sizeFormat(clientStats.down) }}
</a-tag>
</td>
</tr>
<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-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-else>-</a-tag>
</td>
</tr>
<tr>
<td>{{ t('lastOnline') }}</td>
<td><a-tag>{{ formatLastOnline(clientSettings.email || '') }}</a-tag></td>
</tr>
<tr v-if="clientSettings.comment">
<td>{{ t('comment') }}</td>
<td><a-tag class="info-large-tag">{{ clientSettings.comment }}</a-tag></td>
</tr>
<tr v-if="ipLimitEnable">
<td>{{ t('pages.inbounds.IPLimit') }}</td>
<td><a-tag>{{ clientSettings.limitIp }}</a-tag></td>
</tr>
<tr v-if="ipLimitEnable && clientSettings.limitIp > 0">
<td>{{ t('pages.inbounds.IPLimitlog') }}</td>
<td>
<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>
</div>
<a-tag v-else>{{ clientIpsText || t('tgbot.noIpRecord') }}</a-tag>
</div>
<div class="ip-log-actions">
<SyncOutlined :spin="refreshing" @click="loadClientIps" />
<a-tooltip :title="t('pages.inbounds.IPLimitlogclear')">
<DeleteOutlined @click="clearClientIps" />
</a-tooltip>
</div>
</td>
</tr>
</tbody>
</table>
<!-- Remaining / total / expiry -->
<table class="info-table summary-table">
<thead>
<tr>
<th>{{ t('remained') }}</th>
<th>{{ t('pages.inbounds.totalUsage') }}</th>
<th>{{ t('pages.inbounds.expireDate') }}</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<a-tag v-if="clientStats && clientSettings.totalGB > 0" :color="statsColor(clientStats)">{{
getRemainingStats() }}</a-tag>
</td>
<td>
<a-tag v-if="clientSettings.totalGB > 0" :color="clientStats ? statsColor(clientStats) : 'default'">{{
SizeFormatter.sizeFormat(clientSettings.totalGB) }}</a-tag>
<a-tag v-else color="purple">
<InfinityIcon />
</a-tag>
</td>
<td>
<a-tag v-if="clientSettings.expiryTime > 0"
:color="ColorUtils.usageColor(Date.now(), expireDiff, clientSettings.expiryTime)">{{
IntlUtil.formatDate(clientSettings.expiryTime, datepicker) }}</a-tag>
<a-tag v-else-if="clientSettings.expiryTime < 0" color="green">
{{ clientSettings.expiryTime / -86400000 }} {{ t('day') }}
</a-tag>
<a-tag v-else color="purple">
<InfinityIcon />
</a-tag>
</td>
</tr>
</tbody>
</table>
<!-- Telegram chat id -->
<template v-if="tgBotEnable && clientSettings.tgId">
<a-divider>Telegram</a-divider>
<div class="tg-row">
<a-tag color="blue">{{ clientSettings.tgId }}</a-tag>
<a-tooltip :title="t('copy')">
<a-button size="small" @click="copyText(clientSettings.tgId)">
<template #icon>
<CopyOutlined />
</template>
</a-button>
</a-tooltip>
</div>
</template>
<!-- Per-client share links (no QR) -->
<template v-if="dbInbound.hasLink() && links.length > 0">
<a-divider>{{ t('pages.inbounds.copyLink') }}</a-divider>
<div v-for="(link, idx) in links" :key="idx" class="link-panel">
<div class="link-panel-header">
<a-tag color="green">{{ link.remark || `Link ${idx + 1}` }}</a-tag>
<a-tooltip :title="t('copy')">
<a-button size="small" @click="copyText(link.link)">
<template #icon>
<CopyOutlined />
</template>
</a-button>
</a-tooltip>
</div>
<code class="link-panel-text">{{ link.link }}</code>
</div>
</template>
<!-- Subscription URLs folded into the client tab so they sit
with the rest of the per-client data. Only visible when
subscriptions are enabled and this client has a subId. -->
<template v-if="showSubscriptionTab">
<a-divider>{{ t('subscription.title') }}</a-divider>
<div class="link-panel">
<div class="link-panel-header">
<a-tag color="green">{{ t('subscription.title') }}</a-tag>
<a-tooltip :title="t('copy')">
<a-button size="small" @click="copyText(subLink)">
<template #icon>
<CopyOutlined />
</template>
</a-button>
</a-tooltip>
</div>
<a :href="subLink" target="_blank" rel="noopener noreferrer" class="link-panel-anchor">{{ subLink }}</a>
</div>
<div v-if="subSettings.subJsonEnable && subJsonLink" class="link-panel">
<div class="link-panel-header">
<a-tag color="green">JSON</a-tag>
<a-tooltip :title="t('copy')">
<a-button size="small" @click="copyText(subJsonLink)">
<template #icon>
<CopyOutlined />
</template>
</a-button>
</a-tooltip>
</div>
<a :href="subJsonLink" target="_blank" rel="noopener noreferrer" class="link-panel-anchor">{{ subJsonLink
}}</a>
</div>
</template>
</a-tab-pane>
<!-- ============================================================
TAB 2 Inbound: protocol, transport, security, per-protocol
============================================================== --> ============================================================== -->
<a-tab-pane key="inbound" :tab="t('pages.xray.rules.inbound')"> <a-tab-pane key="inbound" :tab="t('pages.xray.rules.inbound')">
<dl class="info-list"> <dl class="info-list">
@ -572,210 +778,6 @@ const showSubscriptionTab = computed(
</div> </div>
</template> </template>
</a-tab-pane> </a-tab-pane>
<!-- ============================================================
TAB 2 Client: per-client info + share links (no QR)
============================================================== -->
<a-tab-pane v-if="showClientTab" key="client" :tab="t('pages.inbounds.client')">
<table class="info-table block">
<tbody>
<tr>
<td>{{ t('pages.inbounds.email') }}</td>
<td>
<a-tag v-if="clientSettings.email" color="green">{{ clientSettings.email }}</a-tag>
<a-tag v-else color="red">{{ t('none') }}</a-tag>
</td>
</tr>
<tr v-if="clientSettings.id">
<td>ID</td>
<td><a-tag>{{ clientSettings.id }}</a-tag></td>
</tr>
<tr v-if="dbInbound.isVMess">
<td>{{ t('security') }}</td>
<td><a-tag>{{ clientSettings.security }}</a-tag></td>
</tr>
<tr v-if="inbound.canEnableTlsFlow()">
<td>Flow</td>
<td>
<a-tag v-if="clientSettings.flow">{{ clientSettings.flow }}</a-tag>
<a-tag v-else color="orange">{{ t('none') }}</a-tag>
</td>
</tr>
<tr v-if="clientSettings.password">
<td>{{ t('password') }}</td>
<td><a-tag class="info-large-tag">{{ clientSettings.password }}</a-tag></td>
</tr>
<tr>
<td>{{ t('status') }}</td>
<td>
<a-tag v-if="isDepleted" color="red">{{ t('depleted') }}</a-tag>
<a-tag v-else-if="isEnable" color="green">{{ t('enabled') }}</a-tag>
<a-tag v-else>{{ t('disabled') }}</a-tag>
</td>
</tr>
<tr v-if="clientStats">
<td>{{ t('usage') }}</td>
<td>
<a-tag color="green">
{{ SizeFormatter.sizeFormat(clientStats.up + clientStats.down) }}
</a-tag>
<a-tag>
{{ SizeFormatter.sizeFormat(clientStats.up) }} /
{{ SizeFormatter.sizeFormat(clientStats.down) }}
</a-tag>
</td>
</tr>
<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-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-else>-</a-tag>
</td>
</tr>
<tr>
<td>{{ t('lastOnline') }}</td>
<td><a-tag>{{ formatLastOnline(clientSettings.email || '') }}</a-tag></td>
</tr>
<tr v-if="clientSettings.comment">
<td>{{ t('comment') }}</td>
<td><a-tag class="info-large-tag">{{ clientSettings.comment }}</a-tag></td>
</tr>
<tr v-if="ipLimitEnable">
<td>{{ t('pages.inbounds.IPLimit') }}</td>
<td><a-tag>{{ clientSettings.limitIp }}</a-tag></td>
</tr>
<tr v-if="ipLimitEnable && clientSettings.limitIp > 0">
<td>{{ t('pages.inbounds.IPLimitlog') }}</td>
<td>
<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>
</div>
<a-tag v-else>{{ clientIpsText || t('tgbot.noIpRecord') }}</a-tag>
</div>
<div class="ip-log-actions">
<SyncOutlined :spin="refreshing" @click="loadClientIps" />
<a-tooltip :title="t('pages.inbounds.IPLimitlogclear')">
<DeleteOutlined @click="clearClientIps" />
</a-tooltip>
</div>
</td>
</tr>
</tbody>
</table>
<!-- Remaining / total / expiry -->
<table class="info-table summary-table">
<thead>
<tr>
<th>{{ t('remained') }}</th>
<th>{{ t('pages.inbounds.totalUsage') }}</th>
<th>{{ t('pages.inbounds.expireDate') }}</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<a-tag v-if="clientStats && clientSettings.totalGB > 0" :color="statsColor(clientStats)">{{
getRemainingStats() }}</a-tag>
</td>
<td>
<a-tag v-if="clientSettings.totalGB > 0" :color="clientStats ? statsColor(clientStats) : 'default'">{{
SizeFormatter.sizeFormat(clientSettings.totalGB) }}</a-tag>
<a-tag v-else color="purple">
<InfinityIcon />
</a-tag>
</td>
<td>
<a-tag v-if="clientSettings.expiryTime > 0"
:color="ColorUtils.usageColor(Date.now(), expireDiff, clientSettings.expiryTime)">{{
IntlUtil.formatDate(clientSettings.expiryTime, datepicker) }}</a-tag>
<a-tag v-else-if="clientSettings.expiryTime < 0" color="green">
{{ clientSettings.expiryTime / -86400000 }} {{ t('day') }}
</a-tag>
<a-tag v-else color="purple">
<InfinityIcon />
</a-tag>
</td>
</tr>
</tbody>
</table>
<!-- Telegram chat id -->
<template v-if="tgBotEnable && clientSettings.tgId">
<a-divider>Telegram</a-divider>
<div class="tg-row">
<a-tag color="blue">{{ clientSettings.tgId }}</a-tag>
<a-tooltip :title="t('copy')">
<a-button size="small" @click="copyText(clientSettings.tgId)">
<template #icon>
<CopyOutlined />
</template>
</a-button>
</a-tooltip>
</div>
</template>
<!-- Per-client share links (no QR) -->
<template v-if="dbInbound.hasLink() && links.length > 0">
<a-divider>{{ t('pages.inbounds.copyLink') }}</a-divider>
<div v-for="(link, idx) in links" :key="idx" class="link-panel">
<div class="link-panel-header">
<a-tag color="green">{{ link.remark || `Link ${idx + 1}` }}</a-tag>
<a-tooltip :title="t('copy')">
<a-button size="small" @click="copyText(link.link)">
<template #icon>
<CopyOutlined />
</template>
</a-button>
</a-tooltip>
</div>
<code class="link-panel-text">{{ link.link }}</code>
</div>
</template>
</a-tab-pane>
<!-- ============================================================
TAB 3 Subscription: clickable subscription URLs
============================================================== -->
<a-tab-pane v-if="showSubscriptionTab" key="subscription" :tab="t('subscription.title')">
<div class="link-panel">
<div class="link-panel-header">
<a-tag color="green">{{ t('subscription.title') }}</a-tag>
<a-tooltip :title="t('copy')">
<a-button size="small" @click="copyText(subLink)">
<template #icon>
<CopyOutlined />
</template>
</a-button>
</a-tooltip>
</div>
<a :href="subLink" target="_blank" rel="noopener noreferrer" class="link-panel-anchor">{{ subLink }}</a>
</div>
<div v-if="subSettings.subJsonEnable && subJsonLink" class="link-panel">
<div class="link-panel-header">
<a-tag color="green">JSON</a-tag>
<a-tooltip :title="t('copy')">
<a-button size="small" @click="copyText(subJsonLink)">
<template #icon>
<CopyOutlined />
</template>
</a-button>
</a-tooltip>
</div>
<a :href="subJsonLink" target="_blank" rel="noopener noreferrer" class="link-panel-anchor">{{ subJsonLink
}}</a>
</div>
</a-tab-pane>
</a-tabs> </a-tabs>
</template> </template>
</a-modal> </a-modal>