| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131 |
- <!doctype html>
- <html lang="zh-CN">
- <head>
- <meta charset="utf-8">
- <meta name="viewport" content="width=device-width, initial-scale=1">
- <title>运行监测 / 监测概览</title>
- <style>
- :root { --ink:#16324f; --muted:#64748b; --line:#d9e1ea; --panel:#fff; --page:#edf2f7; --blue:#1470ef; --ok:#077a52; --warn:#b65c08; --bad:#b3261e; }
- * { box-sizing:border-box; }
- body { margin:0; min-height:100vh; background:var(--page); color:var(--ink); font-family:"Microsoft YaHei",Arial,sans-serif; font-size:13px; }
- 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); }
- h1 { margin:0; font-size:18px; }
- .meta { display:flex; gap:14px; color:var(--muted); }
- .meta b { color:var(--ink); font-weight:600; }
- button { cursor:pointer; border:1px solid #b9c8d5; background:var(--panel); padding:7px 12px; border-radius:4px; }
- button:hover { border-color:var(--blue); color:var(--blue); }
- main { display:grid; grid-template-columns:minmax(340px,420px) minmax(0,1fr); gap:0; min-height:calc(100vh - 57px); }
- aside { padding:16px 18px; background:var(--panel); border-right:1px solid var(--line); overflow:auto; }
- section { display:grid; grid-template-rows:auto minmax(0,1fr) auto; }
- h2 { margin:0 0 10px; font-size:14px; }
- .kpis { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:8px; margin-bottom:14px; }
- .kpi { border:1px solid var(--line); border-radius:5px; padding:9px 10px; }
- .kpi span { display:block; color:var(--muted); font-size:12px; }
- .kpi b { display:block; margin-top:3px; font-size:19px; }
- .rate { height:12px; border-radius:4px; background:#e5eaf0; overflow:hidden; margin:8px 0; }
- .rate i { display:block; height:100%; background:var(--ok); }
- table { width:100%; border-collapse:collapse; }
- th,td { text-align:left; padding:7px 8px; border-bottom:1px solid #edf1f5; }
- th { color:var(--muted); font-size:12px; font-weight:600; }
- .badge { display:inline-block; padding:1px 7px; border-radius:9px; border:1px solid currentColor; font-size:11px; }
- .b-ONLINE,.b-CURRENT { color:var(--ok); }
- .b-OFFLINE { color:var(--bad); }
- .b-STALE,.b-STALE1 { color:var(--warn); }
- .b-UNKNOWN,.b-EMPTY { color:var(--muted); }
- .toolbar { display:flex; justify-content:space-between; gap:10px; padding:10px 18px; background:var(--panel); border-bottom:1px solid var(--line); }
- .map { position:relative; background:#e2e9f0; background-image:linear-gradient(#d3dce5 1px,transparent 1px),linear-gradient(90deg,#d3dce5 1px,transparent 1px); background-size:56px 56px; }
- .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); }
- .point.online { background:var(--ok); }
- .point.offline { background:var(--bad); }
- .point.stale { background:var(--warn); }
- .point.unknown { background:#7f8fa0; }
- .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); }
- .caption { padding:10px 18px; background:var(--panel); border-top:1px solid var(--line); color:var(--muted); }
- @media (max-width:940px){ main{grid-template-columns:1fr;} .map{min-height:360px;} }
- </style>
- </head>
- <body>
- <header>
- <h1>运行监测 / 监测概览</h1>
- <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>
- </header>
- <main>
- <aside>
- <h2>KPI</h2>
- <div class="kpis">
- <div class="kpi"><span>在线</span><b id="online">0</b></div>
- <div class="kpi"><span>离线</span><b id="offline">0</b></div>
- <div class="kpi"><span>过期</span><b id="stale">0</b></div>
- <div class="kpi"><span>未知/缺失</span><b id="unknown">0</b></div>
- <div class="kpi"><span>口径分母</span><b id="denom">0</b></div>
- <div class="kpi"><span>更新</span><b id="updated">--</b></div>
- </div>
- <h2>类型分布</h2>
- <table id="types"><thead><tr><th>类型</th><th>设备</th><th>在线</th><th>离线</th><th>过期</th></tr></thead><tbody></tbody></table>
- <h2 style="margin-top:14px">模拟状态切换</h2>
- <button id="toggle" type="button">GAS-002 在线/离线切换</button>
- <button id="mark-stale" type="button" style="margin-top:6px">WS-001 标记过期</button>
- </aside>
- <section>
- <div class="toolbar"><strong>中央一张图</strong><span class="meta">切换状态会同步地图点位颜色</span></div>
- <div class="map" id="map"></div>
- <div class="caption" id="caption">全部监测点</div>
- </section>
- </main>
- <script>
- const initial = [
- { code:"GAS-001", type:"GAS_SENSOR", state:"ONLINE", x:22, y:30 },
- { code:"GAS-002", type:"GAS_SENSOR", state:"OFFLINE", x:34, y:46 },
- { code:"WS-001", type:"WATER_SUPPLY_SENSOR", state:"STALE", x:56, y:30 },
- { code:"WS-002", type:"WATER_SUPPLY_SENSOR", state:"UNKNOWN", x:64, y:52 },
- { code:"DRAIN-001", type:"SEWAGE_ENV", state:"ONLINE", x:78, y:36 }
- ];
- let devices = initial.map(d => Object.assign({}, d));
- function render() {
- const counts = {ONLINE:0,OFFLINE:0,STALE:0,UNKNOWN:0};
- const types = {};
- 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++; });
- const denom = counts.ONLINE + counts.OFFLINE + counts.STALE;
- const rate = denom ? (counts.ONLINE / denom * 100).toFixed(1) : "0.0";
- document.getElementById("points").textContent = new Set(devices.map(d => d.point || d.code)).size;
- document.getElementById("devices").textContent = devices.length;
- document.getElementById("rate").textContent = rate + "%";
- document.getElementById("freshness").textContent = counts.STALE ? "STALE" : counts.UNKNOWN && !denom ? "EMPTY" : "CURRENT";
- document.getElementById("online").textContent = counts.ONLINE;
- document.getElementById("offline").textContent = counts.OFFLINE;
- document.getElementById("stale").textContent = counts.STALE;
- document.getElementById("unknown").textContent = counts.UNKNOWN;
- document.getElementById("denom").textContent = denom;
- document.getElementById("updated").textContent = new Date().toLocaleTimeString("zh-CN");
- document.getElementById("types").querySelector("tbody").innerHTML = Object.entries(types).map(([code,t]) =>
- `<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("");
- const map = document.getElementById("map");
- map.innerHTML = "";
- devices.forEach(d => {
- const p = document.createElement("div");
- p.className = "point " + d.state.toLowerCase();
- p.style.left = d.x + "%"; p.style.top = d.y + "%"; p.title = d.code + " / " + d.state;
- map.appendChild(p);
- const l = document.createElement("div");
- l.className = "label"; l.style.left = d.x + "%"; l.style.top = d.y + "%"; l.textContent = d.code;
- map.appendChild(l);
- });
- document.getElementById("caption").textContent =
- `在线 ${counts.ONLINE} · 离线 ${counts.OFFLINE} · 过期 ${counts.STALE} · 未知 ${counts.UNKNOWN} · 在线率 ${rate}%`;
- }
- document.getElementById("toggle").addEventListener("click", () => {
- const d = devices.find(x => x.code === "GAS-002");
- d.state = d.state === "ONLINE" ? "OFFLINE" : "ONLINE";
- render();
- });
- document.getElementById("mark-stale").addEventListener("click", () => {
- devices.find(x => x.code === "WS-001").state = "STALE";
- render();
- });
- render();
- </script>
- </body>
- </html>
|