You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

498 lines
11 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<!--
* @Author: chris
* @Date: 2025-09-12 17:20:00
* @LastEditors: chris
* @LastEditTime: 2025-09-22 17:15:25
* @Description: 气象曲线图组件支持7天/30/90天数据切换
-->
<script setup>
import { ref, watch, onMounted, nextTick } from 'vue'
import * as echarts from 'echarts'
import { weatherDict, weatherTypes } from '../config'
// 定义组件 props
const props = defineProps({
// 气象类型
weatherType: {
type: String,
required: true,
validator: (value) => {
// 支持的气象类型
const validTypes = weatherTypes
return validTypes.includes(value)
}
},
// 图表数据 - 修改为只包含当前时间范围的数据
chartData: {
type: Object,
required: true,
// 数据格式: { dates: [], values: [] }
validator: (value) => {
return value &&
Array.isArray(value.dates) &&
Array.isArray(value.values) &&
value.dates.length === value.values.length
}
},
// 当前选中的时间范围,由父组件控制
selectedTimeRange: {
type: Number,
default: 7,
validator: (value) => [7, 30, 90].includes(value)
},
// 加载状态,由父组件控制
loading: {
type: Boolean,
default: false
}
})
// 定义组件事件
const emit = defineEmits(['timeRangeChange'])
// 本地同步父组件传入的选中时间范围
const selectedDays = ref(props.selectedTimeRange)
// 监听父组件传入的选中时间范围变化
watch(() => props.selectedTimeRange, (newValue) => {
selectedDays.value = newValue
})
// ECharts 实例
const chartInstance = ref(null)
// 图表容器
const chartContainer = ref(null)
// 气象类型配置
let weatherTypeConfig = null
// const weatherTypeConfig = {
// temperature: {
// name: '温度',
// unit: '℃',
// color: '#FF6B6B',
// yAxis: {
// name: '温度 (℃)',
// min: function(value) { return value.min - 2; },
// max: function(value) { return value.max + 2; },
// show: true,
// axisLabel: {
// show: true,
// formatter: '{value}',
// color: '#333',
// fontSize: 12
// },
// axisTick: {
// show: true
// },
// axisLine: {
// show: true
// },
// splitLine: {
// show: true,
// lineStyle: {
// type: 'dashed'
// }
// }
// }
// },
// humidity: {
// name: '湿度',
// unit: '%',
// color: '#4ECDC4',
// yAxis: {
// name: '湿度 (%)',
// min: 0,
// max: 100,
// show: true,
// axisLabel: {
// show: true,
// formatter: '{value}'
// },
// axisTick: {
// show: true
// },
// axisLine: {
// show: true
// },
// splitLine: {
// show: true,
// lineStyle: {
// type: 'dashed'
// }
// }
// }
// },
// rainfall: {
// name: '降雨量',
// unit: 'mm',
// color: '#6A0572',
// yAxis: {
// name: '降雨量 (mm)',
// min: 0,
// show: true,
// axisLabel: {
// show: true,
// formatter: '{value}'
// },
// axisTick: {
// show: true
// },
// axisLine: {
// show: true
// },
// splitLine: {
// show: true,
// lineStyle: {
// type: 'dashed'
// }
// }
// }
// },
// windSpeed: {
// name: '风速',
// unit: 'm/s',
// color: '#77DD77',
// yAxis: {
// name: '风速 (m/s)',
// min: 0,
// show: true,
// axisLabel: {
// show: true,
// formatter: '{value}'
// },
// axisTick: {
// show: true
// },
// axisLine: {
// show: true
// },
// splitLine: {
// show: true,
// lineStyle: {
// type: 'dashed'
// }
// }
// }
// },
// pressure: {
// name: '气压',
// unit: 'hPa',
// color: '#845EC2',
// yAxis: {
// name: '气压 (hPa)',
// min: function(value) { return value.min - 5; },
// max: function(value) { return value.max + 5; },
// show: true,
// axisLabel: {
// show: true,
// formatter: '{value}',
// color: '#333',
// fontSize: 12
// },
// axisTick: {
// show: true
// },
// axisLine: {
// show: true
// },
// splitLine: {
// show: true,
// lineStyle: {
// type: 'dashed'
// }
// }
// }
// },
// light: {
// name: '光照',
// unit: 'lux',
// color: '#FFD166',
// yAxis: {
// name: '光照 (lux)',
// min: 0,
// show: true, // 明确显示Y轴
// axisLabel: {
// show: true, // 显示刻度标签
// formatter: '{value}'
// },
// axisTick: {
// show: true // 显示刻度线
// },
// axisLine: {
// show: true // 显示轴线
// },
// splitLine: {
// show: true, // 显示分隔线
// lineStyle: {
// type: 'dashed'
// }
// }
// }
// }
// }
// 初始化图表
const initChart = () => {
if (!chartContainer.value) return
// 销毁已存在的实例
if (chartInstance.value) {
chartInstance.value.dispose()
}
// 创建新实例
chartInstance.value = echarts.init(chartContainer.value)
// 设置图表配置
updateChart()
// 监听窗口大小变化,自动调整图表大小
window.addEventListener('resize', handleResize)
}
// 更新图表数据和配置
const updateChart = () => {
if (!chartInstance.value || !props.chartData) return
const config = weatherTypeConfig[props.weatherType]
const data = props.chartData
const option = {
title: {
text: `${config.name}趋势图 (近24小时)`,
left: 'center',
textStyle: {
fontSize: 16,
fontWeight: 'normal'
}
},
tooltip: {
trigger: 'item',
show: true,
alwaysShowContent: false,
backgroundColor: 'rgba(0, 0, 0, 0.8)',
borderColor: '#555',
borderWidth: 1,
padding: 10,
textStyle: {
color: '#fff',
fontSize: 12
},
formatter: function(params) {
return `日期: ${params.name}<br/>${params.seriesName}: ${params.value} ${config.unit}`
},
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#6a7985'
},
crossStyle: {
color: '#999'
}
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
data: data.dates,
axisLabel: {
// 根据数据量调整显示间隔
interval: Math.ceil(data.dates.length / 10),
rotate: data.dates.length > 15 ? 45 : 0
}
},
yAxis: {
type: 'value',
...config.yAxis
},
series: [
{
name: config.name,
type: 'line',
data: data.values,
smooth: true,
symbol: 'circle',
symbolSize: 6,
showSymbol: true, // 平时不显示数据点
lineStyle: {
width: 3,
color: config.color
},
itemStyle: {
color: config.color,
borderWidth: 2,
borderColor: '#fff'
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: `${config.color}40`
},
{
offset: 1,
color: `${config.color}10`
}
])
},
emphasis: {
focus: 'item', // 只高亮当前鼠标悬停的点
showSymbol: true, // 鼠标悬停时显示数据点
itemStyle: {
symbolSize: 10,
borderWidth: 3,
borderColor: '#fff'
}
},
// 确保鼠标能准确捕捉到数据点
triggerLineEvent: true,
}
]
}
chartInstance.value.setOption(option, true)
}
// 处理窗口大小变化
const handleResize = () => {
if (chartInstance.value) {
chartInstance.value.resize()
}
}
// 监听选中的时间范围变化
watch(selectedDays, (newValue) => {
// 向父组件发送时间范围变化事件
emit('timeRangeChange', newValue)
// 当数据加载完成后updateChart会在props.chartData变化时被调用
})
// 监听props变化
watch(
() => [props.chartData, props.selectedTimeRange],
() => {
nextTick(() => {
updateChart()
})
},
{ deep: true }
)
// 组件挂载时初始化图表
onMounted(() => {
weatherTypeConfig = createWeatherConfig()
initChart()
})
// 组件卸载时清理
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
if (chartInstance.value) {
chartInstance.value.dispose()
}
})
// 暴露更新图表的方法,供父组件调用
defineExpose({
refreshChart: updateChart
})
function createWeatherConfig () {
const config = {}
weatherTypes.forEach(type => {
const typeDict = weatherDict[type]
if (!typeDict) return
config[type] = {
name: typeDict.name,
unit: typeDict.unit,
color: typeDict.color,
yAxis: {
name: `${typeDict.name} (${typeDict.unit})`,
min: 0,
show: true, // 明确显示Y轴
axisLabel: {
show: true, // 显示刻度标签
formatter: '{value}'
},
axisTick: {
show: true // 显示刻度线
},
axisLine: {
show: true // 显示轴线
},
splitLine: {
show: true, // 显示分隔线
lineStyle: {
type: 'dashed'
}
}
}
}
})
return config
}
</script>
<template>
<div class="weather-chart-container">
<!-- 时间选择器 -->
<div class="chart-controls">
<!-- <el-radio-group v-model="selectedDays" class="time-range-selector">
<el-radio-button :value="7" :disabled="loading">近7天</el-radio-button>
<el-radio-button :value="30" :disabled="loading">近30天</el-radio-button>
<el-radio-button :value="90" :disabled="loading">90</el-radio-button>
</el-radio-group> -->
<!-- 加载状态指示器 -->
<!-- <div v-if="loading" class="loading-indicator">
<span class="loading-text">...</span>
</div> -->
</div>
<!-- 图表容器 -->
<div
ref="chartContainer"
class="chart-wrapper"
></div>
</div>
</template>
<style lang="scss" scoped>
// 使用unocss原子化类和scss结合的方案
.weather-chart-container {
@apply w-full h-full bg-white rounded-md p-4 shadow-md;
}
.chart-controls {
@apply mb-4 flex justify-end items-center gap-4;
}
.loading-indicator {
@apply text-xs text-[#606266];
}
.loading-text {
@apply inline-block ml-1;
animation: fadeInOut 1.5s infinite;
}
// 保留自定义动画
@keyframes fadeInOut {
0%, 100% { opacity: 0.3; }
50% { opacity: 1; }
}
.time-range-selector {
@apply bg-[#f5f7fa] rounded-md p-1;
}
.chart-wrapper {
@apply w-full transition-all duration-300 ease-in-out;
height: calc(100% - 50px);
}
</style>