diff --git a/frontend/src/admin/components.tsx b/frontend/src/admin/components.tsx index b2f6fdc..5a99009 100644 --- a/frontend/src/admin/components.tsx +++ b/frontend/src/admin/components.tsx @@ -25,3 +25,7 @@ export { Spinner } from '../components/ui' * 以保持本壳的导出面 == 迁移前的导出面,避免新增符号带来歧义。 */ export { Skeleton as SkeletonBlock } from '../components/ui' + +// ── P1 新增: 数据展示组件(灵感来自 beautifului.dev) ──────────────── +export { EventList, FilterTable, InsightCard, InsightGrid, LoaderGrid, SearchList, TrendingArrow } from '../components/ui' +export type { EventItem, FilterColumn, InsightCardProps } from '../components/ui' diff --git a/frontend/src/admin/pages/Dashboard.tsx b/frontend/src/admin/pages/Dashboard.tsx index 63d54ab..a7ce5db 100644 --- a/frontend/src/admin/pages/Dashboard.tsx +++ b/frontend/src/admin/pages/Dashboard.tsx @@ -14,7 +14,7 @@ import { Link } from 'react-router-dom' import { fetchAdminStats, fetchIngestStatus, fetchDashboard, fetchIngestFailures, fetchDataCompleteness, fetchPredictions } from '../../api/dal' import type { DataCompletenessResponse, IngestFailureItem } from '../../api/dal' import type { AdminStats, IngestSourceStatus, DashboardStats } from '../../api/types' -import { Card, CardBody, CardHeader, SkeletonBlock } from '../components' +import { InsightGrid, InsightCard, LoaderGrid } from '../components' /** 工作流引导(仅首次使用——库里还没有比赛时显示) */ const STEPS = [ @@ -91,7 +91,7 @@ export default function Dashboard() {
{/* ── 待办行:只有真有待办才出现 ── */} {loading ? ( - +
) : todos.length > 0 ? (
{todos.map(t => ( @@ -138,92 +138,65 @@ export default function Dashboard() { )} {/* ── 数据源:单源一行即足,不装成列表 ── */} - - - +
+
+

数据源

+ bzzoiro +
+
{loading ? ( -
- ) : ( - (() => { - const st = bzzoiro - const hasData = st && st.recent_count > 0 - const keyOk = st?.key_configured !== false - return ( -
- Bzzoiro - - {hasData ? ( - <> - {st.recent_count.toLocaleString()} 条 - {st.last_success_at ? new Date(st.last_success_at).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false }) : ''} - - ) : keyOk ? ( - 无数据 - ) : ( - 未配置 Key,去设置 - )} - -
- ) - })() - )} - - + + ) : (() => { + const st = bzzoiro + const hasData = st && st.recent_count > 0 + const keyOk = st?.key_configured !== false + return ( +
+ Bzzoiro + + {hasData ? ( + <> + {st.recent_count.toLocaleString()} 条 + {st.last_success_at ? new Date(st.last_success_at).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false }) : ''} + + ) : keyOk ? ( + 无数据 + ) : ( + 未配置 Key,去设置 + )} + +
+ ) + })()} +
+
- {/* ── 数据概览:预测活动 + 数据量,合并一卡 ── */} - - - - {stats ? ( -
-
-
-
{stats.predictions.last_24h}
-
预测 · 近 24 小时
-
-
-
{stats.predictions.last_7d}
-
预测 · 近 7 天
-
-
-
{stats.predictions.total}
-
预测 · 累计
-
-
-
-
-
{stats.matches?.total ?? 0}
-
比赛总数
-
-
-
{stats.matches?.finished ?? 0}
-
已完赛
-
-
-
{stats.stats?.total ?? 0}
-
统计行
-
-
-
{stats.standings?.total ?? 0}
-
积分榜
-
-
-
- ) : ( -
- )} -
-
+ {/* ── 数据概览:InsightCard 指标卡 ── */} + {stats ? ( +
+ + + + + + + + + + +
+ ) : ( + + )} {/* ── 已入库联赛 ── */} {dash && dash.leagues.length > 0 && ( - - - +
+
+

已入库联赛

+
+
{dash.leagues.map(l => ( @@ -232,8 +205,8 @@ export default function Dashboard() { ))}
- - +
+
)}
) diff --git a/frontend/src/admin/pages/Logs.tsx b/frontend/src/admin/pages/Logs.tsx index a9b5f46..0e09904 100644 --- a/frontend/src/admin/pages/Logs.tsx +++ b/frontend/src/admin/pages/Logs.tsx @@ -10,7 +10,7 @@ import { useEffect, useState, useCallback, useRef } from 'react' import { fetchLogs } from '../../api/dal' import type { LogEntry } from '../../api/types' -import { Card, CardBody, CardHeader, Badge, SectionHeader, Alert, Spinner, SkeletonBlock } from '../components' +import { Badge, SectionHeader, Alert, Card, CardHeader, CardBody, Spinner, FilterTable, type FilterColumn, LoaderGrid } from '../components' import { Button, Input } from '../../components/ui' const LEVELS = ['', 'INFO', 'WARNING', 'ERROR'] as const @@ -23,6 +23,52 @@ const LEVEL_BADGE: Record[] = [ + { + key: 'ts', + label: '时间', + width: '160px', + sortable: true, + render: r => {fmtTs(r.ts)}, + }, + { + key: 'level', + label: '级别', + width: '80px', + sortable: true, + filterable: true, + filterType: 'select', + filterOptions: [ + { value: 'DEBUG', label: 'DEBUG' }, + { value: 'INFO', label: 'INFO' }, + { value: 'WARNING', label: 'WARN' }, + { value: 'ERROR', label: 'ERROR' }, + ], + render: r => { + const badge = LEVEL_BADGE[r.level] ?? { status: 'info' as const, text: r.level } + return {badge.text} + }, + }, + { + key: 'logger', + label: 'Logger', + width: '160px', + sortable: true, + filterable: true, + render: r => ( + + {r.logger} + + ), + }, + { + key: 'message', + label: '消息', + filterable: true, + render: r => {r.message}, + }, +] + function fmtTs(ts: number): string { return new Date(ts * 1000).toLocaleString('zh-CN', { hour12: false }) } @@ -53,26 +99,7 @@ export default function LogsPage() { load() }, [load]) - const scrollRef = useRef(null) - const userScrolledDown = useRef(false) - // 检测用户是否向下滚动回看历史(旧日志在下方) - const handleScroll = () => { - const el = scrollRef.current - if (!el) return - const atTop = el.scrollTop < 50 - userScrolledDown.current = !atTop - } - - // 列表最新在最上面(后端倒序返回);打开页面/自动刷新时定位到顶部=最新。 - // 用户向下滚动回看历史时暂停定位,拉回顶部即恢复。 - useEffect(() => { - if (autoRefresh && !userScrolledDown.current && scrollRef.current) { - scrollRef.current.scrollTop = 0 - } - }, [entries, autoRefresh]) - - // 自动刷新 useEffect(() => { if (timerRef.current) clearInterval(timerRef.current) if (autoRefresh) { @@ -137,36 +164,18 @@ export default function LogsPage() { />
- {/* 日志列表 */} + {/* 日志表格:FilterTable(列排序 + 筛选) */} {loading && entries.length === 0 ? ( -
- {[1, 2, 3, 4, 5].map(i => )} -
+ ) : entries.length === 0 ? (

暂无匹配的日志

) : ( -
- {entries.map((e, i) => { - const badge = LEVEL_BADGE[e.level] ?? { status: 'info' as const, text: e.level } - return ( -
- {fmtTs(e.ts)} - - {badge.text} - - - {e.logger} - - - {e.message} - -
- ) - })} -
+ `${e.ts}-${e.logger}-${e.message.slice(0, 20)}`} + emptyText="暂无匹配的日志" + /> )} diff --git a/frontend/src/components/ui/FilterTable.tsx b/frontend/src/components/ui/FilterTable.tsx index f298cd9..6c5dafb 100644 --- a/frontend/src/components/ui/FilterTable.tsx +++ b/frontend/src/components/ui/FilterTable.tsx @@ -24,7 +24,7 @@ export interface FilterColumn { type SortDir = 'asc' | 'desc' | null -export function FilterTable>({ +export function FilterTable>({ columns, data, rowKey,