Files
AlphaPulse/web/src/pages/news/index.vue
T
eezd e80bcebd75 Replace news infinite scroll with page-based pagination
The news list API now takes page and limit and returns the total count
instead of a cursor, and the news page uses el-pagination with a
persisted page size.
2026-10-13 15:10:04 +08:00

135 lines
4.5 KiB
Vue

<script lang="ts" setup>
import type { NewsBrief, NewsEvent } from "@/apis/news"
import { listNews } from "@/apis/news"
import { useEventSource } from "@/composables/useEventSource"
import { useSourceNames } from "@/composables/useSourceNames"
import NewsDrawer from "@/pages/news/components/NewsDrawer.vue"
import { usePersisted } from "@/utils/storage"
import { formatTime } from "@/utils/text"
const sourceNames = useSourceNames()
const filters = reactive({
source: [] as string[],
q: "",
range: null as [Date, Date] | null
})
const items = ref<NewsBrief[]>([])
const total = ref(0)
const page = ref(1)
const pageSize = usePersisted("news-page-size", 30)
const loading = ref(false)
const pendingCount = ref(0)
const openedId = ref<number | null>(null)
const listBox = ref<HTMLElement>()
// 多次快速翻页时只采用最后一次请求的结果
let requestSeq = 0
async function load() {
const seq = ++requestSeq
loading.value = true
try {
const result = await listNews({
source: filters.source.length ? filters.source : undefined,
q: filters.q || undefined,
start: filters.range?.[0].toISOString(),
end: filters.range?.[1].toISOString(),
page: page.value,
limit: pageSize.value
})
if (seq !== requestSeq)
return
items.value = result.items
total.value = result.total
if (page.value === 1)
pendingCount.value = 0
listBox.value?.scrollTo({ top: 0 })
} finally {
if (seq === requestSeq)
loading.value = false
}
}
function search() {
page.value = 1
load()
}
function changePageSize() {
page.value = 1
load()
}
// 只提示符合当前来源筛选的新条目;关键字和时间范围由刷新后的查询决定
const { connected } = useEventSource<NewsEvent>("/api/news/stream", "news", (event) => {
if (event.kind === "new" && (filters.source.length === 0 || filters.source.includes(event.source_code)))
pendingCount.value++
})
load()
</script>
<template>
<div class="h-full flex flex-col gap-3">
<div class="flex flex-wrap items-center gap-3">
<el-checkbox-group v-model="filters.source" @change="search">
<el-checkbox-button v-for="(name, code) in sourceNames" :key="code" :value="code">
{{ name }}
</el-checkbox-button>
</el-checkbox-group>
<el-input v-model="filters.q" placeholder="关键字" clearable class="w-60" @keyup.enter="search" @clear="search" />
<el-date-picker
v-model="filters.range" type="datetimerange" start-placeholder="开始" end-placeholder="结束"
class="max-w-90" @change="search"
/>
<el-button type="primary" @click="search">
查询
</el-button>
<el-tag :type="connected ? 'success' : 'danger'" size="small">
{{ connected ? "实时推送已连接" : "实时推送断开" }}
</el-tag>
</div>
<el-alert
v-if="pendingCount > 0" :title="`有 ${pendingCount} 条新消息,点击刷新`" type="info" :closable="false"
class="cursor-pointer" @click="search"
/>
<div ref="listBox" v-loading="loading" class="min-h-0 flex-1 overflow-auto">
<div
v-for="item in items" :key="item.id"
class="cursor-pointer border-b border-b-solid border-[var(--el-border-color-lighter)] px-2 py-3 hover:bg-[var(--el-fill-color-light)]"
@click="openedId = item.id"
>
<div class="mb-1 flex items-center gap-2 text-xs text-[var(--el-text-color-secondary)]">
<el-tag size="small">
{{ sourceNames[item.source_code] ?? item.source_code }}
</el-tag>
<span>{{ formatTime(item.published_at) }}</span>
<el-tag v-if="item.revision_no > 1" size="small" type="warning">
已修订 {{ item.revision_no - 1 }} 次
</el-tag>
<el-tag v-if="item.is_paywalled" size="small" type="info">
付费
</el-tag>
</div>
<div v-if="item.title" class="font-bold">
{{ item.title }}
</div>
<div class="line-clamp-3 text-sm text-[var(--el-text-color-regular)]">
{{ item.summary || item.excerpt }}
</div>
</div>
<el-empty v-if="!loading && items.length === 0" description="没有符合条件的新闻" />
</div>
<el-pagination
v-model:current-page="page" v-model:page-size="pageSize" :total="total" :page-sizes="[20, 30, 50, 100]"
layout="total, sizes, prev, pager, next, jumper" background class="justify-end"
@current-change="load" @size-change="changePageSize"
/>
<NewsDrawer v-model="openedId" />
</div>
</template>