「Vue 项目中实现智能时间选择:带业务规则的级联选择器」

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

#创作灵感

公司业务需要,某个时间节点前可以选择到月,某个时间节点后只能选择季度

vue2  Vant2

javascript

import { Cascader, Field, Form, Popup, Button } from 'vant';
import 'vant/lib/index.css';

export default {
  name: 'CascaderPage',
  components: {
    VanCascader: Cascader,
    VanField: Field,
    VanForm: Form,
    VanPopup: Popup,
    VanButton: Button
  },
  props: {
    title: {
      type: String,
      default: '选择时间'
    },
    // 起始年份,默认从当前年往前5年
    startYear: {
      type: Number,
      default: new Date().getFullYear() - 5
    },
    // 结束年份,默认从当前年往后5年
    endYear: {
      type: Number,
      default: new Date().getFullYear() + 5
    }
  },
  data() {
    return {
      showPopup: false,
      cascaderValue: '',
      fieldValue: '',
      selectedResult: '',
      fieldNames: {
        text: 'label',
        value: 'value',
        children: 'children'
      }
    };
  },
  computed: {
    // 生成级联选择的选项
    options() {
      const yearOptions = [];
      
      // 生成年份选项
      for (let year = this.startYear; year <= this.endYear; year++) {
        const quarterOptions = [];
        
        // 生成季度选项
        for (let quarter = 1; quarter <= 4; quarter++) {
          const quarterObj = {
            label: `${year}年第${quarter}季度`,
            value: `${year}-Q${quarter}`
            // 移除默认children属性,避免空选项
          };
          
          // 判断是否需要生成月份选项
          // 规则:2025年9月及之前需要月份,之后不需要
          if (year < 2025) {
            // 2025年之前,所有季度都需要月份
            quarterObj.children = this.getMonthsByQuarter(quarter);
          } else if (year === 2025) {
            // 2025年,只有前3季度需要月份
            if (quarter <= 3) {
              quarterObj.children = this.getMonthsByQuarter(quarter);
            }
          }
          // 2025年之后的年份,不生成月份选项
          
          quarterOptions.push(quarterObj);
        }
        
        yearOptions.push({
          label: `${year}年`,
          value: year.toString(),
          children: quarterOptions
        });
      }
      
      return yearOptions;
    }
  },
  methods: {
    // 根据季度获取对应的月份
    getMonthsByQuarter(quarter) {
      const months = [];
      let startMonth, endMonth;
      
      switch (quarter) {
        case 1:
          startMonth = 1;
          endMonth = 3;
          break;
        case 2:
          startMonth = 4;
          endMonth = 6;
          break;
        case 3:
          startMonth = 7;
          endMonth = 9;
          break;
        case 4:
          startMonth = 10;
          endMonth = 12;
          break;
        default:
          return [];
      }
      
      for (let month = startMonth; month <= endMonth; month++) {
        months.push({
          label: `${month}月`,
          value: month.toString()
        });
      }
      
      return months;
    },
    
    // 处理选择变化 - 只在选择完最后一级选项时关闭
    handleChange(value) {
      const { selectedOptions, tabIndex } = value;
      console.log('选择变化:', value, selectedOptions, tabIndex);
      
      // 如果没有选择值,不处理
      if (!selectedOptions || selectedOptions.length === 0) return;
      
      // 获取当前选中的年份
      const selectedYear = parseInt(selectedOptions[0].value);
      
      // 构建正确格式的selectedValues数组
      const selectedValues = [];
      selectedValues.push(selectedOptions[0].value); // 年份
      
      if (selectedOptions.length >= 2) {
        // 对于季度,提取数字部分
        const quarterStr = selectedOptions[1].value;
        if (quarterStr.includes('-Q')) {
          selectedValues.push(quarterStr.split('-Q')[1]); // 只提取季度数字
        } else {
          selectedValues.push(quarterStr);
        }
      }
      
      if (selectedOptions.length === 3) {
        selectedValues.push(selectedOptions[2].value); // 月份
      }
      
      // 检查是否已经选择到最后一级
      if (selectedOptions.length === 2) {
        // 情况1: 如果是2025年之后或2025年第4季度,选择完季度就关闭
        const quarterValue = selectedOptions[1].value;
        const quarter = parseInt(quarterValue.includes('-Q') ? quarterValue.split('-Q')[1] : quarterValue);
        
        if (selectedYear > 2025 || (selectedYear === 2025 && quarter === 4)) {
          this.onConfirm(selectedValues, selectedOptions);
        }
        // 其他情况:选择完季度后继续选择月份,不关闭
      } else if (selectedOptions.length === 3) {
        // 情况2: 选择完月份后关闭
        this.onConfirm(selectedValues, selectedOptions);
      }
    },
    
    // 确认选择
    onConfirm(selectedValues, selectedOptions) {
      if (!selectedValues || selectedValues.length === 0) return;
      
      // 格式化选中的结果
      const formattedResult = this.formatSelectedResult(selectedValues);
      this.fieldValue = formattedResult;
      this.selectedResult = `您选择了:${formattedResult}`;
      this.showPopup = false;
    },
    
    // 格式化选中的结果
    formatSelectedResult(selectedValues) {
      if (!selectedValues || selectedValues.length === 0) return '';
      
      // 处理selectedValues可能是字符串的情况
      let values = Array.isArray(selectedValues) ? selectedValues : [selectedValues];
      
      // 如果只有一个值,尝试从值中解析出年份
      if (values.length === 1) {
        const value = values[0];
        // 检查是否是季度格式
        if (typeof value === 'string' && value.includes('-Q')) {
          const parts = value.split('-Q');
          const year = parts[0];
          const quarter = parts[1];
          return `${year}年第${quarter}季度`;
        }
        // 只显示年份
        return `${value}年`;
      }
      
      let result = '';
      // 年份
      result += `${values[0]}年`;
      
      // 季度
      if (values.length >= 2) {
        let quarter;
        if (typeof values[1] === 'string' && values[1].includes('-Q')) {
          // 处理字符串格式的季度值
          quarter = values[1].split('-Q')[1];
        } else {
          // 直接使用季度值
          quarter = values[1];
        }
        result += `第${quarter}季度`;
      }
      
      // 月份 - 确保月份显示为两位数格式
      if (values.length === 3) {
        const month = parseInt(values[2]);
        // 格式化月份为两位数
        const formattedMonth = month < 10 ? `0${month}` : `${month}`;
        result += `${formattedMonth}月`;
      }
      
      return result;
    },
    
    // 表单提交
    onSubmit(values) {
      if (!this.fieldValue) {
        this.$toast('请选择时间');
        return;
      }
      this.$toast(`提交成功:${this.fieldValue}`);
    }
  },
  mounted() {
    // 生命周期钩子:设置默认选中最后一项
    try {
      if (this.options.length > 0) {
        const lastYear = this.options[this.options.length - 1];
        if (lastYear.children && lastYear.children.length > 0) {
          const lastQuarter = lastYear.children[lastYear.children.length - 1];
          
          const selected = [
            lastYear.value,
            lastQuarter.value
          ];
          
          // 如果有月份选项,也选中最后一个
          if (lastQuarter.children && lastQuarter.children.length > 0) {
            selected.push(lastQuarter.children[lastQuarter.children.length - 1].value);
          }
          
          this.cascaderValue = selected.join('/');
          this.fieldValue = this.formatSelectedResult(selected);
          this.selectedResult = `您选择了:${this.fieldValue}`;
        }
      }
    } catch (error) {
      console.error('设置默认选中项失败:', error);
    }
  }
};

