酒店入住、机票往返、考勤补卡、预约上门……凡是「选日期」的需求,Vant Calendar 日历几乎都能接住。但它的参数分散在「选择模式」「弹层配置」「区间配置」三组里,加上 confirm 返回的有时是 Date、有时是 Date[],初次上手很容易写错。本文按单选、多选、区间三条主线,把数据结构、formatter 定制和与表单的集成一次讲透。
三种选择模式与返回值
type 决定一切:默认 single,另有 multiple 与 range。它同时决定 confirm 事件的参数类型,这是最容易踩的坑。
| type | confirm 参数 | 默认值 / 相关属性 | 典型场景 |
|---|---|---|---|
single(默认) | Date | default-date 为今天 | 预约日期、生日 |
multiple | Date[] | max-range 限制最多选几天 | 排班、多选打卡日 |
range | [Date, Date] | max-range、allow-same-day | 入住/离店、起止日期 |
switch-mode="none"(平铺全部月份)下,min-date 默认是今天,max-date 默认是六个月后。想选历史日期,必须显式传 :min-date;否则日历上根本不会出现那些月份。单选:Cell + 弹层的最小闭环
v-model:show 控制弹层显隐,选中后由 confirm 统一收口。注意 Date 的月份是从 0 开始的,格式化时要 +1。
<script setup>
import { ref } from 'vue';
const show = ref(false);
const dateText = ref('');
const formatDate = (date: Date) =>
`${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(
date.getDate(),
).padStart(2, '0')}`;
const onConfirm = (value: Date) => {
dateText.value = formatDate(value);
show.value = false;
};
</script>
<template>
<van-cell title="预约日期" :value="dateText || '请选择'" is-link @click="show = true" />
<van-calendar v-model:show="show" title="选择日期" @confirm="onConfirm" />
</template>
想让选完立刻关闭、不要底部确认按钮,加 :show-confirm="false";此时 confirm 会在选中的瞬间触发。标题、按钮文案、主题色分别由 title、confirm-text / confirm-disabled-text、color 控制。
区间选择:起止日期、天数上限与同日
酒店场景里,「住 1 晚」意味着起止是两天,但有些业务允许当天往返。Vant 默认不允许起止同一天,需要 allow-same-day 打开。
<script setup>
import { ref } from 'vue';
const show = ref(false);
const rangeText = ref('');
const minDate = new Date(2026, 0, 1);
const maxDate = new Date(2026, 11, 31);
const onConfirm = (values: Date[]) => {
const [start, end] = values;
const fmt = (d: Date) => `${d.getMonth() + 1}月${d.getDate()}日`;
const nights = Math.round((end.getTime() - start.getTime()) / 86400000);
rangeText.value = `${fmt(start)} - ${fmt(end)}(${nights} 晚)`;
show.value = false;
};
</script>
<template>
<van-cell title="入住 / 离店" :value="rangeText || '请选择'" is-link @click="show = true" />
<van-calendar
v-model:show="show"
type="range"
:min-date="minDate"
:max-date="maxDate"
:max-range="30"
range-prompt="最多选择 30 天"
:allow-same-day="false"
confirm-text="确定"
confirm-disabled-text="请选择离店日期"
color="#1989fa"
@confirm="onConfirm"
@over-range="() => console.log('超出可选天数')"
/>
</template>
区间模式专属属性还有 show-range-prompt(超出时是否提示)与 over-range 事件;多选模式下 max-range 的含义变成「最多选几天」,另有 unselect 事件在取消某个日期时触发。
formatter:给日期加节假日与业务标记
formatter 接收一个 Day 对象、并必须把它返回。可改的字段包括 text(主文案)、topInfo / bottomInfo(上下角标)、className;type 与 date 用于判断,不要随手覆盖。
| Day 字段 | 类型 | 说明 |
|---|---|---|
date | Date | 当前格子的日期 |
type | string | selected / start / middle / end / disabled / start-end 等 |
text / topInfo / bottomInfo | string | 主文案与上下角标 |
className | string | 给格子挂自定义 class |
import type { CalendarDayItem } from 'vant';
const holidayMap: Record<string, string> = {
'1-1': '元旦',
'5-1': '劳动节',
'10-1': '国庆',
};
export function formatter(day: CalendarDayItem): CalendarDayItem {
const month = day.date.getMonth() + 1;
const date = day.date.getDate();
const key = `${month}-${date}`;
if (holidayMap[key]) day.topInfo = holidayMap[key];
if (month === new Date().getMonth() + 1 && date === new Date().getDate()) {
day.text = '今天';
}
// 区间模式下给起止日打业务角标
if (day.type === 'start') day.bottomInfo = '入住';
else if (day.type === 'end') day.bottomInfo = '离店';
else if (day.type === 'start-end') day.bottomInfo = '当日往返';
if (day.type === 'disabled') day.className = 'day-disabled';
return day;
}
<van-calendar
v-model:show="show"
type="range"
:formatter="formatter"
first-day-of-week="1"
@confirm="onConfirm"
/>
first-day-of-week 取值是 0-6(0 = 周日)。中文站点通常设成 1 让周一排第一;它只影响表头顺序,不改变 Date 的计算结果。平铺展示与按月切换
日历不一定非得是弹层。设置 :poppable="false" 后组件直接渲染在页面里,需要自己给容器高度;配合 Vant 4.9.0 新增的 switch-mode,可以只渲染单月并给出上/下月、上/下年切换按钮,避免一次性铺开六个月带来的性能开销。
<van-calendar
title="考勤日历"
:poppable="false"
:show-confirm="false"
switch-mode="year-month"
:min-date="minDate"
:max-date="maxDate"
:style="{ height: '500px' }"
:formatter="formatter"
@panel-change="({ date }) => loadMonthData(date)"
@select="onSelect"
/>
| switch-mode | 行为 |
|---|---|
none(默认) | 平铺展示 min-date 到 max-date 的全部月份,无切换按钮 |
month | 只显示单月,提供上/下月按钮 |
year-month | 单月 + 上/下月 + 上/下年按钮 |
poppable=true 时还能用 position 改成 top / left / right 弹出,或用 teleport 挂载到指定容器。实例方法 reset(date?)、scrollToDate(date)、getSelectedDate() 适合做「回到今天」「清空选择」这类工具栏按钮。
与 Form 集成:只读 Field + 校验
表单里通常把日历当成只读输入框:Field 只负责显示与校验,Calendar 负责取值。name 决定提交时的字段名,:rules 复用 Form 的校验体系。
<script setup>
import { ref } from 'vue';
const show = ref(false);
const dateText = ref('');
const onConfirm = (value: Date) => {
dateText.value = value.toLocaleDateString('zh-CN');
show.value = false;
};
const onSubmit = (values: Record<string, unknown>) => {
console.log('submit', values); // { visitDate: '2026/11/8' }
};
</script>
<template>
<van-form @submit="onSubmit">
<van-cell-group inset>
<van-field
v-model="dateText"
name="visitDate"
label="上门日期"
readonly
is-link
placeholder="请选择"
:rules="[{ required: true, message: '请选择上门日期' }]"
@click="show = true"
/>
</van-cell-group>
<van-calendar v-model:show="show" :min-date="new Date()" @confirm="onConfirm" />
<div style="margin: 16px">
<van-button round block type="primary" native-type="submit">提交</van-button>
</div>
</van-form>
</template>
常见坑速查
| 现象 | 原因 | 处理 |
|---|---|---|
| 历史日期不可选 | 默认 min-date 是今天 | 显式传 :min-date="new Date(2020, 0, 1)" |
confirm 里取 getMonth() 少一月 | JS 月份从 0 开始 | 展示时 +1,构造 Date 时 -1 |
| 选完不关闭弹层 | 没在 confirm 里把 show 置 false | 确认回调中显式关闭,或关闭确认按钮 |
| 六个月日历首屏卡顿 | switch-mode="none" 会渲染全部月份 | 改用 month / year-month |
| formatter 改了却没效果 | 函数没有 return day | 所有分支结束后 return day |
| 当天往返选不了 | 区间模式默认禁止起止同日 | 加 allow-same-day |
总结
Calendar 的心智模型可以压缩成三句话:type 决定返回值形状,min-date/max-date 决定可渲染的月份,formatter 决定每一格长什么样。把这三处定好,剩下的弹层文案、平铺切换、表单集成都是配置活。
日期组件里最贵的不是交互,是时区与月份下标。
—— 先把 Date 格式化成一个函数,再谈业务