| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275 |
- <!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:#16324f; --muted:#64748b; --line:#d9e1ea; --panel:#fff; --page:#edf2f7; --blue:#1470ef; }
- * { 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; justify-content:space-between; align-items:center; padding:14px 20px; background:var(--panel); border-bottom:1px solid var(--line); }
- h1 { margin:0; font-size:18px; }
- .meta { display:flex; gap:14px; color:var(--muted); flex-wrap:wrap; }
- .meta b { color:var(--ink); font-weight:600; }
- button { cursor:pointer; border:1px solid #b9c8d5; background:var(--panel); padding:7px 12px; border-radius:4px; }
- button:hover { border-color:var(--blue); color:var(--blue); }
- select, input[type=text] { border:1px solid #b9c8d5; border-radius:4px; padding:6px 8px; background:#fff; }
- main { display:grid; grid-template-columns:minmax(360px,440px) minmax(0,1fr); min-height:calc(100vh - 57px); }
- 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-width:0; }
- h2 { margin:0 0 10px; font-size:14px; }
- .filters { display:grid; grid-template-columns:1fr 1fr; gap:8px; margin-bottom:12px; }
- .filters label { display:flex; flex-direction:column; gap:4px; color:var(--muted); font-size:12px; }
- table { width:100%; border-collapse:collapse; font-size:12px; }
- th,td { text-align:left; padding:6px 7px; border-bottom:1px solid #edf1f5; }
- th { color:var(--muted); font-weight:600; }
- .dot { display:inline-block; width:10px; height:10px; border-radius:50%; margin-right:5px; vertical-align:-1px; }
- .s-ALARM { background:#d0342c; } .s-WARNING { background:#f0a500; }
- .s-WORK_ORDER { background:#1470ef; } .s-NORMAL { background:#0f8a4d; }
- .s-OFFLINE { background:#8a94a0; } .s-EMPTY { background:#b8c0ca; }
- .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); }
- .map { position:relative; background:#e2e9f0; background-image:linear-gradient(#d3dce5 1px,transparent 1px),linear-gradient(90deg,#d3dce5 1px,transparent 1px); background-size:56px 56px; }
- .marker { position:absolute; width:16px; height:16px; margin:-8px 0 0 -8px; border-radius:50% 50% 50% 2px; transform:rotate(-45deg); border:2px solid #fff; box-shadow:0 1px 4px rgba(16,24,40,.35); cursor:pointer; }
- .marker.s-EMPTY { border-radius:50%; transform:none; }
- .label { position:absolute; font-size:11px; transform:translate(10px,-6px); background:rgba(255,255,255,.92); padding:1px 5px; border-radius:3px; border:1px solid var(--line); pointer-events:none; font-size:11px; }
- .caption { padding:10px 18px; background:var(--panel); border-top:1px solid var(--line); color:var(--muted); }
- .panel { margin-top:14px; border:1px solid var(--line); border-radius:5px; padding:10px 12px; min-height:130px; }
- .panel h3 { margin:0 0 8px; font-size:13px; }
- .panel ul { margin:0; padding-left:18px; }
- .panel li { margin:2px 0; }
- .legend { display:flex; flex-wrap:wrap; gap:12px; 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="total">0</b></span><span>监测点 <b id="points">0</b></span><span>设备 <b id="devices">0</b></span><span>筛选范围 <b id="scope">全部</b></span><span>更新 <b id="updated">--</b></span></div>
- </header>
- <main>
- <aside>
- <h2>筛选(即时作用于中央一张图)</h2>
- <div class="filters">
- <label>行业
- <select id="fIndustry">
- <option value="">全部行业</option>
- <option value="GAS">燃气</option>
- <option value="WATER_SUPPLY">供水</option>
- <option value="DRAINAGE">排水</option>
- </select>
- </label>
- <label>设备类型
- <select id="fType">
- <option value="">全部类型</option>
- <option value="GAS_SENSOR">燃气浓度计</option>
- <option value="WATER_SUPPLY_SENSOR">供水压力计</option>
- <option value="SEWAGE_ENV">污水环境监测</option>
- </select>
- </label>
- <label>点位状态
- <select id="fStatus">
- <option value="">全部状态</option>
- <option value="ALARM">报警</option>
- <option value="WARNING">预警</option>
- <option value="WORK_ORDER">工单处置</option>
- <option value="NORMAL">正常</option>
- <option value="OFFLINE">离线</option>
- <option value="EMPTY">无设备</option>
- </select>
- </label>
- <label>区域
- <input type="text" id="fRegion" placeholder="如:城区A">
- </label>
- </div>
- <div class="toolbar" style="padding:0 0 10px; border-bottom:none">
- <button id="apply" type="button">应用筛选</button>
- <button id="reset" type="button">重置</button>
- <button id="refresh" type="button">手动刷新</button>
- <button id="toggle" type="button">GAS-003 在线/离线</button>
- </div>
- <h2>六态分布</h2>
- <table><thead><tr><th>状态</th><th>标记数</th></tr></thead>
- <tbody id="statusRows"></tbody></table>
- <h2 style="margin-top:14px">区域分布</h2>
- <table><thead><tr><th>区域</th><th>点</th><th>设备</th><th>异常(报/警/单)</th></tr></thead>
- <tbody id="regionRows"></tbody></table>
- <h2 style="margin-top:14px">行业分布</h2>
- <table><thead><tr><th>行业</th><th>点</th><th>设备</th></tr></thead>
- <tbody id="industryRows"></tbody></table>
- <h2 style="margin-top:14px">类型分布</h2>
- <table><thead><tr><th>类型</th><th>点</th><th>设备</th></tr></thead>
- <tbody id="typeRows"></tbody></table>
- <div class="panel" id="detail"><h3>详情</h3><p style="color:var(--muted)">点击地图标记查看监测点或未关联设备详情。</p></div>
- </aside>
- <section>
- <div class="toolbar">
- <strong>中央一张图</strong>
- <div class="legend">
- <span><i class="dot s-ALARM"></i>报警</span>
- <span><i class="dot s-WARNING"></i>预警</span>
- <span><i class="dot s-WORK_ORDER"></i>工单</span>
- <span><i class="dot s-NORMAL"></i>正常</span>
- <span><i class="dot s-OFFLINE"></i>离线</span>
- <span><i class="dot s-EMPTY"></i>无设备</span>
- </div>
- </div>
- <div class="map" id="map"></div>
- <div class="caption" id="caption">全部监测点与独立设备</div>
- </section>
- </main>
- <script>
- const STATUS_ORDER = ["ALARM", "WARNING", "WORK_ORDER", "NORMAL", "OFFLINE", "EMPTY"];
- const INDUSTRY_NAME = { GAS: "燃气", WATER_SUPPLY: "供水", DRAINAGE: "排水" };
- const TYPE_NAME = { GAS_SENSOR: "燃气浓度计", WATER_SUPPLY_SENSOR: "供水压力计", SEWAGE_ENV: "污水环境监测" };
- const fixture = () => ({
- points: [
- { code: "P-GAS-1", name: "燃气监测点-1", region: "城区A", industry: "GAS", type: "GAS_SENSOR", lon: 110.130, lat: 28.440,
- devices: [
- { code: "GAS-001", alarm: false, warning: false, workOrder: false, online: true, reasons: [] },
- { code: "GAS-002", alarm: true, warning: false, workOrder: false, online: true, reasons: ["浓度超限"] } ] },
- { code: "P-GAS-2", name: "燃气监测点-2", region: "城区A", industry: "GAS", type: "GAS_SENSOR", lon: 110.131, lat: 28.441,
- devices: [ { code: "GAS-003", alarm: false, warning: false, workOrder: false, online: false, reasons: [] } ] },
- { code: "P-WS-1", name: "供水监测点-1", region: "城区B", industry: "WATER_SUPPLY", type: "WATER_SUPPLY_SENSOR", lon: 110.150, lat: 28.460,
- devices: [ { code: "WS-001", alarm: false, warning: false, workOrder: true, online: true, reasons: [] } ] },
- { code: "P-WS-2", name: "供水监测点-2", region: "城区B", industry: "WATER_SUPPLY", type: "WATER_SUPPLY_SENSOR", lon: 110.151, lat: 28.461, devices: [] },
- { code: "P-DR-1", name: "污水监测点-1", region: "城区C", industry: "DRAINAGE", type: "SEWAGE_ENV", lon: 110.170, lat: 28.480,
- devices: [ { code: "DRAIN-001", alarm: false, warning: true, workOrder: false, online: true, reasons: ["浓度预警"] } ] }
- ],
- standalone: [
- { code: "EQ-FREE-001", name: "井盖外燃气计", region: "城区A", industry: "GAS", type: "GAS_SENSOR", lon: 110.132, lat: 28.442,
- alarm: false, warning: false, workOrder: false, online: true, reasons: [] },
- { code: "EQ-FREE-002", name: "污水环境监测-独立", region: "城区B", industry: "DRAINAGE", type: "SEWAGE_ENV", lon: 110.152, lat: 28.462,
- alarm: false, warning: true, workOrder: false, online: true, reasons: ["设备预警"] },
- { code: "EQ-FREE-003", name: "压力计-独立", region: "城区C", industry: "WATER_SUPPLY", type: "WATER_SUPPLY_SENSOR", lon: 110.171, lat: 28.481,
- alarm: false, warning: false, workOrder: false, online: false, reasons: ["离线"] }
- ]
- });
- const deviceStatus = d => d.alarm ? "ALARM" : d.warning ? "WARNING" : d.workOrder ? "WORK_ORDER" : d.online ? "NORMAL" : "OFFLINE";
- const pointStatus = p => p.devices.length ? STATUS_ORDER[Math.min(...p.devices.map(d => STATUS_ORDER.indexOf(deviceStatus(d))))] : "EMPTY";
- let data = fixture();
- const filters = { industry: "", deviceType: "", pointStatus: "", region: "" };
- function inScope(marker) {
- if (filters.industry && marker.industry !== filters.industry) return false;
- if (filters.deviceType && marker.type !== filters.deviceType) return false;
- if (filters.pointStatus && marker.status !== filters.pointStatus) return false;
- if (filters.region && marker.region !== filters.region) return false;
- return true;
- }
- function buildMarkers() {
- return data.points.filter(inScope).map(p => ({ kind: "point", ...p, status: pointStatus(p) }))
- .concat(data.standalone.filter(inScope).map(d => ({ kind: "device", ...d, status: deviceStatus(d) })));
- }
- const statusCounts = markers => {
- const counts = {}; STATUS_ORDER.forEach(s => counts[s] = 0);
- markers.forEach(m => counts[m.status]++);
- return counts;
- };
- function render() {
- const markers = buildMarkers();
- const counts = statusCounts(markers);
- const byRegion = {}, byIndustry = {}, byType = {};
- markers.forEach(m => {
- const acc = (b, k) => (b[k] = b[k] || { point: 0, device: 0, status: statusCounts([]) });
- const a = acc(byRegion, m.region); a.point += m.kind === "point" ? 1 : 0; a.device += m.kind === "point" ? m.devices.length : 1; a.status[m.status]++;
- const b = acc(byIndustry, m.industry); b.point += m.kind === "point" ? 1 : 0; b.device += m.kind === "point" ? m.devices.length : 1; b.status[m.status]++;
- const c = acc(byType, m.type); c.point += m.kind === "point" ? 1 : 0; c.device += m.kind === "point" ? m.devices.length : 1; c.status[m.status]++;
- });
- document.getElementById("total").textContent = markers.length;
- document.getElementById("points").textContent = markers.filter(m => m.kind === "point").length;
- document.getElementById("devices").textContent = markers.reduce((n, m) => n + (m.kind === "point" ? m.devices.length : 1), 0);
- document.getElementById("scope").textContent = [filters.industry || "", filters.deviceType || "", filters.pointStatus || "", filters.region || ""].filter(Boolean).join(" / ") || "全部";
- document.getElementById("updated").textContent = new Date().toLocaleTimeString("zh-CN");
- document.getElementById("statusRows").innerHTML = STATUS_ORDER.map(s =>
- `<tr><td><i class="dot s-${s}"></i>${s}</td><td>${counts[s]}</td></tr>`).join("");
- document.getElementById("regionRows").innerHTML = Object.keys(byRegion).sort().map(r => {
- const x = byRegion[r];
- return `<tr><td>${r}</td><td>${x.point}</td><td>${x.device}</td><td>${x.status.ALARM + x.status.WARNING + x.status.WORK_ORDER}</td></tr>`;
- }).join("");
- document.getElementById("industryRows").innerHTML = Object.keys(byIndustry).sort().map(k => {
- const x = byIndustry[k];
- return `<tr><td>${INDUSTRY_NAME[k] || k}</td><td>${x.point}</td><td>${x.device}</td></tr>`;
- }).join("");
- document.getElementById("typeRows").innerHTML = Object.entries(byType)
- .sort((a, b) => b[1].device - a[1].device)
- .map(([k, x]) => `<tr><td>${TYPE_NAME[k] || k}</td><td>${x.point}</td><td>${x.device}</td></tr>`).join("");
- const map = document.getElementById("map");
- map.innerHTML = "";
- const lons = markers.map(m => m.lon), lats = markers.map(m => m.lat);
- const minLon = Math.min(...lons), maxLon = Math.max(...lons), minLat = Math.min(...lats), maxLat = Math.max(...lats);
- const xScale = (lon) => 12 + (lon - minLon) / Math.max(0.001, maxLon - minLon) * 76;
- const yScale = (lat) => 12 + (maxLat - lat) / Math.max(0.001, maxLat - minLat) * 76;
- markers.forEach(m => {
- const el = document.createElement("div");
- el.className = "marker s-" + m.status;
- el.style.left = xScale(m.lon) + "%"; el.style.top = yScale(m.lat) + "%";
- el.title = (m.kind === "point" ? m.name : m.name) + " / " + m.status;
- el.addEventListener("click", () => showDetail(m));
- map.appendChild(el);
- const l = document.createElement("div");
- l.className = "label"; l.style.left = xScale(m.lon) + "%"; l.style.top = yScale(m.lat) + "%";
- l.textContent = m.code + " · " + m.status;
- map.appendChild(l);
- });
- document.getElementById("caption").textContent =
- `共 ${markers.length} 个标记 · ` + STATUS_ORDER.map(s => `${s} ${counts[s]}`).join(" · ");
- }
- function showDetail(m) {
- const box = document.getElementById("detail");
- if (m.kind === "point") {
- box.innerHTML = `<h3>监测点 ${m.code}</h3><ul>
- <li>名称:${m.name} 区域:${m.region} 行业:${INDUSTRY_NAME[m.industry]}</li>
- <li>类型:${TYPE_NAME[m.type] || m.type} 状态:${m.status}</li>
- <li>关联设备(${m.devices.length}):</li>
- ${m.devices.map(d => `<li>${d.code} — ${deviceStatus(d)}${d.reasons.length ? "(" + d.reasons.join("、") + ")" : ""}</li>`).join("")}
- </ul>`;
- } else {
- box.innerHTML = `<h3>未关联设备 ${m.code}</h3><ul>
- <li>名称:${m.name} 区域:${m.region} 行业:${INDUSTRY_NAME[m.industry]}</li>
- <li>类型:${TYPE_NAME[m.type] || m.type} 状态:${m.status}</li>
- ${m.reasons.length ? `<li>原因:${m.reasons.join("、")}</li>` : ""}
- </ul>`;
- }
- }
- document.getElementById("apply").addEventListener("click", () => {
- filters.industry = document.getElementById("fIndustry").value;
- filters.deviceType = document.getElementById("fType").value;
- filters.pointStatus = document.getElementById("fStatus").value;
- filters.region = document.getElementById("fRegion").value.trim();
- render();
- });
- document.getElementById("reset").addEventListener("click", () => {
- Object.keys(filters).forEach(k => filters[k] = "");
- document.getElementById("fIndustry").value = "";
- document.getElementById("fType").value = "";
- document.getElementById("fStatus").value = "";
- document.getElementById("fRegion").value = "";
- render();
- });
- document.getElementById("refresh").addEventListener("click", render);
- document.getElementById("toggle").addEventListener("click", () => {
- const d = data.points.find(p => p.code === "P-GAS-2").devices[0];
- d.online = !d.online;
- render();
- });
- render();
- </script>
- </body>
- </html>
|