省市区街道/乡镇四级联动vue3

最近优化了一个省.市.区/县、乡镇/街道的四级联动组件,技术栈是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"

然后调整数据结构,

<template>
	<el-popover ref="popoverRef" :visible="popoverVisible" :width="dataForm.isInputting ? 460 : 300"
				placement="bottom" trigger="click">
		<template #reference>
			<el-input v-model="dataForm.PCASName" placeholder="请选择省市区街道/乡镇" :size="props.inputSize"
					  @blur="inputBlurFn" @click="popoverVisible =true" @focus="inputFocusFn" @input="pcasInputFn"/>
		</template>
		<div v-if="dataForm.isInputting" v-click-outside="contentClick">
			<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 && !dataForm.isSpecial" :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>
		<div v-else class="aimContent">
			<div v-if="dataForm.dimList.length > 0">
				<div v-for="(item, index) in dataForm.dimList" :key="index" class="dimItem" @click="dimItem(item)">
					{{ item.name }}
				</div>
			</div>
			<div v-else>暂无数据</div>
		</div>
	</el-popover>
</template>

<script setup>
import { unref, ref, watchEffect, onMounted } from "vue"
import { ClickOutside as vClickOutside } from "element-plus"
import { cloneDeep } from "lodash-es"
import pcasCode from "@/const/json/pcas-code.json"
import pinyin from "chinese-to-pinyin"

// <AddressGanged ref="AddressGangedRef" :PCASCode="['110000', '110100', '110101']"  :inputSize="'large'" :PCASName="PCASName"></AddressGanged>

const props = defineProps({
	//省市区乡镇名称
	PCASName: {
		type: String,
		default: ""
	},
	//省市区乡镇code
	PCASCode: {
		type: Array,
		default: []
	},
	//输入框大小('large','small')
	inputSize: {
		type: String,
		default: "default"
	},
})

let dataForm = ref({
	citesList: [],	//城市分组
	areaList: [],	//区县分组
	streetsList: [],	//街道乡镇分组
	dimList: [],	//模糊搜索省市区数据
	codeList: [],	//省市区数据code
	province: "",	//省code
	city: "", 	//城市code
	area: "", 	//区县code
	street: "",	//街道乡镇code
	provinceName: "请选择", //省名称
	cityName: "请选择",// 城市名称
	areaName: "请选择", // 区县名称
	streetName: "请选择", //街道名称
	PCASName: "",//省市区街道名称
	isSpecial: false,//是否是特别行政区
	isInputting: true//是否是输入中
})

onMounted(() => {
	if ( props.PCASName ) {
		dataForm.value.PCASName = props.PCASName
	}
	if ( props.PCASCode.length > 0 ) {
		dataForm.value.codeList = props.PCASCode
		const levels = [ "province", "city", "area", "street" ]
		props.PCASCode.forEach((code, index) => {
			const level = levels[index]
			if ( level ) {
				dataForm.value[level] = code
			}
		})
	}
})

//弹出框是否显示
let popoverVisible = ref(false)

//省市区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) => {
	popoverVisible.value = true
	if ( tab.props.name === "first" ) {
		resetSelections([ "province", "city", "area", "street" ])
	} else if ( tab.props.name === "second" ) {
		resetSelections([ "city", "area", "street" ])
		dataForm.value.citesList = pcasCodeList.find(item => item.code === dataForm.value.province)?.children || []
	} else if ( tab.props.name === "three" ) {
		resetSelections([ "area", "street" ])
		const childrenArray = findChildrenByCode(pcasCodeList, dataForm.value.city)
		dataForm.value.areaList = childrenArray || []
	} else if ( tab.props.name === "four" ) {
		resetSelections([ "street" ])
		const childrenArray = findChildrenByCode(pcasCodeList, dataForm.value.area)
		dataForm.value.streetsList = childrenArray || []
	}
}

//点击省
const provinceItemFn = (val) => {
	popoverVisible.value = true
	dataForm.value.provinceName = val.name
	dataForm.value.PCASName = updatePCASName(val.name)
	dataForm.value.codeList = [ val.code ]
	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
	} else {
		activeName.value = "second"
		dataForm.value.isSpecial = false
	}
}