HTML

  <div class="cascader-container">
    <h2>Vant 时间级联选择器示例</h2>
    
    <van-form @submit="onSubmit">
      <div class="field-container">
        <van-field
          v-model="fieldValue"
          readonly
          label="时间选择"
          placeholder="请选择时间"
          :value="fieldValue"
        />
        <van-button 
          type="primary" 
          @click="showPopup = true"
          style="margin-left: 10px; min-width: 100px;"
        >
          选择时间
        </van-button>
      </div>
    </van-form>
    
    <!-- Popup弹窗 -->
    <van-popup
      v-model="showPopup"
      round
      position="bottom"
      :style="{ height: '80%' }"
    >
      <div class="popup-header">
        <h3>{{ title }}</h3>
        <van-button 
          type="text" 
          @click="showPopup = false"
          style="position: absolute; right: 16px; top: 16px;"
        >
          取消
        </van-button>
      </div>
      
      <van-cascader
        v-model="cascaderValue"
        :options="options"
        :field-names="fieldNames"
        @change="handleChange"
        @close="showPopup = false"
        placeholder=""
      />
    </van-popup>
    
    <div class="result-section" v-if="selectedResult">
      <h3>选择结果</h3>
      <p>{{ selectedResult }}</p>
    </div>
  </div>

css

.cascader-container {
  padding: 20px;
  max-width: 600px;
  margin: 0 auto;
}

.field-container {
  display: flex;
  align-items: center;
}

.popup-header {
  padding: 16px;
  border-bottom: 1px solid #eee;
  position: relative;
  text-align: center;
}

.popup-header h3 {
  margin: 0;
  color: #333;
  font-size: 16px;
}

.cascader-container h2 {
  text-align: center;
  margin-bottom: 20px;
  color: #333;
}

.result-section {
  margin-top: 30px;
  padding: 15px;
  background-color: #f5f5f5;
  border-radius: 8px;
}

.result-section h3 {
  margin-bottom: 10px;
  color: #666;
}

.result-section p {
  color: #333;
  font-size: 16px;
}

vue3 vant4

HTML TS 

<template>
  <div class="date-cascader-container">
    <!-- 添加激活按钮触发显示 -->
    <van-button @click="show = true">选择时间</van-button>
    <van-popup v-model:show="show" round position="bottom">
        <van-cascader
        v-model:selected-values="selectedValues"
        :options="options"
        :title="title"
        @change="handleChange"
        @close="show = false"
        :field-names="fieldNames"
        placeholder=""
        />
    </van-popup>
    <div v-if="selectedValues.length" class="selected-result">
      已选择: {{ formatSelectedResult() }}
    </div>
  </div>
