import { qs, escapeHtml, formatDate, STATUS_LABELS, classLabel, enabledChannelNames } from "../modules/dom.js"; import { apiFetch } from "../modules/api.js"; import { showToast, setServiceStatus, setChannelReady } from "../modules/ui.js"; import { renderTrend } from "../modules/charts.js"; import { renderRobots } from "./robotCards.js"; import { store } from "../modules/store.js"; export async function loadDashboard() { try { const data = await apiFetch("/api/dashboard"); setServiceStatus(true); updateDashboard(data); } catch (error) { setServiceStatus(false); showToast(`无法读取管理数据:${error.message}`, "error"); } } function updateDashboard(data) { store.dashboardData = data; const { summary, system, detection } = data; const channels = system.alert_channels || {}; const channelNames = enabledChannelNames(channels); qs("#todayEventCount").textContent = String(summary.today); qs("#pendingEventCount").textContent = String(summary.pending); qs("#pendingNavCount").textContent = String(summary.pending); qs("#notificationDot").hidden = summary.pending === 0; qs("#modelStatusValue").textContent = system.weights_available ? "运行正常" : "权重缺失"; qs("#modelStatusDetail").textContent = system.weights_available ? "模型文件已就绪" : "请检查 YOLO_WEIGHTS"; qs("#modelStatusValue").className = "text-value"; const robotPayload = data.robots || { robots: [], summary: {} }; const robotSummary = robotPayload.summary || {}; qs("#onlineRobotCount").textContent = String(robotSummary.configured || 0); qs("#robotSummary").textContent = `${robotSummary.configured || 0} / ${robotSummary.total || 2} 已配置 · 成功投递 ${robotSummary.delivered || 0} 次`; qs("#overviewMessage").textContent = summary.pending ? `当前有 ${summary.pending} 条告警事件等待处置,请尽快进入告警中心确认。` : "当前无待处置事件,模型与视频巡检服务保持监测状态。"; qs("#readyWeights").textContent = system.weights_available ? "已就绪" : "缺失"; qs("#readyWeights").className = system.weights_available ? "ready" : "not-ready"; setChannelReady(qs("#readyWechat"), channels.wechat); setChannelReady(qs("#readyFeishu"), channels.feishu); qs("#riskPendingValue").textContent = String(summary.pending); qs("#riskChannelValue").textContent = `${channelNames.length} / 2`; qs("#riskConfidenceValue").textContent = `${Math.round(detection.confidence * 100)}%`; qs("#confirmFramesValue").textContent = `${detection.confirm_frames} 帧`; qs("#cooldownValue").textContent = `${detection.cooldown_seconds} 秒`; qs("#settingWeights").textContent = system.weights; qs("#settingImageSize").textContent = `${detection.image_size} px`; qs("#settingConfidence").textContent = detection.confidence.toFixed(2); qs("#settingIou").textContent = detection.iou.toFixed(2); qs("#settingConfirmFrames").textContent = `${detection.confirm_frames} 帧`; qs("#settingCooldown").textContent = `${detection.cooldown_seconds} 秒`; renderRecentEvents(data.recent_events || []); const visualEvents = store.eventData.length ? store.eventData : data.recent_events || []; renderTrend(visualEvents); updateCommandVisuals(visualEvents, summary, channels, robotSummary); renderRobots(robotPayload, detection); } function updateCommandVisuals(events, summary, channels, robotSummary = {}) { const totalRobots = robotSummary.total || 2; const configuredRobots = robotSummary.configured || 0; const total = summary.total || 0; const closureRate = total ? Math.round(((summary.resolved || 0) / total) * 100) : 100; const notificationRate = Math.round((configuredRobots / totalRobots) * 100); const safetyScore = Math.max(42, Math.min(98, Math.round(notificationRate * 0.35 + closureRate * 0.35 + Math.max(0, 100 - (summary.pending || 0) * 8) * 0.3))); qs("#deviceOnlineRate").textContent = `${notificationRate}%`; qs("#eventClosureRate").textContent = `${closureRate}%`; qs("#notificationCoverage").textContent = `${notificationRate}%`; qs("#safetyScore").textContent = String(safetyScore); qs("#safetyGauge").style.setProperty("--score", safetyScore); qs("#overviewWechatState").textContent = channels.wechat ? "已配置" : "未配置"; qs("#overviewFeishuState").textContent = channels.feishu ? "已配置" : "未配置"; qs("#overviewPolicyText").textContent = `${store.dashboardData?.detection?.confirm_frames || 3} 帧确认`; qs("#overviewDeliveryCount").textContent = `累计成功 ${robotSummary.delivered || 0} 次`; let fireEvents = 0; let smokeEvents = 0; events.forEach((event) => { const classes = event.classes || []; if (classes.includes("fire")) fireEvents += 1; if (classes.includes("smoke")) smokeEvents += 1; }); const detectedTotal = fireEvents + smokeEvents; const fireArc = detectedTotal ? Math.round((fireEvents / detectedTotal) * 100) : 0; const smokeArc = detectedTotal ? 100 - fireArc : 0; qs("#riskTotal").textContent = String(detectedTotal); qs("#fireRiskRatio").textContent = `${detectedTotal ? Math.round((fireEvents / detectedTotal) * 100) : 0}%`; qs("#smokeRiskRatio").textContent = `${detectedTotal ? Math.round((smokeEvents / detectedTotal) * 100) : 0}%`; qs("#riskDonut").style.background = `conic-gradient(var(--red) 0 ${fireArc}%, var(--cyan) ${fireArc}% ${fireArc + smokeArc}%, var(--line) ${fireArc + smokeArc}% 100%)`; } function renderRecentEvents(events) { if (!events.length) { qs("#recentEventList").innerHTML = "
暂无告警事件。开始视频巡检后,满足连续帧条件的告警会显示在这里。
"; return; } qs("#recentEventList").innerHTML = events.map((event) => { const classes = event.classes || []; const primary = classes.includes("fire") ? "fire" : "smoke"; const label = classes.map(classLabel).join("、"); return `