feat(ems\report): 新增温度专属报表

main
zangch@mesnac.com 1 month ago
parent ec693aad62
commit 7636a0028c

@ -0,0 +1,334 @@
import request from '@/utils/request'
import type { AxiosPromise } from 'axios'
// ==================== TS 类型定义 ====================
/** 温度看板通用查询参数 */
export interface TempBoardQuery {
/** 开始时间 yyyy-MM-dd HH:mm:ss */
startTime?: string
/** 结束时间 yyyy-MM-dd HH:mm:ss */
endTime?: string
/** 测点ID列表 */
monitorIds?: string[]
/** 高温阈值默认35 */
highTempThreshold?: number
/** 低温阈值默认10 */
lowTempThreshold?: number
/** 温升过快阈值 ℃/分钟默认1.0 */
rapidRiseThreshold?: number
/** 温度抖动标准差阈值默认2.0 */
stddevThreshold?: number
/** 采样间隔异常阈值 秒默认300 */
gapThresholdSeconds?: number
/** 长时间未更新阈值 秒默认600 */
staleThresholdSeconds?: number
/** TopN 数量默认10 */
topN?: number
/** 单测点趋势查询 */
monitorId?: string
/** 预期采样数(完整率计算用) */
expectedSampleCount?: number
}
/** 测点温度排行项 */
export interface MonitorTempRank {
monitorId: string
monitorName: string
temperature: number
collectTime: string
}
/** 数据新鲜度项 */
export interface FreshnessItem {
monitorId: string
monitorName: string
temperature: number
collectTime: string
ageSeconds: number
}
/** 温度总览 VO */
export interface TempBoardOverviewVO {
monitorCount: number
avgLatestTemp: number
maxLatestTemp: number
maxTempMonitorId: string
minLatestTemp: number
minTempMonitorId: string
highTempTopN: MonitorTempRank[]
lowTempTopN: MonitorTempRank[]
freshnessList: FreshnessItem[]
}
/** 实时监控 VO */
export interface TempBoardRealtimeVO {
monitorId: string
monitorName: string
temperature: number
collectTime: string
recodeTime: string
delaySeconds: number
staleSeconds: number
}
/** 趋势分析 VO */
export interface TempBoardTrendVO {
statTime: string
monitorId: string
monitorName: string
avgTemp: number
maxTemp: number
minTemp: number
changeRate: number
sampleCount: number
peakTemp: number
peakTime: string
valleyTemp: number
valleyTime: string
prevTemp: number
prevTime: string
}
/** 分布分析 VO */
export interface TempBoardDistributionVO {
tempBucket: string
tempBin: number
sampleCount: number
statDate: string
statHour: number
avgTemp: number
monitorId: string
monitorName: string
temperature: number
}
/** 异常预警 VO */
export interface TempBoardAnomalyVO {
monitorId: string
monitorName: string
temperature: number
collectTime: string
anomalyType: string
risePerMin: number
tempStddev: number
startTime: string
endTime: string
maxTemp: number
sampleCount: number
prevTemp: number
prevTime: string
statTime: string
}
/** 对比分析 VO */
export interface TempBoardComparisonVO {
monitorId: string
monitorName: string
avgTemp: number
maxTemp: number
minTemp: number
tempRange: number
tempStddev: number
statDate: string
todayAvg: number
yesterdayAvg: number
diffValue: number
}
/** 数据质量 VO */
export interface TempBoardQualityVO {
monitorId: string
monitorName: string
delayBucket: string
sampleCount: number
collectTime: string
recodeTime: string
delaySeconds: number
prevTime: string
gapSeconds: number
actualCount: number
expectedCount: number
completenessRate: number
firstTime: string
lastTime: string
temperature: number
}
/** 高级分析 VO */
export interface TempBoardAdvancedVO {
fromNode: string
toNode: string
flowCount: number
statTime: string
monitorId: string
monitorName: string
avgTemp: number
sampleCount: number
tempBucket: string
maxTemp: number
minTemp: number
tempStddev: number
avgDelay: number
}
// ==================== API 函数 ====================
const BASE_URL = '/ems/report/tempBoard'
// --- A. 温度总览 ---
/** 温度总览 */
export const getTempOverview = (query: TempBoardQuery): AxiosPromise<TempBoardOverviewVO> =>
request({ url: `${BASE_URL}/overview`, method: 'get', params: query })
// --- B. 实时监控 ---
/** 实时温度明细 */
export const getRealtimeDetail = (query: TempBoardQuery): AxiosPromise<TempBoardRealtimeVO[]> =>
request({ url: `${BASE_URL}/realtime/detail`, method: 'get', params: query })
/** 高温测点 */
export const getHighTempMonitors = (query: TempBoardQuery): AxiosPromise<TempBoardRealtimeVO[]> =>
request({ url: `${BASE_URL}/realtime/highTemp`, method: 'get', params: query })
/** 低温测点 */
export const getLowTempMonitors = (query: TempBoardQuery): AxiosPromise<TempBoardRealtimeVO[]> =>
request({ url: `${BASE_URL}/realtime/lowTemp`, method: 'get', params: query })
/** 长时间未更新测点 */
export const getStaleMonitors = (query: TempBoardQuery): AxiosPromise<TempBoardRealtimeVO[]> =>
request({ url: `${BASE_URL}/realtime/stale`, method: 'get', params: query })
/** 入库延迟排行 */
export const getDelayRanking = (query: TempBoardQuery): AxiosPromise<TempBoardRealtimeVO[]> =>
request({ url: `${BASE_URL}/realtime/delay`, method: 'get', params: query })
// --- C. 趋势分析 ---
/** 单测点分钟趋势 */
export const getMinuteTrend = (query: TempBoardQuery): AxiosPromise<TempBoardTrendVO[]> =>
request({ url: `${BASE_URL}/trend/minute`, method: 'get', params: query })
/** 单测点小时趋势 */
export const getHourlyTrend = (query: TempBoardQuery): AxiosPromise<TempBoardTrendVO[]> =>
request({ url: `${BASE_URL}/trend/hourly`, method: 'get', params: query })
/** 多测点对比趋势 */
export const getMultiCompareTrend = (query: TempBoardQuery): AxiosPromise<TempBoardTrendVO[]> =>
request({ url: `${BASE_URL}/trend/multiCompare`, method: 'get', params: query })
/** 日均温趋势 */
export const getDailyTrend = (query: TempBoardQuery): AxiosPromise<TempBoardTrendVO[]> =>
request({ url: `${BASE_URL}/trend/daily`, method: 'get', params: query })
/** 温度变化率趋势 */
export const getChangeRateTrend = (query: TempBoardQuery): AxiosPromise<TempBoardTrendVO[]> =>
request({ url: `${BASE_URL}/trend/changeRate`, method: 'get', params: query })
/** 峰谷时刻表 */
export const getPeakValley = (query: TempBoardQuery): AxiosPromise<TempBoardTrendVO[]> =>
request({ url: `${BASE_URL}/trend/peakValley`, method: 'get', params: query })
// --- D. 分布分析 ---
/** 温度区间分布 */
export const getIntervalDistribution = (query: TempBoardQuery): AxiosPromise<TempBoardDistributionVO[]> =>
request({ url: `${BASE_URL}/distribution/interval`, method: 'get', params: query })
/** 温度直方图 */
export const getHistogram = (query: TempBoardQuery): AxiosPromise<TempBoardDistributionVO[]> =>
request({ url: `${BASE_URL}/distribution/histogram`, method: 'get', params: query })
/** 温度箱线图数据 */
export const getBoxplotData = (query: TempBoardQuery): AxiosPromise<TempBoardDistributionVO[]> =>
request({ url: `${BASE_URL}/distribution/boxplot`, method: 'get', params: query })
/** 日历热力图 */
export const getCalendarHeatmap = (query: TempBoardQuery): AxiosPromise<TempBoardDistributionVO[]> =>
request({ url: `${BASE_URL}/distribution/calendarHeatmap`, method: 'get', params: query })
/** 小时热力图 */
export const getHourlyHeatmap = (query: TempBoardQuery): AxiosPromise<TempBoardDistributionVO[]> =>
request({ url: `${BASE_URL}/distribution/hourlyHeatmap`, method: 'get', params: query })
// --- E. 异常预警 ---
/** 高温事件 */
export const getHighTempEvents = (query: TempBoardQuery): AxiosPromise<TempBoardAnomalyVO[]> =>
request({ url: `${BASE_URL}/anomaly/highTemp`, method: 'get', params: query })
/** 低温事件 */
export const getLowTempEvents = (query: TempBoardQuery): AxiosPromise<TempBoardAnomalyVO[]> =>
request({ url: `${BASE_URL}/anomaly/lowTemp`, method: 'get', params: query })
/** 连续高温时段 */
export const getContinuousHighTemp = (query: TempBoardQuery): AxiosPromise<TempBoardAnomalyVO[]> =>
request({ url: `${BASE_URL}/anomaly/continuousHighTemp`, method: 'get', params: query })
/** 温升过快事件 */
export const getRapidRiseEvents = (query: TempBoardQuery): AxiosPromise<TempBoardAnomalyVO[]> =>
request({ url: `${BASE_URL}/anomaly/rapidRise`, method: 'get', params: query })
/** 温度抖动异常 */
export const getJitterAnomalies = (query: TempBoardQuery): AxiosPromise<TempBoardAnomalyVO[]> =>
request({ url: `${BASE_URL}/anomaly/jitter`, method: 'get', params: query })
// --- F. 对比分析 ---
/** 测点平均温度排行 */
export const getAvgTempRanking = (query: TempBoardQuery): AxiosPromise<TempBoardComparisonVO[]> =>
request({ url: `${BASE_URL}/comparison/avgRanking`, method: 'get', params: query })
/** 测点稳定性排行 */
export const getStabilityRanking = (query: TempBoardQuery): AxiosPromise<TempBoardComparisonVO[]> =>
request({ url: `${BASE_URL}/comparison/stabilityRanking`, method: 'get', params: query })
/** 今日vs昨日对比 */
export const getDailyDiff = (query: TempBoardQuery): AxiosPromise<TempBoardComparisonVO[]> =>
request({ url: `${BASE_URL}/comparison/dailyDiff`, method: 'get', params: query })
/** 峰值对比 */
export const getPeakCompare = (query: TempBoardQuery): AxiosPromise<TempBoardComparisonVO[]> =>
request({ url: `${BASE_URL}/comparison/peak`, method: 'get', params: query })
/** 波动幅度对比 */
export const getFluctuationCompare = (query: TempBoardQuery): AxiosPromise<TempBoardComparisonVO[]> =>
request({ url: `${BASE_URL}/comparison/fluctuation`, method: 'get', params: query })
// --- G. 数据质量 ---
/** 入库延迟分布 */
export const getDelayDistribution = (query: TempBoardQuery): AxiosPromise<TempBoardQualityVO[]> =>
request({ url: `${BASE_URL}/quality/delayDistribution`, method: 'get', params: query })
/** 时间逆序可疑数据 */
export const getTimeReversal = (query: TempBoardQuery): AxiosPromise<TempBoardQualityVO[]> =>
request({ url: `${BASE_URL}/quality/timeReversal`, method: 'get', params: query })
/** 采样间隔异常 */
export const getSamplingGapAnomalies = (query: TempBoardQuery): AxiosPromise<TempBoardQualityVO[]> =>
request({ url: `${BASE_URL}/quality/samplingGap`, method: 'get', params: query })
/** 数据完整率 */
export const getCompletenessRate = (query: TempBoardQuery): AxiosPromise<TempBoardQualityVO[]> =>
request({ url: `${BASE_URL}/quality/completeness`, method: 'get', params: query })
/** 测点活跃度 */
export const getMonitorActivity = (query: TempBoardQuery): AxiosPromise<TempBoardQualityVO[]> =>
request({ url: `${BASE_URL}/quality/activity`, method: 'get', params: query })
// --- H. 高级分析 ---
/** 桑基图数据 */
export const getSankeyData = (query: TempBoardQuery): AxiosPromise<TempBoardAdvancedVO[]> =>
request({ url: `${BASE_URL}/advanced/sankey`, method: 'get', params: query })
/** 主题河流图数据 */
export const getThemeRiverData = (query: TempBoardQuery): AxiosPromise<TempBoardAdvancedVO[]> =>
request({ url: `${BASE_URL}/advanced/themeRiver`, method: 'get', params: query })
/** 矩形树图数据 */
export const getTreemapData = (query: TempBoardQuery): AxiosPromise<TempBoardAdvancedVO[]> =>
request({ url: `${BASE_URL}/advanced/treemap`, method: 'get', params: query })
/** 旭日图数据 */
export const getSunburstData = (query: TempBoardQuery): AxiosPromise<TempBoardAdvancedVO[]> =>
request({ url: `${BASE_URL}/advanced/sunburst`, method: 'get', params: query })
/** 平行坐标图数据 */
export const getParallelData = (query: TempBoardQuery): AxiosPromise<TempBoardAdvancedVO[]> =>
request({ url: `${BASE_URL}/advanced/parallel`, method: 'get', params: query })

