feat(ems\report): 新增温度专属报表
parent
ec693aad62
commit
7636a0028c
@ -0,0 +1,176 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<!-- 筛选区 -->
|
||||
<el-card shadow="never">
|
||||
<el-form :inline="true" :model="queryForm" size="small">
|
||||
<el-form-item label="时间范围">
|
||||
<el-date-picker
|
||||
v-model="timeRange"
|
||||
type="datetimerange"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
range-separator="至"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
style="width: 380px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="高温阈值">
|
||||
<el-input-number v-model="queryForm.highTempThreshold" :min="0" :max="100" :precision="1" style="width: 120px" />
|
||||
<span class="ml-1 text-xs text-gray-400">℃</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="低温阈值">
|
||||
<el-input-number v-model="queryForm.lowTempThreshold" :min="-50" :max="50" :precision="1" style="width: 120px" />
|
||||
<span class="ml-1 text-xs text-gray-400">℃</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="TopN">
|
||||
<el-input-number v-model="queryForm.topN" :min="3" :max="50" style="width: 100px" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="Search" @click="handleQuery">查询</el-button>
|
||||
<el-button icon="Refresh" @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<!-- KPI 卡片 -->
|
||||
<el-row :gutter="16" class="mt-4">
|
||||
<el-col :span="6">
|
||||
<el-card shadow="hover" class="kpi-card">
|
||||
<el-statistic title="活跃测点数" :value="overview.monitorCount ?? '-'" suffix="个" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card shadow="hover" class="kpi-card">
|
||||
<el-statistic title="平均温度" :value="overview.avgLatestTemp ?? '-'" :precision="2" suffix="℃" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card shadow="hover" class="kpi-card kpi-high">
|
||||
<el-statistic title="最高温度" :value="overview.maxLatestTemp ?? '-'" :precision="2" />
|
||||
<div v-if="overview.maxTempMonitorId" class="text-xs text-gray-400 mt-1">{{ overview.maxTempMonitorId }}</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card shadow="hover" class="kpi-card kpi-low">
|
||||
<el-statistic title="最低温度" :value="overview.minLatestTemp ?? '-'" :precision="2" />
|
||||
<div v-if="overview.minTempMonitorId" class="text-xs text-gray-400 mt-1">{{ overview.minTempMonitorId }}</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- TopN 排行 -->
|
||||
<el-row :gutter="16" class="mt-4">
|
||||
<el-col :span="12">
|
||||
<el-card shadow="never">
|
||||
<template #header><span class="font-bold">高温 Top{{ queryForm.topN }}</span></template>
|
||||
<div ref="highTopNChartRef" style="height: 320px" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-card shadow="never">
|
||||
<template #header><span class="font-bold">低温 Top{{ queryForm.topN }}</span></template>
|
||||
<div ref="lowTopNChartRef" style="height: 320px" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 数据新鲜度 -->
|
||||
<el-card shadow="never" class="mt-4">
|
||||
<template #header><span class="font-bold">数据新鲜度概览</span></template>
|
||||
<el-table :data="overview.freshnessList ?? []" stripe size="small" max-height="400">
|
||||
<el-table-column prop="monitorId" label="测点ID" width="160" />
|
||||
<el-table-column prop="monitorName" label="测点名称" width="200" />
|
||||
<el-table-column prop="temperature" label="当前温度" width="120" align="right">
|
||||
<template #default="{ row }">{{ row.temperature?.toFixed(2) }}℃</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="collectTime" label="采集时间" width="200" />
|
||||
<el-table-column prop="ageSeconds" label="延迟" width="120" align="right">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.ageSeconds < 60 ? 'success' : row.ageSeconds < 300 ? 'warning' : 'danger'" size="small">
|
||||
{{ row.ageSeconds }}s
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, nextTick } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
import { getTempOverview } from '@/api/ems/report/tempBoard'
|
||||
import type { TempBoardQuery, TempBoardOverviewVO } from '@/api/ems/report/tempBoard'
|
||||
|
||||
defineOptions({ name: 'TempBoardOverview' })
|
||||
|
||||
const timeRange = ref<[string, string]>(['', ''])
|
||||
const queryForm = reactive<TempBoardQuery>({ highTempThreshold: 35, lowTempThreshold: 10, topN: 10 })
|
||||
const overview = ref<Partial<TempBoardOverviewVO>>({})
|
||||
const loading = ref(false)
|
||||
const highTopNChartRef = ref<HTMLElement>()
|
||||
const lowTopNChartRef = ref<HTMLElement>()
|
||||
|
||||
function initDefaultTimeRange() {
|
||||
const end = new Date()
|
||||
const start = new Date(end.getTime() - 24 * 3600 * 1000)
|
||||
const fmt = (d: Date) => {
|
||||
const p = (n: number) => n.toString().padStart(2, '0')
|
||||
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`
|
||||
}
|
||||
timeRange.value = [fmt(start), fmt(end)]
|
||||
}
|
||||
|
||||
async function handleQuery() {
|
||||
if (timeRange.value?.[0]) {
|
||||
queryForm.startTime = timeRange.value[0]
|
||||
queryForm.endTime = timeRange.value[1]
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
const { data } = await getTempOverview(queryForm)
|
||||
overview.value = data
|
||||
await nextTick()
|
||||
renderTopNCharts()
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
Object.assign(queryForm, { highTempThreshold: 35, lowTempThreshold: 10, topN: 10 })
|
||||
initDefaultTimeRange()
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
function renderTopNCharts() {
|
||||
if (highTopNChartRef.value && overview.value.highTempTopN) {
|
||||
const chart = echarts.init(highTopNChartRef.value)
|
||||
const d = overview.value.highTempTopN
|
||||
chart.setOption({
|
||||
tooltip: { trigger: 'axis' },
|
||||
grid: { left: 120, right: 50, top: 10, bottom: 20 },
|
||||
xAxis: { type: 'value', name: '℃' },
|
||||
yAxis: { type: 'category', data: d.map(i => i.monitorName || i.monitorId).reverse() },
|
||||
series: [{ type: 'bar', data: d.map(i => i.temperature).reverse(), itemStyle: { color: '#f56c6c' }, label: { show: true, position: 'right', formatter: '{c}℃' } }]
|
||||
})
|
||||
}
|
||||
if (lowTopNChartRef.value && overview.value.lowTempTopN) {
|
||||
const chart = echarts.init(lowTopNChartRef.value)
|
||||
const d = overview.value.lowTempTopN
|
||||
chart.setOption({
|
||||
tooltip: { trigger: 'axis' },
|
||||
grid: { left: 120, right: 50, top: 10, bottom: 20 },
|
||||
xAxis: { type: 'value', name: '℃' },
|
||||
yAxis: { type: 'category', data: d.map(i => i.monitorName || i.monitorId).reverse() },
|
||||
series: [{ type: 'bar', data: d.map(i => i.temperature).reverse(), itemStyle: { color: '#409eff' }, label: { show: true, position: 'right', formatter: '{c}℃' } }]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => { initDefaultTimeRange(); handleQuery() })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.kpi-card { text-align: center }
|
||||
.kpi-high :deep(.el-statistic__number) { color: #f56c6c }
|
||||
.kpi-low :deep(.el-statistic__number) { color: #409eff }
|
||||
</style>
|
||||
@ -0,0 +1,160 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-card shadow="never">
|
||||
<el-form :inline="true" :model="queryForm" size="small">
|
||||
<el-form-item label="时间范围">
|
||||
<el-date-picker v-model="timeRange" type="datetimerange" value-format="YYYY-MM-DD HH:mm:ss"
|
||||
range-separator="至" start-placeholder="开始时间" end-placeholder="结束时间" style="width: 380px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="高温阈值">
|
||||
<el-input-number v-model="queryForm.highTempThreshold" :min="0" :max="100" :precision="1" style="width: 120px" />
|
||||
<span class="ml-1 text-xs text-gray-400">℃</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="低温阈值">
|
||||
<el-input-number v-model="queryForm.lowTempThreshold" :min="-50" :max="50" :precision="1" style="width: 120px" />
|
||||
<span class="ml-1 text-xs text-gray-400">℃</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="未更新阈值">
|
||||
<el-input-number v-model="queryForm.staleThresholdSeconds" :min="60" :max="86400" :step="60" style="width: 120px" />
|
||||
<span class="ml-1 text-xs text-gray-400">秒</span>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="Search" @click="handleQuery">查询</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<!-- 实时温度明细表 -->
|
||||
<el-card shadow="never" class="mt-4">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-bold">实时温度明细</span>
|
||||
<el-tag size="small">共 {{ detailList.length }} 条</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
<el-table v-loading="loading" :data="detailList" stripe size="small" max-height="400">
|
||||
<el-table-column prop="monitorId" label="测点ID" width="150" />
|
||||
<el-table-column prop="monitorName" label="测点名称" width="180" />
|
||||
<el-table-column prop="temperature" label="温度" width="100" align="right">
|
||||
<template #default="{ row }">
|
||||
<span :class="row.temperature >= (queryForm.highTempThreshold ?? 35) ? 'text-red-500 font-bold' : row.temperature <= (queryForm.lowTempThreshold ?? 10) ? 'text-blue-500 font-bold' : ''">
|
||||
{{ row.temperature?.toFixed(2) }}℃
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="collectTime" label="采集时间" width="200" />
|
||||
<el-table-column prop="recodeTime" label="入库时间" width="200" />
|
||||
<el-table-column prop="delaySeconds" label="入库延迟" width="100" align="right">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.delaySeconds < 10 ? 'success' : row.delaySeconds < 30 ? 'warning' : 'danger'" size="small">
|
||||
{{ row.delaySeconds }}s
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="staleSeconds" label="数据新鲜度" width="100" align="right">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.staleSeconds < 60 ? 'success' : row.staleSeconds < 300 ? 'warning' : 'danger'" size="small">
|
||||
{{ formatDuration(row.staleSeconds) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<!-- 高温/低温/未更新 三列布局 -->
|
||||
<el-row :gutter="16" class="mt-4">
|
||||
<el-col :span="8">
|
||||
<el-card shadow="never">
|
||||
<template #header><el-tag type="danger" size="small">高温测点</el-tag></template>
|
||||
<el-table :data="highTempList" stripe size="small" max-height="300">
|
||||
<el-table-column prop="monitorName" label="测点" show-overflow-tooltip />
|
||||
<el-table-column prop="temperature" label="温度" width="80" align="right">
|
||||
<template #default="{ row }"><span class="text-red-500 font-bold">{{ row.temperature?.toFixed(2) }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="collectTime" label="时间" width="160" />
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-card shadow="never">
|
||||
<template #header><el-tag type="primary" size="small">低温测点</el-tag></template>
|
||||
<el-table :data="lowTempList" stripe size="small" max-height="300">
|
||||
<el-table-column prop="monitorName" label="测点" show-overflow-tooltip />
|
||||
<el-table-column prop="temperature" label="温度" width="80" align="right">
|
||||
<template #default="{ row }"><span class="text-blue-500 font-bold">{{ row.temperature?.toFixed(2) }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="collectTime" label="时间" width="160" />
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-card shadow="never">
|
||||
<template #header><el-tag type="warning" size="small">长时间未更新</el-tag></template>
|
||||
<el-table :data="staleList" stripe size="small" max-height="300">
|
||||
<el-table-column prop="monitorName" label="测点" show-overflow-tooltip />
|
||||
<el-table-column prop="temperature" label="温度" width="80" align="right">
|
||||
<template #default="{ row }">{{ row.temperature?.toFixed(2) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="staleSeconds" label="未更新" width="80" align="right">
|
||||
<template #default="{ row }">{{ formatDuration(row.staleSeconds) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { getRealtimeDetail, getHighTempMonitors, getLowTempMonitors, getStaleMonitors } from '@/api/ems/report/tempBoard'
|
||||
import type { TempBoardQuery, TempBoardRealtimeVO } from '@/api/ems/report/tempBoard'
|
||||
|
||||
defineOptions({ name: 'TempBoardRealtime' })
|
||||
|
||||
const timeRange = ref<[string, string]>(['', ''])
|
||||
const queryForm = reactive<TempBoardQuery>({ highTempThreshold: 35, lowTempThreshold: 10, staleThresholdSeconds: 600 })
|
||||
const loading = ref(false)
|
||||
const detailList = ref<TempBoardRealtimeVO[]>([])
|
||||
const highTempList = ref<TempBoardRealtimeVO[]>([])
|
||||
const lowTempList = ref<TempBoardRealtimeVO[]>([])
|
||||
const staleList = ref<TempBoardRealtimeVO[]>([])
|
||||
|
||||
function initTimeRange() {
|
||||
const end = new Date()
|
||||
const start = new Date(end.getTime() - 24 * 3600 * 1000)
|
||||
const fmt = (d: Date) => {
|
||||
const p = (n: number) => n.toString().padStart(2, '0')
|
||||
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`
|
||||
}
|
||||
timeRange.value = [fmt(start), fmt(end)]
|
||||
}
|
||||
|
||||
function formatDuration(seconds: number): string {
|
||||
if (seconds < 60) return `${seconds}s`
|
||||
if (seconds < 3600) return `${Math.floor(seconds / 60)}m${seconds % 60}s`
|
||||
return `${Math.floor(seconds / 3600)}h${Math.floor((seconds % 3600) / 60)}m`
|
||||
}
|
||||
|
||||
async function handleQuery() {
|
||||
if (timeRange.value?.[0]) {
|
||||
queryForm.startTime = timeRange.value[0]
|
||||
queryForm.endTime = timeRange.value[1]
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
const [detailRes, highRes, lowRes, staleRes] = await Promise.all([
|
||||
getRealtimeDetail(queryForm),
|
||||
getHighTempMonitors(queryForm),
|
||||
getLowTempMonitors(queryForm),
|
||||
getStaleMonitors(queryForm)
|
||||
])
|
||||
detailList.value = detailRes.data
|
||||
highTempList.value = highRes.data
|
||||
lowTempList.value = lowRes.data
|
||||
staleList.value = staleRes.data
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
onMounted(() => { initTimeRange(); handleQuery() })
|
||||
</script>
|
||||
Loading…
Reference in New Issue