You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Angular中FullCalendar自定义5分钟最小时长时间网格视图异常问题

Angular FullCalendar 5分钟槽位时间网格首个槽位样式异常修复

问题背景

我在开发Angular应用,用FullCalendar构建类似谷歌日历的组件,目标是支持添加最小时长5分钟的事件,已配置核心参数:

slotDuration: '00:05:00',
slotLabelInterval: '01:00:00',

目前已经实现5分钟事件的创建功能,但自定义时间网格样式时碰到问题:通过CSS调整槽位高度后,次要槽位显示正常,首个时间网格槽位表现不符合预期。

相关代码

App组件TS代码

import { Component, signal, ChangeDetectorRef } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterOutlet } from '@angular/router';
import { FullCalendarModule } from '@fullcalendar/angular';
import {
  CalendarOptions,
  DateSelectArg,
  EventClickArg,
  EventApi,
} from '@fullcalendar/core';
import interactionPlugin from '@fullcalendar/interaction';
import dayGridPlugin from '@fullcalendar/daygrid';
import timeGridPlugin from '@fullcalendar/timegrid';
import { INITIAL_EVENTS, createEventId } from './event-utils';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [CommonModule, RouterOutlet, FullCalendarModule],
  templateUrl: './app.component.html',
  styleUrl: './app.component.scss',
})
export class AppComponent {
  calendarVisible = signal(true);
  calendarOptions = signal<CalendarOptions>({
    plugins: [interactionPlugin, dayGridPlugin, timeGridPlugin],
    headerToolbar: {
      left: 'today prev next title',
      right: '',
    },
    initialView: 'timeGridWeek',
    initialEvents: INITIAL_EVENTS,
    weekends: true,
    editable: true,
    selectable: true,
    selectMirror: true,
    dayMaxEvents: true,
    select: this.handleDateSelect.bind(this),
    eventClick: this.handleEventClick.bind(this),
    eventsSet: this.handleEvents.bind(this),
    allDaySlot: false,
    eventBackgroundColor: '#1e62eccc',
    eventBorderColor: '#1e62eccc',
    height: '100vh',
    slotDuration: '00:15:00', // 注意:此处需改为00:05:00
    slotLabelInterval: '01:00:00',
    scrollTime: '09:00:00',
    eventDurationEditable: true,
    eventShortHeight: 40,
    buttonText: {
      today: 'This Week',
    },
    nowIndicator: true,
    dayHeaderFormat: {
      weekday: 'short',
      day: 'numeric',
      month: 'short',
    },
    titleFormat: {
      year: 'numeric',
      month: 'long',
      day: 'numeric',
    },
    titleRangeSeparator: ' - ',
    dayHeaderContent: (singleDateObject: any) => {
      const fullDate = singleDateObject.date;
      const day = fullDate.getDay();
      const date = fullDate.getDate();
      const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];

      const today = new Date();
      const isToday = fullDate.toDateString() === today.toDateString();

      const dateClass = isToday
        ? 'bg-primary rounded-full h-8 w-8 flex justify-center items-center mt-2 font-normal'
        : '';
      const dayClass = isToday
        ? 'text-primary text-[10px] uppercase'
        : 'font-normal text-[10px] uppercase';
      return {
        html: `<p class="${dayClass}">${dayNames[day]}</p>
               <p class="${dateClass}">${date}</p>`,
      };
    },
  });
  currentEvents = signal<EventApi[]>([]);

  constructor(private changeDetector: ChangeDetectorRef) {}

  handleWeekendsToggle() {
    this.calendarOptions.update((options) => ({
      ...options,
      weekends: !options.weekends,
    }));
  }

  handleDateSelect(selectInfo: DateSelectArg) {
    const title = prompt('Please enter a new title for your event');
    const calendarApi = selectInfo.view.calendar;

    calendarApi.unselect();

    if (title) {
      calendarApi.addEvent({
        id: createEventId(),
        title,
        start: selectInfo.startStr,
        end: selectInfo.endStr,
        allDay: selectInfo.allDay,
      });
    }
  }

  handleEventClick(clickInfo: EventClickArg) {
    if (
      confirm(
        `Are you sure you want to delete the event '${clickInfo.event.title}'`
      )
    ) {
      clickInfo.event.remove();
    }
  }

  handleEvents(events: EventApi[]) {
    this.currentEvents.set(events);
    this.changeDetector.detectChanges();
  }
}

SCSS代码

/* app.component.scss */
:host {
  @apply block bg-base-700 text-base-200 h-screen rounded;
}

.fc-theme-standard {
  @apply bg-base-700 text-base-200;
}

