9.7k

日历

上一页下一页

一个允许用户输入和编辑日期的日期字段组件。

4月
2026
活动日期,2026年4月
<script setup lang="ts">
import type { DateValue } from '@internationalized/date'
import { CalendarDate, fromDate, getLocalTimeZone } from '@internationalized/date'
import { Calendar } from '@/components/ui/calendar'

const date = ref(fromDate(new Date(), getLocalTimeZone())) as Ref<DateValue>
</script>

<template>
  <Calendar
    v-model="date"
    class="rounded-md border shadow-sm"
    layout="month-and-year"
    :min-value="new CalendarDate(1925, 1, 1)"
    :max-value="new CalendarDate(2035, 1, 1)"
  />
</template>

关于

<Calendar /> 组件构建在 Reka UI Calendar 组件之上,该组件使用 @internationalized/date 包来处理日期。

如果您正在寻找范围日历,请查看 Range Calendar 组件。

安装

pnpm dlx shadcn-vue@latest add calendar

使用方法

<script setup lang="ts">
import { Calendar } from '@/components/ui/calendar'
</script>

<template>
  <Calendar />
</template>

日历系统(例如:波斯历 / 回历 / 贾拉里历)

@internationalized/date 支持 13 种日历系统。在此,我们将以波斯历为例,展示如何将日历系统与 <Calendar /> 组件或任何其他日历组件一起使用。

默认的日历系统是 gregory(格里高利历)。
若要使用其他日历系统,您需要通过 defaultPlaceholderplaceholder 属性提供带有所需系统的值。

即使您不使用其他日历系统,也建议在组件中添加 placeholderdefaultPlaceholder

<script setup lang="ts">
import type { DateValue } from '@internationalized/date'
import { getLocalTimeZone, PersianCalendar, toCalendar, today } from '@internationalized/date'
import { Calendar } from '@/registry/new-york-v4/ui/calendar'

const date = ref(today(getLocalTimeZone())) as Ref<DateValue> // no need to add calendar identifier to modelValue when using placeholder

const placeholder = ref(toCalendar(today(getLocalTimeZone()), new PersianCalendar())) as Ref<DateValue>
// or
const defaultPlaceholder = toCalendar(today(getLocalTimeZone()))
</script>

<template>
  <Calendar
    v-model="date"
    v-model:placeholder="placeholder"
    locale="fa-IR"
  />
  <!-- or -->
  <Calendar
    v-model="date"
    :default-placeholder="placeholder"
    locale="fa-IR"
  />
</template>

如果未提供这些属性,则触发的日期默认将使用 gregorian(格里高利历)日历,因为它是最广泛使用的系统。

从日历组件触发的值将根据指定的日历系统标识符而有所不同。

您还可以使用 locale 属性更改区域设置,以匹配日历系统界面。

<script setup lang="ts">
import {
  CalendarDate,
  fromDate,
  getLocalTimeZone,
  parseDate,
  PersianCalendar,
  toCalendar,
  today
} from '@internationalized/date'
import { ref } from 'vue'

const date = ref(toCalendar(new CalendarDate(2025, 1, 1), new PersianCalendar()))
// or
const date = ref(toCalendar(parseDate('2022-02-03'), new PersianCalendar()))
// or
const date = ref(toCalendar(today(getLocalTimeZone()), new PersianCalendar()))
// or
const date = ref(new CalendarDate(new PersianCalendar(), 1404, 1, 1))
// or
const date = ref(toCalendar(fromDate(new Date(), getLocalTimeZone()), new PersianCalendar()))

const placeholder = ref(toCalendar(today(getLocalTimeZone()), new PersianCalendar()))
</script>

<template>
  <Calendar
    v-model="date"
    v-model:placeholder="placeholder"
    locale="fa-IR"
    dir="rtl"
  />
</template>
Farvardin
۱۴۰۵
活动日期,1405年Farvardin
1405/1/22
Farvardin 1405
<script setup lang="ts">
import type { DateValue } from '@internationalized/date'
import { getLocalTimeZone, PersianCalendar, toCalendar, today } from '@internationalized/date'
import { ChevronLeft, ChevronRight } from 'lucide-vue-next'
import { useDateFormatter } from 'reka-ui'
import { toDate } from 'reka-ui/date'
import { Calendar } from '@/components/ui/calendar'

const date = ref(today(getLocalTimeZone())) as Ref<DateValue>
const placeholder = ref(toCalendar(today(getLocalTimeZone()), new PersianCalendar())) as Ref<DateValue>
// or
const defaultPlaceholder = toCalendar(today(getLocalTimeZone()), new PersianCalendar())

const formatter = useDateFormatter('fa')
</script>

<template>
  <div class="**:data-[slot=native-select-icon]:right-[unset] **:data-[slot=native-select-icon]:left-3.5">
    <Calendar
      v-model="date"
      v-model:placeholder="placeholder"
      locale="fa-IR"
      layout="month-and-year"
      class="rounded-md border shadow-sm"
      dir="rtl"
    >
      <template #calendar-next-icon>
        <ChevronLeft />
      </template>

      <template #calendar-prev-icon>
        <ChevronRight />
      </template>
    </Calendar>

    <div class="flex flex-col justify-center items-center gap-2">
      <div>
        {{
          formatter.custom(
            toDate(date, getLocalTimeZone()), {
              numberingSystem: 'latn',
            })
        }}
      </div>

      <div>
        {{ formatter.custom(date.toDate(getLocalTimeZone()), { month: 'short', year: 'numeric' }) }}
      </div>
    </div>
  </div>
</template>

示例

日历系统

createCalendar 导入到您的项目中,将会把所有可用的日历都包含在您的 bundle 中。如果您希望限制支持的日历以减小 bundle 大小,您可以创建自己的实现,只导入所需的类。这样,您的打包器就可以通过 tree-shaking 移除未使用的日历实现。

请查阅 @internationalized/date,特别是关于 日历标识符 的部分。

import { GregorianCalendar, JapaneseCalendar } from '@internationalized/date'

function createCalendar(identifier) {
  switch (identifier) {
    case 'gregory':
      return new GregorianCalendar()
    case 'japanese':
      return new JapaneseCalendar()
    default:
      throw new Error(`Unsupported calendar ${identifier}`)
  }
}
2026年4月
活动日期,2026年4月
<script setup lang="ts">
import type { CalendarIdentifier, DateValue } from '@internationalized/date'
import { createCalendar, getLocalTimeZone, toCalendar, today } from '@internationalized/date'
import { Calendar } from '@/components/ui/calendar'
import { Label } from '@/components/ui/label'
import {
  Select,
  SelectContent,
  SelectGroup,
  SelectItem,
  SelectLabel,
  SelectSeparator,
  SelectTrigger,
  SelectValue,
} from '@/components/ui/select'

const date = ref(today(getLocalTimeZone())) as Ref<DateValue>

const preferences = [
  { locale: 'en-US', label: 'Default', ordering: 'gregory' },
  { label: 'Arabic (Algeria)', locale: 'ar-DZ', territories: 'DJ DZ EH ER IQ JO KM LB LY MA MR OM PS SD SY TD TN YE', ordering: 'gregory islamic islamic-civil islamic-tbla' },
  { label: 'Arabic (United Arab Emirates)', locale: 'ar-AE', territories: 'AE BH KW QA', ordering: 'gregory islamic-umalqura islamic islamic-civil islamic-tbla' },
  { label: 'Arabic (Egypt)', locale: 'AR-EG', territories: 'EG', ordering: 'gregory coptic islamic islamic-civil islamic-tbla' },
  { label: 'Arabic (Saudi Arabia)', locale: 'ar-SA', territories: 'SA', ordering: 'islamic-umalqura gregory islamic islamic-rgsa' },
  { label: 'Farsi (Iran)', locale: 'fa-IR', territories: 'IR', ordering: 'persian gregory islamic islamic-civil islamic-tbla' },
  { label: 'Farsi (Afghanistan)', locale: 'fa-AF', territories: 'AF IR', ordering: 'persian gregory islamic islamic-civil islamic-tbla' },
  { label: 'Amharic (Ethiopia)', locale: 'am-ET', territories: 'ET', ordering: 'gregory ethiopic ethioaa' },
  { label: 'Hebrew (Israel)', locale: 'he-IL', territories: 'IL', ordering: 'gregory hebrew islamic islamic-civil islamic-tbla' },
  { label: 'Hindi (India)', locale: 'hi-IN', territories: 'IN', ordering: 'gregory indian' },
  { label: 'Japanese (Japan)', locale: 'ja-JP', territories: 'JP', ordering: 'gregory japanese' },
  { label: 'Thai (Thailand)', locale: 'th-TH', territories: 'TH', ordering: 'buddhist gregory' },
  { label: 'Chinese (Taiwan)', locale: 'zh-TW', territories: 'TW', ordering: 'gregory roc chinese' },
]

