用法
使用 v-model 指令控制选定的日期。
| 日 | M | T | 三 | T | 五 | 日 |
|---|---|---|---|---|---|---|
30 | 31 | 1 | 2 | 3 | 4 | 5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 1 | 2 | 3 | 4 | 5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 |
<script setup lang="ts">
import { CalendarDate } from '@internationalized/date'
const value = shallowRef(new CalendarDate(2022, 2, 3))
</script>
<template>
<UCalendar v-model="value" />
</template>
<script setup lang="ts">
import { shallowRef } from 'vue'
import { CalendarDate } from '@internationalized/date'
const value = shallowRef(new CalendarDate(2022, 2, 3))
</script>
<template>
<UCalendar v-model="value" />
</template>
使用 default-value prop 在您不需要控制其状态时设置初始值。
| 日 | M | T | 三 | T | 五 | 日 |
|---|---|---|---|---|---|---|
30 | 31 | 1 | 2 | 3 | 4 | 5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 1 | 2 | 3 | 4 | 5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 |
<script setup lang="ts">
import { CalendarDate } from '@internationalized/date'
const defaultValue = shallowRef(new CalendarDate(2022, 2, 6))
</script>
<template>
<UCalendar :default-value="defaultValue" />
</template>
<script setup lang="ts">
import { shallowRef } from 'vue'
import { CalendarDate } from '@internationalized/date'
const defaultValue = shallowRef(new CalendarDate(2022, 2, 6))
</script>
<template>
<UCalendar :default-value="defaultValue" />
</template>
多选
使用 multiple 属性以允许选择多个日期。
| 日 | M | T | 三 | T | 五 | 日 |
|---|---|---|---|---|---|---|
30 | 31 | 1 | 2 | 3 | 4 | 5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 1 | 2 | 3 | 4 | 5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 |
<script setup lang="ts">
import { CalendarDate } from '@internationalized/date'
const value = shallowRef([
new CalendarDate(2022, 2, 4),
new CalendarDate(2022, 2, 6),
new CalendarDate(2022, 2, 8)
])
</script>
<template>
<UCalendar multiple v-model="value" />
</template>
<script setup lang="ts">
import { shallowRef } from 'vue'
import { CalendarDate } from '@internationalized/date'
const value = shallowRef([
new CalendarDate(2022, 2, 4),
new CalendarDate(2022, 2, 6),
new CalendarDate(2022, 2, 8)
])
</script>
<template>
<UCalendar multiple v-model="value" />
</template>
范围
使用 range 属性来选择日期范围。
| 日 | M | T | 三 | T | 五 | 日 |
|---|---|---|---|---|---|---|
30 | 31 | 1 | 2 | 3 | 4 | 5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 1 | 2 | 3 | 4 | 5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 |
<script setup lang="ts">
import { CalendarDate } from '@internationalized/date'
const value = shallowRef({
start: new CalendarDate(2022, 2, 3),
end: new CalendarDate(2022, 2, 20)
})
</script>
<template>
<UCalendar range v-model="value" />
</template>
<script setup lang="ts">
import { shallowRef } from 'vue'
import { CalendarDate } from '@internationalized/date'
const value = shallowRef({
start: new CalendarDate(2022, 2, 3),
end: new CalendarDate(2022, 2, 20)
})
</script>
<template>
<UCalendar range v-model="value" />
</template>
颜色
使用 color 属性来更改日历的颜色。
| 日 | M | T | 三 | T | 五 | 日 |
|---|---|---|---|---|---|---|
26 | 27 | 28 | 29 | 30 | 1 | 2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 | 1 | 2 | 3 | 4 | 5 | 6 |
<template>
<UCalendar color="neutral" />
</template>
变体
使用 variant 属性来更改日历的变体。
| 日 | M | T | 三 | T | 五 | 日 |
|---|---|---|---|---|---|---|
30 | 31 | 1 | 2 | 3 | 4 | 5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 1 | 2 | 3 | 4 | 5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 |
<template>
<UCalendar variant="subtle" />
</template>
尺寸
使用 size 属性来更改日历的大小。
| 日 | M | T | 三 | T | 五 | 日 |
|---|---|---|---|---|---|---|
26 | 27 | 28 | 29 | 30 | 1 | 2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 | 1 | 2 | 3 | 4 | 5 | 6 |
<template>
<UCalendar size="xl" />
</template>
禁用
使用 disabled 属性来禁用日历。
| 日 | M | T | 三 | T | 五 | 日 |
|---|---|---|---|---|---|---|
26 | 27 | 28 | 29 | 30 | 1 | 2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 | 1 | 2 | 3 | 4 | 5 | 6 |
<template>
<UCalendar disabled />
</template>
月份数量
使用 numberOfMonths 属性来更改日历中显示的月份数量。
| 日 | M | T | 三 | T | 五 | 日 |
|---|---|---|---|---|---|---|
26 | 27 | 28 | 29 | 30 | 1 | 2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 | 1 | 2 | 3 | 4 | 5 | 6 |
| 日 | M | T | 三 | T | 五 | 日 |
|---|---|---|---|---|---|---|
31 | 1 | 2 | 3 | 4 | 5 | 6 |
7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 | 29 | 30 | 1 | 2 | 3 | 4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 日 | M | T | 三 | T | 五 | 日 |
|---|---|---|---|---|---|---|
28 | 29 | 30 | 1 | 2 | 3 | 4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 | 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 |
<template>
<UCalendar :number-of-months="3" />
</template>
月份控件
使用 month-controls 属性显示月份控件。默认为 true。
| 日 | M | T | 三 | T | 五 | 日 |
|---|---|---|---|---|---|---|
26 | 27 | 28 | 29 | 30 | 1 | 2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 | 1 | 2 | 3 | 4 | 5 | 6 |
<template>
<UCalendar :month-controls="false" />
</template>
年份控件
使用 year-controls 属性显示年份控件。默认为 true。
| 日 | M | T | 三 | T | 五 | 日 |
|---|---|---|---|---|---|---|
26 | 27 | 28 | 29 | 30 | 1 | 2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 | 1 | 2 | 3 | 4 | 5 | 6 |
<template>
<UCalendar :year-controls="false" />
</template>
固定周数
使用 fixed-weeks 属性以固定周数显示日历。
| 日 | M | T | 三 | T | 五 | 日 |
|---|---|---|---|---|---|---|
26 | 27 | 28 | 29 | 30 | 1 | 2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 | 1 | 2 | 3 | 4 | 5 | 6 |
<template>
<UCalendar :fixed-weeks="false" />
</template>
周数 4.4+
使用 week-numbers 属性在日历中显示周数。
| 日 | M | T | 三 | T | 五 | 日 | |
|---|---|---|---|---|---|---|---|
| 18 | 26 | 27 | 28 | 29 | 30 | 1 | 2 |
| 19 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
| 20 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
| 21 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
| 22 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
| 23 | 31 | 1 | 2 | 3 | 4 | 5 | 6 |
<template>
<UCalendar week-numbers />
</template>
示例
带有标记事件
使用 Chip 组件为特定日期添加事件。
| 日 | M | T | 三 | T | 五 | 日 |
|---|---|---|---|---|---|---|
26 | 27 | 28 | 29 | 30 | 31 | 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | 31 | 1 | 2 | 3 | 4 | 5 |
<script setup lang="ts">
import { CalendarDate } from '@internationalized/date'
const modelValue = shallowRef(new CalendarDate(2022, 1, 10))
function getColorByDate(date: Date) {
const isWeekend = date.getDay() % 6 == 0
const isDayMeeting = date.getDay() % 3 == 0
if (isWeekend) {
return undefined
}
if (isDayMeeting) {
return 'error'
}
return 'success'
}
</script>
<template>
<UCalendar v-model="modelValue">
<template #day="{ day }">
<UChip :show="!!getColorByDate(day.toDate('UTC'))" :color="getColorByDate(day.toDate('UTC'))" size="2xs">
{{ day.day }}
</UChip>
</template>
</UCalendar>
</template>
<script setup lang="ts">
import { shallowRef } from 'vue'
import { CalendarDate } from '@internationalized/date'
const modelValue = shallowRef(new CalendarDate(2022, 1, 10))
function getColorByDate(date: Date) {
const isWeekend = date.getDay() % 6 == 0
const isDayMeeting = date.getDay() % 3 == 0
if (isWeekend) {
return undefined
}
if (isDayMeeting) {
return 'error'
}
return 'success'
}
</script>
<template>
<UCalendar v-model="modelValue">
<template #day="{ day }">
<UChip :show="!!getColorByDate(day.toDate('UTC'))" :color="getColorByDate(day.toDate('UTC'))" size="2xs">
{{ day.day }}
</UChip>
</template>
</UCalendar>
</template>
带有禁用日期
将 is-date-disabled 属性与函数一起使用,可将特定日期标记为禁用。
| 日 | M | T | 三 | T | 五 | 日 |
|---|---|---|---|---|---|---|
26 | 27 | 28 | 29 | 30 | 31 | 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | 31 | 1 | 2 | 3 | 4 | 5 |
<script setup lang="ts">
import type { DateValue } from '@internationalized/date'
import { CalendarDate } from '@internationalized/date'
const modelValue = shallowRef({
start: new CalendarDate(2022, 1, 1),
end: new CalendarDate(2022, 1, 9)
})
const isDateDisabled = (date: DateValue) => {
return date.day >= 10 && date.day <= 16
}
</script>
<template>
<UCalendar v-model="modelValue" :is-date-disabled="isDateDisabled" range />
</template>
<script setup lang="ts">
import { shallowRef } from 'vue'
import type { DateValue } from '@internationalized/date'
import { CalendarDate } from '@internationalized/date'
const modelValue = shallowRef({
start: new CalendarDate(2022, 1, 1),
end: new CalendarDate(2022, 1, 9)
})
const isDateDisabled = (date: DateValue) => {
return date.day >= 10 && date.day <= 16
}
</script>
<template>
<UCalendar v-model="modelValue" :is-date-disabled="isDateDisabled" range />
</template>
带有不可用日期
将 is-date-unavailable 属性与函数一起使用,可将特定日期标记为不可用。
| 日 | M | T | 三 | T | 五 | 日 |
|---|---|---|---|---|---|---|
26 | 27 | 28 | 29 | 30 | 31 | 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | 31 | 1 | 2 | 3 | 4 | 5 |
<script setup lang="ts">
import type { DateValue } from '@internationalized/date'
import { CalendarDate } from '@internationalized/date'
const modelValue = shallowRef({
start: new CalendarDate(2022, 1, 1),
end: new CalendarDate(2022, 1, 9)
})
const isDateUnavailable = (date: DateValue) => {
return date.day >= 10 && date.day <= 16
}
</script>
<template>
<UCalendar v-model="modelValue" :is-date-unavailable="isDateUnavailable" range />
</template>
<script setup lang="ts">
import { shallowRef } from 'vue'
import type { DateValue } from '@internationalized/date'
import { CalendarDate } from '@internationalized/date'
const modelValue = shallowRef({
start: new CalendarDate(2022, 1, 1),
end: new CalendarDate(2022, 1, 9)
})
const isDateUnavailable = (date: DateValue) => {
return date.day >= 10 && date.day <= 16
}
</script>
<template>
<UCalendar v-model="modelValue" :is-date-unavailable="isDateUnavailable" range />
</template>
带有最小/最大日期
使用 min-value 和 max-value 属性来限制日期范围。
| 日 | M | T | 三 | T | 五 | 日 |
|---|---|---|---|---|---|---|
27 | 28 | 29 | 30 | 31 | 1 | 2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
1 | 2 | 3 | 4 | 5 | 6 | 7 |
<script setup lang="ts">
import { CalendarDate } from '@internationalized/date'
const modelValue = shallowRef(new CalendarDate(2023, 9, 10))
const minDate = new CalendarDate(2023, 9, 1)
const maxDate = new CalendarDate(2023, 9, 30)
</script>
<template>
<UCalendar v-model="modelValue" :min-value="minDate" :max-value="maxDate" />
</template>
<script setup lang="ts">
import { shallowRef } from 'vue'
import { CalendarDate } from '@internationalized/date'
const modelValue = shallowRef(new CalendarDate(2023, 9, 10))
const minDate = new CalendarDate(2023, 9, 1)
const maxDate = new CalendarDate(2023, 9, 30)
</script>
<template>
<UCalendar v-model="modelValue" :min-value="minDate" :max-value="maxDate" />
</template>
使用其他日历系统
您可以使用 @internationalized/date 中的其他日历来实现不同的日历系统。
| 日 | M | T | 三 | T | 五 | 日 |
|---|---|---|---|---|---|---|
24 | 25 | 26 | 27 | 28 | 29 | 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | 1 | 2 | 3 | 4 | 5 | 6 |
<script lang="ts" setup>
import { CalendarDate, HebrewCalendar } from '@internationalized/date'
const hebrewDate = shallowRef(new CalendarDate(new HebrewCalendar(), 5781, 1, 1))
</script>
<template>
<UCalendar v-model="hebrewDate" />
</template>
使用外部控件
您可以通过操作传递给 v-model 的日期来通过外部控件控制日历。
| 日 | M | T | 三 | T | 五 | 日 |
|---|---|---|---|---|---|---|
30 | 31 | 1 | 2 | 3 | 4 | 5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 |
<script setup lang="ts">
import { CalendarDate } from '@internationalized/date'
const date = shallowRef(new CalendarDate(2025, 4, 2))
</script>
<template>
<div class="flex flex-col gap-4">
<UCalendar v-model="date" :month-controls="false" :year-controls="false" />
<div class="flex justify-between gap-4">
<UButton color="neutral" variant="outline" @click="date = date.subtract({ months: 1 })">
Prev
</UButton>
<UButton color="neutral" variant="outline" @click="date = date.add({ months: 1 })">
Next
</UButton>
</div>
</div>
</template>
<script setup lang="ts">
import { shallowRef } from 'vue'
import { CalendarDate } from '@internationalized/date'
const date = shallowRef(new CalendarDate(2025, 4, 2))
</script>
<template>
<div class="flex flex-col gap-4">
<UCalendar v-model="date" :month-controls="false" :year-controls="false" />
<div class="flex justify-between gap-4">
<UButton color="neutral" variant="outline" @click="date = date.subtract({ months: 1 })">
Prev
</UButton>
<UButton color="neutral" variant="outline" @click="date = date.add({ months: 1 })">
Next
</UButton>
</div>
</div>
</template>
作为日期选择器
使用 Button 和 Popover 组件来创建日期选择器。
<script setup lang="ts">
import { CalendarDate, DateFormatter, getLocalTimeZone } from '@internationalized/date'
const df = new DateFormatter('en-US', {
dateStyle: 'medium'
})
const modelValue = shallowRef(new CalendarDate(2022, 1, 10))
</script>
<template>
<UPopover>
<UButton color="neutral" variant="subtle" icon="i-lucide-calendar">
{{ modelValue ? df.format(modelValue.toDate(getLocalTimeZone())) : 'Select a date' }}
</UButton>
<template #content>
<UCalendar v-model="modelValue" class="p-2" />
</template>
</UPopover>
</template>
<script setup lang="ts">
import { shallowRef } from 'vue'
import { CalendarDate, DateFormatter, getLocalTimeZone } from '@internationalized/date'
const df = new DateFormatter('en-US', {
dateStyle: 'medium'
})
const modelValue = shallowRef(new CalendarDate(2022, 1, 10))
</script>
<template>
<UPopover>
<UButton color="neutral" variant="subtle" icon="i-lucide-calendar">
{{ modelValue ? df.format(modelValue.toDate(getLocalTimeZone())) : 'Select a date' }}
</UButton>
<template #content>
<UCalendar v-model="modelValue" class="p-2" />
</template>
</UPopover>
</template>
作为日期范围选择器
使用 Button 和 Popover 组件来创建带有预设范围的日期范围选择器。
<script setup lang="ts">
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'
import { DateFormatter, getLocalTimeZone, today } from '@internationalized/date'
const df = new DateFormatter('en-US', { dateStyle: 'medium' })
const tz = getLocalTimeZone()
const breakpoints = useBreakpoints(breakpointsTailwind)
const isDesktop = breakpoints.greaterOrEqual('sm')
const ranges = [
{ label: 'Last 7 days', days: 7 },
{ label: 'Last 14 days', days: 14 },
{ label: 'Last 30 days', days: 30 },
{ label: 'Last 3 months', months: 3 },
{ label: 'Last 6 months', months: 6 },
{ label: 'Last year', years: 1 }
]
const initialEnd = today(tz)
const modelValue = shallowRef({
start: initialEnd.subtract({ days: 14 }),
end: initialEnd
})
const label = computed(() => {
const { start, end } = modelValue.value
if (!start) return 'Pick a date'
if (!end) return df.format(start.toDate(tz))
return `${df.format(start.toDate(tz))} - ${df.format(end.toDate(tz))}`
})
function computeStart(range: typeof ranges[number]) {
const end = today(tz)
return { start: end.subtract({ days: range.days, months: range.months, years: range.years }), end }
}
function isRangeSelected(range: typeof ranges[number]) {
if (!modelValue.value?.start || !modelValue.value?.end) return false
const { start, end } = computeStart(range)
return modelValue.value.start.compare(start) === 0 && modelValue.value.end.compare(end) === 0
}
function selectRange(range: typeof ranges[number]) {
modelValue.value = computeStart(range)
}
</script>
<template>
<UPopover :content="{ align: 'center' }">
<UButton color="neutral" variant="subtle" icon="i-lucide-calendar">
{{ label }}
</UButton>
<template #content>
<div class="flex items-stretch divide-x divide-(--ui-border)">
<div class="hidden sm:flex flex-col justify-center py-2">
<UButton
v-for="(range, index) in ranges"
:key="index"
:label="range.label"
color="neutral"
variant="ghost"
class="rounded-none px-4"
:class="[isRangeSelected(range) ? 'bg-elevated' : 'hover:bg-elevated/50']"
truncate
@click="selectRange(range)"
/>
</div>
<UCalendar v-model="modelValue" class="p-2" :number-of-months="isDesktop ? 2 : 1" range />
</div>
</template>
</UPopover>
</template>
<script setup lang="ts">
import { shallowRef, computed } from 'vue'
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'
import { DateFormatter, getLocalTimeZone, today } from '@internationalized/date'
const df = new DateFormatter('en-US', { dateStyle: 'medium' })
const tz = getLocalTimeZone()
const breakpoints = useBreakpoints(breakpointsTailwind)
const isDesktop = breakpoints.greaterOrEqual('sm')
const ranges = [
{ label: 'Last 7 days', days: 7 },
{ label: 'Last 14 days', days: 14 },
{ label: 'Last 30 days', days: 30 },
{ label: 'Last 3 months', months: 3 },
{ label: 'Last 6 months', months: 6 },
{ label: 'Last year', years: 1 }
]
const initialEnd = today(tz)
const modelValue = shallowRef({
start: initialEnd.subtract({ days: 14 }),
end: initialEnd
})
const label = computed(() => {
const { start, end } = modelValue.value
if (!start) return 'Pick a date'
if (!end) return df.format(start.toDate(tz))
return `${df.format(start.toDate(tz))} - ${df.format(end.toDate(tz))}`
})
function computeStart(range: typeof ranges[number]) {
const end = today(tz)
return { start: end.subtract({ days: range.days, months: range.months, years: range.years }), end }
}
function isRangeSelected(range: typeof ranges[number]) {
if (!modelValue.value?.start || !modelValue.value?.end) return false
const { start, end } = computeStart(range)
return modelValue.value.start.compare(start) === 0 && modelValue.value.end.compare(end) === 0
}
function selectRange(range: typeof ranges[number]) {
modelValue.value = computeStart(range)
}
</script>
<template>
<UPopover :content="{ align: 'center' }">
<UButton color="neutral" variant="subtle" icon="i-lucide-calendar">
{{ label }}
</UButton>
<template #content>
<div class="flex items-stretch divide-x divide-(--ui-border)">
<div class="hidden sm:flex flex-col justify-center py-2">
<UButton
v-for="(range, index) in ranges"
:key="index"
:label="range.label"
color="neutral"
variant="ghost"
class="rounded-none px-4"
:class="[isRangeSelected(range) ? 'bg-elevated' : 'hover:bg-elevated/50']"
truncate
@click="selectRange(range)"
/>
</div>
<UCalendar v-model="modelValue" class="p-2" :number-of-months="isDesktop ? 2 : 1" range />
</div>
</template>
</UPopover>
</template>
API
属性
| 属性 | 默认值 | 类型 |
|---|---|---|
as | 'div' | any此组件应渲染为的元素或组件。 |
nextYearIcon | appConfig.ui.icons.chevronDoubleRight | any用于下一年控件的图标。 |
nextYear | Omit<ButtonProps, LinkPropsKeys>配置下一年按钮。 | |
nextMonthIcon | appConfig.ui.icons.chevronRight | any用于下一月控件的图标。 |
nextMonth | Omit<ButtonProps, LinkPropsKeys>配置下一月按钮。 | |
prevYearIcon | appConfig.ui.icons.chevronDoubleLeft | any用于上一年控件的图标。 |
prevYear | Omit<ButtonProps, LinkPropsKeys>配置上一年按钮。 | |
prevMonthIcon | appConfig.ui.icons.chevronLeft | any用于上一月控件的图标。 |
prevMonth | Omit<ButtonProps, LinkPropsKeys>配置上一月按钮。 | |
color | 'primary' | "primary" | "secondary" | "success" | "info" | "warning" | "error" | "neutral" |
variant | 'solid' | "solid" | "outline" | "soft" | "subtle" |
尺寸 | 'md' | "md" | "xs" | "sm" | "lg" | "xl" |
范围 | R是否可以选定日期范围 | |
multiple | M是否可以选定多个日期 | |
monthControls | true | boolean显示月份控件 |
yearControls | true | boolean显示年份控件 |
defaultValue | CalendarDate | CalendarDateTime | ZonedDateTime | DateRange | DateValue[] | |
modelValue | null | CalendarDate | CalendarDateTime | ZonedDateTime | DateRange | DateValue[] | |
weekNumbers | boolean | |
defaultPlaceholder | CalendarDate | CalendarDateTime | ZonedDateTime | |
placeholder | CalendarDate | CalendarDateTime | ZonedDateTime | |
allowNonContiguousRanges | boolean当与 | |
pagedNavigation | boolean此属性使上一页和下一页按钮按一次显示的月数进行导航,而不是一次一个月 | |
preventDeselect | boolean是否阻止用户在未先选择另一个日期的情况下取消选择日期 | |
maximumDays | number范围内可选择的最大天数 | |
weekStartsOn | 0 | 1 | 2 | 4 | 5 | 3 | 6日历开始的星期几 | |
weekdayFormat | "narrow" | "short" | "long"用于通过 weekdays 插槽属性提供的星期几字符串的格式 | |
fixedWeeks | true | boolean是否始终在日历中显示 6 周 |
maxValue | CalendarDate | CalendarDateTime | ZonedDateTime | |
minValue | CalendarDate | CalendarDateTime | ZonedDateTime | |
numberOfMonths | number一次显示的月份数 | |
disabled | boolean日历是否被禁用 | |
readonly | boolean日历是否为只读 | |
initialFocus | boolean如果为 true,当挂载日历时,日历将聚焦于选定日期、今天或当月的第一天(取决于可见内容) | |
isDateDisabled | (date: DateValue): boolean一个函数,返回日期是否被禁用 | |
isDateUnavailable | (date: DateValue): boolean一个函数,返回日期是否不可用 | |
isDateHighlightable | (date: DateValue): boolean一个函数,返回日期是否可高亮显示 | |
nextPage | (placeholder: DateValue): DateValue返回下一页日历的函数。它在组件内部接收当前占位符作为参数。 | |
prevPage | (placeholder: DateValue): DateValue返回上一页日历的函数。它在组件内部接收当前占位符作为参数。 | |
disableDaysOutsideCurrentView | boolean是否禁用当前视图之外的天数。 | |
fixedDate | "start" | "end"应固定范围的哪一部分 | |
ui | { root?: ClassNameValue; header?: ClassNameValue; body?: ClassNameValue; heading?: ClassNameValue; grid?: ClassNameValue; gridRow?: ClassNameValue; gridWeekDaysRow?: ClassNameValue; gridBody?: ClassNameValue; headCell?: ClassNameValue; headCellWeek?: ClassNameValue; cell?: ClassNameValue; cellTrigger?: ClassNameValue; cellWeek?: ClassNameValue; } |
插槽
| 插槽 | 类型 |
|---|---|
heading | { value: string; } |
day | Pick<CalendarCellTriggerProps, "day"> |
week-day | { day: string; } |
事件
| 事件 | 类型 |
|---|---|
update:modelValue | [value: CalendarModelValue<R, M>] |
update:placeholder | [date: DateValue] & [date: DateValue] |
update:validModelValue | [date: DateRange] |
update:startValue | [date: DateValue | undefined] |
主题
export default defineAppConfig({
ui: {
calendar: {
slots: {
root: '',
header: 'flex items-center justify-between',
body: 'flex flex-col space-y-4 pt-4 sm:flex-row sm:space-x-4 sm:space-y-0',
heading: 'text-center font-medium truncate mx-auto',
grid: 'w-full border-collapse select-none space-y-1 focus:outline-none',
gridRow: 'grid grid-cols-7 place-items-center',
gridWeekDaysRow: 'mb-1 grid w-full grid-cols-7',
gridBody: 'grid',
headCell: 'rounded-md',
headCellWeek: 'rounded-md text-muted',
cell: 'relative text-center',
cellTrigger: [
'm-0.5 relative flex items-center justify-center rounded-full whitespace-nowrap focus-visible:ring-2 focus:outline-none data-disabled:text-muted data-unavailable:line-through data-unavailable:text-muted data-unavailable:pointer-events-none data-today:font-semibold data-[outside-view]:text-muted',
'transition'
],
cellWeek: 'relative text-center text-muted'
},
variants: {
color: {
primary: {
headCell: 'text-primary',
cellTrigger: 'focus-visible:ring-primary'
},
secondary: {
headCell: 'text-secondary',
cellTrigger: 'focus-visible:ring-secondary'
},
success: {
headCell: 'text-success',
cellTrigger: 'focus-visible:ring-success'
},
info: {
headCell: 'text-info',
cellTrigger: 'focus-visible:ring-info'
},
warning: {
headCell: 'text-warning',
cellTrigger: 'focus-visible:ring-warning'
},
error: {
headCell: 'text-error',
cellTrigger: 'focus-visible:ring-error'
},
neutral: {
headCell: 'text-highlighted',
cellTrigger: 'focus-visible:ring-inverted'
}
},
variant: {
solid: '',
outline: '',
soft: '',
subtle: ''
},
size: {
xs: {
heading: 'text-xs',
cell: 'text-xs',
cellWeek: 'text-xs',
headCell: 'text-[10px]',
headCellWeek: 'text-[10px]',
cellTrigger: 'size-7',
body: 'space-y-2 pt-2'
},
sm: {
heading: 'text-xs',
headCell: 'text-xs',
headCellWeek: 'text-xs',
cellWeek: 'text-xs',
cell: 'text-xs',
cellTrigger: 'size-7'
},
md: {
heading: 'text-sm',
headCell: 'text-xs',
headCellWeek: 'text-xs',
cellWeek: 'text-xs',
cell: 'text-sm',
cellTrigger: 'size-8'
},
lg: {
heading: 'text-md',
headCell: 'text-md',
headCellWeek: 'text-md',
cellTrigger: 'size-9 text-md'
},
xl: {
heading: 'text-lg',
headCell: 'text-lg',
headCellWeek: 'text-lg',
cellTrigger: 'size-10 text-lg'
}
},
weekNumbers: {
true: {
gridRow: 'grid-cols-8',
gridWeekDaysRow: 'grid-cols-8 [&>*:first-child]:col-start-2'
}
}
},
compoundVariants: [
{
color: 'primary',
variant: 'solid',
class: {
cellTrigger: 'data-[selected]:bg-primary data-[selected]:text-inverted data-today:not-data-[selected]:text-primary data-[highlighted]:bg-primary/20 hover:not-data-[selected]:bg-primary/20'
}
},
{
color: 'primary',
variant: 'outline',
class: {
cellTrigger: 'data-[selected]:ring data-[selected]:ring-inset data-[selected]:ring-primary/50 data-[selected]:text-primary data-today:not-data-[selected]:text-primary data-[highlighted]:bg-primary/10 hover:not-data-[selected]:bg-primary/10'
}
},
{
color: 'primary',
variant: 'soft',
class: {
cellTrigger: 'data-[selected]:bg-primary/10 data-[selected]:text-primary data-today:not-data-[selected]:text-primary data-[highlighted]:bg-primary/20 hover:not-data-[selected]:bg-primary/20'
}
},
{
color: 'primary',
variant: 'subtle',
class: {
cellTrigger: 'data-[selected]:bg-primary/10 data-[selected]:text-primary data-[selected]:ring data-[selected]:ring-inset data-[selected]:ring-primary/25 data-today:not-data-[selected]:text-primary data-[highlighted]:bg-primary/20 hover:not-data-[selected]:bg-primary/20'
}
},
{
color: 'neutral',
variant: 'solid',
class: {
cellTrigger: 'data-[selected]:bg-inverted data-[selected]:text-inverted data-today:not-data-[selected]:text-highlighted data-[highlighted]:bg-inverted/20 hover:not-data-[selected]:bg-inverted/10'
}
},
{
color: 'neutral',
variant: 'outline',
class: {
cellTrigger: 'data-[selected]:ring data-[selected]:ring-inset data-[selected]:ring-accented data-[selected]:text-default data-[selected]:bg-default data-today:not-data-[selected]:text-highlighted data-[highlighted]:bg-inverted/10 hover:not-data-[selected]:bg-inverted/10'
}
},
{
color: 'neutral',
variant: 'soft',
class: {
cellTrigger: 'data-[selected]:bg-elevated data-[selected]:text-default data-today:not-data-[selected]:text-highlighted data-[highlighted]:bg-inverted/20 hover:not-data-[selected]:bg-inverted/10'
}
},
{
color: 'neutral',
variant: 'subtle',
class: {
cellTrigger: 'data-[selected]:bg-elevated data-[selected]:text-default data-[selected]:ring data-[selected]:ring-inset data-[selected]:ring-accented data-today:not-data-[selected]:text-highlighted data-[highlighted]:bg-inverted/20 hover:not-data-[selected]:bg-inverted/10'
}
}
],
defaultVariants: {
size: 'md',
color: 'primary',
variant: 'solid'
}
}
}
})
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'
export default defineConfig({
plugins: [
vue(),
ui({
ui: {
calendar: {
slots: {
root: '',
header: 'flex items-center justify-between',
body: 'flex flex-col space-y-4 pt-4 sm:flex-row sm:space-x-4 sm:space-y-0',
heading: 'text-center font-medium truncate mx-auto',
grid: 'w-full border-collapse select-none space-y-1 focus:outline-none',
gridRow: 'grid grid-cols-7 place-items-center',
gridWeekDaysRow: 'mb-1 grid w-full grid-cols-7',
gridBody: 'grid',
headCell: 'rounded-md',
headCellWeek: 'rounded-md text-muted',
cell: 'relative text-center',
cellTrigger: [
'm-0.5 relative flex items-center justify-center rounded-full whitespace-nowrap focus-visible:ring-2 focus:outline-none data-disabled:text-muted data-unavailable:line-through data-unavailable:text-muted data-unavailable:pointer-events-none data-today:font-semibold data-[outside-view]:text-muted',
'transition'
],
cellWeek: 'relative text-center text-muted'
},
variants: {
color: {
primary: {
headCell: 'text-primary',
cellTrigger: 'focus-visible:ring-primary'
},
secondary: {
headCell: 'text-secondary',
cellTrigger: 'focus-visible:ring-secondary'
},
success: {
headCell: 'text-success',
cellTrigger: 'focus-visible:ring-success'
},
info: {
headCell: 'text-info',
cellTrigger: 'focus-visible:ring-info'
},
warning: {
headCell: 'text-warning',
cellTrigger: 'focus-visible:ring-warning'
},
error: {
headCell: 'text-error',
cellTrigger: 'focus-visible:ring-error'
},
neutral: {
headCell: 'text-highlighted',
cellTrigger: 'focus-visible:ring-inverted'
}
},
variant: {
solid: '',
outline: '',
soft: '',
subtle: ''
},
size: {
xs: {
heading: 'text-xs',
cell: 'text-xs',
cellWeek: 'text-xs',
headCell: 'text-[10px]',
headCellWeek: 'text-[10px]',
cellTrigger: 'size-7',
body: 'space-y-2 pt-2'
},
sm: {
heading: 'text-xs',
headCell: 'text-xs',
headCellWeek: 'text-xs',
cellWeek: 'text-xs',
cell: 'text-xs',
cellTrigger: 'size-7'
},
md: {
heading: 'text-sm',
headCell: 'text-xs',
headCellWeek: 'text-xs',
cellWeek: 'text-xs',
cell: 'text-sm',
cellTrigger: 'size-8'
},
lg: {
heading: 'text-md',
headCell: 'text-md',
headCellWeek: 'text-md',
cellTrigger: 'size-9 text-md'
},
xl: {
heading: 'text-lg',
headCell: 'text-lg',
headCellWeek: 'text-lg',
cellTrigger: 'size-10 text-lg'
}
},
weekNumbers: {
true: {
gridRow: 'grid-cols-8',
gridWeekDaysRow: 'grid-cols-8 [&>*:first-child]:col-start-2'
}
}
},
compoundVariants: [
{
color: 'primary',
variant: 'solid',
class: {
cellTrigger: 'data-[selected]:bg-primary data-[selected]:text-inverted data-today:not-data-[selected]:text-primary data-[highlighted]:bg-primary/20 hover:not-data-[selected]:bg-primary/20'
}
},
{
color: 'primary',
variant: 'outline',
class: {
cellTrigger: 'data-[selected]:ring data-[selected]:ring-inset data-[selected]:ring-primary/50 data-[selected]:text-primary data-today:not-data-[selected]:text-primary data-[highlighted]:bg-primary/10 hover:not-data-[selected]:bg-primary/10'
}
},
{
color: 'primary',
variant: 'soft',
class: {
cellTrigger: 'data-[selected]:bg-primary/10 data-[selected]:text-primary data-today:not-data-[selected]:text-primary data-[highlighted]:bg-primary/20 hover:not-data-[selected]:bg-primary/20'
}
},
{
color: 'primary',
variant: 'subtle',
class: {
cellTrigger: 'data-[selected]:bg-primary/10 data-[selected]:text-primary data-[selected]:ring data-[selected]:ring-inset data-[selected]:ring-primary/25 data-today:not-data-[selected]:text-primary data-[highlighted]:bg-primary/20 hover:not-data-[selected]:bg-primary/20'
}
},
{
color: 'neutral',
variant: 'solid',
class: {
cellTrigger: 'data-[selected]:bg-inverted data-[selected]:text-inverted data-today:not-data-[selected]:text-highlighted data-[highlighted]:bg-inverted/20 hover:not-data-[selected]:bg-inverted/10'
}
},
{
color: 'neutral',
variant: 'outline',
class: {
cellTrigger: 'data-[selected]:ring data-[selected]:ring-inset data-[selected]:ring-accented data-[selected]:text-default data-[selected]:bg-default data-today:not-data-[selected]:text-highlighted data-[highlighted]:bg-inverted/10 hover:not-data-[selected]:bg-inverted/10'
}
},
{
color: 'neutral',
variant: 'soft',
class: {
cellTrigger: 'data-[selected]:bg-elevated data-[selected]:text-default data-today:not-data-[selected]:text-highlighted data-[highlighted]:bg-inverted/20 hover:not-data-[selected]:bg-inverted/10'
}
},
{
color: 'neutral',
variant: 'subtle',
class: {
cellTrigger: 'data-[selected]:bg-elevated data-[selected]:text-default data-[selected]:ring data-[selected]:ring-inset data-[selected]:ring-accented data-today:not-data-[selected]:text-highlighted data-[highlighted]:bg-inverted/20 hover:not-data-[selected]:bg-inverted/10'
}
}
],
defaultVariants: {
size: 'md',
color: 'primary',
variant: 'solid'
}
}
}
})
]
})