mirror of
https://github.com/yyhuni/xingrin.git
synced 2026-01-31 11:46:16 +08:00
feat(frontend): Add ANSI color support for system logs
- Create AnsiLogViewer component using xterm.js - Replace Monaco Editor with xterm for log viewing - Native ANSI escape code rendering (colors, bold, etc.) - Auto-scroll to bottom, clickable URLs support Benefits: - Colorized logs for better readability - No more escape codes like [32m[0m in UI - Professional terminal-like experience
This commit is contained in:
103
frontend/components/settings/system-logs/ansi-log-viewer.tsx
Normal file
103
frontend/components/settings/system-logs/ansi-log-viewer.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useRef } from "react"
|
||||
import { Terminal } from "@xterm/xterm"
|
||||
import { FitAddon } from "@xterm/addon-fit"
|
||||
import { WebLinksAddon } from "@xterm/addon-web-links"
|
||||
import "@xterm/xterm/css/xterm.css"
|
||||
|
||||
interface AnsiLogViewerProps {
|
||||
content: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function AnsiLogViewer({ content, className }: AnsiLogViewerProps) {
|
||||
const terminalRef = useRef<HTMLDivElement>(null)
|
||||
const xtermRef = useRef<Terminal | null>(null)
|
||||
const fitAddonRef = useRef<FitAddon | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!terminalRef.current) return
|
||||
|
||||
// 创建 Terminal 实例
|
||||
const terminal = new Terminal({
|
||||
fontSize: 12,
|
||||
fontFamily: 'Menlo, Monaco, "Courier New", monospace',
|
||||
theme: {
|
||||
background: "#1e1e1e",
|
||||
foreground: "#d4d4d4",
|
||||
},
|
||||
rows: 30,
|
||||
scrollback: 10000,
|
||||
convertEol: true,
|
||||
disableStdin: true, // 只读模式
|
||||
cursorBlink: false,
|
||||
})
|
||||
|
||||
// 添加插件
|
||||
const fitAddon = new FitAddon()
|
||||
terminal.loadAddon(fitAddon)
|
||||
terminal.loadAddon(new WebLinksAddon())
|
||||
|
||||
// 挂载到 DOM
|
||||
terminal.open(terminalRef.current)
|
||||
fitAddon.fit()
|
||||
|
||||
// 保存引用
|
||||
xtermRef.current = terminal
|
||||
fitAddonRef.current = fitAddon
|
||||
|
||||
// 监听窗口大小变化
|
||||
const handleResize = () => fitAddon.fit()
|
||||
window.addEventListener("resize", handleResize)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", handleResize)
|
||||
terminal.dispose()
|
||||
}
|
||||
}, [])
|
||||
|
||||
// 更新日志内容
|
||||
useEffect(() => {
|
||||
const terminal = xtermRef.current
|
||||
if (!terminal || !content) return
|
||||
|
||||
// 清空终端
|
||||
terminal.clear()
|
||||
|
||||
// 写入新内容
|
||||
terminal.write(content.replace(/\n/g, "\r\n")) // 转换换行符
|
||||
|
||||
// 滚动到底部
|
||||
terminal.scrollToBottom()
|
||||
}, [content])
|
||||
|
||||
// 监听主题变化(可选)
|
||||
useEffect(() => {
|
||||
const terminal = xtermRef.current
|
||||
if (!terminal) return
|
||||
|
||||
// 根据系统主题切换颜色
|
||||
const isDark = window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
terminal.options.theme = isDark
|
||||
? {
|
||||
background: "#1e1e1e",
|
||||
foreground: "#d4d4d4",
|
||||
}
|
||||
: {
|
||||
background: "#ffffff",
|
||||
foreground: "#000000",
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={terminalRef}
|
||||
className={className}
|
||||
style={{
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
export { SystemLogsView } from "./system-logs-view"
|
||||
export { AnsiLogViewer } from "./ansi-log-viewer"
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from "react"
|
||||
import Editor, { type Monaco } from "@monaco-editor/react"
|
||||
import { useColorTheme } from "@/hooks/use-color-theme"
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import { useTranslations } from "next-intl"
|
||||
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { useSystemLogs, useLogFiles } from "@/hooks/use-system-logs"
|
||||
import { LogToolbar } from "./log-toolbar"
|
||||
import { AnsiLogViewer } from "./ansi-log-viewer"
|
||||
|
||||
const DEFAULT_FILE = "xingrin.log"
|
||||
const DEFAULT_LINES = 500
|
||||
|
||||
export function SystemLogsView() {
|
||||
const { currentTheme } = useColorTheme()
|
||||
const t = useTranslations("settings.systemLogs")
|
||||
|
||||
// 状态管理
|
||||
@@ -41,20 +39,6 @@ export function SystemLogsView() {
|
||||
|
||||
const content = useMemo(() => logsData?.content ?? "", [logsData?.content])
|
||||
|
||||
const editorRef = useRef<any>(null)
|
||||
|
||||
// 自动滚动到底部
|
||||
useEffect(() => {
|
||||
const editor = editorRef.current
|
||||
if (!editor) return
|
||||
|
||||
const model = editor.getModel?.()
|
||||
if (!model) return
|
||||
|
||||
const lastLine = model.getLineCount?.() ?? 1
|
||||
editor.revealLine?.(lastLine)
|
||||
}, [content])
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="space-y-4">
|
||||
@@ -67,28 +51,14 @@ export function SystemLogsView() {
|
||||
onLinesChange={setLines}
|
||||
onAutoRefreshChange={setAutoRefresh}
|
||||
/>
|
||||
<div className="h-[calc(100vh-300px)] min-h-[360px] rounded-lg border overflow-hidden">
|
||||
<Editor
|
||||
height="100%"
|
||||
defaultLanguage="log"
|
||||
value={content || t("noContent")}
|
||||
theme={currentTheme.isDark ? "vs-dark" : "light"}
|
||||
onMount={(editor) => {
|
||||
editorRef.current = editor
|
||||
}}
|
||||
options={{
|
||||
readOnly: true,
|
||||
minimap: { enabled: false },
|
||||
fontSize: 12,
|
||||
lineNumbers: "off",
|
||||
scrollBeyondLastLine: false,
|
||||
automaticLayout: true,
|
||||
folding: false,
|
||||
wordWrap: "off",
|
||||
renderLineHighlight: "none",
|
||||
padding: { top: 12, bottom: 12 },
|
||||
}}
|
||||
/>
|
||||
<div className="h-[calc(100vh-300px)] min-h-[360px] rounded-lg border overflow-hidden bg-[#1e1e1e]">
|
||||
{content ? (
|
||||
<AnsiLogViewer content={content} />
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full text-muted-foreground">
|
||||
{t("noContent")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
Reference in New Issue
Block a user