UniApp-X 实现省市区三级联动选择器(UVue + UTS)
·
UniApp-X 实现省市区三级联动选择器(UVue + UTS)
在 UniApp-X 开发中,省市区三级联动选择器是表单场景中非常常见的组件需求。本文将详细讲解如何基于 UVue(UniApp-X 的 Vue 组件)和 UTS 语言,实现一个支持数据回显、联动切换、自定义样式的省市区三级联动选择器,适配 UniApp-X 的跨端特性。
一、组件核心功能
本次实现的省市区选择器具备以下核心特性:
- 基于 UniApp-X 的
picker组件(multiSelector模式)实现三级联动 - 支持对象数组数据源(非纯字符串数组),适配真实业务场景的地区数据格式
- 支持外部传入默认选中值,实现数据回显
- 选中后通过
v-model语法糖向父组件传递完整的地区信息(ID + 名称) - 自定义选择器展示样式,区分已选择 / 未选择状态
<template>
<view class="uni-list">
<view class="uni-list-cell">
<view class="uni-list-cell-db">
<!-- 三级联动 Picker 核心组件 -->
<picker class="picker-multi--test"
mode="multiSelector"
@columnchange="bindMultiPickerColumnChange"
@change="bindMultiPickerChange"
:value="data.multiIndex"
:range="data.multiArray"
range-key="name"
header-text="选择区域">
<!-- 已选择状态展示 -->
<view class="uni-input picker-multi--value" v-if="displayText">
<text>{{displayText}}</text>
<text>》</text>
</view>
<!-- 未选择状态占位 -->
<view v-else class="uni-placeholder">
<text>请选择省市区</text>
<text>》</text>
</view>
</picker>
</view>
</view>
</view>
</template>
<script setup>
import { reactive, watch, nextTick, computed } from 'vue';
// 导入省市区数据源(需自行准备,格式为嵌套对象数组)
import { areaData } from './area.uts'
// 定义组件属性:接收外部传入的已选值
const props = defineProps({
selected: { type: Object, default: () => ({}) }
})
// 定义自定义事件:向父组件传递选中结果
const emit = defineEmits(['update:selected'])
// 类型定义:地区数据结构(适配真实业务的数据源格式)
type AreaItem = {
name : string,
code : string,
children : AreaItem[]
}
// 类型定义:Picker 展示的对象结构
type PickerItem = {
name : string,
id : string
}
// 响应式数据:使用 reactive 保证自动化测试可访问
const data = reactive({
multiArray: [[], [], []] as PickerItem[][], // Picker 展示的三级数据
multiIndex: [0, 0, 0], // Picker 选中索引
sourceData: [] as AreaItem[], // 原始地区数据源(用于联动查找)
hasSelected: false // 是否已选择标记
})
// 计算属性:格式化展示选中的省市区文本
const displayText = computed(() => {
if (!data.hasSelected) return '';
const province = data.multiArray[0][data.multiIndex[0]]?.name || '';
const city = data.multiArray[1][data.multiIndex[1]]?.name || '';
const district = data.multiArray[2][data.multiIndex[2]]?.name || '';
return `${province}/${city}/${district}`;
})
// 初始化加载地区数据
async function loadData() {
try {
// 1. 保存原始数据源,用于后续联动查找
data.sourceData = areaData as AreaItem[];
// 2. 初始化省份列数据
const provinces = areaData.map((item : AreaItem) => ({ name: item.name, code: item.code }));
// 3. 初始化城市列数据(默认取第一个省份的城市)
let cities : PickerItem[] = [];
if (areaData.length > 0 && areaData[0].children) {
cities = areaData[0].children.map((item : AreaItem) => ({ name: item.name, code: item.code }));
}
// 4. 初始化区县列数据(默认取第一个城市的区县)
let districts : PickerItem[] = [];
if (areaData.length > 0 && areaData[0].children && areaData[0].children.length > 0 && areaData[0].children[0].children) {
districts = areaData[0].children[0].children.map((item : AreaItem) => ({ name: item.name, code: item.code }));
}
// 5. 更新 Picker 数据(使用 splice 触发响应式更新)
data.multiArray.splice(0, 1, provinces);
data.multiArray.splice(1, 1, cities);
data.multiArray.splice(2, 1, districts);
data.multiIndex = [0, 0, 0]; // 重置选中索引
} finally {
nextTick(() => {});
}
}
// 从外部属性更新选中状态(用于回显)
const updateFromProps = (selected : any) => {
if (!selected || !selected.provinceId || !data.sourceData.length) return;
// 1. 匹配省份索引
const pIdx = data.sourceData.findIndex((p : any) => p.code === selected.provinceId);
if (pIdx < 0) return;
data.multiIndex[0] = pIdx;
// 2. 更新城市列数据并匹配索引
const cityList = data.sourceData[pIdx].children || [];
data.multiArray.splice(1, 1, cityList.map((c : any) => ({ name: c.name, code: c.code })));
let cIdx = 0;
if (selected.cityId) {
cIdx = cityList.findIndex((c : any) => c.code === selected.cityId) || 0;
}
data.multiIndex[1] = cIdx;
// 3. 更新区县列数据并匹配索引
const districtList = (cityList[cIdx] && cityList[cIdx].children) || [];
data.multiArray.splice(2, 1, districtList.map((d : any) => ({ name: d.name, code: d.code })));
let dIdx = 0;
if (selected.districtId) {
dIdx = districtList.findIndex((d : any) => d.code === selected.districtId) || 0;
}
data.multiIndex[2] = dIdx;
// 标记为已选择
data.hasSelected = true;
}
// 标记数据是否已加载
let dataLoaded = false;
// 监听外部选中值变化,实现数据回显
watch(() => props.selected, (v : any) => {
if (!dataLoaded) {
// 首次加载:先初始化数据,再回显
loadData().then(() => {
dataLoaded = true;
if (v && v.provinceId) updateFromProps(v);
});
} else {
// 非首次:直接回显
updateFromProps(v);
}
}, { deep: true, immediate: true })
// Picker 列切换事件(核心联动逻辑)
const bindMultiPickerColumnChange = (e : UniPickerColumnChangeEvent) => {
data.multiIndex[e.detail.column] = e.detail.value;
switch (e.detail.column) {
case 0: // 切换省份
const provinceIndex = data.multiIndex[0];
if (data.sourceData[provinceIndex]) {
const cityList = data.sourceData[provinceIndex].children || [];
// 更新城市列
data.multiArray.splice(1, 1, cityList.map((item : AreaItem) => ({ name: item.name, id: item.code })));
// 更新区县列(默认第一个城市)
const districtList = (cityList.length > 0 && cityList[0].children) || [];
data.multiArray.splice(2, 1, districtList.map((item : AreaItem) => ({ name: item.name, id: item.code })));
// 重置城市/区县索引
data.multiIndex.splice(1, 1, 0);
data.multiIndex.splice(2, 1, 0);
}
break;
case 1: // 切换城市
const pIndex = data.multiIndex[0];
const cIndex = data.multiIndex[1];
if (data.sourceData[pIndex] && data.sourceData[pIndex].children && data.sourceData[pIndex].children[cIndex]) {
const districtList = data.sourceData[pIndex].children[cIndex].children || [];
// 更新区县列
data.multiArray.splice(2, 1, districtList.map((item : AreaItem) => ({ name: item.name, id: item.code })));
// 重置区县索引
data.multiIndex.splice(2, 1, 0);
}
break;
}
}
// Picker 确认选择事件(向父组件传递结果)
const bindMultiPickerChange = (e : UniPickerChangeEvent) => {
const idxs = e.detail.value;
data.multiIndex = idxs;
data.hasSelected = true;
// 从原始数据源获取完整的选中信息
const p = data.sourceData[idxs[0]];
const c = (p && p.children && p.children[idxs[1]]) || null;
const d = (c && c.children && c.children[idxs[2]]) || null;
// 构造返回结果
const result = {
provinceId: p?.code || '',
provinceName: p?.name || '',
cityId: c?.code || '',
cityName: c?.name || '',
districtId: d?.code || '',
districtName: d?.name || '',
};
// 触发事件,传递结果给父组件
emit('update:selected', result);
}
</script>
<style>
.uni-picker-tips {
font-size: 14px;
color: #333;
margin-top: 5px;
margin-bottom: 15px;
padding: 0 15px;
}
.uni-input {
font-size: 14px;
color: #333;
display: flex;
flex-direction: row;
align-items: center;
padding: 0 40rpx;
justify-content: space-between;
}
.uni-placeholder {
font-size: 14px;
color: gray;
display: flex;
flex-direction: row;
align-items: center;
padding: 0 40rpx;
justify-content: space-between;
}
</style>
二、关键知识点解析
1. 对象数组适配
UniApp-X 的 picker 组件默认支持字符串数组,但真实业务中地区数据多为对象数组(包含 name/code 等字段)。通过设置 range-key="name",可以指定 picker 展示对象中的 name 字段。
2. 联动逻辑实现
- 省份切换:更新城市列和区县列数据,重置城市 / 区县索引为 0
- 城市切换:仅更新区县列数据,重置区县索引为 0
- 数据更新方式:使用
splice而非直接赋值,确保 Vue 响应式系统能检测到数据变化,触发 picker 重新渲染
3. 数据回显实现
通过 watch 监听外部传入的 selected 属性,在数据加载完成后,根据 provinceId/cityId/districtId 匹配对应的索引,更新 picker 的选中状态。
4. 父组件使用方式
<template>
<view>
<AreaPicker :selected="form" @update:selected="handleAreaSelected" />
</view>
</template>
<script setup>
import { ref } from 'vue';
import AreaPicker from '@/components/area-picker/area-picker.uvue';
const form = ref();
// 处理 area-picker 返回的选中结果
function handleAreaSelected(selected) {
if(selected){
form.value.provinceId = selected.provinceId
form.value.provinceName = selected.provinceName
form.value.cityId = selected.cityId
form.value.cityName = selected.cityName
form.value.districtId = selected.districtId
form.value.districtName = selected.districtName
}
}
</script>
<style>
</style>
5.area.uts 测试数据
const areaData = [
{
"name": "北京市",
"code": "110000",
"children": [
{
"name": "北京市",
"code": "110000",
"children": [
{ "name": "东城区", "code": "110101" },
{ "name": "西城区", "code": "110102" },
{ "name": "朝阳区", "code": "110105" },
{ "name": "海淀区", "code": "110108" }
]
}
]
},
{
"name": "上海市",
"code": "310000",
"children": [
{
"name": "上海市",
"code": "310000",
"children": [
{ "name": "黄浦区", "code": "310101" },
{ "name": "徐汇区", "code": "310104" },
{ "name": "长宁区", "code": "310105" },
{ "name": "静安区", "code": "310106" },
{ "name": "浦东新区", "code": "310115" }
]
}
]
},
{
"name": "广东省",
"code": "440000",
"children": [
{
"name": "广州市",
"code": "440100",
"children": [
{ "name": "荔湾区", "code": "440103" },
{ "name": "越秀区", "code": "440104" },
{ "name": "海珠区", "code": "440105" },
{ "name": "天河区", "code": "440106" },
{ "name": "白云区", "code": "440111" }
]
},
{
"name": "深圳市",
"code": "440300",
"children": [
{ "name": "罗湖区", "code": "440303" },
{ "name": "福田区", "code": "440304" },
{ "name": "南山区", "code": "440305" },
{ "name": "宝安区", "code": "440306" },
{ "name": "龙岗区", "code": "440307" }
]
}
]
},
{
"name": "江苏省",
"code": "320000",
"children": [
{
"name": "南京市",
"code": "320100",
"children": [
{ "name": "玄武区", "code": "320102" },
{ "name": "秦淮区", "code": "320104" },
{ "name": "建邺区", "code": "320105" },
{ "name": "鼓楼区", "code": "320106" },
{ "name": "江宁区", "code": "320115" }
]
},
{
"name": "苏州市",
"code": "320500",
"children": [
{ "name": "虎丘区", "code": "320505" },
{ "name": "吴中区", "code": "320506" },
{ "name": "相城区", "code": "320507" },
{ "name": "姑苏区", "code": "320508" },
{ "name": "吴江区", "code": "320509" }
]
}
]
},
{
"name": "浙江省",
"code": "330000",
"children": [
{
"name": "杭州市",
"code": "330100",
"children": [
{ "name": "上城区", "code": "330102" },
{ "name": "拱墅区", "code": "330105" },
{ "name": "西湖区", "code": "330106" },
{ "name": "滨江区", "code": "330108" },
{ "name": "萧山区", "code": "330109" }
]
},
{
"name": "宁波市",
"code": "330200",
"children": [
{ "name": "海曙区", "code": "330203" },
{ "name": "江北区", "code": "330205" },
{ "name": "北仑区", "code": "330206" },
{ "name": "镇海区", "code": "330211" },
{ "name": "鄞州区", "code": "330212" }
]
}
]
}
];
export {
areaData
}
四、注意事项
- 数据源准备:需自行准备
area.uts文件,格式为嵌套的对象数组,包含name/code/children字段 - 响应式更新:UniApp-X 中 picker 的
range数据更新需使用splice,直接赋值可能导致视图不更新 - 跨端兼容:该组件基于 UniApp-X 原生
picker组件实现,可兼容各端,但需注意不同平台的样式差异
总结
- 本次实现的省市区选择器基于 UniApp-X 的
picker组件,通过multiSelector模式实现三级联动,适配对象数组数据源; - 核心亮点是支持外部数据回显和
v-model双向绑定,符合 Vue 组件设计规范; - 关键技术点包括响应式数据更新(
splice)、联动逻辑处理、计算属性格式化展示文本。
该组件可直接集成到 UniApp-X 项目中,适用于表单填写、地址选择等常见业务场景,只需替换 area.uts 中的地区数据源即可适配不同业务需求。
效果如下:

更多推荐
所有评论(0)