Skip to content

DatePicker 日期选择 - Vant 4

📅 DatePicker 日期选择

📆 介绍

🎯 时光穿梭的魔法师,让日期选择变得如此优雅!

DatePicker 就像一位贴心的时间管家 ⏰,轻松帮你在时光长河中精准定位任意时刻!无论是选择生日 🎂、预约时间 📅,还是设定重要纪念日 💝,它都能以最优雅的方式呈现。

核心特色:

  • 🎨 灵活组合:年、月、日任意搭配,想怎么选就怎么选
  • 🎯 精准控制:时间范围随心设定,不会让你选到"史前时代"
  • 🎪 完美搭档:与弹出层组件天作之合,用户体验满分
  • 🎭 个性定制:格式化、过滤功能应有尽有,打造专属时间选择器

📦 引入

通过以下方式来全局注册组件,更多注册方式请参考组件注册

js
import { createApp } from'vue'; import { DatePicker } from'vant'; const app = createApp(); app.use(DatePicker);

🎯 代码演示

🔧 基础用法 - 时光机的第一次启动

🚀 简单三步,开启你的时间之旅!

就像操控一台精密的时光机器,通过 v-model 与当前时刻建立神秘连接 🔗,再用 min-datemax-date 设定时空边界 🌌,防止意外穿越到恐龙时代或遥远未来!

html
js
import { ref } from'vue'; exportdefault { setup() { const currentDate = ref(['2021', '01', '01']); return { minDate: newDate(2020, 0, 1), maxDate: newDate(2025, 5, 1), currentDate, }; }, };

🎨 选项类型 - 时间维度的自由组合

🎭 像搭积木一样自由组合时间维度!

通过神奇的 columns-type 属性,你可以成为时间的建筑师 🏗️,随心所欲地组合年、月、日这三个时间积木!

🎪 创意组合示例:

  • 🗓️ ['year'] - 纯年份模式,适合选择毕业年份
  • 🌙 ['month'] - 纯月份模式,适合选择生日月份
  • 📅 ['year', 'month'] - 年月组合,适合选择入职时间
  • 🌸 ['month', 'day'] - 月日组合,适合选择生日(不关心年份)

想怎么搭配就怎么搭配,时间选择从此告别束缚!

html
js
import { ref } from'vue'; exportdefault { setup() { const currentDate = ref(['2021', '01']); const columnsType = ['year', 'month']; return { minDate: newDate(2020, 0, 1), maxDate: newDate(2025, 5, 1), currentDate, columnsType, }; }, };

🎨 格式化选项 - 时间的美妆师

给时间穿上漂亮的外衣!

通过神奇的 formatter 函数,你可以成为时间的造型师 💄,为每个时间选项量身定制专属的显示格式!让"2021"变成"2021年",让"01"变成"01月",时间显示从此更加优雅动人!

html
js
import { ref } from'vue'; exportdefault { setup() { const currentDate = ref(['2021', '01']); const columnsType = ['year', 'month']; constformatter = (type, option) => { if (type === 'year') { option.text += '年'; } if (type === 'month') { option.text += '月'; } return option; }; return { minDate: newDate(2020, 0, 1), maxDate: newDate(2025, 5, 1), formatter, currentDate, columnsType, }; }, };

🔍 过滤选项 - 时间的筛选大师

🎯 精挑细选,只留下最合适的时间!

通过强大的 filter 函数,你可以成为时间的筛选专家 🕵️‍♀️,自由决定哪些时间选项能够"入选"!想要只显示偶数月份?想要每隔6个月显示一次?统统没问题,让时间选择更加精准高效!

html
js
import { ref } from'vue'; exportdefault { setup() { const currentDate = ref(['2021', '01']); const columnsType = ['year', 'month']; constfilter = (type, options) => { if (type === 'month') { return options.filter((option) =>Number(option.value) % 6 === 0); } return options; }; return { filter, minDate: newDate(2020, 0, 1), maxDate: newDate(2025, 5, 1), currentTime, columnsType, }; }, };

