| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251 |
- /**
- * 两级详情(票 10;CONTEXT.md「两级详情」):地图/台账 → 点位摘要 + 设备卡片 → 设备详情。
- *
- * 为什么单独成 domain 模块:这是一条**导航状态机**(先点位、再设备),与「取数成没成」
- * (`domain/sectionState.js`)和「地图上画什么」(`domain/pointStatus.js`)都不是一回事。
- * 把它放这里,store 只管取数、组件只管画,逻辑脚本能脱浏览器把每一级与每一次返回都走一遍。
- *
- * 「关联设备不作为独立地图标记重复渲染」在这里是**结构性**的:本模块只产出选择与卡片视图,
- * 没有任何一条路径会把设备写回地图标记计划 —— 地图对象永远只来自 `mapStore` 的地图摘要
- * (后端只给「监测点 + 未关联设备」,关联设备根本不在那份载荷里)。
- */
- import { legendIconFor, visualForStatus } from './pointStatus.js'
- // 票 44:设备状态必须走**独立**的设备词表,不得借用点位表(决策票 07/10 的边界)。
- import { deviceVisualForStatus } from './deviceStatus.js'
- /** 详情层级(也是 `data-detail-level` 的取值)。 */
- export const DETAIL_LEVEL = {
- NONE: 'none',
- POINT: 'point',
- EQUIPMENT: 'equipment',
- }
- /** 缺失值的占位符:界面显示它,而不是把「没给」画成 0 或空白(票 16 的立场)。 */
- export const DETAIL_PLACEHOLDER = '—'
- /** 地图对象的两种来源(与 `domain/mapSummary.js` 的 `MAP_OBJECT_KIND` 同名,见那里的定义)。 */
- const KIND_MONITORING_POINT = 'monitoring-point'
- const KIND_UNASSOCIATED_EQUIPMENT = 'unassociated-equipment'
- /** 空选择:什么都没打开(界面据此不渲染详情浮层)。 */
- export function emptyDetailSelection() {
- return { level: DETAIL_LEVEL.NONE, pointCode: '', deviceCode: '' }
- }
- /**
- * 地图对象 → 详情选择。
- *
- * 监测点进**第一级**(点位摘要 + 设备卡片);未关联设备没有点位可摘要,
- * 直接进**第二级**(设备详情)——「优先以监测点作为可点击地图对象」(CONTEXT.md)
- * 不等于「别的对象点了没反应」。认不出的对象返回 `null`:不猜、也不打开一张空卡片。
- * @param {object} object 地图对象(`domain/mapSummary.js` 的形状)
- */
- export function selectionFromMapObject(object) {
- const item = object || {}
- const code = item.code || ''
- if (!code) return null
- if (item.kind === KIND_MONITORING_POINT) {
- return { level: DETAIL_LEVEL.POINT, pointCode: code, deviceCode: '' }
- }
- if (item.kind === KIND_UNASSOCIATED_EQUIPMENT) {
- return { level: DETAIL_LEVEL.EQUIPMENT, pointCode: '', deviceCode: code }
- }
- return null
- }
- /** 打开某个监测点:回到第一级,并把设备选择清掉(换点位不该留着上一台的选中态)。 */
- export function openPoint(selection, pointCode) {
- if (!pointCode) return selection || emptyDetailSelection()
- return { level: DETAIL_LEVEL.POINT, pointCode, deviceCode: '' }
- }
- /**
- * 选择设备卡片:进第二级。点位编码**保留**(返回第一级要靠它),
- * 从地图上的未关联设备进来时 `pointCode` 本来就是空的。
- */
- export function openDevice(selection, deviceCode) {
- const current = selection || emptyDetailSelection()
- if (!deviceCode) return current
- return { level: DETAIL_LEVEL.EQUIPMENT, pointCode: current.pointCode || '', deviceCode }
- }
- /**
- * 返回第一级。没有点位可回(未关联设备直接进的第二级)时收成空选择 ——
- * 「返回点位摘要」不该把用户送到一个并不存在的点位。
- */
- export function backToPoint(selection) {
- const current = selection || emptyDetailSelection()
- if (!current.pointCode) return emptyDetailSelection()
- return { level: DETAIL_LEVEL.POINT, pointCode: current.pointCode, deviceCode: '' }
- }
- /**
- * 关闭详情:回到空选择(界面据此收掉浮层)。
- */
- export function closeDetail() {
- return emptyDetailSelection()
- }
- /**
- * 同一个点位目标是否已经打开(含已经选到设备的那一级)。
- * store 用它决定「点同一个点位不必再取一次数」,纯函数便于断言。
- */
- export function isSamePointTarget(selection, pointCode) {
- const current = selection || emptyDetailSelection()
- if (!pointCode) return false
- return current.pointCode === pointCode && current.level !== DETAIL_LEVEL.NONE
- }
- /**
- * 后端状态 + 后端标签 → 标签视图(**监测点**用;标签优先用后端给的词)。
- *
- * 注意与 `deviceStatusView()` 的分工:本函数读的是**点位**状态词表,
- * 给设备配色必须走 `deviceStatusView()` —— 拿点位表给设备配色是决策票 07/10 点名的越界。
- *
- * `icon`(票 46 返工第 2 条):走 `legendIconFor` **单源接缝** —— 详情里的状态标签与
- * 一张图顶部状态栏共用同一份图标登记与同一张词表(色键/形状/文字同源)。
- */
- export function statusView(status, labelOverride = '') {
- const visual = visualForStatus(status)
- return {
- key: visual.key,
- tone: visual.tone,
- shape: visual.shape,
- label: labelOverride || visual.label,
- icon: legendIconFor(visual.key),
- }
- }
- /**
- * 后端设备状态 + 标签 → 标签视图(票 44 修正越界)。
- *
- * 为什么必须另立一个函数:早先设备卡片也走 `statusView()`,于是**设备状态被拿点位表解释** ——
- * 而两张表的取值集不同(点位有 `EMPTY`「无设备」,设备五态**没有**),
- * 一旦后端给设备回一个点位专有的状态,界面会画出一个设备根本不可能有的态。
- * 现在设备的色/形/文字一律来自 `domain/deviceStatus.js`(独立平行表)。
- *
- * `icon`(票 46 返工第 2 条):与顶部状态栏同源(`legendIconFor`,设备与点位词表的键同名,
- * 五态 + 未发布都有登记);**取值仍走设备表**,只借图标登记,不借状态语义。
- */
- export function deviceStatusView(status, labelOverride = '') {
- const visual = deviceVisualForStatus(status)
- return {
- key: visual.key,
- tone: visual.tone,
- shape: visual.shape,
- label: labelOverride || visual.label,
- icon: legendIconFor(visual.key),
- }
- }
- /**
- * 状态原因 → 一句话。
- *
- * 两种形状都吃:点位详情是 `statusReasons[{status,deviceCode,reason}]`,
- * 设备卡片是 `statusReasons: string[]`。去重保序(后端顺序即优先级顺序,前端不重排)。
- * 一个原因都没有时回空串 —— 界面显示占位符,**不**写成「正常」。
- */
- export function reasonText(reasons) {
- const list = Array.isArray(reasons) ? reasons : []
- const texts = list
- .map((item) => (typeof item === 'string' ? item : (item && item.reason) || ''))
- .map((text) => String(text).trim())
- .filter(Boolean)
- return [...new Set(texts)].join(';')
- }
- /**
- * 关联设备数量。
- *
- * 后端 `deviceStatistics.totalDeviceCount` 是权威值(含「确实 0 台」);
- * 整段统计缺失(`statistics.missing` 记着,见 `domain/pointDetail.js`)时退回
- * 「已返回的卡片数」,而不是把「没给统计」显示成 0 台。
- * @param {object} detail `toMonitoringPointDetail()` 的领域对象
- */
- export function associatedDeviceCount(detail) {
- const current = detail || {}
- const statistics = current.statistics || null
- const statisticsGiven = statistics && Array.isArray(statistics.missing) && statistics.missing.length === 0
- if (statisticsGiven && Number.isFinite(statistics.total)) return statistics.total
- return Array.isArray(current.devices) ? current.devices.length : 0
- }
- /**
- * 设备卡片 → 卡片视图。顺序由调用方保持(`deviceCards()` 不排序)。
- * @param {object} card `toDeviceCard()` 的领域对象
- */
- export function deviceCardView(card) {
- const current = card || {}
- return {
- code: current.code || DETAIL_PLACEHOLDER,
- name: current.name || DETAIL_PLACEHOLDER,
- typeName: current.typeName || '',
- // 票 44 修正越界:**设备**状态走设备词表(原先是 `statusView`,即拿点位表解释设备)。
- status: deviceStatusView(current.status, current.statusLabel),
- reason: reasonText(current.statusReasons),
- statusTime: current.statusTime || '',
- }
- }
- /**
- * 设备卡片列表 —— **原样保持后端返回顺序**(票 10 验收第 3 条)。
- * 这里刻意不排序、不按状态优先级重排:顺序是后端的业务语义(谁重要由后端说)。
- * @param {object} detail `toMonitoringPointDetail()` 的领域对象
- */
- export function deviceCards(detail) {
- const devices = (detail && detail.devices) || []
- return devices.map(deviceCardView)
- }
- /**
- * 点位摘要(第一级的五行):名称 / 编码 / 状态 / 原因 / 关联设备数量。
- * @param {object} detail `toMonitoringPointDetail()` 的领域对象
- */
- export function pointSummary(detail) {
- const current = detail || {}
- return {
- name: current.name || DETAIL_PLACEHOLDER,
- code: current.code || DETAIL_PLACEHOLDER,
- status: statusView(current.status, current.statusLabel),
- reason: reasonText(current.statusReasons),
- deviceCount: associatedDeviceCount(current),
- }
- }
- /**
- * 点位设备计数 KPI 行(票 46 范围第 1 条:能用 KPI/状态卡表达的不再是大段纯文字)。
- *
- * 数据来源是后端 `deviceStatistics`(票 05 已适配成 `statistics`),前端**不重算**:
- * · 统计整段缺失(`statistics.missing` 非空)→ 回 `null`,界面**不画 KPI 行**,
- * 而不是画一排 0 ——「没给统计」与「各态都是 0 台」是两件事(票 16 的虚假零值立场);
- * · 计数 0 是真实值(「确实没有报警设备」),照原样画。
- * 色键/文字走**设备词表**(`deviceStatusView`),不借点位表 —— 这里数的是设备。
- * @param {object} detail `toMonitoringPointDetail()` 的领域对象
- */
- export function pointDeviceCountsView(detail) {
- const statistics = (detail && detail.statistics) || null
- if (!statistics || !Array.isArray(statistics.missing) || statistics.missing.length > 0) return null
- const rows = [
- { key: 'ALARM', label: '报警', count: statistics.alarm },
- { key: 'WARNING', label: '预警', count: statistics.warning },
- { key: 'WORK_ORDER', label: '工单处置', count: statistics.workOrder },
- { key: 'NORMAL', label: '正常', count: statistics.normal },
- { key: 'OFFLINE', label: '离线', count: statistics.offline },
- ]
- return rows.map((row) => ({
- ...row,
- // 色键走**设备词表**(数的是设备,不借点位表 —— 决策票 07/10 的边界,票 44 已收口)。
- tone: deviceStatusView(row.key).tone,
- }))
- }
- /** 从已加载的点位详情里取某台设备的卡片视图(第二级的表头用它;找不到回 null)。 */
- export function deviceCardOf(detail, deviceCode) {
- if (!deviceCode) return null
- const cards = deviceCards(detail)
- return cards.find((card) => card.code === deviceCode) || null
- }
|