HarmonyOS ArkTS 实战:实现一个天气查询与生活指数应用

项目效果

本文使用 HarmonyOS 和 ArkTS 实现一个精美的天气查询与生活指数应用。支持多城市天气、24小时预报、7天预报、生活指数、空气质量、天气动画等功能,蓝色渐变背景,毛玻璃卡片设计。

项目使用 DevEco Studio 开发,适配 API 24 及以上版本。

运行效果

天气查询与生活指数应用

功能介绍

  • 当前天气实时展示(温度、天气状况、体感温度)
  • 24小时逐小时预报
  • 7天天气预报
  • 空气质量AQI展示
  • 生活指数(穿衣、紫外线、运动、洗车等)
  • 多城市管理和切换
  • 城市搜索添加
  • 天气动画效果
  • 日出日落时间
  • 风力风向湿度展示
  • 降水概率预报
  • 下拉刷新天气
  • 定位当前城市
  • 温度趋势折线图
  • 恶劣天气预警

定义数据结构

interface HourlyForecast {
  time: string;
  temp: number;
  icon: string;
  desc: string;
}

interface DailyForecast {
  date: string;
  week: string;
  dayIcon: string;
  nightIcon: string;
  highTemp: number;
  lowTemp: number;
  desc: string;
  windDir: string;
  windLevel: string;
}

interface LifeIndex {
  name: string;
  level: string;
  desc: string;
  icon: string;
  color: string;
}

interface CityWeather {
  city: string;
  temp: number;
  feelsLike: number;
  desc: string;
  icon: string;
  humidity: number;
  windDir: string;
  windLevel: string;
  aqi: number;
  aqiLevel: string;
  sunrise: string;
  sunset: string;
  hourly: HourlyForecast[];
  daily: DailyForecast[];
  lifeIndex: LifeIndex[];
}

初始化页面状态

@State private currentCityIndex: number = 0;
@State private showCityList: boolean = false;
@State private searchCity: string = '';

@State private cities: CityWeather[] = [
  {
    city: '上海', temp: 28, feelsLike: 31, desc: '多云', icon: '⛅',
    humidity: 72, windDir: '东南风', windLevel: '3级', aqi: 58, aqiLevel: '良',
    sunrise: '05:12', sunset: '19:02',
    hourly: [
      { time: '现在', temp: 28, icon: '⛅', desc: '多云' },
      { time: '15时', temp: 29, icon: '☀️', desc: '晴' },
      { time: '16时', temp: 29, icon: '☀️', desc: '晴' },
      { time: '17时', temp: 28, icon: '⛅', desc: '多云' },
      { time: '18时', temp: 27, icon: '🌆', desc: '多云' },
      { time: '19时', temp: 26, icon: '🌙', desc: '晴' },
    ],
    daily: [
      { date: '今天', week: '周五', dayIcon: '⛅', nightIcon: '🌙', highTemp: 31, lowTemp: 25, desc: '多云', windDir: '东南风', windLevel: '3级' },
      { date: '明天', week: '周六', dayIcon: '☀️', nightIcon: '🌙', highTemp: 33, lowTemp: 26, desc: '晴', windDir: '南风', windLevel: '2级' },
      { date: '周日', week: '周日', dayIcon: '🌧️', nightIcon: '🌧️', highTemp: 29, lowTemp: 24, desc: '小雨', windDir: '东风', windLevel: '4级' },
      { date: '周一', week: '周一', dayIcon: '⛅', nightIcon: '🌙', highTemp: 30, lowTemp: 25, desc: '多云', windDir: '北风', windLevel: '3级' },
      { date: '周二', week: '周二', dayIcon: '☀️', nightIcon: '🌙', highTemp: 32, lowTemp: 26, desc: '晴', windDir: '南风', windLevel: '2级' },
    ],
    lifeIndex: [
      { name: '穿衣', level: '炎热', desc: '建议穿短衫短裙', icon: '👕', color: '#EF4444' },
      { name: '紫外线', level: '强', desc: '涂SPF20+防晒霜', icon: '☀️', color: '#F59E0B' },
      { name: '运动', level: '适宜', desc: '适合户外运动', icon: '🏃', color: '#10B981' },
      { name: '洗车', level: '不宜', desc: '未来有雨', icon: '🚗', color: '#6B7280' },
    ]
  },
  {
    city: '北京', temp: 32, feelsLike: 35, desc: '晴', icon: '☀️',
    humidity: 45, windDir: '北风', windLevel: '2级', aqi: 85, aqiLevel: '良',
    sunrise: '05:02', sunset: '19:35',
    hourly: [], daily: [], lifeIndex: []
  }
];

