| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226 |
- <!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; }
- .tabs { display:inline-flex; border:1px solid var(--line); border-radius:5px; overflow:hidden; }
- .tabs button { border:0; border-right:1px solid var(--line); padding:8px 16px; background:#f4f7fa; }
- .tabs button.on { background:var(--blue); color:#fff; }
- .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; }
- main { display:grid; grid-template-columns:minmax(380px,470px) 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 8px; font-size:14px; }
- .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); }
- .dim { margin-bottom:14px; }
- .dim h3 { margin:0 0 6px; font-size:13px; font-weight:600; }
- .dim h3 small { color:var(--muted); font-weight:400; margin-left:6px; }
- .buckets { display:grid; gap:4px; }
- .bucket { display:grid; grid-template-columns:minmax(90px,150px) 1fr 54px 54px; gap:8px; align-items:center; cursor:pointer; padding:2px 4px; border-radius:3px; }
- .bucket:hover { background:#f4f7fa; }
- .bucket.on { background:#e8f0fe; font-weight:600; }
- .bucket .bar { height:11px; background:#e6ecf2; border-radius:3px; overflow:hidden; }
- .bucket .bar i { display:block; height:100%; background:var(--blue); }
- .bucket.on .bar i { background:#0f8a4d; }
- .bucket .num { text-align:right; color:var(--muted); font-variant-numeric:tabular-nums; }
- .map { position:relative; background:#e2e9f0; background-image:linear-gradient(#d3dce5 1px,transparent 1px),linear-gradient(90deg,#d3dce5 1px,transparent 1px); background-size:56px 56px; min-height:200px; }
- .marker { position:absolute; width:14px; height:14px; margin:-7px 0 0 -7px; border-radius:50%; background:#d0342c; border:2px solid #fff; }
- .label { position:absolute; font-size:11px; transform:translate(9px,-6px); 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); }
- .empty { color:var(--muted); padding:12px; border:1px dashed var(--line); border-radius:5px; }
- @media (max-width:980px){ main{grid-template-columns:1fr;} }
- </style>
- </head>
- <body>
- <header>
- <h1>预警处置 / 预警分析</h1>
- <div class="tabs">
- <button id="tabWARNING" class="on" type="button">预警</button>
- <button id="tabALARM" type="button">报警</button>
- </div>
- <div class="meta"><span>事件族 <b id="familyName">预警</b></span><span>命中 <b id="totalCount">0</b></span><span>筛选 <b id="filterText">无</b></span><span>更新 <b id="updated">--</b></span></div>
- </header>
- <main>
- <aside>
- <div class="toolbar" style="padding:0 0 10px; border-bottom:none">
- <button id="clearFilter" type="button">清除全部筛选</button>
- <button id="refresh" type="button">手动刷新</button>
- </div>
- <div id="dimensions"></div>
- <div class="empty" id="empty" style="display:none">当前筛选下没有事件。</div>
- </aside>
- <section>
- <div class="toolbar">
- <strong>中央一张图(当前筛选事件点位)</strong>
- <span id="mapHint" style="color:var(--muted)">点击左侧任一桶即可交叉筛选</span>
- </div>
- <div class="map" id="map"></div>
- <div class="caption" id="caption">点击桶后所有图表与地图同步更新</div>
- </section>
- </main>
- <script>
- const LEVEL_LABEL = { 1: "RED", 2: "YELLOW", 3: "BLUE", 4: "GREEN" };
- const INDUSTRY_LABEL = { GAS: "燃气", WATER_SUPPLY: "供水", DRAINAGE: "排水" };
- const DEVICE_TYPE_LABEL = { GAS_SENSOR: "燃气浓度计", WATER_SUPPLY_SENSOR: "供水压力计", SEWAGE_ENV: "污水环境监测" };
- // Deterministic fixture: type, cause (null = 缺失成因), level, region (null = 未分区), deviceType, day/hour.
- const fixture = {
- WARNING: [
- { no: "MW-001", type: "GAS_CONCENTRATION", cause: "浓度超限", level: 1, region: "城区A", deviceType: "GAS_SENSOR", day: "2026-09-01", hour: 9 },
- { no: "MW-002", type: "GAS_CONCENTRATION", cause: "浓度超限", level: 1, region: "城区A", deviceType: "GAS_SENSOR", day: "2026-09-01", hour: 10 },
- { no: "MW-003", type: "WATER_PRESSURE", cause: null, level: 2, region: "城区B", deviceType: "WATER_SUPPLY_SENSOR", day: "2026-09-01", hour: 11 },
- { no: "MW-004", type: "WATER_PRESSURE", cause: "压力波动", level: 2, region: "城区B", deviceType: "WATER_SUPPLY_SENSOR", day: "2026-09-02", hour: 9 },
- { no: "MW-005", type: "SEWAGE_CONCENTRATION", cause: "浓度超限", level: 2, region: "城区C", deviceType: "SEWAGE_ENV", day: "2026-09-02", hour: 10 },
- { no: "MW-006", type: "SEWAGE_CONCENTRATION", cause: "设备故障", level: 3, region: "城区C", deviceType: "SEWAGE_ENV", day: "2026-09-02", hour: 11 },
- { no: "MW-007", type: "WATER_PRESSURE", cause: null, level: 4, region: null, deviceType: "WATER_SUPPLY_SENSOR", day: "2026-09-02", hour: 12 },
- { no: "MW-008", type: "GAS_CONCENTRATION", cause: "设备故障", level: 9, region: "城区A", deviceType: "GAS_SENSOR", day: "2026-09-02", hour: 13 }
- ],
- ALARM: [
- { no: "MA-001", type: "GAS_CONCENTRATION", cause: "浓度超限", level: 1, region: "城区A", deviceType: "GAS_SENSOR", day: "2026-09-02", hour: 14 },
- { no: "MA-002", type: "WATER_PRESSURE", cause: null, level: 2, region: "城区B", deviceType: "WATER_SUPPLY_SENSOR", day: "2026-09-02", hour: 15 }
- ]
- };
- const START = "2026-09-01T00:00:00+08:00";
- const END = "2026-09-02T23:59:00+08:00";
- let family = "WARNING";
- const filters = { industry: null, level: null, eventType: null, cause: null, deviceType: null, region: null };
- const industryOf = e => e.deviceType === "GAS_SENSOR" ? "GAS" : e.deviceType === "WATER_SUPPLY_SENSOR" ? "WATER_SUPPLY" : "DRAINAGE";
- const levelKeyOf = e => (e.level >= 1 && e.level <= 4) ? String(e.level) : "UNKNOWN";
- const causeKeyOf = e => e.cause || "UNKNOWN_CAUSE";
- const regionKeyOf = e => e.region || "UNASSIGNED_REGION";
- function scoped() {
- return fixture[family].filter(e =>
- (!filters.industry || industryOf(e) === filters.industry) &&
- (!filters.level || levelKeyOf(e) === String(filters.level)) &&
- (!filters.eventType || e.type === filters.eventType) &&
- (!filters.cause || causeKeyOf(e) === filters.cause) &&
- (!filters.deviceType || e.deviceType === filters.deviceType) &&
- (!filters.region || regionKeyOf(e) === filters.region));
- }
- function group(list, keyFn, labelFn) {
- const counts = new Map();
- list.forEach(e => {
- const key = keyFn(e);
- const row = counts.get(key) || { count: 0, label: labelFn(e, key) };
- row.count++;
- counts.set(key, row);
- });
- return counts;
- }
- function renderBuckets(container, title, hint, list, order, keyFn, labelFn, onPick, activeKey) {
- const counts = group(list, keyFn, labelFn);
- const keys = order.filter(k => counts.has(k) || !counts.size).concat(
- [...counts.keys()].filter(k => !order.includes(k)));
- const max = Math.max(1, ...[...counts.values()].map(v => v.count));
- const total = list.length;
- const html = keys.map(k => {
- const row = counts.get(k) || { count: 0, label: labelFn(null, k) };
- const pct = total ? (row.count / total * 100).toFixed(2) : "0.00";
- return `<div class="bucket ${activeKey === k ? "on" : ""}" data-key="${k}">
- <span>${row.label}</span>
- <span class="bar"><i style="width:${row.count / max * 100}%"></i></span>
- <span class="num">${row.count}</span>
- <span class="num">${pct}%</span>
- </div>`;
- }).join("");
- container.insertAdjacentHTML("beforeend",
- `<div class="dim"><h3>${title}<small>${hint}</small></h3><div class="buckets">${html}</div></div>`);
- container.querySelectorAll(".dim:last-child .bucket").forEach(el =>
- el.addEventListener("click", () => onPick(el.dataset.key)));
- }
- function render() {
- const list = scoped();
- const box = document.getElementById("dimensions");
- box.innerHTML = "";
- document.getElementById("empty").style.display = list.length ? "none" : "block";
- document.getElementById("familyName").textContent = family === "WARNING" ? "预警" : "报警";
- document.getElementById("totalCount").textContent = list.length;
- document.getElementById("updated").textContent = new Date().toLocaleTimeString("zh-CN");
- document.getElementById("filterText").textContent =
- Object.entries(filters).filter(([, v]) => v).map(([k, v]) => k + "=" + v).join(", ") || "无";
- renderBuckets(box, "事件类型", "按 warningType 聚合", list, [],
- e => e.type, (e, k) => (e ? e.type : k),
- key => { filters.eventType = filters.eventType === key ? null : key; render(); }, filters.eventType);
- renderBuckets(box, "成因", "缺失成因保留为 未知/缺失成因", list, ["UNKNOWN_CAUSE"],
- e => causeKeyOf(e), (e, k) => (k === "UNKNOWN_CAUSE" ? "未知/缺失成因" : (e ? e.cause : k)),
- key => { filters.cause = filters.cause === key ? null : key; render(); }, filters.cause);
- renderBuckets(box, "行业", "仅正式行业", list, ["GAS", "WATER_SUPPLY", "DRAINAGE"],
- e => industryOf(e), (e, k) => INDUSTRY_LABEL[k] || k,
- key => { filters.industry = filters.industry === key ? null : key; render(); }, filters.industry);
- renderBuckets(box, "设备类型", "智能设备类型", list, Object.keys(DEVICE_TYPE_LABEL),
- e => e.deviceType, (e, k) => DEVICE_TYPE_LABEL[k] || k,
- key => { filters.deviceType = filters.deviceType === key ? null : key; render(); }, filters.deviceType);
- renderBuckets(box, "等级", "1红 2黄 3蓝 4绿", list, ["1", "2", "3", "4", "UNKNOWN"],
- e => levelKeyOf(e), (e, k) => LEVEL_LABEL[k] || "UNKNOWN",
- key => { filters.level = String(filters.level) === key ? null : Number(key); render(); },
- filters.level === null ? null : String(filters.level));
- renderBuckets(box, "区域", "缺失区域保留为 未分区", list, ["UNASSIGNED_REGION"],
- e => regionKeyOf(e), (e, k) => (k === "UNASSIGNED_REGION" ? "未分区" : (e ? e.region : k)),
- key => { filters.region = filters.region === key ? null : key; render(); }, filters.region);
- renderBuckets(box, "时间", "Asia/Shanghai 日桶", list, ["2026-09-01", "2026-09-02"],
- e => e.day, (e, k) => k,
- () => {}, null);
- const map = document.getElementById("map");
- map.innerHTML = "";
- if (list.length) {
- const lons = list.map((e, i) => 110.13 + i * 0.006), lats = list.map((e, i) => 28.44 + (i % 4) * 0.009);
- const minLon = Math.min(...lons), maxLon = Math.max(...lons), minLat = Math.min(...lats), maxLat = Math.max(...lats);
- list.forEach((e, i) => {
- const x = 12 + (lons[i] - minLon) / Math.max(0.001, maxLon - minLon) * 76;
- const y = 12 + (maxLat - lats[i]) / Math.max(0.001, maxLat - minLat) * 76;
- const el = document.createElement("div");
- el.className = "marker";
- el.style.left = x + "%"; el.style.top = y + "%";
- el.title = e.no;
- map.appendChild(el);
- const l = document.createElement("div");
- l.className = "label"; l.style.left = x + "%"; l.style.top = y + "%";
- l.textContent = e.no;
- map.appendChild(l);
- });
- }
- document.getElementById("caption").textContent =
- `${list.length} 个事件点位 · 窗口 ${START} ~ ${END}`;
- }
- document.getElementById("tabWARNING").addEventListener("click", () => setFamily("WARNING"));
- document.getElementById("tabALARM").addEventListener("click", () => setFamily("ALARM"));
- function setFamily(next) {
- family = next;
- Object.keys(filters).forEach(k => filters[k] = null);
- document.getElementById("tabWARNING").classList.toggle("on", family === "WARNING");
- document.getElementById("tabALARM").classList.toggle("on", family === "ALARM");
- render();
- }
- document.getElementById("clearFilter").addEventListener("click", () => {
- Object.keys(filters).forEach(k => filters[k] = null);
- render();
- });
- document.getElementById("refresh").addEventListener("click", render);
- render();
- </script>
- </body>
- </html>
|