EventListPane.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. <!--
  2. 事件列表 + 预警/报警分段切换(票 13)。
  3. 这是一个**可复用**的面板内容组件:当前预警面板(本票挂载)与票 19 的
  4. 历史分析 / 预警效率 / 预警分析三块面板都要求「预警/报警切换」,切换是共享状态
  5. (`eventStore.mode`),所以本组件只消费 store 的 `view`,不做任何字段改名、
  6. 不自己取数、不碰后端字段(t05 静态守卫盯着)。
  7. 三条硬纪律在这里的落点(措辞与色键都来自 domain/eventList.js,组件不重写):
  8. · **列表字段与状态标签随切换同步变化**:`view.switchView` 是唯一一份 ——
  9. 表头(预警编号/报警编号)与状态标签名(预警状态/报警状态)从同一个对象取,
  10. 切换后整张表一起换,不会出现字段与标签对不上;
  11. · **等级 = 色键 + 文字**:每行等级是 StatusTag(tone + 'N级' 文字),
  12. 颜色不是唯一线索;
  13. · **只读**:没有任何写操作入口;事件只带设备编码,不产生地图标记。
  14. `data-*` 是走查断言面:`data-event-switch` 是分段按钮,
  15. `data-event-row` 是列表行,`data-event-level` 是等级标签,
  16. `data-event-status` 是状态文本,`data-event-mode` 是当前切换。
  17. -->
  18. <template>
  19. <div class="event-list-pane" :data-event-mode="view.switchView.key">
  20. <!-- 预警/报警分段切换(共享状态:eventStore.mode,四块面板共用一份) -->
  21. <div class="event-list-pane__switch" role="group" aria-label="预警/报警切换">
  22. <button
  23. v-for="option in switchOptions"
  24. :key="option.key"
  25. type="button"
  26. class="event-list-pane__switch-btn"
  27. :class="{ 'is-active': option.key === view.switchView.key }"
  28. :aria-pressed="option.key === view.switchView.key"
  29. :data-event-switch="option.key"
  30. @click="setMode(option.key)"
  31. >
  32. {{ option.label }}
  33. </button>
  34. </div>
  35. <!--
  36. 票 67:全局筛选「当前范围」在本面板的落点 —— 当前事件端点实测不收片区入参,
  37. 所以整张表不随片区变化。措辞来自 domain/filterCaliber.js 的单一来源,
  38. 与 PanelBlocks 的面板口径说明共用一个断言钩子 `data-panel-gap`。
  39. 放在状态文案之前:读者先知道"这个条件在这里不起作用",再看数。
  40. -->
  41. <p v-if="view.gap" class="event-list-pane__note" data-panel-gap>{{ view.gap }}</p>
  42. <!-- 分区自己的状态(pending / empty / failed / unavailable 各说各的话) -->
  43. <!-- 还没取过数:说"正在取数",不说"没有数据" -->
  44. <p v-if="pending" class="event-list-pane__placeholder" data-event-placeholder>{{ view.placeholder }}</p>
  45. <!-- 有内容:按行渲染;只读,无任何操作按钮 -->
  46. <template v-else-if="view.hasContent">
  47. <table class="event-list-pane__table" data-event-table>
  48. <!-- 2026-10-13 维护者反馈「右侧太长被裁切」:长编号/长名称撑爆 auto 布局,
  49. 右边列整列看不见。改 fixed 布局 + 每格省略号 + title 全值兜底,
  50. 列宽合计恒等于表宽;完整值在悬停 title 与行点开的事件详情卡里。 -->
  51. <colgroup>
  52. <col class="col-level" />
  53. <col class="col-name" />
  54. <col class="col-code" />
  55. <col class="col-status" />
  56. <col class="col-time" />
  57. <col class="col-value" />
  58. <col class="col-device" />
  59. </colgroup>
  60. <thead>
  61. <tr>
  62. <th>等级</th>
  63. <th>{{ view.switchView.nameLabel }}</th>
  64. <th>{{ view.switchView.codeLabel }}</th>
  65. <th>{{ view.switchView.statusLabel }}</th>
  66. <th>时间</th>
  67. <th>量值</th>
  68. <th>设备</th>
  69. </tr>
  70. </thead>
  71. <tbody>
  72. <tr
  73. v-for="row in view.rows"
  74. :key="row.key"
  75. class="event-list-pane__row"
  76. :data-event-row="row.key"
  77. :data-event-level-color="row.level.tone"
  78. tabindex="0"
  79. @click="eventStore.openEvent(row)"
  80. @keydown.enter="eventStore.openEvent(row)"
  81. >
  82. <td data-event-level>
  83. <StatusTag :tone="row.level.tone" :label="row.level.label" />
  84. </td>
  85. <td class="event-list-pane__name" data-event-name :title="row.name">{{ row.name }}</td>
  86. <td class="event-list-pane__mono" data-event-code :title="row.code">{{ row.code }}</td>
  87. <td data-event-status :title="row.statusText + (row.workOrderStateName ? ' · 工单' + row.workOrderStateName : '')">{{ row.statusText }}<span v-if="row.workOrderStateName" data-event-work-order> · 工单{{ row.workOrderStateName }}</span></td>
  88. <td class="event-list-pane__time" data-event-time :title="row.eventTime">{{ row.eventTimeText || row.eventTime }}</td>
  89. <td class="event-list-pane__value" data-event-value :title="row.quantity">{{ row.quantity }}</td>
  90. <td :title="row.deviceName || row.monitoringPointName || ''">
  91. <span v-if="row.deviceName" data-event-device>{{ row.deviceName }}</span>
  92. <span v-else-if="row.monitoringPointName" data-event-device>{{ row.monitoringPointName }}</span>
  93. <span v-else data-event-device class="is-muted">—</span>
  94. </td>
  95. </tr>
  96. </tbody>
  97. </table>
  98. <!--
  99. 票 61:服务端页码分页(共享 PagerNav,规范条文 2/3/4/9 + 跳页节)。
  100. 翻页带 page/pageSize 发请求(页码状态在 eventStore,组件只转发目标页);
  101. 一页时整块不渲染(控件自己按 pageCount>1 收口);
  102. 计数行「第 X/Y 页 · 共 N 条」由控件单源给,不暴露 page/pageSize 参数名(条文 10)。
  103. -->
  104. <div v-if="view.pagination && view.pagination.pageCount > 1" class="event-list-pane__pager" data-event-pager>
  105. <PagerNav
  106. :page="view.pagination.page"
  107. :page-count="view.pagination.pageCount"
  108. :total="view.pagination.total"
  109. :list-name="view.switchView.title"
  110. @page="(target) => eventStore.setCurrentPage(target)"
  111. />
  112. </div>
  113. <p v-if="view.updatedAt" class="event-list-pane__note" data-event-updated>
  114. 数据时间 {{ view.updatedAt }}
  115. </p>
  116. </template>
  117. <!-- 没内容:如实说(空/失败/不可用各有措辞,来自 domain) -->
  118. <p v-else class="event-list-pane__placeholder" data-event-placeholder>{{ view.placeholder }}</p>
  119. </div>
  120. </template>
  121. <script setup>
  122. import { computed } from 'vue'
  123. import { eventListSwitchOptions } from '../domain/eventList.js'
  124. import { sectionPending } from '../domain/panelBlocks.js'
  125. import { useEventStore } from '../stores/eventStore.js'
  126. import PagerNav from './PagerNav.vue'
  127. import StatusTag from './StatusTag.vue'
  128. const eventStore = useEventStore()
  129. const view = computed(() => eventStore.view)
  130. const switchOptions = eventListSwitchOptions()
  131. /** 还没取过数(界面说"正在取数",不说"没有数据")。 */
  132. const pending = computed(() => sectionPending(view.value.section))
  133. function setMode(next) {
  134. eventStore.setMode(next)
  135. }
  136. </script>
  137. <style scoped>
  138. .event-list-pane,
  139. .event-list-pane *,
  140. .event-list-pane *::before,
  141. .event-list-pane *::after {
  142. box-sizing: border-box;
  143. }
  144. .event-list-pane {
  145. display: grid;
  146. gap: calc(4 * var(--u, 1px));
  147. min-height: 0;
  148. }
  149. /* 分段切换:两个等宽的胶囊按钮,选中项高亮(与全局筛选条同一套视觉语言) */
  150. .event-list-pane__switch {
  151. display: inline-flex;
  152. gap: calc(3 * var(--u, 1px));
  153. }
  154. .event-list-pane__switch-btn {
  155. padding: calc(1 * var(--u, 1px)) calc(10 * var(--u, 1px));
  156. border: 1px solid rgba(1, 171, 234, 0.32);
  157. border-radius: calc(8 * var(--u, 1px));
  158. background: rgba(0, 20, 72, 0.6);
  159. color: var(--text-2);
  160. font: inherit;
  161. font-size: var(--fs-caption, 10.5px);
  162. letter-spacing: 0.04em;
  163. cursor: pointer;
  164. }
  165. .event-list-pane__switch-btn:hover,
  166. .event-list-pane__switch-btn:focus-visible {
  167. border-color: rgba(1, 171, 234, 0.6);
  168. color: var(--text-strong);
  169. }
  170. .event-list-pane__switch-btn.is-active {
  171. border-color: rgba(0, 184, 255, 0.85);
  172. background: linear-gradient(180deg, rgba(1, 171, 234, 0.85), rgba(0, 82, 168, 0.85));
  173. color: var(--text-strong);
  174. }
  175. .event-list-pane__table {
  176. width: 100%;
  177. /* 固定布局:列宽由 colgroup 说了算,长内容不再撑爆表格把右列顶出可视区。 */
  178. table-layout: fixed;
  179. border-collapse: collapse;
  180. font-size: var(--fs-caption, 10.5px);
  181. }
  182. /* 列宽(合计 100%):等级/状态/时间/量值/设备是短列,名称与编号吃掉弹性宽度。
  183. 时间列给足宽度完整显示「MM-DD HH:MM」;编号列本就长串必截断,收窄让给时间;
  184. 量值/设备列保证右侧完整可见(维护者反馈的裁切点)。 */
  185. .col-level {
  186. width: 9%;
  187. }
  188. .col-name {
  189. width: 17%;
  190. }
  191. .col-code {
  192. width: 12%;
  193. }
  194. .col-status {
  195. width: 14%;
  196. }
  197. .col-time {
  198. width: 21%;
  199. }
  200. .col-value {
  201. width: 13%;
  202. }
  203. .col-device {
  204. width: 14%;
  205. }
  206. .event-list-pane__table td {
  207. /* 数据格收口:超宽省略号,绝不外溢撑破表格(完整值在 title 与详情卡)。 */
  208. overflow: hidden;
  209. text-overflow: ellipsis;
  210. white-space: nowrap;
  211. }
  212. .event-list-pane__table th {
  213. padding: calc(2 * var(--u, 1px)) calc(3 * var(--u, 1px));
  214. border-bottom: 1px solid rgba(1, 171, 234, 0.35);
  215. color: var(--muted, #5680b0);
  216. font-weight: 700;
  217. letter-spacing: 0.04em;
  218. text-align: left;
  219. /* 表头不截断:允许换行完整显示(「预警编号」不被压成「预警…」)。 */
  220. white-space: normal;
  221. }
  222. .event-list-pane__table td {
  223. padding: calc(2 * var(--u, 1px)) calc(3 * var(--u, 1px));
  224. border-bottom: 1px dashed rgba(1, 171, 234, 0.14);
  225. color: var(--text-2, #88b7e8);
  226. vertical-align: top;
  227. }
  228. .event-list-pane__name {
  229. color: var(--text, #d7ecff);
  230. }
  231. .event-list-pane__mono,
  232. .event-list-pane__time,
  233. .event-list-pane__value {
  234. font-family: var(--num, monospace);
  235. letter-spacing: 0.02em;
  236. }
  237. .event-list-pane__placeholder,
  238. .event-list-pane__note,
  239. .is-muted {
  240. margin: 0;
  241. color: var(--muted, #5680b0);
  242. font-size: var(--fs-caption, 10.5px);
  243. letter-spacing: 0.04em;
  244. }
  245. /* 窄栏采用事件卡片排版,保留全部字段与读取接缝。 */
  246. .event-list-pane__table,
  247. .event-list-pane__table tbody {
  248. display: block;
  249. width: 100%;
  250. }
  251. .event-list-pane__table colgroup { display: none; }
  252. .event-list-pane__table thead {
  253. position: absolute;
  254. width: 1px;
  255. height: 1px;
  256. overflow: hidden;
  257. clip-path: inset(50%);
  258. }
  259. .event-list-pane__row {
  260. display: grid;
  261. grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  262. gap: 5px 8px;
  263. margin-bottom: 8px;
  264. padding: 8px;
  265. border: 1px solid var(--panel-line);
  266. border-radius: 4px;
  267. background: var(--inset);
  268. cursor: pointer;
  269. transition: border-color .15s, background-color .15s;
  270. }
  271. .event-list-pane__row:hover,
  272. .event-list-pane__row:focus-visible {
  273. outline: none;
  274. border-color: var(--cyan-hi);
  275. background: rgba(0, 82, 168, .5);
  276. box-shadow: inset 3px 0 0 var(--cyan-hi);
  277. }
  278. .event-list-pane__table .event-list-pane__row td {
  279. min-width: 0;
  280. padding: 0;
  281. border: 0;
  282. overflow: visible;
  283. white-space: normal;
  284. overflow-wrap: anywhere;
  285. }
  286. .event-list-pane__row td:nth-child(2),
  287. .event-list-pane__row td:nth-child(3),
  288. .event-list-pane__row td:nth-child(7) { grid-column: 1 / -1; }
  289. .event-list-pane__row td:nth-child(2) { font-size: var(--fs-body); }
  290. .event-list-pane__row td::before {
  291. display: block;
  292. color: var(--muted);
  293. font-size: var(--fs-caption);
  294. }
  295. .event-list-pane__row td:nth-child(3)::before { content: '编号'; }
  296. .event-list-pane__row td:nth-child(4)::before { content: '状态 / 工单'; }
  297. .event-list-pane__row td:nth-child(5)::before { content: '时间'; }
  298. .event-list-pane__row td:nth-child(6)::before { content: '量值'; }
  299. .event-list-pane__row td:nth-child(7)::before { content: '设备'; }
  300. </style>