代码演示
如何引入
<script lang="ts">
import { Calendar } from '@fibrae/semi-ui-svelte';
</script>
日视图
日视图的日历模板,可通过 showCurrTime 控制是否显示当前时间的位置红线。
代码
<script lang="ts">
import { Calendar } from '@fibrae/semi-ui-svelte';
// A fixed date keeps the server render and the browser on the same first frame
const displayValue = new Date(2026, 9, 25);
</script>
<Calendar {displayValue} mode="day" />周视图
周视图的日历模板,可通过 showCurrTime 控制是否显示当前时间的位置红线。
代码
<script lang="ts">
import { Calendar } from '@fibrae/semi-ui-svelte';
// A fixed date keeps the server render and the browser on the same first frame
const displayValue = new Date(2026, 9, 25);
</script>
<Calendar {displayValue} mode="week" />月视图
月视图的日历模板。
代码
<script lang="ts">
import { Calendar } from '@fibrae/semi-ui-svelte';
// A fixed date keeps the server render and the browser on the same first frame
const displayValue = new Date(2026, 9, 25);
</script>
<Calendar {displayValue} mode="month" />设置周起始日
可以通过 weekStartsOn 设置周几作为每周第一天,0 代表周日,1 代表周一,以此类推。默认为周日。对月视图、周视图生效。
代码
<script lang="ts">
import { Calendar, Radio, RadioGroup, type WeekStartsOn } from '@fibrae/semi-ui-svelte';
let weekStartsOn = $state(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>
<div>
<RadioGroup aria-label="周起始日" bind:value={weekStartsOn} name="demo-radio-group-vertical" type="button">
{#each days as day, index (day)}
<Radio value={index}>{day}</Radio>
{/each}
</RadioGroup>
<Calendar {displayValue} mode="month" style="margin-top: 20px" weekStartsOn={weekStartsOn as WeekStartsOn} />
</div>多日视图
多日视图模式。 range 必传,左闭右开。
代码
<script lang="ts">
import { Calendar } from '@fibrae/semi-ui-svelte';
const range = [new Date(2020, 8, 26), new Date(2020, 8, 31)];
</script>
<Calendar mode="range" {range} />事件渲染用法
通过 events 传入需要渲染的事件,events 是一个由 event objects 组成的数组,具体形式请参考 Event Object。
事件的内容默认渲染 children 文本;需要自定义 DOM 时用 event snippet,参数 { event } 是 events 中的原对象,可以带任意自定义字段。同一事件在多处显示(如跨天事件)时,snippet 每处各调用一次。
代码
<script lang="ts">
import { Calendar, DatePicker, Radio, RadioGroup, type CalendarMode, type EventObject } from '@fibrae/semi-ui-svelte';
let mode = $state('week');
let displayValue = $state(new Date(2019, 6, 23, 8, 32, 0));
const onChangeDate = (date: Date | Date[] | undefined) => {
if (date instanceof Date) displayValue = date;
};
const dailyEventStyle = 'border-radius: 3px; box-sizing: border-box; border: var(--semi-color-primary) 1px solid; padding: 10px; background-color: var(--semi-color-primary-light-default); height: 100%; overflow: hidden';
const allDayStyle = 'border-radius: 3px; box-sizing: border-box; border: var(--semi-color-bg-1) 1px solid; padding: 2px 4px; background-color: 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 the event snippet.
const events: EventObject[] = [
{
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 = $derived(mode === 'range' ? [new Date(2019, 6, 23), new Date(2019, 6, 26)] : []);
</script>
<RadioGroup aria-label="视图模式" bind:value={mode} type="button">
<Radio value="day">日视图</Radio>
<Radio value="week">周视图</Radio>
<Radio value="month">月视图</Radio>
<Radio value="range">多日视图</Radio>
</RadioGroup>
<br />
<br />
<DatePicker onChange={onChangeDate} value={displayValue} />
<br />
<br />
<Calendar {displayValue} {events} height={400} minEventHeight={40} mode={mode as CalendarMode} {range}>
{#snippet event({ event })}
<div style={event.daily && mode !== 'month' ? dailyEventStyle : allDayStyle}>{event.children}</div>
{/snippet}
</Calendar>自定义渲染
通过 dateGrid snippet(参数 { date, dateString })可以自定义渲染日期单元格/列。需要使用绝对定位。
自定义渲染事件
代码
<script lang="ts">
import { Calendar } from '@fibrae/semi-ui-svelte';
const dailyEventStyle = 'position: absolute; left: 0; right: 0; border-radius: 3px; box-sizing: border-box; border: var(--semi-color-primary) 1px solid; padding: 10px; background-color: 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>
<Calendar {displayValue} height={700} mode="week">
{#snippet dateGrid({ dateString })}
{#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>
{/if}
{/snippet}
</Calendar>自定义渲染单元格样式
可以通过 dateGrid snippet 自定义单元格的背景,月视图的文字 zIndex 默认为 3,如需完全覆盖单元格可以设置更大的 zIndex 来实现。
代码
<script lang="ts">
import { Calendar } from '@fibrae/semi-ui-svelte';
const importantDate = 'position: absolute; left: 0; right: 0; top: 0; bottom: 0; background-color: 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>
<Calendar {displayValue} height={700} mode="month">
{#snippet dateGrid({ dateString })}
{#if importDateStrings.includes(dateString)}
<div style={importantDate}></div>
{/if}
{/snippet}
</Calendar>自定义日期文案
可以通过 dateDisplay snippet(参数 { date })自定义日期文案。
代码
<script lang="ts">
import { Avatar, Calendar } from '@fibrae/semi-ui-svelte';
const displayValue = new Date(2023, 4, 14);
const colors = ['amber', 'blue', 'cyan', 'green', 'grey', 'indigo', 'lime'] as const;
</script>
<Calendar {displayValue} height={400} mode="week">
{#snippet dateDisplay({ date })}
<div style="padding-block: 6px">
<Avatar color={colors[date.getDay()]} size="small">{date.getDate()}</Avatar>
</div>
{/snippet}
</Calendar>API 参考
Calendar
Props
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| allDayEvents | 自定义日、多日、周视图顶部全天事件区域的 snippet,参数 events 为传入的全部事件,需自行筛选,渲染 li 项。 | TypeSnippet<[CalendarAllDayEventsArgs]> | - |
| dateDisplay | 自定义日期文案的 snippet(周、多日视图的列标题与月视图的每个日期格),日视图不使用。 | TypeSnippet<[CalendarDateDisplayArgs]> | - |
| dateGrid | 自定义单元格或列内容的 snippet,参数为日期和 date.toString() 得到的 dateString,需要绝对定位。 | TypeSnippet<[CalendarDateGridArgs]> | - |
| displayValue | 展示日期,未传时为当前日期并在午夜更新。 | Date | - |
| event | 自定义事件内容的 snippet,参数 event 为 events 中的原对象,同一事件在多处显示时每处各调用一次;未提供时渲染 children 文本。 | TypeSnippet<[CalendarEventArgs]> | - |
| events | 渲染的事件,格式见 Event Object。 | EventObject[] | [] |
| header | 自定义头部内容的 snippet。 | Snippet | - |
| height | 日历高度,数字按像素处理。 | number | string | 600 |
| markWeekend | 以灰色区分周末列和工作日。 | boolean | false |
| minEventHeight | 日视图、多日视图以及周视图下事件的最小高度,单位为像素。 | number | - |
| mode | 视图模式,可选 day、week、month、range,变化后立即切换视图。 | CalendarMode | "week" |
| onClick | 单击日期格时触发,参数为事件和该格的日期;日、周、多日视图以半小时为单位,月视图以日为单位。 | Type(event: MouseEvent, value: Date) => void | - |
| onClose | 月视图下展示全部事件的卡片关闭时触发;关闭按钮或再次点击「还有 N 项」传点击事件,Esc 传键盘事件,点击外部传 null。 | Type(event: MouseEvent | KeyboardEvent | null) => void | - |
| onMoreClick | 月视图下点击「还有 N 项」打开卡片时触发,参数为事件、日期和被折叠的事件数;再次点击收起卡片时不触发。 | Type(event: MouseEvent, date: Date, remaining: number) => void | - |
| range | range 模式下展示的日期范围,左闭右开;range 模式必传。 | Date[] | - |
| scrollTop | 日、周、多日视图下展示内容的默认滚动高度。 | number | 400 |
| showCurrTime | 日、周、多日视图下显示当前时间的红线。 | boolean | true |
| timeDisplay | 自定义日、周、多日视图下时间文案的 snippet,参数 time 为小时数。 | TypeSnippet<[CalendarTimeDisplayArgs]> | - |
| weekStartsOn | 以周几作为每周第一天,0 代表周日,1 代表周一,以此类推;对月视图、周视图生效。 | WeekStartsOn | 0 |
| width | 日历宽度,数字按像素处理。 | number | string | - |
class、style 与其他原生属性(如 id、data-*、aria-label)落在当前视图的根元素上;aria-label 优先于默认的表格名称。onClick、onClose、onMoreClick 是 Calendar 自身的回调,不会落到根元素上。
Event Object
events 是一个 event object 组成的数组,event object 约定格式如下:
起始时间和结束时间至少传入一个才会被视为有效事件,两者都缺失的事件会被跳过并给出警告。全天事件只传结束时间时,以结束时间当天为起始;非全天事件只传一端时,补足 1 小时(不跨过当天)。结束时间区间为半开:非全天事件的结束时间恰为 0 点时,只显示在开始当天,不会在次日多出一段。
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| allDay | 全天事件 | boolean | false |
| children | 展示内容,未提供 event snippet 时渲染为文本 | string | number | - |
| end | 事情结束的时间 | Date | - |
| key | required 且要求唯一 | string | - |
| start | 事情起始的时间 | Date | - |
| 其他字段 | 自定义字段原样保留,供 event snippet 读取(如颜色、标题) | any | - |
Accessibility
键盘和焦点
- 日、周、多日视图可以滚动,视图本身是一个 Tab 停靠点,用方向键滚动
- 月视图中折叠事件的「还有 N 项」是按钮,
Enter或空格打开当天的事件卡片,焦点移到卡片的关闭按钮;按Esc或关闭按钮关闭卡片,焦点回到「还有 N 项」 - 日期格与时间槽的点击(React 的
onClick,Vue 的@click,Svelte 的onClick)只响应指针,没有键盘方式
文案规范
设计变量
| 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 |