在 antd mobile 基础上封装一个省市区选择组件 AreaPicker

587 阅读2分钟

在做 React 移动端开发用到 antd-mobile 时,需要一个 选择省市区 的组件,但官方没有,就自己封装一个

🔥 文末有完整的组件代码 AreaPicker.tsx

未标题-1.png

大概的开发过程和原理

一开始我使用 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 就代表江苏省)

PickerViewonChange 中拿到当前 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 {}
}