123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571 |
- <template>
- <div class="temperature-chart">
- <div class="chart-container">
- <!-- 趋势图 -->
- <div ref="trendChartRef" class="trend-chart" style="width: 60%; height: 600px;"></div>
- <!-- 时刻温度对比图 -->
- <div ref="momentChartRef" class="moment-chart" style="width: 40%; height: 600px;"></div>
- </div>
- </div>
- </template>
-
- <script setup>
- import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
- import * as echarts from 'echarts'
-
- // Props
- const props = defineProps({
- chartData: {
- type: Array,
- default: () => []
- },
- sensorColumns: {
- type: Array,
- default: () => []
- }
- })
-
- // 响应式数据
- const trendChartRef = ref(null)
- const momentChartRef = ref(null)
- let trendChartInstance = null
- let momentChartInstance = null
-
- // 当前选中的时刻数据
- const selectedMoment = ref(null)
-
- // 暴露方法给父组件调用
- const resizeChart = () => {
- if (trendChartInstance) {
- trendChartInstance.resize()
- }
- if (momentChartInstance) {
- momentChartInstance.resize()
- }
- }
-
- // 暴露方法
- defineExpose({
- resizeChart
- })
-
- // 初始化图表
- const initChart = () => {
- if (trendChartRef.value && momentChartRef.value) {
- trendChartInstance = echarts.init(trendChartRef.value)
- momentChartInstance = echarts.init(momentChartRef.value)
- updateCharts()
- }
- }
-
- // 更新图表数据
- const updateCharts = () => {
- if (!trendChartInstance || !momentChartInstance || !props.chartData || props.chartData.length === 0) {
- return
- }
-
- // 准备图表数据 - 按时间排序
- const sortedData = [...props.chartData].sort((a, b) => new Date(a.datetime) - new Date(b.datetime))
- const series = []
-
- // 为每个传感器创建数据系列
- props.sensorColumns.forEach(sensor => {
- // 过滤出有效的数据点
- const validDataPoints = sortedData
- .map(item => {
- const value = item[sensor.sensorName]
- return {
- datetime: item.datetime,
- value: value !== null && value !== undefined && !isNaN(value) ? parseFloat(value) : null
- }
- })
- .filter(point => point.value !== null)
-
- // 只有当传感器有有效数据时才添加到系列中
- if (validDataPoints.length > 0) {
- series.push({
- name: sensor.sensorName,
- type: 'line',
- data: validDataPoints.map(point => [point.datetime, point.value]),
- showSymbol: false,
- smooth: true,
- symbol: 'circle',
- symbolSize: 6,
- lineStyle: {
- width: 1
- },
- itemStyle: {
- borderWidth: 1
- }
- })
- }
- })
-
- // 趋势图配置
- const trendOption = {
- title: {
- text: '温度监控趋势图',
- left: 'center',
- textStyle: {
- fontSize: 16,
- fontWeight: 'bold',
- color: '#303133'
- }
- },
- tooltip: {
- trigger: 'axis',
- backgroundColor: 'rgba(255, 255, 255, 0.95)',
- borderColor: '#e4e7ed',
- borderWidth: 1,
- textStyle: {
- color: '#303133'
- },
- axisPointer: {
- type: 'cross',
- label: {
- backgroundColor: '#6a7985'
- }
- },
- formatter: function (params) {
- // 格式化时间显示
- const formatTime = (timeStr) => {
- const date = new Date(timeStr)
- return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
- }
- let result = `<div style="font-weight: bold; margin-bottom: 8px; color: #303133;">${formatTime(params[0].axisValue)}</div>`
- params.forEach(param => {
- if (param.value !== null && param.value !== undefined) {
- // 数据格式为 [datetime, value],所以取第二个元素作为温度值
- const temperature = Array.isArray(param.value) ? param.value[1] : param.value
- result += `<div style="margin: 5px 0; display: flex; align-items: center;">
- <span style="display: inline-block; width: 12px; height: 12px; background: ${param.color}; margin-right: 8px; border-radius: 2px;"></span>
- <span style="color: #606266;">${param.seriesName}: </span>
- <span style="font-weight: bold; color: #303133; margin-left: 4px;">${temperature.toFixed(2)}°C</span>
- </div>`
- }
- })
- return result
- }
- },
- legend: {
- data: series.map(s => s.name),
- top: 30,
- type: 'scroll',
- textStyle: {
- color: '#606266'
- }
- },
- grid: {
- left: '3%',
- right: '4%',
- bottom: '15%',
- top: '15%',
- containLabel: true
- },
- xAxis: {
- type: 'time',
- boundaryGap: false,
- axisLine: {
- lineStyle: {
- color: '#e4e7ed'
- }
- },
- axisLabel: {
- color: '#606266',
- formatter: function (value) {
- // 格式化时间显示
- const date = new Date(value)
- return `${date.getMonth() + 1}/${date.getDate()} ${date.getHours()}:${date.getMinutes().toString().padStart(2, '0')}`
- },
- rotate: 45
- },
- axisTick: {
- lineStyle: {
- color: '#e4e7ed'
- }
- }
- },
- yAxis: {
- type: 'value',
- name: '温度 (°C)',
- nameLocation: 'middle',
- nameGap: 40,
- nameTextStyle: {
- color: '#606266'
- },
- axisLine: {
- lineStyle: {
- color: '#e4e7ed'
- }
- },
- axisLabel: {
- color: '#606266',
- formatter: '{value} °C'
- },
- axisTick: {
- lineStyle: {
- color: '#e4e7ed'
- }
- },
- splitLine: {
- lineStyle: {
- color: '#f0f0f0',
- type: 'dashed'
- }
- },
- // 根据实际数据范围设置坐标轴,最小温度刻度比实际最小温度小2度,最大温度刻度比实际最大温度高2度
- min: function(value) {
- const allTemps = []
- series.forEach(s => {
- s.data.forEach(point => {
- if (Array.isArray(point) && point[1] !== null && point[1] !== undefined) {
- allTemps.push(point[1])
- }
- })
- })
- if (allTemps.length === 0) return 0
- const minTemp = Math.min(...allTemps)
- return parseInt(minTemp - 2)
- },
- max: function(value) {
- const allTemps = []
- series.forEach(s => {
- s.data.forEach(point => {
- if (Array.isArray(point) && point[1] !== null && point[1] !== undefined) {
- allTemps.push(point[1])
- }
- })
- })
- if (allTemps.length === 0) return 100
- const maxTemp = Math.max(...allTemps)
- return parseInt(maxTemp + 2)
- }
- },
- series: series,
- dataZoom: [
- {
- type: 'inside',
- start: 0,
- end: 100
- },
- {
- type: 'slider',
- start: 0,
- end: 100,
- bottom: 10,
- height: 20,
- borderColor: '#e4e7ed',
- fillerColor: 'rgba(64, 158, 255, 0.1)',
- handleStyle: {
- color: '#409eff'
- }
- }
- ]
- }
-
- // 设置趋势图点击事件
- trendChartInstance.off('click')
- trendChartInstance.on('click', (params) => {
- // 处理点击事件,获取点击的时间点
- let clickedTime = null
-
- if (params.componentType === 'series') {
- // 点击在数据系列上
- clickedTime = params.value[0]
- } else if (params.componentType === 'xAxis') {
- // 点击在X轴上
- clickedTime = params.value
- } else if (params.componentType === 'grid') {
- // 点击在图表网格上,需要根据鼠标位置计算时间
- const pointInPixel = [params.event.offsetX, params.event.offsetY]
- const pointInGrid = trendChartInstance.convertFromPixel({seriesIndex: 0}, pointInPixel)
- if (pointInGrid && pointInGrid.length > 0) {
- clickedTime = pointInGrid[0]
- }
- }
-
- if (clickedTime) {
- // 找到最接近的时间点
- const closestDataPoint = sortedData.reduce((closest, current) => {
- const currentDiff = Math.abs(new Date(current.datetime) - new Date(clickedTime))
- const closestDiff = Math.abs(new Date(closest.datetime) - new Date(clickedTime))
- return currentDiff < closestDiff ? current : closest
- })
-
- updateMomentChart(closestDataPoint.datetime)
- }
- })
-
- trendChartInstance.setOption(trendOption, true)
-
- // 初始化时刻图表,默认显示第一个时刻的数据
- if (sortedData.length > 0) {
- const firstTime = sortedData[0].datetime
- updateMomentChart(firstTime)
- }
- }
-
- // 更新时刻温度对比图
- const updateMomentChart = (selectedTime) => {
- if (!momentChartInstance || !props.chartData || props.chartData.length === 0) {
- return
- }
-
- // 找到对应时刻的数据
- const momentData = props.chartData.find(item => item.datetime === selectedTime)
- if (!momentData) {
- return
- }
-
- selectedMoment.value = selectedTime
-
- // 准备时刻数据
- const momentSeries = []
- const colors = ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', '#3ba272', '#fc8452', '#9a60b4']
-
- props.sensorColumns.forEach((sensor, index) => {
- const value = momentData[sensor.sensorName]
- if (value !== null && value !== undefined && !isNaN(value)) {
- momentSeries.push({
- name: sensor.sensorName,
- value: parseFloat(value),
- itemStyle: {
- color: colors[index % colors.length]
- }
- })
- }
- })
-
- // 格式化时间显示
- const formatTime = (timeStr) => {
- const date = new Date(timeStr)
- return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
- }
-
- // 如果没有有效数据,显示空状态
- if (momentSeries.length === 0) {
- const emptyOption = {
- title: {
- text: `时刻温度对比 - ${formatTime(selectedTime)}`,
- left: 'center',
- textStyle: {
- fontSize: 14,
- fontWeight: 'bold',
- color: '#303133'
- }
- },
- graphic: {
- type: 'text',
- left: 'center',
- top: 'middle',
- style: {
- text: '该时刻暂无温度数据',
- fontSize: 16,
- fill: '#909399'
- }
- }
- }
- momentChartInstance.setOption(emptyOption, true)
- return
- }
-
- // 时刻图表配置 - 折线图,y轴为legend,x轴为温度
- const momentOption = {
- title: {
- text: `时刻温度对比 - ${formatTime(selectedTime)}`,
- left: 'center',
- textStyle: {
- fontSize: 14,
- fontWeight: 'bold',
- color: '#303133'
- }
- },
- tooltip: {
- trigger: 'axis',
- backgroundColor: 'rgba(255, 255, 255, 0.95)',
- borderColor: '#e4e7ed',
- borderWidth: 1,
- textStyle: {
- color: '#303133'
- },
- formatter: function (params) {
- return `<div style="font-weight: bold; margin-bottom: 8px; color: #303133;">${params[0].name}</div>
- <div style="margin: 5px 0; display: flex; align-items: center;">
- <span style="display: inline-block; width: 12px; height: 12px; background: ${params[0].color}; margin-right: 8px; border-radius: 2px;"></span>
- <span style="color: #606266;">温度: </span>
- <span style="font-weight: bold; color: #303133; margin-left: 4px;">${params[0].value.toFixed(2)}°C</span>
- </div>`
- }
- },
- grid: {
- left: '15%',
- right: '15%',
- bottom: '5%',
- top: '8%',
- containLabel: true
- },
- xAxis: {
- type: 'value',
- name: '温度 (°C)',
- nameLocation: 'middle',
- nameGap: 30,
- nameTextStyle: {
- color: '#606266'
- },
- axisLine: {
- lineStyle: {
- color: '#e4e7ed'
- }
- },
- axisLabel: {
- color: '#606266',
- formatter: '{value} °C'
- },
- axisTick: {
- lineStyle: {
- color: '#e4e7ed'
- }
- },
- splitLine: {
- lineStyle: {
- color: '#f0f0f0',
- type: 'dashed'
- }
- },
- // 根据实际数据范围设置坐标轴,最小温度刻度比实际最小温度小2度,最大温度刻度比实际最大温度高2度
- min: function(value) {
- const minTemp = Math.min(...momentSeries.map(s => s.value))
- return parseInt(minTemp - 2)
- },
- max: function(value) {
- const maxTemp = Math.max(...momentSeries.map(s => s.value))
- return parseInt(maxTemp + 2)
- }
- },
- yAxis: {
- type: 'category',
- data: momentSeries.map(s => s.name),
- axisLine: {
- lineStyle: {
- color: '#e4e7ed'
- }
- },
- axisLabel: {
- color: '#606266',
- show: true
- },
- axisTick: {
- lineStyle: {
- color: '#e4e7ed'
- }
- }
- },
- series: [
- {
- name: '温度',
- type: 'line',
- data: momentSeries.map(s => s.value),
- symbol: 'circle',
- symbolSize: 8,
- lineStyle: {
- width: 3
- },
- itemStyle: {
- color: '#409eff'
- },
- label: {
- show: true,
- position: 'right',
- formatter: '{c}°C',
- color: '#606266'
- }
- }
- ]
- }
-
- momentChartInstance.setOption(momentOption, true)
- }
-
- // 监听数据变化
- watch(
- () => [props.chartData, props.sensorColumns],
- () => {
- nextTick(() => {
- updateCharts()
- // 延迟重新调整大小,确保DOM已更新
- setTimeout(() => {
- if (trendChartInstance) {
- trendChartInstance.resize()
- }
- if (momentChartInstance) {
- momentChartInstance.resize()
- }
- }, 100)
- })
- },
- { deep: true }
- )
-
- // 监听窗口大小变化
- const handleResize = () => {
- if (trendChartInstance) {
- trendChartInstance.resize()
- }
- if (momentChartInstance) {
- momentChartInstance.resize()
- }
- }
-
- // 生命周期
- onMounted(() => {
- initChart()
- window.addEventListener('resize', handleResize)
- })
-
- onUnmounted(() => {
- if (trendChartInstance) {
- trendChartInstance.dispose()
- trendChartInstance = null
- }
- if (momentChartInstance) {
- momentChartInstance.dispose()
- momentChartInstance = null
- }
- window.removeEventListener('resize', handleResize)
- })
- </script>
-
- <style lang="scss" scoped>
- .temperature-chart {
- width: 100%;
- height: 100%;
- display: flex;
- flex-direction: column;
-
- .chart-container {
- width: 100%;
- height: 100%;
- min-height: 600px;
- flex: 1;
- display: flex;
- flex-direction: row;
- gap: 20px;
-
- .trend-chart {
- width: 60% !important;
- height: 600px !important;
- border: 1px solid #e4e7ed;
- border-radius: 8px;
- background: #ffffff;
- }
-
- .moment-chart {
- width: 40% !important;
- height: 600px !important;
- border: 1px solid #e4e7ed;
- border-radius: 8px;
- background: #ffffff;
- }
- }
- }
- </style>
|