最近优化了一个省.市.区/县、乡镇/街道的四级联动组件,技术栈是element + vue3记录一下。
本来是这样的三级联动:
这个三级联动很简单,直接利用el-select组件把地区值带进去就行了,现在要优化成省.市.区/县、乡镇/街道的四级联动,变成这样:
下面进入正文: (说一下主要流程,最后附上全部代码)
首先要准备省市区和对应编码的JSON文件:
GitHub - modood/Administrative-divisions-of-China: 中华人民共和国行政区划:省级(省份)、 地级(城市)、 县级(区县)、 乡级(乡镇街道)、 村级(村委会居委会) ,中国省市区镇村二级三级四级五级联动地址数据。
可以参考这个地址,直接在浏览器下载也行,git 克隆到本地也行,这个json文件很大,大概两三兆,可以让后端返回。
省份分组时用到了一个三方包,需要把省份转成拼音获取首字母,直接下载就行
yarn add chinese-to-pinyin 或者 npm i chinese-to-pinyin
import pinyin from "chinese-to-pinyin"
然后调整数据结构,
//省份分组
const groupedProvinces = ref({
"A-G": [],
"H": [],
"J-Q": [],
"S-T": [],
"X-Z": [],
"其它": []
})
//分解省市区数据
function extractLocations(data, level = 0, results = { provinces: [], cities: [], districts: [], streets: [] }) {
for ( const item of data ) {
// 根据层级确定当前是省/直辖市、市/区或街道,并存储数据
if ( level === 0 ) {
results.provinces.push(item)
} else if ( level === 1 ) {
results.cities.push(item)
} else if ( level === 2 ) {
results.districts.push(item)
} else if ( level === 3 ) {
results.streets.push(item)
}
// 如果存在子级,递归调用自身
if ( item.children && item.children.length ) {
extractLocations(item.children, level + 1, results)
}
}
return results
}
//省市区数组集合
const pcasList = ref(extractLocations(pcasCodeList))
//按首字母分类的省份
function groupProvinces(provinces) {
pcasList.value.provinces.forEach(province => {
let firstLetter = pinyin(province.name, { removeTone: true }).charAt(0).toUpperCase()
if ( province.name === "澳门特别行政区" || province.name === "台湾省" || province.name === "香港特别行政区" ) {
// 澳门、台湾、香港特殊处理
switch ( province.name ) {
case "澳门特别行政区":
case "台湾省":
case "香港特别行政区":
groupedProvinces.value["其它"].push(province)
break
}
} else {
if ( "ABCDEFG".indexOf(firstLetter) !== -1 ) {
groupedProvinces.value["A-G"].push(province)
} else if ( "H".indexOf(firstLetter) !== -1 ) {
groupedProvinces.value["H"].push(province)
} else if ( "JKLMNOPQ".indexOf(firstLetter) !== -1 ) {
groupedProvinces.value["J-Q"].push(province)
} else if ( "ST".indexOf(firstLetter) !== -1 ) {
groupedProvinces.value["S-T"].push(province)
} else if ( "XYZ".indexOf(firstLetter) !== -1 ) {
groupedProvinces.value["X-Z"].push(province)
} else {
// 其他不识别省份的处理
console.warn("未识别的省份:", province.name)
}
}
})
}
groupProvinces(pcasList.value.provinces)
这样就实现了这个页面了
交互逻辑太多
为了避免文章太长
直接上全部代码
<template>
<el-popover v-model:visible="popoverVisible" :width="460" placement="bottom" trigger="click">
<template #reference>
<el-input v-model="dataForm.PCASName" placeholder="请选择省市区街道/乡镇"/>
</template>
<div>
<el-tabs v-model="activeName" class="demo-tabs" @tab-click="handleClick">
<el-tab-pane :label="dataForm.provinceName" name="first">
<div>
<div v-for="(item, itemName) in groupedProvinces" :key="itemName" class="addressItem">
<div class="left">{{itemName}}</div>
<div class="right">
<div v-for="(item,index) in item" :key="index"
:class="{'active': dataForm.provinceName === item.name }"
class="provinceItem"
@click="provinceItemFn(item)">{{ item.name }}
</div>
</div>
</div>
</div>
</el-tab-pane>
<el-tab-pane v-if="dataForm.province" :label="dataForm.cityName" name="second">
<div class="cityContent">
<div v-for="(item, index) in dataForm.citesList" :key="index"
:class="{'active': dataForm.cityName === item.name }"
class=" cityItem" @click="cityItemFn(item)">
{{ item.name }}
</div>
</div>
</el-tab-pane>
<el-tab-pane v-if="dataForm.city" :label="dataForm.areaName" name="three">
<div class="cityContent">
<div v-for="(item, index) in dataForm.areaList" :key="index"
:class="{'active': dataForm.areaName === item.name }"
class=" cityItem" @click="areaItemFn(item)">
{{ item.name }}
</div>
</div>
</el-tab-pane>
<el-tab-pane v-if="dataForm.area" :label="dataForm.streetName" name="four">
<div class="cityContent">
<div v-for="(item, index) in dataForm.streetsList" :key="index"
:class="{'active': dataForm.streetName === item.name }"
class=" cityItem" @click="streesItemFn(item)">
{{ item.name }}
</div>
</div>
</el-tab-pane>
</el-tabs>
</div>
</el-popover>
</template>
<script setup>
import { reactive, ref, watchEffect } from "vue"
import { cloneDeep } from "lodash-es"
import pcasCode from "@/const/json/pcas-code.json"
import pinyin from "chinese-to-pinyin"
//弹出框是否显示
let popoverVisible = ref(null)
//省市区tab
const activeName = ref("first")
//省市区code数据
let pcasCodeList = cloneDeep(pcasCode)
//省份分组
const groupedProvinces = ref({
"A-G": [],
"H": [],
"J-Q": [],
"S-T": [],
"X-Z": [],
"其它": []
})
//分解省市区数据
function extractLocations(data, level = 0, results = { provinces: [], cities: [], districts: [], streets: [] }) {
for ( const item of data ) {
// 根据层级确定当前是省/直辖市、市/区或街道,并存储数据
if ( level === 0 ) {
results.provinces.push(item)
} else if ( level === 1 ) {
results.cities.push(item)
} else if ( level === 2 ) {
results.districts.push(item)
} else if ( level === 3 ) {
results.streets.push(item)
}
// 如果存在子级,递归调用自身
if ( item.children && item.children.length ) {
extractLocations(item.children, level + 1, results)
}
}
return results
}
//省市区数组集合
const pcasList = ref(extractLocations(pcasCodeList))
//按首字母分类的省份
function groupProvinces(provinces) {
pcasList.value.provinces.forEach(province => {
let firstLetter = pinyin(province.name, { removeTone: true }).charAt(0).toUpperCase()
if ( province.name === "澳门特别行政区" || province.name === "台湾省" || province.name === "香港特别行政区" ) {
// 澳门、台湾、香港特殊处理
switch ( province.name ) {
case "澳门特别行政区":
case "台湾省":
case "香港特别行政区":
groupedProvinces.value["其它"].push(province)
break
}
} else {
if ( "ABCDEFG".indexOf(firstLetter) !== -1 ) {
groupedProvinces.value["A-G"].push(province)
} else if ( "H".indexOf(firstLetter) !== -1 ) {
groupedProvinces.value["H"].push(province)
} else if ( "JKLMNOPQ".indexOf(firstLetter) !== -1 ) {
groupedProvinces.value["J-Q"].push(province)
} else if ( "ST".indexOf(firstLetter) !== -1 ) {
groupedProvinces.value["S-T"].push(province)
} else if ( "XYZ".indexOf(firstLetter) !== -1 ) {
groupedProvinces.value["X-Z"].push(province)
} else {
// 其他不识别省份的处理
console.warn("未识别的省份:", province.name)
}
}
})
}
groupProvinces(pcasList.value.provinces)
//tab栏点击事件
const handleClick = (tab) => {
if ( tab.props.name === "second" ) {
dataForm.value.citesList = pcasCodeList.find(item => item.code === dataForm.value.province)?.children || []
} else if ( tab.props.name === "three" ) {
const childrenArray = findChildrenByCode(pcasCodeList, dataForm.value.city)
dataForm.value.areaList = childrenArray || []
} else if ( tab.props.name === "four" ) {
const childrenArray = findChildrenByCode(pcasCodeList, dataForm.value.area)
dataForm.value.streetsList = childrenArray || []
}
}
let dataForm = ref({
citesList: [], //城市分组
areaList: [], //区县分组
streetsList: [], //街道乡镇分组
province: "", //省code
city: "", //城市code
area: "", //区县code
street: "", //街道乡镇code
provinceName: "请选择", //省名称
cityName: "请选择",// 城市名称
areaName: "请选择", // 区县名称
streetName: "请选择", //街道名称
PCASName: "",//省市区街道名称
isSpecial: false//是否是特别行政区
})
//点击省
const provinceItemFn = (val) => {
dataForm.value.provinceName = val.name
dataForm.value.PCASName = updatePCASName(val.name)
dataForm.value.province = val.code
dataForm.value.citesList = val.children || []
resetSelections([ "city", "area", "street" ])
if ( dataForm.value.provinceName === "台湾省" ) {
dataForm.value.isSpecial = true
popoverVisible.value = false
return
} else {
activeName.value = "second"
}
console.log(val)
}
//点击城市
const cityItemFn = (val) => {
dataForm.value.cityName = val.name
dataForm.value.PCASName = updatePCASName(dataForm.value.provinceName, val.name)
dataForm.value.city = val.code
dataForm.value.areaList = val.children || []
resetSelections([ "area", "street" ])
if ( dataForm.value.provinceName === "澳门特别行政区" || dataForm.value.provinceName === "香港特别行政区" ) {
dataForm.value.isSpecial = true
popoverVisible.value = false
return
} else {
activeName.value = "three"
}
console.log(val)
}
//点击区县
const areaItemFn = (val) => {
dataForm.value.areaName = val.name
dataForm.value.PCASName = updatePCASName(dataForm.value.provinceName, dataForm.value.cityName, val.name)
dataForm.value.area = val.code
dataForm.value.streetsList = val.children
resetSelections([ "street" ])
activeName.value = "four"
console.log(val)
}
//点击街道/乡镇
const streesItemFn = (val) => {
dataForm.value.streetName = val.name
dataForm.value.PCASName = updatePCASName(dataForm.value.provinceName, dataForm.value.cityName, dataForm.value.areaName, val.name)
dataForm.value.street = val.code
popoverVisible.value = false
console.log(val)
}
watchEffect(() => {
//判断某个地区为空时清空输入框内容
if ( !popoverVisible.value && !dataForm.value.isSpecial && ( !dataForm.value.province || !dataForm.value.city || !dataForm.value.area || !dataForm.value.street ) ) {
dataForm.value.PCASName = ""
}
//判断如果手动输入地区如“安徽省/芜湖市/弋江区/瀂港街道”匹配到对应code值等逻辑,否则清空
const parts = dataForm.value.PCASName.split("/")
if ( parts.length === 4 || dataForm.value.isSpecial ) {
const matchedCodes = findCodesByNames(pcasCodeList, parts)
if ( matchedCodes ) {
dataForm.value.province = matchedCodes[0]
dataForm.value.city = matchedCodes[1]
dataForm.value.area = matchedCodes[2]
dataForm.value.street = matchedCodes[3]
dataForm.value.provinceName = parts[0]
dataForm.value.cityName = parts[1]
dataForm.value.areaName = parts[2]
dataForm.value.streetName = parts[3]
dataForm.value.citesList = findChildrenByCode(pcasCodeList, matchedCodes[0])
dataForm.value.areaList = findChildrenByCode(pcasCodeList, matchedCodes[1])
dataForm.value.streetsList = findChildrenByCode(pcasCodeList, matchedCodes[2])
console.log(matchedCodes) // 输出找到的 code 数组
} else {
setTimeout(() => {
dataForm.value.PCASName = ""
resetSelections([ "province", "city", "area", "street" ])
activeName.value = "first"
}, 1000)
}
}
})
//重置选择
const resetSelections = (clearLevels) => {
// 根据传入的层级清除选项
if ( clearLevels.includes("province") ) {
dataForm.value.province = ""
dataForm.value.provinceName = "请选择"
}
if ( clearLevels.includes("city") ) {
dataForm.value.city = ""
dataForm.value.cityName = "请选择"
dataForm.value.areaList = []
}
if ( clearLevels.includes("area") ) {
dataForm.value.areaName = "请选择"
dataForm.value.area = ""
dataForm.value.streetsList = []
}
if ( clearLevels.includes("street") ) {
dataForm.value.streetNameName = "请选择"
dataForm.value.street = ""
}
}
// 更新省市区名称
const updatePCASName = (provinceName = "", cityName = "", areaName = "", streetName = "") => {
const names = [ provinceName, cityName, areaName, streetName ].filter(name => name.trim() !== "")
// 使用“/”连接数组中的名称
return names.join("/")
}
//根据输入框内容匹配对应的code值
function findCodesByNames(data, names, index = 0, codes = []) {
if ( index < names.length ) {
// 根据当前索引的名称查找数据
const found = data.find(item => item.name === names[index])
if ( found ) {
// 如果找到了匹配项,加入 code,并继续递归搜索下一级
codes[index] = found.code
// 如果还有更深级别的名称,则继续递归,否则直接返回 codes
return found.children && index + 1 < names.length ?
findCodesByNames(found.children, names, index + 1, codes) : codes
} else {
// 如果未找到匹配项,证明省市区乡镇匹配错误,返回 false
return false
}
}
// 如果所有省市区乡镇都已成功匹配对应的code,返回 codes
return codes
}
//根据某个code值寻找对应的子集地区数组
function findChildrenByCode(data, targetCode) {
for ( const item of data ) {
if ( item.code === targetCode ) {
return item.children || []
}
if ( item.children ) {
const result = findChildrenByCode(item.children, targetCode)
if ( result ) return result
}
}
return null
}
</script>
<style lang="scss" scoped>
.addressItem{
display: flex;
font-size: 14px;
margin-bottom: 4px;
.left{
min-width: 40px;
color: #ee675b;
margin-right: 16px;
}
.right{
display: flex;
flex-wrap: wrap;
.provinceItem{
margin-right: 18px;
margin-bottom: 10px;
&:hover{
cursor: pointer;
}
}
}
}
.cityContent{
display: flex;
flex-wrap: wrap;
font-size: 14px;
.cityItem{
margin-right: 18px;
margin-bottom: 10px;
cursor: pointer;
}
}
.active{
color: #1166fe !important;
}
</style>
这里我觉得有点冗余的是输入框输入地址和选择省市区乡镇的的联动效果,毕竟大部分人能选的话不会手输,如果不用的话直接禁用输入框就行,省下很多逻辑处理。
现在这这个组件刚写完
肯定涉及到父组件值的传入和子组件的值传出
以后再更新...