一直在追求一针见血的发现问题,解决问题,提出问题 + 了解他崇拜他超过他
🚀 个人主页码到成功✓
🌱 代码仓库wwh
📌 专栏系列

“虫子从来就没有被战胜过”

HarmonyOS应用开发:城市搜索与天气添加功能实现(ArkTS)
在HarmonyOS应用开发中,城市搜索与天气数据关联是天气类App的核心功能模块之一。本文基于ArkTS语言,详细拆解城市搜索、历史记录存储、城市添加去重及UI组件封装的完整实现流程,提供可直接复用的代码方案与关键逻辑解析。

一、功能核心与技术栈

1. 核心功能

  • 城市关键词搜索:调用天气API接口,实时返回匹配城市列表
  • 搜索历史记录:输入框失焦时自动保存非空搜索内容
  • 城市添加与去重:防止重复添加同一城市,并通过Toast提示结果
  • 响应式UI:适配组件状态变化,展示城市名称、经纬度、国家等信息

2. 技术依赖

技术/框架 用途说明
ArkTS (Stage模型) 页面布局与组件逻辑实现
Axios 发起HTTP请求,获取城市搜索API数据
HarmonyOS UI组件 Column/Row/List/TextInput等布局组件
@Builder装饰器 封装可复用的城市信息展示组件

二、数据模型定义

在实现功能前,需先定义核心数据模型,确保数据流转的一致性。以下是关键模型的简化定义(对应代码中model目录下的文件):

1. 城市数据模型(CityModel.ets)

存储已添加城市的核心标识信息

export interface CityModel {
  locationId: number;  // 城市唯一ID(用于去重)
  locationName: string;// 城市名称
}

2. 搜索历史模型(SearchHistory.ets)

记录用户搜索过的城市名称

export interface SearchHistory {
  locationName: string; // 搜索过的城市名称
}

3. 天气UI模型(WeatherUiModel.ets)

存储城市搜索结果与天气展示数据(核心为cityData数组)

// 城市搜索结果单项类型
export interface GeneratedTypeLiteralInterface_3 {
  name: string;    // 城市名
  id: number;      // 城市ID
  lon: string;     // 经度
  lat: string;     // 纬度
  country: string; // 国家
  rank: string;    // 城市评分
}

// 天气UI总模型
export interface WeatherUiModel {
  nowTemp: string;
  nowWeatherText: string;
  // ... 其他天气字段省略
  cityData: GeneratedTypeLiteralInterface_3[]; // 城市搜索结果列表
}

三、核心页面实现(Addto.ets)

页面包含「顶部返回+搜索框」与「城市列表」两大模块,以下分模块解析关键逻辑。

1. 页面状态与生命周期

通过@Link接收父组件传递的状态(如已添加城市列表、侧边栏显示状态),aboutToAppear生命周期初始化搜索框默认值。

import { citySearch } from '../../network/api/WeatherApi';
import { WeatherUiModel, GeneratedTypeLiteralInterface_3 } from '../../model/WeatherUiModel';
import { caty } from '../../model/NowWeatherModel';
import { CityModel } from '../../model/CityModel';
import { promptAction } from '@kit.ArkUI';
import { SearchHistory } from '../../model/SearchHistory';
import { AxiosResponse } from '@ohos/axios';

@Component
export struct Addto {
  // 父组件传递的已添加城市列表(双向绑定)
  @Link cityArr: CityModel[];
  // 搜索框输入内容
  city: string = '';
  // 搜索历史记录(双向绑定)
  @Link searchHistory: SearchHistory[];
  // 父组件传递的初始输入值(如从首页跳转带过来的城市名)
  @Link initialInput: string;
  // 天气UI数据模型(核心存储城市搜索结果)
  @State weatherUiModel: WeatherUiModel = {
    nowTemp: '',
    nowWeatherText: '',
    tempMax: '',
    tempMin: '',
    category: '',
    precip: 0,
    hourlyTemp: [],
    iconDays: [],
    cityData: [], // 城市搜索结果存储数组
    date: '-月-日',
    day: '',
    dayArr: [],
    hoursArr: [],
  };
  // 侧边栏显示状态(双向绑定)
  @Link isShowSideBar1: boolean;

  // 页面即将显示时,初始化搜索框内容
  aboutToAppear(): void {
    this.city = this.initialInput;
  }

  // 后续为build布局与业务方法
}

2. 页面布局(build方法)

采用Column嵌套结构,分为「顶部操作区」和「城市列表区」:

build() {
  Column() {
    // 1. 顶部操作区:返回图标 + 搜索框
    Column() {
      // 返回图标(控制侧边栏显示/隐藏)
      Image($r('app.media.img_3'))
        .width(40)
        .height(40)
        .onClick(() => {
          this.isShowSideBar1 = !this.isShowSideBar1;
        });

      // 搜索框:实时触发搜索 + 失焦保存历史
      TextInput({ 
        placeholder: "请输入要查询的城市", 
        text: $$this.city  // 双向绑定输入内容
      })
        .onChange(() => {
          // 输入变化时,调用接口获取城市列表
          this.initData(this.city);
        })
        .onBlur(() => {
          // 输入框失焦时,保存非空内容到搜索历史
          if (this.city !== '') {
            this.searchHistory.push({ locationName: this.city });
          }
        });
    }
    .alignItems(HorizontalAlign.Start) // 左对齐
    .padding(16); // 内边距

    // 2. 城市列表区:展示搜索结果
    List() {
      ListItem() {
        Column() {
          // 循环渲染城市搜索结果
          ForEach(
            this.weatherUiModel.cityData, 
            (item: GeneratedTypeLiteralInterface_3) => {
              // 调用自定义@Builder组件展示单个城市信息
              wwh(
                item.name, 
                item.id, 
                item.lon, 
                item.lat, 
                item.country, 
                item.rank, 
                this.cityArr
              );
            }
          )
        }
      }
    }
    .padding(16);
  }
  .backgroundColor('#f5f5f5'); // 页面背景色
}

3. 城市搜索接口调用(initData方法)

通过Axios调用天气API的城市搜索接口,解析返回数据并更新到weatherUiModel.cityData

/**
 * 调用城市搜索API,获取匹配城市列表
 * @param name 城市关键词
 */
async initData(name: string) {
  try {
    // 1. 调用API(citySearch为封装的Axios请求方法)
    const response: AxiosResponse = await citySearch(name);
    console.log('城市搜索API返回数据:', JSON.stringify(response));

    // 2. 解析数据(caty为API返回数据的类型定义)
    const resultData = response.data as caty;
    // 清空原有结果,避免数据叠加
    this.weatherUiModel.cityData = [];

    // 3. 遍历数据,转换为UI所需格式并存储
    for (let i = 0; i < resultData.location.length; i++) {
      const location = resultData.location[i];
      this.weatherUiModel.cityData.push({
        name: location.name,
        id: location.id,
        lon: location.lon,
        lat: location.lat,
        country: location.country,
        rank: location.rank,
      });
    }
  } catch (error) {
    console.error('城市搜索API调用失败:', error);
  }
}

四、可复用UI组件封装(@Builder wwh)

通过@Builder装饰器封装单个城市的展示组件,包含城市信息展示与「添加」按钮,实现UI复用与逻辑内聚:

1. 组件实现

/**
 * 城市信息展示组件
 * @param name 城市名
 * @param id 城市ID(用于去重)
 * @param lon 经度
 * @param lat 纬度
 * @param country 国家
 * @param rank 城市评分
 * @param cityArr 已添加城市列表(用于判断是否重复)
 */
