在 vue + element 中实现区域地址(省市区街道)自动选择

直接上效果、上代码吧

效果演示

实现组件

这里区域地址是调的 京东的JSONP接口,这里就把这个组件命名为 JdAddress
组件结构:

JdAddress
    │  index.vue
    │
    └─core
            api.js
            jsonp.js
            longest.js

core/jsonp.js

class JSONP {
  constructor(config = {}) {
    this._reqFlag = 0 // 用来给每个jsop 请求添加唯一标识
    this.timeout = config.timeout ?? 10 * 1000
    this.callbackLabel = config.callbackLabel ?? 'callback' // 传给后台的 callback key 优先级小于 get 方法 options.callbackLabel
  }

  /**
   * jsonp 请求
   * @param url
   * @param [options = {}]
   * @param {string} [options.callbackLabel = 'callback'] 传给后台的 callback key, 优先级大于 config.callbackLabel
   * @return {Promise<{id: number, name: string}[]>}
   */
  get(url, options = {}) {
    const callbackLabel = options.callbackLabel ?? this.callbackLabel
    const jsonpCallbackFnName = 'jsonpCallback' + this._reqFlag
    this._reqFlag++
    const urlObj = new URL(url)
    // https://fts.jd.com/area/get?fid=0&callback=aaa
    const fullUrl = url + (urlObj.search ? '&' : '?') + callbackLabel + '=' + jsonpCallbackFnName

    return new Promise((resolve, reject) => {
      const jsonpScript = document.createElement('script')
      jsonpScript.id = jsonpCallbackFnName
      jsonpScript.src = fullUrl

      window[jsonpCallbackFnName] = (result) => {
        document.getElementById(jsonpCallbackFnName) && document.body.removeChild(jsonpScript)
        resolve(result)
        delete window[jsonpCallbackFnName]
      }
      document.body.appendChild(jsonpScript)
      setTimeout(() => {
        document.getElementById(jsonpCallbackFnName) && document.body.removeChild(jsonpScript)
        reject(`TIMEOUT: no response in ${this.timeout} milliseconds`)
        delete window[jsonpCallbackFnName]
      }, this.timeout)
    })
  }

  static create(config) {
    return new JSONP(config)
  }
}

export default JSONP

core/api.js

import JSONP from './jsonp'

const jsonp = JSONP.create()

export function getArea(id = 0) {
  return jsonp.get('https://fts.jd.com/area/get?fid=' + id)
}

core/longest.js

/**
 * 最长子串
 * 输入 ['weeweadbshow', 'jhsaasrbgddbshow', 'ccbshow'] 输出 bshow
 * @param {string[]} sourceArr
 * @return {string}
 */
export function longest(sourceArr) {
  // 字符串长度排序,优先选择最短的字符串,尽可能的减少性能开支
  sourceArr = string_ArraySort(sourceArr)
  const wholeArr = [] // 最短字符串所能产生的所有子串
  const firstStr = sourceArr.shift() // 以最短子串为基准
  let count = 0 // 结果长度
  let result = '' // 结果

  // 截取子串
  for (let i = 0; i < firstStr.length; i++) {
    for (let j = i + 1; j <= firstStr.length; j++) {
      wholeArr.push(firstStr.substring(i, j))
    }
  }

  // 遍历所有的子串
  for (let i = 0; i < wholeArr.length; i++) {
    let AllArray = [] // 建立一个结果过渡数组

    // 使用正则表达式来检索其他的字符串
    const patt = new RegExp(wholeArr[i])
    for (let j = 0; j < sourceArr.length; j++) {
      const reArr = sourceArr[j].match(patt) // 使用正则表达式来检索,match 函数直接返回结果
      if (reArr) { // 如果没检索到,返回一个false值,如果匹配到就返回结果
        AllArray = AllArray.concat(reArr) // 向结果过渡函数添加值
      }
    }

    if (AllArray.length === sourceArr.length) { // 验证是否在其他字符串中是否都匹配到了子串
      if (AllArray[0].length > count) {
        // 过渡结果
        count = AllArray[0].length
        result = AllArray[0]
      }
    }
  }
  return result
}

// 根据字符串长度排序
function string_ArraySort(strArr) {
  return strArr.sort(function(str1, str2) {
    return str1.length - str2.length
  })
}

index.vue

