|
|
|
|
<!--
|
|
|
|
|
* @Author: chris
|
|
|
|
|
* @Date: 2025-09-12 17:20:00
|
|
|
|
|
* @LastEditors: chris
|
|
|
|
|
* @LastEditTime: 2025-09-11 17:35:22
|
|
|
|
|
* @Description: 气象曲线图组件,支持7天/30天/90天数据切换
|
|
|
|
|
-->
|
|
|
|
|
<script setup>
|
|
|
|
|
import { ref, watch, onMounted, nextTick } from 'vue'
|
|
|
|
|
import * as echarts from 'echarts'
|
|
|
|
|
|
|
|
|
|
// 定义组件 props
|
|
|
|
|
const props = defineProps({
|
|
|
|
|
// 气象类型
|
|
|
|
|
weatherType: {
|
|
|
|
|
type: String,
|
|
|
|
|
required: true,
|
|
|
|
|
validator: (value) => {
|
|
|
|
|
// 支持的气象类型
|
|
|
|
|
const validTypes = ['temperature', 'humidity', 'rainfall', 'windSpeed', 'pressure', 'light']
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
// 气象类型配置
|
|
|
|
|
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}趋势图 (近${props.selectedTimeRange}天)`,
|
|
|
|
|
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(() => {
|
|
|
|
|
initChart()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 组件卸载时清理
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
window.removeEventListener('resize', handleResize)
|
|
|
|
|
if (chartInstance.value) {
|
|
|
|
|
chartInstance.value.dispose()
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 暴露更新图表的方法,供父组件调用
|
|
|
|
|
defineExpose({
|
|
|
|
|
refreshChart: updateChart
|
|
|
|
|
})
|
|
|
|
|
</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>
|