@Builder
function wwh(
  name: string, 
  id: number, 
  lon: string, 
  lat: string, 
  country: string, 
  rank: string, 
  cityArr: CityModel[]
) {
  Column({ space: 10 }) {
    Row() {
      // 左侧:城市信息展示区
      Column() {
        // 1. 城市名称 + 地标图标
        Row() {
          Image($r('app.media.img_8'))
            .width(28)
            .height(28)
            .margin({ right: 8 });
          Text(name)
            .fontSize(20)
            .fontWeight(FontWeight.Bold);
        }
        .margin({ bottom: 12 });

        // 2. 国家信息 + 国旗图标
        Row() {
          Image($r('app.media.img_4'))
            .width(20)
            .height(20)
            .borderRadius(4); // 圆角优化
          Text(country)
            .fontSize(14)
            .margin({ left: 8 });
        }
        .padding(6)
        .backgroundColor('#f5f5f5')
        .borderRadius(8)
        .margin({ bottom: 12 });

        // 3. 城市评分 + 星标图标
        Row() {
          Image($r('app.media.img_6'))
            .width(16)
            .height(16)
            .margin({ right: 4 });
          Text("城市评分")
            .fontSize(12);
          Text(rank)
            .fontColor('#ffd700') // 金色评分
            .margin({ left: 4 });
        }
        .margin({ top: 12 });

        // 4. 经纬度 + 定位图标
        Row() {
          Image($r('app.media.img_5'))
            .width(16)
            .height(16)
            .margin({ right: 8 });
          Text(`经度 ${lon}`)
            .fontSize(12);
          Text("•")
            .margin({ left: 8, right: 8 });
          Text(`纬度 ${lat}`)
            .fontSize(12);
        }
        .padding(8)
        .backgroundColor('#f8f9fa')
        .borderRadius(6);
      }
      .layoutWeight(1); // 占满剩余宽度

      // 右侧:添加按钮
      Image($r('app.media.img_7'))
        .onClick(() => {
          // 调用judge方法判断是否已添加
          if (judge(cityArr, id)) {
            // 未添加则push到已添加列表
            cityArr.push({
              locationId: id,
              locationName: name
            });
          } else {
            // 已添加则提示
            promptAction.showToast({
              message: "已经添加过了",
              duration: 2000 // 提示时长
            });
          }
        })
        .width(40)
        .height(40)
        .padding(8)
        .backgroundColor('#e8f4ff')
        .borderRadius(20); // 圆形按钮
    }
    .padding(16);
  }
  .backgroundColor('#ffffff')
  .borderRadius(24)
  .shadow(12) // 阴影效果,提升层次感
  .margin({ bottom: 12 }); // 组件间距
}

2. 城市去重工具函数(judge)

判断当前城市是否已添加,避免重复存储:

/**
 * 城市去重判断
 * @param cityArr 已添加城市列表
 * @param id 当前城市ID
 * @returns true=未添加,false=已添加
 */
function judge(cityArr: CityModel[], id: number): boolean {
  for (let i = 0; i < cityArr.length; i++) {
    // 通过城市ID判断(ID唯一,比名称判断更准确)
    if (cityArr[i].locationId === id) {
      return false;
    }
  }
  return true;
}

五、关键问题与优化建议

1. 常见问题解决

  • 搜索接口频繁调用:当前onChange实时触发接口,可添加「防抖」(如延迟300ms),减少无效请求。
  • 搜索历史重复:当前onBlur直接push,可在push前判断历史记录中是否已有该城市,避免重复。
  • 列表性能优化:若搜索结果较多,可给List添加itemSpace并使用「懒加载」,提升滚动流畅度。

2. 功能扩展建议

  • 搜索历史管理:添加「清空历史」按钮,支持删除单条历史记录。
  • 城市排序:已添加城市可按「添加时间」或「字母顺序」排序。
  • 错误处理优化:API调用失败时,在页面显示「搜索失败,请重试」的友好提示,而非仅打印日志。

六、总结

本文基于ArkTS实现了HarmonyOS天气App的城市搜索与添加模块,核心亮点在于:

  1. 采用「状态驱动UI」思想,通过@State/@Link实现组件间状态同步;
  2. 封装@Builder组件与工具函数,提升代码复用性与可维护性;
  3. 完善的边界处理(如去重、错误捕获、用户提示),提升用户体验。
Logo

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

更多推荐