Browse Source

feat(cockpit): 工单信息卡片与详情按管理端排版,修报警标题主键

维护者 2026-11-13:

1. 报警列表把内部主键印到了界面上:报警多数没有业务编号(只有 alarmId 哈希),
   卡片标题回落到 alarmId。改为无业务编号时用设备编码,界面不再出现主键。
2. 页签「工单状态」改名「工单信息」。
3. 工单卡片按管理端样式竖向排版:工单号+状态、工单描述、关联设备、设备位置、
   故障时间,右下角「查看详情」。
4. 工单详情按管理端分区:故障描述/优先级/故障发生时间/计划完成时间、设备名称/类型/
   安装位置、处置人/核验人、维修前后附件、延期申请;字号提到正文字号,键值改竖向排布。
   缺值如实写「暂无 / 没有延期信息」,不伪造。

适配器补 faultTime / orderLevelName / 维修前后图片 / 延期申请映射;
后端工单 DTO 补齐这些只读字段由后端工作线完成。

回归:新增 t121;t115/t116/t117/t11(63)/t42(30)/t118/t119/t120 与视频套件全绿;
t11 的页签断言随改名同步更新。
ChenJZ 54 minutes ago
parent
commit
d99ed9b624

+ 52 - 12
src/modules/cockpit/components/EquipmentDetailSections.vue

@@ -256,20 +256,31 @@
                         :data-item-active="order.active ? 'true' : 'false'"
                         data-history-item
                     >
-                        <summary class="equipment-section__item-head">
-                            <span class="equipment-section__item-name">
-                                {{ order.stateName }}
-                                <em v-if="order.typeName"> · {{ order.typeName }}</em>
+                        <summary class="equipment-section__item-head equipment-section__wo-head">
+                            <!-- 维护者 2026-11-13:按管理端工单卡片排版(工单号+状态、描述、关联设备、位置、故障时间、查看详情)。 -->
+                            <span class="equipment-section__wo-title">
+                                <span class="equipment-section__wo-label">工单号</span>
+                                <span class="equipment-section__mono" data-work-order-code>{{ order.title }}</span>
+                                <span class="equipment-section__status-badge" :data-work-order-state-name="order.stateName"
+                                    :data-tone="order.active ? 'lv3' : 'lv4'">{{ order.stateName }}</span>
+                            </span>
+                            <span class="equipment-section__wo-row">
+                                <span class="equipment-section__wo-label">工单描述</span>
+                                <span data-work-order-desc>{{ order.description || '—' }}</span>
                             </span>
-                            <span class="equipment-section__meta">
-                                <span class="equipment-section__mono">{{ order.code }}</span>
-                                <span data-work-order-association>{{ order.associationText }}</span>
-                                <span v-if="order.active" class="equipment-section__active-badge" data-active-badge>{{ order.activeLabel }}</span>
+                            <span class="equipment-section__wo-row">
+                                <span class="equipment-section__wo-label">关联设备</span>
+                                <span data-work-order-device>{{ order.deviceLabel }}</span>
                             </span>
-                            <span class="equipment-section__meta">
-                                <span v-if="order.timeText">创建 {{ order.timeText }}</span>
-                                <span>结案 {{ order.realFinishTime }}</span>
+                            <span class="equipment-section__wo-row">
+                                <span class="equipment-section__wo-label">设备位置</span>
+                                <span data-work-order-location>{{ order.deviceLocation }}</span>
                             </span>
+                            <span class="equipment-section__wo-row">
+                                <span class="equipment-section__wo-label">故障时间</span>
+                                <span data-work-order-fault-time>{{ order.faultTimeText || order.timeText || '—' }}</span>
+                            </span>
+                            <span class="equipment-section__item-more" aria-hidden="true" data-work-order-more>查看详情 ›</span>
                         </summary>
 
                         <!--
@@ -288,7 +299,7 @@
                         <p v-else-if="timeline.hasContent" class="equipment-section__hint" data-wo-timeline-missing>
                             这张工单没有动作记录
                         </p>
-                        <dl class="equipment-section__detail" data-history-detail>
+                        <dl class="equipment-section__detail equipment-section__wo-detail" data-history-detail>
                             <div v-for="row in workOrderDetailRows(order)" :key="row.label" class="equipment-section__detail-row" :data-detail-row="row.label">
                                 <dt>{{ row.label }}</dt>
                                 <dd>{{ row.value }}</dd>
@@ -1104,6 +1115,34 @@ function isPlaceholder(text) {
     white-space: nowrap;
 }
 
