代码演示
如何引入
<script setup lang="ts">
import { Calendar } from '@fibrous/semi-ui-vue';
</script>
日视图
日视图的日历模板,可通过 showCurrTime 控制是否显示当前时间的位置红线。
代码
<script setup lang="ts">
import { Calendar } from '@fibrous/semi-ui-vue';
// A fixed date keeps the server render and the browser on the same first frame
const displayValue = new Date(2026, 9, 25);
</script>
<template>
<Calendar :displayValue="displayValue" mode="day" />
</template>周视图
周视图的日历模板,可通过 showCurrTime 控制是否显示当前时间的位置红线。
代码
<script setup lang="ts">
import { Calendar } from '@fibrous/semi-ui-vue';
// A fixed date keeps the server render and the browser on the same first frame
const displayValue = new Date(2026, 9, 25);
</script>
<template>
<Calendar :displayValue="displayValue" mode="week" />
</template>月视图
月视图的日历模板。
代码
<script setup lang="ts">
import { Calendar } from '@fibrous/semi-ui-vue';
// A fixed date keeps the server render and the browser on the same first frame
const displayValue = new Date(2026, 9, 25);
</script>
<template>
<Calendar :displayValue="displayValue" mode="month" />
</template>设置周起始日
可以通过 weekStartsOn 设置周几作为每周第一天,0 代表周日,1 代表周一,以此类推。默认为周日。对月视图、周视图生效。
代码
<script setup lang="ts">
import { ref } from 'vue';
import { Calendar, Radio, RadioGroup, type CalendarWeekStartsOn, type RadioValue } from '@fibrous/semi-ui-vue';
const weekStartsOn = ref<RadioValue | undefined>(0);
const days = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
// A fixed date keeps the server render and the browser on the same first frame
const displayValue = new Date(2026, 9, 25);
</script>
<template>
<div>
<RadioGroup v-model="weekStartsOn" aria-label="周起始日" name="demo-radio-group-vertical" type="button">
<Radio v-for="(day, index) in days" :key="day" :value="index">{{ day }}</Radio>
</RadioGroup>
<Calendar :displayValue="displayValue" mode="month" :style="{ marginTop: '20px' }" :weekStartsOn="weekStartsOn as CalendarWeekStartsOn" />
</div>
</template>多日视图
多日视图模式。 range 必传,左闭右开。
代码
<script setup lang="ts">
import { Calendar } from '@fibrous/semi-ui-vue';
const range = [new Date(2020, 8, 26), new Date(2020, 8, 31)];
</script>
<template>
<Calendar mode="range" :range="range" />
</template>事件渲染用法
通过 events 传入需要渲染的事件,events 是一个由 event objects 组成的数组,具体形式请参考 Event Object。
事件的内容默认渲染 children 文本;需要自定义 DOM 时用 #event="{ event }" 作用域插槽,event 是 events 中的原对象,可以带任意自定义字段。同一事件在多处显示(如跨天事件)时,插槽每处各调用一次。
代码
<script setup lang="ts">
import { computed, ref, type CSSProperties } from 'vue';
import { Calendar, DatePicker, Radio, RadioGroup, type CalendarEventObject, type CalendarMode, type RadioValue } from '@fibrous/semi-ui-vue';
const mode = ref<RadioValue | undefined>('week');
const displayValue = ref<Date | undefined>(new Date(2019, 6, 23, 8, 32, 0));
const onChangeDate = (value: Date | Date[] | undefined) => {
if (!Array.isArray(value)) displayValue.value = value;
};
const dailyEventStyle: CSSProperties = {
borderRadius: '3px',
boxSizing: 'border-box',
border: 'var(--semi-color-primary) 1px solid',
padding: '10px',
backgroundColor: 'var(--semi-color-primary-light-default)',
height: '100%',
overflow: 'hidden',
};
const allDayStyle: CSSProperties = {
borderRadius: '3px',
boxSizing: 'border-box',
border: 'var(--semi-color-bg-1) 1px solid',
padding: '2px 4px',
backgroundColor: 'var(--semi-color-primary-light-active)',
height: '100%',
overflow: 'hidden',
};
// `daily` marks the events that keep the padded style outside the month view; any extra key is passed to #event.
const events: CalendarEventObject[] = [
{
key: '0',
start: new Date(2019, 5, 25, 14, 45, 0),
end: new Date(2019, 6, 26, 6, 18, 0),
children: '6月25日 14:45 ~ 7月26日 6:18',
},
{
key: '1',
start: new Date(2019, 6, 18, 10, 0, 0),
end: new Date(2019, 6, 30, 8, 0, 0),
children: '7月18日 10:00 ~ 7月30日 8:00',
},
{
key: '2',
start: new Date(2019, 6, 19, 20, 0, 0),
end: new Date(2019, 6, 23, 14, 0, 0),
children: '7月19日 20:00 ~ 7月23日 14:00',
},
{
key: '3',
start: new Date(2019, 6, 21, 6, 0, 0),
end: new Date(2019, 6, 25, 6, 0, 0),
children: '7月21日 6:00 ~ 7月25日 6:00',
},
{ key: '4', allDay: true, start: new Date(2019, 6, 22, 8, 0, 0), children: '7月22日 全天' },
{
key: '5',
start: new Date(2019, 6, 22, 9, 0, 0),
end: new Date(2019, 6, 23, 23, 0, 0),
children: '7月22日 9:00 ~ 7月23日 23:00',
},
{
key: '6',
daily: true,
start: new Date(2019, 6, 23, 8, 32, 0),
end: new Date(2019, 6, 23, 8, 42, 0),
children: '7月23日 8:32',
},
{
key: '7',
daily: true,
start: new Date(2019, 6, 23, 14, 30, 0),
end: new Date(2019, 6, 23, 20, 0, 0),
children: '7月23日 14:30-20:00',
},
{
key: '8',
start: new Date(2019, 6, 25, 8, 0, 0),
end: new Date(2019, 6, 27, 6, 0, 0),
children: '7月25日 8:00 ~ 7月27日 6:00',
},
{
key: '9',
start: new Date(2019, 6, 26, 10, 0, 0),
end: new Date(2019, 6, 27, 16, 0, 0),
children: '7月26日 10:00 ~ 7月27日 16:00',
},
];
const range = computed(() => (mode.value === 'range' ? [new Date(2019, 6, 23), new Date(2019, 6, 26)] : []));
</script>
<template>
<RadioGroup v-model="mode" type="button">
<Radio value="day">日视图</Radio>
<Radio value="week">周视图</Radio>
<Radio value="month">月视图</Radio>
<Radio value="range">多日视图</Radio>
</RadioGroup>
<br />
<br />
<DatePicker :modelValue="displayValue" @update:modelValue="onChangeDate" />
<br />
<br />
<Calendar :displayValue="displayValue" :events="events" :height="400" :minEventHeight="40" :mode="mode as CalendarMode" :range="range">
<template #event="{ event }">
<div :style="event.daily && mode !== 'month' ? dailyEventStyle : allDayStyle">{{ event.children }}</div>
</template>
</Calendar>
</template>自定义渲染
通过 #dateGrid="{ date, dateString }" 作用域插槽可以自定义渲染日期单元格/列。需要使用绝对定位。
自定义渲染事件
代码
<script setup lang="ts">
import type { CSSProperties } from 'vue';
import { Calendar } from '@fibrous/semi-ui-vue';
const dailyEventStyle: CSSProperties = {
position: 'absolute',
left: '0',
right: '0',
borderRadius: '3px',
boxSizing: 'border-box',
border: 'var(--semi-color-primary) 1px solid',
padding: '10px',
backgroundColor: 'var(--semi-color-primary-light-default)',
overflow: 'hidden',
};
const displayValue = new Date(2019, 6, 23, 8, 32, 0);
const targetDateString = new Date(2019, 6, 23).toString();
</script>
<template>
<Calendar :displayValue="displayValue" :height="700" mode="week">
<template #dateGrid="{ dateString }">
<template v-if="dateString === targetDateString">
<div :style="{ ...dailyEventStyle, top: '500px', height: '50px' }">吃饭 🍰</div>
<div :style="{ ...dailyEventStyle, top: '0', height: '400px' }">睡觉 😪</div>
<div :style="{ ...dailyEventStyle, top: '700px', height: '100px' }">打豆豆 🎮</div>
</template>
</template>
</Calendar>
</template>自定义渲染单元格样式
可以通过 #dateGrid 自定义单元格的背景,月视图的文字 zIndex 默认为 3,如需完全覆盖单元格可以设置更大的 zIndex 来实现。
代码
<script setup lang="ts">
import type { CSSProperties } from 'vue';
import { Calendar } from '@fibrous/semi-ui-vue';
const importantDate: CSSProperties = {
position: 'absolute',
left: '0',
right: '0',
top: '0',
bottom: '0',
backgroundColor: 'var(--semi-color-danger-light-default)',
};
const displayValue = new Date(2019, 6, 23, 8, 32, 0);
const importDates = [new Date(2019, 6, 2), new Date(2019, 6, 8), new Date(2019, 6, 19), new Date(2019, 6, 23)];
const importDateStrings = importDates.map((date) => date.toString());
</script>
<template>
<Calendar :displayValue="displayValue" :height="700" mode="month">
<template #dateGrid="{ dateString }">
<div v-if="importDateStrings.includes(dateString)" :style="importantDate" />
</template>
</Calendar>
</template>自定义日期文案
可以通过 #dateDisplay="{ date }" 自定义日期文案。
代码
<script setup lang="ts">
import { Avatar, Calendar } from '@fibrous/semi-ui-vue';
const displayValue = new Date(2023, 4, 14);
const colors = ['amber', 'blue', 'cyan', 'green', 'grey', 'indigo', 'lime'] as const;
</script>
<template>
<Calendar :displayValue="displayValue" :height="400" mode="week">
<template #dateDisplay="{ date }">
<div :style="{ paddingBlock: '6px' }">
<Avatar :color="colors[date.getDay()]" size="small">{{ date.getDate() }}</Avatar>
</div>
</template>
</Calendar>
</template>API 参考
Calendar
Props
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| displayValue | 展示日期,未传时为当前日期并在午夜更新。 | Date | semiGlobalDefault("Calendar", "displayValue") |
| range | range 模式下展示的日期范围,左闭右开;range 模式必传。 | Date[] | semiGlobalDefault("Calendar", "range") |
| events | 渲染的事件,格式见 Event Object。 | EventObject[] | semiDefault("Calendar", "events", []) |
| mode | 视图模式,可选 day、week、month、range,变化后立即切换视图。 | CalendarMode | semiDefault("Calendar", "mode", "week") |
| showCurrTime | 日、周、多日视图下显示当前时间的红线。 | boolean | semiDefault("Calendar", "showCurrTime", true) |
| weekStartsOn | 以周几作为每周第一天,0 代表周日,1 代表周一,以此类推;对月视图、周视图生效。 | WeekStartsOn | semiDefault("Calendar", "weekStartsOn", 0) |
| scrollTop | 日、周、多日视图下展示内容的默认滚动高度。 | number | semiDefault("Calendar", "scrollTop", 400) |
| markWeekend | 以灰色区分周末列和工作日。 | boolean | semiDefault("Calendar", "markWeekend", false) |
| minEventHeight | 日视图、多日视图以及周视图下事件的最小高度,单位为像素。 | number | semiGlobalDefault("Calendar", "minEventHeight") |
| width | 日历宽度,数字按像素处理。 | string | number | semiGlobalDefault("Calendar", "width") |
| height | 日历高度,数字按像素处理。 | string | number | semiDefault("Calendar", "height", 600) |
Events
| 事件 | 说明 | 回调参数 |
|---|---|---|
| click | 单击日期格时触发,参数为事件和该格的日期;日、周、多日视图以半小时为单位,月视图以日为单位。 | [event: MouseEvent, value: Date] |
| close | 月视图下展示全部事件的卡片关闭时触发;关闭按钮或再次点击「还有 N 项」传点击事件,Esc 传键盘事件,点击外部或组件在 KeepAlive 中停用时传 null。 | [event: MouseEvent | KeyboardEvent | null] |
| moreClick | 月视图下点击「还有 N 项」打开卡片时触发,参数为事件、日期和被折叠的事件数;再次点击收起卡片时不触发。 | [event: MouseEvent, date: Date, remaining: number] |
Slots
| 插槽 | 说明 | 作用域参数 |
|---|---|---|
| header | 自定义头部内容。 | any |
| event | 自定义事件内容,参数 event 为 events 中的原对象,同一事件在多处显示时每处各调用一次;未提供时渲染 children 文本。 | CalendarEventSlotProps |
| dateDisplay | 自定义日期文案(周、多日视图的列标题与月视图的每个日期格),日视图不使用。 | CalendarDateDisplaySlotProps |
| dateGrid | 自定义单元格或列的内容,参数为日期和 date.toString() 得到的 dateString,需要绝对定位。 | CalendarDateGridSlotProps |
| timeDisplay | 自定义日、周、多日视图下的时间文案,参数 time 为小时数。 | CalendarTimeDisplaySlotProps |
| allDayEvents | 自定义日、多日、周视图顶部的全天事件区域,参数 events 为传入的全部事件,需自行筛选。 | CalendarAllDayEventsSlotProps |
Exposes
| 名称 | 说明 | 类型 |
|---|---|---|
| 无 |
class、style 与原生属性经 attrs 落在当前视图的根元素上;aria-label 优先于默认的表格名称。
Event Object
events 是一个 event object 组成的数组,event object 约定格式如下:
起始时间和结束时间至少传入一个才会被视为有效事件,两者都缺失的事件会被跳过并给出警告。全天事件只传结束时间时,以结束时间当天为起始;非全天事件只传一端时,补足 1 小时(不跨过当天)。结束时间区间为半开:非全天事件的结束时间恰为 0 点时,只显示在开始当天,不会在次日多出一段。
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| allDay | 全天事件 | boolean | false |
| children | 展示内容,未提供 #event 插槽时渲染为文本 | string | number | - |
| end | 事情结束的时间 | Date | - |
| key | required 且要求唯一 | string | - |
| start | 事情起始的时间 | Date | - |
| 其他字段 | 自定义字段原样保留,供 #event 插槽读取(如颜色、标题) | any | - |
Accessibility
键盘和焦点
- 日、周、多日视图可以滚动,视图本身是一个 Tab 停靠点,用方向键滚动
- 月视图中折叠事件的「还有 N 项」是按钮,
Enter或空格打开当天的事件卡片,焦点移到卡片的关闭按钮;按Esc或关闭按钮关闭卡片,焦点回到「还有 N 项」 - 日期格与时间槽的点击(React 的
onClick,Vue 的@click)只响应指针,没有键盘方式
文案规范
设计变量
| Sass 变量 | 值 | 用法 |
|---|---|---|
| $color-calendar-bg-active | var(--semi-color-primary) | 今日标识颜色 |
| $color-calendar-bg | var(--semi-color-bg-2) | 日历背景颜色 |
| $color-calendar-text-active | var(--semi-color-bg-1) | 日历文字颜色 - 今日 |
| $color-calendar_curr-border | var(--semi-color-danger) | 当前时间标识线颜色 |
| $color-calendar_currCircle-bg-default | var(--semi-color-danger) | 当前时间标识原点颜色 |
| $color-calendar_date-text-default | var(--semi-color-text-0) | 日历文字颜色 - 当月 |
| $color-calendar_day-border | var(--semi-color-border) | 日历描边颜色 |
| $color-calendar_day-text-default | var(--semi-color-text-2) | 日历文字颜色 - 非当月 |
| $color-calendar_sticky-bg | var(--semi-color-bg-2) | 日历背景颜色 - 吸顶部分 |
| $color-calendar_weekend-bg | var(--semi-color-fill-0) | 日历背景颜色 - 非工作日 |
| $width-calendar_day-minWidth | 78px | |
| $height-calendar_day_grid | 60px | |
| $width-calendar_day_grid-minWidth | 130px | |
| $height-calendar_time_grid | $height-calendar_day_grid * 0.5 | |
| $height-calendar_allDay | 26px | |
| $height-calendar_allDay-minHeight | 26px | |
| $width-calendar_currCircle | 8px | |
| $width-calendar_tag_col-minWidth | 70px | |
| $height-calendar_month | 100% | |
| $height-calendar_month_week_skeletion | 100% | |
| $height-calendar_month_grid_wrapper | calc(100% - 27px) | |
| $width-calendar_card | 220px | |
| $width-calendar_today_date | 24px | |
| $height-calendar_today_date | 24px | |
| $height-calendar_body_li | 24px | |
| $height-calendar_day_scroll_wrapper | calc(100% - 28px) | |
| $height-calendar_week_scroll_wrapper | calc(100% - 55px) | |
| $height-calendar_month_skeletion_li | 100% | |
| $height-calendar_month_grid_col | 100% | |
| $height-calendar_month_week_row_event_month | 1em | |
| $width-calendar-border-default | 1px | |
| $width-calendar-border-thick | 1px | |
| $spacing-calendar_ul_li-padding | 0 | |
| $spacing-calendar_ul_li-margin | 0 | |
| $spacing-calendar_sticky_top-top | 0 | |
| $spacing-calendar_sticky_top-right | 0 | |
| $spacing-calendar_sticky_top-left | 0 | |
| $spacing-calendar_sticky_left-left | 0 | |
| $spacing-calendar_event_day-left | 0 | |
| $spacing-calendar_event_day-right | 0 | |
| $spacing-calendar_custom_container-top | 0 | |
| $spacing-calendar_custom_container-right | 0 | |
| $spacing-calendar_custom_container-bottom | 0 | |
| $spacing-calendar_custom_container-left | 0 | |
| $spacing-calendar_currCircle-marginTop | -4px | |
| $spacing-calendar_currLine-left | 0 | |
| $spacing-calendar_currLine-right | 0 | |
| $spacing-calendar_allDay_tag-paddingRight | 8px | |
| $spacing-calendar_allDay_content-paddingLeft | 0 | |
| $spacing-calendar_month_event_card_wrapper-paddingTop | 2px | |
| $spacing-calendar_grid_row_span_child-paddingY | 0 | |
| $spacing-calendar_grid_row_span_child-paddingX | 4px | |
| $spacing-calendar_skeleton_li_child-paddingTop | 4px | |
| $spacing-calendar_month_event_card_close-marginRight | -4px | |
| $spacing-calendar_header_info_date-marginTop | 4px | |
| $spacing-calendar_time-paddingRight | 8px | |
| $spacing-calendar_time_item_span-top | -10px | |
| $spacing-calendar_tag_child-paddingRight | 8px | |
| $spacing-calendar_skeletion_grid_row_li-paddingRight | 8px | |
| $spacing-calendar_month_event_card_wrapper-right | 8px | |
| $spacing-calendar_month_event_card_content-paddingY | 12px | |
| $spacing-calendar_month_event_card_content-paddingX | 0 | |
| $spacing-calendar_month_event_card_ul_li-margin | 0 | |
| $spacing-calendar_month_event_card_ul_li-padding | 0 | |
| $spacing-calendar_header-marginY | 12px | |
| $spacing-calendar_header-marginX | 20px | |
| $spacing-calendar_body-padingY | 0 | |
| $spacing-calendar_body-padingX | 16px | |
| $spacing-calendar_event_items-top | 31px | |
| $spacing-calendar_event_items-right | 0 | |
| $spacing-calendar_event_items-bottom | 20px | |
| $spacing-calendar_event_items-left | 0 | |
| $spacing-calendar_month_skeletion-left | 0 | |
| $spacing-calendar_month_skeletion-right | 0 | |
| $spacing-calendar_month_date-right | 4px | |
| $radius-calendar_currCircle | var(--semi-border-radius-circle) | 日历选中日期标识圆角 |
| $radius-calendar_today_date | var(--semi-border-radius-circle) | 日历选中日期标识圆角 |
| $font-calendar_allDay_tag-lineHeight | $height-calendar_allDay | 日历全天标识文字行高 |
| $font-calendar_today_date-lineHeight | 24px | |
| $font-calendar_day_allDay-lineHeight | 26px | |
| $font-calendar_day_allDay-fontSize | 26px | |
| $font-calendar_month_day-fontSize | 24px | |
| $z-calendar_stickyTop | 20 | |
| $z-calendar_item | 5 | |
| $z-calendar_stickyLeft | 10 | |
| $z-calendar_curr | 8 | |
| $z-calendar_line | 3 |