mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-05-13 09:36:05 +00:00
fix(inbounds): paginate expanded client list, restore ID column, hide empty Remark
- ClientRowTable now applies the General-Settings pageSize to its expanded client list. The 3.0 rewrite dropped pagination, so users with thousands of clients per inbound hit a 30-60s browser hang on expand (#4233). - ID column was marked responsive: ['xs'] so it was hidden on desktop; removed the restriction so it shows as the first column everywhere. - Remark column is now omitted entirely when no inbound has a non-empty remark, matching the existing Node-column pattern.
This commit is contained in:
parent
4c2915586c
commit
3e8a0eb93e
2 changed files with 38 additions and 5 deletions
|
|
@ -31,6 +31,7 @@ const props = defineProps({
|
||||||
onlineClients: { type: Array, default: () => [] },
|
onlineClients: { type: Array, default: () => [] },
|
||||||
lastOnlineMap: { type: Object, default: () => ({}) },
|
lastOnlineMap: { type: Object, default: () => ({}) },
|
||||||
isDarkTheme: { type: Boolean, default: false },
|
isDarkTheme: { type: Boolean, default: false },
|
||||||
|
pageSize: { type: Number, default: 0 },
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits([
|
const emit = defineEmits([
|
||||||
|
|
@ -46,6 +47,20 @@ const emit = defineEmits([
|
||||||
const inbound = computed(() => props.dbInbound.toInbound());
|
const inbound = computed(() => props.dbInbound.toInbound());
|
||||||
const clients = computed(() => inbound.value?.clients || []);
|
const clients = computed(() => inbound.value?.clients || []);
|
||||||
|
|
||||||
|
const currentPage = ref(1);
|
||||||
|
const paginatedClients = computed(() => {
|
||||||
|
if (!props.pageSize || props.pageSize <= 0) return clients.value;
|
||||||
|
const start = (currentPage.value - 1) * props.pageSize;
|
||||||
|
return clients.value.slice(start, start + props.pageSize);
|
||||||
|
});
|
||||||
|
|
||||||
|
watch([clients, () => props.pageSize], () => {
|
||||||
|
const total = clients.value.length;
|
||||||
|
const size = props.pageSize > 0 ? props.pageSize : (total || 1);
|
||||||
|
const maxPage = Math.max(1, Math.ceil(total / size));
|
||||||
|
if (currentPage.value > maxPage) currentPage.value = maxPage;
|
||||||
|
});
|
||||||
|
|
||||||
// === Per-client stats lookup =======================================
|
// === Per-client stats lookup =======================================
|
||||||
const statsMap = computed(() => {
|
const statsMap = computed(() => {
|
||||||
const m = new Map();
|
const m = new Map();
|
||||||
|
|
@ -246,7 +261,7 @@ function confirmBulkDelete() {
|
||||||
<div class="cell cell-expiry">{{ t('pages.inbounds.expireDate') }}</div>
|
<div class="cell cell-expiry">{{ t('pages.inbounds.expireDate') }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-for="client in clients" :key="rowKey(client)" class="client-row"
|
<div v-for="client in paginatedClients" :key="rowKey(client)" class="client-row"
|
||||||
:class="{ 'is-selected': isSelected(rowKey(client)) }">
|
:class="{ 'is-selected': isSelected(rowKey(client)) }">
|
||||||
<div v-if="isRemovable" class="cell cell-select">
|
<div v-if="isRemovable" class="cell cell-select">
|
||||||
<a-checkbox :checked="isSelected(rowKey(client))"
|
<a-checkbox :checked="isSelected(rowKey(client))"
|
||||||
|
|
@ -383,7 +398,7 @@ function confirmBulkDelete() {
|
||||||
|
|
||||||
<!-- ====================== Mobile: card list ======================= -->
|
<!-- ====================== Mobile: card list ======================= -->
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div v-for="client in clients" :key="rowKey(client)" class="client-card"
|
<div v-for="client in paginatedClients" :key="rowKey(client)" class="client-card"
|
||||||
:class="{ 'is-selected': isSelected(rowKey(client)) }">
|
:class="{ 'is-selected': isSelected(rowKey(client)) }">
|
||||||
<div class="client-card-head">
|
<div class="client-card-head">
|
||||||
<a-checkbox v-if="isRemovable" :checked="isSelected(rowKey(client))"
|
<a-checkbox v-if="isRemovable" :checked="isSelected(rowKey(client))"
|
||||||
|
|
@ -474,6 +489,10 @@ function confirmBulkDelete() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<a-pagination v-if="pageSize > 0 && clients.length > pageSize" v-model:current="currentPage"
|
||||||
|
:page-size="pageSize" :total="clients.length" :show-size-changer="false" size="small"
|
||||||
|
class="client-list-pagination" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -687,6 +706,12 @@ function confirmBulkDelete() {
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.client-list-pagination {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 10px 16px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
/* ===== Mobile card list =========================================== */
|
/* ===== Mobile card list =========================================== */
|
||||||
.client-list.is-mobile {
|
.client-list.is-mobile {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
|
|
@ -122,13 +122,19 @@ const visibleInbounds = computed(() => {
|
||||||
// `key`-driven so we can render via the body-cell slot below. AD-Vue 4's
|
// `key`-driven so we can render via the body-cell slot below. AD-Vue 4's
|
||||||
// `responsive` array still works on column defs. Computed so column
|
// `responsive` array still works on column defs. Computed so column
|
||||||
// labels react to live locale switches.
|
// labels react to live locale switches.
|
||||||
|
const hasAnyRemark = computed(() =>
|
||||||
|
props.dbInbounds.some((i) => typeof i?.remark === 'string' && i.remark.trim() !== ''),
|
||||||
|
);
|
||||||
|
|
||||||
const desktopColumns = computed(() => {
|
const desktopColumns = computed(() => {
|
||||||
const cols = [
|
const cols = [
|
||||||
{ title: 'ID', dataIndex: 'id', key: 'id', align: 'right', width: 30, responsive: ['xs'] },
|
{ title: 'ID', dataIndex: 'id', key: 'id', align: 'right', width: 30 },
|
||||||
{ title: t('pages.inbounds.operate'), key: 'action', align: 'center', width: 30 },
|
{ title: t('pages.inbounds.operate'), key: 'action', align: 'center', width: 30 },
|
||||||
{ title: t('pages.inbounds.enable'), key: 'enable', align: 'center', width: 35 },
|
{ title: t('pages.inbounds.enable'), key: 'enable', align: 'center', width: 35 },
|
||||||
{ title: t('pages.inbounds.remark'), dataIndex: 'remark', key: 'remark', align: 'center', width: 60 },
|
|
||||||
];
|
];
|
||||||
|
if (hasAnyRemark.value) {
|
||||||
|
cols.push({ title: t('pages.inbounds.remark'), dataIndex: 'remark', key: 'remark', align: 'center', width: 60 });
|
||||||
|
}
|
||||||
if (props.nodesById.size > 0) {
|
if (props.nodesById.size > 0) {
|
||||||
cols.push({ title: t('pages.inbounds.node'), key: 'node', align: 'center', width: 60 });
|
cols.push({ title: t('pages.inbounds.node'), key: 'node', align: 'center', width: 60 });
|
||||||
}
|
}
|
||||||
|
|
@ -401,6 +407,7 @@ function showQrCodeMenu(dbInbound) {
|
||||||
<div v-if="record.isMultiUser() && isExpanded(record.id)" class="card-clients">
|
<div v-if="record.isMultiUser() && isExpanded(record.id)" class="card-clients">
|
||||||
<ClientRowTable :db-inbound="record" :is-mobile="true" :traffic-diff="trafficDiff" :expire-diff="expireDiff"
|
<ClientRowTable :db-inbound="record" :is-mobile="true" :traffic-diff="trafficDiff" :expire-diff="expireDiff"
|
||||||
:online-clients="onlineClients" :last-online-map="lastOnlineMap" :is-dark-theme="isDarkTheme"
|
:online-clients="onlineClients" :last-online-map="lastOnlineMap" :is-dark-theme="isDarkTheme"
|
||||||
|
:page-size="pageSize"
|
||||||
@edit-client="(p) => emit('edit-client', p)" @qrcode-client="(p) => emit('qrcode-client', p)"
|
@edit-client="(p) => emit('edit-client', p)" @qrcode-client="(p) => emit('qrcode-client', p)"
|
||||||
@info-client="(p) => emit('info-client', p)"
|
@info-client="(p) => emit('info-client', p)"
|
||||||
@reset-traffic-client="(p) => emit('reset-traffic-client', p)"
|
@reset-traffic-client="(p) => emit('reset-traffic-client', p)"
|
||||||
|
|
@ -421,7 +428,8 @@ function showQrCodeMenu(dbInbound) {
|
||||||
<template #expandedRowRender="{ record }">
|
<template #expandedRowRender="{ record }">
|
||||||
<ClientRowTable v-if="record.isMultiUser()" :db-inbound="record" :is-mobile="isMobile"
|
<ClientRowTable v-if="record.isMultiUser()" :db-inbound="record" :is-mobile="isMobile"
|
||||||
:traffic-diff="trafficDiff" :expire-diff="expireDiff" :online-clients="onlineClients"
|
:traffic-diff="trafficDiff" :expire-diff="expireDiff" :online-clients="onlineClients"
|
||||||
:last-online-map="lastOnlineMap" :is-dark-theme="isDarkTheme" @edit-client="(p) => emit('edit-client', p)"
|
:last-online-map="lastOnlineMap" :is-dark-theme="isDarkTheme" :page-size="pageSize"
|
||||||
|
@edit-client="(p) => emit('edit-client', p)"
|
||||||
@qrcode-client="(p) => emit('qrcode-client', p)" @info-client="(p) => emit('info-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)"
|
@reset-traffic-client="(p) => emit('reset-traffic-client', p)"
|
||||||
@delete-client="(p) => emit('delete-client', p)"
|
@delete-client="(p) => emit('delete-client', p)"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue