Uniapp跨平台天气预报应用开发实战教程 – 一站式多端开发指南

2025-09-03 0 280

一、Uniapp框架概述

Uniapp是一个使用Vue.js语法开发所有前端应用的框架,可以编译到iOS、Android、Web以及各种小程序平台。

本教程将带领您从零开始开发一个功能完整的天气预报应用,涵盖以下核心功能:

  • 当前位置自动获取与城市选择
  • 实时天气信息展示
  • 多日天气预报
  • 天气指数和生活建议
  • 主题切换和个性化设置

二、项目结构与配置

1. 创建Uniapp项目

# 使用HBuilderX创建项目或使用CLI
vue create -p dcloudio/uni-preset-vue weather-app

# 选择默认模板
# 进入项目目录
cd weather-app
    

2. 项目目录结构

weather-app/
├── pages/                 // 页面文件
│   ├── index/            // 首页
│   ├── city-list/        // 城市列表页
│   └── settings/         // 设置页
├── components/           // 组件目录
│   ├── weather-card/     // 天气卡片组件
│   ├── daily-forecast/   // 每日预报组件
│   └── air-quality/      // 空气质量组件
├── static/               // 静态资源
├── store/                // Vuex状态管理
├── common/               // 公共工具类
├── App.vue               // 应用配置
├── main.js               // 入口文件
├── manifest.json         // 应用配置
└── pages.json            // 页面配置
    

3. 修改pages.json配置路由

{
    "pages": [
        {
            "path": "pages/index/index",
            "style": {
                "navigationBarTitleText": "天气预报",
                "enablePullDownRefresh": true
            }
        },
        {
            "path": "pages/city-list/city-list",
            "style": {
                "navigationBarTitleText": "城市选择"
            }
        },
        {
            "path": "pages/settings/settings",
            "style": {
                "navigationBarTitleText": "设置"
            }
        }
    ],
    "globalStyle": {
        "navigationBarTextStyle": "white",
        "navigationBarTitleText": "天气预报",
        "navigationBarBackgroundColor": "#4a90e2",
        "backgroundColor": "#f5f5f5"
    }
}
    

三、核心功能实现

1. 首页实现 (pages/index/index.vue)

<template>
    <view class="container" :class="{'dark-theme': isDarkMode}">
        <view class="header">
            <view class="location" @click="chooseCity">
                <text class="icon">📍</text>
                <text class="city-name">{{ currentCity }}</text>
                <text class="icon">▼</text>
            </view>
            <text class="date">{{ currentDate }}</text>
        </view>
        
        <scroll-view scroll-y="true" class="content" @scrolltolower="loadMore">
            <view class="current-weather">
                <text class="temperature">{{ currentWeather.temp }}°</text>
                <text class="description">{{ currentWeather.weather }}</text>
                <view class="details">
                    <text>湿度: {{ currentWeather.humidity }}%</text>
                    <text>风速: {{ currentWeather.windSpeed }}km/h</text>
                    <text>气压: {{ currentWeather.pressure }}hPa</text>
                </view>
            </view>
            
            <daily-forecast :forecastData="dailyForecast"></daily-forecast>
            
            <view class="indexes">
                <view class="index-card" v-for="(item, index) in lifeIndexes" :key="index">
                    <text class="index-name">{{ item.name }}</text>
                    <text class="index-value">{{ item.value }}</text>
                    <text class="index-desc">{{ item.desc }}</text>
                </view>
            </view>
        </scroll-view>
    </view>
</template>

<script>
import dailyForecast from '@/components/daily-forecast/daily-forecast.vue';
import { mapState, mapMutations } from 'vuex';

