deviceStatusStore.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. /**
  2. * 设备状态池的**全局**数据源(维护者 2026-11-04)。
  3. *
  4. * 为什么单独成 store:状态池是全站口径——顶部状态栏在四个一级模式里都常驻,不能挂在
  5. * 「运行监测」的面板分区上,否则不进那个模式就不刷新(这正是本次要修的缺陷)。
  6. * 因此本 store 自己持有两个全局读模型:监测概览台账(在线/离线)与运行概况异常清单
  7. * (逐设备异常状态),在任何一级模式下都能取到同一份证据。
  8. *
  9. * 语义(见 domain 的 deviceStatusPools):工单/报警/预警可同时存在,只有正常与离线互斥。
  10. * 数据没到时 `pools.available:false`,界面如实降级,不补 0。
  11. */
  12. import { computed, ref } from 'vue'
  13. import { defineStore } from 'pinia'
  14. import { fetchMonitoringOverview, fetchMonitoringSituation } from '../services/monitoringApi.js'
  15. import { fetchCurrentEvents } from '../services/eventApi.js'
  16. import { EVENT_FAMILY } from '../domain/events.js'
  17. import { deviceStatusPools } from '../domain/monitoringPanels.js'
  18. import { useMapStore } from './mapStore.js'
  19. export const useDeviceStatusStore = defineStore('deviceStatusStore', () => {
  20. /** 监测概览分区(逐设备 monitoringState) */
  21. const overview = ref(null)
  22. /** 运行概况分区(逐设备异常状态) */
  23. const situation = ref(null)
  24. /** 当前预警/报警事件(用于「设备打了什么标签」:预警=有预警事件的设备,报警=有未处理报警的设备) */
  25. const warnings = ref(null)
  26. const alarms = ref(null)
  27. const loading = ref(false)
  28. const loaded = ref(false)
  29. /** 请求序号:换筛选时旧的在途响应作废。 */
  30. let token = 0
  31. /**
  32. * 三条读模型 → 设备证据。
  33. * 地图那份由 mapStore 提供(它本来就在全模式加载),所以未关联设备的业务状态不必重取。
  34. */
  35. /** 从事件分区取出「涉及的设备编码」(去重)。事件行缺设备编码时不计数,不臆造。 */
  36. function eventDeviceCodes(section) {
  37. const rows = section && section.data && section.data.sections && section.data.sections.events
  38. ? section.data.sections.events.data : null
  39. const codes = new Set()
  40. for (const row of rows || []) {
  41. const code = row && (row.deviceCode || '')
  42. if (code) codes.add(String(code).trim())
  43. }
  44. return codes
  45. }
  46. /**
  47. * 设备标签口径(维护者 2026-11-05):
  48. * · 基础属性只有「正常 / 离线」二选一(按台账 monitoringState);
  49. * · 报警、预警、工单、独立设备是**可叠加标签**,一台设备可同时带上多个;
  50. * · 图例只数设备,不数监测点。
  51. * 因此四类标签各自独立去重,互不排斥,也不与正常/离线互斥。
  52. */
  53. function records() {
  54. const mapStore = useMapStore()
  55. const list = []
  56. const ledger = overview.value && overview.value.data && overview.value.data.devices
  57. for (const device of ledger || []) {
  58. list.push({ deviceCode: device && device.code, monitoringState: device && device.status })
  59. }
  60. for (const object of mapStore.summary.objects || []) {
  61. if (object && object.kind === 'unassociated-equipment') {
  62. list.push({ deviceCode: object.code, labels: ['UNASSOCIATED'] })
  63. }
  64. }
  65. return list
  66. }
  67. const pools = computed(() => {
  68. const ledger = overview.value && overview.value.data && overview.value.data.devices
  69. const devices = Array.isArray(ledger) ? ledger : []
  70. if (!devices.length) return { available: false, counts: {}, total: null }
  71. const mapStore = useMapStore()
  72. const unassociated = new Set(
  73. (mapStore.summary.objects || [])
  74. .filter((object) => object && object.kind === 'unassociated-equipment' && object.code)
  75. .map((object) => String(object.code).trim()),
  76. )
  77. const rowsOf = (section) => {
  78. const events = section && section.data && section.data.sections && section.data.sections.events
  79. return (events && events.data) || []
  80. }
  81. const workOrders = new Set()
  82. for (const section of [warnings.value, alarms.value]) {
  83. for (const row of rowsOf(section)) {
  84. const brief = row && row.workOrder
  85. const state = brief && typeof brief.stateCode === 'number' ? brief.stateCode : null
  86. const open = state !== null && ((state >= 0 && state <= 4) || state === 6)
  87. if (open && row.deviceCode) workOrders.add(String(row.deviceCode).trim())
  88. }
  89. }
  90. // 台账里的设备编码集合:**状态池只数设备**(维护者 2026-11-04/05 口径)。
  91. // 为什么要拿它做交集:现网事件表里存着**不是设备**的编码(实测 15/16/20、
  92. // TEST-CBYL5432168 —— 设备档案接口逐个 404,它们是历史脏值或类型码),
  93. // 若直接按事件行的 deviceCode 入池,预警池会多报 4 台「不存在的设备」(图例 7 台,
  94. // 而地图最多只有 2 个坐标点)。设备池里只该有设备台账里存在的设备。
  95. const ledgerCodes = new Set(devices.map((device) => String((device && device.code) || '').trim()).filter(Boolean))
  96. const onlyDevices = (codes) => new Set([...codes].filter((code) => ledgerCodes.has(code)))
  97. // 设备明细:基础属性(NORMAL/OFFLINE,互斥)+ 可叠加标签(ALARM/WARNING/WORK_ORDER/UNASSOCIATED)
  98. const tagSets = {
  99. ALARM: onlyDevices(eventDeviceCodes(alarms.value)),
  100. WARNING: onlyDevices(eventDeviceCodes(warnings.value)),
  101. WORK_ORDER: onlyDevices(workOrders),
  102. UNASSOCIATED: onlyDevices(unassociated),
  103. }
  104. const byCode = new Map()
  105. for (const device of devices) {
  106. // 台账行是**领域字段**:code/name/status(适配层已从 deviceCode/monitoringState 改名)。
  107. const code = String((device && device.code) || '').trim()
  108. if (!code) continue
  109. const state = String((device && device.status) || '').toUpperCase()
  110. byCode.set(code, {
  111. deviceCode: code,
  112. deviceName: (device && device.name) || code,
  113. // 只有正常与离线互斥;其余按标签判断
  114. base: state === 'OFFLINE' ? 'OFFLINE' : state === 'ONLINE' ? 'NORMAL' : '',
  115. tags: Object.keys(tagSets).filter((key) => tagSets[key].has(code)),
  116. })
  117. }
  118. // 在线/离线按台账状态**显式入桶**,不用「总数-离线」倒推:那样任何没有状态
  119. // (UNKNOWN/空)的设备都会被算成在线,把「不知道」冒充成「在线」。
  120. let online = 0
  121. let offline = 0
  122. let unknownState = 0
  123. for (const device of devices) {
  124. const state = String((device && device.status) || '').toUpperCase()
  125. if (state === 'ONLINE') online += 1
  126. else if (state === 'OFFLINE') offline += 1
  127. else unknownState += 1
  128. }
  129. return {
  130. available: true,
  131. total: devices.length,
  132. devices: [...byCode.values()],
  133. counts: {
  134. // 六个池与 devices[] 的标签**同源同数**(都读 tagSets),不重复计算第二遍。
  135. ALARM: tagSets.ALARM.size,
  136. WARNING: tagSets.WARNING.size,
  137. WORK_ORDER: tagSets.WORK_ORDER.size,
  138. NORMAL: online,
  139. OFFLINE: offline,
  140. // 台账没给状态(或给了认不出的值)的台数:不并入在线,也不并入离线。
  141. UNKNOWN_STATE: unknownState,
  142. UNASSOCIATED: tagSets.UNASSOCIATED.size,
  143. },
  144. }
  145. })
  146. async function refresh(options = {}) {
  147. const mine = ++token
  148. loading.value = true
  149. try {
  150. const query = {
  151. industry: options.industry,
  152. scope: options.scope,
  153. deviceType: options.deviceType,
  154. config: options.config,
  155. fetchImpl: options.fetchImpl,
  156. }
  157. const [overviewSection, situationSection, warningSection, alarmSection] = await Promise.all([
  158. fetchMonitoringOverview(query),
  159. fetchMonitoringSituation(query),
  160. fetchCurrentEvents({ mode: EVENT_FAMILY.WARNING, page: 1, pageSize: 1000, config: options.config, fetchImpl: options.fetchImpl }),
  161. fetchCurrentEvents({ mode: EVENT_FAMILY.ALARM, page: 1, pageSize: 1000, config: options.config, fetchImpl: options.fetchImpl }),
  162. ])
  163. if (mine !== token) return pools.value
  164. overview.value = overviewSection
  165. situation.value = situationSection
  166. warnings.value = warningSection
  167. alarms.value = alarmSection
  168. loaded.value = true
  169. } finally {
  170. if (mine === token) loading.value = false
  171. }
  172. return pools.value
  173. }
  174. /** 仅测试/重置用。 */
  175. function reset() {
  176. token += 1
  177. overview.value = null
  178. situation.value = null
  179. loading.value = false
  180. loaded.value = false
  181. }
  182. /**
  183. * 按选中的状态行筛选设备(与图例同一份台账/标签)。
  184. * 空集合=不筛;「无设备监测点」不参与设备筛选(它是点位特殊状态)。
  185. * @param {string[]} keys 选中的图例键
  186. * @returns {Set<string>|null} 命中的设备编码集;null=不筛
  187. */
  188. function filterDeviceCodes(keys) {
  189. const all = Array.isArray(keys) ? keys.filter(Boolean) : []
  190. if (!all.length) return null
  191. // 「无设备监测点」是点位特殊状态:只筛没有关联设备的空白点位,不参与设备标签匹配。
  192. const emptyPoints = all.includes('EMPTY')
  193. const selected = all.filter((key) => key !== 'EMPTY')
  194. if (!selected.length) return { codes: new Set(), emptyPoints }
  195. const current = pools.value
  196. if (!current.available) return new Set()
  197. const codes = new Set()
  198. for (const device of current.devices || []) {
  199. const hit = selected.some((key) => (key === 'NORMAL' || key === 'OFFLINE')
  200. ? device.base === key
  201. : device.tags.includes(key))
  202. if (hit) codes.add(device.deviceCode)
  203. }
  204. return { codes, emptyPoints }
  205. }
  206. return { overview, situation, pools, filterDeviceCodes, loading, loaded, refresh, reset }
  207. })