/** * 新鲜度领域对象(票 05 交字段与词汇,票 16 交渲染与轮询判定)。 * * 后端形状(`freshness-semantics-demo.html`、`CockpitFreshnessResponse`): * `{ updatedAt, statusTime, serverUpdatedAt, freshness: CURRENT|STALE|EXPIRED|EMPTY, * staleReason, calibrationPeriodSeconds, missedCycles, dataAgeSeconds, retained, message }` * * 票 16 的验收原句「连续两个配置周期未成功更新时显示过期或不可用」对应 `missedCycles >= 2`, * 本层把这条**判据**定下来(纯函数、可断言),但「什么时候去拉、轮询还是推送」归票 15/16。 * * 硬约束(票 16「缺失指标显示占位符,不显示虚假零值」): * 时间为 null 时保持 null,界面显示占位符;`dataAgeSeconds` 缺失也不回 0 * (0 秒会被读成「刚刚更新过」,比空白更误导)。 */ import { fieldReader } from './fields.js' /** 后端新鲜度四态。 */ export const FRESHNESS = { CURRENT: 'CURRENT', STALE: 'STALE', EXPIRED: 'EXPIRED', EMPTY: 'EMPTY', } /** 判定为「已过期」所需的未更新周期数(票 16 的「连续两个配置周期」)。 */ export const EXPIRED_MISSED_CYCLES = 2 /** * 新鲜度响应 → 领域对象。 * @param {object} raw 后端 `CockpitFreshnessResponse` */ export function toFreshness(raw) { const reader = fieldReader(raw) return { deviceCode: reader.optionalStr('deviceCode'), // 三个时间各自独立:缺失就是 null(界面显示占位符)。 updatedAt: reader.optionalStr('updatedAt'), statusTime: reader.optionalStr('statusTime'), serverUpdatedAt: reader.optionalStr('serverUpdatedAt'), freshness: reader.optionalStr('freshness'), staleReason: reader.optionalStr('staleReason'), calibrationPeriodSeconds: reader.optionalNum('calibrationPeriodSeconds'), missedCycles: reader.optionalNum('missedCycles'), dataAgeSeconds: reader.optionalNum('dataAgeSeconds'), // 「保留旧数据」:这是过期与不可用的分界(票 16 要求保留但不伪装成实时)。 retained: reader.bool('retained', false), message: reader.optionalStr('message'), missing: reader.missing, } } /** * 是否已过期。优先用后端给的 `freshness`,缺失时才按 `missedCycles` 回退判定 * ——前端不推翻后端结论,只在后端没给结论时兜底。 * @param {object} freshness toFreshness() 的结果 */ export function isExpired(freshness) { const current = freshness || {} if (current.freshness === FRESHNESS.EXPIRED) return true if (current.freshness === FRESHNESS.CURRENT || current.freshness === FRESHNESS.STALE) return false if (typeof current.missedCycles === 'number') return current.missedCycles >= EXPIRED_MISSED_CYCLES return false } /** 是否还有可用值(过期但保留旧值仍算「有值」)。 */ export function hasValue(freshness) { const current = freshness || {} if (current.freshness === FRESHNESS.EMPTY) return false if (current.retained) return true return !!current.updatedAt } /** * 数据年龄文案。缺失时给占位符而不是「0 秒」。 * @param {object} freshness toFreshness() 的结果 */ export function dataAgeText(freshness) { const age = (freshness && freshness.dataAgeSeconds) if (typeof age !== 'number' || !Number.isFinite(age) || age < 0) return '—' if (age < 60) return `${Math.round(age)} 秒前` if (age < 3600) return `${Math.round(age / 60)} 分钟前` return `${Math.round(age / 3600)} 小时前` }