fix: 修复 Admin 后台多个 bug

- api.ts: 修复 URL 拼接 bug(缺少 API_BASE) 和 204 判断逻辑
- dal.ts: 修复 fetchDashboard 类型错乱,修正 API 端点
- types.ts: 对齐后端 Pydantic 模型
- 所有页面: Badge variant→status, CardHeader children→action
- 移除不存在的后端端点调用
This commit is contained in:
shangfangjian
2026-09-17 02:11:50 +08:00
parent d3284c48c3
commit 6680da7d61
9 changed files with 572 additions and 1642 deletions
+39 -147
View File
@@ -1,29 +1,27 @@
/**
* Admin 后台 - 仪表盘
*
* 系统概览:
* - 数据库表行数统计
* - 最近采集状态
* - 预测统计
* - 最近错误日志
*/
import { useEffect, useState } from 'react'
import { fetchDashboard } from '../dal'
import type { DashboardStats, TableStats, CollectionRecord, ErrorLog } from '../types'
import { Card, CardBody, CardHeader, StatCard, DataTable, Badge, EmptyState } from '../components'
import { fetchDashboard, fetchHealth } from '../dal'
import type { DashboardStats } from '../types'
import { Card, CardBody, CardHeader, StatCard, Badge } from '../components'
export default function Dashboard() {
const [data, setData] = useState<DashboardStats | null>(null)
const [health, setHealth] = useState<any>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
let active = true
setLoading(true)
fetchDashboard()
.then((stats: DashboardStats) => {
if (active) setData(stats)
Promise.all([fetchDashboard(), fetchHealth()])
.then(([stats, h]) => {
if (active) {
setData(stats)
setHealth(h)
}
})
.catch((err: unknown) => {
if (active) setError(err instanceof Error ? err.message : '加载失败')
@@ -31,9 +29,7 @@ export default function Dashboard() {
.finally(() => {
if (active) setLoading(false)
})
return () => {
active = false
}
return () => { active = false }
}, [])
if (error) {
@@ -41,147 +37,43 @@ export default function Dashboard() {
<div className="rounded-lg border border-red-500/30 bg-red-500/10 p-6 text-center text-red-400">
<p className="text-lg font-medium"></p>
<p className="mt-1 text-sm">{error}</p>
<button
onClick={() => window.location.reload()}
className="mt-3 rounded bg-red-500/20 px-4 py-1 text-sm hover:bg-red-500/30"
>
</button>
</div>
)
}
return (
<div className="space-y-6">
{/* ── 统计卡片 ── */}
<div className="grid grid-cols-2 gap-4 lg:grid-cols-4">
<StatCard
label="数据库表数"
value={loading ? '—' : data?.db_tables.length ?? 0}
icon=""
/>
<StatCard
label="总预测数"
value={loading ? '—' : data?.prediction_stats.total_predictions ?? 0}
icon="◆"
/>
<StatCard
label="今日预测"
value={loading ? '—' : data?.prediction_stats.today_predictions ?? 0}
icon="◇"
/>
<StatCard
label="平均延迟"
value={loading ? '—' : `${data?.prediction_stats.avg_latency_ms ?? 0}ms`}
icon="◷"
/>
<StatCard label="健康状态" value={loading ? '—' : (health?.status === 'healthy' ? '✅ 正常' : '⚠️ 异常')} icon="●" />
<StatCard label="联赛数" value={loading ? '—' : data?.leagues.length ?? 0} icon="◫" />
<StatCard label="比赛数" value={loading ? '—' : data?.total_matches ?? 0} icon="◆" />
<StatCard label="预测数" value={loading ? '—' : data?.total_predictions ?? 0} icon="" />
</div>
<div className="grid gap-6 lg:grid-cols-2">
{/* ── 数据库表统计 ── */}
<Card>
<CardHeader title="数据库表状态" />
<CardBody className="p-0">
{loading ? (
<EmptyState text="加载中..." />
) : data && data.db_tables.length > 0 ? (
<DataTable
columns={[
{ key: 'name', label: '表名' },
{ key: 'row_count', label: '行数', width: '80px' },
{
key: 'last_updated',
label: '最后更新',
width: '180px',
render: (row: TableStats) =>
row.last_updated
? new Date(row.last_updated).toLocaleString('zh-CN')
: '—',
},
]}
data={data.db_tables}
rowKey={(row: TableStats) => row.name}
/>
) : (
<EmptyState text="暂无表统计信息" />
)}
</CardBody>
</Card>
{/* ── 最近采集记录 ── */}
<Card>
<CardHeader title="最近采集任务" />
<CardBody className="p-0">
{loading ? (
<EmptyState text="加载中..." />
) : data && data.last_collection.length > 0 ? (
<DataTable
columns={[
{ key: 'source', label: '数据源' },
{ key: 'league_code', label: '联赛', width: '80px' },
{
key: 'status',
label: '状态',
width: '90px',
render: (row: CollectionRecord) => <Badge status={row.status}>{statusLabel(row.status)}</Badge>,
},
{
key: 'finished_at',
label: '完成时间',
width: '160px',
render: (row: CollectionRecord) =>
row.finished_at
? new Date(row.finished_at).toLocaleString('zh-CN')
: '进行中',
},
]}
data={data.last_collection}
rowKey={(row: CollectionRecord) => `${row.source}-${row.started_at}`}
/>
) : (
<EmptyState text="暂无采集记录" />
)}
</CardBody>
</Card>
{/* ── 最近错误日志 ── */}
<Card className="lg:col-span-2">
<CardHeader title="最近错误日志" />
<CardBody className="p-0">
{loading ? (
<EmptyState text="加载中..." />
) : data && data.recent_errors.length > 0 ? (
<DataTable
columns={[
{
key: 'timestamp',
label: '时间',
width: '180px',
render: (row: ErrorLog) => new Date(row.timestamp).toLocaleString('zh-CN'),
},
{
key: 'level',
label: '级别',
width: '80px',
render: (row: ErrorLog) => <Badge status={row.level}>{row.level}</Badge>,
},
{ key: 'source', label: '来源', width: '120px' },
{ key: 'message', label: '消息' },
]}
data={data.recent_errors}
rowKey={(row: ErrorLog) => row.id}
/>
) : (
<EmptyState text="暂无错误日志 ✓" />
)}
</CardBody>
</Card>
</div>
<Card>
<CardHeader title="最近联赛" />
<CardBody>
{loading ? (
<p className="text-gray-400">...</p>
) : data && data.leagues.length > 0 ? (
<div className="flex flex-wrap gap-2">
{data.leagues.map(l => (
<Badge key={l.code} status="info">
{l.name_zh || l.name} ({l.code})
</Badge>
))}
</div>
) : (
<p className="text-gray-400">,</p>
)}
</CardBody>
</Card>
</div>
)
}
function statusLabel(status: string): string {
const map: Record<string, string> = {
running: '运行中',
success: '成功',
failed: '失败',
queued: '排队',
completed: '已完成',
}
return map[status] ?? status
}