• 在 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- 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 {
             28px;
            height: 28px;
            margin-left: 4px;
          }
          .address-loading::v-deep {
            .el-loading-spinner {
               20px;
              height: 20px;
              margin-top: -12px;
              .circular {
                 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 {
            // calc(100% - 20px);
             100%;
          }
        }
      }
    </style>
    
    

    使用

    demo.vue

    <template>
      <div style="padding: 10px">
        地址自动选择演示
        <div style="display: flex; margin-bottom: 20px;  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 请求封装

  • 相关阅读:
    Oracle EXTRACT()函数与to_char() 函数
    Java内部类
    SQL 之 Group By
    Android LayoutInflater布局填充器
    JS 图片转Base64
    C# 事件与委托的区别
    AngularJS的循环输出
    jquery实现button倒计时
    重新理解B/S和C/S的区别
    HashMap与HashTable
  • 原文地址:https://www.cnblogs.com/taohuaya/p/14993561.html
Copyright © 2020-2023  润新知