🌐 Nodejs.cn

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

Mar
2026
Event Date, March 2026
<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>

关于

🌐 About

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

🌐 The <Calendar /> component is built on top of the Reka UI Calendar component, which uses the @internationalized/date package to handle dates.

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

🌐 If you're looking for a range calendar, check out the Range Calendar component.

安装

🌐 Installation

pnpm dlx shadcn-vue@latest add calendar

用法

🌐 Usage

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

<template>
  <Calendar />
</template>

日历系统(例如波斯历 / 伊斯兰历 / 贾拉里历)

🌐 Calendar Systems (Persian / Hijri / Jalali for example)

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

默认的日历系统是 gregory
要使用不同的日历系统,你需要通过 defaultPlaceholderplaceholder 属性提供所需系统的值。

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

🌐 It's recommended to add either the placeholder or defaultPlaceholder to the component even if you don't use any other calendar system

<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 日历,因为它是最广泛使用的系统。

🌐 If none of these props are provided, the emitted dates will use the gregorian calendar by default, since it is the most widely used system.

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

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

🌐 You can also change the locale using the locale prop to match the calendar system interface.

<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>
اسفند
۱۴۰۴
Event Date, ۱۴۰۴ اسفند
1404/12/23
اسفند ۱۴۰۴
<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>

示例

🌐 Examples

日历系统

🌐 Calendar Systems

createCalendar 导入到你的项目中将导致所有可用的日历都包含在你的包中。如果你希望限制支持的日历以减少包的大小,你可以创建自己的实现,只导入所需的类。这样,你的打包工具可以对未使用的日历实现进行树摇优化。

🌐 importing createCalendar into your project will result in all available calendars being included in your bundle. If you wish to limit the supported calendars to reduce bundle sizes, you can create your own implementation that only imports the desired classes. This way, your bundler can tree-shake the unused calendar implementations.

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

🌐 Check @internationalized/date, especially the section on Calendar Identifiers.

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}`)
  }
}
March 2026
Event Date, March 2026
<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>

月份和年份选择器

🌐 Month and Year Selector

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

🌐 Make sure to pass either the placeholder or defaultPlaceholder prop when using this feature.

Mar
2026
Event Date, March 2026
<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>

出生日期选择器

🌐 Date of Birth Picker

<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>

日期和时间选择器

🌐 Date and Time Picker

<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>

自然语言选择器

🌐 Natural Language Picker

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

🌐 This component uses the chrono-node library to parse natural language dates.

Your post will be published on March 16, 2026.
<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>

自定义标题和单元格大小

🌐 Custom Heading and Cell Size

Custom heading
Mar
Event Date, March 2026
<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>