在 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>
结束...
参考: