| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206 |
- /**
- * 设备详情聚合领域对象(票 05;消费方是票 11「各分区可独立成功、失败、为空或过期」)。
- *
- * 后端形状(`equipment-detail-aggregate-demo.html`、`CockpitEquipmentDetailAggregateResponse`):
- * `{ deviceCode, aggregateState, serverTime,
- * archive|metrics|warnings|alarms|alarmWarningRelations|workOrders|timelines|latestImage:
- * { state: SUCCESS|EMPTY|STALE|FAILED, reason, message, data } }`
- *
- * **这是「分区独立」在契约层的原生形态**:每个分区自带 `state` 与 `reason`,
- * 后端把「档案读取失败但指标仍可用」直接表达出来。所以本层的关键动作是
- * 「把每个分区翻成一个 `section()`」,而**不是**让整个详情共用一个成败——
- * 那正是票 05 验收第 6 条与票 11 验收第 3 条要的行为。
- *
- * 注意 `STALE` 分区的 `data` 仍带值(后端保留旧值),落进 `section.retained`。
- */
- import { fieldReader } from './fields.js'
- import { SECTION_STATE, section, sectionStateFromBackend } from './sectionState.js'
- import { toFreshness } from './freshness.js'
- import { toAlarmEvent, toAlarmWarningRelation, toWarningEvent } from './events.js'
- import { toWorkOrder, toWorkOrderTimeline } from './workOrder.js'
- /** 聚合响应的分区名(顺序即界面顺序)。 */
- export const EQUIPMENT_SECTIONS = [
- 'archive',
- 'metrics',
- 'warnings',
- 'alarms',
- 'alarmWarningRelations',
- 'workOrders',
- 'timelines',
- 'latestImage',
- ]
- /** 设备档案 → 领域对象。 */
- export function toEquipmentArchive(raw) {
- const reader = fieldReader(raw, 'archive')
- const image = reader.optionalObj('latestImage')
- return {
- // 编码与名称是档案的标识字段:契约里必有,缺了要记一笔(其余描述性字段允许为空)。
- code: reader.str('deviceCode'),
- name: reader.str('deviceName'),
- model: reader.optionalStr('deviceModel'),
- spec: reader.optionalStr('deviceSpec'),
- manufacturer: reader.optionalStr('manufacturer'),
- type: reader.optionalStr('deviceType'),
- typeName: reader.optionalStr('deviceTypeName'),
- assetValue: reader.optionalNum('assetValue'),
- expectedServiceYears: reader.optionalNum('expectedServiceYears'),
- location: reader.optionalStr('equipmentLocation'),
- ownershipUnit: reader.optionalStr('ownershipUnit'),
- maintainer: reader.optionalStr('maintainer'),
- maintainerPhone: reader.optionalStr('maintainerPhone'),
- installTime: reader.optionalStr('installTime'),
- remark: reader.optionalStr('remark'),
- // 票 11:「最新图片只展示后端指定的一张」——后端档案里就是这个单值。
- image: image ? toDeviceImage(image) : null,
- missing: reader.missing,
- }
- }
- /** 设备图片(票 11:只认后端给的那一张,前端不挑)。 */
- export function toDeviceImage(raw) {
- const reader = fieldReader(raw, 'image')
- return {
- url: reader.str('url'),
- capturedAt: reader.optionalStr('capturedAt'),
- missing: reader.missing,
- }
- }
- /** 指标点位(趋势序列)。 */
- export function toMetricPoint(raw) {
- const reader = fieldReader(raw, 'metricPoint')
- return {
- value: reader.optionalNum('value'),
- status: reader.optionalStr('status'),
- sampledAt: reader.optionalStr('sampledAt'),
- freshness: reader.optionalStr('freshness'),
- }
- }
- /** 单个指标。 */
- export function toMetric(raw) {
- const reader = fieldReader(raw, 'metric')
- return {
- code: reader.str('metricCode'),
- name: reader.optionalStr('metricName'),
- // 缺失指标保持 null(票 16:不显示虚假零值);真实 0 会照原样保留。
- value: reader.optionalNum('value'),
- unit: reader.optionalStr('unit'),
- status: reader.optionalStr('status'),
- sampledAt: reader.optionalStr('sampledAt'),
- freshness: reader.optionalStr('freshness'),
- points: reader.optionalList('points').map(toMetricPoint),
- missing: reader.missing,
- }
- }
- /** 指标分区载荷(票 11 的「最新指标」)。 */
- export function toMetricsPayload(raw) {
- const reader = fieldReader(raw)
- return {
- deviceCode: reader.optionalStr('deviceCode'),
- state: reader.optionalStr('state'),
- stateReason: reader.optionalStr('stateReason'),
- metrics: reader.list('metrics').map(toMetric),
- missing: reader.missing,
- }
- }
- /**
- * 把一个后端分区翻成前端分区。
- * `data` 的转换按分区名分派;`null` 数据(失败/空)不调用转换函数。
- * @param {string} name EQUIPMENT_SECTIONS 之一
- * @param {object} raw 后端 `CockpitSectionResult`
- */
- export function toSection(name, raw) {
- const reader = fieldReader(raw, name)
- const state = sectionStateFromBackend(reader.optionalStr('state'))
- const reason = reader.optionalStr('reason')
- const message = reader.optionalStr('message')
- const payload = reader.raw.data
- // 失败/空分区可能仍带 data(后端保留旧值);只有真有值才转换。
- const hasPayload = payload !== undefined && payload !== null
- let data = null
- if (hasPayload) {
- data = convertSectionData(name, payload)
- }
- return section(state, {
- reason: message || reason,
- data,
- freshness: readSectionFreshness(name, payload),
- // 过期分区保留的是旧值:票 16 要求界面带过期标识而不是当实时值。
- retained: state === SECTION_STATE.STALE,
- })
- }
- function readSectionFreshness(name, payload) {
- if (name === 'metrics' && payload && typeof payload === 'object') {
- return String(payload.state || '')
- }
- return ''
- }
- function convertSectionData(name, payload) {
- if (name === 'archive') return toEquipmentArchive(payload)
- if (name === 'metrics') return toMetricsPayload(payload)
- if (name === 'warnings') return toList(payload, toWarningEvent)
- if (name === 'alarms') return toList(payload, toAlarmEvent)
- if (name === 'alarmWarningRelations') return toList(payload, toAlarmWarningRelation)
- if (name === 'workOrders') return toList(payload, toWorkOrder)
- if (name === 'timelines') return toList(payload, toWorkOrderTimeline)
- if (name === 'latestImage') return toDeviceImage(payload)
- // 后端加了新分区而前端还不认识:原样带出,不静默丢弃。
- return payload
- }
- function toList(payload, mapper) {
- if (!Array.isArray(payload)) return []
- return payload.map(mapper)
- }
- /**
- * 设备详情聚合 → 领域对象。每个分区各自成 `section()`,互不牵连。
- * @param {object} raw 后端 `CockpitEquipmentDetailAggregateResponse`
- */
- export function toEquipmentDetailAggregate(raw) {
- const base = fieldReader(raw)
- const sections = {}
- for (const name of EQUIPMENT_SECTIONS) {
- sections[name] = toSection(name, base.raw[name])
- }
- return {
- deviceCode: base.optionalStr('deviceCode'),
- // 后端给的整体态:只作为参考带出,界面分区状态以各分区自己的为准
- // (这样即使整体态漏标,成功分区也不会被拖成不可用)。
- aggregateState: base.optionalStr('aggregateState'),
- serverTime: base.optionalStr('serverTime'),
- sections,
- missing: base.missing,
- }
- }
- /** 设备新鲜度分区(后端 `freshness` 在独立端点上,不是聚合的一部分)。 */
- export function toDeviceFreshnessSection(raw) {
- const freshness = toFreshness(raw)
- const state = !freshness.freshness
- ? SECTION_STATE.FAILED
- : freshness.freshness === 'EXPIRED' || freshness.freshness === 'STALE'
- ? SECTION_STATE.STALE
- : freshness.freshness === 'EMPTY'
- ? SECTION_STATE.EMPTY
- : SECTION_STATE.SUCCESS
- return section(state, {
- reason: freshness.staleReason,
- data: freshness,
- freshness: freshness.freshness,
- retained: freshness.retained,
- })
- }
|