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

Angular CDK Drag and Drop 资源日历拖拽drop事件卡顿性能优化求助

Angular CDK 拖拽资源日历卡顿优化方案

核心问题根因

  • 放置区域滥用cdkDropList:每个司机每小时的单元格都单独声明了cdkDropList,假设你有20个司机+24小时列,就会生成480个独立放置区域,CDK拖拽过程中需要遍历所有区域计算碰撞,直接导致拖拽过程卡顿
  • 模板中频繁执行函数:ngStyle和*ngIf中直接调用hoursToMs方法,Angular每次变更检测都会重复执行该函数,拖拽过程中变更检测触发频率极高,直接放大性能开销
  • 放置后全量重绘:drop事件接口请求成功后调用initDay重新拉取全量日历数据,所有DOM节点会销毁重建,导致放置后响应卡顿
  • 重复DOM元素冗余:每个货件都内嵌了完整的mat-menu实例,大量重复菜单节点占用内存,拖慢渲染速度
  • 未设置cdkDropList关联:所有cdkDropList没有设置[cdkDropListConnectedTo]属性,CDK会默认和所有同层级放置区域关联,进一步增加碰撞计算量
  • 原生拖拽属性冲突:模板中多处冗余添加了原生draggable="true"属性,和CDK拖拽逻辑冲突,额外触发不必要的事件

具体优化措施

1. 重构放置区域,减少cdkDropList数量

不要给每个小时单元格单独加cdkDropList,改为每个司机列只声明一个cdkDropList,放置时通过鼠标位置计算当前对应的小时,修改后HTML结构示例:

<div *ngFor="let driver of drivers; trackBy: trackByDriver" class="driver-column">
  <div class="column-info" [matTooltip]="driver.driver">
    {{ driver.driverName | slice: 0:21 }}
  </div>
  <!-- 整个司机列作为一个放置区域 -->
  <div 
    class="working-hours-droparea"
    cdkDropList
    id="shipmentCell"
    [cdkDropListConnectedTo]="['containersQueList', 'chassisQueList', 'shipmentsQueList', 'prepullsQueList', 'stagingsQueList']"
    (cdkDropListDropped)="dropIntoShipmentsTable($event, driver)"
  >
    <div *ngFor="let hour of workingHours; trackBy: trackByHour" class="hour-droparea"
      [ngStyle]="hour >= driver.startTimeMs && hour <= driver.endTimeMs ? {background: 'RGBA(0,114,0,0.37)'} : {}"
    >
      <ng-container *ngFor="let shipment of driver.items; trackBy: trackByShipment">
        <span
          (contextmenu)="onRightClick($event, shipment, driver)"
          *ngIf="hour === shipment.timeFromMs"
          [cdkDragData]="shipment"
          [cdkDragDisabled]="shipment.isReturn"
          [ngClass]="'shipment-duration-' + shipment.duration"
          [style.background]="shipment.colorCode"
          cdkDrag
        >
          <small>{{ shipment.originDestination }}</small>
          <br />
          <small class="container"><strong>{{ shipment.container }}</strong></small>
          <br />
          <small>{{ shipment.chassisPool }}</small>
        </span>
      </ng-container>
    </div>
  </div>
</div>
<!-- 全局只保留一个右键菜单 -->
<div
  #rightClickMenuTrigger
  [matMenuTriggerFor]="shipmentRightClick"
  [style.left]="menuTopLeftPosition.x"
  [style.top]="menuTopLeftPosition.y"
  style="visibility: hidden; position: fixed"
></div>
<mat-menu #shipmentRightClick="matMenu">
  <ng-template let-item="item" matMenuContent>
    <!-- 原有菜单项内容 -->
  </ng-template>
</mat-menu>

注意提前把driver.startTime、driver.endTime转为毫秒存在startTimeMs、endTimeMs属性,把shipment.timeFrom转为毫秒存在shipment.timeFromMs属性,不要在模板中调用转换函数

2. 放置时计算对应小时

在dropIntoShipmentsTable函数中通过拖拽事件的Y坐标,计算当前落在哪个小时区间,同时添加请求锁避免重复触发:

// 新增请求锁变量
isProcessingDrop = false;

dropIntoShipmentsTable(event: CdkDragDrop<any>, driver) {
  if (this.isProcessingDrop || event.item.data == null) return;
  this.isProcessingDrop = true;

  // 计算放置位置对应的小时
  const dropAreaRect = event.container.element.nativeElement.getBoundingClientRect();
  const offsetY = event.dropPoint.y - dropAreaRect.top;
  // 每个小时单元格高度,根据你实际的CSS调整
  const hourHeight = 60;
  const hourIndex = Math.max(0, Math.min(this.workingHours.length - 1, Math.floor(offsetY / hourHeight)));
  const targetHour = this.workingHours[hourIndex];

  // 原有updateDriver逻辑,hour替换为targetHour
  let updateDriver;
  if (event.previousContainer.id === "shipmentCell") {
    updateDriver = {
      DriverId: driver,
      id: event.item.data.id,
      TimeFrom: this.moment(targetHour).format("MM/DD/YYYY HH:mm"),
      userId: this.user.id().toString(),
    };
  } else {
    updateDriver = {
      DriverId: driver,
      containerId: event.previousContainer.id === "containersQueList" ? event.item.data.id : null,
      chassisId: event.previousContainer.id === "chassisQueList" ? event.item.data.id : null,
      id: ["shipmentsQueList", "prepullsQueList", "stagingsQueList"].includes(event.previousContainer.id) ? event.item.data.id : null,
      TimeFrom: this.moment(targetHour).format("MM/DD/YYYY HH:mm"),
      userId: this.user.id().toString(),
      isContainer: event.item.data.isContainer,
    };
  }

  this.subs.sink = this.api.assignShipmentToDriver(updateDriver).subscribe(() => {
    // 增量更新本地数据,不要调用initDay全量刷新
    // 1. 把货件从原位置删除
    // 2. 把货件添加到目标司机的targetHour对应位置
    this.isProcessingDrop = false;
  }, () => {
    this.isProcessingDrop = false;
  });
}

3. 补充trackBy函数减少DOM重建

给货件的循环添加trackByShipment函数:

trackByShipment(index: number, shipment: any) {
  return shipment.id;
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.02 19:15:03