|
|
@@ -5,7 +5,9 @@
|
|
|
* 预警/报警沿用票 13 的 mode;历史、效率、分析三块不各自维护一套切换状态。
|
|
|
*/
|
|
|
import { fieldReader } from './fields.js'
|
|
|
-import { eventLevelTag, toEventItem } from './eventList.js'
|
|
|
+import { eventLevelTag, eventListSwitchView, toEventItem } from './eventList.js'
|
|
|
+import { valueText, workOrderRows } from './equipmentDetailView.js'
|
|
|
+import { EVENT_FAMILY } from './events.js'
|
|
|
import { WORK_ORDER_STATES } from './workOrder.js'
|
|
|
import {
|
|
|
barsBlock,
|
|
|
@@ -13,6 +15,7 @@ import {
|
|
|
kpiBlock,
|
|
|
listBlock,
|
|
|
numberish,
|
|
|
+ rowsBlock,
|
|
|
sectionHasContent,
|
|
|
sectionPlaceholder,
|
|
|
sortByCountDesc,
|
|
|
@@ -135,15 +138,20 @@ function sectionView(name, section, blocks, extra = {}) {
|
|
|
export function toEventHistory(raw) {
|
|
|
const reader = fieldReader(raw, 'eventHistory')
|
|
|
const source = reader.raw.sections && reader.raw.sections.events ? reader.raw.sections.events.data : reader.raw.data
|
|
|
+ // 票 61:后端新增逐事件行 `events[]`(形状与 events/current 同构);
|
|
|
+ // 旧的 `sections.events.data` / `data` 形状保留兼容(mock 与历史载荷都吃得下)。
|
|
|
+ const rawEvents = Array.isArray(reader.raw.events) ? reader.raw.events : source
|
|
|
return {
|
|
|
mode: reader.optionalStr('mode') || reader.optionalStr('eventFamily'),
|
|
|
range: rangeView(raw),
|
|
|
- events: Array.isArray(source) ? source.map(toEventItem) : [],
|
|
|
+ events: Array.isArray(rawEvents) ? rawEvents.map(toEventItem) : [],
|
|
|
trend: Array.isArray(reader.raw.rows)
|
|
|
? reader.raw.rows.map((row) => ({ bucket: row.timeBucket || '', count: numberish(row.eventCount) || 0 }))
|
|
|
: list(raw, 'trend').map((item) => ({ bucket: String(item && (item.bucket || item.bucketStart || '')), count: numberish(item && item.count) || 0 })),
|
|
|
bucketed: Array.isArray(reader.raw.rows),
|
|
|
pagination: reader.raw.pagination || {},
|
|
|
+ // 票 61 分页口径(单窗服务端 / 多窗已加载合并集合),由 fetchEventHistory 组装。
|
|
|
+ paging: reader.raw.paging || null,
|
|
|
missing: reader.missing,
|
|
|
}
|
|
|
}
|
|
|
@@ -151,6 +159,8 @@ export function toEventHistory(raw) {
|
|
|
/** 效率载荷 → 领域对象。兼容秒数字段和 averageDuration 聚合对象。 */
|
|
|
export function toHandlingEfficiency(raw) {
|
|
|
const reader = fieldReader(raw, 'handlingEfficiency')
|
|
|
+ const familyRaw = reader.optionalStr('eventFamily') || reader.optionalStr('mode')
|
|
|
+ const familyCode = familyRaw === EVENT_FAMILY.ALARM || familyRaw === EVENT_FAMILY.WARNING ? familyRaw : ''
|
|
|
const family = reader.raw.familyMetrics && typeof reader.raw.familyMetrics === 'object' ? reader.raw.familyMetrics : {}
|
|
|
const summary = reader.raw.summary || {}
|
|
|
const handling = summary.handlingEfficiency || family.handlingEfficiency || reader.raw.handlingEfficiency
|
|
|
@@ -164,7 +174,20 @@ export function toHandlingEfficiency(raw) {
|
|
|
label: item.name,
|
|
|
count: numberish(states[item.code] === undefined ? states[String(item.code)] : states[item.code]) || 0,
|
|
|
}))
|
|
|
+ // 票 65:本端点按**事件族**分流。预警族没有工单口径可画(预警不挂工单),
|
|
|
+ // 但有真实的预警口径读数:条数、逐事件等级、逐桶发生量、工单关联实况;
|
|
|
+ // 处置率/工单状态/结案时长属**报警的工单处置口径**,预警模式下不得上屏。
|
|
|
+ const trendRowsRaw = list(raw, 'durationTrend')
|
|
|
+ const events = list(raw, 'events')
|
|
|
+ const totalEvents = numberish(summary.totalEvents === undefined ? reader.raw.totalEvents : summary.totalEvents)
|
|
|
+ // 等级分布只统计**已返回的**事件行(本端点分页,未取全时如实标记,不拿半份冒充全量)。
|
|
|
+ const levelDistribution = [1, 2, 3, 4].map((level) => ({
|
|
|
+ code: String(level),
|
|
|
+ label: eventLevelTag(level).label,
|
|
|
+ count: events.filter((item) => Number(item && item.level) === level).length,
|
|
|
+ }))
|
|
|
return {
|
|
|
+ family: familyCode,
|
|
|
handlingRate: numberish(handlingValue.value === undefined ? (handlingValue.rate === undefined ? reader.raw.handlingRate : handlingValue.rate) : handlingValue.value),
|
|
|
// 票 20:把 `state` 一并带出 —— 「分母为 0」与「处置率真的是 0%」必须分得开
|
|
|
// (后端在无分母时回 value="0.0000" + state="EMPTY")。
|
|
|
@@ -173,14 +196,25 @@ export function toHandlingEfficiency(raw) {
|
|
|
basis: handlingValue.basis || reader.raw.basis || '',
|
|
|
numerator: numberish(handlingValue.numerator === undefined ? reader.raw.numerator : handlingValue.numerator),
|
|
|
denominator: numberish(handlingValue.denominator === undefined ? reader.raw.denominator : handlingValue.denominator),
|
|
|
- totalEvents: numberish(reader.raw.totalEvents),
|
|
|
+ totalEvents,
|
|
|
handledEvents: numberish(reader.raw.handledEvents),
|
|
|
+ // 票 65:事件回联工单的真实计数(预警族实测 linked=0 / missing=全部)。
|
|
|
+ linkedWorkOrderCount: numberish(summary.linkedWorkOrderCount === undefined ? reader.raw.linkedWorkOrderCount : summary.linkedWorkOrderCount),
|
|
|
+ missingWorkOrderCount: numberish(summary.missingWorkOrderCount === undefined ? reader.raw.missingWorkOrderCount : summary.missingWorkOrderCount),
|
|
|
+ levelDistribution,
|
|
|
+ // 等级分布的分母是否等于该窗口的事件总数(分页未取全时界面如实说明)。
|
|
|
+ levelDistributionComplete: totalEvents === null || events.length >= totalEvents,
|
|
|
responseDuration: durationValue(raw, ['averageResponseDuration', 'averageResponseSeconds']) || durationValue(averageDuration, ['responseDuration', 'responseSeconds', 'response']),
|
|
|
closeDuration: summary.averageDuration
|
|
|
? summary.averageDuration.state === 'EMPTY' ? null : summary.averageDuration
|
|
|
: durationValue(raw, ['averageCloseDuration', 'averageCloseSeconds']) || durationValue(averageDuration, ['closeDuration', 'closeSeconds', 'close']),
|
|
|
workOrderStates: distribution,
|
|
|
- trend: list(raw, 'durationTrend').map((item) => ({
|
|
|
+ // 逐桶**事件发生量**(与下面的结案时长趋势是两个口径,预警族只用这一份)。
|
|
|
+ eventTrend: trendRowsRaw.map((item) => ({
|
|
|
+ bucket: String(item && (item.timeBucket || item.bucket || item.bucketStart || '')),
|
|
|
+ count: numberish(item && item.eventCount) || 0,
|
|
|
+ })),
|
|
|
+ trend: trendRowsRaw.map((item) => ({
|
|
|
bucket: String(item && (item.timeBucket || item.bucket || item.bucketStart || '')),
|
|
|
count: item && item.averageDuration
|
|
|
? item.averageDuration.state === 'EMPTY' ? null : numberish(item.averageDuration.value)
|
|
|
@@ -238,24 +272,50 @@ function historyTrendRows(rows = []) {
|
|
|
return trendRows([...days].map(([bucket, count]) => ({ bucket, count })))
|
|
|
}
|
|
|
|
|
|
+/**
|
|
|
+ * 历史事件清单块(票 61)。
|
|
|
+ *
|
|
|
+ * 两种分页形态由 `data.paging` 决定,**不在这里猜**:
|
|
|
+ * · `mode:'server'`(单窗,近 7 天):`events` 就是后端返回的当前页,
|
|
|
+ * 块只带 `serverPage` 元数据,翻页由 store 重新请求(PanelBlocks 只画控件);
|
|
|
+ * · `mode:'merged'`(多窗,近 30/90 天):`events` 是已加载合并集合,
|
|
|
+ * 块声明 `paginate` 由渲染层切窗口;页脚如实说明合并了几个窗口/加载了多少条
|
|
|
+ * (多窗不存在全局页,不伪造页码)。
|
|
|
+ * 没有 `paging` 元数据(旧 mock/旧载荷)时退化为普通清单块,不画翻页控件。
|
|
|
+ */
|
|
|
+function historyEventListBlock(current) {
|
|
|
+ const events = current.events || []
|
|
|
+ const rows = events.map((item) => ({
|
|
|
+ key: item.code,
|
|
|
+ title: item.name || 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 emptyText = '当前时间范围没有历史事件'
|
|
|
+ const paging = current.paging && typeof current.paging === 'object' ? current.paging : null
|
|
|
+ if (!paging) return listBlock('历史事件', rows, emptyText)
|
|
|
+ if (paging.mode === 'server') {
|
|
|
+ return listBlock('历史事件', rows, emptyText, {
|
|
|
+ serverPage: { page: paging.page, pageCount: paging.pageCount, total: paging.total },
|
|
|
+ })
|
|
|
+ }
|
|
|
+ return listBlock('历史事件', rows, emptyText, {
|
|
|
+ paginate: true,
|
|
|
+ // 页码归属键:范围/族一变就复位(条文 5/6;标量键,不接受对象身份)。
|
|
|
+ // 范围键由请求侧带出(后端回显的 range.type 是 TREND_WINDOW,不能当预设范围用)。
|
|
|
+ pageScope: `${current.mode || ''}|${paging.rangeKey || ''}`,
|
|
|
+ pageNote: `已合并 ${paging.windowCount || 0} 个时间窗 · 已加载 ${paging.loaded || 0} 条`,
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
export function historyAnalysisPanel(data, section) {
|
|
|
const current = data || {}
|
|
|
- const events = current.events || []
|
|
|
return sectionView('historyAnalysis', section, [
|
|
|
// 2026-10-13 图表选型:时间趋势用折线。
|
|
|
trendBlock((current.trend || []).length > 48 ? '历史事件趋势 · 每日合计' : '历史事件趋势', historyTrendRows(current.trend || []), { chart: 'line' }),
|
|
|
- listBlock(
|
|
|
- '历史事件',
|
|
|
- events.slice(0, 12).map((item) => ({
|
|
|
- key: item.code,
|
|
|
- title: item.name || item.code,
|
|
|
- tone: eventLevelTag(item.level).tone,
|
|
|
- toneLabel: eventLevelTag(item.level).label,
|
|
|
- meta: [item.code, item.eventTime, item.deviceName, item.status].filter(Boolean),
|
|
|
- points: [],
|
|
|
- })),
|
|
|
- current.bucketed ? '本端点提供历史时间桶,逐条事件请查看当前事件列表' : '当前时间范围没有历史事件',
|
|
|
- ),
|
|
|
+ historyEventListBlock(current),
|
|
|
], {
|
|
|
label: '历史分析',
|
|
|
range: current.range,
|
|
|
@@ -263,25 +323,241 @@ export function historyAnalysisPanel(data, section) {
|
|
|
})
|
|
|
}
|
|
|
|
|
|
-export function responseEfficiencyPanel(data, section) {
|
|
|
+/**
|
|
|
+ * 预警效率面板的**标题**(票 65):随预警/报警切换一起换,词表复用
|
|
|
+ * `eventListSwitchView` 的单一词条(「预警」/「报警」),不新增第二张表。
|
|
|
+ * 外壳(面板外框标题)与本面板的内部标题都读它,两处由构造一致。
|
|
|
+ */
|
|
|
+export function responseEfficiencyTitle(mode) {
|
|
|
+ return `${eventListSwitchView(mode).label}效率`
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 预警效率面板的**口径说明**(票 65,预警族)。
|
|
|
+ *
|
|
|
+ * 背景(2026-10-21 复核取证,维护者要求「确认预警数据是否正确」):
|
|
|
+ * 后端 `CockpitHandlingEfficiencyService` 的 `stateCounts` 来自 `scopeWorkOrders(...)`,
|
|
|
+ * 其源码注释原句是「范围内**全部工单**(不要求关联报警/事件)」——即处置率/状态分布/
|
|
|
+ * 结案时长都是**工单**口径,与事件族无关。实测两族同样结构:
|
|
|
+ * 预警族 `linkedWorkOrderCount=0`、八态合计 0。因此**预警族确实没有预警自身的处置流程统计**。
|
|
|
+ *
|
|
|
+ * 这段文字是要给值班人员看的,所以修掉上一版的两处毛病:
|
|
|
+ * ① 「本端点」是接口词汇(项目纪律:接口词汇不上大屏)→ 改成说「后端/当前数据」;
|
|
|
+ * ② 前半句先讲「我们没有字段」,读者要读两遍才知道面板上的数是什么
|
|
|
+ * → 改成先讲**面板上这些数是什么、能信什么**,再讲为什么没有工单口径。
|
|
|
+ */
|
|
|
+export const WARNING_EFFICIENCY_GAP =
|
|
|
+ '预警不挂工单,所以这里不画工单口径的处置率、工单状态分布与结案时长;' +
|
|
|
+ '下方的条数、等级分布与发生趋势都是本窗口内预警自身的真实读数。' +
|
|
|
+ '预警的处置流程统计(如「预警→处置动作」流转)后端尚未提供,已在需求池登记,上线后此处会自动换成预警自身的口径'
|
|
|
+
|
|
|
+/**
|
|
|
+ * 预警族 KPI:预警自身可证实的读数(票 65)。
|
|
|
+ * 「已关联/未关联工单」是**真实计数**(预警族实测 0 / 全部),不是 0% 假读数。
|
|
|
+ * 读数缺失走 `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: '预警不挂工单:没有回联工单的预警条数' },
|
|
|
+ ])
|
|
|
+}
|
|
|
+
|
|
|
+/** 报警族 KPI:工单口径(工单是报警的处置手段 —— 票 41/42/52 既有裁定,本票不动)。 */
|
|
|
+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']) },
|
|
|
+ ])
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 响应效率(「预警效率」)面板(票 19;**票 65 两族语义分流**)。
|
|
|
+ *
|
|
|
+ * 预警族与报警族的处置对象根本不同:**工单是报警的处置手段**(票 41/42/52),
|
|
|
+ * 而预警不挂工单。所以同一个端点返回的工单口径字段只在报警模式下有意义;
|
|
|
+ * 预警模式下照画就是「用一个不存在的对象统计预警」(2026-10-19 维护者反馈第 6 条)。
|
|
|
+ *
|
|
|
+ * `extra.mode` 是当前切换族(外壳从 `eventStore.mode` 传入);`data.family` 是后端回显,
|
|
|
+ * 两者都有时以**回显**为准(那份数据确实是哪一族就以哪一族画)。都没有时按报警口径
|
|
|
+ * (工单口径是既有形态,不因本票对旧调用点改造)。
|
|
|
+ *
|
|
|
+ * @param {object|null} data `toHandlingEfficiency()` 的领域对象
|
|
|
+ * @param {object} section 分区
|
|
|
+ * @param {{mode?: string}} [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),
|
|
|
+ barsBlock('预警等级分布', current.levelDistribution || []),
|
|
|
+ trendBlock(trend.length > 48 ? '预警发生趋势 · 每日合计' : '预警发生趋势', trend, { chart: 'line' }),
|
|
|
+ ], {
|
|
|
+ label: responseEfficiencyTitle(EVENT_FAMILY.WARNING),
|
|
|
+ range: current.range,
|
|
|
+ // 口径说明(走票 56 的折叠摘要,`data-panel-note`):说清「预警不挂工单」与契约缺口。
|
|
|
+ note: WARNING_EFFICIENCY_GAP,
|
|
|
+ rangeNote: warningRangeNote(current),
|
|
|
+ })
|
|
|
+ }
|
|
|
return sectionView('responseEfficiency', section, [
|
|
|
- kpiBlock([
|
|
|
- { key: 'handlingRate', label: '处置率', value: handlingRateView(current), hint: handlingRateHint(current) },
|
|
|
- { key: 'responseDuration', label: '平均响应时长', value: durationView(current, ['responseDuration']) },
|
|
|
- { key: 'closeDuration', label: '平均关闭时长', value: durationView(current, ['closeDuration']) },
|
|
|
- ]),
|
|
|
+ alarmEfficiencyKpis(current),
|
|
|
// 2026-10-13 图表选型:工单状态是多类别占比 → 百分比堆叠条(饼图超过 5 类不再适合)。
|
|
|
stackedBlock('工单状态分布', current.workOrderStates || []),
|
|
|
// 时长趋势仍是时间序列 → 折线。
|
|
|
trendBlock('平均结案时长趋势', trendRows((current.trend || []).filter((row) => row.count !== null)), { chart: 'line' }),
|
|
|
+ // 票 65 追加:报警模式下直接列出当前工单(卡片,可点开详情)。
|
|
|
+ // 放在图之后:读者先看结论(处置率/分布),再看逐张明细。
|
|
|
+ workOrderListBlock(extra),
|
|
|
], {
|
|
|
- label: '预警效率',
|
|
|
+ label: responseEfficiencyTitle(EVENT_FAMILY.ALARM),
|
|
|
range: current.range,
|
|
|
- rangeNote: current.denominator === null || current.denominator === undefined ? '' : `可计入工单 ${current.denominator} 张 · 已结案 ${current.numerator === null ? '—' : current.numerator} 张`,
|
|
|
+ // 报警族口径说明:工单是报警的处置手段(与预警族那句并列,切换时一起换)。
|
|
|
+ note: ALARM_EFFICIENCY_BASIS,
|
|
|
+ rangeNote: current.rangeNote === undefined
|
|
|
+ ? (current.denominator === null || current.denominator === undefined
|
|
|
+ ? ''
|
|
|
+ : `可计入工单 ${current.denominator} 张 · 已结案 ${current.numerator === null ? '—' : current.numerator} 张`)
|
|
|
+ : current.rangeNote,
|
|
|
})
|
|
|
}
|
|
|
|
|
|
+/**
|
|
|
+ * 报警族的「当前工单列表」块(票 65)。
|
|
|
+ *
|
|
|
+ * **数据来自另一个端点**(`GET /api/cockpit/v1/work-orders`),不是 `handling-efficiency`:
|
|
|
+ * 后者只给工单**状态计数**、不返回工单实体(实测报警族八态合计 0,但设备维度能查到 7 张工单)。
|
|
|
+ * 所以这个块的行来自 `extra.workOrderList`(store 单独取一次)。
|
|
|
+ *
|
|
|
+ * **两处口径必须是同一批工单**(2026-10-22 维护者反馈「列表没按时间筛」后对齐):
|
|
|
+ * 上方「工单状态分布」的分母来自 `CockpitHandlingEfficiencyService.scopeWorkOrders(...)`,
|
|
|
+ * 那里按 `create_time` 落在窗口内;因此本列表也按 `create_time` + 同一时间窗过滤
|
|
|
+ * (window 由 store 从 `activeConditions()` 传下去)。这样两处统计的是同一批工单,
|
|
|
+ * 不会再出现「分布说 0 张、列表列出 7 张」的自相矛盾。
|
|
|
+ *
|
|
|
+ * 三条如实处理:
|
|
|
+ * · 还没取到 → 不给假空块,`emptyText` 说「正在取数」;
|
|
|
+ * · 取到但是 0 张 → 说「当前没有工单」(这是真实业务结论),不是「加载失败」;
|
|
|
+ * · 取失败 → 说「工单列表未取到」,与「确实没有」是两句不同的话(票 11/18 分区纪律)。
|
|
|
+ *
|
|
|
+ * @param {{workOrderList?: object}} [extra] store 传下来的工单列表分区(`sections.workOrderList`)
|
|
|
+ */
|
|
|
+function workOrderListBlock(extra) {
|
|
|
+ const section = (extra && extra.workOrderList) || null
|
|
|
+ const loaded = !!section && section.loaded === true
|
|
|
+ const data = (section && section.data) || null
|
|
|
+ const orders = (data && data.orders) || []
|
|
|
+ const paging = (data && data.pagination) || null
|
|
|
+
|
|
|
+ if (!loaded) {
|
|
|
+ return listBlock('当前工单列表', [], '正在取数…', { pageNote: '按创建时间落在此窗口,与上方「工单状态分布」是同一批工单' })
|
|
|
+ }
|
|
|
+ if (section.state !== 'success' && section.state !== 'stale' && orders.length === 0) {
|
|
|
+ // 失败/不可用:明确说没取到,不画成「0 张工单」。
|
|
|
+ return listBlock('当前工单列表', [], '工单列表未取到(其余读数不受影响)', {
|
|
|
+ pageNote: '按创建时间落在此窗口,与上方「工单状态分布」是同一批工单',
|
|
|
+ })
|
|
|
+ }
|
|
|
+ return listBlock('当前工单列表', workOrderCardRows(orders), '当前没有工单', {
|
|
|
+ serverPage: paging ? { page: paging.page, pageCount: paging.totalPages, total: paging.total } : undefined,
|
|
|
+ pageNote: paging
|
|
|
+ ? `本窗口内创建的工单共 ${paging.total} 张 · 与上方「工单状态分布」是同一批工单(均按创建时间落在所选范围)`
|
|
|
+ : '按创建时间落在此窗口,与上方「工单状态分布」是同一批工单',
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 工单 → 列表卡片行(票 65 精简形态)。
|
|
|
+ *
|
|
|
+ * 字段选择只取「一眼判断该不该点开」的信息:工单号、状态、设备、描述、创建时间。
|
|
|
+ * 行字段名**贴着共享渲染层已认的形状**(`code`/`name`/`tone`/`toneLabel`/`meta`):
|
|
|
+ * `PanelBlocks` 的 list 块见到 `row.code` 就把整张卡做成可点(点击 emit `open-card`),
|
|
|
+ * 见到 `tone`+`toneLabel` 就画状态标签 —— 所以这里不再发明第二套卡片协议。
|
|
|
+ *
|
|
|
+ * 文本一律复用 `equipmentDetailView.workOrderRows`(单一词表:状态字典 0–7、
|
|
|
+ * 关联文本、时间短读法都在那儿定过一次),本函数只做**卡片需要的再包装**。
|
|
|
+ *
|
|
|
+ * @param {Array} orders `toWorkOrder()` 的领域对象数组
|
|
|
+ */
|
|
|
+export function workOrderCardRows(orders) {
|
|
|
+ const rows = workOrderRows(orders)
|
|
|
+ return rows.map((row) => ({
|
|
|
+ key: row.code,
|
|
|
+ // `code` 是渲染层的可点契约(点开走票 12 的工单时间线)。
|
|
|
+ code: row.code,
|
|
|
+ // 卡片抬头就是工单号:值班人员按编号对工单,不按故障描述认。
|
|
|
+ title: row.code,
|
|
|
+ name: row.code,
|
|
|
+ // 色键必带文字(票 06:标签不能只靠颜色)——文案取自 workOrder.js 的词表。
|
|
|
+ tone: workOrderStateTone(row.stateCodeNumber),
|
|
|
+ toneLabel: row.stateName,
|
|
|
+ stateCode: row.stateCodeNumber,
|
|
|
+ stateName: row.stateName,
|
|
|
+ // 副信息行:来源 / 创建时间 / 关联实况(都有才画,缺的自动不占位)。
|
|
|
+ meta: [
|
|
|
+ row.typeName,
|
|
|
+ row.createTime ? `创建 ${row.createTime}` : '',
|
|
|
+ row.associationText,
|
|
|
+ ].filter(Boolean),
|
|
|
+ // 描述可能很长(实测有「监测报警,当前值:14.60(阈值:-~5),请现场核实处理」):
|
|
|
+ // 列表里由 EllipsisText 省略,完整值在时间线弹窗里看 —— 卡片不为一段长文本撑高整块。
|
|
|
+ description: row.description,
|
|
|
+ reason: row.description,
|
|
|
+ }))
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 工单状态 → `StatusTag` 的色键(票 65 卡片用)。
|
|
|
+ *
|
|
|
+ * **不登记第二张状态词表**:词(待派单…)一律走 `workOrder.js` 的 `WORK_ORDER_STATES`,
|
|
|
+ * 这里只回答「这一态借哪一枚既有色键上色」。
|
|
|
+ *
|
|
|
+ * 为什么映射到既有键而不是新增键:`StatusTag` 的 tone 是**封闭词表**
|
|
|
+ * (`domain/statusTones.js` 的 `TONES`,未知 tone 会退回中性灰),
|
|
|
+ * 新增 `state-closed` 之类必须同时改那张表与样式,而那属于视觉批次;
|
|
|
+ * 本票只需「一眼看出这张工单是等着办、在办还是已了」,借用四档语义色即可:
|
|
|
+ * · 待派单/待接单 → `lv1`(最需立刻响应,借一级告警色);
|
|
|
+ * · 已接单/处理中/待验收 → `lv3`(在办,借三级信息色);
|
|
|
+ * · 延期中 → `lv2`(旁路,需要盯);
|
|
|
+ * · 已结案 → `lv4`(了结,借正常色);
|
|
|
+ * · 已取消/认不出 → `neutral`(中性,不暗示任何态度)。
|
|
|
+ */
|
|
|
+export function workOrderStateTone(stateCode) {
|
|
|
+ if (stateCode === null || stateCode === undefined) return 'neutral'
|
|
|
+ const state = WORK_ORDER_STATES.find((item) => item.code === stateCode)
|
|
|
+ if (!state) return 'neutral'
|
|
|
+ if (state.code === 0 || state.code === 1) return 'lv1'
|
|
|
+ if (state.code === 6) return 'lv2'
|
|
|
+ if (state.terminal && state.code === 5) return 'lv4'
|
|
|
+ if (state.terminal) return 'neutral'
|
|
|
+ return 'lv3'
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+/** 报警族的口径说明(与预警族那句成对:切换时口径说明一起换)。 */
|
|
|
+export const ALARM_EFFICIENCY_BASIS =
|
|
|
+ '报警以工单为处置手段:处置率=已结案工单 ÷ 可计入工单(状态 0–6,排除已取消),单位为工单张数'
|
|
|
+
|
|
|
+/**
|
|
|
+ * 预警族的窗口读数(票 65):条数与工单关联实况都取**本窗口**的计数。
|
|
|
+ * 后端未给任何一项时回空串(不编造「0 条」——那是在说一件没被告知的事)。
|
|
|
+ * 等级分布来自**已返回的事件行**,分页未取全时如实说明分母不全(不拿半份冒充全量)。
|
|
|
+ */
|
|
|
+function warningRangeNote(current) {
|
|
|
+ const parts = []
|
|
|
+ if (numberish(current.totalEvents) !== null) parts.push(`本窗口预警 ${current.totalEvents} 条`)
|
|
|
+ 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) {
|
|
|
const current = data || {}
|
|
|
const highlights = (current.highlights || []).map((item, index) => ({
|