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

时间轴卡片重叠处理咨询:实现分层摆放逻辑

时间轴卡片重叠问题解决方案

我做了一个带卡片的简易时间轴,卡片通过函数定位到时间轴上,但部分卡片出现重叠。想要实现的逻辑是:如果卡片重叠,先把它放到原卡片下方;要是还重叠,就放到时间轴上方。不知道怎么正确实现这个逻辑。


现有代码

计算卡片位置的函数

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.17 18:14:59