浏览代码

fix(cockpit): 设备详情事件卡片等级取字典、单框与内容去重

维护者 2026-11-12 反馈的三处问题:

1. 预警级别要按字典 alert_level 显示(一级预警…),不再只显示「4 级」。
   领域层改为优先取后端下发的 levelName,缺失时回落「N 级」并标记 levelLabelSource;
   前端不内置字典表(字典是后端可配置数据,抄一份迟早与库不一致)。
   后端事件 DTO 增加 levelName 由后端工作线补齐。
2. 列表边框重叠:原来整块用 inset box-shadow 画框、item-head 又各带 border+背景,
   两层框错位叠加。现在每条只保留一个线框——普通项与高亮项都由整块 border 持有,
   头部不再画框,hover/展开只改背景。
3. 卡片内容重复显示了预警编号:改为标题=预警单号,内容=描述(不与单号重复)、
   等级、状态、时间,右下角提示「点击查看详情」。

回归:新增 t119(字典优先/缺项回落、卡片四要素、单框约束);
t115/t116/t117/t118/t11 与视频套件全部通过。
ChenJZ 5 小时之前
父节点
当前提交
2b5c785255

+ 48 - 19
src/modules/cockpit/components/EquipmentDetailSections.vue

@@ -202,19 +202,16 @@
                         data-history-item
                     >
                         <summary class="equipment-section__item-head">
-                            <span class="equipment-section__item-name">{{ event.name }}</span>
-                            <span class="equipment-section__meta">
+                            <!-- 标题=单号;内容=描述、时间、等级、状态;右下角提示可展开看详情。 -->
+                            <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">{{ event.status }}</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 v-if="event.timeText" data-event-time>{{ event.timeText }}</span>
                             </span>
-                            <span class="equipment-section__meta">
-                                <span v-if="event.timeText">{{ event.timeText }}</span>
-                                <span class="equipment-section__mono">{{ event.codeText }}</span>
-                            </span>
-                            <span class="equipment-section__value" :class="{ 'is-muted': isPlaceholder(event.quantity) }" data-event-value>
-                                {{ event.quantity }}
-                            </span>
+                            <span class="equipment-section__item-more" aria-hidden="true" data-event-more>点击查看详情 ›</span>
                         </summary>
                         <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">
