Procházet zdrojové kódy

feat(cockpit): 设备详情事件时间、状态高亮与预警描述

维护者 2026-11-13 四项要求:

1. 时间统一 yyyy-MM-dd HH:mm:ss:新增 formatEventTime(列表与详情共用),
   缺秒时回落原短格式,不补零伪造时间。
2. 去掉笼统的「未解除」徽标,改为高亮真实状态并按状态分色:
   待接收/已接收黄、处置中蓝、已处置紫、已解除绿。
   色键单列 STATUS_DISPLAY_TONE,不改动服务其它面板的语义 tone;
   紫色无既有语义色键,由 tokens.css 新增 --status-handled 单源给出。
3. 详情「来源」改为「预警描述 / 报警描述」,内容为简单描述 + 实际值 + 阈值,
   例:化学需氧量(COD)预警接近上限 / 实际值:14.6 mg/L;阈值:上限 20 mg/L。
   缺值缺阈值就少说一段,不编造数字。
4. 详情等级文字颜色与等级色一致(detailRow 增加 tone,按 data-tone 上色)。

回归:新增 t120(四类断言 + 模板接线);t115/t116/t117/t118/t119/t11 与视频套件全绿。
ChenJZ před 1 hodinou
rodič
revize
b75c45f175

+ 30 - 4
src/modules/cockpit/components/EquipmentDetailSections.vue

@@ -206,9 +206,11 @@
                             <span class="equipment-section__item-name equipment-section__mono" data-event-title>{{ event.title }}</span>
                             <span v-if="event.description" class="equipment-section__item-desc" data-event-desc>{{ event.description }}</span>
                             <span class="equipment-section__meta" data-event-meta>
-                                <span data-event-level>{{ event.levelText }}</span>
-                                <span v-if="event.status" data-event-status>{{ event.status }}</span>
-                                <span v-if="event.active" class="equipment-section__active-badge" data-active-badge>{{ event.activeLabel }}</span>
+                                <span data-event-level :data-tone="event.levelTone">{{ event.levelText }}</span>
+                                <!-- 维护者 2026-11-13:不再用笼统的「未解除」徽标,直接高亮真实状态。 -->
+                                <span v-if="event.status" class="equipment-section__status-badge"
+                                    :data-event-status="event.status" :data-status-tone="event.statusTone"
+                                    :data-tone="event.statusTone">{{ event.status }}</span>
                                 <span v-if="event.timeText" data-event-time>{{ event.timeText }}</span>
                             </span>
                             <span class="equipment-section__item-more" aria-hidden="true" data-event-more>点击查看详情 ›</span>
@@ -216,7 +218,7 @@
                         <dl class="equipment-section__detail" data-history-detail>
                             <div v-for="row in eventDetailRows(event)" :key="row.label" class="equipment-section__detail-row" :data-detail-row="row.label">
                                 <dt>{{ row.label }}</dt>
-                                <dd>{{ row.value }}</dd>
+                                <dd :data-tone="row.tone || undefined" :class="{ 'equipment-section__detail-value--tone': row.tone }">{{ row.value }}</dd>
                             </div>
                         </dl>
                     </details>
@@ -1064,6 +1066,30 @@ function isPlaceholder(text) {
     border-color: transparent;
     background: transparent;
 }
