| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
<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。
要使用不同的日历系统,你需要通过 defaultPlaceholder 或 placeholder 属性提供所需系统的值。
建议即使不使用任何其他日历系统,也在组件中添加 placeholder 或 defaultPlaceholder
🌐 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>| ش | ی | د | س | چ | پ | ج |
|---|---|---|---|---|---|---|
<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}`)
}
}| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
<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
使用此功能时,请确保传入 placeholder 或 defaultPlaceholder 属性。
🌐 Make sure to pass either the placeholder or defaultPlaceholder prop when using this feature.
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
<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.
<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
| Sun | Mon | Tue | Wed | Thu | Fri | Sat |
|---|---|---|---|---|---|---|
<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>