前言
在购物类App中,当用户购买商品下单的时候,需要提供收货地址,地址管理对于用户体验和商城的运营至关重要。
省市区联动组件为用户提供了一种直观、高效的方式来选择地理位置信息。通过联动的方式,用户只需点击或滑动即可浏览和选择相关地区,避免了手动输入的繁琐和错误。
通过本文,读者将了解到实现省市区联动组件的关键技术和注意事项,并能够根据自身需求进行开发和定制。
案例效果

需求分析
在本示例中,用户可以选择省份,然后根据所选省份展示相应的城市列表。当用户选择城市后,应显示该城市对应的区域列表。 业务逻辑应确保省、市、区之间的关联正确,即用户只能选择对应省份和城市的区域。
数据来源和数据结构
数据来源
在初始化地址选择组件时,需要传入省、市、区数据。数据来源可以是多种多样的,例如内部数据库、外部 API 或静态数据文件。 我们可以结合项目的实际情况选择合适的数据来源。
示例中,我们通过外部 API的方式获取省、市、区的数据。如下图所示。 第一个接口,是获取一级数据。
第二个接口,是获取二级数据。其接收一个一级数据的code。例如,使用山东省的code值370000作为参数,获取二级数据,获取山东省下的所有市区结构。
第三个接口,是获取三级数据。其接收一个二级数据的code。例如,使用济南市的code值370100作为参数,获取三级数据。 根据以上3个接口,通过code参数的筛选得到结果。将获取的结果赋值给currentArea变量,代码如下所示。
typescript
//当前选择器区域显示的数据
@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是选择器所需的数据,属性如下:
typescript
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是当前选择器区域选择保存的数据,属性如下:
typescript
export class SelAreaModel {
provinceName: string
provinceCode: number
cityName: string
cityCode: number
districtName: string
}
界面设计
在本示例中,地址选择组件使用Panel 组件,其为可滑动面板,提供一种轻量的内容展示窗口,方便在不同尺寸中切换。 地址内容使用Grid组件,其为网格容器,由"行"和"列"分割的单元格所组成,通过指定"项目"所在的单元格做出各种各样的布局。代码如下所示。
scss
@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'))
}
scss
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上设置监听器,当用户选择一个省份时,根据选中的省份在数据源中获取对应的城市列表。然后,将城市列表绑定到城市选择控件的数据源。类似地,在城市选择控件上设置监听器,当用户选择一个城市时,根据选中的城市在数据源中获取对应的区域列表。最后,将区域列表绑定到区域选择控件的数据源。代码如下所示。
less
//是否显示
@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') })
kotlin
.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 时存在一些不便之处。通过本文,您了解了实现省市区联动组件的关键技术和注意事项,您可以根据自身需求进行开发和定制。