feat(player): 添加音乐播放器页面及核心功能实现
实现音乐播放器页面,包含以下核心功能: 1. 响应式唱片旋转动画和播放控制 2. 歌词同步显示和进度条拖动 3. 自动播放处理及移动端适配优化 4. 粒子效果和视觉优化 5. 元数据动态更新和错误处理
This commit is contained in:
495
player.html
Normal file
495
player.html
Normal file
@@ -0,0 +1,495 @@
|
|||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<!-- 优化 viewport,禁止缩放,适配刘海屏 -->
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
||||||
|
<title>道口艺金 - 音乐播放器</title>
|
||||||
|
|
||||||
|
<meta property="og:type" content="music.song" />
|
||||||
|
<meta property="og:url" content="" id="ogUrl" />
|
||||||
|
<meta property="og:title" content="" id="ogTitle" />
|
||||||
|
<meta property="og:description" content="" id="ogDesc" />
|
||||||
|
<meta property="og:image" content="" id="ogImage" />
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--gold: #d4af37;
|
||||||
|
--gold-light: #f3e5ab;
|
||||||
|
--bg-dark: #050505;
|
||||||
|
}
|
||||||
|
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, "PingFang SC", sans-serif;
|
||||||
|
background-color: var(--bg-dark);
|
||||||
|
color: #fff;
|
||||||
|
/* 核心修复:使用 dvh 适配移动端地址栏,fallback 到 vh */
|
||||||
|
height: 100vh;
|
||||||
|
height: 100dvh;
|
||||||
|
width: 100vw;
|
||||||
|
overflow: hidden; /* 保持单页应用风格 */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- 背景 --- */
|
||||||
|
.backdrop {
|
||||||
|
position: absolute; inset: 0; z-index: 0; pointer-events: none;
|
||||||
|
}
|
||||||
|
.backdrop-img {
|
||||||
|
width: 100%; height: 100%;
|
||||||
|
background-size: cover; background-position: center;
|
||||||
|
filter: blur(35px) brightness(0.4); /* 性能平衡点 */
|
||||||
|
transform: scale(1.1);
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
.particle-system {
|
||||||
|
position: absolute; inset: 0; z-index: 1;
|
||||||
|
}
|
||||||
|
.particle {
|
||||||
|
position: absolute;
|
||||||
|
background: radial-gradient(circle, var(--gold-light) 0%, transparent 70%);
|
||||||
|
border-radius: 50%; opacity: 0;
|
||||||
|
animation: float-up linear forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- 核心布局修复:舞台区 --- */
|
||||||
|
.stage {
|
||||||
|
/* 关键:min-height: 0 允许 flex 项目压缩到比内容更小,防止顶出底部按钮 */
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
padding: 20px 0; /* 给上下留点呼吸空间 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 唱片容器:极度响应式 */
|
||||||
|
.turntable-wrapper {
|
||||||
|
/* 逻辑:尽量大,但不超过宽度的80%,不超过高度的70%(留给歌词和按钮) */
|
||||||
|
width: min(320px, 80vw);
|
||||||
|
height: min(320px, 80vw);
|
||||||
|
/* 如果高度太小,强制缩小唱片 */
|
||||||
|
max-height: 50vh;
|
||||||
|
aspect-ratio: 1/1;
|
||||||
|
position: relative;
|
||||||
|
display: flex; justify-content: center; align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wave-ring {
|
||||||
|
position: absolute; width: 100%; height: 100%; border-radius: 50%;
|
||||||
|
border: 2px solid rgba(212, 175, 55, 0.3); opacity: 0; z-index: -1;
|
||||||
|
}
|
||||||
|
.is-playing .wave-ring { animation: ripple 2.5s infinite linear; }
|
||||||
|
.is-playing .wave-ring:nth-child(2) { animation-delay: 0.8s; }
|
||||||
|
.is-playing .wave-ring:nth-child(3) { animation-delay: 1.6s; }
|
||||||
|
|
||||||
|
.disc-group {
|
||||||
|
width: 96%; height: 96%; border-radius: 50%;
|
||||||
|
position: relative; z-index: 2;
|
||||||
|
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
|
||||||
|
}
|
||||||
|
.is-playing .disc-group { animation: shadow-breath 2s infinite alternate; }
|
||||||
|
|
||||||
|
.disc-rotate {
|
||||||
|
width: 100%; height: 100%; border-radius: 50%;
|
||||||
|
position: relative;
|
||||||
|
animation: rotate-vinyl 8s linear infinite;
|
||||||
|
animation-play-state: paused;
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
.vinyl-texture {
|
||||||
|
position: absolute; inset: 0; border-radius: 50%;
|
||||||
|
background: repeating-radial-gradient(#111 0, #111 2px, #222 3px, #151515 4px);
|
||||||
|
border: 2px solid rgba(255,255,255,0.05);
|
||||||
|
}
|
||||||
|
.album-art {
|
||||||
|
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
|
||||||
|
width: 65%; height: 65%; border-radius: 50%;
|
||||||
|
background-color: #333; background-size: cover; background-position: center;
|
||||||
|
border: 3px solid #0e0e0e;
|
||||||
|
}
|
||||||
|
.arm-wrapper {
|
||||||
|
position: absolute; top: -15%; right: -10%;
|
||||||
|
width: 40%; height: 60%; z-index: 20;
|
||||||
|
transform-origin: 55% 12%; transform: rotate(-35deg);
|
||||||
|
transition: transform 0.6s cubic-bezier(0.5, 0.1, 0.3, 1.2);
|
||||||
|
filter: drop-shadow(3px 5px 5px rgba(0,0,0,0.5)); pointer-events: none;
|
||||||
|
}
|
||||||
|
.is-playing .disc-rotate { animation-play-state: running; }
|
||||||
|
.is-playing .arm-wrapper { transform: rotate(20deg); }
|
||||||
|
|
||||||
|
/* --- 底部控制区 (Fixed priority) --- */
|
||||||
|
.control-deck {
|
||||||
|
position: relative; z-index: 20;
|
||||||
|
/* 增加底部 padding 以适配 iPhone Home 条 */
|
||||||
|
padding: 0 25px max(20px, env(safe-area-inset-bottom));
|
||||||
|
background: linear-gradient(to top, rgba(0,0,0,0.95) 30%, transparent);
|
||||||
|
text-align: center;
|
||||||
|
flex-shrink: 0; /* 禁止被压缩,确保按钮永远可见 */
|
||||||
|
display: flex; flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-title {
|
||||||
|
font-size: 20px; font-weight: 700; color: #fff; margin-bottom: 4px;
|
||||||
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.track-artist {
|
||||||
|
font-size: 13px; color: var(--gold); margin-bottom: 10px;
|
||||||
|
min-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 歌词区 */
|
||||||
|
.lrc-container {
|
||||||
|
height: 60px; /* 进一步减小高度 */
|
||||||
|
margin-bottom: 15px;
|
||||||
|
overflow: hidden; position: relative;
|
||||||
|
mask-image: linear-gradient(to bottom, transparent 0%, #000 25%, #000 75%, transparent 100%);
|
||||||
|
-webkit-mask-image: linear-gradient(to bottom, transparent 0%, #000 25%, #000 75%, transparent 100%);
|
||||||
|
}
|
||||||
|
.lrc-list {
|
||||||
|
transition: transform 0.3s ease-out;
|
||||||
|
transform: translateY(22px); will-change: transform;
|
||||||
|
}
|
||||||
|
.lrc-line {
|
||||||
|
height: 24px; line-height: 24px; font-size: 14px;
|
||||||
|
color: rgba(255,255,255,0.4); text-align: center;
|
||||||
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||||
|
padding: 0 5px;
|
||||||
|
}
|
||||||
|
.lrc-line.active { color: #fff; font-size: 15px; font-weight: bold; }
|
||||||
|
|
||||||
|
/* 进度条 */
|
||||||
|
.progress-row {
|
||||||
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
|
font-size: 11px; color: #999; margin-bottom: 20px; font-family: monospace;
|
||||||
|
}
|
||||||
|
.progress-hitbox {
|
||||||
|
flex: 1; height: 30px; margin: 0 10px;
|
||||||
|
display: flex; align-items: center; cursor: pointer;
|
||||||
|
-webkit-user-select: none; /* 防止长按选中 */
|
||||||
|
}
|
||||||
|
.bar-bg {
|
||||||
|
width: 100%; height: 3px; background: rgba(255,255,255,0.2);
|
||||||
|
border-radius: 4px; position: relative;
|
||||||
|
}
|
||||||
|
.bar-fg {
|
||||||
|
width: 0%; height: 100%; background: var(--gold);
|
||||||
|
border-radius: 4px; position: relative;
|
||||||
|
}
|
||||||
|
.bar-thumb {
|
||||||
|
position: absolute; right: -6px; top: 50%; margin-top: -6px;
|
||||||
|
width: 12px; height: 12px; background: #fff; border-radius: 50%;
|
||||||
|
box-shadow: 0 0 5px rgba(0,0,0,0.5); transform: scale(0); transition: transform 0.1s;
|
||||||
|
}
|
||||||
|
.progress-hitbox:active .bar-thumb, .is-playing .bar-thumb { transform: scale(1); }
|
||||||
|
|
||||||
|
/* 播放按钮 (小尺寸) */
|
||||||
|
.play-btn {
|
||||||
|
width: 60px; height: 60px; margin: 0 auto;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
border: 1px solid rgba(255,255,255,0.2);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 5px 20px rgba(0,0,0,0.3);
|
||||||
|
/* 解决点击响应慢:移除 scale 动画让反馈更直接,或加速 */
|
||||||
|
transition: transform 0.05s;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
.play-btn:active { transform: scale(0.92); background: rgba(255,255,255,0.15); }
|
||||||
|
.is-playing .play-btn { border-color: var(--gold); }
|
||||||
|
|
||||||
|
@keyframes rotate-vinyl { to { transform: rotate(360deg); } }
|
||||||
|
@keyframes ripple { 0% { transform: scale(1); opacity: 0.6; border-width: 4px; } 100% { transform: scale(1.6); opacity: 0; border-width: 0px; } }
|
||||||
|
@keyframes shadow-breath { from { box-shadow: 0 10px 30px rgba(212, 175, 55, 0.1); } to { box-shadow: 0 10px 40px rgba(212, 175, 55, 0.2); } }
|
||||||
|
@keyframes float-up { 0% { transform: translateY(100%) scale(0); opacity: 0; } 50% { opacity: 0.8; } 100% { transform: translateY(-120%) scale(1.5); opacity: 0; } }
|
||||||
|
|
||||||
|
#loading { position: fixed; inset: 0; background: #000; z-index: 999; display: flex; justify-content: center; align-items: center; color: var(--gold); }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div id="loading">LOADING...</div>
|
||||||
|
|
||||||
|
<div class="backdrop">
|
||||||
|
<div class="backdrop-img" id="bgImg"></div>
|
||||||
|
<div class="particle-system" id="particles"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stage" id="stage">
|
||||||
|
<div class="turntable-wrapper">
|
||||||
|
<div class="wave-ring"></div><div class="wave-ring"></div><div class="wave-ring"></div>
|
||||||
|
<div class="arm-wrapper">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 120 200" fill="none">
|
||||||
|
<circle cx="60" cy="20" r="18" fill="#d0d0d0" stroke="#666" stroke-width="2"/>
|
||||||
|
<circle cx="60" cy="20" r="5" fill="#222"/>
|
||||||
|
<path d="M 60 20 Q 55 60 85 110 T 95 170" stroke="#e0e0e0" stroke-width="7" stroke-linecap="round"/>
|
||||||
|
<g transform="translate(88, 162) rotate(25)">
|
||||||
|
<rect x="-6" y="0" width="16" height="26" rx="2" fill="#fff" stroke="#aaa"/>
|
||||||
|
<rect x="0" y="26" width="4" height="4" fill="#333"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="disc-group">
|
||||||
|
<div class="vinyl-sheen"></div>
|
||||||
|
<div class="disc-rotate">
|
||||||
|
<div class="vinyl-texture"></div>
|
||||||
|
<div class="album-art" id="coverImg"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-deck">
|
||||||
|
<div class="track-title" id="songTitle"></div>
|
||||||
|
<div class="track-artist" id="descInfo"></div>
|
||||||
|
|
||||||
|
<div class="lrc-container">
|
||||||
|
<div class="lrc-list" id="lrcList"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="progress-row">
|
||||||
|
<span id="currTime">00:00</span>
|
||||||
|
<div class="progress-hitbox" id="progBox">
|
||||||
|
<div class="bar-bg">
|
||||||
|
<div class="bar-fg" id="barFg">
|
||||||
|
<div class="bar-thumb"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span id="totalTime">00:00</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 播放按钮 -->
|
||||||
|
<div class="play-btn" id="playBtn">
|
||||||
|
<svg id="iconPlay" width="30" height="30" viewBox="0 0 24 24" fill="#fff"><path d="M8 5v14l11-7z"/></svg>
|
||||||
|
<svg id="iconPause" width="30" height="30" viewBox="0 0 24 24" fill="#d4af37" style="display:none;"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<audio id="audio" playsinline></audio>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const els = {
|
||||||
|
stage: document.getElementById('stage'),
|
||||||
|
bg: document.getElementById('bgImg'),
|
||||||
|
cover: document.getElementById('coverImg'),
|
||||||
|
title: document.getElementById('songTitle'),
|
||||||
|
desc: document.getElementById('descInfo'),
|
||||||
|
playBtn: document.getElementById('playBtn'),
|
||||||
|
iconPlay: document.getElementById('iconPlay'),
|
||||||
|
iconPause: document.getElementById('iconPause'),
|
||||||
|
progBox: document.getElementById('progBox'),
|
||||||
|
barFg: document.getElementById('barFg'),
|
||||||
|
currTime: document.getElementById('currTime'),
|
||||||
|
totalTime: document.getElementById('totalTime'),
|
||||||
|
audio: document.getElementById('audio'),
|
||||||
|
loader: document.getElementById('loading'),
|
||||||
|
particleContainer: document.getElementById('particles'),
|
||||||
|
lrcList: document.getElementById('lrcList'),
|
||||||
|
ogUrl: document.getElementById('ogUrl'),
|
||||||
|
ogTitle: document.getElementById('ogTitle'),
|
||||||
|
ogDesc: document.getElementById('ogDesc'),
|
||||||
|
ogImage: document.getElementById('ogImage')
|
||||||
|
};
|
||||||
|
|
||||||
|
let lyrics = [];
|
||||||
|
let lrcLineHeight = 24;
|
||||||
|
let particleInterval;
|
||||||
|
let isDragging = false;
|
||||||
|
let hasInteracted = false; // 标记用户是否交互过
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
const id = new URLSearchParams(window.location.search).get('id');
|
||||||
|
if (!id) return els.loader.innerText = "Missing ID";
|
||||||
|
try {
|
||||||
|
const res = await fetch(`https://pujianchaoyin.com/api/getMusicDetail?id=${id}`);
|
||||||
|
const json = await res.json();
|
||||||
|
if (json.code === 200 && json.data) {
|
||||||
|
render(json.data);
|
||||||
|
setTimeout(() => {
|
||||||
|
els.loader.style.opacity = '0';
|
||||||
|
setTimeout(() => els.loader.remove(), 500);
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
// 初始化尝试播放
|
||||||
|
attemptAutoPlay();
|
||||||
|
} else { els.loader.innerText = "Error Data"; }
|
||||||
|
} catch (e) { console.error(e); els.loader.innerText = "Network Error"; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function render(data) {
|
||||||
|
const imgUrl = `url('${data.img}')`;
|
||||||
|
els.bg.style.backgroundImage = imgUrl;
|
||||||
|
els.cover.style.backgroundImage = imgUrl;
|
||||||
|
els.title.innerText = data.title || "未知";
|
||||||
|
els.audio.src = data.playurl;
|
||||||
|
document.title = data.title;
|
||||||
|
|
||||||
|
if (data.desc && data.desc.trim()) {
|
||||||
|
els.desc.innerText = data.desc;
|
||||||
|
els.desc.style.display = 'block';
|
||||||
|
} else { els.desc.style.display = 'none'; }
|
||||||
|
|
||||||
|
if (data.lrc) parseLyrics(data.lrc);
|
||||||
|
else els.lrcList.innerHTML = '<div class="lrc-line active">纯音乐 / 暂无歌词</div>';
|
||||||
|
|
||||||
|
// Meta
|
||||||
|
els.ogUrl.content = window.location.href;
|
||||||
|
els.ogTitle.content = data.title;
|
||||||
|
els.ogImage.content = data.img;
|
||||||
|
els.ogDesc.content = data.desc || "精选音乐";
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseLyrics(lrcText) {
|
||||||
|
lyrics = [];
|
||||||
|
const lines = lrcText.split('\n');
|
||||||
|
const timeReg = /\[(\d{2}):(\d{2})(\.\d*)?\]/;
|
||||||
|
lines.forEach(line => {
|
||||||
|
const match = timeReg.exec(line);
|
||||||
|
if (match) {
|
||||||
|
const t = parseInt(match[1]) * 60 + parseInt(match[2]) + (match[3] ? parseFloat(match[3]) : 0);
|
||||||
|
lyrics.push({ time: t, text: line.replace(timeReg, '').trim() });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (lyrics.length) els.lrcList.innerHTML = lyrics.map(i => `<div class="lrc-line">${i.text}</div>`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 核心播放控制修复 ---
|
||||||
|
|
||||||
|
// 1. 尝试自动播放
|
||||||
|
function attemptAutoPlay() {
|
||||||
|
const promise = els.audio.play();
|
||||||
|
if (promise !== undefined) {
|
||||||
|
promise.catch(error => {
|
||||||
|
console.log("Autoplay blocked. Waiting for user interaction.");
|
||||||
|
// 添加一个全局的一次性点击事件来解锁音频
|
||||||
|
const unlockAudio = () => {
|
||||||
|
if (els.audio.paused) els.audio.play();
|
||||||
|
document.removeEventListener('click', unlockAudio);
|
||||||
|
document.removeEventListener('touchstart', unlockAudio);
|
||||||
|
};
|
||||||
|
document.addEventListener('click', unlockAudio);
|
||||||
|
document.addEventListener('touchstart', unlockAudio);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. UI状态完全绑定到音频事件,而不是点击事件
|
||||||
|
// 这样可以解决“点两次”的问题,确保UI永远真实反映播放状态
|
||||||
|
els.audio.addEventListener('play', () => {
|
||||||
|
els.stage.classList.add('is-playing');
|
||||||
|
els.iconPlay.style.display = 'none';
|
||||||
|
els.iconPause.style.display = 'block';
|
||||||
|
startParticles();
|
||||||
|
});
|
||||||
|
|
||||||
|
els.audio.addEventListener('pause', () => {
|
||||||
|
els.stage.classList.remove('is-playing');
|
||||||
|
els.iconPlay.style.display = 'block';
|
||||||
|
els.iconPause.style.display = 'none';
|
||||||
|
stopParticles();
|
||||||
|
});
|
||||||
|
|
||||||
|
els.audio.addEventListener('ended', () => {
|
||||||
|
els.barFg.style.width = '0%';
|
||||||
|
els.lrcList.style.transform = `translateY(22px)`;
|
||||||
|
syncLyricUI(-1);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. 播放按钮点击逻辑简化
|
||||||
|
els.playBtn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation(); // 防止冒泡触发上面的全局 unlock
|
||||||
|
if (els.audio.paused) {
|
||||||
|
els.audio.play();
|
||||||
|
} else {
|
||||||
|
els.audio.pause();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- 进度条与歌词 ---
|
||||||
|
els.audio.addEventListener('timeupdate', () => {
|
||||||
|
if (!isDragging) updateUI(els.audio.currentTime);
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateUI(ct) {
|
||||||
|
els.currTime.innerText = formatTime(ct);
|
||||||
|
if (els.audio.duration) els.barFg.style.width = `${(ct / els.audio.duration) * 100}%`;
|
||||||
|
|
||||||
|
if (lyrics.length) {
|
||||||
|
let idx = lyrics.findIndex(l => l.time > ct) - 1;
|
||||||
|
if (idx < 0) idx = (ct >= lyrics[0]?.time) ? 0 : -1;
|
||||||
|
syncLyricUI(idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncLyricUI(idx) {
|
||||||
|
const lines = els.lrcList.children;
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
if (i === idx) lines[i].classList.add('active');
|
||||||
|
else lines[i].classList.remove('active');
|
||||||
|
}
|
||||||
|
if(idx >= 0) els.lrcList.style.transform = `translateY(${-idx * lrcLineHeight + 22}px)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 拖动逻辑 ---
|
||||||
|
function handleSeek(e) {
|
||||||
|
if (!els.audio.duration) return;
|
||||||
|
const rect = els.progBox.getBoundingClientRect();
|
||||||
|
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
|
||||||
|
let pct = (clientX - rect.left) / rect.width;
|
||||||
|
pct = Math.max(0, Math.min(1, pct));
|
||||||
|
const newTime = pct * els.audio.duration;
|
||||||
|
els.audio.currentTime = newTime;
|
||||||
|
updateUI(newTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 仅在进度条上阻止默认行为,允许页面其他部分正常交互(如果需要)
|
||||||
|
els.progBox.addEventListener('touchstart', (e) => { isDragging = true; handleSeek(e); }, {passive: false});
|
||||||
|
els.progBox.addEventListener('touchmove', (e) => {
|
||||||
|
if (isDragging) { e.preventDefault(); handleSeek(e); }
|
||||||
|
}, {passive: false});
|
||||||
|
els.progBox.addEventListener('touchend', () => { isDragging = false; });
|
||||||
|
els.progBox.addEventListener('mousedown', (e) => {
|
||||||
|
isDragging = true; handleSeek(e);
|
||||||
|
const move = (ev) => { if(isDragging) handleSeek(ev); };
|
||||||
|
const up = () => { isDragging = false; document.removeEventListener('mousemove', move); document.removeEventListener('mouseup', up); };
|
||||||
|
document.addEventListener('mousemove', move); document.addEventListener('mouseup', up);
|
||||||
|
});
|
||||||
|
|
||||||
|
function formatTime(s) {
|
||||||
|
if(isNaN(s)) return '00:00';
|
||||||
|
const m = Math.floor(s/60).toString().padStart(2,'0');
|
||||||
|
const sec = Math.floor(s%60).toString().padStart(2,'0');
|
||||||
|
return `${m}:${sec}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function startParticles() {
|
||||||
|
if (particleInterval) return;
|
||||||
|
particleInterval = setInterval(() => {
|
||||||
|
const p = document.createElement('div');
|
||||||
|
p.classList.add('particle');
|
||||||
|
const size = Math.random() * 5 + 2;
|
||||||
|
p.style.width = p.style.height = `${size}px`;
|
||||||
|
p.style.left = `${Math.random() * 100}%`;
|
||||||
|
p.style.bottom = `-20px`;
|
||||||
|
p.style.animationDuration = `${Math.random() * 3 + 4}s`;
|
||||||
|
els.particleContainer.appendChild(p);
|
||||||
|
setTimeout(() => p.remove(), 7000);
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
function stopParticles() { clearInterval(particleInterval); particleInterval = null; }
|
||||||
|
|
||||||
|
init();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user