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
This commit is contained in:
67
frontend/src/views/Bookmarks.vue
Normal file
67
frontend/src/views/Bookmarks.vue
Normal file
@@ -0,0 +1,67 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { NCard, NDataTable, NTag, NSpace, NButton, useMessage } from 'naive-ui'
|
||||
import { bookmarksApi, articlesApi } from '@/api/articles'
|
||||
import { useRouter } from 'vue-router'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
const router = useRouter()
|
||||
const message = useMessage()
|
||||
const items = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const list = await bookmarksApi.list()
|
||||
// 拉详情拿标题
|
||||
const detailed = await Promise.all(
|
||||
list.map(async (b) => {
|
||||
try {
|
||||
const a = await articlesApi.get(b.article_id)
|
||||
return { bookmark: b, article: a }
|
||||
} catch {
|
||||
return { bookmark: b, article: null }
|
||||
}
|
||||
})
|
||||
)
|
||||
items.value = detailed
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function remove(article_id: number) {
|
||||
try {
|
||||
await bookmarksApi.remove(article_id)
|
||||
message.success('已移除')
|
||||
items.value = items.value.filter((x) => x.bookmark.article_id !== article_id)
|
||||
} catch (e: any) {
|
||||
message.error(e?.response?.data?.title || '失败')
|
||||
}
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{ title: '标题', key: 'title', render: (row: any) => row.article?.title || '(已删除)' },
|
||||
{ title: '译文', key: 'title_zh', render: (row: any) => row.article?.title_zh || '—' },
|
||||
{ title: '源', key: 'source', render: (row: any) => row.article?.source?.name || '—' },
|
||||
{ title: '收藏时间', key: 'created_at', render: (row: any) => dayjs(row.bookmark.created_at).format('YYYY-MM-DD HH:mm') },
|
||||
{
|
||||
title: '操作', key: 'action', width: 200,
|
||||
render: (row: any) => h(NSpace, {}, () => [
|
||||
h(NButton, { size: 'small', onClick: () => row.article && router.push(`/article/${row.article.id}`) }, () => '查看'),
|
||||
h(NButton, { size: 'small', type: 'error', ghost: true, onClick: () => remove(row.bookmark.article_id) }, () => '移除'),
|
||||
]),
|
||||
},
|
||||
]
|
||||
|
||||
import { h } from 'vue'
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NCard title="我的收藏">
|
||||
<NDataTable :columns="columns" :data="items" :bordered="false" :loading="loading" />
|
||||
</NCard>
|
||||
</template>
|
||||
Reference in New Issue
Block a user