API

Props

参数说明类型默认值
v-model当前选中的日期string[][]
columns-type选项类型,由 yearmonthday 组成的数组string[]['year', 'month', 'day']
min-date可选的最小时间,精确到日Date十年前
max-date可选的最大时间,精确到日Date十年后
title顶部栏标题string''
confirm-button-text确认按钮文字string确认
cancel-button-text取消按钮文字string取消
show-toolbar是否显示顶部栏booleantrue
loading是否显示加载状态booleanfalse
readonly是否为只读状态,只读状态下无法切换选项booleanfalse
filter选项过滤函数(type: string, options: PickerOption[], values: string[]) => PickerOption[]-
formatter选项格式化函数(type: string, option: PickerOption) => PickerOption-
option-height选项高度,支持 px``vw``vh``rem 单位,默认 px*numberstring*
visible-option-num可见的选项个数*numberstring*
swipe-duration快速滑动时惯性滚动的时长,单位 ms*numberstring*

Events

事件名说明回调参数
confirm点击完成按钮时触发{ selectedValues, selectedOptions, selectedIndexes }
cancel点击取消按钮时触发{ selectedValues, selectedOptions, selectedIndexes }
change选项改变时触发{ selectedValues, selectedOptions, selectedIndexes, columnIndex }

Slots

名称说明参数
toolbar自定义整个顶部栏的内容-
title自定义标题内容-
confirm自定义确认按钮内容-
cancel自定义取消按钮内容-
option自定义选项内容option: PickerOption, index: number
columns-top自定义选项上方内容-
columns-bottom自定义选项下方内容-

方法

通过 ref 可以获取到 Picker 实例并调用实例方法,详见组件实例方法

方法名说明参数返回值
confirm停止惯性滚动并触发 confirm 事件--
getSelectedDate获取当前选中的日期-string[]

类型定义

组件导出以下类型定义:

ts
importtype { DatePickerProps, DatePickerColumnType, DatePickerInstance, } from'vant';

DatePickerInstance 是组件实例的类型,用法如下:

ts
import { ref } from'vue'; importtype { DatePickerInstance } from'vant'; const datePickerRef = ref<DatePickerInstance>(); datePickerRef.value?.confirm();

❓ 常见问题

设置 min-date 或 max-date 后出现页面卡死的情况?

请注意不要在模板中直接使用类似 min-date="new Date()" 的写法,这样会导致每次渲染组件时传入一个新的 Date 对象,而传入新的数据会触发下一次渲染,从而陷入死循环。

正确的做法是将 min-date 作为一个数据定义在 data 函数或 setup 中。

在 iOS 系统上初始化组件失败?

如果你遇到了在 iOS 上无法渲染组件的问题,请确认在创建 Date 对象时没有使用 new Date('2020-01-01') 这样的写法,iOS 不支持以中划线分隔的日期格式,正确写法是 new Date('2020/01/01')

对此问题的详细解释:stackoverflow

在桌面端无法操作组件?

参见桌面端适配

🌟 最佳实践

日期选择器与弹窗的完美结合

vue
<template>
  <div class="date-picker-demo">
    <!-- 触发按钮 -->
    <van-cell 
      title="选择日期" 
      :value="formatDate(selectedDate)" 
      is-link 
      @click="showPicker = true"
    />
    
    <!-- 日期选择弹窗 -->
    <van-popup v-model:show="showPicker" position="bottom">
      <van-date-picker
        v-model="selectedDate"
        :min-date="minDate"
        :max-date="maxDate"
        :formatter="formatter"
        @confirm="onConfirm"
        @cancel="showPicker = false"
      />
    </van-popup>
  </div>
</template>

<script setup>
import { ref } from 'vue';

const showPicker = ref(false);
const selectedDate = ref(['2024', '01', '01']);

// 设置合理的时间范围
const minDate = new Date(2020, 0, 1);
const maxDate = new Date(2030, 11, 31);

