freshness-semantics-demo.html 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  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 Freshness Semantics 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(1180px,calc(100vw - 32px)); margin:28px auto; display:grid; gap:16px; }
  10. header, section { background:#0e1e2d; border:1px solid #21465f; padding:20px; }
  11. h1,h2 { margin:0 0 10px; } h1 { font-size:22px; } h2 { font-size:17px; }
  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. .timeline { display:grid; grid-template-columns:repeat(auto-fit,minmax(180px,1fr)); gap:10px; margin-bottom:18px; }
  17. .phase { border:1px solid #275b74; background:#0b1922; padding:10px; }
  18. .phase strong { display:block; font-size:16px; } .phase span { color:#8fb3c7; font-size:13px; }
  19. .state-card { border:1px solid #275b74; background:#0b1922; padding:16px; }
  20. .state-card.CURRENT { border-color:#68d095; } .state-card.STALE { border-color:#f2c94c; }
  21. .state-card.EXPIRED { border-color:#ff7a7a; } .state-card.EMPTY { border-color:#a9b7c0; }
  22. .state-card.NOT_FOUND { border-color:#ff7a7a; } .state-card.SERVER_ERROR { border-color:#ff7a7a; }
  23. .badge { display:inline-block; font-size:12px; padding:4px 10px; border:1px solid; margin-bottom:10px; }
  24. .CURRENT { color:#68d095; border-color:#68d095; } .STALE { color:#f2c94c; border-color:#f2c94c; }
  25. .EXPIRED { color:#ff7a7a; border-color:#ff7a7a; } .EMPTY { color:#a9b7c0; border-color:#a9b7c0; }
  26. .NOT_FOUND, .SERVER_ERROR { color:#ff7a7a; border-color:#ff7a7a; }
  27. table { width:100%; border-collapse:collapse; } th,td { border:1px solid #275b74; padding:8px; text-align:left; } th { background:#123247; width:220px; }
  28. .mono { font-family:Consolas,monospace; overflow-wrap:anywhere; } pre { max-height:320px; overflow:auto; background:#071019; border:1px solid #275b74; padding:12px; }
  29. .note { color:#8fb3c7; font-size:13px; }
  30. </style>
  31. </head>
  32. <body>
  33. <main>
  34. <header>
  35. <h1>驾驶舱新鲜度与过期语义演示</h1>
  36. <p class="endpoint">GET /api/cockpit/v1/equipments/{deviceCode}/freshness?calibrationPeriodSeconds=60</p>
  37. <p class="endpoint">HTTP:空数据 200 · 非法参数 400 · 未认证 401 · 不存在 404 · 服务故障 500</p>
  38. </header>
  39. <section>
  40. <div class="toolbar">
  41. <button type="button" data-mode="current" class="active">当前</button>
  42. <button type="button" data-mode="stale">一次未更新</button>
  43. <button type="button" data-mode="expired">连续两次未更新</button>
  44. <button type="button" data-mode="expired-cache">缓存过期保留</button>
  45. <button type="button" data-mode="empty">成功空数据</button>
  46. <button type="button" data-mode="not-found">404</button>
  47. <button type="button" data-mode="server-error">500</button>
  48. </div>
  49. <div class="timeline">
  50. <div class="phase"><strong>T0</strong><span>数据更新</span></div>
  51. <div class="phase"><strong>T0 + 1 周期</strong><span>STALE,保留数据</span></div>
  52. <div class="phase"><strong>T0 + 2 周期</strong><span>EXPIRED,数据暂不可用</span></div>
  53. </div>
  54. <div id="detail" class="state-card CURRENT">
  55. <span id="badge" class="badge CURRENT">CURRENT</span>
  56. <h2 id="message">数据为最新</h2>
  57. <table><tbody id="fields"></tbody></table>
  58. </div>
  59. </section>
  60. <section>
  61. <h2>响应契约</h2>
  62. <p class="note">服务端只取 updatedAt 与 statusTime 中最新者;连续两个可配置校准周期未成功更新时标记 EXPIRED,但保留最后一次数据并显式标记。</p>
  63. <pre id="payload" class="mono"></pre>
  64. </section>
  65. </main>
  66. <script>
  67. const clock = '2026-09-02T10:00:00+08:00';
  68. const section = (state, reason, message, data) => ({ state, reason, message, data });
  69. const responses = {
  70. current: { code:200, data:{ requestId:'freshness-current', apiVersion:'v1', readOnly:true, deviceCode:'TC-PS-WS-202609020012',
  71. updatedAt:'2026-09-02T09:59:50+08:00', statusTime:'2026-09-02T09:59:55+08:00', serverUpdatedAt:clock,
  72. freshness:'CURRENT', staleReason:null, calibrationPeriodSeconds:60, missedCycles:0, dataAgeSeconds:5, retained:false, message:null,
  73. section:section('CURRENT','CURRENT',null,{ dataAgeSeconds:5 }) } },
  74. stale: { code:200, data:{ requestId:'freshness-stale', apiVersion:'v1', readOnly:true, deviceCode:'TC-PS-WS-202609020012',
  75. updatedAt:'2026-09-02T09:59:00+08:00', statusTime:null, serverUpdatedAt:clock,
  76. freshness:'STALE', staleReason:'ONE_UPDATE_CYCLE_MISSED', calibrationPeriodSeconds:60, missedCycles:1, dataAgeSeconds:60, retained:true, message:null,
  77. section:section('STALE','ONE_UPDATE_CYCLE_MISSED',null,{ dataAgeSeconds:60, retained:true }) } },
  78. expired: { code:200, data:{ requestId:'freshness-expired', apiVersion:'v1', readOnly:true, deviceCode:'TC-PS-WS-202609020012',
  79. updatedAt:'2026-09-02T09:58:00+08:00', statusTime:null, serverUpdatedAt:clock,
  80. freshness:'EXPIRED', staleReason:'TWO_UPDATE_CYCLES_MISSED', calibrationPeriodSeconds:60, missedCycles:2, dataAgeSeconds:120, retained:true, message:'数据暂不可用/已过期',
  81. section:section('EXPIRED','TWO_UPDATE_CYCLES_MISSED','数据暂不可用/已过期',{ dataAgeSeconds:120, retained:true }) } },
  82. 'expired-cache': { code:200, data:{ requestId:'freshness-expired-cache', apiVersion:'v1', readOnly:true, deviceCode:'TC-PS-WS-202609020012',
  83. updatedAt:'2026-09-02T09:57:30+08:00', statusTime:null, serverUpdatedAt:clock,
  84. freshness:'EXPIRED', staleReason:'TWO_UPDATE_CYCLES_MISSED', calibrationPeriodSeconds:60, missedCycles:3, dataAgeSeconds:150, retained:true, message:'数据暂不可用/已过期',
  85. section:section('EXPIRED','TWO_UPDATE_CYCLES_MISSED','数据暂不可用/已过期',{ dataAgeSeconds:150, retained:true }) } },
  86. empty: { code:200, data:{ requestId:'freshness-empty', apiVersion:'v1', readOnly:true, deviceCode:'TC-PS-WS-202609020012',
  87. updatedAt:null, statusTime:null, serverUpdatedAt:clock, freshness:'EMPTY', staleReason:null,
  88. calibrationPeriodSeconds:60, missedCycles:0, dataAgeSeconds:0, retained:false, message:null,
  89. section:section('EMPTY','NO_SOURCE',null,null) } },
  90. 'not-found': { code:404, error:{ category:'NOT_FOUND', message:'驾驶舱新鲜度数据不存在', requestId:'freshness-404', serverTime:clock } },
  91. 'server-error': { code:500, error:{ category:'SERVER_ERROR', message:'驾驶舱新鲜度数据暂不可用', requestId:'freshness-500', serverTime:clock } }
  92. };
  93. const fields = [
  94. ['updatedAt','数据更新时间'], ['statusTime','状态时间'], ['serverUpdatedAt','服务端时间'],
  95. ['staleReason','过期原因'], ['calibrationPeriodSeconds','校准周期(秒)'], ['missedCycles','未更新周期'],
  96. ['dataAgeSeconds','数据年龄(秒)'], ['retained','保留旧数据']
  97. ];
  98. function render(mode) {
  99. document.querySelectorAll('button[data-mode]').forEach(button => button.classList.toggle('active', button.dataset.mode === mode));
  100. const payload = responses[mode];
  101. const data = payload.data;
  102. const state = payload.error ? payload.error.category : data.freshness;
  103. const card = document.getElementById('detail');
  104. card.className = 'state-card ' + state;
  105. const badge = document.getElementById('badge');
  106. badge.className = 'badge ' + state;
  107. badge.textContent = state;
  108. document.getElementById('message').textContent = payload.error ? payload.error.message : (data.message || '数据为最新');
  109. document.getElementById('fields').innerHTML = payload.error
  110. ? `<tr><th>requestId</th><td class="mono">${payload.error.requestId}</td></tr><tr><th>serverTime</th><td class="mono">${payload.error.serverTime}</td></tr>`
  111. : fields.map(field => {
  112. const value = data[field[0]];
  113. return `<tr><th>${field[1]}</th><td class="mono">${value === null || value === undefined ? '未提供' : value}</td></tr>`;
  114. }).join('');
  115. document.getElementById('payload').textContent = JSON.stringify(payload, null, 2);
  116. }
  117. document.querySelectorAll('button[data-mode]').forEach(button => button.addEventListener('click', () => render(button.dataset.mode)));
  118. render('current');
  119. </script>
  120. </body>
  121. </html>