//点击城市
const cityItemFn = (val) => {
	popoverVisible.value = true
	dataForm.value.cityName = val.name
	dataForm.value.PCASName = updatePCASName(dataForm.value.provinceName, val.name)
	dataForm.value.city = val.code
	dataForm.value.codeList = [ dataForm.value.province, val.code ]
	dataForm.value.areaList = val.children || []
	resetSelections([ "area", "street" ])
	if ( dataForm.value.provinceName === "澳门特别行政区" || dataForm.value.provinceName === "香港特别行政区" ) {
		dataForm.value.isSpecial = true
		popoverVisible.value = false
	} else {
		activeName.value = "three"
		dataForm.value.isSpecial = false
	}
}

//点击区县
const areaItemFn = (val) => {
	popoverVisible.value = true
	dataForm.value.areaName = val.name
	dataForm.value.PCASName = updatePCASName(dataForm.value.provinceName, dataForm.value.cityName, val.name)
	dataForm.value.area = val.code
	dataForm.value.codeList = [ dataForm.value.province, dataForm.value.city, val.code ]
	dataForm.value.streetsList = val.children
	resetSelections([ "street" ])
	activeName.value = "four"
}

//点击街道/乡镇
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
	dataForm.value.codeList = [ dataForm.value.province, dataForm.value.city, dataForm.value.area, val.code ]
	popoverVisible.value = false
}

//点击模糊搜索结果
const dimItem = (val) => {
	dataForm.value.PCASName = val.name
	dataForm.value.codeList = val.code.split("/")
}

//省市区选择区域外内容点击事件
const contentClick = () => {
	popoverVisible.value = false
}

//输入框失去焦点事件
const inputBlurFn = ( (event) => {
	if ( !dataForm.value.isInputting ) popoverVisible.value = false
	if ( ( dataForm.value.codeList.length < 1 || dataForm.value.dimList.length < 1 ) && dataForm.value.PCASName ) {
		resetSelections([ "province", "city", "area", "street" ])
		activeName.value = "first"
		dataForm.value.PCASName = ""
	}
	// console.log("输入框失去焦点事件", dataForm.value.codeList)
} )

//输入框获取焦点事件
const inputFocusFn = (val) => {
	if ( val && dataForm.value.PCASName ) {
		dataForm.value.isInputting = false
		dataForm.value.dimList = findAddressPaths(pcasCodeList, dataForm.value.PCASName)
	} else {
		dataForm.value.isInputting = true
	}
}

//输入框input事件
const pcasInputFn = (val) => {
	if ( val && dataForm.value.PCASName ) {
		dataForm.value.codeList = []
		dataForm.value.isInputting = false
		dataForm.value.dimList = findAddressPaths(pcasCodeList, val)
	} else {
		dataForm.value.isInputting = true
		resetSelections([ "province", "city", "area", "street" ])
		activeName.value = "first"
	}
}

//重置选择
const resetSelections = (clearLevels) => {
	// 根据传入的层级清除选项
	if ( clearLevels.includes("province") ) {
		dataForm.value.province = ""
		dataForm.value.provinceName = "请选择"
		dataForm.value.codeList = []
	}
	if ( clearLevels.includes("city") ) {
		dataForm.value.city = ""
		dataForm.value.cityName = "请选择"
		dataForm.value.areaList = []
		dataForm.value.codeList = [ dataForm.value.province ]
	}
	if ( clearLevels.includes("area") ) {
		dataForm.value.areaName = "请选择"
		dataForm.value.area = ""
		dataForm.value.streetsList = []
		dataForm.value.codeList = [ dataForm.value.province, dataForm.value.city ]
	}
	if ( clearLevels.includes("street") ) {
		dataForm.value.codeList = [ dataForm.value.province, dataForm.value.city, dataForm.value.area ]
		dataForm.value.streetName = "请选择"
		dataForm.value.street = ""
	}
}

// 更新省市区名称
const updatePCASName = (provinceName = "", cityName = "", areaName = "", streetName = "") => {
	const names = [ provinceName, cityName, areaName, streetName ].filter(name => name.trim() !== "")
	// 使用“/”连接数组中的名称
	return names.join("/")
}

//根据某个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
}

//省市区模糊搜索算法
function findAddressPaths(data, searchText, limit = 30) {
	let results = []
	let subEntriesToCheck = [] // 存储子条目等待检查的列表
	let normalizedSearchText = searchText.replace(/\//g, "") // 正规化搜索文本,移除斜杠

	function getNormalizedPath(name) {
		return name.replace(/\//g, "") // 移除路径中的斜杠用于搜索匹配
	}

	// 检查并添加符合条件的省级数据
	for ( const province of data ) {
		const normalizedProvinceName = getNormalizedPath(province.name)
		if ( normalizedProvinceName.includes(normalizedSearchText) ) {
			results.push({ name: province.name, code: province.code })
			if ( results.length >= limit ) {
				return results
			}
		}
		// 将省下属的城市添加到待检查列表
		if ( province.children ) {
			for ( const city of province.children ) {
				subEntriesToCheck.push({
					fullName: province.name + "/" + city.name,
					fullCode: province.code + "/" + city.code,
					children: city.children
				})
			}
		}
	}

	// 处理子级条目(城市或区县)
	function processSubEntries(list) {
		for ( const entry of list ) {
			const normalizedFullName = getNormalizedPath(entry.fullName)
			if ( normalizedFullName.includes(normalizedSearchText) ) {
				results.push({ name: entry.fullName, code: entry.fullCode })
				if ( results.length >= limit ) {
					return
				}
			}
			// 将该级下属的区县添加到待检查列表
			if ( entry.children ) {
				for ( const child of entry.children ) {
					subEntriesToCheck.push({
						fullName: entry.fullName + "/" + child.name,
						fullCode: entry.fullCode + "/" + child.code,
						children: child.children
					})
				}
			}
		}
	}

	// 循环处理所有待检查的子条目
	do {
		const currentLevelToCheck = [ ...subEntriesToCheck ]
		subEntriesToCheck = []
		processSubEntries(currentLevelToCheck)
	} while ( results.length < limit && subEntriesToCheck.length > 0 )

	return results
}

defineExpose({
	dataForm
})
</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;
				color: #1166fe;
			}
		}
	}
}

.cityContent{
	display: flex;
	flex-wrap: wrap;
	font-size: 14px;

	.cityItem{
		margin-right: 18px;
		margin-bottom: 10px;
		cursor: pointer;

		&:hover{
			color: #1166fe;
		}
	}
}


.aimContent{
	height: 300px;
	overflow-y: auto;
}

.dimItem{
	margin: 4px 0;
	cursor: pointer;

	&:hover{
		background: #1166fe;
		color: #FFFFFF;
	}
}

.active{
	color: #1166fe !important;
}

</style>

这样就实现了这个页面了

交互逻辑太多

为了避免文章太长

直接上全部代码
<template>
	<el-popover ref="popoverRef" :visible="popoverVisible" :width="dataForm.isInputting ? 460 : 300"
				placement="bottom" trigger="click">
		<template #reference>
			<el-input v-model="dataForm.PCASName" placeholder="请选择省市区街道/乡镇"
					  @blur="inputBlurFn" @click="popoverVisible =true" @focus="inputFocusFn" @input="pcasInputFn"/>
		</template>
		<div v-if="dataForm.isInputting" v-click-outside="contentClick">
			<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 && !dataForm.isSpecial" :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>
		<div v-else class="aimContent">
			<div v-if="dataForm.dimList.length > 0">
				<div v-for="(item, index) in dataForm.dimList" :key="index" class="dimItem" @click="dimItem(item)">
					{{ item.name }}
				</div>
			</div>
			<div v-else>暂无数据</div>
		</div>
	</el-popover>
</template>

<script setup>
import { unref, ref, watchEffect } from "vue"
import { ClickOutside as vClickOutside } from "element-plus"
import { cloneDeep } from "lodash-es"
import pcasCode from "@/const/json/pcas-code.json"
import pinyin from "chinese-to-pinyin"

let dataForm = ref({
	citesList: [],	//城市分组
	areaList: [],	//区县分组
	streetsList: [],	//街道乡镇分组
	dimList: [],	//模糊搜索省市区数据
	codeList: [],	//省市区数据code
	province: "",	//省code
	city: "", 	//城市code
	area: "", 	//区县code
	street: "",	//街道乡镇code
	provinceName: "请选择", //省名称
	cityName: "请选择",// 城市名称
	areaName: "请选择", // 区县名称
	streetName: "请选择", //街道名称
	PCASName: "",//省市区街道名称
	isSpecial: false,//是否是特别行政区
	isInputting: true//是否是输入中
})

