feat(inbounds): collapse mobile cards to id/email + info button

Mobile inbound cards now show only #id and remark; mobile client cards
show only the status badge and email. The full stat grid (protocol,
port, node, traffic, all-time, clients, expiry — and per-client
remained/online/expiry) moves behind a new info icon that opens an
a-modal, so the list stays scannable on small screens.
This commit is contained in:
MHSanaei
2026-05-13 23:03:19 +02:00
parent 771bc7c8ef
commit 933567d423
2 changed files with 112 additions and 83 deletions
+79 -64
View File
@@ -263,6 +263,14 @@ function isExpanded(id) {
return expandedIds.value.has(id);
}
const statsRecord = ref(null);
function openStats(record) {
statsRecord.value = record;
}
function closeStats() {
statsRecord.value = null;
}
// ============ Pagination ============================================
function paginationFor(rows) {
const size = props.pageSize > 0 ? props.pageSize : rows.length || 1;
@@ -388,13 +396,16 @@ function showQrCodeMenu(dbInbound) {
<div v-if="visibleInbounds.length === 0" class="card-empty"></div>
<div v-for="record in sortedInbounds" :key="record.id" class="inbound-card">
<!-- Header: chevron (multi-user only) + remark + enable + actions -->
<!-- Header: chevron (multi-user only) + id + remark + info + enable + actions -->
<div class="card-head" @click="record.isMultiUser() && toggleExpanded(record.id)">
<RightOutlined v-if="record.isMultiUser()" class="card-expand"
:class="{ 'is-expanded': isExpanded(record.id) }" />
<span class="card-id">#{{ record.id }}</span>
<span class="tag-name">{{ record.remark }}</span>
<div class="card-actions" @click.stop>
<a-tooltip :title="t('info')">
<InfoCircleOutlined class="row-action-trigger" @click="openStats(record)" />
</a-tooltip>
<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 />
@@ -452,69 +463,6 @@ function showQrCodeMenu(dbInbound) {
</div>
</div>
<!-- 2-column labelled stat grid: protocol/port/node + traffic/clients/expiry -->
<div class="card-stats">
<div class="stat-row">
<span class="stat-label">{{ t('pages.inbounds.protocol') }}</span>
<a-tag color="purple">{{ record.protocol }}</a-tag>
<template v-if="record.isVMess || record.isVLess || record.isTrojan || record.isSS || record.isHysteria">
<a-tag color="green">{{ record.isHysteria ? 'UDP' : record.toInbound().stream.network }}</a-tag>
<a-tag v-if="record.toInbound().stream.isTls" color="blue">TLS</a-tag>
<a-tag v-if="record.toInbound().stream.isReality" color="blue">Reality</a-tag>
</template>
</div>
<div class="stat-row">
<span class="stat-label">{{ t('pages.inbounds.port') }}</span>
<a-tag>{{ record.port }}</a-tag>
</div>
<div v-if="hasActiveNode" class="stat-row">
<span class="stat-label">{{ t('pages.inbounds.node') }}</span>
<a-tag v-if="record.nodeId == null" color="default">
{{ t('pages.inbounds.localPanel') }}
</a-tag>
<a-tag v-else-if="nodesById.get(record.nodeId)"
:color="nodesById.get(record.nodeId).status === 'online' ? 'blue' : 'red'">
{{ nodesById.get(record.nodeId).name }}
</a-tag>
<a-tag v-else color="orange">#{{ record.nodeId }}</a-tag>
</div>
<div class="stat-row">
<span class="stat-label">{{ t('pages.inbounds.traffic') }}</span>
<a-tag :color="ColorUtils.usageColor(record.up + record.down, trafficDiff, record.total)">
{{ SizeFormatter.sizeFormat(record.up + record.down) }} /
<template v-if="record.total > 0">{{ SizeFormatter.sizeFormat(record.total) }}</template>
<InfinityIcon v-else />
</a-tag>
</div>
<div class="stat-row">
<span class="stat-label">{{ t('pages.inbounds.allTimeTraffic') }}</span>
<a-tag>{{ SizeFormatter.sizeFormat(record.allTime || 0) }}</a-tag>
</div>
<div v-if="clientCount[record.id]" class="stat-row">
<span class="stat-label">{{ t('clients') }}</span>
<a-tag color="green" class="client-count-tag">{{ clientCount[record.id].clients }}</a-tag>
<a-tag v-if="clientCount[record.id].online.length" color="blue">
{{ clientCount[record.id].online.length }} {{ t('online') }}
</a-tag>
<a-tag v-if="clientCount[record.id].depleted.length" color="red">
{{ clientCount[record.id].depleted.length }} {{ t('depleted') }}
</a-tag>
<a-tag v-if="clientCount[record.id].expiring.length" color="orange">
{{ clientCount[record.id].expiring.length }} {{ t('depletingSoon') }}
</a-tag>
</div>
<div class="stat-row">
<span class="stat-label">{{ t('pages.inbounds.expireDate') }}</span>
<a-tag v-if="record.expiryTime > 0"
:color="ColorUtils.usageColor(Date.now(), expireDiff, record._expiryTime)">
{{ IntlUtil.formatRelativeTime(record.expiryTime) }}
</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"
@@ -530,6 +478,73 @@ function showQrCodeMenu(dbInbound) {
</div>
</div>
<!-- ====================== Mobile: info modal ====================== -->
<a-modal v-if="isMobile" :open="!!statsRecord" :footer="null" :width="360" centered
:title="statsRecord ? `#${statsRecord.id} ${statsRecord.remark || ''}`.trim() : ''" @cancel="closeStats">
<div v-if="statsRecord" class="card-stats">
<div class="stat-row">
<span class="stat-label">{{ t('pages.inbounds.protocol') }}</span>
<a-tag color="purple">{{ statsRecord.protocol }}</a-tag>
<template
v-if="statsRecord.isVMess || statsRecord.isVLess || statsRecord.isTrojan || statsRecord.isSS || statsRecord.isHysteria">
<a-tag color="green">{{ statsRecord.isHysteria ? 'UDP' : statsRecord.toInbound().stream.network }}</a-tag>
<a-tag v-if="statsRecord.toInbound().stream.isTls" color="blue">TLS</a-tag>
<a-tag v-if="statsRecord.toInbound().stream.isReality" color="blue">Reality</a-tag>
</template>
</div>
<div class="stat-row">
<span class="stat-label">{{ t('pages.inbounds.port') }}</span>
<a-tag>{{ statsRecord.port }}</a-tag>
</div>
<div v-if="hasActiveNode" class="stat-row">
<span class="stat-label">{{ t('pages.inbounds.node') }}</span>
<a-tag v-if="statsRecord.nodeId == null" color="default">
{{ t('pages.inbounds.localPanel') }}
</a-tag>
<a-tag v-else-if="nodesById.get(statsRecord.nodeId)"
:color="nodesById.get(statsRecord.nodeId).status === 'online' ? 'blue' : 'red'">
{{ nodesById.get(statsRecord.nodeId).name }}
</a-tag>
<a-tag v-else color="orange">#{{ statsRecord.nodeId }}</a-tag>
</div>
<div class="stat-row">
<span class="stat-label">{{ t('pages.inbounds.traffic') }}</span>
<a-tag :color="ColorUtils.usageColor(statsRecord.up + statsRecord.down, trafficDiff, statsRecord.total)">
{{ SizeFormatter.sizeFormat(statsRecord.up + statsRecord.down) }} /
<template v-if="statsRecord.total > 0">{{ SizeFormatter.sizeFormat(statsRecord.total) }}</template>
<InfinityIcon v-else />
</a-tag>
</div>
<div class="stat-row">
<span class="stat-label">{{ t('pages.inbounds.allTimeTraffic') }}</span>
<a-tag>{{ SizeFormatter.sizeFormat(statsRecord.allTime || 0) }}</a-tag>
</div>
<div v-if="clientCount[statsRecord.id]" class="stat-row">
<span class="stat-label">{{ t('clients') }}</span>
<a-tag color="green" class="client-count-tag">{{ clientCount[statsRecord.id].clients }}</a-tag>
<a-tag v-if="clientCount[statsRecord.id].online.length" color="blue">
{{ clientCount[statsRecord.id].online.length }} {{ t('online') }}
</a-tag>
<a-tag v-if="clientCount[statsRecord.id].depleted.length" color="red">
{{ clientCount[statsRecord.id].depleted.length }} {{ t('depleted') }}
</a-tag>
<a-tag v-if="clientCount[statsRecord.id].expiring.length" color="orange">
{{ clientCount[statsRecord.id].expiring.length }} {{ t('depletingSoon') }}
</a-tag>
</div>
<div class="stat-row">
<span class="stat-label">{{ t('pages.inbounds.expireDate') }}</span>
<a-tag v-if="statsRecord.expiryTime > 0"
:color="ColorUtils.usageColor(Date.now(), expireDiff, statsRecord._expiryTime)">
{{ IntlUtil.formatRelativeTime(statsRecord.expiryTime) }}
</a-tag>
<a-tag v-else color="purple">
<InfinityIcon />
</a-tag>
</div>
</div>
</a-modal>
<!-- ====================== Desktop: a-table ======================== -->
<a-table v-else :columns="columns" :data-source="sortedInbounds" :row-key="(r) => r.id"
:pagination="paginationFor(sortedInbounds)" :scroll="{ x: 1000 }" :style="{ marginTop: '10px' }" size="small"