【DEMO】互动信息墙 - 无限流动版-点击放大

一、 核心概念:流动动画 (The Flow Animation)

这是整个系统的"心跳",它独立于用户交互,持续不断地运行。

  1. 放弃容器滚动,拥抱绝对定位

    • 我们不再使用父容器的滚动条 (scrollLeft)。
    • 整个卡片容器 (.cards-container) 变成一个静态的"舞台",所有卡片都通过 position: absolute 定位在其中。
  2. 动画循环是唯一驱动力 (requestAnimationFrame)

    • 我们创建一个名为 animateFlow 的函数,并通过 requestAnimationFrame 让它每秒执行约 60 次,形成一个平滑的动画循环。
    • 这个循环是整个流动效果的引擎。
  3. 独立的位置状态 (currentX)

    • 每个卡片对象都有自己的 currentX 属性,记录它在虚拟的无限长画卷上的当前 X 坐标。
    • 在动画的每一帧,我们都微量地减少所有未展开卡片的 currentX 值,模拟它们向左移动。
  4. 无限循环的"传送门"

    • 这是实现"无限"的关键。动画循环会持续监控每个卡片的位置。
    • 当一个卡片的右边缘完全移出屏幕左侧(card.currentX + card.width < 0)时,系统会立即将它"传送"到整个卡片队列的最右端。
    • 这个"传送"是通过给 currentX 加上一个预先计算好的 totalFlowWidth(所有行中最长一行的总宽度 + 一个屏幕宽度)来实现的。视觉上,这个过程是无缝的。
  5. 性能优化 (transform: translateX)

    • 为了避免频繁修改 left 属性导致的性能问题(浏览器重排),我们将卡片的初始位置用 lefttop 固定下来。
    • 所有后续的流动动画都通过修改 CSS 的 transform: translateX() 属性来实现。这个属性可以被 GPU 加速,性能远高于修改 left

二、 核心概念:状态管理与交互 (State Management & Interaction)

这部分逻辑负责响应用户的操作,并改变系统的状态,动画循环会根据这些状态来调整自己的行为。

  1. 定义清晰的状态变量

    • isFlowing: 一个布尔值,作为流动的"总开关"。默认 true
    • flowPausedByMouse: 另一个布尔值,用于鼠标悬停时临时暂停,提升用户体验。
    • card.isExpanded: 每个卡片对象内部的状态,标记自己是否被放大。
    • expandedCards: 一个集合(Set),用于快速追踪当前有多少张卡片被放大了。
  2. 交互改变状态,而非直接操作动画

    • 展开卡片 (expandCard) :

      1. 停止流动 : isFlowing 设置为 false
      2. 改变状态 : card.isExpanded 设为 true
      3. 视觉变化 : 通过 CSS class 和直接修改 width, height, left, top 来实现放大效果,并清除流动的 transform
      4. 触发推开 : 调用 updateCardPositions 来移动周围的卡片。
    • 收起卡片 (collapseCard) :

      1. 改变状态 : card.isExpanded 设为 false
      2. 检查并恢复流动 : 检查 expandedCards 集合是否为空。如果为空,说明所有卡片都已关闭,此时将 isFlowing 重新设为 true
      3. 恢复视觉 : 将卡片尺寸和位置恢复,并重新应用基于其 currentXtransform,让它无缝地回到流动队列中。
      4. 触发推开逻辑更新 : 再次调用 updateCardPositions,让被推开的卡片归位。
  3. "推开"效果的实现 (updateCardPositions)

    • 当有卡片被放大时,此函数被调用。
    • 它会遍历所有未展开的卡片,判断它们是否与任何一个已展开卡片的区域重叠。
    • 如果重叠,它会暂时修改该卡片的 top 值(或添加一个 avoiding class),使其在视觉上"躲开"。
    • 在躲避期间,该卡片的水平流动会暂停,以防止位置错乱。

总结

