| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264 |
- <template>
- <div class="main-wrap p-4 flex flex-col gap-y-4">
- <NCard
- ref="mainCardRef"
- class="h-full"
- content-class="h-full flex flex-col"
- :style="cardHeight"
- >
- <!-- 过滤条件 -->
- <div class="flex gap-2 min-h-6 items-center">
- <NForm
- inline
- label-placement="left"
- label-width="auto"
- :show-feedback="false"
- class="flex-1 flex-wrap"
- >
- <NFormItem label="站点(equipNo)">
- <NInput
- v-model:value="filterEquipNo"
- clearable
- placeholder="设备号"
- class="w-200px"
- size="small"
- />
- </NFormItem>
- <NFormItem label="命令类型">
- <NInputNumber
- v-model:value="filterCmdType"
- clearable
- placeholder="cmdType"
- class="w-140px"
- size="small"
- :show-button="false"
- />
- </NFormItem>
- <NFormItem label="时间范围(UTC)">
- <NDatePicker
- v-model:value="filterTimeRange"
- type="datetimerange"
- clearable
- size="small"
- />
- </NFormItem>
- <NFormItem>
- <NButton type="primary" size="small" @click="clickSearch">
- <template #icon><span class="i-mdi:search" /></template>
- 查询
- </NButton>
- </NFormItem>
- <NFormItem>
- <NButton size="small" @click="clickReset">重置</NButton>
- </NFormItem>
- </NForm>
- </div>
- <NDivider style="margin: 16px 0" />
- <!-- 数据表 -->
- <div class="flex flex-1 flex-col min-h-0">
- <div ref="tableWrapper" class="flex-1 min-h-0 overflow-hidden">
- <NDataTable
- :remote="true"
- size="small"
- :columns="tableColumns"
- :data="tableData"
- :row-key="tableRowKey"
- :loading="isRequestLoading"
- striped
- :single-line="false"
- :style="{ height: `${tableHeight}px` }"
- flex-height
- />
- </div>
- <div class="mt-3 flex items-center justify-between">
- <div class="text-xs text-neutral-400 flex flex-col gap-y-0.5">
- <span>默认查询当天数据,按时间降序(最新在前);时间均为 UTC</span>
- <span>
- 共 <b>{{ realTotal.toLocaleString() }}</b> 条
- <template v-if="isTruncated">
- <span class="text-amber-500">
- (仅可浏览前 {{ MAX_WINDOW.toLocaleString() }} 条,请缩小时间范围或增加设备号/命令类型过滤)
- </span>
- </template>
- </span>
- </div>
- <NPagination v-bind="tablePagination" />
- </div>
- </div>
- </NCard>
- </div>
- </template>
- <script setup lang="tsx">
- import {
- NCard,
- NForm,
- NFormItem,
- NInput,
- NInputNumber,
- NDatePicker,
- NButton,
- NDivider,
- NDataTable,
- NPagination,
- useMessage,
- } from 'naive-ui'
- import type { PaginationProps, DataTableColumns } from 'naive-ui'
- import { useFullCard } from '@/composable/comp/useFullCard'
- import { queryMqttMessages } from '@/service/mqttDataManage'
- import type { MqttMessageItem, QueryMessageParams } from '@/service/mqttDataManage/interface'
- defineOptions({ name: 'MqttDataList' })
- const message = useMessage()
- const mainCardRef = useTemplateRef<InstanceType<typeof NCard>>('mainCardRef')
- const { height } = useFullCard(mainCardRef)
- const cardHeight = computed(() => ({
- height: `${height.value === undefined || height.value === 0 ? 480 : height.value}px`,
- }))
- // ------- 过滤条件 -------
- const filterEquipNo = ref('')
- const filterCmdType = ref<number | null>(null)
- const filterTimeRange = ref<[number, number] | null>(null)
- // ------- 表格 -------
- const isRequestLoading = ref(false)
- const tableData = ref<MqttMessageItem[]>([])
- const tableWrapper = ref<HTMLDivElement | null>(null)
- const tableHeight = ref(100)
- // ES from+size 深分页窗口上限(与后端 MaxMessagePageWindow 一致):
- // 分页控件最多翻到前 MAX_WINDOW 条,超出会被后端拒绝。
- const MAX_WINDOW = 10000
- // 真实命中总数(可能远大于 MAX_WINDOW),用于文字展示
- const realTotal = ref(0)
- // 是否被窗口截断(真实总数超过可翻页范围)
- const isTruncated = computed(() => realTotal.value > MAX_WINDOW)
- const tableRowKey = (row: MqttMessageItem) => `${row.equipNo}-${row.timeStamp}-${row.cmdType}`
- // 时区统一 UTC。
- // 记录展示:timeStamp(秒) 为 UTC 绝对时刻,用 UTC getter 显示其 UTC 墙钟时间。
- const pad2 = (n: number) => String(n).padStart(2, '0')
- const formatUtcFromSeconds = (sec: number) => {
- const d = new Date(sec * 1000)
- return (
- `${d.getUTCFullYear()}-${pad2(d.getUTCMonth() + 1)}-${pad2(d.getUTCDate())} `
- + `${pad2(d.getUTCHours())}:${pad2(d.getUTCMinutes())}:${pad2(d.getUTCSeconds())}`
- )
- }
- // 查询范围:日期选择器返回所选“墙钟时间”的本地毫秒,用户按 UTC 语义理解,
- // 故用本地 getter 取出所选墙钟并原样作为 UTC 字符串传给后端(后端按 UTC 解析)。
- const formatPickedAsUtc = (ms: number) => {
- const d = new Date(ms)
- return (
- `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} `
- + `${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`
- )
- }
- const tableColumns = computed<DataTableColumns<MqttMessageItem>>(() => [
- {
- title: 'No.',
- key: 'no',
- width: 60,
- render: (_, index) => {
- const page = tablePagination.page ?? 1
- const pageSize = tablePagination.pageSize ?? 20
- return `${(page - 1) * pageSize + index + 1}`
- },
- },
- { title: '站点(equipNo)', key: 'equipNo', width: 220 },
- { title: '命令类型(cmdType)', key: 'cmdType', width: 150 },
- {
- title: '时间(UTC)',
- key: 'timeStamp',
- width: 200,
- render: (row) => formatUtcFromSeconds(row.timeStamp),
- },
- {
- title: 'event',
- key: 'event',
- render: (row) =>
- row.event && Object.keys(row.event).length > 0 ? JSON.stringify(row.event) : '-',
- },
- ])
- const tablePagination = reactive<PaginationProps>({
- page: 1,
- pageSize: 20,
- showSizePicker: true,
- pageSizes: [20, 50, 100],
- itemCount: 0,
- showQuickJumper: true,
- onUpdatePage: (page: number) => {
- tablePagination.page = page
- handleQuery()
- },
- onUpdatePageSize: (pageSize: number) => {
- tablePagination.pageSize = pageSize
- tablePagination.page = 1
- handleQuery()
- },
- })
- // ------- 查询 -------
- const handleQuery = async () => {
- isRequestLoading.value = true
- try {
- const params: QueryMessageParams = {
- page: tablePagination.page ?? 1,
- size: tablePagination.pageSize ?? 20,
- }
- if (filterEquipNo.value.trim()) params.equipNo = filterEquipNo.value.trim()
- if (filterCmdType.value !== null) params.cmdType = filterCmdType.value
- if (filterTimeRange.value) {
- params.startTime = formatPickedAsUtc(filterTimeRange.value[0])
- params.endTime = formatPickedAsUtc(filterTimeRange.value[1])
- }
- const res = await queryMqttMessages(params)
- tableData.value = res.items ?? []
- realTotal.value = res.total
- // ES from+size 深分页上限:分页控件最多允许翻到前 MAX_WINDOW 条,避免深分页请求失败。
- // 真实总数另用文字展示。
- tablePagination.itemCount = Math.min(res.total, MAX_WINDOW)
- } catch (err) {
- tableData.value = []
- realTotal.value = 0
- tablePagination.itemCount = 0
- message.error(err instanceof Error ? err.message : String(err))
- } finally {
- isRequestLoading.value = false
- }
- }
- const clickSearch = () => {
- tablePagination.page = 1
- handleQuery()
- }
- const clickReset = () => {
- filterEquipNo.value = ''
- filterCmdType.value = null
- filterTimeRange.value = null
- tablePagination.page = 1
- handleQuery()
- }
- onMounted(async () => {
- await handleQuery()
- nextTick(() => {
- const observer = new ResizeObserver((entries) => {
- tableHeight.value = entries[0].contentRect.height
- })
- if (tableWrapper.value) {
- observer.observe(tableWrapper.value)
- }
- })
- })
- </script>
|