/** * Admin 后台 - 系统日志页面(报刊风) * * 查看应用运行日志(内存缓冲,最新在前): * - 级别筛选 + 关键字搜索 * - 自动刷新(10s)可开关 * - 缓冲上限 2000 条,进程重启后清零 */ import { useEffect, useState, useCallback, useRef } from 'react' import { fetchLogs } from '../dal' import type { LogEntry } from '../types' import { Card, CardBody, CardHeader, Badge, SectionHeader, Alert, Spinner, SkeletonBlock } from '../components' const LEVELS = ['', 'INFO', 'WARNING', 'ERROR'] as const const LEVEL_BADGE: Record = { DEBUG: { status: 'info', text: 'DEBUG' }, INFO: { status: 'info', text: 'INFO' }, WARNING: { status: 'warning', text: 'WARN' }, ERROR: { status: 'error', text: 'ERROR' }, CRITICAL: { status: 'error', text: 'FATAL' }, } function fmtTs(ts: number): string { return new Date(ts * 1000).toLocaleString('zh-CN', { hour12: false }) } export default function LogsPage() { const [entries, setEntries] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState('') const [level, setLevel] = useState('') const [keyword, setKeyword] = useState('') const [autoRefresh, setAutoRefresh] = useState(true) const timerRef = useRef | null>(null) const load = useCallback(async () => { try { const d = await fetchLogs({ level: level || undefined, keyword: keyword || undefined, limit: 300 }) setEntries(d.entries) setError('') } catch (err) { setError(err instanceof Error ? err.message.split('\n')[0] : '加载失败') } finally { setLoading(false) } }, [level, keyword]) // 筛选条件变化 → 立即拉取 useEffect(() => { load() }, [load]) const scrollRef = useRef(null) const userScrolledUp = useRef(false) // 检测用户是否向上滚动过 const handleScroll = () => { const el = scrollRef.current if (!el) return const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 50 userScrolledUp.current = !atBottom } // 加载后自动滚动到底部(仅当用户未向上滚动时) useEffect(() => { if (autoRefresh && !userScrolledUp.current && scrollRef.current) { scrollRef.current.scrollTop = scrollRef.current.scrollHeight } }, [entries, autoRefresh]) // 自动刷新 useEffect(() => { if (timerRef.current) clearInterval(timerRef.current) if (autoRefresh) { timerRef.current = setInterval(load, 10_000) } return () => { if (timerRef.current) clearInterval(timerRef.current) } }, [autoRefresh, load]) return (
{error && } 0 ? `显示最新 ${entries.length} 条` : undefined} action={
} /> {/* 筛选栏 */}
{LEVELS.map(lv => ( ))}
setKeyword(e.target.value)} placeholder="搜索关键字(消息 / logger)…" className="field w-full sm:w-64" />
{/* 日志列表 */} {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}
) })}
)}
) }