@@ -1029,6 +1026,13 @@ function isPlaceholder(text) {
 */
 .equipment-section__item--collapsible {
     gap: calc(2 * var(--u, 1px));
+    /* 每条数据一个线框:普通项也由整块持有边框,与高亮项同一层,不再出现错位/重叠。 */
+    padding: calc(4 * var(--u, 1px)) calc(6 * var(--u, 1px));
+    border: 1px solid rgba(1, 171, 234, 0.22);
+    border-radius: calc(6 * var(--u, 1px));
+}
+.equipment-section__item--collapsible[open] {
+    border-color: rgba(1, 171, 234, 0.5);
 }
 
 .equipment-section__item--collapsible[open] {
@@ -1036,22 +1040,29 @@ function isPlaceholder(text) {
     padding-bottom: calc(5 * var(--u, 1px));
 }
 
-/* 事件按等级色强调;未分级预警黄、报警红;非终态工单天蓝。 */
+/*
+    事件按等级色强调;未分级预警黄、报警红;非终态工单天蓝。
+
+    2026-11-12 维护者反馈「高亮框和普通框对不上、边框重叠」:原来整块用 inset box-shadow
+    画了一圈,头部又各带一条 border + 背景,两层框叠在一起。现在**每条只保留一个线框**:
+    普通项用整块 border,高亮项把同一个 border 换成等级色,头部不再画框。
+*/
 .equipment-section__item--active {
     padding-left: calc(6 * var(--u, 1px));
-    border-bottom-color: color-mix(in srgb, var(--detail-accent) 65%, transparent);
+    border: 1px solid color-mix(in srgb, var(--detail-accent) 55%, transparent);
     border-radius: calc(6 * var(--u, 1px));
     background: linear-gradient(90deg, color-mix(in srgb, var(--detail-accent) 20%, transparent), rgba(7, 27, 48, .12) 78%);
-    box-shadow: inset 3px 0 0 var(--detail-accent), inset 0 0 0 1px color-mix(in srgb, var(--detail-accent) 30%, transparent), 0 0 12px color-mix(in srgb, var(--detail-accent) 12%, transparent);
+    box-shadow: inset 3px 0 0 var(--detail-accent), 0 0 12px color-mix(in srgb, var(--detail-accent) 12%, transparent);
 }
 .equipment-section__item--active[open] {
-    border-bottom-color: color-mix(in srgb, var(--detail-accent) 78%, transparent);
+    border-color: color-mix(in srgb, var(--detail-accent) 78%, transparent);
     background: linear-gradient(90deg, color-mix(in srgb, var(--detail-accent) 26%, transparent), rgba(7, 27, 48, .16) 82%);
 }
+/* 高亮项的头部不另画框,避免与整块边框形成双层。 */
 .equipment-section__item--active .equipment-section__item-head,
 .equipment-section__item--collapsible.equipment-section__item--active[open] .equipment-section__item-head {
-    border-color: color-mix(in srgb, var(--detail-accent) 26%, transparent);
-    background: color-mix(in srgb, var(--detail-accent) 6%, transparent);
+    border-color: transparent;
+    background: transparent;
 }
 .equipment-section__active-badge {
     display: inline-flex;
@@ -1067,6 +1078,22 @@ function isPlaceholder(text) {
     white-space: nowrap;
 }
 
+.equipment-section__item-desc {
+    color: var(--text-2, #88b7e8);
+    font-size: var(--fs-caption, 10.5px);
+    line-height: 1.5;
+    overflow-wrap: anywhere;
+}
+
+.equipment-section__item-more {
+    justify-self: end;
+    color: var(--muted, #5680b0);
+    font-size: var(--fs-caption, 10.5px);
+    letter-spacing: .02em;
+}
+
+.equipment-section__item-head:hover .equipment-section__item-more { color: var(--cyan-hi, #00b8ff); }
+
 .equipment-section__item-head {
     display: grid;
     gap: 1px;
@@ -1082,11 +1109,13 @@ function isPlaceholder(text) {
     display: none;
 }
 
-.equipment-section__item-head:hover,
-.equipment-section__item--collapsible[open] .equipment-section__item-head {
-    border-color: rgba(1, 171, 234, 0.35);
+/* hover / 展开只改背景,不再给头部加第二条框——整块边框已经是这一行的唯一线框。 */
+.equipment-section__item-head:hover {
     background: rgba(0, 53, 156, 0.28);
 }
+.equipment-section__item--collapsible[open] .equipment-section__item-head {
+    background: rgba(0, 53, 156, 0.22);
+}
 
 .equipment-section__detail {
     display: grid;

+ 25 - 1
src/modules/cockpit/domain/equipmentDetailView.js

@@ -356,6 +356,24 @@ export function missingNote(value) {
  *   · `timeText`:折叠行的短读法(复用 `shortEventTime`,完整 `eventTime` 保留给展开详情);
  *   · `type` / `deviceCode` / `deviceName`:展开详情要保留「来源与关联对象」(票 46 范围第 3 条)。
  */
+/**
+ * 等级文案:后端已回字典标签就用它(如「一级预警」);没回则回落到「N 级」并标记来源。
+ * 不在前端内置一张 alert_level 字典表——字典是后端可配置数据,抄一份迟早与库不一致。
+ */
+/** 预警/报警描述:名称有信息量就用名称,否则用类型;两者都等于单号时不重复。 */
+function describeEvent(event) {
+    const title = String(event.codeText || event.code || '').trim()
+    const name = eventNameText(event.name)
+    if (name && name !== title) return name
+    const type = eventTypeText(event.type)
+    return type && type !== title ? type : ''
+}
+
+function detailEventLevelLabel(event) {
+    const level = detailEventLevel(event).level
+    return level ? `${level} 级` : '未分级'
+}
+
 export function eventRows(data) {
     const list = Array.isArray(data) ? data : []
 
@@ -370,7 +388,13 @@ export function eventRows(data) {
             code: event.code || DETAIL_PLACEHOLDER,
             codeText: event.codeText || '编码未提供',
             name: eventNameText(event.name) || DETAIL_PLACEHOLDER,
-            levelText: levelNumber ? `${levelNumber} 级` : '未分级',
+            // 卡片标题用单号;名称很多时候等于单号,重复显示没有信息量,改为「预告描述」。
+            title: event.codeText || event.code || DETAIL_PLACEHOLDER,
+            description: describeEvent(event),
+            // 等级名称取后端字典(alert_level / alarm_level)下发的标签;
+            // 缺失时明确说「未提供」,不拿「N 级」冒充字典口径。
+            levelText: eventNameText(event.levelName) || detailEventLevelLabel(event),
+            levelLabelSource: eventNameText(event.levelName) ? 'dictionary' : 'fallback',
             activeColor: level.color,
             levelColor: event.levelColor || '',
             status: eventStatusText(event.status, event.family),