const calendars = [
  { key: 'gregory', name: 'Gregorian' },
  { key: 'japanese', name: 'Japanese' },
  { key: 'buddhist', name: 'Buddhist' },
  { key: 'roc', name: 'Taiwan' },
  { key: 'persian', name: 'Persian' },
  { key: 'indian', name: 'Indian' },
  { key: 'islamic-umalqura', name: 'Islamic (Umm al-Qura)' },
  { key: 'islamic-civil', name: 'Islamic Civil' },
  { key: 'islamic-tbla', name: 'Islamic Tabular' },
  { key: 'hebrew', name: 'Hebrew' },
  { key: 'coptic', name: 'Coptic' },
  { key: 'ethiopic', name: 'Ethiopic' },
  { key: 'ethioaa', name: 'Ethiopic (Amete Alem)' },
]

const locale = ref(preferences[0]?.locale)
const calendar = ref(calendars[0]?.key) as Ref<CalendarIdentifier>

const pref = computed(() => preferences.find(p => p.locale === locale.value))
const preferredCalendars = computed(() => pref.value ? pref.value.ordering.split(' ').map(p => calendars.find(c => c.key === p)).filter(Boolean) : [calendars[0]])
const otherCalendars = computed(() => calendars.filter(c => !preferredCalendars.value.some(p => p!.key === c.key)))

function updateLocale(newLocale: any) {
  locale.value = newLocale
  calendar.value = pref.value!.ordering.split(' ')[0] as any
}

const placeholder = computed(() => toCalendar(today(getLocalTimeZone()), createCalendar(calendar.value)))
</script>

<template>
  <div class="flex flex-col gap-4">
    <Label>Locale</Label>
    <Select
      :model-value="locale"
      @update:model-value="updateLocale"
    >
      <SelectTrigger class="w-full">
        <SelectValue placeholder="Select a fruit" />
      </SelectTrigger>
      <SelectContent>
        <SelectItem
          v-for="(option, index) in preferences"
          :key="index"
          class="text-xs leading-none text-grass11 rounded-[3px] flex items-center h-[25px] pr-[35px] pl-[25px] relative select-none data-[disabled]:text-mauve8 data-[disabled]:pointer-events-none data-[highlighted]:outline-none data-[highlighted]:bg-green9 data-[highlighted]:text-green1"
          :value="option.locale"
        >
          {{ option.label }}
        </SelectItem>
      </SelectContent>
    </Select>

    <Label>Calendar</Label>
    <Select v-model="calendar" class="w-full">
      <SelectTrigger class="w-full">
        <SelectValue placeholder="Select a calendar">
          {{ calendars.find(c => c.key === calendar)?.name }}
        </SelectValue>
      </SelectTrigger>
      <SelectContent>
        <SelectLabel />
        <SelectGroup>
          <SelectItem
            v-for="(option, index) in preferredCalendars"
            :key="index"
            class="text-xs leading-none text-grass11 rounded-[3px] flex items-center h-[25px] pr-[35px] pl-[25px] relative select-none data-[disabled]:text-mauve8 data-[disabled]:pointer-events-none data-[highlighted]:outline-none data-[highlighted]:bg-green9 data-[highlighted]:text-green1"
            :value="option!.key"
          >
            {{ option!.name }}
          </SelectItem>
        </SelectGroup>
        <SelectSeparator />
        <SelectLabel>Other</SelectLabel>
        <SelectGroup>
          <SelectItem
            v-for="(option, index) in otherCalendars"
            :key="index"
            class="text-xs leading-none text-grass11 rounded-[3px] flex items-center h-[25px] pr-[35px] pl-[25px] relative select-none data-[disabled]:text-mauve8 data-[disabled]:pointer-events-none data-[highlighted]:outline-none data-[highlighted]:bg-green9 data-[highlighted]:text-green1"
            :value="option.key"
          >
            {{ option.name }}
          </SelectItem>
        </SelectGroup>
      </SelectContent>
    </Select>

    <Calendar
      v-model="date"
      v-model:placeholder="placeholder"
      :locale="locale"
      class="rounded-md border shadow-sm"
    />
  </div>
</template>

月份和年份选择器

使用此功能时,请确保传递 placeholderdefaultPlaceholder 属性。

4月
2026
活动日期,2026年4月
<script setup lang="ts">
import type { DateValue } from '@internationalized/date'
import type { LayoutTypes } from '@/components/ui/calendar'
import { getLocalTimeZone, today } from '@internationalized/date'
import { ref } from 'vue'
import { Calendar } from '@/components/ui/calendar'
import { Label } from '@/components/ui/label'
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from '@/components/ui/select'

const defaultPlaceholder = today(getLocalTimeZone())
const date = ref(today(getLocalTimeZone())) as Ref<DateValue>
const layout = ref<LayoutTypes>('month-and-year')
</script>

<template>
  <div class="flex flex-col gap-4">
    <Calendar
      v-model="date"
      :default-placeholder="defaultPlaceholder"
      class="rounded-md border shadow-sm"
      :layout
      disable-days-outside-current-view
    />
    <div class="flex flex-col gap-3">
      <Label for="dropdown" class="px-1">
        Dropdown
      </Label>
      <Select
        v-model="layout"
      >
        <SelectTrigger
          id="dropdown"
          size="sm"
          class="bg-background w-full"
        >
          <SelectValue placeholder="Dropdown" />
        </SelectTrigger>
        <SelectContent align="center">
          <SelectItem value="month-and-year">
            Month and Year
          </SelectItem>
          <SelectItem value="month-only">
            Month Only
          </SelectItem>
          <SelectItem value="year-only">
            Year Only
          </SelectItem>
        </SelectContent>
      </Select>
    </div>
  </div>
</template>

出生日期选择器

<script setup lang="ts">
import type { DateValue } from '@internationalized/date'
import { getLocalTimeZone, today } from '@internationalized/date'
import { ChevronDownIcon } from 'lucide-vue-next'
import { Button } from '@/components/ui/button'
import { Calendar } from '@/components/ui/calendar'
import { Label } from '@/components/ui/label'
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from '@/components/ui/popover'

const date = ref(today(getLocalTimeZone())) as Ref<DateValue>
</script>

<template>
  <div class="flex flex-col gap-3">
    <Label for="date" class="px-1">
      Date of birth
    </Label>
    <Popover v-slot="{ close }">
      <PopoverTrigger as-child>
        <Button
          id="date"
          variant="outline"
          class="w-48 justify-between font-normal"
        >
          {{ date ? date.toDate(getLocalTimeZone()).toLocaleDateString() : "Select date" }}
          <ChevronDownIcon />
        </Button>
      </PopoverTrigger>
      <PopoverContent class="w-auto overflow-hidden p-0" align="start">
        <Calendar
          :model-value="date"
          layout="month-and-year"
          @update:model-value="(value) => {
            if (value) {
              date = value
              close()
            }
          }"
        />
      </PopoverContent>
    </Popover>
  </div>
</template>

日期和时间选择器

<script setup lang="ts">
import type { DateValue } from '@internationalized/date'
import { getLocalTimeZone, today } from '@internationalized/date'
import { ChevronDownIcon } from 'lucide-vue-next'
import { Button } from '@/components/ui/button'
import { Calendar } from '@/components/ui/calendar'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from '@/components/ui/popover'

const date = ref(today(getLocalTimeZone())) as Ref<DateValue>
const open = ref(false)
</script>

