简介:微信小程序省市县三级联动示例代码包,面向需要实现地址选择功能的开发者,通过省、市、县三个picker逐级联动,解决行政区划数据筛选与状态同步的核心问题。压缩包共5个文件,包括2个JavaScript逻辑文件(分别负责数据初始化与事件响应、层级筛选),1个WXML页面结构(含三个选择器的range绑定)、1个WXSS样式及1个JSON配置,整体仅27KB,结构紧凑便于快速定位。资源提供了完整的全国省市区JSON数据样例,并演示如何利用parentId字段维护层级关系,核心方法会根据当前省份选项过滤出对应城市列表、再根据城市过滤区县列表,同时实现默认选中首个省份并自动触发下级初始化的体验优化写法,清晰展现数据流与picker组件的配合,可直接复用或二次扩展。已有1461人学习下载,适合微信小程序初学者学习三级联动设计。
1. 项目解析:小程序里最常见的省市县三级联动,这套源码值得跑一遍
做小程序开发的人几乎都绕不开一个场景:填写收货地址时,用户需要在省、市、县三个维度上连续选择。很多开发者第一反应是“这不就是一个 picker-view 套三层吗”,真正动手才发现,数据结构的组织、联动后的索引重置、默认值的回填,每个环节都能卡住人。这套省市县三级联动源码包,解决的就是这件事:一份按行政区划整理的 JSON 数据,一个封装好的 picker-view 组件,外加一个可直接运行的示例页面。你不需要搭后端,也不需要申请地图或定位相关的权限,拿到手就能编译、预览、改样式,适合所有想把地址选择功能快速集成进自己小程序项目里的开发者。
2. 数据设计与技术选型:一份 JSON 是怎么撑起三级联动的
三级联动的核心不只是“三个滚轮”,而是数据怎么组织、怎么加载、怎么在运行时被索引。这一节先讲清楚数据源结构和选型逻辑,因为后面所有代码都是在跟这份数据打交道。
2.1 数据源结构:树形 JSON 是联动的基础
项目里携带的省市区数据不是三个平铺数组,而是一棵嵌套的树。顶层是省,每个省下面挂城市,每个城市下面挂区县。典型结构如下:
[ { "code": "110000", "name": "示例省甲", "children": [ { "code": "110100", "name": "示例市A", "children": [ { "code": "110101", "name": "示例区一" }, { "code": "110102", "name": "示例区二" } ] } ] } ]这份 JSON 每个节点都包含三个字段:code 是行政区划编码,提交给后端时用;name 是展示文本;children 是下一级子节点,叶子节点(也就是区县这一级)没有 children 字段。我一般习惯把所有区划数据整理成这种“省-市-区”的嵌套结构,因为在联动场景里,选完省后直接取province.children就是城市列表,选完城市后取city.children就是区县列表,完全不需要额外查询。
初次使用这份数据时,建议先在开发者工具里用console.log打印一下数据首层,确认字段名和自己业务代码里引用的一致。常见的一个坑是:有人拿到的数据是province、city、area这种不同命名,导致后面取 children 的时候拿到 undefined,整列直接渲染为空。
2.2 本地 JSON 优于接口下发:两个理由
这套源码默认把数据放在了本地静态文件里,而不是每次打开页面都从服务端拉取。对比一下两种方式的特点:
| 对比维度 | 本地静态 JSON | 后端接口下发 |
|---|---|---|
| 首次打开速度 | 秒开,无网络等待 | 受接口耗时影响 |
| 弱网/离线环境 | 可用 | 不可用 |
| 数据更新 | 发版时跟随代码更新 | 可随时热更新 |
| 前端复杂度 | 无请求逻辑 | 需要 loading、失败重试 |
| 主包体积 | 占几十 KB,可接受 | 不占用包体积 |
三级区划数据的体积大概在几十 KB 量级,放进小程序包里基本无感。相比每次打开页面都要请求一次接口,本地加载的体验更接近原生控件,尤其在小程序冷启动之后第一次点开地址选择器时,滑动响应不会有白屏等待。
从另一个角度说,如果你的项目对区划数据的时效性要求特别高,比如要实时跟随国家发布的行政区划变动,那走接口下发更稳妥。但就大部分电商、表单类场景而言,本地 JSON 加定期发版更新已经是足够成熟的做法。这套源码也做了“本地为主”的设计,改动后端方案时只需要替换数据加载函数。
2.3 数据按需加载:省、市、区三个数组的内存组织
拿到一棵完整的树之后,页面里不能直接把整棵树塞给 picker-view-column,需要先把“当前可见的三层”拆成三个平铺数组。源码里的做法是这样:
// 初始化时只取省份列表 const provinces = regionData.map(item => ({ name: item.name, code: item.code, children: item.children })); // 当前选中的省下面挂的所有市 const firstProvince = regionData[0] || {}; const cities = (firstProvince.children || []).map(item => ({ name: item.name, code: item.code, children: item.children })); // 当前选中的市下面挂的所有区县 const firstCity = (cities[0] || {}).children || [];provinces、cities、counties是三个独立数组,分别对应三列的渲染数据源。这样设计有三个好处:一是 setData 时只需要更新某一列的数据,改动面小;二是数组长度直接约束滚轮索引,避免索引越界;三是排查问题时能单独打印某一列的数据,不需要从整棵树里捞。
注意代码里的兜底写法|| []。真实数据有时候会出现某个省下面没有城市,或者某个城市数据缺失的情况,如果不加兜底,undefined.map会直接报错,这是联动组件最常见崩溃来源之一。
3. 核心逻辑拆解:picker-view 三列联动的完整链路
数据准备好之后,重点就是 picker-view 怎么把三列串起来。这一节从 WXML 结构、JS 事件响应、组件封装三层来拆,每一层都对应到具体代码。
3.1 WXML 结构:三列 picker-view 必须配对应高度
页面里的核心结构如下:
<picker-view class="region-picker" value="{{valueIndex}}" bindchange="onRegionChange" > <picker-view-column> <view wx:for="{{provinces}}" wx:key="code" class="picker-item"> {{item.name}} </view> </picker-view-column> <picker-view-column> <view wx:for="{{cities}}" wx:key="code" class="picker-item"> {{item.name}} </view> </picker-view-column> <picker-view-column> <view wx:for="{{counties}}" wx:key="code" class="picker-item"> {{item.name}} </view> </picker-view-column> </picker-view>value属性接收一个二维数组,比如[1, 0, 2],分别表示三列当前滚到的索引。bindchange在用户滑动滚轮停止时触发,事件参数里能拿到底部选中项的索引。注意 picker-view 的高度不是由内容撑开的,必须通过样式指定,否则组件默认高度在很多基础库版本下表现不一致。
配套的 WXSS 至少要有这两条:
.region-picker { height: 480rpx; width: 100%; } .picker-item { height: 80rpx; line-height: 80rpx; text-align: center; }.picker-item的行高必须和 picker-view 里滚轮的视觉高度对应,常见做法是设成 80rpx 左右,这样用户滑动时能看到明显的选中态。如果漏掉行高,文字会挤在一起,滑动时的“吸附感”也会消失。
3.2 联动的响应链:滑动省列时如何重置市和区
联动本质上就是事件驱动:某一列索引变化后,用它去取对应子级数据,然后刷新后面两列的渲染数据。核心逻辑如下:
onRegionChange(e) { const val = e.detail.value; // 三列各自的选中索引 const provinceIndex = val[0]; const cityIndex = val[1]; const province = this.data.regionTree[provinceIndex] || {}; const cities = (province.children || []).map(item => ({ name: item.name, code: item.code, children: item.children })); // 城市索引越界时归零 const safeCityIndex = Math.min(cityIndex, cities.length - 1); const city = cities[safeCityIndex] || {}; const counties = (city.children || []).map(item => ({ name: item.name, code: item.code })); this.setData({ cities, counties, valueIndex: [provinceIndex, safeCityIndex, 0] }); this.triggerEvent('change', { province: province.name, city: city.name, county: (counties[0] || {}).name, provinceCode: province.code, cityCode: city.code, countyCode: (counties[0] || {}).code }); }这段代码是整个联动逻辑的心脏,分成三步:第一步根据省索引取出 province 对象;第二步用safeCityIndex兜底,防止原城市索引在新数据里越界;第三步把区县列表和新的索引数组一起 setData 回去。
safeCityIndex是这个函数里最容易被忽略的细节。假设用户先滑到某省的第 5 个市,然后回到上一层把省换成了只有 2 个市的新省,此时val[1]还是 5,如果不做 min 约束,cities[5]是 undefined,区县列会直接空白。正确做法是像上面这样取最小值归零,保证索引始终在合法范围内。
3.3 封装成组件:从页面逻辑到可复用组件
上面的思路在主页面里能跑,但收货地址往往在多个页面出现,比如结算页和编辑地址页。源码里已经把这个逻辑封装成了region-picker自定义组件,对外只暴露一个bindchange事件。
组件的 properties 定义如下:
Component({ properties: { defaultRegion: { type: Array, value: [] } }, data: { regionTree: [], provinces: [], cities: [], counties: [], valueIndex: [0, 0, 0] }, lifetimes: { attached() { this.initData(); } }, methods: { initData() { /* 初始化逻辑 */ }, onRegionChange(e) { /* 联动逻辑 */ } } });把联动收敛进组件有非常实际的好处:数据初始化、索引管理、事件派发都不需要业务页面操心。业务页只需要像用内置组件一样使用它:
<region-picker bind:change="onRegionSelected" />bind:change是自定义组件触发的事件,业务页面在onRegionSelected(e)里拿e.detail就能得到省市区名称和行政区划编码,直接拼到表单提交参数里。组件还暴露了defaultRegion属性,用于回填已有地址,这一块在第六章再展开。
这里的封装思路同样适用于团队协作:负责公共组件的同学只维护 region-picker 内部逻辑,业务同学只管事件和回填,两边互不干扰,这也是把联动做成可复用组件而不是页面内Page方法的核心原因。
4. 运行全流程:从导入代码到真实交互,每一步都给你对好
源码跑起来不难,但有几个位置容易卡住:目录结构的职责没搞清、样式和事件没对上、真机和模拟器表现不一致。这一章按导入、编译、联调三个步骤拆开讲。
4.1 目录结构:源码包里每个文件夹干什么
拿到代码后,第一件事是认清文件归属。整体结构如下:
├── app.js ├── app.json ├── app.wxss ├── pages/ │ ├── index/ │ │ ├── index.wxml │ │ ├── index.wxss │ │ ├── index.js │ │ └── index.json ├── components/ │ └── region-picker/ │ ├── region-picker.wxml │ ├── region-picker.wxss │ ├── region-picker.js │ └── region-picker.json └── data/ └── pca-code.jsondata/pca-code.json是整套组件的数据源,体积相对较大,不要用编辑器的格式化功能去格式化它,会卡顿;components/region-picker是组件本体,包含模板、样式、逻辑和组件配置;pages/index是示例页,演示了如何引入组件、监听事件、展示选中的省市区。
app.json里需要注册页面和组件,示例项目已经配置好。如果你要集成进自己的项目,需要把 components 目录复制过去,并在目标页面的 json 文件里声明 usingComponents:
{ "usingComponents": { "region-picker": "/components/region-picker/region-picker" } }组件路径一旦写错,编译时会提示Component is not found,初学者经常踩这个坑。注意绝对路径以/开头,相对路径则是从当前页面开始计算。
4.2 编译预览:点击、滑动、回填的完整路径
用微信开发者工具导入项目后,默认会打开示例页。页面正中是一个“选择地址”按钮,点击后底部弹出 picker-view 选择器。滑动三列,顶部展示区域会实时更新,样式大致是:
已选地区:示例省甲 / 示例市A / 示例区一这个展示逻辑在 index.js 里通过监听组件 change 事件实现的用法,也可以作为你接入自己页面的标准模板。示例页的完整交互链路是:点击按钮 → 小程序原生弹层把 picker-view 展示出来 → 用户滑动三列 → 触发组件的 change → 事件冒泡到页面 → 页面 setData 更新展示文本。
打开示例页后建议做三件事验证数据加载是否正常:第一,滑动省列到第三个省,再返回第一个省,观察市和区县是否同步变化;第二,重复快速滑动省列和市列,看是否存在空白(对应前面的 safeCityIndex 逻辑);第三,在编辑器 Network 面板确认没有任何接口请求发出,说明数据完全来自本地 JSON。
代码包的体积不需要做任何特殊配置,改完代码,开发者工具会自动编译刷新。如果你把代码集成到自己项目后发现主包体积偏大,优先检查pca-code.json是否被打进了主包,可以考虑放到分包里。
4.3 真机调试的差异:模拟器没毛病不代表真机没问题
开发工具模拟器上表现正常的组件,到真机上偶尔会出现两种差异:一是 picker-view 的高度在不同机型上渲染不一致,表现为滚轮区域过高或过低;二是 iOS 上滑动比较灵敏,快速滑动时触发 change 的频率与原数据索引不同步。
处理这类问题,一个常见的做法是在真机调试面板里打开wxml审查,直接查看 picker-view 的实际布局高度,和模拟器对比。高度问题调整样式中的 rpx 值即可;索引不同步问题,通常不是源码逻辑错误,而是 setData 回调时序导致,可在onRegionChange里增加防抖处理,或者保证所有 index 都经过“越界兜底”后再 setData。
真机调试的另一个注意点是:如果组件在开发工具里一切正常,但真机上第一列默认不在正确位置,大概率是valueIndex初始化时机问题,需要确保regionTree数据加载完成后再设置 valueIndex。可以在 attached 生命周期里用一个异步回调处理。
5. 避坑手册:三级联动实战里最常见的 5 个翻车现场
这套源码本身跑通没什么难度,但如果你要把它改造成自己的组件,下面几个问题几乎必然遇到。每一条都按“现象 → 原因 → 解决”来写。
5.1 第一列滑动后,后两列显示空白
现象:省列滑动到新的省份时,市、区列内容瞬间消失,界面一片空白。 原因:新省的 children 为空,或者数据还没 setData 完成时用户又快速滑动了另一列,导致取 children 得到 undefined。 解决:所有取子级数据的地方都做兜底,确保province.children || [],并且联动逻辑用safeCityIndex防止越界。
const city = (cities[safeCityIndex] || {}); const counties = (city.children || []).map(...);5.2 修改完省,市列停留在上一个省的索引
现象:从“省甲/市1/区1”切换到“省乙”后,市列显示的还是“市1”,但市1 在省乙里根本不存在。 原因:只更新了省和市的数组,没有同步重置市列的索引,picker-view 的 valueIndex 还停留在旧索引。 解决:设置valueIndex时强制让市、区索引归零,从第一个元素开始展示。
5.3 picker-view 高度怎么设都不生效
现象:样式里写了height: 480rpx,但真机上的滚轮区域高度仍然很怪,文字显示不全。 原因:部分基础库版本对 picker-view 的默认高度比较敏感,同时项目里又未设置 item 行高,导致内容区和选区错位。 解决:同时设置 picker-view 高度和 picker-item 行高,并保证两者单位一致,不要混用 rpx 和 px。
5.4 JSON 文件大到影响编译速度
现象:在开发者工具里每次编辑完代码,编译耗时明显变长,编辑器偶尔卡死。 原因:pca-code.json本身体积大,如果把它放在主包里且每次开发都整体编译,确实会影响编译性能。 解决:音开发阶段可以将这个 JSON 放到分包内,或者单独导入成 js 文件,用module.exports导出对象;发布时再整体打包。
5.5 change 事件重复触发,导致表单值被覆盖
现象:用户滑动一次滚轮,页面上的“已选地区”文字跳了两次,最终显示的不是当前选中项。 原因:picker-view 的 bindchange 在滑动停止时触发一次,但如果组件的 setData 又反过来影响了 valueIndex,可能引发额外的事件回调。 解决:在组件内部维护一个防抖标志位,连续触发时只处理最后一次索引,或者在使用组件时对e.detail的值做一层判断,相同编码不重复处理。
6. 二次开发:把三级联动拓展成四级、默认回填和自定义数据源
源码的价值很多时候不在于开箱即用,而在于方便改。这一章给出三个最常被问到的改造方向,涉及少量代码,但都是完整的可实现路径。
6.1 行政区划数据过时了,怎么更新数据源
区划数据是随行政调整变动的,代码包里内置的数据不可能永远准确。更新方式非常简单:从公开的标准区划数据源导出最新 JSON,保持code/name/children三段结构不变,整体替换data/pca-code.json即可。
替换时要注意两点。一是编码最好保持字符串类型,避免前端比较时出现110101 !== "110101"的隐式转换问题;二是数据层级必须完全一致,如果新数据多了一层“乡镇街道”,要预先决定是保留并在组件里复用,还是直接裁剪掉。我一般会在这个文件里跑一个简单的 Node 脚本校验每个节点的 children 是否存在超深层级,防止改坏。
6.2 从三级升到四级:增加“街道”列
多一个层级需要动三个地方:数据、WXML、联动逻辑。数据源换成包含乡镇街道的四级 JSON;picker-view 里增加第四个picker-view-column;JS 里新增streets数组,并复用同样的联动逻辑,事件派发时把街道名称和编码一并发出去。
onRegionChange(e) { const val = e.detail.value; const streetIndex = val[3] || 0; // ...省、市、区逻辑不变 const street = (streets[streetIndex] || {}).name || ''; this.triggerEvent('change', { province: province.name, city: city.name, county: county.name, street }); }注意四级数据体积至少翻几倍,建议放到分包里加载,避免主包体积告警。
6.3 已选地址回填:defaultRegion 默认值
编辑页会经常遇到“用户之前选过地址,再次打开时降默认定位到已选省市区”的需求。组件已经预留了defaultRegion属性,传入格式是省市区名称或 code 数组。
properties: { defaultRegion: { type: Array, value: [] } }接收默认值后,在组件 data 里找到对应索引,把valueIndex设为该索引,并同步刷新市、区数据。由于需要先等 regionTree 加载完成,建议把回填逻辑放在 attached 生命周期里做一次延时,或把defaultRegion用observer监听,数据初始化后再处理。回填时同样要处理“没有匹配到对应 code”的情况,比如数据更新后某个区被合并,索引回退到 0 而不是报错。
observer(region) { if (!region || !this.data.regionTree.length) return; this.setDefaultRegion(region); }我每次在项目里集成这类组件,都习惯把“默认值回填”当成独立的自测用例跑一遍,专门测试不存在的旧编码和空数据场景。这个习惯帮我避开了大量只在线上才会暴露的空白选择器问题,一套流程下来省心很多。希望这套源码和拆解对你的开发也有同样的帮助。
本文还有配套的精品资源,点击获取