时间轴卡片重叠处理咨询:实现分层摆放逻辑
时间轴卡片重叠问题解决方案
我做了一个带卡片的简易时间轴,卡片通过函数定位到时间轴上,但部分卡片出现重叠。想要实现的逻辑是:如果卡片重叠,先把它放到原卡片下方;要是还重叠,就放到时间轴上方。不知道怎么正确实现这个逻辑。
现有代码
计算卡片位置的函数
const calculateCardPosition = (card) => { const start = -13800000000; const end = 0; let x = ((card.date - start) / (end - start)) * 100; if (x < 10) { x = x + 10; } else if (x > 90) { x = x - 10; } return { x }; };
时间轴组件代码
<section id="timeline" className="bg-gray-100 dark:bg-gray-800 py-10"> <div className="container mx-auto h-96 bg-sections-intro-light dark:bg-sections-intro-dark"> <h2 className="text-3xl font-bold text-center mb-8">Timeline</h2> {/* Timeline */} <div className="relative"> {/* Timeline line */} <div className="border-2 border-gray-300 dark:border-gray-700 absolute w-full h-0 left-0 top-1/2 transform -translate-y-1/2"></div> {/* Timeline cards */} <ul className="gap-4 absolute top-0 left-0 w-full"> {timelineContent.map((item) => ( <li id={item.id} key={item.id} className={`bg-white dark:bg-gray-700 rounded-md shadow-md p-2 cursor-pointer ${activeCard === item.date ? "border-2 border-blue-500" : ""} timeline-card`} style={{ position: "absolute", left: `${calculateCardPosition(item).x}%`, transform: "translateX(-50%)", width: "150px", }} onClick={() => scrollToSection(item.scrollTo)} onMouseEnter={() => handleCardHover(item.date)} onMouseLeave={() => handleCardHover(null)} > <h3 className="text-base font-semibold">{item.title}</h3> {activeCard === item.date && <p className="mt-2 text-sm">{item.description}</p>} </li> ))} </ul> </div> </div> </section>
问题示意图

实现思路与代码修改
要解决重叠问题,核心是跟踪已放置卡片的位置范围,然后为新卡片找到合适的Y轴位置(以时间轴中线为基准,上下偏移)。
步骤1:修改位置计算逻辑,加入Y轴位置判断
先定义核心常量,再通过遍历检查重叠,确定每个卡片的最终位置:
// 批量计算所有卡片的位置,处理重叠 const calculateAllCardPositions = (items) => { const start = -13800000000; const end = 0; // 存储已放置卡片的位置范围:x起止、y起止 const placedCards = []; // 卡片宽度转换为百分比(根据实际容器宽度调整) const cardWidthPercent = (150 / window.innerWidth) * 100; // 时间轴中线Y位置(%) const timelineMidY = 50; // 卡片上下偏移的间距(px),根据卡片高度调整 const cardGap = 60; // 转换为百分比的偏移量 const offsetPercent = (cardGap / window.innerHeight) * 100; return items.map(item => { // 计算基础X位置,保留原边界处理逻辑 let x = ((item.date - start) / (end - start)) * 100; if (x < 10) x += 10; else if (x > 90) x -= 10; // 当前卡片的X范围 const currentCardXStart = x - cardWidthPercent / 2; const currentCardXEnd = x + cardWidthPercent / 2; // 寻找合适的Y偏移 let yOffset = 0; // 初始在时间轴中线上 let isOverlapping = true; let tryBelow = true; // 先尝试下方偏移 let tryCount = 0; while (isOverlapping) { isOverlapping = false; // 检查当前偏移是否和已放置卡片重叠 for (const placed of placedCards) { const xOverlap = currentCardXStart < placed.xEnd && currentCardXEnd > placed.xStart; const currentYStart = timelineMidY + yOffset - offsetPercent / 2; const currentYEnd = timelineMidY + yOffset + offsetPercent / 2; const yOverlap = currentYStart < placed.yEnd && currentYEnd > placed.yStart; if (xOverlap && yOverlap) { isOverlapping = true; break; } } if (isOverlapping) { if (tryBelow) { // 第一次重叠,移到下方 yOffset += offsetPercent; tryBelow = false; } else { // 下方也重叠,移到上方;若仍重叠继续向上偏移 yOffset = tryCount === 0 ? -offsetPercent : yOffset - offsetPercent; tryCount++; } } } // 保存当前卡片的位置范围,供后续卡片判断 placedCards.push({ xStart: currentCardXStart, xEnd: currentCardXEnd, yStart: timelineMidY + yOffset - offsetPercent / 2, yEnd: timelineMidY + yOffset + offsetPercent / 2 }); return { x, y: timelineMidY + yOffset }; }); };
步骤2:修改组件渲染逻辑
在组件中先计算所有卡片的位置,再渲染:
import { useState, useEffect } from 'react'; // 在组件内部 const [cardPositions, setCardPositions] = useState(() => calculateAllCardPositions(timelineContent)); // 监听窗口大小变化,重新计算位置(响应式处理) useEffect(() => { const handleResize = () => { setCardPositions(calculateAllCardPositions(timelineContent)); }; window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, [timelineContent]); // 渲染卡片时使用计算好的位置 <ul className="gap-4 absolute top-0 left-0 w-full"> {timelineContent.map((item, index) => { const pos = cardPositions[index]; return ( <li id={item.id} key={item.id} className={`bg-white dark:bg-gray-700 rounded-md shadow-md p-2 cursor-pointer ${activeCard === item.date ? "border-2 border-blue-500" : ""} timeline-card`} style={{ position: "absolute", left: `${pos.x}%`, top: `${pos.y}%`, transform: "translate(-50%, -50%)", // 同时X、Y方向居中 width: "150px", }} onClick={() => scrollToSection(item.scrollTo)} onMouseEnter={() => handleCardHover(item.date)} onMouseLeave={() => handleCardHover(null)} > <h3 className="text-base font-semibold">{item.title}</h3> {activeCard === item.date && <p className="mt-2 text-sm">{item.description}</p>} </li> ); })} </ul>
说明
- 核心逻辑是先计算每个卡片的X位置,再通过遍历已放置卡片判断是否重叠,依次尝试下方、上方的偏移位置
- 偏移量
cardGap可以根据卡片实际高度调整,确保上下卡片之间有足够间距 - 若遇到大量密集重叠的卡片,可以扩展逻辑,增加更多层级的上下偏移(比如下方第二层、上方第二层)
内容的提问来源于stack exchange,提问作者Tetie
相关产品推荐
相关产品推荐

