
一、 核心概念:流动动画 (The Flow Animation)
这是整个系统的"心跳",它独立于用户交互,持续不断地运行。
-
放弃容器滚动,拥抱绝对定位:
- 我们不再使用父容器的滚动条 (
scrollLeft
)。 - 整个卡片容器 (
.cards-container
) 变成一个静态的"舞台",所有卡片都通过position: absolute
定位在其中。
- 我们不再使用父容器的滚动条 (
-
动画循环是唯一驱动力 (
requestAnimationFrame
) :- 我们创建一个名为
animateFlow
的函数,并通过requestAnimationFrame
让它每秒执行约 60 次,形成一个平滑的动画循环。 - 这个循环是整个流动效果的引擎。
- 我们创建一个名为
-
独立的位置状态 (
currentX
) :- 每个卡片对象都有自己的
currentX
属性,记录它在虚拟的无限长画卷上的当前 X 坐标。 - 在动画的每一帧,我们都微量地减少所有未展开卡片的
currentX
值,模拟它们向左移动。
- 每个卡片对象都有自己的
-
无限循环的"传送门" :
- 这是实现"无限"的关键。动画循环会持续监控每个卡片的位置。
- 当一个卡片的右边缘完全移出屏幕左侧(
card.currentX + card.width < 0
)时,系统会立即将它"传送"到整个卡片队列的最右端。 - 这个"传送"是通过给
currentX
加上一个预先计算好的totalFlowWidth
(所有行中最长一行的总宽度 + 一个屏幕宽度)来实现的。视觉上,这个过程是无缝的。
-
性能优化 (
transform: translateX
) :- 为了避免频繁修改
left
属性导致的性能问题(浏览器重排),我们将卡片的初始位置用left
和top
固定下来。 - 所有后续的流动动画都通过修改 CSS 的
transform: translateX()
属性来实现。这个属性可以被 GPU 加速,性能远高于修改left
。
- 为了避免频繁修改
二、 核心概念:状态管理与交互 (State Management & Interaction)
这部分逻辑负责响应用户的操作,并改变系统的状态,动画循环会根据这些状态来调整自己的行为。
-
定义清晰的状态变量:
isFlowing
: 一个布尔值,作为流动的"总开关"。默认true
。flowPausedByMouse
: 另一个布尔值,用于鼠标悬停时临时暂停,提升用户体验。card.isExpanded
: 每个卡片对象内部的状态,标记自己是否被放大。expandedCards
: 一个集合(Set),用于快速追踪当前有多少张卡片被放大了。
-
交互改变状态,而非直接操作动画:
-
展开卡片 (
expandCard
) :- 停止流动 :
isFlowing
设置为false
。 - 改变状态 :
card.isExpanded
设为true
。 - 视觉变化 : 通过 CSS class 和直接修改
width
,height
,left
,top
来实现放大效果,并清除流动的transform
。 - 触发推开 : 调用
updateCardPositions
来移动周围的卡片。
- 停止流动 :
-
收起卡片 (
collapseCard
) :- 改变状态 :
card.isExpanded
设为false
。 - 检查并恢复流动 : 检查
expandedCards
集合是否为空。如果为空,说明所有卡片都已关闭,此时将isFlowing
重新设为true
。 - 恢复视觉 : 将卡片尺寸和位置恢复,并重新应用基于其
currentX
的transform
,让它无缝地回到流动队列中。 - 触发推开逻辑更新 : 再次调用
updateCardPositions
,让被推开的卡片归位。
- 改变状态 :
-
-
"推开"效果的实现 (
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>