小程序省市级联组件使用

发布于:2025-08-09 ⋅ 阅读:(19) ⋅ 点赞:(0)

背景。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>

    <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>


网站公告

今日签到

点亮在社区的每一天
去签到