فهرست منبع

feat: redesign pagination with exact total_count from external API

- Pagination.vue: use exact total_count, remove estimation logic
  - show all page buttons (<=12 pages) or sliding window with ellipsis (>12)
  - add page number input for direct jump when >12 pages
  - simplify props to currentPage/pageSize/total
- SearchPage.vue: rename estimatedTotal to totalCount, save/restore totalCount in sessionStorage
- retrieval.ts: add total_count to SearchResponse interface
Hugvvn 1 ماه پیش
والد
کامیت
61d1a47c0e
3فایلهای تغییر یافته به همراه196 افزوده شده و 48 حذف شده
  1. 1 0
      src/api/retrieval.ts
  2. 176 16
      src/components/Pagination.vue
  3. 19 32
      src/views/SearchPage.vue

+ 1 - 0
src/api/retrieval.ts

@@ -39,6 +39,7 @@ export interface SearchResponse {
   objs: SearchResult[]
   page: number
   page_size: number
+  total_count: number
 }
 
 export function multidimensionalSearch(data: SearchParams) {

+ 176 - 16
src/components/Pagination.vue

@@ -1,33 +1,135 @@
 <template>
   <div class="pagination-wrapper">
-    <el-pagination
-      v-model:current-page="page"
-      v-model:page-size="pageSize"
-      :page-sizes="[10, 20, 50, 100]"
-      :total="total"
-      layout="total, sizes, prev, pager, next"
-      @current-change="emit('change', page, pageSize)"
-      @size-change="emit('change', 1, pageSize)"
-    />
+    <div class="pagination">
+      <!-- 总数显示 -->
+      <span class="total-info" v-if="total > 0">
+        共 {{ total }} 条
+      </span>
+
+      <!-- 每页条数选择 -->
+      <el-select v-model="localPageSize" class="page-size-select" @change="handleSizeChange">
+        <el-option v-for="size in [10, 20, 30, 50]" :key="size" :label="`${size}条/页`" :value="size" />
+      </el-select>
+
+      <!-- 上一页 -->
+      <button class="page-btn" :disabled="currentPage <= 1" @click="goToPage(currentPage - 1)">&lt;</button>
+
+      <!-- 页码按钮 -->
+      <template v-for="p in displayPages" :key="p">
+        <button v-if="p === -1" class="page-btn ellipsis" disabled key="ellipsis-start">...</button>
+        <button
+          v-else
+          class="page-btn"
+          :class="{ active: p === currentPage }"
+          @click="goToPage(p)"
+        >
+          {{ p }}
+        </button>
+      </template>
+
+      <!-- 下一页 -->
+      <button class="page-btn" :disabled="currentPage >= totalPages" @click="goToPage(currentPage + 1)">&gt;</button>
+
+      <!-- 总页数较多时:跳转输入 -->
+      <template v-if="totalPages > WINDOW_LIMIT">
+        <span class="goto-label">跳至</span>
+        <el-input
+          v-model="gotoInput"
+          class="goto-input"
+          size="small"
+          @keyup.enter="handleGotoPage"
+          @blur="handleGotoPage"
+        />
+        <span class="goto-label">页</span>
+      </template>
+    </div>
   </div>
 </template>
 
 <script setup lang="ts">
-import { ref, watch } from 'vue'
+import { ref, computed, watch } from 'vue'
 
 const props = defineProps<{
-  total: number
   currentPage: number
   pageSize: number
+  total: number
 }>()
 
-const emit = defineEmits<{ change: [page: number, pageSize: number] }>()
+const emit = defineEmits<{
+  change: [page: number, pageSize: number]
+}>()
+
+/** 页码窗口最大宽度(超过此数量则显示省略号 + 跳转输入) */
+const WINDOW_LIMIT = 12
+
+const localPageSize = ref(props.pageSize)
+const gotoInput = ref('')
+
+/** 总页数 */
+const totalPages = computed(() => {
+  if (props.total <= 0) return 1
+  return Math.ceil(props.total / props.pageSize)
+})
+
+/** 生成要显示的页码列表 */
+const displayPages = computed<number[]>(() => {
+  const total = totalPages.value
+  if (total <= WINDOW_LIMIT) {
+    return Array.from({ length: total }, (_, i) => i + 1)
+  }
+
+  const half = Math.floor(WINDOW_LIMIT / 2)
+  let start = Math.max(1, props.currentPage - half)
+  let end = Math.min(total, start + WINDOW_LIMIT - 1)
+
+  // 如果靠近末尾,让窗口贴向末尾
+  if (end - start + 1 < WINDOW_LIMIT) {
+    start = Math.max(1, end - WINDOW_LIMIT + 1)
+  }
+
+  const pages: number[] = []
+
+  // 始终显示第 1 页
+  pages.push(1)
+
+  // 如果窗口与第 1 页不连续,加省略号
+  if (start > 2) pages.push(-1)
+
+  // 显示窗口内的页码
+  for (let i = start; i <= end; i++) {
+    if (i !== 1) pages.push(i)
+  }
+
+  // 如果窗口与最后一页不连续,加省略号
+  if (end < total - 1) pages.push(-1)
 
-const page = ref(props.currentPage)
-const pageSize = ref(props.pageSize)
+  // 始终显示最后一页
+  if (total > 1) pages.push(total)
 
-watch(() => props.currentPage, (v) => { page.value = v })
-watch(() => props.pageSize, (v) => { pageSize.value = v })
+  return pages
+})
+
+function goToPage(p: number) {
+  if (p < 1 || p > totalPages.value) return
+  emit('change', p, localPageSize.value)
+}
+
+function handleSizeChange() {
+  emit('change', 1, localPageSize.value)
+}
+
+function handleGotoPage() {
+  const p = parseInt(gotoInput.value)
+  if (!isNaN(p) && p >= 1 && p <= totalPages.value) {
+    goToPage(p)
+  }
+  gotoInput.value = ''
+}
+
+// 同步 props.pageSize 变化
+watch(() => props.pageSize, (v) => {
+  localPageSize.value = v
+})
 </script>
 
 <style scoped>
@@ -36,4 +138,62 @@ watch(() => props.pageSize, (v) => { pageSize.value = v })
   justify-content: center;
   margin-top: 20px;
 }
