| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243 |
- <!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; }
- button:hover { border-color:var(--blue); color:var(--blue); }
- select { border:1px solid #b9c8d5; border-radius:4px; padding:6px 8px; background:#fff; }
- 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; }
- .filters { display:grid; grid-template-columns:1fr 1fr; gap:8px; margin-bottom:12px; }
- .filters label { display:flex; flex-direction:column; gap:4px; color:var(--muted); font-size:12px; }
- 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; }
- .badge { display:inline-block; padding:1px 8px; border-radius:9px; border:1px solid currentColor; font-size:11px; }
- .b-RED { color:#d0342c; background:#fdecea; } .b-YELLOW { color:#9a6b00; background:#fdf3d7; }
- .b-BLUE { color:#1470ef; background:#e8f0fe; } .b-GREEN { color:#0f8a4d; background:#e5f5ec; }
- .b-UNKNOWN { color:#64748b; background:#eef1f4; }
- .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:15px; height:15px; margin:-7px 0 0 -7px; border-radius:50%; background:#9aa9bb; border:2px solid #fff; box-shadow:0 1px 3px rgba(16,24,40,.3); cursor:pointer; }
- .marker.c-RED { background:#d0342c; } .marker.c-YELLOW { background:#f0a500; }
- .marker.c-BLUE { background:#1470ef; } .marker.c-GREEN { background:#0f8a4d; }
- .marker.c-UNKNOWN { background:#8a94a0; }
- .marker.hot { outline:3px solid var(--ink); 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; }
- .panel.failed { border-color:#d0342c; background:#fdf1f0; }
- .legend { display:flex; flex-wrap:wrap; gap:12px; color:var(--muted); font-size:12px; }
- .legend i { display:inline-block; width:10px; height:10px; border-radius:50%; margin-right:4px; }
- .empty { color:var(--muted); padding:12px; border:1px dashed var(--line); border-radius:5px; }
- @media (max-width:940px){ main{grid-template-columns:1fr;} .map{min-height:340px;} }
- </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="count">0</b></span><span>模式 <b id="modeName">当前预警</b></span><span>工单分区 <b id="woState">--</b></span><span>更新 <b id="updated">--</b></span></div>
- </header>
- <main>
- <aside>
- <h2>筛选</h2>
- <div class="filters">
- <label>等级
- <select id="fLevel"><option value="">全部</option><option value="1">1 级(红)</option><option value="2">2 级(黄)</option><option value="3">3 级(蓝)</option><option value="4">4 级(绿)</option></select>
- </label>
- <label>行业
- <select id="fIndustry"><option value="">全部行业</option><option value="GAS">燃气</option><option value="WATER_SUPPLY">供水</option><option value="DRAINAGE">排水</option></select>
- </label>
- <label>状态
- <select id="fStatus"><option value="">全部状态</option><option value="ACTIVE">进行中</option><option value="PENDING">待处置</option><option value="PROCESSING">处理中</option><option value="RELEASED">已解除</option><option value="HANDLED">已处置</option></select>
- </label>
- </div>
- <div class="toolbar" style="padding:0 0 12px; border-bottom:none">
- <button id="apply" type="button">应用筛选</button>
- <button id="reset" type="button">重置</button>
- <button id="refresh" type="button">手动刷新</button>
- <button id="toggleFail" type="button">模拟工单分区失败</button>
- </div>
- <h2>当前事件列表</h2>
- <table><thead><tr><th>事件</th><th>等级</th><th>数值</th><th>设备</th><th>工单</th><th>上图</th></tr></thead>
- <tbody id="eventRows"></tbody></table>
- <div class="empty" id="empty" style="display:none">当前筛选下没有事件(空列表状态)。</div>
- <div class="panel" id="detail"><h3>详情</h3><p style="color:var(--muted)">点击事件行或地图标记查看设备与工单状态。</p></div>
- </aside>
- <section>
- <div class="toolbar">
- <strong>中央一张图</strong>
- <div class="legend">
- <span><i style="background:#d0342c"></i>1级红</span>
- <span><i style="background:#f0a500"></i>2级黄</span>
- <span><i style="background:#1470ef"></i>3级蓝</span>
- <span><i style="background:#0f8a4d"></i>4级绿</span>
- <span><i style="background:#8a94a0"></i>未知</span>
- </div>
- </div>
- <div class="map" id="map"></div>
- <div class="caption" id="caption">点击事件定位设备;不同等级用红黄蓝绿区分</div>
- </section>
- </main>
- <script>
- const COLOR = { 1: "RED", 2: "YELLOW", 3: "BLUE", 4: "GREEN" };
- const TYPE = { GAS_SENSOR: "燃气浓度计", WATER_SUPPLY_SENSOR: "供水压力计", SEWAGE_ENV: "污水环境监测" };
- const INDUSTRY = { GAS: "燃气", WATER_SUPPLY: "供水", DRAINAGE: "排水" };
- const STATUS_NAME = { ACTIVE: "进行中", PENDING: "待处置", PROCESSING: "处理中", RELEASED: "已解除", HANDLED: "已处置" };
- const WO_STATES = { 1: "待派单", 2: "待接单", 3: "已接单", 4: "处理中", 5: "待验收", 6: "已结案", 7: "已取消" };
- const levelColor = level => (level >= 1 && level <= 4 ? COLOR[level] : "UNKNOWN");
- const warnings = [
- { no: "W-001", name: "燃气浓度预警", level: 1, status: "ACTIVE", value: "12.5", unit: "mg/m³", time: "09:50",
- device: "D-GAS-1", deviceName: "燃气浓度计-001", pointCode: "MP-A-001", pointName: "城区A-燃气1号点", location: "城区A-燃气1号点", type: "GAS_SENSOR", lon: 110.130, lat: 28.440, validation: { state: "VALID", reason: null, renderable: true, crs: "WGS84" },
- wo: { code: "WO-1", state: 4, stateName: "处理中", desc: "燃气浓度处置" } },
- { no: "W-002", name: "燃气压力预警", level: 2, status: "ACTIVE", value: "0.32", unit: "MPa", time: "09:45",
- device: "D-GAS-2", deviceName: "燃气浓度计-002", pointCode: "MP-A-002", pointName: "城区A-燃气2号点", location: "城区A-燃气2号点", type: "GAS_SENSOR", lon: 110.131, lat: 28.441, validation: { state: "VALID", reason: null, renderable: true, crs: "WGS84" }, wo: null },
- { no: "W-003", name: "供水压力预警", level: 3, status: "PENDING", value: "0.18", unit: "MPa", time: "09:40",
- device: "D-WS-1", deviceName: "供水压力计-001", pointCode: "MP-B-001", pointName: "城区B-供水1号点", location: "城区B-供水1号点", type: "WATER_SUPPLY_SENSOR", lon: 110.150, lat: 28.460, validation: { state: "VALID", reason: null, renderable: true, crs: "WGS84" },
- wo: { code: "WO-2", state: 1, stateName: "待派单", desc: "供水压力处置" } },
- { no: "W-004", name: "污水浓度预警", level: 4, status: "RELEASED", value: "18.2", unit: "mg/L", time: "09:35",
- device: "D-DR-1", deviceName: "污水环境监测仪-001", pointCode: "MP-C-001", pointName: "城区C-污水1号点", location: "城区C-污水1号点", type: "SEWAGE_ENV", lon: 110.170, lat: 28.480, validation: { state: "UNVERIFIED", reason: "UNVERIFIED_COORDINATE", renderable: false, crs: "WGS84" }, wo: null },
- { no: "W-005", name: "供水流量预警", level: 9, status: "ACTIVE", value: "66.6", unit: "L/s", time: "09:30",
- device: "D-WS-2", deviceName: "供水压力计-002", pointCode: "MP-B-002", pointName: "城区B-供水2号点", location: "城区B-供水2号点", type: "WATER_SUPPLY_SENSOR", lon: 110.151, lat: 28.461, validation: { state: "VALID", reason: null, renderable: true, crs: "WGS84" },
- wo: { code: "WO-3", state: 6, stateName: "已结案", desc: "供水流量处置" } }
- ];
- const alarms = [
- { no: "A-001", name: "燃气浓度报警", level: 1, status: "HANDLED", value: "22.6", unit: "mg/m³", time: "09:55",
- device: "D-GAS-3", deviceName: "燃气浓度计-003", pointCode: "MP-A-003", pointName: "城区A-燃气3号点", location: "城区A-燃气3号点", type: "GAS_SENSOR", lon: 110.132, lat: 28.442, validation: { state: "VALID", reason: null, renderable: true, crs: "WGS84" }, thresholdMin: 0, thresholdMax: 20,
- wo: { code: "WO-4", state: 7, stateName: "已取消", desc: "燃气浓度处置" } },
- { no: "A-002", name: "污水浓度报警", level: 2, status: "PROCESSING", value: "42.1", unit: "mg/L", time: "09:32",
- device: "D-DR-1", deviceName: "污水环境监测仪-001", pointCode: "MP-C-001", pointName: "城区C-污水1号点", location: "城区C-污水1号点", type: "SEWAGE_ENV", lon: 110.170, lat: 28.480, validation: { state: "UNVERIFIED", reason: "UNVERIFIED_COORDINATE", renderable: false, crs: "WGS84" }, wo: null }
- ];
- let mode = "WARNING";
- let woFailed = false;
- const filters = { level: "", industry: "", status: "" };
- function events() { return mode === "WARNING" ? warnings : alarms; }
- const industryOf = e => e.type === "GAS_SENSOR" ? "GAS" : e.type === "WATER_SUPPLY_SENSOR" ? "WATER_SUPPLY" : "DRAINAGE";
- function render() {
- const filtered = events().filter(e =>
- (!filters.level || String(e.level) === filters.level) &&
- (!filters.industry || industryOf(e) === filters.industry) &&
- (!filters.status || e.status === filters.status));
- document.getElementById("count").textContent = filtered.length;
- document.getElementById("modeName").textContent = mode === "WARNING" ? "当前预警" : "当前报警";
- document.getElementById("woState").textContent = woFailed ? "FAILED" : (filtered.some(e => e.wo) ? "SUCCESS" : "EMPTY");
- document.getElementById("updated").textContent = new Date().toLocaleTimeString("zh-CN");
- document.getElementById("empty").style.display = filtered.length ? "none" : "block";
- document.getElementById("eventRows").innerHTML = filtered.map((e, i) => `
- <tr>
- <td><a href="#" data-i="${i}">${e.no}</a></td>
- <td><span class="badge b-${levelColor(e.level)}">${levelColor(e.level)}</span></td>
- <td>${e.value} ${e.unit}</td>
- <td>${e.device}</td>
- <td>${woFailed ? "--" : e.wo ? `<span class="badge">${WO_STATES[e.wo.state]}</span>` : "无工单"}</td>
- <td>${e.validation.renderable ? "可上图" : "不可上图"}</td>
- </tr>`).join("");
- const map = document.getElementById("map");
- map.innerHTML = "";
- const plottable = filtered.filter(e => e.validation.renderable);
- if (plottable.length) {
- const lons = plottable.map(e => e.lon), lats = plottable.map(e => e.lat);
- const minLon = Math.min(...lons), maxLon = Math.max(...lons), minLat = Math.min(...lats), maxLat = Math.max(...lats);
- const xs = e => 12 + (e.lon - minLon) / Math.max(0.001, maxLon - minLon) * 76;
- const ys = e => 12 + (maxLat - e.lat) / Math.max(0.001, maxLat - minLat) * 76;
- plottable.forEach(e => {
- const el = document.createElement("div");
- el.className = "marker c-" + levelColor(e.level);
- el.style.left = xs(e) + "%"; el.style.top = ys(e) + "%";
- el.title = e.no;
- el.addEventListener("click", () => showDetail(e));
- map.appendChild(el);
- const l = document.createElement("div");
- l.className = "label"; l.style.left = xs(e) + "%"; l.style.top = ys(e) + "%";
- l.textContent = e.no;
- map.appendChild(l);
- });
- }
- document.getElementById("caption").textContent =
- `${mode === "WARNING" ? "预警" : "报警"} ${filtered.length} 条 · 可上图 ${plottable.length} 条 · ` +
- (filtered.map(e => e.no).join("、") || "无");
- document.querySelectorAll("#eventRows a").forEach(a =>
- a.addEventListener("click", ev => { ev.preventDefault(); showDetail(filtered[+a.dataset.i]); }));
- }
- function showDetail(e) {
- const box = document.getElementById("detail");
- box.classList.toggle("failed", woFailed);
- box.innerHTML = `<h3>${e.no} ${e.name}</h3><ul>
- <li>等级:${levelColor(e.level)}(${e.level} 级) 状态:${STATUS_NAME[e.status] || e.status} 时间:${e.time}</li>
- <li>数值:${e.value} ${e.unit} 设备:${e.device}(${e.deviceName})</li>
- <li>位置:${e.location} 类型:${TYPE[e.type]}</li>
- <li>坐标校验:${e.validation.state}${e.validation.reason ? `(${e.validation.reason})` : ""} 可上图:${e.validation.renderable ? "是" : "否"} CRS:${e.validation.crs}</li>
- ${e.thresholdMin != null || e.thresholdMax != null
- ? `<li>报警阈值:${e.thresholdMin ?? "-"} ~ ${e.thresholdMax ?? "-"}</li>`
- : ""}
- ${woFailed
- ? `<li class="empty" style="margin-top:6px">工单状态分区不可用(分区失败独立呈现,事件列表不受影响)。</li>`
- : e.wo
- ? `<li>工单:${e.wo.code} ${e.wo.desc} 状态:<span class="badge">${WO_STATES[e.wo.state]}</span></li>`
- : `<li>关联工单:无</li>`}
- </ul>`;
- document.querySelectorAll(".marker").forEach(m => m.classList.toggle("hot", m.title === e.no));
- }
- document.getElementById("tabWARNING").addEventListener("click", () => setMode("WARNING"));
- document.getElementById("tabALARM").addEventListener("click", () => setMode("ALARM"));
- function setMode(next) {
- mode = next;
- document.getElementById("tabWARNING").classList.toggle("on", mode === "WARNING");
- document.getElementById("tabALARM").classList.toggle("on", mode === "ALARM");
- render();
- }
- document.getElementById("apply").addEventListener("click", () => {
- filters.level = document.getElementById("fLevel").value;
- filters.industry = document.getElementById("fIndustry").value;
- filters.status = document.getElementById("fStatus").value;
- render();
- });
- document.getElementById("reset").addEventListener("click", () => {
- Object.keys(filters).forEach(k => filters[k] = "");
- document.getElementById("fLevel").value = "";
- document.getElementById("fIndustry").value = "";
- document.getElementById("fStatus").value = "";
- render();
- });
- document.getElementById("refresh").addEventListener("click", render);
- document.getElementById("toggleFail").addEventListener("click", () => {
- woFailed = !woFailed;
- document.getElementById("toggleFail").textContent = woFailed ? "恢复工单分区" : "模拟工单分区失败";
- render();
- });
- render();
- </script>
- </body>
- </html>
|