// 格式化显示
const formatter = (type, option) => {
  const suffixMap = {
    year: '年',
    month: '月',
    day: '日'
  };
  option.text += suffixMap[type] || '';
  return option;
};

// 确认选择
const onConfirm = ({ selectedValues }) => {
  selectedDate.value = selectedValues;
  showPicker.value = false;
};

// 格式化显示日期
const formatDate = (date) => {
  if (!date || date.length < 3) return '请选择日期';
  return `${date[0]}年${date[1]}月${date[2]}日`;
};
</script>

响应式日期范围设置

javascript
// 动态设置日期范围
const setupDateRange = (type) => {
  const now = new Date();
  const ranges = {
    // 生日选择:100年前到今天
    birthday: {
      min: new Date(now.getFullYear() - 100, 0, 1),
      max: now
    },
    // 预约选择:今天到30天后
    appointment: {
      min: now,
      max: new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000)
    },
    // 历史记录:10年前到今天
    history: {
      min: new Date(now.getFullYear() - 10, 0, 1),
      max: now
    },
    // 计划安排:今天到1年后
    planning: {
      min: now,
      max: new Date(now.getFullYear() + 1, now.getMonth(), now.getDate())
    }
  };
  
  return ranges[type] || ranges.appointment;
};

// 使用示例
const { min: minDate, max: maxDate } = setupDateRange('birthday');

智能默认值设置

javascript
// 智能设置默认日期
const getSmartDefaultDate = (scenario) => {
  const now = new Date();
  const year = now.getFullYear().toString();
  const month = (now.getMonth() + 1).toString().padStart(2, '0');
  const day = now.getDate().toString().padStart(2, '0');
  
  const scenarios = {
    // 生日场景:默认30年前
    birthday: [(year - 30).toString(), month, day],
    // 预约场景:默认明天
    appointment: [
      year, 
      month, 
      (now.getDate() + 1).toString().padStart(2, '0')
    ],
    // 纪念日场景:默认今天
    anniversary: [year, month, day],
    // 计划场景:默认下周
    planning: [
      year,
      month,
      (now.getDate() + 7).toString().padStart(2, '0')
    ]
  };
  
  return scenarios[scenario] || [year, month, day];
};

💡 使用技巧

多语言日期格式化

javascript
// 国际化日期格式化
const createI18nFormatter = (locale = 'zh-CN') => {
  const formatters = {
    'zh-CN': {
      year: (text) => `${text}年`,
      month: (text) => `${text}月`,
      day: (text) => `${text}日`
    },
    'en-US': {
      year: (text) => text,
      month: (text) => new Date(2000, parseInt(text) - 1).toLocaleDateString('en-US', { month: 'short' }),
      day: (text) => `${text}${getOrdinalSuffix(parseInt(text))}`
    },
    'ja-JP': {
      year: (text) => `${text}年`,
      month: (text) => `${text}月`,
      day: (text) => `${text}日`
    }
  };
  
  const currentFormatter = formatters[locale] || formatters['zh-CN'];
  
  return (type, option) => {
    if (currentFormatter[type]) {
      option.text = currentFormatter[type](option.text);
    }
    return option;
  };
};

// 英文序数词后缀
const getOrdinalSuffix = (num) => {
  const j = num % 10;
  const k = num % 100;
  if (j === 1 && k !== 11) return 'st';
  if (j === 2 && k !== 12) return 'nd';
  if (j === 3 && k !== 13) return 'rd';
  return 'th';
};

// 使用示例
const formatter = createI18nFormatter('en-US');

高级过滤功能

javascript
// 工作日过滤器
const createWorkdayFilter = () => {
  return (type, options) => {
    if (type === 'day') {
      return options.filter(option => {
        const date = new Date(2024, 0, parseInt(option.value)); // 使用2024年1月作为基准
        const dayOfWeek = date.getDay();
        return dayOfWeek !== 0 && dayOfWeek !== 6; // 排除周末
      });
    }
    return options;
  };
};

