overview-infrastructure-demo.html 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  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 {
  9. --ink: #172033;
  10. --muted: #667085;
  11. --line: #d9dee7;
  12. --panel: #ffffff;
  13. --page: #f3f5f8;
  14. --gas: #d92d20;
  15. --water: #1570ef;
  16. --drainage: #067647;
  17. }
  18. * { box-sizing: border-box; }
  19. body {
  20. margin: 0;
  21. min-height: 100vh;
  22. color: var(--ink);
  23. font-family: "Microsoft YaHei", "Segoe UI", Arial, sans-serif;
  24. background: var(--page);
  25. }
  26. header {
  27. display: flex;
  28. flex-wrap: wrap;
  29. gap: 16px;
  30. align-items: center;
  31. justify-content: space-between;
  32. padding: 18px 28px;
  33. background: var(--panel);
  34. border-bottom: 1px solid var(--line);
  35. }
  36. h1 { margin: 0; font-size: 20px; font-weight: 600; }
  37. .status { color: var(--muted); font-size: 13px; }
  38. .status strong { color: #067647; font-weight: 600; }
  39. .summary {
  40. display: grid;
  41. grid-template-columns: repeat(4, minmax(180px, 1fr));
  42. gap: 1px;
  43. background: var(--line);
  44. border-bottom: 1px solid var(--line);
  45. }
  46. .summary div { background: var(--panel); padding: 18px 28px; }
  47. .summary span { display: block; color: var(--muted); font-size: 13px; }
  48. .summary strong { display: block; margin-top: 5px; font-size: 26px; font-weight: 600; }
  49. .layout {
  50. display: grid;
  51. grid-template-columns: minmax(330px, 420px) minmax(0, 1fr);
  52. min-height: calc(100vh - 173px);
  53. }
  54. aside {
  55. border-right: 1px solid var(--line);
  56. background: var(--panel);
  57. }
  58. .filter {
  59. display: flex;
  60. gap: 8px;
  61. padding: 16px 20px;
  62. border-bottom: 1px solid var(--line);
  63. }
  64. button {
  65. min-width: 62px;
  66. height: 32px;
  67. padding: 0 12px;
  68. border: 1px solid var(--line);
  69. border-radius: 4px;
  70. background: #fff;
  71. color: var(--ink);
  72. font-size: 13px;
  73. cursor: pointer;
  74. }
  75. button.active { border-color: #1570ef; background: #eff6ff; color: #175cd3; font-weight: 600; }
  76. .industries { padding: 16px 20px; }
  77. .industry + .industry { margin-top: 12px; border-top: 1px solid var(--line); }
  78. .industry { padding-top: 12px; }
  79. .industry-head {
  80. display: flex;
  81. justify-content: space-between;
  82. align-items: baseline;
  83. font-size: 15px;
  84. font-weight: 600;
  85. }
  86. .industry-head small { color: var(--muted); font-size: 12px; }
  87. .bar {
  88. display: grid;
  89. grid-template-columns: 70px 1fr 36px;
  90. gap: 8px;
  91. align-items: center;
  92. margin-top: 10px;
  93. font-size: 12px;
  94. color: var(--muted);
  95. }
  96. .track { height: 8px; background: #eef1f5; overflow: hidden; }
  97. .fill { height: 100%; width: 0; }
  98. .facility-gas { background: var(--gas); }
  99. .facility-water { background: var(--water); }
  100. .facility-drainage { background: var(--drainage); }
  101. .types {
  102. display: flex;
  103. flex-wrap: wrap;
  104. gap: 6px;
  105. margin-top: 10px;
  106. font-size: 12px;
  107. color: #475467;
  108. }
  109. .types span { padding: 3px 6px; background: #f7f8fa; border-radius: 3px; }
  110. .map-panel {
  111. display: grid;
  112. grid-template-rows: auto minmax(0, 1fr);
  113. background: #dfe5ec;
  114. }
  115. .map-tools {
  116. display: flex;
  117. justify-content: space-between;
  118. gap: 12px;
  119. padding: 14px 20px;
  120. background: var(--panel);
  121. border-bottom: 1px solid var(--line);
  122. font-size: 13px;
  123. }
  124. .legend { display: flex; gap: 14px; color: var(--muted); align-items: center; }
  125. .dot-label { display: inline-flex; align-items: center; gap: 5px; }
  126. .shape { width: 9px; height: 9px; border-radius: 50%; }
  127. .square { border-radius: 2px; }
  128. .map {
  129. position: relative;
  130. overflow: hidden;
  131. background:
  132. linear-gradient(0deg, rgba(255, 255, 255, .48) 1px, transparent 1px),
  133. linear-gradient(90deg, rgba(255, 255, 255, .48) 1px, transparent 1px),
  134. #dfe5ec;
  135. background-size: 80px 80px, 80px 80px;
  136. }
  137. .point { position: absolute; width: 13px; height: 13px; border-radius: 50%; border: 2px solid #fff; box-shadow: 0 1px 4px rgba(16, 24, 40, .24); }
  138. .point.facility { border-radius: 2px; transform: rotate(45deg); }
  139. .point.gas { background: var(--gas); }
  140. .point.water { background: var(--water); }
  141. .point.drainage { background: var(--drainage); }
  142. .point-label {
  143. position: absolute;
  144. transform: translate(14px, -4px);
  145. padding: 2px 5px;
  146. background: rgba(255, 255, 255, .88);
  147. font-size: 11px;
  148. color: #344054;
  149. border-radius: 3px;
  150. white-space: nowrap;
  151. }
  152. @media (max-width: 900px) {
  153. .summary { grid-template-columns: repeat(2, 1fr); }
  154. .layout { grid-template-columns: 1fr; min-height: auto; }
  155. aside { border-right: 0; border-bottom: 1px solid var(--line); }
  156. .map { height: 460px; }
  157. }
  158. </style>
  159. </head>
  160. <body>
  161. <header>
  162. <h1>总体概览 / 基础设施</h1>
  163. <div class="status">更新时间 <strong id="updated-at">2026-09-09 10:00:00 +08:00</strong> · 状态 <strong>CURRENT</strong></div>
  164. </header>
  165. <section class="summary">
  166. <div><span>设施总数</span><strong id="facility-total">418</strong></div>
  167. <div><span>智能设备总数</span><strong id="device-total">1126</strong></div>
  168. <div><span>未知类型</span><strong>0</strong></div>
  169. <div><span>类型错挂</span><strong>0</strong></div>
  170. </section>
  171. <main class="layout">
  172. <aside>
  173. <div class="filter" aria-label="行业筛选">
  174. <button data-industry="ALL" class="active">全部</button>
  175. <button data-industry="GAS">燃气</button>
  176. <button data-industry="WATER_SUPPLY">供水</button>
  177. <button data-industry="DRAINAGE">排水</button>
  178. </div>
  179. <div class="industries" id="industries"></div>
  180. </aside>
  181. <section class="map-panel">
  182. <div class="map-tools">
  183. <strong id="map-title">三行业基础设施一张图</strong>
  184. <div class="legend">
  185. <span class="dot-label"><i class="shape facility gas"></i>设施</span>
  186. <span class="dot-label"><i class="shape gas"></i>智能设备</span>
  187. </div>
  188. </div>
  189. <div class="map" id="map"></div>
  190. </section>
  191. </main>
  192. <script>
  193. const summary = {
  194. updatedAt: "2026-09-09T10:00:00+08:00",
  195. facilityCount: 418,
  196. smartDeviceCount: 1126,
  197. industries: [
  198. { code: "GAS", label: "燃气", facilityCount: 126, smartDeviceCount: 402,
  199. facilityTypes: [{ code: "PIPELINE", name: "燃气管线", count: 86 }, { code: "PUMP_STATION", name: "泵站", count: 40 }],
  200. deviceTypes: [{ code: "GAS_SENSOR", name: "燃气浓度计", count: 402 }] },
  201. { code: "WATER_SUPPLY", label: "供水", facilityCount: 158, smartDeviceCount: 368,
  202. facilityTypes: [{ code: "WATER_PLANT", name: "水厂", count: 38 }, { code: "PIPELINE", name: "供水管线", count: 82 }, { code: "PUMP_STATION", name: "泵站", count: 38 }],
  203. deviceTypes: [{ code: "WATER_SUPPLY_SENSOR", name: "供水压力计", count: 368 }] },
  204. { code: "DRAINAGE", label: "排水", facilityCount: 134, smartDeviceCount: 356,
  205. facilityTypes: [{ code: "PIPELINE", name: "排水管线", count: 96 }, { code: "PUMP_STATION", name: "泵站", count: 38 }],
  206. deviceTypes: [{ code: "RAINWATER_SENSOR", name: "雨水流量计", count: 186 }, { code: "SEWAGE_ENVIRONMENT_SENSOR", name: "污水环境监测仪", count: 170 }] }
  207. ]
  208. };
  209. const points = [
  210. { industry: "GAS", kind: "FACILITY", label: "燃气管线 A", x: 14, y: 32 },
  211. { industry: "GAS", kind: "SMART_DEVICE", label: "燃气浓度计-011", x: 26, y: 54 },
  212. { industry: "GAS", kind: "SMART_DEVICE", label: "燃气浓度计-024", x: 42, y: 26 },
  213. { industry: "WATER_SUPPLY", kind: "FACILITY", label: "第一水厂", x: 56, y: 40 },
  214. { industry: "WATER_SUPPLY", kind: "FACILITY", label: "供水管线 B", x: 68, y: 61 },
  215. { industry: "WATER_SUPPLY", kind: "SMART_DEVICE", label: "供水压力计-008", x: 47, y: 70 },
  216. { industry: "DRAINAGE", kind: "FACILITY", label: "排水管线 C", x: 78, y: 28 },
  217. { industry: "DRAINAGE", kind: "SMART_DEVICE", label: "雨水流量计-015", x: 84, y: 48 },
  218. { industry: "DRAINAGE", kind: "SMART_DEVICE", label: "污水环境监测仪-006", x: 31, y: 80 }
  219. ];
  220. const colorKey = code => code === "WATER_SUPPLY" ? "water" : code.toLowerCase();
  221. const maxCount = Math.max(...summary.industries.flatMap(item => [item.facilityCount, item.smartDeviceCount]));
  222. document.getElementById("updated-at").textContent = summary.updatedAt.replace("T", " ");
  223. document.getElementById("facility-total").textContent = summary.facilityCount.toLocaleString();
  224. document.getElementById("device-total").textContent = summary.smartDeviceCount.toLocaleString();
  225. document.getElementById("industries").innerHTML = summary.industries.map(industry => {
  226. const key = colorKey(industry.code);
  227. const types = [...industry.facilityTypes, ...industry.deviceTypes]
  228. .map(item => `<span>${item.name} ${item.count}</span>`).join("");
  229. return `
  230. <section class="industry">
  231. <div class="industry-head">${industry.label}<small>${industry.code}</small></div>
  232. <div class="bar"><span>设施</span><div class="track"><div class="fill facility-${key}" style="width:${industry.facilityCount / maxCount * 100}%"></div></div><span>${industry.facilityCount}</span></div>
  233. <div class="bar"><span>设备</span><div class="track"><div class="fill facility-${key}" style="width:${industry.smartDeviceCount / maxCount * 100}%"></div></div><span>${industry.smartDeviceCount}</span></div>
  234. <div class="types">${types}</div>
  235. </section>`;
  236. }).join("");
  237. function renderMap(industry) {
  238. const selected = points.filter(item => industry === "ALL" || item.industry === industry);
  239. document.getElementById("map-title").textContent = industry === "ALL"
  240. ? "三行业基础设施一张图"
  241. : `${summary.industries.find(item => item.code === industry).label}基础设施一张图`;
  242. document.getElementById("map").innerHTML = selected.map(point => `
  243. <div class="point ${point.kind === "FACILITY" ? "facility " : ""}${colorKey(point.industry)}" style="left:${point.x}%;top:${point.y}%"></div>
  244. <div class="point-label" style="left:${point.x}%;top:${point.y}%">${point.label}</div>`).join("");
  245. }
  246. document.querySelectorAll("[data-industry]").forEach(button => {
  247. button.addEventListener("click", () => {
  248. document.querySelectorAll("[data-industry]").forEach(item => item.classList.remove("active"));
  249. button.classList.add("active");
  250. renderMap(button.dataset.industry);
  251. });
  252. });
  253. renderMap("ALL");
  254. </script>
  255. </body>
  256. </html>