【鸿蒙应用开发系列】实现省市地区三级联动的选择器组件

2 阅读4分钟

前言

在购物类App中,当用户购买商品下单的时候,需要提供收货地址,地址管理对于用户体验和商城的运营至关重要。

省市区联动组件为用户提供了一种直观、高效的方式来选择地理位置信息。通过联动的方式,用户只需点击或滑动即可浏览和选择相关地区,避免了手动输入的繁琐和错误。

通过本文,读者将了解到实现省市区联动组件的关键技术和注意事项,并能够根据自身需求进行开发和定制。

案例效果

34.gif

需求分析

在本示例中,用户可以选择省份,然后根据所选省份展示相应的城市列表。当用户选择城市后,应显示该城市对应的区域列表。 业务逻辑应确保省、市、区之间的关联正确,即用户只能选择对应省份和城市的区域。

数据来源和数据结构

数据来源

在初始化地址选择组件时,需要传入省、市、区数据。数据来源可以是多种多样的,例如内部数据库、外部 API 或静态数据文件。 我们可以结合项目的实际情况选择合适的数据来源。

示例中,我们通过外部 API的方式获取省、市、区的数据。如下图所示。 image.png 第一个接口,是获取一级数据。

第二个接口,是获取二级数据。其接收一个一级数据的code。例如,使用山东省的code值370000作为参数,获取二级数据,获取山东省下的所有市区结构。

第三个接口,是获取三级数据。其接收一个二级数据的code。例如,使用济南市的code值370100作为参数,获取三级数据。 根据以上3个接口,通过code参数的筛选得到结果。将获取的结果赋值给currentArea变量,代码如下所示。

//当前选择器区域显示的数据
@State currentArea: AreaModel[] = []
//点击选择的数据
@State selectData: SelAreaModel = new SelAreaModel();

//获取省数据
getProvince() {
   AreaPickerViewModel.getProvince().then((data: AreaModel[]) => {
     this.currentArea = data
   }).catch((error) => {
     promptAction.showToast({ message: error })
   })
}

//获取市数据
getCity(code: number) {
   AreaPickerViewModel.getCity(code).then((data: AreaModel[]) => {
     this.currentArea = data
   }).catch((error) => {
     promptAction.showToast({ message: error })
   })
}

//获取区数据
getDistrict(code: number) {
   AreaPickerViewModel.getDistrict(code).then((data: AreaModel[]) => {
     this.currentArea = data
   }).catch((error) => {
     promptAction.showToast({ message: error })
   })
}

数据结构

其中,AreaModel是选择器所需的数据,属性如下:

export class AreaModel {
  id: number  //主键id
  name: string //省市区的名称
  code: number //省市区的地址编码

  constructor(id: number, name: string, code: number) {
    this.id = id
    this.name = name
    this.code = code
  }
}

SelAreaModel是当前选择器区域选择保存的数据,属性如下:

export class SelAreaModel {
  provinceName: string
  provinceCode: number
  cityName: string
  cityCode: number
  districtName: string
}

界面设计

在本示例中,地址选择组件使用Panel组件,其为可滑动面板,提供一种轻量的内容展示窗口,方便在不同尺寸中切换。 地址内容使用Grid组件,其为网格容器,由“行”和“列”分割的单元格所组成,通过指定“项目”所在的单元格做出各种各样的布局。代码如下所示。

@Builder GridLayout() {
    Grid() {
        ForEach(this.currentArea, (item: AreaModel) => {
        GridItem() 
        })
    }
    .columnsTemplate('1fr 1fr 1fr')
    .rowsGap($r('app.float.float10'))
    .width('100%')
    .padding($r('app.float.float10'))
}
Panel(this.show) {
    Column({ space: CommonConstants.FLOAT_NUMBER_10 }) {
    Row() {
    CommonText({ text: '选择区域' })
    Blank()
    Image($r('app.media.icon_close')).width($r('app.float.float20')).onClick(() => {
        this.closePanel()
    })
    }.width('100%').padding({ left: $r('app.float.float20'), right: $r('app.float.float20') })

    Divider().strokeWidth(1).color($r('app.color.top_bg'))

    if (this.currentArea.length > 0) {
        this.GridLayout()
    }

    }.width('100%').padding({ top: $r('app.float.float10'), bottom: $r('app.float.float10') })
}
.type(PanelType.Foldable)
.mode(PanelMode.Full)
.dragBar(false)
.backgroundColor($r('app.color.white'))

数据绑定与联动逻辑

通过@State装饰器和@Builder装饰器,实现数据绑定,通过@Link装饰器,进行父子组件的通信

在省份选择控件GridItem上设置监听器,当用户选择一个省份时,根据选中的省份在数据源中获取对应的城市列表。然后,将城市列表绑定到城市选择控件的数据源。类似地,在城市选择控件上设置监听器,当用户选择一个城市时,根据选中的城市在数据源中获取对应的区域列表。最后,将区域列表绑定到区域选择控件的数据源。代码如下所示。

//是否显示
@Link show: boolean
//获取选择的省市区
@Link areaName: string
//省
@Link province: string
//市
@Link city: string
//区
@Link county: string

//当前省市区选择的下标
@State selectIndex: number = 0;
//点击选择的数据
@State selectData: SelAreaModel = new SelAreaModel();
@State currentArea: AreaModel[] = []
//显示市
@State showCity: boolean = false
//显示区
@State showDistrict: boolean = false

