Files
PC-official/player.html
2025-12-09 15:29:38 +00:00

1272 lines
34 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>朴见潮音</title>
<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>
/* --- 1. 核心变量与重置 --- */
:root {
--theme-color: #d4af37;
--text-main: #ffffff;
--text-sub: rgba(255, 255, 255, 0.65);
--glass-bg: rgba(20, 20, 20, 0.6);
--glass-border: rgba(255, 255, 255, 0.08);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
outline: none;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", Roboto, sans-serif;
background: #1a82ea;
color: var(--text-main);
height: 100vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
/* --- 2. 氛围背景与粒子特效 --- */
.bg-layer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
overflow: hidden;
}
.bg-image {
width: 100%;
height: 100%;
background-size: cover;
background-position: center;
filter: blur(60px) brightness(0.6);
transform: scale(1.1);
transition: transform 10s ease;
opacity: 0.3;
}
/* 播放时的背景呼吸效果 */
body.is-playing .bg-image {
animation: breathe 20s infinite alternate ease-in-out;
}
@keyframes breathe {
from {
transform: scale(1.1);
}
to {
transform: scale(1.3);
}
}
.bg-mask {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(to bottom, rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.5) 50%, rgba(0, 0, 0, 0.9));
}
.particles span {
position: absolute;
bottom: -50px;
background: radial-gradient(circle, rgba(255, 255, 255, 0.8), transparent);
box-shadow: 0 0 15px 2px rgba(255, 255, 255, 0.1);
border-radius: 50%;
animation: floatUp 15s linear infinite;
opacity: 0;
}
@keyframes floatUp {
0% {
transform: translateY(0) scale(0.5);
opacity: 0;
}
20% {
opacity: 0.4;
}
80% {
opacity: 0.2;
}
100% {
transform: translateY(-100vh) scale(1.5);
opacity: 0;
}
}
/* --- 3. 主布局 --- */
.main-container {
order: 1;
position: relative;
z-index: 10;
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding-top: 10vh;
}
/* 顶部区域 (歌名) */
.header-info {
order: 2;
text-align: center;
z-index: 25;
opacity: 0.9;
margin-bottom: 8px;
}
.header-title {
font-size: 22px;
font-weight: 700;
margin-bottom: 6px;
letter-spacing: 0.5px;
text-shadow: 0 4px 10px rgba(0, 0, 0, 0.5);
}
.header-artist {
font-size: 13px;
color: var(--text-sub);
background: rgba(255, 255, 255, 0.1);
padding: 2px 8px;
border-radius: 10px;
display: inline-block;
backdrop-filter: blur(4px);
}
/* --- 4. 核心视图切换区 --- */
.view-wrapper {
order: 1;
position: relative;
width: 100%;
flex: 1;
display: flex;
justify-content: center;
transition: opacity 0.4s ease;
}
/* A. 唱片模式 */
.disc-mode {
position: absolute;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
transition: opacity 0.5s, transform 0.5s;
}
.disc-mode.hidden {
opacity: 0;
pointer-events: none;
transform: scale(0.95);
}
/* 唱臂组件 */
.needle {
position: absolute;
top: -50px;
left: 50%;
width: 80px;
height: 140px;
z-index: 20;
transform-origin: 40px 20px;
/* 修正旋转中心 */
transform: translateX(-10px) rotate(-35deg);
transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1);
pointer-events: none;
filter: drop-shadow(4px 8px 10px rgba(0, 0, 0, 0.5));
/* 增加立体投影 */
}
.needle.playing {
transform: translateX(-10px) rotate(-5deg);
/* 修正角度 */
}
.needle-pivot {
position: absolute;
top: 0;
left: 20px;
width: 40px;
height: 40px;
border-radius: 50%;
background: radial-gradient(circle at 30% 30%, #e0e0e0, #555);
border: 1px solid #444;
z-index: 3;
}
.needle-pivot::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 12px;
height: 12px;
background: #222;
border-radius: 50%;
}
.needle-rod {
position: absolute;
top: 30px;
left: 36px;
width: 8px;
height: 100px;
background: linear-gradient(90deg, #555, #ccc, #555);
z-index: 2;
}
.needle-head {
position: absolute;
bottom: 0;
left: 28px;
width: 24px;
height: 38px;
background: #1a1a1a;
border-radius: 4px;
z-index: 2;
border-top: 2px solid #555;
}
/* 唱片本体容器 */
.disc-container {
width: 55vw;
height: 55vw;
max-width: 300px;
max-height: 300px;
margin-top: 35px;
position: relative;
display: flex;
justify-content: center;
align-items: center;
}
/* --- 增强特效:动态光波 (Ripple) --- */
.ripple {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100%;
height: 100%;
border-radius: 50%;
border: 1px solid rgba(255, 255, 255, 0.1);
opacity: 0;
z-index: 0;
pointer-events: none;
}
/* 播放时触发 ripple 动画 */
.disc-mode.playing .ripple:nth-child(1) {
animation: rippleAnim 2.5s linear infinite;
animation-delay: 0s;
}
.disc-mode.playing .ripple:nth-child(2) {
animation: rippleAnim 2.5s linear infinite;
animation-delay: 0.8s;
}
.disc-mode.playing .ripple:nth-child(3) {
animation: rippleAnim 2.5s linear infinite;
animation-delay: 1.6s;
}
@keyframes rippleAnim {
0% {
width: 100%;
height: 100%;
opacity: 0.4;
border-width: 4px;
}
100% {
width: 160%;
height: 160%;
opacity: 0;
border-width: 0px;
}
}
/* 唱片主体 */
.disc {
width: 100%;
height: 100%;
border-radius: 50%;
background:
radial-gradient(circle, #111 0%, #1a1a1a 100%);
/* 纹理 */
background-image: repeating-radial-gradient(#111 0, #111 2px, #222 3px, #252525 4px);
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.8);
display: flex;
justify-content: center;
align-items: center;
animation: rotate 20s linear infinite;
animation-play-state: paused;
position: relative;
z-index: 2;
border: 6px solid #080808;
}
/* 唱片光泽反射层 (Gloss) */
.disc::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border-radius: 50%;
background: linear-gradient(45deg, transparent 40%, rgba(255, 255, 255, 0.08) 50%, transparent 60%);
z-index: 4;
pointer-events: none;
}
.disc.playing {
animation-play-state: running;
}
.album-cover {
width: 65%;
height: 65%;
border-radius: 50%;
overflow: hidden;
z-index: 3;
border: 4px solid #000;
position: relative;
}
.album-cover img {
height: 100%;
object-fit: cover;
transition: transform 0.3s;
}
/* 封面跳动效果 */
.disc.playing .album-cover img {
animation: beat 1.2s cubic-bezier(0.25, 0.46, 0.45, 0.94) infinite alternate;
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@keyframes beat {
from {
transform: scale(1);
filter: brightness(1);
}
to {
transform: scale(1.03);
filter: brightness(1.05);
}
}
.hint-text {
margin-top: 40px;
font-size: 12px;
color: var(--text-sub);
background: rgba(0, 0, 0, 0.3);
padding: 4px 12px;
border-radius: 20px;
letter-spacing: 1px;
}
/* B. 歌词模式 */
.lyrics-mode {
position: absolute;
top: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
opacity: 0;
pointer-events: none;
transform: translateY(30px);
transition: all 0.5s cubic-bezier(0.25, 0.8, 0.25, 1);
z-index: 50;
}
.lyrics-mode.active {
opacity: 1;
pointer-events: auto;
transform: translateY(0);
}
.lyrics-header {
margin-top: 20px;
margin-bottom: 20px;
font-size: 14px;
color: rgba(255, 255, 255, 0.4);
}
.lyrics-scroll {
width: 85%;
height: 70%;
overflow-y: auto;
scroll-behavior: smooth;
-webkit-mask-image: linear-gradient(to bottom, transparent 0%, black 15%, black 85%, transparent 100%);
mask-image: linear-gradient(to bottom, transparent 0%, black 15%, black 85%, transparent 100%);
}
.lyrics-scroll::-webkit-scrollbar {
display: none;
}
.lyric-line {
padding: 12px 0;
font-size: 16px;
color: rgba(255, 255, 255, 0.4);
transition: all 0.4s ease;
text-align: center;
line-height: 1.5;
}
.lyric-line.active {
color: var(--theme-color);
font-size: 22px;
font-weight: bold;
text-shadow: 0 0 15px rgba(212, 175, 55, 0.4);
transform: scale(1.05);
}
.mini-lyrics {
overflow: hidden;
-webkit-mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.2) 0%, black 40%, black 60%, rgba(0, 0, 0, 0.2) 100%);
mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.2) 0%, black 40%, black 60%, rgba(0, 0, 0, 0.2) 100%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
display: none;
}
.mini-line {
height: 22px;
line-height: 22px;
font-size: 14px;
color: rgba(255, 255, 255, 0.6);
text-align: center;
width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.mini-line.active {
color: var(--theme-color);
font-size: 18px;
font-weight: 700;
}
.mini-line.dim {
opacity: 0.5;
filter: blur(0.4px);
}
/* --- 5. 底部控制区 (Glassmorphism) --- */
.controls-area {
order: 3;
width: 100%;
padding: 20px 25px 40px;
background: var(--glass-bg);
backdrop-filter: blur(20px) saturate(180%);
-webkit-backdrop-filter: blur(20px) saturate(180%);
border-top: 1px solid var(--glass-border);
border-top-left-radius: 24px;
border-top-right-radius: 24px;
z-index: 100;
box-shadow: 0 -10px 40px rgba(0, 0, 0, 0.5);
}
/* 音乐频谱波形 (模拟) */
.music-waves {
display: flex;
justify-content: center;
align-items: flex-end;
height: 16px;
gap: 3px;
margin-bottom: 15px;
opacity: 0.6;
}
.wave-bar {
width: 3px;
background: #fff;
border-radius: 2px;
height: 4px;
transition: height 0.1s;
}
/* 播放时的波形动画 */
body.is-playing .wave-bar {
animation: waveBounce 0.6s infinite ease-in-out alternate;
}
body.is-playing .wave-bar:nth-child(1) {
animation-delay: 0.1s;
animation-duration: 0.5s;
}
body.is-playing .wave-bar:nth-child(2) {
animation-delay: 0.3s;
animation-duration: 0.7s;
}
body.is-playing .wave-bar:nth-child(3) {
animation-delay: 0.0s;
animation-duration: 0.6s;
}
body.is-playing .wave-bar:nth-child(4) {
animation-delay: 0.4s;
animation-duration: 0.5s;
}
body.is-playing .wave-bar:nth-child(5) {
animation-delay: 0.2s;
animation-duration: 0.8s;
}
@keyframes waveBounce {
0% {
height: 4px;
opacity: 0.3;
}
100% {
height: 16px;
opacity: 1;
background: var(--theme-color);
}
}
/* 进度条 */
.progress-container {
display: flex;
align-items: center;
margin-bottom: 25px;
font-size: 11px;
font-weight: 500;
color: rgba(255, 255, 255, 0.5);
font-variant-numeric: tabular-nums;
}
.progress-bar {
flex: 1;
height: 4px;
background: rgba(255, 255, 255, 0.15);
border-radius: 4px;
margin: 0 12px;
position: relative;
cursor: pointer;
overflow: visible;
/* 允许圆点超出 */
}
/* 增加触摸区域 */
.progress-bar::before {
content: '';
position: absolute;
top: -10px;
bottom: -10px;
left: 0;
right: 0;
z-index: 10;
}
.progress-fill {
width: 0%;
height: 100%;
background: linear-gradient(90deg, #fff, var(--theme-color));
border-radius: 4px;
position: relative;
}
.progress-dot {
width: 12px;
height: 12px;
background: #fff;
border-radius: 50%;
position: absolute;
right: -6px;
top: -4px;
box-shadow: 0 0 10px rgba(255, 255, 255, 0.8);
transform: scale(0);
transition: transform 0.2s;
}
.progress-bar:active .progress-dot,
.is-playing .progress-dot {
transform: scale(1);
}
/* 按钮组 */
.btn-group {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 10px;
}
.btn {
background: none;
border: none;
cursor: pointer;
opacity: 0.7;
transition: all 0.2s;
color: #fff;
display: flex;
justify-content: center;
align-items: center;
}
.btn:active {
transform: scale(0.9);
opacity: 1;
}
.btn-play {
width: 65px;
height: 65px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
opacity: 1;
}
.btn-play svg {
fill: #fff;
}
.btn-side {
width: 40px;
height: 40px;
}
/* Loading */
.loader {
position: fixed;
inset: 0;
background: #000;
z-index: 200;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
transition: opacity 0.6s;
}
.loader-text {
margin-top: 15px;
font-size: 12px;
letter-spacing: 3px;
color: rgba(255, 255, 255, 0.5);
animation: blink 1.5s infinite;
}
.spinner {
width: 40px;
height: 40px;
border: 3px solid rgba(255, 255, 255, 0.1);
border-top-color: var(--theme-color);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
@keyframes blink {
50% {
opacity: 0.3;
}
}
</style>
<script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
</head>
<body>
<div class="loader" id="loader">
<div class="spinner"></div>
<div class="loader-text">LOADING...</div>
</div>
<!-- 背景层 -->
<div class="bg-layer">
<div class="bg-image" id="bgImg"></div>
<div class="bg-mask"></div>
<div class="particles" id="particles"></div>
</div>
<div class="main-container">
<!-- 歌名信息 -->
<div class="header-info">
<div class="header-title" id="songTitle">...</div>
<div class="mini-lyrics" id="miniLyrics">
<div class="mini-line dim"></div>
<div class="mini-line active"></div>
<div class="mini-line dim"></div>
</div>
</div>
<!-- 中间视图切换 -->
<div class="view-wrapper" onclick="toggleView()">
<!-- 唱片模式 -->
<div class="disc-mode" id="discMode">
<div class="needle" id="needle">
<div class="needle-pivot"></div>
<div class="needle-rod"></div>
<div class="needle-head"></div>
</div>
<div class="disc-container">
<!-- 动效:光波 -->
<div class="ripple"></div>
<div class="ripple"></div>
<div class="ripple"></div>
<div class="disc" id="disc">
<div class="album-cover">
<img src="" id="coverImg" alt="cover">
</div>
</div>
</div>
</div>
<!-- 歌词模式 -->
<div class="lyrics-mode" id="lyricsMode">
<div class="lyrics-header">歌词 LYRICS</div>
<div class="lyrics-scroll" id="lyricsBox">
<div class="lyric-line">暂无歌词</div>
</div>
</div>
</div>
<!-- 底部控制 -->
<div class="controls-area">
<!-- 模拟音频频谱波形 -->
<div class="music-waves">
<div class="wave-bar"></div>
<div class="wave-bar"></div>
<div class="wave-bar"></div>
<div class="wave-bar"></div>
<div class="wave-bar"></div>
<div class="wave-bar"></div>
</div>
<div class="progress-container">
<span id="currTime">00:00</span>
<div class="progress-bar" id="progressBar">
<div class="progress-fill" id="progressFill">
<div class="progress-dot"></div>
</div>
</div>
<span id="totalTime">00:00</span>
</div>
<div class="btn-group">
<!-- 下载 -->
<button class="btn btn-side" onclick="downloadMusic()" title="下载">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7 10 12 15 17 10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>
</button>
<!-- 上一首 -->
<button class="btn btn-side" onclick="msg('这是单曲展示')">
<svg width="26" height="26" viewBox="0 0 24 24" fill="currentColor">
<path d="M11 18V6l-8.5 6 8.5 6zm.5-6l8.5 6V6l-8.5 6z" />
</svg>
</button>
<!-- 播放/暂停 -->
<button class="btn btn-play" id="playBtn">
<svg id="iconPlay" width="30" height="30" viewBox="0 0 24 24" fill="currentColor">
<path d="M8 5v14l11-7z" />
</svg>
<svg id="iconPause" width="30" height="30" viewBox="0 0 24 24" fill="currentColor" style="display:none">
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" />
</svg>
</button>
<!-- 下一首 -->
<button class="btn btn-side" onclick="msg('这是单曲展示')">
<svg width="26" height="26" viewBox="0 0 24 24" fill="currentColor">
<path d="M4 18l8.5-6L4 6v12zm9-12v12l8.5-6L13 6z" />
</svg>
</button>
<!-- 歌词切换按钮 -->
<button class="btn btn-side" onclick="toggleView(event)">
<svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor">
<path d="M15 15v2h-2v-2h2zm0-8v2h-2V7h2zm-4 4v2H9v-2h2zm0-4v2H9V7h2zm-4 4v2H5v-2h2zm0-4v2H5V7h2zm12 12H3V3h18v16z" />
</svg>
</button>
</div>
</div>
</div>
<audio id="audio" crossorigin="anonymous"></audio>
<script>
// 获取参数
const urlParams = new URLSearchParams(window.location.search);
const musicId = urlParams.get('id');
const lrcParam = urlParams.get('lrc');
const API_URL = `https://pujianchaoyin.com/api/getMusicDetail?id=${musicId}`;
let musicData = {};
let isPlaying = false;
let isLyricView = false;
let lyricLines = [];
const els = {
body: document.body,
loader: document.getElementById('loader'),
bgImg: document.getElementById('bgImg'),
coverImg: document.getElementById('coverImg'),
songTitle: document.getElementById('songTitle'),
audio: document.getElementById('audio'),
playBtn: document.getElementById('playBtn'),
iconPlay: document.getElementById('iconPlay'),
iconPause: document.getElementById('iconPause'),
disc: document.getElementById('disc'),
discMode: document.getElementById('discMode'),
needle: document.getElementById('needle'),
progressBar: document.getElementById('progressBar'),
progressFill: document.getElementById('progressFill'),
currTime: document.getElementById('currTime'),
totalTime: document.getElementById('totalTime'),
lyricsMode: document.getElementById('lyricsMode'),
lyricsBox: document.getElementById('lyricsBox'),
miniLyrics: document.getElementById('miniLyrics')
};
// 初始化
async function init() {
createParticles();
try {
const res = await fetch(API_URL);
const json = await res.json();
if (json.code === 200) {
musicData = json.data;
renderUI(musicData);
const lrcText = musicData.lrc || '';
parseLyrics(lrcText);
if (lrcParam && lrcText.trim()) {
isLyricView = true;
els.discMode.classList.add('hidden');
els.lyricsMode.classList.add('active');
// if (els.miniLyrics) els.miniLyrics.style.display = 'none';
syncLyrics(els.audio.currentTime || 0);
}
} else {
els.songTitle.innerText = "获取失败";
closeLoader();
}
} catch (e) {
console.error(e);
els.songTitle.innerText = "网络错误";
closeLoader();
}
}
function closeLoader() {
els.loader.style.opacity = 0;
setTimeout(() => els.loader.remove(), 600);
}
function renderUI(data) {
els.songTitle.innerText = data.title;
els.coverImg.src = data.img;
els.bgImg.style.backgroundImage = `url('${data.img}')`;
els.audio.src = data.playurl;
// 图片加载后显示
els.coverImg.onload = () => {
closeLoader();
// 尝试自动播放
els.audio.play().catch(err => {
console.warn('自动播放被浏览器阻止', err);
});
};
// 防止图片加载失败导致一直loading
setTimeout(closeLoader, 3000);
document.title = data.title;
document.getElementById('ogUrl').content = window.location.href;
document.getElementById('ogTitle').content = data.title;
document.getElementById('ogDesc').content = data.desc;
document.getElementById('ogImage').content = data.img;
// 分享到微信
initWeixinShare(data);
}
// 播放控制
els.playBtn.addEventListener('click', () => {
if (isPlaying) els.audio.pause();
else els.audio.play();
});
els.audio.addEventListener('play', () => {
isPlaying = true;
updatePlayState();
});
els.audio.addEventListener('pause', () => {
isPlaying = false;
updatePlayState();
});
function updatePlayState() {
if (isPlaying) {
els.body.classList.add('is-playing');
els.needle.classList.add('playing');
els.disc.classList.add('playing');
els.discMode.classList.add('playing'); // 触发 Ripple
els.iconPlay.style.display = 'none';
els.iconPause.style.display = 'block';
} else {
els.body.classList.remove('is-playing');
els.needle.classList.remove('playing');
els.disc.classList.remove('playing');
els.discMode.classList.remove('playing');
els.iconPlay.style.display = 'block';
els.iconPause.style.display = 'none';
}
}
// 进度与歌词
els.audio.addEventListener('timeupdate', () => {
const { currentTime, duration } = els.audio;
if (!duration) return;
const percent = (currentTime / duration) * 100;
els.progressFill.style.width = `${percent}%`;
els.currTime.innerText = formatTime(currentTime);
syncLyrics(currentTime);
});
els.audio.addEventListener('loadedmetadata', () => {
els.totalTime.innerText = formatTime(els.audio.duration);
});
els.progressBar.addEventListener('click', (e) => {
const rect = els.progressBar.getBoundingClientRect();
let p = (e.clientX - rect.left) / rect.width;
p = Math.max(0, Math.min(1, p));
const newTime = p * (els.audio.duration || 0);
els.audio.currentTime = newTime;
syncLyrics(newTime);
if (!isPlaying) els.audio.play();
});
let seeking = false;
function seekByClientX(x) {
const rect = els.progressBar.getBoundingClientRect();
let p = (x - rect.left) / rect.width;
p = Math.max(0, Math.min(1, p));
els.progressFill.style.width = `${p * 100}%`;
const dur = els.audio.duration || 0;
const newTime = p * dur;
if (dur) {
els.audio.currentTime = newTime;
}
syncLyrics(newTime);
}
els.progressBar.addEventListener('mousedown', (e) => {
seeking = true;
seekByClientX(e.clientX);
});
document.addEventListener('mousemove', (e) => {
if (!seeking) return;
seekByClientX(e.clientX);
});
document.addEventListener('mouseup', () => {
if (!seeking) return;
seeking = false;
if (!isPlaying) els.audio.play();
});
els.progressBar.addEventListener('touchstart', (e) => {
if (!e.touches || !e.touches.length) return;
seeking = true;
seekByClientX(e.touches[0].clientX);
e.preventDefault();
}, { passive: false });
document.addEventListener('touchmove', (e) => {
if (!seeking || !e.touches || !e.touches.length) return;
seekByClientX(e.touches[0].clientX);
e.preventDefault();
}, { passive: false });
document.addEventListener('touchend', () => {
if (!seeking) return;
seeking = false;
if (!isPlaying) els.audio.play();
});
// 歌词解析
function parseLyrics(lrc) {
lyricLines = [];
if (!lrc) {
if (els.miniLyrics) els.miniLyrics.style.display = 'none';
els.lyricsBox.innerHTML = '<div class="lyric-line">纯音乐 / 暂无歌词</div>';
return;
}
lrc = lrc.replace(/\r\n/g, '\n').replace(/\\n/g, '\n').replace(/<br\s*\/>?/gi, '\n');
const lines = lrc.split('\n');
const timeReg = /\[(\d{2}):(\d{2})(?:\.(\d{1,3}))?\]/;
els.lyricsBox.innerHTML = '';
lines.forEach(line => {
const match = timeReg.exec(line);
if (match) {
const min = parseInt(match[1]);
const sec = parseInt(match[2]);
const ms = match[3] ? parseFloat("0." + String(match[3]).padEnd(3, '0')) : 0;
const time = min * 60 + sec + ms;
const text = line.replace(/\[.*?\]/g, '').trim();
if (text) {
const div = document.createElement('div');
div.className = 'lyric-line';
div.innerText = text;
div.dataset.time = time;
els.lyricsBox.appendChild(div);
lyricLines.push({ time, el: div });
}
}
});
lyricLines.sort((a, b) => a.time - b.time);
// 移除额外留白,保持精准滚动定位
if (isLyricView && lyricLines.length > 0) {
syncLyrics(els.audio.currentTime || 0);
}
}
// 歌词同步
function syncLyrics(time) {
if (lyricLines.length === 0) return;
let nextIdx = lyricLines.findIndex(l => time < l.time);
let activeIdx = nextIdx === -1 ? lyricLines.length - 1 : Math.max(0, nextIdx - 1);
document.querySelectorAll('.lyric-line.active').forEach(el => el.classList.remove('active'));
const activeEl = lyricLines[activeIdx].el;
activeEl.classList.add('active');
const box = els.lyricsBox;
const offset = activeEl.offsetTop - box.clientHeight / 2 + activeEl.clientHeight / 2;
box.scrollTo({ top: Math.max(0, offset), behavior: seeking ? 'auto' : 'smooth' });
updateMiniLyrics(activeIdx);
}
function updateMiniLyrics(idx) {
if (!els.miniLyrics) return;
if (els.miniLyrics) els.miniLyrics.style.display = isLyricView ? 'none' : 'block';
const lines = els.miniLyrics.querySelectorAll('.mini-line');
const getText = (i) => (lyricLines[i] ? lyricLines[i].el.innerText : '');
if (lines.length >= 3) {
lines[0].className = 'mini-line dim';
lines[1].className = 'mini-line active';
lines[2].className = 'mini-line dim';
lines[0].innerText = getText(Math.max(0, idx - 1));
lines[1].innerText = getText(idx);
lines[2].innerText = getText(Math.min(lyricLines.length - 1, idx + 1));
}
}
// 视图切换
window.toggleView = function (e) {
if (e) e.stopPropagation();
isLyricView = !isLyricView;
if (isLyricView) {
els.discMode.classList.add('hidden');
els.lyricsMode.classList.add('active');
if (els.miniLyrics) els.miniLyrics.style.display = 'none';
syncLyrics(els.audio.currentTime);
} else {
els.discMode.classList.remove('hidden');
els.lyricsMode.classList.remove('active');
if (els.miniLyrics) els.miniLyrics.style.display = 'block';
}
}
window.downloadMusic = async function () {
if (!musicData.playurl) return;
msg('正在准备下载,请稍候...');
try {
const response = await fetch(musicData.playurl);
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `${musicData.title || 'audio'}.mp3`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
msg('下载已开始');
} catch (error) {
console.error('下载失败:', error);
msg('下载失败,请检查网络或稍后重试');
}
}
// --- 微信分享配置 ---
async function initWeixinShare(shareData) {
// 判断是否在微信环境中
if (!/micromessenger/i.test(navigator.userAgent)) {
return;
}
try {
// 1. 从您的后端获取签名
const signatureData = await getWeixinSignature();
// 2. 配置JS-SDK
wx.config({
debug: false, // 调试时可开启
appId: signatureData.appId,
timestamp: signatureData.timestamp,
nonceStr: signatureData.nonceStr,
signature: signatureData.signature,
jsApiList: ['updateAppMessageShareData', 'updateTimelineShareData']
});
// 3. 配置分享内容
wx.ready(() => {
const sharePayload = {
title: shareData.title,
desc: '立即收听:' + shareData.title,
link: window.location.href,
imgUrl: shareData.img,
success: () => msg('分享成功'),
cancel: () => msg('取消分享')
};
wx.updateAppMessageShareData(sharePayload); // 分享给朋友
wx.updateTimelineShareData(sharePayload); // 分享到朋友圈
});
wx.error(err => {
console.error('微信JS-SDK配置失败:', err);
// msg('微信分享配置失败');
});
} catch (error) {
console.error('初始化微信分享失败:', error);
// msg('初始化微信分享失败');
}
}
/**
* @description 从后端获取微信JS-SDK签名
* @returns {Promise<object>} 包含 appId, timestamp, nonceStr, signature 的对象
*
* 后端实现参考 (Node.js):
* 1. 安装 `axios` 和 `sha1` 库: `npm install axios sha1`
* 2. 后端代码需要缓存 access_token 和 jsapi_ticket避免频繁请求微信接口
* 3. 签名算法请严格参考微信官方文档
*/
async function getWeixinSignature() {
// 在这里,您需要向您的后端服务发起一个请求
// 后端需要根据当前的 URL 生成一个有效的签名
// 例如: const res = await fetch('https://your-backend.com/api/weixin-signature?url=' + encodeURIComponent(location.href.split('#')[0]));
// return await res.json();
// --- 以下为模拟数据,请务必替换为真实后端请求 ---
console.warn('正在使用模拟的微信签名数据,请替换为您的后端实现');
return new Promise(resolve => {
resolve({
appId: "YOUR_APP_ID", // 替换为您的公众号AppID
timestamp: "1678886400", // 替换为后端生成的时间戳
nonceStr: "YOUR_NONCE_STR", // 替换为后端生成的随机字符串
signature: "YOUR_SIGNATURE", // 替换为后端生成的签名
});
});
}
function createParticles() {
const container = document.getElementById('particles');
for (let i = 0; i < 20; i++) {
const span = document.createElement('span');
const size = Math.random() * 4 + 2;
span.style.width = `${size}px`;
span.style.height = `${size}px`;
span.style.left = `${Math.random() * 100}%`;
span.style.animationDelay = `${Math.random() * 5}s`;
span.style.animationDuration = `${Math.random() * 10 + 10}s`;
container.appendChild(span);
}
}
function formatTime(s) {
const m = Math.floor(s / 60);
const sec = Math.floor(s % 60);
return `${m.toString().padStart(2, '0')}:${sec.toString().padStart(2, '0')}`;
}
function msg(txt) {
// 简单提示
const div = document.createElement('div');
div.innerText = txt;
div.style.cssText = 'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:rgba(0,0,0,0.7);color:#fff;padding:10px 20px;border-radius:20px;z-index:999;font-size:14px;';
document.body.appendChild(div);
setTimeout(() => div.remove(), 2000);
}
init();
</script>
</body>
</html>