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
相关产品推荐
相关产品推荐

