index.vue 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. <template>
  2. <div class="main-wrap p-4 flex flex-col gap-y-4">
  3. <NCard
  4. ref="mainCardRef"
  5. class="h-full"
  6. content-class="h-full flex flex-col"
  7. :style="cardHeight"
  8. >
  9. <!-- 过滤条件 -->
  10. <div class="flex gap-2 min-h-6 items-center">
  11. <NForm
  12. inline
  13. label-placement="left"
  14. label-width="auto"
  15. :show-feedback="false"
  16. class="flex-1 flex-wrap"
  17. >
  18. <NFormItem label="站点(equipNo)">
  19. <NInput
  20. v-model:value="filterEquipNo"
  21. clearable
  22. placeholder="设备号"
  23. class="w-200px"
  24. size="small"
  25. />
  26. </NFormItem>
  27. <NFormItem label="命令类型">
  28. <NInputNumber
  29. v-model:value="filterCmdType"
  30. clearable
  31. placeholder="cmdType"
  32. class="w-140px"
  33. size="small"
  34. :show-button="false"
  35. />
  36. </NFormItem>
  37. <NFormItem label="时间范围(UTC)">
  38. <NDatePicker
  39. v-model:value="filterTimeRange"
  40. type="datetimerange"
  41. clearable
  42. size="small"
  43. />
  44. </NFormItem>
  45. <NFormItem>
  46. <NButton type="primary" size="small" @click="clickSearch">
  47. <template #icon><span class="i-mdi:search" /></template>
  48. 查询
  49. </NButton>
  50. </NFormItem>
  51. <NFormItem>
  52. <NButton size="small" @click="clickReset">重置</NButton>
  53. </NFormItem>
  54. </NForm>
  55. </div>
  56. <NDivider style="margin: 16px 0" />
  57. <!-- 数据表 -->
  58. <div class="flex flex-1 flex-col min-h-0">
  59. <div ref="tableWrapper" class="flex-1 min-h-0 overflow-hidden">
  60. <NDataTable
  61. :remote="true"
  62. size="small"
  63. :columns="tableColumns"
  64. :data="tableData"
  65. :row-key="tableRowKey"
  66. :loading="isRequestLoading"
  67. striped
  68. :single-line="false"
  69. :style="{ height: `${tableHeight}px` }"
  70. flex-height
  71. />
  72. </div>
  73. <div class="mt-3 flex items-center justify-between">
  74. <div class="text-xs text-neutral-400 flex flex-col gap-y-0.5">
  75. <span>默认查询当天数据,按时间降序(最新在前);时间均为 UTC</span>
  76. <span>
  77. 共 <b>{{ realTotal.toLocaleString() }}</b> 条
  78. <template v-if="isTruncated">
  79. <span class="text-amber-500">
  80. (仅可浏览前 {{ MAX_WINDOW.toLocaleString() }} 条,请缩小时间范围或增加设备号/命令类型过滤)
  81. </span>
  82. </template>
  83. </span>
  84. </div>
  85. <NPagination v-bind="tablePagination" />
  86. </div>
  87. </div>
  88. </NCard>
  89. </div>
  90. </template>
  91. <script setup lang="tsx">
  92. import {
  93. NCard,
  94. NForm,
  95. NFormItem,
  96. NInput,
  97. NInputNumber,
  98. NDatePicker,
  99. NButton,
  100. NDivider,
  101. NDataTable,
  102. NPagination,
  103. useMessage,
  104. } from 'naive-ui'
  105. import type { PaginationProps, DataTableColumns } from 'naive-ui'
  106. import { useFullCard } from '@/composable/comp/useFullCard'
  107. import { queryMqttMessages } from '@/service/mqttDataManage'
  108. import type { MqttMessageItem, QueryMessageParams } from '@/service/mqttDataManage/interface'
  109. defineOptions({ name: 'MqttDataList' })
  110. const message = useMessage()
  111. const mainCardRef = useTemplateRef<InstanceType<typeof NCard>>('mainCardRef')
  112. const { height } = useFullCard(mainCardRef)
  113. const cardHeight = computed(() => ({
  114. height: `${height.value === undefined || height.value === 0 ? 480 : height.value}px`,
  115. }))
  116. // ------- 过滤条件 -------
  117. const filterEquipNo = ref('')
  118. const filterCmdType = ref<number | null>(null)
  119. const filterTimeRange = ref<[number, number] | null>(null)
  120. // ------- 表格 -------
  121. const isRequestLoading = ref(false)
  122. const tableData = ref<MqttMessageItem[]>([])
  123. const tableWrapper = ref<HTMLDivElement | null>(null)
  124. const tableHeight = ref(100)
  125. // ES from+size 深分页窗口上限(与后端 MaxMessagePageWindow 一致):
  126. // 分页控件最多翻到前 MAX_WINDOW 条,超出会被后端拒绝。
  127. const MAX_WINDOW = 10000
  128. // 真实命中总数(可能远大于 MAX_WINDOW),用于文字展示
  129. const realTotal = ref(0)
  130. // 是否被窗口截断(真实总数超过可翻页范围)
  131. const isTruncated = computed(() => realTotal.value > MAX_WINDOW)
  132. const tableRowKey = (row: MqttMessageItem) => `${row.equipNo}-${row.timeStamp}-${row.cmdType}`
  133. // 时区统一 UTC。
  134. // 记录展示:timeStamp(秒) 为 UTC 绝对时刻,用 UTC getter 显示其 UTC 墙钟时间。
  135. const pad2 = (n: number) => String(n).padStart(2, '0')
  136. const formatUtcFromSeconds = (sec: number) => {
  137. const d = new Date(sec * 1000)
  138. return (
  139. `${d.getUTCFullYear()}-${pad2(d.getUTCMonth() + 1)}-${pad2(d.getUTCDate())} `
  140. + `${pad2(d.getUTCHours())}:${pad2(d.getUTCMinutes())}:${pad2(d.getUTCSeconds())}`
  141. )
  142. }
  143. // 查询范围:日期选择器返回所选“墙钟时间”的本地毫秒,用户按 UTC 语义理解,
  144. // 故用本地 getter 取出所选墙钟并原样作为 UTC 字符串传给后端(后端按 UTC 解析)。
  145. const formatPickedAsUtc = (ms: number) => {
  146. const d = new Date(ms)
  147. return (
  148. `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} `
  149. + `${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`
  150. )
  151. }
  152. const tableColumns = computed<DataTableColumns<MqttMessageItem>>(() => [
  153. {
  154. title: 'No.',
  155. key: 'no',
  156. width: 60,
  157. render: (_, index) => {
  158. const page = tablePagination.page ?? 1
  159. const pageSize = tablePagination.pageSize ?? 20
  160. return `${(page - 1) * pageSize + index + 1}`
  161. },
  162. },
  163. { title: '站点(equipNo)', key: 'equipNo', width: 220 },
  164. { title: '命令类型(cmdType)', key: 'cmdType', width: 150 },
  165. {
  166. title: '时间(UTC)',
  167. key: 'timeStamp',
  168. width: 200,
  169. render: (row) => formatUtcFromSeconds(row.timeStamp),
  170. },
  171. {
  172. title: 'event',
  173. key: 'event',
  174. render: (row) =>
  175. row.event && Object.keys(row.event).length > 0 ? JSON.stringify(row.event) : '-',
  176. },
  177. ])
  178. const tablePagination = reactive<PaginationProps>({
  179. page: 1,
  180. pageSize: 20,
  181. showSizePicker: true,
  182. pageSizes: [20, 50, 100],
  183. itemCount: 0,
  184. showQuickJumper: true,
  185. onUpdatePage: (page: number) => {
  186. tablePagination.page = page
  187. handleQuery()
  188. },
  189. onUpdatePageSize: (pageSize: number) => {
  190. tablePagination.pageSize = pageSize
  191. tablePagination.page = 1
  192. handleQuery()
  193. },
  194. })
  195. // ------- 查询 -------
  196. const handleQuery = async () => {
  197. isRequestLoading.value = true
  198. try {
  199. const params: QueryMessageParams = {
  200. page: tablePagination.page ?? 1,
  201. size: tablePagination.pageSize ?? 20,
  202. }
  203. if (filterEquipNo.value.trim()) params.equipNo = filterEquipNo.value.trim()
  204. if (filterCmdType.value !== null) params.cmdType = filterCmdType.value
  205. if (filterTimeRange.value) {
  206. params.startTime = formatPickedAsUtc(filterTimeRange.value[0])
  207. params.endTime = formatPickedAsUtc(filterTimeRange.value[1])
  208. }
  209. const res = await queryMqttMessages(params)
  210. tableData.value = res.items ?? []
  211. realTotal.value = res.total
  212. // ES from+size 深分页上限:分页控件最多允许翻到前 MAX_WINDOW 条,避免深分页请求失败。
  213. // 真实总数另用文字展示。
  214. tablePagination.itemCount = Math.min(res.total, MAX_WINDOW)
  215. } catch (err) {
  216. tableData.value = []
  217. realTotal.value = 0
  218. tablePagination.itemCount = 0
  219. message.error(err instanceof Error ? err.message : String(err))
  220. } finally {
  221. isRequestLoading.value = false
  222. }
  223. }
  224. const clickSearch = () => {
  225. tablePagination.page = 1
  226. handleQuery()
  227. }
  228. const clickReset = () => {
  229. filterEquipNo.value = ''
  230. filterCmdType.value = null
  231. filterTimeRange.value = null
  232. tablePagination.page = 1
  233. handleQuery()
  234. }
  235. onMounted(async () => {
  236. await handleQuery()
  237. nextTick(() => {
  238. const observer = new ResizeObserver((entries) => {
  239. tableHeight.value = entries[0].contentRect.height
  240. })
  241. if (tableWrapper.value) {
  242. observer.observe(tableWrapper.value)
  243. }
  244. })
  245. })
  246. </script>