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

4 weeks ago
<!--
* @Author: chris
* @Date: 2025-09-12 17:20:00
* @LastEditors: chris
2 weeks ago
* @LastEditTime: 2025-09-22 17:15:25
4 weeks ago
* @Description: 气象曲线图组件支持7天/30/90天数据切换
-->
<script setup>
import { ref, watch, onMounted, nextTick } from 'vue'
import * as echarts from 'echarts'
2 weeks ago
import { weatherDict, weatherTypes } from '../config'
4 weeks ago
// 定义组件 props
const props = defineProps({
// 气象类型
weatherType: {
type: String,
required: true,
validator: (value) => {
// 支持的气象类型
2 weeks ago
const validTypes = weatherTypes
4 weeks ago
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)
// 气象类型配置
2 weeks ago
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'
// }
// }
// }
// }
// }
4 weeks ago
// 初始化图表
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: {
2 weeks ago
text: `${config.name}趋势图 (近24小时)`,
4 weeks ago
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(() => {
2 weeks ago
weatherTypeConfig = createWeatherConfig()
4 weeks ago
initChart()
})
// 组件卸载时清理
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
if (chartInstance.value) {
chartInstance.value.dispose()
}
})
// 暴露更新图表的方法,供父组件调用
defineExpose({
refreshChart: updateChart
})
2 weeks ago
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
}
4 weeks ago
</script>
<template>
<div class="weather-chart-container">
<!-- 时间选择器 -->
<div class="chart-controls">
2 weeks ago
<!-- <el-radio-group v-model="selectedDays" class="time-range-selector">
4 weeks ago
<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>
2 weeks ago
</el-radio-group> -->
4 weeks ago
<!-- 加载状态指示器 -->
<!-- <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>