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

Angular Material Calendar异步数据源日期禁用过滤失效问题

问题根因
  • Angular Material 日期选择器的[dateFilter]仅支持同步返回布尔值的过滤函数,传入返回Promise/Observable的异步函数会直接失效,这是尝试async写法后所有日期都无法禁用的核心原因。
  • 初始写法每次触发过滤都会重复发起API请求,且异步订阅返回前函数已经执行return,此时预订日期还未写入阻塞数组,等数据返回时日历已经完成首次渲染,不会自动重新执行过滤。
  • 存在隐式时区问题:日志中第一部分阻塞日期是00:00:00 GMT+02,第二部分预订日期是01:00:00 GMT+02,时间戳不匹配,即便数据加载完成也可能出现匹配失败的情况。
实现方案

核心逻辑:提前预加载全量阻塞日期,不在过滤函数内处理异步逻辑,数据就绪后主动触发日历重绘。

组件类代码修正

import { Component, AfterContentInit, Input, ViewChild, OnChanges, SimpleChanges } from '@angular/core';
import { MatCalendar } from '@angular/material/datepicker';
import { Renderer2 } from '@angular/core';
import { BookingService } from '替换为服务对应路径';
import { Camper, CamperDate } from '替换为类型定义对应路径';
import { of } from 'rxjs';
import { map, catchError } from 'rxjs/operators';

export class CamperBookingDetailsComponent implements AfterContentInit, OnChanges {
  @Input() camper!: Camper;
  @ViewChild('calendar') calendar!: MatCalendar<Date>;

  // 存储阻塞日期的时间戳,Set结构判断存在性性能远高于数组find
  private blockedDateTimestamps = new Set<number>();
  // 标记阻塞日期是否全部加载完成
  isDatesLoaded = false;

  constructor(private _renderer: Renderer2, private _bookingService: BookingService) {}

  ngOnChanges(changes: SimpleChanges): void {
    // 父组件传入的camper变更时,重新加载阻塞日期
    if (changes['camper'] && this.camper) {
      this.loadAllBlockedDates();
    }
  }

  ngAfterContentInit(): void {
    if (this.camper) {
      this.loadAllBlockedDates();
    }
  }

  // 统一加载两类阻塞日期
  private loadAllBlockedDates(): void {
    this.isDatesLoaded = false;
    this.blockedDateTimestamps.clear();

    // 加载camper对象内的阻塞日期
    Object.entries(this.camper.difPrizes).forEach(([key, value]) => {
      if (value === 0) {
        const rawDate = new Date(key);
        // 日期归一化:统一重置为当天0点,消除时分秒、时区偏移导致的时间戳不匹配
        const normalizedDate = new Date(rawDate.getFullYear(), rawDate.getMonth(), rawDate.getDate());
        this.blockedDateTimestamps.add(normalizedDate.getTime());
      }
    });

    // 加载接口返回的预订阻塞日期
    this._bookingService.getBookings$({ camperId: this.camper._id })
      .pipe(
        map(bookings => {
          bookings.forEach(booking => {
            this.getDates(new Date(booking.startDate), new Date(booking.endDate)).forEach(date => {
              const normalizedDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());
              this.blockedDateTimestamps.add(normalizedDate.getTime());
            });
          });
        }),
        catchError(err => {
          console.error('预订日期加载失败', err);
          return of(null);
        })
      )
      .subscribe(() => {
        this.isDatesLoaded = true;
        // 数据就绪后主动触发日历重绘,强制重新执行日期过滤
        if (this.calendar) {
          this.calendar.updateTodaysDate();
        }
      });
  }

  // 纯同步过滤函数,无任何异步逻辑
  blockedDatesFilter = (d: Date): boolean => {
    if (!this.isDatesLoaded) return true;
    const normalizedCheckDate = new Date(d.getFullYear(), d.getMonth(), d.getDate());
    return !this.blockedDateTimestamps.has(normalizedCheckDate.getTime());
  };

  // 生成日期区间内所有日期的工具函数,保留原有逻辑
  private getDates(startDate: Date, endDate: Date): Date[] {
    const dates: Date[] = [];
    const currentDate = new Date(startDate);
    while (currentDate <= endDate) {
      dates.push(new Date(currentDate));
      currentDate.setDate(currentDate.getDate() + 1);
    }
    return dates;
  }

  // 原有业务方法保持不变
  handleMonthSelected(event: Date) { /* 原有逻辑 */ }
  onSelect(event: any) { /* 原有逻辑 */ }
}

模板优化(可选)

增加加载态,避免日期未加载完成时用户误选:

<div class="calendar-wrapper">
  <div *ngIf="!isDatesLoaded" class="calendar-loading">日期加载中...</div>
  <mat-calendar
    #calendar
    *ngIf="isDatesLoaded"
    (monthSelected)="handleMonthSelected($event)"
    (selectedChange)="onSelect($event)"
    [comparisonStart]="selectedDateRange.start"
    [comparisonEnd]="selectedDateRange.end"
    [selected]="selectedDateRange"
    [dateFilter]="blockedDatesFilter"
  >
  </mat-calendar>
</div>
避坑说明
  • 禁止在dateFilter绑定的函数内发起异步请求:该函数会在日历切换月份、渲染日期、选中日期时高频触发,会产生大量冗余请求,且异步返回结果不会触发布局重算。
  • 所有日期必须做归一化处理:接口返回的日期经常带有时分秒和时区偏移,直接比对时间戳很容易出现匹配失败,统一转成当天0点的本地时间再比对可以彻底避免这个问题。
  • 异步数据更新后必须主动调用calendar.updateTodaysDate():该方法会强制日历重新渲染所有日期单元格,重新执行过滤逻辑,否则日历会一直保留首次渲染的状态。
  • 阻塞日期优先用Set<number>存储时间戳:相比数组遍历find方法,Set.has()的时间复杂度为O(1),大日期量下性能优势明显。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.30 17:06:57