// 特殊日期过滤器
const createSpecialDateFilter = (excludeDates = []) => {
  return (type, options, values) => {
    if (type === 'day' && values[0] && values[1]) {
      const year = parseInt(values[0]);
      const month = parseInt(values[1]) - 1;
      
      return options.filter(option => {
        const day = parseInt(option.value);
        const dateStr = `${year}-${(month + 1).toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`;
        return !excludeDates.includes(dateStr);
      });
    }
    return options;
  };
};

// 季度过滤器
const createQuarterFilter = () => {
  return (type, options) => {
    if (type === 'month') {
      // 只显示每季度第一个月:1月、4月、7月、10月
      return options.filter(option => {
        const month = parseInt(option.value);
        return [1, 4, 7, 10].includes(month);
      });
    }
    return options;
  };
};

动态列类型切换

vue
<template>
  <div class="dynamic-date-picker">
    <!-- 模式选择 -->
    <van-radio-group v-model="pickerMode" direction="horizontal">
      <van-radio name="full">完整日期</van-radio>
      <van-radio name="yearMonth">年月</van-radio>
      <van-radio name="monthDay">月日</van-radio>
      <van-radio name="year">仅年份</van-radio>
    </van-radio-group>
    
    <!-- 日期选择器 -->
    <van-date-picker
      v-model="selectedDate"
      :columns-type="currentColumnsType"
      :formatter="formatter"
      @change="onDateChange"
    />
  </div>
</template>

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

const pickerMode = ref('full');
const selectedDate = ref(['2024', '01', '01']);

// 根据模式动态设置列类型
const currentColumnsType = computed(() => {
  const modeMap = {
    full: ['year', 'month', 'day'],
    yearMonth: ['year', 'month'],
    monthDay: ['month', 'day'],
    year: ['year']
  };
  return modeMap[pickerMode.value];
});

// 监听模式变化,调整选中值
watch(pickerMode, (newMode) => {
  const now = new Date();
  const year = now.getFullYear().toString();
  const month = (now.getMonth() + 1).toString().padStart(2, '0');
  const day = now.getDate().toString().padStart(2, '0');
  
  const defaultValues = {
    full: [year, month, day],
    yearMonth: [year, month],
    monthDay: [month, day],
    year: [year]
  };
  
  selectedDate.value = defaultValues[newMode];
});

const formatter = (type, option) => {
  const suffixMap = { year: '年', month: '月', day: '日' };
  option.text += suffixMap[type] || '';
  return option;
};

const onDateChange = ({ selectedValues }) => {
  console.log('日期变化:', selectedValues);
};
</script>

🔧 常见问题解决

iOS 日期兼容性问题

javascript
// iOS 安全的日期创建方法
const createSafeDate = (year, month, day) => {
  // iOS 不支持 'YYYY-MM-DD' 格式,需要使用 'YYYY/MM/DD'
  const safeYear = year || new Date().getFullYear();
  const safeMonth = month || 1;
  const safeDay = day || 1;
  
  // 方法1:使用斜杠分隔
  return new Date(`${safeYear}/${safeMonth}/${safeDay}`);
  
  // 方法2:使用构造函数(推荐)
  // return new Date(safeYear, safeMonth - 1, safeDay);
};

// 日期范围设置的最佳实践
const setupDateRangeSafely = () => {
  // ❌ 错误写法 - 可能在iOS上失败
  // const minDate = new Date('2020-01-01');
  
  // ✅ 正确写法 - 跨平台兼容
  const minDate = new Date(2020, 0, 1); // 月份从0开始
  const maxDate = new Date(2030, 11, 31);
  
  return { minDate, maxDate };
};

性能优化技巧