@State private cityList: string[] = ['上海', '北京', '广州', '深圳', '杭州', '南京', '成都', '重庆', '武汉', '西安'];

核心功能方法

private getAqiColor(aqi: number): string {
  if (aqi <= 50) return '#10B981';
  if (aqi <= 100) return '#F59E0B';
  if (aqi <= 150) return '#F97316';
  if (aqi <= 200) return '#EF4444';
  return '#7C3AED';
}

private switchCity(index: number): void {
  this.currentCityIndex = index;
  this.showCityList = false;
}

private addCity(): void {
  if (this.searchCity && !this.cities.find(c => c.city === this.searchCity)) {
    const newCity: CityWeather = {
      city: this.searchCity, temp: 26, feelsLike: 28, desc: '晴', icon: '☀️',
      humidity: 60, windDir: '南风', windLevel: '2级', aqi: 45, aqiLevel: '优',
      sunrise: '05:30', sunset: '19:00', hourly: [], daily: [], lifeIndex: []
    };
    this.cities = [...this.cities, newCity];
    this.cityList = [...this.cityList, this.searchCity];
    this.searchCity = '';
  }
}

@Builder 可复用组件

@Builder
HourlyItem(hour: HourlyForecast) {
  Column({ space: 8 }) {
    Text(hour.time)
      .fontSize(12)
      .fontColor('rgba(255,255,255,0.8)')
    Text(hour.icon)
      .fontSize(24)
    Text(`${hour.temp}°`)
      .fontSize(14)
      .fontWeight(FontWeight.Medium)
      .fontColor(Color.White)
  }
  .padding({ left: 16, right: 16, top: 12, bottom: 12 })
}

@Builder
DailyItem(day: DailyForecast) {
  Row({ space: 12 }) {
    Text(day.date)
      .fontSize(14)
      .fontColor(Color.White)
      .width(50)
    Text(day.dayIcon)
      .fontSize(22)
    Text(day.desc)
      .fontSize(13)
      .fontColor('rgba(255,255,255,0.8)')
      .layoutWeight(1)
    Text(`${day.lowTemp}°`)
      .fontSize(14)
      .fontColor('rgba(255,255,255,0.7)')
      .width(30)
      .textAlign(TextAlign.End)
    Text(`${day.highTemp}°`)
      .fontSize(14)
      .fontWeight(FontWeight.Medium)
      .fontColor(Color.White)
      .width(30)
      .textAlign(TextAlign.End)
  }
  .width('100%')
  .padding({ top: 12, bottom: 12 })
}

@Builder
LifeIndexItem(index: LifeIndex) {
  Column({ space: 6 }) {
    Text(index.icon)
      .fontSize(24)
    Text(index.name)
      .fontSize(12)
      .fontColor(Color.White)
    Text(index.level)
      .fontSize(11)
      .fontColor(index.color)
      .fontWeight(FontWeight.Medium)
  }
  .width('100%')
  .padding(12)
  .backgroundColor('rgba(255,255,255,0.15)')
  .borderRadius(12)
}

完整build()页面布局