//弹出框是否显示
let popoverVisible = ref(false)

//省市区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) => {
	popoverVisible.value = true
	if ( tab.props.name === "first" ) {
		resetSelections([ "province", "city", "area", "street" ])
	} else if ( tab.props.name === "second" ) {
		resetSelections([ "city", "area", "street" ])
		dataForm.value.citesList = pcasCodeList.find(item => item.code === dataForm.value.province)?.children || []
	} else if ( tab.props.name === "three" ) {
		resetSelections([ "area", "street" ])
		const childrenArray = findChildrenByCode(pcasCodeList, dataForm.value.city)
		dataForm.value.areaList = childrenArray || []
	} else if ( tab.props.name === "four" ) {
		resetSelections([ "street" ])
		const childrenArray = findChildrenByCode(pcasCodeList, dataForm.value.area)
		dataForm.value.streetsList = childrenArray || []
	}
}

//点击省
const provinceItemFn = (val) => {
	popoverVisible.value = true
	dataForm.value.provinceName = val.name
	dataForm.value.PCASName = updatePCASName(val.name)
	dataForm.value.codeList = [ val.code ]
	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
	} else {
		activeName.value = "second"
		dataForm.value.isSpecial = false
	}
}

//点击城市
const cityItemFn = (val) => {
	popoverVisible.value = true
	dataForm.value.cityName = val.name
	dataForm.value.PCASName = updatePCASName(dataForm.value.provinceName, val.name)
	dataForm.value.city = val.code
	dataForm.value.codeList = [ dataForm.value.province, val.code ]
	dataForm.value.areaList = val.children || []
	resetSelections([ "area", "street" ])
	if ( dataForm.value.provinceName === "澳门特别行政区" || dataForm.value.provinceName === "香港特别行政区" ) {
		dataForm.value.isSpecial = true
		popoverVisible.value = false
	} else {
		activeName.value = "three"
		dataForm.value.isSpecial = false
	}
}

//点击区县
const areaItemFn = (val) => {
	popoverVisible.value = true
	dataForm.value.areaName = val.name
	dataForm.value.PCASName = updatePCASName(dataForm.value.provinceName, dataForm.value.cityName, val.name)
	dataForm.value.area = val.code
	dataForm.value.codeList = [ dataForm.value.province, dataForm.value.city, val.code ]
	dataForm.value.streetsList = val.children
	resetSelections([ "street" ])
	activeName.value = "four"
}

//点击街道/乡镇
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
	dataForm.value.codeList = [ dataForm.value.province, dataForm.value.city, dataForm.value.area, val.code ]
	popoverVisible.value = false
}

//点击模糊搜索结果
const dimItem = (val) => {
	dataForm.value.PCASName = val.name
	dataForm.value.codeList = val.code.split('/')
}

//省市区选择区域外内容点击事件
const contentClick = () => {
	// if ( dataForm.value.codeList.length < 3 ) {
	// 	resetSelections([ "province", "city", "area", "street" ])
	// 	activeName.value = "first"
	// 	dataForm.value.PCASName = ""
	// }
	popoverVisible.value = false
}

//输入框失去焦点事件
const inputBlurFn = ( (event) => {
	if ( dataForm.value.codeList.length < 1 || dataForm.value.dimList.length < 1 ) {
		resetSelections([ "province", "city", "area", "street" ])
		activeName.value = "first"
		dataForm.value.PCASName = ""
	}
	popoverVisible.value = false
	// console.log("输入框失去焦点事件", dataForm.value.codeList)
} )

//输入框获取焦点事件
const inputFocusFn = (val) => {
	if ( val && dataForm.value.PCASName ) {
		dataForm.value.isInputting = false
		dataForm.value.dimList = findAddressPaths(pcasCodeList, dataForm.value.PCASName)
	} else {
		dataForm.value.isInputting = true
	}
}

//输入框input事件
const pcasInputFn = (val) => {
	if ( val && dataForm.value.PCASName ) {
		dataForm.value.codeList = []
		dataForm.value.isInputting = false
		dataForm.value.dimList = findAddressPaths(pcasCodeList, val)
	} else {
		dataForm.value.isInputting = true
		resetSelections([ "province", "city", "area", "street" ])
		activeName.value = "first"
	}
}

