586 lines
14 KiB
Vue
586 lines
14 KiB
Vue
<template>
|
||
<div class="weather-container">
|
||
<!-- 顶部温度和天气信息 -->
|
||
<div class="weather-top">
|
||
<div class="current-temp">
|
||
<!-- <span class="temp-value">25°</span> -->
|
||
</div>
|
||
<div class="sun-time">
|
||
<div class="sunrise">
|
||
<img src="@/assets/img/yin.png" alt="日出" />
|
||
<span>{{ forecastData[0].sunrise_time }}</span>
|
||
</div>
|
||
<div class="sunset">
|
||
<img src="@/assets/img/yin.png" alt="日落" />
|
||
<span>{{ forecastData[0].sunset_time }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 天气趋势图 -->
|
||
<div class="weather-trend">
|
||
<div class="trend-line" ref="chartRef"></div>
|
||
</div>
|
||
|
||
<!-- 空气质量指标 -->
|
||
<div class="air-quality">
|
||
<div class="quality-item">
|
||
<span class="label">空气</span>
|
||
<div class="quality-bars">
|
||
<div
|
||
class="bar"
|
||
v-for="i in weatherHourlyData"
|
||
:key="i"
|
||
:style="{ backgroundColor: getAirQualityColor(i.aqi_level) }"
|
||
></div>
|
||
</div>
|
||
</div>
|
||
<div class="quality-item">
|
||
<span class="label">风力</span>
|
||
<div class="quality-bars">
|
||
<div
|
||
class="bar bartwo"
|
||
v-for="i in weatherHourlyData"
|
||
:key="i"
|
||
>{{i.wind_level}}</div>
|
||
</div>
|
||
</div>
|
||
<div class="quality-item">
|
||
<span class="label"></span>
|
||
<div class="quality-barstwo">
|
||
<div
|
||
class="bar bartwo"
|
||
v-for="i in ['08:00','10:00','12:00','14:00','16:00','18:00',]"
|
||
:key="i"
|
||
>{{i}}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 未来天气预报 -->
|
||
<div class="weather-forecast">
|
||
<div
|
||
class="forecast-item"
|
||
v-for="(item, index) in forecastData"
|
||
:key="index"
|
||
>
|
||
<div class="day">{{ item.week_day }}</div>
|
||
<div class="weather-type">{{ item.weather_type }}</div>
|
||
<div class="weather-icon">
|
||
<img :src="weatherIcons[item.weather]" :alt="item.weather" />
|
||
</div>
|
||
<div class="wind_info">{{ item.wind_info }}</div>
|
||
<div class="temp-range">
|
||
<span class="low_temp">{{ item.low_temp }}°</span>
|
||
<div class="temp-bar">
|
||
<div class="bar-inner"
|
||
:title="`${item.high_temp}°C`"
|
||
:style="{ width: item.percentage + '%', background: getTempGradient(item.high_temp) }">
|
||
</div>
|
||
</div>
|
||
<span class="high_temp">{{ item.high_temp }}°</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, reactive, onMounted, onBeforeUnmount } from "vue";
|
||
import useCharts from "@/hooks/useEcharts";
|
||
import type { EChartsOption } from "echarts";
|
||
|
||
// 定义天气类型
|
||
type WeatherType = "sunny" | "cloudy" | "rainy" | "bigRainy";
|
||
|
||
// 导入天气图标
|
||
import sunnyIcon from "@/assets/img/weather/sunny.png";
|
||
import cloudyIcon from "@/assets/img/weather/cloudy.png";
|
||
import rainyIcon from "@/assets/img/weather/rainy.png";
|
||
import bigRainyIcon from "@/assets/img/weather/BigRainy.png";
|
||
|
||
import { weatherForecast, weatherHourly, todayStatusCount, todayHourly } from "@/api/modules/index";
|
||
import { useTodayTime, } from "@/utils/packge";
|
||
|
||
// 天气图标映射
|
||
const weatherIcons: Record<WeatherType, string> = {
|
||
sunny: sunnyIcon,
|
||
cloudy: cloudyIcon,
|
||
rainy: rainyIcon,
|
||
bigRainy: bigRainyIcon,
|
||
};
|
||
|
||
// 定义预报数据类型
|
||
interface ForecastItem {
|
||
week_day: string;
|
||
weather_type: string;
|
||
weather: WeatherType;
|
||
wind_info: string;
|
||
high_temp: number;
|
||
low_temp: number;
|
||
percentage: number;
|
||
sunrise_time: string;
|
||
sunset_time: string;
|
||
}
|
||
const weatherTypeMapping: Record<string, WeatherType> = {
|
||
"晴": "sunny",
|
||
"多云": "cloudy",
|
||
"雨": "rainy",
|
||
"大雨": "bigRainy",
|
||
// 可以继续添加其他天气类型
|
||
};
|
||
|
||
// 天气预报数据
|
||
const forecastData = reactive<ForecastItem[]>([
|
||
{
|
||
week_day: "周二",
|
||
weather_type: "多云",
|
||
weather: "cloudy",
|
||
wind_info: "东南风三级",
|
||
high_temp: 15,
|
||
low_temp: -2,
|
||
percentage: 0,
|
||
sunrise_time: '',
|
||
sunset_time: '',
|
||
},
|
||
{
|
||
week_day: "周三",
|
||
weather_type: "多云",
|
||
weather: "cloudy",
|
||
wind_info: "东南风三级",
|
||
high_temp: 25,
|
||
low_temp: 15,
|
||
percentage: 0,
|
||
sunrise_time: '',
|
||
sunset_time: '',
|
||
},
|
||
{
|
||
week_day: "周四",
|
||
weather_type: "大雨",
|
||
weather: "rainy",
|
||
wind_info: "东南风三级",
|
||
high_temp: 35,
|
||
low_temp: 18,
|
||
percentage: 0,
|
||
sunrise_time: '',
|
||
sunset_time: '',
|
||
},
|
||
]);
|
||
|
||
const { todayTime, getTodayTime } = useTodayTime();
|
||
// 图表容器引用
|
||
const chartRef = ref<HTMLElement | null>(null);
|
||
|
||
// 温度趋势数据
|
||
const temperatureData = reactive({
|
||
times: ["08:00", "09:00","10:00","11:00","12:00", "13:00", "14:00", "15:00", "16:00", "17:00", "18:00"],
|
||
values: [],
|
||
});
|
||
// 温度趋势图表配置
|
||
const chartOptions: EChartsOption = {
|
||
tooltip: {
|
||
trigger: "axis",
|
||
axisPointer: {
|
||
type: "line",
|
||
},
|
||
backgroundColor: "rgba(0,0,0,0.7)",
|
||
borderColor: "rgba(0,255,255,0.5)",
|
||
borderWidth: 1,
|
||
textStyle: {
|
||
color: "#fff",
|
||
fontSize: 24,
|
||
},
|
||
formatter: (params: any) => {
|
||
const data = params[0];
|
||
return `${data.name}<br/>${data.value}°`;
|
||
},
|
||
},
|
||
grid: {
|
||
top: "15%",
|
||
left: "3%",
|
||
right: "4%",
|
||
bottom: "3%",
|
||
containLabel: true,
|
||
},
|
||
xAxis: {
|
||
type: "category",
|
||
boundaryGap: false,
|
||
data: temperatureData.times,
|
||
axisLine: {
|
||
lineStyle: {
|
||
color: "rgba(255,255,255,0.2)",
|
||
},
|
||
},
|
||
axisLabel: {
|
||
show: false,
|
||
color: "#fff",
|
||
fontSize: 24,
|
||
},
|
||
splitLine: {
|
||
show: true,
|
||
lineStyle: {
|
||
color: "rgba(255,255,255,0.1)",
|
||
type: "dashed",
|
||
},
|
||
},
|
||
},
|
||
yAxis: {
|
||
type: "value",
|
||
min: 14,
|
||
max: 25,
|
||
splitNumber: 5,
|
||
axisLabel: {
|
||
color: "#fff",
|
||
fontSize: 24,
|
||
formatter: "{value}°",
|
||
},
|
||
axisLine: {
|
||
show: true,
|
||
lineStyle: {
|
||
color: "rgba(255,255,255,0.2)",
|
||
},
|
||
},
|
||
splitLine: {
|
||
show: true,
|
||
lineStyle: {
|
||
color: "rgba(255,255,255,0.1)",
|
||
type: "dashed",
|
||
},
|
||
},
|
||
},
|
||
series: [
|
||
{
|
||
type: "line",
|
||
data: temperatureData.values,
|
||
smooth: true,
|
||
symbol: "circle",
|
||
symbolSize: 8,
|
||
itemStyle: {
|
||
color: "#fff",
|
||
borderWidth: 2,
|
||
borderColor: "#00ffff",
|
||
},
|
||
lineStyle: {
|
||
color: "#00ffff",
|
||
width: 3,
|
||
},
|
||
areaStyle: {
|
||
color: {
|
||
type: "linear",
|
||
x: 0,
|
||
y: 0,
|
||
x2: 0,
|
||
y2: 1,
|
||
colorStops: [
|
||
{
|
||
offset: 0,
|
||
color: "rgba(0,255,255,0.3)",
|
||
},
|
||
{
|
||
offset: 1,
|
||
color: "rgba(0,255,255,0.1)",
|
||
},
|
||
],
|
||
},
|
||
},
|
||
label: {
|
||
show: false,
|
||
position: "top",
|
||
color: "#fff",
|
||
fontSize: 24,
|
||
formatter: "{c}°",
|
||
},
|
||
},
|
||
],
|
||
};
|
||
const weatherHourlyData = ref([])
|
||
// 初始化图表
|
||
const { initCharts, setOptions, resize } = useCharts(chartRef, chartOptions);
|
||
|
||
// 确保图表在组件挂载后渲染
|
||
onMounted(() => {
|
||
initCharts();
|
||
resize();
|
||
window.addEventListener('resize', handleWindowResize);
|
||
weatherForecast().then((res:any)=>{
|
||
const updatedData = res.data.map((item: any) => ({
|
||
...item,
|
||
percentage: (item.high_temp / 50) * 100, // 计算百分比
|
||
weather: weatherTypeMapping[item.weather_type] || "sunny",
|
||
}));
|
||
forecastData.splice(0, forecastData.length, ...updatedData);
|
||
})
|
||
weatherHourly().then((res:any)=>{
|
||
weatherHourlyData.value = res.data
|
||
temperatureData.values = res.data.map((item: any) => item.temperature);
|
||
|
||
// 计算最小值 & 最大值
|
||
const minTemp = Math.min(...temperatureData.values);
|
||
const maxTemp = Math.max(...temperatureData.values);
|
||
console.log(temperatureData.values, 'temperatureData.values')
|
||
setOptions({ // 手动刷新图表
|
||
yAxis: {
|
||
min: minTemp - 2, // 留一点余量
|
||
max: maxTemp + 2,
|
||
},
|
||
series: [{
|
||
data: temperatureData.values
|
||
}]
|
||
});
|
||
handleWindowResize();
|
||
})
|
||
todayHourly({todayTime: todayTime.value, pilenum: ''}).then((res: any) => {
|
||
console.log(res)
|
||
})
|
||
});
|
||
// 调用 resize 方法更新图表尺寸
|
||
const handleWindowResize = () => {
|
||
resize();
|
||
};
|
||
|
||
// 组件卸载前移除事件监听器
|
||
onBeforeUnmount(() => {
|
||
window.removeEventListener('resize', handleWindowResize);
|
||
});
|
||
// 根据 i 值返回颜色(绿色 -> 红色)
|
||
const getAirQualityColor = (index: number): string => {
|
||
let hue;
|
||
if (index <= 3) {
|
||
// 绿色系:120° -> 90°
|
||
hue = 120 - (index / 3) * 30;
|
||
} else if (index <= 7) {
|
||
// 黄绿色系:90° -> 60°
|
||
hue = 90 - ((index - 3) / 4) * 30;
|
||
} else if (index <= 10) {
|
||
// 橙色系:60° -> 30°
|
||
hue = 60 - ((index - 7) / 3) * 30;
|
||
} else {
|
||
// 红色系:30° -> 0°
|
||
hue = 30 - ((index - 10) / 3) * 30;
|
||
}
|
||
return `hsl(${hue}, 100%, 45%)`;
|
||
};
|
||
const getTempGradient = (high_temp: number): string => {
|
||
let startHue, endHue;
|
||
|
||
if (high_temp < 0) {
|
||
// 冷蓝色调(-20°C 到 0°C)
|
||
startHue = 270;
|
||
endHue = 240 + (high_temp / -20) * 30;
|
||
} else if (high_temp <= 25) {
|
||
// 从蓝绿色到黄色(0°C 到 25°C)
|
||
startHue = 180;
|
||
endHue = 180 - (high_temp / 25) * 120;
|
||
} else {
|
||
// 从橙色到红色(26°C 及以上)
|
||
startHue = 30;
|
||
endHue = 30 - ((high_temp - 25) / 10) * 30;
|
||
}
|
||
|
||
// 确保 hue 在 0~360 范围内
|
||
endHue = Math.max(0, Math.min(360, endHue));
|
||
|
||
const startColor = `hsla(${startHue}, 100%, 45%, 0.8)`;
|
||
const endColor = `hsla(${endHue}, 100%, 45%, 0.6)`;
|
||
|
||
return `linear-gradient(90deg, ${startColor}, ${endColor})`;
|
||
};
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.weather-container {
|
||
width: 100%;
|
||
height: 100%;
|
||
padding: 20px;
|
||
color: #fff;
|
||
|
||
.weather-top {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 40px;
|
||
|
||
.current-temp {
|
||
.temp-value {
|
||
font-size: 80px;
|
||
font-weight: bold;
|
||
font-family: "Arial";
|
||
}
|
||
}
|
||
|
||
.sun-time {
|
||
display: flex;
|
||
gap: 20px;
|
||
|
||
.sunrise,
|
||
.sunset {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
|
||
img {
|
||
width: 24px;
|
||
height: 24px;
|
||
}
|
||
|
||
span {
|
||
font-size: 24px;
|
||
color: #ffffff;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.weather-trend {
|
||
height: 350px;
|
||
|
||
.trend-line {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
}
|
||
|
||
.air-quality {
|
||
margin-bottom: 20px;
|
||
|
||
.quality-item {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 20px;
|
||
|
||
.label {
|
||
width: 60px;
|
||
font-size: 24px;
|
||
color: #ffffff;
|
||
}
|
||
|
||
.quality-bars {
|
||
flex: 1;
|
||
display: flex;
|
||
gap: 4px;
|
||
|
||
.bar {
|
||
flex: 1;
|
||
height: 18px;
|
||
background: rgba(255, 255, 255, 0.1);
|
||
border-radius: 4px;
|
||
|
||
&.active {
|
||
background: #00ffff;
|
||
}
|
||
}
|
||
.bartwo{
|
||
height: 32px;
|
||
background: rgba(255,255,255,0.12);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-weight: 400;
|
||
font-size: 20px;
|
||
color: rgba(255,255,255,0.6);
|
||
line-height: 28px;
|
||
text-align: center;
|
||
font-style: normal;
|
||
text-transform: none;
|
||
}
|
||
}
|
||
.quality-barstwo {
|
||
flex: 1;
|
||
display: flex;
|
||
gap: 4px;
|
||
.bar {
|
||
flex: 1;
|
||
height: 18px;
|
||
}
|
||
.bartwo{
|
||
height: 32px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-weight: 400;
|
||
font-size: 18px;
|
||
color: rgba(255,255,255,0.6);
|
||
line-height: 28px;
|
||
text-align: center;
|
||
font-style: normal;
|
||
text-transform: none;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.weather-forecast {
|
||
.forecast-item {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 22px 0;
|
||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||
|
||
.day {
|
||
width: 80px;
|
||
font-size: 28px;
|
||
color: #ffffff;
|
||
}
|
||
|
||
.weather-type {
|
||
width: 80px;
|
||
font-size: 20px;
|
||
color: #ffffff;
|
||
text-align: center;
|
||
}
|
||
|
||
.weather-icon {
|
||
width: 44px;
|
||
height: 44px;
|
||
/* margin: 0 20px 0 0; */
|
||
margin-right: 20px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
|
||
img {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: contain;
|
||
}
|
||
}
|
||
|
||
.wind_info {
|
||
width: 160px;
|
||
font-size: 24px;
|
||
color: #ffffff;
|
||
}
|
||
|
||
.temp-range {
|
||
flex: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
|
||
.high_temp,
|
||
.low_temp {
|
||
font-size: 28px;
|
||
color: #ffffff;
|
||
width: 40px;
|
||
text-align: right;
|
||
}
|
||
|
||
.temp-bar {
|
||
flex: 1;
|
||
height: 8px;
|
||
background: rgba(255, 255, 255, 0.1);
|
||
border-radius: 4px;
|
||
overflow_temp: hidden;
|
||
|
||
.bar-inner {
|
||
height: 100%;
|
||
background: linear-gradient(90deg, #4ecee6 0%, #4ea8e6 100%);
|
||
border-radius: 4px;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</style>
|