"use client"; import { FormEvent, useEffect, useMemo, useRef, useState } from "react"; import { Loader2, RefreshCw, Search, Trash2 } from "lucide-react"; import { crossfadeIn, pulseFeedback, revealChildren, runScopedMotion } from "@/lib/ui/motion"; type LogLevel = "info" | "warning" | "error"; type LogEntry = { id: string; createdAt: string; level: LogLevel; source: string; message: string; status?: number; method?: string; path?: string; stack?: string; details?: unknown; }; type LogPayload = { entries: LogEntry[]; logPath: string; maxBytes: number; }; const LEVEL_TABS: Array<{ id: LogLevel | "all"; label: string }> = [ { id: "all", label: "全部" }, { id: "error", label: "错误" }, { id: "warning", label: "警告" }, { id: "info", label: "信息" } ]; export function LogManager() { const [payload, setPayload] = useState(null); const [level, setLevel] = useState("all"); const [query, setQuery] = useState(""); const [draftQuery, setDraftQuery] = useState(""); const [loading, setLoading] = useState(true); const [clearing, setClearing] = useState(false); const [error, setError] = useState(null); const [message, setMessage] = useState(null); const logsRef = useRef(null); const contentRef = useRef(null); const feedbackRef = useRef(null); useEffect(() => { void loadLogs(); }, [level, query]); useEffect(() => { return runScopedMotion(logsRef, (scope) => revealChildren(scope)); }, []); useEffect(() => { crossfadeIn(contentRef.current); }, [payload?.entries]); useEffect(() => { pulseFeedback(feedbackRef.current); }, [error, message]); const stats = useMemo(() => { const entries = payload?.entries || []; return { total: entries.length, error: entries.filter((entry) => entry.level === "error").length, warning: entries.filter((entry) => entry.level === "warning").length, info: entries.filter((entry) => entry.level === "info").length }; }, [payload]); async function loadLogs() { setLoading(true); setError(null); try { const params = new URLSearchParams({ level, limit: "200" }); if (query.trim()) params.set("q", query.trim()); const response = await fetch(`/api/logs?${params.toString()}`, { cache: "no-store" }); const nextPayload = await response.json(); if (!response.ok) throw new Error(nextPayload.error || "读取日志失败"); setPayload(nextPayload); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setLoading(false); } } async function clearLogs() { if (!window.confirm("确定清空后台日志?")) return; setClearing(true); setError(null); setMessage(null); try { const response = await fetch("/api/logs", { method: "DELETE" }); const result = await response.json(); if (!response.ok) throw new Error(result.error || "清空日志失败"); setMessage("日志已清空。"); await loadLogs(); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setClearing(false); } } function submitSearch(event: FormEvent) { event.preventDefault(); setQuery(draftQuery.trim()); } return (

日志管理

{stats.total} 条 {stats.error} 错误 {stats.warning} 警告
{LEVEL_TABS.map((tab) => ( ))}
{error || message ? (
{error ?
{error}
: null} {message ?
{message}
: null}
) : null}
日志文件 {payload?.logPath || ".runtime/logs/server-events.jsonl"}
单文件上限 {formatBytes(payload?.maxBytes || 0)}
当前筛选 {levelLabel(level)} / {query || "全部"}
{loading && !payload ? (
正在读取日志
) : payload?.entries.length ? ( payload.entries.map((entry) => ) ) : (
暂无日志
)}
); } function LogRow({ entry }: { entry: LogEntry }) { const details = detailText(entry); return (
{levelLabel(entry.level)}
{entry.message} {formatTime(entry.createdAt)} · {entry.source}{entry.status ? ` · ${entry.status}` : ""}{entry.method || entry.path ? ` · ${[entry.method, entry.path].filter(Boolean).join(" ")}` : ""}
{details ? (
详情
{details}
) : null}
); } function detailText(entry: LogEntry): string { const parts: string[] = []; if (entry.stack) parts.push(entry.stack); if (entry.details !== undefined) parts.push(JSON.stringify(entry.details, null, 2)); return parts.join("\n\n"); } function levelLabel(level: LogLevel | "all"): string { if (level === "error") return "错误"; if (level === "warning") return "警告"; if (level === "info") return "信息"; return "全部"; } function formatTime(value: string): string { const date = new Date(value); if (Number.isNaN(date.getTime())) return value; return date.toLocaleString("zh-CN", { hour12: false }); } function formatBytes(value: number): string { if (!value) return "-"; if (value >= 1024 * 1024) return `${(value / 1024 / 1024).toFixed(1)} MB`; if (value >= 1024) return `${(value / 1024).toFixed(1)} KB`; return `${value} B`; }