feat(温度看板): 优化高级分析页面性能并增加数据懒加载

- 重构API响应数据标准化处理
- 调整接口超时时间为60分钟
main
zangch@mesnac.com 2 months ago
parent a8c8cd6434
commit a358835d9f

@ -1,6 +1,10 @@
import request from '@/utils/request'
import type { AxiosPromise } from 'axios'
type TempBoardResponse<T> = Promise<{
code?: number
msg?: string
data: T
}>
// ==================== TS 类型定义 ====================
@ -173,162 +177,331 @@ export interface TempBoardAdvancedVO {
avgDelay: number
}
// ==================== 响应标准化 ====================
/**
* Jackson BigDecimal
* number toFixed / ECharts
*/
function toNumber(value: unknown): number | undefined {
if (value === null || value === undefined || value === '') {
return undefined
}
const result = Number(value)
return Number.isNaN(result) ? undefined : result
}
/**
* VO java.util.Date
*
*/
function toDateTimeString(value: unknown): string | undefined {
if (value === null || value === undefined || value === '') {
return undefined
}
if (typeof value === 'string') {
return value
}
const date = new Date(value as number)
if (Number.isNaN(date.getTime())) {
return undefined
}
const p = (n: number) => n.toString().padStart(2, '0')
return `${date.getFullYear()}-${p(date.getMonth() + 1)}-${p(date.getDate())} ${p(date.getHours())}:${p(date.getMinutes())}:${p(date.getSeconds())}`
}
function normalizeResponse<T>(promise: Promise<any>, normalizer: (data: any) => T): TempBoardResponse<T> {
return promise.then((res: any) => ({
...res,
// 业务层统一做一次转换,页面就可以继续按 number / string 使用字段。
data: normalizer(res?.data)
}))
}
function normalizeOverview(data: any): TempBoardOverviewVO {
return {
...data,
monitorCount: toNumber(data?.monitorCount) ?? 0,
avgLatestTemp: toNumber(data?.avgLatestTemp) ?? 0,
maxLatestTemp: toNumber(data?.maxLatestTemp) ?? 0,
minLatestTemp: toNumber(data?.minLatestTemp) ?? 0,
highTempTopN: (data?.highTempTopN ?? []).map((item: any) => ({
...item,
temperature: toNumber(item?.temperature) ?? 0,
collectTime: toDateTimeString(item?.collectTime) ?? ''
})),
lowTempTopN: (data?.lowTempTopN ?? []).map((item: any) => ({
...item,
temperature: toNumber(item?.temperature) ?? 0,
collectTime: toDateTimeString(item?.collectTime) ?? ''
})),
freshnessList: (data?.freshnessList ?? []).map((item: any) => ({
...item,
temperature: toNumber(item?.temperature) ?? 0,
collectTime: toDateTimeString(item?.collectTime) ?? '',
ageSeconds: toNumber(item?.ageSeconds) ?? 0
}))
}
}
function normalizeRealtimeList(data: any[]): TempBoardRealtimeVO[] {
return (data ?? []).map((item: any) => ({
...item,
temperature: toNumber(item?.temperature) ?? 0,
collectTime: toDateTimeString(item?.collectTime) ?? '',
recodeTime: toDateTimeString(item?.recodeTime) ?? '',
delaySeconds: toNumber(item?.delaySeconds) ?? 0,
staleSeconds: toNumber(item?.staleSeconds) ?? 0
}))
}
function normalizeTrendList(data: any[]): TempBoardTrendVO[] {
return (data ?? []).map((item: any) => ({
...item,
avgTemp: toNumber(item?.avgTemp) ?? 0,
maxTemp: toNumber(item?.maxTemp) ?? 0,
minTemp: toNumber(item?.minTemp) ?? 0,
changeRate: toNumber(item?.changeRate) ?? 0,
sampleCount: toNumber(item?.sampleCount) ?? 0,
peakTemp: toNumber(item?.peakTemp) ?? 0,
peakTime: toDateTimeString(item?.peakTime) ?? '',
valleyTemp: toNumber(item?.valleyTemp) ?? 0,
valleyTime: toDateTimeString(item?.valleyTime) ?? '',
prevTemp: toNumber(item?.prevTemp) ?? 0,
prevTime: toDateTimeString(item?.prevTime) ?? ''
}))
}
function normalizeDistributionList(data: any[]): TempBoardDistributionVO[] {
return (data ?? []).map((item: any) => ({
...item,
tempBin: toNumber(item?.tempBin) ?? 0,
sampleCount: toNumber(item?.sampleCount) ?? 0,
statHour: toNumber(item?.statHour) ?? 0,
avgTemp: toNumber(item?.avgTemp) ?? 0,
temperature: toNumber(item?.temperature) ?? 0
}))
}
function normalizeAnomalyList(data: any[]): TempBoardAnomalyVO[] {
return (data ?? []).map((item: any) => ({
...item,
temperature: toNumber(item?.temperature) ?? 0,
collectTime: toDateTimeString(item?.collectTime) ?? '',
risePerMin: toNumber(item?.risePerMin) ?? 0,
tempStddev: toNumber(item?.tempStddev) ?? 0,
startTime: toDateTimeString(item?.startTime) ?? '',
endTime: toDateTimeString(item?.endTime) ?? '',
maxTemp: toNumber(item?.maxTemp) ?? 0,
sampleCount: toNumber(item?.sampleCount) ?? 0,
prevTemp: toNumber(item?.prevTemp) ?? 0,
prevTime: toDateTimeString(item?.prevTime) ?? '',
statTime: toDateTimeString(item?.statTime) ?? item?.statTime ?? ''
}))
}
function normalizeComparisonList(data: any[]): TempBoardComparisonVO[] {
return (data ?? []).map((item: any) => ({
...item,
avgTemp: toNumber(item?.avgTemp) ?? 0,
maxTemp: toNumber(item?.maxTemp) ?? 0,
minTemp: toNumber(item?.minTemp) ?? 0,
tempRange: toNumber(item?.tempRange) ?? 0,
tempStddev: toNumber(item?.tempStddev) ?? 0,
todayAvg: toNumber(item?.todayAvg) ?? 0,
yesterdayAvg: toNumber(item?.yesterdayAvg) ?? 0,
diffValue: toNumber(item?.diffValue) ?? 0
}))
}
function normalizeQualityList(data: any[]): TempBoardQualityVO[] {
return (data ?? []).map((item: any) => ({
...item,
sampleCount: toNumber(item?.sampleCount) ?? 0,
collectTime: toDateTimeString(item?.collectTime) ?? '',
recodeTime: toDateTimeString(item?.recodeTime) ?? '',
delaySeconds: toNumber(item?.delaySeconds) ?? 0,
prevTime: toDateTimeString(item?.prevTime) ?? '',
gapSeconds: toNumber(item?.gapSeconds) ?? 0,
actualCount: toNumber(item?.actualCount) ?? 0,
expectedCount: toNumber(item?.expectedCount) ?? 0,
completenessRate: toNumber(item?.completenessRate) ?? 0,
firstTime: toDateTimeString(item?.firstTime) ?? '',
lastTime: toDateTimeString(item?.lastTime) ?? '',
temperature: toNumber(item?.temperature) ?? 0
}))
}
function normalizeAdvancedList(data: any[]): TempBoardAdvancedVO[] {
return (data ?? []).map((item: any) => ({
...item,
flowCount: toNumber(item?.flowCount) ?? 0,
statTime: toDateTimeString(item?.statTime) ?? item?.statTime ?? '',
avgTemp: toNumber(item?.avgTemp) ?? 0,
sampleCount: toNumber(item?.sampleCount) ?? 0,
maxTemp: toNumber(item?.maxTemp) ?? 0,
minTemp: toNumber(item?.minTemp) ?? 0,
tempStddev: toNumber(item?.tempStddev) ?? 0,
avgDelay: toNumber(item?.avgDelay) ?? 0
}))
}
// ==================== API 函数 ====================
const BASE_URL = '/ems/report/tempBoard'
// --- A. 温度总览 ---
/** 温度总览 */
export const getTempOverview = (query: TempBoardQuery): AxiosPromise<TempBoardOverviewVO> =>
request({ url: `${BASE_URL}/overview`, method: 'get', params: query })
export const getTempOverview = (query: TempBoardQuery): TempBoardResponse<TempBoardOverviewVO> =>
normalizeResponse(request({ url: `${BASE_URL}/overview`, method: 'get', params: query }), normalizeOverview)
// --- B. 实时监控 ---
/** 实时温度明细 */
export const getRealtimeDetail = (query: TempBoardQuery): AxiosPromise<TempBoardRealtimeVO[]> =>
request({ url: `${BASE_URL}/realtime/detail`, method: 'get', params: query })
export const getRealtimeDetail = (query: TempBoardQuery): TempBoardResponse<TempBoardRealtimeVO[]> =>
normalizeResponse(request({ url: `${BASE_URL}/realtime/detail`, method: 'get', params: query }), normalizeRealtimeList)
/** 高温测点 */
export const getHighTempMonitors = (query: TempBoardQuery): AxiosPromise<TempBoardRealtimeVO[]> =>
request({ url: `${BASE_URL}/realtime/highTemp`, method: 'get', params: query })
export const getHighTempMonitors = (query: TempBoardQuery): TempBoardResponse<TempBoardRealtimeVO[]> =>
normalizeResponse(request({ url: `${BASE_URL}/realtime/highTemp`, method: 'get', params: query }), normalizeRealtimeList)
/** 低温测点 */
export const getLowTempMonitors = (query: TempBoardQuery): AxiosPromise<TempBoardRealtimeVO[]> =>
request({ url: `${BASE_URL}/realtime/lowTemp`, method: 'get', params: query })
export const getLowTempMonitors = (query: TempBoardQuery): TempBoardResponse<TempBoardRealtimeVO[]> =>
normalizeResponse(request({ url: `${BASE_URL}/realtime/lowTemp`, method: 'get', params: query }), normalizeRealtimeList)
/** 长时间未更新测点 */
export const getStaleMonitors = (query: TempBoardQuery): AxiosPromise<TempBoardRealtimeVO[]> =>
request({ url: `${BASE_URL}/realtime/stale`, method: 'get', params: query })
export const getStaleMonitors = (query: TempBoardQuery): TempBoardResponse<TempBoardRealtimeVO[]> =>
normalizeResponse(request({ url: `${BASE_URL}/realtime/stale`, method: 'get', params: query }), normalizeRealtimeList)
/** 入库延迟排行 */
export const getDelayRanking = (query: TempBoardQuery): AxiosPromise<TempBoardRealtimeVO[]> =>
request({ url: `${BASE_URL}/realtime/delay`, method: 'get', params: query })
export const getDelayRanking = (query: TempBoardQuery): TempBoardResponse<TempBoardRealtimeVO[]> =>
normalizeResponse(request({ url: `${BASE_URL}/realtime/delay`, method: 'get', params: query }), normalizeRealtimeList)
// --- C. 趋势分析 ---
/** 单测点分钟趋势 */
export const getMinuteTrend = (query: TempBoardQuery): AxiosPromise<TempBoardTrendVO[]> =>
request({ url: `${BASE_URL}/trend/minute`, method: 'get', params: query })
export const getMinuteTrend = (query: TempBoardQuery): TempBoardResponse<TempBoardTrendVO[]> =>
normalizeResponse(request({ url: `${BASE_URL}/trend/minute`, method: 'get', params: query }), normalizeTrendList)
/** 单测点小时趋势 */
export const getHourlyTrend = (query: TempBoardQuery): AxiosPromise<TempBoardTrendVO[]> =>
request({ url: `${BASE_URL}/trend/hourly`, method: 'get', params: query })
export const getHourlyTrend = (query: TempBoardQuery): TempBoardResponse<TempBoardTrendVO[]> =>
normalizeResponse(request({ url: `${BASE_URL}/trend/hourly`, method: 'get', params: query }), normalizeTrendList)
/** 多测点对比趋势 */
export const getMultiCompareTrend = (query: TempBoardQuery): AxiosPromise<TempBoardTrendVO[]> =>
request({ url: `${BASE_URL}/trend/multiCompare`, method: 'get', params: query })
export const getMultiCompareTrend = (query: TempBoardQuery): TempBoardResponse<TempBoardTrendVO[]> =>
normalizeResponse(request({ url: `${BASE_URL}/trend/multiCompare`, method: 'get', params: query }), normalizeTrendList)
/** 日均温趋势 */
export const getDailyTrend = (query: TempBoardQuery): AxiosPromise<TempBoardTrendVO[]> =>
request({ url: `${BASE_URL}/trend/daily`, method: 'get', params: query })
export const getDailyTrend = (query: TempBoardQuery): TempBoardResponse<TempBoardTrendVO[]> =>
normalizeResponse(request({ url: `${BASE_URL}/trend/daily`, method: 'get', params: query }), normalizeTrendList)
/** 温度变化率趋势 */
export const getChangeRateTrend = (query: TempBoardQuery): AxiosPromise<TempBoardTrendVO[]> =>
request({ url: `${BASE_URL}/trend/changeRate`, method: 'get', params: query })
export const getChangeRateTrend = (query: TempBoardQuery): TempBoardResponse<TempBoardTrendVO[]> =>
normalizeResponse(request({ url: `${BASE_URL}/trend/changeRate`, method: 'get', params: query }), normalizeTrendList)
/** 峰谷时刻表 */
export const getPeakValley = (query: TempBoardQuery): AxiosPromise<TempBoardTrendVO[]> =>
request({ url: `${BASE_URL}/trend/peakValley`, method: 'get', params: query })
export const getPeakValley = (query: TempBoardQuery): TempBoardResponse<TempBoardTrendVO[]> =>
normalizeResponse(request({ url: `${BASE_URL}/trend/peakValley`, method: 'get', params: query }), normalizeTrendList)
// --- D. 分布分析 ---
/** 温度区间分布 */
export const getIntervalDistribution = (query: TempBoardQuery): AxiosPromise<TempBoardDistributionVO[]> =>
request({ url: `${BASE_URL}/distribution/interval`, method: 'get', params: query })
export const getIntervalDistribution = (query: TempBoardQuery): TempBoardResponse<TempBoardDistributionVO[]> =>
normalizeResponse(request({ url: `${BASE_URL}/distribution/interval`, method: 'get', params: query }), normalizeDistributionList)
/** 温度直方图 */
export const getHistogram = (query: TempBoardQuery): AxiosPromise<TempBoardDistributionVO[]> =>
request({ url: `${BASE_URL}/distribution/histogram`, method: 'get', params: query })
export const getHistogram = (query: TempBoardQuery): TempBoardResponse<TempBoardDistributionVO[]> =>
normalizeResponse(request({ url: `${BASE_URL}/distribution/histogram`, method: 'get', params: query }), normalizeDistributionList)
/** 温度箱线图数据 */
export const getBoxplotData = (query: TempBoardQuery): AxiosPromise<TempBoardDistributionVO[]> =>
request({ url: `${BASE_URL}/distribution/boxplot`, method: 'get', params: query })
export const getBoxplotData = (query: TempBoardQuery): TempBoardResponse<TempBoardDistributionVO[]> =>
normalizeResponse(request({ url: `${BASE_URL}/distribution/boxplot`, method: 'get', params: query }), normalizeDistributionList)
/** 日历热力图 */
export const getCalendarHeatmap = (query: TempBoardQuery): AxiosPromise<TempBoardDistributionVO[]> =>
request({ url: `${BASE_URL}/distribution/calendarHeatmap`, method: 'get', params: query })
export const getCalendarHeatmap = (query: TempBoardQuery): TempBoardResponse<TempBoardDistributionVO[]> =>
normalizeResponse(request({ url: `${BASE_URL}/distribution/calendarHeatmap`, method: 'get', params: query }), normalizeDistributionList)
/** 小时热力图 */
export const getHourlyHeatmap = (query: TempBoardQuery): AxiosPromise<TempBoardDistributionVO[]> =>
request({ url: `${BASE_URL}/distribution/hourlyHeatmap`, method: 'get', params: query })
export const getHourlyHeatmap = (query: TempBoardQuery): TempBoardResponse<TempBoardDistributionVO[]> =>
normalizeResponse(request({ url: `${BASE_URL}/distribution/hourlyHeatmap`, method: 'get', params: query }), normalizeDistributionList)
// --- E. 异常预警 ---
/** 高温事件 */
export const getHighTempEvents = (query: TempBoardQuery): AxiosPromise<TempBoardAnomalyVO[]> =>
request({ url: `${BASE_URL}/anomaly/highTemp`, method: 'get', params: query })
export const getHighTempEvents = (query: TempBoardQuery): TempBoardResponse<TempBoardAnomalyVO[]> =>
normalizeResponse(request({ url: `${BASE_URL}/anomaly/highTemp`, method: 'get', params: query }), normalizeAnomalyList)
/** 低温事件 */
export const getLowTempEvents = (query: TempBoardQuery): AxiosPromise<TempBoardAnomalyVO[]> =>
request({ url: `${BASE_URL}/anomaly/lowTemp`, method: 'get', params: query })
export const getLowTempEvents = (query: TempBoardQuery): TempBoardResponse<TempBoardAnomalyVO[]> =>
normalizeResponse(request({ url: `${BASE_URL}/anomaly/lowTemp`, method: 'get', params: query }), normalizeAnomalyList)
/** 连续高温时段 */
export const getContinuousHighTemp = (query: TempBoardQuery): AxiosPromise<TempBoardAnomalyVO[]> =>
request({ url: `${BASE_URL}/anomaly/continuousHighTemp`, method: 'get', params: query })
export const getContinuousHighTemp = (query: TempBoardQuery): TempBoardResponse<TempBoardAnomalyVO[]> =>
normalizeResponse(request({ url: `${BASE_URL}/anomaly/continuousHighTemp`, method: 'get', params: query }), normalizeAnomalyList)
/** 温升过快事件 */
export const getRapidRiseEvents = (query: TempBoardQuery): AxiosPromise<TempBoardAnomalyVO[]> =>
request({ url: `${BASE_URL}/anomaly/rapidRise`, method: 'get', params: query })
export const getRapidRiseEvents = (query: TempBoardQuery): TempBoardResponse<TempBoardAnomalyVO[]> =>
normalizeResponse(request({ url: `${BASE_URL}/anomaly/rapidRise`, method: 'get', params: query }), normalizeAnomalyList)
/** 温度抖动异常 */
export const getJitterAnomalies = (query: TempBoardQuery): AxiosPromise<TempBoardAnomalyVO[]> =>
request({ url: `${BASE_URL}/anomaly/jitter`, method: 'get', params: query })
export const getJitterAnomalies = (query: TempBoardQuery): TempBoardResponse<TempBoardAnomalyVO[]> =>
normalizeResponse(request({ url: `${BASE_URL}/anomaly/jitter`, method: 'get', params: query }), normalizeAnomalyList)
// --- F. 对比分析 ---
/** 测点平均温度排行 */
export const getAvgTempRanking = (query: TempBoardQuery): AxiosPromise<TempBoardComparisonVO[]> =>
request({ url: `${BASE_URL}/comparison/avgRanking`, method: 'get', params: query })
export const getAvgTempRanking = (query: TempBoardQuery): TempBoardResponse<TempBoardComparisonVO[]> =>
normalizeResponse(request({ url: `${BASE_URL}/comparison/avgRanking`, method: 'get', params: query }), normalizeComparisonList)
/** 测点稳定性排行 */
export const getStabilityRanking = (query: TempBoardQuery): AxiosPromise<TempBoardComparisonVO[]> =>
request({ url: `${BASE_URL}/comparison/stabilityRanking`, method: 'get', params: query })
export const getStabilityRanking = (query: TempBoardQuery): TempBoardResponse<TempBoardComparisonVO[]> =>
normalizeResponse(request({ url: `${BASE_URL}/comparison/stabilityRanking`, method: 'get', params: query }), normalizeComparisonList)
/** 今日vs昨日对比 */
export const getDailyDiff = (query: TempBoardQuery): AxiosPromise<TempBoardComparisonVO[]> =>
request({ url: `${BASE_URL}/comparison/dailyDiff`, method: 'get', params: query })
export const getDailyDiff = (query: TempBoardQuery): TempBoardResponse<TempBoardComparisonVO[]> =>
normalizeResponse(request({ url: `${BASE_URL}/comparison/dailyDiff`, method: 'get', params: query }), normalizeComparisonList)
/** 峰值对比 */
export const getPeakCompare = (query: TempBoardQuery): AxiosPromise<TempBoardComparisonVO[]> =>
request({ url: `${BASE_URL}/comparison/peak`, method: 'get', params: query })
export const getPeakCompare = (query: TempBoardQuery): TempBoardResponse<TempBoardComparisonVO[]> =>
normalizeResponse(request({ url: `${BASE_URL}/comparison/peak`, method: 'get', params: query }), normalizeComparisonList)
/** 波动幅度对比 */
export const getFluctuationCompare = (query: TempBoardQuery): AxiosPromise<TempBoardComparisonVO[]> =>
request({ url: `${BASE_URL}/comparison/fluctuation`, method: 'get', params: query })
export const getFluctuationCompare = (query: TempBoardQuery): TempBoardResponse<TempBoardComparisonVO[]> =>
normalizeResponse(request({ url: `${BASE_URL}/comparison/fluctuation`, method: 'get', params: query }), normalizeComparisonList)
// --- G. 数据质量 ---
/** 入库延迟分布 */
export const getDelayDistribution = (query: TempBoardQuery): AxiosPromise<TempBoardQualityVO[]> =>
request({ url: `${BASE_URL}/quality/delayDistribution`, method: 'get', params: query })
export const getDelayDistribution = (query: TempBoardQuery): TempBoardResponse<TempBoardQualityVO[]> =>
normalizeResponse(request({ url: `${BASE_URL}/quality/delayDistribution`, method: 'get', params: query }), normalizeQualityList)
/** 时间逆序可疑数据 */
export const getTimeReversal = (query: TempBoardQuery): AxiosPromise<TempBoardQualityVO[]> =>
request({ url: `${BASE_URL}/quality/timeReversal`, method: 'get', params: query })
export const getTimeReversal = (query: TempBoardQuery): TempBoardResponse<TempBoardQualityVO[]> =>
normalizeResponse(request({ url: `${BASE_URL}/quality/timeReversal`, method: 'get', params: query }), normalizeQualityList)
/** 采样间隔异常 */
export const getSamplingGapAnomalies = (query: TempBoardQuery): AxiosPromise<TempBoardQualityVO[]> =>
request({ url: `${BASE_URL}/quality/samplingGap`, method: 'get', params: query })
export const getSamplingGapAnomalies = (query: TempBoardQuery): TempBoardResponse<TempBoardQualityVO[]> =>
normalizeResponse(request({ url: `${BASE_URL}/quality/samplingGap`, method: 'get', params: query }), normalizeQualityList)
/** 数据完整率 */
export const getCompletenessRate = (query: TempBoardQuery): AxiosPromise<TempBoardQualityVO[]> =>
request({ url: `${BASE_URL}/quality/completeness`, method: 'get', params: query })
export const getCompletenessRate = (query: TempBoardQuery): TempBoardResponse<TempBoardQualityVO[]> =>
normalizeResponse(request({ url: `${BASE_URL}/quality/completeness`, method: 'get', params: query }), normalizeQualityList)
/** 测点活跃度 */
export const getMonitorActivity = (query: TempBoardQuery): AxiosPromise<TempBoardQualityVO[]> =>
request({ url: `${BASE_URL}/quality/activity`, method: 'get', params: query })
export const getMonitorActivity = (query: TempBoardQuery): TempBoardResponse<TempBoardQualityVO[]> =>
normalizeResponse(request({ url: `${BASE_URL}/quality/activity`, method: 'get', params: query }), normalizeQualityList)
// --- H. 高级分析 ---
/** 桑基图数据 */
export const getSankeyData = (query: TempBoardQuery): AxiosPromise<TempBoardAdvancedVO[]> =>
request({ url: `${BASE_URL}/advanced/sankey`, method: 'get', params: query })
export const getSankeyData = (query: TempBoardQuery): TempBoardResponse<TempBoardAdvancedVO[]> =>
normalizeResponse(request({ url: `${BASE_URL}/advanced/sankey`, method: 'get', params: query }), normalizeAdvancedList)
/** 主题河流图数据 */
export const getThemeRiverData = (query: TempBoardQuery): AxiosPromise<TempBoardAdvancedVO[]> =>
request({ url: `${BASE_URL}/advanced/themeRiver`, method: 'get', params: query })
export const getThemeRiverData = (query: TempBoardQuery): TempBoardResponse<TempBoardAdvancedVO[]> =>
normalizeResponse(request({ url: `${BASE_URL}/advanced/themeRiver`, method: 'get', params: query }), normalizeAdvancedList)
/** 矩形树图数据 */
export const getTreemapData = (query: TempBoardQuery): AxiosPromise<TempBoardAdvancedVO[]> =>
request({ url: `${BASE_URL}/advanced/treemap`, method: 'get', params: query })
export const getTreemapData = (query: TempBoardQuery): TempBoardResponse<TempBoardAdvancedVO[]> =>
normalizeResponse(request({ url: `${BASE_URL}/advanced/treemap`, method: 'get', params: query }), normalizeAdvancedList)
/** 旭日图数据 */
export const getSunburstData = (query: TempBoardQuery): AxiosPromise<TempBoardAdvancedVO[]> =>
request({ url: `${BASE_URL}/advanced/sunburst`, method: 'get', params: query })
export const getSunburstData = (query: TempBoardQuery): TempBoardResponse<TempBoardAdvancedVO[]> =>
normalizeResponse(request({ url: `${BASE_URL}/advanced/sunburst`, method: 'get', params: query }), normalizeAdvancedList)
/** 平行坐标图数据 */
export const getParallelData = (query: TempBoardQuery): AxiosPromise<TempBoardAdvancedVO[]> =>
request({ url: `${BASE_URL}/advanced/parallel`, method: 'get', params: query })
export const getParallelData = (query: TempBoardQuery): TempBoardResponse<TempBoardAdvancedVO[]> =>
normalizeResponse(request({ url: `${BASE_URL}/advanced/parallel`, method: 'get', params: query }), normalizeAdvancedList)

