1. 播放器歌词效果
This commit is contained in:
@@ -84,6 +84,28 @@ export function useAudio() {
|
||||
}
|
||||
}
|
||||
|
||||
const currentIndex = computed(() => tracks.value.findIndex((t) => t.url === activeUrl.value))
|
||||
|
||||
const playAt = (index) => {
|
||||
const list = tracks.value
|
||||
if (!list.length) return
|
||||
const i = ((index % list.length) + list.length) % list.length
|
||||
const url = list[i]?.url
|
||||
if (!url) return
|
||||
if (isPlaying.value) playUrl(url)
|
||||
else setActive(url)
|
||||
}
|
||||
|
||||
const prev = () => {
|
||||
const idx = currentIndex.value
|
||||
playAt(idx < 0 ? 0 : idx - 1)
|
||||
}
|
||||
|
||||
const next = () => {
|
||||
const idx = currentIndex.value
|
||||
playAt(idx < 0 ? 0 : idx + 1)
|
||||
}
|
||||
|
||||
const play = () => playUrl(activeUrl.value)
|
||||
|
||||
const attemptAutoplay = async () => {
|
||||
@@ -154,8 +176,11 @@ export function useAudio() {
|
||||
currentTime,
|
||||
duration,
|
||||
volume,
|
||||
currentIndex,
|
||||
setTracks,
|
||||
setActive,
|
||||
prev,
|
||||
next,
|
||||
play,
|
||||
pause,
|
||||
toggle,
|
||||
|
||||
@@ -46,25 +46,8 @@
|
||||
<!-- 顶部滚动进度条 -->
|
||||
<div class="fixed top-0 left-0 h-[2px] bg-[#a88c6b] z-[55] transition-[width] duration-150" :style="{ width: progress * 100 + '%' }"></div>
|
||||
|
||||
<!-- 右上角:曲目列表 + 自动滚动 -->
|
||||
<!-- 右上角:自动滚动 -->
|
||||
<div class="fixed top-6 right-5 z-50 flex flex-col items-end gap-4">
|
||||
<div class="music-wrap">
|
||||
<div v-if="audio.tracks.value.length > 1" @click="showTrackList = !showTrackList" class="control-btn cursor-pointer" title="曲目">
|
||||
<img src="https://api.iconify.design/lucide:list-music.svg?color=%23a88c6b" class="w-4 h-4" />
|
||||
</div>
|
||||
<div v-if="showTrackList && audio.tracks.value.length" class="track-pop">
|
||||
<div class="track-pop-title">选择曲目</div>
|
||||
<div class="track-pop-list">
|
||||
<div v-for="t in audio.tracks.value" :key="t.url"
|
||||
class="track-item" :class="{ active: t.url === audio.activeUrl.value }"
|
||||
@click="selectTrack(t.url)">
|
||||
<span class="eq" v-if="t.url === audio.activeUrl.value && audio.isPlaying.value"><i></i><i></i><i></i></span>
|
||||
<span class="tname">{{ t.name }}</span>
|
||||
<span class="tcheck" v-if="t.url === audio.activeUrl.value">✓</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div @click="toggleAutoScroll" class="control-btn cursor-pointer transition-all duration-300"
|
||||
:class="isAutoScroll ? 'bg-[#a88c6b]/15 !border-[#a88c6b]/60 shadow-sm' : 'bg-white/60 shadow-sm border-[#a88c6b]/30'">
|
||||
<img v-if="isAutoScroll" src="https://api.iconify.design/lucide:pause.svg?color=%23a88c6b" class="w-4 h-4" />
|
||||
@@ -166,17 +149,52 @@
|
||||
</template>
|
||||
<p v-else class="player-lrc-empty">暂无歌词</p>
|
||||
</div>
|
||||
<!-- 下:左播放,右音量+进度 -->
|
||||
<!-- 下:左 上/播/下,右 列表+音量 / 进度 -->
|
||||
<div class="player-panel-footer">
|
||||
<button type="button" class="player-play-btn" @click="audio.toggle()">
|
||||
<i :class="audio.isPlaying.value ? 'fa-solid fa-pause' : 'fa-solid fa-play'" />
|
||||
</button>
|
||||
<div class="player-transport">
|
||||
<button type="button" class="player-skip-btn" title="上一首" :disabled="audio.tracks.value.length < 2" @click="playPrevTrack">
|
||||
<i class="fa-solid fa-backward-step" />
|
||||
</button>
|
||||
<button type="button" class="player-play-btn" @click="audio.toggle()">
|
||||
<i :class="audio.isPlaying.value ? 'fa-solid fa-pause' : 'fa-solid fa-play'" />
|
||||
</button>
|
||||
<button type="button" class="player-skip-btn" title="下一首" :disabled="audio.tracks.value.length < 2" @click="playNextTrack">
|
||||
<i class="fa-solid fa-forward-step" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="player-panel-sliders">
|
||||
<div class="player-vol-row">
|
||||
<button type="button" class="player-vol-icon" title="音量" @click="toggleMute">
|
||||
<div class="player-list-wrap">
|
||||
<button
|
||||
v-if="audio.tracks.value.length > 1"
|
||||
type="button"
|
||||
class="player-vol-icon"
|
||||
title="曲目列表"
|
||||
@click="showPanelTrackList = !showPanelTrackList; showVolumeSlider = false"
|
||||
>
|
||||
<i class="fa-solid fa-list-ul" />
|
||||
</button>
|
||||
<div v-if="showPanelTrackList && audio.tracks.value.length" class="player-track-pop">
|
||||
<div class="player-track-pop-title">选择曲目</div>
|
||||
<div class="player-track-pop-list">
|
||||
<button
|
||||
v-for="t in audio.tracks.value"
|
||||
:key="t.url"
|
||||
type="button"
|
||||
class="player-track-item"
|
||||
:class="{ active: t.url === audio.activeUrl.value }"
|
||||
@click="selectTrack(t.url)"
|
||||
>
|
||||
<span class="tname">{{ t.name }}</span>
|
||||
<span v-if="t.url === audio.activeUrl.value" class="tcheck">✓</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="player-vol-icon" title="音量" @click="toggleVolumeSlider">
|
||||
<i :class="volumeIconClass" />
|
||||
</button>
|
||||
<div class="player-vol-track">
|
||||
<div v-if="showVolumeSlider" class="player-vol-track">
|
||||
<input
|
||||
type="range"
|
||||
class="player-vol-range"
|
||||
@@ -606,6 +624,8 @@ const showOpenInBrowser = ref(false)
|
||||
const showBlessingDrawer = ref(false)
|
||||
const blessingSubmitted = ref(false)
|
||||
const showPlayerPanel = ref(false)
|
||||
const showVolumeSlider = ref(false)
|
||||
const showPanelTrackList = ref(false)
|
||||
const lrcLines = ref([])
|
||||
const lrcListRef = ref(null)
|
||||
const danmakuLayerRef = ref(null)
|
||||
@@ -622,14 +642,17 @@ const volumeIconClass = computed(() => {
|
||||
if (v < 0.45) return 'fa-solid fa-volume-low'
|
||||
return 'fa-solid fa-volume-high'
|
||||
})
|
||||
let lastVolumeBeforeMute = 0.5
|
||||
const toggleMute = () => {
|
||||
if ((audio.volume.value || 0) > 0.001) {
|
||||
lastVolumeBeforeMute = audio.volume.value
|
||||
audio.setVolume(0)
|
||||
} else {
|
||||
audio.setVolume(lastVolumeBeforeMute || 0.5)
|
||||
}
|
||||
const toggleVolumeSlider = () => {
|
||||
showVolumeSlider.value = !showVolumeSlider.value
|
||||
if (showVolumeSlider.value) showPanelTrackList.value = false
|
||||
}
|
||||
const playPrevTrack = () => {
|
||||
audio.prev()
|
||||
if (data.value.musicRandomEnabled && audio.activeUrl.value) rememberMusicPick(audio.activeUrl.value)
|
||||
}
|
||||
const playNextTrack = () => {
|
||||
audio.next()
|
||||
if (data.value.musicRandomEnabled && audio.activeUrl.value) rememberMusicPick(audio.activeUrl.value)
|
||||
}
|
||||
const activeLrcIndex = computed(() => findLrcIndex(lrcLines.value, audio.currentTime.value))
|
||||
|
||||
@@ -650,6 +673,8 @@ const loadLrcForActive = async () => {
|
||||
|
||||
const openPlayerPanel = async () => {
|
||||
showPlayerPanel.value = true
|
||||
showVolumeSlider.value = false
|
||||
showPanelTrackList.value = false
|
||||
await loadLrcForActive()
|
||||
}
|
||||
|
||||
@@ -1024,6 +1049,7 @@ const openInvitation = () => {
|
||||
}
|
||||
const selectTrack = (url) => {
|
||||
audio.setActive(url)
|
||||
showPanelTrackList.value = false
|
||||
showTrackList.value = false
|
||||
if (data.value.musicRandomEnabled) rememberMusicPick(url)
|
||||
}
|
||||
@@ -1445,7 +1471,7 @@ watch(
|
||||
width: min(82vw, 308px);
|
||||
padding: 14px 14px 12px;
|
||||
border-radius: 26px;
|
||||
background: rgba(255, 252, 247, 0.82);
|
||||
background: rgba(255, 252, 247, 0.39);
|
||||
border: none;
|
||||
box-shadow:
|
||||
0 16px 40px rgba(92, 74, 53, 0.16),
|
||||
@@ -1510,7 +1536,7 @@ watch(
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.player-lrc-list {
|
||||
max-height: 132px;
|
||||
max-height: 250px;
|
||||
overflow-y: auto;
|
||||
padding: 10px 8px;
|
||||
border-radius: 18px;
|
||||
@@ -1520,9 +1546,10 @@ watch(
|
||||
.player-lrc-line {
|
||||
margin: 0;
|
||||
padding: 7px 8px;
|
||||
font-family: var(--font-calligraphy);
|
||||
font-size: 12px;
|
||||
line-height: 1.55;
|
||||
color: #9a9084;
|
||||
color: rgb(154, 144, 132);
|
||||
text-align: center;
|
||||
transition: color 0.2s, transform 0.2s, background 0.2s;
|
||||
border-radius: 999px;
|
||||
@@ -1530,7 +1557,7 @@ watch(
|
||||
.player-lrc-line.active {
|
||||
color: #5c4a35;
|
||||
font-weight: 600;
|
||||
background: rgba(255, 255, 255, 0.55);
|
||||
background: rgba(255, 255, 255, 0.36);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
.player-lrc-empty {
|
||||
@@ -1543,12 +1570,33 @@ watch(
|
||||
.player-panel-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
gap: 10px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.player-transport {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.player-skip-btn {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 999px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #a88c6b;
|
||||
font-size: 12px;
|
||||
background: rgba(168, 140, 107, 0.1);
|
||||
}
|
||||
.player-skip-btn:disabled {
|
||||
opacity: 0.35;
|
||||
pointer-events: none;
|
||||
}
|
||||
.player-play-btn {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 999px;
|
||||
background: linear-gradient(145deg, #c4a484, #a88c6b);
|
||||
color: #fff;
|
||||
@@ -1557,7 +1605,7 @@ watch(
|
||||
justify-content: center;
|
||||
flex: 0 0 auto;
|
||||
box-shadow: 0 6px 14px rgba(168, 140, 107, 0.35);
|
||||
font-size: 15px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.player-play-btn .fa-play { margin-left: 2px; }
|
||||
.player-panel-sliders {
|
||||
@@ -1570,8 +1618,13 @@ watch(
|
||||
.player-vol-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
gap: 4px;
|
||||
color: #a88c6b;
|
||||
min-height: 28px;
|
||||
}
|
||||
.player-list-wrap {
|
||||
position: relative;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.player-vol-icon {
|
||||
width: 28px;
|
||||
@@ -1583,8 +1636,57 @@ watch(
|
||||
color: #a88c6b;
|
||||
font-size: 12px;
|
||||
flex: 0 0 auto;
|
||||
background: transparent;
|
||||
background: rgba(168, 140, 107, 0.1);
|
||||
}
|
||||
.player-track-pop {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: calc(100% + 8px);
|
||||
width: min(70vw, 220px);
|
||||
max-height: 180px;
|
||||
padding: 10px 8px;
|
||||
border-radius: 16px;
|
||||
background: rgba(255, 252, 247, 0.96);
|
||||
box-shadow: 0 12px 28px rgba(92, 74, 53, 0.16);
|
||||
z-index: 30;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.player-track-pop-title {
|
||||
font-size: 11px;
|
||||
color: #9a9084;
|
||||
letter-spacing: 0.12em;
|
||||
padding: 0 6px 6px;
|
||||
}
|
||||
.player-track-pop-list {
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
.player-track-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
padding: 8px 10px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
color: #5c4a35;
|
||||
}
|
||||
.player-track-item.active {
|
||||
background: rgba(168, 140, 107, 0.14);
|
||||
color: #a88c6b;
|
||||
font-weight: 600;
|
||||
}
|
||||
.player-track-item .tname {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.player-track-item .tcheck { font-size: 11px; }
|
||||
.player-vol-track {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
Reference in New Issue
Block a user