mirror of
https://github.com/lqtmcstudio/QZMusic_PC.git
synced 2026-06-20 23:35:06 +08:00
feat: 加入导航按钮,支持前进回退,加入歌单详情界面
This commit is contained in:
89
package-lock.json
generated
89
package-lock.json
generated
@@ -22,6 +22,7 @@
|
||||
"jss": "^10.10.0",
|
||||
"jss-preset-default": "^10.10.0",
|
||||
"pinia": "^3.0.4",
|
||||
"tdesign-vue-next": "^1.17.7",
|
||||
"vue": "^3.4.21",
|
||||
"vue-router": "^4.6.4"
|
||||
},
|
||||
@@ -2026,6 +2027,24 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/sortablejs": {
|
||||
"version": "1.15.9",
|
||||
"resolved": "https://registry.npmmirror.com/@types/sortablejs/-/sortablejs-1.15.9.tgz",
|
||||
"integrity": "sha512-7HP+rZGE2p886PKV9c9OJzLBI6BBJu1O7lJGYnPyG3fS4/duUCcngkNCjsLwIMV+WMqANe3tt4irrXHSIe68OQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/tinycolor2": {
|
||||
"version": "1.4.6",
|
||||
"resolved": "https://registry.npmmirror.com/@types/tinycolor2/-/tinycolor2-1.4.6.tgz",
|
||||
"integrity": "sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/validator": {
|
||||
"version": "13.15.10",
|
||||
"resolved": "https://registry.npmmirror.com/@types/validator/-/validator-13.15.10.tgz",
|
||||
"integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/verror": {
|
||||
"version": "1.10.11",
|
||||
"resolved": "https://registry.npmmirror.com/@types/verror/-/verror-1.10.11.tgz",
|
||||
@@ -6568,6 +6587,12 @@
|
||||
"npm": ">= 3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sortablejs": {
|
||||
"version": "1.15.6",
|
||||
"resolved": "https://registry.npmmirror.com/sortablejs/-/sortablejs-1.15.6.tgz",
|
||||
"integrity": "sha512-aNfiuwMEpfBM/CN6LY0ibyhxPfPbyFeBTYJKCvzkJ2GkUpazIt3H+QIPAMHwqQ7tMKaHz1Qj+rJJCqljnf4p3A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz",
|
||||
@@ -6800,6 +6825,55 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/tdesign-icons-vue-next": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmmirror.com/tdesign-icons-vue-next/-/tdesign-icons-vue-next-0.4.1.tgz",
|
||||
"integrity": "sha512-uDPuTLRORnGcTyVGNoentNaK4V+ZcBmhYwcY3KqDaQQ5rrPeLMxu0ZVmgOEf0JtF2QZiqAxY7vodNEiLUdoRKA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.16.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tdesign-vue-next": {
|
||||
"version": "1.17.7",
|
||||
"resolved": "https://registry.npmmirror.com/tdesign-vue-next/-/tdesign-vue-next-1.17.7.tgz",
|
||||
"integrity": "sha512-mV/9mm/nIS+tfx1oUG1IMMmTPFeZfLmP8bIVEa7S9CpVke2+Yei5i8RBXmDwF/d+OaDoKVgwUq08goSIZfRePQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.22.6",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/sortablejs": "^1.15.1",
|
||||
"@types/tinycolor2": "^1.4.3",
|
||||
"@types/validator": "^13.7.17",
|
||||
"dayjs": "^1.11.10",
|
||||
"lodash-es": "^4.17.21",
|
||||
"mitt": "^3.0.1",
|
||||
"sortablejs": "^1.15.0",
|
||||
"tdesign-icons-vue-next": "~0.4.1",
|
||||
"tinycolor2": "^1.6.0",
|
||||
"validator": "^13.15.23"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": ">=3.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tdesign-vue-next/node_modules/@popperjs/core": {
|
||||
"version": "2.11.8",
|
||||
"resolved": "https://registry.npmmirror.com/@popperjs/core/-/core-2.11.8.tgz",
|
||||
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/popperjs"
|
||||
}
|
||||
},
|
||||
"node_modules/temp-file": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmmirror.com/temp-file/-/temp-file-3.4.0.tgz",
|
||||
@@ -6855,6 +6929,12 @@
|
||||
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinycolor2": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmmirror.com/tinycolor2/-/tinycolor2-1.6.0.tgz",
|
||||
"integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tmp": {
|
||||
"version": "0.2.5",
|
||||
"resolved": "https://registry.npmmirror.com/tmp/-/tmp-0.2.5.tgz",
|
||||
@@ -6995,6 +7075,15 @@
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/validator": {
|
||||
"version": "13.15.26",
|
||||
"resolved": "https://registry.npmmirror.com/validator/-/validator-13.15.26.tgz",
|
||||
"integrity": "sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/varint": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/varint/-/varint-6.0.0.tgz",
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"jss": "^10.10.0",
|
||||
"jss-preset-default": "^10.10.0",
|
||||
"pinia": "^3.0.4",
|
||||
"tdesign-vue-next": "^1.17.7",
|
||||
"vue": "^3.4.21",
|
||||
"vue-router": "^4.6.4"
|
||||
},
|
||||
|
||||
102
src/App.vue
102
src/App.vue
@@ -5,7 +5,27 @@
|
||||
|
||||
<main class="main-content">
|
||||
<header class="header-bar">
|
||||
<div class="header-left"></div>
|
||||
<div class="header-left">
|
||||
<!-- 路由导航按钮 -->
|
||||
<div class="navigation-buttons">
|
||||
<button
|
||||
class="nav-btn"
|
||||
@click="handleBack"
|
||||
:disabled="!canGoBack"
|
||||
title="返回"
|
||||
>
|
||||
<Icon icon="lucide:chevron-left" width="18" />
|
||||
</button>
|
||||
<button
|
||||
class="nav-btn"
|
||||
@click="handleForward"
|
||||
:disabled="!canGoForward"
|
||||
title="前进"
|
||||
>
|
||||
<Icon icon="lucide:chevron-right" width="18" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<WindowControls />
|
||||
</header>
|
||||
|
||||
@@ -30,8 +50,50 @@ 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";
|
||||
import { Icon } from '@iconify/vue';
|
||||
import { ref, watch, onMounted } from 'vue';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
|
||||
const store = usePlayerStore();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
// 跟踪路由历史,判断是否可以返回和前进
|
||||
const canGoBack = ref(true); // 返回按钮始终可用,让浏览器处理
|
||||
const canGoForward = ref(false);
|
||||
|
||||
// 记录之前的路由路径
|
||||
const previousPath = ref(route.path);
|
||||
|
||||
// 监听路由变化,更新前进按钮状态
|
||||
watch(
|
||||
() => route.path,
|
||||
(newPath) => {
|
||||
// 当从歌单页返回主页时,前进按钮应该可用
|
||||
if (newPath === '/' && previousPath.value.startsWith('/playlist/')) {
|
||||
canGoForward.value = true;
|
||||
} else if (newPath.startsWith('/playlist/')) {
|
||||
// 当进入歌单页时,前进按钮不可用
|
||||
canGoForward.value = false;
|
||||
}
|
||||
// 更新之前的路径
|
||||
previousPath.value = newPath;
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 监听返回和前进事件
|
||||
const handleBack = () => {
|
||||
router.back();
|
||||
};
|
||||
|
||||
const handleForward = () => {
|
||||
if (canGoForward.value) {
|
||||
router.forward();
|
||||
// 前进后前进按钮可能不可用,具体取决于历史记录
|
||||
canGoForward.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@@ -142,6 +204,44 @@ html, body, #app {
|
||||
align-items: center;
|
||||
-webkit-app-region: drag; /* Electron 拖拽 */
|
||||
background: var(--header-bg);
|
||||
|
||||
.header-left {
|
||||
padding: 0 20px;
|
||||
-webkit-app-region: no-drag;
|
||||
|
||||
.navigation-buttons {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
|
||||
.nav-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: var(--bg-secondary);
|
||||
border-color: var(--border-light);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 路由视图滚动区 */
|
||||
|
||||
@@ -30,11 +30,11 @@
|
||||
<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)" />
|
||||
<t-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)" />
|
||||
<t-switch v-model="settings.autoPlay" @change="logSetting('autoPlay', settings.autoPlay)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -43,15 +43,15 @@
|
||||
<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)" />
|
||||
<t-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)" />
|
||||
<t-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)" />
|
||||
<t-switch v-model="settings.volumeBoost" @change="logSetting('volumeBoost', settings.volumeBoost)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -60,11 +60,11 @@
|
||||
<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)" />
|
||||
<t-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)" />
|
||||
<t-switch v-model="settings.mediaKeys" @change="logSetting('mediaKeys', settings.mediaKeys)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -73,11 +73,11 @@
|
||||
<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)" />
|
||||
<t-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)" />
|
||||
<t-switch v-model="settings.hqDownload" @change="logSetting('hqDownload', settings.hqDownload)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -6,17 +6,16 @@
|
||||
</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
|
||||
v-for="item in menuItems"
|
||||
:key="item.path"
|
||||
:to="item.path"
|
||||
class="menu-item"
|
||||
active-class="active"
|
||||
:title="item.label"
|
||||
>
|
||||
<Icon :icon="item.icon" width="20" />
|
||||
<span>{{ item.label }}</span>
|
||||
</router-link>
|
||||
</nav>
|
||||
</aside>
|
||||
@@ -24,6 +23,12 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Icon } from '@iconify/vue';
|
||||
|
||||
const menuItems = [
|
||||
{ path: '/', label: '主页', icon: 'lucide:home' },
|
||||
{ path: '/local', label: '本地音乐', icon: 'lucide:hard-drive' },
|
||||
{ path: '/playlist', label: '我的歌单', icon: 'lucide:list-music' }
|
||||
];
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -33,34 +38,52 @@ import { Icon } from '@iconify/vue';
|
||||
background-color: var(--sidebar-bg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px 12px;
|
||||
padding: 24px 16px;
|
||||
border-right: 1px solid var(--sidebar-border);
|
||||
// 针对 Electron 的拖拽区域避免
|
||||
-webkit-app-region: drag;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0 0 0 rgba(0, 0, 0, 0.05);
|
||||
|
||||
&:hover {
|
||||
box-shadow: 2px 0 12px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 40px;
|
||||
gap: 16px;
|
||||
margin-bottom: 48px;
|
||||
padding-left: 12px;
|
||||
-webkit-app-region: no-drag;
|
||||
transition: all 0.3s ease;
|
||||
opacity: 0;
|
||||
animation: fadeInDown 0.6s ease forwards;
|
||||
|
||||
.logo-placeholder {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: linear-gradient(135deg, #ff6b6b, #556270);
|
||||
border-radius: 8px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: linear-gradient(135deg, #6366f1, #8b5cf6);
|
||||
border-radius: 12px;
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
font-size: 18px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 6px 16px rgba(99, 102, 241, 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
.app-name {
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
font-size: 20px;
|
||||
color: var(--text-primary);
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,31 +91,100 @@ import { Icon } from '@iconify/vue';
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
-webkit-app-region: no-drag; // 菜单项可点击,不可拖拽
|
||||
-webkit-app-region: no-drag;
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 16px;
|
||||
border-radius: 10px;
|
||||
gap: 14px;
|
||||
padding: 12px 16px;
|
||||
border-radius: 12px;
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
animation: fadeInLeft 0.5s ease forwards;
|
||||
|
||||
&:nth-child(1) { animation-delay: 0.2s; }
|
||||
&:nth-child(2) { animation-delay: 0.3s; }
|
||||
&:nth-child(3) { animation-delay: 0.4s; }
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
background: linear-gradient(180deg, #6366f1, #8b5cf6);
|
||||
transform: scaleY(0);
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
border-radius: 0 2px 2px 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--nav-item-hover);
|
||||
color: var(--text-primary);
|
||||
transform: translateX(4px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
|
||||
&::before {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
background-color: linear-gradient(135deg, rgba(99, 102, 241, 0.1), rgba(139, 92, 246, 0.1));
|
||||
background: var(--bg-primary);
|
||||
color: #6366f1;
|
||||
font-weight: 500;
|
||||
box-shadow: 0 6px 16px rgba(99, 102, 241, 0.15);
|
||||
transform: translateX(4px);
|
||||
|
||||
&::before {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
|
||||
.iconify {
|
||||
filter: drop-shadow(0 0 4px rgba(99, 102, 241, 0.5));
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
&:hover span {
|
||||
transform: translateX(2px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 动画定义
|
||||
@keyframes fadeInDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInLeft {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,15 +5,17 @@ import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
|
||||
import App from './App.vue'
|
||||
import HomeView from './views/HomeView.vue'
|
||||
import PlaylistDetailView from './views/PlaylistDetailView.vue'
|
||||
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import TDesign from 'tdesign-vue-next'
|
||||
import 'tdesign-vue-next/es/style/index.css'
|
||||
|
||||
const pinia = createPinia()
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes: [
|
||||
{ path: '/', component: HomeView },
|
||||
{ path: '/playlist/:id', component: PlaylistDetailView },
|
||||
// 可扩展其他路由
|
||||
]
|
||||
})
|
||||
@@ -21,5 +23,5 @@ const router = createRouter({
|
||||
const app = createApp(App)
|
||||
app.use(pinia)
|
||||
app.use(router)
|
||||
app.use(ElementPlus)
|
||||
app.use(TDesign)
|
||||
app.mount('#app')
|
||||
|
||||
@@ -261,17 +261,22 @@ const playlistData = ref([
|
||||
|
||||
]);
|
||||
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const handlePlayListClick = (item: any) => {
|
||||
console.log("跳转到歌单详情:", item.id);
|
||||
router.push(`/playlist/${item.id}`);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.home-view {
|
||||
padding-bottom: 40px;
|
||||
padding: 20px 0 40px 0;
|
||||
|
||||
.section-container {
|
||||
margin-bottom: 40px;
|
||||
padding: 0 20px;
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
@@ -282,18 +287,18 @@ const handlePlayListClick = (item: any) => {
|
||||
.section-title {
|
||||
font-size: 24px;
|
||||
font-weight: 800;
|
||||
color: #1a1a1a;
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.more {
|
||||
font-size: 13px;
|
||||
color: #888;
|
||||
color: var(--text-secondary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
&:hover { color: #ff6b6b; }
|
||||
&:hover { color: #6366f1; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
568
src/views/PlaylistDetailView.vue
Normal file
568
src/views/PlaylistDetailView.vue
Normal file
@@ -0,0 +1,568 @@
|
||||
<template>
|
||||
<div class="playlist-detail">
|
||||
<!-- 歌单头部信息 -->
|
||||
<header class="playlist-header">
|
||||
<div class="header-content">
|
||||
<div class="cover-area">
|
||||
<img :src="playlist.info.img" :alt="playlist.info.name" class="playlist-cover" />
|
||||
<div class="cover-overlay"></div>
|
||||
</div>
|
||||
|
||||
<div class="info-area">
|
||||
<div class="playlist-type">歌单</div>
|
||||
<h1 class="playlist-name">{{ playlist.info.name }}</h1>
|
||||
<div class="playlist-meta">
|
||||
<span class="author">{{ playlist.info.author }}</span>
|
||||
<span class="play-count">
|
||||
<Icon icon="lucide:play" width="14" /> {{ playlist.info.play_count }}
|
||||
</span>
|
||||
<span class="song-count">{{ playlist.total }} 首</span>
|
||||
</div>
|
||||
|
||||
<div class="playlist-desc" v-if="playlist.info.desc">
|
||||
{{ playlist.info.desc }}
|
||||
</div>
|
||||
|
||||
<div class="action-buttons">
|
||||
<button class="play-btn">
|
||||
<Icon icon="lucide:play" width="20" /> 播放全部
|
||||
</button>
|
||||
<button class="add-btn">
|
||||
<Icon icon="lucide:plus" width="18" /> 收藏
|
||||
</button>
|
||||
<button class="more-btn">
|
||||
<Icon icon="lucide:more-vertical" width="18" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 歌曲列表 -->
|
||||
<main class="song-list">
|
||||
<div class="list-header">
|
||||
<div class="list-title">歌曲列表</div>
|
||||
<div class="list-info">共 {{ playlist.total }} 首歌曲</div>
|
||||
</div>
|
||||
|
||||
<table class="song-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col-index">#</th>
|
||||
<th class="col-info">歌曲</th>
|
||||
<th class="col-album">专辑</th>
|
||||
<th class="col-duration">时长</th>
|
||||
<th class="col-action">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(song, index) in playlist.list" :key="song.songmid" class="song-item">
|
||||
<td class="col-index">{{ index + 1 }}</td>
|
||||
<td class="col-info">
|
||||
<div class="song-info">
|
||||
<img :src="song.s_img" :alt="song.name" class="song-cover" />
|
||||
<div class="song-detail">
|
||||
<div class="song-name">{{ song.name }}</div>
|
||||
<div class="song-singer">{{ song.singer }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="col-album">{{ song.albumName }}</td>
|
||||
<td class="col-duration">{{ song.interval }}</td>
|
||||
<td class="col-action">
|
||||
<button class="play-action">
|
||||
<Icon icon="lucide:play" width="16" />
|
||||
</button>
|
||||
<button class="more-action">
|
||||
<Icon icon="lucide:more-vertical" width="16" />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { Icon } from '@iconify/vue';
|
||||
|
||||
// 固定歌单数据
|
||||
const playlist = ref({
|
||||
"list": [
|
||||
{
|
||||
"singer": "那束花本是给她",
|
||||
"name": "David Lessenger-地球脉动的小曲We Don't Talk Anymore (纯享版) (那束花本是给她 remix)",
|
||||
"albumName": "万物",
|
||||
"albumId": 286652435,
|
||||
"source": "wy",
|
||||
"interval": "03:17",
|
||||
"songmid": 2750774430,
|
||||
"img": "https://p2.music.126.net/zT9grAc0aicJbxglDtnPXA==/109951172079101681.jpg?param=1024y1024",
|
||||
"m_img": "https://p2.music.126.net/zT9grAc0aicJbxglDtnPXA==/109951172079101681.jpg?param=512y512",
|
||||
"s_img": "https://p2.music.126.net/zT9grAc0aicJbxglDtnPXA==/109951172079101681.jpg?param=128y128",
|
||||
"types": [
|
||||
{
|
||||
"type": "standard",
|
||||
"size": "4.53MB"
|
||||
},
|
||||
{
|
||||
"type": "exhigh",
|
||||
"size": "7.55MB"
|
||||
},
|
||||
{
|
||||
"type": "lossless",
|
||||
"size": "19.43MB"
|
||||
},
|
||||
{
|
||||
"type": "sky",
|
||||
"size": "19.46MB"
|
||||
},
|
||||
{
|
||||
"type": "jyeffect",
|
||||
"size": "69.05MB"
|
||||
},
|
||||
{
|
||||
"type": "jymaster",
|
||||
"size": "118.62MB"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"singer": "Taylor Swift、Brendon Urie",
|
||||
"name": "ME!",
|
||||
"albumName": "Lover",
|
||||
"albumId": 80752440,
|
||||
"source": "wy",
|
||||
"interval": "03:13",
|
||||
"songmid": 1382781549,
|
||||
"img": "https://p2.music.126.net/6CB6Jsmb7k7qiJqfMY5Row==/109951164260234943.jpg?param=1024y1024",
|
||||
"m_img": "https://p2.music.126.net/6CB6Jsmb7k7qiJqfMY5Row==/109951164260234943.jpg?param=512y512",
|
||||
"s_img": "https://p2.music.126.net/6CB6Jsmb7k7qiJqfMY5Row==/109951164260234943.jpg?param=128y128",
|
||||
"types": [
|
||||
{
|
||||
"type": "standard",
|
||||
"size": "4.42MB"
|
||||
},
|
||||
{
|
||||
"type": "exhigh",
|
||||
"size": "7.37MB"
|
||||
},
|
||||
{
|
||||
"type": "lossless",
|
||||
"size": "25.86MB"
|
||||
},
|
||||
{
|
||||
"type": "sky",
|
||||
"size": "23.40MB"
|
||||
},
|
||||
{
|
||||
"type": "jyeffect",
|
||||
"size": "73.40MB"
|
||||
},
|
||||
{
|
||||
"type": "jymaster",
|
||||
"size": "127.41MB"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"singer": "DECO*27、初音ミク",
|
||||
"name": "ラビットホール",
|
||||
"albumName": "ラビットホール",
|
||||
"albumId": 164629017,
|
||||
"source": "wy",
|
||||
"interval": "02:39",
|
||||
"songmid": 2043178301,
|
||||
"img": "https://p2.music.126.net/20QetRSvLjUmyyGF__1ALA==/109951168575689926.jpg?param=1024y1024",
|
||||
"m_img": "https://p2.music.126.net/20QetRSvLjUmyyGF__1ALA==/109951168575689926.jpg?param=512y512",
|
||||
"s_img": "https://p2.music.126.net/20QetRSvLjUmyyGF__1ALA==/109951168575689926.jpg?param=128y128",
|
||||
"types": [
|
||||
{
|
||||
"type": "standard",
|
||||
"size": "3.64MB"
|
||||
},
|
||||
{
|
||||
"type": "exhigh",
|
||||
"size": "6.07MB"
|
||||
},
|
||||
{
|
||||
"type": "lossless",
|
||||
"size": "20.65MB"
|
||||
},
|
||||
{
|
||||
"type": "sky",
|
||||
"size": "20.15MB"
|
||||
},
|
||||
{
|
||||
"type": "jyeffect",
|
||||
"size": "64.58MB"
|
||||
},
|
||||
{
|
||||
"type": "jymaster",
|
||||
"size": "113.57MB"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"singer": "d0tc0mmie、GUMI",
|
||||
"name": "I Can't Wait (feat. GUMI)",
|
||||
"albumName": "I Can't Wait (feat. GUMI)",
|
||||
"albumId": 353993404,
|
||||
"source": "wy",
|
||||
"interval": "01:35",
|
||||
"songmid": 3326907142,
|
||||
"img": "https://p2.music.126.net/0GEf5ziQ8mG0HfRY8djyvg==/109951172408103691.jpg?param=1024y1024",
|
||||
"m_img": "https://p2.music.126.net/0GEf5ziQ8mG0HfRY8djyvg==/109951172408103691.jpg?param=512y512",
|
||||
"s_img": "https://p2.music.126.net/0GEf5ziQ8mG0HfRY8djyvg==/109951172408103691.jpg?param=128y128",
|
||||
"types": [
|
||||
{
|
||||
"type": "standard",
|
||||
"size": "2.19MB"
|
||||
},
|
||||
{
|
||||
"type": "exhigh",
|
||||
"size": "3.65MB"
|
||||
},
|
||||
{
|
||||
"type": "lossless",
|
||||
"size": "12.01MB"
|
||||
},
|
||||
{
|
||||
"type": "hires",
|
||||
"size": "20.59MB"
|
||||
},
|
||||
{
|
||||
"type": "sky",
|
||||
"size": "11.29MB"
|
||||
},
|
||||
{
|
||||
"type": "jyeffect",
|
||||
"size": "36.05MB"
|
||||
},
|
||||
{
|
||||
"type": "jymaster",
|
||||
"size": "65.54MB"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"singer": "梶浦由記",
|
||||
"name": "Sis puella magica!",
|
||||
"albumName": "魔法少女まどか☆マギカ Music Collection",
|
||||
"albumId": 2732401,
|
||||
"source": "wy",
|
||||
"interval": "02:49",
|
||||
"songmid": 28138654,
|
||||
"img": "https://p2.music.126.net/7cYVfSgSZFZeVheUFDGOIg==/109951166197931207.jpg?param=1024y1024",
|
||||
"m_img": "https://p2.music.126.net/7cYVfSgSZFZeVheUFDGOIg==/109951166197931207.jpg?param=512y512",
|
||||
"s_img": "https://p2.music.126.net/7cYVfSgSZFZeVheUFDGOIg==/109951166197931207.jpg?param=128y128",
|
||||
"types": [
|
||||
{
|
||||
"type": "standard",
|
||||
"size": "3.88MB"
|
||||
},
|
||||
{
|
||||
"type": "exhigh",
|
||||
"size": "6.47MB"
|
||||
},
|
||||
{
|
||||
"type": "lossless",
|
||||
"size": "31.72MB"
|
||||
},
|
||||
{
|
||||
"type": "sky",
|
||||
"size": "17.53MB"
|
||||
},
|
||||
{
|
||||
"type": "jyeffect",
|
||||
"size": "59.17MB"
|
||||
},
|
||||
{
|
||||
"type": "jymaster",
|
||||
"size": "100.19MB"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"page": 1,
|
||||
"limit": 5,
|
||||
"total": 53,
|
||||
"source": "wy",
|
||||
"info": {
|
||||
"play_count": "51",
|
||||
"name": "我喜欢的音乐",
|
||||
"img": "https://p1.music.126.net/G3NA3TqQD3L51OkzrhNQmw==/109951172080623907.jpg",
|
||||
"desc": "",
|
||||
"author": "蓝蜻蜓017"
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.playlist-detail {
|
||||
min-height: 100%;
|
||||
|
||||
.playlist-header {
|
||||
background: var(--header-bg);
|
||||
padding: 40px 20px;
|
||||
color: var(--text-primary);
|
||||
|
||||
.header-content {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
gap: 30px;
|
||||
align-items: flex-end;
|
||||
|
||||
.cover-area {
|
||||
position: relative;
|
||||
|
||||
.playlist-cover {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
object-fit: cover;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.cover-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(135deg, rgba(99, 102, 241, 0.2), rgba(139, 92, 246, 0.2));
|
||||
border-radius: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.info-area {
|
||||
flex: 1;
|
||||
padding-bottom: 20px;
|
||||
|
||||
.playlist-type {
|
||||
font-size: 14px;
|
||||
opacity: 0.9;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.playlist-name {
|
||||
font-size: 36px;
|
||||
font-weight: 800;
|
||||
margin: 0 0 16px 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.playlist-meta {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
font-size: 14px;
|
||||
opacity: 0.9;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.author {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.play-count {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.playlist-desc {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
opacity: 0.9;
|
||||
margin-bottom: 24px;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
|
||||
button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 24px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&.play-btn {
|
||||
background: #6366f1;
|
||||
color: white;
|
||||
padding: 12px 24px;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 6px 16px rgba(99, 102, 241, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
&.add-btn {
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-tertiary);
|
||||
border-color: var(--text-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
&.more-btn {
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
padding: 0;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-tertiary);
|
||||
border-color: var(--text-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.song-list {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 30px 20px;
|
||||
|
||||
.list-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.list-title {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.list-info {
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.song-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: var(--bg-primary);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
|
||||
|
||||
th, td {
|
||||
padding: 16px 20px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
th {
|
||||
background: var(--bg-secondary);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.col-index {
|
||||
width: 60px;
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.col-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.col-album {
|
||||
width: 250px;
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.col-duration {
|
||||
width: 100px;
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.col-action {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.song-item {
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.song-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
.song-cover {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
object-fit: cover;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.song-detail {
|
||||
.song-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.song-singer {
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
padding: 6px;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: var(--nav-item-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
&.play-action:hover {
|
||||
color: #6366f1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user