Skip to content

Commit 933567d

Browse files
committed
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.
1 parent 771bc7c commit 933567d

2 files changed

Lines changed: 112 additions & 83 deletions

File tree

frontend/src/pages/inbounds/ClientRowTable.vue

Lines changed: 33 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,14 @@ watch(clients, (list) => {
217217
if (next.size !== selected.value.size) selected.value = next;
218218
});
219219
220+
const statsClient = ref(null);
221+
function openStats(client) {
222+
statsClient.value = client;
223+
}
224+
function closeStats() {
225+
statsClient.value = null;
226+
}
227+
220228
function confirmBulkDelete() {
221229
const picked = clients.value.filter((c) => selected.value.has(rowKey(c)));
222230
if (picked.length === 0) return;
@@ -433,6 +441,9 @@ function confirmBulkDelete() {
433441
<span class="client-email">{{ client.email }}</span>
434442
</a-tooltip>
435443
<div class="client-card-actions">
444+
<a-tooltip :title="t('info')">
445+
<InfoCircleOutlined class="row-icon" @click="openStats(client)" />
446+
</a-tooltip>
436447
<a-switch :checked="client.enable" size="small"
437448
@change="(next) => emit('toggle-enable-client', { dbInbound, client, next })" />
438449
<a-dropdown :trigger="['click']" placement="bottomRight">
@@ -459,52 +470,55 @@ function confirmBulkDelete() {
459470
</a-dropdown>
460471
</div>
461472
</div>
473+
</div>
462474
463-
<div v-if="client.comment && client.comment.trim()" class="client-comment-line">
464-
{{ client.comment.length > 80 ? client.comment.substring(0, 77) + '' : client.comment }}
465-
</div>
466-
467-
<div class="client-card-foot">
475+
<a-modal :open="!!statsClient" :footer="null" :width="360" centered
476+
:title="statsClient ? statsClient.email || t('info') : ''" @cancel="closeStats">
477+
<div v-if="statsClient" class="client-card-foot">
478+
<div v-if="statsClient.comment && statsClient.comment.trim()" class="client-comment-line">
479+
{{ statsClient.comment }}
480+
</div>
468481
<div class="stat-row">
469482
<span class="stat-label">{{ t('pages.inbounds.traffic') }}</span>
470-
<a-tag :color="clientStatsColor(client.email)">
471-
{{ SizeFormatter.sizeFormat(getSum(client.email)) }} /
472-
<InfinityIcon v-if="isUnlimitedTotal(client)" />
473-
<template v-else>{{ totalGbDisplay(client) }}</template>
483+
<a-tag :color="clientStatsColor(statsClient.email)">
484+
{{ SizeFormatter.sizeFormat(getSum(statsClient.email)) }} /
485+
<InfinityIcon v-if="isUnlimitedTotal(statsClient)" />
486+
<template v-else>{{ totalGbDisplay(statsClient) }}</template>
474487
</a-tag>
475488
</div>
476489
<div class="stat-row">
477490
<span class="stat-label">{{ t('remained') }}</span>
478-
<a-tag v-if="isUnlimitedTotal(client)" color="purple" :style="{ border: 'none' }" class="infinite-tag">
491+
<a-tag v-if="isUnlimitedTotal(statsClient)" color="purple" :style="{ border: 'none' }" class="infinite-tag">
479492
<InfinityIcon />
480493
</a-tag>
481-
<a-tag v-else :color="isClientDepleted(client.email) ? 'red' : ''">
482-
{{ SizeFormatter.sizeFormat(getRem(client.email)) }}
494+
<a-tag v-else :color="isClientDepleted(statsClient.email) ? 'red' : ''">
495+
{{ SizeFormatter.sizeFormat(getRem(statsClient.email)) }}
483496
</a-tag>
484497
</div>
485498
<div class="stat-row">
486499
<span class="stat-label">{{ t('pages.inbounds.allTimeTraffic') }}</span>
487-
<a-tag>{{ SizeFormatter.sizeFormat(getAllTime(client.email)) }}</a-tag>
500+
<a-tag>{{ SizeFormatter.sizeFormat(getAllTime(statsClient.email)) }}</a-tag>
488501
</div>
489502
<div class="stat-row">
490503
<span class="stat-label">{{ t('online') }}</span>
491-
<a-tag v-if="client.enable && isClientOnline(client.email)" color="green">{{ t('online') }}</a-tag>
504+
<a-tag v-if="statsClient.enable && isClientOnline(statsClient.email)" color="green">{{ t('online') }}</a-tag>
492505
<a-tag v-else>{{ t('offline') }}</a-tag>
493506
</div>
494507
<div class="stat-row">
495508
<span class="stat-label">{{ t('pages.inbounds.expireDate') }}</span>
496-
<a-tag v-if="client.expiryTime > 0" :color="ColorUtils.userExpiryColor(expireDiff, client, isDarkTheme)">
497-
{{ IntlUtil.formatRelativeTime(client.expiryTime) }}
509+
<a-tag v-if="statsClient.expiryTime > 0"
510+
:color="ColorUtils.userExpiryColor(expireDiff, statsClient, isDarkTheme)">
511+
{{ IntlUtil.formatRelativeTime(statsClient.expiryTime) }}
498512
</a-tag>
499-
<a-tag v-else-if="client.expiryTime < 0" color="green">
500-
{{ -client.expiryTime / 86400000 }}d ({{ t('pages.client.delayedStart') }})
513+
<a-tag v-else-if="statsClient.expiryTime < 0" color="green">
514+
{{ -statsClient.expiryTime / 86400000 }}d ({{ t('pages.client.delayedStart') }})
501515
</a-tag>
502516
<a-tag v-else color="purple">
503517
<InfinityIcon />
504518
</a-tag>
505519
</div>
506520
</div>
507-
</div>
521+
</a-modal>
508522
</template>
509523
510524
<a-pagination v-if="pageSize > 0 && clients.length > pageSize" v-model:current="currentPage"

frontend/src/pages/inbounds/InboundList.vue

Lines changed: 79 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,14 @@ function isExpanded(id) {
263263
return expandedIds.value.has(id);
264264
}
265265
266+
const statsRecord = ref(null);
267+
function openStats(record) {
268+
statsRecord.value = record;
269+
}
270+
function closeStats() {
271+
statsRecord.value = null;
272+
}
273+
266274
// ============ Pagination ============================================
267275
function paginationFor(rows) {
268276
const size = props.pageSize > 0 ? props.pageSize : rows.length || 1;
@@ -388,13 +396,16 @@ function showQrCodeMenu(dbInbound) {
388396
<div v-if="visibleInbounds.length === 0" class="card-empty">—</div>
389397
390398
<div v-for="record in sortedInbounds" :key="record.id" class="inbound-card">
391-
<!-- Header: chevron (multi-user only) + remark + enable + actions -->
399+
<!-- Header: chevron (multi-user only) + id + remark + info + enable + actions -->
392400
<div class="card-head" @click="record.isMultiUser() && toggleExpanded(record.id)">
393401
<RightOutlined v-if="record.isMultiUser()" class="card-expand"
394402
:class="{ 'is-expanded': isExpanded(record.id) }" />
395403
<span class="card-id">#{{ record.id }}</span>
396404
<span class="tag-name">{{ record.remark }}</span>
397405
<div class="card-actions" @click.stop>
406+
<a-tooltip :title="t('info')">
407+
<InfoCircleOutlined class="row-action-trigger" @click="openStats(record)" />
408+
</a-tooltip>
398409
<a-switch :checked="record.enable" size="small" @change="(next) => onSwitchEnable(record, next)" />
399410
<a-dropdown :trigger="['click']" placement="bottomRight">
400411
<MoreOutlined class="row-action-trigger" @click.prevent />
@@ -452,69 +463,6 @@ function showQrCodeMenu(dbInbound) {
452463
</div>
453464
</div>
454465
455-
<!-- 2-column labelled stat grid: protocol/port/node + traffic/clients/expiry -->
456-
<div class="card-stats">
457-
<div class="stat-row">
458-
<span class="stat-label">{{ t('pages.inbounds.protocol') }}</span>
459-
<a-tag color="purple">{{ record.protocol }}</a-tag>
460-
<template v-if="record.isVMess || record.isVLess || record.isTrojan || record.isSS || record.isHysteria">
461-
<a-tag color="green">{{ record.isHysteria ? 'UDP' : record.toInbound().stream.network }}</a-tag>
462-
<a-tag v-if="record.toInbound().stream.isTls" color="blue">TLS</a-tag>
463-
<a-tag v-if="record.toInbound().stream.isReality" color="blue">Reality</a-tag>
464-
</template>
465-
</div>
466-
<div class="stat-row">
467-
<span class="stat-label">{{ t('pages.inbounds.port') }}</span>
468-
<a-tag>{{ record.port }}</a-tag>
469-
</div>
470-
<div v-if="hasActiveNode" class="stat-row">
471-
<span class="stat-label">{{ t('pages.inbounds.node') }}</span>
472-
<a-tag v-if="record.nodeId == null" color="default">
473-
{{ t('pages.inbounds.localPanel') }}
474-
</a-tag>
475-
<a-tag v-else-if="nodesById.get(record.nodeId)"
476-
:color="nodesById.get(record.nodeId).status === 'online' ? 'blue' : 'red'">
477-
{{ nodesById.get(record.nodeId).name }}
478-
</a-tag>
479-
<a-tag v-else color="orange">#{{ record.nodeId }}</a-tag>
480-
</div>
481-
<div class="stat-row">
482-
<span class="stat-label">{{ t('pages.inbounds.traffic') }}</span>
483-
<a-tag :color="ColorUtils.usageColor(record.up + record.down, trafficDiff, record.total)">
484-
{{ SizeFormatter.sizeFormat(record.up + record.down) }} /
485-
<template v-if="record.total > 0">{{ SizeFormatter.sizeFormat(record.total) }}</template>
486-
<InfinityIcon v-else />
487-
</a-tag>
488-
</div>
489-
<div class="stat-row">
490-
<span class="stat-label">{{ t('pages.inbounds.allTimeTraffic') }}</span>
491-
<a-tag>{{ SizeFormatter.sizeFormat(record.allTime || 0) }}</a-tag>
492-
</div>
493-
<div v-if="clientCount[record.id]" class="stat-row">
494-
<span class="stat-label">{{ t('clients') }}</span>
495-
<a-tag color="green" class="client-count-tag">{{ clientCount[record.id].clients }}</a-tag>
496-
<a-tag v-if="clientCount[record.id].online.length" color="blue">
497-
{{ clientCount[record.id].online.length }} {{ t('online') }}
498-
</a-tag>
499-
<a-tag v-if="clientCount[record.id].depleted.length" color="red">
500-
{{ clientCount[record.id].depleted.length }} {{ t('depleted') }}
501-
</a-tag>
502-
<a-tag v-if="clientCount[record.id].expiring.length" color="orange">
503-
{{ clientCount[record.id].expiring.length }} {{ t('depletingSoon') }}
504-
</a-tag>
505-
</div>
506-
<div class="stat-row">
507-
<span class="stat-label">{{ t('pages.inbounds.expireDate') }}</span>
508-
<a-tag v-if="record.expiryTime > 0"
509-
:color="ColorUtils.usageColor(Date.now(), expireDiff, record._expiryTime)">
510-
{{ IntlUtil.formatRelativeTime(record.expiryTime) }}
511-
</a-tag>
512-
<a-tag v-else color="purple">
513-
<InfinityIcon />
514-
</a-tag>
515-
</div>
516-
</div>
517-
518466
<!-- Expanded client list (multi-user only) -->
519467
<div v-if="record.isMultiUser() && isExpanded(record.id)" class="card-clients">
520468
<ClientRowTable :db-inbound="record" :is-mobile="true" :traffic-diff="trafficDiff" :expire-diff="expireDiff"
@@ -530,6 +478,73 @@ function showQrCodeMenu(dbInbound) {
530478
</div>
531479
</div>
532480
481+
<!-- ====================== Mobile: info modal ====================== -->
482+
<a-modal v-if="isMobile" :open="!!statsRecord" :footer="null" :width="360" centered
483+
:title="statsRecord ? `#${statsRecord.id} ${statsRecord.remark || ''}`.trim() : ''" @cancel="closeStats">
484+
<div v-if="statsRecord" class="card-stats">
485+
<div class="stat-row">
486+
<span class="stat-label">{{ t('pages.inbounds.protocol') }}</span>
487+
<a-tag color="purple">{{ statsRecord.protocol }}</a-tag>
488+
<template
489+
v-if="statsRecord.isVMess || statsRecord.isVLess || statsRecord.isTrojan || statsRecord.isSS || statsRecord.isHysteria">
490+
<a-tag color="green">{{ statsRecord.isHysteria ? 'UDP' : statsRecord.toInbound().stream.network }}</a-tag>
491+
<a-tag v-if="statsRecord.toInbound().stream.isTls" color="blue">TLS</a-tag>
492+
<a-tag v-if="statsRecord.toInbound().stream.isReality" color="blue">Reality</a-tag>
493+
</template>
494+
</div>
495+
<div class="stat-row">
496+
<span class="stat-label">{{ t('pages.inbounds.port') }}</span>
497+
<a-tag>{{ statsRecord.port }}</a-tag>
498+
</div>
499+
<div v-if="hasActiveNode" class="stat-row">
500+
<span class="stat-label">{{ t('pages.inbounds.node') }}</span>
501+
<a-tag v-if="statsRecord.nodeId == null" color="default">
502+
{{ t('pages.inbounds.localPanel') }}
503+
</a-tag>
504+
<a-tag v-else-if="nodesById.get(statsRecord.nodeId)"
505+
:color="nodesById.get(statsRecord.nodeId).status === 'online' ? 'blue' : 'red'">
506+
{{ nodesById.get(statsRecord.nodeId).name }}
507+
</a-tag>
508+
<a-tag v-else color="orange">#{{ statsRecord.nodeId }}</a-tag>
509+
</div>
510+
<div class="stat-row">
511+
<span class="stat-label">{{ t('pages.inbounds.traffic') }}</span>
512+
<a-tag :color="ColorUtils.usageColor(statsRecord.up + statsRecord.down, trafficDiff, statsRecord.total)">
513+
{{ SizeFormatter.sizeFormat(statsRecord.up + statsRecord.down) }} /
514+
<template v-if="statsRecord.total > 0">{{ SizeFormatter.sizeFormat(statsRecord.total) }}</template>
515+
<InfinityIcon v-else />
516+
</a-tag>
517+
</div>
518+
<div class="stat-row">
519+
<span class="stat-label">{{ t('pages.inbounds.allTimeTraffic') }}</span>
520+
<a-tag>{{ SizeFormatter.sizeFormat(statsRecord.allTime || 0) }}</a-tag>
521+
</div>
522+
<div v-if="clientCount[statsRecord.id]" class="stat-row">
523+
<span class="stat-label">{{ t('clients') }}</span>
524+
<a-tag color="green" class="client-count-tag">{{ clientCount[statsRecord.id].clients }}</a-tag>
525+
<a-tag v-if="clientCount[statsRecord.id].online.length" color="blue">
526+
{{ clientCount[statsRecord.id].online.length }} {{ t('online') }}
527+
</a-tag>
528+
<a-tag v-if="clientCount[statsRecord.id].depleted.length" color="red">
529+
{{ clientCount[statsRecord.id].depleted.length }} {{ t('depleted') }}
530+
</a-tag>
531+
<a-tag v-if="clientCount[statsRecord.id].expiring.length" color="orange">
532+
{{ clientCount[statsRecord.id].expiring.length }} {{ t('depletingSoon') }}
533+
</a-tag>
534+
</div>
535+
<div class="stat-row">
536+
<span class="stat-label">{{ t('pages.inbounds.expireDate') }}</span>
537+
<a-tag v-if="statsRecord.expiryTime > 0"
538+
:color="ColorUtils.usageColor(Date.now(), expireDiff, statsRecord._expiryTime)">
539+
{{ IntlUtil.formatRelativeTime(statsRecord.expiryTime) }}
540+
</a-tag>
541+
<a-tag v-else color="purple">
542+
<InfinityIcon />
543+
</a-tag>
544+
</div>
545+
</div>
546+
</a-modal>
547+
533548
<!-- ====================== Desktop: a-table ======================== -->
534549
<a-table v-else :columns="columns" :data-source="sortedInbounds" :row-key="(r) => r.id"
535550
:pagination="paginationFor(sortedInbounds)" :scroll="{ x: 1000 }" :style="{ marginTop: '10px' }" size="small"

0 commit comments

Comments
 (0)