从零开始纯血鸿蒙天气预报-城市选择页面

201 阅读2分钟

易得天气

创建数据实体类

export interface SelectCityData {
  hot_international?: Array<CityData>;
  hot_national?: Array<CityData>;
}
export interface LocationData {
  address?: string
  address_component?: AddressComponentData
}

export interface AddressComponentData {
  nation?: string
  province?: string
  city?: string
  district?: string
  street?: string
  street_number?: string
}

创建Model

export default class SelectCityModel {
  obtainHotCityData(): Promise<ResultData<SelectCityData>> {
    return netUtils.netGet<SelectCityData>(Api.SELECT_CITY_API)
  }

  searchCity(searchKey: string): Promise<ResultData<Array<CityData>>> {
    return netUtils.netGet<Array<CityData>>(Api.SEARCH_CITY_API, new Map([
      ['keyword', searchKey],
    ]))
  }

  obtainLocationDataByLocation(location: geoLocationManager.Location): Promise<ResultData<LocationData>> {
    return netUtils.netGet<LocationData>(Api.LOCATION_API, new Map([
      ['location', location.latitude + ',' + location.longitude],
    ]))
  }

  obtainAddedCityData(): Promise<Array<CityData>> {
    return appDatabase.cityDataDao.query()
  }
}

创建ViewModel

@ObservedV2
export default class SelectCityViewModel extends BaseViewModel {
  @Trace selectCityData?: SelectCityData
  @Trace searchResult?: Array<CityData>
  @Trace addedCityData?: Array<CityData>
  @Trace searchResultContentOpacity: number = 0
  @Trace locationData?: LocationData
  // 0-定位中
  // 1-定位结束
  @Trace locationState: number = 0
  private model = new SelectCityModel()

  obtainHotCityData() {
    this.setViewState(ViewState.VIEW_STATE_LOADING)
    this.model.obtainHotCityData()
      .then((response: ResultData<SelectCityData>) => {
        this.selectCityData = response.data
        this.setViewState(ViewState.VIEW_STATE_SUCCESS)
        this.obtainAddedCityData()
        this.obtainLocationPermission()
      })
      .catch((e: Error) => {
        this.setErrorMessage(e.message)
        this.setViewState(ViewState.VIEW_STATE_ERROR)
      })
  }

  searchCity(searchKey: string) {
    this.model.searchCity(searchKey)
      .then((response: ResultData<Array<CityData>>) => {
        let searchResult = response.data
        if (ArrayUtil.isEmpty(searchResult)) {
          ToastUtil.showToast('无匹配城市')
        }
        this.searchResult = searchResult
      })
      .catch(() => {
        this.searchResult = undefined
      })
  }

  obtainAddedCityData() {
    this.model.obtainAddedCityData()
      .then(addedCityData => {
        Logger.e('addedCityData = ' + JSON.stringify(addedCityData))
        this.addedCityData = addedCityData
      })
  }

  obtainLocationPermission() {
    this.locationData = undefined
    this.locationState = 0
    const ps: Permissions[] = ['ohos.permission.APPROXIMATELY_LOCATION', 'ohos.permission.LOCATION']
    TaoYao.with(AppUtil.getContext())
      .runtime()
      .permission(ps)
      .onGranted(() => {
        this.onPermissionGranted()
      })
      .onDenied(() => {
        Logger.e('权限申请失败')
        this.locationData = undefined
        this.locationState = 1
        DialogHelper.showAlertDialog({
          title: '权限设置',
          content: $r('app.string.reason_location_permission'),
          primaryButton: '取消',
          secondaryButton: '去设置',
          onAction: (action) => {
            if (action == DialogAction.SURE) {
              PermissionUtil.requestPermissionsEasy(ps)
                .then(success => {
                  if (success) {
                    this.onPermissionGranted()
                  } else {
                    TaoYao.goToSettingPage(AppUtil.getContext())
                  }
                })
            }
          }
        })
      })
      .request()
  }

  private onPermissionGranted() {
    Logger.e('权限申请成功')
    this.startLocation()
      .then(location => {
        if (location) {
          Logger.e('longitude = ' + location.longitude + ' latitude = ' + location.latitude)
          this.model.obtainLocationDataByLocation(location)
            .then(response => {
              let locationData = response.result
              this.locationData = locationData
              this.locationState = 1
            })
            .catch(() => {
              this.locationData = undefined
              this.locationState = 1
            })
        } else {
          this.locationData = undefined
          this.locationState = 1
        }
      })
  }

