forked from miao-moe/QZMusic_PC
feat: QZ Music for Windows(个人学习项目);Vue主界面+设置界面;Router路由;Pinia全局状态管理;封面取色;IPC通信示例
This commit is contained in:
185
src/App.vue
Normal file
185
src/App.vue
Normal file
@@ -0,0 +1,185 @@
|
||||
<template>
|
||||
<div class="app-container" :class="{ 'dark-mode': store.darkMode }">
|
||||
<SettingsOverlay />
|
||||
<SideBar />
|
||||
|
||||
<main class="main-content">
|
||||
<header class="header-bar">
|
||||
<div class="header-left"></div>
|
||||
<WindowControls />
|
||||
</header>
|
||||
|
||||
<div class="content-scrollable">
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="fade" mode="out-in">
|
||||
<component :is="Component" />
|
||||
</transition>
|
||||
</router-view>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="footer-player">
|
||||
<PlayerBar />
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { usePlayerStore } from './stores/playerStore';
|
||||
import SideBar from './components/layout/SideBar.vue';
|
||||
import WindowControls from './components/layout/WindowControls.vue';
|
||||
import PlayerBar from './components/layout/PlayerBar.vue';
|
||||
import SettingsOverlay from "./components/layout/SettingsOverlay.vue";
|
||||
|
||||
const store = usePlayerStore();
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
/* 主题变量系统 */
|
||||
:root {
|
||||
/* 亮色主题变量 */
|
||||
--bg-primary: #ffffff;
|
||||
--bg-secondary: #f6f8fa;
|
||||
--bg-tertiary: #f9fafb;
|
||||
--text-primary: #333333;
|
||||
--text-secondary: #666666;
|
||||
--text-tertiary: #888888;
|
||||
--border-color: #e0e0e0;
|
||||
--border-light: #f0f0f0;
|
||||
--sidebar-bg: #f6f8fa;
|
||||
--sidebar-border: #e0e0e0;
|
||||
--header-bg: #ffffff;
|
||||
--content-bg: #ffffff;
|
||||
--player-bg: #ffffff;
|
||||
--card-bg: #ffffff;
|
||||
--overlay-bg: rgba(0, 0, 0, 0.4);
|
||||
--settings-modal-bg: white;
|
||||
--settings-sidebar-bg: #f9fafb;
|
||||
--settings-sidebar-border: #eee;
|
||||
--nav-item-hover: #eee;
|
||||
--nav-item-active: #333;
|
||||
--nav-item-active-text: white;
|
||||
--close-btn-color: #999;
|
||||
--close-btn-hover: #333;
|
||||
--dummy-option-border: #f0f0f0;
|
||||
--logo-placeholder-bg: linear-gradient(45deg, #ff6b6b, #4ecdc4);
|
||||
}
|
||||
|
||||
/* 深色主题变量 */
|
||||
.dark-mode {
|
||||
--bg-primary: #1a1a1a;
|
||||
--bg-secondary: #2d2d2d;
|
||||
--bg-tertiary: #333333;
|
||||
--text-primary: #ffffff;
|
||||
--text-secondary: #b0b0b0;
|
||||
--text-tertiary: #808080;
|
||||
--border-color: #404040;
|
||||
--border-light: #3a3a3a;
|
||||
--sidebar-bg: #2d2d2d;
|
||||
--sidebar-border: #404040;
|
||||
--header-bg: #1a1a1a;
|
||||
--content-bg: #1a1a1a;
|
||||
--player-bg: #1a1a1a;
|
||||
--card-bg: #2d2d2d;
|
||||
--overlay-bg: rgba(0, 0, 0, 0.7);
|
||||
--settings-modal-bg: #2d2d2d;
|
||||
--settings-sidebar-bg: #333333;
|
||||
--settings-sidebar-border: #404040;
|
||||
--nav-item-hover: #404040;
|
||||
--nav-item-active: #6366f1;
|
||||
--nav-item-active-text: white;
|
||||
--close-btn-color: #808080;
|
||||
--close-btn-hover: #ffffff;
|
||||
--dummy-option-border: #3a3a3a;
|
||||
--logo-placeholder-bg: linear-gradient(45deg, #6366f1, #8b5cf6);
|
||||
}
|
||||
|
||||
/* 全局重置 */
|
||||
html, body, #app {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
overflow: hidden; /* 防止浏览器默认滚动 */
|
||||
background-color: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
transition: background-color 0.6s ease, color 0.6s ease;
|
||||
}
|
||||
|
||||
/* 布局结构 */
|
||||
.app-container {
|
||||
display: grid;
|
||||
grid-template-columns: 220px 1fr; /* 侧边栏宽度 | 主内容 */
|
||||
grid-template-rows: 1fr 80px; /* 主体高度 | 播放器高度 */
|
||||
transition: background-color 0.6s ease;
|
||||
}
|
||||
|
||||
/* 布局结构样式 */
|
||||
.app-container {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
|
||||
/* 侧边栏跨越第一行 */
|
||||
.sidebar {
|
||||
grid-column: 1 / 2;
|
||||
grid-row: 1 / 2;
|
||||
}
|
||||
|
||||
/* 主内容区 */
|
||||
.main-content {
|
||||
grid-column: 2 / 3;
|
||||
grid-row: 1 / 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
background: var(--content-bg);
|
||||
|
||||
/* 顶部拖拽区 */
|
||||
.header-bar {
|
||||
height: 60px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
-webkit-app-region: drag; /* Electron 拖拽 */
|
||||
background: var(--header-bg);
|
||||
}
|
||||
|
||||
/* 路由视图滚动区 */
|
||||
.content-scrollable {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 20px 40px;
|
||||
background: var(--content-bg);
|
||||
|
||||
/* 隐藏滚动条但保留功能 */
|
||||
&::-webkit-scrollbar { width: 6px; }
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--border-color);
|
||||
border-radius: 3px;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 底部播放器跨越两列 */
|
||||
.footer-player {
|
||||
grid-column: 1 / 3;
|
||||
grid-row: 2 / 3;
|
||||
z-index: 200;
|
||||
background: var(--player-bg);
|
||||
}
|
||||
}
|
||||
|
||||
/* 路由动画 */
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
276
src/components/layout/PlayerBar.vue
Normal file
276
src/components/layout/PlayerBar.vue
Normal file
@@ -0,0 +1,276 @@
|
||||
<template>
|
||||
<div class="player-bar" :style="gradientStyle">
|
||||
<div class="glass-overlay"></div>
|
||||
|
||||
<!-- 新增:顶部进度条,使用专辑图提取的最深色 -->
|
||||
<div class="top-progress">
|
||||
<div class="progress-fill" :style="{ width: store.progressPercentage + '%', backgroundColor: store.progressColor }"></div>
|
||||
</div>
|
||||
|
||||
<div class="bar-content">
|
||||
<div class="side-container left">
|
||||
<div class="track-info">
|
||||
<div class="album-cover">
|
||||
<img :src="store.currentSong.cover" @load="store.extractColors" alt="Album Pic"/>
|
||||
</div>
|
||||
<div class="meta">
|
||||
<div class="song-title">{{ store.currentSong.title }}</div>
|
||||
<div class="artist-name">{{ store.currentSong.artist }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="center-container">
|
||||
<div class="controls-wrapper">
|
||||
<button class="ctrl-btn sm">
|
||||
<Icon icon="lucide:skip-back" width="22" />
|
||||
</button>
|
||||
<button class="ctrl-btn lg play-btn" @click="store.togglePlay">
|
||||
<Icon :icon="store.isPlaying ? 'lucide:pause' : 'lucide:play'" width="28" fill="currentColor" />
|
||||
</button>
|
||||
<button class="ctrl-btn sm">
|
||||
<Icon icon="lucide:skip-forward" width="22" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="side-container right">
|
||||
<div class="extra-controls">
|
||||
<!-- 美化后的时间显示,放在右侧最左侧 -->
|
||||
<div class="time-display">
|
||||
{{ formatTime(store.currentTime) }} / {{ formatTime(store.currentSong.duration) }}
|
||||
</div>
|
||||
|
||||
<Icon icon="lucide:repeat" width="20" class="icon-btn" />
|
||||
<div class="volume-box">
|
||||
<Icon icon="lucide:volume-2" width="20" />
|
||||
<el-slider v-model="store.volume" size="small" class="custom-slider" />
|
||||
</div>
|
||||
<Icon icon="lucide:list-music" width="20"
|
||||
class="icon-btn"
|
||||
:class="{ active: store.showPlaylist }"
|
||||
@click="store.showPlaylist = !store.showPlaylist" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 保留底部细进度条(鼠标悬停可变粗) -->
|
||||
<div class="bottom-progress">
|
||||
<div class="progress-fill" :style="{ width: store.progressPercentage + '%' }"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { usePlayerStore } from '../../stores/playerStore';
|
||||
import { Icon } from '@iconify/vue';
|
||||
|
||||
const store = usePlayerStore();
|
||||
|
||||
const gradientStyle = computed(() => {
|
||||
return {
|
||||
background: `linear-gradient(135deg, ${store.themeColors.primary}, ${store.themeColors.secondary})`
|
||||
};
|
||||
});
|
||||
|
||||
function formatTime(val: number) {
|
||||
if (!val || isNaN(val)) return '0:00';
|
||||
const m = Math.floor(val / 60);
|
||||
const s = Math.floor(val % 60);
|
||||
return `${m}:${s < 10 ? '0' + s : s}`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.player-bar {
|
||||
height: 80px; // 稍微增高以容纳布局
|
||||
width: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: background 0.5s ease; // 颜色切换动画
|
||||
|
||||
.glass-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(255, 255, 255, 0.15); // 稍微提亮
|
||||
backdrop-filter: blur(20px); // 毛玻璃
|
||||
z-index: 1;
|
||||
border-top: 1px solid rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
.bar-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 3%; // 使用百分比 padding 适应缩放
|
||||
color: white; // 假设提取的颜色较深,文字用白色,反之需计算反色
|
||||
}
|
||||
|
||||
/* 左右容器,确保中间绝对居中 */
|
||||
.side-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0; // 防止 flex item 溢出
|
||||
|
||||
&.right {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
/* 左侧信息 */
|
||||
.track-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.album-cover {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
|
||||
flex-shrink: 0;
|
||||
img { width: 100%; height: 100%; object-fit: cover; }
|
||||
}
|
||||
|
||||
.meta {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
.song-title { font-size: 1.1rem; font-weight: 600; text-overflow: ellipsis; overflow: hidden; }
|
||||
.artist-name { font-size: 0.9rem; opacity: 0.8; margin-top: 4px; }
|
||||
}
|
||||
}
|
||||
|
||||
/* 绝对居中容器 */
|
||||
.center-container {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
width: 200px; // 限制宽度
|
||||
|
||||
.controls-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 按钮样式 */
|
||||
.ctrl-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: rgba(255,255,255,0.9);
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
transition: transform 0.2s, color 0.2s;
|
||||
|
||||
&:hover { color: #fff; transform: scale(1.1); }
|
||||
&:active { transform: scale(0.95); }
|
||||
|
||||
&.play-btn {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: rgba(255,255,255,0.2);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
&:hover { background: rgba(255,255,255,0.3); }
|
||||
}
|
||||
}
|
||||
|
||||
/* 右侧图标 */
|
||||
.extra-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
|
||||
.time-display {
|
||||
margin-right: auto;
|
||||
padding-right: 20px; // 与图标保持距离
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
cursor: pointer;
|
||||
opacity: 0.8;
|
||||
transition: opacity 0.2s;
|
||||
&:hover { opacity: 1; }
|
||||
&.active { color: #ffeb3b; opacity: 1; }
|
||||
}
|
||||
|
||||
.volume-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 底部微型进度条 */
|
||||
.bottom-progress {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
background: rgba(255,255,255,0.1);
|
||||
z-index: 3;
|
||||
cursor: pointer;
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: rgba(255,255,255,0.6);
|
||||
transition: width 0.1s linear;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
height: 6px;
|
||||
.progress-fill { background: #fff; }
|
||||
}
|
||||
}
|
||||
/* 顶部进度条 */
|
||||
.top-progress {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
z-index: 4;
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
transition: width 0.2s ease;
|
||||
background-color: white; // fallback
|
||||
}
|
||||
}
|
||||
.time-display {
|
||||
font-family: 'SF Mono', 'Roboto Mono', Menlo, monospace;
|
||||
font-size: 12.5px;
|
||||
font-weight: 500;
|
||||
opacity: 0.9;
|
||||
letter-spacing: 0.5px;
|
||||
min-width: 100px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* 覆盖 Element Slider 样式以适配深色背景 */
|
||||
:deep(.custom-slider) {
|
||||
--el-slider-main-bg-color: rgba(255,255,255,0.8);
|
||||
--el-slider-runway-bg-color: rgba(255,255,255,0.2);
|
||||
.el-slider__bar { background-color: white; }
|
||||
.el-slider__button { border-color: white; width: 12px; height: 12px; }
|
||||
}
|
||||
</style>
|
||||
361
src/components/layout/SettingsOverlay.vue
Normal file
361
src/components/layout/SettingsOverlay.vue
Normal file
@@ -0,0 +1,361 @@
|
||||
<template>
|
||||
<!-- 遮罩层独立动画 -->
|
||||
<transition name="fade-quick">
|
||||
<div v-if="store.showSettings" class="backdrop" @click="store.toggleSettings"></div>
|
||||
</transition>
|
||||
|
||||
<!-- 弹窗动画 -->
|
||||
<transition name="fade-slide">
|
||||
<div v-if="store.showSettings" class="settings-overlay">
|
||||
<div class="settings-modal">
|
||||
<aside class="settings-sidebar">
|
||||
<div class="title">设置</div>
|
||||
<ul class="nav-list">
|
||||
<li v-for="item in menuItems" :key="item.id"
|
||||
:class="{ active: activeTab === item.id }"
|
||||
@click="activeTab = item.id">
|
||||
{{ item.name }}
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
|
||||
<main class="settings-content">
|
||||
<button class="close-btn" @click="store.toggleSettings">
|
||||
<Icon icon="lucide:x" width="24" />
|
||||
</button>
|
||||
|
||||
<transition name="fade" mode="out-in">
|
||||
<!-- 通用设置 -->
|
||||
<div v-if="activeTab === 'general'" class="section-content" key="general">
|
||||
<h2>{{ menuItems.find(i => i.id === activeTab)?.name }}</h2>
|
||||
<div class="dummy-option">
|
||||
<span>深色模式</span>
|
||||
<el-switch v-model="store.darkMode" @change="logSetting('darkMode', store.darkMode)" />
|
||||
</div>
|
||||
<div class="dummy-option">
|
||||
<span>自动播放</span>
|
||||
<el-switch v-model="settings.autoPlay" @change="logSetting('autoPlay', settings.autoPlay)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 播放与音质 -->
|
||||
<div v-else-if="activeTab === 'audio'" class="section-content" key="audio">
|
||||
<h2>{{ menuItems.find(i => i.id === activeTab)?.name }}</h2>
|
||||
<div class="dummy-option">
|
||||
<span>启用高音质 (Hi-Res)</span>
|
||||
<el-switch v-model="settings.hiResAudio" @change="logSetting('hiResAudio', settings.hiResAudio)" />
|
||||
</div>
|
||||
<div class="dummy-option">
|
||||
<span>开启桌面歌词</span>
|
||||
<el-switch v-model="settings.desktopLyrics" @change="logSetting('desktopLyrics', settings.desktopLyrics)" />
|
||||
</div>
|
||||
<div class="dummy-option">
|
||||
<span>音量增强</span>
|
||||
<el-switch v-model="settings.volumeBoost" @change="logSetting('volumeBoost', settings.volumeBoost)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 快捷键 -->
|
||||
<div v-else-if="activeTab === 'shortcut'" class="section-content" key="shortcut">
|
||||
<h2>{{ menuItems.find(i => i.id === activeTab)?.name }}</h2>
|
||||
<div class="dummy-option">
|
||||
<span>启用全局快捷键</span>
|
||||
<el-switch v-model="settings.globalShortcuts" @change="logSetting('globalShortcuts', settings.globalShortcuts)" />
|
||||
</div>
|
||||
<div class="dummy-option">
|
||||
<span>媒体键控制</span>
|
||||
<el-switch v-model="settings.mediaKeys" @change="logSetting('mediaKeys', settings.mediaKeys)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 下载管理 -->
|
||||
<div v-else-if="activeTab === 'download'" class="section-content" key="download">
|
||||
<h2>{{ menuItems.find(i => i.id === activeTab)?.name }}</h2>
|
||||
<div class="dummy-option">
|
||||
<span>下载完成后通知</span>
|
||||
<el-switch v-model="settings.downloadNotify" @change="logSetting('downloadNotify', settings.downloadNotify)" />
|
||||
</div>
|
||||
<div class="dummy-option">
|
||||
<span>高质量下载</span>
|
||||
<el-switch v-model="settings.hqDownload" @change="logSetting('hqDownload', settings.hqDownload)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 关于页面 -->
|
||||
<div v-else class="about-section" key="about">
|
||||
<div class="logo-animate">M</div>
|
||||
<h3>Muse Player</h3>
|
||||
<p class="version">Version 1.0.0 Beta</p>
|
||||
<div class="desc">
|
||||
Designed for music lovers.<br>
|
||||
Crafted with Vue 3 & Electron.
|
||||
</div>
|
||||
<div class="links">
|
||||
<a href="#">GitHub</a>
|
||||
<a href="#">Website</a>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from 'vue';
|
||||
import { usePlayerStore } from '../../stores/playerStore';
|
||||
import { Icon } from '@iconify/vue';
|
||||
|
||||
const store = usePlayerStore();
|
||||
const activeTab = ref('general');
|
||||
|
||||
// 其他设置的本地状态管理
|
||||
const settings = reactive({
|
||||
autoPlay: true,
|
||||
hiResAudio: true,
|
||||
desktopLyrics: false,
|
||||
volumeBoost: false,
|
||||
globalShortcuts: true,
|
||||
mediaKeys: true,
|
||||
downloadNotify: true,
|
||||
hqDownload: true
|
||||
});
|
||||
|
||||
// 切换时打印日志
|
||||
const logSetting = (key: string, value: boolean) => {
|
||||
console.log(`[设置变更] ${key}: ${value ? '开启' : '关闭'}`);
|
||||
// 这里可以添加实际保存设置的逻辑
|
||||
};
|
||||
|
||||
const menuItems = [
|
||||
{ id: 'general', name: '通用' },
|
||||
{ id: 'audio', name: '播放与音质' },
|
||||
{ id: 'shortcut', name: '快捷键' },
|
||||
{ id: 'download', name: '下载管理' },
|
||||
{ id: 'about', name: '关于' },
|
||||
];
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
/* 遮罩层 - 快速淡入淡出 */
|
||||
.fade-quick-enter-active,
|
||||
.fade-quick-leave-active {
|
||||
transition: opacity 0.1s ease;
|
||||
}
|
||||
|
||||
.fade-quick-enter-from,
|
||||
.fade-quick-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* 遮罩层样式 */
|
||||
.backdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: 998;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
/* 弹窗容器 */
|
||||
.settings-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: 999;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
pointer-events: none; /* 防止点击穿透 */
|
||||
|
||||
.settings-modal {
|
||||
width: 70vw;
|
||||
height: 70vh;
|
||||
max-width: 900px;
|
||||
background: var(--settings-modal-bg);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
pointer-events: auto; /* 恢复弹窗内的点击事件 */
|
||||
}
|
||||
|
||||
.settings-sidebar {
|
||||
width: 200px;
|
||||
background: var(--settings-sidebar-bg);
|
||||
padding: 30px 20px;
|
||||
border-right: 1px solid var(--settings-sidebar-border);
|
||||
|
||||
.title {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 30px;
|
||||
padding-left: 10px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.nav-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 8px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
color: var(--text-secondary);
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: var(--nav-item-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: var(--nav-item-active);
|
||||
color: var(--nav-item-active-text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
flex: 1;
|
||||
padding: 40px;
|
||||
position: relative;
|
||||
overflow-y: auto;
|
||||
background: var(--settings-modal-bg);
|
||||
|
||||
.close-btn {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: var(--close-btn-color);
|
||||
|
||||
&:hover {
|
||||
color: var(--close-btn-hover);
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 30px;
|
||||
font-size: 20px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.dummy-option {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 0;
|
||||
border-bottom: 1px solid var(--dummy-option-border);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
span {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 关于页面样式 */
|
||||
.about-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
|
||||
.logo-animate {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background: var(--logo-placeholder-bg);
|
||||
border-radius: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 40px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20px;
|
||||
animation: float 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.version {
|
||||
color: var(--text-tertiary);
|
||||
margin: 10px 0 30px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.desc {
|
||||
line-height: 1.6;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.links {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
|
||||
a {
|
||||
color: var(--text-primary);
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
border-bottom: 1px solid transparent;
|
||||
transition: border 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 弹窗进入动画 - 遮罩已经独立 */
|
||||
.fade-slide-enter-active,
|
||||
.fade-slide-leave-active {
|
||||
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
.fade-slide-enter-from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95) translateY(10px);
|
||||
}
|
||||
|
||||
.fade-slide-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(0.95) translateY(-10px);
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% {
|
||||
transform: translateY(0) rotate(0);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(-10px) rotate(5deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
98
src/components/layout/SideBar.vue
Normal file
98
src/components/layout/SideBar.vue
Normal file
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<aside class="sidebar">
|
||||
<div class="brand">
|
||||
<div class="logo-placeholder">M</div>
|
||||
<span class="app-name">Muse Player</span>
|
||||
</div>
|
||||
|
||||
<nav class="menu">
|
||||
<router-link to="/" class="menu-item" active-class="active">
|
||||
<Icon icon="lucide:home" width="20" />
|
||||
<span>主页</span>
|
||||
</router-link>
|
||||
<router-link to="/local" class="menu-item" active-class="active">
|
||||
<Icon icon="lucide:hard-drive" width="20" />
|
||||
<span>本地音乐</span>
|
||||
</router-link>
|
||||
<router-link to="/playlist" class="menu-item" active-class="active">
|
||||
<Icon icon="lucide:list-music" width="20" />
|
||||
<span>我的歌单</span>
|
||||
</router-link>
|
||||
</nav>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Icon } from '@iconify/vue';
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.sidebar {
|
||||
width: 220px;
|
||||
height: 100%;
|
||||
background-color: var(--sidebar-bg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px 12px;
|
||||
border-right: 1px solid var(--sidebar-border);
|
||||
// 针对 Electron 的拖拽区域避免
|
||||
-webkit-app-region: drag;
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 40px;
|
||||
padding-left: 12px;
|
||||
|
||||
.logo-placeholder {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: linear-gradient(135deg, #ff6b6b, #556270);
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.app-name {
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.menu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
-webkit-app-region: no-drag; // 菜单项可点击,不可拖拽
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 16px;
|
||||
border-radius: 10px;
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 14px;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--nav-item-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
88
src/components/layout/WindowControls.vue
Normal file
88
src/components/layout/WindowControls.vue
Normal file
@@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<div class="window-controls">
|
||||
<div class="settings-btn" @click="store.toggleSettings">
|
||||
<Icon icon="lucide:settings" width="22" />
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<div class="traffic-lights">
|
||||
<div class="light minimize" @click="handleMinimize"></div>
|
||||
<div class="light maximize" @click="handleMaximize"></div>
|
||||
<div class="light close" @click="handleClose"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { usePlayerStore } from '../../stores/playerStore'
|
||||
|
||||
const store = usePlayerStore()
|
||||
|
||||
// ✅ 调用主进程暴露的 API
|
||||
const handleClose = () => {
|
||||
window.electronAPI.closeWindow()
|
||||
}
|
||||
|
||||
const handleMinimize = () => {
|
||||
window.electronAPI.minimizeWindow()
|
||||
}
|
||||
|
||||
const handleMaximize = async () => {
|
||||
// 可选:切换图标(比如最大化/还原)
|
||||
await window.electronAPI.maximizeWindow()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.window-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 24px;
|
||||
height: 100%;
|
||||
gap: 24px; // 设置和红绿灯之间的距离
|
||||
-webkit-app-region: no-drag;
|
||||
|
||||
.settings-btn {
|
||||
cursor: pointer;
|
||||
color: var(--text-tertiary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--text-primary);
|
||||
transform: rotate(45deg); // 简单的交互动画
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
width: 1px;
|
||||
height: 18px;
|
||||
background-color: var(--border-color);
|
||||
}
|
||||
|
||||
.traffic-lights {
|
||||
display: flex;
|
||||
gap: 10px; // 按钮间距拉大
|
||||
|
||||
.light {
|
||||
width: 15px; // 放大按钮 (原12px)
|
||||
height: 15px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
transition: transform 0.1s, opacity 0.2s;
|
||||
|
||||
&:hover { opacity: 0.8; }
|
||||
&:active { transform: scale(0.9); }
|
||||
|
||||
/* 调整了顺序: 最小化-最大化-关闭,符合一般习惯,也可按需调整 */
|
||||
&.close { background-color: #ff5f56; }
|
||||
&.minimize { background-color: #ffbd2e; }
|
||||
&.maximize { background-color: #27c93f; }
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
25
src/main.ts
Normal file
25
src/main.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
// src/main.ts
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
|
||||
import App from './App.vue'
|
||||
import HomeView from './views/HomeView.vue'
|
||||
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
|
||||
const pinia = createPinia()
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes: [
|
||||
{ path: '/', component: HomeView },
|
||||
// 可扩展其他路由
|
||||
]
|
||||
})
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(pinia)
|
||||
app.use(router)
|
||||
app.use(ElementPlus)
|
||||
app.mount('#app')
|
||||
168
src/stores/playerStore.ts
Normal file
168
src/stores/playerStore.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref, computed, watch } from 'vue';
|
||||
|
||||
export const usePlayerStore = defineStore('player', () => {
|
||||
const currentSong = ref({
|
||||
id: 1,
|
||||
title: "Example Track",
|
||||
artist: "AI Artist",
|
||||
cover: "http://p2.music.126.net/W3VMsSEjTdvhz7h3a0oxTg==/17782401556325576.jpg?param=130y130",
|
||||
url: "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3",
|
||||
duration: 0
|
||||
});
|
||||
|
||||
const isPlaying = ref(false);
|
||||
const currentTime = ref(0);
|
||||
const volume = ref(80);
|
||||
const showPlaylist = ref(false);
|
||||
const showSettings = ref(false);
|
||||
const darkMode = ref(false);
|
||||
const themeColors = ref({ primary: '#6366f1', secondary: '#a855f7' });
|
||||
|
||||
// 新增:用于顶部进度条的最深色
|
||||
const progressColor = ref('#ffffff');
|
||||
|
||||
const audio = new Audio(currentSong.value.url);
|
||||
audio.volume = volume.value / 100;
|
||||
|
||||
// 颜色提取函数(轻量版,不依赖外部库)
|
||||
function extractColors() {
|
||||
const img = new Image();
|
||||
img.crossOrigin = 'Anonymous';
|
||||
img.src = currentSong.value.cover + '?t=' + Date.now(); // 避免缓存
|
||||
|
||||
img.onload = () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
// 缩小图片以提高性能
|
||||
const scale = 0.1;
|
||||
canvas.width = Math.floor(img.width * scale);
|
||||
canvas.height = Math.floor(img.height * scale);
|
||||
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||
|
||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
|
||||
// 收集所有像素颜色
|
||||
const colors: { r: number; g: number; b: number; brightness: number }[] = [];
|
||||
|
||||
for (let i = 0; i < imageData.length; i += 4) {
|
||||
const r = imageData[i];
|
||||
const g = imageData[i + 1];
|
||||
const b = imageData[i + 2];
|
||||
const brightness = (r + g + b) / 3;
|
||||
|
||||
colors.push({ r, g, b, brightness });
|
||||
}
|
||||
|
||||
// 计算颜色频率(简单实现)
|
||||
const colorFrequency: Record<string, number> = {};
|
||||
colors.forEach(color => {
|
||||
// 将颜色量化为 16 位色,减少颜色数量
|
||||
const key = `${Math.floor(color.r / 16)}${Math.floor(color.g / 16)}${Math.floor(color.b / 16)}`;
|
||||
colorFrequency[key] = (colorFrequency[key] || 0) + 1;
|
||||
});
|
||||
|
||||
// 转换回 RGB 并按频率排序
|
||||
const sortedColors = Object.entries(colorFrequency)
|
||||
.map(([key, count]) => {
|
||||
const r = parseInt(key[0], 16) * 16;
|
||||
const g = parseInt(key[1], 16) * 16;
|
||||
const b = parseInt(key[2], 16) * 16;
|
||||
return { r, g, b, count };
|
||||
})
|
||||
.sort((a, b) => b.count - a.count);
|
||||
|
||||
// 提取主色调和辅助色调
|
||||
if (sortedColors.length > 0) {
|
||||
// 主色调:频率最高的颜色
|
||||
const primary = sortedColors[0];
|
||||
const primaryColor = `rgb(${primary.r}, ${primary.g}, ${primary.b})`;
|
||||
|
||||
// 辅助色调:选择与主色调亮度差异较大的颜色
|
||||
let secondary = sortedColors[1] || sortedColors[0];
|
||||
let maxBrightnessDiff = Math.abs(primary.brightness - secondary.brightness);
|
||||
|
||||
// 在频率较高的颜色中寻找最合适的辅助色
|
||||
for (let i = 1; i < Math.min(10, sortedColors.length); i++) {
|
||||
const currentBrightness = (sortedColors[i].r + sortedColors[i].g + sortedColors[i].b) / 3;
|
||||
const brightnessDiff = Math.abs(primary.brightness - currentBrightness);
|
||||
|
||||
if (brightnessDiff > maxBrightnessDiff) {
|
||||
maxBrightnessDiff = brightnessDiff;
|
||||
secondary = sortedColors[i];
|
||||
}
|
||||
}
|
||||
|
||||
const secondaryColor = `rgb(${secondary.r}, ${secondary.g}, ${secondary.b})`;
|
||||
|
||||
// 更新主题颜色
|
||||
themeColors.value = {
|
||||
primary: primaryColor,
|
||||
secondary: secondaryColor
|
||||
};
|
||||
|
||||
// 提取进度条颜色(使用主色调的深色版本)
|
||||
const darkPrimary = {
|
||||
r: Math.floor(primary.r * 0.7),
|
||||
g: Math.floor(primary.g * 0.7),
|
||||
b: Math.floor(primary.b * 0.7)
|
||||
};
|
||||
progressColor.value = `rgb(${darkPrimary.r}, ${darkPrimary.g}, ${darkPrimary.b})`;
|
||||
} else {
|
||||
// 默认颜色
|
||||
themeColors.value = { primary: '#6366f1', secondary: '#a855f7' };
|
||||
progressColor.value = '#ffffff';
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 监听歌曲切换时重新提取颜色
|
||||
watch(() => currentSong.value.cover, () => {
|
||||
extractColors();
|
||||
console.log("!!")
|
||||
}, { immediate: true });
|
||||
|
||||
audio.addEventListener('loadedmetadata', () => {
|
||||
currentSong.value.duration = audio.duration;
|
||||
});
|
||||
|
||||
audio.addEventListener('timeupdate', () => {
|
||||
currentTime.value = audio.currentTime;
|
||||
});
|
||||
|
||||
audio.addEventListener('ended', () => {
|
||||
isPlaying.value = false;
|
||||
currentTime.value = 0;
|
||||
});
|
||||
|
||||
function togglePlay() {
|
||||
if (isPlaying.value) {
|
||||
audio.pause();
|
||||
} else {
|
||||
audio.play().catch(e => console.warn("播放失败,需用户交互:", e));
|
||||
}
|
||||
isPlaying.value = !isPlaying.value;
|
||||
}
|
||||
|
||||
function seek(time: number) {
|
||||
audio.currentTime = time;
|
||||
}
|
||||
function toggleSettings() { console.log("!!");showSettings.value = !showSettings.value; }
|
||||
|
||||
watch(volume, (newVol) => {
|
||||
audio.volume = newVol / 100;
|
||||
});
|
||||
|
||||
const progressPercentage = computed(() =>
|
||||
currentSong.value.duration ? (currentTime.value / currentSong.value.duration) * 100 : 0
|
||||
);
|
||||
|
||||
return {
|
||||
currentSong, isPlaying, currentTime, volume, showPlaylist,
|
||||
themeColors, progressPercentage, progressColor,
|
||||
togglePlay, seek, extractColors,
|
||||
showSettings, toggleSettings,
|
||||
darkMode
|
||||
};
|
||||
});
|
||||
378
src/views/HomeView.vue
Normal file
378
src/views/HomeView.vue
Normal file
@@ -0,0 +1,378 @@
|
||||
<template>
|
||||
<div class="home-view">
|
||||
<section class="section-container">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">推荐歌单</h2>
|
||||
<div class="more">查看全部 <Icon icon="lucide:chevron-right" /></div>
|
||||
</div>
|
||||
|
||||
<div class="playlist-grid">
|
||||
<div
|
||||
v-for="item in playlistData"
|
||||
:key="item.id"
|
||||
class="playlist-card"
|
||||
@click="handlePlayListClick(item)"
|
||||
>
|
||||
<div class="cover-wrapper">
|
||||
<img :src="item.img" :alt="item.name" loading="lazy" />
|
||||
|
||||
<div class="play-count">
|
||||
<Icon icon="lucide:play" width="12" />
|
||||
<span>{{ item.play_count }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="playlist-info">
|
||||
<div class="playlist-name">{{ item.name }}</div>
|
||||
<div class="playlist-author">{{ item.author }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { Icon } from '@iconify/vue';
|
||||
|
||||
// 注入你提供的 JSON 数据
|
||||
const playlistData = ref([
|
||||
|
||||
{
|
||||
|
||||
"play_count": "4.7万",
|
||||
|
||||
"id": "17387580241",
|
||||
|
||||
"author": "张大佛爷张大佛爷",
|
||||
|
||||
"name": "巴西Funk:来首精神氮泵助力燃脂",
|
||||
|
||||
"time": "2025-11-02",
|
||||
|
||||
"img": "http://p2.music.126.net/YkpQqGGlHR4DD8BjGTQe8Q==/109951172228935288.jpg",
|
||||
|
||||
"total": 30,
|
||||
|
||||
"desc": "巴西放克(Funk brasileiro),通常在巴西被称为\"Funk\",是一种受嘻哈音乐(Hip Hop)影响的音乐风 格,起源于里约热内卢的贫民区(也称为贫民窟)。里约放克(Funk carioca)最初源自电音放克(Electro)和迈阿密贝 斯(Miami Bass),之后逐渐发展出独特的风格,并成为巴西低收入青少年中最受欢迎的音乐类型之一。\n\n听说巴西Funk和健身很搭噢!",
|
||||
|
||||
"source": "wy"
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
"play_count": "3.6万",
|
||||
|
||||
"id": "14435495551",
|
||||
|
||||
"author": "凌晨一点的莱茵猫",
|
||||
|
||||
"name": "ACG治愈 | 回忆复刻 重温动漫里的温情时刻",
|
||||
|
||||
"time": "2025-10-18",
|
||||
|
||||
"img": "http://p2.music.126.net/uPWibcRbc7u3vDD44gXA0A==/109951172205320822.jpg",
|
||||
|
||||
"total": 99,
|
||||
|
||||
"desc": "不是所有治愈都需要大段对白\n一段旋律就够了\n\n可以是主角低谷时的背景音\n也可以是圆满结局的收尾曲\n\n每一段都裹着当时的情绪\n现在听依旧能暖到心里最软的地方",
|
||||
|
||||
"source": "wy"
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
"play_count": "3万",
|
||||
|
||||
"id": "17438630520",
|
||||
|
||||
"author": "露露在发呆",
|
||||
|
||||
"name": "跑步 •鬼灭之刃超燃和风主题歌",
|
||||
|
||||
"time": "2025-11-15",
|
||||
|
||||
"img": "http://p2.music.126.net/DuKyLyYhKd2YhWsRYkd9DQ==/109951172287476927.jpg",
|
||||
|
||||
"total": 26,
|
||||
|
||||
"desc": "鬼灭之刃即将迎来无限城终极大决战,精选历代TV番、剧场版主题曲与超人气配乐插曲,与动画同样出彩的是其独特美妙的日式和风电子流行曲风,给你今日份的运动计划注入来自鬼灭之刃的二次元能量吧,适配有氧、无氧运动 全适配,高能KPOP祝你今天也能量满满燃爆卡路里!",
|
||||
|
||||
"source": "wy"
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
"play_count": "1.7万",
|
||||
|
||||
"id": "17430413151",
|
||||
|
||||
"author": "心语馆",
|
||||
|
||||
"name": "【高质量轻音】享受宁静 沉浸自然",
|
||||
|
||||
"time": "2025-11-13",
|
||||
|
||||
"img": "http://p2.music.126.net/El8lldCevbObAYAcBz0kvg==/109951172275960287.jpg",
|
||||
|
||||
"total": 72,
|
||||
|
||||
"desc": "当城市喧嚣渐远\n让音符化作自然的信使\n\n钢琴的澄澈如林间晨露\n弦乐的温柔似晚风拂叶\n\n虫鸣与流水声悄然交织\n每一段旋律都在勾勒远山\n\n晴空与旷野的轮廓\n无需刻意追寻\n\n只需闭上眼\n便能在旋律里触摸自然 的呼吸\n\n让身心沉潜于这份不被打扰的宁静\n重拾内心的松弛与澄澈……",
|
||||
|
||||
"source": "wy"
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
"play_count": "4681",
|
||||
|
||||
"id": "17440686473",
|
||||
|
||||
"author": "與鯨島",
|
||||
|
||||
"name": "刘宇|我生如刀锋 自当斩荆棘",
|
||||
|
||||
"time": "2025-11-15",
|
||||
|
||||
"img": "http://p2.music.126.net/l6q13AT-n47NICqDGf90lw==/109951172291475298.jpg",
|
||||
|
||||
"total": 50,
|
||||
|
||||
"desc": "他说:\n“我接受来自四面八方的欢呼鼓舞,也接受不明缘由的流言与我接受来自四面八方的欢呼鼓舞,也 接受不明缘由的流言与误解。我低谷过,但没人能熄灭我跳动的火焰。眼前不变的是光明和希望,再尖锐的噪音也无法打 断已经响起的旋律,仍在舞蹈,就是我的态度。”\n\n“我很早之前就说过,恶评它其实不会伤害到我,哪怕我说的一句话 ,它没有错误,有些人都可以在TA的立场上找出这句话的错误,我觉得这个不太需要去考虑的,因为你做什么事情问心无 愧最重要。”\n\n“我觉得你们也在发光,是因为你们的光汇聚在一起,我才能这么明亮。”\n\n“天赋固然重要,但我想要 才更重要。”\n\n“愿你我皆被温柔以待,身处泥泞也嗅得大地芳香。”\n\n“我想告诉你们的是,是你们的光唤醒了我心中 的那颗种子,我知道有一天,这颗种子,可以成为为你们遮挡伤害的那棵大树,请相信我。”\n\n“谢谢您曾对我说,要去 做那个扇风的人,要让全世界知道这把扇子扇出来的风,叫中国风。”\n\n“国风推广这条路,其实我的初心一直不变,依 旧在前行,也理解我作为公众人物应该负担起更多的责误解。我低谷过,但没人能熄灭我跳动的火焰。眼前不变的是光明 和希望,再尖锐的噪音也无法打断已经响起的旋律,仍在舞蹈,就是我的态度。”\n\n“我很早之前就说过,恶评它其实不 会伤害到我,哪怕我说的一句话,它没有错误,有些人都可以在TA的立场上找出这句话的错误,我觉得这个不太需要去考 虑的,因为你做什么事情问心无愧最重要。”\n\n“我觉得你们也在发光,是因为你们的光汇聚在一起,我才能这么明亮。”\n\n“天赋固然重要,但我想要才更重要。”\n\n“愿你我皆被温柔以待,身处泥泞也嗅得大地芳香。”\n\n“我想告诉你们的是,是你们的光唤醒了我心中的那颗种子,我知道有一天,这颗种子,可以成为为你们遮挡伤害的那棵大树,请相信我。”\n\n“谢谢您曾对我说,要去做那个扇风的人,要让全世界知道这把扇子扇出来的风,叫中国风。”\n\n“国风推广这条路,其实我的初心一直不变,依旧在前行,也理解我作为公众人物应该负担起更多的责任、传播的重任。希望未来不负期望, 能号召更多的年轻的朋友关注和热爱国风、传统文化。”\n\n“把冠军定在道路上,而不是目标上。”\n\n“你们带着特别热 情,特别想要跟你传达他们的爱的时候,你就觉得,这点累算什么。”\n\n“太在意恶评,是对好评的不负责。”\n\n",
|
||||
|
||||
"source": "wy"
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
"play_count": "2.1万",
|
||||
|
||||
"id": "17412466554",
|
||||
|
||||
"author": "走马川行北海边",
|
||||
|
||||
"name": "网上很火韩语歌曲|宿命感拉满氛围神曲",
|
||||
|
||||
"time": "2025-11-08",
|
||||
|
||||
"img": "http://p2.music.126.net/Rf9Nx1eefoGbqGhqq84SBw==/109951172254139596.jpg",
|
||||
|
||||
"total": 72,
|
||||
|
||||
"desc": "前奏一响,便是跨越时空的宿命羁绊。这些火爆全网的韩语神曲,藏着韩剧里初雪拔剑的悸动、跨越百年的深情,也藏着爱而不得的怅惘与命中注定的奔赴。空灵声线交织着缱绻旋律,氛围感直接拉满,每一段旋律都像在诉说: 有些人,遇见即是宿命,哪怕兜兜转转,也终会被命运牵引。戴上耳机,沉浸式坠入这场关于宿命的浪漫与遗憾。",
|
||||
|
||||
"source": "wy"
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
"play_count": "1.2万",
|
||||
|
||||
"id": "17426483259",
|
||||
|
||||
"author": "心语馆",
|
||||
|
||||
"name": "【欧美旋律控】前奏封神 耳熟能详欧美神曲",
|
||||
|
||||
"time": "2025-11-12",
|
||||
|
||||
"img": "http://p2.music.126.net/4EiMzo9LAGbNjwF79ecuZg==/109951172271263928.jpg",
|
||||
|
||||
"total": 67,
|
||||
|
||||
"desc": "这份歌单,是资深乐迷私藏的欧美旋律宝库。从格莱美获奖金曲,到小众音乐人的惊艳之作,每一首都经过时间与口碑的双重检验。\n\n有Adele用灵魂嗓音唱出的《Rolling in the Deep》,那强烈鼓点和凄美钢琴交织,让失恋 的痛楚与坚强直击人心;也有Coldplay用《Viva la Vida》带来的摇滚盛宴,壮丽弦乐搭配激昂节奏,展现帝国兴衰。这 里既有流行、摇滚、民谣等主流风格,也不乏R&B、电子乐等小众类型,满足不同音乐偏好。\n\n无论你是在午后闲暇、深夜独处,还是在通勤路上,这些旋律都能为你带来极致的听觉享受,让你沉浸在欧美音乐的独特魅力中,一听就上瘾,循 环到停不下来 。",
|
||||
|
||||
"source": "wy"
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
"play_count": "4198",
|
||||
|
||||
"id": "17514849470",
|
||||
|
||||
"author": "心语馆",
|
||||
|
||||
"name": "【高质量轻音】清新宁静 明亮自然",
|
||||
|
||||
"time": "2025-12-04",
|
||||
|
||||
"img": "http://p2.music.126.net/v0jiaItY2fD3LuQVrdt_Cg==/109951172384635472.jpg",
|
||||
|
||||
"total": 71,
|
||||
|
||||
"desc": "蝉鸣躲进树荫,落叶在石阶上打了个转。木吉他的弦音裹着松针的气息,手鼓轻得像松鼠踩过腐叶。不必追赶时间,只需闭眼,让音符替你接住林间漏下的每一缕暖阳。",
|
||||
|
||||
"source": "wy"
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
"play_count": "1万",
|
||||
|
||||
"id": "14407617761",
|
||||
|
||||
"author": "露露在发呆",
|
||||
|
||||
"name": "华语R&B:像一只蝴蝶飞过废墟",
|
||||
|
||||
"time": "2025-10-11",
|
||||
|
||||
"img": "http://p2.music.126.net/nYl-Rts9LlhdrGRIx5St-g==/109951172136819703.jpg",
|
||||
|
||||
"total": 58,
|
||||
|
||||
"desc": "我瞒着所有人装作迈过了很多坎,故意显得很开心的样子,事实上只有我知道,阴影就是阴影,有些坎我永远也迈不过去,有些事一时半会释怀,我承认你很好,有些事跟我喜欢你没关系,有些事没在一个频道上,更没办法将两 件事放一起衡量。",
|
||||
|
||||
"source": "wy"
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
"play_count": "8188",
|
||||
|
||||
"id": "17511869819",
|
||||
|
||||
"author": "有只黑猫叫灰心",
|
||||
|
||||
"name": "「韩语宿命感」首尔晚风里的心跳",
|
||||
|
||||
"time": "2025-12-04",
|
||||
|
||||
"img": "http://p2.music.126.net/wXRLNUreaGRVcU7CYe20KA==/109951172381558389.jpg",
|
||||
|
||||
"total": 50,
|
||||
|
||||
"desc": "ᥫᩣ当首尔的晚风裹着便利店的暖光掠过耳机线,韩语歌词里那些轻颤的音节,正藏着和你同频的心跳共振。这是专属于宿命感的BGM——每一段旋律都像被按下慢放的初遇镜头:是走廊转角撞进眼底的视线,是咖啡杯沿碰在一起时的轻响,是晚风里没说出口的「恰好我也喜欢你」。耳机里的每一句,都是藏在旋律里的、只属于你的心动序章✨",
|
||||
|
||||
"source": "wy"
|
||||
|
||||
}
|
||||
|
||||
]);
|
||||
|
||||
const handlePlayListClick = (item: any) => {
|
||||
console.log("跳转到歌单详情:", item.id);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.home-view {
|
||||
padding-bottom: 40px;
|
||||
|
||||
.section-container {
|
||||
margin-bottom: 40px;
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.section-title {
|
||||
font-size: 24px;
|
||||
font-weight: 800;
|
||||
color: #1a1a1a;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.more {
|
||||
font-size: 13px;
|
||||
color: #888;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
&:hover { color: #ff6b6b; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.playlist-grid {
|
||||
display: grid;
|
||||
// 使用 auto-fill 实现响应式列数
|
||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||
gap: 24px;
|
||||
|
||||
.playlist-card {
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
|
||||
&:hover {
|
||||
.cover-wrapper {
|
||||
transform: translateY(-8px);
|
||||
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.15);
|
||||
|
||||
img { transform: scale(1.05); }
|
||||
}
|
||||
.playlist-name { color: #ff6b6b; }
|
||||
}
|
||||
|
||||
.cover-wrapper {
|
||||
position: relative;
|
||||
aspect-ratio: 1;
|
||||
border-radius: 14px;
|
||||
overflow: hidden;
|
||||
background: #f0f0f0;
|
||||
margin-bottom: 12px;
|
||||
transition: all 0.4s ease;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.6s ease;
|
||||
}
|
||||
|
||||
.play-count {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
backdrop-filter: blur(8px);
|
||||
color: white;
|
||||
padding: 4px 10px;
|
||||
border-radius: 20px;
|
||||
font-size: 11px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
|
||||
.playlist-info {
|
||||
.playlist-name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
// 两行文本截断
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.playlist-author {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user