鸿蒙原生应用实战(九)ArkUI 天气预报 App:HTTP 请求 + 定位 + 动效

🌤️ 鸿蒙原生应用实战(九)ArkUI 天气预报 App:HTTP 请求 + 定位 + 动效

博主说: 天气预报是每个手机的基础应用,也是学习「网络请求 + 定位 + 动画」全链路的最佳项目。今天我们用 ArkUI 的 HTTP 网络请求 + 地理定位 API,从零实现一个支持自动定位、未来 7 天预报、天气动效的完整天气 App


📱 应用场景

功能 说明
📍 自动定位 获取当前位置并显示天气
🌡️ 实时温度 当前温度 + 体感温度
📅 7天预报 未来 7 天的天气概况
🎨 天气动效 晴天/阴天/雨天/雪天动态背景
🔍 城市搜索 手动切换城市
🌤️ 天气详情 湿度/风速/紫外线/空气质量

⚙️ 运行环境要求

项目 版本要求
DevEco Studio 5.0.3.800+
HarmonyOS SDK API 12
核心 API @ohos.net.http + @ohos.geoLocation
权限 INTERNET + LOCATION

🛠️ 实战:从零搭建天气预报 App

Step 1:天气数据模型

typescript 复制代码
interface WeatherData {
  city: string;
  temperature: number;     // 当前温度
  feelsLike: number;       // 体感温度
  condition: string;       // 天气状况 (晴/多云/雨等)
  humidity: number;        // 湿度 %
  windSpeed: number;       // 风速
  uvIndex: number;         // 紫外线
  aqi: number;             // 空气质量指数
  forecast: DayForecast[]; // 7天预报
}
interface DayForecast {
  date: string;
  high: number;  low: number;
  condition: string;
  icon: string;
}

Step 2:完整代码

typescript 复制代码
// pages/Index.ets --- 天气预报
import http from '@ohos.net.http';
import geoLocation from '@ohos.geoLocation';
import abilityAccessCtrl from '@ohos.abilityAccessCtrl';

@Entry
@Component
struct WeatherApp {
  @State weather: WeatherData | null = null;
  @State isLoading: boolean = true;
  @State cityName: string = '北京';
  @State currentPage: number = 0;  // 0=今天, 1=7天
  @State locationEnabled: boolean = false;

  // 天气动画状态
  @State animationType: 'sunny' | 'cloudy' | 'rainy' | 'snowy' = 'sunny';

  aboutToAppear() {
    this.requestLocation();
  }

  async requestLocation() {
    try {
      const atManager = abilityAccessCtrl.createAtManager();
      await atManager.requestPermissionsFromUser(
        getContext(this), ['ohos.permission.LOCATION']
      );
      this.locationEnabled = true;

      const loc = await geoLocation.getCurrentLocation({
        priority: geoLocation.LocationRequestPriority.FIRST_FIX,
        timeoutMs: 5000
      });
      // 用经纬度获取城市名
      this.fetchWeatherByCoords(loc.latitude, loc.longitude);
    } catch {
      // 定位失败,用默认城市
      this.fetchWeatherByCity('北京');
    }
  }

  async fetchWeatherByCoords(lat: number, lon: number) {
    this.isLoading = true;
    try {
      const req = http.createHttp();
      const resp = await req.request(
        `https://api.openweathermap.org/data/2.5/weather?lat=${lat}&lon=${lon}&appid=YOUR_API_KEY&units=metric&lang=zh_cn`,
        { method: http.RequestMethod.GET, expectDataType: http.HttpDataType.OBJECT }
      );
      const data = JSON.parse(resp.result as string);
      this.cityName = data.name;
      this.parseWeatherData(data);
    } catch {
      this.fetchWeatherByCity('北京');
    }
    this.isLoading = false;
  }

