原生小程序中自定义三级联动(省市区)控件

1.起源

本来已经实现纯前端省-市-区联动,接口返回省-市-区字符串即可,后面又改需求从接口渲染,页面展示中文接口传id数组;

2.对比

✅ 用mode="region"的场景 【纯前端】

1.仅需要省市区名称,无需自定义 ID(如简单的表单提交、地址展示);

2.追求开发效率,不想维护数据 / 写联动逻辑;

3.注重用户体验,需要真机端的原生搜索 / 选择交互;

4.业务需要全国完整的省市区数据,且无需频繁更新。

✅ 用mode="multiSelector"的场景 【通过服务】

1.需要给省市区绑定自定义业务 ID(如省市区 ID,对接后端接口);

2.不需要省市区全量数据,仅需自定义部分数据(如仅显示某几个省份);

3.需要修改省市区名称(如改英文、改简称),或增加额外字段(如邮编);

4.非省市区的多列联动选择(如选择「年级 - 班级 - 学号」「品牌 - 系列 - 型号」)。

3.实现(mode="multiSelector")

这里只介绍mode="multiSelector"通过接口获取省市区数据,并页面选中出现文字,传给接口是对应省市区id数组,另一种后面有时间在补...
(wxml+js+wxss)效果图:

css 复制代码
<view class="content-v">
	<view class="form-container" style="position: relative;z-index: 1;background-color: #fff;">
		<view class="form-item">
			<label class="form-label">所在地区</label>
			<picker mode="multiSelector" bindchange="bindMultiPickerChange" bindcolumnchange="bindMultiPickerColumnChange"
				value="{{multiIndex}}" range="{{multiArray}}" catchtap="{{isReadonly ? 'noop' : ''}}">
				<view class="picker-view" style="{{isReadonly ? 'color:#999;' : ''}}">
					{{formData.fullRegion || '请选择省市区'}}
					<text class="arrow" style="{{isReadonly ? 'display:none;' : ''}}">▼</text>
				</view>
			</picker>
		</view>
	</view>
