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

Angular实现类Jira看板的可拖拽调整小部件功能求助

Angular 看板小部件实现方案

针对你需要的Jira式看板小部件功能(10×10网格插入、拖拽、尺寸调整、删除),以下是基于需求修改的具体实现,解决动态插入溢出等问题:

一、网格布局基础(解决组件溢出)

用CSS Grid实现固定10×10网格,限制单元格尺寸并处理容器溢出:

.dashboard-grid {
  display: grid;
  grid-template-columns: repeat(10, 200px); /* 10列,每列200px */
  grid-template-rows: repeat(10, 200px); /* 10行,每行200px */
  gap: 10px;
  padding: 10px;
  width: fit-content;
  max-width: 100vw;
  overflow-x: auto; /* 横向溢出时滚动 */
  min-height: calc(100vh - 120px); /* 预留底部滑块空间 */
}

.widget {
  position: relative;
  background: #fff;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
  overflow: hidden;
}

二、动态组件插入与空位检测

1. 定义Widget数据结构

存储小部件的位置、尺寸和组件类型:

import { Type } from '@angular/core';

interface Widget {
  id: string;
  componentType: Type<any>;
  col: number; // 网格列位置(从1开始)
  row: number; // 网格行位置(从1开始)
  width: number; // 占用列数
  height: number; // 占用行数
}

2. 网格占用追踪与空位查找

维护10×10的网格状态,确保插入的小部件不会重叠或溢出:

// 初始化网格:0表示未占用,非0表示对应widget的id
private grid: (string | number)[][] = Array(10).fill(0).map(() => Array(10).fill(0));
public widgets: Widget[] = [];

// 查找首个可容纳指定尺寸的空位
private findFirstAvailableSlot(width: number, height: number): {col: number, row: number} | null {
  for (let row = 0; row <= 10 - height; row++) {
    for (let col = 0; col <= 10 - width; col++) {
      let isAvailable = true;
      // 检查目标区域是否全部未占用
      for (let r = row; r < row + height; r++) {
        for (let c = col; c < col + width; c++) {
          if (this.grid[r][c] !== 0) {
            isAvailable = false;
            break;
          }
        }
        if (!isAvailable) break;
      }
      if (isAvailable) {
        return { col: col + 1, row: row + 1 }; // 转为网格1-based索引
      }
    }
  }
  return null;
}

3. 底部滑块点击插入逻辑

点击滑块项时,自动插入到首个空位:

import { ReadingNowTrackerComponent } from './path-to-your-component';

onWidgetSelect(componentType: Type<any>) {
  const slot = this.findFirstAvailableSlot(1, 1); // 默认插入1×1尺寸
  if (!slot) {
    alert('网格已满,无法添加新小部件');
    return;
  }

  const newWidget: Widget = {
    id: `widget-${Date.now()}`,
    componentType,
    col: slot.col,
    row: slot.row,
    width: 1,
    height: 1
  };

  this.widgets.push(newWidget);
  this.markGridAsOccupied(newWidget);
}

// 标记网格为已占用
private markGridAsOccupied(widget: Widget) {
  const startRow = widget.row - 1;
  const startCol = widget.col - 1;
  for (let r = startRow; r < startRow + widget.height; r++) {
    for (let c = startCol; c < startCol + widget.width; c++) {
      this.grid[r][c] = widget.id;
    }
  }
}

4. 动态组件渲染模板

用ngComponentOutlet渲染动态组件,绑定网格位置样式:

<div class="dashboard-grid">
  <ng-template 
    *ngFor="let widget of widgets"
    [ngComponentOutlet]="widget.componentType"
    [style.grid-column]="`span ${widget.width}`"
    [style.grid-row]="`span ${widget.height}`"
    class="widget"
    draggable="true"
    (dragstart)="onDragStart($event, widget)"
    (dragover)="onDragOver($event)"
    (drop)="onDrop($event, widget)"
  >
    <!-- 小部件操作控件 -->
    <div class="resize-handle" (mousedown)="startResize($event, widget)"></div>
    <button class="delete-btn" (click)="deleteWidget(widget)">×</button>
  </ng-template>
</div>

<!-- 底部滑块 -->
<div class="widget-slider">
  <div class="slider-item" (click)="onWidgetSelect(ReadingNowTrackerComponent)">
    阅读追踪器
  </div>
  <!-- 其他小部件项 -->
</div>

三、拖拽移动功能

实现小部件拖拽交换位置,确保移动后不会重叠:

private draggedWidget: Widget | null = null;

onDragStart(event: DragEvent, widget: Widget) {
  this.draggedWidget = widget;
  event.dataTransfer!.setData('widgetId', widget.id);
}

onDragOver(event: DragEvent) {
  event.preventDefault(); // 允许放置
}

onDrop(event: DragEvent, targetWidget: Widget) {
  event.preventDefault();
  if (!this.draggedWidget || this.draggedWidget.id === targetWidget.id) return;

  // 先释放拖拽部件的原位置
  this.releaseGridSlot(this.draggedWidget);
  // 释放目标部件的位置
  this.releaseGridSlot(targetWidget);

  // 检查交换位置是否可行(两个部件的区域不重叠)
  const draggedSlotValid = this.checkSlotAvailability(this.draggedWidget, targetWidget.col, targetWidget.row);
  const targetSlotValid = this.checkSlotAvailability(targetWidget, this.draggedWidget.col, this.draggedWidget.row);

  if (draggedSlotValid && targetSlotValid) {
    // 交换两个部件的位置
    [this.draggedWidget.col, targetWidget.col] = [targetWidget.col, this.draggedWidget.col];
    [this.draggedWidget.row, targetWidget.row] = [targetWidget.row, this.draggedWidget.row];
    
    // 重新标记网格占用
    this.markGridAsOccupied(this.draggedWidget);
    this.markGridAsOccupied(targetWidget);
  } else {
    // 恢复原位置占用
    this.markGridAsOccupied(this.draggedWidget);
    this.markGridAsOccupied(targetWidget);
    alert('目标位置无法放置');
  }
  this.draggedWidget = null;
}

// 释放网格位置
private releaseGridSlot(widget: Widget) {
  const startRow = widget.row - 1;
  const startCol = widget.col - 1;
  for (let r = startRow; r < startRow + widget.height; r++) {
    for (let c = startCol; c < startCol + widget.width; c++) {
      this.grid[r][c] = 0;
    }
  }
}

// 检查部件在指定位置是否可用
private checkSlotAvailability(widget: Widget, targetCol: number, targetRow: number): boolean {
  const startRow = targetRow - 1;
  const startCol = targetCol - 1;
  // 检查是否超出网格边界
  if (startRow + widget.height > 10 || startCol + widget.width > 10) return false;
  // 检查是否与其他部件重叠
  for (let r = startRow; r < startRow + widget.height; r++) {
    for (let c = startCol; c < startCol + widget.width; c++) {
      if (this.grid[r][c] !== 0) return false;
    }
  }
  return true;
}

四、尺寸调整功能

通过右下角手柄实现小部件尺寸调整,限制在网格范围内:

.resize-handle {
  position: absolute;
  right: 5px;
  bottom: 5px;
  width: 15px;
  height: 15px;
  background: #ccc;
  cursor: se-resize;
}

.delete-btn {
  position: absolute;
  top: 5px;
  right: 5px;
  border: none;
  background: rgba(0,0,0,0.2);
  color: white;
  border-radius: 50%;
  width: 20px;
  height: 20px;
  cursor: pointer;
}
private resizingWidget: Widget | null = null;
private startX: number = 0;
private startY: number = 0;
private startWidth: number = 0;
private startHeight: number = 0;

startResize(event: MouseEvent, widget: Widget) {
  event.preventDefault();
  this.resizingWidget = widget;
  this.startX = event.clientX;
  this.startY = event.clientY;
  this.startWidth = widget.width;
  this.startHeight = widget.height;

  document.addEventListener('mousemove', this.onResizeMove.bind(this));
  document.addEventListener('mouseup', this.stopResize.bind(this));
}

onResizeMove(event: MouseEvent) {
  if (!this.resizingWidget) return;

  const deltaX = event.clientX - this.startX;
  const deltaY = event.clientY - this.startY;
  // 根据鼠标移动距离计算新尺寸(每200px对应1格)
  const newWidth = Math.max(1, Math.min(10 - this.resizingWidget.col + 1, this.startWidth + Math.round(deltaX / 200)));
  const newHeight = Math.max(1, Math.min(10 - this.resizingWidget.row + 1, this.startHeight + Math.round(deltaY / 200)));

  // 检查新尺寸是否可用
  this.releaseGridSlot(this.resizingWidget);
  const isAvailable = this.checkSlotAvailability(this.resizingWidget, this.resizingWidget.col, this.resizingWidget.row);

  if (isAvailable) {
    this.resizingWidget.width = newWidth;
    this.resizingWidget.height = newHeight;
    this.markGridAsOccupied(this.resizingWidget);
  } else {
    // 恢复原尺寸和网格占用
    this.markGridAsOccupied({...this.resizingWidget, width: this.startWidth, height: this.startHeight});
  }
}

stopResize() {
  this.resizingWidget = null;
  document.removeEventListener('mousemove', this.onResizeMove.bind(this));
  document.removeEventListener('mouseup', this.stopResize.bind(this));
}

五、删除功能

删除小部件并释放对应网格位置:

deleteWidget(widget: Widget) {
  this.releaseGridSlot(widget);
  this.widgets = this.widgets.filter(w => w.id !== widget.id);
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 03:35:05