  async startLocation(): Promise<geoLocationManager.Location | undefined> {
    const isLocationEnabled = LocationUtil.isLocationEnabled()
    Logger.e('isLocationEnabled = ' + isLocationEnabled)
    if (!isLocationEnabled) {
      return undefined
    }
    return LocationUtil.getCurrentLocationEasy()
  }

  @Monitor("searchResult")
  onSearchResultChange(monitor: IMonitor) {
    Logger.e('onSearchResultChange')
    let now = monitor.value<Array<CityData>>()?.now
    setTimeout(() => {
      this.searchResultContentOpacity = ArrayUtil.isEmpty(now) ? 0 : 1
    }, 20)
  }

  clearSearchResult() {
    this.searchResult = undefined
  }
}

封装多状态组件

@ComponentV2
export struct MultipleStatusLayout {
  @Param message: string = '暂无数据'
  @Param viewState: string = ViewState.VIEW_STATE_LOADING
  @BuilderParam contentView: () => void

  build() {
    Stack() {
      if (this.viewState == ViewState.VIEW_STATE_LOADING) {
        LoadingProgress()
          .color($r('app.color.color_999999'))
          .width(48)
          .height(48)
      } else if (this.viewState == ViewState.VIEW_STATE_ERROR) {
        Text(this.message)
          .fontColor($r('app.color.color_999999'))
          .fontSize(14)
      } else {
        this.contentView()
      }
    }
    .width('100%')
    .height('100%')
  }
}

MultipleStatusLayout使用示例

MultipleStatusLayout({
  message: this.selectCityVm.errorMessage,
  viewState: this.selectCityVm.viewState,
  contentView: () => {
    this.contentView()
  }
})

选择城市用到的组件主要是List、Grid

@Builder
contentView() {
  List() {
    ListItem() {
      this.locationButton()
    }

    ListItem() {
      this.header('国内热门城市')
    }

    ListItem() {
      this.content(this.selectCityVm.selectCityData?.hot_national)
    }

    ListItem() {
      this.header('国际热门城市')
    }

    ListItem() {
      this.content(this.selectCityVm.selectCityData?.hot_international)
    }
  }
  .width('100%')
  .height('100%')
  .edgeEffect(EdgeEffect.Spring, { alwaysEnabled: true })
}
@Builder
locationButton() {
  Row({ space: 4 }) {
    Image($r('app.media.writing_icon_location1'))
      .width(18)
      .height(18)
    Text(this.selectCityVm.locationState == 0 ? '定位中...' :
      (this.selectCityVm.locationData?.address_component?.district ?? '定位失败'))
      .fontSize(13)
      .fontColor($r('app.color.text_color_01'))
  }
  .clickEffect({ level: ClickEffectLevel.HEAVY, scale: 0.8 })
  .padding({
    left: 12,
    top: 12,
    right: 12,
    bottom: 12
  })
  .backgroundColor($r('app.color.card_color_06'))
  .borderRadius(100)
  .margin({ left: 16, top: 12, bottom: 12 })
  .onClick(() => {
    if (!this.selectCityVm.locationData && this.selectCityVm.locationState == 1) {
      this.selectCityVm.obtainLocationPermission()
    }
  })
}
@Builder
header(header: string) {
  Text(header)
    .fontColor($r('app.color.text_color_01'))
    .fontSize(20)
    .fontWeight(FontWeight.Bold)
    .padding({ left: 16 })
    .margin({ top: 8 })
}
@Builder
content(cityList?: Array<CityData>) {
  Grid() {
    ForEach(cityList, (cityData: CityData) => {
      GridItem() {
        Text(cityData.name)
          .textAlign(TextAlign.Center)
          .clickEffect({ level: ClickEffectLevel.HEAVY, scale: 0.8 })
          .fontColor(this.hasAdded(cityData) ? $r('app.color.app_main') : $r('app.color.text_color_01'))
          .fontSize(13)
          .padding({ top: 12, bottom: 12 })
          .backgroundColor($r('app.color.card_color_06'))
          .borderRadius(100)
          .width('100%')
          .onClick(() => {
            this.gotoWeatherPreviewPage(cityData)
          })
      }
    }, (cityData: CityData): string => {
      return cityData.cityid ?? ''
    })
  }
  .columnsTemplate('1fr 1fr 1fr 1fr')
  .columnsGap(16)
  .rowsGap(16)
  .maxCount(4)
  .layoutDirection(GridDirection.Row)
  .padding({
    left: 16,
    top: 12,
    right: 16,
    bottom: 16
  })
}
@Builder
searchResultContent() {
  if (ArrayUtil.isNotEmpty(this.selectCityVm.searchResult)) {
    List() {
      ForEach(this.selectCityVm.searchResult, (cityData: CityData) => {
        ListItem() {
          Text(StrUtil.isEmpty(cityData.prov) ? cityData.name + ' - ' + cityData.country :
            cityData.name + ' - ' + cityData.prov + ' - ' + cityData.country)
            .clickEffect({ level: ClickEffectLevel.HEAVY, scale: 0.8 })
            .fontColor(this.hasAdded(cityData) ? $r('app.color.app_main') : $r('app.color.text_color_01'))
            .fontSize(15)
            .padding({
              left: 16,
              top: 12,
              right: 16,
              bottom: 12
            })
            .fontWeight(FontWeight.Bold)
            .width('100%')
            .onClick(() => {
              this.gotoWeatherPreviewPage(cityData)
            })
        }
      }, (cityData: CityData): string => {
        return cityData.cityid ?? ''
      })
      ListItem() {
        Divider()
          .strokeWidth(px2vp(AppUtil.getNavigationIndicatorHeight()))
          .color($r('app.color.transparent'))
      }
    }
    .width('100%')
    .height('100%')
    .edgeEffect(EdgeEffect.Spring, { alwaysEnabled: true })
    .animatableOpacity(this.selectCityVm.searchResultContentOpacity)
    .animation({ duration: 222, curve: Curve.Linear })
    .backgroundColor($r('app.color.bg_color'))
  }
}

