134 lines
4.2 KiB
Vue
134 lines
4.2 KiB
Vue
|
|
<script setup lang="ts">
|
||
|
|
import { onMounted, ref, watch } from 'vue'
|
||
|
|
import { useRouter } from 'vue-router'
|
||
|
|
import {
|
||
|
|
NCard, NSpace, NTag, NText, NSelect, NInput, NButton, NEmpty, NSkeleton, NSpin,
|
||
|
|
} from 'naive-ui'
|
||
|
|
import { articlesApi, sourcesApi, type ArticleListItem, type Source } from '@/api/articles'
|
||
|
|
import { useAuthStore } from '@/stores/auth'
|
||
|
|
import dayjs from 'dayjs'
|
||
|
|
import relativeTime from 'dayjs/plugin/relativeTime'
|
||
|
|
import 'dayjs/locale/zh-cn'
|
||
|
|
dayjs.extend(relativeTime)
|
||
|
|
dayjs.locale('zh-cn')
|
||
|
|
|
||
|
|
const router = useRouter()
|
||
|
|
const auth = useAuthStore()
|
||
|
|
const items = ref<ArticleListItem[]>([])
|
||
|
|
const sources = ref<Source[]>([])
|
||
|
|
const loading = ref(false)
|
||
|
|
const cursor = ref<string | null>(null)
|
||
|
|
const exhausted = ref(false)
|
||
|
|
const sourceFilter = ref<string[]>([])
|
||
|
|
const q = ref('')
|
||
|
|
|
||
|
|
const sourceOptions = ref<{ label: string; value: string }[]>([])
|
||
|
|
|
||
|
|
async function load() {
|
||
|
|
if (loading.value) return
|
||
|
|
loading.value = true
|
||
|
|
try {
|
||
|
|
const resp = await articlesApi.list({
|
||
|
|
source: sourceFilter.value.join(',') || undefined,
|
||
|
|
q: q.value || undefined,
|
||
|
|
cursor: cursor.value || undefined,
|
||
|
|
limit: 50,
|
||
|
|
})
|
||
|
|
items.value.push(...resp.items)
|
||
|
|
cursor.value = resp.next_cursor
|
||
|
|
if (!cursor.value) exhausted.value = true
|
||
|
|
} finally {
|
||
|
|
loading.value = false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function loadSources() {
|
||
|
|
sources.value = await sourcesApi.list()
|
||
|
|
sourceOptions.value = sources.value.map((s) => ({ label: s.name, value: s.slug }))
|
||
|
|
}
|
||
|
|
|
||
|
|
function refresh() {
|
||
|
|
items.value = []
|
||
|
|
cursor.value = null
|
||
|
|
exhausted.value = false
|
||
|
|
load()
|
||
|
|
}
|
||
|
|
|
||
|
|
function open(a: ArticleListItem) {
|
||
|
|
router.push(`/article/${a.id}`)
|
||
|
|
}
|
||
|
|
|
||
|
|
function star(a: ArticleListItem) {
|
||
|
|
// 简单:登录的 star 接口后续
|
||
|
|
a.is_starred = !a.is_starred
|
||
|
|
}
|
||
|
|
|
||
|
|
function fmtTime(s?: string | null) {
|
||
|
|
if (!s) return '—'
|
||
|
|
return dayjs(s).fromNow()
|
||
|
|
}
|
||
|
|
|
||
|
|
onMounted(async () => {
|
||
|
|
await loadSources()
|
||
|
|
await load()
|
||
|
|
})
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<template>
|
||
|
|
<NSpace vertical>
|
||
|
|
<NSpace align="center" justify="space-between">
|
||
|
|
<NSpace>
|
||
|
|
<NSelect
|
||
|
|
v-model:value="sourceFilter"
|
||
|
|
multiple
|
||
|
|
clearable
|
||
|
|
placeholder="按源筛选"
|
||
|
|
:options="sourceOptions"
|
||
|
|
style="min-width: 240px"
|
||
|
|
@update:value="refresh"
|
||
|
|
/>
|
||
|
|
<NInput v-model:value="q" placeholder="关键词搜索" clearable style="width: 200px"
|
||
|
|
@keyup.enter="refresh" @clear="refresh" />
|
||
|
|
<NButton @click="refresh">刷新</NButton>
|
||
|
|
</NSpace>
|
||
|
|
<NText depth="3">{{ items.length }} 条</NText>
|
||
|
|
</NSpace>
|
||
|
|
|
||
|
|
<NSpin :show="loading && items.length === 0">
|
||
|
|
<NSkeleton v-if="loading && items.length === 0" :repeat="4" />
|
||
|
|
<NEmpty v-else-if="items.length === 0 && !loading" description="暂无新闻" />
|
||
|
|
<div v-else>
|
||
|
|
<NCard
|
||
|
|
v-for="a in items"
|
||
|
|
:key="a.id"
|
||
|
|
class="article-card"
|
||
|
|
hoverable
|
||
|
|
@click="open(a)"
|
||
|
|
>
|
||
|
|
<NSpace vertical :size="4">
|
||
|
|
<NSpace align="center" :size="8">
|
||
|
|
<NTag size="small" type="info">{{ a.source.name }}</NTag>
|
||
|
|
<NTag v-if="a.lang_src" size="small">{{ a.lang_src }}</NTag>
|
||
|
|
<NTag v-if="a.translation_status !== 'ok'" size="small" type="warning">
|
||
|
|
{{ a.translation_status }}
|
||
|
|
</NTag>
|
||
|
|
<NText depth="3" style="font-size: 12px">{{ fmtTime(a.published_at || a.fetched_at) }}</NText>
|
||
|
|
</NSpace>
|
||
|
|
<div style="font-size: 16px; font-weight: 600; color: #333">{{ a.title }}</div>
|
||
|
|
<div v-if="a.title_zh" style="font-size: 15px; color: #2080f0; font-weight: 500;">
|
||
|
|
{{ a.title_zh }}
|
||
|
|
</div>
|
||
|
|
<div v-if="a.summary_zh" style="color: #666; font-size: 13px; margin-top: 4px">
|
||
|
|
{{ a.summary_zh.slice(0, 200) }}{{ a.summary_zh.length > 200 ? '…' : '' }}
|
||
|
|
</div>
|
||
|
|
</NSpace>
|
||
|
|
</NCard>
|
||
|
|
<NSpace v-if="!exhausted" justify="center" style="margin: 16px 0">
|
||
|
|
<NButton :loading="loading" @click="load">加载更多</NButton>
|
||
|
|
</NSpace>
|
||
|
|
<NText v-else depth="3" style="display:block; text-align:center; padding: 16px">— 到底了 —</NText>
|
||
|
|
</div>
|
||
|
|
</NSpin>
|
||
|
|
</NSpace>
|
||
|
|
</template>
|