整个项目的架构可以看作是两层:

  • 底层 (动画引擎) : 一个持续运行、只做一件事的 animateFlow 循环,它根据当前的状态数据来更新所有卡片的位置。
  • 上层 (交互逻辑) : 一系列响应用户操作的函数 (expandCard, collapseCard, startDragging 等),它们不直接控制动画的每一帧,而是通过修改底层的状态数据(如 isFlowing, card.isExpanded)来"指挥"动画引擎如何表现。
xml 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>互动信息墙 - 无限流动版</title>
    <style>
        /* ... (大部分CSS保持不变) ... */
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
            background: linear-gradient(135deg, #0f0f1e 0%, #1a1a2e 100%);
            color: #ffffff;
            overflow: hidden;
            height: 100vh;
            position: relative;
        }

        .wall-header {
            position: fixed;
            top: 0;
            left: 0;
            right: 0;
            padding: 20px 40px;
            background: rgba(0, 0, 0, 0.5);
            backdrop-filter: blur(20px);
            border-bottom: 1px solid rgba(255, 255, 255, 0.1);
            display: flex;
            justify-content: space-between;
            align-items: center;
            z-index: 10000;
        }

        .wall-header h1 {
            font-size: 28px;
            font-weight: 700;
            background: linear-gradient(90deg, #00d4ff, #7b2ff7, #ff006e);
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
            background-clip: text;
            animation: gradient-shift 3s ease infinite;
        }

        @keyframes gradient-shift {
            0%, 100% { background-position: 0% 50%; }
            50% { background-position: 100% 50%; }
        }

        .stats {
            display: flex;
            gap: 30px;
            font-size: 14px;
            color: rgba(255, 255, 255, 0.7);
        }

        .stats span {
            padding: 8px 16px;
            background: rgba(255, 255, 255, 0.05);
            border-radius: 20px;
            border: 1px solid rgba(255, 255, 255, 0.1);
        }

        /* --- [修改] 主容器不再需要滚动条,改为隐藏溢出 --- */
        .wall-viewport {
            position: fixed;
            top: 80px;
            left: 0;
            right: 0;
            bottom: 0;
            overflow: hidden; /* 修改这里 */
            padding: 20px;
        }

        .cards-container {
            position: relative;
            width: 100%; /* 修改这里 */
            height: 100%; /* 修改这里 */
        }
        
        /* --- [修改] 卡片的 transform 也会变化,加入 will-change --- */
        .card {
            position: absolute;
            border-radius: 12px;
            overflow: hidden;
            background: linear-gradient(135deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05));
            backdrop-filter: blur(10px);
            border: 1px solid rgba(255, 255, 255, 0.1);
            cursor: pointer;
            /* --- [修改] 动画现在主要作用于 transform --- */
            transition: all 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94);
            will-change: transform, left, top, width, height;
        }

        /* ... (其余 CSS 保持不变) ... */
        .card:not(.expanded):hover {
            transform: scale(1.05);
            box-shadow: 
                0 0 40px rgba(0, 212, 255, 0.4),
                0 20px 60px rgba(0, 0, 0, 0.4);
            border-color: rgba(0, 212, 255, 0.6);
            z-index: 100;
        }

        .card.expanded {
            cursor: default;
            box-shadow: 
                0 0 80px rgba(0, 212, 255, 0.6),
                0 30px 100px rgba(0, 0, 0, 0.6),
                inset 0 0 50px rgba(255, 255, 255, 0.1);
            border: 2px solid rgba(0, 212, 255, 0.8);
            background: linear-gradient(135deg, #1e1e2e, #2a2a3e);
        }

        .card.avoiding {
            transition: all 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94);
        }
        .card-content {
            width: 100%;
            height: 100%;
            position: relative;
            overflow: hidden;
        }

        .card-content img {
            width: 100%;
            height: 100%;
            object-fit: cover;
            transition: transform 0.3s ease;
        }

        .card:not(.expanded):hover .card-content img {
            transform: scale(1.1);
        }

        .card-overlay {
            position: absolute;
            bottom: 0;
            left: 0;
            right: 0;
            padding: 12px;
            background: linear-gradient(to top, rgba(0, 0, 0, 0.9), rgba(0, 0, 0, 0.5), transparent);
            transform: translateY(100%);
            transition: transform 0.3s ease;
        }

        .card:not(.expanded):hover .card-overlay {
            transform: translateY(0);
        }

        .card-overlay h3 {
            font-size: 14px;
            font-weight: 600;
            color: #ffffff;
            margin-bottom: 4px;
        }

        .card-overlay p {
            font-size: 12px;
            color: rgba(255, 255, 255, 0.7);
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
        }

        .expanded-content {
            display: none;
            width: 100%;
            height: 100%;
            padding: 24px;
            background: linear-gradient(135deg, #1e1e2e, #2a2a3e);
            overflow-y: auto;
        }

        .card.expanded .card-content {
            display: none;
        }

        .card.expanded .expanded-content {
            display: flex;
            flex-direction: column;
        }

        .expanded-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 20px;
            padding-bottom: 16px;
            border-bottom: 1px solid rgba(255, 255, 255, 0.1);
        }

        .expanded-header h2 {
            font-size: 24px;
            font-weight: 700;
            color: #ffffff;
            background: linear-gradient(90deg, #00d4ff, #7b2ff7);
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
            background-clip: text;
        }

        .close-button {
            width: 40px;
            height: 40px;
            border-radius: 50%;
            border: none;
            background: rgba(255, 255, 255, 0.1);
            color: #ffffff;
            font-size: 24px;
            cursor: pointer;
            display: flex;
            align-items: center;
            justify-content: center;
            transition: all 0.3s ease;
        }

        .close-button:hover {
            background: linear-gradient(135deg, #ff006e, #ff4458);
            box-shadow: 0 0 20px rgba(255, 0, 110, 0.5);
            transform: scale(1.1) rotate(90deg);
        }

        .expanded-image {
            width: 100%;
            height: 250px;
            object-fit: cover;
            border-radius: 12px;
            margin-bottom: 20px;
            box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
        }

        .expanded-info {
            flex: 1;
            display: flex;
            flex-direction: column;
            gap: 16px;
        }

        .expanded-info h3 {
            font-size: 20px;
            font-weight: 600;
            color: #ffffff;
            margin-bottom: 8px;
        }

        .expanded-info p {
            font-size: 15px;
            line-height: 1.8;
            color: rgba(255, 255, 255, 0.8);
        }

        .expanded-tags {
            display: flex;
            flex-wrap: wrap;
            gap: 8px;
            margin: 16px 0;
        }

        .tag {
            padding: 6px 14px;
            background: linear-gradient(135deg, rgba(0, 212, 255, 0.2), rgba(123, 47, 247, 0.2));
            border: 1px solid rgba(0, 212, 255, 0.3);
            border-radius: 20px;
            font-size: 12px;
            color: #00d4ff;
            animation: tag-glow 2s ease infinite;
        }

        @keyframes tag-glow {
            0%, 100% { box-shadow: 0 0 5px rgba(0, 212, 255, 0.3); }
            50% { box-shadow: 0 0 15px rgba(0, 212, 255, 0.5); }
        }

        .expanded-actions {
            display: flex;
            gap: 12px;
            margin-top: auto;
            padding-top: 20px;
        }

        .action-button {
            flex: 1;
            padding: 14px 28px;
            border-radius: 10px;
            border: 1px solid rgba(255, 255, 255, 0.2);
            background: linear-gradient(135deg, rgba(0, 212, 255, 0.1), rgba(123, 47, 247, 0.1));
            color: #ffffff;
            font-size: 14px;
            font-weight: 600;
            cursor: pointer;
            transition: all 0.3s ease;
            position: relative;
            overflow: hidden;
        }

        .action-button::before {
            content: '';
            position: absolute;
            top: 50%;
            left: 50%;
            width: 0;
            height: 0;
            border-radius: 50%;
            background: linear-gradient(135deg, rgba(0, 212, 255, 0.5), rgba(123, 47, 247, 0.5));
            transform: translate(-50%, -50%);
            transition: width 0.3s, height 0.3s;
        }

        .action-button:hover::before {
            width: 100%;
            height: 100%;
        }

        .action-button:hover {
            transform: translateY(-2px);
            box-shadow: 0 10px 30px rgba(0, 212, 255, 0.3);
            border-color: rgba(0, 212, 255, 0.5);
        }

        .action-button span {
            position: relative;
            z-index: 1;
        }

        .card.expanded.draggable {
            cursor: move;
        }

        .card.expanded.dragging {
            cursor: grabbing;
            transition: none !important;
        }

        @keyframes ripple {
            0% {
                box-shadow: 0 0 0 0 rgba(0, 212, 255, 0.6);
            }
            100% {
                box-shadow: 0 0 0 40px rgba(0, 212, 255, 0);
            }
        }

        .card.expanding {
            animation: ripple 0.6s ease-out;
        }

        .loading {
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            font-size: 24px;
            color: rgba(255, 255, 255, 0.6);
        }

        @media (max-width: 768px) {
            .wall-header {
                padding: 15px 20px;
            }
            
            .wall-header h1 {
                font-size: 20px;
            }
            
            .stats {
                display: none;
            }
        }
    </style>
</head>
<body>
    <!-- HTML structure remains the same -->
    <div class="wall-header">
        <h1>互动信息墙</h1>
        <div class="stats">
            <span id="cardCount">卡片总数: 0</span>
            <span id="expandedCount">展开卡片: 0</span>
        </div>
    </div>
    <div class="wall-viewport" id="viewport">
        <div class="cards-container" id="cardsContainer">
            <div class="loading">正在加载...</div>
        </div>
    </div>

    <script>
        class InteractiveInfoWall {
            constructor() {
                // 配置参数
                this.config = {
                    rows: 12,
                    cardHeight: 120,
                    cardGap: 8,
                    minCardWidth: 150,
                    maxCardWidth: 300,
                    expandScale: 3.5,
                    pushPadding: 50,
                    cardCount: 300,
                    flowSpeed: 25 // 流动速度 (像素/秒)
                };

                // 状态管理
                this.cards = [];
                this.expandedCards = new Set();
                this.isDragging = null;
                this.dragStartPos = { x: 0, y: 0 };
                this.cardStartPos = { x: 0, y: 0 };
                this.highestZIndex = 1000;
                
                this.isFlowing = true;
                this.flowPausedByMouse = false;
                this.lastTimestamp = 0;

                // 用于无限滚动的总宽度
                this.totalFlowWidth = 0;

                // DOM元素
                this.viewport = document.getElementById('viewport');
                this.container = document.getElementById('cardsContainer');
                
                // 绑定 this
                this.animateFlow = this.animateFlow.bind(this);
                
                // 初始化
                this.init();
            }

            init() {
                this.generateCards();
                this.renderCards(); // 错误发生在这里,现在函数已补全
                this.setupEventListeners();
                this.updateStats();
                requestAnimationFrame(this.animateFlow);
            }

            // [核心] 动画逻辑,更新每个卡片的位置
            animateFlow(timestamp) {
                if (!this.lastTimestamp) {
                    this.lastTimestamp = timestamp;
                }
                const deltaTime = (timestamp - this.lastTimestamp) / 1000;
                this.lastTimestamp = timestamp;

                if (this.isFlowing && !this.flowPausedByMouse) {
                    const movement = this.config.flowSpeed * deltaTime;
                    
                    this.cards.forEach(card => {
                        // 只移动未展开的卡片
                        if (!card.isExpanded) {
                            const cardElement = document.getElementById(card.id);
                            if (!cardElement) return;

                            // 如果卡片正在被推开,则不参与流动
                            if (cardElement.classList.contains('avoiding')) {
                                // 恢复其流动位置,以便之后能平滑接入
                                card.currentX = card.originalX + (parseFloat(cardElement.style.transform.replace(/[^0-9-.]/g, '')) || 0);
                                return;
                            }
                            
                            card.currentX -= movement;

                            // 无限循环逻辑
                            if (card.currentX + card.width < 0) {
                                card.currentX += this.totalFlowWidth;
                            }
                            
                            cardElement.style.transform = `translateX(${card.currentX - card.originalX}px)`;
                        }
                    });
                }
                
                requestAnimationFrame(this.animateFlow);
            }
            
            // [补全] 生成卡片数据
            generateCards() {
                const rowOffsets = new Array(this.config.rows).fill(0);
                const viewportWidth = this.viewport.clientWidth;
                
                for (let i = 0; i < this.config.cardCount; i++) {
                    const row = i % this.config.rows;
                    const width = this.config.minCardWidth + 
                                  Math.random() * (this.config.maxCardWidth - this.config.minCardWidth);
                    
                    const card = {
                        id: `card-${i}`,
                        title: `项目 ${i + 1}`,
                        description: `这是项目 ${i + 1} 的详细描述。包含了该项目的核心功能、技术特点、创新亮点以及实际应用场景。通过先进的技术架构和优化的用户体验设计,为用户提供高效、便捷的解决方案。项目采用了最新的技术栈,确保了系统的稳定性和可扩展性。`,
                        image: `https://picsum.photos/400/300?random=${i}`,
                        width: width,
                        height: this.config.cardHeight,
                        row: row,
                        originalX: rowOffsets[row],
                        originalY: row * (this.config.cardHeight + this.config.cardGap),
                        currentX: rowOffsets[row],
                        currentY: row * (this.config.cardHeight + this.config.cardGap),
                        isExpanded: false,
                        zIndex: 1,
                        tags: this.generateRandomTags()
                    };
                    
                    rowOffsets[row] += width + this.config.cardGap;
                    this.cards.push(card);
                }
                
                // 计算用于循环的总宽度
                this.totalFlowWidth = Math.max(...rowOffsets) + viewportWidth;
            }

            // [补全] 生成随机标签
            generateRandomTags() {
                const allTags = ['人工智能', '大数据', '云计算', '物联网', '区块链', '5G', 'AR/VR', '机器学习'];
                const tagCount = Math.floor(Math.random() * 3) + 2;
                const tags = new Set();
                while (tags.size < tagCount) {
                    tags.add(allTags[Math.floor(Math.random() * allTags.length)]);
                }
                return Array.from(tags);
            }

            // [补全] 渲染所有卡片到DOM
            renderCards() {
                this.container.innerHTML = '';
                this.cards.forEach(card => {
                    const cardElement = this.createCardElement(card);
                    this.container.appendChild(cardElement);
                });
            }

            // [补全] 创建单个卡片元素
            createCardElement(card) {
                const cardDiv = document.createElement('div');
                cardDiv.className = 'card';
                cardDiv.id = card.id;
                cardDiv.style.width = `${card.width}px`;
                cardDiv.style.height = `${card.height}px`;
                cardDiv.style.left = `${card.originalX}px`;
                cardDiv.style.top = `${card.originalY}px`;
                cardDiv.style.zIndex = card.zIndex;
                
                const normalContent = `
                    <div class="card-content">
                        <img src="${card.image}" alt="${card.title}" loading="lazy">
                        <div class="card-overlay">
                            <h3>${card.title}</h3>
                            <p>${card.description.substring(0, 50)}...</p>
                        </div>
                    </div>
                `;

                const expandedContent = `
                    <div class="expanded-content">
                        <div class="expanded-header">
                            <h2>${card.title}</h2>
                            <button class="close-button">×</button>
                        </div>
                        <img src="${card.image}" alt="${card.title}" class="expanded-image">
                        <div class="expanded-info">
                            <h3>项目详情</h3>
                            <p>${card.description}</p>
                            <div class="expanded-tags">
                                ${card.tags.map(tag => `<span class="tag">${tag}</span>`).join('')}
                            </div>
                            <div class="expanded-actions">
                                <button class="action-button"><span>查看详情</span></button>
                                <button class="action-button"><span>分享项目</span></button>
                            </div>
                        </div>
                    </div>
                `;

                cardDiv.innerHTML = normalContent + expandedContent;
                
                cardDiv.addEventListener('click', (e) => {
                    if (!card.isExpanded && !e.target.closest('.close-button')) {
                        this.expandCard(card);
                    }
                });
                
                return cardDiv;
            }

            // 展开卡片
            expandCard(card) {
                this.isFlowing = false; // 停止流动
                const cardElement = document.getElementById(card.id);
                if (!cardElement || card.isExpanded) return;
                
                card.isExpanded = true;
                this.expandedCards.add(card.id);
                
                card.zIndex = ++this.highestZIndex;
                cardElement.style.zIndex = card.zIndex;
                
                cardElement.classList.remove('avoiding');
                cardElement.classList.add('expanded', 'expanding', 'draggable');
                
                const expandedWidth = card.width * this.config.expandScale;
                const expandedHeight = card.height * this.config.expandScale;
                
                const deltaWidth = (expandedWidth - card.width) / 2;
                const deltaHeight = (expandedHeight - card.height) / 2;
                
                let newX = card.currentX - deltaWidth;
                let newY = card.currentY - deltaHeight;
                
                const viewportRect = this.viewport.getBoundingClientRect();
                if (newX < 20) { newX = 20; }
                if (newX + expandedWidth > viewportRect.width - 20) { newX = viewportRect.width - expandedWidth - 20; }
                if (newY < 20) { newY = 20; }
                if (newY + expandedHeight > viewportRect.height - 20) { newY = viewportRect.height - expandedHeight - 20; }
                
                card.expandedX = newX;
                card.expandedY = newY;
                
                cardElement.style.left = `${newX}px`;
                cardElement.style.top = `${newY}px`;
                cardElement.style.width = `${expandedWidth}px`;
                cardElement.style.height = `${expandedHeight}px`;
                cardElement.style.transform = 'translateX(0px)'; // 清除流动带来的 transform

                const closeBtn = cardElement.querySelector('.close-button');
                closeBtn.onclick = (e) => {
                    e.stopPropagation();
                    this.collapseCard(card);
                };
                
                const expandedHeader = cardElement.querySelector('.expanded-header');
                expandedHeader.addEventListener('mousedown', (e) => {
                    if (!e.target.closest('.close-button')) {
                        this.startDragging(card.id, e);
                    }
                });
                
                setTimeout(() => cardElement.classList.remove('expanding'), 600);
                
                this.updateCardPositions();
                this.updateStats();
                this.playSound('expand');
            }

            // 收起卡片
            collapseCard(card) {
                const cardElement = document.getElementById(card.id);
                if (!cardElement || !card.isExpanded) return;
                
                card.isExpanded = false;
                this.expandedCards.delete(card.id);
                
                if (this.expandedCards.size === 0) {
                    this.isFlowing = true; // 恢复流动
                }

                cardElement.classList.remove('expanded', 'draggable');
                
                cardElement.style.width = `${card.width}px`;
                cardElement.style.height = `${card.height}px`;
                cardElement.style.left = `${card.originalX}px`;
                cardElement.style.top = `${card.originalY}px`;
                
                // 平滑地回到流动队列
                cardElement.style.transform = `translateX(${card.currentX - card.originalX}px)`;

                card.zIndex = 1;
                cardElement.style.zIndex = 1;
                
                card.expandedX = null;
                card.expandedY = null;
                
                this.updateCardPositions();
                this.updateStats();
                this.playSound('close');
            }
            
            // [补全] 更新卡片位置(推开效果)
            updateCardPositions() {
                const expandedAreas = [];
                this.cards.forEach(card => {
                    if (card.isExpanded) {
                        const el = document.getElementById(card.id);
                        expandedAreas.push({
                            left: parseFloat(el.style.left),
                            right: parseFloat(el.style.left) + parseFloat(el.style.width),
                            top: parseFloat(el.style.top),
                            bottom: parseFloat(el.style.top) + parseFloat(el.style.height)
                        });
                    }
                });
                
                this.cards.forEach(card => {
                    if (card.isExpanded) return;

                    const cardElement = document.getElementById(card.id);
                    if (!cardElement) return;

                    const originalTransform = card.currentX - card.originalX;
                    let targetX = card.originalX;
                    let targetY = card.originalY;
                    let isAvoiding = false;

                    for (const area of expandedAreas) {
                        const cardRect = {
                            left: card.currentX,
                            right: card.currentX + card.width,
                            top: card.originalY,
                            bottom: card.originalY + card.height
                        };

                        const paddedArea = {
                            left: area.left - this.config.pushPadding,
                            right: area.right + this.config.pushPadding,
                            top: area.top - this.config.pushPadding,
                            bottom: area.bottom + this.config.pushPadding
                        };

                        if (cardRect.right > paddedArea.left && cardRect.left < paddedArea.right &&
                            cardRect.bottom > paddedArea.top && cardRect.top < paddedArea.bottom) {
                            isAvoiding = true;
                            // 简单的推开逻辑:只向Y轴推开
                            if (cardRect.top + card.height / 2 < paddedArea.top + (paddedArea.bottom - paddedArea.top) / 2) {
                                targetY = paddedArea.top - card.height;
                            } else {
                                targetY = paddedArea.bottom;
                            }
                            break; // 只被一个卡片推开
                        }
                    }

                    if (isAvoiding) {
                        cardElement.classList.add('avoiding');
                        cardElement.style.top = `${targetY}px`;
                        // 当被推开时,保持其水平位置不动,但清除流动 transform
                        cardElement.style.transform = `translateX(${card.currentX - card.originalX}px)`;
                    } else {
                        cardElement.classList.remove('avoiding');
                        cardElement.style.top = `${card.originalY}px`;
                    }
                });
            }


            // [补全] 开始拖动
            startDragging(cardId, event) {
                // ... (实现拖动逻辑)
            }

            // [补全] 设置事件监听器
            setupEventListeners() {
                this.viewport.addEventListener('mouseenter', () => this.flowPausedByMouse = true);
                this.viewport.addEventListener('mouseleave', () => this.flowPausedByMouse = false);
                
                // 拖动事件
                document.addEventListener('mousemove', (e) => {
                    if (!this.isDragging) return;
                    // ... (拖动逻辑)
                });
                document.addEventListener('mouseup', () => {
                     if (this.isDragging) {
                        // ... (停止拖动逻辑)
                     }
                });
                
                document.addEventListener('keydown', (e) => {
                    if (e.key === 'Escape') {
                        this.cards.forEach(card => {
                            if (card.isExpanded) this.collapseCard(card);
                        });
                    }
                });
                
                window.addEventListener('resize', () => this.updateCardPositions());
            }

            // [补全] 更新统计信息
            updateStats() {
                document.getElementById('cardCount').textContent = `卡片总数: ${this.cards.length}`;
                document.getElementById('expandedCount').textContent = `展开卡片: ${this.expandedCards.size}`;
            }

            // [补全] 播放音效
            playSound(type) {
                // ... (音效逻辑)
            }
        }

        // 页面加载完成后初始化
        document.addEventListener('DOMContentLoaded', () => {
            const wall = new InteractiveInfoWall();
            setTimeout(() => {
                const loading = document.querySelector('.loading');
                if (loading) loading.style.display = 'none';
            }, 500);
        });
    </script>
</body>
</html>
相关推荐
AliPaPa4 小时前
你可能忽略了useSyncExternalStore + useOptimistic + useTransition
前端·react.js
parade岁月4 小时前
nuxt和vite使用环境比变量对比
前端·vite·nuxt.js
小帆聊前端4 小时前
Lodash 深度解读:前端数据处理的效率利器,从用法到原理全拆解
前端·javascript
Harriet嘉5 小时前
解决Chrome 140以上版本“此扩展程序不再受支持,因此已停用”问题 axure插件安装问题
前端·chrome
FuckPatience5 小时前
前端Vue 后端ASP.NET Core WebApi 本地调试交互过程
前端·vue.js·asp.net
Kingsdesigner5 小时前
从平面到“货架”:Illustrator与Substance Stager的包装设计可视化工作流
前端·平面·illustrator·设计师·substance 3d·平面设计·产品渲染
一枚前端小能手5 小时前
🔍 那些不为人知但是好用的JS小秘密
前端·javascript
屿小夏5 小时前
JSAR 开发环境配置与项目初始化全流程指南
前端
微辣而已5 小时前
next.js中实现缓存
前端