mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-05-17 02:07:29 +08:00
5ba9e6591a
MusicFull.vue 与 MusicFullMobile.vue 各自持有的 setTextColors / currentBackground / animationFrame / isDark 合并到共享 composable, 消除两份几乎一致的包装逻辑。Mobile 的 --bg-color 差异通过 writeBgColor option 注入,行为等价。
2013 lines
53 KiB
Vue
2013 lines
53 KiB
Vue
<template>
|
||
<n-drawer
|
||
v-model:show="isVisible"
|
||
height="100%"
|
||
placement="bottom"
|
||
:style="{ background: playerStore.playMusic.primaryColor || background }"
|
||
:to="`#layout-main`"
|
||
:z-index="9998"
|
||
>
|
||
<div
|
||
id="mobile-drawer-target"
|
||
:class="[
|
||
config.theme,
|
||
`cover-style-${config.mobileCoverStyle}`,
|
||
{ 'is-landscape': isLandscape },
|
||
{ 'is-dark': isDark }
|
||
]"
|
||
>
|
||
<!-- 顶部控制按钮 -->
|
||
<div v-if="playMusic?.playLoading" class="loading-overlay">
|
||
<i class="ri-loader-4-line loading-icon"></i>
|
||
</div>
|
||
<div
|
||
class="control-btn absolute left-5"
|
||
:class="{ 'pure-mode': config.pureModeEnabled }"
|
||
@click="closeMusicFull"
|
||
>
|
||
<i class="ri-arrow-down-s-line"></i>
|
||
</div>
|
||
|
||
<!-- 右上角设置按钮 -->
|
||
<div
|
||
class="control-btn absolute right-5 flex items-center gap-2"
|
||
:class="[
|
||
{ 'pure-mode': config.pureModeEnabled },
|
||
hasSleepTimerActive ? '!w-auto !px-2' : ''
|
||
]"
|
||
>
|
||
<!-- 定时器倒计时显示 -->
|
||
<div
|
||
v-if="hasSleepTimerActive"
|
||
class="flex items-center gap-1 px-2 py-1 rounded-full bg-black/30 backdrop-blur-sm text-xs text-white/90"
|
||
@click="showPlayerSettings = true"
|
||
>
|
||
<i class="ri-timer-line text-green-400"></i>
|
||
<span class="font-medium tabular-nums">{{ sleepTimerDisplayText }}</span>
|
||
</div>
|
||
<div @click="showPlayerSettings = true">
|
||
<i class="ri-more-2-fill"></i>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 播放设置弹窗 -->
|
||
<mobile-player-settings v-model:visible="showPlayerSettings" />
|
||
|
||
<!-- 全屏歌词页面 - 竖屏模式下 -->
|
||
<transition name="fade">
|
||
<div v-if="showFullLyrics && !isLandscape" class="fullscreen-lyrics" :class="config.theme">
|
||
<div class="fullscreen-header">
|
||
<div class="song-title" v-html="playMusic.name"></div>
|
||
<div class="artist-name">
|
||
<span v-for="(item, index) in artistList" :key="index">
|
||
{{ item.name }}{{ index < artistList.length - 1 ? ' / ' : '' }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
ref="lyricsScrollerRef"
|
||
class="lyrics-scroller"
|
||
@touchstart="handleTouchStart"
|
||
@touchmove="handleTouchMove"
|
||
@touchend="handleTouchEnd"
|
||
@scroll="handleScroll"
|
||
>
|
||
<div class="lyrics-padding-top"></div>
|
||
<!-- 无时间戳歌词提示 -->
|
||
<div v-if="!supportAutoScroll" class="lyric-line no-scroll-tip">
|
||
<span>{{ t('player.lrc.noAutoScroll') }}</span>
|
||
</div>
|
||
<div
|
||
v-for="(item, index) in lrcArray"
|
||
:key="index"
|
||
:id="`lyric-line-${index}`"
|
||
class="lyric-line"
|
||
:class="{
|
||
'now-text': index === nowIndex,
|
||
'hover-text': item.text && item.startTime !== -1
|
||
}"
|
||
@click="item.startTime !== -1 ? setAudioTime(index) : null"
|
||
>
|
||
<!-- 逐字歌词显示 -->
|
||
<div
|
||
v-if="item.hasWordByWord && item.words && item.words.length > 0"
|
||
class="word-by-word-lyric"
|
||
>
|
||
<template v-for="(word, wordIndex) in item.words" :key="wordIndex">
|
||
<span class="lyric-word" :style="getWordStyle(index, wordIndex, word)">
|
||
{{ word.text }} </span
|
||
><span class="lyric-word" v-if="word.space"> </span></template
|
||
>
|
||
</div>
|
||
<!-- 普通歌词显示 -->
|
||
<span v-else :style="getLrcStyle(index)">{{ item.text }}</span>
|
||
<div v-if="config.showTranslation && item.trText" class="translation">
|
||
{{ item.trText }}
|
||
</div>
|
||
</div>
|
||
<div class="lyrics-padding-bottom"></div>
|
||
</div>
|
||
</div>
|
||
</transition>
|
||
|
||
<!-- 主要内容区域 - 竖屏模式下的普通布局 -->
|
||
<transition name="fade">
|
||
<div v-if="!showFullLyrics && !isLandscape" class="ios-layout-container">
|
||
<!-- 封面区域 -->
|
||
<div
|
||
class="cover-container"
|
||
:class="{
|
||
'record-style': config.mobileCoverStyle === 'record',
|
||
'square-style': config.mobileCoverStyle === 'square',
|
||
'full-style': config.mobileCoverStyle === 'full',
|
||
paused: !play
|
||
}"
|
||
@click="cycleCoverStyle"
|
||
>
|
||
<div class="img-wrapper">
|
||
<n-image
|
||
ref="PicImgRef"
|
||
:src="getImgUrl(playMusic?.picUrl, '500y500')"
|
||
lazy
|
||
preview-disabled
|
||
class="cover-image"
|
||
:class="{ 'full-blend': config.mobileCoverStyle === 'full' }"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="px-2 flex-1 flex flex-col justify-around w-[85%]">
|
||
<!-- 歌曲信息 -->
|
||
<div class="song-info">
|
||
<div class="song-title-container">
|
||
<h1 class="song-title" v-html="playMusic.name"></h1>
|
||
</div>
|
||
<p class="song-artist">
|
||
<span
|
||
v-for="(item, index) in artistList"
|
||
:key="index"
|
||
class="artist-name"
|
||
@click="handleArtistClick(item.id)"
|
||
>
|
||
{{ item.name }}
|
||
{{ index < artistList.length - 1 ? ' / ' : '' }}
|
||
</span>
|
||
</p>
|
||
<div class="favorite-icon" @click="toggleFavorite">
|
||
<i class="ri-heart-3-fill" :class="{ favorite: isFavorite }"></i>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 歌词区域 -->
|
||
<div class="lyrics-container" v-if="!config.hideLyrics" @click="showFullLyricScreen">
|
||
<div v-if="lrcArray.length > 0" class="lyrics-wrapper">
|
||
<div v-for="(line, idx) in visibleLyrics" :key="idx" class="lyric-line">
|
||
<!-- 逐字歌词显示 -->
|
||
<div
|
||
v-if="line.hasWordByWord && line.words && line.words.length > 0"
|
||
class="word-by-word-lyric"
|
||
>
|
||
<template v-for="(word, wordIndex) in line.words" :key="wordIndex">
|
||
<span
|
||
class="lyric-word"
|
||
:style="getWordStyle(line.originalIndex, wordIndex, word)"
|
||
>
|
||
{{ word.text }}</span
|
||
><span v-if="word.space"> </span></template
|
||
>
|
||
</div>
|
||
<!-- 普通歌词显示 -->
|
||
<span v-else>{{ line.text }}</span>
|
||
</div>
|
||
</div>
|
||
<div v-else class="no-lyrics">
|
||
{{ t('player.lrc.noLrc') }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</transition>
|
||
|
||
<!-- 横屏模式布局 -->
|
||
<div v-if="isLandscape" class="landscape-layout">
|
||
<!-- 左侧封面和进度条 -->
|
||
<div class="landscape-left-section">
|
||
<div
|
||
class="landscape-cover-container cover-container"
|
||
:class="{
|
||
'record-style': config.mobileCoverStyle === 'record',
|
||
'square-style': config.mobileCoverStyle === 'square',
|
||
'full-style': config.mobileCoverStyle === 'full',
|
||
paused: !play
|
||
}"
|
||
@click="cycleCoverStyle"
|
||
>
|
||
<div class="img-wrapper">
|
||
<n-image
|
||
:src="getImgUrl(playMusic?.picUrl, '500y500')"
|
||
lazy
|
||
preview-disabled
|
||
class="cover-image"
|
||
:class="{ 'full-blend': config.mobileCoverStyle === 'full' }"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 左侧进度条 -->
|
||
<div class="landscape-progress-container">
|
||
<div class="time-info">
|
||
<span class="current-time">{{ secondToMinute(nowTime) }}</span>
|
||
<span class="total-time">{{ secondToMinute(allTime) }}</span>
|
||
</div>
|
||
<div
|
||
class="apple-style-progress"
|
||
@click="handleProgressBarClick"
|
||
@mousedown="handleMouseDown"
|
||
>
|
||
<div class="progress-track">
|
||
<div
|
||
class="progress-fill"
|
||
:style="{ width: `${(nowTime / Math.max(1, allTime)) * 100}%` }"
|
||
></div>
|
||
<div
|
||
class="progress-thumb"
|
||
:class="{ active: isThumbDragging || isMouseDragging }"
|
||
:style="{ left: `${(nowTime / Math.max(1, allTime)) * 100}%` }"
|
||
@touchstart="handleThumbTouchStart"
|
||
@touchmove="handleThumbTouchMove"
|
||
@touchend="handleThumbTouchEnd"
|
||
@mousedown="handleMouseDown"
|
||
></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 右侧歌词区域 -->
|
||
<div class="landscape-lyrics-section">
|
||
<!-- 歌曲信息放置在顶部 -->
|
||
<div class="landscape-song-info">
|
||
<div class="flex flex-col flex-1">
|
||
<h1 class="song-title" v-html="playMusic.name"></h1>
|
||
<p class="song-artist">
|
||
<span
|
||
v-for="(item, index) in artistList"
|
||
:key="index"
|
||
class="artist-name"
|
||
@click="handleArtistClick(item.id)"
|
||
>
|
||
{{ item.name }}{{ index < artistList.length - 1 ? ' / ' : '' }}
|
||
</span>
|
||
</p>
|
||
</div>
|
||
<div class="favorite-icon landscape" @click="toggleFavorite">
|
||
<i class="ri-heart-3-fill" :class="{ favorite: isFavorite }"></i>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 歌词滚动区域 -->
|
||
<div
|
||
ref="landscapeLyricsRef"
|
||
class="landscape-lyrics-scroller"
|
||
@touchstart="handleTouchStart"
|
||
@touchmove="handleTouchMove"
|
||
@touchend="handleTouchEnd"
|
||
@scroll="handleScroll"
|
||
>
|
||
<div class="lyrics-padding-top"></div>
|
||
<!-- 无时间戳歌词提示 -->
|
||
<div v-if="!supportAutoScroll" class="lyric-line no-scroll-tip">
|
||
<span>{{ t('player.lrc.noAutoScroll') }}</span>
|
||
</div>
|
||
<div
|
||
v-for="(item, index) in lrcArray"
|
||
:key="index"
|
||
:id="`landscape-lyric-line-${index}`"
|
||
class="lyric-line"
|
||
:class="{
|
||
'now-text': index === nowIndex,
|
||
'hover-text': item.text && item.startTime !== -1
|
||
}"
|
||
@click="item.startTime !== -1 ? setAudioTime(index) : null"
|
||
>
|
||
<!-- 逐字歌词显示 -->
|
||
<div
|
||
v-if="item.hasWordByWord && item.words && item.words.length > 0"
|
||
class="word-by-word-lyric"
|
||
>
|
||
<template v-for="(word, wordIndex) in item.words" :key="wordIndex">
|
||
<span class="lyric-word" :style="getWordStyle(index, wordIndex, word)">
|
||
{{ word.text }} </span
|
||
><span class="lyric-word" v-if="word.space"> </span></template
|
||
>
|
||
</div>
|
||
<!-- 普通歌词显示 -->
|
||
<span v-else :style="getLrcStyle(index)">{{ item.text }}</span>
|
||
<div v-if="config.showTranslation && item.trText" class="translation">
|
||
{{ item.trText }}
|
||
</div>
|
||
</div>
|
||
<div class="lyrics-padding-bottom"></div>
|
||
</div>
|
||
|
||
<!-- 右下角控制按钮 -->
|
||
<div class="landscape-main-controls">
|
||
<div class="main-button prev" @click="prevSong">
|
||
<i class="ri-skip-back-fill"></i>
|
||
</div>
|
||
<div class="main-button play-pause" @click="togglePlay">
|
||
<i :class="playIcon"></i>
|
||
</div>
|
||
<div class="main-button next" @click="nextSong">
|
||
<i class="ri-skip-forward-fill"></i>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 竖屏模式的控制区域 -->
|
||
<div
|
||
v-if="!isLandscape"
|
||
class="unified-controls"
|
||
:class="{ 'fullscreen-mode': showFullLyrics }"
|
||
>
|
||
<!-- 进度条 (苹果风格) -->
|
||
<div class="progress-container">
|
||
<div class="time-info">
|
||
<span class="current-time">{{ secondToMinute(nowTime) }}</span>
|
||
<span class="total-time">{{ secondToMinute(allTime) }}</span>
|
||
</div>
|
||
<div
|
||
class="apple-style-progress"
|
||
@click="handleProgressBarClick"
|
||
@mousedown="handleMouseDown"
|
||
>
|
||
<div class="progress-track">
|
||
<div
|
||
class="progress-fill"
|
||
:style="{ width: `${(nowTime / Math.max(1, allTime)) * 100}%` }"
|
||
></div>
|
||
<div
|
||
class="progress-thumb"
|
||
:class="{ active: isThumbDragging || isMouseDragging }"
|
||
:style="{ left: `${(nowTime / Math.max(1, allTime)) * 100}%` }"
|
||
@touchstart="handleThumbTouchStart"
|
||
@touchmove="handleThumbTouchMove"
|
||
@touchend="handleThumbTouchEnd"
|
||
@mousedown="handleMouseDown"
|
||
></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 控制按钮 -->
|
||
<div class="control-buttons">
|
||
<!-- 返回按钮,仅在全屏歌词模式下显示 -->
|
||
<div v-if="showFullLyrics" class="back-button" @click.stop="closeFullLyrics">
|
||
<i class="ri-arrow-down-s-line"></i>
|
||
</div>
|
||
<div class="side-button" @click="togglePlayMode">
|
||
<i :class="[playModeIcon, { 'intelligence-active': playMode === 3 }]"></i>
|
||
</div>
|
||
<div class="main-button prev" @click="prevSong">
|
||
<i class="ri-skip-back-fill"></i>
|
||
</div>
|
||
<div class="main-button play-pause" @click="togglePlay">
|
||
<i :class="playIcon"></i>
|
||
</div>
|
||
<div class="main-button next" @click="nextSong">
|
||
<i class="ri-skip-forward-fill"></i>
|
||
</div>
|
||
<div class="side-button" @click="showPlaylist">
|
||
<i class="iconfont icon-list"></i>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</n-drawer>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { useWindowSize } from '@vueuse/core';
|
||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||
import { useI18n } from 'vue-i18n';
|
||
|
||
import MobilePlayerSettings from '@/components/player/MobilePlayerSettings.vue';
|
||
import {
|
||
allTime,
|
||
artistList,
|
||
correctionTime,
|
||
lrcArray,
|
||
nowIndex,
|
||
nowTime,
|
||
playMusic,
|
||
setAudioTime,
|
||
sound,
|
||
textColors,
|
||
useLyricProgress
|
||
} from '@/hooks/MusicHook';
|
||
import { useArtist } from '@/hooks/useArtist';
|
||
import { useLyricBackground } from '@/hooks/useLyricBackground';
|
||
import { usePlayMode } from '@/hooks/usePlayMode';
|
||
import { audioService } from '@/services/audioService';
|
||
import { usePlayerStore } from '@/store/modules/player';
|
||
import { DEFAULT_LYRIC_CONFIG, LyricConfig } from '@/types/lyric';
|
||
import { getImgUrl, secondToMinute } from '@/utils';
|
||
import { getTextColors } from '@/utils/linearColor';
|
||
import { showBottomToast } from '@/utils/shortcutToast';
|
||
|
||
const { t } = useI18n();
|
||
const playerStore = usePlayerStore();
|
||
|
||
// 播放控制相关
|
||
const play = computed(() => playerStore.isPlay);
|
||
const playIcon = computed(() => (play.value ? 'ri-pause-fill' : 'ri-play-fill'));
|
||
|
||
// 播放设置弹窗
|
||
const showPlayerSettings = ref(false);
|
||
|
||
// 定时器相关
|
||
const sleepTimerRefresh = ref(0);
|
||
let sleepTimerInterval: ReturnType<typeof setInterval> | null = null;
|
||
|
||
const hasSleepTimerActive = computed(() => playerStore.hasSleepTimerActive);
|
||
|
||
const sleepTimerDisplayText = computed(() => {
|
||
void sleepTimerRefresh.value; // 触发响应式更新
|
||
|
||
const timer = playerStore.sleepTimer;
|
||
if (timer.type === 'time' && timer.endTime) {
|
||
const remaining = Math.max(0, timer.endTime - Date.now());
|
||
const totalSeconds = Math.floor(remaining / 1000);
|
||
const minutes = Math.floor(totalSeconds / 60);
|
||
const seconds = totalSeconds % 60;
|
||
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
||
}
|
||
if (timer.type === 'songs' && timer.remainingSongs) {
|
||
return `${timer.remainingSongs}首`;
|
||
}
|
||
if (timer.type === 'end') {
|
||
return '列表结束';
|
||
}
|
||
return '';
|
||
});
|
||
|
||
// 启动/停止定时器刷新
|
||
watch(
|
||
hasSleepTimerActive,
|
||
(active) => {
|
||
if (active && playerStore.sleepTimer.type === 'time') {
|
||
if (!sleepTimerInterval) {
|
||
sleepTimerInterval = setInterval(() => {
|
||
sleepTimerRefresh.value = Date.now();
|
||
}, 1000);
|
||
}
|
||
} else {
|
||
if (sleepTimerInterval) {
|
||
clearInterval(sleepTimerInterval);
|
||
sleepTimerInterval = null;
|
||
}
|
||
}
|
||
},
|
||
{ immediate: true }
|
||
);
|
||
|
||
// 播放模式
|
||
const { playMode, playModeIcon, playModeText, togglePlayMode: togglePlayModeBase } = usePlayMode();
|
||
// 打开播放列表
|
||
const showPlaylist = () => {
|
||
playerStore.setPlayListDrawerVisible(true);
|
||
};
|
||
|
||
// 喜欢歌曲
|
||
const isFavorite = computed(() => {
|
||
return playerStore.favoriteList.includes(playMusic.value.id as number);
|
||
});
|
||
|
||
const toggleFavorite = () => {
|
||
if (isFavorite.value) {
|
||
playerStore.removeFromFavorite(playMusic.value.id as number);
|
||
} else {
|
||
playerStore.addToFavorite(playMusic.value.id as number);
|
||
}
|
||
};
|
||
|
||
// 歌词全屏控制
|
||
const showFullLyrics = ref(false);
|
||
const isAutoScrollEnabled = ref(true);
|
||
const lyricsScrollerRef = ref<HTMLElement | null>(null);
|
||
const isTouchScrolling = ref(false);
|
||
const touchStartY = ref(0);
|
||
const lastScrollTop = ref(0);
|
||
const autoScrollTimer = ref<number | null>(null);
|
||
const isSongChanging = ref(false);
|
||
|
||
// 横屏检测相关
|
||
const { width, height } = useWindowSize();
|
||
const isLandscape = computed(() => width.value > height.value);
|
||
const landscapeLyricsRef = ref<HTMLElement | null>(null);
|
||
|
||
// 监听横屏变化
|
||
watch(isLandscape, (newVal) => {
|
||
if (newVal) {
|
||
// 横屏模式下,确保歌词容器可见并滚动到当前歌词
|
||
nextTick(() => {
|
||
setTimeout(() => {
|
||
scrollToCurrentLyric(true, landscapeLyricsRef.value);
|
||
}, 300);
|
||
});
|
||
}
|
||
});
|
||
|
||
// 显示全屏歌词
|
||
// 显示全屏歌词
|
||
const showFullLyricScreen = () => {
|
||
showFullLyrics.value = true;
|
||
|
||
// 使用多次延迟尝试滚动,确保能够滚动到当前歌词
|
||
nextTick(() => {
|
||
scrollToCurrentLyric(true);
|
||
|
||
setTimeout(() => {
|
||
scrollToCurrentLyric(true);
|
||
}, 200);
|
||
|
||
setTimeout(() => {
|
||
scrollToCurrentLyric(true);
|
||
}, 500);
|
||
});
|
||
};
|
||
|
||
const supportAutoScroll = computed(() => {
|
||
return lrcArray.value.length > 0 && lrcArray.value[0].startTime !== -1;
|
||
});
|
||
|
||
// 关闭全屏歌词
|
||
const closeFullLyrics = () => {
|
||
showFullLyrics.value = false;
|
||
if (autoScrollTimer.value) {
|
||
clearTimeout(autoScrollTimer.value);
|
||
autoScrollTimer.value = null;
|
||
}
|
||
};
|
||
|
||
// 滚动到当前歌词,添加错误处理和日志
|
||
const scrollToCurrentLyric = (immediate = false, customScrollerRef?: HTMLElement | null) => {
|
||
try {
|
||
const scrollerRef = customScrollerRef || lyricsScrollerRef.value;
|
||
if (!scrollerRef) {
|
||
console.log('歌词容器引用不存在');
|
||
return;
|
||
}
|
||
|
||
if (!supportAutoScroll.value) {
|
||
console.log('歌词不支持自动滚动');
|
||
return;
|
||
}
|
||
|
||
// 如果用户正在手动滚动,不打断他们的操作
|
||
if (isTouchScrolling.value && !immediate) {
|
||
return;
|
||
}
|
||
|
||
const prefix = customScrollerRef ? 'landscape-' : '';
|
||
const activeEl = document.getElementById(`${prefix}lyric-line-${nowIndex.value}`);
|
||
if (!activeEl) {
|
||
console.log(`找不到当前歌词元素: ${prefix}lyric-line-${nowIndex.value}`);
|
||
return;
|
||
}
|
||
|
||
const containerRect = scrollerRef.getBoundingClientRect();
|
||
const lineRect = activeEl.getBoundingClientRect();
|
||
|
||
// 优化滚动位置计算,确保当前歌词在视图中央
|
||
const scrollTop =
|
||
scrollerRef.scrollTop +
|
||
(lineRect.top - containerRect.top) -
|
||
containerRect.height / 2 +
|
||
lineRect.height / 2;
|
||
|
||
console.log(`滚动到歌词 #${nowIndex.value}, 位置: ${scrollTop}px`);
|
||
|
||
scrollerRef.scrollTo({
|
||
top: scrollTop,
|
||
behavior: immediate ? 'auto' : 'smooth'
|
||
});
|
||
} catch (err) {
|
||
console.error('滚动歌词出错:', err);
|
||
}
|
||
};
|
||
|
||
// 监听歌词变化,自动滚动
|
||
watch(nowIndex, (newIndex, oldIndex) => {
|
||
console.log(`歌词索引变化: ${oldIndex} -> ${newIndex}`);
|
||
|
||
// 歌曲切换时不自动滚动
|
||
if (isSongChanging.value) return;
|
||
|
||
// 在竖屏全屏歌词模式下滚动
|
||
if (showFullLyrics.value) {
|
||
nextTick(() => {
|
||
scrollToCurrentLyric(false);
|
||
});
|
||
}
|
||
// 在横屏模式下滚动
|
||
else if (isLandscape.value) {
|
||
nextTick(() => {
|
||
scrollToCurrentLyric(false, landscapeLyricsRef.value);
|
||
});
|
||
}
|
||
});
|
||
|
||
// 当显示状态变化时,触发滚动
|
||
watch(showFullLyrics, (newVal) => {
|
||
if (newVal) {
|
||
nextTick(() => {
|
||
setTimeout(() => {
|
||
scrollToCurrentLyric(true);
|
||
}, 300);
|
||
});
|
||
}
|
||
});
|
||
|
||
// 监听音乐播放时间变化,触发歌词滚动更新
|
||
watch(nowTime, () => {
|
||
// 只有当系统不是由于用户手动拖动进度条而更新时间时才触发滚动
|
||
if (!isThumbDragging.value && !isTouchScrolling.value) {
|
||
// 在竖屏全屏歌词模式下滚动
|
||
if (showFullLyrics.value) {
|
||
scrollToCurrentLyric(false);
|
||
}
|
||
// 在横屏模式下滚动
|
||
else if (isLandscape.value) {
|
||
scrollToCurrentLyric(false, landscapeLyricsRef.value);
|
||
}
|
||
}
|
||
});
|
||
|
||
// 处理滚动事件
|
||
const handleScroll = () => {
|
||
if (!isTouchScrolling.value) return;
|
||
|
||
// 用户手动滚动时,临时停止自动滚动
|
||
isAutoScrollEnabled.value = false;
|
||
|
||
// 清除之前的计时器
|
||
if (autoScrollTimer.value) {
|
||
clearTimeout(autoScrollTimer.value);
|
||
}
|
||
|
||
// 设置新的计时器,3秒后恢复自动滚动
|
||
autoScrollTimer.value = window.setTimeout(() => {
|
||
isAutoScrollEnabled.value = true;
|
||
isTouchScrolling.value = false;
|
||
|
||
// 滚动到当前歌词
|
||
if (showFullLyrics.value) {
|
||
scrollToCurrentLyric(false);
|
||
} else if (isLandscape.value) {
|
||
scrollToCurrentLyric(false, landscapeLyricsRef.value);
|
||
}
|
||
}, 3000);
|
||
};
|
||
|
||
// 触摸相关事件
|
||
const handleTouchStart = (e: TouchEvent) => {
|
||
touchStartY.value = e.touches[0].clientY;
|
||
|
||
// 根据当前模式获取正确的滚动容器
|
||
const scrollerRef = showFullLyrics.value
|
||
? lyricsScrollerRef.value
|
||
: isLandscape.value
|
||
? landscapeLyricsRef.value
|
||
: lyricsScrollerRef.value;
|
||
|
||
lastScrollTop.value = scrollerRef?.scrollTop || 0;
|
||
isTouchScrolling.value = true;
|
||
|
||
// 用户开始触摸时,暂时停止自动滚动
|
||
isAutoScrollEnabled.value = false;
|
||
|
||
// 清除之前可能存在的计时器
|
||
if (autoScrollTimer.value) {
|
||
clearTimeout(autoScrollTimer.value);
|
||
autoScrollTimer.value = null;
|
||
}
|
||
};
|
||
|
||
const handleTouchMove = () => {
|
||
if (!isTouchScrolling.value) return;
|
||
// 实际的滚动处理由浏览器默认行为完成
|
||
};
|
||
|
||
const handleTouchEnd = () => {
|
||
// 设置计时器,3秒后恢复自动滚动
|
||
if (autoScrollTimer.value) {
|
||
clearTimeout(autoScrollTimer.value);
|
||
}
|
||
|
||
autoScrollTimer.value = window.setTimeout(() => {
|
||
isAutoScrollEnabled.value = true;
|
||
isTouchScrolling.value = false;
|
||
|
||
// 恢复自动滚动到当前歌词
|
||
if (showFullLyrics.value) {
|
||
scrollToCurrentLyric(true);
|
||
} else if (isLandscape.value) {
|
||
scrollToCurrentLyric(true, landscapeLyricsRef.value);
|
||
}
|
||
}, 3000);
|
||
};
|
||
|
||
// 封面样式循环切换
|
||
const cycleCoverStyle = () => {
|
||
const styles = ['record', 'square', 'full'];
|
||
const currentIdx = styles.indexOf(config.value.mobileCoverStyle);
|
||
const nextIdx = (currentIdx + 1) % styles.length;
|
||
config.value.mobileCoverStyle = styles[nextIdx] as 'record' | 'square' | 'full';
|
||
|
||
// 添加动画反馈
|
||
const container = document.querySelector('.cover-container');
|
||
if (container) {
|
||
container.classList.add('style-changing');
|
||
setTimeout(() => {
|
||
container.classList.remove('style-changing');
|
||
}, 500);
|
||
}
|
||
};
|
||
|
||
// 进度条相关
|
||
const isThumbDragging = ref(false);
|
||
const progressContainerWidth = ref(0);
|
||
|
||
// 鼠标拖动进度条相关变量
|
||
const isMouseDragging = ref(false);
|
||
|
||
// 处理进度条点击
|
||
const handleProgressBarClick = (e: MouseEvent) => {
|
||
if (!sound.value) return;
|
||
|
||
e.stopPropagation(); // 阻止事件冒泡
|
||
const progressBar = e.currentTarget as HTMLElement;
|
||
const rect = progressBar.getBoundingClientRect();
|
||
const offsetX = e.clientX - rect.left;
|
||
progressContainerWidth.value = rect.width;
|
||
|
||
const percentage = offsetX / rect.width;
|
||
const newTime = Math.max(0, Math.min(percentage * allTime.value, allTime.value));
|
||
|
||
console.log(`进度条点击: ${percentage.toFixed(2)}, 新时间: ${newTime.toFixed(2)}`);
|
||
|
||
audioService.seek(newTime);
|
||
nowTime.value = newTime;
|
||
};
|
||
|
||
// 鼠标按下事件
|
||
const handleMouseDown = (e: MouseEvent) => {
|
||
if (e.button !== 0) return; // 只处理左键点击
|
||
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
isMouseDragging.value = true;
|
||
|
||
// 立即更新位置
|
||
const progressBar = (e.currentTarget as HTMLElement).closest(
|
||
'.apple-style-progress'
|
||
) as HTMLElement;
|
||
if (progressBar) {
|
||
const rect = progressBar.getBoundingClientRect();
|
||
const offsetX = e.clientX - rect.left;
|
||
const percentage = Math.max(0, Math.min(1, offsetX / rect.width));
|
||
const newTime = percentage * allTime.value;
|
||
|
||
nowTime.value = newTime;
|
||
console.log(`鼠标按下,位置: ${percentage.toFixed(2)}, 时间: ${newTime.toFixed(2)}秒`);
|
||
}
|
||
|
||
// 添加全局鼠标事件监听
|
||
document.addEventListener('mousemove', handleMouseMove);
|
||
document.addEventListener('mouseup', handleMouseUp);
|
||
};
|
||
|
||
// 鼠标移动事件
|
||
const handleMouseMove = (e: MouseEvent) => {
|
||
if (!isMouseDragging.value || !sound.value) return;
|
||
|
||
e.preventDefault();
|
||
|
||
// 查找当前视图中的进度条元素
|
||
const progressBar = isLandscape.value
|
||
? document.querySelector('.landscape-left-section .apple-style-progress')
|
||
: document.querySelector('.unified-controls .apple-style-progress');
|
||
|
||
if (!progressBar) return;
|
||
|
||
const rect = (progressBar as HTMLElement).getBoundingClientRect();
|
||
const offsetX = e.clientX - rect.left;
|
||
const percentage = Math.max(0, Math.min(1, offsetX / rect.width));
|
||
const newTime = percentage * allTime.value;
|
||
|
||
nowTime.value = newTime;
|
||
console.log(`鼠标移动,位置: ${percentage.toFixed(2)}, 时间: ${newTime.toFixed(2)}秒`);
|
||
};
|
||
|
||
// 鼠标释放事件
|
||
const handleMouseUp = (e: MouseEvent) => {
|
||
if (!isMouseDragging.value || !sound.value) return;
|
||
|
||
e.preventDefault();
|
||
|
||
// 释放时跳转到指定位置
|
||
audioService.seek(nowTime.value);
|
||
console.log(`鼠标释放,跳转到: ${nowTime.value.toFixed(2)}秒`);
|
||
|
||
isMouseDragging.value = false;
|
||
|
||
// 移除全局事件监听
|
||
document.removeEventListener('mousemove', handleMouseMove);
|
||
document.removeEventListener('mouseup', handleMouseUp);
|
||
};
|
||
|
||
// 处理滑块拖动
|
||
const handleThumbTouchStart = (e: TouchEvent) => {
|
||
e.preventDefault(); // 阻止默认行为
|
||
e.stopPropagation(); // 阻止事件冒泡
|
||
isThumbDragging.value = true;
|
||
|
||
// 获取进度条宽度
|
||
const target = e.currentTarget as HTMLElement;
|
||
const progressBar = target.parentElement?.parentElement as HTMLElement;
|
||
if (progressBar) {
|
||
progressContainerWidth.value = progressBar.getBoundingClientRect().width;
|
||
console.log(`进度条宽度: ${progressContainerWidth.value}px`);
|
||
}
|
||
};
|
||
|
||
const handleThumbTouchMove = (e: TouchEvent) => {
|
||
if (!isThumbDragging.value || !sound.value) return;
|
||
|
||
e.preventDefault(); // 阻止默认行为
|
||
|
||
const touch = e.touches[0];
|
||
const target = e.currentTarget as HTMLElement;
|
||
const progressBar = target.parentElement?.parentElement as HTMLElement;
|
||
const rect = progressBar.getBoundingClientRect();
|
||
const offsetX = touch.clientX - rect.left;
|
||
|
||
// 计算百分比并限制在0-1之间
|
||
const percentage = Math.max(0, Math.min(1, offsetX / rect.width));
|
||
const newTime = percentage * allTime.value;
|
||
|
||
// 实时更新UI,但不频繁seek
|
||
nowTime.value = newTime;
|
||
|
||
console.log(`thumb拖动: ${percentage.toFixed(2)}, 时间: ${newTime.toFixed(2)}`);
|
||
};
|
||
|
||
const handleThumbTouchEnd = (e: TouchEvent) => {
|
||
if (!isThumbDragging.value || !sound.value) return;
|
||
|
||
e.preventDefault(); // 阻止默认行为
|
||
e.stopPropagation(); // 阻止事件冒泡
|
||
|
||
// 拖动结束时执行seek操作
|
||
console.log(`拖动结束,跳转到: ${nowTime.value.toFixed(2)}秒`);
|
||
audioService.seek(nowTime.value);
|
||
isThumbDragging.value = false;
|
||
};
|
||
|
||
// 背景相关(由 composable 管理)
|
||
const { isDark, applyBackground } = useLyricBackground({
|
||
writeBgColor: () => playerStore.playMusic.primaryColor || undefined
|
||
});
|
||
const config = ref<LyricConfig>({ ...DEFAULT_LYRIC_CONFIG });
|
||
|
||
// 可见歌词计算
|
||
const visibleLyrics = computed(() => {
|
||
const centerIndex = nowIndex.value;
|
||
const numLines = 3;
|
||
const halfLines = Math.floor(numLines / 2);
|
||
|
||
let startIdx = centerIndex - halfLines;
|
||
let endIdx = centerIndex + halfLines;
|
||
|
||
// 处理奇偶数行数的情况
|
||
if (numLines % 2 === 0) {
|
||
endIdx -= 1;
|
||
}
|
||
|
||
// 处理边界情况
|
||
if (startIdx < 0) {
|
||
startIdx = 0;
|
||
endIdx = Math.min(numLines - 1, lrcArray.value.length - 1);
|
||
}
|
||
|
||
if (endIdx >= lrcArray.value.length) {
|
||
endIdx = lrcArray.value.length - 1;
|
||
startIdx = Math.max(0, endIdx - numLines + 1);
|
||
}
|
||
|
||
// 返回带有原始索引的歌词数组
|
||
return lrcArray.value.slice(startIdx, endIdx + 1).map((item, idx) => ({
|
||
...item,
|
||
originalIndex: startIdx + idx
|
||
}));
|
||
});
|
||
|
||
const props = defineProps({
|
||
modelValue: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
background: {
|
||
type: String,
|
||
default: ''
|
||
}
|
||
});
|
||
|
||
const themeMusic = {
|
||
light: 'linear-gradient(to bottom, #ffffff, #f5f5f5)',
|
||
dark: 'linear-gradient(to bottom, #1a1a1a, #000000)'
|
||
};
|
||
|
||
const emit = defineEmits(['update:modelValue']);
|
||
|
||
const isVisible = computed({
|
||
get: () => props.modelValue,
|
||
set: (value) => emit('update:modelValue', value)
|
||
});
|
||
|
||
const targetBackground = computed(() => {
|
||
if (config.value.theme !== 'default') {
|
||
return themeMusic[config.value.theme] || props.background;
|
||
}
|
||
return props.background;
|
||
});
|
||
|
||
// 监听目标背景变化并更新文字颜色
|
||
watch(
|
||
targetBackground,
|
||
(newBg) => {
|
||
if (newBg) {
|
||
applyBackground(newBg);
|
||
}
|
||
},
|
||
{ immediate: true }
|
||
);
|
||
|
||
// 组件卸载清理
|
||
onBeforeUnmount(() => {
|
||
if (autoScrollTimer.value) {
|
||
clearTimeout(autoScrollTimer.value);
|
||
}
|
||
|
||
// 清理鼠标事件监听
|
||
document.removeEventListener('mousemove', handleMouseMove);
|
||
document.removeEventListener('mouseup', handleMouseUp);
|
||
});
|
||
|
||
const { navigateToArtist } = useArtist();
|
||
|
||
const handleArtistClick = (id: number) => {
|
||
isVisible.value = false;
|
||
navigateToArtist(id);
|
||
};
|
||
|
||
// 播放控制功能
|
||
const togglePlay = () => {
|
||
try {
|
||
playerStore.setPlay(playMusic.value);
|
||
} catch (error) {
|
||
console.error('播放出错:', error);
|
||
}
|
||
};
|
||
|
||
const nextSong = () => {
|
||
playerStore.nextPlay();
|
||
};
|
||
|
||
const prevSong = () => {
|
||
playerStore.prevPlay();
|
||
};
|
||
|
||
const togglePlayMode = () => {
|
||
togglePlayModeBase();
|
||
showBottomToast(playModeText.value);
|
||
};
|
||
|
||
const closeMusicFull = () => {
|
||
isVisible.value = false;
|
||
playerStore.setMusicFull(false);
|
||
};
|
||
|
||
// 添加对 playMusic.id 的监听,歌曲切换时滚动到顶部
|
||
watch(
|
||
() => playMusic.value.id,
|
||
(newId, oldId) => {
|
||
// 只在歌曲真正切换时滚动到顶部
|
||
if (newId !== oldId && newId) {
|
||
isSongChanging.value = true;
|
||
// 延迟滚动,确保 nowIndex 已重置
|
||
setTimeout(() => {
|
||
// 在全屏歌词模式下滚动到顶部
|
||
if (showFullLyrics.value && lyricsScrollerRef.value) {
|
||
lyricsScrollerRef.value.scrollTo({
|
||
top: 0,
|
||
behavior: 'smooth'
|
||
});
|
||
}
|
||
// 在横屏模式下滚动到顶部
|
||
else if (isLandscape.value && landscapeLyricsRef.value) {
|
||
landscapeLyricsRef.value.scrollTo({
|
||
top: 0,
|
||
behavior: 'smooth'
|
||
});
|
||
}
|
||
// 延迟恢复自动滚动,等待歌词数据更新
|
||
setTimeout(() => {
|
||
isSongChanging.value = false;
|
||
}, 300);
|
||
}, 100);
|
||
}
|
||
}
|
||
);
|
||
|
||
// 加载保存的配置
|
||
onMounted(() => {
|
||
const savedConfig = localStorage.getItem('music-full-config');
|
||
if (savedConfig) {
|
||
config.value = { ...config.value, ...JSON.parse(savedConfig) };
|
||
}
|
||
|
||
// 初始化自动滚动状态
|
||
isAutoScrollEnabled.value = true;
|
||
isTouchScrolling.value = false;
|
||
|
||
// 等待DOM元素渲染完成后初始化歌词滚动
|
||
nextTick(() => {
|
||
if (isVisible.value) {
|
||
// 在横屏模式下
|
||
if (isLandscape.value) {
|
||
setTimeout(() => {
|
||
scrollToCurrentLyric(true, landscapeLyricsRef.value);
|
||
}, 500);
|
||
}
|
||
// 在全屏歌词模式下
|
||
else if (showFullLyrics.value) {
|
||
setTimeout(() => {
|
||
scrollToCurrentLyric(true);
|
||
}, 500);
|
||
}
|
||
}
|
||
});
|
||
});
|
||
|
||
// 当显示状态变化时,更新封面与背景融合效果
|
||
watch(isVisible, (newVal) => {
|
||
if (newVal) {
|
||
// 播放器显示时,重新设置背景颜色
|
||
if (targetBackground.value) {
|
||
applyBackground(targetBackground.value);
|
||
}
|
||
} else {
|
||
showFullLyrics.value = false;
|
||
if (autoScrollTimer.value) {
|
||
clearTimeout(autoScrollTimer.value);
|
||
autoScrollTimer.value = null;
|
||
}
|
||
}
|
||
});
|
||
|
||
// 添加getLrcStyle函数
|
||
const { getLrcStyle: originalLrcStyle } = useLyricProgress();
|
||
|
||
// 修改 getLrcStyle 函数
|
||
const getLrcStyle = (index: number) => {
|
||
const colors = textColors.value || getTextColors;
|
||
const originalStyle = originalLrcStyle(index);
|
||
|
||
if (index === nowIndex.value) {
|
||
// 当前播放的歌词,使用渐变效果
|
||
// 只有原始样式包含 backgroundImage 时才设置 color: transparent
|
||
// 否则前奏阶段文字会因 transparent 而不可见
|
||
if (originalStyle.backgroundImage) {
|
||
return {
|
||
...originalStyle,
|
||
backgroundImage: originalStyle.backgroundImage
|
||
.replace(/#ffffff/g, colors.active)
|
||
.replace(/#ffffff8a/g, `${colors.primary}`),
|
||
backgroundClip: 'text',
|
||
WebkitBackgroundClip: 'text',
|
||
color: 'transparent'
|
||
};
|
||
}
|
||
// 当前行但播放时间未到(前奏/间奏),用高亮色显示
|
||
return {
|
||
color: colors.active
|
||
};
|
||
}
|
||
|
||
// 非当前播放的歌词,使用普通颜色
|
||
return {
|
||
color: colors.primary
|
||
};
|
||
};
|
||
|
||
// 逐字歌词样式函数
|
||
const getWordStyle = (lineIndex: number, _wordIndex: number, word: any) => {
|
||
const colors = textColors.value || getTextColors();
|
||
// 如果不是当前行,返回普通样式
|
||
if (lineIndex !== nowIndex.value) {
|
||
return {
|
||
color: colors.primary,
|
||
transition: 'color 0.3s ease',
|
||
// 重置背景相关属性
|
||
backgroundImage: 'none',
|
||
WebkitTextFillColor: 'initial'
|
||
};
|
||
}
|
||
|
||
// 当前行的逐字效果,应用歌词矫正时间
|
||
const currentTime = (nowTime.value + correctionTime.value) * 1000; // 转换为毫秒,确保与word时间单位一致
|
||
|
||
// 直接使用绝对时间比较
|
||
const wordStartTime = word.startTime; // 单词开始的绝对时间(毫秒)
|
||
const wordEndTime = word.startTime + word.duration;
|
||
|
||
if (currentTime >= wordStartTime && currentTime < wordEndTime) {
|
||
// 当前正在播放的单词 - 使用渐变进度效果
|
||
const progress = Math.min((currentTime - wordStartTime) / word.duration, 1);
|
||
const progressPercent = Math.round(progress * 100);
|
||
|
||
return {
|
||
backgroundImage: `linear-gradient(to right, ${colors.active} 0%, ${colors.active} ${progressPercent}%, ${colors.primary} ${progressPercent}%, ${colors.primary} 100%)`,
|
||
backgroundClip: 'text',
|
||
WebkitBackgroundClip: 'text',
|
||
WebkitTextFillColor: 'transparent',
|
||
textShadow: `0 0 8px ${colors.active}40`,
|
||
transition: 'all 0.1s ease'
|
||
};
|
||
} else if (currentTime >= wordEndTime) {
|
||
// 已经播放过的单词 - 纯色显示
|
||
return {
|
||
color: colors.active,
|
||
WebkitTextFillColor: 'initial',
|
||
transition: 'none'
|
||
};
|
||
} else {
|
||
// 还未播放的单词 - 普通状态
|
||
return {
|
||
color: colors.primary,
|
||
WebkitTextFillColor: 'initial',
|
||
transition: 'none'
|
||
};
|
||
}
|
||
};
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
#mobile-drawer-target {
|
||
@apply top-0 left-0 absolute overflow-hidden flex flex-col w-full h-full;
|
||
animation-duration: 300ms;
|
||
|
||
// 通用控制按钮样式
|
||
.main-button {
|
||
@apply flex items-center justify-center cursor-pointer transition-all duration-200 rounded-full;
|
||
|
||
i {
|
||
@apply text-2xl;
|
||
color: var(--text-color-active);
|
||
}
|
||
|
||
&.play-pause {
|
||
i {
|
||
@apply text-4xl;
|
||
}
|
||
}
|
||
|
||
&:hover {
|
||
transform: scale(1.05);
|
||
}
|
||
|
||
&:active {
|
||
transform: scale(0.95);
|
||
}
|
||
}
|
||
|
||
// 通用进度条样式
|
||
.apple-style-progress {
|
||
@apply relative flex items-center cursor-pointer;
|
||
touch-action: none; // 确保触摸事件正常工作
|
||
|
||
.progress-track {
|
||
@apply relative w-full h-2 bg-white bg-opacity-20 rounded-full;
|
||
|
||
.progress-fill {
|
||
@apply absolute top-0 left-0 h-full bg-white rounded-full;
|
||
box-shadow: 0 0 8px rgba(255, 255, 255, 0.5);
|
||
z-index: 1;
|
||
transition: width 0.1s linear;
|
||
}
|
||
|
||
.progress-thumb {
|
||
@apply absolute top-1/2 -translate-y-1/2 -translate-x-1/2 rounded-full bg-white;
|
||
box-shadow: 0 0 8px rgba(255, 255, 255, 0.6);
|
||
z-index: 2;
|
||
transition: transform 0.15s ease-out;
|
||
|
||
&.active {
|
||
transform: translate(-50%, -50%) scale(1.3);
|
||
box-shadow: 0 0 12px rgba(255, 255, 255, 0.9);
|
||
}
|
||
|
||
&:active {
|
||
transform: translate(-50%, -50%) scale(1.3);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 通用唱片样式
|
||
.record-style-common {
|
||
@apply rounded-full overflow-hidden relative;
|
||
aspect-ratio: 1/1;
|
||
|
||
&::before {
|
||
content: '';
|
||
@apply absolute top-0 left-0 w-full h-full rounded-full z-10;
|
||
background: radial-gradient(
|
||
circle at center,
|
||
transparent 38%,
|
||
rgba(0, 0, 0, 0.15) 38%,
|
||
rgba(0, 0, 0, 0.15) 39%,
|
||
rgba(255, 255, 255, 0.1) 39%,
|
||
rgba(255, 255, 255, 0.1) 39.5%,
|
||
rgba(0, 0, 0, 0.08) 39.5%,
|
||
rgba(0, 0, 0, 0.08) 40.5%,
|
||
rgba(0, 0, 0, 0.2) 40.5%,
|
||
rgba(0, 0, 0, 0.2) 41.5%,
|
||
rgba(0, 0, 0, 0.6) 41.5%,
|
||
rgba(0, 0, 0, 0.6) 100%
|
||
);
|
||
pointer-events: none;
|
||
animation: spin 20s linear infinite;
|
||
animation-play-state: running;
|
||
}
|
||
|
||
&::after {
|
||
content: '';
|
||
@apply absolute w-6 h-6 rounded-full bg-gray-900 z-20;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.4);
|
||
}
|
||
|
||
&.paused {
|
||
&::before,
|
||
&::after {
|
||
animation-play-state: paused;
|
||
}
|
||
}
|
||
|
||
.img-wrapper {
|
||
@apply rounded-full overflow-hidden border-solid border-black z-0;
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
|
||
&::after {
|
||
content: '';
|
||
@apply absolute top-0 left-0 w-full h-full rounded-full z-[2];
|
||
background: linear-gradient(
|
||
135deg,
|
||
rgba(255, 255, 255, 0.05) 0%,
|
||
rgba(255, 255, 255, 0) 50%,
|
||
rgba(0, 0, 0, 0.05) 100%
|
||
);
|
||
pointer-events: none;
|
||
}
|
||
}
|
||
|
||
.cover-image {
|
||
@apply w-full h-full rounded-full border-[2px] border-gray-900;
|
||
animation: spin 20s linear infinite;
|
||
animation-play-state: running;
|
||
}
|
||
|
||
&.paused .cover-image {
|
||
animation-play-state: paused;
|
||
}
|
||
}
|
||
|
||
// 通用时间显示样式
|
||
.time-info {
|
||
@apply flex justify-between items-center mb-2;
|
||
|
||
.current-time,
|
||
.total-time {
|
||
@apply text-sm;
|
||
color: var(--text-color-primary);
|
||
opacity: 0.8;
|
||
}
|
||
}
|
||
|
||
// 通用收藏按钮样式
|
||
.favorite-icon {
|
||
@apply cursor-pointer transition-all duration-200;
|
||
|
||
i {
|
||
@apply text-xl;
|
||
color: var(--text-color-primary);
|
||
|
||
&.favorite {
|
||
@apply text-red-500 !important;
|
||
}
|
||
}
|
||
|
||
&:hover {
|
||
transform: scale(1.1);
|
||
}
|
||
|
||
&:active {
|
||
transform: scale(0.9);
|
||
}
|
||
|
||
&.landscape {
|
||
i {
|
||
@apply text-3xl;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 通用歌曲信息样式
|
||
.song-info-common {
|
||
@apply z-[9995];
|
||
|
||
.song-title {
|
||
@apply font-bold line-clamp-1;
|
||
color: var(--text-color-active);
|
||
}
|
||
|
||
.song-artist {
|
||
@apply font-medium line-clamp-1;
|
||
color: var(--text-color-primary);
|
||
opacity: 0.9;
|
||
|
||
.artist-name {
|
||
@apply cursor-pointer;
|
||
|
||
&:hover {
|
||
@apply underline;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 横屏模式布局
|
||
&.is-landscape {
|
||
.landscape-layout {
|
||
@apply flex flex-row w-full h-full overflow-hidden px-8 gap-4;
|
||
|
||
// 左侧区域
|
||
.landscape-left-section {
|
||
@apply h-full flex flex-col items-center justify-center pt-6 pb-6 px-3 relative;
|
||
width: 35%;
|
||
min-width: 320px;
|
||
max-width: 480px;
|
||
|
||
// 封面
|
||
.landscape-cover-container {
|
||
@apply flex-shrink-0 mx-auto mb-4 z-[9995];
|
||
width: 85%;
|
||
max-width: 260px;
|
||
min-width: 180px;
|
||
|
||
&.record-style {
|
||
@extend .record-style-common;
|
||
|
||
.img-wrapper {
|
||
@apply border-[20px];
|
||
width: 90%;
|
||
height: 90%;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 左侧进度条
|
||
.landscape-progress-container {
|
||
@apply mt-0 mb-2 px-2 w-full max-w-md;
|
||
|
||
.apple-style-progress {
|
||
height: 48px; // 增加高度使更容易点击
|
||
|
||
.progress-thumb {
|
||
@apply w-5 h-5;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 右侧区域
|
||
.landscape-lyrics-section {
|
||
@apply h-full flex-1 flex flex-col relative;
|
||
|
||
// 歌曲信息
|
||
.landscape-song-info {
|
||
@apply flex justify-between items-center pt-5 z-[9995] px-4;
|
||
@extend .song-info-common;
|
||
|
||
.song-title {
|
||
@apply text-2xl mb-1;
|
||
}
|
||
|
||
.song-artist {
|
||
@apply text-base;
|
||
}
|
||
}
|
||
|
||
// 歌词滚动区域
|
||
.landscape-lyrics-scroller {
|
||
@apply h-full w-full overflow-y-auto pt-24 pb-24;
|
||
scroll-behavior: smooth;
|
||
-webkit-overflow-scrolling: touch;
|
||
mask-image: linear-gradient(
|
||
to bottom,
|
||
transparent 5%,
|
||
black 15%,
|
||
black 85%,
|
||
transparent 95%
|
||
);
|
||
-webkit-mask-image: linear-gradient(
|
||
to bottom,
|
||
transparent 5%,
|
||
black 15%,
|
||
black 85%,
|
||
transparent 95%
|
||
);
|
||
}
|
||
|
||
// 控制按钮
|
||
.landscape-main-controls {
|
||
@apply fixed bottom-6 right-6 flex items-center z-[10000];
|
||
|
||
.main-button {
|
||
@apply mx-2;
|
||
width: 54px;
|
||
height: 54px;
|
||
background-color: rgba(255, 255, 255, 0.15);
|
||
backdrop-filter: blur(8px);
|
||
border-radius: 50%;
|
||
|
||
&.play-pause {
|
||
width: 70px;
|
||
height: 70px;
|
||
background-color: rgba(255, 255, 255, 0.25);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 竖屏模式布局
|
||
&:not(.is-landscape) {
|
||
.ios-layout-container {
|
||
@apply flex flex-col items-center justify-between w-full h-full pt-10;
|
||
padding-bottom: 180px; // 为控制区域留出空间
|
||
|
||
// 封面样式
|
||
.cover-container {
|
||
@apply relative mb-6 transition-all duration-500 border-gray-900 z-[9995];
|
||
|
||
&.style-changing {
|
||
animation: styleChange 0.5s ease;
|
||
}
|
||
|
||
&.record-style {
|
||
@extend .record-style-common;
|
||
@apply w-72 h-72;
|
||
|
||
.img-wrapper {
|
||
@apply border-[40px];
|
||
width: 90%;
|
||
height: 90%;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 歌曲信息
|
||
.song-info {
|
||
@apply flex flex-col items-center mb-5 w-full z-[9995];
|
||
@extend .song-info-common;
|
||
|
||
.song-title-container {
|
||
@apply w-full text-center;
|
||
|
||
.song-title {
|
||
@apply text-2xl inline-block;
|
||
}
|
||
}
|
||
|
||
.song-artist {
|
||
@apply text-base mb-2;
|
||
}
|
||
|
||
.ri-heart-3-fill {
|
||
@apply text-2xl;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 统一控制区域
|
||
.unified-controls {
|
||
@apply fixed bottom-0 left-0 right-0 px-6 pt-6 pb-6;
|
||
background: linear-gradient(to top, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0) 100%);
|
||
height: 230px;
|
||
pointer-events: auto;
|
||
z-index: 10000 !important;
|
||
|
||
.progress-container {
|
||
@apply w-full mb-6;
|
||
|
||
.apple-style-progress {
|
||
height: 40px;
|
||
|
||
.progress-thumb {
|
||
@apply w-4 h-4;
|
||
}
|
||
}
|
||
}
|
||
|
||
.control-buttons {
|
||
@apply flex items-center justify-between w-full px-4;
|
||
|
||
.side-button {
|
||
@apply w-10 h-10 flex items-center justify-center cursor-pointer transition-all duration-200;
|
||
|
||
i {
|
||
@apply text-2xl;
|
||
color: var(--text-color-primary);
|
||
|
||
&.intelligence-active {
|
||
@apply text-green-500;
|
||
}
|
||
}
|
||
|
||
&:hover {
|
||
i {
|
||
color: var(--text-color-active);
|
||
}
|
||
}
|
||
}
|
||
|
||
.main-button {
|
||
@apply w-14 h-14;
|
||
|
||
i {
|
||
@apply text-3xl;
|
||
}
|
||
|
||
&.play-pause {
|
||
@apply w-16 h-16 bg-white/15 rounded-full backdrop-blur-sm;
|
||
|
||
i {
|
||
@apply text-4xl;
|
||
}
|
||
}
|
||
|
||
&:hover:not(.play-pause) {
|
||
i {
|
||
color: var(--text-color-active);
|
||
}
|
||
}
|
||
|
||
&.play-pause:hover {
|
||
@apply bg-white/30;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 旋转动画
|
||
@keyframes spin {
|
||
0% {
|
||
transform: rotate(0deg);
|
||
}
|
||
100% {
|
||
transform: rotate(360deg);
|
||
}
|
||
}
|
||
|
||
// 加载动画
|
||
.loading-overlay {
|
||
@apply absolute top-0 left-0 w-full h-full flex items-center justify-center;
|
||
background-color: rgba(0, 0, 0, 0.5);
|
||
z-index: 9999999999;
|
||
|
||
.loading-icon {
|
||
font-size: 36px;
|
||
color: white;
|
||
animation: spin 1s linear infinite;
|
||
}
|
||
}
|
||
|
||
// 根据封面样式调整容器布局
|
||
#mobile-drawer-target.cover-style-record {
|
||
.ios-layout-container .cover-container {
|
||
@apply mt-4;
|
||
}
|
||
}
|
||
|
||
#mobile-drawer-target.cover-style-full {
|
||
.ios-layout-container {
|
||
@apply pt-0;
|
||
}
|
||
}
|
||
|
||
// 过渡动画
|
||
.fade-enter-active,
|
||
.fade-leave-active {
|
||
transition: opacity 0.3s ease;
|
||
}
|
||
|
||
.fade-enter-from,
|
||
.fade-leave-to {
|
||
opacity: 0;
|
||
}
|
||
|
||
@keyframes styleChange {
|
||
0% {
|
||
opacity: 0.7;
|
||
transform: scale(0.95);
|
||
}
|
||
50% {
|
||
opacity: 0.9;
|
||
transform: scale(1.03);
|
||
}
|
||
100% {
|
||
opacity: 1;
|
||
transform: scale(1);
|
||
}
|
||
}
|
||
|
||
@keyframes clickPulse {
|
||
0% {
|
||
opacity: 0.5;
|
||
transform: scale(1);
|
||
}
|
||
50% {
|
||
opacity: 1;
|
||
transform: scale(1.1);
|
||
}
|
||
100% {
|
||
opacity: 1;
|
||
transform: scale(1);
|
||
}
|
||
}
|
||
|
||
@keyframes pulse {
|
||
0% {
|
||
opacity: 0.9;
|
||
}
|
||
50% {
|
||
opacity: 1;
|
||
}
|
||
100% {
|
||
opacity: 0.9;
|
||
}
|
||
}
|
||
|
||
.favorite-icon {
|
||
@apply cursor-pointer transition-all duration-200;
|
||
|
||
i {
|
||
@apply text-xl;
|
||
color: var(--text-color-primary);
|
||
|
||
&.favorite {
|
||
@apply text-red-500 !important;
|
||
}
|
||
}
|
||
|
||
&:hover {
|
||
transform: scale(1.1);
|
||
}
|
||
|
||
&:active {
|
||
transform: scale(0.9);
|
||
}
|
||
|
||
&.landscape {
|
||
@apply mt-2;
|
||
i {
|
||
@apply text-2xl;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 歌曲标题容器样式
|
||
.song-title-container {
|
||
@apply w-full flex items-center justify-center relative;
|
||
|
||
.song-title {
|
||
@apply text-center text-2xl font-bold max-w-[80%] truncate;
|
||
color: var(--text-color-active);
|
||
}
|
||
}
|
||
|
||
// 通用歌词样式
|
||
.lyric-line {
|
||
@apply cursor-pointer transition-all duration-300 font-medium;
|
||
font-weight: 500;
|
||
letter-spacing: var(--lyric-letter-spacing, 0);
|
||
line-height: var(--lyric-line-height, 1.6);
|
||
color: var(--text-color-primary);
|
||
opacity: 0.8;
|
||
|
||
&.no-scroll-tip {
|
||
@apply text-base opacity-60 cursor-default py-2;
|
||
color: var(--text-color-primary);
|
||
font-weight: normal;
|
||
|
||
span {
|
||
padding-right: 0;
|
||
}
|
||
}
|
||
|
||
span {
|
||
background-clip: text !important;
|
||
-webkit-background-clip: text !important;
|
||
}
|
||
|
||
&.now-text {
|
||
@apply font-medium py-4;
|
||
color: var(--text-color-active);
|
||
opacity: 1;
|
||
}
|
||
|
||
&.clicked {
|
||
animation: clickPulse 0.3s ease-in-out;
|
||
}
|
||
|
||
.translation {
|
||
@apply font-normal opacity-70 mt-1 text-base;
|
||
}
|
||
|
||
// 逐字歌词样式
|
||
.word-by-word-lyric {
|
||
@apply flex flex-wrap justify-center;
|
||
|
||
.lyric-word {
|
||
@apply inline-block;
|
||
font-weight: inherit;
|
||
font-size: inherit;
|
||
letter-spacing: inherit;
|
||
line-height: inherit;
|
||
cursor: inherit;
|
||
position: relative;
|
||
padding-right: 0 !important;
|
||
|
||
&:hover {
|
||
background-color: rgba(255, 255, 255, 0.1);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 全屏歌词相关样式
|
||
.fullscreen-lyrics {
|
||
@apply flex flex-col w-full h-full relative;
|
||
|
||
&.light {
|
||
background: linear-gradient(to bottom, #ffffff, #f5f5f5);
|
||
}
|
||
|
||
&.dark {
|
||
background: linear-gradient(to bottom, #1a1a1a, #000000);
|
||
}
|
||
|
||
.fullscreen-header {
|
||
@apply pt-16 pb-4 px-6 flex flex-col items-center fixed top-0 left-0 w-full z-10;
|
||
background: linear-gradient(to bottom, rgba(0, 0, 0, 0.2) 0%, rgba(0, 0, 0, 0) 100%);
|
||
pointer-events: auto;
|
||
|
||
.song-title {
|
||
@apply text-xl font-semibold text-center mb-1 max-w-full line-clamp-1;
|
||
color: var(--text-color-active);
|
||
}
|
||
|
||
.artist-name {
|
||
@apply text-sm text-opacity-80 text-center;
|
||
color: var(--text-color-primary);
|
||
}
|
||
}
|
||
|
||
.lyrics-scroller {
|
||
@apply flex-1 overflow-y-auto px-4;
|
||
scroll-behavior: smooth;
|
||
-webkit-overflow-scrolling: touch;
|
||
mask-image: linear-gradient(to bottom, transparent 0%, black 10%, black 90%, transparent 100%);
|
||
-webkit-mask-image: linear-gradient(
|
||
to bottom,
|
||
transparent 0%,
|
||
black 10%,
|
||
black 90%,
|
||
transparent 100%
|
||
);
|
||
padding-top: 100px;
|
||
padding-bottom: 200px;
|
||
margin-bottom: 180px;
|
||
margin-top: 90px;
|
||
|
||
.lyrics-padding-top {
|
||
height: 70px;
|
||
min-height: 70px;
|
||
}
|
||
|
||
.lyrics-padding-bottom {
|
||
height: 150px;
|
||
min-height: 150px;
|
||
}
|
||
|
||
.lyric-line {
|
||
@apply px-6 py-4 text-center;
|
||
font-size: var(--lyric-font-size, 22px);
|
||
|
||
span {
|
||
padding-right: 10px;
|
||
}
|
||
}
|
||
|
||
.now-text {
|
||
@apply text-2xl;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 必要的控制按钮样式
|
||
.control-btn {
|
||
@apply w-9 h-9 flex items-center justify-center rounded cursor-pointer transition-all duration-300 z-[9999];
|
||
background: rgba(142, 142, 142, 0.192);
|
||
backdrop-filter: blur(12px);
|
||
top: calc(var(--safe-area-inset-top, 0) + 20px);
|
||
|
||
i {
|
||
@apply text-xl;
|
||
color: var(--text-color-active);
|
||
}
|
||
|
||
&.pure-mode {
|
||
background: transparent;
|
||
backdrop-filter: none;
|
||
|
||
&:not(:hover) {
|
||
i {
|
||
opacity: 0;
|
||
}
|
||
}
|
||
}
|
||
|
||
&:hover {
|
||
background: rgba(126, 121, 121, 0.2);
|
||
i {
|
||
opacity: 1;
|
||
}
|
||
}
|
||
}
|
||
|
||
#mobile-drawer-target {
|
||
// 横屏模式下的歌词样式
|
||
&.is-landscape {
|
||
.landscape-lyrics-section {
|
||
.landscape-lyrics-scroller {
|
||
.lyrics-padding-top {
|
||
height: 30px;
|
||
min-height: 30px;
|
||
}
|
||
|
||
.lyrics-padding-bottom {
|
||
height: 100px;
|
||
min-height: 100px;
|
||
}
|
||
|
||
.lyric-line {
|
||
@apply px-4 py-3 text-left;
|
||
font-size: 26px;
|
||
}
|
||
|
||
.now-text {
|
||
@apply text-3xl;
|
||
}
|
||
}
|
||
}
|
||
|
||
.word-by-word-lyric {
|
||
@apply justify-start;
|
||
}
|
||
}
|
||
|
||
.unified-controls {
|
||
&.fullscreen-mode {
|
||
background: linear-gradient(to top, rgba(0, 0, 0, 0.8) 0%, rgba(0, 0, 0, 0) 100%);
|
||
}
|
||
|
||
.back-button {
|
||
@apply absolute top-4 left-1/2 -translate-x-1/2 w-10 h-10 flex items-center justify-center bg-black bg-opacity-30 rounded-2xl;
|
||
|
||
i {
|
||
@apply text-4xl;
|
||
color: var(--text-color-primary);
|
||
}
|
||
}
|
||
}
|
||
|
||
.ios-layout-container {
|
||
.lyrics-container {
|
||
@apply w-full flex-grow flex flex-col items-center justify-center mb-6 overflow-hidden cursor-pointer;
|
||
|
||
.lyrics-wrapper {
|
||
@apply w-full flex flex-col items-center justify-center;
|
||
|
||
.lyric-line {
|
||
@apply text-center py-1 transition-all duration-300 opacity-70;
|
||
|
||
&:nth-child(2) {
|
||
@apply text-lg font-medium opacity-100;
|
||
color: var(--text-color-active);
|
||
}
|
||
|
||
.translation {
|
||
@apply text-sm opacity-60 mt-1;
|
||
}
|
||
}
|
||
}
|
||
|
||
.lyric-word {
|
||
@apply px-[2px];
|
||
}
|
||
|
||
.no-lyrics {
|
||
@apply text-center text-base opacity-60;
|
||
color: var(--text-color-primary);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.cover-container {
|
||
// 方形封面样式
|
||
&.square-style {
|
||
@apply w-[85%] shadow-2xl shadow-black/50 rounded-xl overflow-hidden mt-8 aspect-square;
|
||
|
||
.cover-image {
|
||
@apply w-full h-full;
|
||
transition: transform 0.3s ease-out;
|
||
|
||
&:active {
|
||
transform: scale(0.95);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 全屏封面样式
|
||
&.full-style {
|
||
@apply w-full max-h-[50vh] relative overflow-hidden;
|
||
|
||
&::after {
|
||
content: '';
|
||
position: absolute;
|
||
bottom: 0;
|
||
left: 0;
|
||
right: 0;
|
||
height: 40%;
|
||
background: linear-gradient(
|
||
transparent,
|
||
var(--bg-color, rgba(25, 25, 25, 1)) 70%,
|
||
var(--bg-color, rgba(25, 25, 25, 1))
|
||
);
|
||
z-index: 1;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.cover-image {
|
||
@apply w-full h-auto shadow-lg;
|
||
|
||
&.full-blend {
|
||
mix-blend-mode: luminosity;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.is-dark {
|
||
.square-style {
|
||
@apply shadow-2xl shadow-black/50;
|
||
}
|
||
}
|
||
</style>
|