feat: 添加歌曲请求站页面及静态资源

style: 调整标签气泡动效样式和布局

refactor: 优化标签碰撞检测算法和位置计算

docs: 更新README文件说明

chore: 添加相关图片和CSS文件资源
This commit is contained in:
DESKTOP-RQ919RC\Pc
2025-09-18 19:03:24 +08:00
parent 35e3a11427
commit 8542840577
23 changed files with 1396 additions and 4 deletions

287
song-request-station.html Normal file
View File

@@ -0,0 +1,287 @@
<!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></style>
<link rel="stylesheet" href="./static/css/song-request-station.css" />
</head>
<body>
<!-- <div class="container" id="bubbleContainer"></div> -->
<div class="container">
<div class="container-box mar1200">
<img class="logo" src="./static/img/logo.png" alt="" />
<div class="header">
<img class="halo" src="./static/img/halo.png" />
<img class="record-black" src="./static/img/record-black.svg" />
<div class="record-circle"></div>
<img class="star-icon" src="./static/img/star-icon.png" alt="" />
<img class="bj-2" src="./static/img/song-request-bj-2.svg" alt="" />
<img class="love-little" src="./static/img/love-little.svg" alt="" />
<img class="music-icon" src="./static/img/music-icon.svg" alt="" />
<img class="bj" src="./static/img/song-request-bj.svg" alt="" />
<img class="love-big" src="./static/img/love-big.svg" alt="" />
<img class="music-score" src="./static/img/music-score.png" />
<img class="robot" src="./static/img/robot.png" />
<img class="text" src="./static/img/song-request-text.svg" />
<img class="face" src="./static/img/smiling-face.png" />
<img class="star-icon-2" src="./static/img/star-icon-2.png" />
<img class="ai-music" src="./static/img/ai-music.png" />
<img class="green-glow" src="./static/img/green-glow.png" />
<img class="shadow" src="./static/img/shadow.png" />
</div>
<div class="list-box">
<div class="list" id="bubbleContainer"></div>
</div>
</div>
</div>
<script>
// 标签数据
const tags = ["前端开发", "JavaScript", "CSS动画", "HTML5", "React", "Vue", "TypeScript", "Node.js", "UI设计", "用户体验", "响应式布局", "性能优化", "微信小程序", "PWA", "Canvas", "SVG", "WebGL", "数据可视化", "模块化", "组件化", "", "", "", "", ""];
// 获取容器
const container = document.getElementById("bubbleContainer");
// 创建空白标签数组,用于存储所有空白标签的信息
const emptyTags = [];
// 存储所有标签的位置和大小信息
const allTags = [];
const tagSizes = []; // 存储每个标签的实际尺寸
const defaultTagWidth = 120; // 默认标签宽度
const defaultTagHeight = 40; // 默认标签高度
// 计算容器尺寸
function getContainerDimensions() {
const containerWidth = container.offsetWidth;
const containerHeight = container.offsetHeight;
return { containerWidth, containerHeight };
}
// 获取标签的实际尺寸
function getTagDimensions(tagText) {
// 创建一个临时标签来测量实际尺寸
const tempTag = document.createElement("div");
tempTag.className = "bubble-tag";
tempTag.textContent = tagText;
tempTag.style.position = "absolute";
tempTag.style.visibility = "hidden";
tempTag.style.left = "-9999px";
tempTag.style.top = "-9999px";
document.body.appendChild(tempTag);
const width = tempTag.offsetWidth;
const height = tempTag.offsetHeight;
document.body.removeChild(tempTag);
// 确保至少有默认的尺寸
return {
width: Math.max(width, defaultTagWidth),
height: Math.max(height, defaultTagHeight),
};
}
// 检查两个标签是否碰撞
function isColliding(tag1, tag2, padding = 20) {
// 判断是否为空白标签
const isTag1Empty = tag1.isEmpty === true;
const isTag2Empty = tag2.isEmpty === true;
// 如果其中一个是空白标签,允许它们重叠
if (isTag1Empty || isTag2Empty) {
return false;
}
// 计算两个标签之间的距离
const dx = Math.abs(tag1.x + tag1.width / 2 - (tag2.x + tag2.width / 2));
const dy = Math.abs(tag1.y + tag1.height / 2 - (tag2.y + tag2.height / 2));
// 如果距离小于两个标签半径之和加上内边距,则发生碰撞
return dx < tag1.width / 2 + tag2.width / 2 + padding && dy < tag1.height / 2 + tag2.height / 2 + padding;
}
// 计算理想的网格大小以实现均匀分布
function calculateGridSize(totalTags, containerWidth, containerHeight, avgTagSize) {
// 估计每个标签需要的空间(包括间距)
const tagSpacing = avgTagSize + 60; // 标签直径 + 间距
// 计算大致的行数和列数
const idealColumns = Math.ceil(Math.sqrt((totalTags * containerWidth) / containerHeight));
const idealRows = Math.ceil(totalTags / idealColumns);
// 确保网格不会太拥挤
const actualColumns = Math.min(idealColumns, Math.floor(containerWidth / tagSpacing));
const actualRows = Math.min(idealRows, Math.floor(containerHeight / tagSpacing));
return { columns: Math.max(1, actualColumns), rows: Math.max(1, actualRows) };
}
// 为新标签找到一个不与其他标签碰撞的位置,优先使用均匀分布
function findNonCollidingPosition(tagWidth, tagHeight, containerWidth, containerHeight, totalTags, currentIndex) {
const maxAttempts = 200;
let attempts = 0;
// 计算网格大小
const avgTagSize = (tagWidth + tagHeight) / 2;
const { columns, rows } = calculateGridSize(totalTags, containerWidth, containerHeight, avgTagSize);
// 首先尝试网格均匀分布位置
while (attempts < maxAttempts * 0.7) {
attempts++;
// 计算理想的网格位置
const gridX = (currentIndex % columns) * (containerWidth / columns);
const gridY = Math.floor(currentIndex / columns) * (containerHeight / rows);
// 添加一些随机偏移,避免完全规则排列,但保持大致均匀
const randomOffset = 0.2; // 20%的随机偏移
const xOffset = (((Math.random() - 0.5) * containerWidth) / columns) * randomOffset;
const yOffset = (((Math.random() - 0.5) * containerHeight) / rows) * randomOffset;
// 计算最终位置,确保不会超出容器
const x = Math.max(0, Math.min(containerWidth - tagWidth, gridX + xOffset));
const y = Math.max(0, Math.min(containerHeight - tagHeight, gridY + yOffset));
const newTag = { x, y, width: tagWidth, height: tagHeight };
// 检查与所有已有标签是否碰撞
let colliding = false;
for (let i = 0; i < allTags.length; i++) {
if (isColliding(newTag, allTags[i])) {
colliding = true;
break;
}
}
if (!colliding) {
return { x, y };
}
}
// 如果网格分布尝试失败,回退到随机位置搜索
while (attempts < maxAttempts) {
attempts++;
// 随机生成一个位置
const x = Math.random() * (containerWidth - tagWidth);
const y = Math.random() * (containerHeight - tagHeight);
const newTag = { x, y, width: tagWidth, height: tagHeight };
// 检查与所有已有标签是否碰撞
let colliding = false;
for (let i = 0; i < allTags.length; i++) {
if (isColliding(newTag, allTags[i])) {
colliding = true;
break;
}
}
if (!colliding) {
return { x, y };
}
}
// 如果尝试次数用完仍然找不到完全不碰撞的位置,返回随机位置
return {
x: Math.random() * (containerWidth - tagWidth),
y: Math.random() * (containerHeight - tagHeight),
};
}
// 创建标签并添加动画
function createTags() {
const { containerWidth, containerHeight } = getContainerDimensions();
const totalTags = tags.length;
// 清空容器
container.innerHTML = "";
allTags.length = 0;
tagSizes.length = 0;
tags.forEach((tagText, index) => {
// 创建标签元素
const tag = document.createElement("div");
tag.className = "bubble-tag";
tag.textContent = tagText;
// 判断是否为空白标签
const isEmpty = tagText === "";
// 获取标签的实际尺寸
const { width, height } = getTagDimensions(tagText);
tagSizes.push({ width, height });
// 查找不碰撞的位置
const { x, y } = findNonCollidingPosition(width, height, containerWidth, containerHeight, totalTags, index);
tag.style.left = `${x}px`;
tag.style.top = `${y}px`;
// 保存标签信息
const tagInfo = { x, y, width, height, element: tag };
// 如果是空白标签,添加标记并设置不同的样式
if (isEmpty) {
tagInfo.isEmpty = true;
tag.style.backgroundColor = "transparent";
tag.style.zIndex = "0";
tag.style.border = "1px dashed rgba(150, 150, 255, 0.5)";
tag.style.pointerEvents = "none";
emptyTags.push(tagInfo);
} else {
tag.style.zIndex = "1";
}
allTags.push(tagInfo);
// 随机大小
const randomSize = 0.9 + Math.random() * 0.4; // 稍微增大了最小尺寸
tag.style.transform = `scale(${randomSize})`;
// 随机动画延迟
const delay = Math.random() * 5;
tag.style.animationDelay = `${delay}s`;
// 随机动画持续时间
const duration = 10 + Math.random() * 10; // 延长动画周期,使效果更流畅
tag.style.animation = `float ${duration}s infinite ease-in-out, pulse 3s infinite alternate`;
// 非空白标签设置背景色
if (!isEmpty) {
// 随机背景色 - 模仿截图中的颜色范围
const hue = 250 + Math.random() * 60; // 紫色到粉色的范围
const lightness = 85 + Math.random() * 10; // 亮度调整
tag.style.backgroundColor = `hsla(${hue}, 70%, ${lightness}%, 0.9)`;
}
// 添加到容器
container.appendChild(tag);
// 非空白标签添加点击效果
if (!isEmpty) {
tag.addEventListener("click", () => {
tag.style.animation = "none";
tag.style.transform = "scale(1.3)";
tag.style.zIndex = "10";
setTimeout(() => {
tag.style.animation = `float ${duration}s infinite ease-in-out, pulse 3s infinite alternate`;
tag.style.animationDelay = "0s";
tag.style.zIndex = "1";
}, 300);
});
}
});
}
// 窗口大小变化时重新创建标签
window.addEventListener("resize", () => {
createTags();
});
// 初始创建标签
createTags();
</script>
</body>
</html>