在日常前端开发中,地址选择器是非常常见的功能,尤其是包含“省、市、区”三级联动的组件。借助 china-region 这个库,我们可以非常轻松地实现这一需求,并与 Vue3 组件系统自然集成。本文将带你一步步构建一个带有回显功能的省市区选择器。
技术栈
- Vue 3 +
<script setup>
- TypeScript
- china-region(省市区数据)
- 自定义
Select
组件(来自 shadcn-vue,可以根据需要选用)
需求描述
一个三级联动的选择器,用户选择省份后自动加载对应城市,选择城市后再加载对应的区县。更改省级或市级的选择时,其下的选择器也能对应的清空值并且获取新的选取列表。同时支持通过 props
设置默认选中项。
依赖安装
npm install china-region
编写组件
1. 基础数据和 Props 接收
const props = defineProps<{
province?: string;
city?: string;
district?: string;
}>();
const provinces = ref(getProvinces());
const selectedProvince = ref(props.province || "");
const selectedCity = ref(props.city || "");
const selectedDistrict = ref(props.district || "");
支持外部传入 默认选中省市区
,非常适合做表单回显。
2. 响应式联动逻辑
监听省份,更新城市
watch(selectedProvince, (newProvince) => {
selectedCity.value = "";
selectedDistrict.value = "";
if (newProvince) {
// 因为输入框的value被绑定为了地区名称,而getPrefectures函数只接收行政区号,所以这里需要先根据省份名称获取行政区号
const provinceCode = getCodeByProvinceName(newProvince);
cities.value = getPrefectures(provinceCode);
if (props.city) {
const targetCity = cities.value.find((city) => city.name === props.city);
if (targetCity) selectedCity.value = props.city;
}
} else {
cities.value = [];
}
}, { immediate: true });
监听城市,更新区县
watch(selectedCity, (newCity) => {
selectedDistrict.value = "";
if (newCity) {
// china-region没有提供根据城市名获取行政区号的函数,所以只能在cities中查找到与newcity同名的城市的区号
const cityCode = cities.value.find((item) => item.name === newCity)?.code;
districts.value = getCounties(cityCode);
if (props.district) {
const targetDistrict = districts.value.find(
(district) => district.name === props.district,
);
if (targetDistrict) selectedDistrict.value = props.district;
}
} else {
districts.value = [];
}
}, { immediate: true });
3. 输出选中地址
const selectedRegion = computed(() => {
// 可以根据需要返回相对应的数据格式,这里的数据格式为字符串,示例:中国,浙江省,杭州市,西湖区
return `中国,${selectedProvince.value},${selectedCity.value},${selectedDistrict.value}`;
});
defineExpose({ selectedRegion });
在父组件可以通过 ref
引用组件,拿到完整的地址信息。
模板部分(UI)
<template>
<div>
<div class="text-sm">地区选择</div>
<div class="flex gap-4">
<!-- 省 -->
<Select v-model="selectedProvince">
<SelectTrigger class="w-[100px]">
<SelectValue placeholder="请选择省份" />
</SelectTrigger>
<SelectContent class="h-60">
<!-- china-region返回的数据结构也支持使用行政区号,这里使用了地区名称-->
<SelectItem
v-for="item in provinces"
:key="item.name"
:value="item.name"
>
{{ item.name }}
</SelectItem>
</SelectContent>
</Select>
<!-- 市 -->
<Select v-model="selectedCity" :disabled="!selectedProvince || !cities.length">
<SelectTrigger class="w-[100px]">
<SelectValue placeholder="请选择城市" />
</SelectTrigger>
<SelectContent class="h-60">
<SelectItem
v-for="item in cities"
:key="item.name"
:value="item.name"
>
{{ item.name }}
</SelectItem>
</SelectContent>
</Select>
<!-- 区 -->
<Select v-model="selectedDistrict" :disabled="!selectedCity">
<SelectTrigger class="w-[100px]">
<SelectValue placeholder="请选择区县" />
</SelectTrigger>
<SelectContent class="max-h-60">
<SelectItem
v-for="item in districts"
:key="item.name"
:value="item.name"
>
{{ item.name }}
</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</template>
这里的样式简单的使用了Tailwind Css,也可以根据需要加入自定义的样式
完整的组件代码
<script setup lang="ts">
import { ref, computed, watch } from "vue";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
getProvinces,
getPrefectures,
getCounties,
getCodeByProvinceName,
} from "china-region";
// 1. 接收父组件的 `props`
const props = defineProps<{
province?: string;
city?: string;
district?: string;
}>();
// 2. 省份数据
const provinces = ref(getProvinces());
const selectedProvince = ref(props.province || "");
const selectedCity = ref(props.city || "");
const selectedDistrict = ref(props.district || "");
const cities = ref([]);
const districts = ref([]);
// 3. 监听 `selectedProvince`,更新 `cities` 并恢复 `props.city`
watch(
selectedProvince,
(newProvince) => {
selectedCity.value = "";
selectedDistrict.value = "";
if (newProvince) {
const provinceCode = getCodeByProvinceName(newProvince);
cities.value = getPrefectures(provinceCode);
// 如果 `props.city` 存在,并且 `cities` 里有这个城市,则选中
if (props.city) {
const targetCity = cities.value.find(
(city) => city.name === props.city,
);
if (targetCity) {
selectedCity.value = props.city;
}
}
} else {
cities.value = [];
}
},
{ immediate: true },
);
// 4. 监听 `selectedCity`,更新 `districts` 并恢复 `props.district`
watch(
selectedCity,
(newCity) => {
selectedDistrict.value = "";
if (newCity) {
const cityCode = cities.value.find((item) => item.name === newCity).code;
districts.value = getCounties(cityCode);
// 如果 `props.district` 存在,并且 `districts` 里有这个区县,则选中
if (props.district) {
const targetDistrict = districts.value.find(
(district) => district.name === props.district,
);
if (targetDistrict) {
selectedDistrict.value = props.district;
}
}
} else {
districts.value = [];
}
},
{ immediate: true },
);
// 5. 监听 `props` 变化,确保 `props.city` 和 `props.district` 正确赋值
watch(
() => props.province,
(newVal) => {
if (newVal) {
selectedProvince.value = newVal;
}
},
);
watch(
() => props.city,
(newVal) => {
if (newVal && cities.value.some((city) => city.name === newVal)) {
selectedCity.value = newVal;
}
},
);
watch(
() => props.district,
(newVal) => {
if (
newVal &&
districts.value.some((district) => district.name === newVal)
) {
selectedDistrict.value = newVal;
}
},
);
// 6. 计算选中的地区
const selectedRegion = computed(() => {
return `中国,${selectedProvince.value},${selectedCity.value},${selectedDistrict.value}`;
});
// 7. 让父组件可以访问选中的数据
defineExpose({
selectedRegion,
});
</script>
<template>
<div>
<div class="text-sm">工作地区</div>
<!-- 省份选择 -->
<div class="flex gap-4">
<Select v-model="selectedProvince">
<SelectTrigger class="w-[100px]">
<SelectValue placeholder="请选择省份" />
</SelectTrigger>
<SelectContent class="h-60">
<SelectItem
v-for="item in provinces"
:key="item.name"
:value="item.name"
>
{{ item.name }}
</SelectItem>
</SelectContent>
</Select>
<!-- 城市选择 -->
<Select
v-model="selectedCity"
:disabled="!selectedProvince || !cities.length"
>
<SelectTrigger class="w-[100px]">
<SelectValue placeholder="请选择城市" />
</SelectTrigger>
<SelectContent class="h-60">
<SelectItem
v-for="item in cities"
:key="item.name"
:value="item.name"
>
{{ item.name }}
</SelectItem>
</SelectContent>
</Select>
<!-- 区县选择 -->
<Select v-model="selectedDistrict" :disabled="!selectedCity">
<SelectTrigger class="w-[100px]">
<SelectValue placeholder="请选择区县" />
</SelectTrigger>
<SelectContent class="max-h-60">
<SelectItem
v-for="item in districts"
:key="item.name"
:value="item.name"
>
{{ item.name }}
</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</template>
总结
本组件的优势:
- 使用
china-region
获取权威行政区域数据; - 响应式联动逻辑清晰;
- 支持外部
props
赋值,方便做表单回显; - 使用
defineExpose
暴露接口,增强复用性。
这个组件可以作为表单组件的基础模块,配合表单校验、提交逻辑后非常实用。
这个组件只实现了最简单的三级地区选择以及回显,下一步可以尝试:
- 添加“清空选择”按钮;
- 使用 pinia 等状态管理库联动其他表单项。
如果你觉得这篇文章对你有帮助,欢迎点赞收藏!有问题也欢迎留言讨论!