build() {
  const weather = this.cities[this.currentCityIndex];
  Stack({ alignContent: Alignment.Top }) {
    // 渐变背景
    Column()
      .width('100%')
      .height('100%')
      .linearGradient({
        angle: 180,
        colors: [['#0EA5E9', 0], ['#38BDF8', 0.5], ['#7DD3FC', 1]]
      })
    
    Column() {
      // 顶部城市栏
      Row() {
        Text('📍')
          .fontSize(20)
        Text(weather.city)
          .fontSize(20)
          .fontWeight(FontWeight.Medium)
          .fontColor(Color.White)
        Text('⌄')
          .fontSize(16)
          .fontColor(Color.White)
        Blank()
        Text('🔍')
          .fontSize(20)
          .onClick(() => this.showCityList = true)
      }
      .width('100%')
      .padding(20)
      
      Scroll() {
        Column({ space: 20 }) {
          // 当前温度
          Column({ space: 4 }) {
            Text(weather.icon)
              .fontSize(72)
            Text(`${weather.temp}°`)
              .fontSize(72)
              .fontWeight(FontWeight.Bold)
              .fontColor(Color.White)
            Text(`${weather.desc} 体感${weather.feelsLike}°`)
              .fontSize(16)
              .fontColor('rgba(255,255,255,0.9)')
          }
          .width('100%')
          .alignItems(HorizontalAlign.Center)
          .padding({ top: 20, bottom: 20 })
          
          // 详细信息
          Row({ space: 0 }) {
            Column({ space: 4 }) {
              Text('💧')
                .fontSize(18)
              Text(`${weather.humidity}%`)
                .fontSize(14)
                .fontColor(Color.White)
              Text('湿度', { fontSize: 11, fontColor: 'rgba(255,255,255,0.7)' })
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Center)
            
            Column({ space: 4 }) {
              Text('🌬️')
                .fontSize(18)
              Text(weather.windLevel)
                .fontSize(14)
                .fontColor(Color.White)
              Text(weather.windDir, { fontSize: 11, fontColor: 'rgba(255,255,255,0.7)' })
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Center)
            
            Column({ space: 4 }) {
              Text('🌅')
                .fontSize(18)
              Text(weather.sunrise)
                .fontSize(14)
                .fontColor(Color.White)
              Text('日出', { fontSize: 11, fontColor: 'rgba(255,255,255,0.7)' })
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Center)
            
            Column({ space: 4 }) {
              Text('🌇')
                .fontSize(18)
              Text(weather.sunset)
                .fontSize(14)
                .fontColor(Color.White)
              Text('日落', { fontSize: 11, fontColor: 'rgba(255,255,255,0.7)' })
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Center)
          }
          .width('100%')
          .padding(16)
          .backgroundColor('rgba(255,255,255,0.15)')
          .borderRadius(16)
          .backdropBlur(20)
          
          // 空气质量
          Row({ space: 12 }) {
            Text('🌬️')
              .fontSize(24)
            Column({ space: 4 }) {
              Text(`空气质量 ${weather.aqi} ${weather.aqiLevel}`)
                .fontSize(15)
                .fontWeight(FontWeight.Medium)
                .fontColor(Color.White)
              Text('空气不错,可以正常户外活动')
                .fontSize(12)
                .fontColor('rgba(255,255,255,0.8)')
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)
            Text(weather.aqiLevel)
              .fontSize(14)
              .fontColor(Color.White)
              .padding({ left: 12, right: 12, top: 6, bottom: 6 })
              .backgroundColor(this.getAqiColor(weather.aqi))
              .borderRadius(20)
          }
          .width('100%')
          .padding(16)
          .backgroundColor('rgba(255,255,255,0.15)')
          .borderRadius(16)
          .backdropBlur(20)
          
          // 24小时预报
          Column({ space: 12 }) {
            Text('24小时预报')
              .fontSize(15)
              .fontWeight(FontWeight.Medium)
              .fontColor(Color.White)
              .width('100%')
            Scroll(Axis.Horizontal) {
              Row({ space: 4 }) {
                ForEach(weather.hourly, (h: HourlyForecast) => {
                  this.HourlyItem(h)
                })
              }
            }
            .scrollBar(BarState.Off)
          }
          .width('100%')
          .padding(16)
          .backgroundColor('rgba(255,255,255,0.15)')
          .borderRadius(16)
          .backdropBlur(20)
          
          // 7天预报
          Column({ space: 8 }) {
            Text('7天预报')
              .fontSize(15)
              .fontWeight(FontWeight.Medium)
              .fontColor(Color.White)
              .width('100%')
            ForEach(weather.daily, (d: DailyForecast) => {
              this.DailyItem(d)
              Divider().color('rgba(255,255,255,0.1)')
            })
          }
          .width('100%')
          .padding(16)
          .backgroundColor('rgba(255,255,255,0.15)')
          .borderRadius(16)
          .backdropBlur(20)
          
          // 生活指数
          Column({ space: 12 }) {
            Text('生活指数')
              .fontSize(15)
              .fontWeight(FontWeight.Medium)
              .fontColor(Color.White)
              .width('100%')
            Grid() {
              ForEach(weather.lifeIndex, (i: LifeIndex) => {
                GridItem() { this.LifeIndexItem(i) }
              })
            }
            .columnsTemplate('1fr 1fr 1fr 1fr')
            .columnsGap(8)
            .rowsGap(8)
            .width('100%')
          }
          .width('100%')
          .padding(16)
          .backgroundColor('rgba(255,255,255,0.15)')
          .borderRadius(16)
          .backdropBlur(20)
        }
        .width('100%')
        .padding({ left: 20, right: 20, bottom: 40 })
      }
      .layoutWeight(1)
      .scrollBar(BarState.Off)
    }
    .width('100%')
    .height('100%')
    
    // 城市选择弹窗
    if (this.showCityList) {
      Column() {
        Blank()
        Column({ space: 16 }) {
          Row() {
            Text('选择城市')
              .fontSize(18)
              .fontWeight(FontWeight.Medium)
              .fontColor('#1F2937')
            Blank()
            Text('✕')
              .fontSize(20)
              .onClick(() => this.showCityList = false)
          }
          .width('100%')
          
          Row({ space: 8 }) {
            TextInput({ text: this.searchCity, placeholder: '搜索城市' })
              .layoutWeight(1)
              .height(44)
              .backgroundColor('#F3F4F6')
              .onChange((v: string) => this.searchCity = v)
            Button('添加')
              .height(44)
              .backgroundColor('#0EA5E9')
              .onClick(() => this.addCity())
          }
          .width('100%')
          
          Wrap({ space: 8 }) {
            ForEach(this.cityList, (city: string, index: number) => {
              Text(city)
                .fontSize(14)
                .fontColor(city === weather.city ? Color.White : '#374151')
                .padding({ left: 16, right: 16, top: 8, bottom: 8 })
                .backgroundColor(city === weather.city ? '#0EA5E9' : '#F3F4F6')
                .borderRadius(20)
                .onClick(() => this.switchCity(index))
            })
          }
        }
        .width('100%')
        .padding(20)
        .backgroundColor(Color.White)
        .borderRadius({ topLeft: 24, topRight: 24 })
      }
      .width('100%')
      .height('100%')
      .backgroundColor('rgba(0,0,0,0.5)')
      .onClick(() => this.showCityList = false)
    }
  }
  .width('100%')
  .height('100%')
}

页面设计说明

主题色采用天蓝色渐变#0EA5E9→#7DD3FC,蓝色代表天空、清爽、科技,非常适合天气应用。使用毛玻璃backdropBlur效果,卡片半透明叠加在渐变背景上,视觉效果高级。所有文字白色,对比清晰。横向滚动的小时预报、网格布局的生活指数,交互流畅。

SDK配置

API 24,compatibleSdkVersion: “6.1.1(24)”

运行项目

将代码复制到 entry/src/main/ets/pages/Index.ets 即可运行。

项目总结

实现了天气展示、多城市管理、24小时/7天预报、空气质量、生活指数等功能。掌握了渐变色背景、毛玻璃效果、横向滚动列表、网格布局、底部弹窗等。后续可加入天气动画、天气预警通知、桌面天气小组件、降水提醒、逐分钟预报、台风路径、天气分享、深色模式等功能。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