waterHomeModel.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418
  1. import { normalizeDeviceRow } from './waterSupplyUiModel.js'
  2. import { getFacilityStatusMeta, getMonitoringPointIcon, getMonitoringPointStatus } from './gisMapColors.js'
  3. /**
  4. * 供水首页工作台数据模型(T05 #48)。
  5. *
  6. * 首页只组合既有只读接口,不在本模块调用后端;所有口径(供水范围、工单状态、
  7. * 报警等级、在线率)都收敛到这里,便于用 node 单测锁定。
  8. */
  9. // 工单状态 0-7,与 ADR-0010 权威枚举一致
  10. const WORK_ORDER_STATUS_META = {
  11. 0: { text: '待派单', type: 'danger' },
  12. 1: { text: '待接单', type: 'warning' },
  13. 2: { text: '已接单', type: 'primary' },
  14. 3: { text: '处理中', type: 'warning' },
  15. 4: { text: '待验收', type: 'primary' },
  16. 5: { text: '已结案', type: 'success' },
  17. 6: { text: '延期中', type: 'warning' },
  18. 7: { text: '已取消', type: 'info' }
  19. }
  20. const WORK_ORDER_LEVEL_META = {
  21. 0: { text: '紧急', type: 'danger' },
  22. 1: { text: '一般', type: 'warning' },
  23. 2: { text: '低', type: 'info' }
  24. }
  25. const ALARM_LEVEL_META = {
  26. 1: { text: '一级报警', type: 'danger' },
  27. 2: { text: '二级报警', type: 'warning' },
  28. 3: { text: '三级报警', type: 'primary' },
  29. 4: { text: '四级报警', type: 'success' }
  30. }
  31. /**
  32. * 供水设备根类型:设备接口 /equipment-base 的数据范围参数,取值对应 EquipmentTypeEnum
  33. * 的 1-供水设备。设备与工单两处取数都以此限定供水范围,避免各自写死。
  34. */
  35. export const WATER_DEVICE_ROOT_TYPE_ID = 1
  36. export const HOME_QUICK_ENTRIES = [
  37. { key: 'abnormal', label: '设备异常管理', suffix: '/device/abnormal', icon: 'WarningFilled' },
  38. { key: 'alarm', label: '报警管理', suffix: '/monitorAlarm/index', icon: 'BellFilled' },
  39. { key: 'audit', label: '报警审核', suffix: '/monitorAlarm/audit', icon: 'Finished' },
  40. { key: 'map', label: '监测报警一张图', suffix: '/waterSupplyAlarmGIS', query: { rootTypeId: WATER_DEVICE_ROOT_TYPE_ID }, icon: 'MapLocation' }
  41. ]
  42. /** 供水工单统计/列表的固定范围参数:设备父类别=供水。 */
  43. export function buildHomeWorkOrderQuery(extra = {}) {
  44. return {
  45. deviceTypeParentId: String(WATER_DEVICE_ROOT_TYPE_ID),
  46. pageNum: 1,
  47. pageSize: 5,
  48. ...extra
  49. }
  50. }
  51. /** 供水设施总数:overview 会追加 facilityType=total 合计行,须排除避免重复累计。 */
  52. export function normalizeFacilityTotal(facilityOverview = []) {
  53. const rows = Array.isArray(facilityOverview) ? facilityOverview : []
  54. return rows
  55. .filter(item => item && item.facilityType !== 'total')
  56. .reduce((sum, item) => sum + Number(item.totalCount ?? item.count ?? 0), 0)
  57. }
  58. /** A 区指挥台四项核心总量。 */
  59. export function normalizeHomeKpis({ facilityRows = [], ledgerStats = {}, alarmStats = {} } = {}) {
  60. const realtimeAlarm = Number(alarmStats.realtimeAlarmCount ?? alarmStats.realtimeCount ?? 0)
  61. return [
  62. { key: 'facility', label: '设施总数', value: normalizeFacilityTotal(facilityRows), unit: '处', tone: 'primary' },
  63. { key: 'device', label: '监测设备', value: Number(ledgerStats.total ?? 0), unit: '台', tone: 'primary' },
  64. { key: 'online', label: '在线设备', value: Number(ledgerStats.online ?? 0), unit: '台', tone: 'primary' },
  65. { key: 'alarm', label: '实时报警', value: realtimeAlarm, unit: '条', tone: realtimeAlarm > 0 ? 'danger' : 'primary' }
  66. ]
  67. }
  68. /**
  69. * 统一设备统计口径:/equipment-base/statistics 返回 total/online/offline/alarm,
  70. * 首页所需的在线率在此推导,避免各页面各自计算。
  71. */
  72. export function normalizeDeviceStatistics(stats = {}) {
  73. const total = Number(stats.total ?? 0)
  74. const online = Number(stats.online ?? 0)
  75. const offline = Number(stats.offline ?? Math.max(total - online, 0))
  76. const alarm = Number(stats.alarm ?? 0)
  77. return {
  78. total,
  79. online,
  80. offline,
  81. alarm,
  82. onlineRate: total > 0 ? Math.round(online * 1000 / total) / 10 : 0
  83. }
  84. }
  85. function countByStatus(stat, status) {
  86. const counts = Array.isArray(stat?.counts) ? stat.counts : []
  87. const hit = counts.find(item => Number(item?.status) === Number(status))
  88. return Number(hit?.count ?? 0)
  89. }
  90. /** 工单状态统计:待处理 0/1,处理中 2/3/6,待验收 4,已完成 5/7。 */
  91. export function normalizeWorkOrderStatusStat(stat = {}) {
  92. const pendingCount = countByStatus(stat, 0) + countByStatus(stat, 1)
  93. const processingCount = countByStatus(stat, 2) + countByStatus(stat, 3) + countByStatus(stat, 6)
  94. const verifyingCount = countByStatus(stat, 4)
  95. const finishedCount = countByStatus(stat, 5) + countByStatus(stat, 7)
  96. const byStatus = Object.entries(WORK_ORDER_STATUS_META)
  97. .map(([status, meta]) => ({
  98. status: Number(status),
  99. label: meta.text,
  100. type: meta.type,
  101. count: countByStatus(stat, Number(status))
  102. }))
  103. return {
  104. total: Number(stat?.total ?? 0),
  105. pendingCount,
  106. processingCount,
  107. verifyingCount,
  108. finishedCount,
  109. byStatus
  110. }
  111. }
  112. /** C 区数据总览卡片:报警总数、报警中设备、待验收工单、在线率。 */
  113. export function normalizeAlarmOverviewCards({ dashboard = {}, deviceStats = {}, workOrderStat = {} } = {}) {
  114. const normalized = normalizeWorkOrderStatusStat(workOrderStat)
  115. return [
  116. { key: 'alarmTotal', label: '报警总数', value: Number(dashboard.totalCount ?? 0), unit: '条', tone: 'primary' },
  117. { key: 'alarmingDevice', label: '报警中设备', value: Number(dashboard.alarmingDeviceCount ?? 0), unit: '台', tone: Number(dashboard.alarmingDeviceCount ?? 0) > 0 ? 'danger' : 'primary' },
  118. { key: 'verifyingOrder', label: '待验收工单', value: normalized.verifyingCount, unit: '单', tone: normalized.verifyingCount > 0 ? 'warning' : 'primary' },
  119. { key: 'onlineRate', label: '在线率', value: formatOnlineRate(deviceStats.onlineRate), unit: '%', tone: 'primary' }
  120. ]
  121. }
  122. function formatOnlineRate(value) {
  123. const numeric = Number(value)
  124. return Number.isFinite(numeric) ? Number(numeric.toFixed(1)) : 0
  125. }
  126. /** 报警趋势:按四个等级分别成系列,空天补 0。 */
  127. export function normalizeAlarmTrend(trend = []) {
  128. const items = Array.isArray(trend) ? trend : []
  129. const dates = items.map(item => item?.date || '-')
  130. const levels = [1, 2, 3, 4].map(level => ({
  131. name: `级${level}`,
  132. text: (ALARM_LEVEL_META[level] || {}).text || `${level}级报警`,
  133. data: items.map(item => Number(item[`level${level}Count`] ?? 0))
  134. }))
  135. return { dates, levels }
  136. }
  137. /** 占比饼图:空值名称兜底、数量转数字、只取最大值前若干项。 */
  138. export function normalizePieRatio(items = [], fallbackName = '未分类', top = 6) {
  139. const rows = (Array.isArray(items) ? items : [])
  140. .map(item => ({
  141. name: String(item?.name || '').trim() || fallbackName,
  142. count: Number(item?.count ?? 0)
  143. }))
  144. .filter(item => item.count > 0)
  145. .sort((a, b) => b.count - a.count)
  146. return rows.slice(0, top)
  147. }
  148. export function alarmLevelText(level) {
  149. const numeric = Number(level)
  150. const meta = ALARM_LEVEL_META[numeric]
  151. return meta?.text || (Number.isFinite(numeric) && numeric > 0 ? `${numeric}级报警` : '未分级')
  152. }
  153. const ALARM_DISPLAY_STATUS_META = {
  154. 1: { text: '待审核', type: 'warning' },
  155. 2: { text: '已转工单', type: 'primary' },
  156. 3: { text: '已解除', type: 'success' }
  157. }
  158. const ALARM_DISPLAY_RELEASED = 3
  159. /**
  160. * A 区实时报警列表。
  161. *
  162. * 实时列表只展示仍需要关注的报警:已解除(displayStatus=3)直接过滤;
  163. * 待审核 / 已转工单行保留展示状态与关联工单号,供首页按状态分流跳转。
  164. *
  165. * 后端 5c14625 起实时行已携带 displayStatus;为兼容旧进程/旧产物返回的
  166. * “只有 orderNo 没有 displayStatus” 的行,这里按工单是否在办兜底推导:
  167. * 有未取消工单 -> 已转工单,否则 -> 待审核。兜底只影响缺失字段的行,
  168. * 后端明确给出的展示状态始终优先。
  169. */
  170. export function normalizeRealtimeAlarmRows(rows = [], limit = 5) {
  171. const list = (Array.isArray(rows) ? rows : [])
  172. .filter(row => derivedDisplayStatus(row) !== ALARM_DISPLAY_RELEASED)
  173. .slice()
  174. .sort((a, b) => String(b?.alarmTime || '').localeCompare(String(a?.alarmTime || '')))
  175. .slice(0, limit)
  176. .map(row => {
  177. const displayStatus = derivedDisplayStatus(row)
  178. return {
  179. id: row?.id ?? row?.alarmId,
  180. deviceCode: row?.deviceCode ?? '-',
  181. deviceName: row?.deviceName || row?.deviceCode || '监测设备',
  182. warningType: row?.warningType || '监测报警',
  183. alarmLevel: Number(row?.alarmLevel ?? 0),
  184. alarmLevelText: row?.alarmLevelText || alarmLevelText(row?.alarmLevel),
  185. alarmTime: row?.alarmTime || '-',
  186. location: row?.equipmentLocation || '-',
  187. displayStatus,
  188. displayStatusText: row?.displayStatusText
  189. || ALARM_DISPLAY_STATUS_META[displayStatus]?.text
  190. || '待审核',
  191. displayStatusType: ALARM_DISPLAY_STATUS_META[displayStatus]?.type || 'warning',
  192. orderNo: row?.orderNo || row?.relatedOrderNo || ''
  193. }
  194. })
  195. return list
  196. }
  197. function derivedDisplayStatus(row = {}) {
  198. const explicit = Number(row?.displayStatus)
  199. if (Number.isFinite(explicit) && explicit !== 0) {
  200. return explicit
  201. }
  202. // 与后端 resolveDisplayStatus 同序:在办工单优先,
  203. // 其次才看报警是否已处理/误报解除,避免把“已转工单”误判成“已解除”。
  204. const hasOrder = String(row?.orderNo || row?.relatedOrderNo || '').trim().length > 0
  205. const orderStatus = Number(row?.orderStatus ?? row?.relatedOrderStatus ?? -1)
  206. const liveOrder = hasOrder && orderStatus !== 7
  207. if (liveOrder) {
  208. return 2
  209. }
  210. return Number(row?.alarmStatus) === 1 ? ALARM_DISPLAY_RELEASED : 1
  211. }
  212. /**
  213. * 实时报警跳转目标:已转工单带工单号 -> 设备维修派单;其余(含待审核)-> 报警审核。
  214. */
  215. export function realtimeAlarmJumpTarget(row = {}) {
  216. if (Number(row?.displayStatus) === 2) {
  217. const orderNo = String(row?.orderNo || '').trim()
  218. if (orderNo) {
  219. return { path: '/device/maintain', query: { keyword: orderNo } }
  220. }
  221. }
  222. return {
  223. path: '/monitorAlarm/audit',
  224. query: {
  225. alarmId: row?.id ?? row?.alarmId ?? undefined,
  226. deviceCode: row?.deviceCode || undefined
  227. }
  228. }
  229. }
  230. /** B 区异常待办清单:复用设备行标准化,避免口径漂移。 */
  231. export function normalizeAbnormalTodoRows(rows = [], limit = 6) {
  232. return (Array.isArray(rows) ? rows : []).slice(0, limit).map(normalizeDeviceRow)
  233. }
  234. /** B 区选中设备摘要。 */
  235. export function normalizeDeviceSummary(row = {}) {
  236. const normalized = normalizeDeviceRow(row)
  237. return {
  238. ...normalized,
  239. name: row.equipmentName || row.deviceName || '-',
  240. code: row.equipmentCode || row.deviceCode || '-',
  241. location: row.equipmentLocation || row.location || '-',
  242. district: row.district || '-',
  243. typeName: normalized.equipmentTypeName || '-'
  244. }
  245. }
  246. /** A 区工单进展列表。 */
  247. export function normalizeWorkOrderRows(rows = [], limit = 5) {
  248. return (Array.isArray(rows) ? rows : [])
  249. .slice()
  250. .sort((a, b) => String(b?.createTime || '').localeCompare(String(a?.createTime || '')))
  251. .slice(0, limit)
  252. .map(row => {
  253. const orderStatus = Number(row?.orderStatus ?? -1)
  254. const orderLevel = Number(row?.orderLevel ?? -1)
  255. const statusMeta = WORK_ORDER_STATUS_META[orderStatus] || { text: '未知', type: 'info' }
  256. const levelMeta = WORK_ORDER_LEVEL_META[orderLevel] || { text: '-', type: 'info' }
  257. return {
  258. orderId: row?.orderId,
  259. orderNo: row?.orderNo || '-',
  260. orderStatus,
  261. orderStatusText: statusMeta.text,
  262. orderStatusType: statusMeta.type,
  263. orderLevel,
  264. orderLevelText: levelMeta.text,
  265. orderLevelType: levelMeta.type,
  266. deviceCode: row?.deviceCode || '-',
  267. deviceName: row?.deviceName || '-',
  268. orderDesc: row?.orderDesc || '-',
  269. createTime: row?.createTime || '-'
  270. }
  271. })
  272. }
  273. function finiteCoordinate(value) {
  274. const numeric = Number(value)
  275. return Number.isFinite(numeric) ? numeric : null
  276. }
  277. function hasCoordinates(point = {}) {
  278. const lng = finiteCoordinate(point?.lng ?? point?.longitude)
  279. const lat = finiteCoordinate(point?.lat ?? point?.latitude)
  280. return lng !== null && lat !== null
  281. }
  282. function normalizeHomeOverviewPoint(point, kind) {
  283. const lng = finiteCoordinate(point?.lng ?? point?.longitude)
  284. const lat = finiteCoordinate(point?.lat ?? point?.latitude)
  285. if (lng === null || lat === null) return null
  286. if (kind === 'facility') {
  287. if (point?.type === 'pipe') return null
  288. // 设施状态语义统一由共享色板定义,首页与设施 GIS 一张图同源
  289. const statusMeta = getFacilityStatusMeta(point?.type, point?.status)
  290. return {
  291. ...point,
  292. id: point?.id ?? point?.featureId,
  293. kind,
  294. name: point?.name || point?.facilityName || point?.code || '供水设施',
  295. code: point?.code || point?.facilityCode || '-',
  296. lng,
  297. lat,
  298. statusKey: statusMeta.key,
  299. statusText: statusMeta.text,
  300. iconUrl: getMonitoringPointIcon({ statusKey: statusMeta.key }),
  301. meta: point?.road || '供水设施'
  302. }
  303. }
  304. if (kind === 'alarm') {
  305. return {
  306. ...point,
  307. id: point?.alarmId ?? point?.id,
  308. kind,
  309. name: point?.deviceName || point?.name || point?.deviceCode || '报警点',
  310. code: point?.deviceCode || point?.code || '-',
  311. lng,
  312. lat,
  313. statusKey: 'alarm',
  314. iconUrl: getMonitoringPointIcon({ statusKey: 'alarm' }),
  315. meta: point?.warningType || '监测报警'
  316. }
  317. }
  318. const normalized = normalizeDeviceRow(point)
  319. const statusKey = getMonitoringPointStatus(normalized)
  320. return {
  321. ...point,
  322. kind,
  323. id: point?.equipmentId ?? point?.id ?? normalized.equipmentCode ?? point?.deviceCode,
  324. name: normalized.equipmentName || normalized.equipmentCode || '监测设备',
  325. code: normalized.equipmentCode || point?.deviceCode || '-',
  326. lng,
  327. lat,
  328. statusKey,
  329. iconUrl: getMonitoringPointIcon({ ...normalized, statusKey }),
  330. meta: normalized.equipmentTypeName || '监测设备'
  331. }
  332. }
  333. /**
  334. * 首页全域态势:聚合设施、设备与报警三类一张图数据,统一交给公共 GisMapCanvas 渲染。
  335. *
  336. * 同一设备的报警点与设备点按 deviceCode 合并,报警状态优先于设备自身状态;
  337. * 设施点位与管网线段原样透传,颜色/图标沿用各一张图规范。
  338. */
  339. export function buildHomeGisOverview({
  340. facilityPoints = [],
  341. facilityLines = [],
  342. devicePoints = [],
  343. alarmPoints = []
  344. } = {}) {
  345. const deviceList = (Array.isArray(devicePoints) ? devicePoints : [])
  346. .map(point => normalizeHomeOverviewPoint(point, 'device'))
  347. .filter(Boolean)
  348. const alarmList = (Array.isArray(alarmPoints) ? alarmPoints : [])
  349. .map(point => normalizeHomeOverviewPoint(point, 'alarm'))
  350. .filter(Boolean)
  351. const merged = new Map()
  352. deviceList.forEach(point => {
  353. const key = String(point.code && point.code !== '-' ? point.code : point.id || '')
  354. if (key) merged.set(key, point)
  355. })
  356. alarmList.forEach(point => {
  357. const key = String(point.code && point.code !== '-' ? point.code : point.id || '')
  358. const existing = key ? merged.get(key) : undefined
  359. if (!key || !existing || existing.statusKey !== 'alarm') {
  360. if (key) merged.set(key, point)
  361. }
  362. })
  363. const facilityPointList = (Array.isArray(facilityPoints) ? facilityPoints : [])
  364. .map(point => normalizeHomeOverviewPoint(point, 'facility'))
  365. .filter(Boolean)
  366. const lines = (Array.isArray(facilityLines) ? facilityLines : [])
  367. .map(line => ({
  368. ...line,
  369. name: line?.name || line?.pipeName || '供水管网',
  370. color: '#f1c40f',
  371. points: (line?.points || [])
  372. .map(point => ({
  373. lng: finiteCoordinate(point?.lng ?? point?.longitude),
  374. lat: finiteCoordinate(point?.lat ?? point?.latitude)
  375. }))
  376. .filter(point => point.lng !== null && point.lat !== null)
  377. }))
  378. .filter(line => (line.points || []).length >= 2)
  379. return {
  380. points: [...facilityPointList, ...merged.values()].filter(hasCoordinates),
  381. lines
  382. }
  383. }