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,