| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678 |
- /**
- * 统一实体详情领域边界(issue 65)。
- *
- * 列表只传 { kind, id };本模块负责身份校验、不同后端载荷的正规化和紧凑详情视图。
- * point/device 沿用既有详情链,不在此重复实现。
- */
- import { eventLevelFromBucket, eventLevelTag, eventStatusText } from './eventList.js'
- import { DEFAULT_TONE } from './statusTones.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.displayCode || current.id].filter(Boolean).join(' · '),
- tone: current.tone || 'neutral',
- status: current.statusText || '',
- // 等级与状态各有自己的语义色:等级沿用卡片那张四级词表,不与状态色混用。
- levelLabel: current.levelLabel || '',
- levelTone: current.levelTone || '',
- // 类别是身份而不是普通字段:抬头要能一眼看出是什么对象。
- 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('设施编码', source.code || identity.id), field('设施类别', facilityCategoryText(category)), field('运行状态', status), field('数据更新时间', source.updatedAt)]),
- section('位置与归属', [field('位置描述', location, true), field('所属片区', source.districtName), field('坐标', coordinatesText(source.coordinates), true)]),
- section('业务详情', fields),
- ]),
- }
- }
- /** 设施状态 → 语义色键(0 正常=绿、1 维护中=黄、2 异常=红;认不出走中性)。
- * 维护者第 15 轮:运行信息的指标行也要按设施状态着色,所以本函数对外导出(原先是模块私有)。 */
- export 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'
- }
- /**
- * 数值型指标的**单位登记**(维护者 2026-10-29「所有数值都要带单位」)。
- *
- * 只在这里写一次,摘要网格与业务明细都从这里取 —— 两处各写一份单位必然漂移
- * (已经踩过:摘要写「长度(米)」、明细写「长度」+ 单位,同一份数据两种长相)。
- * 单位来自后端领域注释,不由前端猜。
- */
- const COMMON_UNITS = {
- lengthMeters: '米',
- diameterMillimeters: '毫米',
- designCapacity: '万m³/d',
- currentCapacity: '万m³/d',
- designHead: 'm',
- storageCapacity: 'm³',
- pumpCount: '台',
- powerCapacity: 'kW',
- assetValue: '元',
- useLifeYears: '年',
- riskScore: '分',
- }
- /**
- * 同一字段名在不同设施里量纲不同:泵站设计流量是 m³/h,排水工程设施设计流量是 m³/s。
- * 所以单位必须**按类别**登记,"一个键一个单位"会张冠李戴。
- */
- const UNIT_BY_CATEGORY = {
- PIPE_NETWORK: {},
- WATER_PLANT: {},
- PUMP_STATION: { designFlow: 'm³/h' },
- ENGINEERING_FACILITY: { designFlow: 'm³/s' },
- EMERGENCY_RESOURCE: {},
- HAZARD: {},
- PROTECTION_TARGET: {},
- }
- /** 字段在该设施类别下的单位;没登记返回空串(纯文本字段)。 */
- function unitOf(category, key) {
- const scoped = UNIT_BY_CATEGORY[category] || {}
- return scoped[key] !== undefined ? scoped[key] : COMMON_UNITS[key] || ''
- }
- /** 摘要与明细共用的中文字段名(维护者 2026-10-29:一律中文、不带括注)。 */
- const SUMMARY_LABELS = {
- lengthMeters: '长度',
- diameterMillimeters: '管径',
- material: '材质',
- designCapacity: '设计能力',
- currentCapacity: '当前能力',
- treatmentProcess: '处理工艺',
- designFlow: '设计流量',
- designHead: '设计扬程',
- storageCapacity: '调蓄容积',
- pumpCount: '水泵数量',
- powerCapacity: '装机功率',
- assetValue: '资产价值',
- useLifeYears: '预计使用年限',
- riskScore: '风险分值',
- riskLevel: '风险等级',
- fmodel: '型号',
- model: '型号',
- specification: '规格',
- maintainer: '维护人',
- level: '等级',
- rectifyDeadline: '整改期限',
- rectifyPerson: '整改人',
- assessmentTime: '评估时间',
- facilityLevel: '设施等级',
- }
- /** 同类字段在不同设施里的业务名不同(隐患等级 vs 设施等级),差异只在这里登记。 */
- const SUMMARY_LABELS_BY_CATEGORY = {
- HAZARD: { level: '隐患等级' },
- ENGINEERING_FACILITY: { facilityLevel: '设施等级' },
- }
- function assetSummary(category, a, status, location) {
- // 摘要只挑"一眼要看的读数",键与明细同源;单位统一由 METRIC_UNITS 提供。
- const byCategory = {
- WATER_PLANT: ['currentCapacity', 'designCapacity', 'treatmentProcess'],
- ENGINEERING_FACILITY: ['facilityLevel', 'designFlow', 'storageCapacity'],
- PUMP_STATION: ['pumpCount', 'designFlow', 'powerCapacity'],
- PIPE_NETWORK: ['lengthMeters', 'diameterMillimeters', 'material'],
- HAZARD: ['level', 'rectifyDeadline', 'rectifyPerson'],
- PROTECTION_TARGET: ['riskLevel', 'riskScore', 'assessmentTime'],
- EMERGENCY_RESOURCE: ['model', 'specification', 'maintainer'],
- }
- const labels = { ...SUMMARY_LABELS, ...SUMMARY_LABELS_BY_CATEGORY[category] }
- const rows = (byCategory[category] || []).map((key) => {
- const unit = unitOf(category, key)
- const raw = a[key]
- // 只有真有单位时才拼单位;纯文本字段(材质/负责人…)原样。
- const value = unit ? withUnitText(raw, unit) : raw
- return [labels[key] || SUMMARY_LABELS[key] || key, value]
- })
- return summaryItems([['状态', status], ...rows, ['位置', 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
- }
- /** 类别显示名:一律中文(维护者 2026-10-29:取消英文括注)。 */
- export function facilityCategoryText(category) {
- return codedText(category, FACILITY_CATEGORY_TEXT)
- }
- /** 设施状态(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: '管网类型', key: 'networkType', format: (v) => codedText(v, NETWORK_TYPE_TEXT) },
- // 二级分类=原来的"管网等级":后端 network_level 只用于区分排水管道(污水/雨水)。
- // 非排水管这个字段没有业务含义,按要求显示「—」而不是隐藏。
- {
- label: '二级分类',
- key: 'networkLevel',
- always: true,
- format: (v) => (drainage ? codedText(v, NETWORK_LEVEL_TEXT) : ''),
- invalidNote: drainage ? '' : '仅排水管道适用',
- },
- { label: '长度', key: 'lengthMeters' },
- { label: '管径', key: 'diameterMillimeters' },
- { label: '材质', key: 'material' },
- { label: '管养单位', key: 'managementUnit' },
- { label: '建设日期', key: 'buildDate' },
- { label: '备注', key: 'remark', wide: true },
- ]
- }
- const ASSET_FIELD_ROWS = {
- PIPE_NETWORK: null, // 见 pipesFields():需要按管网类型决定二级分类是否适用
- WATER_PLANT: [
- { label: '水源类型', key: 'waterSourceType' },
- { label: '设计能力', key: 'designCapacity' },
- { label: '当前能力', key: 'currentCapacity' },
- { 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: '设施类型', key: 'facilityType', format: (v) => codedText(v, ENGINEERING_FACILITY_TYPE_TEXT) },
- { label: '设施等级', key: 'facilityLevel' },
- { label: '管理单位', key: 'managementUnit' },
- { label: '建设日期', key: 'buildDate' },
- { label: '设计流量', key: 'designFlow' },
- { label: '设计扬程', key: 'designHead' },
- { label: '调蓄容积', key: 'storageCapacity' },
- { label: '关联设备', key: 'equipmentIds', wide: true },
- { label: '备注', key: 'remark', wide: true },
- ],
- PUMP_STATION: [
- { label: '泵站类型', key: 'stationType' },
- { label: '水泵数量', key: 'pumpCount' },
- { label: '设计流量', key: 'designFlow' },
- { label: '设计扬程', key: 'designHead' },
- { label: '装机功率', key: 'powerCapacity' },
- { label: '投产日期', key: 'commissionDate' },
- { label: '负责人', key: 'principal' },
- { label: '联系电话', key: 'contactPhone' },
- { label: '备注', key: 'remark', wide: true },
- ],
- EMERGENCY_RESOURCE: [
- { label: '设备类型', key: 'equipmentTypeId' },
- { label: '型号', key: 'model' },
- { label: '规格', key: 'specification', wide: true },
- { label: '制造商', key: 'manufacturer' },
- { label: '资产价值', key: 'assetValue' },
- { label: '预计使用年限', key: 'useLifeYears' },
- { label: '安装时间', key: 'installTime' },
- { label: '维护人', key: 'maintainer' },
- { label: '维护电话', key: 'maintainerPhone' },
- { label: '权属单位', key: 'ownershipUnit' },
- { label: '设备图片', key: 'image' },
- { label: '备注', key: 'remark', wide: true },
- ],
- HAZARD: [
- { label: '隐患类型', key: 'hazardType' },
- { label: '隐患等级', key: 'level' },
- { label: '来源', key: 'source' },
- { label: '隐患描述', key: 'description', wide: true },
- { label: '关联管网', key: 'networkId' },
- { label: '关联设备', key: 'equipmentId' },
- { label: '关联工单', 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: '关联管网', key: 'networkId' },
- { label: '风险等级', key: 'riskLevel' },
- { label: '风险分值', key: 'riskScore' },
- { label: '风险颜色', key: 'riskColor' },
- { label: '评估模型', 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) => {
- // 单位一律由 `unitOf(category, key)` 给:同一字段名在不同设施里量纲可能不同。
- const raw = row.format ? row.format(a[row.key]) : withUnitText(a[row.key], unitOf(category, row.key))
- // 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(' → ')
- }
- /**
- * 事件等级 → 色键 + 文字。
- *
- * **三种输入形状都要认**(维护者 2026-10-29 走查:「等级明明能拿到却标成未知」):
- * · 后端原始事件行:`level: 1`(数字);
- * · 列表行视图 `eventRowView`:`level: { tone, label }`(已翻译过的对象);
- * · 移动端预警/报警详情:`warningLevel / alarmLevel`(数字或等级名)。
- * 详情面板首屏先用列表行 prefill、随后被远端覆盖,两条路都要能读出同一个等级 ——
- * 早先只认数字,prefill 的等级对象就被判成"等级未知"。
- */
- /** 「二级预警」「3级」「四级」→ 1..4;认不出回 null(不猜等级)。 */
- function numericLevelOf(value) {
- const text = value === null || value === undefined ? '' : String(value).trim()
- if (!text) return null
- const arabic = /([1-4])\s*级/.exec(text)
- if (arabic) return Number(arabic[1])
- const cn = /([一二三四])\s*级/.exec(text)
- if (cn) return { 一: 1, 二: 2, 三: 3, 四: 4 }[cn[1]]
- return null
- }
- function eventLevelFrom(source) {
- const raw = source || {}
- // 已经翻译过的对象:原样沿用(这正是列表与详情一致的结构性保证)。
- const tagged = raw.level
- if (tagged && typeof tagged === 'object' && tagged.label) {
- return { tone: tagged.tone || DEFAULT_TONE, label: tagged.label }
- }
- // 移动端字段名与等级名(如 "二级预警")走桶解析;认不出回「等级未知」。
- const direct = raw.level ?? raw.warningLevel ?? raw.alarmLevel
- if (typeof direct === 'number') return eventLevelTag(direct)
- // 中文等级名(后端会下发「二级预警」「三级报警」这类人话)→ 阿拉伯数字。
- const numeric = numericLevelOf(direct)
- if (numeric !== null) return eventLevelTag(numeric)
- const fromBucket = eventLevelFromBucket(raw.levelCode ?? raw.levelColor ?? direct, direct)
- return eventLevelTag(fromBucket)
- }
- 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 level = eventLevelFrom(source)
- 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
- // 展示用的事件编码一律取**业务编号**(warningNo / alarmNo),主键只用于取数。
- const displayCode = text(source.codeText || source.code || source.warningNo || source.alarmNo || source.eventCode) || identity.id
- return {
- ...identity,
- name,
- displayCode,
- title: name,
- kindLabel: family === 'ALARM' ? '报警' : family === 'WARNING' ? '预警' : '事件',
- family,
- statusText: status,
- tone: level.tone || 'neutral',
- // 抬头直接显示等级(与卡片同一张词表、同一套色),不再只写在字段里。
- levelLabel: level.label,
- levelTone: level.tone,
- raw: source,
- // 等级带语义色(与卡片同一张词表),是摘要里唯一的等级呈现位置。
- summary: summaryItems([['等级', level.label, level.tone], ['状态', 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('事件编码', displayCode), field('状态', status), field('发生时间', source.eventTime || source.createTime)]),
- section('监测读数', [
- field('指标', source.type || source.warningType || source.warningCode),
- // 读数与阈值都带单位(维护者 2026-10-29:数值型指标必须有单位提示)。
- field('实际值', withUnit(source.value ?? source.actualValue, source.unit || source.warningUnit)),
- field('阈值', threshold(source)),
- field('超出幅度', withUnit(source.deviationRatio, '%')),
- ]),
- 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, '项')),
- field('督办记录', listCount(source.supervisionList, '条')),
- ]),
- ]),
- }
- }
- 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], ['优先级', orderLevelText(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('优先级', orderLevelText(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)]),
- ]),
- }
- }
- /** 工单优先级码值(0 紧急 / 1 一般 / 2 低)→ 中文;认不出回码。 */
- function orderLevelText(value) {
- const code = value === null || value === undefined ? '' : String(value).trim()
- if (!code) return ''
- return { '0': '紧急', '1': '一般', '2': '低' }[code] || code
- }
- 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,tone]) => ({ label, value: String(value), tone: tone || '' })) }
- 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() }
- /**
- * 读数 + 单位。三条纪律(维护者 2026-10-29):
- * · 没值就整句不出(不拼半截 "— μS/cm");
- * · **有值但后端没给单位**时如实标注「单位未发布」,不假装无单位是个读数;
- * · 有单位则一律追加 —— 数值型指标不允许裸数字上屏。
- */
- function withUnit(value, unit) {
- const hasValue = value !== null && value !== undefined && String(value).trim() !== ''
- if (!hasValue) return ''
- return unit ? `${value} ${unit}` : `${value}(单位未发布)`
- }
- 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' }
|