背景。uni-data-picker组件用起来不方便。调整后级联效果欠佳,会关闭弹窗需要重新选择。
- 解决方案。让cursor使用uniapp 原生组件生成懒加载省市级联
js
<template>
<view class="picker-cascader">
<view class="cascader-label">
<text v-if="required" class="required-mark">*</text>
<text class="label-text">{{ label }}</text>
</view>
<picker
mode="multiSelector"
:range="range"
:value="defaultValue"
:disabled="disabled || readonly"
@change="handleChange"
@cancel="handleCancel"
@columnchange="handleColumnChange"
@confirm="handleConfirm">
<view class="picker-input" :data-disabled="disabled || readonly">
<text v-if="displayText" class="picker-text">{{ displayText }}</text>
<text v-else class="picker-placeholder">{{ placeholder }}</text>
<text class="picker-arrow">></text>
</view>
</picker>
</view>
</template>
<script>
import { getProvinceList, getCityListByProvince, getCountyListByCity } from '@/api/regionApi.js';
import { getProvinceListMock, getCityListByProvinceMock, getCountyListByCityMock } from '@/mock/regionMock.js';
export default {
name: 'PickerCascader',
props: {
/**
* 标签文本
*/
label: {
type: String,
default: '所在地区'
},
/**
* 绑定的值,支持字符串格式 "provinceCode,cityCode,countyCode" 或对象格式 {provinceCode: "110000", cityCode: "110100", countyCode: "110101"}
*/
regionStr: {
type: [String, Object],
default: ''
},
/**
* 占位符文本
*/
placeholder: {
type: String,
default: '请选择省市区'
},
/**
* 是否禁用
*/
disabled: {
type: Boolean,
default: false
},
/**
* 是否只读
*/
readonly: {
type: Boolean,
default: false
},
/**
* 最大选择级数,支持2-3级
*/
maxLevel: {
type: Number,
default: 3,
validator: function (value) {
return value >= 2 && value <= 3;
}
},
/**
* 是否必填
*/
required: {
type: Boolean,
default: false
}
},
data() {
return {
// picker的range数据,格式为二维数组
range: [],
// picker的value数据,格式为数组,表示每列选中的索引
defaultValue: [0, 0, 0],
// 省份数据
provinces: [],
// 城市数据缓存,格式为 {provinceCode: cities}
cityCache: {},
// 县级数据缓存,格式为 {cityCode: counties}
countyCache: {},
// 当前选中的编码
selectedCodes: ['', '', ''],
// 当前选中的文本
selectedTexts: ['', '', ''],
// 是否正在加载数据
loading: false
};
},
computed: {
/**
* 显示文本
*/
displayText() {
const texts = this.selectedTexts.filter((text) => text);
return texts.length > 0 ? texts.join(' ') : '';
}
},
watch: {
/**
* 监听value 变化,更新选中值
*/
regionStr: {
handler(newVal) {
console.log('value变化', newVal);
this.initFromValue(newVal);
},
immediate: true
}
},
mounted() {
this.initData();
},
methods: {
/**
* 初始化数据
*/
async initData() {
try {
this.loading = true;
console.log('PickerCascader 开始初始化数据...');
await this.loadProvinces();
this.initRange();
this.initFromValue(this.regionStr);
console.log('PickerCascader 数据初始化完成');
console.log('省份数据:', this.provinces.length, '个');
console.log('range数据:', this.range);
} catch (error) {
console.error('初始化数据失败:', error);
} finally {
this.loading = false;
}
},
/**
* 加载省份数据
*/
async loadProvinces() {
try {
console.log('开始加载省份数据...');
const res = await getProvinceList();
if (res.code === 200 && Array.isArray(res.data)) {
this.provinces = res.data;
console.log('从API获取省份数据成功:', this.provinces.length, '个省份');
} else {
// 使用mock数据
console.log('API返回异常,使用mock数据');
const mockRes = getProvinceListMock();
this.provinces = mockRes.data;
}
console.log('省份数据加载完成:', this.provinces.length, '个省份');
} catch (error) {
console.error('获取省份列表失败:', error);
// 使用mock数据
const mockRes = getProvinceListMock();
this.provinces = mockRes.data;
console.log('使用mock数据,省份数量:', this.provinces.length);
}
},
/**
* 初始化range数据
*/
initRange() {
// 初始化省份列
const provinceColumn =
this.provinces && this.provinces.length > 0
? this.provinces.map((province) => ({
text: province.name,
code: province.code
}))
: [];
// 初始化城市列(空数据,等待选择省份后加载)
const cityColumn = [];
// 初始化县级列(空数据,等待选择城市后加载)
const countyColumn = [];
this.range = [provinceColumn, cityColumn, countyColumn];
},
/**
* 从value初始化选中值
*/
initFromValue(value) {
if (!value) {
this.resetSelection();
return;
}
let provinceCode = '';
let cityCode = '';
let countyCode = '';
if (typeof value === 'string') {
const codes = value.split(',');
provinceCode = codes[0] || '';
cityCode = codes[1] || '';
countyCode = codes[2] || '';
} else if (typeof value === 'object') {
provinceCode = value.provinceCode || '';
cityCode = value.cityCode || '';
countyCode = value.countyCode || '';
}
this.setSelectionByCodes(provinceCode, cityCode, countyCode);
},
/**
* 根据编码设置选中值
*/
async setSelectionByCodes(provinceCode, cityCode, countyCode) {
if (!provinceCode) {
this.resetSelection();
return;
}
// 查找省份索引
const provinceIndex = this.provinces.findIndex((p) => p.code === provinceCode);
if (provinceIndex === -1) {
this.resetSelection();
return;
}
// 设置省份选中
this.value[0] = provinceIndex;
this.selectedCodes[0] = provinceCode;
this.selectedTexts[0] = this.provinces[provinceIndex].name;
// 加载城市数据
await this.loadCities(provinceCode, provinceIndex);
if (cityCode && this.range[1] && this.range[1].length > 0) {
// 查找城市索引
const cities = this.range[1];
const cityIndex = cities.findIndex((c) => c.code === cityCode);
if (cityIndex !== -1) {
this.value[1] = cityIndex;
this.selectedCodes[1] = cityCode;
this.selectedTexts[1] = cities[cityIndex].text;
// 如果是三级联动,加载县级数据
if (this.maxLevel === 3) {
await this.loadCounties(cityCode, provinceIndex, cityIndex);
if (countyCode && this.range[2] && this.range[2].length > 0) {
// 查找县级索引
const counties = this.range[2];
const countyIndex = counties.findIndex((c) => c.code === countyCode);
if (countyIndex !== -1) {
this.value[2] = countyIndex;
this.selectedCodes[2] = countyCode;
this.selectedTexts[2] = counties[countyIndex].text;
}
}
}
}
}
// 强制更新
this.$forceUpdate();
},
/**
* 重置选中值
*/
resetSelection() {
this.value = [0, 0, 0];
this.selectedCodes = ['', '', ''];
this.selectedTexts = ['', '', ''];
},
/**
* 加载城市数据
*/
async loadCities(provinceCode, provinceIndex) {
console.log('开始加载城市数据,省份编码:', provinceCode);
// 检查缓存
if (this.cityCache[provinceCode]) {
console.log('使用缓存的城市数据:', this.cityCache[provinceCode].length, '个城市');
this.range[1] = this.cityCache[provinceCode];
return;
}
try {
const res = await getCityListByProvince(provinceCode);
let cities = [];
if (res.code === 200 && Array.isArray(res.data)) {
cities = res.data;
console.log('从API获取城市数据成功:', cities.length, '个城市');
} else {
// 使用mock数据
console.log('API返回异常,使用mock数据');
const mockRes = getCityListByProvinceMock(provinceCode);
cities = mockRes.data;
}
// 转换为picker所需格式
const cityColumn =
cities && cities.length > 0
? cities.map((city) => ({
text: city.name,
code: city.code
}))
: [];
console.log('城市数据转换完成:', cityColumn.length, '个城市');
// 缓存数据
this.cityCache[provinceCode] = cityColumn;
this.range[1] = cityColumn;
// 重置后续列的选中值
this.value[1] = 0;
this.value[2] = 0;
this.selectedCodes[1] = '';
this.selectedCodes[2] = '';
this.selectedTexts[1] = '';
this.selectedTexts[2] = '';
// 清空县级数据
this.range[2] = [];
console.log('城市数据加载完成,range更新为:', this.range);
// 强制更新
this.$forceUpdate();
} catch (error) {
console.error('获取城市列表失败:', error);
// 使用mock数据
const mockRes = getCityListByProvinceMock(provinceCode);
const cities = mockRes.data;
const cityColumn =
cities && cities.length > 0
? cities.map((city) => ({
text: city.name,
code: city.code
}))
: [];
this.cityCache[provinceCode] = cityColumn;
this.range[1] = cityColumn;
console.log('使用mock数据,城市数量:', cityColumn.length);
this.$forceUpdate();
}
},
/**
* 加载县级数据
*/
async loadCounties(cityCode, provinceIndex, cityIndex) {
console.log('开始加载县级数据,城市编码:', cityCode);
// 检查缓存
if (this.countyCache[cityCode]) {
console.log('使用缓存的县级数据:', this.countyCache[cityCode].length, '个县区');
this.range[2] = this.countyCache[cityCode];
return;
}
try {
const res = await getCountyListByCity(cityCode);
let counties = [];
if (res.code === 200 && Array.isArray(res.data)) {
counties = res.data;
console.log('从API获取县级数据成功:', counties.length, '个县区');
} else {
// 使用mock数据
console.log('API返回异常,使用mock数据');
const mockRes = getCountyListByCityMock(cityCode);
counties = mockRes.data;
}
// 转换为picker所需格式
const countyColumn =
counties && counties.length > 0
? counties.map((county) => ({
text: county.name,
code: county.code
}))
: [];
console.log('县级数据转换完成:', countyColumn.length, '个县区');
// 缓存数据
this.countyCache[cityCode] = countyColumn;
this.range[2] = countyColumn;
// 重置县级选中值
this.value[2] = 0;
this.selectedCodes[2] = '';
this.selectedTexts[2] = '';
console.log('县级数据加载完成,range更新为:', this.range);
// 强制更新
this.$forceUpdate();
} catch (error) {
console.error('获取县级列表失败:', error);
// 使用mock数据
const mockRes = getCountyListByCityMock(cityCode);
const counties = mockRes.data;
const countyColumn =
counties && counties.length > 0
? counties.map((county) => ({
text: county.name,
code: county.code
}))
: [];
this.countyCache[cityCode] = countyColumn;
this.range[2] = countyColumn;
console.log('使用mock数据,县级数量:', countyColumn.length);
this.$forceUpdate();
}
},
/**
* 处理列变化事件
*/
async handleColumnChange(e) {
const { column, value } = e.detail;
console.log('列变化事件:', { column, value, currentRange: this.range });
// 更新选中索引
this.value[column] = value;
if (column === 0) {
// 省份变化
if (this.range[0] && this.range[0][value]) {
const provinceCode = this.range[0][value].code;
const provinceName = this.range[0][value].text;
console.log('选择省份:', { provinceCode, provinceName });
this.selectedCodes[0] = provinceCode;
this.selectedTexts[0] = provinceName;
// 加载城市数据
await this.loadCities(provinceCode, value);
}
// 重置后续列的选中值
this.value[1] = 0;
this.value[2] = 0;
this.selectedCodes[1] = '';
this.selectedCodes[2] = '';
this.selectedTexts[1] = '';
this.selectedTexts[2] = '';
// 清空县级数据
this.range[2] = [];
} else if (column === 1) {
// 城市变化
if (this.range[1] && this.range[1][value]) {
const cityCode = this.range[1][value].code;
const cityName = this.range[1][value].text;
console.log('选择城市:', { cityCode, cityName });
this.selectedCodes[1] = cityCode;
this.selectedTexts[1] = cityName;
// 如果是三级联动,加载县级数据
if (this.maxLevel === 3) {
await this.loadCounties(cityCode, this.value[0], value);
}
}
// 重置县级选中值
this.value[2] = 0;
this.selectedCodes[2] = '';
this.selectedTexts[2] = '';
} else if (column === 2) {
// 县级变化
if (this.range[2] && this.range[2][value]) {
const countyCode = this.range[2][value].code;
const countyName = this.range[2][value].text;
console.log('选择县级:', { countyCode, countyName });
this.selectedCodes[2] = countyCode;
this.selectedTexts[2] = countyName;
}
}
// 强制更新
this.$forceUpdate();
},
/**
* 处理选择确认事件
*/
handleChange(e) {
const { value } = e.detail;
console.log('选择确认事件:', { value, range: this.range });
// 更新选中索引
this.value = value;
// 更新选中编码和文本
for (let i = 0; i < value.length; i++) {
if (this.range[i] && this.range[i][value[i]] && value[i] >= 0) {
this.selectedCodes[i] = this.range[i][value[i]].code;
this.selectedTexts[i] = this.range[i][value[i]].text;
}
}
// 触发change事件
const result = this.formatResult();
console.log('最终结果:', result);
this.$emit('change', result);
},
/**
* 处理确认事件
*/
handleConfirm(e) {
console.log('确认事件:', e);
// 这里可以添加额外的确认逻辑
},
/**
* 处理取消事件
*/
handleCancel() {
this.$emit('cancel');
},
/**
* 格式化结果
*/
formatResult() {
const codes = this.selectedCodes.filter((code) => code);
const texts = this.selectedTexts.filter((text) => text);
// 根据maxLevel返回相应格式
if (this.maxLevel === 2) {
return codes.slice(0, 2).join(',');
} else {
return codes.join(',');
}
}
}
};
</script>
<style scoped>
.picker-cascader {
background-color: #fff;
border-radius: 12rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
}
.cascader-label {
display: flex;
align-items: center;
margin-bottom: 20rpx;
}
.required-mark {
color: #ff4757;
font-size: 28rpx;
margin-right: 8rpx;
font-weight: bold;
}
.label-text {
font-size: 28rpx;
color: #333;
font-weight: 500;
}
.picker-input {
display: flex;
align-items: center;
justify-content: space-between;
height: 88rpx;
padding: 0 24rpx;
border: 2rpx solid #e1e5e9;
border-radius: 8rpx;
background-color: #fff;
transition: all 0.3s ease;
}
.picker-input:active {
border-color: #2979ff;
box-shadow: 0 0 0 4rpx rgba(41, 121, 255, 0.1);
}
.picker-text {
font-size: 28rpx;
color: #333;
flex: 1;
}
.picker-placeholder {
font-size: 28rpx;
color: #999;
flex: 1;
}
.picker-arrow {
font-size: 24rpx;
color: #999;
transform: rotate(90deg);
}
/* 禁用状态 */
.picker-input[data-disabled='true'] {
background-color: #f8f9fa;
color: #999;
cursor: not-allowed;
}
.picker-input[data-disabled='true'] .picker-text,
.picker-input[data-disabled='true'] .picker-placeholder {
color: #999;
}
</style>