8

气,文章一发出来第二天就被51CTO博客某一个人给抄了去了,也不注明出处作者,抄的连文章里面的链接也没了,真的就是一键复制粘贴啊。

公司开始做小程序了,小程序的省市区三级联动picker组件mode="region",之前也有接触过,这一次一上来先尝试了一下,发现不能和之前公司的地址库结合,因为之前项目都是和后端通过地区编码来交互的,这个自带的无法满足现有的情况。于是用小程序picker组件的多列选择器自制了一个。
开始主题之前先说一下地址数据的问题,我司的数据也是我自己2020年8月从高德接口导出来的最新数据并同步给后端的:

接口地址:https://restapi.amap.com/v3/c...

高德行政区查询接口文档

通过这个接口可以获取全国的行政区数据,至于拿到数据以后组装成什么格式,这里不细说了,都是基本功,只要留意着点省直辖县,市直辖镇等特殊级别关系的情况。

当然小程序省市区选择器数据咱们也是有办法弄到,只是,现有在用的数据库不宜大动,于是没有用,传送门也放在这里吧。

小程序官方地区选择器数据

当时我有这么两个格式:一个是树形的,拿到就可以用:
树形json

然而一看大小,好家伙,440kb,太大了,用不了。

另一个是平铺格式的:

image

这个96kb,虽然还是有点大,压缩一下,勉强可以用了。只是这不是一个树形结构,所以前期还要准备几个方法备用。

地区编码是有其规则的,比如,省级行政区是两位,后四位是“0000”,地市一级的是四位,后两位是“00”,到区县一级则是完整的6位。
于是先把平铺的json,过滤出省市区三个级别的数组出来。

// region.js
// 这个data就是那个平铺格式的json
const data = require('./data.js');

const list = []
const province = []
const city = []
const area = []

Object.entries(data).forEach(val => {
  const key = Number.parseInt(val[0])
  const model = { key: val[0], value: val[1] }
  list.push(model)
  if (!(key % 1e4)) {
    province.push(model)
  } else if (!(key % 100)) {
    city.push(model)
  } else {
    const num = Number(val[0].substr(2))
    if (num > 9000) {
      city.push(model)
    } else {
      area.push(model)
    }
  }
})

module.exports = {
  srcList: list,
  srcProvince: province,
  srcCity: city,
  srcArea: area,
}

这样,省一级的行政区数组就拿到了,地市一级和区县一级的则是每次根据选择的上一级的地区编码,来过滤出其管辖的下一级行政区划的数组:

// createTree.js
const region  = require('./region');

module.exports = {
  /**
   * load city list by province data
   *
   * @param province: { key: 330000, value: '浙江省' }
   * @returns {Array}
   */
  loadCity (province) {
    if (province && Object.keys(province).length) {
      const list = region.srcCity.filter(val => {
        const num = Number.parseInt(province.key)
        return (val.key - num) < 1e4 && (val.key % num) < 1e4
      })
      // Municipalities directly under the central government
      return list.length ? list : [province]
    } else return []
  },

  /**
   * load area list by city data
   *
   * @param city: { key: 330100, value: '杭州市' }
   * @returns {Array}
   */
  loadArea (city) {
    if (city && Object.keys(city).length) {
      const cityKey = Number.parseInt(city.key)
      const isNotProvince = cityKey % 1e4
      const calcNum = isNotProvince ? 100 : 1e4
      const list = region.srcArea.filter(val => {
        return (val.key - cityKey) < calcNum && val.key % cityKey < calcNum
      })
      // Prefecture-level city
      return list.length ? list : [city]
    } else return []
  },
}

好了,准备工作完毕,进入小程序页面。

<picker 
  class="picker" 
  mode="multiSelector" 
  bindchange="bindMultiPickerChange" 
  bindcolumnchange="bindMultiPickerColumnChange" 
  bindcancel="cancel"
  value="{{regionIndex}}"
  range="{{regionArray}}"
  range-key="value">
  <mp-cell ext-class="utils__item">
    <text class="text">所在地区</text>
    <input bindinput="inputChange" 
    readonly 
    disabled 
    value="{{areaText}}" 
    class="weui-input" 
    placeholder="所在地区"/>
  </mp-cell>
</picker>

页面元素结构就是这样,没什么好说的,这个可以直接看小程序picker中多列选择器的文档。先看一下,页面用到的相关数据吧。

data: {
    areaText: '', // 显示的省市区文字
    blockArray: [], // 显示的省市区三级数组,二维数组
    blockIndex: [0, 0, 0], // 显示出来的选择下标,默认,[0,0,0]
    regionArray: [], // 选择器当前的省市区三级数组,二维数组
    regionIndex: [0, 0, 0], // 选择器当前选中的下标
    provinceList: srcProvince, // 省级数组,
    cityList: [], // 地市级数组,
    areaList: [], // 区县数组
}

可以看到,我这里有两个array和index的数组,这是因为,其中一个保存的是已被选择的数据,另一个则是当前正在选择的数据,主要考虑到一个取消功能。你要把之前选中的保存起来,如果点击了取消按钮,才能还原为上次选择的数据。

const  { srcProvince } = require('../../utils/region.js');
const { 
    loadCity, 
    loadArea, 
} =  require('../../utils/createTree.js');

