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

