| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145 |
- <!doctype html>
- <html lang="zh-CN">
- <head>
- <meta charset="utf-8" />
- <meta name="viewport" content="width=device-width, initial-scale=1" />
- <title>Cockpit Type Metrics Demo</title>
- <style>
- :root { color-scheme: dark; }
- body { margin:0; min-height:100vh; background:#08131f; color:#e8f1f8; font:15px/1.5 "Microsoft YaHei",sans-serif; }
- main { width:min(1240px,calc(100vw - 32px)); margin:28px auto; display:grid; gap:16px; }
- header, section { background:#0e1e2d; border:1px solid #21465f; padding:20px; }
- h1,h2 { margin:0 0 10px; } h1 { font-size:22px; } h2 { font-size:17px; }
- .endpoint { color:#78a2b8; font-family:Consolas,monospace; margin:0; }
- .toolbar { display:flex; gap:8px; flex-wrap:wrap; margin-bottom:14px; }
- button { border:1px solid #2f7098; background:#123247; color:#dff2ff; padding:8px 14px; cursor:pointer; }
- button.active { outline:2px solid #61c2ff; }
- .state-grid { display:grid; grid-template-columns:repeat(4,minmax(140px,1fr)); gap:10px; margin-bottom:16px; }
- .state { background:#0b1922; border:1px solid #275b74; padding:10px; }
- .state strong { display:block; font-size:16px; } .state span { color:#8fb3c7; font-size:13px; }
- .state.ok strong { color:#68d095; } .state.warning strong { color:#f2c94c; } .state.empty strong { color:#a9b7c0; } .state.error strong { color:#ff7a7a; }
- .cards { display:grid; grid-template-columns:repeat(auto-fit,minmax(210px,1fr)); gap:12px; }
- .card { background:#0b1922; border:1px solid #275b74; padding:12px; }
- .card-top { display:flex; justify-content:space-between; gap:8px; align-items:flex-start; }
- .metric-name { font-weight:700; } .metric-code { color:#78a2b8; font-family:Consolas,monospace; font-size:12px; }
- .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; }
- .badge { font-size:12px; padding:3px 8px; border:1px solid; white-space:nowrap; }
- .normal { color:#68d095; border-color:#68d095; } .warning { color:#f2c94c; border-color:#f2c94c; } .alarm { color:#ff7a7a; border-color:#ff7a7a; }
- .unknown { color:#a9b7c0; border-color:#a9b7c0; } .fresh { color:#68d095; border-color:#68d095; } .stale { color:#f2c94c; border-color:#f2c94c; }
- .history { margin-top:10px; } .trend { display:block; width:100%; height:42px; }
- .mono { font-family:Consolas,monospace; overflow-wrap:anywhere; } .note { color:#8fb3c7; font-size:13px; }
- pre { max-height:360px; overflow:auto; background:#071019; border:1px solid #275b74; padding:12px; }
- .missing { display:grid; place-items:center; min-height:220px; border:1px dashed #3f667d; color:#a9b7c0; }
- </style>
- </head>
- <body>
- <main>
- <header>
- <h1>驾驶舱设备类型指标演示</h1>
- <p class="endpoint">GET /api/cockpit/v1/equipments/{deviceCode}/metrics/current</p>
- <p class="endpoint">GET /api/cockpit/v1/equipments/{deviceCode}/metrics/history?startTime=&endTime=&page=1&pageSize=100</p>
- </header>
- <section>
- <div class="toolbar">
- <button type="button" data-mode="gas" class="active">燃气</button>
- <button type="button" data-mode="supply">供水</button>
- <button type="button" data-mode="drainage">排水</button>
- <button type="button" data-mode="sewage">污水环境</button>
- <button type="button" data-mode="empty">成功空数据</button>
- <button type="button" data-mode="stale">过期数据</button>
- <button type="button" data-mode="failure">加载失败</button>
- </div>
- <div id="detail">
- <div class="state-grid">
- <div class="state ok"><strong id="state-value">CURRENT</strong><span>响应状态</span></div>
- <div class="state"><strong id="state-reason">READY</strong><span>状态原因</span></div>
- <div class="state"><strong id="device-type">-</strong><span>设备类型</span></div>
- <div class="state"><strong id="read-only">true</strong><span>只读接口</span></div>
- </div>
- <h2>指标卡与历史趋势</h2>
- <div id="metrics" class="cards"></div>
- </div>
- <div id="missing" class="missing" hidden><div><strong id="missing-title"></strong><p class="note" id="missing-reason"></p></div></div>
- </section>
- <section>
- <h2>当前响应契约</h2>
- <p class="note">页面只按接口返回的 metricCode / metricName / unit / points 渲染,不硬编码数据库字段;未知指标保留 UNKNOWN 展示。</p>
- <pre id="payload" class="mono"></pre>
- </section>
- </main>
- <script>
- const demoClock = '2026-09-02T10:00:00+08:00';
- const current = (deviceCode, deviceType, deviceTypeName, metrics, state='CURRENT', reason='READY') => ({
- code:200, msg:'操作成功', data:{ requestId:'type-metrics-demo-' + deviceCode, apiVersion:'v1', readOnly:true,
- deviceCode, deviceType, deviceTypeName, state, stateReason:reason, metrics, startTime:null, endTime:null,
- timezone:'Asia/Shanghai', page:1, pageSize:0, total:0 }
- });
- const point = (value, sampledAt, status='NORMAL') => ({ value, status, sampledAt, freshness:'HISTORICAL' });
- const metric = (code, name, value, unit, status, sampledAt, points=[]) => ({ metricCode:code, metricName:name, value, unit, status, sampledAt, freshness:'CURRENT', points });
- const responses = {
- gas: current('TC-RQ-202609020001', 'gas_pressure', '燃气压力计', [
- 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')]),
- metric('TEMPERATURE', '介质温度', 23.4, '℃', 'NORMAL', '2026-09-02T09:59:40+08:00')
- ]),
- supply: current('TC-GS-202609020005', 'gs_pressure', '供水压力计', [
- metric('PRESSURE', '供水压力', 0.29, 'MPa', 'NORMAL', '2026-09-02T09:59:45+08:00'),
- metric('FLOW', '瞬时流量', 42.7, 'm³/h', 'WARNING', '2026-09-02T09:59:45+08:00')
- ]),
- drainage: current('TC-PS-YS-202609020009', 'ws_rainflow', '雨水流量计', [
- metric('FLOW', '雨水流量', 138.2, 'L/s', 'NORMAL', '2026-09-02T09:59:50+08:00'),
- metric('LIQUID_LEVEL', '液位', 1.18, 'm', 'NORMAL', '2026-09-02T09:59:50+08:00')
- ]),
- sewage: current('TC-PS-WS-202609020012', 'ws_sewage_environment', '污水环境监测设备', [
- metric('SUSPENDED_SOLIDS', '悬浮物', 12.5, 'mg/L', 'NORMAL', '2026-09-02T09:59:30+08:00'),
- metric('COD', 'COD', 18.2, 'mg/L', 'WARNING', '2026-09-02T09:59:30+08:00'),
- metric('AMMONIA_NITROGEN', '氨氮', 0.8, 'mg/L', 'NORMAL', '2026-09-02T09:59:30+08:00'),
- metric('CONDUCTIVITY', '电导率', 780, 'μS/cm', 'NORMAL', '2026-09-02T09:59:30+08:00'),
- metric('PH', 'pH', 7.2, '', 'NORMAL', '2026-09-02T09:59:30+08:00'),
- metric('UNKNOWN', '厂商私有参数', 1, '', 'UNKNOWN', '2026-09-02T09:59:30+08:00')
- ]),
- empty: current('TC-RQ-202609020021', 'gas_pressure', '燃气压力计', [], 'EMPTY', 'NO_METRICS'),
- stale: current('TC-GS-202609020022', 'gs_pressure', '供水压力计', [
- metric('PRESSURE', '供水压力', 0.31, 'MPa', 'NORMAL', '2026-09-02T09:58:00+08:00')
- ], 'STALE', 'SAMPLE_OUTDATED'),
- failure: { code:500, msg:'驾驶舱设备指标数据暂不可用', data:{ category:'SERVER_ERROR', message:'驾驶舱设备指标数据暂不可用', requestId:'type-metrics-demo-500', serverTime:demoClock } }
- };
- responses.stale.data.metrics[0].freshness = 'STALE';
- function trend(points) {
- if (!points || points.length < 2) return '<div class="note history">暂无历史序列</div>';
- const values = points.map(item => item.value);
- const min = Math.min(...values), max = Math.max(...values);
- const range = max === min ? 1 : max - min;
- const coords = points.map((item, index) => (10 + index * 80 / (points.length - 1)).toFixed(1) + ',' + (36 - (item.value - min) / range * 28).toFixed(1)).join(' ');
- return '<svg class="trend history" viewBox="0 0 100 42" preserveAspectRatio="none"><polyline points="' + coords + '" fill="none" stroke="#61c2ff" stroke-width="2" /></svg>';
- }
- function render(mode) {
- document.querySelectorAll('button[data-mode]').forEach(button => button.classList.toggle('active', button.dataset.mode === mode));
- const payload = responses[mode], success = payload.code === 200;
- document.getElementById('detail').hidden = !success;
- document.getElementById('missing').hidden = success;
- document.getElementById('payload').textContent = JSON.stringify(payload, null, 2);
- if (!success) {
- document.getElementById('missing-title').textContent = 'HTTP ' + payload.code + ':' + payload.msg;
- document.getElementById('missing-reason').textContent = '失败状态独立展示,不与空数据或过期数据混淆。';
- return;
- }
- const data = payload.data;
- document.getElementById('state-value').textContent = data.state;
- document.getElementById('state-reason').textContent = data.stateReason;
- document.getElementById('device-type').textContent = data.deviceType;
- document.getElementById('read-only').textContent = data.readOnly;
- document.getElementById('metrics').innerHTML = data.metrics.map(item => `
- <article class="card">
- <div class="card-top">
- <div><div class="metric-name">${item.metricName}</div><div class="metric-code">${item.metricCode}</div></div>
- <div><span class="badge ${String(item.status).toLowerCase()}">${item.status || '-'}</span><span class="badge ${item.freshness === 'STALE' ? 'stale' : 'fresh'}">${item.freshness}</span></div>
- </div>
- <div class="metric-value">${item.value ?? '—'}${item.unit ? '<small>' + item.unit + '</small>' : ''}</div>
- ${trend(item.points)}
- </article>`).join('');
- }
- document.querySelectorAll('button[data-mode]').forEach(button => button.addEventListener('click', () => render(button.dataset.mode)));
- render('gas');
- </script>
- </body>
- </html>
|