PHP使用echarts制作一个很漂亮的天气预报网站(曲线图+实况+未来一周预报)
引言
干净利落的天气特效, 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();});
}