import { useEffect, useState } from 'react' import { RefreshCw, Maximize2, Minimize2, Users, Signal, TriangleAlert, Cpu, MemoryStick, HardDrive, Download, Upload } from 'lucide-react' import { toast } from 'sonner' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table' import api from '@/api/client' import { getSystemMetrics, type SystemMetrics } from '@/api/system' import { createRemoteSession, fetchStreams, fetchViewerSessions, sendRemoteCommand, type StreamInfo, type ViewerSessionInfo, } from '@/api/streams' import { formatBytes } from '@/lib/status' import { PageHeader } from '@/components/page-header' import { StatCard } from '@/components/stat-card' interface NodeMetric { id: string name: string online: boolean metrics?: { cpu_percent?: number | null memory_percent?: number | null network_in_mbps?: number | null network_out_mbps?: number | null viewer_count?: number | null } | null } const QUALITIES = ['1080p', '720p', '480p', '360p'] export default function LiveBoard() { const [streams, setStreams] = useState([]) const [streamId, setStreamId] = useState() const [sessions, setSessions] = useState([]) const [selected, setSelected] = useState>(new Set()) const [loading, setLoading] = useState(true) const [systemMetrics, setSystemMetrics] = useState(null) const [nodes, setNodes] = useState([]) const [quality, setQuality] = useState('auto') const [nodeId, setNodeId] = useState('auto') const [wallMode, setWallMode] = useState(false) async function load() { try { const programs = await fetchStreams(true) setStreams(programs) const nextId = streamId || programs.find((p) => p.status === 'live')?.id if (nextId) { setStreamId(nextId) setSessions(await fetchViewerSessions(nextId)) } else { setSessions([]) } } catch { toast.error('看板数据加载失败') } finally { try { setSystemMetrics(await getSystemMetrics()) } catch { setSystemMetrics(null) } try { const nodeResponse = await api.get('/nodes') setNodes(nodeResponse.data) } catch { setNodes([]) } setLoading(false) } } useEffect(() => { void load() const timer = window.setInterval(load, 5000) return () => window.clearInterval(timer) }, [streamId]) async function controlViewers() { if (!streamId || selected.size === 0) return try { const session = await createRemoteSession(streamId) const targets = Array.from(selected) if (nodeId !== 'auto') await sendRemoteCommand(streamId, session.token, 'switch_node', targets, nodeId) if (quality !== 'auto') await sendRemoteCommand(streamId, session.token, 'switch_quality', targets, quality) if (nodeId === 'auto' && quality === 'auto') await sendRemoteCommand(streamId, session.token, 'restore_auto', targets) toast.success(`已向 ${selected.size} 个观众下发节点与清晰度策略`) } catch { toast.error('控制命令下发失败,请确认节目已启用审计式播放控制') } } const activeSessions = sessions.filter((s) => Date.now() - new Date(s.last_heartbeat).getTime() < 30000) const buffering = activeSessions.filter( (s) => s.playback_state === 'buffering' || Number(s.buffered_seconds || 0) < 1, ) const toggleSelect = (id: string) => { setSelected((prev) => { const next = new Set(prev) if (next.has(id)) next.delete(id) else next.add(id) return next }) } return (
节点实时指标
{nodes.map((node) => (
{node.name} {node.online ? '在线' : '离线'}
CPU:{node.metrics?.cpu_percent?.toFixed(1) ?? '-'}%
内存:{node.metrics?.memory_percent?.toFixed(1) ?? '-'}%
NET IN:{node.metrics?.network_in_mbps?.toFixed(2) ?? '-'} Mbps
NET OUT:{node.metrics?.network_out_mbps?.toFixed(2) ?? '-'} Mbps
观众:{node.metrics?.viewer_count ?? 0}
))}
{streamId && ( 审计式观众会话
0 && selected.size === sessions.length} onChange={(e) => setSelected(e.target.checked ? new Set(sessions.map((s) => s.session_id)) : new Set())} className="h-4 w-4 accent-primary" /> 会话 节点 清晰度 状态 缓冲 远程调整 最后心跳 错误 {loading && sessions.length === 0 ? ( 加载中… ) : sessions.length === 0 ? ( 暂无观众会话 ) : ( sessions.map((s) => ( toggleSelect(s.session_id)} className="h-4 w-4 accent-primary" /> {s.session_id.slice(0, 12)}… {s.node_name || '未知'} {s.quality || '自动'} {s.playback_state} {(Number(s.buffered_seconds || 0)).toFixed(1)}s {s.remote_adjusted ? 已调整 : '否'} {new Date(s.last_heartbeat).toLocaleTimeString()} {s.error || '-'} )) )}
)}
) }