  async fetchWeatherByCity(city: string) {
    this.isLoading = true;
    try {
      const req = http.createHttp();
      const resp = await req.request(
        `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=YOUR_API_KEY&units=metric&lang=zh_cn`,
        { method: http.RequestMethod.GET, expectDataType: http.HttpDataType.OBJECT }
      );
      const data = JSON.parse(resp.result as string);
      this.parseWeatherData(data);

      // 同时获取7天预报
      const resp2 = await req.request(
        `https://api.openweathermap.org/data/2.5/forecast?q=${city}&appid=YOUR_API_KEY&units=metric&lang=zh_cn`,
        { method: http.RequestMethod.GET }
      );
      const forecastData = JSON.parse(resp2.result as string);
      this.parseForecast(forecastData);
    } catch (err) {
      AlertDialog.show({ message: '获取天气数据失败,请检查网络' });
    }
    this.isLoading = false;
  }

  parseWeatherData(data: any) {
    this.weather = {
      city: data.name,
      temperature: Math.round(data.main.temp),
      feelsLike: Math.round(data.main.feels_like),
      condition: data.weather[0].description,
      humidity: data.main.humidity,
      windSpeed: data.wind.speed,
      uvIndex: 0,
      aqi: 0,
      forecast: []
    };
    this.updateAnimation(data.weather[0].main);
  }

  parseForecast(data: any) {
    if (!this.weather) return;
    const daily: DayForecast[] = [];
    const groups: Record<string, any[]> = {};
    
    for (const item of data.list) {
      const date = item.dt_txt.split(' ')[0];
      if (!groups[date]) groups[date] = [];
      groups[date].push(item);
    }

    let count = 0;
    for (const date of Object.keys(groups)) {
      if (count >= 7) break;
      const items = groups[date];
      const highs = items.map((i: any) => i.main.temp_max);
      const lows = items.map((i: any) => i.main.temp_min);
      daily.push({
        date: date,
        high: Math.round(Math.max(...highs)),
        low: Math.round(Math.min(...lows)),
        condition: items[4]?.weather[0]?.description || items[0].weather[0].description,
        icon: items[0].weather[0].icon
      });
      count++;
    }
    this.weather.forecast = daily;
  }

  updateAnimation(condition: string) {
    if (condition.includes('Rain') || condition.includes('Drizzle')) {
      this.animationType = 'rainy';
    } else if (condition.includes('Snow')) {
      this.animationType = 'snowy';
    } else if (condition.includes('Cloud')) {
      this.animationType = 'cloudy';
    } else {
      this.animationType = 'sunny';
    }
  }

  getWeatherIcon(): string {
    const icons: Record<string, string> = {
      'sunny': '☀️', 'cloudy': '⛅', 'rainy': '🌧️', 'snowy': '❄️'
    };
    return icons[this.animationType] || '☀️';
  }

