| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384 |
- /**
- * 新鲜度领域对象(票 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)} 小时前`
- }
|