2183 lines
81 KiB
HTML
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>
|