| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140 |
- <!doctype html>
- <html lang="zh-CN">
- <head>
- <meta charset="utf-8" />
- <meta name="viewport" content="width=device-width, initial-scale=1" />
- <title>Cockpit Equipment Detail Aggregate Demo</title>
- <style>
- body { margin:0; min-height:100vh; background:#08131f; color:#e8f1f8; font:15px/1.5 "Microsoft YaHei",sans-serif; }
- main { width:min(1280px,calc(100vw - 32px)); margin:28px auto; display:grid; gap:16px; }
- header, section { background:#0e1e2d; border:1px solid #21465f; padding:20px; }
- h1,h2,h3 { margin:0 0 10px; } h1 { font-size:22px; } h2 { font-size:17px; } h3 { font-size:15px; }
- .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; }
- .sections { display:grid; grid-template-columns:repeat(auto-fit,minmax(260px,1fr)); gap:12px; }
- .section { border:1px solid #275b74; background:#0b1922; padding:12px; }
- .section.success { border-color:#68d095; } .section.empty { border-color:#a9b7c0; }
- .section.stale { border-color:#f2c94c; } .section.failed { border-color:#ff7a7a; }
- .badge { display:inline-block; font-size:12px; padding:3px 8px; border:1px solid; margin-bottom:8px; }
- .success .badge { color:#68d095; border-color:#68d095; } .empty .badge { color:#a9b7c0; border-color:#a9b7c0; }
- .stale .badge { color:#f2c94c; border-color:#f2c94c; } .failed .badge { color:#ff7a7a; border-color:#ff7a7a; }
- table { width:100%; border-collapse:collapse; } th,td { border:1px solid #275b74; padding:6px; text-align:left; } th { background:#123247; }
- .mono { font-family:Consolas,monospace; overflow-wrap:anywhere; } pre { max-height:340px; overflow:auto; background:#071019; border:1px solid #275b74; padding:12px; }
- .note { color:#8fb3c7; font-size:13px; } .missing { color:#a9b7c0; font-style:italic; }
- </style>
- </head>
- <body>
- <main>
- <header>
- <h1>设备详情聚合与独立分区演示</h1>
- <p class="endpoint">GET /api/cockpit/v1/equipments/{deviceCode}/detail-aggregate</p>
- <p class="endpoint">分区重试复用既有:/archive · /metrics/current · /events/current · /work-orders · /work-orders/{code}/timeline</p>
- </header>
- <section>
- <div class="toolbar">
- <button type="button" data-mode="full" class="active">完整详情</button>
- <button type="button" data-mode="partial">局部失败</button>
- <button type="button" data-mode="empty">成功空数据</button>
- </div>
- <div id="sections" class="sections"></div>
- </section>
- <section>
- <h2>响应契约</h2>
- <p class="note">每个 sectionResult 独立返回 SUCCESS / EMPTY / STALE / FAILED;FAILED 分区保留原因与提示,不覆盖成功兄弟分区。最新图片继承档案分区,档案失败时显示来源分区失败。</p>
- <pre id="payload" class="mono"></pre>
- </section>
- </main>
- <script>
- const section = (state, reason, data, message) => ({ state, reason, message, data });
- const image = { url:'https://cockpit.example/latest.jpg', capturedAt:'2026-08-30T10:20:00+08:00' };
- const archive = {
- deviceCode:'TC-PS-WS-202609020012', deviceName:'污水环境监测设备', deviceModel:'WS-PRO-2000',
- deviceSpec:'悬浮物/COD/氨氮/电导率/PH', manufacturer:'沅陵专用仪表厂', deviceTypeName:'污水环境监测设备',
- assetValue:128000, expectedServiceYears:10, equipmentLocation:'太常片区', ownershipUnit:'沅陵县排水管理所',
- latestImage:image
- };
- const metric = { metricCode:'COD', metricName:'COD', value:18.2, unit:'mg/L', status:'WARNING', sampledAt:'2026-09-02T09:59:30+08:00', freshness:'CURRENT' };
- const warning = { warningNo:'WRN-20260902-001', warningName:'污水COD预警', level:2, levelColor:'YELLOW', status:'PROCESSING', eventTime:'2026-09-02T09:59:30+08:00' };
- const alarm = { alarmId:'ALM-20260902-001', warningName:'污水COD报警', level:1, levelColor:'RED', status:'UNHANDLED', eventTime:'2026-09-02T10:00:00+08:00' };
- const relation = { relationId:'REL-001', warningNo:warning.warningNo, alarmId:alarm.alarmId, relatedAt:'2026-09-02T10:00:00+08:00' };
- const workOrder = { workOrderCode:'WO-2026090200001', orderTypeName:'故障维修', stateCode:4, stateName:'处理中', orderDesc:'处理异常' };
- const timeline = { workOrder:{ ...workOrder }, timelineEntries:[{ logId:1, operationName:'创建工单', operationDesc:'处理异常', operatorName:'系统', operatedAt:'2026-09-02T09:00:00+08:00' }] };
- const metrics = { deviceCode:archive.deviceCode, state:'CURRENT', stateReason:'READY', metrics:[metric] };
- const responses = {
- full: {
- aggregateState:'READY',
- archive:section('SUCCESS','SUCCESS',archive), metrics:section('SUCCESS','SUCCESS',metrics),
- warnings:section('SUCCESS','SUCCESS',[warning]), alarms:section('SUCCESS','SUCCESS',[alarm]),
- alarmWarningRelations:section('SUCCESS','SUCCESS',[relation]),
- workOrders:section('SUCCESS','SUCCESS',[workOrder]), timelines:section('SUCCESS','SUCCESS',[timeline]),
- latestImage:section('SUCCESS','SUCCESS',image)
- },
- partial: {
- aggregateState:'PARTIAL',
- archive:section('FAILED','READ_FAILURE',null,'设备档案数据暂不可用'),
- metrics:section('STALE','SAMPLE_OUTDATED',{ ...metrics, state:'STALE' }),
- warnings:section('SUCCESS','SUCCESS',[warning]), alarms:section('SUCCESS','SUCCESS',[alarm]),
- alarmWarningRelations:section('SUCCESS','SUCCESS',[relation]),
- workOrders:section('FAILED','READ_FAILURE',null,'工单数据暂不可用'),
- timelines:section('FAILED','SOURCE_SECTION_FAILED',null,'工单分区失败,时间线不可用'),
- latestImage:section('FAILED','SOURCE_SECTION_FAILED',null,'设备档案失败,图片不可用')
- },
- empty: {
- aggregateState:'EMPTY',
- archive:section('EMPTY','NO_ARCHIVE',null), metrics:section('EMPTY','NO_METRICS',null),
- warnings:section('EMPTY','NO_EVENTS',null), alarms:section('EMPTY','NO_EVENTS',null),
- alarmWarningRelations:section('EMPTY','NO_EVENTS',null),
- workOrders:section('EMPTY','NO_WORK_ORDERS',null), timelines:section('EMPTY','NO_WORK_ORDERS',null),
- latestImage:section('EMPTY','NO_IMAGE',null)
- }
- };
- function renderSection(name, result) {
- const rows = [];
- const data = result.data;
- if (name === 'archive' && data) {
- rows.push(['设备编码', data.deviceCode], ['设备名称', data.deviceName], ['设备型号', data.deviceModel], ['权属单位', data.ownershipUnit]);
- } else if (name === 'metrics' && data) {
- rows.push(['状态', `${data.state} / ${data.stateReason}`], ['最新COD', `${data.metrics[0].value} ${data.metrics[0].unit}`]);
- } else if (name === 'warnings' && data) {
- data.forEach(item => rows.push([item.warningNo, `${item.warningName} · ${item.status}`]));
- } else if (name === 'alarms' && data) {
- data.forEach(item => rows.push([item.alarmId, `${item.warningName} · ${item.status}`]));
- } else if (name === 'alarmWarningRelations' && data) {
- data.forEach(item => rows.push([item.relationId, `${item.warningNo} ⇄ ${item.alarmId}`]));
- } else if (name === 'workOrders' && data) {
- data.forEach(item => rows.push([item.workOrderCode, `${item.stateCode} ${item.stateName}`]));
- } else if (name === 'timelines' && data) {
- data.forEach(item => rows.push([item.workOrder.workOrderCode, `${item.timelineEntries.length} 条时间线`]));
- } else if (name === 'latestImage' && data) {
- rows.push(['图片地址', data.url], ['采集时间', data.capturedAt]);
- }
- const body = result.data
- ? '<table><tbody>' + rows.map(row => `<tr><th>${row[0]}</th><td class="mono">${row[1]}</td></tr>`).join('') + '</tbody></table>'
- : `<p class="missing">${result.message || '暂无数据'}</p>`;
- return `<article class="section ${result.state.toLowerCase()}">
- <span class="badge">${result.state}</span>
- <h3>${name}</h3>
- ${result.reason ? `<p class="note">reason:${result.reason}</p>` : ''}
- ${body}
- </article>`;
- }
- function render(mode) {
- document.querySelectorAll('button[data-mode]').forEach(button => button.classList.toggle('active', button.dataset.mode === mode));
- const payload = responses[mode];
- document.getElementById('sections').innerHTML = Object.keys(payload)
- .filter(key => key !== 'aggregateState')
- .map(key => renderSection(key, payload[key])).join('');
- document.getElementById('payload').textContent = JSON.stringify({
- code:200, msg:'操作成功', data:{ requestId:'equipment-detail-demo', apiVersion:'v1', readOnly:true,
- deviceCode:archive.deviceCode, aggregateState:payload.aggregateState, serverTime:'2026-09-02T10:00:00+08:00', ...payload }
- }, null, 2);
- }
- document.querySelectorAll('button[data-mode]').forEach(button => button.addEventListener('click', () => render(button.dataset.mode)));
- render('full');
- </script>
- </body>
- </html>
|