這份指南要打造一個用來預訂會議室的行事曆:時間軸上每間會議室各佔一列,預訂可以拖曳到合適的位置,有一條規則防止同一間會議室被預訂兩次,還有一個檔案能把預訂加入任何行事曆應用程式。它使用的是 @needless-ui/angular 中的行事曆元件,和 Needless UI 的其他部分一樣採用 MIT 授權,因此資源時間軸在任何應用程式中都能免費使用。

會議室與預訂

會議室就是資源:有 id 和 title,其他資訊都放在 data 中。預訂就是活動,帶有一般的 ISO 時間(例如 2026-10-23T09:00),並在 resource 中存放所屬會議室的 id。一筆預訂也可以用清單同時包含多間會議室或多位人員。

每間會議室一列

把兩者連同 view="timeline" 一起傳給 nui-scheduler。dayStart 和 dayEnd 讓一天只顯示會議室的開放時間,snapMinutes 讓預訂以 15 分鐘為單位移動,businessHours 則以灰底標示會議室關閉的時間。nuiSchedulerResource 負責繪製每一列的標題。

booking.html
<nui-scheduler
[events]="bookings()"
[resources]="rooms"
view="timeline"
dayStart="8"
dayEnd="19"
snapMinutes="15"
[businessHours]="hours"
editable
selectable
[allow]="free"
(slotSelect)="book($event)"
(eventChange)="move($event)"
(rangeChange)="range.set($event)"
>
<ng-template nuiSchedulerResource let-room>{{ room.title }}</ng-template>
</nui-scheduler>
<button nuiButton type="button" (click)="download()">Add to calendar</button>

預訂與移動

行事曆從不自行變更你的資料,只會回報使用者做了什麼。在會議室的列上拖曳會選取一段時間,slotSelect 會連同會議室一起回報這段時間。拖曳預訂或其末端時,eventChange 會回報新的時間;如果移到了另一間會議室,也會回報新的會議室。更新 signal,時間軸就會跟著改變。

booking.ts
import { Component, signal } from '@angular/core';
import { NuiButton } from '@needless-ui/angular/button';
import {
NuiScheduler,
type NuiSchedulerChange,
type NuiSchedulerEvent,
NuiSchedulerResourceTemplate,
type NuiSchedulerSlot,
} from '@needless-ui/angular/scheduler';
import { downloadIcs } from './booking-ics';

@Component({
selector: 'app-bookings',
imports: [NuiButton, NuiScheduler, NuiSchedulerResourceTemplate],
templateUrl: './booking.html',
})
export class Bookings {
/** Rooms are resources; bookings are events, with their room in `resource`. */
protected readonly rooms = [
{ id: 'atlas', title: 'Atlas' },
{ id: 'boreal', title: 'Boreal' },
];
protected readonly bookings = signal<NuiSchedulerEvent[]>([]);
/** Open on weekdays from 8:00 to 19:00: other time is shaded. */
protected readonly hours = [{ days: [1, 2, 3, 4, 5], start: '08:00', end: '19:00' }];
/** The days on screen: load their bookings when it changes. */
protected readonly range = signal<{ start: string; end: string } | null>(null);

protected book({ start, end, resource }: NuiSchedulerSlot): void {
const booking = { id: crypto.randomUUID(), title: 'New booking', start, end, resource };
this.bookings.update((all) => [...all, booking]);
}

protected move({ event, start, end, resource }: NuiSchedulerChange): void {
this.bookings.update((all) =>
all.map((b) =>
b.id === event.id ? { ...b, start, end, resource: resource ?? b.resource } : b,
),
);
}

/** A room holds one booking at a time. */
protected readonly free = (time: NuiSchedulerChange | NuiSchedulerSlot): boolean => {
const id = 'event' in time ? time.event.id : null;
const room = 'event' in time ? (time.resource ?? time.event.resource) : time.resource;
return !this.bookings().some(
(b) => b.id !== id && b.resource === room && b.start < time.end && b.end > time.start,
);
};

protected download(): void {
downloadIcs(this.bookings(), 'bookings');
}
}

避免重複預訂

建立或移動預訂之前,行事曆會先詢問 allow。它傳回 false 時,拖曳會顯示為紅色,放開後也不會有任何變化;用鍵盤操作時,行事曆會朗讀該時間無法使用。這個範例會檢查同一間會議室的其他預訂;請在伺服器上再檢查一次,最終仍以伺服器為準。

載入畫面上的日期

rangeChange 會回報顯示的第一天和最後一天:開始時回報一次,之後使用者每次翻頁時再回報一次。這樣你就可以只取得這些日期的預訂,用 httpResource 或任何你喜歡的請求方式都可以。

加入行事曆

nuiSchedulerToIcs() 會把預訂寫成 iCalendar 檔案,Google Calendar、Outlook 和 Apple Calendar 都能匯入。nuiSchedulerFromIcs() 則會讀回這種檔案,並把其他時區的時間換算成使用者所在時區的時間。

booking-ics.ts
import { type NuiSchedulerEvent, nuiSchedulerToIcs } from '@needless-ui/angular/scheduler';

/** Saves events as an .ics file, which Google Calendar, Outlook and Apple Calendar import. */
export function downloadIcs(events: readonly NuiSchedulerEvent[], name: string): void {
const file = new Blob([nuiSchedulerToIcs(events, { name })], { type: 'text/calendar' });
const link = document.createElement('a');
link.href = URL.createObjectURL(file);
link.download = `${name}.ics`;
link.click();
URL.revokeObjectURL(link.href);
}

鍵盤與螢幕閱讀器

這一切不用滑鼠也能完成。時間軸在 Tab 鍵順序中只佔一個位置:方向鍵可在時間和會議室之間移動,Shift 加方向鍵可選取時間,按 Enter 鍵即可預訂。在預訂上按 Alt 加方向鍵,可以把它移到另一個時間或另一間會議室。每筆預訂都是一個按鈕,以其標題、會議室、日期和時間命名。

如需在日檢視中並排顯示會議室、使用週期性預訂或了解其他所有選項,請參閱行事曆頁面及其 API。