monitoring-overview-demo.html 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. <!doctype html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="utf-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1">
  6. <title>运行监测 / 监测概览</title>
  7. <style>
  8. :root { --ink:#16324f; --muted:#64748b; --line:#d9e1ea; --panel:#fff; --page:#edf2f7; --blue:#1470ef; --ok:#077a52; --warn:#b65c08; --bad:#b3261e; }
  9. * { box-sizing:border-box; }
  10. body { margin:0; min-height:100vh; background:var(--page); color:var(--ink); font-family:"Microsoft YaHei",Arial,sans-serif; font-size:13px; }
  11. header { display:flex; flex-wrap:wrap; gap:12px; justify-content:space-between; align-items:center; padding:14px 20px; background:var(--panel); border-bottom:1px solid var(--line); }
  12. h1 { margin:0; font-size:18px; }
  13. .meta { display:flex; gap:14px; color:var(--muted); }
  14. .meta b { color:var(--ink); font-weight:600; }
  15. button { cursor:pointer; border:1px solid #b9c8d5; background:var(--panel); padding:7px 12px; border-radius:4px; }
  16. button:hover { border-color:var(--blue); color:var(--blue); }
  17. main { display:grid; grid-template-columns:minmax(340px,420px) minmax(0,1fr); gap:0; min-height:calc(100vh - 57px); }
  18. aside { padding:16px 18px; background:var(--panel); border-right:1px solid var(--line); overflow:auto; }
  19. section { display:grid; grid-template-rows:auto minmax(0,1fr) auto; }
  20. h2 { margin:0 0 10px; font-size:14px; }
  21. .kpis { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:8px; margin-bottom:14px; }
  22. .kpi { border:1px solid var(--line); border-radius:5px; padding:9px 10px; }
  23. .kpi span { display:block; color:var(--muted); font-size:12px; }
  24. .kpi b { display:block; margin-top:3px; font-size:19px; }
  25. .rate { height:12px; border-radius:4px; background:#e5eaf0; overflow:hidden; margin:8px 0; }
  26. .rate i { display:block; height:100%; background:var(--ok); }
  27. table { width:100%; border-collapse:collapse; }
  28. th,td { text-align:left; padding:7px 8px; border-bottom:1px solid #edf1f5; }
  29. th { color:var(--muted); font-size:12px; font-weight:600; }
  30. .badge { display:inline-block; padding:1px 7px; border-radius:9px; border:1px solid currentColor; font-size:11px; }
  31. .b-ONLINE,.b-CURRENT { color:var(--ok); }
  32. .b-OFFLINE { color:var(--bad); }
  33. .b-STALE,.b-STALE1 { color:var(--warn); }
  34. .b-UNKNOWN,.b-EMPTY { color:var(--muted); }
  35. .toolbar { display:flex; justify-content:space-between; gap:10px; padding:10px 18px; background:var(--panel); border-bottom:1px solid var(--line); }
  36. .map { position:relative; background:#e2e9f0; background-image:linear-gradient(#d3dce5 1px,transparent 1px),linear-gradient(90deg,#d3dce5 1px,transparent 1px); background-size:56px 56px; }
  37. .point { position:absolute; width:14px; height:14px; margin:-7px 0 0 -7px; border-radius:50%; background:#9aa9bb; border:2px solid #fff; box-shadow:0 1px 3px rgba(16,24,40,.25); }
  38. .point.online { background:var(--ok); }
  39. .point.offline { background:var(--bad); }
  40. .point.stale { background:var(--warn); }
  41. .point.unknown { background:#7f8fa0; }
  42. .label { position:absolute; font-size:11px; transform:translateX(10px); background:rgba(255,255,255,.9); padding:1px 5px; border-radius:3px; border:1px solid var(--line); }
  43. .caption { padding:10px 18px; background:var(--panel); border-top:1px solid var(--line); color:var(--muted); }
  44. @media (max-width:940px){ main{grid-template-columns:1fr;} .map{min-height:360px;} }
  45. </style>
  46. </head>
  47. <body>
  48. <header>
  49. <h1>运行监测 / 监测概览</h1>
  50. <div class="meta"><span>监测点 <b id="points">0</b></span><span>设备 <b id="devices">0</b></span><span>在线率 <b id="rate">0%</b></span><span>新鲜度 <b id="freshness">--</b></span></div>
  51. </header>
  52. <main>
  53. <aside>
  54. <h2>KPI</h2>
  55. <div class="kpis">
  56. <div class="kpi"><span>在线</span><b id="online">0</b></div>
  57. <div class="kpi"><span>离线</span><b id="offline">0</b></div>
  58. <div class="kpi"><span>过期</span><b id="stale">0</b></div>
  59. <div class="kpi"><span>未知/缺失</span><b id="unknown">0</b></div>
  60. <div class="kpi"><span>口径分母</span><b id="denom">0</b></div>
  61. <div class="kpi"><span>更新</span><b id="updated">--</b></div>
  62. </div>
  63. <h2>类型分布</h2>
  64. <table id="types"><thead><tr><th>类型</th><th>设备</th><th>在线</th><th>离线</th><th>过期</th></tr></thead><tbody></tbody></table>
  65. <h2 style="margin-top:14px">模拟状态切换</h2>
  66. <button id="toggle" type="button">GAS-002 在线/离线切换</button>
  67. <button id="mark-stale" type="button" style="margin-top:6px">WS-001 标记过期</button>
  68. </aside>
  69. <section>
  70. <div class="toolbar"><strong>中央一张图</strong><span class="meta">切换状态会同步地图点位颜色</span></div>
  71. <div class="map" id="map"></div>
  72. <div class="caption" id="caption">全部监测点</div>
  73. </section>
  74. </main>
  75. <script>
  76. const initial = [
  77. { code:"GAS-001", type:"GAS_SENSOR", state:"ONLINE", x:22, y:30 },
  78. { code:"GAS-002", type:"GAS_SENSOR", state:"OFFLINE", x:34, y:46 },
  79. { code:"WS-001", type:"WATER_SUPPLY_SENSOR", state:"STALE", x:56, y:30 },
  80. { code:"WS-002", type:"WATER_SUPPLY_SENSOR", state:"UNKNOWN", x:64, y:52 },
  81. { code:"DRAIN-001", type:"SEWAGE_ENV", state:"ONLINE", x:78, y:36 }
  82. ];
  83. let devices = initial.map(d => Object.assign({}, d));
  84. function render() {
  85. const counts = {ONLINE:0,OFFLINE:0,STALE:0,UNKNOWN:0};
  86. const types = {};
  87. devices.forEach(d => { counts[d.state]++; (types[d.type] = types[d.type] || {online:0,offline:0,stale:0,n:0})[d.state.toLowerCase()]++; types[d.type].n++; });
  88. const denom = counts.ONLINE + counts.OFFLINE + counts.STALE;
  89. const rate = denom ? (counts.ONLINE / denom * 100).toFixed(1) : "0.0";
  90. document.getElementById("points").textContent = new Set(devices.map(d => d.point || d.code)).size;
  91. document.getElementById("devices").textContent = devices.length;
  92. document.getElementById("rate").textContent = rate + "%";
  93. document.getElementById("freshness").textContent = counts.STALE ? "STALE" : counts.UNKNOWN && !denom ? "EMPTY" : "CURRENT";
  94. document.getElementById("online").textContent = counts.ONLINE;
  95. document.getElementById("offline").textContent = counts.OFFLINE;
  96. document.getElementById("stale").textContent = counts.STALE;
  97. document.getElementById("unknown").textContent = counts.UNKNOWN;
  98. document.getElementById("denom").textContent = denom;
  99. document.getElementById("updated").textContent = new Date().toLocaleTimeString("zh-CN");
  100. document.getElementById("types").querySelector("tbody").innerHTML = Object.entries(types).map(([code,t]) =>
  101. `<tr><td>${code}</td><td>${t.n}</td><td>${t.online||0}</td><td>${t.offline||0}</td><td>${t.stale||0}</td></tr>`).join("");
  102. const map = document.getElementById("map");
  103. map.innerHTML = "";
  104. devices.forEach(d => {
  105. const p = document.createElement("div");
  106. p.className = "point " + d.state.toLowerCase();
  107. p.style.left = d.x + "%"; p.style.top = d.y + "%"; p.title = d.code + " / " + d.state;
  108. map.appendChild(p);
  109. const l = document.createElement("div");
  110. l.className = "label"; l.style.left = d.x + "%"; l.style.top = d.y + "%"; l.textContent = d.code;
  111. map.appendChild(l);
  112. });
  113. document.getElementById("caption").textContent =
  114. `在线 ${counts.ONLINE} · 离线 ${counts.OFFLINE} · 过期 ${counts.STALE} · 未知 ${counts.UNKNOWN} · 在线率 ${rate}%`;
  115. }
  116. document.getElementById("toggle").addEventListener("click", () => {
  117. const d = devices.find(x => x.code === "GAS-002");
  118. d.state = d.state === "ONLINE" ? "OFFLINE" : "ONLINE";
  119. render();
  120. });
  121. document.getElementById("mark-stale").addEventListener("click", () => {
  122. devices.find(x => x.code === "WS-001").state = "STALE";
  123. render();
  124. });
  125. render();
  126. </script>
  127. </body>
  128. </html>