.fc-toolbar {
  @apply bg-base-600 text-base-200 p-2;
}

.fc-daygrid-day,
.fc-timegrid-col {
  @apply bg-base-600 border-base-500;
}

.fc-daygrid-day:hover,
.fc-timegrid-col:hover {
  @apply bg-base-500;
}

.fc-event {
  @apply bg-primary text-base-200;
}

.fc-scroller {
  //   @apply scrollbar-thin scrollbar-thumb-base-500 scrollbar-track-base-700;
}

.fc-button {
  @apply bg-base-500 text-base-200 hover:bg-base-400 border border-base-500 rounded;
}

.fc-button-active {
  @apply bg-primary text-base-200;
}

::ng-deep {
  .fc-theme-standard td,
  .fc-theme-standard th {
    @apply border border-base-500;
  }

  .fc-v-event .fc-event-main {
    @apply bg-primary text-base-200 border-transparent;
  }

  .fc-timegrid-event-harness-inset .fc-timegrid-event,
  .fc-timegrid-event.fc-event-mirror,
  .fc-timegrid-more-link {
    @apply text-base-200 border-transparent ring-0 shadow;
  }

  .fc-theme-standard .fc-scrollgrid {
    @apply border-base-500 rounded;
  }

  .fc-scrollgrid-sync-inner {
    @apply py-2;
  }

  .fc .fc-timegrid-col.fc-day-today {
    @apply bg-primary/5 text-base-200;
  }

  button.fc-button.fc-button-primary {
    @apply bg-transparent rounded-full focus-within:right-0 focus:ring-0 focus-within:outline-none focus:outline-none active:bg-transparent focus-visible:outline-none focus-visible:ring-0 focus-visible:border-0 outline-none outline-0 outline-offset-0;
    @apply hover:bg-base-400 first-of-type:px-4 first-of-type:py-1.5 first-of-type:h-max border-0 first-of-type:border pr-2.5 pl-2 last-of-type:pl-2.5 last-of-type:pr-2 py-2 cursor-pointer #{!important};
  }

  button.fc-today-button.fc-button.fc-button-primary {
    @apply bg-transparent rounded-full text-sm ring-0 border-base-200 text-base-200;
  }

  .fc .fc-timegrid-slot {
    @apply h-4 before:h-4 before:flex;
  }

  .fc .fc-timegrid-slot-minor {
    @apply border-transparent h-4 before:h-4 before:flex;
  }

  .fc-timegrid-event-harness {
    @apply border-base-200 rounded border-l-4;
  }

  .fc-v-event .fc-event-main {
    @apply px-3 py-1;
  }

  .fc-timegrid-event-short .fc-event-main-frame {
    @apply px-0 py-0;
  }

  .fc-toolbar-chunk {
    @apply flex gap-2 items-center;
  }
}

问题分析

首个槽位异常的核心原因是:FullCalendar对scrollTime指定的起始时间槽位(这里是09:00)的标签容器做了特殊布局,默认高度和自定义设置的槽位高度不匹配,导致视觉上出现错位或高度异常。另外当前TS代码中slotDuration还是15分钟,需要先修正为目标的5分钟。

修复方案

1. 修正Calendar配置

先把TS代码中的slotDuration改为5分钟:

slotDuration: '00:05:00',

2. 调整SCSS样式,兼容首个槽位

修改::ng-deep中的槽位相关样式,强制重置首个槽位的标签容器和内容容器高度:

::ng-deep {
  // ... 保留原有其他样式

  // 统一所有槽位高度,覆盖默认计算值
  .fc .fc-timegrid-slot {
    @apply h-4 before:h-4 before:flex;
    height: 16px !important;
  }

  .fc .fc-timegrid-slot-minor {
    @apply border-transparent h-4 before:h-4 before:flex;
  }

  // 修复首个槽位的标签容器高度冲突
  .fc-timegrid-axis-chunk > .fc-timegrid-slot-label-frame {
    @apply h-4;
    height: 16px !important;
  }

  // 确保首个槽位内容容器高度匹配
  .fc-timegrid-slot:first-child .fc-timegrid-slot-content {
    @apply h-4;
    height: 16px !important;
  }

  // 适配短事件高度,避免溢出
  .fc-timegrid-event-short .fc-event-main-frame {
    @apply h-3 py-0 px-2;
  }
}

3. 验证效果

修改后刷新页面,所有槽位(包括首个)的高度会统一为16px(h-4),5分钟事件的创建和显示也会正常适配槽位高度。

内容的提问来源于stack exchange,提问作者justAnotherDev

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 09:49:51