【React】动态SVG连接线实现:图片与按钮的可视化映射

目录

  • 前言
  • 一、实现步骤
    • [1. 准备DOM引用](#1. 准备DOM引用)
    • [2. 计算连接线路径](#2. 计算连接线路径)
    • [3. 渲染连接线](#3. 渲染连接线)
    • [4. 添加交互效果](#4. 添加交互效果)
    • [5. 响应窗口变化和滚动](#5. 响应窗口变化和滚动)
    • [6. 优化路径计算(曲线路径)](#6. 优化路径计算(曲线路径))
  • 二、核心知识点解析
    • [1. DOM位置获取](#1. DOM位置获取)
    • [2. SVG路径绘制](#2. SVG路径绘制)
    • [3. React Refs系统](#3. React Refs系统)
    • [4. 事件处理](#4. 事件处理)
    • [5. 性能优化](#5. 性能优化)
  • [三、完整组件示例 + 运行效果](#三、完整组件示例 + 运行效果)
  • 四、高级优化技巧
    • [1. 性能优化](#1. 性能优化)
    • [2. 动画效果](#2. 动画效果)
    • [3. 响应设计](#3. 响应设计)
    • [4. 虚拟化渲染](#4. 虚拟化渲染)

前言

本文基于React 18 + TypeScript环境,详细讲解如何在SVG图形和按钮之间实现动态连接线。这种连接线不是通过拖拽改变,而是基于数据配置自动生成。

核心在于根据用户在配置弹窗中的选择,动态生成并渲染图标与按钮之间的连接线。


提示:以下是本篇文章正文内容,下面案例可供参考

一、实现步骤

实现SVG连接线的关键步骤:

​​(1)准备DOM引用​​:为端口和出口创建Ref

​​(2)计算路径​​:基于元素位置计算SVG路径

​​(3)渲染连线​​:使用SVG 元素
(4)添加交互:实现悬停高亮效果
(5)响应变化:监听窗口大小和滚动事件

1. 准备DOM引用

首先需要为每个端口和出口创建DOM引用,以便获取它们的位置信息:

typescript 复制代码
// 创建Refs存储端口和出口的DOM元素
const portNodeRefs = useRef<Record<string, HTMLElement | null>>({});
const exitNodeRefs = useRef<Record<string, HTMLElement | null>>({});
const containerRef = useRef<HTMLDivElement | null>(null);

// 在渲染函数中绑定Ref
{ports.map((port) => (
  <div
    key={port.name}
    ref={(el) => (portNodeRefs.current[port.name] = el)}
    // ...其他属性
  >
    {/* 端口图标 */}
  </div>
))}

{landingOptions.map((option) => (
  <div
    key={option.value}
    ref={(el) => (exitNodeRefs.current[option.value] = el)}
    // ...其他属性
  >
    {/* 出口按钮 */}
  </div>
))}

2. 计算连接线路径

这是核心步骤,需要计算每个端口到其分配出口的连线路径:

typescript 复制代码
const recomputeLines = () => {
  const container = containerRef.current;
  if (!container) return;
  
  const containerRect = container.getBoundingClientRect();
  const newLines: { path: string; port: string; exit: string }[] = [];
  
  // 遍历所有端口和其分配的出口
  Object.entries(portLandingMap).forEach(([portName, exits]) => {
    exits?.forEach((exitVal) => {
      const portEl = portNodeRefs.current[portName];
      const exitEl = exitNodeRefs.current[exitVal];
      
      if (!portEl || !exitEl) return;
      
      // 获取端口和出口的位置
      const portRect = portEl.getBoundingClientRect();
      const exitRect = exitEl.getBoundingClientRect();
      
      // 计算相对容器的坐标
      const startX = portRect.left - containerRect.left + portRect.width / 2;
      const startY = portRect.bottom - containerRect.top;
      const endX = exitRect.left - containerRect.left + exitRect.width / 2;
      const endY = exitRect.top - containerRect.top;
      
      // 计算路径(这里使用简单的折线,实际可优化为曲线)
      const path = `M ${startX} ${startY} L ${endX} ${endY}`;
      
      newLines.push({ path, port: portName, exit: exitVal });
    });
  });
  
  setLines(newLines);
};

3. 渲染连接线

在组件中渲染SVG路径:

typescript 复制代码
<div ref={containerRef} style={{ position: 'relative' }}>
  {/* 端口和出口的渲染 */}
  
  {/* SVG容器用于绘制连接线 */}
  <svg
    style={{
      position: 'absolute',
      top: 0,
      left: 0,
      width: '100%',
      height: '100%',
      pointerEvents: 'none', // 避免连线拦截鼠标事件
    }}
  >
    {lines.map((line, index) => (
      <path
        key={index}
        d={line.path}
        stroke="#1f78ff" // 线条颜色
        strokeWidth="2"  // 线条宽度
        fill="none"
      />
    ))}
  </svg>
</div>

4. 添加交互效果

实现鼠标悬停时高亮相关连线的效果:

typescript 复制代码
// 状态记录当前悬停的连线
const [hoverLineKey, setHoverLineKey] = useState<string | null>(null);

// 在渲染连线时添加交互
{lines.map((line, index) => {
  const lineKey = `${line.port}-${line.exit}`;
  return (
    <path
      key={index}
      d={line.path}
      stroke={hoverLineKey === lineKey ? '#ff0000' : '#1f78ff'}
      strokeWidth={hoverLineKey === lineKey ? 3 : 2}
      fill="none"
      onMouseEnter={() => setHoverLineKey(lineKey)}
      onMouseLeave={() => setHoverLineKey(null)}
    />
  );
})}

5. 响应窗口变化和滚动

确保在窗口大小变化或容器滚动时重新计算连线位置:

typescript 复制代码
useEffect(() => {
  const handleResize = () => recomputeLines();
  window.addEventListener('resize', handleResize);
  
  const container = containerRef.current;
  if (container) {
    container.addEventListener('scroll', recomputeLines);
  }
  
  return () => {
    window.removeEventListener('resize', handleResize);
    if (container) {
      container.removeEventListener('scroll', recomputeLines);
    }
  };
}, []);

6. 优化路径计算(曲线路径)

更美观的贝塞尔曲线实现:

typescript 复制代码
// 计算曲线路径
const controlPointY = (startY + endY) / 2;
const path = `M ${startX} ${startY} 
              C ${startX} ${controlPointY}, 
                ${endX} ${controlPointY}, 
                ${endX} ${endY}`;

二、核心知识点解析

1. DOM位置获取

使用getBoundingClientRect()获取元素位置和尺寸:

typescript 复制代码
// 关键计算步骤:
const containerRect = container.getBoundingClientRect();
const portRect = portEl.getBoundingClientRect();

// 计算相对容器位置的坐标
const px = portRect.left - containerRect.left + portRect.width / 2;
const py = portRect.bottom - containerRect.top;

2. SVG路径绘制

SVG 元素的d属性定义路径:
M x y:移动到指定坐标
L x y:画直线到指定坐标
C x1 y1, x2 y2, x y:三次贝塞尔曲线

typescript 复制代码
// SVG路径命令:
// M = moveto (起点)
// L = lineto (直线)
// C = curveto (贝塞尔曲线)
// Z = closepath (闭合路径)

const path = `M ${startX} ${startY} L ${midX} ${midY} L ${endX} ${endY}`;

3. React Refs系统

三种Ref使用场景:

​​DOM元素引用​​:useRef(null)+ ref={ref}

​​存储可变值​​:替代类组件的实例变量

​​转发Refs​​:forwardRef访问子组件DOM

typescript 复制代码
// 动态ref管理模式
const refs = useRef<Record<string, HTMLElement | null>>({});

// 在渲染循环中绑定
{elements.map(item => (
  <div
    key={item.id}
    ref={(el) => {
      refs.current[item.id] = el;
    }}
  />
))}

4. 事件处理

React合成事件系统:

使用onMouseEnter/onMouseLeave代替原生事件

事件委托提高性能

使用useEffect清理事件监听器

5. 性能优化

typescript 复制代码
// 1. 使用useLayoutEffect避免闪烁
useLayoutEffect(() => {
  recomputeLines();
}, [dependencies]);

// 2. 防抖处理
const debouncedRecompute = useMemo(
  () => debounce(recomputeLines, 100),
  []
);

// 3. 条件重计算
const shouldRecompute = useMemo(() => {
  return JSON.stringify(portLandingMap) !== lastMappingRef.current;
}, [portLandingMap]);

三、完整组件示例 + 运行效果

📢注意:我现在给的这个套完整示例,仅作为参考,跟上面的步骤还是不太一样的,比如缺少编辑弹窗等,但是为了方便直接运行,看到连线效果,改动后如下:

typescript 复制代码
// App.tsx
import React, { useRef, useState, useLayoutEffect, useCallback, useMemo } from 'react';

interface Port {
  name: string;
  isWan?: boolean;
  active?: boolean;
}

interface LandingOption {
  value: string;
  label: string;
}

interface ConnectionLine {
  path: string;
  port: string;
  exit: string;
}

const PortConnectionDemo: React.FC = () => {
  // 状态管理
  const [ports] = useState<Port[]>([
    { name: 'eth0', isWan: true, active: true },
    { name: 'eth1', isWan: false, active: true },
    { name: 'eth2', isWan: false, active: false },
    { name: 'eth3', isWan: false, active: true },
  ]);
  
  const [landingOptions] = useState<LandingOption[]>([
    { value: 'LA', label: 'Los Angeles' },
    { value: 'SG', label: 'Singapore' },
    { value: 'NY', label: 'New York' },
  ]);
  
  const [portLandingMap] = useState<Record<string, string[]>>({
    eth1: ['LA', 'SG'],
    eth3: ['NY'],
  });
  
  // DOM引用
  const portNodeRefs = useRef<Record<string, HTMLDivElement | null>>({});
  const exitNodeRefs = useRef<Record<string, HTMLDivElement | null>>({});
  const containerRef = useRef<HTMLDivElement>(null);
  const linesSvgRef = useRef<SVGSVGElement>(null);
  
  // 连线状态
  const [lines, setLines] = useState<ConnectionLine[]>([]);
  const [hoverLineKey, setHoverLineKey] = useState<string | null>(null);
  
  // 核心连线计算函数
  const recomputeLines = useCallback(() => {
    const container = containerRef.current;
    if (!container) {
      if (lines.length) setLines([]);
      return;
    }

    const containerRect = container.getBoundingClientRect();
    const basePortRadius = 5;
    const minExitRadius = 4;
    const exitRadiusCache: Record<string, number> = {};
    const newLines: ConnectionLine[] = [];
    const exitBusYCache: Record<string, number> = {};
    const exitConnections: Record<string, string[]> = {};

    // 建立出口连接关系映射
    Object.keys(portLandingMap).forEach((portName) => {
      const exits = portLandingMap[portName];
      exits?.forEach((exitVal) => {
        if (!exitConnections[exitVal]) exitConnections[exitVal] = [];
        exitConnections[exitVal].push(portName);
      });
    });

    // 计算每个出口的总线Y坐标
    const getBusYForExit = (exitVal: string): number => {
      if (exitBusYCache[exitVal] !== undefined) return exitBusYCache[exitVal];
      
      const exitEl = exitNodeRefs.current[exitVal];
      const eyTop = exitEl ? exitEl.getBoundingClientRect().top - containerRect.top : 0;
      const connectedPorts = Object.keys(portLandingMap).filter((portName) =>
        portLandingMap[portName]?.includes(exitVal)
      );
      
      const portBottoms: number[] = [];
      connectedPorts.forEach((portName) => {
        const portEl = portNodeRefs.current[portName];
        if (portEl) {
          portBottoms.push(portEl.getBoundingClientRect().bottom - containerRect.top);
        }
      });
      
      const avgPortBottom = portBottoms.length
        ? portBottoms.reduce((a, b) => a + b, 0) / portBottoms.length
        : eyTop - 50;
      
      // 40% 靠上位置,避免总线过低
      const busY = Math.round(avgPortBottom + (eyTop - avgPortBottom) * 0.4);
      exitBusYCache[exitVal] = busY;
      return busY;
    };

    // 为每个端口到出口计算路径
    for (const portName of Object.keys(portLandingMap)) {
      const exits = portLandingMap[portName];
      if (!exits || exits.length === 0) continue;
      
      const portEl = portNodeRefs.current[portName];
      if (!portEl) continue;
      
      const portRect = portEl.getBoundingClientRect();
      const startX = portRect.left - containerRect.left + portRect.width / 2;
      const startY = portRect.bottom - containerRect.top;
      
      // 检查端口是否在容器可见范围内
      if (startX < 0 || startX > containerRect.width) continue;
      
      for (const exitVal of exits) {
        const exitEl = exitNodeRefs.current[exitVal];
        if (!exitEl) continue;
        
        const exitRect = exitEl.getBoundingClientRect();
        const endX = exitRect.left - containerRect.left + exitRect.width / 2;
        const endY = exitRect.top - containerRect.top;
        
        // 检查出口是否在容器可见范围内
        if (endX < 0 || endX > containerRect.width) continue;
        
        const direction = endX >= startX ? 1 : -1;
        const busY = getBusYForExit(exitVal);
        
        // 计算分层偏移,避免重叠
        const connectedPorts = exitConnections[exitVal] || [];
        const laneIndex = connectedPorts.indexOf(portName);
        const laneCount = connectedPorts.length || 1;
        const laneSpread = Math.min(26, Math.max(8, laneCount * 5));
        const laneOffset = laneCount === 1 ? 0 : (laneIndex / (laneCount - 1) - 0.5) * laneSpread;
        const laneBusY = busY + laneOffset;
        
        // 计算圆角半径
        const downSpace = laneBusY - startY;
        const upSpace = endY - laneBusY;
        
        let portRadius = Math.min(
          basePortRadius,
          Math.max(3, Math.floor(Math.min(downSpace, upSpace) * 0.55))
        );
        
        if (Math.min(downSpace, upSpace) < basePortRadius * 1.6) {
          portRadius = Math.min(portRadius, Math.max(2, Math.floor(Math.min(downSpace, upSpace) * 0.45)));
        }
        
        const canUsePortArc = downSpace > portRadius * 1.8 && portRadius >= 2;
        const portVertEndY = Math.max(startY + 4, laneBusY - portRadius);
        const sweep1 = direction > 0 ? 0 : 1;
        const sweep2 = direction > 0 ? 1 : 0;
        
        // 出口侧圆角计算
        if (exitRadiusCache[exitVal] === undefined) {
          const upSpaceAtExit = endY - busY;
          let exitRadius = Math.min(Math.max(minExitRadius, portRadius), Math.floor(upSpaceAtExit * 0.55));
          
          if (upSpaceAtExit < portRadius * 1.6) {
            exitRadius = Math.min(exitRadius, Math.max(minExitRadius, Math.floor(upSpaceAtExit * 0.45)));
          }
          
          // 协调端口和出口圆角
          if (Math.abs(exitRadius - portRadius) > 3) {
            exitRadius = exitRadius > portRadius ? portRadius + 3 : exitRadius;
          }
          exitRadiusCache[exitVal] = Math.max(minExitRadius, exitRadius);
        }
        
        let exitRadius = exitRadiusCache[exitVal];
        const exitLaneUpSpace = endY - laneBusY;
        
        if (exitLaneUpSpace < exitRadius + 6) {
          const availableSpace = exitLaneUpSpace - 4;
          exitRadius = availableSpace >= minExitRadius ? Math.min(exitRadius, availableSpace) : 0;
        }
        
        const canUseExitArc = exitRadius >= minExitRadius;
        const midStartX = startX + direction * portRadius;
        const midEndX = endX - direction * (canUseExitArc ? exitRadius : 0);
        
        let useSimple = false;
        if (Math.abs(midEndX - midStartX) < portRadius * 2.2) {
          useSimple = true;
        }
        
        let path: string;
        
        if (useSimple) {
          const arcY = (startY + endY) / 2;
          if (canUsePortArc && canUseExitArc) {
            path = [
              `M ${startX} ${startY}`,
              `V ${arcY - portRadius}`,
              `A ${portRadius} ${portRadius} 0 0 ${sweep1} ${startX + direction * portRadius} ${arcY}`,
              `H ${endX - direction * exitRadius}`,
              `A ${exitRadius} ${exitRadius} 0 0 ${sweep2} ${endX} ${arcY + exitRadius}`,
              `V ${endY}`,
            ].join(' ');
          } else {
            // 简化路径:直线连接
            path = `M ${startX} ${startY} V ${arcY} H ${endX} V ${endY}`;
          }
        } else {
          // 标准路径:带圆角的折线
          const arc1EndY = laneBusY;
          if (canUsePortArc && canUseExitArc) {
            path = [
              `M ${startX} ${startY}`,
              `V ${portVertEndY}`,
              `A ${portRadius} ${portRadius} 0 0 ${sweep1} ${startX + direction * portRadius} ${arc1EndY}`,
              `H ${midEndX}`,
              `A ${exitRadius} ${exitRadius} 0 0 ${sweep2} ${endX} ${laneBusY + exitRadius}`,
              `V ${endY}`,
            ].join(' ');
          } else {
            // 降级为直角折线
            path = `M ${startX} ${startY} V ${laneBusY} H ${endX} V ${endY}`;
          }
        }
        
        newLines.push({ path, port: portName, exit: exitVal });
      }
    }
    
    // 只有当连线确实变化时才更新状态
    const sameLength = newLines.length === lines.length;
    let sameContent = sameLength;
    if (sameLength) {
      for (let i = 0; i < newLines.length; i++) {
        if (newLines[i].path !== lines[i].path) {
          sameContent = false;
          break;
        }
      }
    }
    if (!sameContent) {
      setLines(newLines);
    }
  }, [lines, portLandingMap]);

  // 初始化和响应式更新
  useLayoutEffect(() => {
    recomputeLines();
  }, [recomputeLines]);

  // 窗口大小变化监听
  useLayoutEffect(() => {
    const handleResize = () => {
      recomputeLines();
    };
    
    window.addEventListener('resize', handleResize);
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, [recomputeLines]);

  // 颜色映射
  const exitColorMap = useMemo(() => {
    const baseColors = ['#1f78ff', '#10b981', '#f59e0b', '#6366f1', '#ef4444'];
    const map: Record<string, string> = {};
    landingOptions.forEach((opt, idx) => {
      map[opt.value] = baseColors[idx % baseColors.length];
    });
    return map;
  }, [landingOptions]);

  // 处理鼠标悬停
  const handlePortMouseEnter = useCallback((portName: string) => {
    setHoverLineKey(`${portName}::ALL`);
  }, []);

  const handlePortMouseLeave = useCallback(() => {
    setHoverLineKey(null);
  }, []);

  const handleExitMouseEnter = useCallback((exitVal: string) => {
    setHoverLineKey(`ANY::${exitVal}`);
  }, []);

  const handleExitMouseLeave = useCallback(() => {
    setHoverLineKey(null);
  }, []);

  const handleLineMouseEnter = useCallback((port: string, exit: string) => {
    setHoverLineKey(`${port}::${exit}`);
  }, []);

  const handleLineMouseLeave = useCallback(() => {
    setHoverLineKey(null);
  }, []);

  return (
    <div 
      ref={containerRef}
      style={{ 
        position: 'relative', 
        padding: '40px 20px',
        background: 'linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%)',
        minHeight: '500px',
        borderRadius: '12px',
        border: '1px solid #e2e8f0'
      }}
    >
      {/* 标题 */}
      <div style={{ textAlign: 'center', marginBottom: '40px' }}>
        <h2 style={{ color: '#1e293b', margin: 0 }}>网络端口连接示意图</h2>
        <p style={{ color: '#64748b', margin: '8px 0 0 0' }}>
          展示端口与出口之间的动态连接关系
        </p>
      </div>

      {/* 端口区域 */}
      <div style={{ 
        display: 'flex', 
        justifyContent: 'center',
        gap: '30px',
        marginBottom: '120px',
        flexWrap: 'wrap' as const
      }}>
        {ports.map((port) => (
          <div
            key={port.name}
            ref={(el) => {
              if (el) portNodeRefs.current[port.name] = el;
            }}
            style={{
              padding: '16px',
              border: `2px solid ${port.active ? '#1f78ff' : '#cbd5e1'}`,
              borderRadius: '8px',
              background: port.active ? '#ffffff' : '#f8fafc',
              boxShadow: port.active ? '0 4px 12px rgba(31, 120, 255, 0.15)' : 'none',
              textAlign: 'center',
              minWidth: '80px',
              transition: 'all 0.3s ease',
              opacity: port.active ? 1 : 0.6,
              cursor: 'pointer'
            }}
            onMouseEnter={() => handlePortMouseEnter(port.name)}
            onMouseLeave={handlePortMouseLeave}
          >
            <div style={{ 
              fontSize: '24px', 
              marginBottom: '8px',
              color: port.active ? '#1f78ff' : '#94a3b8'
            }}>
              {port.isWan ? '🌐' : '🔌'}
            </div>
            <div style={{ 
              fontWeight: '600',
              color: port.active ? '#1e293b' : '#94a3b8'
            }}>
              {port.name}
            </div>
            {port.isWan && (
              <div style={{ 
                fontSize: '12px', 
                color: '#64748b',
                marginTop: '4px'
              }}>
                WAN
              </div>
            )}
            {!port.active && (
              <div style={{ 
                fontSize: '12px', 
                color: '#ef4444',
                marginTop: '4px'
              }}>
                未激活
              </div>
            )}
          </div>
        ))}
      </div>

      {/* 出口区域 */}
      <div style={{ 
        display: 'flex', 
        justifyContent: 'center',
        gap: '25px',
        flexWrap: 'wrap' as const
      }}>
        {landingOptions.map((option) => (
          <div
            key={option.value}
            ref={(el) => {
              if (el) exitNodeRefs.current[option.value] = el;
            }}
            style={{
              padding: '12px 24px',
              background: exitColorMap[option.value],
              color: 'white',
              borderRadius: '6px',
              fontWeight: '600',
              boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
              cursor: 'pointer',
              transition: 'all 0.3s ease',
              minWidth: '100px',
              textAlign: 'center'
            }}
            onMouseEnter={() => handleExitMouseEnter(option.value)}
            onMouseLeave={handleExitMouseLeave}
          >
            {option.label}
          </div>
        ))}
      </div>

      {/* SVG连接线容器 */}
      <svg
        ref={linesSvgRef}
        style={{
          position: 'absolute',
          top: 0,
          left: 0,
          width: '100%',
          height: '100%',
          pointerEvents: 'none',
          zIndex: 1
        }}
      >
        {lines.map((line, index) => {
          const lineKey = `${line.port}::${line.exit}`;
          const isHovered = 
            hoverLineKey === lineKey ||
            hoverLineKey === `${line.port}::ALL` ||
            hoverLineKey === `ANY::${line.exit}`;
          
          const baseColor = exitColorMap[line.exit] || '#1f78ff';
          
          return (
            <path
              key={index}
              d={line.path}
              stroke={baseColor}
              strokeWidth={isHovered ? 3 : 2}
              fill="none"
              strokeLinecap="round"
              strokeLinejoin="round"
              style={{
                opacity: isHovered ? 1 : 0.8,
                filter: isHovered ? 'drop-shadow(0 0 6px rgba(0,0,0,0.3))' : 'none',
                transition: 'all 0.3s ease',
                pointerEvents: 'stroke' as const
              }}
              onMouseEnter={() => handleLineMouseEnter(line.port, line.exit)}
              onMouseLeave={handleLineMouseLeave}
            />
          );
        })}
      </svg>

      {/* 图例说明 */}
      <div style={{
        position: 'absolute',
        bottom: '20px',
        left: '20px',
        background: 'rgba(255, 255, 255, 0.9)',
        padding: '12px 16px',
        borderRadius: '8px',
        border: '1px solid #e2e8f0',
        fontSize: '12px',
        color: '#64748b'
      }}>
        <div style={{ fontWeight: '600', marginBottom: '4px' }}>图例说明:</div>
        <div>• 实线边框:端口已激活</div>
        <div>• 虚线边框:端口未激活</div>
        <div>• 悬停效果:高亮相关连接线</div>
        <div>• 圆角路径:自适应圆角计算</div>
        <div>• 分层处理:避免连线重叠</div>
      </div>
    </div>
  );
};

export default PortConnectionDemo;

四、高级优化技巧

1. 性能优化

typescript 复制代码
// 使用防抖避免频繁重计算
const recomputeLines = useCallback(debounce(() => {
  // 计算逻辑...
}, 100), [portLandingMap, landingOptions]);

// 使用React.memo避免不必要重渲染
const PortIcon = React.memo(({ port }) => {
  // 渲染逻辑
});

2. 动画效果

typescript 复制代码
// 使用react-spring添加动画
import { useSpring, animated } from 'react-spring';

const AnimatedPath = animated.path;

// 在组件中使用
<AnimatedPath
  d={line.path}
  style={{
    stroke: colorSpring,
    strokeWidth: widthSpring
  }}
/>

3. 响应设计

typescript 复制代码
/* 使用CSS媒体查询适应不同屏幕 */
@media (max-width: 768px) {
  .port-container {
    flex-direction: column;
  }
  
  .exit-container {
    flex-wrap: wrap;
  }
}

4. 虚拟化渲染

对于大量连线的情况,使用虚拟化技术:

typescript 复制代码
import { useVirtual } from 'react-virtual';

const virtualizer = useVirtual({
  size: lines.length,
  parentRef: containerRef,
  estimateSize: useCallback(() => 20, []),
});

{virtualizer.virtualItems.map((virtualRow) => (
  <path
    key={virtualRow.index}
    d={lines[virtualRow.index].path}
    // ...其他属性
    style={{
      position: 'absolute',
      top: 0,
      left: 0,
      width: '100%',
      height: `${virtualRow.size}px`,
      transform: `translateY(${virtualRow.start}px)`
    }}
  />
))}
相关推荐
小刘不知道叫啥2 小时前
React 源码揭秘 | suspense 和 unwind流程
前端·javascript·react.js
szial3 小时前
为什么 React 推荐 “不可变更新”:深入理解 React 的核心设计理念
前端·react.js·前端框架
mapbar_front3 小时前
面试是一门学问
前端·面试
90后的晨仔3 小时前
Vue 3 中 Provide / Inject 在异步时不起作用原因分析(二)?
前端·vue.js
90后的晨仔3 小时前
Vue 3 中 Provide / Inject 在异步时不起作用原因分析(一)?
前端·vue.js
90后的晨仔3 小时前
Vue 异步组件(defineAsyncComponent)全指南:写给新手的小白实战笔记
前端·vue.js
木易 士心4 小时前
Vue 与 React 深度对比:底层原理、开发体验与实际性能
前端·javascript·vue.js
冷冷的菜哥4 小时前
react多文件分片上传——支持拖拽与进度展示
前端·react.js·typescript·多文件上传·分片上传
玄魂4 小时前
VChart 官网上线 智能助手与分享功能
前端·llm·数据可视化