current-warnings-demo.html 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  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; }
  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. .tabs { display:inline-flex; border:1px solid var(--line); border-radius:5px; overflow:hidden; }
  14. .tabs button { border:0; border-right:1px solid var(--line); padding:8px 16px; background:#f4f7fa; }
  15. .tabs button.on { background:var(--blue); color:#fff; }
  16. .meta { display:flex; gap:14px; color:var(--muted); flex-wrap:wrap; }
  17. .meta b { color:var(--ink); font-weight:600; }
  18. button { cursor:pointer; border:1px solid #b9c8d5; background:var(--panel); padding:7px 12px; border-radius:4px; }
  19. button:hover { border-color:var(--blue); color:var(--blue); }
  20. select { border:1px solid #b9c8d5; border-radius:4px; padding:6px 8px; background:#fff; }
  21. main { display:grid; grid-template-columns:minmax(360px,440px) minmax(0,1fr); min-height:calc(100vh - 57px); }
  22. aside { padding:16px 18px; background:var(--panel); border-right:1px solid var(--line); overflow:auto; }
  23. section { display:grid; grid-template-rows:auto minmax(0,1fr) auto; min-width:0; }
  24. h2 { margin:0 0 10px; font-size:14px; }
  25. .filters { display:grid; grid-template-columns:1fr 1fr; gap:8px; margin-bottom:12px; }
  26. .filters label { display:flex; flex-direction:column; gap:4px; color:var(--muted); font-size:12px; }
  27. table { width:100%; border-collapse:collapse; font-size:12px; }
  28. th,td { text-align:left; padding:6px 7px; border-bottom:1px solid #edf1f5; }
  29. th { color:var(--muted); font-weight:600; }
  30. .badge { display:inline-block; padding:1px 8px; border-radius:9px; border:1px solid currentColor; font-size:11px; }
  31. .b-RED { color:#d0342c; background:#fdecea; } .b-YELLOW { color:#9a6b00; background:#fdf3d7; }
  32. .b-BLUE { color:#1470ef; background:#e8f0fe; } .b-GREEN { color:#0f8a4d; background:#e5f5ec; }
  33. .b-UNKNOWN { color:#64748b; background:#eef1f4; }
  34. .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); }
  35. .map { position:relative; background:#e2e9f0; background-image:linear-gradient(#d3dce5 1px,transparent 1px),linear-gradient(90deg,#d3dce5 1px,transparent 1px); background-size:56px 56px; }
  36. .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; }
  37. .marker.c-RED { background:#d0342c; } .marker.c-YELLOW { background:#f0a500; }
  38. .marker.c-BLUE { background:#1470ef; } .marker.c-GREEN { background:#0f8a4d; }
  39. .marker.c-UNKNOWN { background:#8a94a0; }
  40. .marker.hot { outline:3px solid var(--ink); outline-offset:1px; }
  41. .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; }
  42. .caption { padding:10px 18px; background:var(--panel); border-top:1px solid var(--line); color:var(--muted); }
  43. .panel { margin-top:14px; border:1px solid var(--line); border-radius:5px; padding:10px 12px; min-height:120px; }
  44. .panel h3 { margin:0 0 8px; font-size:13px; }
  45. .panel ul { margin:0; padding-left:18px; }
  46. .panel.failed { border-color:#d0342c; background:#fdf1f0; }
  47. .legend { display:flex; flex-wrap:wrap; gap:12px; color:var(--muted); font-size:12px; }
  48. .legend i { display:inline-block; width:10px; height:10px; border-radius:50%; margin-right:4px; }
  49. .empty { color:var(--muted); padding:12px; border:1px dashed var(--line); border-radius:5px; }
  50. @media (max-width:940px){ main{grid-template-columns:1fr;} .map{min-height:340px;} }
  51. </style>
  52. </head>
  53. <body>
  54. <header>
  55. <h1>预警处置 / 当前预警</h1>
  56. <div class="tabs">
  57. <button id="tabWARNING" class="on" type="button">当前预警</button>
  58. <button id="tabALARM" type="button">当前报警</button>
  59. </div>
  60. <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>
  61. </header>
  62. <main>
  63. <aside>
  64. <h2>筛选</h2>
  65. <div class="filters">
  66. <label>等级
  67. <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>
  68. </label>
  69. <label>行业
  70. <select id="fIndustry"><option value="">全部行业</option><option value="GAS">燃气</option><option value="WATER_SUPPLY">供水</option><option value="DRAINAGE">排水</option></select>
  71. </label>
  72. <label>状态
  73. <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>
  74. </label>
  75. </div>
  76. <div class="toolbar" style="padding:0 0 12px; border-bottom:none">
  77. <button id="apply" type="button">应用筛选</button>
  78. <button id="reset" type="button">重置</button>
  79. <button id="refresh" type="button">手动刷新</button>
  80. <button id="toggleFail" type="button">模拟工单分区失败</button>
  81. </div>
  82. <h2>当前事件列表</h2>
  83. <table><thead><tr><th>事件</th><th>等级</th><th>数值</th><th>设备</th><th>工单</th><th>上图</th></tr></thead>
  84. <tbody id="eventRows"></tbody></table>
  85. <div class="empty" id="empty" style="display:none">当前筛选下没有事件(空列表状态)。</div>
  86. <div class="panel" id="detail"><h3>详情</h3><p style="color:var(--muted)">点击事件行或地图标记查看设备与工单状态。</p></div>
  87. </aside>
  88. <section>
  89. <div class="toolbar">
  90. <strong>中央一张图</strong>
  91. <div class="legend">
  92. <span><i style="background:#d0342c"></i>1级红</span>
  93. <span><i style="background:#f0a500"></i>2级黄</span>
  94. <span><i style="background:#1470ef"></i>3级蓝</span>
  95. <span><i style="background:#0f8a4d"></i>4级绿</span>
  96. <span><i style="background:#8a94a0"></i>未知</span>
  97. </div>
  98. </div>
  99. <div class="map" id="map"></div>
  100. <div class="caption" id="caption">点击事件定位设备;不同等级用红黄蓝绿区分</div>
  101. </section>
  102. </main>
  103. <script>
  104. const COLOR = { 1: "RED", 2: "YELLOW", 3: "BLUE", 4: "GREEN" };
  105. const TYPE = { GAS_SENSOR: "燃气浓度计", WATER_SUPPLY_SENSOR: "供水压力计", SEWAGE_ENV: "污水环境监测" };
  106. const INDUSTRY = { GAS: "燃气", WATER_SUPPLY: "供水", DRAINAGE: "排水" };
  107. const STATUS_NAME = { ACTIVE: "进行中", PENDING: "待处置", PROCESSING: "处理中", RELEASED: "已解除", HANDLED: "已处置" };
  108. const WO_STATES = { 1: "待派单", 2: "待接单", 3: "已接单", 4: "处理中", 5: "待验收", 6: "已结案", 7: "已取消" };
  109. const levelColor = level => (level >= 1 && level <= 4 ? COLOR[level] : "UNKNOWN");
  110. const warnings = [
  111. { no: "W-001", name: "燃气浓度预警", level: 1, status: "ACTIVE", value: "12.5", unit: "mg/m³", time: "09:50",
  112. 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" },
  113. wo: { code: "WO-1", state: 4, stateName: "处理中", desc: "燃气浓度处置" } },
  114. { no: "W-002", name: "燃气压力预警", level: 2, status: "ACTIVE", value: "0.32", unit: "MPa", time: "09:45",
  115. 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 },
  116. { no: "W-003", name: "供水压力预警", level: 3, status: "PENDING", value: "0.18", unit: "MPa", time: "09:40",
  117. 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" },
  118. wo: { code: "WO-2", state: 1, stateName: "待派单", desc: "供水压力处置" } },
  119. { no: "W-004", name: "污水浓度预警", level: 4, status: "RELEASED", value: "18.2", unit: "mg/L", time: "09:35",
  120. 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 },
  121. { no: "W-005", name: "供水流量预警", level: 9, status: "ACTIVE", value: "66.6", unit: "L/s", time: "09:30",
  122. 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" },
  123. wo: { code: "WO-3", state: 6, stateName: "已结案", desc: "供水流量处置" } }
  124. ];
  125. const alarms = [
  126. { no: "A-001", name: "燃气浓度报警", level: 1, status: "HANDLED", value: "22.6", unit: "mg/m³", time: "09:55",
  127. 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,
  128. wo: { code: "WO-4", state: 7, stateName: "已取消", desc: "燃气浓度处置" } },
  129. { no: "A-002", name: "污水浓度报警", level: 2, status: "PROCESSING", value: "42.1", unit: "mg/L", time: "09:32",
  130. 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 }
  131. ];
  132. let mode = "WARNING";
  133. let woFailed = false;
  134. const filters = { level: "", industry: "", status: "" };
  135. function events() { return mode === "WARNING" ? warnings : alarms; }
  136. const industryOf = e => e.type === "GAS_SENSOR" ? "GAS" : e.type === "WATER_SUPPLY_SENSOR" ? "WATER_SUPPLY" : "DRAINAGE";
  137. function render() {
  138. const filtered = events().filter(e =>
  139. (!filters.level || String(e.level) === filters.level) &&
  140. (!filters.industry || industryOf(e) === filters.industry) &&
  141. (!filters.status || e.status === filters.status));
  142. document.getElementById("count").textContent = filtered.length;
  143. document.getElementById("modeName").textContent = mode === "WARNING" ? "当前预警" : "当前报警";
  144. document.getElementById("woState").textContent = woFailed ? "FAILED" : (filtered.some(e => e.wo) ? "SUCCESS" : "EMPTY");
  145. document.getElementById("updated").textContent = new Date().toLocaleTimeString("zh-CN");
  146. document.getElementById("empty").style.display = filtered.length ? "none" : "block";
  147. document.getElementById("eventRows").innerHTML = filtered.map((e, i) => `
  148. <tr>
  149. <td><a href="#" data-i="${i}">${e.no}</a></td>
  150. <td><span class="badge b-${levelColor(e.level)}">${levelColor(e.level)}</span></td>
  151. <td>${e.value} ${e.unit}</td>
  152. <td>${e.device}</td>
  153. <td>${woFailed ? "--" : e.wo ? `<span class="badge">${WO_STATES[e.wo.state]}</span>` : "无工单"}</td>
  154. <td>${e.validation.renderable ? "可上图" : "不可上图"}</td>
  155. </tr>`).join("");
  156. const map = document.getElementById("map");
  157. map.innerHTML = "";
  158. const plottable = filtered.filter(e => e.validation.renderable);
  159. if (plottable.length) {
  160. const lons = plottable.map(e => e.lon), lats = plottable.map(e => e.lat);
  161. const minLon = Math.min(...lons), maxLon = Math.max(...lons), minLat = Math.min(...lats), maxLat = Math.max(...lats);
  162. const xs = e => 12 + (e.lon - minLon) / Math.max(0.001, maxLon - minLon) * 76;
  163. const ys = e => 12 + (maxLat - e.lat) / Math.max(0.001, maxLat - minLat) * 76;
  164. plottable.forEach(e => {
  165. const el = document.createElement("div");
  166. el.className = "marker c-" + levelColor(e.level);
  167. el.style.left = xs(e) + "%"; el.style.top = ys(e) + "%";
  168. el.title = e.no;
  169. el.addEventListener("click", () => showDetail(e));
  170. map.appendChild(el);
  171. const l = document.createElement("div");
  172. l.className = "label"; l.style.left = xs(e) + "%"; l.style.top = ys(e) + "%";
  173. l.textContent = e.no;
  174. map.appendChild(l);
  175. });
  176. }
  177. document.getElementById("caption").textContent =
  178. `${mode === "WARNING" ? "预警" : "报警"} ${filtered.length} 条 · 可上图 ${plottable.length} 条 · ` +
  179. (filtered.map(e => e.no).join("、") || "无");
  180. document.querySelectorAll("#eventRows a").forEach(a =>
  181. a.addEventListener("click", ev => { ev.preventDefault(); showDetail(filtered[+a.dataset.i]); }));
  182. }
  183. function showDetail(e) {
  184. const box = document.getElementById("detail");
  185. box.classList.toggle("failed", woFailed);
  186. box.innerHTML = `<h3>${e.no} ${e.name}</h3><ul>
  187. <li>等级:${levelColor(e.level)}(${e.level} 级) 状态:${STATUS_NAME[e.status] || e.status} 时间:${e.time}</li>
  188. <li>数值:${e.value} ${e.unit} 设备:${e.device}(${e.deviceName})</li>
  189. <li>位置:${e.location} 类型:${TYPE[e.type]}</li>
  190. <li>坐标校验:${e.validation.state}${e.validation.reason ? `(${e.validation.reason})` : ""} 可上图:${e.validation.renderable ? "是" : "否"} CRS:${e.validation.crs}</li>
  191. ${e.thresholdMin != null || e.thresholdMax != null
  192. ? `<li>报警阈值:${e.thresholdMin ?? "-"} ~ ${e.thresholdMax ?? "-"}</li>`
  193. : ""}
  194. ${woFailed
  195. ? `<li class="empty" style="margin-top:6px">工单状态分区不可用(分区失败独立呈现,事件列表不受影响)。</li>`
  196. : e.wo
  197. ? `<li>工单:${e.wo.code} ${e.wo.desc} 状态:<span class="badge">${WO_STATES[e.wo.state]}</span></li>`
  198. : `<li>关联工单:无</li>`}
  199. </ul>`;
  200. document.querySelectorAll(".marker").forEach(m => m.classList.toggle("hot", m.title === e.no));
  201. }
  202. document.getElementById("tabWARNING").addEventListener("click", () => setMode("WARNING"));
  203. document.getElementById("tabALARM").addEventListener("click", () => setMode("ALARM"));
  204. function setMode(next) {
  205. mode = next;
  206. document.getElementById("tabWARNING").classList.toggle("on", mode === "WARNING");
  207. document.getElementById("tabALARM").classList.toggle("on", mode === "ALARM");
  208. render();
  209. }
  210. document.getElementById("apply").addEventListener("click", () => {
  211. filters.level = document.getElementById("fLevel").value;
  212. filters.industry = document.getElementById("fIndustry").value;
  213. filters.status = document.getElementById("fStatus").value;
  214. render();
  215. });
  216. document.getElementById("reset").addEventListener("click", () => {
  217. Object.keys(filters).forEach(k => filters[k] = "");
  218. document.getElementById("fLevel").value = "";
  219. document.getElementById("fIndustry").value = "";
  220. document.getElementById("fStatus").value = "";
  221. render();
  222. });
  223. document.getElementById("refresh").addEventListener("click", render);
  224. document.getElementById("toggleFail").addEventListener("click", () => {
  225. woFailed = !woFailed;
  226. document.getElementById("toggleFail").textContent = woFailed ? "恢复工单分区" : "模拟工单分区失败";
  227. render();
  228. });
  229. render();
  230. </script>
  231. </body>
  232. </html>