javascript
// 大数据量优化
const optimizeForLargeData = () => {
  // 使用虚拟滚动减少DOM节点
  const visibleOptionNum = 5; // 减少可见选项数量
  
  // 延迟加载选项
  const lazyLoadOptions = (type, currentValues) => {
    if (type === 'year') {
      // 只加载当前年份前后10年
      const currentYear = parseInt(currentValues[0]) || new Date().getFullYear();
      const startYear = currentYear - 10;
      const endYear = currentYear + 10;
      
      return Array.from({ length: endYear - startYear + 1 }, (_, i) => ({
        text: (startYear + i).toString(),
        value: (startYear + i).toString()
      }));
    }
    return [];
  };
  
  return { visibleOptionNum, lazyLoadOptions };
};

// 防抖优化
const createDebouncedHandler = (handler, delay = 300) => {
  let timeoutId;
  return (...args) => {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => handler(...args), delay);
  };
};

// 使用示例
const debouncedChange = createDebouncedHandler((values) => {
  console.log('日期变化:', values);
  // 执行复杂的业务逻辑
}, 300);

数据验证与错误处理

javascript
// 日期有效性验证
const validateDateSelection = (selectedValues, minDate, maxDate) => {
  if (!selectedValues || selectedValues.length === 0) {
    return { valid: false, error: '请选择日期' };
  }
  
  try {
    const [year, month, day] = selectedValues;
    const selectedDate = new Date(
      parseInt(year), 
      parseInt(month) - 1, 
      parseInt(day)
    );
    
    // 检查日期是否有效
    if (isNaN(selectedDate.getTime())) {
      return { valid: false, error: '无效的日期' };
    }
    
    // 检查是否在允许范围内
    if (minDate && selectedDate < minDate) {
      return { valid: false, error: '日期不能早于最小日期' };
    }
    
    if (maxDate && selectedDate > maxDate) {
      return { valid: false, error: '日期不能晚于最大日期' };
    }
    
    return { valid: true, date: selectedDate };
  } catch (error) {
    return { valid: false, error: '日期格式错误' };
  }
};

// 错误处理组件
const DatePickerWithValidation = {
  setup() {
    const selectedDate = ref([]);
    const errorMessage = ref('');
    
    const onConfirm = ({ selectedValues }) => {
      const validation = validateDateSelection(
        selectedValues, 
        minDate.value, 
        maxDate.value
      );
      
      if (validation.valid) {
        selectedDate.value = selectedValues;
        errorMessage.value = '';
        // 执行确认逻辑
      } else {
        errorMessage.value = validation.error;
        // 显示错误提示
        showToast(validation.error);
      }
    };
    
    return { selectedDate, errorMessage, onConfirm };
  }
};

🎨 设计灵感

主题化日期选择器

css
/* 春天主题 */
.date-picker-spring {
  --van-picker-option-text-color: #52c41a;
  --van-picker-option-selected-text-color: #389e0d;
  --van-picker-toolbar-height: 44px;
  --van-picker-action-text-color: #52c41a;
}

