| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216 |
- /**
- * 设备状态池的**全局**数据源(维护者 2026-11-04)。
- *
- * 为什么单独成 store:状态池是全站口径——顶部状态栏在四个一级模式里都常驻,不能挂在
- * 「运行监测」的面板分区上,否则不进那个模式就不刷新(这正是本次要修的缺陷)。
- * 因此本 store 自己持有两个全局读模型:监测概览台账(在线/离线)与运行概况异常清单
- * (逐设备异常状态),在任何一级模式下都能取到同一份证据。
- *
- * 语义(见 domain 的 deviceStatusPools):工单/报警/预警可同时存在,只有正常与离线互斥。
- * 数据没到时 `pools.available:false`,界面如实降级,不补 0。
- */
- import { computed, ref } from 'vue'
- import { defineStore } from 'pinia'
- import { fetchMonitoringOverview, fetchMonitoringSituation } from '../services/monitoringApi.js'
- import { fetchCurrentEvents } from '../services/eventApi.js'
- import { EVENT_FAMILY } from '../domain/events.js'
- import { deviceStatusPools } from '../domain/monitoringPanels.js'
- import { useMapStore } from './mapStore.js'
- export const useDeviceStatusStore = defineStore('deviceStatusStore', () => {
- /** 监测概览分区(逐设备 monitoringState) */
- const overview = ref(null)
- /** 运行概况分区(逐设备异常状态) */
- const situation = ref(null)
- /** 当前预警/报警事件(用于「设备打了什么标签」:预警=有预警事件的设备,报警=有未处理报警的设备) */
- const warnings = ref(null)
- const alarms = ref(null)
- const loading = ref(false)
- const loaded = ref(false)
- /** 请求序号:换筛选时旧的在途响应作废。 */
- let token = 0
- /**
- * 三条读模型 → 设备证据。
- * 地图那份由 mapStore 提供(它本来就在全模式加载),所以未关联设备的业务状态不必重取。
- */
- /** 从事件分区取出「涉及的设备编码」(去重)。事件行缺设备编码时不计数,不臆造。 */
- function eventDeviceCodes(section) {
- const rows = section && section.data && section.data.sections && section.data.sections.events
- ? section.data.sections.events.data : null
- const codes = new Set()
- for (const row of rows || []) {
- const code = row && (row.deviceCode || '')
- if (code) codes.add(String(code).trim())
- }
- return codes
- }
- /**
- * 设备标签口径(维护者 2026-11-05):
- * · 基础属性只有「正常 / 离线」二选一(按台账 monitoringState);
- * · 报警、预警、工单、独立设备是**可叠加标签**,一台设备可同时带上多个;
- * · 图例只数设备,不数监测点。
- * 因此四类标签各自独立去重,互不排斥,也不与正常/离线互斥。
- */
- function records() {
- const mapStore = useMapStore()
- const list = []
- const ledger = overview.value && overview.value.data && overview.value.data.devices
- for (const device of ledger || []) {
- list.push({ deviceCode: device && device.code, monitoringState: device && device.status })
- }
- for (const object of mapStore.summary.objects || []) {
- if (object && object.kind === 'unassociated-equipment') {
- list.push({ deviceCode: object.code, labels: ['UNASSOCIATED'] })
- }
- }
- return list
- }
- const pools = computed(() => {
- const ledger = overview.value && overview.value.data && overview.value.data.devices
- const devices = Array.isArray(ledger) ? ledger : []
- if (!devices.length) return { available: false, counts: {}, total: null }
- const mapStore = useMapStore()
- const unassociated = new Set(
- (mapStore.summary.objects || [])
- .filter((object) => object && object.kind === 'unassociated-equipment' && object.code)
- .map((object) => String(object.code).trim()),
- )
- const rowsOf = (section) => {
- const events = section && section.data && section.data.sections && section.data.sections.events
- return (events && events.data) || []
- }
- const workOrders = new Set()
- for (const section of [warnings.value, alarms.value]) {
- for (const row of rowsOf(section)) {
- const brief = row && row.workOrder
- const state = brief && typeof brief.stateCode === 'number' ? brief.stateCode : null
- const open = state !== null && ((state >= 0 && state <= 4) || state === 6)
- if (open && row.deviceCode) workOrders.add(String(row.deviceCode).trim())
- }
- }
- // 台账里的设备编码集合:**状态池只数设备**(维护者 2026-11-04/05 口径)。
- // 为什么要拿它做交集:现网事件表里存着**不是设备**的编码(实测 15/16/20、
- // TEST-CBYL5432168 —— 设备档案接口逐个 404,它们是历史脏值或类型码),
- // 若直接按事件行的 deviceCode 入池,预警池会多报 4 台「不存在的设备」(图例 7 台,
- // 而地图最多只有 2 个坐标点)。设备池里只该有设备台账里存在的设备。
- const ledgerCodes = new Set(devices.map((device) => String((device && device.code) || '').trim()).filter(Boolean))
- const onlyDevices = (codes) => new Set([...codes].filter((code) => ledgerCodes.has(code)))
- // 设备明细:基础属性(NORMAL/OFFLINE,互斥)+ 可叠加标签(ALARM/WARNING/WORK_ORDER/UNASSOCIATED)
- const tagSets = {
- ALARM: onlyDevices(eventDeviceCodes(alarms.value)),
- WARNING: onlyDevices(eventDeviceCodes(warnings.value)),
- WORK_ORDER: onlyDevices(workOrders),
- UNASSOCIATED: onlyDevices(unassociated),
- }
- const byCode = new Map()
- for (const device of devices) {
- // 台账行是**领域字段**:code/name/status(适配层已从 deviceCode/monitoringState 改名)。
- const code = String((device && device.code) || '').trim()
- if (!code) continue
- const state = String((device && device.status) || '').toUpperCase()
- byCode.set(code, {
- deviceCode: code,
- deviceName: (device && device.name) || code,
- // 只有正常与离线互斥;其余按标签判断
- base: state === 'OFFLINE' ? 'OFFLINE' : state === 'ONLINE' ? 'NORMAL' : '',
- tags: Object.keys(tagSets).filter((key) => tagSets[key].has(code)),
- })
- }
- // 在线/离线按台账状态**显式入桶**,不用「总数-离线」倒推:那样任何没有状态
- // (UNKNOWN/空)的设备都会被算成在线,把「不知道」冒充成「在线」。
- let online = 0
- let offline = 0
- let unknownState = 0
- for (const device of devices) {
- const state = String((device && device.status) || '').toUpperCase()
- if (state === 'ONLINE') online += 1
- else if (state === 'OFFLINE') offline += 1
- else unknownState += 1
- }
- return {
- available: true,
- total: devices.length,
- devices: [...byCode.values()],
- counts: {
- // 六个池与 devices[] 的标签**同源同数**(都读 tagSets),不重复计算第二遍。
- ALARM: tagSets.ALARM.size,
- WARNING: tagSets.WARNING.size,
- WORK_ORDER: tagSets.WORK_ORDER.size,
- NORMAL: online,
- OFFLINE: offline,
- // 台账没给状态(或给了认不出的值)的台数:不并入在线,也不并入离线。
- UNKNOWN_STATE: unknownState,
- UNASSOCIATED: tagSets.UNASSOCIATED.size,
- },
- }
- })
- async function refresh(options = {}) {
- const mine = ++token
- loading.value = true
- try {
- const query = {
- industry: options.industry,
- scope: options.scope,
- deviceType: options.deviceType,
- config: options.config,
- fetchImpl: options.fetchImpl,
- }
- const [overviewSection, situationSection, warningSection, alarmSection] = await Promise.all([
- fetchMonitoringOverview(query),
- fetchMonitoringSituation(query),
- fetchCurrentEvents({ mode: EVENT_FAMILY.WARNING, page: 1, pageSize: 1000, config: options.config, fetchImpl: options.fetchImpl }),
- fetchCurrentEvents({ mode: EVENT_FAMILY.ALARM, page: 1, pageSize: 1000, config: options.config, fetchImpl: options.fetchImpl }),
- ])
- if (mine !== token) return pools.value
- overview.value = overviewSection
- situation.value = situationSection
- warnings.value = warningSection
- alarms.value = alarmSection
- loaded.value = true
- } finally {
- if (mine === token) loading.value = false
- }
- return pools.value
- }
- /** 仅测试/重置用。 */
- function reset() {
- token += 1
- overview.value = null
- situation.value = null
- loading.value = false
- loaded.value = false
- }
- /**
- * 按选中的状态行筛选设备(与图例同一份台账/标签)。
- * 空集合=不筛;「无设备监测点」不参与设备筛选(它是点位特殊状态)。
- * @param {string[]} keys 选中的图例键
- * @returns {Set<string>|null} 命中的设备编码集;null=不筛
- */
- function filterDeviceCodes(keys) {
- const all = Array.isArray(keys) ? keys.filter(Boolean) : []
- if (!all.length) return null
- // 「无设备监测点」是点位特殊状态:只筛没有关联设备的空白点位,不参与设备标签匹配。
- const emptyPoints = all.includes('EMPTY')
- const selected = all.filter((key) => key !== 'EMPTY')
- if (!selected.length) return { codes: new Set(), emptyPoints }
- const current = pools.value
- if (!current.available) return new Set()
- const codes = new Set()
- for (const device of current.devices || []) {
- const hit = selected.some((key) => (key === 'NORMAL' || key === 'OFFLINE')
- ? device.base === key
- : device.tags.includes(key))
- if (hit) codes.add(device.deviceCode)
- }
- return { codes, emptyPoints }
- }
- return { overview, situation, pools, filterDeviceCodes, loading, loaded, refresh, reset }
- })
|