refactor: 前端 keyset 分页推广 + cursor_date→cursor_val 统一

- AdvancedSearchRequest.cursor_date → cursor_val(通用游标值字段)
- SearchView 移除 sort===date 守卫,所有排序模式统一使用 keyset
- HomeView 游标字段同步更新
- 类型定义同步
This commit is contained in:
34047007@qq.com
2026-07-27 16:16:35 +08:00
parent 52a0d01823
commit 388cbaaa95
4 changed files with 39 additions and 43 deletions
+2 -2
View File
@@ -68,8 +68,8 @@ class AdvancedSearchRequest(BaseModel):
page: int = Field(1, ge=1)
page_size: int = Field(20, ge=1, le=100)
sort: str = "date"
# keyset 游标分页(设了 cursor 后 page 参数被忽略,不做 COUNT)
cursor_date: str | None = None # 上一页最后一条的 pub_dateISO 日期
# keyset 游标分页(所有排序模式通用,设了 cursor 后 page 参数被忽略,不做 COUNT)
cursor_val: str | None = None # 上一页最后一条的排序列值(字符串,服务端按 sort 模式解析
cursor_id: str | None = None # 上一页最后一条的 id(UUID 字符串)
# ── PubMed 筛选器参数 ──
+1 -1
View File
@@ -330,7 +330,7 @@ export interface SearchRequestBody {
page: number
page_size: number
sort?: string
cursor_date?: string
cursor_val?: string
cursor_id?: string
year_from?: number
year_to?: number
+23 -33
View File
@@ -63,9 +63,9 @@ const pageSize = ref(Number(localStorage.getItem('search:pageSize')) || 20)
// 从 URL 恢复的页码(syncSearchToUrl 写入)
const restoredPage = ref(1)
// ── Keyset 游标分页(sort=date 时使用,跳过 COUNT ──
// ── Keyset 游标分页(所有排序模式通用,跳过 COUNT 和 OFFSET ──
const keysetPage = ref(1)
const keysetCursors = ref<Array<{cursor_date: string, cursor_id: string} | null>>([null])
const keysetCursors = ref<Array<{cursor_val: string, cursor_id: string} | null>>([null])
const keysetHasMore = ref(false)
const minYear = computed(() => yearCounts.value.length ? Math.min(...yearCounts.value.map(y => y.year)) : 2000)
@@ -279,21 +279,18 @@ const { page, total, goToPage } = usePagination({
}
if (field.value !== 'all') body.field = field.value
// P3-6: precision_mode 不再发送(后端已忽略)
// ── Keyset 游标分页(sort=date 时跳过 COUNT,纯翻页模式 ──
const useKeyset = sort.value === 'date'
if (useKeyset) {
if (p === 1) {
// ── Keyset 游标分页(所有排序模式通用,跳过 COUNT + OFFSET ──
if (p === 1) {
delete (body as any).page
keysetCursors.value = [null]; keysetPage.value = 1
} else {
const entry = keysetCursors.value[p]
if (entry) {
delete (body as any).page
keysetCursors.value = [null]; keysetPage.value = 1
} else {
const entry = keysetCursors.value[p]
if (entry) {
delete (body as any).page
body.cursor_date = entry.cursor_date
body.cursor_id = entry.cursor_id
}
// cursor 不存在时保留 body.page,回退到 offset 分页
body.cursor_val = entry.cursor_val
body.cursor_id = entry.cursor_id
}
// cursor 不存在时保留 body.page,回退到 offset 分页
}
// 年份 / 日期(datePreset 与 year_* 互斥)
const yf = Number(yearFromStr.value)
@@ -343,26 +340,19 @@ const { page, total, goToPage } = usePagination({
if (negativeResult.value) body.negative_result = negativeResult.value
const { data } = await api.post('/features/search/advanced', body, { signal })
results.value = data.items || []
if (useKeyset) {
// 第一页走 COUNT,存下总数;后面沿用第一页的数值
if (p === 1) total.value = data.total || 0
keysetHasMore.value = data.has_more || false
keysetPage.value = p
// 从当前页最后一条保存游标,供下一页使用
const items = data.items || []
if (items.length > 0) {
const last = items[items.length - 1]
keysetCursors.value[p + 1] = {
cursor_date: last.pub_date || last.article_date || '',
cursor_id: last.id,
}
// 当两个日期都为空时,不设游标(避免空串 fromisoformat 失败)
if (!last.pub_date && !last.article_date) {
delete keysetCursors.value[p + 1]
}
// 游标分页:首页走 COUNT 存 total,后续页沿用;cursor 和数据一起返回
if (p === 1) total.value = data.total || 0
keysetHasMore.value = data.has_more || false
keysetPage.value = p
// 从响应保存游标,供下一页使用
const items = data.items || []
if (items.length > 0 && data.has_more && data.cursor_val) {
keysetCursors.value[p + 1] = {
cursor_val: data.cursor_val,
cursor_id: data.cursor_id || items[items.length - 1].id,
}
} else {
total.value = data.total || 0
delete keysetCursors.value[p + 1]
}
yearCounts.value = data.year_counts || []
} catch (e: any) {
+13 -7
View File
@@ -149,7 +149,7 @@ async function fetchData(resetPage = true) {
if (searchParams.value.negative_result) body.negative_result = searchParams.value.negative_result
// keyset 游标
if (cursorDate.value && cursorId.value) {
body.cursor_date = cursorDate.value
body.cursor_val = cursorDate.value
body.cursor_id = cursorId.value
}
const { data } = await api.post('/features/search/advanced', body)
@@ -160,12 +160,18 @@ async function fetchData(resetPage = true) {
feedItems.value.push(...(data.items || []))
}
hasMoreItems.value = data.has_more ?? false
// 记录游标(取最后一条
const items = data.items || []
if (items.length > 0) {
const last = items[items.length - 1]
cursorDate.value = (last.article_date || last.pub_date)?.slice(0, 10) || null
cursorId.value = last.id || null
// keyset 游标(全部使用服务端返回的游标,通用所有排序模式
if (searchParams.value.sort !== 'date') {
// 非 date 排序由服务端返回 cursor_val,用 response 字段
if (data.cursor_val) {
cursorDate.value = data.cursor_val
cursorId.value = data.cursor_id
}
} else {
if (data.cursor_val && data.cursor_id) {
cursorDate.value = data.cursor_val
cursorId.value = data.cursor_id
}
}
searched.value = true
} catch (e) { toast.apiError(e, '搜索文献失败,请重试') }