import { normalizeDeviceRow } from './waterSupplyUiModel.js' import { getFacilityStatusMeta, getMonitoringPointIcon, getMonitoringPointStatus } from './gisMapColors.js' /** * 供水首页工作台数据模型(T05 #48)。 * * 首页只组合既有只读接口,不在本模块调用后端;所有口径(供水范围、工单状态、 * 报警等级、在线率)都收敛到这里,便于用 node 单测锁定。 */ // 工单状态 0-7,与 ADR-0010 权威枚举一致 const WORK_ORDER_STATUS_META = { 0: { text: '待派单', type: 'danger' }, 1: { text: '待接单', type: 'warning' }, 2: { text: '已接单', type: 'primary' }, 3: { text: '处理中', type: 'warning' }, 4: { text: '待验收', type: 'primary' }, 5: { text: '已结案', type: 'success' }, 6: { text: '延期中', type: 'warning' }, 7: { text: '已取消', type: 'info' } } const WORK_ORDER_LEVEL_META = { 0: { text: '紧急', type: 'danger' }, 1: { text: '一般', type: 'warning' }, 2: { text: '低', type: 'info' } } const ALARM_LEVEL_META = { 1: { text: '一级报警', type: 'danger' }, 2: { text: '二级报警', type: 'warning' }, 3: { text: '三级报警', type: 'primary' }, 4: { text: '四级报警', type: 'success' } } /** * 供水设备根类型:设备接口 /equipment-base 的数据范围参数,取值对应 EquipmentTypeEnum * 的 1-供水设备。设备与工单两处取数都以此限定供水范围,避免各自写死。 */ export const WATER_DEVICE_ROOT_TYPE_ID = 1 export const HOME_QUICK_ENTRIES = [ { key: 'abnormal', label: '设备异常管理', suffix: '/device/abnormal', icon: 'WarningFilled' }, { key: 'alarm', label: '报警管理', suffix: '/monitorAlarm/index', icon: 'BellFilled' }, { key: 'audit', label: '报警审核', suffix: '/monitorAlarm/audit', query: { rootTypeId: WATER_DEVICE_ROOT_TYPE_ID }, icon: 'Finished' }, { key: 'map', label: '监测报警一张图', suffix: '/waterSupplyAlarmGIS', query: { rootTypeId: WATER_DEVICE_ROOT_TYPE_ID }, icon: 'MapLocation' } ] /** 供水工单统计/列表的固定范围参数:设备父类别=供水。 */ export function buildHomeWorkOrderQuery(extra = {}) { return { deviceTypeParentId: String(WATER_DEVICE_ROOT_TYPE_ID), pageNum: 1, pageSize: 5, ...extra } } /** 供水设施总数:overview 会追加 facilityType=total 合计行,须排除避免重复累计。 */ export function normalizeFacilityTotal(facilityOverview = []) { const rows = Array.isArray(facilityOverview) ? facilityOverview : [] return rows .filter(item => item && item.facilityType !== 'total') .reduce((sum, item) => sum + Number(item.totalCount ?? item.count ?? 0), 0) } /** A 区指挥台四项核心总量。 */ export function normalizeHomeKpis({ facilityRows = [], ledgerStats = {}, alarmStats = {} } = {}) { const realtimeAlarm = Number(alarmStats.realtimeAlarmCount ?? alarmStats.realtimeCount ?? 0) return [ { key: 'facility', label: '设施总数', value: normalizeFacilityTotal(facilityRows), unit: '处', tone: 'primary' }, { key: 'device', label: '监测设备', value: Number(ledgerStats.total ?? 0), unit: '台', tone: 'primary' }, { key: 'online', label: '在线设备', value: Number(ledgerStats.online ?? 0), unit: '台', tone: 'primary' }, { key: 'alarm', label: '实时报警', value: realtimeAlarm, unit: '条', tone: realtimeAlarm > 0 ? 'danger' : 'primary' } ] } /** * 统一设备统计口径:/equipment-base/statistics 返回 total/online/offline/alarm, * 首页所需的在线率在此推导,避免各页面各自计算。 */ export function normalizeDeviceStatistics(stats = {}) { const total = Number(stats.total ?? 0) const online = Number(stats.online ?? 0) const offline = Number(stats.offline ?? Math.max(total - online, 0)) const alarm = Number(stats.alarm ?? 0) return { total, online, offline, alarm, onlineRate: total > 0 ? Math.round(online * 1000 / total) / 10 : 0 } } function countByStatus(stat, status) { const counts = Array.isArray(stat?.counts) ? stat.counts : [] const hit = counts.find(item => Number(item?.status) === Number(status)) return Number(hit?.count ?? 0) } /** 工单状态统计:待处理 0/1,处理中 2/3/6,待验收 4,已完成 5/7。 */ export function normalizeWorkOrderStatusStat(stat = {}) { const pendingCount = countByStatus(stat, 0) + countByStatus(stat, 1) const processingCount = countByStatus(stat, 2) + countByStatus(stat, 3) + countByStatus(stat, 6) const verifyingCount = countByStatus(stat, 4) const finishedCount = countByStatus(stat, 5) + countByStatus(stat, 7) const byStatus = Object.entries(WORK_ORDER_STATUS_META) .map(([status, meta]) => ({ status: Number(status), label: meta.text, type: meta.type, count: countByStatus(stat, Number(status)) })) return { total: Number(stat?.total ?? 0), pendingCount, processingCount, verifyingCount, finishedCount, byStatus } } /** C 区数据总览卡片:报警总数、报警中设备、待验收工单、在线率。 */ export function normalizeAlarmOverviewCards({ dashboard = {}, deviceStats = {}, workOrderStat = {} } = {}) { const normalized = normalizeWorkOrderStatusStat(workOrderStat) return [ { key: 'alarmTotal', label: '报警总数', value: Number(dashboard.totalCount ?? 0), unit: '条', tone: 'primary' }, { key: 'alarmingDevice', label: '报警中设备', value: Number(dashboard.alarmingDeviceCount ?? 0), unit: '台', tone: Number(dashboard.alarmingDeviceCount ?? 0) > 0 ? 'danger' : 'primary' }, { key: 'verifyingOrder', label: '待验收工单', value: normalized.verifyingCount, unit: '单', tone: normalized.verifyingCount > 0 ? 'warning' : 'primary' }, { key: 'onlineRate', label: '在线率', value: formatOnlineRate(deviceStats.onlineRate), unit: '%', tone: 'primary' } ] } function formatOnlineRate(value) { const numeric = Number(value) return Number.isFinite(numeric) ? Number(numeric.toFixed(1)) : 0 } /** 报警趋势:按四个等级分别成系列,空天补 0。 */ export function normalizeAlarmTrend(trend = []) { const items = Array.isArray(trend) ? trend : [] const dates = items.map(item => item?.date || '-') const levels = [1, 2, 3, 4].map(level => ({ name: `级${level}`, text: (ALARM_LEVEL_META[level] || {}).text || `${level}级报警`, data: items.map(item => Number(item[`level${level}Count`] ?? 0)) })) return { dates, levels } } /** 占比饼图:空值名称兜底、数量转数字、只取最大值前若干项。 */ export function normalizePieRatio(items = [], fallbackName = '未分类', top = 6) { const rows = (Array.isArray(items) ? items : []) .map(item => ({ name: String(item?.name || '').trim() || fallbackName, count: Number(item?.count ?? 0) })) .filter(item => item.count > 0) .sort((a, b) => b.count - a.count) return rows.slice(0, top) } export function alarmLevelText(level) { const numeric = Number(level) const meta = ALARM_LEVEL_META[numeric] return meta?.text || (Number.isFinite(numeric) && numeric > 0 ? `${numeric}级报警` : '未分级') } const ALARM_DISPLAY_STATUS_META = { 1: { text: '待审核', type: 'warning' }, 2: { text: '已转工单', type: 'primary' }, 3: { text: '已解除', type: 'success' } } const ALARM_DISPLAY_RELEASED = 3 /** * A 区实时报警列表。 * * 实时列表只展示仍需要关注的报警:已解除(displayStatus=3)直接过滤; * 待审核 / 已转工单行保留展示状态与关联工单号,供首页按状态分流跳转。 * * 后端 5c14625 起实时行已携带 displayStatus;为兼容旧进程/旧产物返回的 * “只有 orderNo 没有 displayStatus” 的行,这里按工单是否在办兜底推导: * 有未取消工单 -> 已转工单,否则 -> 待审核。兜底只影响缺失字段的行, * 后端明确给出的展示状态始终优先。 */ export function normalizeRealtimeAlarmRows(rows = [], limit = 5) { const list = (Array.isArray(rows) ? rows : []) .filter(row => derivedDisplayStatus(row) !== ALARM_DISPLAY_RELEASED) .slice() .sort((a, b) => String(b?.alarmTime || '').localeCompare(String(a?.alarmTime || ''))) .slice(0, limit) .map(row => { const displayStatus = derivedDisplayStatus(row) return { id: row?.id ?? row?.alarmId, deviceCode: row?.deviceCode ?? '-', deviceName: row?.deviceName || row?.deviceCode || '监测设备', warningType: row?.warningType || '监测报警', alarmLevel: Number(row?.alarmLevel ?? 0), alarmLevelText: row?.alarmLevelText || alarmLevelText(row?.alarmLevel), alarmTime: row?.alarmTime || '-', location: row?.equipmentLocation || '-', displayStatus, displayStatusText: row?.displayStatusText || ALARM_DISPLAY_STATUS_META[displayStatus]?.text || '待审核', displayStatusType: ALARM_DISPLAY_STATUS_META[displayStatus]?.type || 'warning', orderNo: row?.orderNo || row?.relatedOrderNo || '' } }) return list } function derivedDisplayStatus(row = {}) { const explicit = Number(row?.displayStatus) if (Number.isFinite(explicit) && explicit !== 0) { return explicit } // 与后端 resolveDisplayStatus 同序:在办工单优先, // 其次才看报警是否已处理/误报解除,避免把“已转工单”误判成“已解除”。 const hasOrder = String(row?.orderNo || row?.relatedOrderNo || '').trim().length > 0 const orderStatus = Number(row?.orderStatus ?? row?.relatedOrderStatus ?? -1) const liveOrder = hasOrder && orderStatus !== 7 if (liveOrder) { return 2 } return Number(row?.alarmStatus) === 1 ? ALARM_DISPLAY_RELEASED : 1 } /** * 实时报警跳转目标:已转工单带工单号 -> 设备维修派单;其余(含待审核)-> 报警审核。 */ export function realtimeAlarmJumpTarget(row = {}) { if (Number(row?.displayStatus) === 2) { const orderNo = String(row?.orderNo || '').trim() if (orderNo) { return { path: '/device/maintain', query: { keyword: orderNo } } } } return { path: '/monitorAlarm/audit', query: { rootTypeId: WATER_DEVICE_ROOT_TYPE_ID, alarmId: row?.id ?? row?.alarmId ?? undefined, deviceCode: row?.deviceCode || undefined } } } /** B 区异常待办清单:复用设备行标准化,避免口径漂移。 */ export function normalizeAbnormalTodoRows(rows = [], limit = 6) { return (Array.isArray(rows) ? rows : []).slice(0, limit).map(normalizeDeviceRow) } /** B 区选中设备摘要。 */ export function normalizeDeviceSummary(row = {}) { const normalized = normalizeDeviceRow(row) return { ...normalized, name: row.equipmentName || row.deviceName || '-', code: row.equipmentCode || row.deviceCode || '-', location: row.equipmentLocation || row.location || '-', district: row.district || '-', typeName: normalized.equipmentTypeName || '-' } } /** A 区工单进展列表。 */ export function normalizeWorkOrderRows(rows = [], limit = 5) { return (Array.isArray(rows) ? rows : []) .slice() .sort((a, b) => String(b?.createTime || '').localeCompare(String(a?.createTime || ''))) .slice(0, limit) .map(row => { const orderStatus = Number(row?.orderStatus ?? -1) const orderLevel = Number(row?.orderLevel ?? -1) const statusMeta = WORK_ORDER_STATUS_META[orderStatus] || { text: '未知', type: 'info' } const levelMeta = WORK_ORDER_LEVEL_META[orderLevel] || { text: '-', type: 'info' } return { orderId: row?.orderId, orderNo: row?.orderNo || '-', orderStatus, orderStatusText: statusMeta.text, orderStatusType: statusMeta.type, orderLevel, orderLevelText: levelMeta.text, orderLevelType: levelMeta.type, deviceCode: row?.deviceCode || '-', deviceName: row?.deviceName || '-', orderDesc: row?.orderDesc || '-', createTime: row?.createTime || '-' } }) } function finiteCoordinate(value) { const numeric = Number(value) return Number.isFinite(numeric) ? numeric : null } function hasCoordinates(point = {}) { const lng = finiteCoordinate(point?.lng ?? point?.longitude) const lat = finiteCoordinate(point?.lat ?? point?.latitude) return lng !== null && lat !== null } function normalizeHomeOverviewPoint(point, kind) { const lng = finiteCoordinate(point?.lng ?? point?.longitude) const lat = finiteCoordinate(point?.lat ?? point?.latitude) if (lng === null || lat === null) return null if (kind === 'facility') { if (point?.type === 'pipe') return null // 设施状态语义统一由共享色板定义,首页与设施 GIS 一张图同源 const statusMeta = getFacilityStatusMeta(point?.type, point?.status) return { ...point, id: point?.id ?? point?.featureId, kind, name: point?.name || point?.facilityName || point?.code || '供水设施', code: point?.code || point?.facilityCode || '-', lng, lat, statusKey: statusMeta.key, statusText: statusMeta.text, iconUrl: getMonitoringPointIcon({ statusKey: statusMeta.key }), meta: point?.road || '供水设施' } } if (kind === 'alarm') { return { ...point, id: point?.alarmId ?? point?.id, kind, name: point?.deviceName || point?.name || point?.deviceCode || '报警点', code: point?.deviceCode || point?.code || '-', lng, lat, statusKey: 'alarm', iconUrl: getMonitoringPointIcon({ statusKey: 'alarm' }), meta: point?.warningType || '监测报警' } } const normalized = normalizeDeviceRow(point) const statusKey = getMonitoringPointStatus(normalized) return { ...point, kind, id: point?.equipmentId ?? point?.id ?? normalized.equipmentCode ?? point?.deviceCode, name: normalized.equipmentName || normalized.equipmentCode || '监测设备', code: normalized.equipmentCode || point?.deviceCode || '-', lng, lat, statusKey, iconUrl: getMonitoringPointIcon({ ...normalized, statusKey }), meta: normalized.equipmentTypeName || '监测设备' } } /** * 首页全域态势:聚合设施、设备与报警三类一张图数据,统一交给公共 GisMapCanvas 渲染。 * * 同一设备的报警点与设备点按 deviceCode 合并,报警状态优先于设备自身状态; * 设施点位与管网线段原样透传,颜色/图标沿用各一张图规范。 */ export function buildHomeGisOverview({ facilityPoints = [], facilityLines = [], devicePoints = [], alarmPoints = [] } = {}) { const deviceList = (Array.isArray(devicePoints) ? devicePoints : []) .map(point => normalizeHomeOverviewPoint(point, 'device')) .filter(Boolean) const alarmList = (Array.isArray(alarmPoints) ? alarmPoints : []) .map(point => normalizeHomeOverviewPoint(point, 'alarm')) .filter(Boolean) const merged = new Map() deviceList.forEach(point => { const key = String(point.code && point.code !== '-' ? point.code : point.id || '') if (key) merged.set(key, point) }) alarmList.forEach(point => { const key = String(point.code && point.code !== '-' ? point.code : point.id || '') const existing = key ? merged.get(key) : undefined if (!key || !existing || existing.statusKey !== 'alarm') { if (key) merged.set(key, point) } }) const facilityPointList = (Array.isArray(facilityPoints) ? facilityPoints : []) .map(point => normalizeHomeOverviewPoint(point, 'facility')) .filter(Boolean) const lines = (Array.isArray(facilityLines) ? facilityLines : []) .map(line => ({ ...line, name: line?.name || line?.pipeName || '供水管网', color: '#f1c40f', points: (line?.points || []) .map(point => ({ lng: finiteCoordinate(point?.lng ?? point?.longitude), lat: finiteCoordinate(point?.lat ?? point?.latitude) })) .filter(point => point.lng !== null && point.lat !== null) })) .filter(line => (line.points || []).length >= 2) return { points: [...facilityPointList, ...merged.values()].filter(hasCoordinates), lines } }