Files
diary-news/frontend/src/views/Sources.vue
Mavis 60b062daf2 feat: initial MVP - FastAPI backend + Vue3 frontend + docker-compose
- backend: FastAPI + SQLAlchemy 2.0(async) + asyncpg + Alembic
- 7 API routes: auth/me/articles/sources/bookmarks/subscriptions/admin
- models: User/Source/Article/Bookmark/Subscription/ApiToken
- services: RSS fetcher (feedparser) + Tencent TMT translator with quota + cache + local NLLB fallback
- workers: APScheduler + asyncio pipeline (fetch -> dedupe -> insert -> translate)
- seed scripts: create_user, seed_sources (5 RSS: Reuters/BBC/Al Jazeera/NHK/DW)
- frontend: Vue 3 + Vite + Naive UI + Pinia + vue-router
- pages: Login, Feed (24h), ArticleDetail, Sources, Bookmarks, AdminSources
- deploy: docker-compose (postgres/redis/api/worker/frontend/caddy)
- docs: README, DEPLOY, architecture, acceptance
2026-06-07 21:51:01 +08:00

81 lines
2.6 KiB
Vue

<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { NCard, NDataTable, NTag, NSpace, NText, NButton, useMessage } from 'naive-ui'
import { sourcesApi, adminApi, type Source } from '@/api/articles'
import { useAuthStore } from '@/stores/auth'
import dayjs from 'dayjs'
const auth = useAuthStore()
const message = useMessage()
const sources = ref<Source[]>([])
const health = ref<any[]>([])
const loading = ref(false)
async function load() {
sources.value = await sourcesApi.list()
if (auth.isOwner) {
try { health.value = await adminApi.health() } catch { /* noop */ }
}
}
async function triggerRefresh(id: number) {
try {
await adminApi.refresh(id)
message.success('已加入抓取队列')
} catch (e: any) {
message.error(e?.response?.data?.title || '触发失败')
}
}
function fmtTime(s?: string | null) {
if (!s) return '—'
return dayjs(s).format('YYYY-MM-DD HH:mm')
}
const columns = [
{ title: '名称', key: 'name' },
{ title: '源', key: 'slug' },
{ title: '地区', key: 'region' },
{ title: '优先级', key: 'priority' },
{
title: '状态', key: 'enabled',
render: (row: Source) => h(NTag, { type: row.enabled ? 'success' : 'default', size: 'small' }, () => row.enabled ? '启用' : '停用'),
},
{ title: '抓取间隔', key: 'fetch_interval_min', render: (r: Source) => `${r.fetch_interval_min}m` },
{ title: '上次抓取', key: 'last_fetched_at', render: (r: Source) => fmtTime(r.last_fetched_at) },
{ title: '上次状态', key: 'last_status' },
{
title: '操作', key: 'action', width: 120,
render: (row: Source) => h(NSpace, {}, () => [
auth.isOwner && row.enabled
? h(NButton, { size: 'small', onClick: () => triggerRefresh(row.id) }, () => '立即抓取')
: null,
]),
},
]
import { h } from 'vue'
const healthColumns = [
{ title: '源', key: 'slug' },
{ title: '24h 新增', key: 'article_count_24h' },
{ title: '连续失败', key: 'consecutive_failures' },
{ title: '当前间隔', key: 'fetch_interval_min', render: (r: any) => `${r.fetch_interval_min}m` },
{ title: '上次', key: 'last_fetched_at', render: (r: any) => fmtTime(r.last_fetched_at) },
{ title: '状态', key: 'last_status' },
]
onMounted(load)
</script>
<template>
<NSpace vertical>
<NCard title="采集源(只读)">
<NDataTable :columns="columns" :data="sources" :bordered="false" :pagination="false" />
</NCard>
<NCard v-if="auth.isOwner" title="源健康(Owner)">
<NDataTable :columns="healthColumns" :data="health" :bordered="false" :pagination="false" />
</NCard>
</NSpace>
</template>