handling-efficiency-demo.html 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  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. select { border:1px solid #b9c8d5; border-radius:4px; padding:6px 8px; background:#fff; }
  20. main { display:grid; grid-template-columns:minmax(360px,450px) minmax(0,1fr); min-height:calc(100vh - 57px); }
  21. aside { padding:16px 18px; background:var(--panel); border-right:1px solid var(--line); overflow:auto; }
  22. section { display:grid; grid-template-rows:auto minmax(0,1fr) auto; min-width:0; }
  23. h2 { margin:0 0 10px; font-size:14px; }
  24. .kpis { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:8px; margin-bottom:14px; }
  25. .kpi { border:1px solid var(--line); border-radius:5px; padding:8px 10px; }
  26. .kpi span { display:block; color:var(--muted); font-size:11px; }
  27. .kpi b { display:block; margin-top:2px; font-size:17px; }
  28. .basis { border-left:3px solid var(--blue); background:#f5f8fc; padding:8px 10px; color:var(--muted); margin-bottom:14px; font-size:12px; }
  29. table { width:100%; border-collapse:collapse; font-size:12px; }
  30. th,td { text-align:left; padding:6px 7px; border-bottom:1px solid #edf1f5; }
  31. th { color:var(--muted); font-weight:600; }
  32. .statebars { display:grid; gap:5px; margin-bottom:14px; }
  33. .staterow { display:grid; grid-template-columns:78px 1fr 34px; gap:8px; align-items:center; cursor:pointer; }
  34. .staterow .bar { height:12px; background:#e6ecf2; border-radius:3px; overflow:hidden; }
  35. .staterow .bar i { display:block; height:100%; background:var(--blue); }
  36. .staterow.on .bar i { background:#0f8a4d; }
  37. .staterow.on { font-weight:600; }
  38. .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); }
  39. .chart { display:flex; align-items:flex-end; gap:8px; height:150px; padding:10px 18px; background:#fff; border-bottom:1px solid var(--line); }
  40. .chart .col { flex:1; min-width:22px; display:flex; flex-direction:column; justify-content:flex-end; align-items:center; gap:4px; }
  41. .chart .col i { display:block; width:100%; background:#b65c08; border-radius:3px 3px 0 0; }
  42. .chart .col small { font-size:10px; color:var(--muted); }
  43. .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:160px; }
  44. .marker { position:absolute; width:14px; height:14px; margin:-7px 0 0 -7px; border-radius:50%; background:#d0342c; border:2px solid #fff; }
  45. .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; }
  46. .caption { padding:10px 18px; background:var(--panel); border-top:1px solid var(--line); color:var(--muted); }
  47. .empty { color:var(--muted); padding:12px; border:1px dashed var(--line); border-radius:5px; margin-top:10px; }
  48. @media (max-width:940px){ main{grid-template-columns:1fr;} }
  49. </style>
  50. </head>
  51. <body>
  52. <header>
  53. <h1>预警处置 / 预警效率</h1>
  54. <div class="tabs">
  55. <button id="tabWARNING" class="on" type="button">预警</button>
  56. <button id="tabALARM" type="button">报警</button>
  57. </div>
  58. <div class="meta"><span>事件族 <b id="familyName">预警</b></span><span>范围 <b id="rangeText">--</b></span><span>更新 <b id="updated">--</b></span></div>
  59. </header>
  60. <main>
  61. <aside>
  62. <h2>效率 KPI</h2>
  63. <div class="kpis">
  64. <div class="kpi"><span>事件总数</span><b id="totalEvents">0</b></div>
  65. <div class="kpi"><span>已结案</span><b id="handledCount">0</b></div>
  66. <div class="kpi"><span>处理中(1-5)</span><b id="inProgress">0</b></div>
  67. <div class="kpi"><span>已取消(7)</span><b id="cancelled">0</b></div>
  68. <div class="kpi"><span>无关联工单</span><b id="missing">0</b></div>
  69. <div class="kpi"><span>处置效率</span><b id="efficiency">0%</b></div>
  70. </div>
  71. <div class="basis">处置效率口径:已结案工单数 ÷ 事件总数(basis = CLOSED_WORK_ORDERS_OVER_TOTAL_EVENTS)。本页不做预警真伪或缺报/误报判断。</div>
  72. <h2>工单状态分布(七态不合并,点击筛选)</h2>
  73. <div class="statebars" id="stateBars"></div>
  74. <h2>平均处置耗时</h2>
  75. <div class="kpis">
  76. <div class="kpi"><span>平均耗时(小时)</span><b id="avgDuration">--</b></div>
  77. <div class="kpi"><span>样本数</span><b id="avgSamples">0</b></div>
  78. <div class="kpi"><span>统计范围</span><b>已结案</b></div>
  79. </div>
  80. <h2 style="margin-top:14px">事件列表</h2>
  81. <table><thead><tr><th>事件</th><th>设备</th><th>等级</th><th>工单状态</th><th>耗时(h)</th></tr></thead>
  82. <tbody id="eventRows"></tbody></table>
  83. <div class="empty" id="empty" style="display:none">当前筛选下没有事件。</div>
  84. </aside>
  85. <section>
  86. <div class="toolbar">
  87. <strong>每小时平均处置耗时趋势</strong>
  88. <span style="color:var(--muted)">柱高 = 该小时已结案工单的平均耗时</span>
  89. </div>
  90. <div class="chart" id="chart"></div>
  91. <div class="toolbar">
  92. <strong>中央一张图</strong>
  93. <span id="filterLabel" style="color:var(--muted)">未筛选</span>
  94. <button id="clearFilter" type="button">清除状态筛选</button>
  95. <button id="refresh" type="button">手动刷新</button>
  96. </div>
  97. <div class="map" id="map"></div>
  98. <div class="caption" id="caption">点击左侧状态条筛选事件与地图点位</div>
  99. </section>
  100. </main>
  101. <script>
  102. const STATES = [
  103. { code: 1, name: "待派单" }, { code: 2, name: "待接单" }, { code: 3, name: "已接单" },
  104. { code: 4, name: "处理中" }, { code: 5, name: "待验收" }, { code: 6, name: "已结案" },
  105. { code: 7, name: "已取消" }
  106. ];
  107. const makeFixture = (prefix, specs) => specs.map((s, i) => ({
  108. no: prefix + String(i + 1).padStart(3, "0"),
  109. device: `D-${s.ind}-${i + 1}`,
  110. level: s.level, hour: s.hour, state: s.state, duration: s.duration,
  111. lon: 110.13 + i * 0.005, lat: 28.44 + (i % 5) * 0.008
  112. }));
  113. const fixture = {
  114. WARNING: makeFixture("EW-", [
  115. { ind: "GAS", level: 1, hour: 8, state: 1, duration: null },
  116. { ind: "GAS", level: 2, hour: 8, state: 6, duration: 2.0 },
  117. { ind: "WS", level: 2, hour: 9, state: 4, duration: null },
  118. { ind: "DR", level: 3, hour: 9, state: 5, duration: null },
  119. { ind: "WS", level: 4, hour: 10, state: 6, duration: 1.5 },
  120. { ind: "GAS", level: 1, hour: 10, state: 7, duration: null },
  121. { ind: "DR", level: 2, hour: 11, state: null, duration: null },
  122. { ind: "DR", level: 1, hour: 11, state: null, duration: null },
  123. { ind: "GAS", level: 1, hour: 11, state: 6, duration: 3.25 }
  124. ]),
  125. ALARM: makeFixture("EA-", [
  126. { ind: "GAS", level: 1, hour: 8, state: 6, duration: 4.0 },
  127. { ind: "WS", level: 2, hour: 10, state: 4, duration: null },
  128. { ind: "DR", level: 1, hour: 11, state: null, duration: null }
  129. ])
  130. };
  131. let family = "WARNING";
  132. let stateFilter = null;
  133. function render() {
  134. const all = fixture[family];
  135. const scoped = all;
  136. const stateCounts = {};
  137. STATES.forEach(s => stateCounts[s.code] = 0);
  138. let linked = 0, missing = 0;
  139. scoped.forEach(e => {
  140. if (e.state === null) missing++;
  141. else { linked++; stateCounts[e.state]++; }
  142. });
  143. const handled = stateCounts[6];
  144. const inProgress = [1, 2, 3, 4, 5].reduce((n, c) => n + stateCounts[c], 0);
  145. const durations = scoped.filter(e => e.state === 6 && e.duration !== null).map(e => e.duration);
  146. const avg = durations.length ? (durations.reduce((a, b) => a + b, 0) / durations.length) : null;
  147. const filtered = stateFilter === null
  148. ? scoped
  149. : stateFilter === "NONE" ? scoped.filter(e => e.state === null)
  150. : scoped.filter(e => e.state === stateFilter);
  151. document.getElementById("familyName").textContent = family === "WARNING" ? "预警" : "报警";
  152. document.getElementById("rangeText").textContent = "2026-09-02 08:00 ~ 12:00 (+08:00)";
  153. document.getElementById("updated").textContent = new Date().toLocaleTimeString("zh-CN");
  154. document.getElementById("totalEvents").textContent = scoped.length;
  155. document.getElementById("handledCount").textContent = handled;
  156. document.getElementById("inProgress").textContent = inProgress;
  157. document.getElementById("cancelled").textContent = stateCounts[7];
  158. document.getElementById("missing").textContent = missing;
  159. document.getElementById("efficiency").textContent =
  160. scoped.length ? (handled / scoped.length * 100).toFixed(1) + "%" : "0.0%";
  161. document.getElementById("avgDuration").textContent = avg === null ? "--" : avg.toFixed(2);
  162. document.getElementById("avgSamples").textContent = durations.length;
  163. const maxState = Math.max(1, ...Object.values(stateCounts));
  164. document.getElementById("stateBars").innerHTML = STATES.map(s => `
  165. <div class="staterow ${stateFilter === s.code ? "on" : ""}" data-code="${s.code}">
  166. <span>${s.code} ${s.name}</span>
  167. <span class="bar"><i style="width:${stateCounts[s.code] / maxState * 100}%"></i></span>
  168. <span>${stateCounts[s.code]}</span>
  169. </div>`).join("");
  170. document.querySelectorAll(".staterow").forEach(row =>
  171. row.addEventListener("click", () => {
  172. const code = Number(row.dataset.code);
  173. stateFilter = stateFilter === code ? null : code;
  174. render();
  175. }));
  176. document.getElementById("empty").style.display = filtered.length ? "none" : "block";
  177. document.getElementById("eventRows").innerHTML = filtered.map(e => `
  178. <tr>
  179. <td>${e.no}</td><td>${e.device}</td>
  180. <td>${e.level}</td>
  181. <td>${e.state === null ? '<span style="color:#64748b">无关联工单</span>' : STATES.find(s => s.code === e.state).name}</td>
  182. <td>${e.duration === null ? "--" : e.duration.toFixed(2)}</td>
  183. </tr>`).join("");
  184. const hours = {};
  185. scoped.forEach(e => {
  186. const bucket = "0" + e.hour + ":00";
  187. (hours[bucket] = hours[bucket] || []).push(e);
  188. });
  189. const bucketKeys = Object.keys(hours).sort();
  190. const maxAvg = Math.max(0.5, ...bucketKeys.map(k => {
  191. const ds = hours[k].filter(e => e.state === 6 && e.duration !== null).map(e => e.duration);
  192. return ds.length ? ds.reduce((a, b) => a + b, 0) / ds.length : 0;
  193. }));
  194. document.getElementById("chart").innerHTML = bucketKeys.map(k => {
  195. const ds = hours[k].filter(e => e.state === 6 && e.duration !== null).map(e => e.duration);
  196. const value = ds.length ? ds.reduce((a, b) => a + b, 0) / ds.length : 0;
  197. return `<div class="col"><small>${value ? value.toFixed(1) + "h" : "--"}</small>
  198. <i style="height:${value / maxAvg * 110}px"></i><small>${k}</small></div>`;
  199. }).join("");
  200. const map = document.getElementById("map");
  201. map.innerHTML = "";
  202. document.getElementById("filterLabel").textContent =
  203. stateFilter === null ? "未筛选"
  204. : stateFilter === "NONE" ? "无关联工单"
  205. : STATES.find(s => s.code === stateFilter).name;
  206. if (filtered.length) {
  207. const lons = filtered.map(e => e.lon), lats = filtered.map(e => e.lat);
  208. const minLon = Math.min(...lons), maxLon = Math.max(...lons), minLat = Math.min(...lats), maxLat = Math.max(...lats);
  209. const xs = e => 12 + (e.lon - minLon) / Math.max(0.001, maxLon - minLon) * 76;
  210. const ys = e => 12 + (maxLat - e.lat) / Math.max(0.001, maxLat - minLat) * 76;
  211. filtered.forEach(e => {
  212. const el = document.createElement("div");
  213. el.className = "marker";
  214. el.style.left = xs(e) + "%"; el.style.top = ys(e) + "%";
  215. el.title = e.no;
  216. map.appendChild(el);
  217. const l = document.createElement("div");
  218. l.className = "label"; l.style.left = xs(e) + "%"; l.style.top = ys(e) + "%";
  219. l.textContent = e.no;
  220. map.appendChild(l);
  221. });
  222. }
  223. document.getElementById("caption").textContent =
  224. `${filtered.length} 个事件点位(状态筛选:${document.getElementById("filterLabel").textContent})`;
  225. }
  226. document.getElementById("tabWARNING").addEventListener("click", () => setFamily("WARNING"));
  227. document.getElementById("tabALARM").addEventListener("click", () => setFamily("ALARM"));
  228. function setFamily(next) {
  229. family = next;
  230. stateFilter = null;
  231. document.getElementById("tabWARNING").classList.toggle("on", family === "WARNING");
  232. document.getElementById("tabALARM").classList.toggle("on", family === "ALARM");
  233. render();
  234. }
  235. document.getElementById("clearFilter").addEventListener("click", () => { stateFilter = null; render(); });
  236. document.getElementById("refresh").addEventListener("click", render);
  237. render();
  238. </script>
  239. </body>
  240. </html>