export default {
    components: { dailyForecast },
    data() {
        return {
            currentDate: '',
            currentWeather: {
                temp: '--',
                weather: '--',
                humidity: '--',
                windSpeed: '--',
                pressure: '--'
            },
            dailyForecast: [],
            lifeIndexes: []
        };
    },
    computed: {
        ...mapState(['currentCity', 'isDarkMode'])
    },
    onLoad() {
        this.getCurrentDate();
        this.getLocation();
    },
    onPullDownRefresh() {
        this.loadWeatherData();
        setTimeout(() => {
            uni.stopPullDownRefresh();
        }, 1000);
    },
    methods: {
        ...mapMutations(['setCurrentCity']),
        
        getCurrentDate() {
            const date = new Date();
            const options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' };
            this.currentDate = date.toLocaleDateString('zh-CN', options);
        },
        
        async getLocation() {
            try {
                const res = await uni.getLocation({ type: 'wgs84' });
                this.getCityNameFromCoords(res.latitude, res.longitude);
            } catch (error) {
                console.error('获取位置失败', error);
                this.loadWeatherData(); // 使用默认城市
            }
        },
        
        async getCityNameFromCoords(lat, lon) {
            // 这里使用高德地图逆地理编码API
            try {
                const response = await uni.request({
                    url: `https://restapi.amap.com/v3/geocode/regeo?key=YOUR_API_KEY&location=${lon},${lat}`
                });
                const city = response.data.regeocode.addressComponent.city;
                this.setCurrentCity(city);
                this.loadWeatherData();
            } catch (error) {
                console.error('获取城市名称失败', error);
            }
        },
        
        async loadWeatherData() {
            uni.showLoading({ title: '加载中...' });
            try {
                // 获取实时天气
                const currentResponse = await uni.request({
                    url: `https://api.openweathermap.org/data/2.5/weather?q=${this.currentCity}&units=metric&appid=YOUR_API_KEY&lang=zh_cn`
                });
                this.processCurrentWeather(currentResponse.data);
                
                // 获取天气预报
                const forecastResponse = await uni.request({
                    url: `https://api.openweathermap.org/data/2.5/forecast?q=${this.currentCity}&units=metric&appid=YOUR_API_KEY&lang=zh_cn`
                });
                this.processForecast(forecastResponse.data);
                
                // 获取生活指数
                this.getLifeIndexes();
            } catch (error) {
                console.error('获取天气数据失败', error);
                uni.showToast({ title: '获取数据失败', icon: 'none' });
            } finally {
                uni.hideLoading();
            }
        },
        
        processCurrentWeather(data) {
            this.currentWeather = {
                temp: Math.round(data.main.temp),
                weather: data.weather[0].description,
                humidity: data.main.humidity,
                windSpeed: data.wind.speed,
                pressure: data.main.pressure
            };
        },
        
        processForecast(data) {
            // 处理5天天气预报数据
            const dailyData = {};
            data.list.forEach(item => {
                const date = item.dt_txt.split(' ')[0];
                if (!dailyData[date]) {
                    dailyData[date] = {
                        date: date,
                        temp_min: item.main.temp_min,
                        temp_max: item.main.temp_max,
                        weather: item.weather[0].main,
                        icon: item.weather[0].icon
                    };
                } else {
                    dailyData[date].temp_min = Math.min(dailyData[date].temp_min, item.main.temp_min);
                    dailyData[date].temp_max = Math.max(dailyData[date].temp_max, item.main.temp_max);
                }
            });
            
            this.dailyForecast = Object.values(dailyData).slice(0, 5);
        },
        
        getLifeIndexes() {
            // 模拟生活指数数据
            this.lifeIndexes = [
                { name: '舒适度', value: '较舒适', desc: '天气较好,感觉舒适' },
                { name: '紫外线', value: '中等', desc: '需要涂抹防晒霜' },
                { name: '穿衣', value: '适中', desc: '建议穿着轻便衣物' },
                { name: '运动', value: '适宜', desc: '适合户外运动' }
            ];
        },
        
        chooseCity() {
            uni.navigateTo({
                url: '/pages/city-list/city-list'
            });
        },
        
        loadMore() {
            // 加载更多数据
            console.log('加载更多');
        }
    }
};
</script>

