| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218 |
- <!--
- 事件详情(票 14)。
- 数据来源是 `eventStore.selectedEvent`(票 13 的列表行对象),字段映射全在
- `domain/eventDetail.js`(纯函数)—— 组件只画,不改名、不判准入。
- 两处刻意的取舍:
- · 详情画在事件列表面板内、「返回事件列表」只收起本卡:返回时列表位置、族与筛选条件
- 一个字节没动(列表在 store,收起详情不触发任何重取)—— 验收第 4 条是**结构性**保证;
- · 定位按钮把「设备 + 关联管线」的编码交给 store → 地图服务:事件不新建地图实体;
- 定位不成时如实给原因(无有效坐标 / 目标不在已加载范围 / 地图未就绪),
- 事件本身仍可正常查看 —— 验收第 5 条。
- -->
- <template>
- <section v-if="event" class="event-detail" data-event-detail :data-event-detail-code="view.code">
- <header class="event-detail__head">
- <span class="event-detail__title">事件详情</span>
- <button type="button" class="event-detail__action" data-event-detail-back @click="eventStore.closeEvent()">
- 返回事件列表
- </button>
- </header>
- <div class="event-detail__body">
- <h3 class="event-detail__name" data-event-detail-name>{{ view.name }}</h3>
- <dl class="event-detail__rows" data-event-detail-rows>
- <div class="event-detail__row">
- <dt>事件编码</dt>
- <dd class="event-detail__mono" data-event-detail-code-value>{{ view.code }}</dd>
- </div>
- <div class="event-detail__row">
- <dt>设备编码</dt>
- <dd class="event-detail__mono" data-event-detail-device>{{ view.deviceCode }}</dd>
- </div>
- <div class="event-detail__row">
- <dt>指标</dt>
- <dd data-event-detail-metric>{{ view.metric }}</dd>
- </div>
- <div class="event-detail__row">
- <dt>阈值</dt>
- <dd class="event-detail__mono" data-event-detail-threshold>{{ view.threshold }}</dd>
- </div>
- <div class="event-detail__row">
- <dt>实际值</dt>
- <dd class="event-detail__mono" data-event-detail-value>{{ view.value }}</dd>
- </div>
- <div class="event-detail__row">
- <dt>等级</dt>
- <dd data-event-detail-level>
- <StatusTag :tone="view.level.tone" :label="view.level.label" />
- </dd>
- </div>
- <div class="event-detail__row">
- <dt>时间</dt>
- <dd class="event-detail__time" data-event-detail-time>{{ view.eventTime }}</dd>
- </div>
- <div class="event-detail__row">
- <dt>状态</dt>
- <dd data-event-detail-status>{{ view.status }}</dd>
- </div>
- <div class="event-detail__row">
- <dt>关联工单</dt>
- <dd data-event-detail-work-order>{{ view.workOrderCode ? `${view.workOrderCode} · ${view.workOrderStateName || '状态未发布'}` : '未关联工单或关系未发布' }}</dd>
- </div>
- <div class="event-detail__row">
- <dt>位置</dt>
- <dd :class="{ 'is-muted': !view.hasLocation }" data-event-detail-location>{{ view.location }}</dd>
- </div>
- </dl>
- <!--
- 定位反馈:成功的确认与失败的原因都写在这里。
- 「事件仍可查看」(验收第 5 条)= 本段落无论成败都只是这一段文字,
- 不遮字段、不禁用返回。
- -->
- <p v-if="eventStore.selectedEvent && eventStore.selectedEvent.refreshNotice" class="event-detail__feedback" data-event-detail-refresh>
- {{ eventStore.selectedEvent.refreshNotice }}
- </p>
- <p
- v-if="locateNote"
- class="event-detail__feedback"
- :class="{ 'is-bad': locateIsFailure }"
- :data-event-detail-locate-result="locate.result"
- role="status"
- >
- {{ locateNote }}
- </p>
- <button type="button" class="event-detail__locate" data-event-detail-locate @click="eventStore.locateEvent()">
- 定位关联设备
- </button>
- </div>
- </section>
- </template>
- <script setup>
- import { computed } from 'vue'
- import { useEventStore } from '../stores/eventStore.js'
- import { eventDetailView } from '../domain/eventDetail.js'
- import { LOCATE_RESULT } from '../domain/mapLocate.js'
- import StatusTag from './StatusTag.vue'
- const eventStore = useEventStore()
- const event = computed(() => eventStore.selectedEvent)
- const view = computed(() => eventDetailView(event.value))
- const locate = computed(() => eventStore.locate)
- /** 定位结果的一句话(还没点过 = 空串,不预告结果)。 */
- const locateNote = computed(() => {
- const current = locate.value || {}
- if (!current.result) return ''
- if (current.result === LOCATE_RESULT.OK) return '已定位,并在地图上高亮关联设备与管线'
- return current.reasonText || '定位未完成'
- })
- /** 失败(含「目标还没上图」)用警示色;成功用常态色。 */
- const locateIsFailure = computed(() => (locate.value || {}).result !== LOCATE_RESULT.OK)
- </script>
- <style scoped>
- .event-detail {
- display: grid;
- gap: calc(4 * var(--u, 1px));
- min-height: 0;
- border: 1px solid rgba(96, 220, 255, 0.45);
- background: linear-gradient(180deg, rgba(12, 48, 86, 0.72), rgba(3, 14, 28, 0.92));
- padding: calc(5 * var(--u, 1px)) calc(6 * var(--u, 1px));
- }
- .event-detail__head {
- display: flex;
- align-items: center;
- gap: calc(6 * var(--u, 1px));
- }
- .event-detail__title {
- flex: 1;
- color: var(--text, #e6f5ff);
- font-weight: 700;
- letter-spacing: 0.06em;
- }
- .event-detail__action,
- .event-detail__locate {
- padding: calc(1 * var(--u, 1px)) calc(8 * var(--u, 1px));
- border: 1px solid rgba(58, 170, 226, 0.42);
- background: rgba(8, 34, 60, 0.6);
- color: #9dc2d9;
- font: inherit;
- font-size: var(--fs-caption, 10.5px);
- letter-spacing: 0.04em;
- cursor: pointer;
- }
- .event-detail__action:hover,
- .event-detail__action:focus-visible,
- .event-detail__locate:hover,
- .event-detail__locate:focus-visible {
- border-color: rgba(79, 216, 255, 0.7);
- color: #fff;
- }
- .event-detail__name {
- margin: 0;
- color: var(--text, #e6f5ff);
- font-size: var(--fs-body, 13px);
- }
- .event-detail__rows {
- display: grid;
- gap: calc(2 * var(--u, 1px));
- margin: 0;
- font-size: var(--fs-caption, 10.5px);
- }
- .event-detail__row {
- display: grid;
- grid-template-columns: 58px minmax(0, 1fr);
- gap: calc(4 * var(--u, 1px));
- padding-bottom: calc(1 * var(--u, 1px));
- border-bottom: 1px dashed rgba(58, 170, 226, 0.14);
- }
- .event-detail__row dt {
- color: var(--muted, #6d92ae);
- }
- .event-detail__row dd {
- margin: 0;
- color: var(--text, #e6f5ff);
- word-break: break-word;
- }
- .event-detail__mono,
- .event-detail__time {
- font-family: var(--num, monospace);
- letter-spacing: 0.02em;
- }
- .event-detail__feedback {
- margin: 0;
- color: #8fe9c4;
- font-size: var(--fs-caption, 10.5px);
- letter-spacing: 0.04em;
- }
- .event-detail__feedback.is-bad {
- color: #ffb3bb;
- }
- .is-muted {
- color: var(--muted, #6d92ae);
- }
- .event-detail__locate {
- justify-self: start;
- }
- </style>
|