<template>
  <div class="JdAddress">
    <div class="broad-wrap">
      <el-select
        v-for="(item, index) in areaSelects"
        :key="item.field"
        v-model="item.selectedId"
        class="address-select"
        size="mini"
        placeholder="请选择"
        @change="(id) => handleSelectChange(index, id)"
      >
        <el-option
          v-for="opt in item.options"
          :key="opt.id"
          :label="opt.name"
          :value="opt.id"
        />
      </el-select>
      <span v-loading="loading" class="address-loading"></span>
    </div>
    <div class="exact-warp">
      <div :class="['broad-display', {p10: addressPrefix.length}]">{{ addressPrefix }}</div>
      <div class="input-warp">
        <el-input
          v-model="addressDetail"
          class="address-input"
          size="mini"
          placeholder="请输入详细地址"
          @change="handleAddressDetailChange"
        />
      </div>
    </div>
  </div>
</template>

<script>

import { getArea } from './core/api'
import { longest } from './core/longest'
import { jsonDeepCopy } from '@/utils'

/**
 * @typedef EmitParams
 * @type {Object}
 * @property {Array<{id: number, name: string}>} fullArea - 选中的区域数组
 * @property {string} addressDetail - 输入的地址详情
 * @property {string} fullAddress - 完整的地址
 * @property {{
 * selectedId: null | number,
 * field: ('province' | 'city' | 'district' | 'town'),
 * options: Array<{id: number, name: string}>
 * }} areaSelects - 结构化的原始数据
 */

export default {
  name: 'JdAddress',
  props: {
    autoSelectAddress: {
      type: String,
      default: ''
    }
  },
  data() {
    return {
      areaSelects: this.getInitialAreaSelects(),
      addressPrefix: '', // 四级全称
      addressDetail: '',
      loading: false
    }
  },
  watch: {
    autoSelectAddress() {
      this.init()
    }
  },
  mounted() {
    this.init()
  },
  methods: {
    getInitialAreaSelects() {
      return [
        { selectedId: null, field: 'province', options: [] },
        { selectedId: null, field: 'city', options: [] },
        { selectedId: null, field: 'district', options: [] },
        { selectedId: null, field: 'town', options: [] }
      ]
    },

    async getAreaWithLoading(id) {
      this.loading = true
      try {
        return await getArea(id)
      } catch (e) {
        this.$message.warning(`获取${id} 子区域失败: ${e}`)
      } finally {
        this.loading = false
      }
    },

    async init() {
      this.areaSelects = this.getInitialAreaSelects()
      this.addressPrefix = ''
      this.addressDetail = ''

      const proviceOptions = await this.getAreaWithLoading(4744) // 4746 中国
      const copyAreaSelects = jsonDeepCopy(this.areaSelects)
      copyAreaSelects[0].options = proviceOptions ?? []
      this.areaSelects = copyAreaSelects

      // 自动输入
      if (!this.autoSelectAddress) return
      const parseAddressResult = await this.parseAutoSelectAddress(this.areaSelects, this.autoSelectAddress)
      this.areaSelects = parseAddressResult.areaSelects
      this.addressDetail = parseAddressResult.addressDetail
      this.redisplay()

      this.emitChange()
    },

    async handleSelectChange(selectIndex, id) {
      // 重置 selectIndex 后面的
      this.resetSelectOption(selectIndex)
      const copyAreaSelects = jsonDeepCopy(this.areaSelects)
      // 赋值 selectIndex 下一个
      if (selectIndex < copyAreaSelects.length - 1) {
        copyAreaSelects[selectIndex + 1].options = await this.getAreaWithLoading(id)
      }
      this.areaSelects = copyAreaSelects

      this.redisplay()
      this.emitChange()
    },

    handleAddressDetailChange(val) {
      this.addressDetail = val
      this.emitChange()
    },

    // 清空当前 select index 后的 select 所有的 options/ selectedId
    resetSelectOption(index) {
      const copyAreaSelects = jsonDeepCopy(this.areaSelects)
      for (let i = 0; i < copyAreaSelects.length; i++) {
        if (i > index) {
          copyAreaSelects[i].selectedId = null
          copyAreaSelects[i].options = []
        }
      }
      this.areaSelects = copyAreaSelects
    },

    // 重新组合显示选中的四级
    redisplay() {
      let addressPrefix = ''
      for (let i = 0; i < this.areaSelects.length; i++) {
        if (!this.areaSelects[i].selectedId) break
        const selected = this.areaSelects[i].options.find(opt => opt.id === this.areaSelects[i].selectedId)
        if (selected) addressPrefix += selected.name
      }
      this.addressPrefix = addressPrefix
    },

    emitChange() {
      const fullArea = []
      for (let i = 0; i < this.areaSelects.length; i++) {
        const select = this.areaSelects[i]
        if (!select.selectedId) {
          break
        }
        for (let j = 0; j < select.options.length; j++) {
          const opt = select.options[j]
          if (select.selectedId === opt.id) {
            fullArea.push(opt)
            break
          }
        }
      }
      const fullAddress = fullArea.reduce((acc, cur) => acc + cur.name, '') + this.addressDetail
      const copyAreaSelects = jsonDeepCopy(this.areaSelects)
      // emitParams
      /**
       * @type {EmitParams}
       */
      const emitParams = { fullArea, addressDetail: this.addressDetail, fullAddress, areaSelects: copyAreaSelects }
      this.$emit('change', emitParams)
    },

    // 浙江省金华市婺城区 东街道人民东路1188号中外运仓库(门口左边第一幢17-18号尚尔仓库)
    async parseAutoSelectAddress(areaSelects, addr) {
      const copyAreaSelects = jsonDeepCopy(areaSelects)
      if (!addr) return { areaSelects: copyAreaSelects, fullAreas: [], addressDetail: '' }
      const fullAreas = []
      let pAddr = addr
      for (let i = 0; i < copyAreaSelects.length; i++) {
        const currentOptions = copyAreaSelects[i].options
        let latestSubStr = '' // 最近的匹配的最佳子串
        let bestOptIndex = -1
        for (let j = 0; j < currentOptions.length; j++) {
          const opt = currentOptions[j]
          const optName = i === 0 ? opt.name + '省' : opt.name // 省后缀
          const longestSubStr = longest([pAddr, optName])
          if (longestSubStr.length > latestSubStr.length && longestSubStr.length >= 2) {
            latestSubStr = longestSubStr
            bestOptIndex = j
          }
        }
        if (bestOptIndex < 0) {
          return { areaSelects: copyAreaSelects, /* fullAreas ,*/ addressDetail: pAddr }
        }
        // 取出 最长子串
        const area = currentOptions[bestOptIndex]
        // 第二级 options 赋值
        const nextOptions = await this.getAreaWithLoading(area.id)
        // 不是最后一个
        if (i < copyAreaSelects.length - 1) copyAreaSelects[i + 1].options = [...nextOptions]

        if (latestSubStr) {
          const start = pAddr.indexOf(latestSubStr)
          if (start >= 0) {
            pAddr = pAddr.slice(start + latestSubStr.length)
          }
        }
        fullAreas.push(area)
        if (area) {
          copyAreaSelects[i].selectedId = area.id
        }
      }
      return { areaSelects: copyAreaSelects, /* fullAreas ,*/ addressDetail: pAddr }
    }
  }
}
</script>