  build() {
    Stack() {
      // ---- 天气动效背景 ----
      Column()
        .width('100%').height('100%')
        .backgroundColor(
          this.animationType === 'sunny' ? '#4A90D9' :
          this.animationType === 'cloudy' ? '#8E9EAB' :
          this.animationType === 'rainy' ? '#4A5568' : '#B8C6D0'
        )

      // ---- 主内容 ----
      if (this.isLoading) {
        Column() {
          LoadingProgress().width(48).height(48).color('#fff')
          Text('获取天气数据...').fontSize(16).fontColor('#fff').margin({ top: 16 })
        }
      } else if (this.weather) {
        Column() {
          // 城市 + 刷新
          Row() {
            Text(this.weather.city).fontSize(20).fontWeight(FontWeight.Bold).fontColor('#fff')
            Text('↻').fontSize(18).fontColor('rgba(255,255,255,0.7)').margin({ left: 8 })
              .onClick(() => { this.fetchWeatherByCity(this.cityName); })
          }.padding({ top: 40 })

          // 温度
          Text(this.getWeatherIcon()).fontSize(64).margin({ top: 8 })
          Text(`${this.weather.temperature}°C`).fontSize(64).fontWeight(FontWeight.Bold)
            .fontColor('#fff').margin({ top: 4 })
          Text(this.weather.condition).fontSize(18).fontColor('rgba(255,255,255,0.8)')
          Text(`体感 ${this.weather.feelsLike}°C`).fontSize(14).fontColor('rgba(255,255,255,0.6)')
            .margin({ top: 4 })

          // 详情卡片
          Row() {
            WeatherDetail('💧', '湿度', `${this.weather.humidity}%`)
            WeatherDetail('💨', '风速', `${this.weather.windSpeed} m/s`)
            WeatherDetail('☀️', '紫外线', `${this.weather.uvIndex}`)
            WeatherDetail('🌫️', 'AQI', `${this.weather.aqi}`)
          }
          .width('94%').padding(16)
          .backgroundColor('rgba(255,255,255,0.2)')
          .borderRadius(16).margin({ top: 16 })

          // 7天预报
          if (this.weather.forecast.length > 0) {
            Text('📅 未来 7 天').fontSize(16).fontWeight(FontWeight.Bold)
              .fontColor('#fff').margin({ top: 16, bottom: 8 })

            List() {
              ForEach(this.weather.forecast, (day: DayForecast) => {
                ListItem() {
                  Row() {
                    Text(day.date.substring(5)).fontSize(14).fontColor('#fff').width(50)
                    Text(this.getConditionEmoji(day.condition)).fontSize(20).width(30)
                    Text(day.condition).fontSize(13).fontColor('rgba(255,255,255,0.7)').layoutWeight(1)
                    Text(`${day.low}°`).fontSize(14).fontColor('rgba(255,255,255,0.6)')
                    Text(`${day.high}°`).fontSize(14).fontColor('#fff').fontWeight(FontWeight.Bold).width(40)
                  }
                  .padding(10).width('94%')
                  .backgroundColor('rgba(255,255,255,0.1)')
                  .borderRadius(8).margin({ top: 4 })
                }
              }, (day: DayForecast) => day.date)
            }
            .height(200)
          }
        }
        .width('100%').height('100%')
        .alignItems(HorizontalAlign.Center)
        .justifyContent(FlexAlign.Start)
      }
    }
    .width('100%').height('100%')
  }

  getConditionEmoji(condition: string): string {
    if (condition.includes('晴')) return '☀️';
    if (condition.includes('云') || condition.includes('阴')) return '☁️';
    if (condition.includes('雨')) return '🌧️';
    if (condition.includes('雪')) return '❄️';
    return '🌤️';
  }
}

@Builder
function WeatherDetail(icon: string, label: string, value: string) {
  Column() {
    Text(icon).fontSize(24)
    Text(label).fontSize(12).fontColor('rgba(255,255,255,0.7)').margin({ top: 4 })
    Text(value).fontSize(16).fontColor('#fff').fontWeight(FontWeight.Bold).margin({ top: 2 })
  }
}

官方文档: HarmonyOS 应用开发文档

相关推荐
金启攻1 小时前
鸿蒙原生应用开发实战(三):数据管理与多页面交互——渔获记录、装备管理与个人中心
harmonyos
伶俜661 小时前
鸿蒙原生应用实战(四)ArkUI 语音变声器:录音 + 4 种音效 + 音调变换算法
算法·华为·harmonyos
HwJack202 小时前
HarmonyOS APP开发终结“户外运动数据失踪”的玄学:玩透穿戴设备 P2P 穿透与心跳保活的心法
华为·harmonyos·p2p
逻极2 小时前
HTTP/HTTPS 协议从入门到精通:从原理到性能提升400%的完整路径(协议优化实战)
网络协议·http·性能优化·https·tls
芒鸽2 小时前
HarmonyOS 网络编程实战:HTTP、WebSocket 与 Socket 通信详解
网络·http·harmonyos
努力的lpp2 小时前
渗透主流工具完整参数手册(sqlmap、Nmap、Hydra、Dirsearch、Xray)
javascript·网络协议·测试工具·安全·http·工具
风满城332 小时前
鸿蒙原生应用实战(二):数独游戏核心逻辑开发 — 棋盘渲染与交互
harmonyos
李白的天不白3 小时前
http https
网络协议·http·https