.date-picker-spring .van-picker__toolbar {
  background: linear-gradient(135deg, #a8e6cf 0%, #dcedc1 100%);
}

/* 夏天主题 */
.date-picker-summer {
  --van-picker-option-text-color: #1890ff;
  --van-picker-option-selected-text-color: #096dd9;
  --van-picker-action-text-color: #1890ff;
}

.date-picker-summer .van-picker__toolbar {
  background: linear-gradient(135deg, #87ceeb 0%, #98d8e8 100%);
}

/* 秋天主题 */
.date-picker-autumn {
  --van-picker-option-text-color: #fa8c16;
  --van-picker-option-selected-text-color: #d46b08;
  --van-picker-action-text-color: #fa8c16;
}

.date-picker-autumn .van-picker__toolbar {
  background: linear-gradient(135deg, #ffd89b 0%, #19547b 100%);
}

/* 冬天主题 */
.date-picker-winter {
  --van-picker-option-text-color: #722ed1;
  --van-picker-option-selected-text-color: #531dab;
  --van-picker-action-text-color: #722ed1;
}

.date-picker-winter .van-picker__toolbar {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}

动画效果增强

css
/* 选项切换动画 */
.date-picker-animated .van-picker-column__item {
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}

.date-picker-animated .van-picker-column__item--selected {
  transform: scale(1.1);
  font-weight: bold;
  text-shadow: 0 0 8px rgba(24, 144, 255, 0.3);
}

/* 工具栏动画 */
.date-picker-animated .van-picker__toolbar {
  animation: slideInDown 0.3s ease-out;
}

@keyframes slideInDown {
  from {
    transform: translateY(-100%);
    opacity: 0;
  }
  to {
    transform: translateY(0);
    opacity: 1;
  }
}

/* 选项列表动画 */
.date-picker-animated .van-picker__columns {
  animation: fadeInUp 0.4s ease-out;
}

@keyframes fadeInUp {
  from {
    transform: translateY(20px);
    opacity: 0;
  }
  to {
    transform: translateY(0);
    opacity: 1;
  }
}

/* 悬浮效果 */
.date-picker-floating {
  border-radius: 16px;
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
  backdrop-filter: blur(10px);
  background: rgba(255, 255, 255, 0.95);
}

创意交互效果

vue
<template>
  <div class="creative-date-picker">
    <!-- 3D 翻转效果 -->
    <div class="flip-container" :class="{ flipped: isFlipped }">
      <div class="flipper">
        <div class="front">
          <van-cell 
            title="选择日期" 
            :value="displayDate" 
            @click="showPicker"
          />
        </div>
        <div class="back">
          <van-date-picker
            v-model="selectedDate"
            @confirm="onConfirm"
            @cancel="hidePicker"
          />
        </div>
      </div>
    </div>
    
    <!-- 粒子效果背景 -->
    <div class="particles" v-if="showParticles">
      <div 
        v-for="i in 20" 
        :key="i" 
        class="particle"
        :style="getParticleStyle(i)"
      ></div>
    </div>
  </div>
</template>

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

const isFlipped = ref(false);
const showParticles = ref(false);
const selectedDate = ref(['2024', '01', '01']);

const displayDate = computed(() => {
  const [year, month, day] = selectedDate.value;
  return `${year}年${month}月${day}日`;
});

const showPicker = () => {
  isFlipped.value = true;
  showParticles.value = true;
};

const hidePicker = () => {
  isFlipped.value = false;
  showParticles.value = false;
};

const onConfirm = ({ selectedValues }) => {
  selectedDate.value = selectedValues;
  hidePicker();
};

const getParticleStyle = (index) => {
  const angle = (index * 18) % 360;
  const radius = 100 + Math.random() * 50;
  const x = Math.cos(angle * Math.PI / 180) * radius;
  const y = Math.sin(angle * Math.PI / 180) * radius;
  
  return {
    left: `calc(50% + ${x}px)`,
    top: `calc(50% + ${y}px)`,
    animationDelay: `${index * 0.1}s`
  };
};
</script>

<style scoped>
.flip-container {
  perspective: 1000px;
  width: 100%;
  height: 300px;
}

.flipper {
  transition: transform 0.6s;
  transform-style: preserve-3d;
  position: relative;
  width: 100%;
  height: 100%;
}

.flipped .flipper {
  transform: rotateY(180deg);
}

.front, .back {
  backface-visibility: hidden;
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
}

.back {
  transform: rotateY(180deg);
}

.particle {
  position: absolute;
  width: 4px;
  height: 4px;
  background: #1890ff;
  border-radius: 50%;
  animation: float 3s ease-in-out infinite;
}

@keyframes float {
  0%, 100% { transform: translateY(0px) scale(1); opacity: 1; }
  50% { transform: translateY(-20px) scale(1.2); opacity: 0.7; }
}
</style>

🚀 高级功能扩展

智能日期推荐系统

javascript
// 智能日期推荐引擎
class SmartDateRecommender {
  constructor() {
    this.userHistory = [];
    this.preferences = {};
  }
  
  // 记录用户选择历史
  recordSelection(date, context) {
    this.userHistory.push({
      date,
      context,
      timestamp: Date.now()
    });
    
    // 保持历史记录在合理范围内
    if (this.userHistory.length > 100) {
      this.userHistory.shift();
    }
    
    this.updatePreferences();
  }
  
  // 更新用户偏好
  updatePreferences() {
    const recentSelections = this.userHistory.slice(-20);
    
    // 分析偏好的星期几
    const dayOfWeekCount = {};
    recentSelections.forEach(({ date }) => {
      const dayOfWeek = new Date(date).getDay();
      dayOfWeekCount[dayOfWeek] = (dayOfWeekCount[dayOfWeek] || 0) + 1;
    });
    
    this.preferences.preferredDayOfWeek = Object.keys(dayOfWeekCount)
      .reduce((a, b) => dayOfWeekCount[a] > dayOfWeekCount[b] ? a : b);
    
    // 分析偏好的时间段
    const monthCount = {};
    recentSelections.forEach(({ date }) => {
      const month = new Date(date).getMonth();
      monthCount[month] = (monthCount[month] || 0) + 1;
    });
    
    this.preferences.preferredMonth = Object.keys(monthCount)
      .reduce((a, b) => monthCount[a] > monthCount[b] ? a : b);
  }
  
  // 生成智能推荐
  getRecommendations(context, count = 5) {
    const now = new Date();
    const recommendations = [];
    
    // 基于上下文的推荐
    switch (context) {
      case 'meeting':
        // 会议推荐:工作日,上午时间
        recommendations.push(...this.getWorkdayRecommendations(now, count));
        break;
      case 'birthday':
        // 生日推荐:基于历史生日选择
        recommendations.push(...this.getBirthdayRecommendations(count));
        break;
      case 'vacation':
        // 假期推荐:周末或节假日
        recommendations.push(...this.getVacationRecommendations(now, count));
        break;
      default:
        recommendations.push(...this.getGeneralRecommendations(now, count));
    }
    
    return recommendations.slice(0, count);
  }
  
  // 工作日推荐
  getWorkdayRecommendations(baseDate, count) {
    const recommendations = [];
    let date = new Date(baseDate);
    
    while (recommendations.length < count) {
      date.setDate(date.getDate() + 1);
      const dayOfWeek = date.getDay();
      
      // 跳过周末
      if (dayOfWeek !== 0 && dayOfWeek !== 6) {
        recommendations.push({
          date: new Date(date),
          reason: '工作日推荐',
          confidence: 0.8
        });
      }
    }
    
    return recommendations;
  }
  
  // 生日推荐
  getBirthdayRecommendations(count) {
    const recommendations = [];
    const currentYear = new Date().getFullYear();
    
    // 基于历史生日选择推荐
    const birthdayHistory = this.userHistory.filter(h => h.context === 'birthday');
    
    birthdayHistory.forEach(({ date }) => {
      const birthDate = new Date(date);
      const thisYearBirthday = new Date(currentYear, birthDate.getMonth(), birthDate.getDate());
      
      recommendations.push({
        date: thisYearBirthday,
        reason: '历史生日记录',
        confidence: 0.9
      });
    });
    
    return recommendations.slice(0, count);
  }
  
  // 假期推荐
  getVacationRecommendations(baseDate, count) {
    const recommendations = [];
    let date = new Date(baseDate);
    
    while (recommendations.length < count) {
      date.setDate(date.getDate() + 1);
      const dayOfWeek = date.getDay();
      
      // 推荐周末
      if (dayOfWeek === 0 || dayOfWeek === 6) {
        recommendations.push({
          date: new Date(date),
          reason: '周末推荐',
          confidence: 0.7
        });
      }
    }
    
    return recommendations;
  }
  
  // 通用推荐
  getGeneralRecommendations(baseDate, count) {
    const recommendations = [];
    
    // 明天
    const tomorrow = new Date(baseDate);
    tomorrow.setDate(tomorrow.getDate() + 1);
    recommendations.push({
      date: tomorrow,
      reason: '明天',
      confidence: 0.6
    });
    
    // 下周同一天
    const nextWeek = new Date(baseDate);
    nextWeek.setDate(nextWeek.getDate() + 7);
    recommendations.push({
      date: nextWeek,
      reason: '下周同一天',
      confidence: 0.5
    });
    
    // 下个月同一天
    const nextMonth = new Date(baseDate);
    nextMonth.setMonth(nextMonth.getMonth() + 1);
    recommendations.push({
      date: nextMonth,
      reason: '下个月同一天',
      confidence: 0.4
    });
    
    return recommendations.slice(0, count);
  }
}

// 使用示例
const recommender = new SmartDateRecommender();

// 在日期选择器中集成推荐功能
const DatePickerWithRecommendations = {
  setup() {
    const recommendations = ref([]);
    
    const loadRecommendations = (context) => {
      recommendations.value = recommender.getRecommendations(context);
    };
    
    const selectRecommendation = (recommendation) => {
      const date = recommendation.date;
      selectedDate.value = [
        date.getFullYear().toString(),
        (date.getMonth() + 1).toString().padStart(2, '0'),
        date.getDate().toString().padStart(2, '0')
      ];
      
      // 记录选择
      recommender.recordSelection(date, currentContext.value);
    };
    
    return {
      recommendations,
      loadRecommendations,
      selectRecommendation
    };
  }
};

多日期选择器

vue
<template>
  <div class="multi-date-picker">
    <div class="selected-dates">
      <h3>已选择的日期 ({{ selectedDates.length }})</h3>
      <div class="date-chips">
        <van-tag
          v-for="(date, index) in selectedDates"
          :key="index"
          closeable
          @close="removeDate(index)"
        >
          {{ formatDate(date) }}
        </van-tag>
      </div>
    </div>
    
    <van-date-picker
      v-model="currentDate"
      @confirm="addDate"
      :min-date="minDate"
      :max-date="maxDate"
    />
    
    <div class="actions">
      <van-button @click="clearAll" type="default">清空所有</van-button>
      <van-button @click="confirmSelection" type="primary">确认选择</van-button>
    </div>
  </div>
</template>

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

const selectedDates = ref([]);
const currentDate = ref(['2024', '01', '01']);
const minDate = new Date(2020, 0, 1);
const maxDate = new Date(2030, 11, 31);

const addDate = ({ selectedValues }) => {
  const dateStr = selectedValues.join('-');
  
  // 检查是否已存在
  const exists = selectedDates.value.some(date => 
    date.join('-') === dateStr
  );
  
  if (!exists) {
    selectedDates.value.push([...selectedValues]);
    selectedDates.value.sort((a, b) => {
      const dateA = new Date(a[0], a[1] - 1, a[2]);
      const dateB = new Date(b[0], b[1] - 1, b[2]);
      return dateA - dateB;
    });
  } else {
    showToast('该日期已选择');
  }
};

const removeDate = (index) => {
  selectedDates.value.splice(index, 1);
};

const clearAll = () => {
  selectedDates.value = [];
};

const confirmSelection = () => {
  if (selectedDates.value.length === 0) {
    showToast('请至少选择一个日期');
    return;
  }
  
  emit('confirm', selectedDates.value);
};

const formatDate = (date) => {
  return `${date[0]}年${date[1]}月${date[2]}日`;
};
</script>

<style scoped>
.selected-dates {
  padding: 16px;
  background: #f7f8fa;
  margin-bottom: 16px;
}

.date-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 8px;
}

.actions {
  display: flex;
  gap: 16px;
  padding: 16px;
}
</style>

📚 延伸阅读

技术文档

设计指南

用户体验

相关组件

基於Vant構建的企業級移動端解決方案