mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-02 04:52:50 +08:00
fix: progress bar smooth via rAF with direct Date.now(), queue panel moved outside player div
- Progress bar reads playStartedAt directly in requestAnimationFrame loop bypassing Vue reactivity caching for smooth 60fps updates - Queue panel moved to sibling of player-bar (was trapped inside causing z-index issues) - Added progress thumb dot on hover - Click on progress shows 'seek not available' tooltip Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
dc9d181783
commit
2c87f1cff9
2 files changed
+59
-28
No files matched your search
@@ -1,15 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="player-bar frosted-glass" v-if="currentSong">
|
<div class="player-wrapper" v-if="currentSong">
|
||||||
|
<Queue :open="showQueue" @close="showQueue = false" />
|
||||||
|
|
||||||
|
<div class="player-bar frosted-glass">
|
||||||
<!-- Progress bar -->
|
<!-- Progress bar -->
|
||||||
<div
|
<div
|
||||||
class="progress-bar-container"
|
class="progress-bar-container"
|
||||||
ref="progressBarRef"
|
ref="progressBarRef"
|
||||||
@mousedown="onProgressMouseDown"
|
@click="onProgressClick"
|
||||||
@mousemove="onProgressHover"
|
@mousemove="onProgressHover"
|
||||||
@mouseleave="progressTooltipVisible = false"
|
@mouseleave="progressTooltipVisible = false"
|
||||||
>
|
>
|
||||||
<div class="progress-bar-bg">
|
<div class="progress-bar-bg">
|
||||||
<div class="progress-bar-fill" :style="{ width: progressPercent + '%' }" />
|
<div class="progress-bar-fill" :style="{ width: progressPercent + '%' }" />
|
||||||
|
<div class="progress-bar-thumb" :style="{ left: progressPercent + '%' }" />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="progressTooltipVisible"
|
v-if="progressTooltipVisible"
|
||||||
@@ -56,14 +60,14 @@
|
|||||||
@input="onVolumeChange"
|
@input="onVolumeChange"
|
||||||
class="volume-slider"
|
class="volume-slider"
|
||||||
/>
|
/>
|
||||||
<button class="control-btn" @click="toggleQueue">
|
<button class="control-btn" :class="{ active: showQueue }" @click="showQueue = !showQueue">
|
||||||
<Icon icon="mdi:playlist-music" />
|
<Icon icon="mdi:playlist-music" />
|
||||||
</button>
|
</button>
|
||||||
<RouterLink to="/lyrics" class="control-btn lyrics-btn">
|
<RouterLink to="/lyrics" class="control-btn lyrics-btn">
|
||||||
<Icon icon="mdi:microphone" />
|
<Icon icon="mdi:microphone" />
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</div>
|
</div>
|
||||||
<Queue :open="showQueue" @close="showQueue = false" />
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -80,13 +84,13 @@ const store = usePlayerStore();
|
|||||||
const activeBot = computed(() => store.activeBot);
|
const activeBot = computed(() => store.activeBot);
|
||||||
const currentSong = computed(() => store.currentSong);
|
const currentSong = computed(() => store.currentSong);
|
||||||
|
|
||||||
// Progress bar state
|
// Progress — use manual timer instead of relying on reactive getters
|
||||||
const progressBarRef = ref<HTMLElement | null>(null);
|
|
||||||
const currentElapsed = ref(0);
|
const currentElapsed = ref(0);
|
||||||
const progressPercent = ref(0);
|
const progressPercent = ref(0);
|
||||||
const progressTooltipVisible = ref(false);
|
const progressTooltipVisible = ref(false);
|
||||||
const progressTooltipX = ref(0);
|
const progressTooltipX = ref(0);
|
||||||
const progressTooltipTime = ref('0:00');
|
const progressTooltipTime = ref('0:00');
|
||||||
|
const progressBarRef = ref<HTMLElement | null>(null);
|
||||||
let rafId: number | null = null;
|
let rafId: number | null = null;
|
||||||
|
|
||||||
function formatTime(seconds: number): string {
|
function formatTime(seconds: number): string {
|
||||||
@@ -97,22 +101,33 @@ function formatTime(seconds: number): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateProgress() {
|
function updateProgress() {
|
||||||
const elapsed = store.elapsed;
|
// Directly compute elapsed from raw values (bypass Vue reactivity)
|
||||||
|
const bot = store.activeBot;
|
||||||
|
if (bot?.playing && !bot.paused && store.playStartedAt > 0) {
|
||||||
|
currentElapsed.value = (Date.now() - store.playStartedAt) / 1000;
|
||||||
|
} else if (bot?.paused) {
|
||||||
|
currentElapsed.value = store.pausedElapsed;
|
||||||
|
}
|
||||||
|
|
||||||
const duration = currentSong.value?.duration ?? 0;
|
const duration = currentSong.value?.duration ?? 0;
|
||||||
currentElapsed.value = elapsed;
|
progressPercent.value = duration > 0
|
||||||
progressPercent.value = duration > 0 ? Math.min((elapsed / duration) * 100, 100) : 0;
|
? Math.min((currentElapsed.value / duration) * 100, 100)
|
||||||
|
: 0;
|
||||||
|
|
||||||
rafId = requestAnimationFrame(updateProgress);
|
rafId = requestAnimationFrame(updateProgress);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onProgressMouseDown(e: MouseEvent) {
|
function onProgressClick(e: MouseEvent) {
|
||||||
const bar = progressBarRef.value;
|
const bar = progressBarRef.value;
|
||||||
if (!bar) return;
|
if (!bar) return;
|
||||||
const rect = bar.getBoundingClientRect();
|
const rect = bar.getBoundingClientRect();
|
||||||
const ratio = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
|
const ratio = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
|
||||||
const duration = currentSong.value?.duration ?? 0;
|
const duration = currentSong.value?.duration ?? 0;
|
||||||
// Visual-only seek (server-side seeking not supported)
|
const seekTime = ratio * duration;
|
||||||
progressPercent.value = ratio * 100;
|
// Can't seek server-side FFmpeg stream — show tooltip with target time
|
||||||
progressTooltipTime.value = formatTime(ratio * duration);
|
progressTooltipTime.value = `跳转不可用 (${formatTime(seekTime)})`;
|
||||||
|
progressTooltipVisible.value = true;
|
||||||
|
setTimeout(() => { progressTooltipVisible.value = false; }, 1500);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onProgressHover(e: MouseEvent) {
|
function onProgressHover(e: MouseEvent) {
|
||||||
@@ -134,11 +149,6 @@ onUnmounted(() => {
|
|||||||
if (rafId !== null) cancelAnimationFrame(rafId);
|
if (rafId !== null) cancelAnimationFrame(rafId);
|
||||||
});
|
});
|
||||||
|
|
||||||
function toggleQueue() {
|
|
||||||
showQueue.value = !showQueue.value;
|
|
||||||
console.log('Queue panel toggled:', showQueue.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function togglePlay() {
|
function togglePlay() {
|
||||||
if (store.isPlaying) {
|
if (store.isPlaying) {
|
||||||
store.pause();
|
store.pause();
|
||||||
@@ -178,33 +188,38 @@ function cycleMode() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.player-bar {
|
.player-wrapper {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-bar {
|
||||||
height: var(--player-height);
|
height: var(--player-height);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 24px;
|
padding: 0 24px;
|
||||||
z-index: 100;
|
|
||||||
border-top: 1px solid var(--border-color);
|
border-top: 1px solid var(--border-color);
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-bar-container {
|
.progress-bar-container {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -4px;
|
top: -6px;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
height: 8px;
|
height: 12px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
z-index: 101;
|
z-index: 101;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-end;
|
align-items: center;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
.progress-bar-bg { height: 4px; }
|
.progress-bar-bg { height: 4px; }
|
||||||
.progress-bar-fill { height: 4px; }
|
.progress-bar-thumb { opacity: 1; transform: scale(1); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,6 +229,7 @@ function cycleMode() {
|
|||||||
background: var(--border-color);
|
background: var(--border-color);
|
||||||
transition: height 0.15s ease;
|
transition: height 0.15s ease;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
border-radius: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-bar-fill {
|
.progress-bar-fill {
|
||||||
@@ -222,8 +238,22 @@ function cycleMode() {
|
|||||||
left: 0;
|
left: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: var(--color-primary);
|
background: var(--color-primary);
|
||||||
border-radius: 0 1px 1px 0;
|
border-radius: 1px;
|
||||||
transition: height 0.15s ease;
|
// No transition — updated via rAF for smooth movement
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-thumb {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
background: var(--color-primary);
|
||||||
|
border-radius: 50%;
|
||||||
|
transform: scale(0);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.15s, transform 0.15s;
|
||||||
|
margin-left: -5px;
|
||||||
|
margin-top: -5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-tooltip {
|
.progress-tooltip {
|
||||||
@@ -287,6 +317,7 @@ function cycleMode() {
|
|||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
transition: opacity var(--transition-fast);
|
transition: opacity var(--transition-fast);
|
||||||
&:hover { opacity: 1; }
|
&:hover { opacity: 1; }
|
||||||
|
&.active { opacity: 1; color: var(--color-primary); }
|
||||||
}
|
}
|
||||||
|
|
||||||
.mode-btn {
|
.mode-btn {
|
||||||
|
|||||||
@@ -96,8 +96,8 @@ onMounted(async () => {
|
|||||||
]);
|
]);
|
||||||
playlist.value = detailRes.data.playlist;
|
playlist.value = detailRes.data.playlist;
|
||||||
songs.value = songsRes.data.songs;
|
songs.value = songsRes.data.songs;
|
||||||
} catch (err) {
|
} catch (err: any) {
|
||||||
console.error('Failed to load playlist:', err);
|
console.error('Failed to load playlist:', err?.response?.status, err?.message);
|
||||||
playlist.value = null;
|
playlist.value = null;
|
||||||
songs.value = [];
|
songs.value = [];
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
Reference in new issue
Block a user