Files
MeowBox-Core/theme/full-app.html
T
2025-12-02 17:48:54 +08:00

2183 lines
81 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Meow Music - 音乐播放器</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
/* 默认粉色主题 */
--bg-gradient: linear-gradient(-45deg, #ff9a9e, #fad0c4, #ffd1ff, #a18cd1);
--primary-btn-bg: linear-gradient(45deg, #FFB7C5, #FF69B4);
--primary-btn-hover: linear-gradient(45deg, #FF69B4, #FFB7C5);
--text-gradient: linear-gradient(45deg, #FF8FAB, #FF69B4);
--glass-bg: rgba(255, 255, 255, 0.75);
--glass-border: 2px solid white;
--input-border: #ffe8f5;
--input-focus: #ff8cc6;
--shadow-color: rgba(255, 154, 158, 0.3);
--tab-active-bg: white;
--tab-active-color: #FF69B4;
--logo-emoji: '🐱';
--sparkle-content: '✨';
}
[data-theme="blue"] {
/* 极客蓝主题 */
--bg-gradient: linear-gradient(-45deg, #4facfe, #00f2fe, #a18cd1, #e0c3fc);
--primary-btn-bg: linear-gradient(45deg, #4facfe, #00f2fe);
--primary-btn-hover: linear-gradient(45deg, #00f2fe, #4facfe);
--text-gradient: linear-gradient(45deg, #4facfe, #00f2fe);
--glass-bg: rgba(255, 255, 255, 0.85);
--glass-border: 1px solid rgba(255, 255, 255, 0.8);
--input-border: #e0f7fa;
--input-focus: #4facfe;
--shadow-color: rgba(66, 153, 225, 0.3);
--tab-active-bg: white;
--tab-active-color: #4facfe;
--logo-emoji: '🎵';
--sparkle-content: '';
}
body {
font-family: 'Nunito', 'Segoe UI', 'Microsoft YaHei', sans-serif;
background: var(--bg-gradient);
background-size: 400% 400%;
animation: gradientBG 15s ease infinite;
min-height: 100vh;
color: #555;
overflow-x: hidden;
/* 默认光标 */
cursor: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="%23FF69B4" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19l7-7 3 3-7 7-3-3z"></path><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"></path><path d="M2 2l7.586 7.586"></path><circle cx="11" cy="11" r="2"></circle></svg>'), auto;
transition: background 0.5s ease;
}
/* 点击爱心特效 - 强制增强 */
.click-heart {
position: fixed;
pointer-events: none;
font-size: 24px; /*稍微大一点*/
animation: heartFloat 0.8s ease-out forwards;
z-index: 2147483647 !important; /* 最大z-index */
user-select: none;
}
@keyframes heartFloat {
0% { transform: translateY(0) scale(1); opacity: 1; }
100% { transform: translateY(-60px) scale(1.5) rotate(15deg); opacity: 0; }
}
@keyframes gradientBG {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
@keyframes heartFloat {
0% { transform: translateY(0) scale(1); opacity: 1; }
100% { transform: translateY(-50px) scale(1.5); opacity: 0; }
}
/* 主题切换按钮 */
.theme-switch {
position: fixed;
top: 20px;
right: 20px;
z-index: 1000;
background: rgba(255, 255, 255, 0.5);
backdrop-filter: blur(10px);
padding: 10px;
border-radius: 50%;
border: 1px solid white;
cursor: pointer;
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
font-size: 24px;
transition: all 0.3s;
display: none;
}
.theme-switch:hover { transform: rotate(30deg) scale(1.1); }
.container {
max-width: 1200px;
margin: 0 auto;
padding: 40px 20px;
padding-bottom: 120px;
}
.auth-page {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
}
.auth-box {
padding: 50px;
border-radius: 40px;
width: 420px;
position: relative;
overflow: hidden;
background: var(--glass-bg);
backdrop-filter: blur(30px);
border: var(--glass-border);
box-shadow: 0 20px 50px var(--shadow-color);
transform-style: preserve-3d;
transition: transform 0.3s;
}
.auth-box:hover { transform: translateY(-5px) rotateX(2deg); }
.auth-box h1 {
text-align: center;
background: var(--text-gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 30px;
font-size: 32px;
font-weight: 900;
filter: drop-shadow(0 2px 4px rgba(255,182,193,0.5));
}
.tabs {
display: flex;
gap: 15px;
margin-bottom: 25px;
background: rgba(255, 255, 255, 0.5);
padding: 8px;
border-radius: 25px;
}
.tab {
flex: 1;
padding: 12px;
background: transparent;
border: none;
border-radius: 20px;
cursor: pointer;
font-size: 16px;
font-weight: 700;
color: #999;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.tab.active {
background: var(--tab-active-bg);
color: var(--tab-active-color);
box-shadow: 0 5px 15px var(--shadow-color);
transform: scale(1.05);
}
.form { display: none; }
.form.active { display: block; animation: slideIn 0.4s ease; }
@keyframes slideIn {
from { opacity: 0; transform: translateX(30px) scale(0.9); }
to { opacity: 1; transform: translateX(0) scale(1); }
}
input {
width: 100%;
padding: 18px 25px;
margin: 12px 0;
border: 2px solid transparent;
border-radius: 25px;
font-size: 15px;
background: rgba(255, 255, 255, 0.8);
box-shadow: inset 0 2px 5px rgba(0,0,0,0.02);
transition: all 0.3s;
color: #555;
}
input:focus {
outline: none;
border-color: var(--input-focus);
background: white;
box-shadow: 0 0 0 5px var(--shadow-color);
transform: translateY(-2px);
}
button.primary {
width: 100%;
padding: 18px;
background: var(--primary-btn-bg);
color: white;
border: none;
border-radius: 25px;
cursor: pointer;
font-size: 18px;
font-weight: bold;
margin-top: 20px;
transition: all 0.3s;
box-shadow: 0 10px 25px var(--shadow-color);
position: relative;
overflow: hidden;
}
button.primary::before {
content: var(--sparkle-content);
position: absolute;
left: 20px;
animation: spin 2s infinite linear;
}
button.primary::after {
content: var(--sparkle-content);
position: absolute;
right: 20px;
animation: spin 2s infinite linear reverse;
}
button.primary:hover {
transform: translateY(-3px) scale(1.02);
box-shadow: 0 15px 35px var(--shadow-color);
background: var(--primary-btn-hover);
}
.message { padding: 15px; border-radius: 20px; margin: 15px 0; text-align: center; font-weight: 600; font-size: 14px; }
.error { background: #FFF0F0; color: #FF6B6B; border: 2px solid #FFC9C9; }
.success { background: #F0FFF4; color: #48BB78; border: 2px solid #9AE6B4; }
.info { background: #EBF8FF; color: #4299E1; border: 2px solid #90CDF4; }
/* 主应用界面 */
.app-page { display: none; }
.header {
padding: 20px 30px;
border-radius: 35px;
margin-bottom: 35px;
display: flex;
justify-content: space-between;
align-items: center;
background: rgba(255, 255, 255, 0.6);
backdrop-filter: blur(20px);
border: var(--glass-border);
box-shadow: 0 10px 30px var(--shadow-color);
}
.logo {
font-size: 26px;
font-weight: 900;
background: var(--text-gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
display: flex;
align-items: center;
gap: 12px;
}
.logo::before {
content: var(--logo-emoji);
font-size: 32px;
-webkit-text-fill-color: initial;
animation: bounce 2s infinite;
filter: drop-shadow(0 5px 5px rgba(0,0,0,0.1));
}
.user-info { display: flex; align-items: center; gap: 20px; }
.btn {
padding: 10px 25px;
border: none;
border-radius: 25px;
cursor: pointer;
font-size: 14px;
font-weight: 700;
transition: all 0.3s;
}
.btn-logout {
background: white;
color: #FF69B4;
border: 2px solid #FFE4E1;
box-shadow: 0 5px 15px var(--shadow-color);
}
[data-theme="blue"] .btn-logout { color: #4facfe; border-color: #e0f7fa; }
.btn-logout:hover {
transform: rotate(5deg) scale(1.05);
}
.main-content {
background: rgba(255, 255, 255, 0.5);
backdrop-filter: blur(25px);
border-radius: 45px;
padding: 40px;
box-shadow: 0 20px 60px var(--shadow-color);
border: 2px solid rgba(255, 255, 255, 0.6);
min-height: 600px;
position: relative;
}
.search-box {
margin-bottom: 40px;
position: relative;
z-index: 2;
display: flex;
gap: 15px;
align-items: center;
}
.search-inputs {
flex: 1;
display: flex;
gap: 12px;
}
.search-input {
flex: 1;
padding: 18px 25px;
border: 2px solid var(--input-border);
border-radius: 30px;
font-size: 18px;
transition: all 0.3s ease;
background: #fff;
box-shadow: 0 4px 15px var(--shadow-color);
}
.search-input:focus {
outline: none;
border-color: var(--input-focus);
box-shadow: 0 5px 20px var(--shadow-color);
transform: translateY(-2px);
}
.search-input::placeholder {
color: #999;
}
.search-btn {
padding: 0 35px;
border-radius: 35px;
border: none;
background: var(--primary-btn-bg);
color: white;
font-weight: 800;
font-size: 16px;
cursor: pointer;
box-shadow: 0 10px 20px var(--shadow-color);
transition: all 0.3s;
white-space: nowrap;
display: flex;
align-items: center;
gap: 5px;
}
.search-btn:hover {
transform: translateY(-2px) scale(1.05);
box-shadow: 0 15px 30px var(--shadow-color);
background: var(--primary-btn-hover);
}
.search-btn:active { transform: scale(0.95); }
.nav-tabs {
display: flex;
gap: 20px;
margin-bottom: 40px;
justify-content: center;
background: white;
padding: 10px;
border-radius: 35px;
width: fit-content;
margin-left: auto;
margin-right: auto;
box-shadow: 0 10px 30px var(--shadow-color);
}
.nav-tab {
padding: 15px 35px;
background: transparent;
border: none;
cursor: pointer;
font-size: 16px;
font-weight: 800;
color: #999;
border-radius: 30px;
transition: all 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55);
}
.nav-tab.active {
background: var(--primary-btn-bg);
color: white;
box-shadow: 0 8px 20px var(--shadow-color);
transform: translateY(-3px);
}
.nav-tab:hover:not(.active) { color: #FF69B4; background: #FFF0F5; }
[data-theme="blue"] .nav-tab:hover:not(.active) { color: #4facfe; background: #e0f7fa; }
.tab-content { display: none; }
.tab-content.active { display: block; animation: popUp 0.6s cubic-bezier(0.34, 1.56, 0.64, 1); }
@keyframes popUp {
from { opacity: 0; transform: scale(0.9) translateY(20px); }
to { opacity: 1; transform: scale(1) translateY(0); }
}
.song-list { display: grid; gap: 20px; }
.song-item {
background: rgba(255, 255, 255, 0.9);
padding: 20px 30px;
border-radius: 30px;
display: flex;
justify-content: space-between;
align-items: center;
transition: all 0.3s;
border: 2px solid transparent;
box-shadow: 0 5px 15px rgba(0,0,0,0.03);
}
.song-item:hover {
transform: translateY(-5px) scale(1.01);
box-shadow: 0 15px 35px var(--shadow-color);
border-color: var(--input-focus);
background: white;
}
.song-title {
font-weight: 800;
font-size: 17px;
color: #444;
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 12px;
}
.song-title::before {
content: '🎵';
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
background: #FFF0F5;
border-radius: 50%;
font-size: 16px;
color: #FF69B4;
}
[data-theme="blue"] .song-title::before { background: #e0f7fa; color: #4facfe; }
.song-artist {
color: #999;
font-size: 14px;
padding-left: 48px;
font-weight: 600;
}
.btn-icon {
padding: 12px 24px;
border: none;
border-radius: 25px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
font-weight: 700;
display: flex;
align-items: center;
gap: 5px;
}
.btn-play {
background: #FFF0F5;
color: #FF69B4;
}
[data-theme="blue"] .btn-play { background: #e0f7fa; color: #4facfe; }
.btn-play:hover {
background: var(--primary-btn-bg);
color: white;
transform: scale(1.1);
box-shadow: 0 8px 20px var(--shadow-color);
}
.btn-add {
background: #F0FFF4;
color: #48BB78;
}
.btn-add:hover {
background: #48BB78;
color: white;
transform: scale(1.1) rotate(5deg);
}
.btn-remove {
background: #FFF5F5;
color: #F56565;
}
.btn-remove:hover {
background: #F56565;
color: white;
transform: scale(1.1) rotate(-5deg);
}
/* 悬浮胶囊播放器 Pro Max */
.player {
position: fixed;
bottom: 30px;
left: 50%;
transform: translateX(-50%);
width: 92%;
max-width: 750px;
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(40px);
-webkit-backdrop-filter: blur(40px);
padding: 15px 35px;
box-shadow: 0 25px 70px var(--shadow-color);
display: none;
border-radius: 60px;
border: 2px solid rgba(255, 255, 255, 0.9);
z-index: 1000;
transition: all 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.player:hover {
transform: translateX(-50%) translateY(-5px);
box-shadow: 0 35px 80px var(--shadow-color);
background: rgba(255, 255, 255, 0.95);
}
.player.active { display: flex; align-items: center; gap: 25px; animation: floatIn 0.8s cubic-bezier(0.34, 1.56, 0.64, 1); }
@keyframes floatIn {
from { transform: translate(-50%, 150px); opacity: 0; }
to { transform: translate(-50%, 0); opacity: 1; }
}
.player-info {
display: flex;
flex-direction: column;
min-width: 160px;
border: none;
margin: 0;
padding: 0;
}
.player-title { font-size: 16px; color: #333; margin-bottom: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 160px; }
.player-artist { font-size: 13px; color: var(--primary-dark); }
.player-controls { margin: 0; flex: 1; }
.player audio {
height: 45px;
border-radius: 25px;
width: 100%;
filter: drop-shadow(0 5px 10px rgba(255, 182, 193, 0.2));
}
/* 音乐律动波纹 */
.music-waves {
display: flex;
align-items: flex-end;
height: 30px;
gap: 3px;
margin-right: 10px;
}
.wave-bar {
width: 4px;
background: linear-gradient(to top, #FFB7C5, #FF69B4);
border-radius: 2px;
animation: wave 1s ease-in-out infinite;
}
.wave-bar:nth-child(1) { height: 10px; animation-delay: 0.1s; }
.wave-bar:nth-child(2) { height: 20px; animation-delay: 0.2s; }
.wave-bar:nth-child(3) { height: 15px; animation-delay: 0.3s; }
.wave-bar:nth-child(4) { height: 25px; animation-delay: 0.4s; }
@keyframes wave {
0%, 100% { transform: scaleY(1); }
50% { transform: scaleY(1.5); }
}
/* 旋转唱片 */
.vinyl-record {
width: 50px;
height: 50px;
border-radius: 50%;
background: #333;
background-image: repeating-radial-gradient(#333 0, #333 2px, #444 3px, #444 4px);
border: 2px solid white;
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
display: flex;
align-items: center;
justify-content: center;
animation: spin 4s linear infinite;
animation-play-state: paused; /* 默认暂停 */
}
.vinyl-record.playing { animation-play-state: running; }
.vinyl-record::after {
content: '';
width: 16px;
height: 16px;
background: var(--primary-color);
border-radius: 50%;
border: 4px solid white;
}
@keyframes spin { 100% { transform: rotate(360deg); } }
.playlist-item {
background: white;
padding: 30px;
border-radius: 35px;
margin-bottom: 20px;
cursor: pointer;
transition: all 0.4s;
border: 3px solid transparent;
box-shadow: 0 10px 30px rgba(0,0,0,0.03);
display: flex;
align-items: center;
gap: 25px;
position: relative;
overflow: hidden;
}
.playlist-item::after {
content: '';
position: absolute;
top: 0; right: 0; bottom: 0; left: 0;
background: linear-gradient(45deg, transparent, rgba(255,255,255,0.5), transparent);
transform: translateX(-100%);
transition: 0.5s;
}
.playlist-item::before {
content: '📂';
font-size: 36px;
background: #FFF0F5;
padding: 20px;
border-radius: 25px;
box-shadow: inset 0 0 10px rgba(255, 182, 193, 0.2);
}
.playlist-item:hover {
transform: scale(1.03) translateY(-5px);
border-color: var(--primary-color);
box-shadow: 0 20px 50px rgba(255, 182, 193, 0.25);
}
.playlist-item:hover::after { transform: translateX(100%); }
.playlist-name { font-size: 20px; font-weight: 800; color: #444; margin-bottom: 5px; }
.playlist-count { color: #999; font-size: 15px; font-weight: 600; display: flex; align-items: center; gap: 5px; }
/* 歌词显示区域 */
.lyrics-container {
position: fixed;
right: 30px;
top: 50%;
transform: translateY(-50%);
width: 350px;
max-height: 70vh;
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(30px);
border-radius: 35px;
padding: 30px 20px;
box-shadow: 0 20px 60px rgba(255, 182, 193, 0.25);
border: 2px solid rgba(255, 255, 255, 0.9);
overflow: hidden;
display: none;
z-index: 999;
}
.lyrics-container.active { display: block; animation: slideInRight 0.5s ease; }
@keyframes slideInRight {
from { opacity: 0; transform: translateY(-50%) translateX(50px); }
to { opacity: 1; transform: translateY(-50%) translateX(0); }
}
.lyrics-header {
text-align: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 2px solid rgba(255, 182, 193, 0.3);
}
.lyrics-title {
font-size: 18px;
font-weight: 800;
background: linear-gradient(45deg, #FF8FAB, #FF69B4);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 5px;
}
.lyrics-artist {
font-size: 14px;
color: #999;
font-weight: 600;
}
.lyrics-content {
height: calc(70vh - 120px);
overflow-y: auto;
padding: 10px;
scroll-behavior: smooth;
}
.lyrics-content::-webkit-scrollbar { width: 4px; }
.lyrics-content::-webkit-scrollbar-track { background: rgba(255, 182, 193, 0.1); border-radius: 10px; }
.lyrics-content::-webkit-scrollbar-thumb { background: var(--primary-color); border-radius: 10px; }
.lyric-line {
text-align: center;
padding: 12px 10px;
font-size: 16px;
color: #999;
transition: all 0.3s;
cursor: pointer;
border-radius: 15px;
font-weight: 600;
line-height: 1.6;
}
.lyric-line:hover {
background: rgba(255, 182, 193, 0.1);
color: #666;
}
.lyric-line.active {
font-size: 20px;
color: var(--primary-dark);
font-weight: 800;
background: rgba(255, 182, 193, 0.15);
transform: scale(1.05);
}
.lyric-line.passed {
color: #ccc;
}
.no-lyrics {
text-align: center;
color: #999;
padding: 50px 20px;
font-size: 14px;
}
.lyrics-toggle {
position: fixed;
right: 30px;
top: 50%;
transform: translateY(-50%);
width: 50px;
height: 50px;
background: linear-gradient(45deg, #FFB7C5, #FF69B4);
border-radius: 50%;
display: none;
align-items: center;
justify-content: center;
color: white;
font-size: 20px;
cursor: pointer;
box-shadow: 0 10px 30px rgba(255, 105, 180, 0.4);
transition: all 0.3s;
z-index: 998;
border: 3px solid white;
}
.lyrics-toggle.active { display: flex; }
.lyrics-toggle:hover {
transform: translateY(-50%) scale(1.1);
box-shadow: 0 15px 40px rgba(255, 105, 180, 0.5);
}
/* 📱 移动端适配媒体查询 */
@media screen and (max-width: 768px) {
/* 容器调整 */
.container {
padding: 10px;
width: 100%;
overflow-x: hidden;
}
/* 登录框适配 */
.auth-box {
width: 90%;
padding: 30px 20px;
margin: 0 auto;
}
.auth-box h1 { font-size: 26px; }
/* 头部调整 */
.header {
flex-direction: row;
padding: 15px;
margin-bottom: 20px;
border-radius: 25px;
}
.logo { font-size: 20px; }
.logo::before { font-size: 24px; }
.user-info span { display: none; } /* 手机端隐藏用户名,只留退出按钮 */
/* 主内容区域 */
.main-content {
padding: 20px 15px;
border-radius: 30px;
min-height: calc(100vh - 150px); /* 适应屏幕高度 */
}
/* 搜索框 */
.search-box {
flex-direction: column;
gap: 10px;
}
.search-inputs {
flex-direction: column;
width: 100%;
gap: 10px;
}
.search-input {
padding: 15px 20px;
font-size: 16px;
width: 100%;
}
.search-btn {
width: 100%;
padding: 15px;
font-size: 16px;
border-radius: 25px;
}
/* 导航标签 - 横向滚动 */
.nav-tabs {
width: 100%;
justify-content: flex-start;
padding: 5px;
gap: 8px;
overflow-x: auto;
white-space: nowrap;
-webkit-overflow-scrolling: touch; /* iOS平滑滚动 */
}
.nav-tabs::-webkit-scrollbar { display: none; } /* 隐藏滚动条 */
.nav-tab {
padding: 10px 20px;
font-size: 14px;
flex-shrink: 0;
}
/* 歌曲列表 */
.song-item {
flex-direction: column;
align-items: flex-start;
gap: 15px;
padding: 15px;
}
.song-info { width: 100%; }
.song-actions {
width: 100%;
display: flex;
justify-content: space-between;
gap: 10px;
}
.song-actions button {
flex: 1;
justify-content: center;
padding: 10px;
}
/* 悬浮播放器适配 */
.player {
width: 95%;
bottom: 10px;
padding: 12px 15px;
flex-wrap: wrap;
gap: 10px;
border-radius: 30px;
justify-content: space-between;
}
/* 唱片变小 */
.vinyl-record {
width: 40px;
height: 40px;
border-width: 1px;
}
/* 播放器信息区域 */
.player-info {
flex: 1;
min-width: 0; /* 防止溢出 */
margin-right: 5px;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.player-title {
font-size: 14px;
max-width: 100px;
}
.player-artist { font-size: 12px; }
/* 播放器控制区域 */
.player-controls {
width: 100%;
flex: 0 0 100%; /* 强制占满一行 */
order: 3; /* 换行显示 */
margin-top: 10px;
display: flex;
align-items: center;
justify-content: space-between;
}
/* 音频控件调整 */
.player audio {
height: 40px;
width: 100%;
margin: 0 10px;
}
/* 隐藏不重要的元素 */
#playModeBtn { font-size: 12px; padding: 5px; }
.music-waves { display: none; } /* 手机端隐藏波纹节省空间 */
/* 模态框适配 */
.modal-content {
width: 90%;
margin: 50% auto;
padding: 20px;
}
/* 歌词区域移动端适配 */
.lyrics-container {
position: fixed;
right: 0;
left: 0;
top: auto;
bottom: 0;
transform: none;
width: 100%;
max-height: 50vh;
border-radius: 30px 30px 0 0;
padding: 20px 15px;
}
.lyrics-container.active {
animation: slideInUp 0.4s ease;
}
@keyframes slideInUp {
from { transform: translateY(100%); }
to { transform: translateY(0); }
}
.lyrics-content {
height: calc(50vh - 100px);
}
.lyrics-toggle {
right: 20px;
top: auto;
bottom: 100px;
transform: none;
width: 45px;
height: 45px;
font-size: 18px;
}
.lyrics-toggle:hover {
transform: scale(1.1);
}
}
/* 设备管理样式 */
.device-form {
background: rgba(255, 255, 255, 0.8);
padding: 25px;
border-radius: 30px;
margin-bottom: 30px;
box-shadow: 0 10px 30px rgba(0,0,0,0.05);
}
.device-input-group {
display: flex;
gap: 15px;
margin-bottom: 15px;
}
.device-input {
flex: 1;
padding: 15px 25px;
border: 2px solid var(--input-border);
border-radius: 25px;
font-size: 16px;
background: white;
}
.device-item {
background: white;
padding: 20px 30px;
border-radius: 30px;
margin-bottom: 15px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 5px 15px rgba(0,0,0,0.03);
border: 2px solid transparent;
transition: all 0.3s;
}
.device-item:hover {
transform: translateY(-3px);
box-shadow: 0 15px 35px var(--shadow-color);
border-color: var(--input-focus);
}
.device-info {
display: flex;
flex-direction: column;
gap: 5px;
}
.device-name {
font-weight: 800;
font-size: 18px;
color: #444;
}
.device-mac {
color: #999;
font-family: monospace;
font-size: 14px;
}
.device-status {
display: inline-block;
padding: 4px 12px;
border-radius: 15px;
font-size: 12px;
font-weight: 700;
margin-left: 10px;
}
.status-online { background: #F0FFF4; color: #48BB78; }
.status-offline { background: #FFF5F5; color: #F56565; }
</style>
</head>
<body>
<ul class="bg-bubbles">
<li></li><li></li><li></li><li></li><li></li>
<li></li><li></li><li></li><li></li><li></li>
</ul>
<!-- 登录注册页面 -->
<div id="authPage" class="auth-page">
<div class="auth-box">
<h1>🎵 Meow Music</h1>
<div class="tabs">
<button class="tab active" onclick="switchAuthTab('login')">登录</button>
<button class="tab" onclick="switchAuthTab('register')">注册</button>
</div>
<div id="authMessage"></div>
<form id="loginForm" class="form active" onsubmit="handleLogin(event)">
<input type="text" id="loginUsername" placeholder="用户名" required>
<input type="password" id="loginPassword" placeholder="密码" required>
<button type="submit" class="primary">登录</button>
</form>
<form id="registerForm" class="form" onsubmit="handleRegister(event)">
<input type="text" id="regUsername" placeholder="用户名" required>
<input type="email" id="regEmail" placeholder="邮箱" required>
<input type="password" id="regPassword" placeholder="密码(至少6位)" required minlength="6">
<button type="submit" class="primary">注册</button>
</form>
</div>
</div>
<!-- 主应用页面 -->
<div id="appPage" class="app-page">
<div class="container">
<header class="header">
<div class="logo">🎵 Meow Music</div>
<div class="user-info">
<span id="username"></span>
<button class="btn btn-logout" onclick="handleLogout()">退出</button>
</div>
</header>
<div class="main-content">
<div class="search-box">
<div class="search-inputs">
<input type="text" class="search-input" id="searchInput"
placeholder="搜索歌曲..." onkeypress="handleSearchKeypress(event)">
<input type="text" class="search-input" id="artistInput"
placeholder="歌手(可选)" onkeypress="handleSearchKeypress(event)">
</div>
<button class="search-btn" onclick="searchMusic()">搜索</button>
</div>
<div class="nav-tabs">
<button class="nav-tab active" onclick="switchTab('search')">搜索</button>
<button class="nav-tab" onclick="switchTab('favorite')">我喜欢</button>
<button class="nav-tab" onclick="switchTab('playlists')">我的歌单</button>
<button class="nav-tab" onclick="switchTab('devices')">设备管理</button>
</div>
<div id="searchTab" class="tab-content active">
<div id="searchResults"></div>
</div>
<div id="favoriteTab" class="tab-content">
<div id="favoriteList"></div>
</div>
<div id="playlistsTab" class="tab-content">
<div class="playlist-header">
<h2>我的歌单</h2>
<button class="btn-icon btn-add" onclick="showCreatePlaylistModal()">+ 新建歌单</button>
</div>
<div id="playlistsList"></div>
</div>
<div id="devicesTab" class="tab-content">
<div class="device-form">
<h2 style="margin-bottom: 20px; color: #444;">🔌 绑定新设备</h2>
<div class="device-input-group">
<input type="text" class="device-input" id="bindMac" placeholder="MAC地址 (例如: 80:b5:4e:d4:fa:80)">
<input type="text" class="device-input" id="bindName" placeholder="设备名称 (可选)">
</div>
<div class="hint" style="margin-bottom: 15px; color: #888; font-size: 14px; padding-left: 10px;">
💡 MAC地址可以在ESP32启动日志中找到
</div>
<button class="btn primary" onclick="bindDevice()" style="margin-top: 0;">绑定设备</button>
</div>
<h2 style="margin: 30px 0 20px 10px; color: #444;">📱 我的设备</h2>
<div id="devicesList"></div>
</div>
</div>
</div>
<!-- 歌词显示按钮 -->
<div id="lyricsToggle" class="lyrics-toggle" onclick="toggleLyrics()">🎤</div>
<!-- 歌词显示区域 -->
<div id="lyricsContainer" class="lyrics-container">
<div class="lyrics-header">
<div class="lyrics-title" id="lyricsTitle">-</div>
<div class="lyrics-artist" id="lyricsArtist">-</div>
</div>
<div class="lyrics-content" id="lyricsContent">
<div class="no-lyrics">暂无歌词</div>
</div>
</div>
<!-- 播放器 -->
<div id="player" class="player">
<!-- 旋转唱片 -->
<div class="vinyl-record" id="vinylRecord"></div>
<div class="player-info">
<div style="flex: 1;">
<div class="player-title" id="playerTitle">-</div>
<div class="player-artist" id="playerArtist">-</div>
</div>
<button class="btn-icon" onclick="switchPlayMode()" id="playModeBtn" title="切换播放模式" style="background: transparent; color: var(--primary-dark); margin-right: 5px;">
▶️ 顺序
</button>
</div>
<!-- 律动波纹 -->
<div class="music-waves" id="musicWaves" style="opacity: 0;">
<div class="wave-bar"></div>
<div class="wave-bar"></div>
<div class="wave-bar"></div>
<div class="wave-bar"></div>
</div>
<div class="player-controls">
<button class="btn-icon" onclick="playPrevious()" style="background: #FFF0F5; color: var(--primary-dark); margin-right: 10px;">⏮️</button>
<audio id="audioPlayer" controls style="flex: 1;"></audio>
<button class="btn-icon" onclick="playNext()" style="background: #FFF0F5; color: var(--primary-dark); margin-left: 10px;">⏭️</button>
</div>
</div>
</div>
<!-- 创建歌单模态框 -->
<div id="createPlaylistModal" class="modal">
<div class="modal-content">
<div class="modal-title">✨ 创建新歌单 ✨</div>
<input type="text" id="newPlaylistName" placeholder="给歌单起个好听的名字..." style="width: 100%; padding: 15px; border: 2px solid #FFE4E1; border-radius: 20px; margin-bottom: 15px;">
<input type="text" id="newPlaylistDesc" placeholder="写点心情描述吧..." style="width: 100%; padding: 15px; border: 2px solid #FFE4E1; border-radius: 20px;">
<div class="modal-buttons">
<button class="btn-cancel" onclick="hideCreatePlaylistModal()">取消</button>
<button class="btn-confirm" onclick="createPlaylist()">创建</button>
</div>
</div>
</div>
<script>
// 变量声明必须在最前面
let token = localStorage.getItem('token');
let currentUser = null;
let currentSong = null;
let playlists = [];
let currentPlaylist = null;
let playMode = 'list'; // 'list'=顺序 | 'single'=单曲循环 | 'random'=随机 | 'loop'=列表循环
let currentSongList = [];
let currentIndex = 0;
let lyricsData = []; // 存储解析后的歌词数据
let currentLyricIndex = -1; // 当前歌词行索引
// 主题管理
let currentTheme = localStorage.getItem('theme') || 'pink';
// 初始化检查
document.addEventListener('DOMContentLoaded', () => {
applyTheme();
if (token) {
checkAuth();
} else {
showAuthPage();
}
});
function toggleTheme() {
currentTheme = currentTheme === 'pink' ? 'blue' : 'pink';
localStorage.setItem('theme', currentTheme);
applyTheme();
}
function applyTheme() {
const body = document.body;
const switchBtn = document.querySelector('.theme-switch');
if (currentTheme === 'blue') {
body.setAttribute('data-theme', 'blue');
if(switchBtn) switchBtn.textContent = '💻';
} else {
body.removeAttribute('data-theme');
if(switchBtn) switchBtn.textContent = '🎨';
}
}
// 魔法光标特效 - 高密度流畅版
document.addEventListener('mousemove', function(e) {
// 几乎不丢弃,保证流畅性 (只丢弃10%以防浏览器卡顿,之前是丢弃50%)
if (Math.random() > 0.9) return;
const sparkle = document.createElement('div');
sparkle.classList.add('sparkle');
// 随机大小
const size = Math.random() * 6 + 3; // 稍微大一点点
// 随机扩散方向
const tx = (Math.random() - 0.5) * 60;
const ty = (Math.random() - 0.5) * 60;
// 随机颜色 (白色为主,偶尔出现粉色/蓝色)
const colors = ['#FFF', '#FFF', '#FFF', '#FFB7C5', '#87CEEB'];
const color = colors[Math.floor(Math.random() * colors.length)];
sparkle.style.width = `${size}px`;
sparkle.style.height = `${size}px`;
sparkle.style.left = `${e.clientX}px`;
sparkle.style.top = `${e.clientY}px`;
sparkle.style.backgroundColor = color; // 应用颜色
sparkle.style.setProperty('--tx', `${tx}px`);
sparkle.style.setProperty('--ty', `${ty}px`);
document.body.appendChild(sparkle);
setTimeout(() => sparkle.remove(), 1000);
});
// 点击爱心特效 - 强制启用
document.addEventListener('click', function(e) {
// 不再判断主题,强制显示
// 也不判断点击目标,全屏有效
const heart = document.createElement('div');
heart.classList.add('click-heart');
heart.innerHTML = ['💖', '✨', '🌸', '🎀'][Math.floor(Math.random() * 4)];
heart.style.left = `${e.clientX}px`;
heart.style.top = `${e.clientY}px`;
document.body.appendChild(heart);
setTimeout(() => heart.remove(), 1000);
});
// 控制播放动画
// 认证相关
function switchAuthTab(tab) {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.form').forEach(f => f.classList.remove('active'));
if (tab === 'login') {
document.querySelectorAll('.tab')[0].classList.add('active');
document.getElementById('loginForm').classList.add('active');
} else {
document.querySelectorAll('.tab')[1].classList.add('active');
document.getElementById('registerForm').classList.add('active');
}
clearAuthMessage();
}
function showAuthMessage(msg, type) {
const msgDiv = document.getElementById('authMessage');
msgDiv.innerHTML = `<div class="message ${type}">${msg}</div>`;
}
function clearAuthMessage() {
document.getElementById('authMessage').innerHTML = '';
}
async function handleRegister(e) {
e.preventDefault();
const username = document.getElementById('regUsername').value.trim();
const email = document.getElementById('regEmail').value.trim();
const password = document.getElementById('regPassword').value;
showAuthMessage('注册中...', 'info');
try {
const response = await fetch('/api/auth/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, email, password })
});
const result = await response.json();
if (response.ok) {
token = result.token;
localStorage.setItem('token', token);
currentUser = result.user;
showAuthMessage('注册成功!', 'success');
setTimeout(() => showAppPage(), 1000);
} else {
showAuthMessage(result.error || '注册失败', 'error');
}
} catch (error) {
showAuthMessage('网络错误: ' + error.message, 'error');
}
}
async function handleLogin(e) {
e.preventDefault();
const username = document.getElementById('loginUsername').value.trim();
const password = document.getElementById('loginPassword').value;
showAuthMessage('登录中...', 'info');
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const result = await response.json();
if (response.ok) {
token = result.token;
localStorage.setItem('token', token);
currentUser = result.user;
showAuthMessage('登录成功!', 'success');
setTimeout(() => showAppPage(), 1000);
} else {
showAuthMessage(result.error || '登录失败', 'error');
}
} catch (error) {
showAuthMessage('网络错误: ' + error.message, 'error');
}
}
async function checkAuth() {
try {
const response = await fetch('/api/auth/me', {
headers: { 'Authorization': 'Bearer ' + token }
});
if (response.ok) {
currentUser = await response.json();
showAppPage();
} else {
localStorage.removeItem('token');
token = null;
showAuthPage();
}
} catch (error) {
showAuthPage();
}
}
function handleLogout() {
localStorage.removeItem('token');
token = null;
currentUser = null;
showAuthPage();
// 退出登录时,隐藏主题切换按钮,并强制恢复粉色主题
document.querySelector('.theme-switch').style.display = 'none';
if (currentTheme === 'blue') {
document.body.removeAttribute('data-theme'); // 强制移除蓝色主题属性
// 恢复粉色系特效
document.body.style.cursor = `url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="%23FF69B4" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19l7-7 3 3-7 7-3-3z"></path><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"></path><path d="M2 2l7.586 7.586"></path><circle cx="11" cy="11" r="2"></circle></svg>'), auto`;
}
}
function showAuthPage() {
document.getElementById('authPage').style.display = 'flex';
document.getElementById('appPage').style.display = 'none';
}
function showAppPage() {
document.getElementById('authPage').style.display = 'none';
document.getElementById('appPage').style.display = 'block';
document.getElementById('username').textContent = currentUser.username;
// 登录成功后显示主题切换按钮,并应用用户保存的主题
document.querySelector('.theme-switch').style.display = 'block';
applyTheme(); // 重新应用用户的主题设置
loadPlaylists();
}
// 标签切换
function switchTab(tab) {
document.querySelectorAll('.nav-tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
if (tab === 'search') {
document.querySelectorAll('.nav-tab')[0].classList.add('active');
document.getElementById('searchTab').classList.add('active');
} else if (tab === 'favorite') {
document.querySelectorAll('.nav-tab')[1].classList.add('active');
document.getElementById('favoriteTab').classList.add('active');
loadFavorites();
} else if (tab === 'playlists') {
document.querySelectorAll('.nav-tab')[2].classList.add('active');
document.getElementById('playlistsTab').classList.add('active');
loadPlaylists();
} else if (tab === 'devices') {
document.querySelectorAll('.nav-tab')[3].classList.add('active');
document.getElementById('devicesTab').classList.add('active');
loadDevices();
}
}
// 设备管理功能
async function loadDevices() {
const listDiv = document.getElementById('devicesList');
listDiv.innerHTML = '<div class="loading">加载中...</div>';
try {
const response = await fetch('/api/device/list', {
headers: { 'Authorization': 'Bearer ' + token }
});
const data = await response.json();
if (data.success && data.devices && data.devices.length > 0) {
const html = data.devices.map(device => `
<div class="device-item">
<div class="device-info">
<div class="device-name">${device.device_name || 'ESP32音乐播放器'}</div>
<div class="device-mac">MAC: ${device.mac}</div>
</div>
<div style="display: flex; align-items: center;">
<span class="device-status ${device.is_active ? 'status-online' : 'status-offline'}">
${device.is_active ? '🟢 在线' : '🔴 离线'}
</span>
<button class="btn-icon btn-remove" style="margin-left: 15px;" onclick="unbindDevice('${device.mac}')">解绑</button>
</div>
</div>
`).join('');
listDiv.innerHTML = html;
} else {
listDiv.innerHTML = '<div class="empty-state">还没有绑定的设备</div>';
}
} catch (error) {
listDiv.innerHTML = '<div class="empty-state">加载失败: ' + error.message + '</div>';
}
}
async function bindDevice() {
const macInput = document.getElementById('bindMac');
const nameInput = document.getElementById('bindName');
const mac = formatMac(macInput.value.trim());
const name = nameInput.value.trim();
if (!mac) {
alert('请输入MAC地址');
return;
}
try {
const response = await fetch('/api/device/bind-direct', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token
},
body: JSON.stringify({ mac: mac, device_name: name })
});
const data = await response.json();
if (response.ok && data.success) {
alert('✅ 绑定成功!');
macInput.value = '';
nameInput.value = '';
loadDevices();
} else {
alert('❌ 绑定失败: ' + (data.message || '未知错误'));
}
} catch (error) {
alert('❌ 网络错误: ' + error.message);
}
}
async function unbindDevice(mac) {
if (!confirm('确定要解绑此设备吗?')) return;
try {
const response = await fetch('/api/device/unbind', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token
},
body: JSON.stringify({ mac: mac })
});
const data = await response.json();
if (response.ok && data.success) {
alert('✅ 解绑成功');
loadDevices();
} else {
alert('❌ 解绑失败: ' + (data.message || '未知错误'));
}
} catch (error) {
alert('❌ 网络错误: ' + error.message);
}
}
function formatMac(mac) {
// 移除所有非十六进制字符
let cleaned = mac.replace(/[^A-Fa-f0-9]/g, '');
// 如果长度不是12,返回原值让后端处理或报错
if (cleaned.length !== 12) return mac;
// 格式化为 AA:BB:CC:DD:EE:FF
return cleaned.match(/.{2}/g).join(':').toUpperCase();
}
// MAC输入框自动格式化
document.getElementById('bindMac').addEventListener('input', function(e) {
const val = e.target.value.replace(/[^A-Fa-f0-9]/g, '');
if (val.length === 12 && !e.target.value.includes(':')) {
e.target.value = formatMac(val);
}
});
// 搜索
function handleSearchKeypress(e) {
if (e.key === 'Enter') {
searchMusic();
}
}
async function searchMusic() {
const query = document.getElementById('searchInput').value.trim();
const artist = document.getElementById('artistInput').value.trim();
if (!query) {
alert('请输入歌曲名称');
return;
}
const resultsDiv = document.getElementById('searchResults');
resultsDiv.innerHTML = '<div class="loading">🔍 搜索中...</div>';
try {
const response = await fetch(`/api/search?query=${encodeURIComponent(query)}`);
const data = await response.json();
if (data && data.length > 0) {
// 如果指定了歌手,进行过滤
let filteredData = data;
if (artist) {
filteredData = data.filter(song => {
const songArtist = (song.artist || song.Artist || '').toLowerCase();
return songArtist.includes(artist.toLowerCase());
});
if (filteredData.length === 0) {
resultsDiv.innerHTML = `<div class="empty-state">未找到歌手"${artist}"的相关歌曲<br><small>找到 ${data.length} 首同名歌曲,但歌手不匹配</small></div>`;
return;
}
}
displaySearchResults(filteredData);
} else {
resultsDiv.innerHTML = '<div class="empty-state">未找到结果</div>';
}
} catch (error) {
console.error('搜索失败:', error);
resultsDiv.innerHTML = '<div class="empty-state">搜索失败,请重试</div>';
}
}
function displaySearchResults(songs) {
const html = '<div class="song-list">' + songs.map(song => `
<div class="song-item">
<div class="song-info">
<div class="song-title">${song.title || '未知歌曲'}</div>
<div class="song-artist">${song.artist || '未知艺术家'}</div>
</div>
<div class="song-actions">
<button class="btn-icon btn-play" onclick='playSong(${JSON.stringify(song)})'>播放</button>
<button class="btn-icon btn-add" onclick='addToFavorite(${JSON.stringify(song)})'>喜欢</button>
</div>
</div>
`).join('') + '</div>';
document.getElementById('searchResults').innerHTML = html;
}
// 播放相关
function playSong(song, index = -1) {
console.log('====== 播放歌曲 ======');
console.log('原始歌曲对象:', song);
console.log('所有字段:', Object.keys(song));
// 规范化歌曲对象(支持多种命名格式)
const normalizedSong = {
title: song.title || song.Title || '未知歌曲',
artist: song.artist || song.Artist || '未知艺术家',
url: song.url || song.link || song.audio_url || song.audio_full_url ||
song.AudioURL || song.AudioFullURL || song.URL || '',
lyric_url: song.lyric_url || song.LyricURL || song.lyricUrl || '',
cover_url: song.cover_url || song.CoverURL || song.coverUrl || ''
};
console.log('规范化后的歌曲:', normalizedSong);
console.log('URL检查 - url:', song.url, 'link:', song.link, 'AudioURL:', song.AudioURL, 'AudioFullURL:', song.AudioFullURL);
console.log('歌词URL:', normalizedSong.lyric_url);
currentSong = normalizedSong;
if (index >= 0) {
currentIndex = index;
}
const player = document.getElementById('player');
const audio = document.getElementById('audioPlayer');
const title = document.getElementById('playerTitle');
const artist = document.getElementById('playerArtist');
console.log('正在播放:', normalizedSong.title, '-', normalizedSong.artist);
console.log('最终音频URL:', normalizedSong.url);
title.textContent = normalizedSong.title;
artist.textContent = normalizedSong.artist;
if (!normalizedSong.url) {
console.error('URL为空!歌曲对象:', song);
alert('音乐链接无效,无法播放\n\n请按F12打开控制台查看详细信息');
return;
}
audio.src = normalizedSong.url;
player.classList.add('active');
audio.play().catch(e => {
console.error('播放失败:', e);
alert('播放失败: ' + e.message);
});
// 加载歌词
loadLyrics(song);
}
// 歌词相关功能
function toggleLyrics() {
const container = document.getElementById('lyricsContainer');
container.classList.toggle('active');
}
async function loadLyrics(song) {
const lyricsContent = document.getElementById('lyricsContent');
const lyricsTitle = document.getElementById('lyricsTitle');
const lyricsArtist = document.getElementById('lyricsArtist');
const lyricsToggle = document.getElementById('lyricsToggle');
// 更新歌词区域标题
lyricsTitle.textContent = song.title || song.Title || '未知歌曲';
lyricsArtist.textContent = song.artist || song.Artist || '未知艺术家';
// 构建歌词URL
const lyricUrl = song.lyric_url || song.LyricURL || song.lyricUrl;
if (!lyricUrl) {
lyricsContent.innerHTML = '<div class="no-lyrics">该歌曲暂无歌词</div>';
lyricsToggle.classList.remove('active');
return;
}
try {
const response = await fetch(lyricUrl);
const lyricText = await response.text();
if (!lyricText || lyricText.trim() === '') {
lyricsContent.innerHTML = '<div class="no-lyrics">该歌曲暂无歌词</div>';
lyricsToggle.classList.remove('active');
return;
}
// 解析LRC歌词
lyricsData = parseLRC(lyricText);
if (lyricsData.length === 0) {
lyricsContent.innerHTML = '<div class="no-lyrics">歌词格式错误</div>';
lyricsToggle.classList.remove('active');
return;
}
// 显示歌词
displayLyrics();
lyricsToggle.classList.add('active');
} catch (error) {
console.error('加载歌词失败:', error);
lyricsContent.innerHTML = '<div class="no-lyrics">歌词加载失败</div>';
lyricsToggle.classList.remove('active');
}
}
// 解析LRC格式歌词
function parseLRC(lrcText) {
const lines = lrcText.split('\n');
const result = [];
const timeRegex = /\[(\d{2}):(\d{2})\.(\d{2,3})\]/g;
lines.forEach(line => {
const matches = [...line.matchAll(timeRegex)];
if (matches.length > 0) {
const text = line.replace(timeRegex, '').trim();
if (text) {
matches.forEach(match => {
const minutes = parseInt(match[1]);
const seconds = parseInt(match[2]);
const milliseconds = parseInt(match[3].padEnd(3, '0'));
const time = minutes * 60 + seconds + milliseconds / 1000;
result.push({ time, text });
});
}
}
});
// 按时间排序
result.sort((a, b) => a.time - b.time);
return result;
}
// 显示歌词列表
function displayLyrics() {
const lyricsContent = document.getElementById('lyricsContent');
const html = lyricsData.map((lyric, index) =>
`<div class="lyric-line" data-index="${index}" data-time="${lyric.time}" onclick="seekToLyric(${lyric.time})">${lyric.text}</div>`
).join('');
lyricsContent.innerHTML = html;
currentLyricIndex = -1;
}
// 更新当前歌词高亮
function updateLyrics(currentTime) {
if (lyricsData.length === 0) return;
// 找到当前应该高亮的歌词
let newIndex = -1;
for (let i = 0; i < lyricsData.length; i++) {
if (currentTime >= lyricsData[i].time) {
newIndex = i;
} else {
break;
}
}
if (newIndex !== currentLyricIndex) {
currentLyricIndex = newIndex;
const lines = document.querySelectorAll('.lyric-line');
lines.forEach((line, index) => {
line.classList.remove('active', 'passed');
if (index === currentLyricIndex) {
line.classList.add('active');
// 滚动到当前歌词
line.scrollIntoView({ behavior: 'smooth', block: 'center' });
} else if (index < currentLyricIndex) {
line.classList.add('passed');
}
});
}
}
// 点击歌词跳转播放位置
function seekToLyric(time) {
const audio = document.getElementById('audioPlayer');
audio.currentTime = time;
}
// 监听音频时间更新
document.addEventListener('DOMContentLoaded', function() {
const audio = document.getElementById('audioPlayer');
if (audio) {
// 播放/暂停动画控制
audio.addEventListener('play', () => {
document.getElementById('vinylRecord').classList.add('playing');
document.getElementById('musicWaves').style.opacity = '1';
});
audio.addEventListener('pause', () => {
document.getElementById('vinylRecord').classList.remove('playing');
document.getElementById('musicWaves').style.opacity = '0';
});
// 歌词同步更新
audio.addEventListener('timeupdate', () => {
updateLyrics(audio.currentTime);
});
// 播放结束处理
audio.addEventListener('ended', function() {
if (playMode === 'single') {
audio.currentTime = 0;
audio.play();
} else if (playMode === 'loop' || playMode === 'random') {
playNext();
} else if (playMode === 'list') {
if (currentIndex < currentSongList.length - 1) {
playNext();
}
}
});
}
});
// 播放列表中的歌曲
function playFromList(songs, index) {
currentSongList = songs;
currentIndex = index;
playSong(songs[index], index);
}
// 播放全部
function playAll(songs) {
if (!songs || songs.length === 0) {
alert('没有可播放的歌曲');
return;
}
currentSongList = songs;
currentIndex = 0;
playSong(songs[0], 0);
}
// 下一首
function playNext() {
if (currentSongList.length === 0) return;
if (playMode === 'single') {
// 单曲循环:重新播放当前歌曲
const audio = document.getElementById('audioPlayer');
audio.currentTime = 0;
audio.play();
return;
}
if (playMode === 'random') {
// 随机播放
currentIndex = Math.floor(Math.random() * currentSongList.length);
} else {
// 顺序播放和列表循环
currentIndex = (currentIndex + 1) % currentSongList.length;
if (playMode === 'list' && currentIndex === 0) {
// 顺序播放到最后一首后停止
return;
}
}
playSong(currentSongList[currentIndex], currentIndex);
}
// 上一首
function playPrevious() {
if (currentSongList.length === 0) return;
if (playMode === 'random') {
// 随机播放
currentIndex = Math.floor(Math.random() * currentSongList.length);
} else {
// 顺序播放和循环播放
currentIndex = (currentIndex - 1 + currentSongList.length) % currentSongList.length;
}
playSong(currentSongList[currentIndex], currentIndex);
}
// 切换播放模式
function switchPlayMode() {
const modes = ['list', 'loop', 'single', 'random'];
const modeNames = {
'list': '▶️ 顺序',
'loop': '🔁 列表循环',
'single': '🔂 单曲循环',
'random': '🔀 随机'
};
const currentModeIndex = modes.indexOf(playMode);
playMode = modes[(currentModeIndex + 1) % modes.length];
document.getElementById('playModeBtn').textContent = modeNames[playMode];
console.log('播放模式:' + modeNames[playMode]);
}
// 监听音频播放结束事件
document.addEventListener('DOMContentLoaded', function() {
const audio = document.getElementById('audioPlayer');
if (audio) {
audio.addEventListener('ended', function() {
if (playMode === 'single') {
// 单曲循环
audio.currentTime = 0;
audio.play();
} else if (playMode === 'loop' || playMode === 'random') {
// 列表循环或随机播放
playNext();
} else if (playMode === 'list') {
// 顺序播放:如果不是最后一首,播放下一首
if (currentIndex < currentSongList.length - 1) {
playNext();
}
}
});
}
});
// 喜欢的歌曲
async function addToFavorite(song) {
try {
// 转换字段名为后端期望的格式
const songData = {
title: song.title || song.Title || '',
artist: song.artist || song.Artist || '',
audio_url: song.url || song.link || song.audio_url || '',
audio_full_url: song.url || song.link || song.audio_full_url || '',
m3u8_url: song.m3u8_url || song.M3U8URL || '',
lyric_url: song.lyric_url || song.LyricURL || '',
cover_url: song.cover_url || song.CoverURL || '',
duration: song.duration || song.Duration || 0
};
console.log('添加到收藏,发送数据:', songData);
const response = await fetch('/api/favorite/add', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token
},
body: JSON.stringify(songData)
});
if (response.ok) {
alert('已添加到"我喜欢"');
} else {
alert('添加失败');
}
} catch (error) {
alert('添加失败: ' + error.message);
}
}
// 存储当前收藏列表
let favoriteSongs = [];
async function loadFavorites() {
const listDiv = document.getElementById('favoriteList');
listDiv.innerHTML = '<div class="loading">加载中...</div>';
try {
const response = await fetch('/api/favorite/list', {
headers: { 'Authorization': 'Bearer ' + token }
});
const songs = await response.json();
// 保存到全局变量
favoriteSongs = songs || [];
if (songs && songs.length > 0) {
// 添加播放全部按钮
const playAllBtn = `
<div style="margin-bottom: 15px;">
<button class="btn-icon" onclick="playAllFavorites()" style="background: #667eea; color: white; padding: 10px 20px;">
▶️ 播放全部 (${songs.length}首)
</button>
</div>
`;
const songsHtml = songs.map((song, index) => `
<div class="song-item">
<div class="song-info">
<div class="song-title">${song.title || song.Title || '未知歌曲'}</div>
<div class="song-artist">${song.artist || song.Artist || '未知艺术家'}</div>
</div>
<div class="song-actions">
<button class="btn-icon btn-play" onclick="playFavoriteAtIndex(${index})">播放</button>
<button class="btn-icon btn-remove" onclick='removeFromFavorite(${JSON.stringify(song)})'>移除</button>
</div>
</div>
`).join('');
listDiv.innerHTML = playAllBtn + '<div class="song-list">' + songsHtml + '</div>';
} else {
listDiv.innerHTML = '<div class="empty-state">还没有喜欢的歌曲</div>';
}
} catch (error) {
listDiv.innerHTML = '<div class="empty-state">加载失败</div>';
}
}
// 播放收藏列表中的所有歌曲
function playAllFavorites() {
if (favoriteSongs.length > 0) {
playAll(favoriteSongs);
}
}
// 播放收藏列表中的指定歌曲
function playFavoriteAtIndex(index) {
if (favoriteSongs.length > 0) {
playFromList(favoriteSongs, index);
}
}
async function removeFromFavorite(song) {
try {
const response = await fetch('/api/favorite/remove', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token
},
body: JSON.stringify(song)
});
if (response.ok) {
loadFavorites();
} else {
alert('移除失败');
}
} catch (error) {
alert('移除失败: ' + error.message);
}
}
// 歌单管理
async function loadPlaylists() {
const listDiv = document.getElementById('playlistsList');
listDiv.innerHTML = '<div class="loading">加载中...</div>';
try {
const response = await fetch('/api/user/playlists', {
headers: { 'Authorization': 'Bearer ' + token }
});
playlists = await response.json();
if (playlists && playlists.length > 0) {
const html = playlists.map(pl => `
<div class="playlist-item" onclick='viewPlaylist(${JSON.stringify(pl)})'>
<div class="playlist-name">${pl.name}</div>
<div class="playlist-count">${pl.songs ? pl.songs.length : 0} 首歌曲</div>
</div>
`).join('');
listDiv.innerHTML = html;
} else {
listDiv.innerHTML = '<div class="empty-state">还没有歌单</div>';
}
} catch (error) {
listDiv.innerHTML = '<div class="empty-state">加载失败</div>';
}
}
function showCreatePlaylistModal() {
document.getElementById('createPlaylistModal').classList.add('active');
}
function hideCreatePlaylistModal() {
document.getElementById('createPlaylistModal').classList.remove('active');
document.getElementById('newPlaylistName').value = '';
document.getElementById('newPlaylistDesc').value = '';
}
async function createPlaylist() {
const name = document.getElementById('newPlaylistName').value.trim();
const description = document.getElementById('newPlaylistDesc').value.trim();
if (!name) {
alert('请输入歌单名称');
return;
}
try {
const response = await fetch('/api/user/playlists/create', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token
},
body: JSON.stringify({ name, description })
});
if (response.ok) {
hideCreatePlaylistModal();
loadPlaylists();
} else {
alert('创建失败');
}
} catch (error) {
alert('创建失败: ' + error.message);
}
}
function viewPlaylist(playlist) {
alert('歌单详情功能开发中...\n\n歌单名:' + playlist.name + '\n歌曲数:' + (playlist.songs ? playlist.songs.length : 0));
}
</script>
</body>
</html>