feat: add Vue.js WebUI — YesPlayMusic-inspired SPA with all pages and components

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
saopig1andClaude Opus 4.6 committed 2026-03-30 00:58:33 +08:00
1 parent 2a966e914c
commit 694e3c953b
25 files changed
+5388 -1

No files matched your search

+166
View File
@@ -0,0 +1,166 @@
<template>
<div class="player-bar frosted-glass" v-if="currentSong">
<div class="player-left">
<CoverArt :url="currentSong.coverUrl" :size="40" />
<div class="song-info">
<div class="song-name">{{ currentSong.name }}</div>
<div class="song-artist">{{ currentSong.artist }}</div>
</div>
</div>
<div class="player-center">
<button class="control-btn" @click="store.prev()">
<Icon icon="mdi:skip-previous" />
</button>
<button class="play-btn" @click="togglePlay">
<Icon :icon="store.isPlaying ? 'mdi:pause' : 'mdi:play'" />
</button>
<button class="control-btn" @click="store.next()">
<Icon icon="mdi:skip-next" />
</button>
</div>
<div class="player-right">
<Icon icon="mdi:volume-high" class="volume-icon" />
<input
type="range"
min="0"
max="100"
:value="activeBot?.volume ?? 75"
@input="onVolumeChange"
class="volume-slider"
/>
<RouterLink to="/lyrics" class="control-btn lyrics-btn">
<Icon icon="mdi:microphone" />
</RouterLink>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { Icon } from '@iconify/vue';
import { usePlayerStore } from '../stores/player.js';
import CoverArt from './CoverArt.vue';
const store = usePlayerStore();
const activeBot = computed(() => store.activeBot);
const currentSong = computed(() => store.currentSong);
function togglePlay() {
if (store.isPlaying) {
store.pause();
} else {
store.resume();
}
}
function onVolumeChange(e: Event) {
const target = e.target as HTMLInputElement;
store.setVolume(parseInt(target.value));
}
</script>
<style lang="scss" scoped>
.player-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: var(--player-height);
display: flex;
align-items: center;
padding: 0 24px;
z-index: 100;
border-top: 1px solid var(--border-color);
}
.player-left {
display: flex;
align-items: center;
gap: 12px;
width: 240px;
}
.song-info {
min-width: 0;
}
.song-name {
font-size: 13px;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.song-artist {
font-size: 11px;
color: var(--text-secondary);
}
.player-center {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
gap: 20px;
}
.control-btn {
font-size: 20px;
opacity: 0.7;
transition: opacity var(--transition-fast);
&:hover { opacity: 1; }
}
.play-btn {
width: 32px;
height: 32px;
background: var(--color-primary);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
color: white;
transition: transform var(--transition-fast);
&:hover { transform: scale(1.08); }
&:active { transform: scale(0.95); }
}
.player-right {
width: 240px;
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
}
.volume-icon {
font-size: 18px;
opacity: 0.6;
}
.volume-slider {
width: 80px;
height: 3px;
appearance: none;
background: var(--border-color);
border-radius: 2px;
outline: none;
&::-webkit-slider-thumb {
appearance: none;
width: 12px;
height: 12px;
background: var(--color-primary);
border-radius: 50%;
cursor: pointer;
}
}
.lyrics-btn {
margin-left: 8px;
}
</style>