feat: Implement core backend services for team owner management, SQLite persistence, and logging, alongside frontend monitoring and record views.

This commit is contained in:
2026-01-30 18:15:50 +08:00
parent e61430b60d
commit 165c6d69b9
7 changed files with 844 additions and 125 deletions

View File

@@ -0,0 +1,204 @@
import { useState, useEffect, useRef, useCallback } from 'react'
import { Terminal, Play, Pause, Trash2, ChevronDown } from 'lucide-react'
interface LogEntry {
type: string
timestamp: string
level: string
message: string
email?: string
module?: string
}
const levelColors: Record<string, string> = {
success: 'text-green-400',
error: 'text-red-400',
warning: 'text-yellow-400',
info: 'text-cyan-400',
}
const levelBgColors: Record<string, string> = {
success: 'bg-green-900/20',
error: 'bg-red-900/20',
warning: 'bg-yellow-900/20',
info: 'bg-cyan-900/20',
}
interface LiveLogViewerProps {
maxLogs?: number
autoScroll?: boolean
className?: string
}
export default function LiveLogViewer({
maxLogs = 200,
autoScroll: initialAutoScroll = true,
className = '',
}: LiveLogViewerProps) {
const [logs, setLogs] = useState<LogEntry[]>([])
const [isConnected, setIsConnected] = useState(false)
const [isPaused, setIsPaused] = useState(false)
const [autoScroll, setAutoScroll] = useState(initialAutoScroll)
const logContainerRef = useRef<HTMLDivElement>(null)
const eventSourceRef = useRef<EventSource | null>(null)
const pausedLogsRef = useRef<LogEntry[]>([])
// 连接 SSE
const connect = useCallback(() => {
if (eventSourceRef.current) {
eventSourceRef.current.close()
}
const es = new EventSource('/api/logs/stream')
eventSourceRef.current = es
es.onopen = () => {
setIsConnected(true)
}
es.onmessage = (event) => {
try {
const data = JSON.parse(event.data) as LogEntry
if (data.type === 'connected') {
console.log('SSE connected:', data)
return
}
if (data.type === 'log') {
if (isPaused) {
pausedLogsRef.current.push(data)
} else {
setLogs((prev) => {
const newLogs = [...prev, data]
return newLogs.slice(-maxLogs)
})
}
}
} catch (e) {
console.error('Parse error:', e)
}
}
es.onerror = () => {
setIsConnected(false)
// 自动重连
setTimeout(connect, 3000)
}
}, [isPaused, maxLogs])
// 组件挂载时连接
useEffect(() => {
connect()
return () => {
if (eventSourceRef.current) {
eventSourceRef.current.close()
}
}
}, [connect])
// 自动滚动
useEffect(() => {
if (autoScroll && logContainerRef.current) {
logContainerRef.current.scrollTop = logContainerRef.current.scrollHeight
}
}, [logs, autoScroll])
// 恢复暂停的日志
const handleResume = () => {
setIsPaused(false)
if (pausedLogsRef.current.length > 0) {
setLogs((prev) => {
const newLogs = [...prev, ...pausedLogsRef.current]
pausedLogsRef.current = []
return newLogs.slice(-maxLogs)
})
}
}
const handleClear = () => {
setLogs([])
}
return (
<div className={`bg-slate-900 rounded-xl border border-slate-700 overflow-hidden ${className}`}>
{/* Header */}
<div className="flex items-center justify-between px-4 py-2 bg-slate-800/50 border-b border-slate-700">
<div className="flex items-center gap-2">
<Terminal className="h-4 w-4 text-green-400" />
<span className="text-sm font-medium text-slate-200"></span>
<span className={`w-2 h-2 rounded-full ${isConnected ? 'bg-green-500 animate-pulse' : 'bg-red-500'}`} />
<span className="text-xs text-slate-400">
{isConnected ? '已连接' : '连接中...'}
</span>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-slate-500">{logs.length} </span>
<button
onClick={() => isPaused ? handleResume() : setIsPaused(true)}
className={`p-1.5 rounded-lg transition-colors ${isPaused
? 'bg-yellow-500/20 text-yellow-400 hover:bg-yellow-500/30'
: 'bg-slate-700 text-slate-400 hover:bg-slate-600'
}`}
title={isPaused ? '继续' : '暂停'}
>
{isPaused ? <Play className="h-3.5 w-3.5" /> : <Pause className="h-3.5 w-3.5" />}
</button>
<button
onClick={handleClear}
className="p-1.5 rounded-lg bg-slate-700 text-slate-400 hover:bg-slate-600 transition-colors"
title="清空"
>
<Trash2 className="h-3.5 w-3.5" />
</button>
<button
onClick={() => setAutoScroll(!autoScroll)}
className={`p-1.5 rounded-lg transition-colors ${autoScroll
? 'bg-blue-500/20 text-blue-400'
: 'bg-slate-700 text-slate-400'
}`}
title={autoScroll ? '自动滚动: 开' : '自动滚动: 关'}
>
<ChevronDown className="h-3.5 w-3.5" />
</button>
</div>
</div>
{/* Log Content */}
<div
ref={logContainerRef}
className="h-80 overflow-y-auto p-3 font-mono text-xs space-y-0.5"
>
{logs.length === 0 ? (
<div className="flex items-center justify-center h-full text-slate-500">
...
</div>
) : (
logs.map((log, index) => (
<div
key={index}
className={`flex gap-2 px-2 py-0.5 rounded ${levelBgColors[log.level] || ''}`}
>
<span className="text-slate-500 flex-shrink-0">{log.timestamp}</span>
<span className={`flex-shrink-0 uppercase font-semibold w-16 ${levelColors[log.level] || 'text-slate-400'}`}>
[{log.level}]
</span>
<span className="text-slate-400 flex-shrink-0">[{log.module}]</span>
{log.email && (
<span className="text-purple-400 flex-shrink-0 truncate max-w-[150px]">{log.email}</span>
)}
<span className="text-slate-200 flex-1">{log.message}</span>
</div>
))
)}
</div>
{/* Paused indicator */}
{isPaused && pausedLogsRef.current.length > 0 && (
<div className="px-4 py-2 bg-yellow-500/10 border-t border-yellow-500/30 text-yellow-400 text-xs text-center">
- {pausedLogsRef.current.length}
</div>
)}
</div>
)
}