微信小程序里的picker组件提供了多种选择器模式,其中多列选择器(mode="multiSelector")常被用来做省市区三级联动。很多初学者把网上找来的省市区数据直接塞进range属性,结果滑动第一列后,第二列没有跟着变化,或者第三列数据错乱。这背后的核心原因,是数据源没有按层级依赖关系组织,也没有在列滑动时动态裁剪下级数组。本文将围绕数据源结构、事件响应以及自定义数据源三个角度,详细说明如何稳定实现三级联动。

省市区三级联动的数据源结构要求
在使用picker的multiSelector模式时,range需要是一个二维数组,例如[provinceArray, cityArray, districtArray]。但三级联动并不是三个独立数组简单拼起来,而是后一级的内容依赖于前一级的选中项。常见做法是把原始数据整理成树形或带parentId的扁平结构:省级对象有code和name,市级对象有code、name和parentCode,区县级同理。初始化时,第一列来自所有省级;第二列由当前选中省级的code过滤得出;第三列再由当前市级过滤。
如果直接采用某些接口返回的“省-市-区”嵌套json,要注意小程序setData对大数组的性能开销。建议将原始数据放在data外的js模块中,页面只保留当前三列需要的子集。下面示例展示如何从扁平数据生成初始三列:
// region.js 扁平数据示例
const flatList = [
{ code: '11', name: '北京市', parent: '0' },
{ code: '1101', name: '市辖区', parent: '11' },
{ code: '110101', name: '东城区', parent: '1101' },
{ code: '44', name: '广东省', parent: '0' },
{ code: '4401', name: '广州市', parent: '44' },
{ code: '440106', name: '天河区', parent: '4401' }
];
function getChildren(parent) {
return flatList.filter(function (item) {
return item.parent === parent;
}).map(function (item) {
return { code: item.code, name: item.name };
});
}
const province = getChildren('0');
const city = getChildren(province[0].code);
const district = getChildren(city[0].code);
console.log(province, city, district);
这种结构的优势在于,无论数据源是国标行政区划还是业务自定义区域,只要遵循parent依赖,就能复用同一套过滤函数。相反,若把数据写成固定的三维数组,后期维护或扩展海外地区会非常麻烦。
columnchange事件与动态更新逻辑
当用户滑动某一列时,picker会触发bindcolumnchange事件,事件对象中包含column(被改变的列索引,从0开始)和value(该列新的选中下标)。我们必须在这个回调里,根据改变的列去重算其后所有列的数据,并通过setData更新range及对应的value数组,否则界面不会联动。
例如用户改变了第一列(省),那么第二列(市)要重置为新区划下的第一市,第三列(区)也要跟着重置。若只改变第二列,则只需重算第三列。注意value数组也要同步修正,否则会出现高亮位置错位。下面代码演示了典型处理:
Page({
data: {
range: [[], [], []],
value: [0, 0, 0]
},
onColumnChange: function (e) {
const column = e.detail.column;
const val = e.detail.value;
const range = this.data.range;
const value = this.data.value;
value[column] = val;
if (column === 0) {
const city = getChildren(range[0][val].code);
const district = getChildren(city[0].code);
this.setData({
range: [range[0], city, district],
value: [val, 0, 0]
});
} else if (column === 1) {
const district = getChildren(range[1][val].code);
this.setData({
range: [range[0], range[1], district],
value: [value[0], val, 0]
});
}
}
});
这里容易踩的坑是,在setData里直接修改range的引用却忘了更新value,导致小程序报“value长度与range不匹配”的警告。另外,如果数据源很大,频繁setData整个二维数组可能造成卡顿,此时可只setData变化的那一列,例如range[1]用字符串路径range[1]赋值。
切换到自定义数据源的实现方式
并非所有业务都需要国标省市区,比如物流系统只开通了部分省份,或企业内部系统要选“大区-分公司-站点”。此时只需替换前面getChildren所依赖的扁平数组,其余联动逻辑完全不用改。自定义数据源可以是后端下发的json,也可以是前端写死的配置,重点是保持parent字段的连贯性。
假设我们要做一个“华南三省市”的精简选择器,数据源可以写成这样:
const customList = [
{ code: '44', name: '广东省', parent: '0' },
{ code: '45', name: '广西', parent: '0' },
{ code: '46', name: '海南', parent: '0' },
{ code: '4401', name: '广州市', parent: '44' },
{ code: '4402', name: '深圳市', parent: '44' },
{ code: '4501', name: '南宁', parent: '45' },
{ code: '4601', name: '海口', parent: '46' },
{ code: '440101', name: '天河', parent: '4401' }
];
function getCustomChildren(parent) {
return customList.filter(function (i) {
return i.parent === parent;
}).map(function (i) {
return { code: i.code, name: i.name };
});
}
在页面onLoad时,把flatList换成customList并调用同样的初始化函数即可。如果自定义数据源带有异步加载需求,比如选了省才去请求市,那么onColumnChange里要先置空下级列,等接口返回再填充,同时用wx.showLoading避免用户重复操作。这种思路下,picker的disabled属性也可临时开启,保证数据就绪前不会提交错误值。
总体来看,微信小程序picker的三级联动难点不在组件本身,而在数据建模与状态同步。只要把握“父级code驱动子级过滤”和“columnchange中重置后续value”两个原则,无论是标准省市区还是完全自定义的三级结构,都能以少量代码稳健运行。