| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248 |
- <!doctype html>
- <html lang="zh-CN">
- <head>
- <meta charset="utf-8">
- <meta name="viewport" content="width=device-width, initial-scale=1">
- <title>基础设施 / 管理信息</title>
- <style>
- :root { --ink:#172033; --muted:#667085; --line:#d9dee7; --panel:#fff; --page:#f3f5f8;
- --accent:#1570ef; --ok:#067647; --warn:#b54708; --idle:#98a2b3; }
- * { box-sizing:border-box; }
- body { margin:0; min-height:100vh; background:var(--page); color:var(--ink);
- font-family:"Microsoft YaHei",Arial,sans-serif; font-size:13px; }
- header { display:flex; flex-wrap:wrap; gap:12px; align-items:center; justify-content:space-between;
- padding:14px 20px; background:var(--panel); border-bottom:1px solid var(--line); }
- h1 { margin:0; font-size:18px; }
- .meta { display:flex; flex-wrap:wrap; gap:14px; color:var(--muted); }
- .meta b { color:var(--ink); font-weight:600; }
- button { height:30px; min-width:76px; padding:0 12px; border:1px solid #d0d5dd; border-radius:4px;
- background:var(--panel); color:var(--ink); cursor:pointer; font-size:13px; }
- button:hover { border-color:var(--accent); color:var(--accent); }
- main { display:grid; grid-template-columns:minmax(360px,440px) minmax(0,1fr); min-height:calc(100vh - 59px); }
- aside { padding:16px 18px; background:var(--panel); border-right:1px solid var(--line); overflow:auto; }
- section { display:grid; grid-template-rows:auto minmax(0,1fr) auto; min-height:0; }
- h2 { margin:0 0 10px; font-size:14px; font-weight:600; }
- h2.mid { margin-top:20px; }
- .kpis { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:10px; margin-bottom:6px; }
- .kpi { border:1px solid var(--line); border-radius:6px; padding:10px 12px; }
- .kpi span { display:block; color:var(--muted); font-size:12px; }
- .kpi b { display:block; margin-top:4px; font-size:20px; font-weight:600; }
- table { width:100%; border-collapse:collapse; }
- th { text-align:left; font-size:12px; color:var(--muted); font-weight:600;
- padding:6px 8px; border-bottom:1px solid var(--line); }
- th.num, td.num { text-align:right; }
- td { padding:8px; border-bottom:1px solid #eef1f5; }
- tr.unit { cursor:pointer; }
- tr.unit:hover td { background:#f8fafc; }
- tr.unit.sel td { background:#eff6ff; }
- tr.unit.unmaintained .name { color:var(--warn); }
- .name { font-weight:600; }
- .code { display:block; color:var(--muted); font-size:11px; font-weight:400; }
- .badge { display:inline-block; padding:1px 7px; border-radius:10px; font-size:11px; line-height:18px;
- border:1px solid currentColor; }
- .b-OWNED { color:var(--ok); }
- .b-LEASED { color:var(--accent); }
- .b-ENTRUSTED { color:var(--warn); }
- .b-UNKNOWN { color:var(--idle); }
- .dist { display:grid; gap:6px; }
- .dist .row { display:grid; grid-template-columns:92px minmax(0,1fr) 78px; gap:8px; align-items:center; font-size:12px; }
- .track { height:8px; background:#eef1f5; }
- .fill { height:100%; background:var(--accent); }
- .toolbar { display:flex; align-items:center; justify-content:space-between; gap:12px;
- padding:12px 18px; background:var(--panel); border-bottom:1px solid var(--line); }
- .map { position:relative; background:#eef2f6;
- background-image:linear-gradient(#dde3ea 1px,transparent 1px),linear-gradient(90deg,#dde3ea 1px,transparent 1px);
- background-size:64px 64px; }
- .point { position:absolute; width:12px; height:12px; margin:-6px 0 0 -6px; border-radius:50%;
- background:#9fb3c8; border:2px solid #fff; box-shadow:0 1px 3px rgba(16,24,40,.25); }
- .point.facility { border-radius:3px; }
- .point.in-scope { background:var(--accent); }
- .point.unmaintained { background:var(--warn); }
- .plabel { position:absolute; padding:1px 5px; border-radius:3px; background:rgba(255,255,255,.93);
- border:1px solid var(--line); font-size:11px; white-space:nowrap; color:#344054; }
- .caption { padding:10px 18px; background:var(--panel); border-top:1px solid var(--line); color:var(--muted); }
- .caption b { color:var(--ink); }
- .legend { position:absolute; left:14px; bottom:14px; padding:6px 10px; border-radius:4px;
- background:rgba(255,255,255,.92); border:1px solid var(--line); color:var(--muted); font-size:12px; }
- @media (max-width:940px) { main { grid-template-columns:1fr; } .map { min-height:380px; } }
- </style>
- </head>
- <body>
- <header>
- <h1>基础设施 / 管理信息</h1>
- <div class="meta">
- <span>单位 <b id="orgCount">0</b></span>
- <span>设施 <b id="facilityCount">0</b></span>
- <span>智能设备 <b id="equipmentCount">0</b></span>
- <span>未维护 <b id="unmaintainedCount">0</b></span>
- <span>更新 <b id="updatedAt">--</b></span>
- </div>
- <button id="refresh" type="button">手动刷新</button>
- </header>
- <main>
- <aside>
- <h2>管理单位</h2>
- <table>
- <thead>
- <tr><th>单位</th><th class="num">设施</th><th class="num">智能设备</th></tr>
- </thead>
- <tbody id="units"></tbody>
- </table>
- <h2 class="mid">权属分布</h2>
- <div class="dist" id="ownership"></div>
- </aside>
- <section>
- <div class="toolbar">
- <strong id="mapTitle">中央一张图</strong>
- <span class="meta">选择单位可筛选地图点位</span>
- </div>
- <div class="map" id="map">
- <div class="legend">方块=设施 · 圆点=智能设备 · 蓝=当前单位 · 橙=未维护</div>
- </div>
- <div class="caption" id="caption">未选择单位,展示全部管理权属点位</div>
- </section>
- </main>
- <script>
- const fixture = {
- updatedAt: "2026-09-02T09:59:50+08:00",
- units: [
- { code: "ORG-WS-A", name: "供水一公司", industry: "WATER_SUPPLY", unmaintained: false },
- { code: "ORG-GAS-A", name: "燃气公司", industry: "GAS", unmaintained: false },
- { code: "ORG-WS-B", name: "供水二公司", industry: "WATER_SUPPLY", unmaintained: false },
- { code: "UNMAINTAINED", name: "未维护", industry: "DRAINAGE", unmaintained: true }
- ],
- objects: [
- { code: "F-WS-001", kind: "FACILITY", unit: "ORG-WS-A", ownership: "OWNED", x: 22, y: 30 },
- { code: "F-WS-002", kind: "FACILITY", unit: "ORG-WS-A", ownership: "OWNED", x: 34, y: 44 },
- { code: "E-WS-001", kind: "SMART_DEVICE", unit: "ORG-WS-A", ownership: "LEASED", x: 43, y: 25 },
- { code: "F-WS-003", kind: "FACILITY", unit: "ORG-WS-B", ownership: "ENTRUSTED", x: 58, y: 36 },
- { code: "E-WS-002", kind: "SMART_DEVICE", unit: "ORG-WS-B", ownership: "UNKNOWN", x: 66, y: 52 },
- { code: "F-GAS-001", kind: "FACILITY", unit: "ORG-GAS-A", ownership: "OWNED", x: 30, y: 66 },
- { code: "E-GAS-001", kind: "SMART_DEVICE", unit: "ORG-GAS-A", ownership: "OWNED", x: 44, y: 74 },
- { code: "F-DR-001", kind: "FACILITY", unit: "UNMAINTAINED", ownership: "OWNED", x: 78, y: 30 },
- { code: "E-DR-001", kind: "SMART_DEVICE", unit: "UNMAINTAINED", ownership: "UNKNOWN", x: 86, y: 58 }
- ]
- };
- const OWNERSHIP_TYPES = ["OWNED", "LEASED", "ENTRUSTED", "UNKNOWN"];
- let state = { unit: null, lastRefresh: null };
- function objectsFor(unitCode) {
- return unitCode ? fixture.objects.filter(o => o.unit === unitCode) : fixture.objects.slice();
- }
- function countsFor(unitCode) {
- const list = objectsFor(unitCode);
- return {
- facility: list.filter(o => o.kind === "FACILITY").length,
- equipment: list.filter(o => o.kind === "SMART_DEVICE").length
- };
- }
- function ownershipDistribution(unitCode) {
- return OWNERSHIP_TYPES.map(type => {
- const list = objectsFor(unitCode).filter(o => o.ownership === type);
- return {
- type: type,
- facility: list.filter(o => o.kind === "FACILITY").length,
- equipment: list.filter(o => o.kind === "SMART_DEVICE").length,
- total: list.length
- };
- }).filter(item => item.total > 0)
- .sort((a, b) => b.total - a.total || (a.type < b.type ? -1 : 1));
- }
- function renderHeader() {
- const counts = countsFor(state.unit);
- document.getElementById("orgCount").textContent = state.unit ? 1 : fixture.units.length;
- document.getElementById("facilityCount").textContent = counts.facility;
- document.getElementById("equipmentCount").textContent = counts.equipment;
- document.getElementById("unmaintainedCount").textContent = objectsFor("UNMAINTAINED").length;
- document.getElementById("updatedAt").textContent = state.lastRefresh || fixture.updatedAt;
- }
- function renderUnits() {
- const rows = fixture.units.map(unit => {
- const counts = countsFor(unit.code);
- const selected = state.unit === unit.code ? " sel" : "";
- const flag = unit.unmaintained ? " unmaintained" : "";
- return `<tr class="unit${selected}${flag}" data-code="${unit.code}">
- <td><span class="name">${unit.name}</span><span class="code">${unit.code} · ${unit.industry}</span></td>
- <td class="num">${counts.facility}</td>
- <td class="num">${counts.equipment}</td>
- </tr>`;
- }).join("");
- const allSelected = state.unit === null ? " sel" : "";
- document.getElementById("units").innerHTML =
- `<tr class="unit${allSelected}" data-code=""><td><span class="name">全部单位</span><span class="code">ALL</span></td>
- <td class="num">${fixture.objects.filter(o => o.kind === "FACILITY").length}</td>
- <td class="num">${fixture.objects.filter(o => o.kind === "SMART_DEVICE").length}</td></tr>` + rows;
- }
- function renderOwnership() {
- const distribution = ownershipDistribution(state.unit);
- const max = distribution.length ? distribution[0].total : 1;
- document.getElementById("ownership").innerHTML = distribution.map(item =>
- `<div class="row">
- <span class="badge b-${item.type}">${item.type}</span>
- <div class="track"><div class="fill" style="width:${item.total / max * 100}%"></div></div>
- <span>${item.facility} / ${item.equipment}</span>
- </div>`).join("") || '<span class="code">无数据</span>';
- }
- function renderMap() {
- const map = document.getElementById("map");
- map.querySelectorAll(".point,.plabel").forEach(node => node.remove());
- fixture.objects
- .filter(obj => state.unit === null || obj.unit === state.unit)
- .forEach(obj => {
- const point = document.createElement("div");
- point.className = "point " + (obj.kind === "FACILITY" ? "facility " : "")
- + (obj.unit === "UNMAINTAINED" ? "unmaintained" : "in-scope");
- point.style.left = obj.x + "%";
- point.style.top = obj.y + "%";
- point.title = obj.code + " · " + obj.unit + " · " + obj.ownership;
- map.appendChild(point);
- const label = document.createElement("div");
- label.className = "plabel";
- label.style.left = "calc(" + obj.x + "% + 10px)";
- label.style.top = "calc(" + obj.y + "% - 9px)";
- label.textContent = obj.code;
- map.appendChild(label);
- });
- const unit = fixture.units.find(u => u.code === state.unit);
- document.getElementById("mapTitle").textContent = "中央一张图 · " + (unit ? unit.name : "全部单位");
- document.getElementById("caption").innerHTML = unit
- ? "地图筛选 <b>" + unit.code + "</b> · 设施 <b>" + countsFor(unit.code).facility
- + "</b> · 智能设备 <b>" + countsFor(unit.code).equipment + "</b>"
- : "未选择单位,展示全部管理权属点位 · 橙色为未维护桶";
- }
- function render() {
- renderHeader();
- renderUnits();
- renderOwnership();
- renderMap();
- }
- document.getElementById("units").addEventListener("click", event => {
- const row = event.target.closest("tr.unit");
- if (!row) return;
- const code = row.dataset.code;
- state.unit = code === "" || code === state.unit ? null : code;
- render();
- });
- document.getElementById("refresh").addEventListener("click", () => {
- state.lastRefresh = new Date().toISOString();
- state.unit = null;
- render();
- });
- render();
- </script>
- </body>
- </html>
|