使用 `china-region` 实现 Vue3 + TS 的省市区三级联动选择器

发布于:2025-04-08 ⋅ 阅读:(34) ⋅ 点赞:(0)

在日常前端开发中,地址选择器是非常常见的功能,尤其是包含“省、市、区”三级联动的组件。借助 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 等状态管理库联动其他表单项。

如果你觉得这篇文章对你有帮助,欢迎点赞收藏!有问题也欢迎留言讨论!