+.pagination {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+  flex-wrap: wrap;
+  justify-content: center;
+}
+.total-info {
+  font-size: 14px;
+  color: #909399;
+  white-space: nowrap;
+  margin-right: 4px;
+}
+.page-size-select {
+  width: 110px;
+}
+.page-btn {
+  min-width: 32px;
+  height: 32px;
+  padding: 0 8px;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  background: #fff;
+  color: #606266;
+  font-size: 14px;
+  cursor: pointer;
+  transition: all 0.2s;
+}
+.page-btn:hover:not(:disabled):not(.active) {
+  color: #409eff;
+  border-color: #c6e2ff;
+  background: #ecf5ff;
+}
+.page-btn.active {
+  color: #409eff;
+  border-color: #409eff;
+  font-weight: 600;
+}
+.page-btn:disabled {
+  color: #c0c4cc;
+  cursor: not-allowed;
+}
+.page-btn.ellipsis {
+  border: none;
+  background: transparent;
+  cursor: default;
+}
+.goto-label {
+  font-size: 13px;
+  color: #909399;
+  white-space: nowrap;
+  margin-left: 8px;
+}
+.goto-input {
+  width: 56px;
+  margin-left: 2px;
+  margin-right: 2px;
+}
 </style>

+ 19 - 32
src/views/SearchPage.vue

@@ -5,12 +5,11 @@
       v-model:docType="docType"
       v-model:department="department"
       v-model:dateRange="dateRange"
-      v-model:keyword="filterKeyword"
       @change="handleFilterChange"
     />
     <div class="result-area">
       <div class="result-header">
-        <StatsOverview :total="total" />
+        <StatsOverview :count="totalCount" />
         <SortDropdown @change="handleSort" />
       </div>
       <div class="result-list" v-loading="loading">
@@ -27,14 +26,13 @@
           <el-empty description="暂无搜索结果" />
         </div>
       </div>