</view>
css 复制代码
const unitapi = require('../../utils/distanceUtil.js');
Page({

	/**
	 * 页面的初始数据
	 */
	data: {

		// ========== 替换原生region,新增三级联动核心数据 ==========
		regionOriginData: [], // 接口/兜底的省市区原始数据(含id/name/children)
		multiArray: [], // 多列选择器显示的纯名称数组 [[省1,省2],[市1,市2],[区1,区2]]
		multiIndex: [0, 0, 0], // 多列选择器选中索引 [省索引,市索引,区索引]
		selectedRegionIds: [], // 最终省市区ID数组 [省id,市id,区id]

		formData: {
			province: '', //省名称
			city: '', //市名称
			district: '', //区名称
			fullRegion: '', // 拼接回显用
			selectedServices: [],
			selectedServicesText: '',

		},
	},

	/**
	 * 生命周期函数--监听页面加载
	 */
	onLoad(options) {
		this.queryS_S_Q(); // 加载省市区数据(接口+本地兜底)
	},
	// ========== 重构:加载省市区数据+初始化三级联动 ==========
	queryS_S_Q() {
		let that = this;
		// 本地兜底数据(接口失败时用,和接口字段一致:id/name/children)
		const mockRegionData = [{
				id: '110000',
				name: '北京市',
				children: [{
					id: '110100',
					name: '北京市',
					children: [{
						id: '110101',
						name: '东城区'
					}, {
						id: '110102',
						name: '西城区'
					}, {
						id: '110105',
						name: '朝阳区'
					}]
				}]
			},
			{
				id: '310000',
				name: '上海市',
				children: [{
					id: '310100',
					name: '上海市',
					children: [{
						id: '310101',
						name: '黄浦区'
					}, {
						id: '310104',
						name: '徐汇区'
					}]
				}]
			},
			{
				id: '440000',
				name: '广东省',
				children: [{
					id: '440100',
					name: '广州市',
					children: [{
						id: '440103',
						name: '荔湾区'
					}, {
						id: '440104',
						name: '越秀区'
					}]
				}, {
					id: '440300',
					name: '深圳市',
					children: [{
						id: '440303',
						name: '罗湖区'
					}, {
						id: '440304',
						name: '福田区'
					}]
				}]
			},
			{
				id: '320000',
				name: '江苏省',
				children: [{
					id: '320100',
					name: '南京市',
					children: [{
						id: '320102',
						name: '玄武区'
					}, {
						id: '320104',
						name: '秦淮区'
					}]
				}, {
					id: '320500',
					name: '苏州市',
					children: [{
						id: '320505',
						name: '虎丘区'
					}, {
						id: '320506',
						name: '吴中区'
					}]
				}]
			}
		];

		unitapi.getS_S_Q_api().then((res) => {
			console.warn("【调试1】接口原始返回:", res);
			// 宽松校验接口数据
			let originData = [];
			if (Array.isArray(res) === true) {
				originData = res;
				console.warn("【省市区接口】使用接口第一条:", originData[0]);
			} else {
				originData = mockRegionData;
				console.warn("【接口请求失败】,使用本地模拟数据");
				wx.showToast({
					title: '地区数据加载中',
					icon: 'none',
					duration: 1500
				});
			}

			// 初始化三级联动的显示数据和默认ID
			const provinceList = originData;
			const cityList = provinceList[0]?.children || [];
			const distList = cityList[0]?.children || [];
			// 多列显示的纯名称数组
			const multiArray = [
				provinceList.map(item => item.name),
				cityList.map(item => item.name),
				distList.map(item => item.name)
			];
			// 默认选中的第一个省市区ID
			const defaultIds = [
				provinceList[0]?.id || '',
				cityList[0]?.id || '',
				distList[0]?.id || ''
			];
			// 默认拼接名称
			const defaultFullName = `${provinceList[0]?.name || ''}-${cityList[0]?.name || ''}-${distList[0]?.name || ''}`;

			that.setData({
				regionOriginData: originData,
				multiArray,
				multiIndex: [0, 0, 0],
				selectedRegionIds: defaultIds,
				'formData.fullRegion': defaultFullName,
				'formData.province': provinceList[0]?.name || '',
				'formData.city': cityList[0]?.name || '',
				'formData.district': distList[0]?.name || ''
			}, () => {
				console.log("【初始化成功】默认省市区ID:", that.data.selectedRegionIds);
			});
		}).catch(err => {
			console.error("【接口失败】", err);
			// 接口报错直接用本地兜底数据初始化
			const mockRegionData = [{
				id: '110000',
				name: '北京市',
				children: [{
					id: '110100',
					name: '北京市',
					children: [{
						id: '110101',
						name: '东城区'
					}, {
						id: '110102',
						name: '西城区'
					}]
				}]
			}];
			const multiArray = [
				mockRegionData.map(item => item.name),
				mockRegionData[0]?.children.map(item => item.name) || [],
				mockRegionData[0]?.children[0]?.children.map(item => item.name) || []
			];
			const defaultIds = [mockRegionData[0]?.id || '', mockRegionData[0]?.children[0]?.id || '', mockRegionData[0]?.children[0]?.children[0]?.id || ''];
			that.setData({
				regionOriginData: mockRegionData,
				multiArray,
				multiIndex: [0, 0, 0],
				selectedRegionIds: defaultIds,
				'formData.fullRegion': `${mockRegionData[0]?.name || ''}-${mockRegionData[0]?.children[0]?.name || ''}-${mockRegionData[0]?.children[0]?.children[0]?.name || ''}`
			});
			wx.showToast({
				title: '地区接口失败,使用本地数据',
				icon: 'none'
			});
		});
	},
	// ========== 新增:三级联动列滚动事件(切省更市,切市更区) ==========
	bindMultiPickerColumnChange(e) {
		const {
			column,
			value
		} = e.detail; // column=0(省)/1(市)/2(区),value=滚动后索引
		let {
			multiIndex,
			regionOriginData
		} = this.data;
		multiIndex[column] = value; // 更新当前列索引

		// 字段名适配(默认id/name/children,接口恢复后改这里即可)
		const NAME_FIELD = 'name';
		const CHILD_FIELD = 'children';

		// 切换省份(列0):重置市、区索引为0,更新市、区列表
		if (column === 0) {
			const provinceItem = regionOriginData[value] || {};
			const cityList = provinceItem[CHILD_FIELD] || [];
			const distList = cityList[0]?.[CHILD_FIELD] || [];
			this.setData({
				multiIndex: [value, 0, 0],
				'multiArray[1]': cityList.map(item => item[NAME_FIELD]),
				'multiArray[2]': distList.map(item => item[NAME_FIELD])
			});
		}
		// 切换城市(列1):重置区索引为0,更新区列表
		if (column === 1) {
			const provinceItem = regionOriginData[multiIndex[0]] || {};
			const cityList = provinceItem[CHILD_FIELD] || [];
			const cityItem = cityList[value] || {};
			const distList = cityItem[CHILD_FIELD] || [];
			this.setData({
				multiIndex: [multiIndex[0], value, 0],
				'multiArray[2]': distList.map(item => item[NAME_FIELD])
			});
		}
	},
	// ========== 新增:三级联动选择确认事件(核心:获取ID+更新页面) ==========
	bindMultiPickerChange(e) {
		const that = this;
		const multiIndex = e.detail.value; // 最终选中的索引 [省,市,区]
		const {
			regionOriginData
		} = that.data;

		// 字段名适配(默认id/name/children,接口恢复后仅改这3行!)
		const ID_FIELD = 'id';
		const NAME_FIELD = 'name';
		const CHILD_FIELD = 'children';

		// 根据索引精准获取省市区项(无匹配失败可能,因为数据来自同一源)
		const provinceItem = regionOriginData[multiIndex[0]] || {};
		const cityList = provinceItem[CHILD_FIELD] || [];
		const cityItem = cityList[multiIndex[1]] || {};
		const distList = cityItem[CHILD_FIELD] || [];
		const distItem = distList[multiIndex[2]] || {};

		// 精准获取ID和名称(无空值、无匹配失败)
		const provId = provinceItem[ID_FIELD] || '';
		const cityId = cityItem[ID_FIELD] || '';
		const distId = distItem[ID_FIELD] || '';
		const provName = provinceItem[NAME_FIELD] || '';
		const cityName = cityItem[NAME_FIELD] || '';
		const distName = distItem[NAME_FIELD] || '';
		const selectedRegionIds = [provId, cityId, distId];
		const fullRegion = `${provName}-${cityName}-${distName}`;

		// 更新页面显示和存储ID
		that.setData({
			multiIndex,
			selectedRegionIds,
			'formData.province': provName,
			'formData.city': cityName,
			'formData.district': distName,
			'formData.fullRegion': fullRegion
		}, () => {
			// 每次选择都稳定打印ID,无任何报错
			console.log('✅ 页面显示:', fullRegion);
			console.log('★★★ 省市区ID数组:★★★', selectedRegionIds);
		});
	},

})
bash 复制代码
.content-v {
  
}

