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