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

2183 lines
81 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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>