hasAdded(cityData: CityData) {
  const find = this.selectCityVm.addedCityData?.find(it => it.cityid == cityData.cityid)
  return find != undefined
}

gotoWeatherPreviewPage(cityData: CityData) {
  ZRouter.getInstance()
    .withParam(Constants.CITY_DATA, cityData)
    .setPopListener(() => {
      setTimeout(() => {
        AppRuntimeData.getInstance().addCity(cityData)
      }, 500)
    })
    .push(RouterConstants.WEATHER_PREVIEW_PAGE)
}
@AnimatableExtend(List)
function animatableOpacity(opacity: number) {
  .opacity(opacity)
}

添加城市天气预览页面(未完成)

@Route({ name: RouterConstants.WEATHER_PREVIEW_PAGE })
@ComponentV2
export struct WeatherPreviewPage {
  @Computed
  get previewCityData() {
    return ZRouter.getInstance().getParamByKey<CityData>(Constants.CITY_DATA)
  }

  @Builder
  functionButton(action: string, block: () => void) {
    Text(action)
      .textAlign(TextAlign.Center)
      .clickEffect({ level: ClickEffectLevel.HEAVY, scale: 0.8 })
      .fontColor($r('app.color.special_white'))
      .fontSize(14)
      .padding({ left: 16, right: 16 })
      .backgroundColor(ColorUtils.alpha($r('app.color.special_black'), 0.5))
      .borderRadius(100)
      .height(32)
      .onClick(block)
  }

  build() {
    NavDestination() {
      Stack() {
        Text(this.previewCityData.name)
          .fontSize(22)
          .height('100%')
        Row() {
          this.functionButton('取消', () => {
            ZRouter.getInstance().pop()
          })
          this.functionButton('添加', () => {
            ZRouter.getInstance().popWithResult(this.previewCityData)
          })
        }
        .width('100%')
        .padding({ left: 12, top: px2vp(AppUtil.getStatusBarHeight()) + 12, right: 12 })
        .justifyContent(FlexAlign.SpaceBetween)
      }
      .alignContent(Alignment.Top)
    }
    .hideTitleBar(true)
    .height('100%')
    .width('100%')
    .onReady((context) => {
      ZRouter.animateMgr()
        .registerAnimParam(this, context)
        .setEnterAnimate({ duration: 500, curve: Curve.LinearOutSlowIn })
        .setExitAnimate({ duration: 500, curve: Curve.LinearOutSlowIn })
        .addAnimateOptions(new TranslateAnimationOptions({ y: '100%' }))
    })
    .onDisAppear(() => {
      ZRouter.animateMgr().unregisterAnim(this)
    })
    .attributeModifier(ZRouter.animateMgr().modifier(this))
    .backgroundColor(Color.Black)
    .backgroundColor($r('app.color.bg_color'))
  }
}

效果图

2025-02-14 16.17.12.gif

申请位置权限效果图:

2025-02-19 17.11.00.gif

添加城市效果图(添加城市的预览由于天气主页面还未完成,只展示城市名称):

2025-02-19 17.21.47.gif