|
|
@@ -5,13 +5,21 @@
|
|
|
* 预警/报警沿用票 13 的 mode;历史、效率、分析三块不各自维护一套切换状态。
|
|
|
*/
|
|
|
import { fieldReader } from './fields.js'
|
|
|
-import { eventLevelTag, eventListSwitchView, toEventItem } from './eventList.js'
|
|
|
+import {
|
|
|
+ causeText,
|
|
|
+ eventLevelTag,
|
|
|
+ eventLevelTagFromBucket,
|
|
|
+ eventListSwitchView,
|
|
|
+ eventTypeText,
|
|
|
+ localizeCodeText,
|
|
|
+ toEventItem,
|
|
|
+} from './eventList.js'
|
|
|
import { valueText, workOrderRows } from './equipmentDetailView.js'
|
|
|
import { EVENT_FAMILY } from './events.js'
|
|
|
import { WORK_ORDER_STATES } from './workOrder.js'
|
|
|
-// 票 67:全局行业筛选**作用不到**全量工单端点(实测不收该入参),措辞取单一来源。
|
|
|
-import { industryGap } from './filterCaliber.js'
|
|
|
+import { industryLabel } from './filterOptions.js'
|
|
|
import {
|
|
|
+ CODE_TEXT,
|
|
|
barsBlock,
|
|
|
donutBlock,
|
|
|
kpiBlock,
|
|
|
@@ -39,22 +47,48 @@ function list(raw, key) {
|
|
|
return Array.isArray(value) ? value : []
|
|
|
}
|
|
|
|
|
|
-function countRows(rows, key = 'count') {
|
|
|
+/**
|
|
|
+ * 旧形状(`dimensions` 不是数组时的 `types`/`industries` 等)→ 行。
|
|
|
+ *
|
|
|
+ * 票 66:加 `translate` 参数 —— 同一个转换要能带自己的词表(类型/成因/行业各一张),
|
|
|
+ * 但**行形状与降序口径只有这一份**,不因为加翻译就复制一份 `countRows`。
|
|
|
+ */
|
|
|
+function countRows(rows, key = 'count', translate = null) {
|
|
|
return list(rows, key).map((item) => {
|
|
|
const reader = fieldReader(item, 'eventAnalysisRow')
|
|
|
+ const code = reader.optionalStr('code') || reader.optionalStr('name') || reader.optionalStr('label')
|
|
|
+ const label = reader.optionalStr('name') || reader.optionalStr('label') || reader.optionalStr('code')
|
|
|
return {
|
|
|
- code: reader.optionalStr('code') || reader.optionalStr('name') || reader.optionalStr('label'),
|
|
|
- label: reader.optionalStr('name') || reader.optionalStr('label') || reader.optionalStr('code'),
|
|
|
+ code,
|
|
|
+ label: translate ? translate(code, label) : label,
|
|
|
count: numberish(reader.raw.count) || 0,
|
|
|
}
|
|
|
})
|
|
|
}
|
|
|
|
|
|
+/**
|
|
|
+ * 行业桶 → 中文(票 66):先走全局筛选的**同一张**行业词表(`filterOptions.industryLabel`
|
|
|
+ * —— 它把后端码 GAS/WATER_SUPPLY/DRAINAGE/MANHOLE 翻成燃气/供水/排水/窨井),
|
|
|
+ * 认不出的回 `CODE_TEXT` 的既有口径(行业码是**数据值**,照原样带出,见 `panelBlocks.CODE_TEXT` 注释)。
|
|
|
+ * 不新造第二份行业词表 —— 筛选条、地图图例、本面板必须说同一个词。
|
|
|
+ */
|
|
|
+function industryText(code, label) {
|
|
|
+ return industryLabel(code) || localizeCodeText(code, label, CODE_TEXT)
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 等级桶 → 行(票 66)。
|
|
|
+ *
|
|
|
+ * 后端 `LEVEL` 桶的形状是 `{bucketCode: '1'..'4'|'UNKNOWN', bucketLabel: 'RED'..'GREEN'|'UNKNOWN'}`
|
|
|
+ * —— **码是等级号、label 是颜色名**,两族都一样。旧实现只读码:码在就侥幸对、只有颜色名时
|
|
|
+ * 整块落「等级未知」。现在两种都认(`eventLevelTagFromBucket`),中文名仍只来自 `LEVEL_TAGS`。
|
|
|
+ */
|
|
|
function levelRows(rows) {
|
|
|
- return (Array.isArray(rows) ? rows : []).map((item) => {
|
|
|
- const level = Number(item && (item.level === undefined ? item.code : item.level))
|
|
|
- const tag = eventLevelTag(level)
|
|
|
- return { code: String(level), label: tag.label, count: numberish(item && item.count) || 0 }
|
|
|
+ return (Array.isArray(rows) ? rows : []).map((raw) => {
|
|
|
+ const source = raw || {}
|
|
|
+ const code = source.code !== undefined ? String(source.code) : source.level === undefined ? '' : String(source.level)
|
|
|
+ const tag = eventLevelTagFromBucket(source.code !== undefined ? source.code : source.level, source.label)
|
|
|
+ return { code, label: tag.label, count: numberish(source.count) || 0 }
|
|
|
})
|
|
|
}
|
|
|
|
|
|
@@ -171,6 +205,14 @@ export function toHandlingEfficiency(raw) {
|
|
|
? Object.fromEntries(reader.raw.workOrderStateCounts.map((row) => [row.stateCode, row.count]))
|
|
|
: family.workOrderStates || reader.raw.workOrderStates || reader.raw.workOrderStateDistribution || {}
|
|
|
const averageDuration = summary.averageDuration || reader.raw.averageDuration || {}
|
|
|
+ // 票 78:预警**自身**的处置流程聚合(票 74 契约,后端已交付)。两字段为预警族专属,
|
|
|
+ // 报警族与旧载荷为 null —— null 保真(票 42/20 同一纪律):缺失不数成 0,界面据此占位。
|
|
|
+ const warningEfficiencyRaw = summary.warningHandlingEfficiency && typeof summary.warningHandlingEfficiency === 'object'
|
|
|
+ ? summary.warningHandlingEfficiency
|
|
|
+ : null
|
|
|
+ const warningDurationRaw = summary.warningDuration && typeof summary.warningDuration === 'object'
|
|
|
+ ? summary.warningDuration
|
|
|
+ : null
|
|
|
const distribution = WORK_ORDER_STATES.map((item) => ({
|
|
|
code: String(item.code),
|
|
|
label: item.name,
|
|
|
@@ -188,6 +230,16 @@ export function toHandlingEfficiency(raw) {
|
|
|
label: eventLevelTag(level).label,
|
|
|
count: events.filter((item) => Number(item && item.level) === level).length,
|
|
|
}))
|
|
|
+ // 票 78:六态分布行(DRAFT…CLOSED,脏状态由后端归入「未知」桶一并下发)。
|
|
|
+ // 行的码/中文名/计数**全部以后端回显为准** —— 词表取 WarningWorkflow 中文名,
|
|
|
+ // 前端不另造一套状态词;字段缺失(旧载荷/报警族)回 null,界面不画假块。
|
|
|
+ const warningStates = Array.isArray(reader.raw.warningStateCounts)
|
|
|
+ ? reader.raw.warningStateCounts.map((row) => ({
|
|
|
+ code: row && row.stateCode !== undefined && row.stateCode !== null ? String(row.stateCode) : '',
|
|
|
+ label: (row && row.stateName) || (row && row.stateCode) || '',
|
|
|
+ count: numberish(row && row.count) || 0,
|
|
|
+ }))
|
|
|
+ : null
|
|
|
return {
|
|
|
family: familyCode,
|
|
|
handlingRate: numberish(handlingValue.value === undefined ? (handlingValue.rate === undefined ? reader.raw.handlingRate : handlingValue.rate) : handlingValue.value),
|
|
|
@@ -211,6 +263,24 @@ export function toHandlingEfficiency(raw) {
|
|
|
? summary.averageDuration.state === 'EMPTY' ? null : summary.averageDuration
|
|
|
: durationValue(raw, ['averageCloseDuration', 'averageCloseSeconds']) || durationValue(averageDuration, ['closeDuration', 'closeSeconds', 'close']),
|
|
|
workOrderStates: distribution,
|
|
|
+ // 票 78:预警自身处置流程读数(已解除率/平均处置时长聚合 + 六态分布行)。
|
|
|
+ // 三者都是**预警族专属**:报警族与旧载荷为 null,消费点据此占位/不画假块。
|
|
|
+ warningStates,
|
|
|
+ warningEfficiency: warningEfficiencyRaw ? {
|
|
|
+ value: numberish(warningEfficiencyRaw.value),
|
|
|
+ numerator: numberish(warningEfficiencyRaw.numerator),
|
|
|
+ denominator: numberish(warningEfficiencyRaw.denominator),
|
|
|
+ state: warningEfficiencyRaw.state || '',
|
|
|
+ reason: warningEfficiencyRaw.reason || '',
|
|
|
+ basis: warningEfficiencyRaw.basis || '',
|
|
|
+ } : null,
|
|
|
+ warningDuration: warningDurationRaw ? {
|
|
|
+ value: numberish(warningDurationRaw.value),
|
|
|
+ unit: warningDurationRaw.unit || '',
|
|
|
+ sampleCount: numberish(warningDurationRaw.sampleCount),
|
|
|
+ state: warningDurationRaw.state || '',
|
|
|
+ reason: warningDurationRaw.reason || '',
|
|
|
+ } : null,
|
|
|
// 逐桶**事件发生量**(与下面的结案时长趋势是两个口径,预警族只用这一份)。
|
|
|
eventTrend: trendRowsRaw.map((item) => ({
|
|
|
bucket: String(item && (item.timeBucket || item.bucket || item.bucketStart || '')),
|
|
|
@@ -228,22 +298,43 @@ export function toHandlingEfficiency(raw) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-/** 多维分析载荷 → 领域对象。 */
|
|
|
+/**
|
|
|
+ * 多维分析载荷 → 领域对象(票 19;**票 66 加码→中文翻译层**)。
|
|
|
+ *
|
|
|
+ * 票 66 的要点:后端 `buckets[].bucketLabel` 对 `EVENT_TYPE`/`INDUSTRY`/`LEVEL` 下发的
|
|
|
+ * 就是**英文码**(实测 2026-10-22 两族都如此),照 `bucketLabel` 直画等于把接口代号印上大屏。
|
|
|
+ * 所以这里按维度各挂一张词表(类型/成因各一张,行业/等级复用既有单源),
|
|
|
+ * 领域对象交出去的 `label` 一律是**可上屏的中文**;认不出的码回原码(数据值口径,见
|
|
|
+ * `eventList.localizeCodeText`),行与计数**不丢**(丢一行 = 分布合计对不上总数,那是谎话)。
|
|
|
+ *
|
|
|
+ * `causes`(票 66 新增):`CAUSE` 维度此前没有任何消费点。后端两个族都**恒定**回这一维
|
|
|
+ * (缺成因也走 `UNKNOWN_CAUSE` 兜底桶,源码注释原句 "never hides missing causes"),
|
|
|
+ * 面板据此决定要不要画「按成因」块 —— 没返回这一维就**不画假块**。
|
|
|
+ */
|
|
|
export function toEventAnalysis(raw) {
|
|
|
const reader = fieldReader(raw, 'eventAnalysis')
|
|
|
const dimensions = reader.raw.dimensions
|
|
|
const source = Array.isArray(dimensions)
|
|
|
? Object.fromEntries(dimensions.map((dimension) => [dimension.dimensionCode, dimension.buckets || []]))
|
|
|
: dimensions || reader.raw
|
|
|
- const dimensionRows = (key) => (source[key] || []).map((bucket) => ({ code: bucket.bucketCode, label: bucket.bucketLabel || bucket.bucketCode, count: numberish(bucket.count) || 0 }))
|
|
|
+ const dimensionRows = (key, translate) => (source[key] || []).map((bucket) => ({
|
|
|
+ code: bucket.bucketCode,
|
|
|
+ label: translate ? translate(bucket.bucketCode, bucket.bucketLabel) : bucket.bucketLabel || bucket.bucketCode,
|
|
|
+ count: numberish(bucket.count) || 0,
|
|
|
+ }))
|
|
|
const highlights = list(raw, 'highlights').map((item) => {
|
|
|
const row = fieldReader(item, 'highlight')
|
|
|
return { title: row.optionalStr('title') || row.optionalStr('name'), detail: row.optionalStr('detail') || row.optionalStr('description'), count: numberish(row.raw.count) }
|
|
|
})
|
|
|
+ const familyRaw = reader.optionalStr('eventFamily') || reader.optionalStr('mode')
|
|
|
return {
|
|
|
- types: Array.isArray(dimensions) ? dimensionRows('EVENT_TYPE') : countRows(source, 'types').concat(countRows(source, 'byType')),
|
|
|
+ // 当前族(后端回显):面板的「事件」口径说明据此换词(预警/报警两族各说各的)。
|
|
|
+ family: familyRaw === EVENT_FAMILY.ALARM || familyRaw === EVENT_FAMILY.WARNING ? familyRaw : '',
|
|
|
+ types: Array.isArray(dimensions) ? dimensionRows('EVENT_TYPE', eventTypeText) : countRows(source, 'types', eventTypeText).concat(countRows(source, 'byType', eventTypeText)),
|
|
|
levels: Array.isArray(dimensions) ? levelRows(dimensionRows('LEVEL')) : levelRows(source.levels || source.byLevel),
|
|
|
- industries: Array.isArray(dimensions) ? dimensionRows('INDUSTRY') : countRows(source, 'industries').concat(countRows(source, 'byIndustry')),
|
|
|
+ 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)),
|
|
|
+ // 片区不上翻译层:后端给的就是中文名(城南/城北/太常/未分区),码是 '0'/'1'/'2'。
|
|
|
regions: Array.isArray(dimensions) ? dimensionRows('DISTRICT') : countRows(source, 'regions').concat(countRows(source, 'byRegion')),
|
|
|
highlights,
|
|
|
range: rangeView(raw),
|
|
|
@@ -335,34 +426,78 @@ export function responseEfficiencyTitle(mode) {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 预警效率面板的**口径说明**(票 65,预警族)。
|
|
|
+ * 预警效率面板的**口径说明**(票 65 立项;**票 78 改写**)。
|
|
|
*
|
|
|
- * 背景(2026-10-21 复核取证,维护者要求「确认预警数据是否正确」):
|
|
|
- * 后端 `CockpitHandlingEfficiencyService` 的 `stateCounts` 来自 `scopeWorkOrders(...)`,
|
|
|
- * 其源码注释原句是「范围内**全部工单**(不要求关联报警/事件)」——即处置率/状态分布/
|
|
|
- * 结案时长都是**工单**口径,与事件族无关。实测两族同样结构:
|
|
|
- * 预警族 `linkedWorkOrderCount=0`、八态合计 0。因此**预警族确实没有预警自身的处置流程统计**。
|
|
|
+ * 背景:票 65 当时实测预警族没有预警自身的处置流程统计(工单口径两族混同),
|
|
|
+ * 文案如实写下「后端尚未提供、已登记需求池」。票 74 已把契约补上(后端 `983fc27a`):
|
|
|
+ * 预警族现在回 `warningStateCounts`(WarningWorkflow 六态)、`warningHandlingEfficiency`
|
|
|
+ * (已解除 ÷ 窗口内预警数)、`warningDuration`(发布→已解除平均时长)——
|
|
|
+ * 旧文案从今天起就是谎话,必须改写。
|
|
|
*
|
|
|
- * 这段文字是要给值班人员看的,所以修掉上一版的两处毛病:
|
|
|
- * ① 「本端点」是接口词汇(项目纪律:接口词汇不上大屏)→ 改成说「后端/当前数据」;
|
|
|
- * ② 前半句先讲「我们没有字段」,读者要读两遍才知道面板上的数是什么
|
|
|
- * → 改成先讲**面板上这些数是什么、能信什么**,再讲为什么没有工单口径。
|
|
|
+ * 新版只说三件事(措辞纪律沿用票 65 复核结论:不写接口词汇,先讲读数再讲口径):
|
|
|
+ * ① 这些数是**预警自身**的处置流程统计,工单口径只在报警效率出现(两族不混);
|
|
|
+ * ② 已解除率的分母是本窗口预警条数(第一张卡),分子是已解除(CLOSED)条数;
|
|
|
+ * ③ 平均处置时长从预警发布计到已解除。
|
|
|
*/
|
|
|
export const WARNING_EFFICIENCY_GAP =
|
|
|
- '预警不挂工单,所以这里不画工单口径的处置率、工单状态分布与结案时长;' +
|
|
|
- '下方的条数、等级分布与发生趋势都是本窗口内预警自身的真实读数。' +
|
|
|
- '预警的处置流程统计(如「预警→处置动作」流转)后端尚未提供,已在需求池登记,上线后此处会自动换成预警自身的口径'
|
|
|
+ '预警不挂工单,工单口径的处置率与时长只在报警效率出现;' +
|
|
|
+ '这里的条数、已解除率、平均处置时长与预警状态分布都是本窗口内预警自身的处置流程统计。' +
|
|
|
+ '已解除率=已解除的预警 ÷ 本窗口预警条数(分母即第一张卡的条数),平均处置时长从预警发布计到已解除'
|
|
|
+
|
|
|
+/**
|
|
|
+ * 已解除率 → 界面读数(票 78,预警族)。
|
|
|
+ *
|
|
|
+ * 与 `handlingRateView` 同一条判据的预警族实例(两处判据一致,只是读的字段不同):
|
|
|
+ * · 聚合缺失(旧载荷/报警族)→ `—`:我们**不知道**,不冒充任何读数;
|
|
|
+ * · `state=EMPTY` 或分母 0(后端 reason=NO_WARNINGS)→ `不适用`:
|
|
|
+ * 那个 "0.0000" 是「没有分母」的占位,照画就是「已解除率 0.0%」的假读数;
|
|
|
+ * · 其余 → 百分比(后端给 0–1 四位小数,与报警族处置率同一种读法)。
|
|
|
+ */
|
|
|
+export function warningResolveRateView(current) {
|
|
|
+ const row = (current && current.warningEfficiency) || null
|
|
|
+ if (!row) return '—'
|
|
|
+ if (row.state === 'EMPTY' || (typeof row.denominator === 'number' && row.denominator === 0)) return '不适用'
|
|
|
+ if (row.value === null || row.value === undefined) return '—'
|
|
|
+ return `${(row.value * 100).toFixed(1)}%`
|
|
|
+}
|
|
|
+
|
|
|
+/** 已解除率不适用时的说明(挂在 KPI 的 hint 上;缺失数据不给编造的解释)。 */
|
|
|
+export function warningResolveRateHint(current) {
|
|
|
+ const row = (current && current.warningEfficiency) || null
|
|
|
+ if (row && (row.state === 'EMPTY' || (typeof row.denominator === 'number' && row.denominator === 0))) {
|
|
|
+ return '本窗口没有预警,已解除率不适用'
|
|
|
+ }
|
|
|
+ return ''
|
|
|
+}
|
|
|
|
|
|
/**
|
|
|
- * 预警族 KPI:预警自身可证实的读数(票 65)。
|
|
|
- * 「已关联/未关联工单」是**真实计数**(预警族实测 0 / 全部),不是 0% 假读数。
|
|
|
+ * 平均处置时长 → 界面读数(票 78,预警族)。
|
|
|
+ * `state=EMPTY`(reason=NO_CLOSED_WARNINGS,窗口内没有已解除的预警)与缺失都画占位符
|
|
|
+ * —— 与报警族「平均关闭时长」的 EMPTY 处理(`closeDuration`)同一条纪律:不画 0 小时冒充。
|
|
|
+ */
|
|
|
+export function warningDurationView(current) {
|
|
|
+ const row = (current && current.warningDuration) || null
|
|
|
+ if (!row || row.state === 'EMPTY') return '—'
|
|
|
+ return formatDuration(row.value, row.unit)
|
|
|
+}
|
|
|
+
|
|
|
+/** 平均处置时长无样本时的说明(占位符旁边说清「为什么没有数」)。 */
|
|
|
+export function warningDurationHint(current) {
|
|
|
+ const row = (current && current.warningDuration) || null
|
|
|
+ return row && row.state === 'EMPTY' ? '窗口内还没有已解除的预警,暂无时长样本' : ''
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 预警族 KPI:预警自身的处置流程读数(票 78;票 65 立的三卡已随票 74 契约升级)。
|
|
|
+ * 三张卡=条数 / 已解除率 / 平均处置时长,读 `summary.warning*` 新字段;
|
|
|
+ * 「已关联/未关联工单」退到窗口读数(真实计数仍在,只是不占 KPI 位 —— 见 `warningRangeNote`)。
|
|
|
* 读数缺失走 `valueText` 的占位符(票 22/42 纪律:不把「没有」画成 0)。
|
|
|
*/
|
|
|
function warningEfficiencyKpis(current) {
|
|
|
return kpiBlock([
|
|
|
{ key: 'warningTotal', label: '预警条数', value: valueText(current.totalEvents), unit: '条' },
|
|
|
- { key: 'warningLinked', label: '已关联工单', value: valueText(current.linkedWorkOrderCount), unit: '条', hint: '预警不挂工单:该读数是事件回联到工单的实况' },
|
|
|
- { key: 'warningMissing', label: '未关联工单', value: valueText(current.missingWorkOrderCount), unit: '条', hint: '预警不挂工单:没有回联工单的预警条数' },
|
|
|
+ { key: 'warningResolvedRate', label: '已解除率', value: warningResolveRateView(current), hint: warningResolveRateHint(current) },
|
|
|
+ { key: 'warningDuration', label: '平均处置时长', value: warningDurationView(current), hint: warningDurationHint(current) },
|
|
|
])
|
|
|
}
|
|
|
|
|
|
@@ -398,12 +533,19 @@ export function responseEfficiencyPanel(data, section, extra) {
|
|
|
const trend = historyTrendRows(current.eventTrend || [])
|
|
|
return sectionView('responseEfficiency', section, [
|
|
|
warningEfficiencyKpis(current),
|
|
|
+ // 票 78:预警状态分布(WarningWorkflow 六态 + 脏状态的「未知」桶)。
|
|
|
+ // 行的中文名由后端 `stateName` 下发,前端不另造状态词;字段缺失(旧载荷/
|
|
|
+ // 报警族)不画假块 —— 缺失不画假 0 的同一纪律。全 0 也是真实分布(缺桶计 0)。
|
|
|
+ ...(Array.isArray(current.warningStates) && current.warningStates.length
|
|
|
+ ? [barsBlock('预警状态分布', current.warningStates)]
|
|
|
+ : []),
|
|
|
barsBlock('预警等级分布', current.levelDistribution || []),
|
|
|
trendBlock(trend.length > 48 ? '预警发生趋势 · 每日合计' : '预警发生趋势', trend, { chart: 'line' }),
|
|
|
], {
|
|
|
label: responseEfficiencyTitle(EVENT_FAMILY.WARNING),
|
|
|
range: current.range,
|
|
|
- // 口径说明(走票 56 的折叠摘要,`data-panel-note`):说清「预警不挂工单」与契约缺口。
|
|
|
+ // 口径说明(走票 56 的折叠摘要,`data-panel-note`):说清「预警不挂工单」
|
|
|
+ // 与这些数是预警自身的处置流程统计、已解除率的分母(票 78 改写)。
|
|
|
note: WARNING_EFFICIENCY_GAP,
|
|
|
rangeNote: warningRangeNote(current),
|
|
|
})
|
|
|
@@ -457,26 +599,25 @@ function workOrderListBlock(extra) {
|
|
|
const orders = (data && data.orders) || []
|
|
|
const paging = (data && data.pagination) || null
|
|
|
|
|
|
- // 票 67:工单列表走全量工单端点,它**不收行业入参**(实测:非法行业也 200、响应逐字节相同),
|
|
|
- // 所以切行业时上方「工单状态分布」会变、本列表不变。不说明就是一条看不出来的假象
|
|
|
- // (同一屏两个数各说各话,正是维护者反馈第 8 条抱怨的形态)。措辞归 domain/filterCaliber.js。
|
|
|
- const industryNote = industryGap('workOrderList')
|
|
|
- const pageNote = (text) => [text, industryNote].filter(Boolean).join(' · ')
|
|
|
+ // 票 89:片区与行业两轴缺口均已关闭 —— `/work-orders` 自票 77 收 `industry`、
|
|
|
+ // 部署实测后确认也收 `district`(非法值 400、district=0/1 读数不同),本列表与上方
|
|
|
+ // 「工单状态分布」现在按同一批工单收窄,不再需要块级缺口说明。
|
|
|
+ const WORK_ORDER_BASIS = '按创建时间落在此窗口,与上方「工单状态分布」是同一批工单'
|
|
|
|
|
|
if (!loaded) {
|
|
|
- return listBlock('当前工单列表', [], '正在取数…', { pageNote: pageNote('按创建时间落在此窗口,与上方「工单状态分布」是同一批工单') })
|
|
|
+ return listBlock('当前工单列表', [], '正在取数…', { pageNote: WORK_ORDER_BASIS })
|
|
|
}
|
|
|
if (section.state !== 'success' && section.state !== 'stale' && orders.length === 0) {
|
|
|
// 失败/不可用:明确说没取到,不画成「0 张工单」。
|
|
|
return listBlock('当前工单列表', [], '工单列表未取到(其余读数不受影响)', {
|
|
|
- pageNote: pageNote('按创建时间落在此窗口,与上方「工单状态分布」是同一批工单'),
|
|
|
+ pageNote: WORK_ORDER_BASIS,
|
|
|
})
|
|
|
}
|
|
|
return listBlock('当前工单列表', workOrderCardRows(orders), '当前没有工单', {
|
|
|
serverPage: paging ? { page: paging.page, pageCount: paging.totalPages, total: paging.total } : undefined,
|
|
|
- pageNote: pageNote(paging
|
|
|
+ pageNote: paging
|
|
|
? `本窗口内创建的工单共 ${paging.total} 张 · 与上方「工单状态分布」是同一批工单(均按创建时间落在所选范围)`
|
|
|
- : '按创建时间落在此窗口,与上方「工单状态分布」是同一批工单'),
|
|
|
+ : WORK_ORDER_BASIS,
|
|
|
})
|
|
|
}
|
|
|
|
|
|
@@ -553,33 +694,74 @@ export const ALARM_EFFICIENCY_BASIS =
|
|
|
'报警以工单为处置手段:处置率=已结案工单 ÷ 可计入工单(状态 0–6,排除已取消),单位为工单张数'
|
|
|
|
|
|
/**
|
|
|
- * 预警族的窗口读数(票 65):条数与工单关联实况都取**本窗口**的计数。
|
|
|
- * 后端未给任何一项时回空串(不编造「0 条」——那是在说一件没被告知的事)。
|
|
|
+ * 预警族的窗口读数(票 65 立法;票 78 扩到处置流程读数):
|
|
|
+ * 条数(即已解除率的分母)、已解除条数(分子)、时长样本数与工单关联实况都取**本窗口**计数。
|
|
|
+ * 后端未给任何一项时回空串(不编造「0 条」——那是在说一件没被告知的事);
|
|
|
+ * EMPTY 的聚合不给读数(「本窗口没有预警」已经由 KPI 的 hint 说清,不在这里重复半句)。
|
|
|
* 等级分布来自**已返回的事件行**,分页未取全时如实说明分母不全(不拿半份冒充全量)。
|
|
|
*/
|
|
|
function warningRangeNote(current) {
|
|
|
const parts = []
|
|
|
if (numberish(current.totalEvents) !== null) parts.push(`本窗口预警 ${current.totalEvents} 条`)
|
|
|
+ const efficiency = current.warningEfficiency
|
|
|
+ if (efficiency && efficiency.state !== 'EMPTY' && numberish(efficiency.numerator) !== null) {
|
|
|
+ parts.push(`已解除 ${efficiency.numerator} 条`)
|
|
|
+ }
|
|
|
+ const duration = current.warningDuration
|
|
|
+ if (duration && duration.state !== 'EMPTY' && numberish(duration.sampleCount) !== null && duration.sampleCount > 0) {
|
|
|
+ parts.push(`时长样本 ${duration.sampleCount} 条`)
|
|
|
+ }
|
|
|
if (numberish(current.linkedWorkOrderCount) !== null) parts.push(`已关联工单 ${current.linkedWorkOrderCount} 条`)
|
|
|
if (numberish(current.missingWorkOrderCount) !== null) parts.push(`未关联工单 ${current.missingWorkOrderCount} 条`)
|
|
|
if (current.levelDistributionComplete === false) parts.push('等级分布仅统计已返回的事件页,非全量')
|
|
|
return parts.join(' · ')
|
|
|
}
|
|
|
|
|
|
-export function eventAnalysisPanel(data, section) {
|
|
|
+/**
|
|
|
+ * 预警分析面板的**「事件」口径说明**(票 66)。
|
|
|
+ *
|
|
|
+ * 维护者 2026-10-19 反馈第 7 条的第二个问题:面板上同时出现「事件类型」「事件等级」「历史事件」
|
|
|
+ * 「重点识别」,但界面上从没说过「事件」在预警/报警两族下分别指什么、与「预警」「报警」
|
|
|
+ * 是什么关系,值班人员只能猜。这里给一句话口径。
|
|
|
+ *
|
|
|
+ * 词一律走 `eventListSwitchView`(预警/报警的**唯一**词表,票 13 立的),本函数不写字面量:
|
|
|
+ * 切族时说明文字必然跟着换,不会出现「说明说预警、数据是报警」。
|
|
|
+ */
|
|
|
+export function eventAnalysisBasis(family) {
|
|
|
+ const view = eventListSwitchView(family)
|
|
|
+ const other = eventListSwitchView(view.key === EVENT_FAMILY.ALARM ? EVENT_FAMILY.WARNING : EVENT_FAMILY.ALARM)
|
|
|
+ return `「事件」是${view.label}与${other.label}两类告警的合称:本面板只统计当前选中的「${view.label}」,` +
|
|
|
+ `上面的「事件类型」「事件等级」也都只指这些${view.label};` +
|
|
|
+ '报警以工单为处置手段、预警不挂工单,工单是处置记录,本身不算事件'
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 预警分析面板(票 19;票 66 加翻译层与「事件」口径说明)。
|
|
|
+ *
|
|
|
+ * @param {object|null} data `toEventAnalysis()` 的领域对象(label 已是中文)
|
|
|
+ * @param {object} section 分区
|
|
|
+ * @param {{mode?: string}} [extra] 当前切换族(与效率面板同一接缝)
|
|
|
+ */
|
|
|
+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)
|
|
|
return sectionView('eventAnalysis', section, [
|
|
|
// 2026-10-13 维护者反馈:条形图四连排视觉疲劳——类别少的构成改环形图(行业=行业色、
|
|
|
// 类型=图表色序),等级/片区保留条形(条形适合作排名比较)。行业行保留联动。
|
|
|
// 票 37 走查扩展:类型/行业/片区是排名类别按计数降序(并列保序);
|
|
|
// 事件等级是**固定序**(1–4 级语义阶梯),保持后端字典序不打乱。
|
|
|
donutBlock('按事件类型', sortByCountDesc(current.types || [], (row) => row.count)),
|
|
|
+ ...(causes.length ? [donutBlock('按成因', causes)] : []),
|
|
|
donutBlock(
|
|
|
'按行业分布',
|
|
|
sortByCountDesc(
|
|
|
@@ -594,6 +776,8 @@ export function eventAnalysisPanel(data, section) {
|
|
|
], {
|
|
|
label: '预警分析',
|
|
|
range: current.range,
|
|
|
+ // 票 66:一句话说清「事件」指什么(走票 56 的折叠摘要,data-panel-note)。
|
|
|
+ note: eventAnalysisBasis(family),
|
|
|
})
|
|
|
}
|
|
|
|