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

React实现单败淘汰赛UI:奇数选手时连接线问题求助

单淘汰赛UI连接线问题解决方案

核心问题分析

直接用DOM元素绘制连接线会干扰布局流,且视口外元素因浏览器裁剪机制无法正常渲染。改用SVG绘制连接线是最优方案——SVG独立于DOM布局,支持任意区域绘图,滚动时也能完整显示线条。

具体实现步骤

1. 重构组件结构

将选手卡片与SVG绘图容器分层,SVG置于底层(绝对定位),卡片在上层,避免布局冲突:

import { useRef, useEffect } from 'react';

const SingleElimination = ({ rounds }) => {
  const svgRef = useRef(null);
  const matchRefs = useRef({});

  return (
    <div className="tournament-container">
      {/* SVG容器:负责绘制所有连接线 */}
      <svg ref={svgRef} className="connection-lines"></svg>
      {/* 选手轮次容器 */}
      <div className="rounds-container">
        {rounds.map((round, roundIndex) => (
          <div key={roundIndex} className="round">
            {round.matches.map((match, matchIndex) => (
              <div 
                key={matchIndex} 
                className="match"
                ref={el => matchRefs.current[`${roundIndex}-${matchIndex}`] = el}
              >
                {match.players.map(player => (
                  <div key={player.id} className="player-card">
                    {player.name}
                  </div>
                ))}
              </div>
            ))}
          </div>
        ))}
      </div>
    </div>
  );
};

export default SingleElimination;

2. 编写动态绘图逻辑

通过getBoundingClientRect()获取选手卡片位置,计算并生成SVG路径:

const drawConnections = () => {
  const svg = svgRef.current;
  const container = svg?.parentElement;
  if (!svg || !container) return;

  // 清空旧线条
  svg.innerHTML = '';
  const containerRect = container.getBoundingClientRect();

  // 遍历每一轮(最后一轮无需连接)
  rounds.forEach((round, roundIndex) => {
    if (roundIndex >= rounds.length - 1) return;
    const nextRound = rounds[roundIndex + 1];

    // 每两场比赛对应下一轮一场比赛
    for (let i = 0; i < round.matches.length; i += 2) {
      const match1 = matchRefs.current[`${roundIndex}-${i}`];
      const match2 = matchRefs.current[`${roundIndex}-${i + 1}`];
      const nextMatch = matchRefs.current[`${roundIndex + 1}-${i / 2}`];

      if (!match1 || !match2 || !nextMatch) continue;

      // 转换为SVG内部坐标(消除容器视口偏移影响)
      const rect1 = match1.getBoundingClientRect();
      const rect2 = match2.getBoundingClientRect();
      const nextRect = nextMatch.getBoundingClientRect();

      const x1 = rect1.right - containerRect.left;
      const y1 = rect1.top + rect1.height / 2 - containerRect.top;
      const x2 = rect2.right - containerRect.left;
      const y2 = rect2.top + rect2.height / 2 - containerRect.top;
      const xNext = nextRect.left - containerRect.left;
      const yNext = nextRect.top + nextRect.height / 2 - containerRect.top;

      // 计算中间水平节点,生成直角连接线
      const midX = (x1 + xNext) / 2;

      // 创建SVG路径元素(需使用SVG命名空间)
      const path1 = document.createElementNS('http://www.w3.org/2000/svg', 'path');
      path1.setAttribute('d', `M ${x1} ${y1} L ${midX} ${y1} L ${midX} ${yNext} L ${xNext} ${yNext}`);
      path1.setAttribute('stroke', '#333');
      path1.setAttribute('stroke-width', '2');
      path1.setAttribute('fill', 'none');

      const path2 = document.createElementNS('http://www.w3.org/2000/svg', 'path');
      path2.setAttribute('d', `M ${x2} ${y2} L ${midX} ${y2} L ${midX} ${yNext} L ${xNext} ${yNext}`);
      path2.setAttribute('stroke', '#333');
      path2.setAttribute('stroke-width', '2');
      path2.setAttribute('fill', 'none');

      svg.appendChild(path1);
      svg.appendChild(path2);
    }
  });
};

3. 绑定更新触发时机

在组件挂载、数据更新、滚动或窗口 resize 时重新绘制线条:

useEffect(() => {
  drawConnections();

  const handleUpdate = () => drawConnections();
  container?.addEventListener('scroll', handleUpdate);
  window.addEventListener('resize', handleUpdate);

  return () => {
    container?.removeEventListener('scroll', handleUpdate);
    window.removeEventListener('resize', handleUpdate);
  };
}, [rounds]);

4. 配套CSS调整

确保容器和卡片布局稳定,SVG不干扰交互:

.tournament-container {
  position: relative;
  overflow: auto;
  min-height: 100vh;
  padding: 20px;
}

.connection-lines {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  pointer-events: none; /* 避免遮挡卡片交互 */
}

.rounds-container {
  display: flex;
  gap: 80px; /* 轮次间预留连接线空间 */
  position: relative;
  z-index: 1; /* 确保卡片在SVG上方 */
}

.round {
  display: flex;
  flex-direction: column;
  gap: 40px; /* 比赛间间距 */
}

.match {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.player-card {
  padding: 12px 24px;
  background: #fff;
  border: 1px solid #e0e0e0;
  border-radius: 4px;
  min-width: 160px;
  box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}

优化建议

  • 若需要平滑曲线,可将路径中的L指令替换为贝塞尔曲线C,比如M ${x1} ${y1} C ${midX} ${y1}, ${midX} ${yNext}, ${xNext} ${yNext},让线条更美观。
  • 若轮次数据频繁更新,可添加防抖函数减少重绘次数,提升性能。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.07 06:49:54