PHP使用echarts制作一个很漂亮的天气预报网站(曲线图+实况+未来一周预报)

发布于:2025-09-15 ⋅ 阅读:(15) ⋅ 点赞:(0)

引言

干净利落的天气特效, 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();
    });
}