引言
干净利落的天气特效, echarts中使用到了xAxis.axisLabel. rich, 以为echarts只能输出文字呢, 没想到还可以加入图片, 自定义了天气图标, 可以直接当成天气网站使用, 附线上运行预览效果, 有需要源码的可以联系我
实现效果
网页我已经做好了, ✋🏻 点击在线预览运行后效果
实现流程
难点在于echarts的 rich里的模板定义和formatter模板输出, echarts默认图标和文字的间距很小, axisLabel设置了行高为30, 页面css使用的tailwind
- 初始化缓存对象:使用了FileCache缓存类,可设置缓存过期时间。
- 请求天气接口:如果缓存数据返回false,重新请求天气数据,更新缓存。
- 设置twig变量值:我这边项目使用了twig模板,和thinkphp模板原理应该差不多, 不熟悉thinkphp。
- 实况和7日数据:直接使用twig模板变量输出,如湿度:{{ weather.data[0].humidity }}。
- echarts :因为天气接口字段格式是固定的,所以给echarts变量赋值直接写了0-6的索引值。rich里定义了天气api的9种图标,formatter根据星期几去匹配替换对应的图标和内容
上代码
控制端
// weather get
$cache = FileCache::createCache();
$cache->setCachedir(BASE_PATH);
$json_data = $cache->get('weather_101010100', true);
if (empty($json_data)) {
//appid和appsecret请去天气api申请,http://tianqiapi.com/user,注册就可以请求3000次
$weather = file_get_contents('http://v1.yiketianqi.com/api?unescape=1&version=v91&appid=你的参数&appsecret=你的参数&ext=life&cityid=101010100');
$json_data = json_decode($weather, true);
$cache->set('weather_101010100', $json_data, 300);
}
$this->assign['weather'] = $json_data;
模板端
<main class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- 温度图表卡片 -->
<div class="lg:col-span-2 bg-white rounded-xl p-5 card-shadow transition-all duration-300 hover:shadow-lg">
<div class="flex justify-between items-center mb-4">
<h2 class="text-lg font-semibold text-gray-800">{{ weather.city }}温度趋势图</h2>
<div class="text-sm text-gray-500 flex items-center">
<i class="fa fa-refresh mr-1"></i>
<span id="updateTime">更新于: {{ weather.update_time }}</span>
</div>
</div>
<div class="h-[350px] md:h-[400px] w-full">
<div id="temperatureChart" class="w-full h-full" style="user-select: none; -webkit-tap-highlight-color: rgba(0, 0, 0, 0); position: relative;"><div style="position: relative; width: 720px; height: 400px; padding: 0px; margin: 0px; border-width: 0px; cursor: default;"><canvas data-zr-dom-id="zr_0" width="1440" height="800" style="position: absolute; left: 0px; top: 0px; width: 720px; height: 400px; user-select: none; -webkit-tap-highlight-color: rgba(0, 0, 0, 0); padding: 0px; margin: 0px; border-width: 0px;"></canvas></div></div>
</div>
</div>
<!-- 天气信息卡片 -->
<div class="bg-white rounded-xl p-5 card-shadow transition-all duration-300 hover:shadow-lg">
<h2 class="text-lg font-semibold text-gray-800 mb-2">{{ weather.city }}今日天气</h2>
<div>
<!-- 今日概览 -->
<div class="text-center p-4">
<div class="flex justify-center items-center space-x-4 my-3">
<img class="mb-3" style="width: 45px; height: 45px; " src="/static/icon/{{ weather.data[0].wea_img }}.png">
<div>
<div class="text-3xl font-bold text-gray-800">{{ weather.data[0].tem }}°</div>
<div class="text-gray-500 text-sm">{{ weather.data[0].wea }}</div>
</div>
</div>
<div class="text-sm text-gray-600">
{{ weather.data[0].narrative }}
</div>
</div>
</div>
<!--湿度-->
<div class="flex justify-between items-center p-3 mb-3 bg-gray-50 rounded-xl" data-doubao-line="137" data-doubao-column="21" data-doubao-key="43">
<div class="flex items-center text-gray-600" data-doubao-line="138" data-doubao-column="25" data-doubao-key="44">
<i class="fa fa-tint text-cool mr-2" data-doubao-line="139" data-doubao-column="29" data-doubao-key="45"></i>
<span data-doubao-line="140" data-doubao-column="29" data-doubao-key="46">湿度</span>
</div>
<span class="font-medium" data-doubao-line="142" data-doubao-column="25" data-doubao-key="47">{{ weather.data[0].humidity }}</span>
</div>
<!--./湿度-->
<!--紫外线-->
<div class="flex justify-between items-center p-3 mb-3 bg-gray-50 rounded-xl" data-doubao-line="153" data-doubao-column="21" data-doubao-key="53">
<div class="flex items-center text-gray-600" data-doubao-line="154" data-doubao-column="25" data-doubao-key="54">
<i class="fa fa-umbrella text-secondary mr-2" data-doubao-line="155" data-doubao-column="29" data-doubao-key="55"></i>
<span data-doubao-line="156" data-doubao-column="29" data-doubao-key="56">紫外线</span>
</div>
<span class="font-medium text-accent" data-doubao-line="158" data-doubao-column="25" data-doubao-key="57">{{ weather.data[0].uvDescription }}</span>
</div>
<!--./紫外线-->
<!--item3-->
<div class="flex justify-between items-center p-3 mb-3 bg-gray-50 rounded-xl" data-doubao-line="161" data-doubao-column="21" data-doubao-key="58">
<div class="flex items-center text-gray-600" data-doubao-line="162" data-doubao-column="25" data-doubao-key="59">
<i class="fa fa-cloud text-neutral mr-2" data-doubao-line="163" data-doubao-column="29" data-doubao-key="60"></i>
<span data-doubao-line="164" data-doubao-column="29" data-doubao-key="61">能见度</span>
</div>
<span class="font-medium" data-doubao-line="166" data-doubao-column="25" data-doubao-key="62">{{ weather.data[0].visibility }}</span>
</div>
<!--./item3-->
<!-- 提示 -->
<div class="p-3 bg-blue-50 rounded-lg border border-blue-100">
<h3 class="font-medium text-primary mb-2 flex items-center">
<i class="fa fa-lightbulb-o mr-2"></i>温馨提示
</h3>
<p class="text-sm text-gray-600">
{{ weather.data[0].index[3].desc }}
</p>
</div>
<!-- ./提示 -->
</div>
<!-- 7日天气列表 -->
<div class="lg:col-span-3 bg-white rounded-xl p-5 mb-6 card-shadow transition-all duration-300 hover:shadow-lg">
<h2 class="text-lg font-semibold text-gray-800 mb-4">{{ weather.city }}未来天气</h2>
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-7 gap-4">
<!-- 每日天气卡片 - 动态生成 -->
<div class="bg-gray-50 rounded-lg p-4 text-center hover:bg-gray-100 transition-colors duration-200">
<div class="font-medium">{{ weather.data[0].date }}</div>
<div class="text-sm text-gray-500 mb-2">{{ weather.data[0].week }}</div>
<img class="mb-3" style="width: 30px; height: 30px; margin: 0 auto 0.75rem;" src="/static/icon/{{ weather.data[0].wea_img }}.png">
<div class="text-sm text-gray-600 mb-2">{{ weather.data[0].wea }}</div>
<div class="flex justify-center items-center gap-2">
<span class="text-warm">{{ weather.data[0].tem2 }}°</span>
<span class="text-gray-300">/</span>
<span class="text-cool">{{ weather.data[0].tem1 }}°</span>
</div>
<div class="text-xs text-gray-500 mt-2">
<i class="fa fa-tint mr-1"></i>{{ weather.data[0].humidity }}
</div>
<div class="text-xs text-gray-500">
<i class="fa fa-wind mr-1"></i>{{ weather.data[0].win[0] }}{{ weather.data[0].win_speed }}
</div>
</div>
<div class="bg-gray-50 rounded-lg p-4 text-center hover:bg-gray-100 transition-colors duration-200">
<div class="font-medium">{{ weather.data[1].date }}</div>
<div class="text-sm text-gray-500 mb-2">{{ weather.data[1].week }}</div>
<img class="mb-1" style="width: 30px; height: 30px; margin: 0 auto 0.75rem;" src="/static/icon/{{ weather.data[1].wea_img }}.png">
<div class="text-sm text-gray-600 mb-2">{{ weather.data[1].wea }}</div>
<div class="flex justify-center items-center gap-2">
<span class="text-warm">{{ weather.data[1].tem2 }}°</span>
<span class="text-gray-300">/</span>
<span class="text-cool">{{ weather.data[1].tem1 }}°</span>
</div>
<div class="text-xs text-gray-500 mt-2">
<i class="fa fa-tint mr-1"></i>{{ weather.data[1].humidity }}
</div>
<div class="text-xs text-gray-500">
<i class="fa fa-wind mr-1"></i>{{ weather.data[1].win[0] }}{{ weather.data[1].win_speed }}
</div>
</div>
<div class="bg-gray-50 rounded-lg p-4 text-center hover:bg-gray-100 transition-colors duration-200">
<div class="font-medium">{{ weather.data[2].date }}</div>
<div class="text-sm text-gray-500 mb-2">{{ weather.data[2].week }}</div>
<img class="mb-1" style="width: 30px; height: 30px; margin: 0 auto 0.75rem;" src="/static/icon/{{ weather.data[2].wea_img }}.png">
<div class="text-sm text-gray-600 mb-2">{{ weather.data[2].wea }}</div>
<div class="flex justify-center items-center gap-2">
<span class="text-warm">{{ weather.data[2].tem2 }}°</span>
<span class="text-gray-300">/</span>
<span class="text-cool">{{ weather.data[2].tem1 }}°</span>
</div>
<div class="text-xs text-gray-500 mt-2">
<i class="fa fa-tint mr-1"></i>{{ weather.data[2].humidity }}
</div>
<div class="text-xs text-gray-500">
<i class="fa fa-wind mr-1"></i>{{ weather.data[2].win[0] }}{{ weather.data[2].win_speed }}
</div>
</div>
<div class="bg-gray-50 rounded-lg p-4 text-center hover:bg-gray-100 transition-colors duration-200">
<div class="font-medium">{{ weather.data[3].date }}</div>
<div class="text-sm text-gray-500 mb-2">{{ weather.data[3].week }}</div>
<img class="mb-1" style="width: 30px; height: 30px; margin: 0 auto 0.75rem;" src="/static/icon/{{ weather.data[3].wea_img }}.png">
<div class="text-sm text-gray-600 mb-2">{{ weather.data[3].wea }}</div>
<div class="flex justify-center items-center gap-2">
<span class="text-warm">{{ weather.data[3].tem2 }}°</span>
<span class="text-gray-300">/</span>
<span class="text-cool">{{ weather.data[3].tem1 }}°</span>
</div>
<div class="text-xs text-gray-500 mt-2">
<i class="fa fa-tint mr-1"></i>{{ weather.data[3].humidity }}
</div>
<div class="text-xs text-gray-500">
<i class="fa fa-wind mr-1"></i>{{ weather.data[3].win[0] }}{{ weather.data[3].win_speed }}
</div>
</div>
<div class="bg-gray-50 rounded-lg p-4 text-center hover:bg-gray-100 transition-colors duration-200">
<div class="font-medium">{{ weather.data[4].date }}</div>
<div class="text-sm text-gray-500 mb-2">{{ weather.data[4].week }}</div>
<img class="mb-1" style="width: 30px; height: 30px; margin: 0 auto 0.75rem;" src="/static/icon/{{ weather.data[4].wea_img }}.png">
<div class="text-sm text-gray-600 mb-2">{{ weather.data[4].wea }}</div>
<div class="flex justify-center items-center gap-2">
<span class="text-warm">{{ weather.data[4].tem2 }}°</span>
<span class="text-gray-300">/</span>
<span class="text-cool">{{ weather.data[4].tem1 }}°</span>
</div>
<div class="text-xs text-gray-500 mt-2">
<i class="fa fa-tint mr-1"></i>{{ weather.data[4].humidity }}
</div>
<div class="text-xs text-gray-500">
<i class="fa fa-wind mr-1"></i>{{ weather.data[4].win[0] }}{{ weather.data[4].win_speed }}
</div>
</div>
<div class="bg-gray-50 rounded-lg p-4 text-center hover:bg-gray-100 transition-colors duration-200">
<div class="font-medium">{{ weather.data[5].date }}</div>
<div class="text-sm text-gray-500 mb-2">{{ weather.data[5].week }}</div>
<img class="mb-1" style="width: 30px; height: 30px; margin: 0 auto 0.75rem;" src="/static/icon/{{ weather.data[5].wea_img }}.png">
<div class="text-sm text-gray-600 mb-2">{{ weather.data[5].wea }}</div>
<div class="flex justify-center items-center gap-2">
<span class="text-warm">{{ weather.data[5].tem2 }}°</span>
<span class="text-gray-300">/</span>
<span class="text-cool">{{ weather.data[5].tem1 }}°</span>
</div>
<div class="text-xs text-gray-500 mt-2">
<i class="fa fa-tint mr-1"></i>{{ weather.data[5].humidity }}
</div>
<div class="text-xs text-gray-500">
<i class="fa fa-wind mr-1"></i>{{ weather.data[5].win[0] }}{{ weather.data[5].win_speed }}
</div>
</div>
<div class="bg-gray-50 rounded-lg p-4 text-center hover:bg-gray-100 transition-colors duration-200">
<div class="font-medium">{{ weather.data[6].date }}</div>
<div class="text-sm text-gray-500 mb-2">{{ weather.data[6].week }}</div>
<img class="mb-1" style="width: 30px; height: 30px; margin: 0 auto 0.75rem;" src="/static/icon/{{ weather.data[6].wea_img }}.png">
<div class="text-sm text-gray-600 mb-2">{{ weather.data[6].wea }}</div>
<div class="flex justify-center items-center gap-2">
<span class="text-warm">{{ weather.data[6].tem2 }}°</span>
<span class="text-gray-300">/</span>
<span class="text-cool">{{ weather.data[6].tem1 }}°</span>
</div>
<div class="text-xs text-gray-500 mt-2">
<i class="fa fa-tint mr-1"></i>{{ weather.data[6].humidity }}
</div>
<div class="text-xs text-gray-500">
<i class="fa fa-wind mr-1"></i>{{ weather.data[6].win[0] }}{{ weather.data[6].win_speed }}
</div>
</div>
</div>
</div>
</main>
JS部分
document.addEventListener('DOMContentLoaded', function() {
initTemperatureChart();
});
// 未来7天的天气数据
const weatherData = {
days: ['{{ weather.data[0].week }}', '{{ weather.data[1].week }}', '{{ weather.data[2].week }}', '{{ weather.data[3].week }}', '{{ weather.data[4].week }}', '{{ weather.data[5].week }}', '{{ weather.data[6].week }}'],
dates: ['{{ weather.data[0].date }}', '{{ weather.data[1].date }}', '{{ weather.data[2].date }}', '{{ weather.data[3].date }}', '{{ weather.data[4].date }}', '{{ weather.data[5].date }}', '{{ weather.data[6].date }}'],
highTemp: [{{ weather.data[0].tem1 }}, {{ weather.data[1].tem1 }}, {{ weather.data[2].tem1 }}, {{ weather.data[3].tem1 }}, {{ weather.data[4].tem1 }}, {{ weather.data[5].tem1 }}, {{ weather.data[6].tem1 }}],
lowTemp: [{{ weather.data[0].tem2 }}, {{ weather.data[1].tem2 }}, {{ weather.data[2].tem2 }}, {{ weather.data[3].tem2 }}, {{ weather.data[4].tem2 }}, {{ weather.data[5].tem2 }}, {{ weather.data[6].tem2 }}],
weather: ['{{ weather.data[0].wea }}', '{{ weather.data[1].wea }}', '{{ weather.data[2].wea }}', '{{ weather.data[3].wea }}', '{{ weather.data[4].wea }}', '{{ weather.data[5].wea }}', '{{ weather.data[6].wea }}']
};
// 初始化ECharts图表
function initTemperatureChart() {
// 获取图表容器
const chartDom = document.getElementById('temperatureChart');
const myChart = echarts.init(chartDom);
// 配置图表
const option = {
backgroundColor: 'transparent',
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(255, 255, 255, 0.9)',
borderColor: '#eee',
borderWidth: 1,
textStyle: { color: '#333' },
formatter: function(params) {
let param = params[0];
return `<div class="font-medium">${weatherData.dates[param.dataIndex]}</div>
<div>最高温度: ${weatherData.highTemp[param.dataIndex]}°C</div>
<div>最低温度: ${weatherData.lowTemp[param.dataIndex]}°C</div>
<div>天气: ${weatherData.weather[param.dataIndex]}</div>`;
}
},
legend: {
data: ['最高温度', '最低温度'],
top: 0,
textStyle: { color: '#666' }
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: [{
type: 'category',
boundaryGap: false,
data: weatherData.days.map((day, i) => `${weatherData.days[i]}`),
axisLine: {
lineStyle: {
color: '#ddd'
}
},
position: 'top',
axisLabel: {
lineHeight: 30,
color: '#666',
rich: {
// 模板1:美食图标(本地/在线图片均可,此处用在线图标)
icon_qing: {
width: 25, // 图标宽度
height: 25, // 图标高度
backgroundColor: {
image: '/static/icon/qing.png' // 图标地址
}
},
icon_yun: {
width: 25, // 图标宽度
height: 25, // 图标高度
backgroundColor: {
image: '/static/icon/yun.png' // 图标地址
}
},
icon_yin: {
width: 25, // 图标宽度
height: 25, // 图标高度
backgroundColor: {
image: '/static/icon/yin.png' // 图标地址
}
},
icon_lei: {
width: 25, // 图标宽度
height: 25, // 图标高度
backgroundColor: {
image: '/static/icon/lei.png' // 图标地址
}
},
icon_xue: {
width: 25, // 图标宽度
height: 25, // 图标高度
backgroundColor: {
image: '/static/icon/xue.png' // 图标地址
}
},
icon_shachen: {
width: 25, // 图标宽度
height: 25, // 图标高度
backgroundColor: {
image: '/static/icon/shachen.png' // 图标地址
}
},
icon_wu: {
width: 25, // 图标宽度
height: 25, // 图标高度
backgroundColor: {
image: '/static/icon/wu.png' // 图标地址
}
},
icon_bingbao: {
width: 25, // 图标宽度
height: 25, // 图标高度
backgroundColor: {
image: '/static/icon/bingbao.png' // 图标地址
}
},
icon_yu: {
width: 25, // 图标宽度
height: 25, // 图标高度
backgroundColor: {
image: '/static/icon/yu.png' // 图标地址
}
}
},
// 2. 调用 rich 模板,组合“图标+文字”
formatter: function(value) {
// 根据 X 轴数据(value)匹配对应的图标模板
switch (value) {
case '{{ weather.data[0].week }}':
return '{{ weather.data[0].date }}\n{icon_{{ weather.data[0].wea_img }}|}\n{{ weather.data[0].wea }}\n';
case '{{ weather.data[1].week }}':
return '{{ weather.data[1].date }}\n{icon_{{ weather.data[1].wea_img }}|}\n{{ weather.data[1].wea }}\n';
case '{{ weather.data[2].week }}':
return '{{ weather.data[2].date }}\n{icon_{{ weather.data[2].wea_img }}|}\n{{ weather.data[2].wea }}\n';
case '{{ weather.data[3].week }}':
return '{{ weather.data[3].date }}\n{icon_{{ weather.data[3].wea_img }}|}\n{{ weather.data[3].wea }}\n';
case '{{ weather.data[4].week }}':
return '{{ weather.data[4].date }}\n{icon_{{ weather.data[4].wea_img }}|}\n{{ weather.data[4].wea }}\n';
case '{{ weather.data[5].week }}':
return '{{ weather.data[5].date }}\n{icon_{{ weather.data[5].wea_img }}|}\n{{ weather.data[5].wea }}\n';
case '{{ weather.data[6].week }}':
return '{{ weather.data[6].date }}\n{icon_{{ weather.data[6].wea_img }}|}\n{{ weather.data[6].wea }}\n';
default:
return value;
}
},
// 标签横向对齐(避免图标偏移)
align: 'center'
}
}, {
type: 'category',
boundaryGap: false,
data: weatherData.days.map((day, i) => `${weatherData.days[i]}`),
axisLine: {
lineStyle: {
color: '#ddd'
}
},
position: 'bottom',
axisLabel: {
lineHeight: 20,
color: '#666'
}
}, ],
yAxis: {
type: 'value',
name: '',
nameTextStyle: { color: '#666' },
axisLine: {
lineStyle: { color: '#ddd' }
},
splitLine: {
lineStyle: { color: '#f0f0f0' }
},
axisLabel: {
formatter: '{value}',
color: '#666'
},
min: function(value) {
return value.min - 2; // 最小值向下调整2度
},
max: function(value) {
return value.max + 2; // 最大值向上调整2度
}
},
series: [
{
name: '最高温度',
type: 'line',
data: weatherData.highTemp,
symbol: 'circle',
symbolSize: 8,
lineStyle: {
width: 3,
color: '#F56C6C' // 暖色调
},
itemStyle: {
color: '#F56C6C',
borderWidth: 2,
borderColor: '#fff',
shadowBlur: 4,
shadowColor: 'rgba(245, 108, 108, 0.5)'
},
emphasis: {
scale: true,
symbolSize: 10
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(245, 108, 108, 0.3)' },
{ offset: 1, color: 'rgba(245, 108, 108, 0)' }
])
}
},
{
name: '最低温度',
type: 'line',
data: weatherData.lowTemp,
symbol: 'circle',
symbolSize: 8,
lineStyle: {
width: 3,
color: '#4E5BA6' // 冷色调
},
itemStyle: {
color: '#4E5BA6',
borderWidth: 2,
borderColor: '#fff',
shadowBlur: 4,
shadowColor: 'rgba(78, 91, 166, 0.5)'
},
emphasis: {
scale: true,
symbolSize: 10
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(78, 91, 166, 0.3)' },
{ offset: 1, color: 'rgba(78, 91, 166, 0)' }
])
}
}
]
};
// 设置图表配置项
myChart.setOption(option);
// 响应窗口大小变化
window.addEventListener('resize', function() {
myChart.resize();
});
}