Trong hướng dẫn này, bạn sẽ xây dựng một lịch đặt phòng họp: một timeline có một dòng riêng cho mỗi phòng, những lượt đặt chỗ bạn kéo được đến đúng vị trí, một quy tắc giữ cho một phòng không bị đặt hai lần, và một tệp giúp thêm lượt đặt chỗ vào bất kỳ lịch nào. Lịch này dùng bộ lập lịch từ @needless-ui/angular, theo giấy phép MIT như toàn bộ Needless UI, nên timeline tài nguyên miễn phí cho mọi ứng dụng.

Phòng và lượt đặt chỗ

Phòng là tài nguyên, gồm id, title và mọi thứ khác trong data. Lượt đặt chỗ là sự kiện với thời gian ISO thông thường, chẳng hạn 2026-10-23T09:00, cùng id của phòng trong resource. Một lượt đặt chỗ có thể gắn với nhiều phòng, hoặc nhiều người, dưới dạng danh sách.

Mỗi phòng một dòng

Truyền cả hai vào nui-scheduler với view="timeline". dayStart và dayEnd giới hạn ngày trong giờ mở cửa, snapMinutes di chuyển lượt đặt chỗ theo bước 15 phút, còn businessHours tô mờ thời gian các phòng đóng cửa. nuiSchedulerResource vẽ tiêu đề của mỗi dòng.

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>

Đặt phòng và di chuyển

Bộ lập lịch không bao giờ tự thay đổi dữ liệu của bạn: nó chỉ báo lại những gì người dùng làm. Kéo qua dòng của một phòng sẽ chọn thời gian, và slotSelect báo thời gian đó cùng với phòng. Khi bạn kéo một lượt đặt chỗ, hoặc điểm kết thúc của nó, eventChange sẽ báo thời gian mới, kèm phòng mới nếu nó được chuyển sang phòng khác. Cập nhật signal, và timeline sẽ thay đổi theo.

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');
}
}

Không đặt trùng

Bộ lập lịch hỏi allow trước khi một lượt đặt chỗ được tạo hoặc di chuyển. Khi câu trả lời là false, thao tác kéo hiện màu đỏ và thả ra không có tác dụng, còn khi dùng bàn phím, bộ lập lịch sẽ báo rằng khung giờ đó không còn trống. Ví dụ này kiểm tra các lượt đặt chỗ khác của cùng phòng; hãy kiểm tra lại trên máy chủ của bạn, vì máy chủ mới là nơi quyết định cuối cùng.

Tải những ngày đang hiển thị

rangeChange báo ngày đầu tiên và cuối cùng đang hiển thị, một lần lúc đầu và lại mỗi khi người dùng chuyển trang, để bạn chỉ tải các lượt đặt chỗ của những ngày đó, bằng httpResource hoặc bất kỳ yêu cầu nào bạn muốn.

Thêm vào lịch

nuiSchedulerToIcs() ghi các lượt đặt chỗ thành một tệp iCalendar, loại tệp mà Google Calendar, Outlook và Apple Calendar đều nhập được. nuiSchedulerFromIcs() đọc ngược lại một tệp như vậy, và chuyển thời gian từ các múi giờ khác sang múi giờ của người dùng.

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);
}

Bàn phím và trình đọc màn hình

Mọi thứ đều làm được mà không cần chuột. Timeline chỉ chiếm một điểm dừng Tab: phím mũi tên di chuyển qua các giờ và các phòng, Shift kèm phím mũi tên chọn thời gian, và Enter đặt phòng vào thời gian đó. Trên một lượt đặt chỗ, Alt kèm phím mũi tên chuyển nó sang giờ hoặc phòng khác. Mỗi lượt đặt chỗ là một nút được đặt tên theo tiêu đề, phòng, ngày và giờ của nó.

Về các phòng xếp cạnh nhau trong chế độ xem ngày, lượt đặt chỗ lặp lại và mọi tùy chọn khác, hãy xem trang của bộ lập lịch và API của nó.