map-snapshot-demo.html 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  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>Cockpit Map Snapshot Demo</title>
  7. <style>
  8. body { margin:0; min-height:100vh; background:#08131f; color:#e8f1f8; font:15px/1.5 "Microsoft YaHei",sans-serif; }
  9. main { width:min(1220px,calc(100vw - 32px)); margin:28px auto; display:grid; gap:16px; }
  10. header, section { background:#0e1e2d; border:1px solid #21465f; padding:20px; }
  11. h1 { margin:0 0 6px; font-size:22px; } .endpoint { color:#78a2b8; font-family:Consolas,monospace; margin:0; }
  12. .layout { display:grid; grid-template-columns:minmax(0,2fr) minmax(260px,1fr); gap:16px; }
  13. .toolbar { display:flex; gap:10px; align-items:center; flex-wrap:wrap; margin-bottom:14px; }
  14. button { border:1px solid #2f7098; background:#123247; color:#dff2ff; padding:8px 14px; cursor:pointer; }
  15. button:disabled { opacity:.45; cursor:not-allowed; }
  16. .map { position:relative; height:480px; overflow:hidden; background:linear-gradient(135deg,#0b1a28,#102b3b); border:1px solid #275b74; }
  17. .map::before { content:""; position:absolute; inset:0; background-image:linear-gradient(rgba(60,130,160,.12) 1px,transparent 1px),linear-gradient(90deg,rgba(60,130,160,.12) 1px,transparent 1px); background-size:48px 48px; }
  18. .marker { position:absolute; width:26px; height:26px; border-radius:50%; display:grid; place-items:center; font-size:11px; font-weight:700; border:2px solid rgba(255,255,255,.75); transform:translate(-50%,-50%); box-shadow:0 0 14px currentColor; }
  19. .point { background:#20a35d; color:#052; } .device { background:#3d87d8; color:#021c33; border-radius:6px; }
  20. .marker span { position:absolute; top:30px; left:50%; transform:translateX(-50%); white-space:nowrap; color:#cfe8f6; text-shadow:0 1px 3px #000; }
  21. .stats { display:grid; grid-template-columns:repeat(4,1fr); gap:8px; margin-bottom:12px; }
  22. .card { background:#0b1922; border:1px solid #275b74; padding:10px; } .card strong { display:block; font-size:20px; } .card span { color:#8fb3c7; }
  23. table { width:100%; border-collapse:collapse; } th,td { border:1px solid #275b74; padding:8px; text-align:left; } th { background:#123247; }
  24. .invalid { color:#f0ad4e; } .valid { color:#68d095; } .note { color:#8fb3c7; font-size:13px; }
  25. </style>
  26. </head>
  27. <body>
  28. <main>
  29. <header>
  30. <h1>驾驶舱地图快照分页演示</h1>
  31. <p class="endpoint">GET /api/cockpit/v1/map-snapshot?page=1&amp;pageSize=2</p>
  32. </header>
  33. <div class="layout">
  34. <section>
  35. <div class="toolbar">
  36. <button id="load">开始加载第 1 页</button>
  37. <button id="reset">重新开始</button>
  38. <span id="status" class="note">地图框架已就绪,等待分页数据。</span>
  39. </div>
  40. <div class="stats">
  41. <div class="card"><strong id="loaded-count">0</strong><span>已加载对象</span></div>
  42. <div class="card"><strong id="renderable-count">0</strong><span>可渲染对象</span></div>
  43. <div class="card"><strong id="blocked-count">0</strong><span>坐标禁渲染</span></div>
  44. <div class="card"><strong id="complete">否</strong><span>加载完成</span></div>
  45. </div>
  46. <div id="map" class="map" aria-label="一张图地图框架"></div>
  47. </section>
  48. <section>
  49. <h2>不可渲染对象</h2>
  50. <table>
  51. <thead><tr><th>编码</th><th>状态</th><th>原因</th></tr></thead>
  52. <tbody id="blocked"></tbody>
  53. </table>
  54. <p class="note">已关联设备的点位由监测点承载,不重复生成独立设备点位。</p>
  55. </section>
  56. </div>
  57. </main>
  58. <script>
  59. const snapshotAt='2026-09-02T10:00:00+08:00';
  60. const pages=[
  61. {page:1,pageSize:2,snapshotAt,total:7,batchUpdatedAt:snapshotAt,complete:false,monitoringPoints:[
  62. {pointCode:'MP-001',pointName:'供水干管监测点',pointStatus:'NORMAL',statusReasons:[],longitude:110.141,latitude:28.452,coordinateValidation:{state:'VALID',reason:null,renderable:true,crs:'WGS84'}},
  63. {pointCode:'MP-002',pointName:'燃气调压站监测点',pointStatus:'ALARM',statusReasons:['EQ-ASSOC-001 报警'],longitude:110.142,latitude:28.453,coordinateValidation:{state:'VALID',reason:null,renderable:true,crs:'WGS84'}}
  64. ],unassociatedEquipment:[]},
  65. {page:2,pageSize:2,snapshotAt,total:7,batchUpdatedAt:snapshotAt,complete:false,monitoringPoints:[
  66. {pointCode:'MP-003',pointName:'污水排口监测点',pointStatus:'WARNING',statusReasons:['EQ-FREE-003 预警'],longitude:110.143,latitude:28.454,coordinateValidation:{state:'UNVERIFIED',reason:'UNVERIFIED_COORDINATE',renderable:false,crs:'WGS84'}}
  67. ],unassociatedEquipment:[
  68. {deviceCode:'EQ-FREE-001',deviceName:'供水压力计-001',longitude:181,latitude:28.455,coordinateValidation:{state:'INVALID',reason:'LONGITUDE_OUT_OF_RANGE',renderable:false,crs:'WGS84'}}
  69. ]},
  70. {page:3,pageSize:2,snapshotAt,total:7,batchUpdatedAt:snapshotAt,complete:false,monitoringPoints:[],unassociatedEquipment:[
  71. {deviceCode:'EQ-FREE-002',deviceName:'智能井盖-002',longitude:110.145,latitude:28.456,coordinateValidation:{state:'VALID',reason:null,renderable:true,crs:'WGS84'}},
  72. {deviceCode:'EQ-FREE-003',deviceName:'污水环境监测仪-003',longitude:110.146,latitude:28.457,coordinateValidation:{state:'UNVERIFIED',reason:'UNVERIFIED_COORDINATE',renderable:false,crs:'WGS84'}}
  73. ]},
  74. {page:4,pageSize:2,snapshotAt,total:7,batchUpdatedAt:snapshotAt,complete:true,monitoringPoints:[],unassociatedEquipment:[
  75. {deviceCode:'EQ-FREE-004',deviceName:'零坐标设备-004',longitude:0,latitude:0,coordinateValidation:{state:'INVALID',reason:'ZERO_COORDINATE',renderable:false,crs:'WGS84'}}
  76. ]}
  77. ];
  78. let pageIndex=0, loaded=0, renderable=0, blocked=0;
  79. const map=document.getElementById('map'), statusText=document.getElementById('status'), loadButton=document.getElementById('load');
  80. function percent(value,min,max){ return ((value-min)/(max-min)*82+9).toFixed(2)+'%'; }
  81. function marker(item,isPoint){
  82. const marker=document.createElement('div');
  83. marker.className='marker '+(isPoint?'point':'device');
  84. marker.style.left=percent(item.longitude,110.135,110.152);
  85. marker.style.top=percent(item.latitude,28.445,28.462);
  86. marker.textContent=isPoint?'点':'设';
  87. const label=document.createElement('span'); label.textContent=isPoint?item.pointName:item.deviceName; marker.appendChild(label);
  88. map.appendChild(marker);
  89. }
  90. function blockedRow(item,isPoint){
  91. const row=document.createElement('tr'); row.className='invalid';
  92. row.innerHTML=`<td>${isPoint?item.pointCode:item.deviceCode}</td><td>${item.coordinateValidation.state}</td><td>${item.coordinateValidation.reason}</td>`;
  93. document.getElementById('blocked').appendChild(row);
  94. }
  95. function loadPage(){
  96. if(pageIndex>=pages.length) return;
  97. const page=pages[pageIndex++];
  98. page.monitoringPoints.forEach(item=>add(item,true));
  99. page.unassociatedEquipment.forEach(item=>add(item,false));
  100. loaded+=page.monitoringPoints.length+page.unassociatedEquipment.length;
  101. document.getElementById('loaded-count').textContent=loaded;
  102. document.getElementById('renderable-count').textContent=renderable;
  103. document.getElementById('blocked-count').textContent=blocked;
  104. document.getElementById('complete').textContent=page.complete?'是':'否';
  105. statusText.textContent=page.complete?`快照 ${snapshotAt} 已加载完成。`:`已加载第 ${page.page} 页,快照 ${snapshotAt} 保持一致。`;
  106. loadButton.disabled=page.complete;
  107. loadButton.textContent=page.complete?'加载完成':`加载第 ${page.page+1} 页`;
  108. }
  109. function add(item,isPoint){
  110. if(item.coordinateValidation.renderable){ marker(item,isPoint); renderable++; }
  111. else { blockedRow(item,isPoint); blocked++; }
  112. }
  113. loadButton.addEventListener('click',loadPage);
  114. document.getElementById('reset').addEventListener('click',()=>{
  115. pageIndex=0; loaded=0; renderable=0; blocked=0;
  116. map.querySelectorAll('.marker').forEach(x=>x.remove());
  117. document.getElementById('blocked').innerHTML='';
  118. document.getElementById('loaded-count').textContent='0'; document.getElementById('renderable-count').textContent='0';
  119. document.getElementById('blocked-count').textContent='0'; document.getElementById('complete').textContent='否';
  120. statusText.textContent='地图框架已就绪,等待分页数据。'; loadButton.disabled=false; loadButton.textContent='开始加载第 1 页';
  121. });
  122. </script>
  123. </body>
  124. </html>