.form-container {
  background: #dd9595;
  border-radius: 40rpx;
  scrollbar-width: thin;
}

.form-container::-webkit-scrollbar {
  width: 2rpx;
}

.form-item {
  margin-bottom: 30rpx;
  display: flex;
  justify-content: space-between;
  align-items: center;
  min-height: 80rpx;
  padding: 0 10rpx;
}

.form-label {
  font-size: 28rpx;
  color: #333;
  white-space: normal;
  word-break: break-all;
  height: auto;
  min-height: 40rpx;
  max-width: 224rpx;
  line-height: 40rpx;
  margin-bottom: 0;
}

.picker-view {
  height: 60rpx;
  line-height: 60rpx;
  background: #fff;
  border-radius: 8rpx;
  padding: 0 20rpx;
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-size: 28rpx;
  color: #666;
  box-sizing: border-box;
  min-width: 400rpx;
}

.arrow {
  font-size: 24rpx;
  color: #999;
  margin-left: 10rpx;
  line-height: 60rpx;
}
4.放入app.json里第一行,默认加载页
相关推荐
说私域19 小时前
社群招募文案的核心构建要点与工具赋能路径——基于AI智能名片链动2+1模式商城小程序的实践研究
人工智能·小程序·私域运营
_ZeroKing21 小时前
自制智能门锁:NFC 刷卡 + 小程序远程开锁(完整实战记录)
嵌入式硬件·小程序·notepad++·arduino
郑州光合科技余经理21 小时前
可独立部署的Java同城O2O系统架构:技术落地
java·开发语言·前端·后端·小程序·系统架构·uni-app
阿斌_bingyu7091 天前
眼镜店AR在线试戴小程序技术解决方案
小程序·ar
计算机毕设指导61 天前
基于微信小程序的智能停车场管理系统【源码文末联系】
java·spring boot·微信小程序·小程序·tomcat·maven·intellij-idea
2501_933907211 天前
如何选择西安优质小程序开发服务与本凡码农合作?
科技·微信小程序·小程序
说私域1 天前
破局互联网产品开发困境:开源AI智能名片链动2+1模式S2B2C商城小程序的实践与启示
人工智能·小程序·开源·私域运营
宁夏雨科网2 天前
文具办公用品小程序商城,开发一个难吗
小程序·商城小程序·文具小程序·文具商城
说私域2 天前
开源链动2+1模式商城小程序在深度分销数字化转型中的应用研究
人工智能·小程序·开源·流量运营·私域运营