DatePicker 日期选择 - Vant 4
📅 DatePicker 日期选择
📆 介绍
🎯 时光穿梭的魔法师,让日期选择变得如此优雅!
DatePicker 就像一位贴心的时间管家 ⏰,轻松帮你在时光长河中精准定位任意时刻!无论是选择生日 🎂、预约时间 📅,还是设定重要纪念日 💝,它都能以最优雅的方式呈现。
✨ 核心特色:
- 🎨 灵活组合:年、月、日任意搭配,想怎么选就怎么选
- 🎯 精准控制:时间范围随心设定,不会让你选到"史前时代"
- 🎪 完美搭档:与弹出层组件天作之合,用户体验满分
- 🎭 个性定制:格式化、过滤功能应有尽有,打造专属时间选择器
📦 引入
通过以下方式来全局注册组件,更多注册方式请参考组件注册。
import { createApp } from'vue'; import { DatePicker } from'vant'; const app = createApp(); app.use(DatePicker);🎯 代码演示
🔧 基础用法 - 时光机的第一次启动
🚀 简单三步,开启你的时间之旅!
就像操控一台精密的时光机器,通过 v-model 与当前时刻建立神秘连接 🔗,再用 min-date 和 max-date 设定时空边界 🌌,防止意外穿越到恐龙时代或遥远未来!
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']- 月日组合,适合选择生日(不关心年份)
想怎么搭配就怎么搭配,时间选择从此告别束缚!
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月",时间显示从此更加优雅动人!
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个月显示一次?统统没问题,让时间选择更加精准高效!
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 | 选项类型,由 year、month 和 day 组成的数组 | string[] | ['year', 'month', 'day'] |
| min-date | 可选的最小时间,精确到日 | Date | 十年前 |
| max-date | 可选的最大时间,精确到日 | Date | 十年后 |
| title | 顶部栏标题 | string | '' |
| confirm-button-text | 确认按钮文字 | string | 确认 |
| cancel-button-text | 取消按钮文字 | string | 取消 |
| show-toolbar | 是否显示顶部栏 | boolean | true |
| loading | 是否显示加载状态 | boolean | false |
| readonly | 是否为只读状态,只读状态下无法切换选项 | boolean | false |
| filter | 选项过滤函数 | (type: string, options: PickerOption[], values: string[]) => PickerOption[] | - |
| formatter | 选项格式化函数 | (type: string, option: PickerOption) => PickerOption | - |
| option-height | 选项高度,支持 px``vw``vh``rem 单位,默认 px | *number | string* |
| visible-option-num | 可见的选项个数 | *number | string* |
| swipe-duration | 快速滑动时惯性滚动的时长,单位 ms | *number | string* |
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[] |
类型定义
组件导出以下类型定义:
importtype { DatePickerProps, DatePickerColumnType, DatePickerInstance, } from'vant';DatePickerInstance 是组件实例的类型,用法如下:
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。
在桌面端无法操作组件?
参见桌面端适配。
🌟 最佳实践
日期选择器与弹窗的完美结合
<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>响应式日期范围设置
// 动态设置日期范围
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');智能默认值设置
// 智能设置默认日期
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];
};💡 使用技巧
多语言日期格式化
// 国际化日期格式化
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');高级过滤功能
// 工作日过滤器
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;
};
};动态列类型切换
<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 日期兼容性问题
// 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 };
};性能优化技巧
// 大数据量优化
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);数据验证与错误处理
// 日期有效性验证
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 };
}
};🎨 设计灵感
主题化日期选择器
/* 春天主题 */
.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%);
}动画效果增强
/* 选项切换动画 */
.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);
}创意交互效果
<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>🚀 高级功能扩展
智能日期推荐系统
// 智能日期推荐引擎
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
};
}
};多日期选择器
<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>📚 延伸阅读
技术文档
- Date 对象详解 - JavaScript 日期处理
- Intl.DateTimeFormat - 国际化日期格式
- CSS 时间函数 - CSS 时间相关函数
设计指南
- 移动端日期选择设计 - Material Design 日期选择器
- iOS 日期选择器指南 - Apple 设计规范
- 无障碍日期选择 - ARIA 日期选择器模式
用户体验
相关组件
- TimePicker 时间选择 - 时间选择组件
- Picker 选择器 - 基础选择器组件
- Calendar 日历 - 日历组件
- Popup 弹出层 - 弹出层组件
- Cell 单元格 - 单元格组件
- Field 输入框 - 表单输入组件
- Form 表单 - 表单组件