|
|
@@ -0,0 +1,145 @@
|
|
|
+<!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>
|