小程序省市级联组件使用

背景。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>
相关推荐
拾光拾趣录10 分钟前
🔥9种继承写法全解,第7种99%人没用过?⚠️
前端·面试
李梦晓14 分钟前
git 提交代码到别的分支
前端·git
LIUENG15 分钟前
Vue2 中的响应式原理
前端·vue.js
陈随易16 分钟前
VSCode v1.103发布,AI编程任务列表,可用GPT 5和Claude 4.1
前端·后端·程序员
视觉CG31 分钟前
【JS】扁平树数据转为树结构
android·java·javascript
wordbaby35 分钟前
以0deg为起点,探讨CSS线性渐变的方向
前端·css
猩猩程序员38 分钟前
宣布 Rust 1.89.0 发布
前端
Spider_Man1 小时前
Node.js 胡编乱造机:让代码帮你写鸡汤,灵感不求人!🧙‍♂️✨
前端·javascript·node.js
BUG收容所所长1 小时前
如何用React快速搭建一个AI语音合成应用?从零到一的完整实战指南
前端·javascript·react.js
Jerry_Rod1 小时前
Electron一小时新手快速入门
前端·electron