|
|
@@ -16,6 +16,9 @@ import {
|
|
|
toEventItem,
|
|
|
} from './eventList.js'
|
|
|
import { valueText, workOrderRows } from './equipmentDetailView.js'
|
|
|
+import { serverPageView } from './pagination.js'
|
|
|
+import { statusText, statusTone } from './eventStatusVocab.js'
|
|
|
+import { incidentTrendGapText } from './incidentTrend.js'
|
|
|
import { EVENT_FAMILY } from './events.js'
|
|
|
import { WORK_ORDER_STATES } from './workOrder.js'
|
|
|
import { industryLabel } from './filterOptions.js'
|
|
|
@@ -290,6 +293,11 @@ export function toHandlingEfficiency(raw) {
|
|
|
bucket: String(item && (item.timeBucket || item.bucket || item.bucketStart || '')),
|
|
|
count: numberish(item && item.eventCount) || 0,
|
|
|
})),
|
|
|
+ // 票 70:已关闭报警数(**事件级**口径:多少条报警的回联工单已结案)。
|
|
|
+ // 来源是逐桶的 `handledCount` 求和(后端 `durationTrend` 按事件回联口径给)。
|
|
|
+ // 与「工单状态分布」里的已结案**工单张数**不是同一口径,两者都在屏上,各带口径说明。
|
|
|
+ // 桶缺该字段(预警族/旧载荷)⇒ null:**不知道不是 0**。
|
|
|
+ closedAlarmCount: closedAlarmCount(trendRowsRaw),
|
|
|
trend: trendRowsRaw.map((item) => ({
|
|
|
bucket: String(item && (item.timeBucket || item.bucket || item.bucketStart || '')),
|
|
|
count: item && item.averageDuration
|
|
|
@@ -297,6 +305,24 @@ export function toHandlingEfficiency(raw) {
|
|
|
: numberish(item && (item.handlingEfficiency === undefined ? item.count : item.handlingEfficiency)),
|
|
|
unit: item && item.averageDuration ? item.averageDuration.unit || '' : '',
|
|
|
})),
|
|
|
+ // 票 71 返工(维护者 2026-10-29 第 2 条):逐日「总数 / 新增数 / 处置数」。
|
|
|
+ // 数据源是**事件行**(需带 status + eventTime + updatedTime),由 store 取全后
|
|
|
+ // 注入 raw.incidentTrend(见 stores/eventStore.js 的 incidentTrendFor)——
|
|
|
+ // 算它要事件行取全,而 domain 转换只该看形状、不该再发请求。
|
|
|
+ // 缺这一段(旧载荷/事件行没取全)⇒ null:界面据此说「不画」的原因,不画假线。
|
|
|
+ incidentTrend: reader.raw.incidentTrend && typeof reader.raw.incidentTrend === 'object' ? reader.raw.incidentTrend : null,
|
|
|
+ // 票 71:本窗口的**事件行**(后端已按事件时间倒序、分页下发)。
|
|
|
+ // 报警效率/预警效率面板的「报警列表 / 预警列表」块就画这一份 —— 与 KPI
|
|
|
+ // 「统计数」(`summary.totalEvents`)是**同一次过滤**的结果(同一请求、同一范围),
|
|
|
+ // 分页只切窗口不改口径。行的投影在 `eventListRowView`(与票 13/65 的卡片同一套形状)。
|
|
|
+ events: events.map(toEventItem),
|
|
|
+ // 票 71:后端分页回显(页码/页大小/总数/总页数)。前端不自行推算(票 61 纪律)。
|
|
|
+ pagination: reader.raw.pagination && typeof reader.raw.pagination === 'object' ? reader.raw.pagination : null,
|
|
|
+ // 票 71:**多窗合并**的分页形态(`{mode:'merged', windowCount, loaded, rangeKey}`,
|
|
|
+ // 由 `mergeEfficiencyPayloads` 给)。单窗载荷没有这一段 ⇒ null(走服务端页码)。
|
|
|
+ // 少了这条透传,多窗合并后会拿第一窗的 `pagination` 当全局页 —— 列表会画出
|
|
|
+ // 「共 total 条」但只列出首页行(t71 的 F6/F7 钉住这件事)。
|
|
|
+ paging: reader.raw.paging && typeof reader.raw.paging === 'object' ? reader.raw.paging : null,
|
|
|
range: rangeView(raw),
|
|
|
missing: reader.missing,
|
|
|
}
|
|
|
@@ -338,6 +364,13 @@ export function toEventAnalysis(raw) {
|
|
|
levels: Array.isArray(dimensions) ? levelRows(dimensionRows('LEVEL')) : levelRows(source.levels || source.byLevel),
|
|
|
industries: Array.isArray(dimensions) ? dimensionRows('INDUSTRY', industryText) : countRows(source, 'industries', industryText).concat(countRows(source, 'byIndustry', industryText)),
|
|
|
causes: Array.isArray(dimensions) ? dimensionRows('CAUSE', causeText) : countRows(source, 'causes', causeText).concat(countRows(source, 'byCause', causeText)),
|
|
|
+ // 维护者 2026-10-30 第 4 条:后端 CAUSE 维**只有 UNKNOWN_CAUSE 一个桶**
|
|
|
+ // (实测报警 2 条 / 预警 1 条全部落在「未知/缺失成因」——数据库里没有成因字段),
|
|
|
+ // 所以「按成因」是一块永远说「未知」的假数据。改用 DEVICE_TYPE:
|
|
|
+ // 两族都有值且后端已给中文名(漏失监测仪 / 仁科环境监测器 / 平升液位流量终端…)。
|
|
|
+ deviceTypes: Array.isArray(dimensions)
|
|
|
+ ? dimensionRows('DEVICE_TYPE')
|
|
|
+ : countRows(source, 'deviceTypes').concat(countRows(source, 'byDeviceType')),
|
|
|
// 片区不上翻译层:后端给的就是中文名(城南/城北/太常/未分区),码是 '0'/'1'/'2'。
|
|
|
regions: Array.isArray(dimensions) ? dimensionRows('DISTRICT') : countRows(source, 'regions').concat(countRows(source, 'byRegion')),
|
|
|
highlights,
|
|
|
@@ -380,21 +413,55 @@ function historyTrendRows(rows = []) {
|
|
|
* (多窗不存在全局页,不伪造页码)。
|
|
|
* 没有 `paging` 元数据(旧 mock/旧载荷)时退化为普通清单块,不画翻页控件。
|
|
|
*/
|
|
|
+/**
|
|
|
+ * 事件行 → **卡片行**(票 71 抽出;历史事件、报警列表、预警列表三处共用这一份)。
|
|
|
+ *
|
|
|
+ * 为什么抽出来:票 71 要求「报警/预警两族共用同一套列表块形态」以及「卡片形态与既有
|
|
|
+ * 事件卡一致」。三处各写一份投影必然漂移(改了等级色却漏了状态色这种),
|
|
|
+ * 而「共用」这件事**只能由同一份代码保证**,不能靠抄得像。
|
|
|
+ *
|
|
|
+ * 字段一律来自 `eventRowView`(票 13/65 立的事件行唯一投影):等级与状态双色键、
|
|
|
+ * 内容摘要、设备/编号/时间/工单。本函数只做**卡片再包装**,不新造第二套字段口径。
|
|
|
+ *
|
|
|
+ * @param {Array} events `toEventItem()` 的领域行
|
|
|
+ * @param {string} family `WARNING`/`ALARM`(身份族,决定详情抽屉取哪一族)
|
|
|
+ */
|
|
|
+export function eventCardRows(events, family) {
|
|
|
+ const code = String(family || '').toUpperCase()
|
|
|
+ return (Array.isArray(events) ? events : []).map((item) => {
|
|
|
+ const view = eventRowView({ ...item, family: code })
|
|
|
+ return {
|
|
|
+ key: view.key,
|
|
|
+ title: view.name,
|
|
|
+ identityKind: CARD_ACTION.EVENT,
|
|
|
+ identityCode: view.identityCode,
|
|
|
+ identityFamily: view.identityFamily || family,
|
|
|
+ identityPayload: view,
|
|
|
+ identityReason: view.identityReason,
|
|
|
+ // 卡片主行:等级(带色)+ 名称 + 处理状态(带色)
|
|
|
+ tone: view.level.tone,
|
|
|
+ toneLabel: view.level.label,
|
|
|
+ // 状态**按族取词**(维护者 2026-10-29 第 1 条):预警六态、报警二态是两套词表,
|
|
|
+ // 共用一张会把 PENDING 译成「待处理」(实为待接收)、让 UNHANDLED 漏英文码上屏。
|
|
|
+ // 该端点未下发 status 时如实说「状态未发布」,不猜成某个状态。
|
|
|
+ statusTone: statusText(code, item.status) ? statusTone(code, item.status) : 'neutral',
|
|
|
+ statusLabel: statusText(code, item.status) || '状态未发布',
|
|
|
+ // 内容摘要:让用户直接看到"大概发生了什么"
|
|
|
+ summaryText: view.summaryText,
|
|
|
+ // 副信息:设备 / 编号 / 时间 / 工单
|
|
|
+ meta: [
|
|
|
+ view.deviceLabel ? `设备 ${view.deviceLabel}` : '',
|
|
|
+ view.code,
|
|
|
+ view.eventTimeText || view.eventTime,
|
|
|
+ view.workOrderStateName ? `工单${view.workOrderStateName}` : '',
|
|
|
+ ].filter(Boolean),
|
|
|
+ points: [],
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
function historyEventListBlock(current) {
|
|
|
- const events = current.events || []
|
|
|
- const rows = events.map((item) => ({
|
|
|
- key: item.code,
|
|
|
- title: item.name || item.code,
|
|
|
- identityKind: CARD_ACTION.EVENT,
|
|
|
- identityCode: item.code,
|
|
|
- identityFamily: String(current.mode || item.family || '').toUpperCase(),
|
|
|
- identityPayload: { ...eventRowView(item), family: String(current.mode || item.family || '').toUpperCase() },
|
|
|
- identityReason: item.code ? '' : '缺少事件编码,无法展开',
|
|
|
- tone: eventLevelTag(item.level).tone,
|
|
|
- toneLabel: eventLevelTag(item.level).label,
|
|
|
- meta: [item.code, item.eventTime, item.deviceName, item.status].filter(Boolean),
|
|
|
- points: [],
|
|
|
- }))
|
|
|
+ const rows = eventCardRows(current.events, String(current.mode || '').toUpperCase())
|
|
|
const emptyText = '当前时间范围没有历史事件'
|
|
|
const paging = current.paging && typeof current.paging === 'object' ? current.paging : null
|
|
|
if (!paging) return listBlock('历史事件', rows, emptyText)
|
|
|
@@ -443,6 +510,113 @@ export function responseEfficiencyTitle(mode) {
|
|
|
return `${eventListSwitchView(mode).label}效率`
|
|
|
}
|
|
|
|
|
|
+/**
|
|
|
+ * 预警/报警**「发生 · 处置」逐日趋势块**(票 71 返工,维护者 2026-10-29 第 2 条)。
|
|
|
+ *
|
|
|
+ * ── 推翻上一版的原因(记在这里,免得下一个人又画回去)──
|
|
|
+ * 上一版画「平均结案时长」:后端逐桶样本极稀(7 天 169 个小时桶里只有 1 个有样本),
|
|
|
+ * 按日聚合后整个窗口**只剩一个点** —— 一个点连不成线,屏上「只有点没有线」。
|
|
|
+ * 维护者改判口径:**每一日的总数 / 新增数 / 处置数**(条数口径,每天都有值,画得出走势)。
|
|
|
+ *
|
|
|
+ * ── 三条线的语义(算法见 `domain/incidentTrend.js`)──
|
|
|
+ * · 新增数 = 当天发布的条数(`eventTime` 落在当天);
|
|
|
+ * · 处置数 = 当天走到**本族终态**的条数(预警 CLOSED / 报警 HANDLED,按 `updatedTime` 归日);
|
|
|
+ * · 总数 = 截至当天结束的**累计**条数。
|
|
|
+ *
|
|
|
+ * **两族共用这一个构造函数**(票 71 要求 4):差异只在词表与终态码,
|
|
|
+ * 而「哪一态算终态」由 `eventStatusVocab.terminalStatuses(family)` 回答。
|
|
|
+ *
|
|
|
+ * @param {object} current 领域对象(需含 `incidentTrend`,由 `toHandlingEfficiency` 产出)
|
|
|
+ * @param {string} family `WARNING`/`ALARM`
|
|
|
+ */
|
|
|
+export function efficiencyIncidentTrendBlock(current, family) {
|
|
|
+ const view = eventListSwitchView(family)
|
|
|
+ const label = `${view.label}发生处置趋势`
|
|
|
+ const trend = current.incidentTrend || null
|
|
|
+ const days = (trend && trend.days) || null
|
|
|
+ // 取不全/无记录/窗口不合法 ⇒ **不画线**,由块级文案说清原因(不画偏低的假线)。
|
|
|
+ const emptyText = incidentTrendGapText(trend && trend.reason, view.key)
|
|
|
+ const points = (days || []).map((day) => ({
|
|
|
+ key: day.date,
|
|
|
+ // X 轴短读法 MM/DD;完整日期与三个读数进悬停。
|
|
|
+ label: `${day.date.slice(5, 7)}/${day.date.slice(8, 10)}`,
|
|
|
+ // 主轴读数用「新增」(每日增量是这个问题的主角);处置数与未处置数走各自序列。
|
|
|
+ count: day.added,
|
|
|
+ hint: `${day.date} · 新增 ${day.added} · 处置 ${day.handled} · 未处置 ${day.pending}`,
|
|
|
+ }))
|
|
|
+ return trendBlock(label, points, {
|
|
|
+ chart: 'line',
|
|
|
+ compact: true,
|
|
|
+ emptyText,
|
|
|
+ // 多序列(沿用维护者 2026-10-23 的双线成例):三条线共用 x 轴与联合最大值 + 图例。
|
|
|
+ // 三个色键各表一个语义:新增=lvl2 关注、处置=lvl4 了结、累计=lvl3 规模。
|
|
|
+ series: days
|
|
|
+ ? [
|
|
|
+ // 维护者 2026-10-30 第 2 条:图例悬停原先显示「整段求和」,对存量(未处置)毫无意义,
|
|
|
+ // 也和线上的读数对不上。改成每条序列自己的口径:流量算合计、存量算末日。
|
|
|
+ { key: 'added', label: `新增${view.label}`, tone: 'lv2', counts: days.map((day) => day.added), summary: 'sum' },
|
|
|
+ { key: 'handled', label: `处置${view.label}`, tone: 'lv4', counts: days.map((day) => day.handled), summary: 'sum' },
|
|
|
+ // 维护者 2026-10-29 第 3 条:原「累计」改为「未处置」(截至当日仍压着的条数)。
|
|
|
+ // 标签按族:预警的终态是「已解除」⇒ 补集叫「未解除」(与 KPI 卡「未解除预警」同词),
|
|
|
+ // 报警的终态是「已处理」⇒ 补集叫「未处置」。用词跟 KPI 卡一致,数字才能互相对上。
|
|
|
+ { key: 'pending', label: `${family === 'WARNING' ? '未解除' : '未处置'}${view.label}`, tone: 'lv1', counts: days.map((day) => day.pending), summary: 'last' },
|
|
|
+ ]
|
|
|
+ : [],
|
|
|
+ // 口径必须上屏(不能只活在这段注释里):处置时刻用的是更新时间,累计是截至当日。
|
|
|
+ aside: family === 'WARNING' ? '未解除=截至当日仍未解除 · 处置按更新时间归日' : '未处置=截至当日仍未处置 · 处置按更新时间归日',
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 报警/预警**列表块**(票 71)—— 同样只有这一份,参数化 family。
|
|
|
+ *
|
|
|
+ * ── 口径三件事 ──
|
|
|
+ * ① 行来自**同一次筛选**的事件集合,与 KPI「统计数」同源,不另取一套口径;
|
|
|
+ * ② 分页走服务端页码(单窗)或前端集合分页(多窗合并),回显一律经 `serverPageView`;
|
|
|
+ * ③ 卡片形态与既有事件卡一致(`eventCardRows` → `eventRowView`),可点开详情。
|
|
|
+ *
|
|
|
+ * **状态必须按族取词**(维护者 2026-10-29 第 1 条反馈):预警六态、报警二态,
|
|
|
+ * 由 `eventStatusVocab` 回答 —— 共用一张表会把 `PENDING` 译成「待处理」(实为待接收)、
|
|
|
+ * 让报警的 `UNHANDLED` 直接漏英文码上屏。样式(卡片/标签形态)两族共用,词条各自定义。
|
|
|
+ *
|
|
|
+ * @param {object} current 领域对象
|
|
|
+ * @param {string} family `WARNING`/`ALARM`
|
|
|
+ */
|
|
|
+export function efficiencyEventListBlock(current, family) {
|
|
|
+ const view = eventListSwitchView(family)
|
|
|
+ const label = `${view.label}列表`
|
|
|
+ const rows = eventCardRows(current.events, view.key)
|
|
|
+ const page = current.pagination || null
|
|
|
+ const total = numberish(current.totalEvents)
|
|
|
+ // 多窗(近 30/90 天):`events` 是各窗合并后的**已加载集合**,没有全局页。
|
|
|
+ const paging = current.paging && typeof current.paging === 'object' ? current.paging : null
|
|
|
+ // 页脚口径:列表条数的分母就是 KPI 那张「统计数」卡,写明同源。
|
|
|
+ const basisNote = total === null
|
|
|
+ ? `与上方「统计${view.label}数」同源(同一次筛选结果)`
|
|
|
+ : `本窗口${view.label}共 ${total} 条 · 与上方「统计${view.label}数」同源(同一次筛选结果)`
|
|
|
+ if (paging && paging.mode === 'merged') {
|
|
|
+ return listBlock(label, rows, `当前时间范围没有${view.label}`, {
|
|
|
+ paginate: true,
|
|
|
+ pageScope: `${view.key}|${paging.rangeKey || ''}`,
|
|
|
+ pageNote: (paging.windowCount || 0) > 1
|
|
|
+ ? `已合并 ${paging.windowCount} 个时间窗 · 已加载 ${paging.loaded || 0} 条(多窗范围不对单页做服务端分页)· ${basisNote}`
|
|
|
+ : `已加载 ${paging.loaded || 0} 条(清单取自本窗口全部事件,前端分页)· ${basisNote}`,
|
|
|
+ })
|
|
|
+ }
|
|
|
+ // 分页回显**必须走 `serverPageView`**(票 61 单源):越界夹回/缺 totalPages 补算/空集 1 页
|
|
|
+ // 都在那里定义。自己拼字段会漏掉夹回 —— 实测直传 `page:99` 会画出「第 99/3 页」。
|
|
|
+ // 兜底总数只用**后端回显的 total**,不拿 `rows.length` 顶:本块的行可能是多窗合并的
|
|
|
+ // 全量集合(92 条),而 `pagination` 是单窗某页的回显 —— 两者混用会画出
|
|
|
+ // 「共 92 条 / 19 页」却只对应一页的行(E4 钉住)。
|
|
|
+ const serverPage = page ? serverPageView(page, total === null ? 0 : total) : undefined
|
|
|
+
|
|
|
+ return listBlock(label, rows, `当前时间范围没有${view.label}`, {
|
|
|
+ serverPage,
|
|
|
+ pageNote: basisNote,
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
/**
|
|
|
* 预警效率面板的**口径说明**(票 65 立项;**票 78 改写**)。
|
|
|
*
|
|
|
@@ -459,8 +633,9 @@ export function responseEfficiencyTitle(mode) {
|
|
|
*/
|
|
|
export const WARNING_EFFICIENCY_GAP =
|
|
|
'预警不挂工单,工单口径的处置率与时长只在报警效率出现;' +
|
|
|
- '这里的条数、已解除率、平均处置时长与预警状态分布都是本窗口内预警自身的处置流程统计。' +
|
|
|
- '已解除率=已解除的预警 ÷ 本窗口预警条数(分母即第一张卡的条数),平均处置时长从预警发布计到已解除'
|
|
|
+ '这里的预警数、未解除、未处置、处置率、平均处置时长与预警状态分布都是本窗口内预警自身的处置流程统计。' +
|
|
|
+ '未解除=待接收/已接收/处置中/已处置(不含草稿与已解除),未处置=其中还没人接手的三态;' +
|
|
|
+ '处置率=已解除的预警 ÷ 本窗口预警条数(分母即第一张卡的条数),平均处置时长从预警发布计到已解除'
|
|
|
|
|
|
/**
|
|
|
* 已解除率 → 界面读数(票 78,预警族)。
|
|
|
@@ -506,83 +681,236 @@ export function warningDurationHint(current) {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 预警族 KPI:预警自身的处置流程读数(票 78;票 65 立的三卡已随票 74 契约升级)。
|
|
|
- * 三张卡=条数 / 已解除率 / 平均处置时长,读 `summary.warning*` 新字段;
|
|
|
- * 「已关联/未关联工单」退到窗口读数(真实计数仍在,只是不占 KPI 位 —— 见 `warningRangeNote`)。
|
|
|
- * 读数缺失走 `valueText` 的占位符(票 22/42 纪律:不把「没有」画成 0)。
|
|
|
+ * 3+2 栅格的**列跨度**(票 70)。
|
|
|
+ *
|
|
|
+ * 上排三卡等分、下排两卡填满整行 ⇒ 用**统一的 6 栏栅格**表达:
|
|
|
+ * 上排每张占 2 栏(2+2+2),下排每张占 3 栏(3+3)。
|
|
|
+ * 跨度值写在卡片上(`span`),渲染层据此排版 —— **领域层给的是意图,
|
|
|
+ * 渲染层只照着摆**,两边都不再各写一份「3+2 怎么排」。
|
|
|
+ *
|
|
|
+ * 共用一条栅格规则(两族一样),但**成员表分别定义**(见下两个函数):
|
|
|
+ * 预警与报警的指标不是同一批,混成一张表就又变成「两族一样」。
|
|
|
+ */
|
|
|
+export const KPI_GRID_SPAN = { third: 2, half: 3 }
|
|
|
+
|
|
|
+/**
|
|
|
+ * 预警族 3+2 指标(票 70 维护者给定)。
|
|
|
+ *
|
|
|
+ * **上排**:统计预警数 / 未解除预警 / 未处置预警
|
|
|
+ * **下排**:处置率 / 平均处置时长
|
|
|
+ *
|
|
|
+ * 口径(每一条都有出处,不是自拟):
|
|
|
+ * · 统计预警数 = `summary.totalEvents`(本窗口预警条数;处置率的分母就是它)。
|
|
|
+ * · 未解除预警 = 六态里 PENDING/RECEIVED/PROCESSING/HANDLED 之和 —— 与后端
|
|
|
+ * `CockpitOverviewModulesService.UNRESOLVED_STATUSES` **同一条口径**
|
|
|
+ * (该常量就是这四态;DRAFT/CLOSED 不算未解除)。
|
|
|
+ * · 未处置预警 = 六态里 PENDING/RECEIVED/PROCESSING 之和(「还没人接手」的一批)。
|
|
|
+ * · 处置率 = 已解除 ÷ 本窗口预警条数(`warningHandlingEfficiency`,后端同算法)。
|
|
|
+ * · 平均处置时长 = 发布→已解除的平均时长(`warningDuration`,单位取后端 `unit`)。
|
|
|
+ *
|
|
|
+ * **缺值一律占位符,不补 0**(票 05/16 纪律):六态没给(旧载荷/报警族)时
|
|
|
+ * 未解除/未处置都是 `—`(我们**不知道**,不是 0)。
|
|
|
*/
|
|
|
function warningEfficiencyKpis(current) {
|
|
|
+ const states = current.warningStates
|
|
|
+ const counts = states && states.length ? warningStateCounts(states) : null
|
|
|
return kpiBlock([
|
|
|
- { key: 'warningTotal', label: '预警条数', value: valueText(current.totalEvents), unit: '条' },
|
|
|
- { key: 'warningResolvedRate', label: '已解除率', value: warningResolveRateView(current), hint: warningResolveRateHint(current) },
|
|
|
- { key: 'warningDuration', label: '平均处置时长', value: warningDurationView(current), hint: warningDurationHint(current) },
|
|
|
- ])
|
|
|
+ { key: 'warningTotal', label: '预警数', value: valueText(current.totalEvents), unit: '条', span: KPI_GRID_SPAN.third },
|
|
|
+ {
|
|
|
+ key: 'warningUnresolved',
|
|
|
+ label: '未解除预警',
|
|
|
+ value: counts === null ? valueText(null) : valueText(counts.unresolved),
|
|
|
+ unit: '条',
|
|
|
+ span: KPI_GRID_SPAN.third,
|
|
|
+ hint: counts === null ? '' : '待接收、已接收、处置中、已处置(不含草稿与已解除)',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: 'warningUnhandled',
|
|
|
+ label: '未处置预警',
|
|
|
+ value: counts === null ? valueText(null) : valueText(counts.unhandled),
|
|
|
+ unit: '条',
|
|
|
+ span: KPI_GRID_SPAN.third,
|
|
|
+ hint: counts === null ? '' : '待接收、已接收、处置中(还没人接手的一批)',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: 'warningResolvedRate',
|
|
|
+ label: '处置率',
|
|
|
+ value: warningResolveRateView(current),
|
|
|
+ hint: warningResolveRateHint(current),
|
|
|
+ span: KPI_GRID_SPAN.half,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: 'warningDuration',
|
|
|
+ label: '平均处置时长',
|
|
|
+ value: warningDurationView(current),
|
|
|
+ // 票 70 要求 4:「单位与含义在卡上自明」—— 含义不能只藏在 hover 的 hint 里。
|
|
|
+ // 单位由后端 warningDuration.unit 决定(不写死 h),含义用卡内小字常显。
|
|
|
+ caption: '发布 → 已解除',
|
|
|
+ hint: warningDurationHint(current),
|
|
|
+ span: KPI_GRID_SPAN.half,
|
|
|
+ },
|
|
|
+ ], { layout: 'three-plus-two' })
|
|
|
}
|
|
|
|
|
|
-/** 报警族 KPI:工单口径(工单是报警的处置手段 —— 票 41/42/52 既有裁定,本票不动)。 */
|
|
|
+/**
|
|
|
+ * 报警族 3+2 指标(票 70 维护者给定)。
|
|
|
+ *
|
|
|
+ * **上排**:统计报警数 / 未关联工单 / 已关闭报警
|
|
|
+ * **下排**:处置率 / 平均关闭时长
|
|
|
+ *
|
|
|
+ * 口径:
|
|
|
+ * · 统计报警数 = `summary.totalEvents`(本窗口报警条数)。
|
|
|
+ * · 未关联工单 = `summary.missingWorkOrderCount`(报警没回联到工单的条数)。
|
|
|
+ * · 已关闭报警 = 本窗口内**已结案工单**(状态 5)的条数 —— 读
|
|
|
+ * `durationTrend[].handledCount` 的和(逐桶的已结案计数,后端按事件回联口径给)。
|
|
|
+ * · 处置率 = 已结案工单 ÷ 可计入工单(状态 0–6,排除已取消)。
|
|
|
+ * · 平均关闭时长 = `summary.averageDuration`(工单结案时长,单位取后端 `unit`)。
|
|
|
+ *
|
|
|
+ * 与预警族一样:**缺值占位符、不补 0**。已关闭报警缺趋势口径(预警族/旧载荷)时是 `—`。
|
|
|
+ */
|
|
|
function alarmEfficiencyKpis(current) {
|
|
|
return kpiBlock([
|
|
|
- { key: 'handlingRate', label: '处置率', value: handlingRateView(current), hint: handlingRateHint(current) },
|
|
|
- { key: 'responseDuration', label: '平均响应时长', value: durationView(current, ['responseDuration']) },
|
|
|
- { key: 'closeDuration', label: '平均关闭时长', value: durationView(current, ['closeDuration']) },
|
|
|
- ])
|
|
|
+ { key: 'alarmTotal', label: '报警数', value: valueText(current.totalEvents), unit: '条', span: KPI_GRID_SPAN.third },
|
|
|
+ {
|
|
|
+ key: 'alarmUnlinked',
|
|
|
+ label: '未关联工单',
|
|
|
+ value: valueText(current.missingWorkOrderCount),
|
|
|
+ unit: '条',
|
|
|
+ span: KPI_GRID_SPAN.third,
|
|
|
+ hint: '本窗口报警里还没回联到工单的条数',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: 'alarmClosed',
|
|
|
+ label: '已关闭报警',
|
|
|
+ value: valueText(current.closedAlarmCount),
|
|
|
+ unit: '条',
|
|
|
+ span: KPI_GRID_SPAN.third,
|
|
|
+ hint: '本窗口内回联工单已结案的报警条数(事件口径)',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: 'handlingRate',
|
|
|
+ label: '处置率',
|
|
|
+ value: handlingRateView(current),
|
|
|
+ hint: handlingRateHint(current),
|
|
|
+ span: KPI_GRID_SPAN.half,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: 'closeDuration',
|
|
|
+ label: '平均关闭时长',
|
|
|
+ value: durationView(current, ['closeDuration']),
|
|
|
+ // 票 70 要求 4:含义常显(不靠 hover)。单位取后端 averageDuration.unit。
|
|
|
+ caption: '工单结案耗时',
|
|
|
+ hint: closeDurationHint(current),
|
|
|
+ span: KPI_GRID_SPAN.half,
|
|
|
+ },
|
|
|
+ ], { layout: 'three-plus-two' })
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 六态分布行 → 预警族两个计数(未解除 / 未处置)。
|
|
|
+ *
|
|
|
+ * **口径与后端同源**(不是自拟):后端 `CockpitOverviewModulesService` 的
|
|
|
+ * `UNRESOLVED_STATUSES = PENDING / RECEIVED / PROCESSING / HANDLED` —— 未解除就是这四态之和
|
|
|
+ * (DRAFT 还没发布、CLOSED 已了结,都不算未解除)。未处置取其中「还没人接手」的三态
|
|
|
+ * (PENDING / RECEIVED / PROCESSING)—— HANDLED 已处置过,不算未处置。
|
|
|
+ *
|
|
|
+ * 分布缺失(`warningStates` 为 null)时回 null:**不知道就是不知道,不画 0**。
|
|
|
+ */
|
|
|
+function warningStateCounts(states) {
|
|
|
+ const byCode = {}
|
|
|
+ for (const row of states) byCode[String(row.code)] = numberish(row.count) || 0
|
|
|
+ const sum = (...codes) => codes.reduce((total, code) => total + (byCode[code] || 0), 0)
|
|
|
+ return {
|
|
|
+ unresolved: sum('PENDING', 'RECEIVED', 'PROCESSING', 'HANDLED'),
|
|
|
+ unhandled: sum('PENDING', 'RECEIVED', 'PROCESSING'),
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 已关闭报警数(报警族,票 70)—— 逐桶 `handledCount` 求和。
|
|
|
+ *
|
|
|
+ * 来源是 `durationTrend[].handledCount` —— 后端逐桶回「该桶内已结案的报警数」
|
|
|
+ * (`CockpitHandlingEfficiencyService.durationTrend`:报警族按事件回联到已结案工单计)。
|
|
|
+ * 报警族没有 `warning*` 那样的专属聚合字段,用这一份**逐桶计数**求和是对的:
|
|
|
+ * 它是**事件级**口径(多少条报警已了结),与工单张数口径(`workOrderStateCounts`)不同。
|
|
|
+ *
|
|
|
+ * **缺值不补 0**:桶缺该字段(预警族/旧载荷)或根本没有趋势行 ⇒ null
|
|
|
+ * (不是「0 条已关闭」)。
|
|
|
+ */
|
|
|
+function closedAlarmCount(trendRows) {
|
|
|
+ const rows = Array.isArray(trendRows) ? trendRows : []
|
|
|
+ if (!rows.length) return null
|
|
|
+ let sum = 0
|
|
|
+ for (const row of rows) {
|
|
|
+ const count = numberish(row && row.handledCount)
|
|
|
+ if (count === null) return null
|
|
|
+ sum += count
|
|
|
+ }
|
|
|
+ return sum
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 平均关闭时长的提示(报警族,票 70)。
|
|
|
+ *
|
|
|
+ * `closeDuration` 为 null 有两种来由:后端 `averageDuration.state=EMPTY`(窗口内没有已结案工单)
|
|
|
+ * 或旧载荷根本没有这个聚合。两种都给占位符,但**说清是哪一种** ——
|
|
|
+ * 「没有样本」与「后端没给」不是同一句话。
|
|
|
+ */
|
|
|
+function closeDurationHint(current) {
|
|
|
+ const duration = current && current.closeDuration
|
|
|
+ if (duration) return ''
|
|
|
+ return '窗口内没有已结案工单,暂无关闭时长样本'
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 响应效率(「预警效率」)面板(票 19;**票 65 两族语义分流**)。
|
|
|
+ * 响应效率(「预警效率」)面板(票 19;票 65 两族分流;票 71 三轮返工)。
|
|
|
*
|
|
|
- * 预警族与报警族的处置对象根本不同:**工单是报警的处置手段**(票 41/42/52),
|
|
|
- * 而预警不挂工单。所以同一个端点返回的工单口径字段只在报警模式下有意义;
|
|
|
- * 预警模式下照画就是「用一个不存在的对象统计预警」(2026-10-19 维护者反馈第 6 条)。
|
|
|
+ * 块序:KPI → 趋势 → 分布 → **列表垫底**(维护者第 16 轮第 8 条)。列表可达近百行,
|
|
|
+ * 排在中间会把走势与分布挤出首屏;读者先看结论与走势,逐条清单最后才展开。
|
|
|
*
|
|
|
- * `extra.mode` 是当前切换族(外壳从 `eventStore.mode` 传入);`data.family` 是后端回显,
|
|
|
- * 两者都有时以**回显**为准(那份数据确实是哪一族就以哪一族画)。都没有时按报警口径
|
|
|
- * (工单口径是既有形态,不因本票对旧调用点改造)。
|
|
|
+ * 预警族已按维护者 2026-10-29 反馈删除「预警等级分布」与「预警发生趋势」两块。
|
|
|
*
|
|
|
* @param {object|null} data `toHandlingEfficiency()` 的领域对象
|
|
|
* @param {object} section 分区
|
|
|
- * @param {{mode?: string}} [extra] 当前切换族
|
|
|
+ * @param {{mode?: string, workOrderList?: object}} [extra] 当前族与工单列表分区
|
|
|
*/
|
|
|
export function responseEfficiencyPanel(data, section, extra) {
|
|
|
const current = data || {}
|
|
|
const family = current.family || (extra && extra.mode) || EVENT_FAMILY.ALARM
|
|
|
if (family === EVENT_FAMILY.WARNING) {
|
|
|
- // 发生趋势与历史分析同一条「多桶→按日合并」口径(报复用,不另写一份采样)。
|
|
|
- const trend = historyTrendRows(current.eventTrend || [])
|
|
|
return sectionView('responseEfficiency', section, [
|
|
|
warningEfficiencyKpis(current),
|
|
|
+ // 票 71:逐日 新增/处置/未处置 三条线(排最前,与报警族同一块构造)。
|
|
|
+ efficiencyIncidentTrendBlock(current, EVENT_FAMILY.WARNING),
|
|
|
// 票 78:预警状态分布(WarningWorkflow 六态 + 脏状态的「未知」桶)。
|
|
|
- // 行的中文名由后端 `stateName` 下发,前端不另造状态词;字段缺失(旧载荷/
|
|
|
- // 报警族)不画假块 —— 缺失不画假 0 的同一纪律。全 0 也是真实分布(缺桶计 0)。
|
|
|
- // 维护者第 15 轮(预警模式):状态分布改**堆叠条**(六态一条读出流程推进),
|
|
|
- // 等级分布改**饼图**(四级是固定语义阶梯,环图比排行更直观)。
|
|
|
...(Array.isArray(current.warningStates) && current.warningStates.length
|
|
|
- ? [stackedBlock('预警状态分布', current.warningStates)]
|
|
|
+ ? [stackedBlock('预警状态分布', current.warningStates.map((row) => ({
|
|
|
+ key: row.code,
|
|
|
+ label: row.label,
|
|
|
+ count: row.count,
|
|
|
+ })))]
|
|
|
: []),
|
|
|
- donutBlock('预警等级分布', current.levelDistribution || [], { layout: 'compact' }),
|
|
|
- trendBlock(trend.length > 48 ? '预警发生趋势 · 每日合计' : '预警发生趋势', trend, { chart: 'line', compact: true }),
|
|
|
+ efficiencyEventListBlock(current, EVENT_FAMILY.WARNING),
|
|
|
], {
|
|
|
label: responseEfficiencyTitle(EVENT_FAMILY.WARNING),
|
|
|
range: current.range,
|
|
|
- // 口径说明(走票 56 的折叠摘要,`data-panel-note`):说清「预警不挂工单」
|
|
|
- // 与这些数是预警自身的处置流程统计、已解除率的分母(票 78 改写)。
|
|
|
note: WARNING_EFFICIENCY_GAP,
|
|
|
rangeNote: warningRangeNote(current),
|
|
|
})
|
|
|
}
|
|
|
return sectionView('responseEfficiency', section, [
|
|
|
alarmEfficiencyKpis(current),
|
|
|
- // 2026-10-13 图表选型:工单状态是多类别占比 → 百分比堆叠条(饼图超过 5 类不再适合)。
|
|
|
+ // 票 71:逐日 新增/处置/未处置 三条线。
|
|
|
+ efficiencyIncidentTrendBlock(current, EVENT_FAMILY.ALARM),
|
|
|
stackedBlock('工单状态分布', current.workOrderStates || []),
|
|
|
- // 时长趋势仍是时间序列 → 折线。
|
|
|
- trendBlock('平均结案时长趋势', trendRows((current.trend || []).filter((row) => row.count !== null)), { chart: 'line', compact: true }),
|
|
|
- // 票 65 追加:报警模式下直接列出当前工单(卡片,可点开详情)。
|
|
|
- // 放在图之后:读者先看结论(处置率/分布),再看逐张明细。
|
|
|
+ // 维护者第 16 轮(第 8 条):**列表放最下面** —— 趋势与分布提上来,逐条清单垫底。
|
|
|
+ // 原来夹在趋势和分布之间,把走势挤到了下方。
|
|
|
+ efficiencyEventListBlock(current, EVENT_FAMILY.ALARM),
|
|
|
+ // 工单内容不属本票(维护者第 3 条交由另一会话),保持原样。
|
|
|
workOrderListBlock(extra),
|
|
|
], {
|
|
|
label: responseEfficiencyTitle(EVENT_FAMILY.ALARM),
|
|
|
range: current.range,
|
|
|
- // 报警族口径说明:工单是报警的处置手段(与预警族那句并列,切换时一起换)。
|
|
|
note: ALARM_EFFICIENCY_BASIS,
|
|
|
rangeNote: current.rangeNote === undefined
|
|
|
? (current.denominator === null || current.denominator === undefined
|
|
|
@@ -641,6 +969,20 @@ function workOrderListBlock(extra) {
|
|
|
})
|
|
|
}
|
|
|
|
|
|
+/**
|
|
|
+ * 工单 → 列表卡片行(票 65 精简形态)。
|
|
|
+ *
|
|
|
+ * 字段选择只取「一眼判断该不该点开」的信息:工单号、状态、设备、描述、创建时间。
|
|
|
+ * 行字段名**贴着共享渲染层已认的形状**(`code`/`name`/`tone`/`toneLabel`/`meta`):
|
|
|
+ * `PanelBlocks` 的 list 块见到 `row.code` 就把整张卡做成可点(点击 emit `open-card`),
|
|
|
+ * 见到 `tone`+`toneLabel` 就画状态标签 —— 所以这里不再发明第二套卡片协议。
|
|
|
+ *
|
|
|
+ * 文本一律复用 `equipmentDetailView.workOrderRows`(单一词表:状态字典 0–7、
|
|
|
+ * 关联文本、时间短读法都在那儿定过一次),本函数只做**卡片需要的再包装**。
|
|
|
+ *
|
|
|
+ * @param {Array} orders `toWorkOrder()` 的领域对象数组
|
|
|
+ */
|
|
|
+
|
|
|
/**
|
|
|
* 工单 → 列表卡片行(票 65 精简形态)。
|
|
|
*
|
|
|
@@ -685,6 +1027,23 @@ export function workOrderCardRows(orders) {
|
|
|
}))
|
|
|
}
|
|
|
|
|
|
+/**
|
|
|
+ * 工单状态 → `StatusTag` 的色键(票 65 卡片用)。
|
|
|
+ *
|
|
|
+ * **不登记第二张状态词表**:词(待派单…)一律走 `workOrder.js` 的 `WORK_ORDER_STATES`,
|
|
|
+ * 这里只回答「这一态借哪一枚既有色键上色」。
|
|
|
+ *
|
|
|
+ * 为什么映射到既有键而不是新增键:`StatusTag` 的 tone 是**封闭词表**
|
|
|
+ * (`domain/statusTones.js` 的 `TONES`,未知 tone 会退回中性灰),
|
|
|
+ * 新增 `state-closed` 之类必须同时改那张表与样式,而那属于视觉批次;
|
|
|
+ * 本票只需「一眼看出这张工单是等着办、在办还是已了」,借用四档语义色即可:
|
|
|
+ * · 待派单/待接单 → `lv1`(最需立刻响应,借一级告警色);
|
|
|
+ * · 已接单/处理中/待验收 → `lv3`(在办,借三级信息色);
|
|
|
+ * · 延期中 → `lv2`(旁路,需要盯);
|
|
|
+ * · 已结案 → `lv4`(了结,借正常色);
|
|
|
+ * · 已取消/认不出 → `neutral`(中性,不暗示任何态度)。
|
|
|
+ */
|
|
|
+
|
|
|
/**
|
|
|
* 工单状态 → `StatusTag` 的色键(票 65 卡片用)。
|
|
|
*
|
|
|
@@ -715,7 +1074,8 @@ export function workOrderStateTone(stateCode) {
|
|
|
|
|
|
/** 报警族的口径说明(与预警族那句成对:切换时口径说明一起换)。 */
|
|
|
export const ALARM_EFFICIENCY_BASIS =
|
|
|
- '报警以工单为处置手段:处置率=已结案工单 ÷ 可计入工单(状态 0–6,排除已取消),单位为工单张数'
|
|
|
+ '报警以工单为处置手段:处置率=已结案工单 ÷ 可计入工单(状态 0–6,排除已取消),单位为工单张数;' +
|
|
|
+ '已关闭报警按事件口径数(本窗口报警里回联工单已结案的条数),与工单张数口径不同'
|
|
|
|
|
|
/**
|
|
|
* 预警族的窗口读数(票 65 立法;票 78 扩到处置流程读数):
|
|
|
@@ -764,33 +1124,75 @@ export function eventAnalysisBasis(family) {
|
|
|
*
|
|
|
* @param {object|null} data `toEventAnalysis()` 的领域对象(label 已是中文)
|
|
|
* @param {object} section 分区
|
|
|
- * @param {{mode?: string}} [extra] 当前切换族(与效率面板同一接缝)
|
|
|
+ * @param {{mode?: string, historyEvents?: Array}} [extra] 当前切换族与事件行(后者供报警族归类)
|
|
|
*/
|
|
|
+/**
|
|
|
+ * 报警族的「按事件类型」桶(维护者 2026-10-29)。
|
|
|
+ *
|
|
|
+ * ── 为什么不能用后端的 EVENT_TYPE ──
|
|
|
+ * 后端多维分析对报警族回的 `EVENT_TYPE` 是 **`UNKNOWN_EVENT_TYPE`**(实测:30 天窗口只有
|
|
|
+ * 一个「未知事件类型」桶)。根因是报警**没有预警类型码**:报警事件的 `eventType` 是
|
|
|
+ * **设备类型 id**(`"13"`/`"15"`),不是预警那套 `WARN-xxx` 码,后端归不出类型就落进未知桶。
|
|
|
+ * 所以屏上「按事件类型」对报警族恒为空/未知 —— 维护者反馈「不应该获取不到类型」正是这个。
|
|
|
+ *
|
|
|
+ * ── 用什么代替 ──
|
|
|
+ * 报警事件的**真实类目在 `eventName`**(实测:漏失监测 / COD监测 / 氨氮监测 / OFFLINE),
|
|
|
+ * 这才是值班人员嘴里说的「类型」。事件行本来就要取(见 `fetchIncidentTrend`),
|
|
|
+ * 这里按 `eventName` 聚合,不额外发请求、不猜。
|
|
|
+ *
|
|
|
+ * @param {object} current `toEventAnalysis()` 的领域对象
|
|
|
+ * @param {string} family 当前族
|
|
|
+ * @param {object} [extra] 面板额外输入(`historyEvents`:带 name 的事件行)
|
|
|
+ * @returns {Array<{code: string, label: string, count: number}>|null} 认不出族或没行时回 null(用后端桶)
|
|
|
+ */
|
|
|
+function alarmEventTypeRows(current, family, extra) {
|
|
|
+ if (String(family || '').toUpperCase() !== EVENT_FAMILY.ALARM) return null
|
|
|
+ const rows = (extra && extra.historyEvents) || null
|
|
|
+ if (!Array.isArray(rows) || !rows.length) return null
|
|
|
+ const counts = new Map()
|
|
|
+ for (const row of rows) {
|
|
|
+ // `name` 是 `toEventItem` 从后端 `eventName` 翻出来的领域字段。
|
|
|
+ const label = String((row && row.name) || '').trim()
|
|
|
+ // 没有名称的不丢掉,归入「未命名」并**如实显示**(丢行会让合计对不上条数)。
|
|
|
+ const key = label || '未命名'
|
|
|
+ counts.set(key, (counts.get(key) || 0) + 1)
|
|
|
+ }
|
|
|
+ return [...counts.entries()]
|
|
|
+ .map(([label, count]) => ({ code: label, label, count }))
|
|
|
+ .sort((left, right) => right.count - left.count)
|
|
|
+}
|
|
|
export function eventAnalysisPanel(data, section, extra) {
|
|
|
const current = data || {}
|
|
|
const family = current.family || (extra && extra.mode) || EVENT_FAMILY.WARNING
|
|
|
- const highlights = (current.highlights || []).map((item, index) => ({
|
|
|
- key: `${item.title || '重点'}-${index}`,
|
|
|
- title: item.title || '重点识别',
|
|
|
- meta: [item.detail, item.count === null ? '' : `事件 ${item.count} 条`].filter(Boolean),
|
|
|
- points: [],
|
|
|
- }))
|
|
|
// 成因块(票 66):**后端回出这一维才画**。真实两族都恒定回(缺成因走 UNKNOWN_CAUSE 兜底桶),
|
|
|
// 但旧载荷/mock 可能没有这一维 —— 没有就不画假块(票 05 的「缺值不画假 0」同一立场),
|
|
|
// 顺便避免空环形图落进渲染层那句「当前范围内没有监测点与未关联设备」(那是监测概览的措辞)。
|
|
|
- const causes = sortByCountDesc(current.causes || [], (row) => row.count)
|
|
|
+ // 维护者 2026-10-30 第 4 条:「按成因」的数据源是 multidimensional-analysis 的 CAUSE 维,
|
|
|
+ // 而该维实测**恒为 UNKNOWN_CAUSE**(「未知/缺失成因」)—— 等于没有这项数据。
|
|
|
+ // 换成后端确定有值的 DEVICE_TYPE(设备类型),两族都能出真实类目。
|
|
|
+ const deviceTypes = sortByCountDesc(current.deviceTypes || [], (row) => row.count)
|
|
|
+ // 后端把**全部**设备类型都列出来了(13 类,其中 11 类计数为 0)——
|
|
|
+ // 零计数桶留着就是一屏零。过滤掉(0 本来就不占类别,总数不变)。
|
|
|
+ .filter((row) => row.count > 0)
|
|
|
+ // 报警族的「按事件类型」改由事件行的 `eventName` 归类(后端对该族恒回未知桶);
|
|
|
+ // 预警族仍用后端 EVENT_TYPE 桶(那套 `WARN-xxx` 码对预警是准的)。
|
|
|
+ const alarmTypes = alarmEventTypeRows(current, family, extra)
|
|
|
+ const typeRows = alarmTypes || current.types || []
|
|
|
return sectionView('eventAnalysis', section, [
|
|
|
// 2026-10-13 维护者反馈:条形图四连排视觉疲劳——类别少的构成改环形图(行业=行业色、
|
|
|
// 类型=图表色序),等级/片区保留条形(条形适合作排名比较)。行业行保留联动。
|
|
|
// 票 37 走查扩展:类型/行业/片区是排名类别按计数降序(并列保序);
|
|
|
// 事件等级是**固定序**(1–4 级语义阶梯),保持后端字典序不打乱。
|
|
|
// 维护者第 15 轮:类型是**多类别**,排行(由大到小)比环图更读得清;成因改堆叠条。
|
|
|
- rankingBarsBlock('按事件类型', sortByCountDesc(current.types || [], (row) => row.count), {
|
|
|
+ rankingBarsBlock('按事件类型', sortByCountDesc(typeRows, (row) => row.count), {
|
|
|
limit: 0,
|
|
|
variant: 'rank',
|
|
|
unit: '起',
|
|
|
+ // 报警族的类别来自事件名称(后端对该族的 EVENT_TYPE 恒为未知桶),如实标注。
|
|
|
+ aside: alarmTypes ? '按事件名称归类(报警无预警类型码)' : '',
|
|
|
}),
|
|
|
- ...(causes.length ? [stackedBlock('按成因', causes)] : []),
|
|
|
+ // 后端没回设备类型这一维时不画假块(缺值不画假的同一纪律)。
|
|
|
+ ...(deviceTypes.length ? [stackedBlock('按设备类型', deviceTypes)] : []),
|
|
|
donutBlock(
|
|
|
'按行业分布',
|
|
|
sortByCountDesc(
|
|
|
@@ -802,7 +1204,6 @@ export function eventAnalysisPanel(data, section, extra) {
|
|
|
// 维护者第 15 轮:等级(固定四级阶梯)与片区(类别少)都改饼图。
|
|
|
donutBlock('按事件等级', current.levels || [], { layout: 'compact' }),
|
|
|
donutBlock('按片区分布', sortByCountDesc(current.regions || [], (row) => row.count), { layout: 'compact' }),
|
|
|
- listBlock('重点识别', highlights, '当前没有识别到重点'),
|
|
|
], {
|
|
|
label: '预警分析',
|
|
|
range: current.range,
|