//重置选择
const resetSelections = (clearLevels) => {
	// 根据传入的层级清除选项
	if ( clearLevels.includes("province") ) {
		dataForm.value.province = ""
		dataForm.value.provinceName = "请选择"
		dataForm.value.codeList = []
	}
	if ( clearLevels.includes("city") ) {
		dataForm.value.city = ""
		dataForm.value.cityName = "请选择"
		dataForm.value.areaList = []
		dataForm.value.codeList = [ dataForm.value.province ]
	}
	if ( clearLevels.includes("area") ) {
		dataForm.value.areaName = "请选择"
		dataForm.value.area = ""
		dataForm.value.streetsList = []
		dataForm.value.codeList = [ dataForm.value.province, dataForm.value.city ]
	}
	if ( clearLevels.includes("street") ) {
		dataForm.value.codeList = [ dataForm.value.province, dataForm.value.city, dataForm.value.area ]
		dataForm.value.streetName = "请选择"
		dataForm.value.street = ""
	}
}

// 更新省市区名称
const updatePCASName = (provinceName = "", cityName = "", areaName = "", streetName = "") => {
	const names = [ provinceName, cityName, areaName, streetName ].filter(name => name.trim() !== "")
	// 使用“/”连接数组中的名称
	return names.join("/")
}

//根据某个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
}

//省市区模糊搜索算法
function findAddressPaths(data, searchText, limit = 30) {
	let results = [];
	let subEntriesToCheck = []; // 存储子条目等待检查的列表
	let normalizedSearchText = searchText.replace(/\//g, ''); // 正规化搜索文本,移除斜杠

	function getNormalizedPath(name) {
		return name.replace(/\//g, ''); // 移除路径中的斜杠用于搜索匹配
	}

	// 检查并添加符合条件的省级数据
	for (const province of data) {
		const normalizedProvinceName = getNormalizedPath(province.name);
		if (normalizedProvinceName.includes(normalizedSearchText)) {
			results.push({ name: province.name, code: province.code });
			if (results.length >= limit) {
				return results;
			}
		}
		// 将省下属的城市添加到待检查列表
		if (province.children) {
			for (const city of province.children) {
				subEntriesToCheck.push({
					fullName: province.name + "/" + city.name,
					fullCode: province.code + "/" + city.code,
					children: city.children
				});
			}
		}
	}

	// 处理子级条目(城市或区县)
	function processSubEntries(list) {
		for (const entry of list) {
			const normalizedFullName = getNormalizedPath(entry.fullName);
			if (normalizedFullName.includes(normalizedSearchText)) {
				results.push({ name: entry.fullName, code: entry.fullCode });
				if (results.length >= limit) {
					return;
				}
			}
			// 将该级下属的区县添加到待检查列表
			if (entry.children) {
				for (const child of entry.children) {
					subEntriesToCheck.push({
						fullName: entry.fullName + "/" + child.name,
						fullCode: entry.fullCode + "/" + child.code,
						children: child.children
					});
				}
			}
		}
	}

	// 循环处理所有待检查的子条目
	do {
		const currentLevelToCheck = [...subEntriesToCheck];
		subEntriesToCheck = [];
		processSubEntries(currentLevelToCheck);
	} while (results.length < limit && subEntriesToCheck.length > 0);

	return results;
}

defineExpose({
	dataForm
})
</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;
				color: #1166fe;
			}
		}
	}
}

.cityContent{
	display: flex;
	flex-wrap: wrap;
	font-size: 14px;

	.cityItem{
		margin-right: 18px;
		margin-bottom: 10px;
		cursor: pointer;

		&:hover{
			color: #1166fe;
		}
	}
}


.aimContent{
	height: 300px;
	overflow-y: auto;
}

.dimItem{
	margin: 4px 0;
	cursor: pointer;

	&:hover{
		background: #1166fe;
		color: #FFFFFF;
	}
}

.active{
	color: #1166fe !important;
}

</style>

这里我觉得有点冗余的是输入框输入地址和选择省市区乡镇的的联动效果,毕竟大部分人能选的话不会手输,如果不用的话直接禁用输入框就行,省下很多逻辑处理。

现在这这个组件刚写完

肯定涉及到父组件值的传入和子组件的值传出

以后再更新...

  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值