<style scoped lang="scss">
  .JdAddress {
    max-width: 742px;
    .broad-wrap {
      display: flex;
      .address-select {
        margin-right: 10px;
      }
      .address-select {
        margin-right: 10px;
      }
      .address-select:last-of-type {
        margin-right: 0;
      }
      .address-loading {
        width: 28px;
        height: 28px;
        margin-left: 4px;
      }
      .address-loading::v-deep {
        .el-loading-spinner {
          width: 20px;
          height: 20px;
          margin-top: -12px;
          .circular {
            width: 20px;
            height: 20px;
          }
        }
      }
    }
    .exact-warp {
      display: flex;
      margin-top: 6px;
      .broad-display {
        display: flex;
        align-items: center;
        font-size: 12px;
        color: #606266;
        white-space: nowrap;
      }
      .broad-display.p10 {
        padding-right: 10px;
      }
      .input-warp {
        //width: calc(100% - 20px);
        width: 100%;
      }
    }
  }
</style>

使用

demo.vue

<template>
  <div style="padding: 10px">
    地址自动选择演示
    <div style="display: flex; margin-bottom: 20px; width: 820px">
      <el-input
        v-model="addressInput"
        class="address-input"
        size="mini"
        placeholder="请输入要解析的地址"
      />
      <el-button size="mini" @click="handleAddressParse">地址解析</el-button>
    </div>
    <jd-address :auto-select-address="autoSelectAddress" @change="handleAddrChange" />
  </div>
</template>

<script>

import JdAddress from '../path/components/JdAddress'
export default {
  name: 'FOO',
  components: { JdAddress },
  data() {
    return {
      addressInput: '',
      autoSelectAddress: ''
    }
  },
  methods: {
    handleAddrChange(address) {
      console.log('address', address)
    },

    handleAddressParse() {
      this.autoSelectAddress = this.addressInput
    }
  }
}
</script>

结束...

参考:

jsonp 请求封装

posted @ 2021-07-10 12:18  暗恋桃埖源  阅读(3999)  评论(3编辑  收藏  举报