|
|
@@ -1,671 +1,686 @@
|
|
|
/**
|
|
|
- * 总体概览模式四个二级面板的**领域与视图层**(票 20)。
|
|
|
+ * 总体概览模式四块面板的**领域与视图层**(2026-10-22 四模块重做 · 票 02)。
|
|
|
*
|
|
|
- * 四块面板:态势总览 `situationSummary` / 基础设施 `infrastructureSummary` /
|
|
|
- * 运行监测 `monitoringSummary` / 预警设置 `alertSettings`。
|
|
|
+ * 四块面板(身份与左右栏顺序的唯一来源是 `domain/panelCatalog.js`,本文件只决定"每块画什么"):
|
|
|
+ * 左栏 感知设备 `perceptionDevices` / 基础设施 `pipeNetwork`
|
|
|
+ * 右栏 运行监测-预警研判 `warningStudy` / 运行监测-报警处置 `alarmDisposal`
|
|
|
*
|
|
|
- * 分工与票 17/18/19 完全一致(本票沿用同一形状,不造第二套):
|
|
|
- * 1. **契约适配**:后端面板级聚合读模型的载荷 → 领域对象(字段改名、缺失记账、码翻人话);
|
|
|
- * 2. **视图计划**:领域对象 → 组件可以直接画的块。
|
|
|
- * 块的构造与码表在 `domain/panelBlocks.js`;本文件只写"放哪些块"。
|
|
|
+ * 三层分工沿用票 17–20 已定的成例,一字不改:
|
|
|
+ * 1. **契约适配**:端点载荷 → 领域对象(字段改名、缺失记账、码翻人话);
|
|
|
+ * 2. **视图计划**:领域对象 → 组件可以直接画的块(块画法在 `domain/panelBlocks.js`);
|
|
|
+ * 3. **取数**:`services/` + `stores/`(**本票不接线**,见下)。
|
|
|
*
|
|
|
- * 纯函数:不 import service、不碰 DOM、不自己发请求。
|
|
|
- *
|
|
|
- * ── 三处后端契约现状,如实记在这里(不猜、也不拿别的字段冒充)──
|
|
|
- *
|
|
|
- * ① **本模式的三个端点都不给点位编码或坐标**(逐类读过三个响应类与三个 Service)。
|
|
|
- * 所以「点面板指标联动一张图」在**概览自己的数据上做不到** ——
|
|
|
- * 票 17 那条 `mapLocate` 接缝需要"点位编码 → 回联已加载对象",
|
|
|
- * 而这里连编码都没有,没有可绑的东西。本票的联动因此走**两条真实存在的路**:
|
|
|
- * · 指标/事件族 → 切到「预警处置」模式落到真实事件列表(那里的事件带 `deviceCode`,
|
|
|
- * 再由票 13/14 的既有链路跳到点位);
|
|
|
- * · 行业构成行 → 写全局行业筛选(`filterStore.setIndustry`,**真的**驱动地图与所有面板)。
|
|
|
- * **不伪造**一次地图定位,也不在概览里造"设备→点位"跳转。
|
|
|
+ * 本票的交付边界(票 02 原文「可先按契约数据结构画,数据源/后端接线归后续票 05–08」):
|
|
|
+ * 这四块目前**没有 store 喂数据**,分区停在「正在取数」;05–08 把规格 §1 的四个新端点
|
|
|
+ * 接上后本文件的块形状不再改动 —— 这正是本票的目的:先把身份、顺序、画法钉死。
|
|
|
*
|
|
|
- * ② **`/overview/infrastructure` 没有区域分布**(`Totals` + `Industries` + `Excluded`)。
|
|
|
- * 验收第 2 条点名的「区域分布」数据真实存在于票 17 的 `/infrastructure/base-archives`
|
|
|
- * (`districtDistribution`),且「基础档案」面板已经在画它。本层**复用票 17 的纯适配函数**
|
|
|
- * `toBaseArchives()` 取那一段 —— 只引适配层,**不引块构造器**(那才是票 18 警告的耦合)。
|
|
|
+ * 口径唯一来源:`.scratch/cockpit-overview-redesign/specs/2026-10-22-overview-four-modules-contract.md`
|
|
|
+ * §2 感知设备 / §3 基础设施 / §4 预警研判 / §5 报警处置 / §6 UI 契约。
|
|
|
*
|
|
|
- * ③ **`/overview/monitoring` 的状态桶没有"故障"**(`online/offline/stale/unknownOrMissing`)。
|
|
|
- * 验收第 3 条点名的「故障统计」在后端**不存在**独立桶,且「是否等同故障」是票 25
|
|
|
- * 至今未答的业务问题(`research/panel-field-coverage.md` §5)。维护者 2026-09-29 裁定:
|
|
|
- * **不发明"故障"数**,四个真实桶按本义呈现,缺口写进 `MONITORING_FAULT_GAP`。
|
|
|
- *
|
|
|
- * 后端形状依据(`E:\pipe-ner-server`,逐类读过响应类与 Service):
|
|
|
- * `CockpitOverviewInfrastructureResponse` / `CockpitOverviewMonitoringResponse` /
|
|
|
- * `CockpitOverviewWarningSettingsResponse`、
|
|
|
- * `CockpitOverview{Infrastructure,Monitoring,WarningSettings}Service`。
|
|
|
- * BigDecimal 在线上是字符串(`getValue()` 带 `@JsonFormat(shape=STRING)`),故走 `numberish`。
|
|
|
+ * 纯函数:不 import service、不碰 DOM、不自己发请求。
|
|
|
+ * 渲染层(`components/PanelBlocks.vue`)仍**不认识 panelId、不认识后端字段**。
|
|
|
*/
|
|
|
import { fieldReader } from './fields.js'
|
|
|
import { DETAIL_PLACEHOLDER } from './detailFlow.js'
|
|
|
import { valueText } from './equipmentDetailView.js'
|
|
|
-import { industryLabel } from './filterOptions.js'
|
|
|
+import { SECTION_STATE } from './sectionState.js'
|
|
|
import {
|
|
|
- barsBlock,
|
|
|
- codeText,
|
|
|
+ cardsBlock,
|
|
|
donutBlock,
|
|
|
kpiBlock,
|
|
|
numberish,
|
|
|
panelMissingNote,
|
|
|
+ rankingBarsBlock,
|
|
|
ratioText,
|
|
|
- rowsBlock,
|
|
|
sectionHasContent,
|
|
|
sectionPlaceholder,
|
|
|
- sortByCountDesc,
|
|
|
- stackedBlock,
|
|
|
trendBlock,
|
|
|
} from './panelBlocks.js'
|
|
|
-// 票 17 的纯适配层:只为拿 `districtDistribution`(见文件头第 ② 条)。
|
|
|
-// 引的是 `toBaseArchives` 这一个纯函数,不是它那四块面板的构造器。
|
|
|
-import { toBaseArchives } from './infrastructurePanels.js'
|
|
|
-// 票 19 的处置率读数与映射:态势总览与预警效率必须用**同一条**判据
|
|
|
-// (否则"分母为 0"在一个面板是 `—`、在另一个是 `0.0%`)。
|
|
|
-import { handlingRateView, handlingRateHint } from './warningResponsePanels.js'
|
|
|
-// 后端 0–7 状态码的唯一前端字典(票 20 收口)。
|
|
|
-import { WORK_ORDER_STATES } from './workOrder.js'
|
|
|
-
|
|
|
-/** 总体概览模式的四个 panelId(与 `domain/panelCatalog.js` 的目录一致,顺序即界面顺序)。 */
|
|
|
-export const OVERVIEW_PANEL_IDS = [
|
|
|
- 'situationSummary',
|
|
|
- 'infrastructureSummary',
|
|
|
- 'monitoringSummary',
|
|
|
- 'alertSettings',
|
|
|
-]
|
|
|
+
|
|
|
+/** 总体概览模式的四个 panelId(顺序即界面顺序:左栏两块在前,右栏两块在后)。 */
|
|
|
+export const OVERVIEW_PANEL_IDS = ['perceptionDevices', 'pipeNetwork', 'warningStudy', 'alarmDisposal']
|
|
|
|
|
|
/**
|
|
|
- * 「故障统计」在后端契约里没有独立桶的如实说明(验收第 3 条的缺口,见文件头第 ③ 条)。
|
|
|
+ * 设备大类(后端根类型码 1/2/3/4)→ 行业色键 / 行业图标 / 全局行业筛选键。
|
|
|
*
|
|
|
- * 维护者 2026-09-29 裁定:故障语义复杂("数据过期"未必是故障,可能只是网络波动),
|
|
|
- * 因此**不合成**一个"故障"数,四个真实状态桶按本义呈现。
|
|
|
+ * 三张表都按**后端根类型码**索引(不是中文名):码是稳定键,中文只用于展示(ADR 0003 同一条立场)。
|
|
|
+ * 筛选键取 `domain/filterOptions.js` 的既有行业键,所以点类型卡走的是与顶部行业 chip **同一条**筛选,
|
|
|
+ * 不是第二个筛选入口。
|
|
|
*/
|
|
|
-export const MONITORING_FAULT_GAP =
|
|
|
- '后端监测概览的状态桶是「在线 / 离线 / 滞后 / 状态未知」四态,没有独立的「故障」桶;' +
|
|
|
- '本面板照实呈现这四态,不把「滞后」或「状态未知」折算成故障(故障口径待业务确认)'
|
|
|
-
|
|
|
-/** 概览的联动落点:指标点击切到预警处置模式(见文件头第 ① 条)。 */
|
|
|
-export const OVERVIEW_LINK_MODES = {
|
|
|
- WARNING: 'warningResponse',
|
|
|
- ALARM: 'warningResponse',
|
|
|
- EFFICIENCY: 'warningResponse',
|
|
|
-}
|
|
|
+const TYPE_TONES = { 1: 'industry:water', 2: 'industry:drainage', 3: 'industry:gas', 4: 'industry:manhole' }
|
|
|
+const TYPE_ICONS = { 1: 'water', 2: 'drainage', 3: 'gas', 4: 'manhole' }
|
|
|
+const TYPE_FILTER_KEYS = { 1: 'water', 2: 'drainage', 3: 'gas', 4: 'manhole' }
|
|
|
+
|
|
|
|
|
|
// ================================================================ 契约适配
|
|
|
|
|
|
+/** 比例读数(`{value, numerator, denominator, state, reason}`)→ 领域对象。 */
|
|
|
+function toRatio(raw) {
|
|
|
+ const rate = fieldReader(raw, 'ratio')
|
|
|
+ return {
|
|
|
+ value: numberish(rate.raw.value),
|
|
|
+ numerator: numberish(rate.raw.numerator),
|
|
|
+ denominator: numberish(rate.raw.denominator),
|
|
|
+ state: rate.optionalStr('state'),
|
|
|
+ reason: rate.optionalStr('reason'),
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/** 今日/昨日/差值(规格 §0:「较昨日」= 今日新增 − 昨日新增,可为负)。 */
|
|
|
+function toToday(raw) {
|
|
|
+ const today = fieldReader(raw, 'today')
|
|
|
+ return {
|
|
|
+ newCount: numberish(today.raw.newCount),
|
|
|
+ yesterdayCount: numberish(today.raw.yesterdayCount),
|
|
|
+ delta: numberish(today.raw.delta),
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
/**
|
|
|
- * 概览基础设施(`/overview/infrastructure`)→ 领域对象。
|
|
|
- * 验收第 2 条:设施底数、行业构成、区域分布。
|
|
|
+ * 感知设备(规格 §2 `/overview/perception`)→ 领域对象。
|
|
|
*
|
|
|
- * `districtDistribution` **不在本端点上**(见文件头第 ② 条),由 `toBaseArchives()` 的那一份补,
|
|
|
- * 本函数只做本端点自己的字段。
|
|
|
+ * 交接说明(票 05):`types` 已按后端固定顺序(供水/排水/燃气/窨井)原样带出,
|
|
|
+ * **本层不重排、不补项**;`note` 就是后端给的监测内容小字(规格 §2 的固定常量映射)。
|
|
|
*/
|
|
|
-export function toOverviewInfrastructure(raw) {
|
|
|
- const reader = fieldReader(raw, 'overviewInfrastructure')
|
|
|
- const totals = fieldReader(reader.raw.totals, 'overviewInfrastructure.totals')
|
|
|
+export function toPerception(raw) {
|
|
|
+ const reader = fieldReader(raw, 'overviewPerception')
|
|
|
+ const totals = fieldReader(reader.raw.totals, 'overviewPerception.totals')
|
|
|
+ const excluded = reader.raw.excluded || {}
|
|
|
|
|
|
return {
|
|
|
updatedAt: reader.optionalStr('updatedAt'),
|
|
|
- facilityCount: numberish(totals.raw.facilityCount),
|
|
|
- smartDeviceCount: numberish(totals.raw.smartDeviceCount),
|
|
|
- // 行业构成:后端已排好序,本层不重排;`label` 缺失时按码翻(不显示裸码)。
|
|
|
- industries: reader.optionalList('industries').map((item) => {
|
|
|
- const row = fieldReader(item, 'overviewIndustry')
|
|
|
+ deviceCount: numberish(totals.raw.deviceCount),
|
|
|
+ onlineCount: numberish(totals.raw.onlineCount),
|
|
|
+ offlineCount: numberish(totals.raw.offlineCount),
|
|
|
+ onlineRate: toRatio(totals.raw.onlineRate),
|
|
|
+ // 类型卡:四项固定顺序由后端给;卡内小字 = `monitoringContent`。
|
|
|
+ // 维护者 2026-10-23:供水/排水这两张卡下的小字「尽可能精简」——
|
|
|
+ // 真实内容很长(供水 4 项、排水 8 项,如「排水瞬时流量监测 / 排水流速监测 / …」),
|
|
|
+ // 所以展示层压缩成「最多两项 + 等」,**完整原文进 title 悬停可读**,不丢信息。
|
|
|
+ types: reader.optionalList('byType').map((item) => {
|
|
|
+ const row = fieldReader(item, 'overviewPerception.byType')
|
|
|
+ const monitoringContent = row.optionalStr('monitoringContent')
|
|
|
return {
|
|
|
code: row.optionalStr('code'),
|
|
|
- label: row.optionalStr('label') || industryLabel(row.optionalStr('code')) || row.optionalStr('code'),
|
|
|
- key: row.optionalStr('key'),
|
|
|
- facilityCount: numberish(row.raw.facilityCount),
|
|
|
- smartDeviceCount: numberish(row.raw.smartDeviceCount),
|
|
|
- facilityTypes: (row.raw.facilityTypes || []).map((type) => {
|
|
|
- const row2 = fieldReader(type, 'facilityType')
|
|
|
- // 票 38(第 2 条):`scale` 不再透传——真实值是 "km"(长度单位)不是规模语义,
|
|
|
- // 长度已由 lengthKm 承担,界面没有任何「规模」展示位(保留它只会诱导出第二次误用)。
|
|
|
- return { code: row2.optionalStr('code'), name: row2.optionalStr('name') || row2.optionalStr('code'), count: numberish(row2.raw.count), lengthKm: numberish(row2.raw.lengthKm) }
|
|
|
- }),
|
|
|
+ label: row.optionalStr('name') || row.optionalStr('code'),
|
|
|
+ count: numberish(row.raw.deviceCount) ?? 0,
|
|
|
+ note: conciseMonitoring(monitoringContent),
|
|
|
+ noteTitle: monitoringContent,
|
|
|
+ }
|
|
|
+ }),
|
|
|
+ // 片区卡:三项固定(城南/城北/太常),无数据是 0。
|
|
|
+ districts: reader.optionalList('byDistrict').map((item) => {
|
|
|
+ const row = fieldReader(item, 'overviewPerception.byDistrict')
|
|
|
+ const district = row.raw.district
|
|
|
+ return {
|
|
|
+ code: district === null || district === undefined ? 'UNASSIGNED' : String(district),
|
|
|
+ label: row.optionalStr('name') || row.optionalStr('district'),
|
|
|
+ count: numberish(row.raw.deviceCount) ?? 0,
|
|
|
}
|
|
|
}),
|
|
|
excluded: {
|
|
|
- unknownTypeCount: numberish((reader.raw.excluded || {}).unknownTypeCount),
|
|
|
- typeKindMismatchCount: numberish((reader.raw.excluded || {}).typeKindMismatchCount),
|
|
|
+ unknownTypeCount: numberish(excluded.unknownTypeCount),
|
|
|
+ unassignedDistrictCount: numberish(excluded.unassignedDistrictCount),
|
|
|
},
|
|
|
missing: reader.missing,
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-/**
|
|
|
- * 概览运行监测(`/overview/monitoring`)→ 领域对象。
|
|
|
- * 验收第 3 条:在线率、离线/故障统计、监测覆盖。
|
|
|
- *
|
|
|
- * 端点入参只有 `industry`(控制器逐行读过)——不收区域/类型/状态。
|
|
|
- */
|
|
|
-export function toOverviewMonitoring(raw) {
|
|
|
- const reader = fieldReader(raw, 'overviewMonitoring')
|
|
|
- const counts = fieldReader(reader.raw.counts, 'overviewMonitoring.counts')
|
|
|
- const status = fieldReader(counts.raw.status, 'overviewMonitoring.status')
|
|
|
- const onlineRate = fieldReader(counts.raw.onlineRate, 'overviewMonitoring.onlineRate')
|
|
|
- const metricFreshness = fieldReader(reader.raw.metricFreshness, 'overviewMonitoring.metricFreshness')
|
|
|
- const scope = reader.optionalObj('formalScope') || {}
|
|
|
+/** 基础设施(规格 §3 `/overview/pipe-network`)→ 领域对象。 */
|
|
|
+export function toPipeNetwork(raw) {
|
|
|
+ const reader = fieldReader(raw, 'overviewPipeNetwork')
|
|
|
+ const totals = fieldReader(reader.raw.totals, 'overviewPipeNetwork.totals')
|
|
|
+ const excluded = reader.raw.excluded || {}
|
|
|
|
|
|
return {
|
|
|
updatedAt: reader.optionalStr('updatedAt'),
|
|
|
- // 规模与覆盖
|
|
|
- deviceCount: numberish(counts.raw.deviceCount),
|
|
|
- monitoringPointCount: numberish(counts.raw.monitoringPointCount),
|
|
|
- monitoringFacilityCount: numberish(counts.raw.monitoringFacilityCount),
|
|
|
- infrastructureFacilityCount: numberish(counts.raw.infrastructureFacilityCount),
|
|
|
- formalIndustries: Array.isArray(scope.industries) ? scope.industries.filter(Boolean) : [],
|
|
|
- // 在线率:`state` 为 EMPTY 表示分母为 0(那时 `value` 的 0 是占位,不是 0%)。
|
|
|
- onlineRate: numberish(onlineRate.raw.value),
|
|
|
- onlineCount: numberish(onlineRate.raw.onlineCount),
|
|
|
- onlineDenominator: numberish(onlineRate.raw.denominator),
|
|
|
- onlineState: onlineRate.optionalStr('state'),
|
|
|
- // 四个真实状态桶(**没有"故障"**,见 MONITORING_FAULT_GAP)。
|
|
|
- deviceStatus: {
|
|
|
- online: numberish(status.raw.online),
|
|
|
- offline: numberish(status.raw.offline),
|
|
|
- stale: numberish(status.raw.stale),
|
|
|
- unknownOrMissing: numberish(status.raw.unknownOrMissing),
|
|
|
- denominator: numberish(status.raw.denominator),
|
|
|
- },
|
|
|
- // **本端点独有的**逐设备指标最近采集时间(票 18 的缺口在这里有值,见其 I2/BT-8)。
|
|
|
- metricFreshness: {
|
|
|
- status: metricFreshness.optionalStr('status'),
|
|
|
- updatedAt: metricFreshness.optionalStr('updatedAt'),
|
|
|
- serverUpdatedAt: metricFreshness.optionalStr('serverUpdatedAt'),
|
|
|
- },
|
|
|
- // 类型分布:后端按 industry×deviceType 给,本层不重排。
|
|
|
- typeDistribution: reader.optionalList('typeDistribution').map((item) => {
|
|
|
- const row = fieldReader(item, 'overviewTypeDistribution')
|
|
|
- const type = fieldReader(row.raw.deviceType, 'overviewTypeDistribution.deviceType')
|
|
|
- const industry = fieldReader(row.raw.industry, 'overviewTypeDistribution.industry')
|
|
|
+ pipeCount: numberish(totals.raw.pipeCount),
|
|
|
+ lengthKm: numberish(totals.raw.lengthKm),
|
|
|
+ // 三类固定顺序:供水/排水/燃气(后端已排好,本层不重排)。
|
|
|
+ types: reader.optionalList('byPipeType').map((item) => {
|
|
|
+ const row = fieldReader(item, 'overviewPipeNetwork.byPipeType')
|
|
|
return {
|
|
|
- industryCode: industry.optionalStr('code'),
|
|
|
- industryLabel: industry.optionalStr('label') || industryLabel(industry.optionalStr('code')),
|
|
|
- typeCode: type.optionalStr('code'),
|
|
|
- typeName: type.optionalStr('name') || type.optionalStr('code'),
|
|
|
- deviceCount: numberish(row.raw.deviceCount),
|
|
|
+ code: row.optionalStr('code'),
|
|
|
+ label: row.optionalStr('name') || row.optionalStr('code'),
|
|
|
+ count: numberish(row.raw.count) ?? 0,
|
|
|
+ lengthKm: numberish(row.raw.lengthKm),
|
|
|
+ ratio: numberish(row.raw.ratio),
|
|
|
}
|
|
|
}),
|
|
|
- selectedIndustry: (reader.optionalObj('selectedIndustry') || {}).label || '',
|
|
|
- excluded: {
|
|
|
- unknownTypeCount: numberish((reader.raw.excluded || {}).unknownTypeCount),
|
|
|
- typeKindMismatchCount: numberish((reader.raw.excluded || {}).typeKindMismatchCount),
|
|
|
- },
|
|
|
+ excluded: { unclassifiedCount: numberish(excluded.unclassifiedCount) },
|
|
|
missing: reader.missing,
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-/**
|
|
|
- * 概览预警设置(`/overview/warning-settings`)→ 领域对象。
|
|
|
- * 验收第 4 条:保持合同名称,只读展示预警/报警统计。
|
|
|
- *
|
|
|
- * **一次响应同时给预警与报警两族**(`warning` + `alarm`),所以态势总览与预警设置
|
|
|
- * **共用这一次取数**(Q1 裁定),不做 WARNING/ALARM 切换。
|
|
|
- *
|
|
|
- * **票 42 起两族口径分叉**(后端 `b047a60d`,维护者 2026-10-14 裁定):
|
|
|
- * 预警族只给**预警自身生命周期**(状态/等级/行业/片区分布 + 已解除率),
|
|
|
- * 工单口径六字段(`handled/pending/cancelledCount/unlinkedEventCount/invalidWorkOrderStateCount/workOrderStates`)
|
|
|
- * 恒为 `null`;报警族**保留工单口径**(报警靠工单闭环)并**并存** `alarm_status` 生命周期分布。
|
|
|
- * `null` 在本层原样透传 —— 「不适用」与「真的是 0」必须在视图层分得开(票 22 纪律)。
|
|
|
- */
|
|
|
-export function toOverviewWarningSettings(raw) {
|
|
|
- const reader = fieldReader(raw, 'overviewWarningSettings')
|
|
|
- const freshness = fieldReader(reader.raw.freshness, 'overviewWarningSettings.freshness')
|
|
|
-
|
|
|
- /** 一族(预警或报警)→ 领域对象。 */
|
|
|
- const family = (key) => {
|
|
|
- const row = fieldReader(reader.raw[key], `overviewWarningSettings.${key}`)
|
|
|
- // 工单状态分布:**缺失(null)=该族不走工单口径**(预警族恒如此),
|
|
|
- // 绝不给它补 8 根假零条 —— 那是把"不适用"画成"全是 0"(票 42 验收第 4 条)。
|
|
|
- const states = row.optionalObj('workOrderStates')
|
|
|
- const efficiency = row.raw.handlingEfficiency || {}
|
|
|
- // 分布桶直读:后端已按族全铺且 `sum(桶)==total`,前端**不补 0、不重排、不翻码**
|
|
|
- //(`label` 就是后端给的人话)。后端没给某组时记 `null`,视图整块跳过(不画空块)。
|
|
|
- const buckets = (name) => {
|
|
|
- const items = row.raw[name]
|
|
|
- if (!Array.isArray(items)) return null
|
|
|
- return items.map((item) => {
|
|
|
- const cell = fieldReader(item, `overviewWarningSettings.${key}.${name}`)
|
|
|
- return { code: cell.optionalStr('code'), label: cell.optionalStr('label'), count: numberish(cell.raw.count) || 0 }
|
|
|
- })
|
|
|
- }
|
|
|
- return {
|
|
|
- family: row.optionalStr('eventFamily') || key.toUpperCase(),
|
|
|
- state: row.optionalStr('state'),
|
|
|
- stateReason: row.optionalStr('stateReason'),
|
|
|
- total: numberish(row.raw.total),
|
|
|
- // 工单口径五读数:预警族恒 `null`(视图显示「不适用」),报警族保留工单语义。
|
|
|
- handled: numberish(row.raw.handled),
|
|
|
- pending: numberish(row.raw.pending),
|
|
|
- cancelledCount: numberish(row.raw.cancelledCount),
|
|
|
- unlinkedEventCount: numberish(row.raw.unlinkedEventCount),
|
|
|
- invalidWorkOrderStateCount: numberish(row.raw.invalidWorkOrderStateCount),
|
|
|
- // 速率连同 `state/reason` 一起带("分母为 0/没有预警"与"真的是 0%"必须分得开);
|
|
|
- // 语义按族不同:预警族=已解除÷总数,报警族=工单口径处置率(决策票 06)。
|
|
|
- efficiency: {
|
|
|
- handlingRate: numberish(efficiency.value),
|
|
|
- handlingState: efficiency.state || '',
|
|
|
- handlingReason: efficiency.reason || null,
|
|
|
- denominator: numberish(efficiency.denominator),
|
|
|
- numerator: numberish(efficiency.numerator),
|
|
|
+/** 预警研判(规格 §4 `/overview/warning-study`)→ 领域对象。 */
|
|
|
+export function toWarningStudy(raw) {
|
|
|
+ const reader = fieldReader(raw, 'overviewWarningStudy')
|
|
|
+ const unresolved = fieldReader(reader.raw.unresolved, 'overviewWarningStudy.unresolved')
|
|
|
+ const statuses = fieldReader(unresolved.raw.statuses, 'overviewWarningStudy.unresolved.statuses')
|
|
|
+ const ranking = fieldReader(reader.raw.typeRanking, 'overviewWarningStudy.typeRanking')
|
|
|
+
|
|
|
+ return {
|
|
|
+ updatedAt: reader.optionalStr('updatedAt'),
|
|
|
+ today: toToday(reader.raw.today),
|
|
|
+ unresolved: {
|
|
|
+ count: numberish(unresolved.raw.count),
|
|
|
+ // 固定四键(规格 §4);缺的给 0(后端契约固定铺满)。
|
|
|
+ statuses: {
|
|
|
+ PENDING: numberish(statuses.raw.PENDING) ?? 0,
|
|
|
+ RECEIVED: numberish(statuses.raw.RECEIVED) ?? 0,
|
|
|
+ PROCESSING: numberish(statuses.raw.PROCESSING) ?? 0,
|
|
|
+ HANDLED: numberish(statuses.raw.HANDLED) ?? 0,
|
|
|
},
|
|
|
- workOrderStates: states === null ? null : WORK_ORDER_STATES.map((state) => {
|
|
|
- const value = states[state.code] === undefined ? states[String(state.code)] : states[state.code]
|
|
|
- return { code: String(state.code), label: state.name, count: numberish(value) || 0 }
|
|
|
- }),
|
|
|
- // 票 42:自身生命周期分布(两族都有;`statusDistribution` 的码表两族不同 ——
|
|
|
- // 预警是 `WarningWorkflow` 六态、报警是 `alarm_status` 两态,label 由后端给)。
|
|
|
- statusDistribution: buckets('statusDistribution'),
|
|
|
- levelDistribution: buckets('levelDistribution'),
|
|
|
- industryDistribution: buckets('industryDistribution'),
|
|
|
- districtDistribution: buckets('districtDistribution'),
|
|
|
- // 趋势:桶是后端给的(`range` 声明窗口后由服务端过滤),本层不重采样。
|
|
|
- // `handled` 的含义按族不同:预警=桶内已解除,报警=桶内关联工单已结案。
|
|
|
- trend: row.optionalList('trend').map((point) => {
|
|
|
- const cell = fieldReader(point, 'warningTrendPoint')
|
|
|
- return { key: cell.optionalStr('bucket'), label: cell.optionalStr('bucket'), count: numberish(cell.raw.total) || 0 }
|
|
|
+ },
|
|
|
+ // 等级卡:一级→四级固定顺序,无数据 0。
|
|
|
+ levels: reader.optionalList('byLevel').map((item) => {
|
|
|
+ const row = fieldReader(item, 'overviewWarningStudy.byLevel')
|
|
|
+ return {
|
|
|
+ code: String(row.raw.level ?? ''),
|
|
|
+ label: row.optionalStr('name') || String(row.raw.level ?? ''),
|
|
|
+ count: numberish(row.raw.count) ?? 0,
|
|
|
+ }
|
|
|
+ }),
|
|
|
+ typeRanking: {
|
|
|
+ rangeDays: numberish(ranking.raw.rangeDays),
|
|
|
+ total: numberish(ranking.raw.total),
|
|
|
+ // 后端已按计数降序且并列按码升序(规格 §4),本层不重排。
|
|
|
+ top: ranking.optionalList('top').map((item) => {
|
|
|
+ const row = fieldReader(item, 'overviewWarningStudy.typeRanking.top')
|
|
|
+ return {
|
|
|
+ code: row.optionalStr('code'),
|
|
|
+ label: row.optionalStr('name') || row.optionalStr('code'),
|
|
|
+ count: numberish(row.raw.count) ?? 0,
|
|
|
+ }
|
|
|
}),
|
|
|
- }
|
|
|
+ },
|
|
|
+ missing: reader.missing,
|
|
|
}
|
|
|
+}
|
|
|
+
|
|
|
+/** 时长读数(规格 §5:`{value, unit, sampleCount, invalidSampleCount, state, reason}`)→ 领域对象。 */
|
|
|
+function toDuration(raw) {
|
|
|
+ const row = fieldReader(raw, 'duration')
|
|
|
+ return {
|
|
|
+ value: numberish(row.raw.value),
|
|
|
+ unit: row.optionalStr('unit') || 'HOURS',
|
|
|
+ sampleCount: numberish(row.raw.sampleCount),
|
|
|
+ // 负时长被后端剔除的样本数(规格 §5 的 `invalidSampleCount`):只说"剔除了几个",
|
|
|
+ // 不拿无效样本凑平均,也不静默丢。
|
|
|
+ invalidSampleCount: numberish(row.raw.invalidSampleCount),
|
|
|
+ state: row.optionalStr('state'),
|
|
|
+ reason: row.optionalStr('reason'),
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/** 报警处置(规格 §5 `/overview/alarm-disposal`)→ 领域对象。 */
|
|
|
+export function toAlarmDisposal(raw) {
|
|
|
+ const reader = fieldReader(raw, 'overviewAlarmDisposal')
|
|
|
+ const conversion = fieldReader(reader.raw.workOrderConversion, 'overviewAlarmDisposal.workOrderConversion')
|
|
|
+ const disposal = fieldReader(reader.raw.disposal, 'overviewAlarmDisposal.disposal')
|
|
|
|
|
|
return {
|
|
|
updatedAt: reader.optionalStr('updatedAt'),
|
|
|
- displayName: reader.optionalStr('displayName'),
|
|
|
- // 窗口回显:`range.type` 就是本票请求时声明的 `LAST_7_DAYS`(验收第 1 条的"近 7 日")。
|
|
|
- rangeType: ((reader.raw.range || {}).type) || '',
|
|
|
- rangeStart: ((reader.raw.range || {}).start) || '',
|
|
|
- rangeEnd: ((reader.raw.range || {}).end) || '',
|
|
|
- freshness: freshness.optionalStr('status'),
|
|
|
- warning: family('warning'),
|
|
|
- alarm: family('alarm'),
|
|
|
- selectedMode: reader.optionalStr('selectedMode'),
|
|
|
+ today: toToday(reader.raw.today),
|
|
|
+ conversion: {
|
|
|
+ convertedCount: numberish(conversion.raw.convertedCount),
|
|
|
+ unconvertedCount: numberish(conversion.raw.unconvertedCount),
|
|
|
+ totalCount: numberish(conversion.raw.totalCount),
|
|
|
+ },
|
|
|
+ // 处置率与两个时长都**带着 state**:分母/样本为 0 时不显示 0%,显示占位符(规格 §5/§6.7)。
|
|
|
+ rate: toRatio(disposal.raw.rate),
|
|
|
+ responseDuration: toDuration(disposal.raw.avgResponseDuration),
|
|
|
+ disposalDuration: toDuration(disposal.raw.avgDisposalDuration),
|
|
|
+ levels: reader.optionalList('byLevel').map((item) => {
|
|
|
+ const row = fieldReader(item, 'overviewAlarmDisposal.byLevel')
|
|
|
+ return {
|
|
|
+ code: String(row.raw.level ?? ''),
|
|
|
+ label: row.optionalStr('name') || String(row.raw.level ?? ''),
|
|
|
+ count: numberish(row.raw.count) ?? 0,
|
|
|
+ }
|
|
|
+ }),
|
|
|
+ // 近 7 日:固定 7 点、含 0 值、顺序从早到晚(后端保证;本层不补点)。
|
|
|
+ // 每天两个读数(维护者 2026-10-23):`count` = 当天新增;`unhandledTotal` = 截至当天未处理累计。
|
|
|
+ // `unhandledTotal` 是后加字段,老响应没有它 → 记 null(视图层据此决定画不画第二条线,
|
|
|
+ // 不补 0 冒充"积压为零")。
|
|
|
+ sevenDayTrend: reader.optionalList('sevenDayTrend').map((point) => {
|
|
|
+ const row = fieldReader(point, 'overviewAlarmDisposal.sevenDayTrend')
|
|
|
+ return {
|
|
|
+ date: row.optionalStr('date'),
|
|
|
+ count: numberish(row.raw.count) ?? 0,
|
|
|
+ unhandledTotal: numberish(row.raw.unhandledTotal),
|
|
|
+ }
|
|
|
+ }),
|
|
|
missing: reader.missing,
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+// ================================================================ 视图计划
|
|
|
+
|
|
|
+/**
|
|
|
+ * 比例读数 → 界面文本:`state: EMPTY`(分母 0)显示占位符,**不显示 0%**(规格 §5 末句)。
|
|
|
+ * 与票 18/20 的在线率判据同一条,不另写第二套。
|
|
|
+ */
|
|
|
+function ratioView(rate) {
|
|
|
+ const row = rate || {}
|
|
|
+ const available = row.state !== 'EMPTY' && typeof row.denominator === 'number' && row.denominator > 0
|
|
|
+ return available ? ratioText(row.value) : DETAIL_PLACEHOLDER
|
|
|
+}
|
|
|
+
|
|
|
/**
|
|
|
- * 区域分布(票 17 的 `/infrastructure/base-archives`)→ 供基础设施面板用的领域行。
|
|
|
+ * 等级卡 → 带等级色的行(票 11 视觉对齐)。
|
|
|
*
|
|
|
- * 单独一个薄函数而不是在视图里直接调 `toBaseArchives`:这样"本面板的区域分布来自哪个端点"
|
|
|
- * 只有一处可查,且视图层不必认识票 17 的载荷形状。
|
|
|
- * 票 38:`scale` 不再带出(真实后端恒 null,「规模」读数已从界面删除);`lengthKm` 保留。
|
|
|
+ * 等级色键 lv1..lv4 由**等级码 1..4** 直接给出(ADR 0006/0008 的既有调色板),
|
|
|
+ * 不因为「一级」在预警里是最高优先、在报警里也是最高优先而另写一份映射。
|
|
|
+ * 徽标(icon)不画:设计稿的等级卡只有数字 + 名称。
|
|
|
*/
|
|
|
-export function toOverviewRegions(raw) {
|
|
|
- const archives = toBaseArchives(raw)
|
|
|
- return { regions: archives.regions || [], lengthKm: archives.lengthKm, missing: archives.missing }
|
|
|
+function withLevelTones(levels) {
|
|
|
+ return levels.map((row) => ({ ...row, valueTone: LEVEL_TONES[row.code] || '' }))
|
|
|
}
|
|
|
|
|
|
-// ================================================================ 视图计划
|
|
|
+/** 等级码 → 色键(两块的等级卡共用这一张表)。 */
|
|
|
+const LEVEL_TONES = { 1: 'lv1', 2: 'lv2', 3: 'lv3', 4: 'lv4' }
|
|
|
|
|
|
-/** 状态桶 → KPI 项(四态照实呈现,不合成"故障")。 */
|
|
|
-function statusKpis(status) {
|
|
|
- const row = status || {}
|
|
|
- return [
|
|
|
- { key: 'online', label: '在线', value: valueText(row.online), unit: '台' },
|
|
|
- { key: 'offline', label: '离线', value: valueText(row.offline), unit: '台' },
|
|
|
- // `STALE` 在契约手册里是"滞后",**不是**故障(见 MONITORING_FAULT_GAP)。
|
|
|
- { key: 'stale', label: '滞后', value: valueText(row.stale), unit: '台' },
|
|
|
- { key: 'unknownOrMissing', label: '状态未知', value: valueText(row.unknownOrMissing), unit: '台' },
|
|
|
- ]
|
|
|
+/**
|
|
|
+ * 把「监测内容」压成卡内一行小字(维护者 2026-10-23:供水/排水卡下的小字尽可能精简)。
|
|
|
+ *
|
|
|
+ * 原始内容来自库(`equipment_warning_type` 派生,实测供水 4 项、排水 8 项,最长约 90 字),
|
|
|
+ * 直接上屏会撑满两行还被裁切。压缩规则都是**去冗余、不改变含义**:
|
|
|
+ * 1. 按 `/` 切成条目;
|
|
|
+ * 2. 去掉条目里的行业前缀(卡名已经是「供水设备」「排水设备」,前缀重复);
|
|
|
+ * 3. 去掉每个条目结尾的「监测」("供水压力监测" → "压力");
|
|
|
+ * 4. 最多留 **两项**,超出加「等」。
|
|
|
+ * 例:供水 → `压力 / 瞬时流量 等`;排水 → `水位 / 瞬时流量 等`。
|
|
|
+ *
|
|
|
+ * 完整原文不进这里,由调用方放进 `noteTitle`(渲染层用作 title 悬停),所以没有信息丢失。
|
|
|
+ */
|
|
|
+function conciseMonitoring(text) {
|
|
|
+ const raw = String(text == null ? '' : text).trim()
|
|
|
+ if (!raw) return ''
|
|
|
+ const items = raw
|
|
|
+ .split('/')
|
|
|
+ .map((part) => part.trim())
|
|
|
+ .map((part) => part.replace(/^(供水|排水|燃气|窨井)/, '').replace(/监测$/, '').trim())
|
|
|
+ .filter(Boolean)
|
|
|
+ if (!items.length) return ''
|
|
|
+ const kept = items.slice(0, 2)
|
|
|
+ return items.length > kept.length ? `${kept.join(' / ')} 等` : kept.join(' / ')
|
|
|
}
|
|
|
|
|
|
-// 曾有一个 `actionableBarsBlock()` 局部函数(票 20 立,给每行补 `actionKey`):
|
|
|
-// 它**从未被调用过**(票 37 复核:全仓 grep 唯一命中是它自己的定义),
|
|
|
-// 且与本票新增的 `barsBlock(label, rows, { action })` 是同一件事的两种写法 ——
|
|
|
-// 「同一件事两处实现」正是维护者 2026-10-13 点名的「早晚不一致」来源,故随本票删除。
|
|
|
-// 可点分布块现在只有一条路:`barsBlock(..., { action })`(见 `domain/panelBlocks.js`)。
|
|
|
-
|
|
|
-/** 在线率读数:分母为 0 显示占位符,不画 0.0%(票 18 的纪律,此处复用同一判据)。 */
|
|
|
-function onlineRateView(data) {
|
|
|
- const row = data || {}
|
|
|
- const available = row.onlineState !== 'EMPTY' && typeof row.onlineDenominator === 'number' && row.onlineDenominator > 0
|
|
|
- return available ? ratioText(row.onlineRate) : DETAIL_PLACEHOLDER
|
|
|
+/** 折线点列 → 纯数值列表(多序列 API 只要值,日期标签共用主轴的 points)。 */
|
|
|
+function trendOf(sevenDayTrend) {
|
|
|
+ return (sevenDayTrend || []).map((point) => numberish(point.count) || 0)
|
|
|
}
|
|
|
|
|
|
-function onlineRateHint(data) {
|
|
|
- const row = data || {}
|
|
|
- const available = row.onlineState !== 'EMPTY' && typeof row.onlineDenominator === 'number' && row.onlineDenominator > 0
|
|
|
- if (available) return `在线 ${valueText(row.onlineCount)} / 分母 ${valueText(row.onlineDenominator)}`
|
|
|
- return '当前范围内没有可参与计算的设备,在线率不适用'
|
|
|
+/**
|
|
|
+ * 「总未处理」序列(维护者 2026-10-23 反馈第 2 条)。
|
|
|
+ *
|
|
|
+ * 口径:**截至当天仍未处理**的累计条数(`alarm_status = 0`,0=未处理 / 1=已处理 ——
|
|
|
+ * 见后端 `AlarmRecordStatusEnum`)。它不是"当天新增里未处理的",而是"到那天为止积压了多少",
|
|
|
+ * 所以能和"当天新增"放在同一张图上做对照:新增归零而积压不为零,读起来就是"最近没新报警、
|
|
|
+ * 但历史未处理的还在那里"。
|
|
|
+ *
|
|
|
+ * 数据来源:后端按日给 `sevenDayTrend[].unhandledTotal`(新字段);还没升级的后端
|
|
|
+ * (返回里没有该字段)时回空列表 —— 那样图例会少一条线,而不是画一条假的 0 线。
|
|
|
+ */
|
|
|
+function unhandledOf(current) {
|
|
|
+ const rows = (current && current.sevenDayTrend) || []
|
|
|
+ if (!rows.some((point) => point.unhandledTotal !== undefined && point.unhandledTotal !== null)) return []
|
|
|
+ return rows.map((point) => numberish(point.unhandledTotal) || 0)
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 态势总览面板视图(验收第 1 条)。
|
|
|
+ * 未解除卡片的第三行小字:「处置中预警」(设计稿原句)。
|
|
|
*
|
|
|
- * 跨行业 KPI:设施底数、监测规模、在线率、当前预警/报警、预警已解除率 + 近 7 日趋势。
|
|
|
+ * 只有**真的**取到处置中条数(>0)才报这个数;否则回落到「待处置预警总量」的口径说明——
|
|
|
+ * 不编一个读数,也不把 0 写成「处置中 0 条」这种看起来像正常业务的假读数。
|
|
|
+ */
|
|
|
+function unresolvedCaption(unresolved) {
|
|
|
+ const processing = numberish((unresolved || {}).statuses && (unresolved || {}).statuses.PROCESSING)
|
|
|
+ if (processing !== null && processing > 0) return `处置中预警 ${processing} 条`
|
|
|
+ return '含待办 / 已接收 / 处置中 / 已处置'
|
|
|
+}
|
|
|
+
|
|
|
+/** 后端 `reason` 码 → 给人读的半句(认不出的回空串,不把接口词汇甩到大屏上) */
|
|
|
+const EMPTY_REASON_TEXT = {
|
|
|
+ NO_DEVICES: '当前没有设备',
|
|
|
+ NO_ALARMS: '当前没有已核实报警',
|
|
|
+ NO_RESPONSE_LOGS: '没有接单日志',
|
|
|
+ NO_DISPOSAL_LOGS: '没有验收通过日志',
|
|
|
+}
|
|
|
+
|
|
|
+function emptyReasonText(reason) {
|
|
|
+ return Object.prototype.hasOwnProperty.call(EMPTY_REASON_TEXT, reason) ? EMPTY_REASON_TEXT[reason] : ''
|
|
|
+}
|
|
|
+
|
|
|
+/** 比例读数的口径说明(悬停读全:分子/分母,或"分母为 0 不适用")。 */
|
|
|
+function ratioHint(rate) {
|
|
|
+ const row = rate || {}
|
|
|
+ const available = row.state !== 'EMPTY' && typeof row.denominator === 'number' && row.denominator > 0
|
|
|
+ if (!available) {
|
|
|
+ const why = emptyReasonText(row.reason)
|
|
|
+ return why ? `${why},该比例不适用(不显示 0%)` : '分母为 0,该比例不适用(不显示 0%)'
|
|
|
+ }
|
|
|
+ return `分子 ${valueText(row.numerator)} / 分母 ${valueText(row.denominator)}`
|
|
|
+}
|
|
|
+
|
|
|
+/** 单位码 → 人话(接口词汇不上大屏)。 */
|
|
|
+function durationUnitText(unit) {
|
|
|
+ const dictionary = { HOURS: '小时', MINUTES: '分钟' }
|
|
|
+ return dictionary[unit] || ''
|
|
|
+}
|
|
|
+
|
|
|
+/** 时长读数 → 界面文本(无有效样本 → 占位符,不显示 0 小时)。 */
|
|
|
+function durationView(duration) {
|
|
|
+ const row = duration || {}
|
|
|
+ if (row.state === 'EMPTY' || numberish(row.value) === null) return DETAIL_PLACEHOLDER
|
|
|
+ return valueText(row.value)
|
|
|
+}
|
|
|
+
|
|
|
+/** 时长的口径说明:单位 + 样本数 + 被剔除的无效样本(规格 §5 的 `sampleCount/invalidSampleCount`)。 */
|
|
|
+function durationHint(duration) {
|
|
|
+ const row = duration || {}
|
|
|
+ if (row.state === 'EMPTY' || numberish(row.value) === null) {
|
|
|
+ const why = emptyReasonText(row.reason)
|
|
|
+ return why ? `${why},该时长不适用` : '无有效样本,该时长不适用'
|
|
|
+ }
|
|
|
+ const unit = durationUnitText(row.unit)
|
|
|
+ const sample = typeof row.sampleCount === 'number' ? `,样本 ${row.sampleCount}` : ''
|
|
|
+ const invalid = typeof row.invalidSampleCount === 'number' && row.invalidSampleCount > 0
|
|
|
+ ? `(已剔除 ${row.invalidSampleCount} 个无效样本)`
|
|
|
+ : ''
|
|
|
+ return `单位 ${unit || row.unit}${sample}${invalid}`
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 本面板「有内容可画」的判据(规格 §6.7 的例外 + 盘点 §7.2)。
|
|
|
*
|
|
|
- * 数据来自**三份取数**(基础设施 / 运行监测 / 预警设置)——它们是各自独立成败的分区,
|
|
|
- * 所以这里把三份都作为输入,缺哪份就少哪几项读数,而**不**让整块面板消失。
|
|
|
- * @param {object} data `{ infrastructure, monitoring, warningSettings }` 三份领域对象
|
|
|
- * @param {object} section 分区(取预警设置那份的成败作本面板的代表)
|
|
|
+ * 常规分区只有 SUCCESS/STALE 有内容可画;但**固定项面板**(类型卡/片区卡/等级卡)在
|
|
|
+ * 「后端说没有数据、却仍给固定行」时必须照画 0 —— 否则「无数据 0」会被占位吞掉。
|
|
|
+ * 触发形状固定为 `state = EMPTY` 且 `data != null`(fetcher 的空判据只认关键段缺失,
|
|
|
+ * 不按计数为 0 判空,所以 EMPTY 也会带 data —— 两处判据是配套的)。
|
|
|
+ *
|
|
|
+ * 基础设施**不适用**这条例外:没有管道就不画假环(ticket 06 的验收原句)。
|
|
|
*/
|
|
|
-export function situationSummaryPanel(data, section) {
|
|
|
- const current = data || {}
|
|
|
- const infra = current.infrastructure || null
|
|
|
- const monitoring = current.monitoring || null
|
|
|
- const warnings = current.warningSettings || null
|
|
|
- const warning = (warnings && warnings.warning) || null
|
|
|
- const alarm = (warnings && warnings.alarm) || null
|
|
|
+function fixedCardsSection(section) {
|
|
|
+ if (sectionHasContent(section)) return true
|
|
|
+ return !!section && section.state === SECTION_STATE.EMPTY && section.data !== null && section.data !== undefined
|
|
|
+}
|
|
|
+
|
|
|
+/** 排除项说明(规格 §2/§3 的 `excluded`:不静默丢,见 G9)。 */
|
|
|
+function excludedText(parts) {
|
|
|
+ const text = parts.filter(Boolean).join(' · ')
|
|
|
+ return text ? `口径剔除:${text}` : ''
|
|
|
+}
|
|
|
|
|
|
+/**
|
|
|
+ * 感知设备面板(规格 §6.2)。
|
|
|
+ *
|
|
|
+ * 顶部 4 指标卡(总数/在线/离线/在线率)→ 4 张设备类型卡(含监测内容小字)→ 3 张片区卡。
|
|
|
+ * 在线率分母为 0 时显示占位符;无状态记录的设备计离线(G7 裁定)。
|
|
|
+ */
|
|
|
+export function perceptionDevicesPanel(data, section, extra) {
|
|
|
+ const current = data || {}
|
|
|
+ // 当前生效的全局行业筛选键(外壳喂的标量):只用来画类型卡的选中态。
|
|
|
+ const activeIndustry = (extra && extra.industry) || ''
|
|
|
const blocks = [
|
|
|
- kpiBlock([
|
|
|
- { key: 'facilityCount', label: '设施底数', value: valueText(infra && infra.facilityCount), unit: '处' },
|
|
|
- { key: 'smartDeviceCount', label: '监测规模', value: valueText(infra && infra.smartDeviceCount), unit: '台' },
|
|
|
- { key: 'deviceCount', label: '智能设备', value: valueText(monitoring && monitoring.deviceCount), unit: '台' },
|
|
|
- { key: 'monitoringPointCount', label: '监测点', value: valueText(monitoring && monitoring.monitoringPointCount), unit: '个' },
|
|
|
- { key: 'onlineRate', label: '在线率', value: onlineRateView(monitoring), hint: onlineRateHint(monitoring) },
|
|
|
- { key: 'warningTotal', label: '当前预警', value: valueText(warning && warning.total), unit: '条', link: 'WARNING', linkHint: '跳到预警处置模式的当前预警列表' },
|
|
|
- { key: 'alarmTotal', label: '当前报警', value: valueText(alarm && alarm.total), unit: '条', link: 'ALARM', linkHint: '跳到预警处置模式的当前报警列表' },
|
|
|
- {
|
|
|
- key: 'handlingRate',
|
|
|
- label: '预警已解除率',
|
|
|
- value: warning ? handlingRateView(warning.efficiency) : DETAIL_PLACEHOLDER,
|
|
|
- hint: warning ? warningRateHint(warning.efficiency) : '',
|
|
|
- link: 'EFFICIENCY',
|
|
|
- linkHint: '跳到预警处置模式的预警效率明细',
|
|
|
- },
|
|
|
- ]),
|
|
|
- // 近 7 日趋势:预警与报警**两条序列**(后端一次给两族,故不需要切换)。
|
|
|
- // 2026-10-13 图表选型:时间趋势用折线(维护者表格「有历史信息,带入时间趋势」行)。
|
|
|
- trendBlock('近 7 日预警趋势', (warning && warning.trend) || [], { chart: 'line' }),
|
|
|
- trendBlock('近 7 日报警趋势', (alarm && alarm.trend) || [], { chart: 'line' }),
|
|
|
+ // 顶部四卡(设计稿 2026-10-23 视觉对齐):读数在上、标签在下,每卡顶部一条语义色横条,
|
|
|
+ // 读数按语义着色(在线绿 / 离线红 / 其余白)—— 色与横条都是 domain 给的 valueTone/accent。
|
|
|
+ kpiBlock(
|
|
|
+ [
|
|
|
+ { key: 'deviceCount', label: '设备总数', value: valueText(current.deviceCount), unit: '台', accent: 'info' },
|
|
|
+ { key: 'online', label: '在线', value: valueText(current.onlineCount), unit: '台', valueTone: 'lv4', accent: 'lv4' },
|
|
|
+ { key: 'offline', label: '离线', value: valueText(current.offlineCount), unit: '台', valueTone: 'lv1', accent: 'lv1' },
|
|
|
+ {
|
|
|
+ key: 'onlineRate',
|
|
|
+ label: '在线率',
|
|
|
+ value: ratioView(current.onlineRate),
|
|
|
+ hint: ratioHint(current.onlineRate),
|
|
|
+ accent: 'info',
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ { layout: 'value-first' },
|
|
|
+ ),
|
|
|
+ // 类型卡 2x2(设计稿):卡内小字=后端的监测内容;行业图标 + 行业色边框;
|
|
|
+ // 可点 → 写全局行业筛选(与条形图同一条 panel-action: industry 接缝,不新造交互)。
|
|
|
+ cardsBlock(
|
|
|
+ '按设备类型',
|
|
|
+ (current.types || []).map((row) => ({
|
|
|
+ ...row,
|
|
|
+ tone: TYPE_TONES[row.code] || '',
|
|
|
+ icon: TYPE_ICONS[row.code] || '',
|
|
|
+ actionKey: TYPE_FILTER_KEYS[row.code] || '',
|
|
|
+ // 选中态:当前行业筛选正好是这张卡代表的行业(再点一次即取消,走外壳既有 toggle 语义)。
|
|
|
+ selected: Boolean(activeIndustry) && TYPE_FILTER_KEYS[row.code] === activeIndustry,
|
|
|
+ })),
|
|
|
+ { fixed: true, layout: 'two-by-two', action: 'industry', aside: '点击按行业筛选' },
|
|
|
+ ),
|
|
|
+ // 片区卡:城南/城北/太常固定三项,无数据画 0(规格 §6.2 第 3 条);一排三张。
|
|
|
+ cardsBlock('按设备片区', current.districts || [], { fixed: true, layout: 'three', aside: '片区在网设备' }),
|
|
|
]
|
|
|
|
|
|
return {
|
|
|
- name: 'situationSummary',
|
|
|
- label: '态势总览',
|
|
|
+ name: 'perceptionDevices',
|
|
|
+ label: '感知设备',
|
|
|
section,
|
|
|
- hasContent: sectionHasContent(section),
|
|
|
+ // 空态仍要画 4 类型卡 / 3 片区卡的 0(规格 §6.7 的例外)。
|
|
|
+ hasContent: fixedCardsSection(section),
|
|
|
blocks,
|
|
|
- // 联动出口:指标上的 `link` 由外壳翻成"切到预警处置模式"(见文件头第 ① 条)。
|
|
|
- links: [
|
|
|
- { key: 'WARNING', label: '查看当前预警列表', mode: OVERVIEW_LINK_MODES.WARNING },
|
|
|
- { key: 'ALARM', label: '查看当前报警列表', mode: OVERVIEW_LINK_MODES.ALARM },
|
|
|
- { key: 'EFFICIENCY', label: '查看处置效率明细', mode: OVERVIEW_LINK_MODES.EFFICIENCY },
|
|
|
- ],
|
|
|
- linkNote: '点预警/报警/已解除率读数可跳到「预警处置」模式的事件列表(本模式端点不含点位编码,不在地图上定位)',
|
|
|
- rangeNote: warnings && warnings.rangeType === 'LAST_7_DAYS' ? '趋势窗口:近 7 日(后端按声明窗口过滤)' : '',
|
|
|
+ excludedNote: excludedText([
|
|
|
+ current.excluded && current.excluded.unknownTypeCount ? `类型未知 ${valueText(current.excluded.unknownTypeCount)} 台` : '',
|
|
|
+ current.excluded && current.excluded.unassignedDistrictCount
|
|
|
+ ? `未分片区 ${valueText(current.excluded.unassignedDistrictCount)} 台(不进片区卡)`
|
|
|
+ : '',
|
|
|
+ ]),
|
|
|
+ note: '设备数取台账全量;无状态记录计离线',
|
|
|
missing: panelMissingNote(current),
|
|
|
placeholder: sectionPlaceholder(section),
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 基础设施面板视图(验收第 2 条)。
|
|
|
- * 设施底数 + 行业构成 + 区域分布(区域来自票 17 的 `base-archives`,见文件头第 ② 条)。
|
|
|
+ * 基础设施面板(规格 §6.3)。
|
|
|
+ *
|
|
|
+ * 顶部 2 指标卡(管道数/管线里程 km)→ 环形饼图(三类管道条数与占比,图例逐项带计数)。
|
|
|
*/
|
|
|
-export function infrastructureSummaryPanel(data, section) {
|
|
|
+export function pipeNetworkPanel(data, section) {
|
|
|
const current = data || {}
|
|
|
- const infra = current.infrastructure || {}
|
|
|
- // `regions` 是 `toOverviewRegions()` 的领域对象(`{regions, lengthKm, scale}`),
|
|
|
- // 不是行数组本身 —— 它连同里程/规模一起带出来,界面才有得画。
|
|
|
- const regionData = current.regions || null
|
|
|
- const regions = (regionData && regionData.regions) || []
|
|
|
-
|
|
|
const blocks = [
|
|
|
+ // 两卡按设计稿:图标+标签 → 大号数值+单位 → 口径小字(`caption`)。
|
|
|
kpiBlock([
|
|
|
- { key: 'facilityCount', label: '设施总数', value: valueText(infra.facilityCount), unit: '处' },
|
|
|
- { key: 'smartDeviceCount', label: '智能设备', value: valueText(infra.smartDeviceCount), unit: '台' },
|
|
|
- { key: 'industryCount', label: '行业数', value: String((infra.industries || []).length), unit: '个' },
|
|
|
- // 里程来自票 17 那份档案(本端点不给),取不到时显示占位符。
|
|
|
- // 票 38(第 2 条):「规模」读数已删除——真实后端 totals.scale 恒 null,是空读数。
|
|
|
- // 票 38 第二轮(维护者复验反馈第 1 条):概览这块 `wide` 让管线里程独占整行——
|
|
|
- // 四个读数里它数值最长(真实 62.60552),并排会挤压;`wide` 由 domain 声明,
|
|
|
- // 渲染层只认这个通用标志、不再按 key 特判(与「谁独占整行归 domain」同一条纪律)。
|
|
|
- { key: 'lengthKm', label: '管线里程', value: valueText(regionData && regionData.lengthKm), unit: 'km', wide: true },
|
|
|
- ]),
|
|
|
- // 2026-10-13 维护者反馈:条形图太多易疲劳——行业构成是「类别少、占比」场景,改环形图;
|
|
|
- // 行业行走行业语义色(与行业切图同源);图例行保留联动(点它写全局行业筛选,接缝不变)。
|
|
|
- // 票 37 走查扩展:行业是排名类别,两块都按计数降序(并列保序)。
|
|
|
- donutBlock(
|
|
|
- '行业构成 · 设施',
|
|
|
- sortByCountDesc(
|
|
|
- (infra.industries || []).map((row) => ({ code: row.code, label: row.label, count: row.facilityCount || 0, actionKey: row.key })),
|
|
|
- (row) => row.count,
|
|
|
- ),
|
|
|
- { action: 'industry' },
|
|
|
- ),
|
|
|
+ {
|
|
|
+ key: 'pipeCount',
|
|
|
+ label: '管道数',
|
|
|
+ value: valueText(current.pipeCount),
|
|
|
+ unit: '根',
|
|
|
+ caption: '供水 · 排水 · 燃气',
|
|
|
+ },
|
|
|
+ { key: 'lengthKm', label: '管线里程', value: valueText(current.lengthKm), unit: 'km', caption: '管网总长' },
|
|
|
+ ], { layout: 'two' }),
|
|
|
+ // 环形居中、图例在下横排。
|
|
|
+ // 2026-10-23 维护者第 3 条:标题**不再重复管道数量**(上面两张指标卡已经给了),
|
|
|
+ // 就叫「管道分类占比」—— 同一屏里同一个数说两遍只是噪音。
|
|
|
donutBlock(
|
|
|
- '行业构成 · 智能设备',
|
|
|
- sortByCountDesc(
|
|
|
- (infra.industries || []).map((row) => ({ code: row.code, label: row.label, count: row.smartDeviceCount || 0, actionKey: row.key })),
|
|
|
- (row) => row.count,
|
|
|
- ),
|
|
|
- { action: 'industry' },
|
|
|
+ '管道分类占比',
|
|
|
+ (current.types || []).map((row) => ({
|
|
|
+ code: row.code,
|
|
|
+ label: row.label,
|
|
|
+ count: row.count,
|
|
|
+ // 环形几何按计数画;里程进悬停说明(图例仍逐项带条数)。
|
|
|
+ hint: numberish(row.lengthKm) === null ? '' : `${row.lengthKm} km`,
|
|
|
+ })),
|
|
|
+ { layout: 'stack-below' },
|
|
|
),
|
|
|
- barsBlock('片区分布', sortByCountDesc(regions, (row) => row.count)),
|
|
|
]
|
|
|
|
|
|
return {
|
|
|
- name: 'infrastructureSummary',
|
|
|
+ name: 'pipeNetwork',
|
|
|
label: '基础设施',
|
|
|
section,
|
|
|
hasContent: sectionHasContent(section),
|
|
|
blocks,
|
|
|
- // 联动出口:行业构成行可写全局行业筛选(真的驱动地图与所有面板)。
|
|
|
- industryLinks: (infra.industries || [])
|
|
|
- .filter((row) => row.key)
|
|
|
- .map((row) => ({ code: row.code, key: row.key, label: row.label })),
|
|
|
- linkNote: '点行业行可把该行业设为全局筛选条件(同时作用于一张图与各面板)',
|
|
|
- // 区域分布来自哪个端点,界面如实说明(本端点自己没有这一段)。
|
|
|
- gap: regions.length ? '' : '片区分布取自 `/infrastructure/base-archives`(本概览端点不含片区字段),当前未取到',
|
|
|
- excludedNote:
|
|
|
- infra.excluded && (infra.excluded.unknownTypeCount || infra.excluded.typeKindMismatchCount)
|
|
|
- ? `口径剔除:类型未知 ${valueText(infra.excluded.unknownTypeCount)} · 类型不匹配 ${valueText(infra.excluded.typeKindMismatchCount)}`
|
|
|
+ excludedNote: excludedText([
|
|
|
+ current.excluded && current.excluded.unclassifiedCount
|
|
|
+ ? `未分类管线 ${valueText(current.excluded.unclassifiedCount)} 条(计入总数、不进饼图)`
|
|
|
: '',
|
|
|
+ ]),
|
|
|
+ note: '里程按管长合计折算;占比按条数',
|
|
|
missing: panelMissingNote(current),
|
|
|
placeholder: sectionPlaceholder(section),
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 运行监测面板视图(验收第 3 条)。
|
|
|
- * 在线率 + 四态统计 + 监测覆盖 + 类型分布。
|
|
|
+ * 运行监测-预警研判面板(规格 §6.4)。
|
|
|
*
|
|
|
- * `data` 就是**本块自己的领域对象**(`section.data`)—— 单源面板直读,不套一层壳。
|
|
|
- * ⚠️ 票 20 真机走查逮到的真缺陷:本函数原先读 `current.monitoring`(以为拿到的是
|
|
|
- * `{ monitoring }` 包装),而外壳传进来的是裸领域对象,于是**整块读数全是占位符 `—`**;
|
|
|
- * 而逻辑闸门当时全绿,因为**测试喂的正是那个错误的包装形状**(测试与实现犯了同一个错)。
|
|
|
+ * 顶部 2 指标卡(今日新增带差值、未解除)→ 4 张固定等级卡 → 类型排行 TOP3(可点)。
|
|
|
+ * 排行标题严格为 `近30日 · 共XX起 · 点击筛选`(XX = 后端给的近 30 日总数)。
|
|
|
*/
|
|
|
-export function monitoringSummaryPanel(data, section) {
|
|
|
- const monitoring = data || {}
|
|
|
-
|
|
|
+export function warningStudyPanel(data, section) {
|
|
|
+ const current = data || {}
|
|
|
+ const today = current.today || {}
|
|
|
+ const ranking = current.typeRanking || {}
|
|
|
+ // 排行标题按设计稿拆成两段:块标题「预警类型排行」+ 右上角小字「近30日 · 共XX起 · 点击筛选」。
|
|
|
+ // 小字整句仍是需求原文的锁定文案(拆位置不拆字),闸门按整句断言。
|
|
|
+ const rankingAside = `近30日 · 共${valueText(ranking.total)}起 · 点击筛选`
|
|
|
const blocks = [
|
|
|
kpiBlock([
|
|
|
- { key: 'onlineRate', label: '在线率', value: onlineRateView(monitoring), hint: onlineRateHint(monitoring) },
|
|
|
- ...statusKpis(monitoring.deviceStatus),
|
|
|
- ]),
|
|
|
- rowsBlock('监测覆盖', [
|
|
|
- { key: 'monitoringPointCount', label: '监测点', value: valueText(monitoring.monitoringPointCount) },
|
|
|
- { key: 'deviceCount', label: '智能设备', value: valueText(monitoring.deviceCount) },
|
|
|
- { key: 'monitoringFacilityCount', label: '监测设施', value: valueText(monitoring.monitoringFacilityCount) },
|
|
|
- { key: 'infrastructureFacilityCount', label: '基础设施', value: valueText(monitoring.infrastructureFacilityCount) },
|
|
|
+ // 差值:正红负绿(规格 §6.6),块构造器按 `delta` 自动补文案与色调。
|
|
|
+ { key: 'warningToday', label: '今日新增', value: valueText(today.newCount), unit: '条', delta: today.delta },
|
|
|
+ // 未解除的第三行小字是「处置中预警」——处置中条数是本契约里可证实的读数(不再另编口径)。
|
|
|
{
|
|
|
- key: 'formalIndustries',
|
|
|
- label: '正式行业范围',
|
|
|
- value: (monitoring.formalIndustries || []).map((code) => industryLabel(code) || codeText(code)).join(' · ') || DETAIL_PLACEHOLDER,
|
|
|
+ key: 'warningUnresolved',
|
|
|
+ label: '未解除',
|
|
|
+ value: valueText((current.unresolved || {}).count),
|
|
|
+ unit: '条',
|
|
|
+ caption: unresolvedCaption(current.unresolved),
|
|
|
},
|
|
|
- ]),
|
|
|
- // 票 37 走查扩展(维护者 2026-10-14):类型分布是「多类别排名」场景,
|
|
|
- // 与监测概览同口径——按设备数从多到少(并列保持后端次序)。
|
|
|
- barsBlock(
|
|
|
- '类型分布 · 智能设备',
|
|
|
- sortByCountDesc(
|
|
|
- (monitoring.typeDistribution || []).map((row) => ({
|
|
|
- code: `${row.industryCode || ''}|${row.typeCode || ''}`,
|
|
|
- label: [row.industryLabel, row.typeName].filter(Boolean).join(' · ') || row.typeCode,
|
|
|
- count: row.deviceCount || 0,
|
|
|
- })),
|
|
|
- (row) => row.count,
|
|
|
- ),
|
|
|
+ ], { layout: 'two' }),
|
|
|
+ // 等级卡:一级→四级固定四项,无数据 0(规格 §6.4 第 2 条);数字与边框同等级色。
|
|
|
+ cardsBlock(
|
|
|
+ '未解除预警 · 分级',
|
|
|
+ withLevelTones(current.levels || []),
|
|
|
+ { fixed: true, layout: 'four' },
|
|
|
),
|
|
|
+ // 全类型可点:动作键 `warningType`(落点:切「预警处置」模式并按类型过滤本地列表)。
|
|
|
+ // 维护者 2026-10-23 第 4 条:不再只列前三,所有类型都排出来(limit: 0 = 全部)。
|
|
|
+ rankingBarsBlock('预警类型排行', ranking.top || [], {
|
|
|
+ limit: 0,
|
|
|
+ action: 'warningType',
|
|
|
+ variant: 'rank',
|
|
|
+ unit: '起',
|
|
|
+ aside: rankingAside,
|
|
|
+ }),
|
|
|
]
|
|
|
|
|
|
- // 逐设备最近采集时间:**本端点独有**(票 18 的缺口 I2/BT-8 在这里有值)。
|
|
|
- const freshness = monitoring.metricFreshness || {}
|
|
|
- if (freshness.updatedAt || freshness.status) {
|
|
|
- blocks.push(
|
|
|
- rowsBlock('最近采集时间', [
|
|
|
- { key: 'metricStatus', label: '采集状态', value: codeText(freshness.status) },
|
|
|
- { key: 'metricUpdatedAt', label: '最近采集', value: valueText(freshness.updatedAt) },
|
|
|
- { key: 'metricServerUpdatedAt', label: '服务端时间', value: valueText(freshness.serverUpdatedAt) },
|
|
|
- ]),
|
|
|
- )
|
|
|
- }
|
|
|
-
|
|
|
return {
|
|
|
- name: 'monitoringSummary',
|
|
|
- label: '运行监测',
|
|
|
+ name: 'warningStudy',
|
|
|
+ label: '运行监测-预警研判',
|
|
|
section,
|
|
|
- hasContent: sectionHasContent(section),
|
|
|
+ // 无预警时等级卡要画 4 个 0(规格 §6.4 第 2 条 + §6.7 例外)。
|
|
|
+ hasContent: fixedCardsSection(section),
|
|
|
blocks,
|
|
|
- // 验收第 3 条的「故障统计」缺口:如实说明,不合成一个"故障"数(见文件头第 ③ 条)。
|
|
|
- gap: MONITORING_FAULT_GAP,
|
|
|
- missing: panelMissingNote(monitoring),
|
|
|
+ linkNote: '点类型排行会切到「预警处置」模式并按该预警类型过滤当前列表(规格 §6.4 第 4 条;类型筛选支持归票 07)',
|
|
|
+ note: '未解除含待办/处置中/已处置',
|
|
|
+ missing: panelMissingNote(current),
|
|
|
placeholder: sectionPlaceholder(section),
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 票 42:预警族速率的说明。预警族的速率是**已解除率**(已解除 ÷ 预警总数),
|
|
|
- * 与报警族/预警处置模式的**工单口径处置率**不是一个东西 —— hint 用词必须分开,
|
|
|
- * 不让「已解除 19 / 预警 31」被读成工单处置率。判据复用 `handlingRateView`(票 20 单源)。
|
|
|
- */
|
|
|
-function warningRateHint(efficiency) {
|
|
|
- const row = efficiency || {}
|
|
|
- if (handlingRateView(row) === '不适用') return '当前范围没有预警,已解除率不适用'
|
|
|
- if (typeof row.numerator === 'number' && typeof row.denominator === 'number') {
|
|
|
- return `已解除 ${row.numerator} / 预警总数 ${row.denominator}`
|
|
|
- }
|
|
|
- return ''
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * 票 42:自身生命周期分布块(状态/等级/行业/片区)。后端已按族全铺且计数自洽,
|
|
|
- * 前端不补 0、不重排、不翻码;某组后端没给时**整块跳过** —— 「不适用」显式化,不画空块。
|
|
|
- * `title` 是界面用语(「预警」/「报警」),与 KPI 的机器键(`WARNING-*`)分工。
|
|
|
- */
|
|
|
-function lifecycleBars(title, row) {
|
|
|
- const groups = [
|
|
|
- // 生命周期状态/等级是**固定语义序**(后端字典),不重排;
|
|
|
- // 行业/片区是排名类别——票 37 走查扩展:按计数降序(并列保序)。
|
|
|
- ['statusDistribution', '生命周期状态', false],
|
|
|
- ['levelDistribution', '等级分布', false],
|
|
|
- ['industryDistribution', '行业分布', true],
|
|
|
- ['districtDistribution', '片区分布', true],
|
|
|
- ]
|
|
|
- return groups
|
|
|
- .filter(([name]) => Array.isArray(row[name]))
|
|
|
- .map(([name, label, sortDesc]) =>
|
|
|
- barsBlock(`${title} · ${label}`, sortDesc ? sortByCountDesc(row[name], (item) => item.count) : row[name]),
|
|
|
- )
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * 预警族的块(票 42:只给**预警自身生命周期**,工单口径整体不出现 ——
|
|
|
- * 「预警不走工单流程」,画工单字段等于把不适用画成读数)。
|
|
|
+ * 运行监测-报警处置面板(规格 §6.5)。
|
|
|
+ *
|
|
|
+ * 第一行 3 指标卡(今日新增带差值/已转工单/未转工单)→ 第二行 3 指标卡
|
|
|
+ * (处置率/平均响应时长/平均处置时长)→ 4 张固定等级卡 → 近 7 日折线(标题【近7日报警数】)。
|
|
|
+ * 比例与时长在分母/样本为 0 时显示占位符(规格 §5 末句 + §6.7)。
|
|
|
*/
|
|
|
-function warningFamilyBlocks(family) {
|
|
|
- const row = family || {}
|
|
|
- const familyKey = row.family || 'WARNING'
|
|
|
- return [
|
|
|
+export function alarmDisposalPanel(data, section) {
|
|
|
+ const current = data || {}
|
|
|
+ const today = current.today || {}
|
|
|
+ const conversion = current.conversion || {}
|
|
|
+ const rate = current.rate || {}
|
|
|
+ const response = current.responseDuration || {}
|
|
|
+ const disposal = current.disposalDuration || {}
|
|
|
+ const blocks = [
|
|
|
+ // 第一行(设计稿):今日新增(带较昨日差值)、已转工单、未转工单;每卡第三行是口径小字。
|
|
|
kpiBlock([
|
|
|
- { key: `${familyKey}-total`, label: '预警总数', value: valueText(row.total), unit: '条' },
|
|
|
+ { key: 'alarmToday', label: '今日新增', value: valueText(today.newCount), unit: '条', delta: today.delta },
|
|
|
+ { key: 'alarmConverted', label: '已转工单', value: valueText(conversion.convertedCount), unit: '条', caption: '已派发处置' },
|
|
|
+ { key: 'alarmUnconverted', label: '未转工单', value: valueText(conversion.unconvertedCount), unit: '条', caption: '待核实转派' },
|
|
|
+ ], { layout: 'three' }),
|
|
|
+ kpiBlock([
|
|
|
+ { key: 'alarmRate', label: '报警处置率', value: ratioView(rate), hint: ratioHint(rate), caption: '已解除占比' },
|
|
|
{
|
|
|
- key: `${familyKey}-rate`,
|
|
|
- label: '已解除率',
|
|
|
- value: handlingRateView(row.efficiency),
|
|
|
- hint: warningRateHint(row.efficiency),
|
|
|
- link: 'EFFICIENCY',
|
|
|
- linkHint: '跳到预警处置模式的预警效率明细',
|
|
|
+ key: 'responseDuration',
|
|
|
+ label: '平均响应时长',
|
|
|
+ value: durationView(response),
|
|
|
+ unit: durationUnitText(response.unit),
|
|
|
+ hint: durationHint(response),
|
|
|
+ caption: '处置响应',
|
|
|
},
|
|
|
- ]),
|
|
|
- ...lifecycleBars('预警', row),
|
|
|
- ]
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * 报警族的块(票 42:**保留工单口径** —— 维护者 2026-10-14 裁定「报警靠工单闭环」,
|
|
|
- * 同时并存 `alarm_status` 自身生命周期分布,两者不同源、不互相冒充)。
|
|
|
- */
|
|
|
-function alarmFamilyBlocks(family) {
|
|
|
- const row = family || {}
|
|
|
- const familyKey = row.family || 'ALARM'
|
|
|
- const states = row.workOrderStates || []
|
|
|
- return [
|
|
|
- kpiBlock([
|
|
|
- { key: `${familyKey}-total`, label: '报警总数', value: valueText(row.total), unit: '条' },
|
|
|
- { key: `${familyKey}-handled`, label: '已处理', value: valueText(row.handled), unit: '条' },
|
|
|
- { key: `${familyKey}-pending`, label: '待处理', value: valueText(row.pending), unit: '条' },
|
|
|
- { key: `${familyKey}-cancelled`, label: '已取消', value: valueText(row.cancelledCount), unit: '条' },
|
|
|
- { key: `${familyKey}-unlinked`, label: '未关联工单', value: valueText(row.unlinkedEventCount), unit: '条' },
|
|
|
{
|
|
|
- key: `${familyKey}-rate`,
|
|
|
- label: '处置率',
|
|
|
- value: handlingRateView(row.efficiency),
|
|
|
- hint: handlingRateHint(row.efficiency),
|
|
|
- link: 'EFFICIENCY',
|
|
|
+ key: 'disposalDuration',
|
|
|
+ label: '平均处置时长',
|
|
|
+ value: durationView(disposal),
|
|
|
+ unit: durationUnitText(disposal.unit),
|
|
|
+ hint: durationHint(disposal),
|
|
|
+ caption: '处置完成',
|
|
|
},
|
|
|
- ]),
|
|
|
- // 工单状态分布:0–7 全铺(缺的态是真实计数 0,不是"没有数据")。
|
|
|
- // 2026-10-13 图表选型:状态是多类别占比 → 百分比堆叠条。
|
|
|
- stackedBlock('报警 · 工单状态分布', states),
|
|
|
- // 票 42:报警自身生命周期(UNHANDLED/HANDLED 两桶 + 等级/行业/片区)并存。
|
|
|
- ...lifecycleBars('报警', row),
|
|
|
+ ], { layout: 'three' }),
|
|
|
+ // 等级卡:一级→四级固定四项,无数据 0(规格 §6.5 第 3 条);数字与边框同等级色。
|
|
|
+ cardsBlock('已核实报警 · 分级', withLevelTones(current.levels || []), { fixed: true, layout: 'four' }),
|
|
|
+ // 折线:标题严格为「【近7日报警数】」,X 轴 MM-DD、悬停读全日期(hint),含 0 点;
|
|
|
+ // 线色按设计稿取 lv2(橙),tone 由 domain 给、色值仍只在 tokens.css 声明一份。
|
|
|
+ trendBlock(
|
|
|
+ '【近7日报警数】',
|
|
|
+ (current.sevenDayTrend || []).map((point) => ({
|
|
|
+ key: point.date,
|
|
|
+ label: shortDate(point.date),
|
|
|
+ count: point.count,
|
|
|
+ hint: point.date,
|
|
|
+ })),
|
|
|
+ // 两条线并列(维护者 2026-10-23 反馈第 2 条):
|
|
|
+ // ① 当天新增(当天 create_time 的条数);
|
|
|
+ // ② 总未处理(截至当天仍未处理的累计条数,`alarm_status = 0`)。
|
|
|
+ // 为什么不再挂一句口径小字:单条线 + 一个"近7日新增"标签说不清"为什么有的时候是 0",
|
|
|
+ // 维护者要的是**用第二条线做对照**(总未处理非 0 而新增为 0,一看就知道是"这段时间没新报警")。
|
|
|
+ // 两条线各按联合最大值归一并带图例,读者自己就能读出这层关系,不需要解释文案。
|
|
|
+ {
|
|
|
+ chart: 'line',
|
|
|
+ series: [
|
|
|
+ { key: 'newAlarms', label: '当天新增', tone: 'lv2', counts: trendOf(current.sevenDayTrend) },
|
|
|
+ { key: 'unhandledTotal', label: '总未处理', tone: 'lv1', counts: unhandledOf(current) },
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ ),
|
|
|
]
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * 预警设置面板视图(验收第 4 条:保持合同名称,只读展示预警/报警统计)。
|
|
|
- *
|
|
|
- * **两族都画**(Q1 裁定:后端一次给两族,做切换等于把已有数据藏起来)。
|
|
|
- *
|
|
|
- * `data` 是**本块自己的领域对象**(`section.data`,即 `toOverviewWarningSettings()` 的结果)。
|
|
|
- * ⚠️ 与 `monitoringSummaryPanel` 同一个坑(真机走查逮到):原先读 `current.warningSettings`
|
|
|
- * 那层不存在的包装,于是整块 KPI 全是 `—`。单源面板一律直读 `data`。
|
|
|
- */
|
|
|
-export function alertSettingsPanel(data, section) {
|
|
|
- const warnings = data || {}
|
|
|
- const warning = warnings.warning || null
|
|
|
- const alarm = warnings.alarm || null
|
|
|
-
|
|
|
- // 票 42:两族口径不同,块结构也不同 —— 预警族纯自身生命周期,报警族工单口径 + 自身生命周期。
|
|
|
- const blocks = [...warningFamilyBlocks(warning), ...alarmFamilyBlocks(alarm)]
|
|
|
|
|
|
return {
|
|
|
- name: 'alertSettings',
|
|
|
- label: '预警设置',
|
|
|
+ name: 'alarmDisposal',
|
|
|
+ label: '运行监测-报警处置',
|
|
|
section,
|
|
|
- hasContent: sectionHasContent(section),
|
|
|
+ // 无报警时等级卡要画 4 个 0、7 日折线画 7 个 0 点(规格 §6.5 + §6.7 例外)。
|
|
|
+ hasContent: fixedCardsSection(section),
|
|
|
blocks,
|
|
|
- links: [
|
|
|
- { key: 'WARNING', label: '查看当前预警列表', mode: OVERVIEW_LINK_MODES.WARNING },
|
|
|
- { key: 'ALARM', label: '查看当前报警列表', mode: OVERVIEW_LINK_MODES.ALARM },
|
|
|
- { key: 'EFFICIENCY', label: '查看处置效率明细', mode: OVERVIEW_LINK_MODES.EFFICIENCY },
|
|
|
- ],
|
|
|
- // 面板名保持合同名「预警设置」(验收第 4 条原句),但两族口径不同 —— 说明这一点。
|
|
|
- note: '本面板按合同名称保留为「预警设置」:预警族为预警自身生命周期统计(不走工单),报警族为工单口径 + 报警自身生命周期并存;本模式端点不含点位编码,不联动地图',
|
|
|
- rangeNote: warnings.rangeType === 'LAST_7_DAYS' ? '统计窗口:近 7 日' : '',
|
|
|
- missing: panelMissingNote(warnings),
|
|
|
+ // 面板口径说明(顶部折叠区)。**不再**把"上方全量 / 折线近7日"写成一句 ——
|
|
|
+ // 维护者 2026-10-23 第 2 条明确不要那类多余说明;折线改成两条线 + 图例后,
|
|
|
+ // "新增归零而积压不为零"这层关系读者自己看得出来,不需要一段文字去解释。
|
|
|
+ note: '基础集合为已核实报警;时长只计有日志的样本',
|
|
|
+ missing: panelMissingNote(current),
|
|
|
placeholder: sectionPlaceholder(section),
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-/** panelId → 视图构造函数(本模式自己的四块)。 */
|
|
|
-export const OVERVIEW_PANEL_BUILDERS = {
|
|
|
- situationSummary: situationSummaryPanel,
|
|
|
- infrastructureSummary: infrastructureSummaryPanel,
|
|
|
- monitoringSummary: monitoringSummaryPanel,
|
|
|
- alertSettings: alertSettingsPanel,
|
|
|
+/** `YYYY-MM-DD` → 轴标签 `MM-DD`(悬停仍给完整日期,见上面折线的 `hint`)。 */
|
|
|
+function shortDate(date) {
|
|
|
+ const text = typeof date === 'string' ? date.trim() : ''
|
|
|
+ return text.length >= 10 && text[4] === '-' && text[7] === '-' ? text.slice(5) : text
|
|
|
}
|
|
|
|
|
|
-/**
|
|
|
- * 一个面板的视图。`data` 为 null(失败/空)时仍返回结构完整的视图(块为空),
|
|
|
- * 组件据此画占位说明 —— 「失败」不该让整块面板消失,那会连面板标题都没了。
|
|
|
- */
|
|
|
-export function overviewPanelView(panelId, data, section) {
|
|
|
- const build = OVERVIEW_PANEL_BUILDERS[panelId]
|
|
|
- if (!build) return null
|
|
|
- return build(data, section)
|
|
|
+/** panelId → 视图构造函数(本模式自己的四块)。 */
|
|
|
+export const OVERVIEW_PANEL_BUILDERS = {
|
|
|
+ perceptionDevices: perceptionDevicesPanel,
|
|
|
+ pipeNetwork: pipeNetworkPanel,
|
|
|
+ warningStudy: warningStudyPanel,
|
|
|
+ alarmDisposal: alarmDisposalPanel,
|
|
|
}
|