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

Angular 13组织架构图:父节点与子节点曲线连接线实现问题

解决Angular 13组织架构图父子节点曲线连接线的对齐与自适应问题

核心问题分析

当前实现的SVG曲线未关联子节点的实际位置,固定路径无法匹配子卡片中心,且子节点展开后容器宽度变化时曲线无法自适应调整。以下是针对性解决方案:

步骤1:获取子节点位置与偏移量

在组件类中通过ViewChildren获取所有子节点实例,结合ElementRef获取DOM元素,计算每个子节点中心相对于父节点连接线容器(#nodeLinesContainer)中心的水平偏移。

import { Component, Input, ViewChild, ViewChildren, QueryList, ElementRef, AfterViewInit, OnDestroy } from '@angular/core';

@Component({
  selector: 'stakeholders-map-node',
  templateUrl: './stakeholders-map-node.component.html',
  styleUrls: ['./stakeholders-map-node.component.scss']
})
export class StakeholdersMapNodeComponent implements AfterViewInit, OnDestroy {
  @Input() nodeId: any;
  @ViewChild('nodeLinesContainer') nodeLinesContainer!: ElementRef<HTMLDivElement>;
  @ViewChildren('childNode') childNodes!: QueryList<ElementRef>;
  private resizeObservers: ResizeObserver[] = [];
  pathDataList: string[] = [];

  get node() {
    // 保留原有节点数据获取逻辑
    return /* 你的节点数据 */;
  }

  get centerIndex() {
    // 保留原有计算逻辑
    if (!this.node) { return 0 }
    const length = this.node.childrenCount();
    if (length % 2 === 0) {
      const lowerHalf = (length / 2) - 1;
      const upperHalf = lowerHalf + 1;
      return (lowerHalf + upperHalf) / 2;
    }
    return Math.floor(length / 2);
  }

  ngAfterViewInit(): void {
    this.updatePaths();
    this.setupResizeListeners();
    // 监听子节点列表变化(递归渲染时新增/移除节点)
    this.childNodes.changes.subscribe(() => {
      this.updatePaths();
      this.setupResizeListeners();
    });
  }

  ngOnDestroy(): void {
    this.resizeObservers.forEach(observer => observer.disconnect());
  }

  private setupResizeListeners(): void {
    // 销毁旧监听
    this.resizeObservers.forEach(observer => observer.disconnect());
    this.resizeObservers = [];

    // 监听连接线容器尺寸变化
    const containerObserver = new ResizeObserver(() => this.updatePaths());
    containerObserver.observe(this.nodeLinesContainer.nativeElement);
    this.resizeObservers.push(containerObserver);

    // 监听每个子节点尺寸变化
    this.childNodes.forEach(child => {
      const observer = new ResizeObserver(() => this.updatePaths());
      observer.observe(child.nativeElement);
      this.resizeObservers.push(observer);
    });
  }

  private updatePaths(): void {
    if (!this.node || !this.nodeLinesContainer) return;
    const containerRect = this.nodeLinesContainer.nativeElement.getBoundingClientRect();
    const containerCenterX = containerRect.width / 2;
    this.pathDataList = [];

    this.childNodes.forEach((child, index) => {
      const childRect = child.nativeElement.getBoundingClientRect();
      const childCenterX = childRect.left - containerRect.left + childRect.width / 2;
      const offset = childCenterX - containerCenterX;

      if (index === this.centerIndex) {
        // 中心节点:垂直直线对齐子节点中心
        this.pathDataList.push(`M ${childCenterX},0 L ${childCenterX},75`);
      } else if (index < this.centerIndex) {
        // 左侧节点:贝塞尔曲线,控制弯曲程度适配偏移量
        const controlOffset = Math.abs(offset) * 1.5;
        this.pathDataList.push(`M ${containerCenterX},0 C ${containerCenterX + controlOffset},0 ${childCenterX - controlOffset},75 ${childCenterX},75`);
      } else {
        // 右侧节点:贝塞尔曲线,控制弯曲程度适配偏移量
        const controlOffset = Math.abs(offset) * 1.5;
        this.pathDataList.push(`M ${containerCenterX},0 C ${containerCenterX - controlOffset},0 ${childCenterX + controlOffset},75 ${childCenterX},75`);
      }
    });
  }
}

步骤2:修改模板动态绑定路径

将原模板中固定的SVG path 替换为绑定组件中计算的pathDataList,移除固定viewBox让SVG自适应容器尺寸:

<!-- CHILDREN  -->
<div class="node-card">{{ node.name }}</div>
<div class="tree-children" *ngIf="node.isExpanded">
  <div class="tree-elements-group">
    <div #nodeLinesContainer class="node-lines flex flex-row">
      <div *ngFor="let child of node.children; let i = index" class="line">
        <svg width="100%" height="75" preserveAspectRatio="none">
          <path [d]="pathDataList[i]" stroke="#71CAAE" stroke-width="5" fill="none" vector-effect="non-scaling-stroke"></path>
        </svg>
      </div>
    </div>
    <ng-container *ngFor="let child of node.children; let i = index">
      <stakeholders-map-node
        #childNode
        [nodeId]="child"
        class="tree-node tree-child"></stakeholders-map-node>
    </ng-container>
  </div>
</div>

步骤3:调整CSS确保布局对齐

为.line和子节点添加样式,保证SVG容器与对应子节点宽度匹配:

.node-lines {
  position: relative;
  width: 100%;
  margin-bottom: 1rem; // 根据布局调整间距
}

.line {
  flex: 1;
  position: relative;
}

.tree-child {
  flex: 1;
  // 确保子节点宽度均匀分配,与.line容器一一对应
}

关键说明

  • 动态路径计算:通过getBoundingClientRect()获取元素实际位置,生成适配子节点中心的贝塞尔曲线
  • 自适应监听:使用ResizeObserver实时监听容器和子节点尺寸变化,自动更新曲线路径
  • 递归兼容:监听子节点列表的动态变化,确保递归渲染时曲线始终对齐正确

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.18 02:25:56