+/* 状态徽标:按预警状态色高亮(待接收/已接收黄、处置中蓝、已处置紫、已解除绿)。 */
+.equipment-section__status-badge {
+    display: inline-flex;
+    align-items: center;
+    padding: 0 calc(6 * var(--u, 1px));
+    border: 1px solid currentcolor;
+    border-radius: 999px;
+    background: color-mix(in srgb, currentcolor 18%, transparent);
+    font-size: var(--fs-caption, 10.5px);
+    line-height: calc(16 * var(--u, 1px));
+    letter-spacing: .04em;
+    white-space: nowrap;
+}
+/* 等级与状态共用的色键映射(色值只在 tokens.css 声明一份)。 */
+[data-tone="lv1"] { color: var(--lv1); }
+[data-tone="lv2"] { color: var(--lv2); }
+[data-tone="lv3"] { color: var(--lv3); }
+[data-tone="lv4"] { color: var(--lv4); }
+[data-tone="handled"] { color: var(--status-handled); }
+[data-tone="neutral"] { color: var(--neutral); }
+.equipment-section__detail-value--tone { font-weight: 600; }
+/* 预警描述允许多行(简单描述一行、实际值/阈值一行)。 */
+.equipment-section__detail-row dd { white-space: pre-line; }
+
 .equipment-section__active-badge {
     display: inline-flex;
     align-items: center;

+ 44 - 8
src/modules/cockpit/domain/equipmentDetailView.js

@@ -34,8 +34,8 @@ import { TIME_LABELS } from './fields.js'
 import { SECTION_STATE, section } from './sectionState.js'
 import { WORK_ORDER_STATES, workOrderStateName } from './workOrder.js'
 import { tracksByOrder } from './workOrderTimeline.js'
-import { eventLevelTag, shortEventTime, eventStatusText, eventTypeText, eventNameText } from './eventList.js'
-import { statusVocabulary } from './eventStatusVocab.js'
+import { eventLevelTag, shortEventTime, formatEventTime, eventStatusText, eventTypeText, eventNameText } from './eventList.js'
+import { statusVocabulary, statusDisplayTone } from './eventStatusVocab.js'
 import { pagedRows } from './pagination.js'
 import { detailEventLevel, collectionCellAssessment, collectionGroupAccent } from './equipmentDetailColors.js'
 
@@ -369,6 +369,34 @@ function describeEvent(event) {
     return type && type !== title ? type : ''
 }
 
+/**
+ * 预警/报警描述(维护者 2026-11-13):
+ * 简单描述(指标 + 接近/超过上下限)+ 实际值 + 阈值,例:
+ *   化学需氧量(COD)预警接近上限
+ *   实际值:14.6 mg/L;阈值:上限 20 mg/L
+ *
+ * 缺值就少说一段,不编造、不补零;什么都没有时回空串(渲染层给占位符)。
+ */
+function eventDescriptionText(event) {
+    const metric = event && event.type ? eventTypeText(event.type) : ''
+    const value = typeof event?.value === 'number' && Number.isFinite(event.value) ? event.value : null
+    const unit = event?.unit || ''
+    const min = typeof event?.thresholdMinValue === 'number' ? event.thresholdMinValue : null
+    const max = typeof event?.thresholdMaxValue === 'number' ? event.thresholdMaxValue : null
+    let direction = ''
+    if (value !== null && max !== null) direction = value >= max ? '超过上限' : '接近上限'
+    else if (value !== null && min !== null) direction = value <= min ? '低于下限' : '接近下限'
+    const head = [metric, direction].filter(Boolean).join('')
+    const detail = []
+    if (value !== null) detail.push(`实际值:${value}${unit ? ' ' + unit : ''}`)
+    if (min !== null || max !== null) {
+        const scope = min !== null && max !== null ? `下限 ${min} / 上限 ${max}`
+            : max !== null ? `上限 ${max}` : `下限 ${min}`
+        detail.push(`阈值:${scope}${unit ? ' ' + unit : ''}`)
+    }
+    return [head, detail.join(';')].filter(Boolean).join('\n')
+}
+
 function detailEventLevelLabel(event) {
     const level = detailEventLevel(event).level
     return level ? `${level} 级` : '未分级'
@@ -394,6 +422,7 @@ export function eventRows(data) {
             // 等级名称取后端字典(alert_level / alarm_level)下发的标签;
             // 缺失时明确说「未提供」,不拿「N 级」冒充字典口径。
             levelText: eventNameText(event.levelName) || detailEventLevelLabel(event),
+            levelTone: level.tone,
             levelLabelSource: eventNameText(event.levelName) ? 'dictionary' : 'fallback',
             activeColor: level.color,
             levelColor: event.levelColor || '',
@@ -404,7 +433,12 @@ export function eventRows(data) {
             levelTag: { tone: levelTag.tone, label: levelTag.label },
             categoryKey: levelNumber === null ? 'level:unknown' : `level:${levelNumber}`,
             categoryLabel: levelNumber === null ? '等级未知' : levelTag.label,
-            timeText: shortEventTime(event.eventTime),
+            // 维护者 2026-11-13:列表与详情统一用 yyyy-MM-dd HH:mm:ss。
+            timeText: formatEventTime(event.eventTime),
+            family: String(event.family || '').toUpperCase(),
+            // 状态徽标高亮色(待接收/已接收黄、处置中蓝、已处置紫、已解除绿)。
+            statusTone: statusDisplayTone(event.family, event.status),
+            descriptionText: eventDescriptionText(event),
             type: eventTypeText(event.type),
             deviceCode: event.deviceCode || '',
             deviceName: event.deviceName || '',
@@ -592,22 +626,24 @@ export function categoryNote(chips, categoryKey) {
  * `keepEmpty` 的项是票 46 范围第 3 条点名要**始终保留**的四件事:
  * 事件时间 / 状态 / 来源 / 关联对象;其余项有值才画(不铺一排 `—` 噪音)。
  */
-function detailRow(label, value, keepEmpty = false) {
+function detailRow(label, value, keepEmpty = false, tone = '') {
     const text = valueText(value)
     if (!keepEmpty && text === DETAIL_PLACEHOLDER) return null
-    return { label, value: text }
+    return { label, value: text, tone }
 }
 
 /** 一条预警/报警 → 展开后的详情行(时间、状态、来源、关联设备都在,缺值给占位符)。 */
 export function eventDetailRows(row) {
     const current = row || {}
     return detailRowList([
-        detailRow(TIME_LABELS.event, current.eventTime, true),
+        detailRow(TIME_LABELS.event, formatEventTime(current.eventTime), true),
         detailRow('状态', current.status, true),
-        detailRow('来源', current.type, true),
+        // 维护者 2026-11-13:原「来源」改为「预警描述 / 报警描述」,内容含简单描述与实际值、阈值。
+        detailRow(current.family === 'ALARM' ? '报警描述' : '预警描述', current.descriptionText, true),
         detailRow('关联设备', current.deviceName || current.deviceCode, true),
         detailRow('编码', current.codeText || '编码未提供'),
-        detailRow('等级', current.levelText),
+        // 等级文字颜色与等级色一致(维护者 2026-11-13 第 4 条)。
+        detailRow('等级', current.levelText, false, current.levelTone),
         detailRow('量值', current.quantity),
         detailRow('位置', current.location),
     ])

+ 15 - 0
src/modules/cockpit/domain/eventList.js

@@ -332,6 +332,21 @@ export function shortEventTime(value) {
     return `${month}-${day} ${hour}:${minute}`
 }
 
+/**
+ * ISO 时间戳 → `yyyy-MM-dd HH:mm:ss`(维护者 2026-11-13:列表与详情统一用完整时间)。
+ *
+ * 解析不了(缺秒/非 ISO)就回原串——不猜、不补零造一个假时间。
+ * 只影响显示;排序仍走原值。
+ */
+export function formatEventTime(value) {
+    const text = typeof value === 'string' ? value.trim() : ''
+    if (!text) return ''
+    const matched = /^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2}):(\d{2})/.exec(text)
+    if (!matched) return shortEventTime(text)
+    const [, year, month, day, hour, minute, second] = matched
+    return `${year}-${month}-${day} ${hour}:${minute}:${second}`
+}
+
 /** 有内容可画的分区状态(过期是有值,内容照画)。 */
 const CONTENT_STATES = [SECTION_STATE.SUCCESS, SECTION_STATE.STALE]
 

+ 30 - 0
src/modules/cockpit/domain/eventStatusVocab.js

@@ -28,6 +28,36 @@
 /** 状态标签的语义色键(沿用 `statusTones.js` 的四档,不新造色)。 */
 const TONE = { PENDING: 'lv2', ACTIVE: 'lv3', DONE: 'lv4', NEUTRAL: 'neutral' }
 
+/**
+ * 列表状态徽标高亮色(维护者 2026-11-13 指定):
+ * 待接收/已接收=黄、处置中=蓝、已处置=紫、已解除=绿。
+ *
+ * 与语义色键 `tone` 分开:`tone` 服务标签点/图例的中性语义,已被其它面板消费;
+ * 这里是「设备详情预警列表」的状态高亮口径,改它不动别处。紫色无既有语义色键,
+ * 由 tokens.css 的 --status-handled 单源给出。
+ */
+export const STATUS_DISPLAY_TONE = {
+    WARNING: {
+        PENDING: 'lv2',
+        RECEIVED: 'lv2',
+        PROCESSING: 'lv3',
+        HANDLED: 'handled',
+        CLOSED: 'lv4',
+    },
+    ALARM: {
+        UNHANDLED: 'lv2',
+        HANDLED: 'lv4',
+    },
+}
+
+/** 某族某状态的高亮色键;认不出回中性(不猜成某个真实态的颜色)。 */
+export function statusDisplayTone(family, status) {
+    const key = String(family || '').trim().toUpperCase()
+    const code = String(status || '').trim().toUpperCase()
+    const table = STATUS_DISPLAY_TONE[key]
+    return (table && table[code]) || 'neutral'
+}
+
 /**
  * 预警六态(`EarlyWarningStatusEnum`,码与中文名逐条照抄后端枚举)。
  *

+ 3 - 0
src/modules/cockpit/styles/tokens.css

@@ -75,6 +75,9 @@
     --lv3: #01abea;
     --lv4: #27d17a;
     --neutral: #5680b0;
+    /* 维护者 2026-11-13:预警「已处置」用紫色高亮(四档语义色里没有紫色位)。
+       只服务设备详情事件列表的状态徽标,不并入 lv1–lv4 的优先级语义。 */
+    --status-handled: #a970ff;
     /* 点位状态统计:连接中断、无设备、未知状态、关联缺失各用独立颜色。
        票 45(维护者裁定):「无关联设备」是**附加状态**,色键改**绿色系黄绿**(与正常在线同族、
        与 lv4 正绿区分),且只在设备没有业务异常时替代正常态;业务异常按业务状态色显示。 */