Row({ space: CommonConstants.FLOAT_NUMBER_12 }) {
    //显示省
    if (this.selectIndex == 0 && !this.selectData.provinceName) {
        Column({ space: CommonConstants.FLOAT_NUMBER_2 }) {
        Text('请选择')
                .fontColor(this.selectIndex == 0 ? $r('app.color.logo_color') : $r('app.color.text_color'))
                .fontSize($r('app.float.font14'))
        Rect({ width: CommonConstants.FLOAT_NUMBER_18, height: CommonConstants.FLOAT_NUMBER_2 })
                .fill(this.selectIndex == 0 ? $r('app.color.logo_color') : $r('app.color.white'))
    }
    } else {
        Column({ space: CommonConstants.FLOAT_NUMBER_2 }) {
        Text(this.selectData.provinceName)
                .fontColor(this.selectIndex == 0 ? $r('app.color.logo_color') : $r('app.color.text_color'))
        .fontSize($r('app.float.font14'))
        .onClick(() => {
            this.selectIndex = 0
            this.showCity = false
            this.selectData.cityName = ''
            this.selectData.cityCode = 0
            this.currentArea = []
            this.getProvince()
        })
        Rect({ width: CommonConstants.FLOAT_NUMBER_18, height: CommonConstants.FLOAT_NUMBER_2 })
                .fill(this.selectIndex == 0 ? $r('app.color.logo_color') : $r('app.color.white'))
}
}
    //显示市
    if (this.showCity) {
        if (this.selectIndex == 1 && !this.selectData.cityName) {
            Column({ space: CommonConstants.FLOAT_NUMBER_2 }) {
            Text('请选择')
                  .fontColor(this.selectIndex == 1 ? $r('app.color.logo_color') : $r('app.color.text_color'))
                  .fontSize($r('app.float.font14'))
            Rect({ width: CommonConstants.FLOAT_NUMBER_18, height: CommonConstants.FLOAT_NUMBER_2 })
                  .fill(this.selectIndex == 1 ? $r('app.color.logo_color') : $r('app.color.white'))
        }
        } else {
              Column({ space: CommonConstants.FLOAT_NUMBER_2 }) {
                Text(this.selectData.cityName)
                  .fontColor(this.selectIndex == 1 ? $r('app.color.logo_color') : $r('app.color.text_color'))
                  .fontSize($r('app.float.font14'))
                  .onClick(() => {
                    this.selectIndex = 1
                    this.currentArea = []
                    this.showDistrict = false
                    this.selectData.districtName = ''
                    this.getCity(this.selectData.provinceCode)
                  })
                Rect({ width: CommonConstants.FLOAT_NUMBER_18, height: CommonConstants.FLOAT_NUMBER_2 })
                  .fill(this.selectIndex == 1 ? $r('app.color.logo_color') : $r('app.color.white'))
}
}
}

        //显示区
        if (this.showDistrict) {
        if (this.selectIndex == 2 && !this.selectData.districtName) {
            Column({ space: CommonConstants.FLOAT_NUMBER_2 }) {
            Text('请选择')
                  .fontColor(this.selectIndex == 2 ? $r('app.color.logo_color') : $r('app.color.text_color'))
                  .fontSize($r('app.float.font14'))
        Rect({ width: CommonConstants.FLOAT_NUMBER_18, height: CommonConstants.FLOAT_NUMBER_2 })
                  .fill(this.selectIndex == 2 ? $r('app.color.logo_color') : $r('app.color.white'))
        }
        } else {
            Column({ space: CommonConstants.FLOAT_NUMBER_2 }) {
            Text(this.selectData.districtName)
                  .fontColor(this.selectIndex == 2 ? $r('app.color.logo_color') : $r('app.color.text_color'))
            .fontSize($r('app.float.font14'))
            .onClick(() => {
                this.selectIndex = 2
                this.currentArea = []
                this.getDistrict(this.selectData.cityCode)
            })
            Rect({ width: CommonConstants.FLOAT_NUMBER_18, height: CommonConstants.FLOAT_NUMBER_2 })
                  .fill(this.selectIndex == 2 ? $r('app.color.logo_color') : $r('app.color.white'))
}
}
}

}.width('100%').padding({ left: $r('app.float.float10'), right: $r('app.float.float10') })
.onClick(() => {
if (this.selectIndex === 0) {
this.selectData.provinceName = item.name;
this.selectData.provinceCode = item.code;
this.selectData.cityName = "";
this.selectData.districtName = "";
this.currentArea = []
this.showCity = true;
setTimeout(() => {
this.selectIndex = 1;
this.getCity(item.code)
}, 200)
} else if (this.selectIndex === 1) {
this.selectData.cityName = item.name;
this.selectData.cityCode = item.code;
this.selectData.districtName = "";
this.currentArea = []
this.showDistrict = true;
setTimeout(() => {
this.selectIndex = 2;
this.getDistrict(item.code)
}, 200)

} else if (this.selectIndex === 2) {
this.selectData.districtName = item.name;
this.province = this.selectData.provinceName
this.city = this.selectData.cityName
this.county = this.selectData.districtName
this.areaName = this.selectData.provinceName + "/" + this.selectData.cityName + "/" +   this.selectData.districtName;
this.reset()
}
})

总结

目前,鸿蒙 4.0 及其对应 SDK API 9.0 版本尚处于磨合阶段,可能会出现某些瑕疵,或者在使用官方 API 时存在一些不便之处。通过本文,您了解了实现省市区联动组件的关键技术和注意事项,您可以根据自身需求进行开发和定制。