EventListPane.vue 15 KB

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