equipment-detail-aggregate-demo.html 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  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 Equipment Detail Aggregate 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(1280px,calc(100vw - 32px)); margin:28px auto; display:grid; gap:16px; }
  10. header, section { background:#0e1e2d; border:1px solid #21465f; padding:20px; }
  11. h1,h2,h3 { margin:0 0 10px; } h1 { font-size:22px; } h2 { font-size:17px; } h3 { font-size:15px; }
  12. .endpoint { color:#78a2b8; font-family:Consolas,monospace; margin:0; }
  13. .toolbar { display:flex; gap:8px; flex-wrap:wrap; margin-bottom:14px; }
  14. button { border:1px solid #2f7098; background:#123247; color:#dff2ff; padding:8px 14px; cursor:pointer; }
  15. button.active { outline:2px solid #61c2ff; }
  16. .sections { display:grid; grid-template-columns:repeat(auto-fit,minmax(260px,1fr)); gap:12px; }
  17. .section { border:1px solid #275b74; background:#0b1922; padding:12px; }
  18. .section.success { border-color:#68d095; } .section.empty { border-color:#a9b7c0; }
  19. .section.stale { border-color:#f2c94c; } .section.failed { border-color:#ff7a7a; }
  20. .badge { display:inline-block; font-size:12px; padding:3px 8px; border:1px solid; margin-bottom:8px; }
  21. .success .badge { color:#68d095; border-color:#68d095; } .empty .badge { color:#a9b7c0; border-color:#a9b7c0; }
  22. .stale .badge { color:#f2c94c; border-color:#f2c94c; } .failed .badge { color:#ff7a7a; border-color:#ff7a7a; }
  23. table { width:100%; border-collapse:collapse; } th,td { border:1px solid #275b74; padding:6px; text-align:left; } th { background:#123247; }
  24. .mono { font-family:Consolas,monospace; overflow-wrap:anywhere; } pre { max-height:340px; overflow:auto; background:#071019; border:1px solid #275b74; padding:12px; }
  25. .note { color:#8fb3c7; font-size:13px; } .missing { color:#a9b7c0; font-style:italic; }
  26. </style>
  27. </head>
  28. <body>
  29. <main>
  30. <header>
  31. <h1>设备详情聚合与独立分区演示</h1>
  32. <p class="endpoint">GET /api/cockpit/v1/equipments/{deviceCode}/detail-aggregate</p>
  33. <p class="endpoint">分区重试复用既有:/archive · /metrics/current · /events/current · /work-orders · /work-orders/{code}/timeline</p>
  34. </header>
  35. <section>
  36. <div class="toolbar">
  37. <button type="button" data-mode="full" class="active">完整详情</button>
  38. <button type="button" data-mode="partial">局部失败</button>
  39. <button type="button" data-mode="empty">成功空数据</button>
  40. </div>
  41. <div id="sections" class="sections"></div>
  42. </section>
  43. <section>
  44. <h2>响应契约</h2>
  45. <p class="note">每个 sectionResult 独立返回 SUCCESS / EMPTY / STALE / FAILED;FAILED 分区保留原因与提示,不覆盖成功兄弟分区。最新图片继承档案分区,档案失败时显示来源分区失败。</p>
  46. <pre id="payload" class="mono"></pre>
  47. </section>
  48. </main>
  49. <script>
  50. const section = (state, reason, data, message) => ({ state, reason, message, data });
  51. const image = { url:'https://cockpit.example/latest.jpg', capturedAt:'2026-08-30T10:20:00+08:00' };
  52. const archive = {
  53. deviceCode:'TC-PS-WS-202609020012', deviceName:'污水环境监测设备', deviceModel:'WS-PRO-2000',
  54. deviceSpec:'悬浮物/COD/氨氮/电导率/PH', manufacturer:'沅陵专用仪表厂', deviceTypeName:'污水环境监测设备',
  55. assetValue:128000, expectedServiceYears:10, equipmentLocation:'太常片区', ownershipUnit:'沅陵县排水管理所',
  56. latestImage:image
  57. };
  58. const metric = { metricCode:'COD', metricName:'COD', value:18.2, unit:'mg/L', status:'WARNING', sampledAt:'2026-09-02T09:59:30+08:00', freshness:'CURRENT' };
  59. const warning = { warningNo:'WRN-20260902-001', warningName:'污水COD预警', level:2, levelColor:'YELLOW', status:'PROCESSING', eventTime:'2026-09-02T09:59:30+08:00' };
  60. const alarm = { alarmId:'ALM-20260902-001', warningName:'污水COD报警', level:1, levelColor:'RED', status:'UNHANDLED', eventTime:'2026-09-02T10:00:00+08:00' };
  61. const relation = { relationId:'REL-001', warningNo:warning.warningNo, alarmId:alarm.alarmId, relatedAt:'2026-09-02T10:00:00+08:00' };
  62. const workOrder = { workOrderCode:'WO-2026090200001', orderTypeName:'故障维修', stateCode:4, stateName:'处理中', orderDesc:'处理异常' };
  63. const timeline = { workOrder:{ ...workOrder }, timelineEntries:[{ logId:1, operationName:'创建工单', operationDesc:'处理异常', operatorName:'系统', operatedAt:'2026-09-02T09:00:00+08:00' }] };
  64. const metrics = { deviceCode:archive.deviceCode, state:'CURRENT', stateReason:'READY', metrics:[metric] };
  65. const responses = {
  66. full: {
  67. aggregateState:'READY',
  68. archive:section('SUCCESS','SUCCESS',archive), metrics:section('SUCCESS','SUCCESS',metrics),
  69. warnings:section('SUCCESS','SUCCESS',[warning]), alarms:section('SUCCESS','SUCCESS',[alarm]),
  70. alarmWarningRelations:section('SUCCESS','SUCCESS',[relation]),
  71. workOrders:section('SUCCESS','SUCCESS',[workOrder]), timelines:section('SUCCESS','SUCCESS',[timeline]),
  72. latestImage:section('SUCCESS','SUCCESS',image)
  73. },
  74. partial: {
  75. aggregateState:'PARTIAL',
  76. archive:section('FAILED','READ_FAILURE',null,'设备档案数据暂不可用'),
  77. metrics:section('STALE','SAMPLE_OUTDATED',{ ...metrics, state:'STALE' }),
  78. warnings:section('SUCCESS','SUCCESS',[warning]), alarms:section('SUCCESS','SUCCESS',[alarm]),
  79. alarmWarningRelations:section('SUCCESS','SUCCESS',[relation]),
  80. workOrders:section('FAILED','READ_FAILURE',null,'工单数据暂不可用'),
  81. timelines:section('FAILED','SOURCE_SECTION_FAILED',null,'工单分区失败,时间线不可用'),
  82. latestImage:section('FAILED','SOURCE_SECTION_FAILED',null,'设备档案失败,图片不可用')
  83. },
  84. empty: {
  85. aggregateState:'EMPTY',
  86. archive:section('EMPTY','NO_ARCHIVE',null), metrics:section('EMPTY','NO_METRICS',null),
  87. warnings:section('EMPTY','NO_EVENTS',null), alarms:section('EMPTY','NO_EVENTS',null),
  88. alarmWarningRelations:section('EMPTY','NO_EVENTS',null),
  89. workOrders:section('EMPTY','NO_WORK_ORDERS',null), timelines:section('EMPTY','NO_WORK_ORDERS',null),
  90. latestImage:section('EMPTY','NO_IMAGE',null)
  91. }
  92. };
  93. function renderSection(name, result) {
  94. const rows = [];
  95. const data = result.data;
  96. if (name === 'archive' && data) {
  97. rows.push(['设备编码', data.deviceCode], ['设备名称', data.deviceName], ['设备型号', data.deviceModel], ['权属单位', data.ownershipUnit]);
  98. } else if (name === 'metrics' && data) {
  99. rows.push(['状态', `${data.state} / ${data.stateReason}`], ['最新COD', `${data.metrics[0].value} ${data.metrics[0].unit}`]);
  100. } else if (name === 'warnings' && data) {
  101. data.forEach(item => rows.push([item.warningNo, `${item.warningName} · ${item.status}`]));
  102. } else if (name === 'alarms' && data) {
  103. data.forEach(item => rows.push([item.alarmId, `${item.warningName} · ${item.status}`]));
  104. } else if (name === 'alarmWarningRelations' && data) {
  105. data.forEach(item => rows.push([item.relationId, `${item.warningNo} ⇄ ${item.alarmId}`]));
  106. } else if (name === 'workOrders' && data) {
  107. data.forEach(item => rows.push([item.workOrderCode, `${item.stateCode} ${item.stateName}`]));
  108. } else if (name === 'timelines' && data) {
  109. data.forEach(item => rows.push([item.workOrder.workOrderCode, `${item.timelineEntries.length} 条时间线`]));
  110. } else if (name === 'latestImage' && data) {
  111. rows.push(['图片地址', data.url], ['采集时间', data.capturedAt]);
  112. }
  113. const body = result.data
  114. ? '<table><tbody>' + rows.map(row => `<tr><th>${row[0]}</th><td class="mono">${row[1]}</td></tr>`).join('') + '</tbody></table>'
  115. : `<p class="missing">${result.message || '暂无数据'}</p>`;
  116. return `<article class="section ${result.state.toLowerCase()}">
  117. <span class="badge">${result.state}</span>
  118. <h3>${name}</h3>
  119. ${result.reason ? `<p class="note">reason:${result.reason}</p>` : ''}
  120. ${body}
  121. </article>`;
  122. }
  123. function render(mode) {
  124. document.querySelectorAll('button[data-mode]').forEach(button => button.classList.toggle('active', button.dataset.mode === mode));
  125. const payload = responses[mode];
  126. document.getElementById('sections').innerHTML = Object.keys(payload)
  127. .filter(key => key !== 'aggregateState')
  128. .map(key => renderSection(key, payload[key])).join('');
  129. document.getElementById('payload').textContent = JSON.stringify({
  130. code:200, msg:'操作成功', data:{ requestId:'equipment-detail-demo', apiVersion:'v1', readOnly:true,
  131. deviceCode:archive.deviceCode, aggregateState:payload.aggregateState, serverTime:'2026-09-02T10:00:00+08:00', ...payload }
  132. }, null, 2);
  133. }
  134. document.querySelectorAll('button[data-mode]').forEach(button => button.addEventListener('click', () => render(button.dataset.mode)));
  135. render('full');
  136. </script>
  137. </body>
  138. </html>