onLoad() { // 初始化数据,这里还涉及到一个数据回显,比较麻烦,放到后面再说
  let cityList, areaList,
  cityList = loadCity(srcProvince[0]);
  areaList = loadArea(cityList[0]);
  this.setData({
    blockArray: [srcProvince, cityList, areaList],
    regionArray: [srcProvince, cityList, areaList],
    cityList,
    areaList,
  })
},

bindMultiPickerChange (e) { // 点击确定按钮的事件
    const oldKey = this.data.blockArray[2][this.data.blockIndex[2]].key; // 这是留住上次选中的第三级区域代码
    const newKey = this.data.regionArray[2][this.data.regionIndex[2]].key; // 本次选中的第三级区域代码
    
    if(oldKey !== newKey) { // 如果本次选择和之前的不一样,则修改数据
      this.setData({
        blockArray: this.data.regionArray,
        regionIndex: e.detail.value,
        blockIndex: e.detail.value,
        areaText: `${this.data.regionArray[0][this.data.regionIndex[0]].value}${this.data.regionArray[1][this.data.regionIndex[1]].value}${this.data.regionArray[2][this.data.regionIndex[2]].value}`
      })
    }
  },
  
bindMultiPickerColumnChange (e) { // 这是选择器列数据发生变化的时候,就是选择器滚动的时候
    const regionIndex = this.data.regionIndex;
    // 这个 column 就是发生变化的是第几列,0,1,2,value则是滚动下标
    regionIndex[e.detail.column] = e.detail.value;
    // 下面的处理是当选择上一级的时候,要把下一级下标设置为0,防止下一级下标越界
    if(e.detail.column === 0) {
      regionIndex[2] = 0;
      regionIndex[1] = 0;
    } else if(e.detail.column === 1) {
      regionIndex[2] = 0;
    }
    const provinceItem = srcProvince[regionIndex[0]];
    let cityList = [], areaList = [];
    cityList = loadCity(provinceItem);
    areaList = loadArea(cityList[regionIndex[1]]);

    this.setData({
      regionArray: [srcProvince, cityList, areaList],
      regionIndex: regionIndex,
      cityList,
      areaList,
    })
  },
  
  cancel() { // 点击取消的时候需要把数据还原回选择之前的状态
    this.setData({ 
      regionArray: this.cloneDeep(this.data.blockArray),
      regionIndex: this.cloneDeep(this.data.blockIndex), // 这里需要将数组深拷贝,否则,第二次进入取消下标会乱掉
    });
  },
  
  cloneDeep(initArr) {
    let arr = [];
    arr = JSON.parse(JSON.stringify(initArr));
    return arr;
  }

这个选择器的逻辑就是以上这些,主要是选择器列变化的时候要把当前列下一级的行政区划数组找出来,并且留意用户取消的情况。

最后再说说数据回显,现在是没有数据的时候,就是默认的[0,0,0]的情况,处理很简单,否则,就是有数据的情况,我们跟后端之间的交互都是通过最后一级的行政编码来的,就是我这边给后台就只有一个areaCode,后端返回也只有一个areaCode。因为这个在无论我们当前项目还是在这个小程序的后台项目上,逻辑上都不会存在只有省一级或者只有省市两级的情况,所以。只要有数据,这个areaCode必然是区县一级的代码。于是在createTree.js里增加一个方法:

/*
  * areaCodeToMap
  * @param {string} code
  * @param {array} list
  */

  areaCodeToMap(code, list) {
    const result = {
      map: {},
      index: 0,
    };
    list.map((item, i) => {
      if(item.key === code) {
        result.map = item;
        result.index = i;
      }
    })
    return result;
  },

这是通过这个areaCode把上面两级的行政区对象及其下标找出来,只要有了上面两级的行政区对象,则又可以通过之前的loadCity和loadArea两个方法设置选择器数据了:

setArea() { // 省市区数据回显
    let index = 0;
    let cityList = [], areaList = [], provinceIndex = 0, cityIndex = 0, areaIndex = 0;
    const areaCode = this.data.form.areaCode;
    if(areaCode) {
      // 省级信息回显
      const province = areaCodeToMap(`${areaCode.substring(0, 2)}0000`, srcProvince);
      const provinceItem = province.map;
      provinceIndex = province.index;

      // 地市级信息回显
      cityList = loadCity(provinceItem);
      const city = areaCodeToMap(`${areaCode.substring(0, 4)}00`, cityList);
      const cityItem = city.map;
      cityIndex = city.index;

      // 区县级信息回显
      areaList = loadArea(cityItem);
      const area = areaCodeToMap(areaCode, areaList);
      const areaItem = area.map;
      areaIndex = area.index;
      
      this.setData({
        blockArray: [srcProvince, cityList, areaList],
        regionArray: [srcProvince, cityList, areaList],
        blockIndex: [provinceIndex, cityIndex, areaIndex],
        regionIndex: [provinceIndex, cityIndex, areaIndex],
        cityList,
        areaList,
      })
      this.setData({
        areaText: `${this.data.regionArray[0][this.data.regionIndex[0]].value}${this.data.regionArray[1][this.data.regionIndex[1]].value}${this.data.regionArray[2][this.data.regionIndex[2]].value}`
      })
}

上真机效果图:

image

其实这里还涉及到省市区选择器和地图选址组件的交互,比如选择完地区以后,将经纬度设置成该地区的经纬度,进入地图后定位到该地区,还有在地图定位完位置后根据地图返回的areaCode再调用一遍setArea方法直接回显省市区等等,这些不是本文探讨范围,不再细说了。

文章写完了,喜欢的给个赞吧!


文心雕刺
144 声望3 粉丝