<style>
.container {
    min-height: 100vh;
    background: linear-gradient(to bottom, #4a90e2, #f5f5f5);
    padding: 20rpx;
}

.dark-theme {
    background: linear-gradient(to bottom, #2c3e50, #1a1a1a);
    color: #ffffff;
}

.header {
    text-align: center;
    padding: 40rpx 0;
}

.location {
    display: flex;
    justify-content: center;
    align-items: center;
    margin-bottom: 20rpx;
}

.city-name {
    font-size: 36rpx;
    font-weight: bold;
    margin: 0 20rpx;
}

.date {
    font-size: 28rpx;
    color: #666;
}

.dark-theme .date {
    color: #ccc;
}

.current-weather {
    text-align: center;
    padding: 40rpx 0;
}

.temperature {
    font-size: 100rpx;
    font-weight: bold;
    display: block;
}

.description {
    font-size: 36rpx;
    display: block;
    margin: 20rpx 0;
}

.details {
    display: flex;
    justify-content: space-around;
    margin-top: 40rpx;
}

.details text {
    font-size: 24rpx;
}

.content {
    height: calc(100vh - 200rpx);
}

.indexes {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
    margin-top: 40rpx;
}

.index-card {
    width: 48%;
    background-color: rgba(255, 255, 255, 0.8);
    border-radius: 16rpx;
    padding: 20rpx;
    margin-bottom: 20rpx;
    box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
}

.dark-theme .index-card {
    background-color: rgba(50, 50, 50, 0.8);
}

.index-name {
    font-size: 28rpx;
    font-weight: bold;
    display: block;
}

.index-value {
    font-size: 32rpx;
    display: block;
    margin: 10rpx 0;
}

.index-desc {
    font-size: 24rpx;
    color: #666;
    display: block;
}

.dark-theme .index-desc {
    color: #ccc;
}
</style>
    

2. Vuex状态管理 (store/index.js)

import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

const store = new Vuex.Store({
    state: {
        currentCity: '北京',
        isDarkMode: false,
        favoriteCities: []
    },
    mutations: {
        setCurrentCity(state, city) {
            state.currentCity = city;
            // 保存到本地存储
            uni.setStorageSync('currentCity', city);
        },
        toggleDarkMode(state) {
            state.isDarkMode = !state.isDarkMode;
            uni.setStorageSync('isDarkMode', state.isDarkMode);
        },
        addFavoriteCity(state, city) {
            if (!state.favoriteCities.includes(city)) {
                state.favoriteCities.push(city);
                uni.setStorageSync('favoriteCities', JSON.stringify(state.favoriteCities));
            }
        },
        removeFavoriteCity(state, city) {
            const index = state.favoriteCities.indexOf(city);
            if (index > -1) {
                state.favoriteCities.splice(index, 1);
                uni.setStorageSync('favoriteCities', JSON.stringify(state.favoriteCities));
            }
        }
    },
    actions: {
        loadStorageData({ commit }) {
            // 从本地存储加载数据
            const city = uni.getStorageSync('currentCity');
            if (city) commit('setCurrentCity', city);
            
            const isDarkMode = uni.getStorageSync('isDarkMode');
            if (isDarkMode) commit('toggleDarkMode');
            
            const favorites = uni.getStorageSync('favoriteCities');
            if (favorites) {
                state.favoriteCities = JSON.parse(favorites);
            }
        }
    }
});

export default store;
    

3. 城市选择页面 (pages/city-list/city-list.vue)

<template>
    <view class="city-list">
        <view class="search-bar">
            <input 
                class="search-input" 
                placeholder="搜索城市" 
                v-model="searchQuery"
                @input="searchCities"
            />
        </view>
        
        <scroll-view scroll-y="true" class="list-content">
            <view class="section" v-if="favoriteCities.length">
                <text class="section-title">收藏城市</text>
                <view class="city-item" v-for="city in favoriteCities" :key="city" @click="selectCity(city)">
                    <text>{{ city }}</text>
                    <text class="icon" @click.stop="removeFavorite(city)">❤️</text>
                </view>
            </view>
            
            <view class="section">
                <text class="section-title">热门城市</text>
                <view class="city-item" v-for="city in hotCities" :key="city" @click="selectCity(city)">
                    <text>{{ city }}</text>
                    <text class="icon" @click.stop="addFavorite(city)">🤍</text>
                </view>
            </view>
            
            <view class="section" v-if="searchResults.length">
                <text class="section-title">搜索结果</text>
                <view class="city-item" v-for="city in searchResults" :key="city" @click="selectCity(city)">
                    <text>{{ city }}</text>
                </view>
            </view>
        </scroll-view>
    </view>
</template>

<script>
import { mapState, mapMutations } from 'vuex';

export default {
    data() {
        return {
            searchQuery: '',
            searchResults: [],
            hotCities: ['北京', '上海', '广州', '深圳', '杭州', '成都', '武汉', '西安']
        };
    },
    computed: {
        ...mapState(['favoriteCities'])
    },
    methods: {
        ...mapMutations(['setCurrentCity', 'addFavoriteCity', 'removeFavoriteCity']),
        
        searchCities() {
            if (this.searchQuery.length > 1) {
                // 模拟搜索,实际应调用API
                this.searchResults = this.hotCities.filter(city => 
                    city.includes(this.searchQuery)
                );
            } else {
                this.searchResults = [];
            }
        },
        
        selectCity(city) {
            this.setCurrentCity(city);
            uni.navigateBack();
        },
        
        addFavorite(city) {
            this.addFavoriteCity(city);
            uni.showToast({ title: '已添加收藏', icon: 'success' });
        },
        
        removeFavorite(city) {
            this.removeFavoriteCity(city);
            uni.showToast({ title: '已取消收藏', icon: 'success' });
        }
    }
};
</script>

<style>
.city-list {
    padding: 20rpx;
}

.search-bar {
    margin-bottom: 30rpx;
}

.search-input {
    height: 80rpx;
    background-color: #f5f5f5;
    border-radius: 40rpx;
    padding: 0 30rpx;
    font-size: 28rpx;
}

.list-content {
    height: calc(100vh - 120rpx);
}

.section {
    margin-bottom: 40rpx;
}

.section-title {
    font-size: 30rpx;
    font-weight: bold;
    display: block;
    margin-bottom: 20rpx;
    color: #666;
}

.city-item {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 24rpx 0;
    border-bottom: 1rpx solid #eee;
}

.city-item:active {
    background-color: #f9f9f9;
}

.icon {
    font-size: 36rpx;
}
</style>
    

四、多端适配与发布

1. 条件编译处理平台差异

// 在需要区分平台的地方使用条件编译
async shareWeather() {
    // #ifdef MP-WEIXIN
    await uni.share({
        title: `${this.currentCity}天气情况`,
        path: `/pages/index/index?city=${this.currentCity}`
    });
    // #endif
    
    // #ifdef APP-PLUS
    await uni.share({
        type: 0,
        href: `https://weather.example.com/?city=${this.currentCity}`,
        title: `${this.currentCity}天气情况`
    });
    // #endif
    
    // #ifdef H5
    alert('分享功能在H5端需要自定义实现');
    // #endif
}
    

2. manifest.json配置

{
    "name": "天气预报",
    "appid": "__UNI__XXXXXX",
    "description": "多功能天气预报应用",
    "versionName": "1.0.0",
    "versionCode": "100",
    "transformPx": false,
    "app-plus": {
        "usingComponents": true,
        "nvueStyleCompiler": "uni-app",
        "compilerVersion": 3,
        "splashscreen": {
            "alwaysShowBeforeRender": true,
            "waiting": true,
            "autoclose": true,
            "delay": 0
        }
    },
    "mp-weixin": {
        "appid": "wxxxxxxxxxxxxxxxx",
        "setting": {
            "urlCheck": false
        },
        "usingComponents": true
    },
    "h5": {
        "router": {
            "mode": "hash"
        },
        "template": "index.html"
    }
}
    

五、性能优化技巧

  1. 图片优化:使用WebP格式,合理压缩图片大小
  2. 数据缓存:合理使用本地存储减少API请求
  3. 组件懒加载:使用异步组件减少初始加载时间
  4. 减少DOM节点:优化页面结构,避免过多嵌套
  5. API请求优化:合并请求,使用节流防抖

六、常见问题与解决方案

问题 解决方案
跨域问题 使用uni.request代理或配置服务器CORS
样式兼容性 使用rpx单位,测试多端样式表现
API限制 实现请求缓存,避免频繁调用
打包体积过大 使用分包加载,压缩静态资源

七、总结

本教程详细介绍了如何使用Uniapp开发一个功能完整的天气预报应用,涵盖了从项目创建到多端发布的全过程。通过这个实战项目,您可以学习到:

  1. Uniapp的基本结构和开发模式
  2. Vue.js在Uniapp中的应用
  3. API调用和数据管理
  4. 多端适配和条件编译
  5. 性能优化和最佳实践

Uniapp作为跨平台开发框架,大大提高了开发效率,使开发者能够用一套代码同时发布到多个平台。希望本教程能帮助您快速掌握Uniapp开发技能。

Uniapp跨平台天气预报应用开发实战教程 - 一站式多端开发指南
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

淘吗网 uniapp Uniapp跨平台天气预报应用开发实战教程 – 一站式多端开发指南 https://www.taomawang.com/web/uniapp/1021.html

下一篇:

已经没有下一篇了!

常见问题

相关文章

发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务