酒店入住、机票往返、考勤补卡、预约上门……凡是「选日期」的需求,Vant Calendar 日历几乎都能接住。但它的参数分散在「选择模式」「弹层配置」「区间配置」三组里,加上 confirm 返回的有时是 Date、有时是 Date[],初次上手很容易写错。本文按单选、多选、区间三条主线,把数据结构、formatter 定制和与表单的集成一次讲透。

三种选择模式与返回值

type 决定一切:默认 single,另有 multiple 与 range。它同时决定 confirm 事件的参数类型,这是最容易踩的坑。

typeconfirm 参数默认值 / 相关属性典型场景
single(默认)Datedefault-date 为今天预约日期、生日
multipleDate[]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。

vue SingleDate.vue
<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 打开。

vue DateRange.vue
<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 字段类型说明
dateDate当前格子的日期
typestringselected / start / middle / end / disabled / start-end 等
text / topInfo / bottomInfostring主文案与上下角标
classNamestring给格子挂自定义 class
ts formatter.ts
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;
}
html 挂载 formatter
<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,可以只渲染单月并给出上/下月、上/下年切换按钮,避免一次性铺开六个月带来的性能开销。

html 页面内嵌日历
<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 的校验体系。

vue 表单中的日期字段
<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 格式化成一个函数,再谈业务