瀏覽代碼

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.
MHSanaei 20 小時之前
父節點
當前提交
3e8a0eb93e
共有 2 個文件被更改,包括 38 次插入5 次删除
  1. 27 2
      frontend/src/pages/inbounds/ClientRowTable.vue
  2. 11 3
      frontend/src/pages/inbounds/InboundList.vue

+ 27 - 2
frontend/src/pages/inbounds/ClientRowTable.vue

@@ -31,6 +31,7 @@ const props = defineProps({
   onlineClients: { type: Array, default: () => [] },
   lastOnlineMap: { type: Object, default: () => ({}) },
   isDarkTheme: { type: Boolean, default: false },
+  pageSize: { type: Number, default: 0 },
 });
 
 const emit = defineEmits([
@@ -46,6 +47,20 @@ const emit = defineEmits([
 const inbound = computed(() => props.dbInbound.toInbound());
 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 =======================================
 const statsMap = computed(() => {
   const m = new Map();
@@ -246,7 +261,7 @@ function confirmBulkDelete() {
         <div class="cell cell-expiry">{{ t('pages.inbounds.expireDate') }}</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)) }">
         <div v-if="isRemovable" class="cell cell-select">
           <a-checkbox :checked="isSelected(rowKey(client))"
@@ -383,7 +398,7 @@ function confirmBulkDelete() {
 
     <!-- ====================== Mobile: card list ======================= -->
     <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)) }">
         <div class="client-card-head">
           <a-checkbox v-if="isRemovable" :checked="isSelected(rowKey(client))"
@@ -474,6 +489,10 @@ function confirmBulkDelete() {
         </div>
       </div>
     </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>
 </template>
 
@@ -687,6 +706,12 @@ function confirmBulkDelete() {
   padding: 0 !important;
 }
 
+.client-list-pagination {
+  display: flex;
+  justify-content: center;
+  padding: 10px 16px 4px;
+}
+
 /* ===== Mobile card list =========================================== */
 .client-list.is-mobile {
   display: flex;

+ 11 - 3
frontend/src/pages/inbounds/InboundList.vue

@@ -122,13 +122,19 @@ const visibleInbounds = computed(() => {
 // `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
 // labels react to live locale switches.
+const hasAnyRemark = computed(() =>
+  props.dbInbounds.some((i) => typeof i?.remark === 'string' && i.remark.trim() !== ''),
+);
+
 const desktopColumns = computed(() => {
   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.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) {
     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">
             <ClientRowTable :db-inbound="record" :is-mobile="true" :traffic-diff="trafficDiff" :expire-diff="expireDiff"
               :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)"
               @info-client="(p) => emit('info-client', p)"
               @reset-traffic-client="(p) => emit('reset-traffic-client', p)"
@@ -421,7 +428,8 @@ function showQrCodeMenu(dbInbound) {
         <template #expandedRowRender="{ record }">
           <ClientRowTable v-if="record.isMultiUser()" :db-inbound="record" :is-mobile="isMobile"
             :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)"
             @reset-traffic-client="(p) => emit('reset-traffic-client', p)"
             @delete-client="(p) => emit('delete-client', p)"