infrastructure-emergency-support-demo.html 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  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>基础设施 / 应急保障 Demo</title>
  7. <style>
  8. body {
  9. margin: 0;
  10. font-family: "Segoe UI", "Microsoft YaHei", sans-serif;
  11. color: #18212f;
  12. background: #eef2f7;
  13. }
  14. header {
  15. display: flex;
  16. align-items: center;
  17. justify-content: space-between;
  18. padding: 16px 24px;
  19. color: white;
  20. background: #17324d;
  21. }
  22. main {
  23. max-width: 1180px;
  24. margin: 20px auto;
  25. padding: 0 16px;
  26. display: grid;
  27. grid-template-columns: 300px 1fr;
  28. gap: 16px;
  29. }
  30. .panel {
  31. background: white;
  32. border: 1px solid #d8e0ea;
  33. border-radius: 6px;
  34. padding: 14px;
  35. box-shadow: 0 2px 5px rgba(23,50,77,.06);
  36. }
  37. .filters, .fixtures {
  38. display: grid;
  39. gap: 10px;
  40. }
  41. label, select, button {
  42. display: block;
  43. width: 100%;
  44. }
  45. label {
  46. font-size: 12px;
  47. color: #52606d;
  48. }
  49. select, button {
  50. padding: 8px;
  51. border: 1px solid #b9c4d0;
  52. background: white;
  53. border-radius: 4px;
  54. }
  55. button {
  56. cursor: pointer;
  57. background: #2563a8;
  58. border-color: #2563a8;
  59. color: white;
  60. }
  61. button.secondary {
  62. background: white;
  63. color: #17324d;
  64. }
  65. .metrics {
  66. display: grid;
  67. grid-template-columns: repeat(3, 1fr);
  68. gap: 8px;
  69. }
  70. .metric, .state {
  71. padding: 8px;
  72. border: 1px solid #d8e0ea;
  73. border-radius: 4px;
  74. background: #f8fafc;
  75. font-size: 12px;
  76. }
  77. .metric strong, .state strong {
  78. display: block;
  79. font-size: 20px;
  80. color: #17324d;
  81. }
  82. .state.ready { border-color: #8fc7a3; }
  83. .state.empty { border-color: #b9c4d0; }
  84. .state.unavailable { border-color: #d98c7c; }
  85. .map {
  86. height: 350px;
  87. margin-top: 10px;
  88. position: relative;
  89. border: 1px solid #c9d4df;
  90. border-radius: 4px;
  91. background: linear-gradient(#eaf2f7 1px, transparent 1px) 0 0/40px 40px,
  92. linear-gradient(90deg, #eaf2f7 1px, transparent 1px) 0 0/40px 40px, white;
  93. }
  94. .point {
  95. position: absolute;
  96. width: 12px;
  97. height: 12px;
  98. border-radius: 50%;
  99. transform: translate(-50%, -50%);
  100. }
  101. .point::after {
  102. content: attr(data-code);
  103. position: absolute;
  104. left: 14px;
  105. top: -5px;
  106. white-space: nowrap;
  107. font-size: 10px;
  108. color: #253849;
  109. }
  110. .emergency_resource { background: #2563a8; }
  111. .hazard_source { background: #c9483b; }
  112. .protection_target { background: #24825b; }
  113. .distribution {
  114. margin-top: 12px;
  115. display: grid;
  116. grid-template-columns: repeat(2, 1fr);
  117. gap: 12px;
  118. }
  119. .bar {
  120. height: 10px;
  121. background: #dce4eb;
  122. border-radius: 5px;
  123. overflow: hidden;
  124. }
  125. .bar span {
  126. display: block;
  127. height: 100%;
  128. background: #2563a8;
  129. }
  130. .row {
  131. display: flex;
  132. justify-content: space-between;
  133. font-size: 12px;
  134. margin-top: 6px;
  135. }
  136. table {
  137. width: 100%;
  138. margin-top: 10px;
  139. border-collapse: collapse;
  140. font-size: 12px;
  141. }
  142. th, td {
  143. text-align: left;
  144. padding: 6px;
  145. border-bottom: 1px solid #e3e9ef;
  146. }
  147. .endpoint {
  148. margin: 0;
  149. font-size: 12px;
  150. color: #aebdcb;
  151. }
  152. @media (max-width: 760px) {
  153. main { grid-template-columns: 1fr; }
  154. .metrics, .distribution { grid-template-columns: 1fr; }
  155. }
  156. </style>
  157. </head>
  158. <body>
  159. <header>
  160. <div>
  161. <strong>基础设施 / 应急保障</strong>
  162. <p class="endpoint">GET /api/cockpit/v1/infrastructure/emergency-support</p>
  163. </div>
  164. <span id="updated-at"></span>
  165. </header>
  166. <main>
  167. <section class="panel">
  168. <div class="filters">
  169. <div>
  170. <label for="fixture">数据状态</label>
  171. <select id="fixture">
  172. <option value="complete">完整资源</option>
  173. <option value="partial">部分缺失</option>
  174. <option value="associated">关联报警 / 工单</option>
  175. </select>
  176. </div>
  177. <div>
  178. <label for="category">类别</label>
  179. <select id="category">
  180. <option value="">全部</option>
  181. <option value="EMERGENCY_RESOURCE">应急资源</option>
  182. <option value="HAZARD_SOURCE">危险源</option>
  183. <option value="PROTECTION_TARGET">防护目标</option>
  184. </select>
  185. </div>
  186. <div>
  187. <label for="area">区域</label>
  188. <select id="area">
  189. <option value="">全部</option>
  190. <option value="CN">CN</option>
  191. <option value="TC">TC</option>
  192. <option value="UNKNOWN">UNKNOWN</option>
  193. </select>
  194. </div>
  195. <button id="refresh" type="button">手动刷新</button>
  196. </div>
  197. </section>
  198. <section class="panel">
  199. <div class="metrics">
  200. <div class="metric"><strong id="emergency-count">0</strong>应急资源</div>
  201. <div class="metric"><strong id="hazard-count">0</strong>危险源</div>
  202. <div class="metric"><strong id="protection-count">0</strong>防护目标</div>
  203. </div>
  204. <div class="metrics" style="margin-top:8px">
  205. <div class="state ready" id="state-emergency"><strong>READY</strong><span>应急资源</span></div>
  206. <div class="state ready" id="state-hazard"><strong>READY</strong><span>危险源</span></div>
  207. <div class="state ready" id="state-protection"><strong>READY</strong><span>防护目标</span></div>
  208. </div>
  209. <div class="map" id="map"></div>
  210. <div class="distribution">
  211. <div>
  212. <strong>行业分布</strong>
  213. <div id="industry-distribution"></div>
  214. </div>
  215. <div>
  216. <strong>区域分布</strong>
  217. <div id="area-distribution"></div>
  218. </div>
  219. </div>
  220. <table>
  221. <thead>
  222. <tr><th>对象</th><th>类别</th><th>行业</th><th>区域</th><th>预警</th><th>报警</th><th>工单</th></tr>
  223. </thead>
  224. <tbody id="items"></tbody>
  225. </table>
  226. </section>
  227. </main>
  228. <script>
  229. const objects = {
  230. complete: [
  231. item("ER-WS-001", "EMERGENCY_RESOURCE", "WATER_SUPPLY", "CN", 20, 24, 0, 0, 0),
  232. item("ER-GAS-001", "EMERGENCY_RESOURCE", "GAS", "CN", 32, 66, 0, 0, 0),
  233. item("HS-WS-001", "HAZARD_SOURCE", "WATER_SUPPLY", "TC", 62, 34, 0, 0, 0),
  234. item("PT-DR-001", "PROTECTION_TARGET", "DRAINAGE", "UNKNOWN", 72, 72, 0, 0, 0)
  235. ],
  236. partial: [
  237. item("ER-WS-002", "EMERGENCY_RESOURCE", "WATER_SUPPLY", "CN", 20, 30, 0, 0, 0),
  238. item("PT-GAS-001", "PROTECTION_TARGET", "GAS", "CN", 70, 42, 0, 0, 0)
  239. ],
  240. associated: [
  241. item("ER-WS-001", "EMERGENCY_RESOURCE", "WATER_SUPPLY", "CN", 20, 24, 2, 1, 3),
  242. item("HS-WS-001", "HAZARD_SOURCE", "WATER_SUPPLY", "TC", 58, 36, 1, 1, 1),
  243. item("PT-DR-001", "PROTECTION_TARGET", "DRAINAGE", "UNKNOWN", 76, 70, 0, 0, 1)
  244. ]
  245. };
  246. const states = {
  247. complete: ["READY", "READY", "READY"],
  248. partial: ["READY", "UNAVAILABLE", "EMPTY"],
  249. associated: ["READY", "READY", "READY"]
  250. };
  251. function item(code, category, industry, area, x, y, warning, alarm, workOrder) {
  252. return {code, category, industry, area, x, y, warning, alarm, workOrder};
  253. }
  254. function currentObjects() {
  255. const category = document.getElementById("category").value;
  256. const area = document.getElementById("area").value;
  257. return objects[document.getElementById("fixture").value].filter(function(object) {
  258. return (!category || object.category === category) && (!area || object.area === area);
  259. });
  260. }
  261. function render() {
  262. const fixture = document.getElementById("fixture").value;
  263. const selected = currentObjects();
  264. const counts = {
  265. EMERGENCY_RESOURCE: 0,
  266. HAZARD_SOURCE: 0,
  267. PROTECTION_TARGET: 0
  268. };
  269. selected.forEach(function(object) { counts[object.category]++; });
  270. document.getElementById("emergency-count").textContent = counts.EMERGENCY_RESOURCE;
  271. document.getElementById("hazard-count").textContent = counts.HAZARD_SOURCE;
  272. document.getElementById("protection-count").textContent = counts.PROTECTION_TARGET;
  273. const stateElements = ["state-emergency", "state-hazard", "state-protection"];
  274. states[fixture].forEach(function(state, index) {
  275. const element = document.getElementById(stateElements[index]);
  276. element.className = "state " + state.toLowerCase();
  277. element.querySelector("strong").textContent = state;
  278. });
  279. const map = document.getElementById("map");
  280. map.innerHTML = "";
  281. selected.forEach(function(object) {
  282. const point = document.createElement("div");
  283. point.className = "point " + object.category.toLowerCase();
  284. point.style.left = object.x + "%";
  285. point.style.top = object.y + "%";
  286. point.dataset.code = object.code;
  287. point.title = object.code + " / " + object.industry + " / " + object.area;
  288. map.appendChild(point);
  289. });
  290. renderDistribution("industry-distribution", groupBy(selected, "industry"));
  291. renderDistribution("area-distribution", groupBy(selected, "area"));
  292. document.getElementById("items").innerHTML = selected.map(function(object) {
  293. return "<tr><td>" + object.code + "</td><td>" + categoryName(object.category) +
  294. "</td><td>" + object.industry + "</td><td>" + object.area +
  295. "</td><td>" + object.warning + "</td><td>" + object.alarm +
  296. "</td><td>" + object.workOrder + "</td></tr>";
  297. }).join("");
  298. const summary = selected.reduce(function(total, object) {
  299. total.warning += object.warning;
  300. total.alarm += object.alarm;
  301. total.workOrder += object.workOrder;
  302. return total;
  303. }, {warning: 0, alarm: 0, workOrder: 0});
  304. document.getElementById("updated-at").textContent =
  305. "事件摘要 " + summary.warning + " / " + summary.alarm + " / " + summary.workOrder +
  306. ",更新于 " + new Date().toLocaleTimeString("zh-CN");
  307. }
  308. function groupBy(values, key) {
  309. return values.reduce(function(result, value) {
  310. result[value[key]] = (result[value[key]] || 0) + 1;
  311. return result;
  312. }, {});
  313. }
  314. function renderDistribution(id, groups) {
  315. const element = document.getElementById(id);
  316. element.innerHTML = "";
  317. Object.keys(groups).forEach(function(key) {
  318. const row = document.createElement("div");
  319. row.innerHTML = "<div class='row'><span>" + key + "</span><span>" + groups[key] + "</span></div>" +
  320. "<div class='bar'><span style='width:" + (groups[key] * 20) + "%'></span></div>";
  321. element.appendChild(row);
  322. });
  323. if (!Object.keys(groups).length) {
  324. element.innerHTML = "<div class='row'><span>EMPTY</span><span>0</span></div>";
  325. }
  326. }
  327. function categoryName(category) {
  328. return {
  329. EMERGENCY_RESOURCE: "应急资源",
  330. HAZARD_SOURCE: "危险源",
  331. PROTECTION_TARGET: "防护目标"
  332. }[category];
  333. }
  334. document.getElementById("fixture").addEventListener("change", render);
  335. document.getElementById("category").addEventListener("change", render);
  336. document.getElementById("area").addEventListener("change", render);
  337. document.getElementById("refresh").addEventListener("click", render);
  338. render();
  339. </script>
  340. </body>
  341. </html>