背景。uni-data-picker組件用起來不方便。調整后級聯效果欠佳,會關閉彈窗需要重新選擇。
- 解決方案。讓cursor使用uniapp 原生組件生成懶加載省市級聯
<template><view class="picker-cascader"><view class="cascader-label"><text v-if="required" class="required-mark">*</text><text class="label-text">{{ label }}</text></view><pickermode="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>