Angular 16拖拽式流程图构建器开发需求(带箭头连接)
Angular 16 拖拽式流程图构建器实现方案
Angular Material的待办拖拽示例仅支持线性列表排序,无法满足自由拖拽定位+卡片连线的需求。以下是基于Angular CDK Drag-Drop和SVG的实现方案:
核心功能实现拆解
- 自由拖拽定位:利用
@angular/cdk/drag-drop的自由拖拽模式,支持卡片在画布内任意移动 - 卡片连线:通过SVG绘制箭头,实现卡片间的连接关系管理
- 实时更新:拖拽卡片时同步更新连线坐标
步骤1:基础组件搭建
卡片组件(支持拖拽和连接点)
创建可拖拽的卡片组件,包含顶部/底部连接点用于触发连线:
// flow-card.component.ts import { Component, Input, Output, EventEmitter } from '@angular/core'; import { CdkDrag, CdkDragMove } from '@angular/cdk/drag-drop'; @Component({ selector: 'app-flow-card', template: ` <div class="flow-card" cdkDrag (cdkDragMoved)="onDragMove($event)" [cdkDragBoundary]="dragBoundary" [style.left.px]="position.x" [style.top.px]="position.y"> <div class="connection-points"> <div class="top-point" (click)="triggerConnection('top')"></div> <div class="bottom-point" (click)="triggerConnection('bottom')"></div> </div> <div class="card-content">{{ cardId }}</div> </div> `, styles: [` .flow-card { position: absolute; width: 180px; min-height: 80px; background: #ffffff; border: 1px solid #e0e0e0; border-radius: 4px; padding: 12px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); cursor: move; } .connection-points { position: absolute; left: 50%; transform: translateX(-50%); width: 8px; } .top-point, .bottom-point { width: 8px; height: 8px; background: #1976d2; border-radius: 50%; cursor: crosshair; } .top-point { top: -4px; } .bottom-point { bottom: -4px; } .card-content { margin: 20px 0 0; } `] }) export class FlowCardComponent { @Input() cardId: string; @Input() position: { x: number, y: number }; @Input() dragBoundary: string; @Output() cardPositionUpdated = new EventEmitter<{ id: string, pos: { x: number, y: number } }>(); @Output() connectionTriggered = new EventEmitter<{ cardId: string, direction: string }>(); onDragMove(event: CdkDragMove) { const newPos = event.source.getFreeDragPosition(); this.cardPositionUpdated.emit({ id: this.cardId, pos: newPos }); } triggerConnection(direction: string) { this.connectionTriggered.emit({ cardId: this.cardId, direction }); } }
步骤2:画布与连线管理组件
父组件负责管理所有卡片、连线数据,通过SVG绘制箭头:
// flow-builder.component.ts import { Component, ViewChild, ElementRef } from '@angular/core'; interface FlowCard { id: string; position: { x: number, y: number }; } interface FlowConnection { startId: string; endId: string; startPoint: { x: number, y: number }; endPoint: { x: number, y: number }; } @Component({ selector: 'app-flow-builder', template: ` <div class="flow-canvas" #canvas> <!-- SVG连线层 --> <svg class="connections-layer"> <line *ngFor="let conn of connections" [attr.x1]="conn.startPoint.x" [attr.y1]="conn.startPoint.y" [attr.x2]="conn.endPoint.x" [attr.y2]="conn.endPoint.y" stroke="#1976d2" stroke-width="2" marker-end="url(#arrowhead)"/> <defs> <marker id="arrowhead" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto"> <polygon points="0 0, 10 3, 0 6" fill="#1976d2"/> </marker> </defs> </svg> <!-- 卡片列表 --> <app-flow-card *ngFor="let card of cards" [cardId]="card.id" [position]="card.position" [dragBoundary]="'.flow-canvas'" (cardPositionUpdated)="updateCardPosition($event)" (connectionTriggered)="handleConnection($event)"> </app-flow-card> <!-- 添加卡片按钮 --> <button class="add-btn" (click)="addNewCard()">+ 添加卡片</button> </div> `, styles: [` .flow-canvas { position: relative; width: 100%; height: 85vh; border: 1px solid #f0f0f0; background-color: #fafafa; overflow: auto; } .connections-layer { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; } .add-btn { position: fixed; bottom: 20px; right: 20px; padding: 8px 16px; background: #1976d2; color: white; border: none; border-radius: 4px; cursor: pointer; } `] }) export class FlowBuilderComponent { @ViewChild('canvas') canvasRef: ElementRef; cards: FlowCard[] = [ { id: '初始化节点', position: { x: 150, y: 100 } }, { id: '处理节点', position: { x: 400, y: 100 } } ]; connections: FlowConnection[] = []; pendingConnection: { cardId: string, direction: string } | null = null; addNewCard() { this.cards.push({ id: `节点${this.cards.length + 1}`, position: { x: Math.random() * 600, y: Math.random() * 400 } }); } updateCardPosition(event: { id: string, pos: { x: number, y: number } }) { const targetCard = this.cards.find(c => c.id === event.id); if (targetCard) { targetCard.position = event.pos; this.refreshConnectionsForCard(event.id); } } handleConnection(event: { cardId: string, direction: string }) { if (!this.pendingConnection) { // 启动新的连线 this.pendingConnection = event; } else { // 完成连线创建 const startCard = this.cards.find(c => c.id === this.pendingConnection.cardId); const endCard = this.cards.find(c => c.id === event.cardId); if (startCard && endCard) { const startPoint = this.getCardConnectionPoint(startCard, this.pendingConnection.direction); const endPoint = this.getCardConnectionPoint(endCard, event.direction); this.connections.push({ startId: startCard.id, endId: endCard.id, startPoint, endPoint }); } this.pendingConnection = null; } } getCardConnectionPoint(card: FlowCard, direction: string) { const cardWidth = 180; const cardHeight = 80; switch (direction) { case 'top': return { x: card.position.x + cardWidth / 2, y: card.position.y }; case 'bottom': return { x: card.position.x + cardWidth / 2, y: card.position.y + cardHeight }; default: return card.position; } } refreshConnectionsForCard(cardId: string) { this.connections.forEach(conn => { if (conn.startId === cardId) { const card = this.cards.find(c => c.id === cardId); conn.startPoint = this.getCardConnectionPoint(card, 'bottom'); } if (conn.endId === cardId) { const card = this.cards.find(c => c.id === cardId); conn.endPoint = this.getCardConnectionPoint(card, 'top'); } }); } }
扩展优化建议
- 连线删除:给SVG线条添加
pointer-events: stroke,绑定点击事件实现删除 - 卡片样式自定义:通过
@Input()传递样式类或颜色参数 - 流程图导出:将
cards和connections序列化为JSON,支持保存/加载 - 对齐辅助:拖拽时显示网格线或磁吸对齐效果
内容的提问来源于stack exchange,提问作者Poojith
相关产品推荐
相关产品推荐

