| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121 |
- <!doctype html>
- <html lang="zh-CN">
- <head>
- <meta charset="utf-8" />
- <meta name="viewport" content="width=device-width, initial-scale=1" />
- <title>Cockpit Freshness Semantics Demo</title>
- <style>
- body { margin:0; min-height:100vh; background:#08131f; color:#e8f1f8; font:15px/1.5 "Microsoft YaHei",sans-serif; }
- main { width:min(1180px,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; }
- .timeline { display:grid; grid-template-columns:repeat(auto-fit,minmax(180px,1fr)); gap:10px; margin-bottom:18px; }
- .phase { border:1px solid #275b74; background:#0b1922; padding:10px; }
- .phase strong { display:block; font-size:16px; } .phase span { color:#8fb3c7; font-size:13px; }
- .state-card { border:1px solid #275b74; background:#0b1922; padding:16px; }
- .state-card.CURRENT { border-color:#68d095; } .state-card.STALE { border-color:#f2c94c; }
- .state-card.EXPIRED { border-color:#ff7a7a; } .state-card.EMPTY { border-color:#a9b7c0; }
- .state-card.NOT_FOUND { border-color:#ff7a7a; } .state-card.SERVER_ERROR { border-color:#ff7a7a; }
- .badge { display:inline-block; font-size:12px; padding:4px 10px; border:1px solid; margin-bottom:10px; }
- .CURRENT { color:#68d095; border-color:#68d095; } .STALE { color:#f2c94c; border-color:#f2c94c; }
- .EXPIRED { color:#ff7a7a; border-color:#ff7a7a; } .EMPTY { color:#a9b7c0; border-color:#a9b7c0; }
- .NOT_FOUND, .SERVER_ERROR { color:#ff7a7a; border-color:#ff7a7a; }
- table { width:100%; border-collapse:collapse; } th,td { border:1px solid #275b74; padding:8px; text-align:left; } th { background:#123247; width:220px; }
- .mono { font-family:Consolas,monospace; overflow-wrap:anywhere; } pre { max-height:320px; overflow:auto; background:#071019; border:1px solid #275b74; padding:12px; }
- .note { color:#8fb3c7; font-size:13px; }
- </style>
- </head>
- <body>
- <main>
- <header>
- <h1>驾驶舱新鲜度与过期语义演示</h1>
- <p class="endpoint">GET /api/cockpit/v1/equipments/{deviceCode}/freshness?calibrationPeriodSeconds=60</p>
- <p class="endpoint">HTTP:空数据 200 · 非法参数 400 · 未认证 401 · 不存在 404 · 服务故障 500</p>
- </header>
- <section>
- <div class="toolbar">
- <button type="button" data-mode="current" class="active">当前</button>
- <button type="button" data-mode="stale">一次未更新</button>
- <button type="button" data-mode="expired">连续两次未更新</button>
- <button type="button" data-mode="expired-cache">缓存过期保留</button>
- <button type="button" data-mode="empty">成功空数据</button>
- <button type="button" data-mode="not-found">404</button>
- <button type="button" data-mode="server-error">500</button>
- </div>
- <div class="timeline">
- <div class="phase"><strong>T0</strong><span>数据更新</span></div>
- <div class="phase"><strong>T0 + 1 周期</strong><span>STALE,保留数据</span></div>
- <div class="phase"><strong>T0 + 2 周期</strong><span>EXPIRED,数据暂不可用</span></div>
- </div>
- <div id="detail" class="state-card CURRENT">
- <span id="badge" class="badge CURRENT">CURRENT</span>
- <h2 id="message">数据为最新</h2>
- <table><tbody id="fields"></tbody></table>
- </div>
- </section>
- <section>
- <h2>响应契约</h2>
- <p class="note">服务端只取 updatedAt 与 statusTime 中最新者;连续两个可配置校准周期未成功更新时标记 EXPIRED,但保留最后一次数据并显式标记。</p>
- <pre id="payload" class="mono"></pre>
- </section>
- </main>
- <script>
- const clock = '2026-09-02T10:00:00+08:00';
- const section = (state, reason, message, data) => ({ state, reason, message, data });
- const responses = {
- current: { code:200, data:{ requestId:'freshness-current', apiVersion:'v1', readOnly:true, deviceCode:'TC-PS-WS-202609020012',
- updatedAt:'2026-09-02T09:59:50+08:00', statusTime:'2026-09-02T09:59:55+08:00', serverUpdatedAt:clock,
- freshness:'CURRENT', staleReason:null, calibrationPeriodSeconds:60, missedCycles:0, dataAgeSeconds:5, retained:false, message:null,
- section:section('CURRENT','CURRENT',null,{ dataAgeSeconds:5 }) } },
- stale: { code:200, data:{ requestId:'freshness-stale', apiVersion:'v1', readOnly:true, deviceCode:'TC-PS-WS-202609020012',
- updatedAt:'2026-09-02T09:59:00+08:00', statusTime:null, serverUpdatedAt:clock,
- freshness:'STALE', staleReason:'ONE_UPDATE_CYCLE_MISSED', calibrationPeriodSeconds:60, missedCycles:1, dataAgeSeconds:60, retained:true, message:null,
- section:section('STALE','ONE_UPDATE_CYCLE_MISSED',null,{ dataAgeSeconds:60, retained:true }) } },
- expired: { code:200, data:{ requestId:'freshness-expired', apiVersion:'v1', readOnly:true, deviceCode:'TC-PS-WS-202609020012',
- updatedAt:'2026-09-02T09:58:00+08:00', statusTime:null, serverUpdatedAt:clock,
- freshness:'EXPIRED', staleReason:'TWO_UPDATE_CYCLES_MISSED', calibrationPeriodSeconds:60, missedCycles:2, dataAgeSeconds:120, retained:true, message:'数据暂不可用/已过期',
- section:section('EXPIRED','TWO_UPDATE_CYCLES_MISSED','数据暂不可用/已过期',{ dataAgeSeconds:120, retained:true }) } },
- 'expired-cache': { code:200, data:{ requestId:'freshness-expired-cache', apiVersion:'v1', readOnly:true, deviceCode:'TC-PS-WS-202609020012',
- updatedAt:'2026-09-02T09:57:30+08:00', statusTime:null, serverUpdatedAt:clock,
- freshness:'EXPIRED', staleReason:'TWO_UPDATE_CYCLES_MISSED', calibrationPeriodSeconds:60, missedCycles:3, dataAgeSeconds:150, retained:true, message:'数据暂不可用/已过期',
- section:section('EXPIRED','TWO_UPDATE_CYCLES_MISSED','数据暂不可用/已过期',{ dataAgeSeconds:150, retained:true }) } },
- empty: { code:200, data:{ requestId:'freshness-empty', apiVersion:'v1', readOnly:true, deviceCode:'TC-PS-WS-202609020012',
- updatedAt:null, statusTime:null, serverUpdatedAt:clock, freshness:'EMPTY', staleReason:null,
- calibrationPeriodSeconds:60, missedCycles:0, dataAgeSeconds:0, retained:false, message:null,
- section:section('EMPTY','NO_SOURCE',null,null) } },
- 'not-found': { code:404, error:{ category:'NOT_FOUND', message:'驾驶舱新鲜度数据不存在', requestId:'freshness-404', serverTime:clock } },
- 'server-error': { code:500, error:{ category:'SERVER_ERROR', message:'驾驶舱新鲜度数据暂不可用', requestId:'freshness-500', serverTime:clock } }
- };
- const fields = [
- ['updatedAt','数据更新时间'], ['statusTime','状态时间'], ['serverUpdatedAt','服务端时间'],
- ['staleReason','过期原因'], ['calibrationPeriodSeconds','校准周期(秒)'], ['missedCycles','未更新周期'],
- ['dataAgeSeconds','数据年龄(秒)'], ['retained','保留旧数据']
- ];
- function render(mode) {
- document.querySelectorAll('button[data-mode]').forEach(button => button.classList.toggle('active', button.dataset.mode === mode));
- const payload = responses[mode];
- const data = payload.data;
- const state = payload.error ? payload.error.category : data.freshness;
- const card = document.getElementById('detail');
- card.className = 'state-card ' + state;
- const badge = document.getElementById('badge');
- badge.className = 'badge ' + state;
- badge.textContent = state;
- document.getElementById('message').textContent = payload.error ? payload.error.message : (data.message || '数据为最新');
- document.getElementById('fields').innerHTML = payload.error
- ? `<tr><th>requestId</th><td class="mono">${payload.error.requestId}</td></tr><tr><th>serverTime</th><td class="mono">${payload.error.serverTime}</td></tr>`
- : fields.map(field => {
- const value = data[field[0]];
- return `<tr><th>${field[1]}</th><td class="mono">${value === null || value === undefined ? '未提供' : value}</td></tr>`;
- }).join('');
- document.getElementById('payload').textContent = JSON.stringify(payload, null, 2);
- }
- document.querySelectorAll('button[data-mode]').forEach(button => button.addEventListener('click', () => render(button.dataset.mode)));
- render('current');
- </script>
- </body>
- </html>
|