Files
rssKeeper/frontend/src/views/Dashboard.vue
T

193 lines
6.6 KiB
Vue
Raw Normal View History

<template>
<div>
<h1 class="page-title">📊 仪表盘</h1>
<!-- 统计卡片 -->
<el-row :gutter="20" class="stats-row">
<el-col :span="6" v-for="stat in statsCards" :key="stat.key">
<el-card class="stat-card" shadow="hover">
<div class="stat-value" :style="{ color: stat.color }">{{ stat.value }}</div>
<div class="stat-label">{{ stat.label }}</div>
</el-card>
</el-col>
</el-row>
<!-- 健康度概览 + 最近活动 -->
<el-row :gutter="20" style="margin-top: 20px;">
<el-col :span="14">
<el-card shadow="hover">
<template #header>
<div style="display: flex; justify-content: space-between; align-items: center;">
<span>🩺 RSS 源健康度</span>
<el-button text size="small" @click="$router.push('/feeds')">查看全部</el-button>
</div>
</template>
<el-table :data="healthData" size="small" v-loading="loadingHealth">
<el-table-column prop="title" label="源名称" min-width="200" show-overflow-tooltip>
<template #default="scope">
<el-link :href="scope.row.url" target="_blank" type="primary">{{ scope.row.title }}</el-link>
</template>
</el-table-column>
<el-table-column prop="health_label" label="状态" width="80">
<template #default="scope">
<el-tag :type="healthTagType(scope.row.health_status)" size="small">
{{ scope.row.health_label }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="success_rate" label="成功率" width="90">
<template #default="scope">
{{ scope.row.success_rate }}%
</template>
</el-table-column>
<el-table-column prop="article_count" label="文章数" width="80" />
<el-table-column prop="days_since_fetch" label="未更新(天)" width="100">
<template #default="scope">
{{ scope.row.days_since_fetch !== null ? scope.row.days_since_fetch : '-' }}
</template>
</el-table-column>
</el-table>
</el-card>
</el-col>
<el-col :span="10">
<el-card shadow="hover">
<template #header>
<span>📋 最近抓取活动</span>
</template>
<el-timeline v-loading="loadingActivity">
<el-timeline-item
v-for="log in recentActivity"
:key="log.id"
:type="log.status === 'success' ? 'success' : 'danger'"
:icon="log.status === 'success' ? 'CircleCheck' : 'CircleClose'"
>
<div style="font-size: 13px;">
<strong>{{ log.feed_title }}</strong>
<el-tag :type="log.status === 'success' ? 'success' : 'danger'" size="small" style="margin-left: 8px;">
{{ log.status === 'success' ? '成功' : '失败' }}
</el-tag>
</div>
<div style="font-size: 12px; color: #a0aec0; margin-top: 4px;">
{{ log.status === 'success' ? `获取 ${log.articles_fetched} 篇文章` : log.error_message }}
· {{ formatTime(log.created_at) }}
</div>
</el-timeline-item>
</el-timeline>
</el-card>
</el-col>
</el-row>
<!-- 分类分布 -->
<el-row style="margin-top: 20px;">
<el-col :span="24">
<el-card shadow="hover">
<template #header>
<span>📂 分类分布</span>
</template>
<div v-if="categoryStats.length === 0" style="text-align: center; padding: 40px; color: #a0aec0;">
暂无数据
</div>
<el-row :gutter="20" v-else>
<el-col :span="4" v-for="cat in categoryStats" :key="cat.name">
<div style="text-align: center; padding: 16px;">
<div style="font-size: 24px; font-weight: bold; color: #63b3ed;">{{ cat.count }}</div>
<div style="font-size: 14px; color: #a0aec0; margin-top: 4px;">{{ cat.name || '未分类' }}</div>
</div>
</el-col>
</el-row>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import { dashboardApi, feedsApi } from '../api/index.js'
const stats = ref({})
const healthData = ref([])
const recentActivity = ref([])
const loadingHealth = ref(false)
const loadingActivity = ref(false)
const categoryStats = ref([])
const statsCards = computed(() => [
{ key: 'feeds', label: 'RSS 源总数', value: stats.value.total_feeds || 0, color: '#63b3ed' },
{ key: 'articles', label: '文章总数', value: stats.value.total_articles || 0, color: '#68d391' },
{ key: 'healthy', label: '健康源数', value: stats.value.healthy_feeds || 0, color: '#48bb78' },
{ key: 'today', label: '今日抓取', value: stats.value.today_fetches || 0, color: '#f6ad55' },
])
const healthTagType = (status) => {
const map = { healthy: 'success', warning: 'warning', unhealthy: 'danger', unknown: 'info' }
return map[status] || 'info'
}
const formatTime = (iso) => {
if (!iso) return '-'
const d = new Date(iso)
return d.toLocaleString('zh-CN', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })
}
const loadStats = async () => {
try {
stats.value = await dashboardApi.stats()
} catch (e) {
console.error('加载统计失败', e)
}
}
const loadHealth = async () => {
loadingHealth.value = true
try {
const res = await dashboardApi.health({ limit: 10 })
healthData.value = res.items || []
} catch (e) {
console.error('加载健康度失败', e)
} finally {
loadingHealth.value = false
}
}
const loadActivity = async () => {
loadingActivity.value = true
try {
const res = await dashboardApi.recentActivity()
recentActivity.value = res.items || []
} catch (e) {
console.error('加载活动失败', e)
} finally {
loadingActivity.value = false
}
}
const loadCategories = async () => {
try {
const feeds = await feedsApi.list({ limit: 1000 })
const cats = {}
for (const f of feeds.items || []) {
const c = f.category || '未分类'
cats[c] = (cats[c] || 0) + 1
}
categoryStats.value = Object.entries(cats).map(([name, count]) => ({ name, count }))
} catch (e) {
console.error('加载分类失败', e)
}
}
onMounted(() => {
loadStats()
loadHealth()
loadActivity()
loadCategories()
})
</script>
<style scoped>
.stats-row {
margin-bottom: 10px;
}
</style>