<template>
  <div class="flex gap-4">
    <div class="flex flex-col gap-3">
      <Label for="date-picker" class="px-1">
        Date
      </Label>
      <Popover v-model:open="open">
        <PopoverTrigger as-child>
          <Button
            id="date-picker"
            variant="outline"
            class="w-32 justify-between font-normal"
          >
            {{ date ? date.toDate(getLocalTimeZone()).toLocaleDateString() : "Select date" }}
            <ChevronDownIcon />
          </Button>
        </PopoverTrigger>
        <PopoverContent class="w-auto overflow-hidden p-0" align="start">
          <Calendar
            :model-value="date"
            @update:model-value="(value) => {
              if (value) {
                date = value
                open = false
              }
            }"
          />
        </PopoverContent>
      </Popover>
    </div>
    <div class="flex flex-col gap-3">
      <Label for="time-picker" class="px-1">
        Time
      </Label>
      <Input
        id="time-picker"
        type="time"
        step="1"
        default-value="10:30:00"
        class="bg-background appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none"
      />
    </div>
  </div>
</template>

自然语言选择器

此组件使用 chrono-node 库来解析自然语言日期。

您的文章将于 2026年4月13日 发布。
<script lang="ts">
export function formatDate(date: Date | undefined) {
  if (!date) {
    return ''
  }
  return date.toLocaleDateString('en-US', {
    day: '2-digit',
    month: 'long',
    year: 'numeric',
  })
}
</script>

<script setup lang="ts">
import { fromDate, getLocalTimeZone } from '@internationalized/date'
import { parseDate } from 'chrono-node'
import { CalendarIcon } from 'lucide-vue-next'
import { Button } from '@/components/ui/button'
import { Calendar } from '@/components/ui/calendar'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from '@/components/ui/popover'

const inputValue = ref('In 2 days')
const nativeDate = computed(() => {
  return parseDate(inputValue.value)
})
const open = ref(false)
</script>

<template>
  <div class="flex flex-col gap-3">
    <Label for="date" class="px-1">
      Schedule Date
    </Label>
    <div class="relative flex gap-2">
      <Input
        id="date"
        :model-value="inputValue"
        placeholder="Tomorrow or next week"
        class="bg-background pr-10"
        @update:model-value="(value) => {
          if (value) {
            inputValue = value.toString()
            nativeDate = parseDate(value.toString())
          }
        }"
      />
      <Popover v-model:open="open">
        <PopoverTrigger as-child>
          <Button
            id="date-picker"
            variant="ghost"
            class="absolute top-1/2 right-2 size-6 -translate-y-1/2"
          >
            <CalendarIcon class="size-3.5" />
            <span class="sr-only">Select date</span>
          </Button>
        </PopoverTrigger>
        <PopoverContent class="w-auto overflow-hidden p-0" align="end">
          <Calendar
            :model-value="fromDate(nativeDate!, getLocalTimeZone())"
            @update:model-value="(value) => {
              if (value) {
                nativeDate = value.toDate(getLocalTimeZone())
                inputValue = formatDate(value.toDate(getLocalTimeZone()))
                open = false
              }
            }"
          />
        </PopoverContent>
      </Popover>
    </div>
    <div class="text-muted-foreground px-1 text-sm">
      Your post will be published on
      <span class="font-medium">{{ formatDate(nativeDate!) }}</span>.
    </div>
  </div>
</template>

自定义标题和单元格大小

自定义标题
4月
活动日期,2026年4月
<script setup lang="ts">
import type { DateValue } from '@internationalized/date'
import { getLocalTimeZone, today } from '@internationalized/date'
import { Calendar } from '@/components/ui/calendar'

const date = ref(today(getLocalTimeZone())) as Ref<DateValue>
const defaultPlaceholder = today(getLocalTimeZone())
</script>

<template>
  <Calendar
    v-model="date"
    :default-placeholder="defaultPlaceholder"
    weekday-format="short"
    class="rounded-md border shadow-sm **:data-[slot=calendar-cell-trigger]:size-12!"
  >
    <template #calendar-heading="{ date, month }">
      <div class="flex gap-2 items-center">
        <div>
          Custom heading
        </div>
        <component :is="month" :date="date" />
      </div>
    </template>
  </Calendar>
</template>