+/* 工单卡片:管理端同款竖向排布,字号提到正文级(维护者 2026-11-13 要求加大)。 */
+.equipment-section__wo-head { gap: calc(4 * var(--u, 1px)); }
+.equipment-section__wo-title {
+    display: flex;
+    align-items: center;
+    gap: calc(8 * var(--u, 1px));
+    min-width: 0;
+    padding-bottom: calc(4 * var(--u, 1px));
+    border-bottom: 1px dashed rgba(1, 171, 234, 0.28);
+    font-size: var(--fs-body, 14px);
+}
+.equipment-section__wo-label {
+    flex: none;
+    min-width: calc(64 * var(--u, 1px));
+    color: var(--muted, #5680b0);
+    font-size: var(--fs-body, 14px);
+}
+.equipment-section__wo-row {
+    display: flex;
+    align-items: flex-start;
+    gap: calc(8 * var(--u, 1px));
+    font-size: var(--fs-body, 14px);
+    line-height: 1.6;
+}
+.equipment-section__wo-row > span:last-child { min-width: 0; overflow-wrap: anywhere; }
+/* 工单详情:面板较窄,两列键值改为竖向一行一项,字号与正文一致。 */
+.equipment-section__wo-detail { gap: calc(6 * var(--u, 1px)); }
+
 .equipment-section__item-desc {
     color: var(--text-2, #88b7e8);
     font-size: var(--fs-caption, 10.5px);
@@ -1159,6 +1198,7 @@ function isPlaceholder(text) {
     gap: var(--space-2, 8px);
 }
 
+.equipment-section__wo-detail .equipment-section__detail-row { grid-template-columns: calc(96 * var(--u, 1px)) minmax(0, 1fr); font-size: var(--fs-body, 14px); }
 .equipment-section__detail-row dt {
     color: var(--muted, #5680b0);
     font-size: var(--fs-caption, 10.5px);

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

@@ -172,7 +172,7 @@ export const EQUIPMENT_SECTION_PANELS = [
     { name: 'metrics', label: '最新指标', kind: 'metrics' },
     { name: 'warnings', label: '预警信息', kind: 'events' },
     { name: 'alarms', label: '报警信息', kind: 'events' },
-    { name: 'workOrders', label: '工单状态', kind: 'orders' },
+    { name: 'workOrders', label: '工单信息', kind: 'orders' },
     { name: 'latestImage', label: '设备图片', kind: 'image', mergedInto: 'archive' },
 ]
 
@@ -190,7 +190,7 @@ export const EQUIPMENT_TABS = [
     { name: 'metrics', label: '最新指标', sections: ['metrics'] },
     { name: 'warnings', label: '预警信息', sections: ['warnings'] },
     { name: 'alarms', label: '报警信息', sections: ['alarms'] },
-    { name: 'workOrders', label: '工单状态', sections: ['workOrders'] },
+    { name: 'workOrders', label: '工单信息', sections: ['workOrders'] },
 ]
 
 /** 默认页签(维护者点名「设备档案(默认)」)。 */
@@ -420,8 +420,9 @@ export function eventRows(data) {
             code: event.code || DETAIL_PLACEHOLDER,
             codeText: event.codeText || '编码未提供',
             name: eventNameText(event.name) || DETAIL_PLACEHOLDER,
-            // 卡片标题用单号;名称很多时候等于单号,重复显示没有信息量,改为「预告描述」。
-            title: event.codeText || event.code || DETAIL_PLACEHOLDER,
+            // 卡片标题用业务单号;报警普遍没有业务编号(只有 alarmId 主键哈希),
+            // 那种情况下退回设备编码,绝不把内部主键印到界面上(维护者 2026-11-13)。
+            title: event.codeText || event.deviceCode || event.code || DETAIL_PLACEHOLDER,
             description: describeEvent(event),
             // 等级名称取后端字典(alert_level / alarm_level)下发的标签;
             // 缺失时明确说「未提供」,不拿「N 级」冒充字典口径。
@@ -476,6 +477,20 @@ export function workOrderRows(data) {
             stateName: workOrderStateName(order.stateCode) || (/[\u3400-\u9fff]/u.test(order.stateName || '') ? order.stateName : '状态未知'),
             typeName: order.orderTypeName || '',
             description: order.description || '',
+            // 维护者 2026-11-13:卡片按管理端样式展示(工单号/描述/关联设备/设备位置/故障时间)。
+            title: order.code || DETAIL_PLACEHOLDER,
+            deviceLabel: [order.deviceName, order.deviceCode ? `(${order.deviceCode})` : ''].filter(Boolean).join('') || DETAIL_PLACEHOLDER,
+            deviceLocation: order.deviceLocation || DETAIL_PLACEHOLDER,
+            faultTimeText: formatEventTime(order.faultTime),
+            // 详情要用的其余字段原样带到行视图(组件不回头翻领域对象)。
+            deviceTypeName: order.deviceTypeName || '',
+            orderLevel: order.orderLevel ?? null,
+            orderLevelName: order.orderLevelName || '',
+            receiveUserName: order.receiveUserName || '',
+            verifyUserName: order.verifyUserName || '',
+            maintenanceBeforeImages: order.maintenanceBeforeImages || [],
+            maintenanceAfterImages: order.maintenanceAfterImages || [],
+            delayApply: order.delayApply || null,
             associationText: order.associatedWithCurrentEvent === true ? '已关联当前事件'
                 : order.associatedWithCurrentEvent === false ? '未关联当前事件' : '当前事件关联关系未发布',
             createTime: order.createTime || '',
@@ -660,18 +675,36 @@ export function eventDetailRows(row) {
 export function workOrderDetailRows(row) {
     const current = row || {}
     return detailRowList([
-        detailRow(TIME_LABELS.workOrderCreated, current.createTime, true),
         detailRow('状态', current.stateName, true),
-        detailRow('来源', current.source || current.typeName, true),
-        detailRow('关联对象', current.associationText, true),
-        detailRow('编码', current.code),
-        detailRow('计划结案', current.planFinishTime),
-        detailRow('实际结案', current.realFinishTime, true),
-        detailRow('说明', current.description),
+        detailRow('故障描述', current.description, true),
+        detailRow('优先级', current.orderLevelName || (current.orderLevel !== null && current.orderLevel !== undefined ? `${current.orderLevel} 级` : '')),
+        detailRow('故障发生时间', current.faultTimeText || current.timeText, true),
+        detailRow('计划完成时间', formatEventTime(current.planFinishTime)),
+        detailRow('设备名称', current.deviceLabel),
+        detailRow('设备类型', current.deviceTypeName),
+        detailRow('安装位置', current.deviceLocation),
+        detailRow('处置人', current.receiveUserName),
+        detailRow('核验人', current.verifyUserName),
+        detailRow('维修前附件', imageSummary(current.maintenanceBeforeImages)),
+        detailRow('维修后附件', imageSummary(current.maintenanceAfterImages)),
+        detailRow('延期申请', delayApplyText(current.delayApply), true),
+        detailRow('实际结案', current.realFinishTime),
     ])
 }
 
 /** 详情行清单:剔除「可选项没值」的 null(必选项即使占位符也保留)。 */
+/** 附件数量摘要:有图给「N 项」,没给就说没有(不把缺省当成功)。 */
+function imageSummary(list) {
+    return Array.isArray(list) && list.length ? `${list.length} 项` : '暂无'
+}
+
+/** 延期申请一句话:状态 · 原因 · 新期限;完全没有申请时明确写出来。 */
+function delayApplyText(apply) {
+    if (!apply) return '没有延期信息'
+    const parts = [apply.statusName || apply.status, apply.reason, formatEventTime(apply.deadline)].filter(Boolean)
+    return parts.length ? parts.join(' · ') : '没有延期信息'
+}
+
 function detailRowList(rows) {
     return rows.filter(Boolean)
 }

+ 29 - 0
src/modules/cockpit/domain/workOrder.js

@@ -83,6 +83,15 @@ export function toWorkOrder(raw) {
         repairResult: source.optionalStr('repairResult'),
         receiveUserName: source.optionalStr('receiveUserName'),
         verifyUserName: source.optionalStr('verifyUserName'),
+        // 故障时间:管理端工单卡片与详情都用它(用户 2026-11-13 参考图一/图二)。
+        faultTime: source.optionalStr('faultTime'),
+        // 优先级中文名:后端按等级字典下发;缺失时前端只显示码,不自造词表。
+        orderLevelName: source.optionalStr('orderLevelName'),
+        // 维修前/后附件:后端解析成数组,缺失为空数组(不是「无附件」的断言)。
+        maintenanceBeforeImages: imageList(source, 'maintenanceBeforeImages'),
+        maintenanceAfterImages: imageList(source, 'maintenanceAfterImages'),
+        // 延期申请:无申请时后端给 null,界面显示「没有延期信息」。
+        delayApply: delayApplyView(source),
         associatedWithCurrentEvent: typeof source.raw.associatedWithCurrentEvent === 'boolean'
             ? source.raw.associatedWithCurrentEvent : null,
         event: association
@@ -95,6 +104,26 @@ export function toWorkOrder(raw) {
     }
 }
 
+/** 图片字段:接受数组或逗号分隔字符串,空值回空数组(不把「没给」当成「没图」)。 */
+function imageList(source, key) {
+    const value = source.optionalList(key)
+    if (value.length) return value.filter((item) => typeof item === 'string' && item.trim())
+    const raw = source.optionalStr(key)
+    return raw ? raw.split(',').map((item) => item.trim()).filter(Boolean) : []
+}
+
+/** 延期申请视图:四个字段任一缺失就整体视为「没有可展示的延期信息」,不拼半份。 */
+function delayApplyView(source) {
+    const apply = source.optionalObj('delayApply')
+    const reader = fieldReader(apply, 'delayApply')
+    const status = reader.optionalStr('status')
+    const reason = reader.optionalStr('reason')
+    const deadline = reader.optionalStr('deadline')
+    const statusName = reader.optionalStr('statusName')
+    if (!status && !reason && !deadline) return null
+    return { status: status || '', statusName: statusName || '', reason: reason || '', deadline: deadline || '' }
+}
+
 /**
  * 时间线 → 领域对象。轨迹按 `operatedAt` 升序(后端顺序不保证时序,票 12 要按序讲动作)。
  * @param {object} raw 后端 `CockpitWorkOrderTimelineResponse`