在做 React 移动端开发用到 antd-mobile
时,需要一个 选择省市区 的组件,但官方没有,就自己封装一个
🔥 文末有完整的组件代码 AreaPicker.tsx
大概的开发过程和原理
一开始我使用 Picker
实现,但发现有问题,原本点确定选项后 picker 再次展现时 会始终保持当前的选中状态,而在 onSelect
动态设置多列数据 后就不能保持选中状态。原因大概是 Picker
需要先渲染列数据 再设置选中项到中间,Picker
会自定设置选中状态,当 columns
数据更新滞后就无法设置之前对应项的选中状态
既然无法直接使用 Picker
,我就选择用 Popup
(弹层)和 PickerView
进行封装
首先需要 地区数据,看到在 Vant 组件中有这么一个包 @vant/area-data
可以直接拿来用,它是地区码和地区名称构成的键值对格式 大概是这样:
{
province_list: { '110000': '北京市', '120000': '天津市', '130000': '河北省', ... },
city_list: { '110100': '北京市', '120100': '天津市', '130100': '石家庄市', ... },
county_list: { 110101: '东城区', 110102: '西城区', '110105': '朝阳区', ... }
}
安装下,进行引用
npm i @vant/area-data -S
import { areaList } from '@vant/area-data'
const { province_list, city_list, county_list } = areaList
关键点在于 Picker
滑动项时 要动态设置 columns
的值,比如选项不同省 就出现对应市的列表 选择市出现区县列表。地区码是6位 其中前两位代表省 中间两位代表市 最后两位代表区县(例如 320000
中的 32
就代表江苏省)
在 PickerView
的 onChange
中拿到当前 value 利用动态的正则表达式将对应的列表筛选出来,再处理成 columns
需要的数据格式 { label: string, value: string | null }[]
,这里示例下滑动省设置相应的市:
import { PickerColumnItem, PickerValue } from 'antd-mobile/es/components/picker-view'
interface LooseObjType<T = any> {
[key: string]: T
}
// 处理成PickerView的columns需要的格式
const processAreaData = (source: LooseObjType): PickerColumnItem[] => {
return [
{ label: '请选择', value: '' },
...Object.keys(source).map((key) => ({
label: source[key],
value: key,
}))
]
}
const handleChange = (currentVal: PickerValue[]) => {
const [ provinceVal, cityVal ] = currentVal
const cityList = JSON.parse(JSON.stringify(city_list)) // 浅拷贝
if (provinceVal) {
const req = new RegExp(`^${provinceVal.slice(0, 2)}`)
for (const k in cityList) {
if (!req.test(k)) {
delete cityList[k] // 将不需要的删掉,过滤出对应的
}
}
setCityList(processAreaData(cityList))
} else {
setCityList([])
}
}
有两个 value 状态,一个是随着 onChange
变化的,一个点击 确定按钮 时记录选中的值,方便组件再次展现时保持当前选定的状态
// 变化的值(onChange)
const [ value, setValue ] = useState<PickerValue[]>([ '', '', '' ])
// 确定的值(只在点击确定按钮时记录)
const determinantVal = useRef<PickerValue[]>([ '', '', '' ])
完整代码
AreaPicker.tsx
import React, { useState, useRef, useEffect } from 'react'
import { Popup, PickerView, Button } from 'antd-mobile'
import { PickerColumnItem, PickerValue } from 'antd-mobile/es/components/picker-view'
import classNames from 'classnames'
import { areaList } from '@vant/area-data'
import { LooseObjType } from '@/types/common'
import './style.scss'
interface AreaPickerProps {
visible: boolean // picker是否显示
className?: string
onClose: () => void // picker内部点击关闭或遮罩时
onConfirm?: (val: PickerValue[]) => void
}
const AreaPicker: React.FC<AreaPickerProps> = (props) => {
const { visible, className, onClose, onConfirm } = props
const { province_list, city_list, county_list } = areaList // 省市区
// 变化的值(change)
const [ value, setValue ] = useState<PickerValue[]>([ '', '', '' ])
// 确定的值(confirm 点击确定按钮后记录)
const determinantVal = useRef<PickerValue[]>([ '', '', '' ])
const [ provinceList, setProvinceList ] = useState<PickerColumnItem[]>([])
const [ cityList, setCityList ] = useState<PickerColumnItem[]>([])
const [ countyList, setCountyList ] = useState<PickerColumnItem[]>([])
const classes = classNames('area-picker-popup', className)
// 处理地区数据
const processAreaData = (source: LooseObjType): PickerColumnItem[] => {
return [
{ label: '请选择', value: '' },
...Object.keys(source).map((key) => ({
label: source[key],
value: key,
}))
]
}
// 根据传入的值设置城市和区县列表
const updateCityAndCountyList = (currentVal: PickerValue[]) => {
const [ provinceVal, cityVal ] = currentVal
const cityList = JSON.parse(JSON.stringify(city_list)) // 浅拷贝
const countyList = JSON.parse(JSON.stringify(county_list))
if (provinceVal) {
const _pattern = new RegExp(`^${provinceVal.slice(0, 2)}`)
for (const k in cityList) {
if (!_pattern.test(k)) {
delete cityList[k]
}
}
setCityList(processAreaData(cityList))
} else {
setCityList([])
}
if (cityVal) {
const _pattern = new RegExp(`^${cityVal.slice(0, 4)}`)
for (const k in countyList) {
if (!_pattern.test(k)) {
delete countyList[k]
}
}
setCountyList(processAreaData(countyList))
} else {
setCountyList([])
}
}
// 点击确定按钮
const handleConfirm = () => {
determinantVal.current = [...value]
onClose()
onConfirm && onConfirm([...value])
}
// 滑动选择时,根据省筛选市 根据市筛选区县
const handleChange = (val: PickerValue[]) => {
updateCityAndCountyList(val)
setValue(val)
}
// mounted时设置省列表
useEffect(() => {
setProvinceList(processAreaData(province_list))
}, [])
// 当弹框显示时设置上次点确定时的值和列表
useEffect(() => {
if (visible) {
updateCityAndCountyList(determinantVal.current)
setValue(determinantVal.current)
}
}, [visible])
return (
<Popup
className={classes}
visible={visible}
onMaskClick={onClose}
>
<div className="popup-hd fx fx-hb fx-vc">
<Button fill="none" onClick={onClose}>
取消
</Button>
<Button
color="primary"
fill="none"
onClick={handleConfirm}
>
确定
</Button>
</div>
<div className="popup-bd">
<PickerView
columns={[ provinceList, cityList, countyList ]}
value={value}
onChange={(val) => handleChange(val)}
/>
</div>
</Popup>
)
}
export default AreaPicker
style.scss
.area-picker-popup {
.adm-popup-body {
border-top-left-radius: 8px;
border-top-right-radius: 8px;
overflow: hidden;
}
.popup-hd {
padding: 4px;
border-bottom: 1px solid #eee;
}
.popup-bd {}
}