type-metrics-demo.html 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  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 Type Metrics Demo</title>
  7. <style>
  8. :root { color-scheme: dark; }
  9. body { margin:0; min-height:100vh; background:#08131f; color:#e8f1f8; font:15px/1.5 "Microsoft YaHei",sans-serif; }
  10. main { width:min(1240px,calc(100vw - 32px)); margin:28px auto; display:grid; gap:16px; }
  11. header, section { background:#0e1e2d; border:1px solid #21465f; padding:20px; }
  12. h1,h2 { margin:0 0 10px; } h1 { font-size:22px; } h2 { font-size:17px; }
  13. .endpoint { color:#78a2b8; font-family:Consolas,monospace; margin:0; }
  14. .toolbar { display:flex; gap:8px; flex-wrap:wrap; margin-bottom:14px; }
  15. button { border:1px solid #2f7098; background:#123247; color:#dff2ff; padding:8px 14px; cursor:pointer; }
  16. button.active { outline:2px solid #61c2ff; }
  17. .state-grid { display:grid; grid-template-columns:repeat(4,minmax(140px,1fr)); gap:10px; margin-bottom:16px; }
  18. .state { background:#0b1922; border:1px solid #275b74; padding:10px; }
  19. .state strong { display:block; font-size:16px; } .state span { color:#8fb3c7; font-size:13px; }
  20. .state.ok strong { color:#68d095; } .state.warning strong { color:#f2c94c; } .state.empty strong { color:#a9b7c0; } .state.error strong { color:#ff7a7a; }
  21. .cards { display:grid; grid-template-columns:repeat(auto-fit,minmax(210px,1fr)); gap:12px; }
  22. .card { background:#0b1922; border:1px solid #275b74; padding:12px; }
  23. .card-top { display:flex; justify-content:space-between; gap:8px; align-items:flex-start; }
  24. .metric-name { font-weight:700; } .metric-code { color:#78a2b8; font-family:Consolas,monospace; font-size:12px; }
  25. .metric-value { font-size:26px; font-weight:700; margin:8px 0 4px; } .metric-value small { font-size:13px; font-weight:400; color:#8fb3c7; margin-left:4px; }
  26. .badge { font-size:12px; padding:3px 8px; border:1px solid; white-space:nowrap; }
  27. .normal { color:#68d095; border-color:#68d095; } .warning { color:#f2c94c; border-color:#f2c94c; } .alarm { color:#ff7a7a; border-color:#ff7a7a; }
  28. .unknown { color:#a9b7c0; border-color:#a9b7c0; } .fresh { color:#68d095; border-color:#68d095; } .stale { color:#f2c94c; border-color:#f2c94c; }
  29. .history { margin-top:10px; } .trend { display:block; width:100%; height:42px; }
  30. .mono { font-family:Consolas,monospace; overflow-wrap:anywhere; } .note { color:#8fb3c7; font-size:13px; }
  31. pre { max-height:360px; overflow:auto; background:#071019; border:1px solid #275b74; padding:12px; }
  32. .missing { display:grid; place-items:center; min-height:220px; border:1px dashed #3f667d; color:#a9b7c0; }
  33. </style>
  34. </head>
  35. <body>
  36. <main>
  37. <header>
  38. <h1>驾驶舱设备类型指标演示</h1>
  39. <p class="endpoint">GET /api/cockpit/v1/equipments/{deviceCode}/metrics/current</p>
  40. <p class="endpoint">GET /api/cockpit/v1/equipments/{deviceCode}/metrics/history?startTime=&amp;endTime=&amp;page=1&amp;pageSize=100</p>
  41. </header>
  42. <section>
  43. <div class="toolbar">
  44. <button type="button" data-mode="gas" class="active">燃气</button>
  45. <button type="button" data-mode="supply">供水</button>
  46. <button type="button" data-mode="drainage">排水</button>
  47. <button type="button" data-mode="sewage">污水环境</button>
  48. <button type="button" data-mode="empty">成功空数据</button>
  49. <button type="button" data-mode="stale">过期数据</button>
  50. <button type="button" data-mode="failure">加载失败</button>
  51. </div>
  52. <div id="detail">
  53. <div class="state-grid">
  54. <div class="state ok"><strong id="state-value">CURRENT</strong><span>响应状态</span></div>
  55. <div class="state"><strong id="state-reason">READY</strong><span>状态原因</span></div>
  56. <div class="state"><strong id="device-type">-</strong><span>设备类型</span></div>
  57. <div class="state"><strong id="read-only">true</strong><span>只读接口</span></div>
  58. </div>
  59. <h2>指标卡与历史趋势</h2>
  60. <div id="metrics" class="cards"></div>
  61. </div>
  62. <div id="missing" class="missing" hidden><div><strong id="missing-title"></strong><p class="note" id="missing-reason"></p></div></div>
  63. </section>
  64. <section>
  65. <h2>当前响应契约</h2>
  66. <p class="note">页面只按接口返回的 metricCode / metricName / unit / points 渲染,不硬编码数据库字段;未知指标保留 UNKNOWN 展示。</p>
  67. <pre id="payload" class="mono"></pre>
  68. </section>
  69. </main>
  70. <script>
  71. const demoClock = '2026-09-02T10:00:00+08:00';
  72. const current = (deviceCode, deviceType, deviceTypeName, metrics, state='CURRENT', reason='READY') => ({
  73. code:200, msg:'操作成功', data:{ requestId:'type-metrics-demo-' + deviceCode, apiVersion:'v1', readOnly:true,
  74. deviceCode, deviceType, deviceTypeName, state, stateReason:reason, metrics, startTime:null, endTime:null,
  75. timezone:'Asia/Shanghai', page:1, pageSize:0, total:0 }
  76. });
  77. const point = (value, sampledAt, status='NORMAL') => ({ value, status, sampledAt, freshness:'HISTORICAL' });
  78. const metric = (code, name, value, unit, status, sampledAt, points=[]) => ({ metricCode:code, metricName:name, value, unit, status, sampledAt, freshness:'CURRENT', points });
  79. const responses = {
  80. gas: current('TC-RQ-202609020001', 'gas_pressure', '燃气压力计', [
  81. metric('PRESSURE', '管道压力', 0.36, 'MPa', 'NORMAL', '2026-09-02T09:59:40+08:00', [point(.38,'09:40'),point(.37,'09:50'),point(.36,'10:00')]),
  82. metric('TEMPERATURE', '介质温度', 23.4, '℃', 'NORMAL', '2026-09-02T09:59:40+08:00')
  83. ]),
  84. supply: current('TC-GS-202609020005', 'gs_pressure', '供水压力计', [
  85. metric('PRESSURE', '供水压力', 0.29, 'MPa', 'NORMAL', '2026-09-02T09:59:45+08:00'),
  86. metric('FLOW', '瞬时流量', 42.7, 'm³/h', 'WARNING', '2026-09-02T09:59:45+08:00')
  87. ]),
  88. drainage: current('TC-PS-YS-202609020009', 'ws_rainflow', '雨水流量计', [
  89. metric('FLOW', '雨水流量', 138.2, 'L/s', 'NORMAL', '2026-09-02T09:59:50+08:00'),
  90. metric('LIQUID_LEVEL', '液位', 1.18, 'm', 'NORMAL', '2026-09-02T09:59:50+08:00')
  91. ]),
  92. sewage: current('TC-PS-WS-202609020012', 'ws_sewage_environment', '污水环境监测设备', [
  93. metric('SUSPENDED_SOLIDS', '悬浮物', 12.5, 'mg/L', 'NORMAL', '2026-09-02T09:59:30+08:00'),
  94. metric('COD', 'COD', 18.2, 'mg/L', 'WARNING', '2026-09-02T09:59:30+08:00'),
  95. metric('AMMONIA_NITROGEN', '氨氮', 0.8, 'mg/L', 'NORMAL', '2026-09-02T09:59:30+08:00'),
  96. metric('CONDUCTIVITY', '电导率', 780, 'μS/cm', 'NORMAL', '2026-09-02T09:59:30+08:00'),
  97. metric('PH', 'pH', 7.2, '', 'NORMAL', '2026-09-02T09:59:30+08:00'),
  98. metric('UNKNOWN', '厂商私有参数', 1, '', 'UNKNOWN', '2026-09-02T09:59:30+08:00')
  99. ]),
  100. empty: current('TC-RQ-202609020021', 'gas_pressure', '燃气压力计', [], 'EMPTY', 'NO_METRICS'),
  101. stale: current('TC-GS-202609020022', 'gs_pressure', '供水压力计', [
  102. metric('PRESSURE', '供水压力', 0.31, 'MPa', 'NORMAL', '2026-09-02T09:58:00+08:00')
  103. ], 'STALE', 'SAMPLE_OUTDATED'),
  104. failure: { code:500, msg:'驾驶舱设备指标数据暂不可用', data:{ category:'SERVER_ERROR', message:'驾驶舱设备指标数据暂不可用', requestId:'type-metrics-demo-500', serverTime:demoClock } }
  105. };
  106. responses.stale.data.metrics[0].freshness = 'STALE';
  107. function trend(points) {
  108. if (!points || points.length < 2) return '<div class="note history">暂无历史序列</div>';
  109. const values = points.map(item => item.value);
  110. const min = Math.min(...values), max = Math.max(...values);
  111. const range = max === min ? 1 : max - min;
  112. const coords = points.map((item, index) => (10 + index * 80 / (points.length - 1)).toFixed(1) + ',' + (36 - (item.value - min) / range * 28).toFixed(1)).join(' ');
  113. return '<svg class="trend history" viewBox="0 0 100 42" preserveAspectRatio="none"><polyline points="' + coords + '" fill="none" stroke="#61c2ff" stroke-width="2" /></svg>';
  114. }
  115. function render(mode) {
  116. document.querySelectorAll('button[data-mode]').forEach(button => button.classList.toggle('active', button.dataset.mode === mode));
  117. const payload = responses[mode], success = payload.code === 200;
  118. document.getElementById('detail').hidden = !success;
  119. document.getElementById('missing').hidden = success;
  120. document.getElementById('payload').textContent = JSON.stringify(payload, null, 2);
  121. if (!success) {
  122. document.getElementById('missing-title').textContent = 'HTTP ' + payload.code + ':' + payload.msg;
  123. document.getElementById('missing-reason').textContent = '失败状态独立展示,不与空数据或过期数据混淆。';
  124. return;
  125. }
  126. const data = payload.data;
  127. document.getElementById('state-value').textContent = data.state;
  128. document.getElementById('state-reason').textContent = data.stateReason;
  129. document.getElementById('device-type').textContent = data.deviceType;
  130. document.getElementById('read-only').textContent = data.readOnly;
  131. document.getElementById('metrics').innerHTML = data.metrics.map(item => `
  132. <article class="card">
  133. <div class="card-top">
  134. <div><div class="metric-name">${item.metricName}</div><div class="metric-code">${item.metricCode}</div></div>
  135. <div><span class="badge ${String(item.status).toLowerCase()}">${item.status || '-'}</span><span class="badge ${item.freshness === 'STALE' ? 'stale' : 'fresh'}">${item.freshness}</span></div>
  136. </div>
  137. <div class="metric-value">${item.value ?? '—'}${item.unit ? '<small>' + item.unit + '</small>' : ''}</div>
  138. ${trend(item.points)}
  139. </article>`).join('');
  140. }
  141. document.querySelectorAll('button[data-mode]').forEach(button => button.addEventListener('click', () => render(button.dataset.mode)));
  142. render('gas');
  143. </script>
  144. </body>
  145. </html>