
简介这份资源面向微信小程序开发者聚焦地址选择场景中常见的省市县三级联动需求提供一套可直接参考的完整实现方案。压缩包共5个文件以js逻辑代码为主配合wxml页面结构、wxss样式与json配置整体约27KB体量轻便便于快速导入现有项目或作为学习样例。资源围绕数据源设计展开采用含id、name、parentId的JSON结构组织全国行政区划并通过picker组件分别承载省、市、县三级选择配合handleProvinceChange等事件方法逐级过滤下级选项同时处理页面加载时的默认选中与初始化逻辑。已有1464人学习下载适合希望掌握小程序组件使用、事件监听与数据筛选思路的初中级开发者可据此理解三级联动的完整链路并迁移到其他级联选择场景。1. 省市县三级联动一个被低估的小程序基础组件做微信小程序表单时地址选择几乎是绕不开的需求。用户注册要填籍贯下单要填收货地址门店入驻要填经营所在地——只要涉及地域信息省市县三级联动就会准时出现。很多开发者第一反应是找现成组件结果要么样式对不上设计稿要么数据源不全要么在低端安卓机上滚动卡成幻灯片。这个项目的价值就在这儿它把三级联动拆成了一个可以独立运行、独立调试、独立改造的完整小程序工程数据、逻辑、视图三层分得清清楚楚。适合两类人刚接触小程序、想搞明白 picker 多列联动到底怎么跑起来的新手以及接过地址表单需求、被数据结构和滚动性能折腾过的熟手。下面按「能干什么、怎么跑、怎么改」的顺序拆。2. 项目结构与技术选型原生小程序的三层拆分2.1 目录结构长什么样这个工程是标准的原生小程序结构没有引入任何第三方 UI 框架也没有用 uni-app、Taro 这类跨端方案。原因很直接三级联动本身不复杂引入框架反而增加构建链路和调试成本。目录大致如下miniprogram-address/ ├── app.js ├── app.json ├── app.wxss ├── pages/ │ └── address/ │ ├── address.js │ ├── address.json │ ├── address.wxml │ └── address.wxss ├── utils/ │ └── region.js └── data/ └── region.jsonpages/address是唯一的功能页承载选择器、回显和确认逻辑。data/region.json存的是省市县三级树形数据utils/region.js负责把树形数据转成 picker 需要的多列数组并处理索引到名称的映射。app.json里只注册这一个页面app.wxss放全局重置样式。这种拆法的好处是数据与逻辑分离换数据源不用动页面代码改交互不用碰数据文件。2.2 为什么用 picker 的 multiSelector 模式小程序原生picker组件支持三种模式selector单列、multiSelector多列、time和date。三级联动对应的是multiSelector它允许你传入一个二维数组每一列独立滚动并通过bindcolumnchange事件拿到当前滚动到哪一列、哪一行。picker modemultiSelector range{{multiArray}} value{{multiIndex}} bindchangeonConfirm bindcolumnchangeonColumnChange view classpicker-value{{selectedText || 请选择省市区}}/view /pickerrange绑定二维数组multiArray结构是[省份数组, 城市数组, 区县数组]value绑定当前三列的选中索引multiIndexbindcolumnchange在任意一列滚动时触发用来动态刷新后两列的数据bindchange只在点「确定」时触发用来做最终回显。这里有个容易忽略的点bindcolumnchange触发时multiIndex还没更新必须从事件对象的detail.column和detail.value里取当前列和行号手动改数组再setData。2.3 数据源的组织方式region.json采用嵌套结构省下面挂市市下面挂区县[ { name: 某省, children: [ { name: 某市, children: [{ name: 某区 }, { name: 某县 }] } ] } ]这种结构直观、易维护但 picker 要的是「列数组」所以utils/region.js里做了一层转换// 把树形数据转成 picker 需要的三列数组 function buildMultiArray(regionTree, provinceIndex 0, cityIndex 0) { const provinces regionTree.map(p p.name); const cities (regionTree[provinceIndex]?.children || []).map(c c.name); const districts (regionTree[provinceIndex]?.children?.[cityIndex]?.children || []) .map(d d.name); return [provinces, cities, districts]; }参数说明regionTree是完整树形数据provinceIndex和cityIndex是当前选中的省、市下标用来决定后两列取哪一支。返回值直接喂给range。注意这里用了可选链?.小程序基础库较老时可能不支持常见做法是改成逐层判断或者把基础库调到 2.9 以上。3. 核心逻辑拆解一次滚动到底改了什么3.1 初始化首屏就要有可选项页面onLoad时不能只把省份列填上否则用户看到后两列是空的体验很差。正确做法是默认选中第一省、第一市三列同时有值const regionTree require(../../data/region.json); const { buildMultiArray } require(../../utils/region.js); Page({ data: { multiArray: [], multiIndex: [0, 0, 0], selectedText: }, onLoad() { const multiArray buildMultiArray(regionTree, 0, 0); this.setData({ multiArray }); } });multiIndex初始化为[0, 0, 0]和multiArray的三列一一对应。selectedText留空等用户确认后再拼成「省 市 区」的字符串。这里不要一上来就回显第一项否则用户没操作却显示了一个地址容易造成误提交。3.2 列滚动联动刷新的关键分支bindcolumnchange是整个组件的核心。它要处理三种情况滚的是省列、市列还是区县列。滚省列时市和区县都要重置滚市列时只重置区县滚区县列时什么都不用刷。onColumnChange(e) { const { column, value } e.detail; const { multiArray, multiIndex } this.data; const nextIndex [...multiIndex]; nextIndex[column] value; if (column 0) { // 滚省市、区县全部重置 const cities regionTree[value].children || []; const districts cities[0]?.children || []; multiArray[1] cities.map(c c.name); multiArray[2] districts.map(d d.name); nextIndex[1] 0; nextIndex[2] 0; } else if (column 1) { // 滚市只重置区县 const districts regionTree[nextIndex[0]].children[value].children || []; multiArray[2] districts.map(d d.name); nextIndex[2] 0; } this.setData({ multiArray, multiIndex: nextIndex }); }逻辑说明column是当前滚动的列号value是该列滚到的行号。滚省列时用新的省下标取市列表再用第一个市取区县列表同时把后两列索引归零避免越界。滚市列时省下标不变用nextIndex[0]和新的市下标取区县。滚区县列时直接跳过因为后面没有依赖列了。这里最容易翻车的是multiArray的原地修改——setData对数组是引用比较直接改multiArray[1]有时不触发视图更新稳妥做法是先const nextArray [...multiArray]再改。3.3 确认回显把索引翻译成文字用户点「确定」时触发bindchange事件里带的是最终三列索引。要把它翻译成可读地址同时保留索引供后续编辑回填onConfirm(e) { const [p, c, d] e.detail.value; const province regionTree[p]; const city province.children[c]; const district city.children[d]; const selectedText ${province.name} ${city.name} ${district.name}; this.setData({ multiIndex: [p, c, d], selectedText }); }参数说明e.detail.value是长度为 3 的数组分别对应省、市、区县的行号。拼字符串时用空格分隔方便展示如果后端要存编码就在这一步同时取出code字段一起setData。注意不要在这里再调buildMultiArray否则会把用户刚选好的后两列又重置回第一项。3.4 编辑回填从已有地址反推索引二次编辑场景下页面拿到的是「某省 某市 某区」这样的字符串或编码需要反推出三列索引再重建multiArray。常见做法是遍历树形数据做匹配function findIndexByNames(tree, province, city, district) { const p tree.findIndex(item item.name province); const c (tree[p]?.children || []).findIndex(item item.name city); const d (tree[p]?.children?.[c]?.children || []) .findIndex(item item.name district); return [Math.max(p, 0), Math.max(c, 0), Math.max(d, 0)]; }拿到索引后用buildMultiArray(regionTree, p, c)重建三列再把multiIndex设为[p, c, d]selectedText设为原字符串。这样用户打开编辑页时picker 直接停在他上次选的位置不用重新滚一遍。匹配失败时用Math.max(..., 0)兜底到第一项避免出现-1导致 picker 空白。4. 跑起来与避坑从开发者工具到真机4.1 本地运行的最小步骤把工程目录拖进微信开发者工具AppID选测试号即可不需要后端服务。编译后进入pages/address应该能看到一个「请选择省市区」的点击区域点开是三列滚轮。操作顺序建议按「滚省 → 看市变 → 滚市 → 看区县变 → 点确定 → 看回显」走一遍确认联动链路完整。如果首屏三列有一列是空的先检查region.json里对应层级有没有children字段再检查buildMultiArray的默认下标有没有越界。4.2 常见问题一滚省之后市列不刷新现象是滚了省份市列还是上一个省的城市。原因通常是onColumnChange里只改了multiIndex没改multiArray或者改了multiArray但没重新setData。解决方式是确认multiArray[1]和multiArray[2]都被赋了新数组并且setData里同时传了multiArray和multiIndex。另外注意setData的键名要和data里一致写成multiArray而不是multiarray。4.3 常见问题二区县列出现 undefined现象是滚到某些市时区县列显示undefined。原因是该市在数据里没有children字段map出来是空数组picker 拿不到值。解决方式是在buildMultiArray和onColumnChange里统一用|| []兜底并且在数据准备阶段做一次校验把没有区县的市补一个「市辖区」占位或者直接过滤掉。血泪经验是数据源里直辖市的结构和普通省不一样直辖市下面往往直接挂区没有市级中间层转换时要单独判断。4.4 常见问题三真机上滚动卡顿现象是开发者工具里流畅真机低端安卓上滚轮发涩。原因多半是region.json体积过大每次setData都把整棵树或大数组传过去。解决方式是只传当前三列的名称数组不要传原始树multiArray用map生成新数组而不是引用原数据如果数据量确实大考虑按省懒加载首屏只加载省份列滚到某省再请求该省下的市。注意setData单次数据量建议控制在 256KB 以内超了会明显掉帧。4.5 常见问题四编辑回填后 picker 位置不对现象是回填了文字但点开 picker 还停在第一项。原因是只设了selectedText没设multiIndex或者设了multiIndex但multiArray还是初始的三列。解决方式是回填时先算索引再用索引重建multiArray最后同时setData三个字段。顺序不能反否则 picker 会按旧数组去定位新索引出现错位。5. 二次开发换数据源与抽成通用组件5.1 换数据源时只动两个文件如果项目要换成别的行政区划数据只需要替换data/region.json并确认utils/region.js里的字段名对得上。当前用的是name和children如果新数据用label和sub就在buildMultiArray里做一层映射不要改页面代码。这样页面层完全无感回归测试也只需要跑一遍联动链路。我一般会先在utils里加一个normalizeRegion函数把外部数据统一成内部结构再喂给后续逻辑避免字段差异渗透到视图层。5.2 抽成自定义组件的边界这个工程目前是单页实现如果要在多个页面复用建议抽成components/region-picker。组件对外暴露三个属性value初始地址字符串或编码、disabled是否只读、level支持省、省市、省市区三种粒度。事件只抛一个change回传{ text, codes, indexes }。抽组件时注意把regionTree通过properties或behavior注入不要让组件内部require固定路径的数据文件否则换数据源还得改组件源码。验证方法是写一个最小页面只放组件和一行回显文字跑通再往业务页里嵌。5.3 一个容易忽略的验证习惯改完联动逻辑后不要只测「正常选一遍」。我习惯固定跑三条用例一是从最后一个省滚到第一个省看市列是否归零二是选一个有区县的市再选一个没有区县的市看区县列是否兜底三是编辑回填一个中间位置的地址点开 picker 看是否停对位置。这三条覆盖了索引越界、空数据、回填错位三类高频问题。从那以后我每次改 picker 相关代码都强制走一遍这三条比肉眼扫代码靠谱得多。希望帮到你。本文还有配套的精品资源点击获取