@ -28,7 +28,8 @@ axios.defaults.headers['clientid'] = import.meta.env.VITE_APP_CLIENT_ID;
// 创建 axios 实例
const service = axios.create({
baseURL: import.meta.env.VITE_APP_BASE_API,
timeout: 50000,
// 接口超时时间60000000ms = 60分钟
timeout: 60000000,
transitional: {
// 超时错误更明确
clarifyTimeoutError: true

@ -7,13 +7,13 @@
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-button type="primary" icon="Search" :loading="loadingMap.sankey" @click="handleQuery"></el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 桑基图 -->
<el-card shadow="never" class="mt-4">
<el-card v-loading="loadingMap.sankey" shadow="never" class="mt-4">
<template #header>
<div class="card-header">
<span class="font-bold">温区流转桑基图</span>
@ -24,7 +24,7 @@
</el-card>
<!-- 主题河流图 -->
<el-card shadow="never" class="mt-4">
<el-card v-loading="loadingMap.river" shadow="never" class="mt-4">
<template #header>
<div class="card-header">
<span class="font-bold">温度主题河流图</span>
@ -37,7 +37,7 @@
<!-- 矩形树图 + 旭日图 -->
<el-row :gutter="16" class="mt-4">
<el-col :xs="24" :sm="12">
<el-card shadow="never">
<el-card v-loading="loadingMap.treemap" shadow="never">
<template #header>
<div class="card-header">
<span class="font-bold">温度矩形树图按测点平均温度</span>
@ -48,7 +48,7 @@
</el-card>
</el-col>
<el-col :xs="24" :sm="12">
<el-card shadow="never">
<el-card v-loading="loadingMap.sunburst" shadow="never">
<template #header>
<div class="card-header">
<span class="font-bold">温度旭日图温区测点</span>
@ -61,7 +61,7 @@
</el-row>
<!-- 平行坐标图 -->
<el-card shadow="never" class="mt-4">
<el-card v-loading="loadingMap.parallel" shadow="never" class="mt-4">
<template #header>
<div class="card-header">
<span class="font-bold">测点多维温度画像平行坐标图</span>
@ -74,7 +74,7 @@
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, nextTick } from 'vue'
import { ref, shallowRef, reactive, onMounted, onBeforeUnmount, 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'
@ -86,14 +86,46 @@ defineOptions({ name: 'TempBoardAdvanced' })
const timeRange = ref<[string, string]>(['', ''])
const queryForm = reactive<TempBoardQuery>({})
const loading = ref(false)
// loading loading
const loadingMap = reactive({
sankey: false,
river: false,
treemap: false,
sunburst: false,
parallel: false
})
// 使 shallowRef
const sankeyData = shallowRef<TempBoardAdvancedVO[]>([])
const riverData = shallowRef<TempBoardAdvancedVO[]>([])
const treemapData = shallowRef<TempBoardAdvancedVO[]>([])
const sunburstData = shallowRef<TempBoardAdvancedVO[]>([])
const parallelData = shallowRef<TempBoardAdvancedVO[]>([])
// DOM
const sankeyChartRef = ref<HTMLElement>()
const themeRiverChartRef = ref<HTMLElement>()
const treemapChartRef = ref<HTMLElement>()
const sunburstChartRef = ref<HTMLElement>()
const parallelChartRef = ref<HTMLElement>()
const { getChart } = useChartResize(sankeyChartRef, themeRiverChartRef, treemapChartRef, sunburstChartRef, parallelChartRef)
const { getChart, disposeAll } = useChartResize(sankeyChartRef, themeRiverChartRef, treemapChartRef, sunburstChartRef, parallelChartRef)
// ==================== ====================
/** 每次查询生成唯一 ID旧请求结果自动丢弃 */
let currentQueryId = 0
let abortController: AbortController | null = null
function cancelPending() {
if (abortController) {
abortController.abort()
}
abortController = new AbortController()
}
// ==================== ====================
/** 初始化默认时间范围昨天8:30 ~ 今天8:30 */
function initTimeRange() {
@ -105,23 +137,98 @@ function initTimeRange() {
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 }
// ==================== ====================
/** 记录已触发的图表IntersectionObserver 只触发一次 */
const loadedCards = reactive(new Set<string>())
/** 通用懒加载:进入视口后发起请求并渲染 */
function lazyLoadChart(
cardKey: string,
elRef: { value: HTMLElement | undefined },
fetchFn: (params: TempBoardQuery) => Promise<any>,
renderFn: (data: TempBoardAdvancedVO[]) => void,
loadingKey: keyof typeof loadingMap
) {
if (loadedCards.has(cardKey) || !elRef.value) return
//
loadedCards.add(cardKey)
const queryId = currentQueryId
loadingMap[loadingKey] = true
fetchFn(queryForm).then(res => {
//
if (queryId === currentQueryId) {
nextTick(() => renderFn(res.data))
}
}).catch(err => {
if (err?.name !== 'CanceledError' && err?.name !== 'AbortError') {
console.error(`[${cardKey}] 加载失败:`, err)
}
}).finally(() => {
if (queryId === currentQueryId) {
loadingMap[loadingKey] = false
}
})
}
/** 为每个图表容器注册 IntersectionObserver */
const observerMap = new Map<HTMLElement, IntersectionObserver>()
function registerObserver(
cardKey: string,
elRef: { value: HTMLElement | undefined },
fetchFn: (params: TempBoardQuery) => Promise<any>,
renderFn: (data: TempBoardAdvancedVO[]) => void,
loadingKey: keyof typeof loadingMap
) {
// DOM
nextTick(() => {
const el = elRef.value
if (!el || observerMap.has(el)) return
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
lazyLoadChart(cardKey, elRef, fetchFn, renderFn, loadingKey)
observer.disconnect()
observerMap.delete(el)
}
},
{ threshold: 0.05 }
)
observer.observe(el)
observerMap.set(el, observer)
})
}
// ==================== ====================
async function handleQuery() {
if (timeRange.value?.[0]) {
queryForm.startTime = timeRange.value[0]
queryForm.endTime = timeRange.value[1]
}
//
cancelPending()
currentQueryId++
//
loadedCards.clear()
observerMap.forEach(obs => obs.disconnect())
observerMap.clear()
//
registerObserver('sankey', sankeyChartRef, getSankeyData, renderSankey, 'sankey')
registerObserver('river', themeRiverChartRef, getThemeRiverData, renderThemeRiver, 'river')
registerObserver('treemap', treemapChartRef, getTreemapData, renderTreemap, 'treemap')
registerObserver('sunburst', sunburstChartRef, getSunburstData, renderSunburst, 'sunburst')
registerObserver('parallel', parallelChartRef, getParallelData, renderParallel, 'parallel')
}
// ==================== ====================
/** 桑基图 */
function renderSankey(data: TempBoardAdvancedVO[]) {
const chart = getChart(sankeyChartRef)
@ -129,10 +236,11 @@ function renderSankey(data: TempBoardAdvancedVO[]) {
const nodes = new Set<string>()
data.forEach(d => { nodes.add(d.fromNode!); nodes.add(d.toNode!) })
chart.setOption({
animation: false,
tooltip: {
trigger: 'item',
formatter: (p: any) => p.dataType === 'edge'
? `${p.data.source}${p.data.target}<br/>流量: <b>${p.data.value}</b>`
? `${p.data.source}${p.data.target}<br/>设备数: <b>${p.data.value}</b>`
: `${p.name}`
},
series: [{
@ -145,14 +253,15 @@ function renderSankey(data: TempBoardAdvancedVO[]) {
label: { color: '#333', fontSize: 12 },
itemStyle: { borderWidth: 0 }
}]
})
}, { notMerge: true, lazyUpdate: true })
}
/** 主题河流图 */
/** 主题河流图 - 开启降采样 */
function renderThemeRiver(data: TempBoardAdvancedVO[]) {
const chart = getChart(themeRiverChartRef)
if (!chart || !data.length) return
chart.setOption({
animation: false,
tooltip: {
trigger: 'axis',
formatter: (params: any) => {
@ -164,21 +273,27 @@ function renderThemeRiver(data: TempBoardAdvancedVO[]) {
singleAxis: { type: 'time', axisLabel: { color: '#666' } },
series: [{
type: 'themeRiver',
sampling: 'lttb',
data: data.map(d => [d.statTime, d.avgTemp, d.monitorName || d.monitorId]),
emphasis: { itemStyle: { shadowBlur: 20, shadowColor: 'rgba(0,0,0,0.8)' } },
label: { show: false }
}]
})
}, { notMerge: true, lazyUpdate: true })
}
/** 矩形树图 */
/** 矩形树图 - tooltip 使用 Map 替代 O(n) find */
function renderTreemap(data: TempBoardAdvancedVO[]) {
const chart = getChart(treemapChartRef)
if (!chart || !data.length) return
// Map tooltip hover O(n) 线
const itemMap = new Map(data.map(d => [d.monitorName || d.monitorId, d]))
chart.setOption({
animation: false,
tooltip: {
formatter: (p: any) => {
const item = data.find(d => d.monitorName === p.name || d.monitorId === p.name)
const item = itemMap.get(p.name)
return `${p.name}<br/>平均温度: <b>${p.value?.toFixed(2)}℃</b>${item ? `<br/>样本数: ${item.sampleCount}` : ''}`
}
},
@ -201,32 +316,44 @@ function renderTreemap(data: TempBoardAdvancedVO[]) {
{ itemStyle: { borderColor: '#fff', borderWidth: 1, gapWidth: 1 } }
]
}]
})
}, { notMerge: true, lazyUpdate: true })
}
/** 旭日图 */
/** 旭日图 - 同一温区内同名测点合并 sampleCount */
function renderSunburst(data: TempBoardAdvancedVO[]) {
const chart = getChart(sunburstChartRef)
if (!chart || !data.length) return
//
const bucketMap = new Map<string, { name: string; value: number; children: any[] }>()
const colorMap: Record<string, string> = {
'低温': '#409eff', '偏低': '#67c23a', '正常': '#95de64',
'偏高': '#e6a23c', '高温': '#f56c6c'
}
// key = tempBucket, value = { name, children: Map<monitorName, aggregated> }
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,
itemStyle: { color: colorMap[d.tempBucket!] || undefined }
})
const mName = d.monitorName || d.monitorId
// children sampleCount
const existing = bucket.children.find(c => c.name === mName)
if (existing) {
existing.value += d.sampleCount ?? 0
} else {
bucket.children.push({
name: mName,
value: d.sampleCount ?? 0,
itemStyle: { color: colorMap[d.tempBucket!] || undefined }
})
}
bucket.value += d.sampleCount ?? 0
})
chart.setOption({
animation: false,
tooltip: { formatter: '{b}: {c}样本' },
series: [{
type: 'sunburst',
@ -239,23 +366,26 @@ function renderSunburst(data: TempBoardAdvancedVO[]) {
itemStyle: { borderWidth: 2, borderColor: '#fff' },
emphasis: { focus: 'ancestor' }
}]
})
}, { notMerge: true, lazyUpdate: true })
}
/** 平行坐标图 */
/** 平行坐标图 - 恢复 per-monitor series + scroll legend保留 IQR 轴范围 + 关闭动画 */
function renderParallel(data: TempBoardAdvancedVO[]) {
const chart = getChart(parallelChartRef)
if (!chart || !data.length) return
const dimNames = ['测点', '平均温度', '最高温度', '最低温度', '标准差', '平均延迟(s)']
const fields = ['avgTemp', 'maxTemp', 'minTemp', 'tempStddev', 'avgDelay'] as const
const colors = ['#409eff', '#67c23a', '#e6a23c', '#f56c6c', '#9b59b6', '#1abc9c', '#e74c3c', '#2ecc71']
// IQR线
// Top 100
const limited = data.slice(0, 100)
// IQR
function calcAxisRange(field: string): { min: number; max: number } {
const values = data.map(d => (d as any)[field] as number).filter(v => v != null).sort((a, b) => a - b)
const values = limited.map(d => (d as any)[field] as number).filter(v => v != null).sort((a, b) => a - b)
if (values.length === 0) return { min: 0, max: 100 }
if (values.length <= 2) {
//
const range = values[values.length - 1] - values[0]
const pad = Math.max(range * 0.1, 1)
return { min: Math.floor(values[0] - pad), max: Math.ceil(values[values.length - 1] + pad) }
@ -263,33 +393,38 @@ function renderParallel(data: TempBoardAdvancedVO[]) {
const q1 = values[Math.floor(values.length * 0.25)]
const q3 = values[Math.floor(values.length * 0.75)]
const iqr = q3 - q1
// Q1 - 1.5*IQR ~ Q3 + 1.5*IQR
const lower = Math.min(q1 - 1.5 * iqr, values[0])
const upper = Math.max(q3 + 1.5 * iqr, values[values.length - 1])
const pad = Math.max((upper - lower) * 0.05, 0.5)
return { min: Math.floor(lower - pad), max: Math.ceil(upper + pad) }
}
//
const axisRanges = fields.map(f => calcAxisRange(f))
chart.setOption({
animation: false,
tooltip: { trigger: 'item' },
legend: {
data: data.map(d => d.monitorName || d.monitorId),
data: limited.map(d => d.monitorName || d.monitorId),
top: 0,
type: 'scroll',
textStyle: { color: '#666', fontSize: 11 }
},
parallelAxis: [
{ type: 'category', data: data.map(d => d.monitorName || d.monitorId), dim: 0, name: dimNames[0], axisLabel: { rotate: 30, color: '#666' } },
{
type: 'category',
data: limited.map(d => d.monitorName || d.monitorId),
dim: 0,
name: dimNames[0],
axisLabel: { rotate: 30, color: '#666', fontSize: 10 }
},
...fields.map((f, i) => ({
type: 'value',
type: 'value' as const,
dim: i + 1,
name: dimNames[i + 1],
min: axisRanges[i].min,
max: axisRanges[i].max,
nameLocation: 'end',
nameLocation: 'end' as const,
nameTextStyle: { color: '#333', fontSize: 12 },
axisLabel: { color: '#666', fontSize: 11 },
splitLine: { show: true, lineStyle: { color: '#eee' } },
@ -297,17 +432,30 @@ function renderParallel(data: TempBoardAdvancedVO[]) {
}))
],
parallel: { left: 100, right: 50, top: 50, bottom: 30 },
series: data.map((d, idx) => ({
series: limited.map((d, idx) => ({
name: d.monitorName || d.monitorId,
type: 'parallel',
data: [[d.monitorName || d.monitorId, d.avgTemp, d.maxTemp, d.minTemp, d.tempStddev, d.avgDelay]],
lineStyle: { width: 2, opacity: 0.7, color: colors[idx % colors.length] },
smooth: true
}))
})
}, { notMerge: true, lazyUpdate: true })
}
onMounted(() => { initTimeRange(); handleQuery() })
// ==================== ====================
onMounted(() => {
initTimeRange()
handleQuery()
})
onBeforeUnmount(() => {
//
observerMap.forEach(obs => obs.disconnect())
observerMap.clear()
cancelPending()
disposeAll()
})
</script>
<style scoped>

Loading…
Cancel
Save