Uniapp实现省市区地址选择器组件开发实战教程
书接上篇,我们今天继续来学习一下 UniApp 如何实现一个省市区联动地址选择器组件,附带了完整的设计思路和源码示例,以及常见避坑指南和开发实践。
此系列文章将带领你从移动端跨平台开发入门到精通,如果你也喜欢关注APP、小程序、公众号、H5等等应用的开发,可以持续关注后续更新,避免错过宝贵的知识分享。
致开发者的忠告: AI编程盛行的今天,我们并不是不需要学习技术,而是更应该专研技术,拥有把控全局的架构设计思维才能在AI盛行的未来有立足之地。
言归正传,咱们今天继续来干一件每个项目都躲不掉、但很多新手都写不利索的事(省市区联动选择组件):从“写死”到“封神”——手把手教你造一个能上线的地址选择器。
你可能会搜到一堆现成组件,什么uview的、vant的、zx-area-picker的,装个包就能用。但问题是:
-
面试问你“联动怎么实现的”,你说“我装了个包”?
-
项目要定制显示模式,插件不支持,你改源码改到怀疑人生?
-
用户选了“北京市”,市列表出来两个“北京市”,区县对不上,你都不知道为啥?
今天咱就自己造一个轮子。 从最新数据、到组件设计、到坑点排查,全流程走一遍。而且样式全用 UnoCSS 原子类,不用写一行烦人的<style>,改起来巨爽!
一、地基要稳:最新行政区划数据从哪来?
血泪教训:千万别用五年前百度搜的“省市区.json”,那里面的“莱芜市”还在,“亳州市”可能写成“毫州市”。
我推荐两个靠谱来源:
-
chinese_regions NPM包——民政部官方数据源,2026年1月30日刚更新
-
GB/T 2260国家标准——六位编码,前两位省、中间两位市、后两位区
咱今天为了教学方便,直接把最新JSON数据内嵌到代码里。这样你不用搭后端、不用配网络请求,复制就能跑。
数据格式:省、市、区三个平铺数组,通过p_code、c_code关联。平铺比嵌套更灵活,好筛选。
关键过滤:
-
省份(
province):不要过滤00结尾,因为所有省份code都以00结尾(如110000),它们是有效项。 -
城市(
city):过滤掉code以00结尾的条目(汇总码,如110100是“市辖区”,一般不应作为市级选项)。 -
区县(
county):过滤掉code以00结尾的条目(汇总码)。
(完整数据见本文最后附录)
二、组件设计:要支持哪些场景?
一个合格的地址组件,至少得满足这几种场景:
场景A:三级联显(传统模式)省、市、区三个下拉框并排,选省筛市,选市筛区。适合PC后台、表单页。
场景B:仅显示已选区(紧凑模式)用户选完省市区,界面上只显示“广东省-深圳市-南山区”这一行文字,点击弹窗修改。适合移动端、个人中心页。
场景C:字段单独绑定表单里省是省、市是市、区是区,三个字段分别存数据库。组件不渲染UI,只负责逻辑联动。
所以我们的组件要支持:
<!-- 用法1:三级下拉联动(默认) --><region-pickerv-model="address"/><!-- 用法2:紧凑模式,点击弹窗选择 --><region-pickerv-model="address"mode="compact"/><!-- 用法3:纯逻辑联动,绑定省市区编码 --><region-picker:province.sync="form.provinceCode":city.sync="form.cityCode":district.sync="form.districtCode"/>
三、核心逻辑:联动是怎么实现的?
很多小白写联动,上来就在模板里写三个<picker>,然后监听省份change去筛城市,监听城市change去筛区。然后就开始怀疑人生——为什么选了“北京市”,城市列表有两个“北京市”?为什么选了“市辖区”,区县列表几百个?
根源在于:你没有理解数据关联的本质。
🔑 关键1:直辖市的特殊处理
北京市、上海市、天津市、重庆市——它们的“省”和“市”是同名同级的。比如“北京市”作为省,它下面只有一个市(编码110100),名字也叫“北京市”。如果你不加处理,用户选了省“北京市”,市列表应该只有这一个市,而不是把全国叫“北京市”的都列出来。
解决方案:筛选城市时,用p_code(省级编码)匹配,千万不要用p_name。
🔑 关键2:汇总码要过滤
GB/T 2260标准里,以00结尾的是汇总码(如110000代表北京市整体,110100代表市辖区整体)。这些不能作为具体地址——你见过谁家收货地址写“北京市市辖区”的?
解决方案:初始化城市列表和区县列表时,过滤掉所有code以00结尾的条目。
🔑 关键3:联动是“递推”的
正确的逻辑是:
-
省份变化 → 重置城市、区县 → 根据省份编码筛选城市列表
-
城市变化 → 重置区县 → 根据城市编码筛选区县列表
-
区县变化 → 触发change事件,返回完整地址对象
不能反过来,不能在区县变化时去改省份——这是死循环。
四、开干!组件代码逐行解析
第一步:准备数据(address-data.js)
// 民政部2026年1月最新数据·精简教学版// 完整版请通过 `npm install chinese_regions` 获取exportconstregionData= {province: [ { code: 110000, name: '北京市' }, { code: 120000, name: '天津市' }, { code: 130000, name: '河北省' }, { code: 310000, name: '上海市' }, { code: 440000, name: '广东省' }, { code: 510000, name: '四川省' },// ... 共34个省级单位(完整版) ],city: [// 直辖市(省、市同名) { p_code: 110000, p_name: '北京市', code: 110100, name: '北京市' }, { p_code: 120000, p_name: '天津市', code: 120100, name: '天津市' }, { p_code: 310000, p_name: '上海市', code: 310100, name: '上海市' },// 河北省 { p_code: 130000, p_name: '河北省', code: 130100, name: '石家庄市' }, { p_code: 130000, p_name: '河北省', code: 130200, name: '唐山市' },// 广东省 { p_code: 440000, p_name: '广东省', code: 440100, name: '广州市' }, { p_code: 440000, p_name: '广东省', code: 440300, name: '深圳市' },// 四川省 { p_code: 510000, p_name: '四川省', code: 510100, name: '成都市' },// ... 完整版有339个地级市 ],county: [// 北京市辖区 { p_code: 110000, p_name: '北京市', c_code: 110100, c_name: '北京市', code: 110101, name: '东城区' }, { p_code: 110000, p_name: '北京市', c_code: 110100, c_name: '北京市', code: 110102, name: '西城区' },// 广州市辖区 { p_code: 440000, p_name: '广东省', c_code: 440100, c_name: '广州市', code: 440103, name: '荔湾区' }, { p_code: 440000, p_name: '广东省', c_code: 440100, c_name: '广州市', code: 440104, name: '越秀区' },// 深圳市辖区 { p_code: 440000, p_name: '广东省', c_code: 440300, c_name: '深圳市', code: 440303, name: '罗湖区' }, { p_code: 440000, p_name: '广东省', c_code: 440300, c_name: '深圳市', code: 440304, name: '福田区' },// ... 完整版有2846个区县,可以自行下载 ]}
第二步:组件完整代码(region-picker.vue)
<template><viewclass="region-picker"><!-- 模式1:三级下拉联动(cascade) --><viewv-if="mode === 'cascade'"class="flex items-center gap-20rpx flex-wrap"><!-- 省选择器 --><pickermode="selector":range="provinceList"range-key="name":value="provinceIndex"@change="onProvinceChange":disabled="disabled"><viewclass="flex items-center justify-between px-30rpx py-20rpx bg-gray-100 rounded-8rpx min-w-200rpx"><text:class="['text-28rpx', selectedProvince ? 'text-dark' : 'text-gray-400']">{{ selectedProvince?.name || '请选择省' }}</text><viewclass="i-carbon:arrow-down text-16rpx text-gray-500"></view></view></picker><!-- 市选择器 --><pickermode="selector":range="cityList"range-key="name":value="cityIndex"@change="onCityChange":disabled="disabled || !selectedProvince"><viewclass="flex items-center justify-between px-30rpx py-20rpx bg-gray-100 rounded-8rpx min-w-200rpx"><text:class="['text-28rpx', selectedCity ? 'text-dark' : 'text-gray-400']">{{ selectedCity?.name || '请选择市' }}</text><viewclass="i-carbon:arrow-down text-16rpx text-gray-500"></view></view></picker><!-- 区选择器 --><pickermode="selector":range="countyList"range-key="name":value="countyIndex"@change="onCountyChange":disabled="disabled || !selectedCity"><viewclass="flex items-center justify-between px-30rpx py-20rpx bg-gray-100 rounded-8rpx min-w-200rpx"><text:class="['text-28rpx', selectedCounty ? 'text-dark' : 'text-gray-400']">{{ selectedCounty?.name || '请选择区' }}</text><viewclass="i-carbon:arrow-down text-16rpx text-gray-500"></view></view></picker></view><!-- 模式2:紧凑模式(compact)—— 仅显示已选区,点击弹窗 --><viewv-else-if="mode === 'compact'"class="flex items-center justify-between px-30rpx py-24rpx bg-white border border-gray-200 rounded-12rpx"@click="openPopup"><text:class="['text-28rpx', addressText ? 'text-dark' : 'text-gray-400']">{{ addressText || placeholder }}</text><viewclass="i-carbon:chevron-right text-16rpx text-gray-500"></view></view><!-- 模式3:字段绑定模式(field)—— 纯逻辑,不渲染UI --><viewv-else-if="mode === 'field'"class="hidden"></view><!-- 紧凑模式的底部弹窗 --><uni-popupref="popupRef"type="bottom"><viewclass="bg-white rounded-t-16rpx pb-safe-bottom"><viewclass="flex justify-between items-center px-30rpx py-30rpx border-b border-gray-100"><textclass="text-28rpx text-gray-500"@click="closePopup">取消</text><textclass="text-28rpx font-medium">选择地区</text><textclass="text-28rpx text-blue-600"@click="confirmPopup">确定</text></view><viewclass="p-30rpx max-h-600rpx overflow-y-auto"><!-- 弹窗内复用三级联动组件,省市区选择器 --><viewclass="flex flex-col gap-20rpx"><!-- 省选择 --><view@click.stop="showProvincePickerInPopup"><viewclass="flex items-center justify-between p-20rpx bg-gray-50 rounded-8rpx"><text:class="selectedProvince ? 'text-dark' : 'text-gray-400'">{{ selectedProvince?.name || '请选择省' }}</text><viewclass="i-carbon:chevron-down text-16rpx text-gray-500"></view></view></view><!-- 市选择(省选完才可点) --><viewv-if="selectedProvince"@click.stop="showCityPickerInPopup"><viewclass="flex items-center justify-between p-20rpx bg-gray-50 rounded-8rpx"><text:class="selectedCity ? 'text-dark' : 'text-gray-400'">{{ selectedCity?.name || '请选择市' }}</text><viewclass="i-carbon:chevron-down text-16rpx text-gray-500"></view></view></view><!-- 区选择(市选完才可点) --><viewv-if="selectedCity"@click.stop="showCountyPickerInPopup"><viewclass="flex items-center justify-between p-20rpx bg-gray-50 rounded-8rpx"><text:class="selectedCounty ? 'text-dark' : 'text-gray-400'">{{ selectedCounty?.name || '请选择区' }}</text><viewclass="i-carbon:chevron-down text-16rpx text-gray-500"></view></view></view></view></view></view></uni-popup><!-- 内置的临时选择器弹窗(用于紧凑模式下的级联选择) --><uni-popupref="innerPickerPopup"type="center"><viewclass="bg-white w-600rpx rounded-16rpx p-30rpx"><picker-viewv-if="currentPickerType":value="pickerValue"@change="onPickerViewChange"class="w-full h-400rpx"><picker-view-column><viewv-for="item in currentColumnData":key="item.code"class="flex items-center justify-center h-80rpx"><text:class="['text-32rpx', item.code === selectedTempCode ? 'text-blue-600 font-medium' : 'text-dark']">{{ item.name }}</text></view></picker-view-column></picker-view><viewclass="flex justify-end mt-30rpx gap-20rpx"><textclass="px-30rpx py-20rpx text-gray-500"@click="closeInnerPicker">取消</text><textclass="px-30rpx py-20rpx text-blue-600"@click="confirmInnerPicker">确定</text></view></view></uni-popup></view></template><scriptsetup>import { ref, computed, watch, onMounted } from'vue'import { regionData } from'./address-data.js'constprops=defineProps({// 显示模式:cascade-三级下拉,compact-紧凑(点击弹窗),field-只处理字段绑定mode: {type: String,default: 'cascade' },// v-model 绑定完整地址字符串(如"广东省-深圳市-南山区")modelValue: {type: String,default: '' },// 单独绑定省、市、区编码province: [String, Number],city: [String, Number],district: [String, Number],// 占位符(紧凑模式)placeholder: {type: String,default: '请选择地区' },// 是否禁用disabled: {type: Boolean,default: false }})constemit=defineEmits(['update:modelValue','update:province','update:city','update:district','change'])// ---------- 数据列表(已过滤汇总码)----------// 省份列表:保留所有(code以00结尾是正常编码,不过滤)constprovinceList=ref(regionData.province)// 城市列表:根据选中的省份动态计算,并过滤掉code以00结尾的汇总码constcityList=computed(() => {if (!selectedProvince.value) return []returnregionData.city.filter(c=>c.p_code===selectedProvince.value.code&&!String(c.code).endsWith('00') // 过滤汇总码 )})// 区县列表:根据选中的城市动态计算,并过滤掉code以00结尾的汇总码constcountyList=computed(() => {if (!selectedCity.value) return []returnregionData.county.filter(c=>c.c_code===selectedCity.value.code&&!String(c.code).endsWith('00') // 过滤汇总码 )})// ---------- 选中状态 ----------constselectedProvince=ref(null)constselectedCity=ref(null)constselectedCounty=ref(null)// 用于picker显示的索引constprovinceIndex=computed(() => {if (!selectedProvince.value) return-1returnprovinceList.value.findIndex(p=>p.code===selectedProvince.value.code)})constcityIndex=computed(() => {if (!selectedCity.value) return-1returncityList.value.findIndex(c=>c.code===selectedCity.value.code)})constcountyIndex=computed(() => {if (!selectedCounty.value) return-1returncountyList.value.findIndex(c=>c.code===selectedCounty.value.code)})// 完整地址字符串(用于紧凑模式显示)constaddressText=computed(() => {if (selectedProvince.value&&selectedCity.value&&selectedCounty.value) {return`${selectedProvince.value.name}-${selectedCity.value.name}-${selectedCounty.value.name}` }return''})// ---------- 联动事件 ----------constonProvinceChange= (e) => {constindex=e.detail.valueconstprovince=provinceList.value[index]selectedProvince.value=provinceselectedCity.value=nullselectedCounty.value=nullemit('update:province', province.code)}constonCityChange= (e) => {constindex=e.detail.valueconstcity=cityList.value[index]selectedCity.value=cityselectedCounty.value=nullemit('update:city', city.code)}constonCountyChange= (e) => {constindex=e.detail.valueconstcounty=countyList.value[index]selectedCounty.value=countyemit('update:district', county.code)constfullAddress=`${selectedProvince.value.name}-${selectedCity.value.name}-${county.name}`emit('update:modelValue', fullAddress)emit('change', {province: selectedProvince.value,city: selectedCity.value,district: county,fullAddress })}// ---------- 紧凑模式弹窗逻辑 ----------constpopupRef=ref(null)constinnerPickerPopup=ref(null)constcurrentPickerType=ref(null) // 'province', 'city', 'county'constpickerValue=ref([0])constselectedTempCode=ref(null)consttempProvince=ref(null)consttempCity=ref(null)consttempCounty=ref(null)constopenPopup= () => {// 初始化临时选中为当前选中tempProvince.value=selectedProvince.valuetempCity.value=selectedCity.valuetempCounty.value=selectedCounty.valuepopupRef.value?.open()}constclosePopup= () => {popupRef.value?.close()}constconfirmPopup= () => {// 将临时选中同步到正式选中selectedProvince.value=tempProvince.valueselectedCity.value=tempCity.valueselectedCounty.value=tempCounty.valueif (selectedProvince.value) emit('update:province', selectedProvince.value.code)if (selectedCity.value) emit('update:city', selectedCity.value.code)if (selectedCounty.value) {emit('update:district', selectedCounty.value.code)constfullAddress=`${selectedProvince.value.name}-${selectedCity.value.name}-${selectedCounty.value.name}`emit('update:modelValue', fullAddress)emit('change', {province: selectedProvince.value,city: selectedCity.value,district: selectedCounty.value,fullAddress }) }closePopup()}// 打开内嵌选择器(省/市/区)constshowProvincePickerInPopup= () => {currentPickerType.value='province'constlist=provinceList.valueconstindex=tempProvince.value?list.findIndex(p=>p.code===tempProvince.value.code) : 0pickerValue.value= [index>=0?index : 0]selectedTempCode.value=list[pickerValue.value[0]]?.codeinnerPickerPopup.value?.open()}constshowCityPickerInPopup= () => {if (!tempProvince.value) returncurrentPickerType.value='city'constlist=cityListForTemp.valueconstindex=tempCity.value?list.findIndex(c=>c.code===tempCity.value.code) : 0pickerValue.value= [index>=0?index : 0]selectedTempCode.value=list[pickerValue.value[0]]?.codeinnerPickerPopup.value?.open()}constshowCountyPickerInPopup= () => {if (!tempCity.value) returncurrentPickerType.value='county'constlist=countyListForTemp.valueconstindex=tempCounty.value?list.findIndex(c=>c.code===tempCounty.value.code) : 0pickerValue.value= [index>=0?index : 0]selectedTempCode.value=list[pickerValue.value[0]]?.codeinnerPickerPopup.value?.open()}// 临时列表(基于tempProvince/tempCity)constcityListForTemp=computed(() => {if (!tempProvince.value) return []returnregionData.city.filter(c=>c.p_code===tempProvince.value.code&&!String(c.code).endsWith('00') )})constcountyListForTemp=computed(() => {if (!tempCity.value) return []returnregionData.county.filter(c=>c.c_code===tempCity.value.code&&!String(c.code).endsWith('00') )})constcurrentColumnData=computed(() => {if (currentPickerType.value==='province') returnprovinceList.valueif (currentPickerType.value==='city') returncityListForTemp.valueif (currentPickerType.value==='county') returncountyListForTemp.valuereturn []})constonPickerViewChange= (e) => {constval=e.detail.value[0]pickerValue.value= [val]selectedTempCode.value=currentColumnData.value[val]?.code}constconfirmInnerPicker= () => {constselectedItem=currentColumnData.value.find(item=>item.code===selectedTempCode.value)if (currentPickerType.value==='province') {tempProvince.value=selectedItemtempCity.value=nulltempCounty.value=null } elseif (currentPickerType.value==='city') {tempCity.value=selectedItemtempCounty.value=null } elseif (currentPickerType.value==='county') {tempCounty.value=selectedItem }innerPickerPopup.value?.close()currentPickerType.value=null}constcloseInnerPicker= () => {innerPickerPopup.value?.close()currentPickerType.value=null}// ---------- 初始化默认值(从props回显)----------letisInitializing=false// 防止初始化时重复触发更新constinitFromProps= () => {isInitializing=true// 优先使用单独的省市区编码if (props.province) {constprovince=provinceList.value.find(p=>p.code==props.province)if (province) selectedProvince.value=province }if (props.city&&selectedProvince.value) {constcity=regionData.city.find(c=>c.code==props.city&&c.p_code===selectedProvince.value.code)if (city&&!String(city.code).endsWith('00')) selectedCity.value=city }if (props.district&&selectedCity.value) {constcounty=regionData.county.find(c=>c.code==props.district&&c.c_code===selectedCity.value.code)if (county&&!String(county.code).endsWith('00')) selectedCounty.value=county }// 如果没传单独编码,尝试从modelValue解析(格式:省-市-区)if (!props.province&&props.modelValue) {constparts=props.modelValue.split('-')if (parts.length===3) {constprovinceName=parts[0], cityName=parts[1], countyName=parts[2]constprovince=provinceList.value.find(p=>p.name===provinceName)if (province) {selectedProvince.value=provinceconstcity=regionData.city.find(c=>c.name===cityName&&c.p_code===province.code&&!String(c.code).endsWith('00'))if (city) {selectedCity.value=cityconstcounty=regionData.county.find(c=>c.name===countyName&&c.c_code===city.code&&!String(c.code).endsWith('00'))if (county) selectedCounty.value=county } } } }isInitializing=false}watch(() =>props.province, initFromProps)watch(() =>props.city, initFromProps)watch(() =>props.district, initFromProps)watch(() =>props.modelValue, initFromProps)onMounted(() => {initFromProps()})</script>
五、使用示例:五种场景全覆盖(UnoCSS 友好)
场景1:基础三级联动(v-model绑定完整地址)
<template><viewclass="p-30rpx"><viewclass="mb-20rpx text-32rpx font-medium">收货地址</view><region-pickerv-model="address"/><viewclass="mt-20rpx text-28rpx text-gray-600"> 选中地址:{{ address || '未选择' }}</view></view></template><scriptsetup>import { ref } from'vue'constaddress=ref('')</script>
场景2:紧凑模式(仅显示已选区,点击弹窗)
<region-pickermode="compact"v-model="address"placeholder="点击选择收货地区"/>
应用场景:个人中心页、订单确认页——界面清爽,不占空间。
场景3:字段单独绑定(省、市、区分开存)
<template><viewclass="p-30rpx"><region-pickermode="field":province.sync="form.province":city.sync="form.city":district.sync="form.district"@change="handleChange"/><!-- 自定义UI展示 --><viewclass="flex flex-col gap-20rpx"><viewclass="flex items-center"><textclass="w-150rpx text-gray-500">省份编码</text><text>{{ form.province }}</text></view><viewclass="flex items-center"><textclass="w-150rpx text-gray-500">城市编码</text><text>{{ form.city }}</text></view><viewclass="flex items-center"><textclass="w-150rpx text-gray-500">区县编码</text><text>{{ form.district }}</text></view></view></view></template><scriptsetup>import { reactive } from'vue'constform=reactive({province: '',city: '',district: ''})consthandleChange= (e) => {console.log('地址变化', e)}</script>
场景4:设置默认值(回显)
<region-pickerv-model="address":province="440000"<!-- 广东省编码 --> :city="440100" <!-- 广州市编码 --> :district="440106" <!-- 天河区编码 -->/>
用户进来就直接选中“广东省-广州市-天河区”,不用重新点。
场景5:自定义样式 + 禁用
<region-pickerv-model="address":disabled="true"class="opacity-60"/>
六、血泪坑合集:联动失效、位置错乱终极解决方案
💥 坑1:选了“北京市”,市列表出现两个“北京市”
现象:数据里北京市作为省(110000)有一条,作为市(110100)也有一条,筛选时把省本身也筛出来了。原因:直接用p_name === province.name匹配,省和市同名导致误筛。✅ 解决方案:用p_code匹配,不要用p_name。代码中已全部采用p_code。
💥 坑2:选了“市辖区”,区县列表爆出几百个
现象:市辖区(110100)下面包含了全市所有区县,是对的。但问题是——用户不该选“市辖区”!原因:“市辖区”是以00结尾的汇总码,不是具体的行政区。✅ 解决方案:在初始化城市列表时,过滤掉所有code.endsWith('00')的条目。
cityList.value=regionData.city.filter(c=>!String(c.code).endsWith('00'))
💥 坑3:切换省份时,城市索引越界
现象:之前选了“广东省-深圳市”,然后切到“北京市”。城市列表只剩一个“北京市”,但cityIndex还在指向“深圳市”的位置(比如1),导致picker显示空白。原因:重置城市时没有把selectedCity置为null。✅ 解决方案:在onProvinceChange里必须清空selectedCity和selectedCounty。
selectedCity.value=nullselectedCounty.value=null
💥 坑4:v-model绑定后,第一次点击没反应
现象:页面加载完,点picker不弹窗。原因:picker的range是空数组,导致无选项可弹。✅ 解决方案:确保provinceList在onLoad时就有数据。我们的数据是同步加载的,没问题。
💥 坑5:字段绑定模式下,外部改了province,组件不更新
现象:父组件通过:province.sync传了440000,子组件没有切换到广东。原因:没有用watch监听props变化。✅ 解决方案:加上watch(() => props.province, initFromProps)等。
💥 坑6:初始化时错误地过滤了省份
现象:省份列表不全,比如只有几个省。原因:错误地用了filter(p => !String(p.code).endsWith('00')),所有省份code都以00结尾,导致全被过滤。✅ 解决方案:省份不要过滤,只过滤市和区。
七、附录:完整省市区数据获取
由于篇幅限制,我没办法把339个地市、2846个区县的JSON数据全部贴在这里。
推荐通过NPM安装官方数据包:
npm install chinese_regions
然后在项目中使用:
importregionsfrom'chinese_regions'assert { type: 'json' }// regions 结构:{ province: [], city: [], county: [] }
如果你需要直接复制JSON,可以访问民政部官方网站或GitHub上搜索“china-regions”获取最新资源。
八、结语:造轮子是为了不重复造轮子
小老弟,今天带你手写这个组件,不是为了让你以后每个项目都自己写一遍。而是让你在被别人造的轮子坑了的时候,有能力自己修。
下次你再用别人的地址组件,一看报错:
-
你知道可能是没过滤
00结尾的汇总码 -
你知道直辖市的省、市同名但不同code
-
你知道联动失效八成是没清空下级选中状态
这就叫“内功”。
而且现在你还会用 UnoCSS 写样式,再也不需要写那些冗长的<style>块了,一个class="flex items-center p-20rpx"就能搞定所有布局,干净利落!
好了,代码在手上,数据也有了,现在就去你的项目里试试——把你以前那个老掉牙的省市区.json换掉,用上2026年最新数据,跑通联动,再顺手加个compact模式。
遇到问题别硬扛,把报错截图、代码片段扔群里,或者直接来敲我。
加油,未来的全栈大佬!💪如果你也对移动端跨端开发感兴趣,关注我,后续还有更多优质文章分享!

往期相关文章推荐
夜雨聆风
