
简介微信小程序省市县三级联动示例代码包面向需要实现地址选择功能的开发者通过省、市、县三个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 classregion-picker value{{valueIndex}} bindchangeonRegionChange picker-view-column view wx:for{{provinces}} wx:keycode classpicker-item {{item.name}} /view /picker-view-column picker-view-column view wx:for{{cities}} wx:keycode classpicker-item {{item.name}} /view /picker-view-column picker-view-column view wx:for{{counties}} wx:keycode classpicker-item {{item.name}} /view /picker-view-column /picker-viewvalue属性接收一个二维数组比如[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:changeonRegionSelected /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、联动逻辑。数据源换成包含乡镇街道的四级 JSONpicker-view 里增加第四个picker-view-columnJS 里新增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); }我每次在项目里集成这类组件都习惯把“默认值回填”当成独立的自测用例跑一遍专门测试不存在的旧编码和空数据场景。这个习惯帮我避开了大量只在线上才会暴露的空白选择器问题一套流程下来省心很多。希望这套源码和拆解对你的开发也有同样的帮助。本文还有配套的精品资源点击获取