infrastructure-management-information-demo.html 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  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:#172033; --muted:#667085; --line:#d9dee7; --panel:#fff; --page:#f3f5f8;
  9. --accent:#1570ef; --ok:#067647; --warn:#b54708; --idle:#98a2b3; }
  10. * { box-sizing:border-box; }
  11. body { margin:0; min-height:100vh; background:var(--page); color:var(--ink);
  12. font-family:"Microsoft YaHei",Arial,sans-serif; font-size:13px; }
  13. header { display:flex; flex-wrap:wrap; gap:12px; align-items:center; justify-content:space-between;
  14. padding:14px 20px; background:var(--panel); border-bottom:1px solid var(--line); }
  15. h1 { margin:0; font-size:18px; }
  16. .meta { display:flex; flex-wrap:wrap; gap:14px; color:var(--muted); }
  17. .meta b { color:var(--ink); font-weight:600; }
  18. button { height:30px; min-width:76px; padding:0 12px; border:1px solid #d0d5dd; border-radius:4px;
  19. background:var(--panel); color:var(--ink); cursor:pointer; font-size:13px; }
  20. button:hover { border-color:var(--accent); color:var(--accent); }
  21. main { display:grid; grid-template-columns:minmax(360px,440px) minmax(0,1fr); min-height:calc(100vh - 59px); }
  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-height:0; }
  24. h2 { margin:0 0 10px; font-size:14px; font-weight:600; }
  25. h2.mid { margin-top:20px; }
  26. .kpis { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:10px; margin-bottom:6px; }
  27. .kpi { border:1px solid var(--line); border-radius:6px; padding:10px 12px; }
  28. .kpi span { display:block; color:var(--muted); font-size:12px; }
  29. .kpi b { display:block; margin-top:4px; font-size:20px; font-weight:600; }
  30. table { width:100%; border-collapse:collapse; }
  31. th { text-align:left; font-size:12px; color:var(--muted); font-weight:600;
  32. padding:6px 8px; border-bottom:1px solid var(--line); }
  33. th.num, td.num { text-align:right; }
  34. td { padding:8px; border-bottom:1px solid #eef1f5; }
  35. tr.unit { cursor:pointer; }
  36. tr.unit:hover td { background:#f8fafc; }
  37. tr.unit.sel td { background:#eff6ff; }
  38. tr.unit.unmaintained .name { color:var(--warn); }
  39. .name { font-weight:600; }
  40. .code { display:block; color:var(--muted); font-size:11px; font-weight:400; }
  41. .badge { display:inline-block; padding:1px 7px; border-radius:10px; font-size:11px; line-height:18px;
  42. border:1px solid currentColor; }
  43. .b-OWNED { color:var(--ok); }
  44. .b-LEASED { color:var(--accent); }
  45. .b-ENTRUSTED { color:var(--warn); }
  46. .b-UNKNOWN { color:var(--idle); }
  47. .dist { display:grid; gap:6px; }
  48. .dist .row { display:grid; grid-template-columns:92px minmax(0,1fr) 78px; gap:8px; align-items:center; font-size:12px; }
  49. .track { height:8px; background:#eef1f5; }
  50. .fill { height:100%; background:var(--accent); }
  51. .toolbar { display:flex; align-items:center; justify-content:space-between; gap:12px;
  52. padding:12px 18px; background:var(--panel); border-bottom:1px solid var(--line); }
  53. .map { position:relative; background:#eef2f6;
  54. background-image:linear-gradient(#dde3ea 1px,transparent 1px),linear-gradient(90deg,#dde3ea 1px,transparent 1px);
  55. background-size:64px 64px; }
  56. .point { position:absolute; width:12px; height:12px; margin:-6px 0 0 -6px; border-radius:50%;
  57. background:#9fb3c8; border:2px solid #fff; box-shadow:0 1px 3px rgba(16,24,40,.25); }
  58. .point.facility { border-radius:3px; }
  59. .point.in-scope { background:var(--accent); }
  60. .point.unmaintained { background:var(--warn); }
  61. .plabel { position:absolute; padding:1px 5px; border-radius:3px; background:rgba(255,255,255,.93);
  62. border:1px solid var(--line); font-size:11px; white-space:nowrap; color:#344054; }
  63. .caption { padding:10px 18px; background:var(--panel); border-top:1px solid var(--line); color:var(--muted); }
  64. .caption b { color:var(--ink); }
  65. .legend { position:absolute; left:14px; bottom:14px; padding:6px 10px; border-radius:4px;
  66. background:rgba(255,255,255,.92); border:1px solid var(--line); color:var(--muted); font-size:12px; }
  67. @media (max-width:940px) { main { grid-template-columns:1fr; } .map { min-height:380px; } }
  68. </style>
  69. </head>
  70. <body>
  71. <header>
  72. <h1>基础设施 / 管理信息</h1>
  73. <div class="meta">
  74. <span>单位 <b id="orgCount">0</b></span>
  75. <span>设施 <b id="facilityCount">0</b></span>
  76. <span>智能设备 <b id="equipmentCount">0</b></span>
  77. <span>未维护 <b id="unmaintainedCount">0</b></span>
  78. <span>更新 <b id="updatedAt">--</b></span>
  79. </div>
  80. <button id="refresh" type="button">手动刷新</button>
  81. </header>
  82. <main>
  83. <aside>
  84. <h2>管理单位</h2>
  85. <table>
  86. <thead>
  87. <tr><th>单位</th><th class="num">设施</th><th class="num">智能设备</th></tr>
  88. </thead>
  89. <tbody id="units"></tbody>
  90. </table>
  91. <h2 class="mid">权属分布</h2>
  92. <div class="dist" id="ownership"></div>
  93. </aside>
  94. <section>
  95. <div class="toolbar">
  96. <strong id="mapTitle">中央一张图</strong>
  97. <span class="meta">选择单位可筛选地图点位</span>
  98. </div>
  99. <div class="map" id="map">
  100. <div class="legend">方块=设施 · 圆点=智能设备 · 蓝=当前单位 · 橙=未维护</div>
  101. </div>
  102. <div class="caption" id="caption">未选择单位,展示全部管理权属点位</div>
  103. </section>
  104. </main>
  105. <script>
  106. const fixture = {
  107. updatedAt: "2026-09-02T09:59:50+08:00",
  108. units: [
  109. { code: "ORG-WS-A", name: "供水一公司", industry: "WATER_SUPPLY", unmaintained: false },
  110. { code: "ORG-GAS-A", name: "燃气公司", industry: "GAS", unmaintained: false },
  111. { code: "ORG-WS-B", name: "供水二公司", industry: "WATER_SUPPLY", unmaintained: false },
  112. { code: "UNMAINTAINED", name: "未维护", industry: "DRAINAGE", unmaintained: true }
  113. ],
  114. objects: [
  115. { code: "F-WS-001", kind: "FACILITY", unit: "ORG-WS-A", ownership: "OWNED", x: 22, y: 30 },
  116. { code: "F-WS-002", kind: "FACILITY", unit: "ORG-WS-A", ownership: "OWNED", x: 34, y: 44 },
  117. { code: "E-WS-001", kind: "SMART_DEVICE", unit: "ORG-WS-A", ownership: "LEASED", x: 43, y: 25 },
  118. { code: "F-WS-003", kind: "FACILITY", unit: "ORG-WS-B", ownership: "ENTRUSTED", x: 58, y: 36 },
  119. { code: "E-WS-002", kind: "SMART_DEVICE", unit: "ORG-WS-B", ownership: "UNKNOWN", x: 66, y: 52 },
  120. { code: "F-GAS-001", kind: "FACILITY", unit: "ORG-GAS-A", ownership: "OWNED", x: 30, y: 66 },
  121. { code: "E-GAS-001", kind: "SMART_DEVICE", unit: "ORG-GAS-A", ownership: "OWNED", x: 44, y: 74 },
  122. { code: "F-DR-001", kind: "FACILITY", unit: "UNMAINTAINED", ownership: "OWNED", x: 78, y: 30 },
  123. { code: "E-DR-001", kind: "SMART_DEVICE", unit: "UNMAINTAINED", ownership: "UNKNOWN", x: 86, y: 58 }
  124. ]
  125. };
  126. const OWNERSHIP_TYPES = ["OWNED", "LEASED", "ENTRUSTED", "UNKNOWN"];
  127. let state = { unit: null, lastRefresh: null };
  128. function objectsFor(unitCode) {
  129. return unitCode ? fixture.objects.filter(o => o.unit === unitCode) : fixture.objects.slice();
  130. }
  131. function countsFor(unitCode) {
  132. const list = objectsFor(unitCode);
  133. return {
  134. facility: list.filter(o => o.kind === "FACILITY").length,
  135. equipment: list.filter(o => o.kind === "SMART_DEVICE").length
  136. };
  137. }
  138. function ownershipDistribution(unitCode) {
  139. return OWNERSHIP_TYPES.map(type => {
  140. const list = objectsFor(unitCode).filter(o => o.ownership === type);
  141. return {
  142. type: type,
  143. facility: list.filter(o => o.kind === "FACILITY").length,
  144. equipment: list.filter(o => o.kind === "SMART_DEVICE").length,
  145. total: list.length
  146. };
  147. }).filter(item => item.total > 0)
  148. .sort((a, b) => b.total - a.total || (a.type < b.type ? -1 : 1));
  149. }
  150. function renderHeader() {
  151. const counts = countsFor(state.unit);
  152. document.getElementById("orgCount").textContent = state.unit ? 1 : fixture.units.length;
  153. document.getElementById("facilityCount").textContent = counts.facility;
  154. document.getElementById("equipmentCount").textContent = counts.equipment;
  155. document.getElementById("unmaintainedCount").textContent = objectsFor("UNMAINTAINED").length;
  156. document.getElementById("updatedAt").textContent = state.lastRefresh || fixture.updatedAt;
  157. }
  158. function renderUnits() {
  159. const rows = fixture.units.map(unit => {
  160. const counts = countsFor(unit.code);
  161. const selected = state.unit === unit.code ? " sel" : "";
  162. const flag = unit.unmaintained ? " unmaintained" : "";
  163. return `<tr class="unit${selected}${flag}" data-code="${unit.code}">
  164. <td><span class="name">${unit.name}</span><span class="code">${unit.code} · ${unit.industry}</span></td>
  165. <td class="num">${counts.facility}</td>
  166. <td class="num">${counts.equipment}</td>
  167. </tr>`;
  168. }).join("");
  169. const allSelected = state.unit === null ? " sel" : "";
  170. document.getElementById("units").innerHTML =
  171. `<tr class="unit${allSelected}" data-code=""><td><span class="name">全部单位</span><span class="code">ALL</span></td>
  172. <td class="num">${fixture.objects.filter(o => o.kind === "FACILITY").length}</td>
  173. <td class="num">${fixture.objects.filter(o => o.kind === "SMART_DEVICE").length}</td></tr>` + rows;
  174. }
  175. function renderOwnership() {
  176. const distribution = ownershipDistribution(state.unit);
  177. const max = distribution.length ? distribution[0].total : 1;
  178. document.getElementById("ownership").innerHTML = distribution.map(item =>
  179. `<div class="row">
  180. <span class="badge b-${item.type}">${item.type}</span>
  181. <div class="track"><div class="fill" style="width:${item.total / max * 100}%"></div></div>
  182. <span>${item.facility} / ${item.equipment}</span>
  183. </div>`).join("") || '<span class="code">无数据</span>';
  184. }
  185. function renderMap() {
  186. const map = document.getElementById("map");
  187. map.querySelectorAll(".point,.plabel").forEach(node => node.remove());
  188. fixture.objects
  189. .filter(obj => state.unit === null || obj.unit === state.unit)
  190. .forEach(obj => {
  191. const point = document.createElement("div");
  192. point.className = "point " + (obj.kind === "FACILITY" ? "facility " : "")
  193. + (obj.unit === "UNMAINTAINED" ? "unmaintained" : "in-scope");
  194. point.style.left = obj.x + "%";
  195. point.style.top = obj.y + "%";
  196. point.title = obj.code + " · " + obj.unit + " · " + obj.ownership;
  197. map.appendChild(point);
  198. const label = document.createElement("div");
  199. label.className = "plabel";
  200. label.style.left = "calc(" + obj.x + "% + 10px)";
  201. label.style.top = "calc(" + obj.y + "% - 9px)";
  202. label.textContent = obj.code;
  203. map.appendChild(label);
  204. });
  205. const unit = fixture.units.find(u => u.code === state.unit);
  206. document.getElementById("mapTitle").textContent = "中央一张图 · " + (unit ? unit.name : "全部单位");
  207. document.getElementById("caption").innerHTML = unit
  208. ? "地图筛选 <b>" + unit.code + "</b> · 设施 <b>" + countsFor(unit.code).facility
  209. + "</b> · 智能设备 <b>" + countsFor(unit.code).equipment + "</b>"
  210. : "未选择单位,展示全部管理权属点位 · 橙色为未维护桶";
  211. }
  212. function render() {
  213. renderHeader();
  214. renderUnits();
  215. renderOwnership();
  216. renderMap();
  217. }
  218. document.getElementById("units").addEventListener("click", event => {
  219. const row = event.target.closest("tr.unit");
  220. if (!row) return;
  221. const code = row.dataset.code;
  222. state.unit = code === "" || code === state.unit ? null : code;
  223. render();
  224. });
  225. document.getElementById("refresh").addEventListener("click", () => {
  226. state.lastRefresh = new Date().toISOString();
  227. state.unit = null;
  228. render();
  229. });
  230. render();
  231. </script>
  232. </body>
  233. </html>