| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210 |
- <!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; }
- * { 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); flex-wrap:wrap; }
- .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(360px,440px) minmax(0,1fr); 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; min-width:0; }
- h2 { margin:0 0 10px; font-size:14px; }
- .status-grid { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:8px; margin-bottom:14px; }
- .status-card { border:1px solid var(--line); border-radius:5px; padding:8px 10px; }
- .status-card span { display:block; color:var(--muted); font-size:11px; }
- .status-card b { display:block; margin-top:2px; font-size:18px; }
- .t-NORMAL { color:#0f8a4d; } .t-WARNING { color:#f0a500; } .t-ALARM { color:#d0342c; }
- .t-EXPIRED { color:#b65c08; } .t-OFFLINE { color:#8a94a0; } .t-UNKNOWN { color:#64748b; }
- table { width:100%; border-collapse:collapse; font-size:12px; }
- th,td { text-align:left; padding:6px 7px; border-bottom:1px solid #edf1f5; }
- th { color:var(--muted); font-weight:600; }
- .toolbar { display:flex; flex-wrap:wrap; gap:10px; justify-content:space-between; align-items:center; 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; }
- .marker { 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,.3); }
- .marker.s-NORMAL { background:#0f8a4d; } .marker.s-WARNING { background:#f0a500; }
- .marker.s-ALARM { background:#d0342c; } .marker.s-EXPIRED { background:#b65c08; }
- .marker.s-OFFLINE { background:#8a94a0; } .marker.s-UNKNOWN { background:#64748b; }
- .marker.hot { outline:3px solid var(--blue); outline-offset:1px; }
- .label { position:absolute; font-size:11px; transform:translate(9px,-7px); background:rgba(255,255,255,.92); padding:1px 5px; border-radius:3px; border:1px solid var(--line); pointer-events:none; }
- .caption { padding:10px 18px; background:var(--panel); border-top:1px solid var(--line); color:var(--muted); }
- .panel { margin-top:14px; border:1px solid var(--line); border-radius:5px; padding:10px 12px; min-height:120px; }
- .panel h3 { margin:0 0 8px; font-size:13px; }
- .panel ul { margin:0; padding-left:18px; }
- .trend { display:flex; align-items:flex-end; gap:6px; height:130px; padding:10px 0; border-bottom:1px solid var(--line); }
- .bar { flex:1; display:flex; flex-direction:column; justify-content:flex-end; align-items:center; gap:4px; min-width:18px; }
- .bar i { display:block; width:100%; border-radius:3px 3px 0 0; }
- .bar.w i { background:#f0a500; } .bar.a i { background:#d0342c; }
- .bar small { font-size:10px; color:var(--muted); }
- .legend { display:flex; gap:14px; color:var(--muted); font-size:12px; }
- .legend i { display:inline-block; width:10px; height:10px; border-radius:2px; margin-right:4px; }
- @media (max-width:940px){ main{grid-template-columns:1fr;} .map{min-height:320px;} }
- </style>
- </head>
- <body>
- <header>
- <h1>运行监测 / 运行概况</h1>
- <div class="meta"><span>设备 <b id="devices">0</b></span><span>异常设备 <b id="abnormal">0</b></span><span>新鲜度 <b id="freshness">--</b></span><span>窗口 <b id="window">--</b></span></div>
- </header>
- <main>
- <aside>
- <h2>监测参数状态分布</h2>
- <div class="status-grid" id="statusGrid"></div>
- <h2>预警 / 报警趋势(每小时)</h2>
- <div class="legend"><span><i style="background:#f0a500"></i>预警</span><span><i style="background:#d0342c"></i>报警</span></div>
- <div class="trend" id="trend"></div>
- <h2>行业覆盖</h2>
- <table><thead><tr><th>行业</th><th>设备</th><th>异常</th></tr></thead>
- <tbody id="coverageRows"></tbody></table>
- <h2 style="margin-top:14px">异常设备(点击联动中央一张图 / 设备详情)</h2>
- <table><thead><tr><th>设备</th><th>类型</th><th>状态</th></tr></thead>
- <tbody id="abnormalRows"></tbody></table>
- <div class="panel" id="detail"><h3>详情</h3><p style="color:var(--muted)">点击异常设备查看详情;地图上对应标记会高亮。</p></div>
- </aside>
- <section>
- <div class="toolbar">
- <strong>中央一张图</strong>
- <span>
- <button id="toggle" type="button">D-GAS-3 报警/正常切换</button>
- <button id="refresh" type="button">手动刷新</button>
- </span>
- </div>
- <div class="map" id="map"></div>
- <div class="caption" id="caption">异常设备点位(越界坐标设备不出现在地图上)</div>
- </section>
- </main>
- <script>
- const STATUS = ["NORMAL", "WARNING", "ALARM", "EXPIRED", "OFFLINE", "UNKNOWN"];
- const TYPE_NAME = { GAS_SENSOR: "燃气浓度计", WATER_SUPPLY_SENSOR: "供水压力计", SEWAGE_ENV: "污水环境监测" };
- const INDUSTRY_NAME = { GAS: "燃气", WATER_SUPPLY: "供水", DRAINAGE: "排水" };
- const ABNORMAL = new Set(["WARNING", "ALARM", "EXPIRED", "OFFLINE"]);
- const fixtures = () => ({
- window: { start: "2026-09-02T00:00:00+08:00", end: "2026-09-02T02:00:00+08:00" },
- devices: [
- { code: "D-GAS-1", name: "燃气浓度计-001", industry: "GAS", type: "GAS_SENSOR", status: "NORMAL", region: "城区A", lon: 110.130, lat: 28.440, reasons: [] },
- { code: "D-GAS-2", name: "燃气浓度计-002", industry: "GAS", type: "GAS_SENSOR", status: "WARNING", region: "城区A", lon: 110.131, lat: 28.441, reasons: ["流量波动"] },
- { code: "D-GAS-3", name: "燃气浓度计-003", industry: "GAS", type: "GAS_SENSOR", status: "ALARM", region: "城区A", lon: 110.132, lat: 28.442, reasons: ["浓度超限"] },
- { code: "D-WS-1", name: "供水压力计-001", industry: "WATER_SUPPLY", type: "WATER_SUPPLY_SENSOR", status: "EXPIRED", region: "城区B", lon: 110.150, lat: 28.460, reasons: ["采样过期"] },
- { code: "D-WS-2", name: "供水压力计-002", industry: "WATER_SUPPLY", type: "WATER_SUPPLY_SENSOR", status: "OFFLINE", region: "城区B", lon: 110.151, lat: 28.461, reasons: ["离线"] },
- { code: "D-DR-1", name: "污水环境监测仪-001", industry: "DRAINAGE", type: "SEWAGE_ENV", status: "NORMAL", region: "城区C", lon: 110.170, lat: 28.480, reasons: [] },
- { code: "D-EXCL-GAS", name: "越界坐标燃气计", industry: "GAS", type: "GAS_SENSOR", status: "ALARM", region: "城区A", lon: 181, lat: 28.443, reasons: ["坐标越界"] }
- ],
- warnings: [
- { no: "W-GAS-1", time: "00:30", industry: "GAS" },
- { no: "W-GAS-2", time: "01:15", industry: "GAS" },
- { no: "W-WS-1", time: "01:45", industry: "WATER_SUPPLY" }
- ],
- alarms: [
- { no: "A-GAS-1", time: "00:40", industry: "GAS" },
- { no: "A-DR-1", time: "01:10", industry: "DRAINAGE" }
- ]
- });
- let data = fixtures();
- const buckets = [0, 1, 2].map(h => ({ hour: "0" + h + ":00", warning: 0, alarm: 0 }));
- function rebuildTrends() {
- buckets.forEach(b => (b.warning = 0, b.alarm = 0));
- data.warnings.forEach(w => buckets[+w.time.slice(0, 2)].warning++);
- data.alarms.forEach(a => buckets[+a.time.slice(0, 2)].alarm++);
- }
- function render() {
- const abnormal = data.devices.filter(d => ABNORMAL.has(d.status)).sort((a, b) => a.code.localeCompare(b.code));
- const counts = {}; STATUS.forEach(s => counts[s] = 0);
- const byIndustry = {};
- data.devices.forEach(d => {
- counts[d.status]++;
- const row = byIndustry[d.industry] = byIndustry[d.industry] || { device: 0, abnormal: 0 };
- row.device++;
- if (ABNORMAL.has(d.status)) row.abnormal++;
- });
- const renderableAbnormal = abnormal.filter(d => Math.abs(d.lon) <= 180 && Math.abs(d.lat) <= 90);
- document.getElementById("devices").textContent = data.devices.length;
- document.getElementById("abnormal").textContent = abnormal.length;
- document.getElementById("freshness").textContent = counts.EXPIRED ? "STALE" : (data.devices.length ? "CURRENT" : "EMPTY");
- document.getElementById("window").textContent = data.window.start + " ~ " + data.window.end;
- document.getElementById("statusGrid").innerHTML = STATUS.map(s =>
- `<div class="status-card"><span>${s}</span><b class="t-${s}">${counts[s]}</b></div>`).join("");
- const maxCount = Math.max(1, ...buckets.flatMap(b => [b.warning, b.alarm]));
- document.getElementById("trend").innerHTML = buckets.map(b => `
- <div class="bar w"><i style="height:${b.warning / maxCount * 88}px"></i></div>
- <div class="bar a"><i style="height:${b.alarm / maxCount * 88}px"></i></div>
- <small>${b.hour}</small>`).join("");
- document.getElementById("coverageRows").innerHTML = Object.keys(byIndustry).sort().map(k =>
- `<tr><td>${INDUSTRY_NAME[k]}</td><td>${byIndustry[k].device}</td><td>${byIndustry[k].abnormal}</td></tr>`).join("");
- document.getElementById("abnormalRows").innerHTML = abnormal.map((d, index) =>
- `<tr><td><a href="#" data-i="${index}">${d.code}</a></td><td>${TYPE_NAME[d.type]}</td><td class="t-${d.status}">${d.status}</td></tr>`).join("");
- const map = document.getElementById("map");
- map.innerHTML = "";
- const lons = renderableAbnormal.map(d => d.lon), lats = renderableAbnormal.map(d => d.lat);
- const minLon = Math.min(...lons), maxLon = Math.max(...lons), minLat = Math.min(...lats), maxLat = Math.max(...lats);
- const xs = d => 12 + (d.lon - minLon) / Math.max(0.001, maxLon - minLon) * 76;
- const ys = d => 12 + (maxLat - d.lat) / Math.max(0.001, maxLat - minLat) * 76;
- renderableAbnormal.forEach(d => {
- const el = document.createElement("div");
- el.className = "marker s-" + d.status;
- el.style.left = xs(d) + "%"; el.style.top = ys(d) + "%";
- el.title = d.code + " / " + d.status;
- map.appendChild(el);
- const l = document.createElement("div");
- l.className = "label"; l.style.left = xs(d) + "%"; l.style.top = ys(d) + "%";
- l.textContent = d.code;
- map.appendChild(l);
- });
- document.getElementById("caption").textContent =
- `异常 ${abnormal.length} 台 · 地图可定位 ${renderableAbnormal.length} 台 · 越界坐标 ${abnormal.length - renderableAbnormal.length} 台`;
- document.querySelectorAll("#abnormalRows a").forEach(a =>
- a.addEventListener("click", ev => { ev.preventDefault(); select(abnormal[+a.dataset.i], map); }));
- }
- function select(device, map) {
- const box = document.getElementById("detail");
- box.innerHTML = `<h3>异常设备 ${device.code}</h3><ul>
- <li>名称:${device.name} 区域:${device.region} 行业:${INDUSTRY_NAME[device.industry]}</li>
- <li>类型:${TYPE_NAME[device.type]} 状态:${device.status}</li>
- <li>原因:${device.reasons.join("、") || "无"}</li>
- </ul>`;
- map.querySelectorAll(".marker").forEach((m, index) => {
- const hot = m.title.startsWith(device.code + " ");
- m.classList.toggle("hot", hot);
- if (hot) {
- m.scrollIntoView({ block: "nearest", inline: "nearest" });
- }
- });
- }
- document.getElementById("toggle").addEventListener("click", () => {
- const d = data.devices.find(x => x.code === "D-GAS-3");
- d.status = d.status === "ALARM" ? "NORMAL" : "ALARM";
- render();
- });
- document.getElementById("refresh").addEventListener("click", () => {
- document.getElementById("window").textContent =
- new Date().toLocaleString("zh-CN").slice(0, 19) + "+08:00";
- render();
- });
- rebuildTrends();
- render();
- </script>
- </body>
- </html>
|