-      <div v-if="total > 0" class="pagination-wrapper">
-        <Pagination
-          :total="total"
-          :current-page="page"
-          :page-size="pageSize"
-          @change="handlePageChange"
-        />
-      </div>
+      <Pagination
+        v-if="searched"
+        :current-page="page"
+        :page-size="pageSize"
+        :total="totalCount"
+        @change="handlePageChange"
+      />
     </div>
 
     <!-- 文档预览弹窗 -->
@@ -98,19 +96,20 @@ const searchParams = reactive({
 const docType = ref('全部')
 const department = ref('全部')
 const dateRange = ref<[string, string] | null>(null)
-const filterKeyword = ref('')
 
 // ============================================================
 // 搜索结果状态
 // ============================================================
 const loading = ref(false)
 const results = ref<any[]>([])
-const total = ref(0)
 const page = ref(1)
 const pageSize = ref(10)
 const searched = ref(false)
 const hasRestored = ref(false) // 防止重复恢复
 
+// 搜索结果总数(外部 API total_count)
+const totalCount = ref(0)
+
 // 本地浏览量增量(解决 mapResultToItem 每次创建新对象导致更新丢失的问题)
 const browseCountDelta = reactive<Record<string, number>>({})
 
@@ -130,11 +129,10 @@ interface StoredState {
   docType: string
   department: string
   dateRange: [string, string] | null
-  filterKeyword: string
   results: any[]
-  total: number
   page: number
   pageSize: number
+  totalCount: number
   searched: boolean
   browseCountDelta: Record<string, number>
 }
@@ -147,11 +145,10 @@ function saveState() {
     docType: docType.value,
     department: department.value,
     dateRange: dateRange.value,
-    filterKeyword: filterKeyword.value,
     results: results.value,
-    total: total.value,
     page: page.value,
     pageSize: pageSize.value,
+    totalCount: totalCount.value,
     searched: searched.value,
     browseCountDelta: { ...browseCountDelta },
   }
@@ -181,11 +178,10 @@ function restoreState(): boolean {
     docType.value = state.docType
     department.value = state.department
     dateRange.value = state.dateRange
-    filterKeyword.value = state.filterKeyword
     results.value = state.results || []
-    total.value = state.total ?? 0
     page.value = state.page ?? 1
     pageSize.value = state.pageSize ?? 10
+    totalCount.value = state.totalCount ?? 0
     searched.value = true
     hasRestored.value = true
 
@@ -268,6 +264,7 @@ onDeactivated(() => {
 function handleSearch(kw: string) {
   searchParams.query = kw
   page.value = 1
+  totalCount.value = 0
   doSearch()
 }
 
@@ -279,20 +276,17 @@ function handleFilterChange(params: any) {
   if (params.department !== undefined) {
     searchParams.issuing_authority = params.department === '全部' ? '' : params.department
   }
-  // keyword 仅在明确传入时更新,避免空字符串覆盖已有搜索词
-  if (params.keyword && params.keyword !== '') {
-    filterKeyword.value = params.keyword
-    searchParams.query = params.keyword
-  }
   if (params.publishDateFrom) searchParams.start_time = params.publishDateFrom
   if (params.publishDateTo) searchParams.end_time = params.publishDateTo
   page.value = 1
+  totalCount.value = 0
   doSearch()
 }
 
 function handleSort(sortBy: string) {
   searchParams.sort_mode = sortBy
   page.value = 1
+  totalCount.value = 0
   doSearch()
 }
 
@@ -463,16 +457,15 @@ async function doSearch() {
     if (res.code !== undefined && String(res.code) !== '1' && String(res.code) !== '000000') {
       ElMessage.error(res.message || '检索失败')
       results.value = []
-      total.value = 0
       return
     }
     const data = res.data
     if (data) {
       results.value = data.objs || []
-      total.value = data.total_count ?? data.objs?.length ?? 0
+      // 使用外部 API 返回的 total_count
+      totalCount.value = data.total_count || 0
     } else {
       results.value = []
-      total.value = 0
     }
   } catch (e: any) {
     ElMessage.error(e.message || '检索失败')
@@ -504,12 +497,6 @@ async function doSearch() {
 .empty-result {
   padding: 40px 0;
 }
-.pagination-wrapper {
-  display: flex;
-  justify-content: center;
-  margin-top: 24px;
-}
-
 .preview-container {
   height: 75vh;
   min-height: 500px;