@ -0,0 +1,175 @@
<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>
<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><span class="font-bold">温区流转桑基图</span></template>
<div ref="sankeyChartRef" style="height: 400px" />
</el-card>
<!-- 主题河流图 -->
<el-card shadow="never" class="mt-4">
<template #header><span class="font-bold">温度主题河流图</span></template>
<div ref="themeRiverChartRef" style="height: 400px" />
</el-card>
<!-- 矩形树图 + 旭日图 -->
<el-row :gutter="16" class="mt-4">
<el-col :span="12">
<el-card shadow="never">
<template #header><span class="font-bold">温度矩形树图按测点平均温度</span></template>
<div ref="treemapChartRef" style="height: 400px" />
</el-card>
</el-col>
<el-col :span="12">
<el-card shadow="never">
<template #header><span class="font-bold">温度旭日图温区测点</span></template>
<div ref="sunburstChartRef" style="height: 400px" />
</el-card>
</el-col>
</el-row>
<!-- 平行坐标图 -->
<el-card shadow="never" class="mt-4">
<template #header><span class="font-bold">测点多维温度画像平行坐标图</span></template>
<div ref="parallelChartRef" style="height: 400px" />
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, nextTick } from 'vue'
import * as echarts from 'echarts'
import { getSankeyData, getThemeRiverData, getTreemapData, getSunburstData, getParallelData } from '@/api/ems/report/tempBoard'
import type { TempBoardQuery, TempBoardAdvancedVO } from '@/api/ems/report/tempBoard'
defineOptions({ name: 'TempBoardAdvanced' })
const timeRange = ref<[string, string]>(['', ''])
const queryForm = reactive<TempBoardQuery>({})
const loading = ref(false)
const sankeyChartRef = ref<HTMLElement>()
const themeRiverChartRef = ref<HTMLElement>()
const treemapChartRef = ref<HTMLElement>()
const sunburstChartRef = ref<HTMLElement>()
const parallelChartRef = ref<HTMLElement>()
function initTimeRange() {
const end = new Date(); const start = new Date(end.getTime() - 7 * 24 * 3600 * 1000)
const p = (n: number) => n.toString().padStart(2, '0')
const fmt = (d: Date) => `${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 [skRes, trRes, tmRes, sbRes, plRes] = await Promise.all([
getSankeyData(queryForm), getThemeRiverData(queryForm),
getTreemapData(queryForm), getSunburstData(queryForm), getParallelData(queryForm)
])
await nextTick()
renderSankey(skRes.data)
renderThemeRiver(trRes.data)
renderTreemap(tmRes.data)
renderSunburst(sbRes.data)
renderParallel(plRes.data)
} finally { loading.value = false }
}
function renderSankey(data: TempBoardAdvancedVO[]) {
if (!sankeyChartRef.value || !data.length) return
const chart = echarts.init(sankeyChartRef.value)
const nodes = new Set<string>()
data.forEach(d => { nodes.add(d.fromNode!); nodes.add(d.toNode!) })
chart.setOption({
tooltip: { trigger: 'item' },
series: [{
type: 'sankey', layout: 'none', emphasis: { focus: 'adjacency' },
data: [...nodes].map(n => ({ name: n })),
links: data.map(d => ({ source: d.fromNode, target: d.toNode, value: d.flowCount })),
lineStyle: { color: 'gradient', curveness: 0.5 }
}]
})
}
function renderThemeRiver(data: TempBoardAdvancedVO[]) {
if (!themeRiverChartRef.value || !data.length) return
const chart = echarts.init(themeRiverChartRef.value)
chart.setOption({
tooltip: { trigger: 'axis' },
series: [{
type: 'themeRiver',
data: data.map(d => [d.statTime, d.avgTemp, d.monitorName || d.monitorId]),
emphasis: { itemStyle: { shadowBlur: 20, shadowColor: 'rgba(0,0,0,0.8)' } }
}]
})
}
function renderTreemap(data: TempBoardAdvancedVO[]) {
if (!treemapChartRef.value || !data.length) return
const chart = echarts.init(treemapChartRef.value)
chart.setOption({
tooltip: { formatter: (p: any) => `${p.name}: ${p.value?.toFixed(2)}℃ (${data.find(d => d.monitorName === p.name || d.monitorId === p.name)?.sampleCount}样本)` },
series: [{
type: 'treemap',
data: data.map(d => ({ name: d.monitorName || d.monitorId, value: d.avgTemp })),
leafDepth: 1, roam: false,
label: { show: true, formatter: '{b}\n{c}℃' }
}]
})
}
function renderSunburst(data: TempBoardAdvancedVO[]) {
if (!sunburstChartRef.value || !data.length) return
const chart = echarts.init(sunburstChartRef.value)
//
const bucketMap = new Map<string, { name: string; value: number; children: any[] }>()
data.forEach(d => {
if (!bucketMap.has(d.tempBucket!)) {
bucketMap.set(d.tempBucket!, { name: d.tempBucket!, value: 0, children: [] })
}
const bucket = bucketMap.get(d.tempBucket!)!
bucket.children.push({ name: d.monitorName || d.monitorId, value: d.sampleCount })
bucket.value += d.sampleCount ?? 0
})
chart.setOption({
tooltip: { formatter: '{b}: {c}样本' },
series: [{ type: 'sunburst', data: [...bucketMap.values()], radius: ['15%', '90%'], label: { rotate: 'radial' } }]
})
}
function renderParallel(data: TempBoardAdvancedVO[]) {
if (!parallelChartRef.value || !data.length) return
const chart = echarts.init(parallelChartRef.value)
const dimNames = ['测点', '平均温度', '最高温度', '最低温度', '标准差', '平均延迟(s)']
const fields = ['avgTemp', 'maxTemp', 'minTemp', 'tempStddev', 'avgDelay'] as const
chart.setOption({
tooltip: { trigger: 'item' },
parallelAxis: [
{ type: 'category', data: data.map(d => d.monitorName || d.monitorId), dim: 0, name: dimNames[0], axisLabel: { rotate: 30 } },
...fields.map((f, i) => ({ dim: i + 1, name: dimNames[i + 1] }))
],
parallel: { left: 100, right: 50, top: 50, bottom: 30 },
series: [{
type: 'parallel',
data: data.map(d => [d.monitorName || d.monitorId, d.avgTemp, d.maxTemp, d.minTemp, d.tempStddev, d.avgDelay]),
lineStyle: { width: 2, opacity: 0.5 }
}]
})
}
onMounted(() => { initTimeRange(); handleQuery() })
</script>

@ -0,0 +1,158 @@
<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.rapidRiseThreshold" :min="0.1" :max="10" :precision="1" :step="0.1" style="width: 120px" />
<span class="ml-1 text-xs text-gray-400">/min</span>
</el-form-item>
<el-form-item label="抖动阈值">
<el-input-number v-model="queryForm.stddevThreshold" :min="0.1" :max="10" :precision="1" :step="0.1" 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-row :gutter="16" class="mt-4">
<el-col :span="6">
<el-card shadow="hover" class="kpi-card"><el-statistic title="高温事件" :value="highTempList.length" /></el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="kpi-card"><el-statistic title="低温事件" :value="lowTempList.length" /></el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="kpi-card"><el-statistic title="温升过快" :value="rapidRiseList.length" /></el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="kpi-card"><el-statistic title="温度抖动" :value="jitterList.length" /></el-card>
</el-col>
</el-row>
<!-- 高温事件 + 低温事件 -->
<el-row :gutter="16" class="mt-4">
<el-col :span="12">
<el-card shadow="never">
<template #header><el-tag type="danger" size="small">高温事件明细</el-tag></template>
<el-table :data="highTempList" stripe size="small" max-height="320">
<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="12">
<el-card shadow="never">
<template #header><el-tag type="primary" size="small">低温事件明细</el-tag></template>
<el-table :data="lowTempList" stripe size="small" max-height="320">
<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-row>
<!-- 连续高温时段 -->
<el-card shadow="never" class="mt-4">
<template #header><span class="font-bold">连续高温时段</span></template>
<el-table :data="continuousList" stripe size="small" max-height="300">
<el-table-column prop="monitorId" label="测点ID" width="150" />
<el-table-column prop="monitorName" label="测点名称" width="180" />
<el-table-column prop="startTime" label="开始时间" width="200" />
<el-table-column prop="endTime" label="结束时间" width="200" />
<el-table-column prop="maxTemp" label="最高温" width="100" align="right">
<template #default="{ row }"><span class="text-red-500 font-bold">{{ row.maxTemp?.toFixed(2) }}</span></template>
</el-table-column>
<el-table-column prop="sampleCount" label="持续样本数" width="100" align="right" />
</el-table>
</el-card>
<!-- 温升过快 + 温度抖动 -->
<el-row :gutter="16" class="mt-4">
<el-col :span="12">
<el-card shadow="never">
<template #header><el-tag type="warning" size="small">温升过快事件</el-tag></template>
<el-table :data="rapidRiseList" stripe size="small" max-height="320">
<el-table-column prop="monitorName" label="测点" show-overflow-tooltip />
<el-table-column prop="risePerMin" label="温升速率" width="90" align="right">
<template #default="{ row }"><span class="text-orange-500 font-bold">{{ row.risePerMin?.toFixed(2) }}</span></template>
</el-table-column>
<el-table-column prop="collectTime" label="时间" width="160" />
</el-table>
</el-card>
</el-col>
<el-col :span="12">
<el-card shadow="never">
<template #header><el-tag type="info" size="small">温度抖动异常按小时</el-tag></template>
<el-table :data="jitterList" stripe size="small" max-height="320">
<el-table-column prop="monitorName" label="测点" show-overflow-tooltip />
<el-table-column prop="tempStddev" label="标准差" width="80" align="right">
<template #default="{ row }"><span class="text-purple-500 font-bold">{{ row.tempStddev?.toFixed(4) }}</span></template>
</el-table-column>
<el-table-column prop="statTime" label="时段" width="160" />
</el-table>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { getHighTempEvents, getLowTempEvents, getContinuousHighTemp, getRapidRiseEvents, getJitterAnomalies } from '@/api/ems/report/tempBoard'
import type { TempBoardQuery, TempBoardAnomalyVO } from '@/api/ems/report/tempBoard'
defineOptions({ name: 'TempBoardAnomaly' })
const timeRange = ref<[string, string]>(['', ''])
const queryForm = reactive<TempBoardQuery>({ highTempThreshold: 35, lowTempThreshold: 10, rapidRiseThreshold: 1.0, stddevThreshold: 2.0 })
const loading = ref(false)
const highTempList = ref<TempBoardAnomalyVO[]>([])
const lowTempList = ref<TempBoardAnomalyVO[]>([])
const continuousList = ref<TempBoardAnomalyVO[]>([])
const rapidRiseList = ref<TempBoardAnomalyVO[]>([])
const jitterList = ref<TempBoardAnomalyVO[]>([])
function initTimeRange() {
const end = new Date()
const start = new Date(end.getTime() - 24 * 3600 * 1000)
const p = (n: number) => n.toString().padStart(2, '0')
const fmt = (d: Date) => `${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 [htRes, ltRes, cRes, rrRes, jRes] = await Promise.all([
getHighTempEvents(queryForm), getLowTempEvents(queryForm),
getContinuousHighTemp(queryForm), getRapidRiseEvents(queryForm), getJitterAnomalies(queryForm)
])
highTempList.value = htRes.data; lowTempList.value = ltRes.data
continuousList.value = cRes.data; rapidRiseList.value = rrRes.data; jitterList.value = jRes.data
} finally { loading.value = false }
}
onMounted(() => { initTimeRange(); handleQuery() })
</script>

