|
|
@@ -0,0 +1,515 @@
|
|
|
+/**
|
|
|
+ * 统一实体详情领域边界(issue 65)。
|
|
|
+ *
|
|
|
+ * 列表只传 { kind, id };本模块负责身份校验、不同后端载荷的正规化和紧凑详情视图。
|
|
|
+ * point/device 沿用既有详情链,不在此重复实现。
|
|
|
+ */
|
|
|
+import { eventLevelTag, eventStatusText } from './eventList.js'
|
|
|
+import { toWorkOrder, toWorkOrderTimeline, workOrderStateName } from './workOrder.js'
|
|
|
+import { trackOf } from './workOrderTimeline.js'
|
|
|
+
|
|
|
+export const ENTITY_DETAIL_KIND = Object.freeze({
|
|
|
+ FACILITY: 'facility',
|
|
|
+ EMERGENCY: 'emergency',
|
|
|
+ EVENT: 'event',
|
|
|
+ WORK_ORDER: 'work-order',
|
|
|
+})
|
|
|
+
|
|
|
+const KINDS = Object.freeze(Object.values(ENTITY_DETAIL_KIND))
|
|
|
+const PLACEHOLDER = '—'
|
|
|
+
|
|
|
+export function entityIdentity(input, fallbackKind = '') {
|
|
|
+ const source = input && typeof input === 'object' ? input : {}
|
|
|
+ const kind = String(source.kind || source.identityKind || fallbackKind || '').trim()
|
|
|
+ const id = String(source.id ?? source.code ?? source.identityCode ?? '').trim()
|
|
|
+ const category = String(source.category || source.identityCategory || '').trim().toUpperCase()
|
|
|
+ const family = String(source.family || source.eventFamily || source.identityFamily || '').trim().toUpperCase()
|
|
|
+ return { kind: KINDS.includes(kind) ? kind : '', id, category, family, valid: KINDS.includes(kind) && !!id }
|
|
|
+}
|
|
|
+
|
|
|
+export function sameEntity(left, right) {
|
|
|
+ const a = entityIdentity(left)
|
|
|
+ const b = entityIdentity(right)
|
|
|
+ return a.valid && b.valid && a.kind === b.kind && a.id === b.id && a.category === b.category && a.family === b.family
|
|
|
+}
|
|
|
+
|
|
|
+/** AjaxResult / page / list 三种常见外形收成同一份有效载荷。 */
|
|
|
+export function unwrapEntityPayload(raw) {
|
|
|
+ let value = raw
|
|
|
+ if (value && typeof value === 'object' && typeof value.code === 'number') {
|
|
|
+ if (value.code !== 200) return null
|
|
|
+ value = value.data
|
|
|
+ }
|
|
|
+ if (value && typeof value === 'object' && value.data !== undefined && !Array.isArray(value.data)) {
|
|
|
+ const keys = Object.keys(value)
|
|
|
+ if (keys.every((key) => ['data', 'msg', 'message', 'success', 'timestamp'].includes(key))) value = value.data
|
|
|
+ }
|
|
|
+ return value
|
|
|
+}
|
|
|
+
|
|
|
+export function normalizePage(raw) {
|
|
|
+ const payload = unwrapEntityPayload(raw)
|
|
|
+ if (Array.isArray(payload)) return { rows: payload, page: 1, pageSize: payload.length, total: payload.length, totalPages: 1 }
|
|
|
+ const source = payload && typeof payload === 'object' ? payload : {}
|
|
|
+ const rows = [source.rows, source.records, source.list, source.items, source.content].find(Array.isArray) || []
|
|
|
+ const paging = source.pagination && typeof source.pagination === 'object' ? source.pagination : source
|
|
|
+ const page = positive(paging.page ?? paging.current ?? paging.pageNum, 1)
|
|
|
+ const pageSize = positive(paging.pageSize ?? paging.size ?? paging.limit, rows.length)
|
|
|
+ const total = nonNegative(paging.total, rows.length)
|
|
|
+ const totalPages = positive(paging.totalPages ?? paging.pages, pageSize ? Math.ceil(total / pageSize) : 1)
|
|
|
+ return { rows, page, pageSize, total, totalPages }
|
|
|
+}
|
|
|
+
|
|
|
+export function normalizeEntityDetail(identityInput, raw, prefill = null) {
|
|
|
+ const identity = entityIdentity(identityInput)
|
|
|
+ if (!identity.valid) return null
|
|
|
+ const payload = unwrapEntityPayload(raw)
|
|
|
+ const page = normalizePage(payload)
|
|
|
+ let source = payload
|
|
|
+ if (Array.isArray(payload)) source = payload.find((row) => matchesId(row, identity.id)) || null
|
|
|
+ else if (payload && typeof payload === 'object' && page.rows.length) source = page.rows.find((row) => matchesId(row, identity.id)) || null
|
|
|
+ const seed = prefill && typeof prefill === 'object' ? prefill : {}
|
|
|
+ source = source && typeof source === 'object' ? { ...seed, ...source } : { ...seed }
|
|
|
+ if (identity.kind === ENTITY_DETAIL_KIND.WORK_ORDER) return normalizeWorkOrder(identity, source)
|
|
|
+ if (identity.kind === ENTITY_DETAIL_KIND.EVENT) return normalizeEvent(identity, source)
|
|
|
+ return normalizeAsset(identity, source)
|
|
|
+}
|
|
|
+
|
|
|
+export function prefilledEntityDetail(identity, prefill) {
|
|
|
+ return normalizeEntityDetail(identity, {}, prefill)
|
|
|
+}
|
|
|
+
|
|
|
+export function entityDetailView(detail) {
|
|
|
+ const current = detail || {}
|
|
|
+ return {
|
|
|
+ title: current.title || current.name || current.id || '实体详情',
|
|
|
+ subtitle: current.subtitle || [current.kindLabel, current.id].filter(Boolean).join(' · '),
|
|
|
+ tone: current.tone || 'neutral',
|
|
|
+ status: current.statusText || '',
|
|
|
+ // 类别是身份而不是普通字段:抬头要能一眼看出「中文(英文码)」。
|
|
|
+ categoryText: current.categoryText || '',
|
|
|
+ summary: Array.isArray(current.summary) ? current.summary : [],
|
|
|
+ tabs: Array.isArray(current.tabs) ? current.tabs : [],
|
|
|
+ relatedAlarms: Array.isArray(current.relatedAlarms) ? current.relatedAlarms : [],
|
|
|
+ stage: Array.isArray(current.stage) ? current.stage : [],
|
|
|
+ sections: Array.isArray(current.sections) ? current.sections : [],
|
|
|
+ people: Array.isArray(current.people) ? current.people : [],
|
|
|
+ devices: Array.isArray(current.devices) ? current.devices : [],
|
|
|
+ sectionMap: current.sectionMap && typeof current.sectionMap === 'object' ? current.sectionMap : {},
|
|
|
+ attachments: Array.isArray(current.attachments) ? current.attachments : [],
|
|
|
+ records: Array.isArray(current.records) ? current.records : [],
|
|
|
+ track: current.track || null,
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function normalizeAsset(identity, source) {
|
|
|
+ const emergency = identity.kind === ENTITY_DETAIL_KIND.EMERGENCY
|
|
|
+ const category = text(source.category || identity.category).toUpperCase()
|
|
|
+ const attributes = source.attributes && typeof source.attributes === 'object' ? source.attributes : {}
|
|
|
+ const name = text(source.name || source.facilityName || source.resourceName || source.title) || identity.id
|
|
|
+ // 状态是码值时翻成中文(0 正常 / 1 维护中 / 2 异常);后端若直接给人话则原样用。
|
|
|
+ const statusRaw = text(source.statusName || source.status || source.stateName || source.state)
|
|
|
+ const status = source.statusName || source.stateName ? statusRaw : facilityStatusText(statusRaw) || statusRaw
|
|
|
+ const location = text(source.address || source.location || source.position)
|
|
|
+ const fields = assetFields(category, attributes)
|
|
|
+ return {
|
|
|
+ ...identity, category, name, title: name,
|
|
|
+ kindLabel: emergency ? '应急对象' : '设施', statusText: status, tone: facilityStatusTone(source.status), raw: source,
|
|
|
+ categoryText: facilityCategoryText(category),
|
|
|
+ coordinates: source.coordinates || null,
|
|
|
+ summary: assetSummary(category, attributes, status, location),
|
|
|
+ tabs: [tab('overview', '基本信息'), tab('business', '业务详情'), tab('related', '关联信息')],
|
|
|
+ people: assetPeople(attributes),
|
|
|
+ devices: assetDevices(attributes),
|
|
|
+ attachments: assetAttachments(attributes),
|
|
|
+ sections: compactSections([
|
|
|
+ section('基本信息', [field('设施编码(code)', source.code || identity.id), field('设施类别(category)', facilityCategoryText(category)), field('运行状态(status)', status), field('数据更新时间(updatedAt)', source.updatedAt)]),
|
|
|
+ section('位置与归属', [field('位置描述(location)', location, true), field('所属片区(districtName)', source.districtName), field('坐标(coordinates)', coordinatesText(source.coordinates), true)]),
|
|
|
+ section('业务详情', fields),
|
|
|
+ ]),
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/** 设施状态 → 语义色键(0 正常=绿、1 维护中=黄、2 异常=红;认不出走中性)。 */
|
|
|
+function facilityStatusTone(status) {
|
|
|
+ const code = status === null || status === undefined ? '' : String(status).trim()
|
|
|
+ if (code === '0') return 'lv4'
|
|
|
+ if (code === '1') return 'lv2'
|
|
|
+ if (code === '2') return 'lv1'
|
|
|
+ return 'neutral'
|
|
|
+}
|
|
|
+
|
|
|
+function assetSummary(category, a, status, location) {
|
|
|
+ const byCategory = {
|
|
|
+ WATER_PLANT: [['当前能力', a.currentCapacity], ['设计能力', a.designCapacity], ['处理工艺', a.treatmentProcess]],
|
|
|
+ ENGINEERING_FACILITY: [['设施等级', a.facilityLevel], ['设计流量', a.designFlow], ['调蓄容积', a.storageCapacity]],
|
|
|
+ PUMP_STATION: [['水泵数量', a.pumpCount], ['设计流量', a.designFlow], ['装机功率', a.powerCapacity]],
|
|
|
+ PIPE_NETWORK: [['长度(米)', a.lengthMeters], ['管径(毫米)', a.diameterMillimeters], ['材质', a.material]],
|
|
|
+ HAZARD: [['隐患等级', a.level], ['整改期限', a.rectifyDeadline], ['整改人', a.rectifyPerson]],
|
|
|
+ PROTECTION_TARGET: [['风险等级', a.riskLevel], ['风险分值', a.riskScore], ['评估时间', a.assessmentTime]],
|
|
|
+ EMERGENCY_RESOURCE: [['型号', a.model], ['规格', a.specification], ['维护人', a.maintainer]],
|
|
|
+ }
|
|
|
+ return summaryItems([['状态', status], ...(byCategory[category] || []), ['位置', location]])
|
|
|
+}
|
|
|
+function assetPeople(a) { return peopleItems([['负责人', a.principal], ['维护人', a.maintainer], ['整改人', a.rectifyPerson], ['联系电话', a.contactPhone || a.maintainerPhone]]) }
|
|
|
+function assetAttachments(a) { return attachmentItems(a.images || a.image) }
|
|
|
+function assetDevices(a) {
|
|
|
+ if (Array.isArray(a.devices)) return a.devices.map((item) => ({
|
|
|
+ id: item.id || '', code: item.code || item.id || '', name: item.name || item.code || item.id || '设备',
|
|
|
+ type: item.type || '', location: item.location || '', status: deviceStatusText(item),
|
|
|
+ }))
|
|
|
+ return String(a.equipmentIds || a.equipmentId || '').split(',').map((code) => code.trim()).filter(Boolean).map((code) => ({ name: code, code, type: '', location: '', status: '状态未发布' }))
|
|
|
+}
|
|
|
+function deviceStatusText(item) {
|
|
|
+ if (item.onlineStatus === 0) return '离线'
|
|
|
+ if (item.alarmStatus === 1) return '报警'
|
|
|
+ if (item.currentStatus === 1) return '维修'
|
|
|
+ if (item.currentStatus === 2) return '报废'
|
|
|
+ if (item.onlineStatus === 1) return '在线'
|
|
|
+ return '状态未发布'
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 设施枚举词表(维护者 2026-10-29 走查逐条点名的口径)。
|
|
|
+ *
|
|
|
+ * 后端这些字段是**码值**(status/networkType/networkLevel),直接印数字就是漏码;
|
|
|
+ * 但认不出的值也不能编一个中文,所以统一走 `codedText()`:命中词表回「中文」,
|
|
|
+ * 未命中回「码」本身(与 CODE_TEXT 的立场一致)。
|
|
|
+ */
|
|
|
+const FACILITY_STATUS_TEXT = { '0': '正常', '1': '维护中', '2': '异常' }
|
|
|
+const NETWORK_TYPE_TEXT = { '0': '燃气管网', '1': '排水管网', '2': '供水管网' }
|
|
|
+const NETWORK_LEVEL_TEXT = { '0': '污水', '1': '雨水' }
|
|
|
+/** 排水工程设施类型(0 闸门 / 1 调蓄设施 / 2 泵站):同样是码值,一并直译。 */
|
|
|
+const ENGINEERING_FACILITY_TYPE_TEXT = { '0': '闸门', '1': '调蓄设施', '2': '泵站' }
|
|
|
+/** 类别中文名:界面上用「中文 + 英文码」,只印英文等于把接口词汇漏上大屏。 */
|
|
|
+const FACILITY_CATEGORY_TEXT = {
|
|
|
+ PIPE_NETWORK: '管网',
|
|
|
+ WATER_PLANT: '水厂',
|
|
|
+ PUMP_STATION: '泵站',
|
|
|
+ ENGINEERING_FACILITY: '排水工程设施',
|
|
|
+ EMERGENCY_RESOURCE: '应急资源',
|
|
|
+ HAZARD: '危险源',
|
|
|
+ PROTECTION_TARGET: '防护目标',
|
|
|
+}
|
|
|
+
|
|
|
+/** 码 → 显示值;给出词表时「中文(码)」,未命中回码本身(不猜、不留空)。 */
|
|
|
+function codedText(value, dictionary, withCode = false) {
|
|
|
+ const code = value === null || value === undefined ? '' : String(value).trim()
|
|
|
+ if (!code) return ''
|
|
|
+ const label = dictionary && dictionary[code]
|
|
|
+ if (!label) return code
|
|
|
+ return withCode ? `${label}(${code})` : label
|
|
|
+}
|
|
|
+
|
|
|
+/** 类别显示名:「中文(英文码)」;认不出的类别只回码。 */
|
|
|
+export function facilityCategoryText(category) {
|
|
|
+ return codedText(category, FACILITY_CATEGORY_TEXT, true)
|
|
|
+}
|
|
|
+
|
|
|
+/** 设施状态(0 正常 / 1 维护中 / 2 异常)→ 中文;空值如实空。 */
|
|
|
+export function facilityStatusText(status) {
|
|
|
+ return codedText(status, FACILITY_STATUS_TEXT)
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 设施详情字段表(维护者 2026-10-29 设计规范)。
|
|
|
+ *
|
|
|
+ * 三条口径写死在这一处,别的层不再重复判断:
|
|
|
+ * 1. **命名一律中文**,必要时用括号补英文码/后端字段名(英文只作补充,不作主名);
|
|
|
+ * 2. **凡有量纲的读数必带单位**;单位来自后端领域注释(m³/d、m³/h、m、mm、m³、kW…),
|
|
|
+ * 不从前端猜;
|
|
|
+ * 3. 某些指标只在特定子类型下有效(如管网「二级分类」只对**排水管**有意义),
|
|
|
+ * 无效时**保留该行并显示「—」**,不是把它删掉 —— 删掉会让读者以为这个设施没这个属性。
|
|
|
+ *
|
|
|
+ * 每项形如 { label, key, unit?, format?, always? }:
|
|
|
+ * · `format` 把码值翻成中文(枚举词表只在这里登记);
|
|
|
+ * · `always` 表示"无效也占位"(与 compactSections 的缺值剔除相反)。
|
|
|
+ */
|
|
|
+function pipesFields(a) {
|
|
|
+ const drainage = String(a.networkType ?? '').trim() === '1'
|
|
|
+ return [
|
|
|
+ { label: '管网类型(networkType)', key: 'networkType', format: (v) => codedText(v, NETWORK_TYPE_TEXT, true) },
|
|
|
+ // 二级分类=原来的"管网等级":后端 network_level 只用于区分排水管道(污水/雨水)。
|
|
|
+ // 非排水管这个字段没有业务含义,按要求显示「—」而不是隐藏。
|
|
|
+ {
|
|
|
+ label: '二级分类(networkLevel)',
|
|
|
+ key: 'networkLevel',
|
|
|
+ always: true,
|
|
|
+ format: (v) => (drainage ? codedText(v, NETWORK_LEVEL_TEXT, true) : ''),
|
|
|
+ invalidNote: drainage ? '' : '仅排水管道适用',
|
|
|
+ },
|
|
|
+ { label: '长度', key: 'lengthMeters', unit: '米' },
|
|
|
+ { label: '管径', key: 'diameterMillimeters', unit: '毫米' },
|
|
|
+ { label: '材质', key: 'material' },
|
|
|
+ { label: '管养单位(managementUnit)', key: 'managementUnit' },
|
|
|
+ { label: '建设日期', key: 'buildDate' },
|
|
|
+ { label: '备注', key: 'remark', wide: true },
|
|
|
+ ]
|
|
|
+}
|
|
|
+
|
|
|
+const ASSET_FIELD_ROWS = {
|
|
|
+ PIPE_NETWORK: null, // 见 pipesFields():需要按管网类型决定二级分类是否适用
|
|
|
+ WATER_PLANT: [
|
|
|
+ { label: '水源类型(waterSourceType)', key: 'waterSourceType' },
|
|
|
+ { label: '设计能力', key: 'designCapacity', unit: '万m³/d' },
|
|
|
+ { label: '当前能力', key: 'currentCapacity', unit: '万m³/d' },
|
|
|
+ { label: '处理工艺', key: 'treatmentProcess', wide: true },
|
|
|
+ { label: '投产日期', key: 'commissionDate' },
|
|
|
+ { label: '水质标准', key: 'waterQualityStandard' },
|
|
|
+ { label: '负责人', key: 'principal' },
|
|
|
+ { label: '联系电话', key: 'contactPhone' },
|
|
|
+ { label: '备注', key: 'remark', wide: true },
|
|
|
+ ],
|
|
|
+ ENGINEERING_FACILITY: [
|
|
|
+ { label: '设施类型(facilityType)', key: 'facilityType', format: (v) => codedText(v, ENGINEERING_FACILITY_TYPE_TEXT, true) },
|
|
|
+ { label: '设施等级', key: 'facilityLevel' },
|
|
|
+ { label: '管理单位(managementUnit)', key: 'managementUnit' },
|
|
|
+ { label: '建设日期', key: 'buildDate' },
|
|
|
+ { label: '设计流量', key: 'designFlow', unit: 'm³/s' },
|
|
|
+ { label: '设计扬程', key: 'designHead', unit: 'm' },
|
|
|
+ { label: '调蓄容积', key: 'storageCapacity', unit: 'm³' },
|
|
|
+ { label: '关联设备', key: 'equipmentIds', wide: true },
|
|
|
+ { label: '备注', key: 'remark', wide: true },
|
|
|
+ ],
|
|
|
+ PUMP_STATION: [
|
|
|
+ { label: '泵站类型(stationType)', key: 'stationType' },
|
|
|
+ { label: '水泵数量', key: 'pumpCount', unit: '台' },
|
|
|
+ { label: '设计流量', key: 'designFlow', unit: 'm³/h' },
|
|
|
+ { label: '设计扬程', key: 'designHead', unit: 'm' },
|
|
|
+ { label: '装机功率', key: 'powerCapacity', unit: 'kW' },
|
|
|
+ { label: '投产日期', key: 'commissionDate' },
|
|
|
+ { label: '负责人', key: 'principal' },
|
|
|
+ { label: '联系电话', key: 'contactPhone' },
|
|
|
+ { label: '备注', key: 'remark', wide: true },
|
|
|
+ ],
|
|
|
+ EMERGENCY_RESOURCE: [
|
|
|
+ { label: '设备类型(equipmentTypeId)', key: 'equipmentTypeId' },
|
|
|
+ { label: '型号', key: 'model' },
|
|
|
+ { label: '规格', key: 'specification', wide: true },
|
|
|
+ { label: '制造商', key: 'manufacturer' },
|
|
|
+ { label: '资产价值', key: 'assetValue', unit: '元' },
|
|
|
+ { label: '预计使用年限', key: 'useLifeYears', unit: '年' },
|
|
|
+ { label: '安装时间', key: 'installTime' },
|
|
|
+ { label: '维护人', key: 'maintainer' },
|
|
|
+ { label: '维护电话', key: 'maintainerPhone' },
|
|
|
+ { label: '权属单位(ownershipUnit)', key: 'ownershipUnit' },
|
|
|
+ { label: '设备图片', key: 'image' },
|
|
|
+ { label: '备注', key: 'remark', wide: true },
|
|
|
+ ],
|
|
|
+ HAZARD: [
|
|
|
+ { label: '隐患类型(hazardType)', key: 'hazardType' },
|
|
|
+ { label: '隐患等级', key: 'level' },
|
|
|
+ { label: '来源', key: 'source' },
|
|
|
+ { label: '隐患描述', key: 'description', wide: true },
|
|
|
+ { label: '关联管网(networkId)', key: 'networkId' },
|
|
|
+ { label: '关联设备(equipmentId)', key: 'equipmentId' },
|
|
|
+ { label: '关联工单(workOrderId)', key: 'workOrderId' },
|
|
|
+ { label: '报告单位', key: 'reportUnit' },
|
|
|
+ { label: '报告时间', key: 'reportTime' },
|
|
|
+ { label: '整改方案', key: 'rectifyPlan', wide: true },
|
|
|
+ { label: '整改期限', key: 'rectifyDeadline' },
|
|
|
+ { label: '整改人', key: 'rectifyPerson' },
|
|
|
+ { label: '整改反馈', key: 'rectifyFeedback', wide: true },
|
|
|
+ { label: '隐患图片', key: 'images', wide: true },
|
|
|
+ { label: '备注', key: 'remark', wide: true },
|
|
|
+ ],
|
|
|
+ PROTECTION_TARGET: [
|
|
|
+ { label: '关联管网(networkId)', key: 'networkId' },
|
|
|
+ { label: '风险等级', key: 'riskLevel' },
|
|
|
+ { label: '风险分值', key: 'riskScore', unit: '分' },
|
|
|
+ { label: '风险颜色', key: 'riskColor' },
|
|
|
+ { label: '评估模型(assessmentModel)', key: 'assessmentModel' },
|
|
|
+ { label: '评估时间', key: 'assessmentTime' },
|
|
|
+ { label: '风险因素', key: 'riskFactors', wide: true },
|
|
|
+ { label: '危险源', key: 'hazardSources', wide: true },
|
|
|
+ { label: '备注', key: 'remark', wide: true },
|
|
|
+ ],
|
|
|
+}
|
|
|
+
|
|
|
+/** 读数 + 单位(认不出的值留给 field() 变占位符,不拼半截 "— m")。 */
|
|
|
+function withUnitText(value, unit) {
|
|
|
+ if (value === null || value === undefined || String(value).trim() === '') return ''
|
|
|
+ return unit ? `${value} ${unit}` : String(value)
|
|
|
+}
|
|
|
+
|
|
|
+function assetFields(category, a) {
|
|
|
+ const rows = category === 'PIPE_NETWORK' ? pipesFields(a) : ASSET_FIELD_ROWS[category] || []
|
|
|
+ return rows.map((row) => {
|
|
|
+ const raw = row.format ? row.format(a[row.key]) : withUnitText(a[row.key], row.unit)
|
|
|
+ // always 行(如非排水管的二级分类)保留占位;其余缺值由 compactSections 剔除。
|
|
|
+ const item = field(row.label, raw, row.wide === true)
|
|
|
+ if (row.always) {
|
|
|
+ item.always = true
|
|
|
+ if (row.invalidNote && item.value === PLACEHOLDER) item.note = row.invalidNote
|
|
|
+ }
|
|
|
+ return item
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+function coordinatesText(value) {
|
|
|
+ if (!value || typeof value !== 'object') return ''
|
|
|
+ const start = [value.longitude, value.latitude].filter((item) => item !== null && item !== undefined).join(', ')
|
|
|
+ const end = [value.endLongitude, value.endLatitude].filter((item) => item !== null && item !== undefined).join(', ')
|
|
|
+ return [start, end].filter(Boolean).join(' → ')
|
|
|
+}
|
|
|
+
|
|
|
+function normalizeEvent(identity, source) {
|
|
|
+ const envelope = source && source.basicInfo ? source : {}
|
|
|
+ const basic = envelope.basicInfo && typeof envelope.basicInfo === 'object' ? envelope.basicInfo : source
|
|
|
+ source = { ...basic,
|
|
|
+ disposalHistory: envelope.disposalHistory || basic.disposalHistory,
|
|
|
+ attachmentList: envelope.attachmentList || basic.attachmentList,
|
|
|
+ alarmList: envelope.alarmList || basic.alarmList,
|
|
|
+ gisLocation: envelope.gisLocation || basic.gisLocation,
|
|
|
+ }
|
|
|
+ const family = String(identity.family || source.family || source.eventFamily || (source.alarmId || source.alarmNo ? 'ALARM' : source.warningId || source.warningNo ? 'WARNING' : '')).toUpperCase()
|
|
|
+ const levelValue = source.level ?? source.warningLevel ?? source.alarmLevel
|
|
|
+ const level = eventLevelTag(levelValue)
|
|
|
+ const status = eventStatusText(source.status || source.state) || text(source.statusName || source.stateName)
|
|
|
+ const name = text(source.name || source.warningName || source.alarmName || source.eventName || source.deviceName || source.title) || identity.id
|
|
|
+ return {
|
|
|
+ ...identity,
|
|
|
+ name,
|
|
|
+ title: name,
|
|
|
+ kindLabel: family === 'ALARM' ? '报警' : family === 'WARNING' ? '预警' : '事件',
|
|
|
+ family,
|
|
|
+ statusText: status,
|
|
|
+ tone: level.tone || 'neutral',
|
|
|
+ raw: source,
|
|
|
+ summary: summaryItems([['等级', level.label], ['状态', status], ['当前值', withUnit(source.value ?? source.actualValue, source.unit || source.warningUnit)], ['发生时间', source.eventTime || source.createTime]]),
|
|
|
+ tabs: family === 'WARNING'
|
|
|
+ ? [tab('overview', '预警信息'), tab('records', '处置记录'), tab('alarms', '关联报警'), tab('attachments', '附件')]
|
|
|
+ : [tab('overview', '报警信息'), tab('devices', '设备信息'), tab('records', '处置记录'), tab('attachments', '附件')],
|
|
|
+ relatedAlarms: Array.isArray(source.alarmList) ? source.alarmList.map(relatedAlarm) : [],
|
|
|
+ stage: eventStage(family, status, source.disposalHistory),
|
|
|
+ people: peopleItems([['发布人', source.publisherName || source.publisher], ['处置人', source.handlerName || source.handler], ['核验人', source.reviewerName || source.reviewer]]),
|
|
|
+ devices: deviceItems(source),
|
|
|
+ attachments: attachmentItems(source.attachmentList),
|
|
|
+ records: recordItems(source.disposalHistory || source.logs),
|
|
|
+ sections: compactSections([
|
|
|
+ section('事件摘要', [field('事件编码', identity.id), field('等级', level.label), field('状态', status), field('发生时间', source.eventTime || source.createTime)]),
|
|
|
+ section('监测读数', [field('指标', source.type || source.warningType || source.warningCode), field('当前值', withUnit(source.value ?? source.actualValue, source.unit || source.warningUnit)), field('阈值', threshold(source))]),
|
|
|
+ section('关联信息', [field('设备', source.deviceName || source.deviceCode), field('监测点', source.monitoringPointName || source.monitoringPointCode), field('工单', source.workOrderCode || source.orderNo), field('位置', source.location || source.address || locationText(source.gisLocation), true)]),
|
|
|
+ section('处置与附件', [field('处置记录', listCount(source.disposalHistory, '条')), field('关联报警', listCount(source.alarmList, '条')), field('附件', listCount(source.attachmentList, '项'))]),
|
|
|
+ ]),
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function normalizeWorkOrder(identity, source) {
|
|
|
+ const detailSource = source && source.order ? source : { order: source, logs: source.logs || [] }
|
|
|
+ const raw = { ...(detailSource.order || {}) }
|
|
|
+ if (!raw.workOrderCode && !raw.orderNo) raw.workOrderCode = identity.id
|
|
|
+ if (raw.stateCode === undefined && raw.orderStatus === undefined && raw.status !== undefined) raw.stateCode = Number(raw.status)
|
|
|
+ const order = toWorkOrder(raw)
|
|
|
+ const timelineRaw = detailSource.logs || source.timeline || source.timelineEntries || source.records
|
|
|
+ let track = null
|
|
|
+ if (Array.isArray(timelineRaw)) {
|
|
|
+ track = trackOf(toWorkOrderTimeline({ workOrder: raw, timelineEntries: timelineRaw.map((item) => ({
|
|
|
+ operationName: item.operationName || item.operationType || item.action,
|
|
|
+ operationDesc: item.operationDesc || item.remark || item.content,
|
|
|
+ operatorName: item.operatorName || item.operator || item.createByName,
|
|
|
+ operatedAt: item.operatedAt || item.operationTime || item.createTime,
|
|
|
+ stateCode: item.stateCode ?? item.orderStatus,
|
|
|
+ stateName: item.stateName || item.orderStatusName,
|
|
|
+ })) }))
|
|
|
+ } else if (source.workOrder && Array.isArray(source.timelineEntries)) {
|
|
|
+ track = trackOf(toWorkOrderTimeline(source))
|
|
|
+ }
|
|
|
+ const stateText = order.stateName || workOrderStateName(order.stateCode)
|
|
|
+ return {
|
|
|
+ ...identity,
|
|
|
+ name: order.code || identity.id,
|
|
|
+ title: order.orderTypeName || order.code || identity.id,
|
|
|
+ kindLabel: '工单',
|
|
|
+ statusText: stateText,
|
|
|
+ tone: workOrderTone(order.stateCode),
|
|
|
+ order,
|
|
|
+ track,
|
|
|
+ raw: source,
|
|
|
+ summary: summaryItems([['工单号', order.code], ['状态', stateText], ['优先级', order.orderLevel], ['计划完成', order.planFinishTime]]),
|
|
|
+ tabs: [tab('overview', '工单信息'), tab('flow', '处理流程'), tab('devices', '设备信息'), tab('attachments', '附件')],
|
|
|
+ stage: workOrderStage(order.stateCode),
|
|
|
+ people: peopleItems([['处置人', order.receiveUserName], ['核验人', order.verifyUserName]]),
|
|
|
+ devices: order.deviceCode || order.deviceName ? [{ name: order.deviceName || order.deviceCode, code: order.deviceCode, type: order.deviceTypeName, location: order.deviceLocation }] : [],
|
|
|
+ attachments: attachmentItems([raw.maintenanceBeforeImg, raw.maintenanceAfterImg, ...timelineAttachments(timelineRaw)]),
|
|
|
+ records: recordItems(timelineRaw),
|
|
|
+ sections: compactSections([
|
|
|
+ section('工单摘要', [field('工单号', order.code), field('类型', order.orderTypeName), field('等级', order.orderLevel), field('状态', stateText)]),
|
|
|
+ section('时间信息', [field('创建时间', order.createTime), field('计划完成', order.planFinishTime), field('实际完成', order.realFinishTime)]),
|
|
|
+ section('设备信息', [field('设备', order.deviceName || order.deviceCode), field('设备类型', order.deviceTypeName), field('安装位置', order.deviceLocation, true)]),
|
|
|
+ section('处置内容', [field('处置人', order.receiveUserName), field('核验人', order.verifyUserName), field('故障描述', order.description, true), field('处置结果', order.repairResult, true)]),
|
|
|
+ ]),
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+function tab(key, label) { return { key, label } }
|
|
|
+function relatedAlarm(item) { return { code: item.alarmNo || item.alarmId || item.id || '', name: item.alarmName || item.content || item.deviceName || '关联报警', status: item.auditStatusName || item.alarmStatus || item.status || '' } }
|
|
|
+function summaryItems(entries) { return entries.filter(([,value]) => value !== null && value !== undefined && String(value).trim() !== '').map(([label,value]) => ({ label, value: String(value) })) }
|
|
|
+function peopleItems(entries) { return entries.filter(([,name]) => name).map(([role,name]) => ({ role, name: String(name) })) }
|
|
|
+function deviceItems(source) {
|
|
|
+ const name = source.deviceName || source.monitoringPointName || ''
|
|
|
+ const code = source.deviceCode || source.monitoringPointCode || ''
|
|
|
+ return name || code ? [{ name: name || code, code, location: source.location || source.address || '' }] : []
|
|
|
+}
|
|
|
+function attachmentItems(value) {
|
|
|
+ const list = Array.isArray(value) ? value : value ? String(value).split(',') : []
|
|
|
+ return list.flatMap((item) => {
|
|
|
+ if (!item) return []
|
|
|
+ if (typeof item === 'object') return [{ name: item.attachmentName || item.fileName || item.name || '附件', url: item.attachmentUrl || item.url || '', type: item.attachmentType || item.type || '' }]
|
|
|
+ const url = String(item).trim(); return url ? [{ name: url.split('/').pop() || '附件', url, type: '' }] : []
|
|
|
+ })
|
|
|
+}
|
|
|
+function recordItems(value) {
|
|
|
+ return (Array.isArray(value) ? value : []).map((item) => ({
|
|
|
+ title: item.operationName || item.operationType || item.action || item.disposalStatus || '处置记录',
|
|
|
+ time: item.operatedAt || item.operationTime || item.createTime || item.disposalTime || '',
|
|
|
+ operator: item.operatorName || item.operator || item.createByName || item.disposalUser || '',
|
|
|
+ description: item.operationDesc || item.remark || item.content || item.disposalContent || '',
|
|
|
+ }))
|
|
|
+}
|
|
|
+function eventStage(family, status, records) {
|
|
|
+ const labels = family === 'ALARM' ? ['产生', '确认', '处置', '关闭'] : ['发布', '接收', '处置', '核验', '解除']
|
|
|
+ const normalized = String(status || '').toUpperCase()
|
|
|
+ let active = normalized.includes('RESOLV') || normalized.includes('CLEAR') || normalized.includes('CLOSE') ? labels.length - 1
|
|
|
+ : normalized.includes('PROCESS') || normalized.includes('HANDL') ? Math.min(2, labels.length - 1)
|
|
|
+ : normalized.includes('RECEIV') || normalized.includes('CONFIRM') ? 1 : 0
|
|
|
+ if (Array.isArray(records) && records.length) active = Math.max(active, Math.min(2, labels.length - 1))
|
|
|
+ return labels.map((label,index) => ({ label, state: index < active ? 'done' : index === active ? 'active' : 'pending' }))
|
|
|
+}
|
|
|
+function workOrderStage(code) {
|
|
|
+ const labels = ['创建','派单','接单','处置','验收','完成']
|
|
|
+ const n = Number(code); const active = n === 6 ? 3 : n === 7 ? 0 : Number.isInteger(n) ? Math.max(0, Math.min(n, 5)) : 0
|
|
|
+ return labels.map((label,index) => ({ label, state: index < active ? 'done' : index === active ? 'active' : 'pending' }))
|
|
|
+}
|
|
|
+function timelineAttachments(records) { return (Array.isArray(records) ? records : []).flatMap((item) => item.attachList || item.attachments || []) }
|
|
|
+
|
|
|
+/**
|
|
|
+ * 缺值剔除:always 行例外 —— 业务上"这个设施没有该属性"时要显式显示占位符
|
|
|
+ * (维护者 2026-10-29:非排水管的二级分类显示「—」,不是把整行藏掉)。
|
|
|
+ */
|
|
|
+function compactSections(sections) {
|
|
|
+ return sections
|
|
|
+ .map((group) => ({ ...group, fields: group.fields.filter((item) => item.always || item.value !== PLACEHOLDER) }))
|
|
|
+ .filter((group) => group.fields.length)
|
|
|
+}
|
|
|
+function section(title, fields) { return { title, fields } }
|
|
|
+function field(label, value, wide = false) { return { label, value: display(value), wide, always: false, note: '' } }
|
|
|
+function display(value) { return value === null || value === undefined || String(value).trim() === '' ? PLACEHOLDER : String(value) }
|
|
|
+function text(value) { return value === null || value === undefined ? '' : String(value).trim() }
|
|
|
+function withUnit(value, unit) { return value === null || value === undefined || value === '' ? '' : [value, unit].filter(Boolean).join(' ') }
|
|
|
+function threshold(source) {
|
|
|
+ const min = source.thresholdMinValue ?? source.minValue
|
|
|
+ const max = source.thresholdMaxValue ?? source.maxValue
|
|
|
+ if (min === undefined && max === undefined) return ''
|
|
|
+ return withUnit(min !== undefined && max !== undefined ? `${min} ~ ${max}` : min !== undefined ? `≥ ${min}` : `≤ ${max}`, source.unit)
|
|
|
+}
|
|
|
+function positive(value, fallback) { const n = Number(value); return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback }
|
|
|
+function nonNegative(value, fallback) { const n = Number(value); return Number.isFinite(n) && n >= 0 ? Math.floor(n) : fallback }
|
|
|
+function listCount(value, unit) { return Array.isArray(value) ? `${value.length} ${unit}` : '' }
|
|
|
+function locationText(value) {
|
|
|
+ if (!value || typeof value !== 'object') return ''
|
|
|
+ return value.location || value.address || [value.longitude, value.latitude].filter((item) => item !== undefined && item !== null).join(', ')
|
|
|
+}
|
|
|
+function matchesId(row, id) { return !!row && [row.id, row.code, row.workOrderCode, row.warningNo, row.alarmId, row.facilityCode, row.resourceId].some((value) => String(value ?? '').trim() === id) }
|
|
|
+function workOrderTone(code) { if (code === 5) return 'lv4'; if (code === 7) return 'lv1'; if (code === 6) return 'lv2'; return 'lv3' }
|