基於Vue+ElementUI的省市區地址選擇通用組件

一、緣由

在項目開發過程中,有一個需求是省市區地址選擇的功能,一開始想的是直接使用靜態地址資源庫本地打包,但這種方式不方便維護,於是放棄。後來又想直接讓後臺返回全部地址數據,然後使用級聯選擇器進行選擇,但發現數據傳輸量有點大且處理過程耗時,於是又摒棄了這種方法。最後還是決定採用異步的方式進行省市區地址選擇,即先查詢省份列表,然後根據選擇的省份code查詢城市列表,最後根據選擇的城市列表獲取區/縣列表,最終根據應用場景不同,給出了兩種實現方案。

其中後臺總共需要提供4個接口,一個查詢所有省份的接口,一個根據省份code查詢其下所有城市的接口,一個根據城市code查詢其下所有區/縣的接口,以及一個根據地址code轉換成省市區三個code值的接口。

// 本人項目中使用的四個接口
`${this.API.province}/${countryCode}` // 根據國家code查詢省份列表,中國固定爲156,可以拓展
`${this.API.city }/${provinceCode}` // 根據省份code查詢城市列表
`${this.API.area}/${cityCode}` // 根據城市code查詢區/縣列表
`${this.API.addressCode}/${addressCode}` // 地址code轉換爲省市區code

二、基於el-cascader 級聯選擇器的單選擇框實現方案

<template>
  <el-row>
    <el-cascader
      size="small"
      :options="city.options"
      :props="props"
      v-model="cityValue"
      @active-item-change="handleItemChange"
      @change="cityChange">
    </el-cascader>
  </el-row>
</template>

<script>
export default {
  name: 'addressSelector',
  props: {
    areaCode: null
  },

  model: {
    prop: 'areaCode',
    event: 'cityChange'
  },

  data () {
    return {
      // 所在省市
      city: {
        obj: {},
        options: []
      },
      props: { // 級聯選擇器的屬性配置
        value: 'value',
        children: 'cities',
        checkStrictly: true
      },
      cityValue: [], // 城市代碼
    }
  },
  computed: {
  },
  created () {
    this._initData()
  },
  mounted () {
  },
  methods: {
    _initData () {
      this.$http({
        method: 'get',
        url: this.API.province + '/156' // 中國
      }).then(res => {
        this.city.options = res.data.body.map(item => { // 所在省市
          return {
            value: item.provinceCode,
            label: item.provinceName,
            cities: []
          }
        })
      })
    },
    getCodeByAreaCode (code) {
      if (code == undefined) return false
      this.$http({
        method: 'get',
        url: this.API.addressCode + '/' + code
      })
      .then(res => {
        if (res.data.code === this.API.SUCCESS) {
          let provinceCode =  res.data.body.provinceCode
          let cityCode = res.data.body.cityCode
          let areaCode = res.data.body.areaCode
          this.cityValue = [provinceCode, cityCode, areaCode]
          this.handleItemChange([provinceCode, cityCode])
        }
      })
      .finally(res => {
      })
    },
    handleItemChange (value) {
      let a = (item) => {
        this.$http({
          method: 'get',
          url: this.API.city + '/' + value[0],
        }).then(res => {
          item.cities = res.data.body.map(ite => {
            return {
              value: ite.cityCode,
              label: ite.cityName,
              cities: []
            }
          })
          if(value.length === 2){ // 如果傳入的value.length===2 && 先執行的a(),說明是傳入了areaCode,需要初始化多選框
            b(item)
          }
        }).finally(_ => {
        })
      }
      let b = (item) => {
        if (value.length === 2) {
          item.cities.find(ite => {
            if (ite.value === value[1]) {
              if (!ite.cities.length) {
                this.$http({
                  method: 'get',
                  url: this.API.area + '/' + value[1]
                }).then(res => {
                  ite.cities = res.data.body.map(ite => {
                    return {
                      value: ite.areaCode,
                      label: ite.areaName,
                    }
                  })
                }).finally(_ => {
                })
              }
            }
          })
        }
      }
      this.city.options.find(item => {
        if (item.value === value[0]) {
          if (item.cities.length) {
            b(item)
          } else {
            a(item)
          }
          return true
        }
      })
    },
    getCityCode () {
      return this.cityValue[2]
    },
    reset () {
      this.cityValue = []
    },
    cityChange (value) {
      if (value.length === 3) {
        this.$emit('cityChange', value[2])
      } else {
        this.$emit('cityChange', null)
      }
    }
  },
  watch: {
    areaCode: {
      deep: true,
      immediate: true,
      handler (newVal) {
        if (newVal) {
          this.getCodeByAreaCode(newVal)
        } else {
          this.$nextTick(() => {
            this.reset()
          })
        }
      }
    }
  }
}
</script>

<style lang="less" scoped>
</style>

最終效果如下(動圖):

截圖:

三、基於el-select選擇器的多選擇框實現方案