@ -0,0 +1,135 @@
<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>
<el-button type="primary" icon="Search" @click="handleQuery"></el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 平均温度排行 -->
<el-row :gutter="16" class="mt-4">
<el-col :span="12">
<el-card shadow="never">
<template #header><span class="font-bold">测点平均温度排行</span></template>
<div ref="avgRankChartRef" style="height: 360px" />
</el-card>
</el-col>
<el-col :span="12">
<el-card shadow="never">
<template #header><span class="font-bold">测点稳定性排行标准差升序</span></template>
<div ref="stabilityChartRef" style="height: 360px" />
</el-card>
</el-col>
</el-row>
<!-- 今日vs昨日 + 波动幅度 -->
<el-row :gutter="16" class="mt-4">
<el-col :span="12">
<el-card shadow="never">
<template #header><span class="font-bold">今日 vs 昨日 温度对比</span></template>
<div ref="dailyDiffChartRef" style="height: 360px" />
</el-card>
</el-col>
<el-col :span="12">
<el-card shadow="never">
<template #header><span class="font-bold">波动幅度对比</span></template>
<div ref="fluctuationChartRef" style="height: 360px" />
</el-card>
</el-col>
</el-row>
<!-- 峰值对比表 -->
<el-card shadow="never" class="mt-4">
<template #header><span class="font-bold">峰值温度排行</span></template>
<el-table :data="peakList" stripe size="small" max-height="400">
<el-table-column prop="monitorId" label="测点ID" width="150" />
<el-table-column prop="monitorName" label="测点名称" width="200" />
<el-table-column prop="maxTemp" label="峰值温度" width="120" align="right">
<template #default="{ row }"><span class="font-bold">{{ row.maxTemp?.toFixed(2) }}</span></template>
</el-table-column>
<el-table-column prop="avgTemp" label="平均温度" width="120" align="right">
<template #default="{ row }">{{ row.avgTemp?.toFixed(2) }}</template>
</el-table-column>
<el-table-column prop="tempRange" label="波动幅度" width="120" align="right">
<template #default="{ row }">{{ row.tempRange?.toFixed(2) }}</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 { getAvgTempRanking, getStabilityRanking, getDailyDiff, getPeakCompare, getFluctuationCompare } from '@/api/ems/report/tempBoard'
import type { TempBoardQuery, TempBoardComparisonVO } from '@/api/ems/report/tempBoard'
defineOptions({ name: 'TempBoardComparison' })
const timeRange = ref<[string, string]>(['', ''])
const queryForm = reactive<TempBoardQuery>({})
const loading = ref(false)
const peakList = ref<TempBoardComparisonVO[]>([])
const avgRankChartRef = ref<HTMLElement>()
const stabilityChartRef = ref<HTMLElement>()
const dailyDiffChartRef = ref<HTMLElement>()
const fluctuationChartRef = ref<HTMLElement>()
function initTimeRange() {
const end = new Date(); const start = new Date(end.getTime() - 24 * 3600 * 1000)
const p = (n: number) => n.toString().padStart(2, '0')
const fmt = (d: Date) => `${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 renderBarH(el: HTMLElement | undefined, data: TempBoardComparisonVO[], field: string, color: string, label: string) {
if (!el || !data.length) return
const chart = echarts.init(el)
chart.setOption({
tooltip: { trigger: 'axis' },
grid: { left: 120, right: 50, top: 10, bottom: 20 },
xAxis: { type: 'value', name: label },
yAxis: { type: 'category', data: data.map(d => d.monitorName || d.monitorId).reverse() },
series: [{ type: 'bar', data: data.map(d => d[field as keyof TempBoardComparisonVO]).reverse(), itemStyle: { color }, label: { show: true, position: 'right', formatter: '{c}' } }]
})
}
async function handleQuery() {
if (timeRange.value?.[0]) { queryForm.startTime = timeRange.value[0]; queryForm.endTime = timeRange.value[1] }
loading.value = true
try {
const [avgRes, stabRes, diffRes, peakRes, fluctRes] = await Promise.all([
getAvgTempRanking(queryForm), getStabilityRanking(queryForm),
getDailyDiff(queryForm), getPeakCompare(queryForm), getFluctuationCompare(queryForm)
])
peakList.value = peakRes.data
await nextTick()
renderBarH(avgRankChartRef.value, avgRes.data, 'avgTemp', '#e6a23c', '℃')
renderBarH(stabilityChartRef.value, stabRes.data, 'tempStddev', '#67c23a', 'σ')
renderBarH(fluctuationChartRef.value, fluctRes.data, 'tempRange', '#409eff', '℃')
// vs
if (dailyDiffChartRef.value && diffRes.data.length) {
const chart = echarts.init(dailyDiffChartRef.value)
chart.setOption({
tooltip: { trigger: 'axis' },
legend: { data: ['今日', '昨日'] },
grid: { left: 120, right: 30, top: 40, bottom: 20 },
yAxis: { type: 'category', data: diffRes.data.map(d => d.monitorName || d.monitorId) },
xAxis: { type: 'value', name: '℃' },
series: [
{ name: '今日', type: 'bar', data: diffRes.data.map(d => d.todayAvg), itemStyle: { color: '#409eff' } },
{ name: '昨日', type: 'bar', data: diffRes.data.map(d => d.yesterdayAvg), itemStyle: { color: '#909399' } }
]
})
}
} finally { loading.value = false }
}
onMounted(() => { initTimeRange(); handleQuery() })
</script>

@ -0,0 +1,208 @@
<template>
<div class="app-container">
<!-- 筛选区 -->
<el-card shadow="never" class="filter-card">
<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" suffix="℃">
<template #suffix>
<span class="text-xs"></span>
<span v-if="overview.maxTempMonitorId" class="text-xs text-gray-400 ml-2">{{ overview.maxTempMonitorId }}</span>
</template>
</el-statistic>
</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" suffix="℃">
<template #suffix>
<span class="text-xs"></span>
<span v-if="overview.minTempMonitorId" class="text-xs text-gray-400 ml-2">{{ overview.minTempMonitorId }}</span>
</template>
</el-statistic>
</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, type TempBoardOverviewVO, type TempBoardQuery } 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>()
/** 初始化默认时间范围最近24小时 */
function initDefaultTimeRange() {
const end = new Date()
const start = new Date(end.getTime() - 24 * 3600 * 1000)
const fmt = (d: Date) => {
const pad = (n: number) => n.toString().padStart(2, '0')
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(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() {
queryForm.highTempThreshold = 35
queryForm.lowTempThreshold = 10
queryForm.topN = 10
initDefaultTimeRange()
handleQuery()
}
function renderTopNCharts() {
// TopN
if (highTopNChartRef.value && overview.value.highTempTopN) {
const chart = echarts.init(highTopNChartRef.value)
const data = overview.value.highTempTopN
chart.setOption({
tooltip: { trigger: 'axis' },
grid: { left: 120, right: 40, top: 10, bottom: 20 },
xAxis: { type: 'value', name: '℃' },
yAxis: { type: 'category', data: data.map(d => d.monitorName || d.monitorId).reverse() },
series: [{
type: 'bar',
data: data.map(d => d.temperature).reverse(),
itemStyle: { color: '#f56c6c' },
label: { show: true, position: 'right', formatter: '{c}℃' }
}]
})
}
// TopN
if (lowTopNChartRef.value && overview.value.lowTempTopN) {
const chart = echarts.init(lowTopNChartRef.value)
const data = overview.value.lowTempTopN
chart.setOption({
tooltip: { trigger: 'axis' },
grid: { left: 120, right: 40, top: 10, bottom: 20 },
xAxis: { type: 'value', name: '℃' },
yAxis: { type: 'category', data: data.map(d => d.monitorName || d.monitorId).reverse() },
series: [{
type: 'bar',
data: data.map(d => d.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,139 @@
<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>
<el-button type="primary" icon="Search" @click="handleQuery"></el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 温度区间分布 + 直方图 -->
<el-row :gutter="16" class="mt-4">
<el-col :span="12">
<el-card shadow="never">
<template #header><span class="font-bold">温度区间分布</span></template>
<div ref="intervalChartRef" style="height: 360px" />
</el-card>
</el-col>
<el-col :span="12">
<el-card shadow="never">
<template #header><span class="font-bold">温度直方图1分箱</span></template>
<div ref="histogramChartRef" style="height: 360px" />
</el-card>
</el-col>
</el-row>
<!-- 日历热力图 -->
<el-card shadow="never" class="mt-4">
<template #header><span class="font-bold">日历热力图</span></template>
<div ref="calendarHeatmapRef" style="height: 280px" />
</el-card>
<!-- 小时热力图 -->
<el-card shadow="never" class="mt-4">
<template #header><span class="font-bold">小时热力图</span></template>
<div ref="hourlyHeatmapRef" style="height: 320px" />
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, nextTick } from 'vue'
import * as echarts from 'echarts'
import { getIntervalDistribution, getHistogram, getCalendarHeatmap, getHourlyHeatmap } from '@/api/ems/report/tempBoard'
import type { TempBoardQuery, TempBoardDistributionVO } from '@/api/ems/report/tempBoard'
defineOptions({ name: 'TempBoardDistribution' })
const timeRange = ref<[string, string]>(['', ''])
const queryForm = reactive<TempBoardQuery>({})
const loading = ref(false)
const intervalChartRef = ref<HTMLElement>()
const histogramChartRef = ref<HTMLElement>()
const calendarHeatmapRef = ref<HTMLElement>()
const hourlyHeatmapRef = ref<HTMLElement>()
function initTimeRange() {
const end = new Date()
const start = new Date(end.getTime() - 7 * 24 * 3600 * 1000)
const p = (n: number) => n.toString().padStart(2, '0')
const fmt = (d: Date) => `${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 [intervalRes, histRes, calRes, hourRes] = await Promise.all([
getIntervalDistribution(queryForm), getHistogram(queryForm),
getCalendarHeatmap(queryForm), getHourlyHeatmap(queryForm)
])
await nextTick()
renderIntervalChart(intervalRes.data)
renderHistogramChart(histRes.data)
renderCalendarHeatmap(calRes.data)
renderHourlyHeatmap(hourRes.data)
} finally { loading.value = false }
}
function renderIntervalChart(data: TempBoardDistributionVO[]) {
if (!intervalChartRef.value || !data.length) return
const chart = echarts.init(intervalChartRef.value)
chart.setOption({
tooltip: { trigger: 'item' },
legend: { orient: 'vertical', left: 'left' },
series: [{ type: 'pie', radius: ['40%', '70%'], data: data.map(d => ({ name: d.tempBucket, value: d.sampleCount })), emphasis: { itemStyle: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(0,0,0,0.5)' } } }]
})
}
function renderHistogramChart(data: TempBoardDistributionVO[]) {
if (!histogramChartRef.value || !data.length) return
const chart = echarts.init(histogramChartRef.value)
chart.setOption({
tooltip: { trigger: 'axis' },
grid: { left: 60, right: 30, top: 30, bottom: 40 },
xAxis: { type: 'category', data: data.map(d => `${d.tempBin}`), name: '温度' },
yAxis: { type: 'value', name: '样本数' },
series: [{ type: 'bar', data: data.map(d => d.sampleCount), itemStyle: { color: '#67c23a' } }]
})
}
function renderCalendarHeatmap(data: TempBoardDistributionVO[]) {
if (!calendarHeatmapRef.value || !data.length) return
const chart = echarts.init(calendarHeatmapRef.value)
const dates = data.map(d => d.statDate!)
const range = dates.length ? [dates[0], dates[dates.length - 1]] : []
chart.setOption({
tooltip: { formatter: (p: any) => `${p.value[0]}: ${p.value[1]?.toFixed(2)}` },
visualMap: { min: 10, max: 30, calculable: true, orient: 'horizontal', left: 'center', bottom: 0, inRange: { color: ['#313695', '#4575b4', '#74add1', '#abd9e9', '#fee090', '#fdae61', '#f46d43', '#d73027'] } },
calendar: { range, cellSize: ['auto', 30] },
series: [{ type: 'heatmap', coordinateSystem: 'calendar', data: data.map(d => [d.statDate, d.avgTemp]) }]
})
}
function renderHourlyHeatmap(data: TempBoardDistributionVO[]) {
if (!hourlyHeatmapRef.value || !data.length) return
const chart = echarts.init(hourlyHeatmapRef.value)
const hours = Array.from({ length: 24 }, (_, i) => `${i}:00`)
const dates = [...new Set(data.map(d => d.statDate!))]
chart.setOption({
tooltip: { formatter: (p: any) => `${p.value[0]} ${hours[p.value[1]]}: ${p.value[2]?.toFixed(2)}` },
grid: { left: 100, right: 60, top: 10, bottom: 60 },
xAxis: { type: 'category', data: hours, splitArea: { show: true } },
yAxis: { type: 'category', data: dates, splitArea: { show: true } },
visualMap: { min: 10, max: 30, calculable: true, orient: 'horizontal', left: 'center', bottom: 0 },
series: [{ type: 'heatmap', data: data.map(d => [d.statHour, d.statDate, d.avgTemp]), label: { show: false }, emphasis: { itemStyle: { shadowBlur: 10, shadowColor: 'rgba(0,0,0,0.5)' } } }]
})
}
onMounted(() => { initTimeRange(); handleQuery() })
</script>

@ -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,149 @@
<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.gapThresholdSeconds" :min="60" :max="86400" :step="60" style="width: 140px" />
<span class="ml-1 text-xs text-gray-400"></span>
</el-form-item>
<el-form-item label="预期采样数">
<el-input-number v-model="queryForm.expectedSampleCount" :min="1" :max="100000" style="width: 140px" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery"></el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 入库延迟分布 -->
<el-row :gutter="16" class="mt-4">
<el-col :span="12">
<el-card shadow="never">
<template #header><span class="font-bold">入库延迟分布</span></template>
<div ref="delayDistChartRef" style="height: 300px" />
</el-card>
</el-col>
<el-col :span="12">
<el-card shadow="never">
<template #header><span class="font-bold">数据完整率</span></template>
<el-table :data="completenessList" stripe size="small" max-height="300">
<el-table-column prop="monitorName" label="测点" show-overflow-tooltip />
<el-table-column prop="actualCount" label="实际数" width="80" align="right" />
<el-table-column prop="completenessRate" label="完整率" width="120" align="right">
<template #default="{ row }">
<el-progress :percentage="Math.round((row.completenessRate ?? 0) * 100)" :stroke-width="12"
:color="row.completenessRate >= 0.9 ? '#67c23a' : row.completenessRate >= 0.7 ? '#e6a23c' : '#f56c6c'" />
</template>
</el-table-column>
</el-table>
</el-card>
</el-col>
</el-row>
<!-- 时间逆序可疑数据 -->
<el-card shadow="never" class="mt-4">
<template #header>
<div class="flex items-center justify-between">
<span class="font-bold">时间逆序可疑数据recodeTime &lt; collectTime</span>
<el-tag type="danger" size="small">{{ timeReversalList.length }} </el-tag>
</div>
</template>
<el-table :data="timeReversalList" stripe size="small" max-height="300">
<el-table-column prop="monitorId" label="测点ID" width="150" />
<el-table-column prop="monitorName" label="测点名称" width="180" />
<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="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="danger" size="small">{{ row.delaySeconds }}s</el-tag></template>
</el-table-column>
</el-table>
</el-card>
<!-- 采样间隔异常 + 测点活跃度 -->
<el-row :gutter="16" class="mt-4">
<el-col :span="12">
<el-card shadow="never">
<template #header><span class="font-bold">采样间隔异常</span></template>
<el-table :data="samplingGapList" stripe size="small" max-height="300">
<el-table-column prop="monitorName" label="测点" show-overflow-tooltip />
<el-table-column prop="gapSeconds" label="间隔(秒)" width="100" align="right">
<template #default="{ row }"><el-tag type="warning" size="small">{{ row.gapSeconds }}s</el-tag></template>
</el-table-column>
<el-table-column prop="prevTime" label="前次时间" width="160" />
<el-table-column prop="collectTime" label="本次时间" width="160" />
</el-table>
</el-card>
</el-col>
<el-col :span="12">
<el-card shadow="never">
<template #header><span class="font-bold">测点活跃度</span></template>
<el-table :data="activityList" stripe size="small" max-height="300">
<el-table-column prop="monitorName" label="测点" show-overflow-tooltip />
<el-table-column prop="actualCount" label="样本数" width="80" align="right" />
<el-table-column prop="firstTime" label="首次" width="160" />
<el-table-column prop="lastTime" label="末次" width="160" />
</el-table>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, nextTick } from 'vue'
import * as echarts from 'echarts'
import { getDelayDistribution, getTimeReversal, getSamplingGapAnomalies, getCompletenessRate, getMonitorActivity } from '@/api/ems/report/tempBoard'
import type { TempBoardQuery, TempBoardQualityVO } from '@/api/ems/report/tempBoard'
defineOptions({ name: 'TempBoardQuality' })
const timeRange = ref<[string, string]>(['', ''])
const queryForm = reactive<TempBoardQuery>({ gapThresholdSeconds: 300, expectedSampleCount: 1440 })
const loading = ref(false)
const delayDistChartRef = ref<HTMLElement>()
const timeReversalList = ref<TempBoardQualityVO[]>([])
const samplingGapList = ref<TempBoardQualityVO[]>([])
const completenessList = ref<TempBoardQualityVO[]>([])
const activityList = ref<TempBoardQualityVO[]>([])
function initTimeRange() {
const end = new Date(); const start = new Date(end.getTime() - 24 * 3600 * 1000)
const p = (n: number) => n.toString().padStart(2, '0')
const fmt = (d: Date) => `${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 [ddRes, trRes, sgRes, crRes, actRes] = await Promise.all([
getDelayDistribution(queryForm), getTimeReversal(queryForm),
getSamplingGapAnomalies(queryForm), getCompletenessRate(queryForm), getMonitorActivity(queryForm)
])
timeReversalList.value = trRes.data; samplingGapList.value = sgRes.data
completenessList.value = crRes.data; activityList.value = actRes.data
await nextTick()
//
if (delayDistChartRef.value && ddRes.data.length) {
const chart = echarts.init(delayDistChartRef.value)
chart.setOption({
tooltip: { trigger: 'axis' },
grid: { left: 60, right: 30, top: 30, bottom: 30 },
xAxis: { type: 'category', data: ddRes.data.map(d => d.delayBucket) },
yAxis: { type: 'value', name: '样本数' },
series: [{ type: 'bar', data: ddRes.data.map(d => d.sampleCount), itemStyle: { color: '#e6a23c' } }]
})
}
} finally { loading.value = false }
}
onMounted(() => { initTimeRange(); handleQuery() })
</script>

@ -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>

@ -0,0 +1,113 @@
<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 v-model="queryForm.monitorId" placeholder="输入测点ID" clearable style="width: 200px" />
</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><span class="font-bold">分钟趋势</span></template>
<div ref="minuteChartRef" style="height: 360px" />
</el-card>
<!-- 小时趋势 -->
<el-card shadow="never" class="mt-4">
<template #header><span class="font-bold">小时趋势含最高/最低温带</span></template>
<div ref="hourlyChartRef" style="height: 360px" />
</el-card>
<!-- 日均温趋势 -->
<el-card shadow="never" class="mt-4">
<template #header><span class="font-bold">日均温趋势</span></template>
<div ref="dailyChartRef" style="height: 320px" />
</el-card>
<!-- 峰谷时刻表 -->
<el-card shadow="never" class="mt-4">
<template #header><span class="font-bold">峰谷时刻表</span></template>
<el-table :data="peakValleyList" stripe size="small">
<el-table-column prop="monitorId" label="测点ID" width="150" />
<el-table-column prop="monitorName" label="测点名称" width="180" />
<el-table-column prop="peakTemp" label="峰值温度" width="100" align="right">
<template #default="{ row }"><span class="text-red-500 font-bold">{{ row.peakTemp?.toFixed(2) }}</span></template>
</el-table-column>
<el-table-column prop="peakTime" label="峰值时间" width="200" />
<el-table-column prop="valleyTemp" label="谷值温度" width="100" align="right">
<template #default="{ row }"><span class="text-blue-500 font-bold">{{ row.valleyTemp?.toFixed(2) }}</span></template>
</el-table-column>
<el-table-column prop="valleyTime" label="谷值时间" width="200" />
</el-table>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, nextTick } from 'vue'
import * as echarts from 'echarts'
import { getMinuteTrend, getHourlyTrend, getDailyTrend, getPeakValley } from '@/api/ems/report/tempBoard'
import type { TempBoardQuery, TempBoardTrendVO } from '@/api/ems/report/tempBoard'
defineOptions({ name: 'TempBoardTrend' })
const timeRange = ref<[string, string]>(['', ''])
const queryForm = reactive<TempBoardQuery>({ monitorId: '' })
const loading = ref(false)
const minuteChartRef = ref<HTMLElement>()
const hourlyChartRef = ref<HTMLElement>()
const dailyChartRef = ref<HTMLElement>()
const peakValleyList = ref<TempBoardTrendVO[]>([])
function initTimeRange() {
const end = new Date()
const start = new Date(end.getTime() - 24 * 3600 * 1000)
const p = (n: number) => n.toString().padStart(2, '0')
const fmt = (d: Date) => `${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 renderLineChart(el: HTMLElement | undefined, data: TempBoardTrendVO[], options?: { showBand?: boolean }) {
if (!el || !data.length) return
const chart = echarts.init(el)
const times = data.map(d => d.statTime!)
const series: any[] = [{ name: '平均温度', type: 'line', data: data.map(d => d.avgTemp), smooth: true, lineStyle: { width: 2 } }]
if (options?.showBand) {
series.push({ name: '最高温度', type: 'line', data: data.map(d => d.maxTemp), lineStyle: { width: 1, type: 'dashed' }, itemStyle: { opacity: 0.5 } })
series.push({ name: '最低温度', type: 'line', data: data.map(d => d.minTemp), lineStyle: { width: 1, type: 'dashed' }, itemStyle: { opacity: 0.5 }, areaStyle: { color: 'rgba(64,158,255,0.1)' } })
}
chart.setOption({ tooltip: { trigger: 'axis' }, legend: { data: series.map(s => s.name) }, grid: { left: 60, right: 30, top: 40, bottom: 30 }, xAxis: { type: 'category', data: times }, yAxis: { type: 'value', name: '℃' }, series })
}
async function handleQuery() {
if (timeRange.value?.[0]) {
queryForm.startTime = timeRange.value[0]
queryForm.endTime = timeRange.value[1]
}
loading.value = true
try {
const [minRes, hourRes, dayRes, pvRes] = await Promise.all([
getMinuteTrend(queryForm),
getHourlyTrend(queryForm),
getDailyTrend(queryForm),
getPeakValley(queryForm)
])
await nextTick()
renderLineChart(minuteChartRef.value, minRes.data)
renderLineChart(hourlyChartRef.value, hourRes.data, { showBand: true })
renderLineChart(dailyChartRef.value, dayRes.data, { showBand: true })
peakValleyList.value = pvRes.data
} finally { loading.value = false }
}
onMounted(() => { initTimeRange(); handleQuery() })
</script>
Loading…
Cancel
Save