</template>

<script setup>
import { ref, computed, watch, onMounted } from 'vue';

// 先定义show变量
const show = ref(false);
// 选中的值
const selectedValues = ref([]);

// 配置级联选择的字段名
const fieldNames = {
  text: 'label',
  value: 'value',
  children: 'children'
};

// 组件参数
const props = defineProps({
  title: {
    type: String,
    default: '选择时间'
  },
  // 起始年份,默认从当前年往前5年
  startYear: {
    type: Number,
    default: new Date().getFullYear() - 5
  },
  // 结束年份,默认从当前年往后5年
  endYear: {
    type: Number,
    default: new Date().getFullYear() + 5
  }
});

// 生成级联选择的选项
const options = computed(() => {
  const yearOptions = [];
  
  // 生成年份选项
  for (let year = props.startYear; year <= props.endYear; year++) {
    const quarterOptions = [];
    
    // 生成季度选项
    for (let quarter = 1; quarter <= 4; quarter++) {
      const quarterObj = {
        label: `${year}年第${quarter}季度`,
        value: `${year}-Q${quarter}`
        // 移除默认children属性,避免空选项
      };
      
      // 判断是否需要生成月份选项
      // 规则:2025年9月及之前需要月份,之后不需要
      if (year < 2025) {
        // 2025年之前,所有季度都需要月份
        quarterObj.children = getMonthsByQuarter(quarter);
      } else if (year === 2025) {
        // 2025年,只有前3季度需要月份
        if (quarter <= 3) {
          quarterObj.children = getMonthsByQuarter(quarter);
        }
      }
      // 2025年之后的年份,不生成月份选项
      
      quarterOptions.push(quarterObj);
    }
    
    yearOptions.push({
      label: `${year}年`,
      value: year.toString(),
      children: quarterOptions
    });
  }
  
  return yearOptions;
});

// 根据季度获取对应的月份
function getMonthsByQuarter(quarter) {
  const months = [];
  let startMonth, endMonth;
  
  switch (quarter) {
    case 1:
      startMonth = 1;
      endMonth = 3;
      break;
    case 2:
      startMonth = 4;
      endMonth = 6;
      break;
    case 3:
      startMonth = 7;
      endMonth = 9;
      break;
    case 4:
      startMonth = 10;
      endMonth = 12;
      break;
    default:
      return [];
  }
  
  for (let month = startMonth; month <= endMonth; month++) {
    months.push({
      label: `${month}月`,
      value: month.toString()
    });
  }
  
  return months;
}

// 处理选择变化 - 只在选择完最后一级选项时关闭
const handleChange = ({ selectedValues, selectedOptions }) => {
  console.log('选择变化:', selectedValues, selectedOptions);
  
  // 如果没有选择值,不处理
  if (!selectedOptions || selectedOptions.length === 0) return;
  
  // 获取当前选中的年份
  const selectedYear = parseInt(selectedOptions[0].value);
  
  // 检查是否已经选择到最后一级
  if (selectedOptions.length === 2) {
    // 情况1: 如果是2025年之后或2025年第4季度,选择完季度就关闭
    const quarter = parseInt(selectedOptions[1].value.split('-Q')[1]);
    if (selectedYear > 2025 || (selectedYear === 2025 && quarter === 4)) {
      show.value = false;
    }
    // 其他情况:选择完季度后继续选择月份,不关闭
  } else if (selectedOptions.length === 3) {
    // 情况2: 选择完月份后关闭
    show.value = false;
  }
};

// 格式化选中的结果
const formatSelectedResult = () => {
  if (selectedValues.value.length === 0) return '';
  
  let result = '';
  // 年份
  result += `${selectedValues.value[0]}年`;
  
  // 季度
  if (selectedValues.value.length >= 2) {
    const quarter = selectedValues.value[1].split('-Q')[1];
    result += `第${quarter}季度`;
  }
  
  // 月份
  if (selectedValues.value.length === 3) {
    result += `${selectedValues.value[2]}月`;
  }
  
  return result;
};

// 暴露选中的值
const emits = defineEmits(['update:modelValue']);
watch(selectedValues, (newVal) => {
  emits('update:modelValue', newVal);
});

// 生命周期钩子:设置默认选中最后一项
onMounted(() => {
  try {
    if (options.value.length > 0) {
      const lastYear = options.value[options.value.length - 1];
      if (lastYear.children && lastYear.children.length > 0) {
        const lastQuarter = lastYear.children[lastYear.children.length - 1];
        
        const selected = [
          lastYear.value,
          lastQuarter.value
        ];
        
        // 如果有月份选项,也选中最后一个
        if (lastQuarter.children && lastQuarter.children.length > 0) {
          selected.push(lastQuarter.children[lastQuarter.children.length - 1].value);
        }
        
        selectedValues.value = selected;
      }
    }
  } catch (error) {
    console.error('设置默认选中项失败:', error);
  }
});
</script>

<style scoped>
.date-cascader-container {
  margin: 20px;
  min-height: 200px; /* 确保容器有足够高度 */
}
</style>