<template>
  <div id="addressHorizontalSelect">
    <el-row>
      <el-col
        :span="span">
        <el-select
          size="small"
          v-model="provinceCode"
          @focus="getProvinces"
          @change="changeProvince"
          :placeholder="$t('省')"
          filterable>
          <el-option
            v-for="item in provinceList"
            :key="item.provinceCode"
            :label="item.provinceName"
            :value="item.provinceCode">
          </el-option>
        </el-select>
      </el-col>
      <el-col
        :span="span"
        v-if="!hideCity">
        <el-select
          size="small"
          v-model="cityCode"
          @focus="getCities"
          @change="changeCity"
          :placeholder="$t('市')"
          filterable>
          <el-option
            v-for="item in cityList"
            :key="item.cityCode"
            :label="item.cityName"
            :value="item.cityCode">
          </el-option>
        </el-select>
      </el-col>
      <el-col
        :span="span"
        v-if="!hideCity && !hideArea">
        <el-select
          size="small"
          v-model="areaCode"
          @focus="getAreas"
          @change="changeArea"
          :placeholder="$t('區/縣')"
          filterable>
          <el-option
            v-for="item in areaList"
            :key="item.areaCode"
            :label="item.areaName"
            :value="item.areaCode">
          </el-option>
        </el-select>
      </el-col>
    </el-row>
  </div>
</template>

<script>
export default {
  name: 'addressHorizontalSelect',

  components: {},

  props: {
    hideCity: { // 隱藏市
      type: Boolean,
      default: false
    },
    hideArea: { // 隱藏區/縣
      type: Boolean,
      default: false
    },
    addressCode: null // 地址編碼
  },

  model: {
    prop: 'addressCode',
    event: 'addressSelect'
  },

  data() {
    return {
      provinceList: [], // 省份列表
      cityList: [], // 城市列表
      areaList: [], // 區/縣列表
      provinceCode: '', // 省份編碼
      cityCode: '', // 城市編碼
      areaCode: '', // 區/縣編碼
      cityFlag: false, // 避免重複請求的標誌
      provinceFlag: false,
      areaFlag: false
    }
  },

  computed: {
    span () {
      if (this.hideCity) {
        return 24
      }
      if (this.hideArea) {
        return 12
      }
      return 8
    }
  },

  watch: {
  },

  created () {
    this.getProvinces()
  },

  methods: {
    /**
     * 獲取數據
     * @param {Array} array 列表
     * @param {String} url 請求url
     * @param {String} code 編碼(上一級編碼)
     */
    fetchData (array, url, code) {
      this.$http({
        method: 'get',
        url: url + '/' + code
      })
      .then(res => {
        if (res.data.code === this.API.SUCCESS) {
          let body = res.data.body || []
          array.splice(0, array.length, ...body)
        }
      })
      .catch(err => {
        console.log(err)
      })
      .finally(res => {
      })
    },
    // 根據國家編碼獲取省份列表
    getProvinces () {
      if (this.provinceFlag) {
        return
      }
      this.fetchData(this.provinceList, this.API.province, 156)
      this.provinceFlag = true
    },
    // 省份修改,拉取對應城市列表
    changeProvince (val) {
      this.fetchData(this.cityList, this.API.city, this.provinceCode)
      this.cityFlag = true
      this.cityCode = ''
      this.areaCode = ''
      this.$emit('addressSelect', val)
    },
    // 根據省份編碼獲取城市列表
    getCities () {
      if (this.cityFlag) {
        return
      }
      if (this.provinceCode) {
        this.fetchData(this.cityList, this.API.city, this.provinceCode)
        this.cityFlag = true
      }
    },
    // 城市修改,拉取對應區域列表
    changeCity (val) {
      this.fetchData(this.areaList, this.API.area, this.cityCode)
      this.areaFlag = true
      this.areaCode = ''
      this.$emit('addressSelect', val)
    },
    // 根據城市編碼獲取區域列表
    getAreas () {
      if (this.areaFlag) {
        return
      }
      if (this.cityCode) {
        this.fetchData(this.areaList, this.API.area, this.cityCode)
      }
    },
    // 區域修改
    changeArea (val) {
      this.$emit('addressSelect', val)
    },
    // 重置省市區/縣編碼
    reset () {
      this.provinceCode = '',
      this.cityCode = '',
      this.areaCode = ''
    },
    // 地址編碼轉換成省市區列表
    addressCodeToList (addressCode) {
      if (!addressCode) return false
      this.$http({
        method: 'get',
        url: this.API.addressCode + '/' + addressCode
      })
      .then(res => {
        let data = res.data.body
        if (!data) return
        if (data.provinceCode) {
          this.provinceCode = data.provinceCode
          this.fetchData(this.cityList, this.API.city, this.provinceCode)
        } else if (data.cityCode) {
          this.cityCode = data.cityCode
          this.fetchData(this.areaList, this.API.area, this.cityCode)
        } else if (data.areaCode) {
          this.areaCode = data.areaCode
        }
      })
      .finally(res => {
      })
    }
  },

  watch: {
    addressCode: {
      deep: true,
      immediate: true,
      handler (newVal) {
        if (newVal) {
          this.addressCodeToList(newVal)
        } else {
          this.$nextTick(() => {
            this.reset()
          })
        }
      }
    }
  }
}
</script>

<style lang="less" scoped>
</style>

實現效果如下(動圖):

四、總結

兩個組件都實現了雙向綁定,根據場景不同可以使用不同的組件,如果讀者有需求,根據自己的接口和場景進行修改即可。

當拓展至大洲-國家-省-市-區-街道等時,第一種級聯選擇器的方案就會暴露出拓展性較差的問題,隨着層級加深,數據結構會變得複雜,而第二種方案明顯可拓展性更強

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章