Files
AlgerMusicPlayer/src/renderer/layout/components/MusicFull.vue
T

733 lines
18 KiB
Vue
Raw Normal View History

2021-11-09 11:34:09 +08:00
<template>
<n-drawer
2025-01-13 22:55:46 +08:00
v-model:show="isVisible"
height="100%"
placement="bottom"
:style="{ background: currentBackground || background }"
:to="`#layout-main`"
:z-index="9998"
>
2025-01-18 03:25:21 +08:00
<div id="drawer-target" :class="[config.theme]">
<div
2025-01-18 03:25:21 +08:00
class="control-btn absolute top-8 left-8"
:class="{ 'pure-mode': config.pureModeEnabled }"
@click="closeMusicFull"
2025-01-18 03:25:21 +08:00
>
<i class="ri-arrow-down-s-line"></i>
</div>
<n-popover trigger="click" placement="bottom">
<template #trigger>
<div
class="control-btn absolute top-8 right-8"
:class="{ 'pure-mode': config.pureModeEnabled }"
>
<i class="ri-settings-3-line"></i>
</div>
</template>
<lyric-settings ref="lyricSettingsRef" />
</n-popover>
<div
v-show="!config.hideCover"
class="music-img"
:style="{ color: textColors.theme === 'dark' ? '#000000' : '#ffffff' }"
>
<n-image
ref="PicImgRef"
:src="getImgUrl(playMusic?.picUrl, '500y500')"
class="img"
lazy
preview-disabled
/>
<div>
<div class="music-content-name">{{ playMusic.name }}</div>
<div class="music-content-singer">
2025-01-13 22:55:46 +08:00
<n-ellipsis
class="text-ellipsis"
line-clamp="2"
:tooltip="{
contentStyle: { maxWidth: '600px' },
zIndex: 99999
}"
2025-01-13 22:13:46 +08:00
>
2025-01-13 22:55:46 +08:00
<span
v-for="(item, index) in artistList"
2025-01-13 22:55:46 +08:00
:key="index"
class="cursor-pointer hover:text-green-500"
@click="handleArtistClick(item.id)"
>
{{ item.name }}
{{ index < artistList.length - 1 ? ' / ' : '' }}
2025-01-13 22:55:46 +08:00
</span>
</n-ellipsis>
</div>
</div>
</div>
2025-01-18 03:25:21 +08:00
<div class="music-content" :class="{ center: config.centerLyrics && config.hideCover }">
<n-layout
ref="lrcSider"
class="music-lrc"
2025-01-18 03:25:21 +08:00
:style="{
height: config.hidePlayBar ? '85vh' : '65vh',
width: isMobile ? '100vw' : config.hideCover ? '50vw' : '500px'
2025-01-18 03:25:21 +08:00
}"
:native-scrollbar="false"
2025-01-04 00:26:30 +08:00
@mouseover="mouseOverLayout"
@mouseleave="mouseLeaveLayout"
>
2025-01-18 03:25:21 +08:00
<!-- 歌曲信息 -->
<div ref="lrcContainer" class="music-lrc-container">
<div
v-if="config.hideCover"
class="music-info-header"
:style="{ textAlign: config.centerLyrics ? 'center' : 'left' }"
>
<div class="music-info-name">{{ playMusic.name }}</div>
<div class="music-info-singer">
<span
v-for="(item, index) in artistList"
:key="index"
class="cursor-pointer hover:text-green-500"
@click="handleArtistClick(item.id)"
>
{{ item.name }}
{{ index < artistList.length - 1 ? ' / ' : '' }}
</span>
</div>
</div>
<div
v-for="(item, index) in lrcArray"
:id="`music-lrc-text-${index}`"
:key="index"
class="music-lrc-text"
:class="{ 'now-text': index === nowIndex, 'hover-text': item.text }"
@click="setAudioTime(index)"
>
<span :style="getLrcStyle(index)">{{ item.text }}</span>
2025-01-18 03:25:21 +08:00
<div v-show="config.showTranslation" class="music-lrc-text-tr">
{{ item.trText }}
</div>
</div>
<!-- 无歌词 -->
<div v-if="!lrcArray.length" class="music-lrc-text mt-40">
2025-02-19 01:01:43 +08:00
<span>{{ t('player.lrc.noLrc') }}</span>
</div>
</div>
</n-layout>
<!-- 时间矫正 -->
<!-- <div class="music-content-time">
2023-12-27 14:39:52 +08:00
<n-button @click="reduceCorrectionTime(0.2)">-</n-button>
<n-button @click="addCorrectionTime(0.2)">+</n-button>
</div> -->
2021-11-09 11:34:09 +08:00
</div>
</div>
2023-12-18 15:24:57 +08:00
</n-drawer>
2021-11-09 11:34:09 +08:00
</template>
<script setup lang="ts">
import { useDebounceFn } from '@vueuse/core';
2025-01-18 03:25:21 +08:00
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
2025-02-19 01:01:43 +08:00
import { useI18n } from 'vue-i18n';
2025-01-18 03:25:21 +08:00
import LyricSettings from '@/components/lyric/LyricSettings.vue';
import {
artistList,
lrcArray,
nowIndex,
playMusic,
setAudioTime,
textColors,
useLyricProgress
} from '@/hooks/MusicHook';
import { usePlayerStore } from '@/store';
2025-03-19 22:48:28 +08:00
import { useSettingsStore } from '@/store/modules/settings';
import { getImgUrl, isMobile } from '@/utils';
import { animateGradient, getHoverBackgroundColor, getTextColors } from '@/utils/linearColor';
2025-02-19 01:01:43 +08:00
const { t } = useI18n();
// 定义 refs
const lrcSider = ref<any>(null);
const isMouse = ref(false);
const lrcContainer = ref<HTMLElement | null>(null);
const currentBackground = ref('');
const animationFrame = ref<number | null>(null);
const isDark = ref(false);
2025-01-18 03:25:21 +08:00
const showStickyHeader = ref(false);
const lyricSettingsRef = ref<InstanceType<typeof LyricSettings>>();
interface LyricConfig {
hideCover: boolean;
centerLyrics: boolean;
fontSize: number;
letterSpacing: number;
lineHeight: number;
showTranslation: boolean;
theme: 'default' | 'light' | 'dark';
hidePlayBar: boolean;
pureModeEnabled: boolean;
}
// 移除 computed 配置
const config = ref<LyricConfig>({
hideCover: false,
centerLyrics: false,
fontSize: 22,
letterSpacing: 0,
lineHeight: 1.5,
showTranslation: true,
theme: 'default',
hidePlayBar: false,
pureModeEnabled: false
});
// 监听设置组件的配置变化
watch(
() => lyricSettingsRef.value?.config,
(newConfig) => {
if (newConfig) {
config.value = newConfig;
}
},
{ deep: true, immediate: true }
);
// 监听本地配置变化,保存到 localStorage
watch(
() => config.value,
(newConfig) => {
localStorage.setItem('music-full-config', JSON.stringify(newConfig));
if (lyricSettingsRef.value) {
lyricSettingsRef.value.config = newConfig;
}
},
{ deep: true }
);
2021-11-09 11:34:09 +08:00
const props = defineProps({
2025-01-13 22:55:46 +08:00
modelValue: {
type: Boolean,
default: false
2021-11-09 11:34:09 +08:00
},
background: {
type: String,
default: ''
}
});
2021-11-09 11:34:09 +08:00
2025-01-18 03:25:21 +08:00
const themeMusic = {
light: 'linear-gradient(to bottom, #ffffff, #f5f5f5)',
dark: 'linear-gradient(to bottom, #1a1a1a, #000000)'
};
2025-01-13 22:55:46 +08:00
const emit = defineEmits(['update:modelValue']);
const isVisible = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
});
2021-11-09 11:34:09 +08:00
// 歌词滚动方法
2025-01-19 13:35:10 +08:00
const lrcScroll = (behavior: ScrollBehavior = 'smooth', forceTop: boolean = false) => {
if (!isVisible.value || !lrcSider.value) return;
if (forceTop) {
lrcSider.value.scrollTo({
top: 0,
behavior
});
return;
}
if (isMouse.value) return;
2025-01-18 03:25:21 +08:00
const nowEl = document.querySelector(`#music-lrc-text-${nowIndex.value}`) as HTMLElement;
2025-01-19 13:35:10 +08:00
if (nowEl) {
2025-01-18 03:25:21 +08:00
const containerHeight = lrcSider.value.$el.clientHeight;
const elementTop = nowEl.offsetTop;
const scrollTop = elementTop - containerHeight / 2 + nowEl.clientHeight / 2;
lrcSider.value.scrollTo({
top: scrollTop,
behavior
});
2021-11-09 11:34:09 +08:00
}
};
const debouncedLrcScroll = useDebounceFn(lrcScroll, 200);
2021-11-09 11:34:09 +08:00
const mouseOverLayout = () => {
2025-01-10 22:49:55 +08:00
if (isMobile.value) {
return;
}
isMouse.value = true;
};
2025-01-18 03:25:21 +08:00
2021-11-09 11:34:09 +08:00
const mouseLeaveLayout = () => {
2025-01-10 22:49:55 +08:00
if (isMobile.value) {
return;
}
2021-11-09 11:34:09 +08:00
setTimeout(() => {
isMouse.value = false;
lrcScroll();
}, 2000);
};
watch(nowIndex, () => {
debouncedLrcScroll();
});
watch(
2025-01-13 22:55:46 +08:00
() => isVisible.value,
() => {
2025-01-13 22:55:46 +08:00
if (isVisible.value) {
nextTick(() => {
lrcScroll('instant');
});
}
}
);
2025-01-18 03:25:21 +08:00
const setTextColors = (background: string) => {
if (!background) {
textColors.value = getTextColors();
document.documentElement.style.setProperty('--hover-bg-color', getHoverBackgroundColor(false));
document.documentElement.style.setProperty('--text-color-primary', textColors.value.primary);
document.documentElement.style.setProperty('--text-color-active', textColors.value.active);
return;
}
// 更新文字颜色
textColors.value = getTextColors(background);
isDark.value = textColors.value.active === '#000000';
document.documentElement.style.setProperty(
'--hover-bg-color',
getHoverBackgroundColor(isDark.value)
);
document.documentElement.style.setProperty('--text-color-primary', textColors.value.primary);
document.documentElement.style.setProperty('--text-color-active', textColors.value.active);
// 处理背景颜色动画
if (currentBackground.value) {
if (animationFrame.value) {
cancelAnimationFrame(animationFrame.value);
}
const result = animateGradient(currentBackground.value, background, (gradient) => {
currentBackground.value = gradient;
});
if (typeof result === 'number') {
animationFrame.value = result;
}
} else {
currentBackground.value = background;
}
};
// 监听背景变化
watch(
() => props.background,
(newBg) => {
2025-01-18 03:25:21 +08:00
if (config.value.theme === 'default') {
setTextColors(newBg);
} else {
2025-01-18 03:25:21 +08:00
setTextColors(themeMusic[config.value.theme] || props.background);
}
},
{ immediate: true }
);
// 修改 useLyricProgress 的使用方式
const { getLrcStyle: originalLrcStyle } = useLyricProgress();
// 修改 getLrcStyle 函数
const getLrcStyle = (index: number) => {
const colors = textColors.value || getTextColors;
const originalStyle = originalLrcStyle(index);
if (index === nowIndex.value) {
// 当前播放的歌词,使用渐变效果
return {
...originalStyle,
backgroundImage: originalStyle.backgroundImage
?.replace(/#ffffff/g, colors.active)
.replace(/#ffffff8a/g, `${colors.primary}`),
backgroundClip: 'text',
WebkitBackgroundClip: 'text',
color: 'transparent'
};
}
// 非当前播放的歌词,使用普通颜色
return {
color: colors.primary
};
};
// 组件卸载时清理动画
onBeforeUnmount(() => {
if (animationFrame.value) {
cancelAnimationFrame(animationFrame.value);
}
});
2025-03-19 22:48:28 +08:00
const settingsStore = useSettingsStore();
2025-01-13 22:13:46 +08:00
const handleArtistClick = (id: number) => {
2025-01-13 22:55:46 +08:00
isVisible.value = false;
2025-03-19 22:48:28 +08:00
settingsStore.currentArtistId = id;
2025-01-13 22:13:46 +08:00
};
2025-03-19 22:48:28 +08:00
const setData = computed(() => settingsStore.setData);
// 监听字体变化并更新 CSS 变量
watch(
() => [setData.value.fontFamily, setData.value.fontScope],
([newFont, fontScope]) => {
const defaultFonts =
'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif';
// 如果不是歌词模式或全局模式,使用默认字体
if (fontScope !== 'lyric' && fontScope !== 'global') {
document.documentElement.style.setProperty('--current-font-family', defaultFonts);
return;
}
if (newFont === 'system-ui') {
document.documentElement.style.setProperty('--current-font-family', defaultFonts);
} else {
// 处理多个字体,确保每个字体名都被正确引用
const fontList = newFont.split(',').map((font) => {
const trimmedFont = font.trim();
// 如果字体名包含空格或特殊字符,添加引号(如果还没有引号的话)
return /[\s'"()]/.test(trimmedFont) && !/^['"].*['"]$/.test(trimmedFont)
? `"${trimmedFont}"`
: trimmedFont;
});
// 将选择的字体和默认字体组合
document.documentElement.style.setProperty(
'--current-font-family',
`${fontList.join(', ')}, ${defaultFonts}`
);
}
},
{ immediate: true }
);
2025-01-18 03:25:21 +08:00
// 监听配置变化并保存到本地存储
watch(
() => config.value,
(newConfig) => {
localStorage.setItem('music-full-config', JSON.stringify(newConfig));
},
{ deep: true }
);
// 监听滚动事件
const handleScroll = () => {
if (!lrcSider.value || !config.value.hideCover) return;
const { scrollTop } = lrcSider.value.$el;
showStickyHeader.value = scrollTop > 100;
};
const playerStore = usePlayerStore();
const closeMusicFull = () => {
isVisible.value = false;
playerStore.setMusicFull(false);
};
2025-01-18 03:25:21 +08:00
// 添加滚动监听
onMounted(() => {
if (lrcSider.value?.$el) {
lrcSider.value.$el.addEventListener('scroll', handleScroll);
}
});
// 移除滚动监听
onBeforeUnmount(() => {
if (animationFrame.value) {
cancelAnimationFrame(animationFrame.value);
}
if (lrcSider.value?.$el) {
lrcSider.value.$el.removeEventListener('scroll', handleScroll);
}
});
// 监听字体大小变化
watch(
() => config.value.fontSize,
(newSize) => {
document.documentElement.style.setProperty('--lyric-font-size', `${newSize}px`);
}
);
// 监听主题变化
watch(
() => config.value.theme,
(newTheme) => {
const newBackground = themeMusic[newTheme] || props.background;
setTextColors(newBackground);
},
{ immediate: true }
);
// 添加文字间距监听
watch(
() => config.value.letterSpacing,
(newSpacing) => {
document.documentElement.style.setProperty('--lyric-letter-spacing', `${newSpacing}px`);
}
);
// 添加行高监听
watch(
() => config.value.lineHeight,
(newLineHeight) => {
document.documentElement.style.setProperty('--lyric-line-height', newLineHeight.toString());
}
);
// 加载保存的配置
onMounted(() => {
const savedConfig = localStorage.getItem('music-full-config');
if (savedConfig) {
config.value = { ...config.value, ...JSON.parse(savedConfig) };
}
if (lrcSider.value?.$el) {
lrcSider.value.$el.addEventListener('scroll', handleScroll);
}
});
2025-01-19 13:35:10 +08:00
// 添加对 playMusic 的监听
watch(playMusic, () => {
nextTick(() => {
lrcScroll('instant', true);
});
});
defineExpose({
2025-01-18 03:25:21 +08:00
lrcScroll,
config
});
2021-11-09 11:34:09 +08:00
</script>
<style scoped lang="scss">
2023-12-22 09:50:03 +08:00
@keyframes round {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.drawer-back {
@apply absolute bg-cover bg-center;
2023-12-22 09:50:03 +08:00
z-index: -1;
width: 200%;
height: 200%;
top: -50%;
left: -50%;
}
2023-12-27 14:39:52 +08:00
.drawer-back.paused {
animation-play-state: paused;
}
2021-11-09 11:34:09 +08:00
#drawer-target {
2025-01-18 03:25:21 +08:00
@apply top-0 left-0 absolute overflow-hidden rounded px-24 flex items-center justify-center w-full h-full py-8;
2021-11-09 11:34:09 +08:00
animation-duration: 300ms;
2021-11-09 11:34:09 +08:00
.music-img {
@apply flex-1 flex justify-center mr-16 flex-col;
max-width: 360px;
max-height: 360px;
2021-11-09 11:34:09 +08:00
.img {
2025-01-18 03:25:21 +08:00
@apply rounded-xl w-full h-full shadow-2xl;
2021-11-09 11:34:09 +08:00
}
}
.music-content {
@apply flex flex-col justify-center items-center relative;
2025-01-18 03:25:21 +08:00
width: 500px;
&.center {
@apply w-full;
.music-lrc {
@apply w-full max-w-3xl mx-auto;
}
.music-lrc-text {
@apply text-center;
}
}
2021-11-09 11:34:09 +08:00
&-name {
2025-01-13 22:55:46 +08:00
@apply font-bold text-2xl pb-1 pt-4;
2021-11-09 11:34:09 +08:00
}
2021-11-09 11:34:09 +08:00
&-singer {
@apply text-base;
2021-11-09 11:34:09 +08:00
}
}
.music-content-time {
2023-12-27 14:39:52 +08:00
display: none;
@apply flex justify-center items-center;
2023-12-27 14:39:52 +08:00
}
2025-01-18 03:25:21 +08:00
.music-lrc-container {
padding-top: 30vh;
}
2021-11-09 11:34:09 +08:00
.music-lrc {
background-color: inherit;
2023-12-19 14:42:53 +08:00
width: 500px;
2021-11-09 11:34:09 +08:00
height: 550px;
2025-01-18 03:25:21 +08:00
position: relative;
mask-image: linear-gradient(to bottom, transparent 0%, black 10%, black 90%, transparent 100%);
2025-01-18 03:25:21 +08:00
-webkit-mask-image: linear-gradient(
to bottom,
transparent 0%,
black 10%,
black 90%,
transparent 100%
);
.music-info-header {
@apply mb-8;
.music-info-name {
@apply text-4xl font-bold mb-2;
color: var(--text-color-active);
}
.music-info-singer {
@apply text-base;
color: var(--text-color-primary);
}
}
2021-11-09 11:34:09 +08:00
&-text {
@apply text-2xl cursor-pointer font-bold px-2 py-4;
transition: all 0.3s ease;
background-color: transparent;
2025-01-18 03:25:21 +08:00
font-size: var(--lyric-font-size, 22px) !important;
letter-spacing: var(--lyric-letter-spacing, 0) !important;
line-height: var(--lyric-line-height, 2) !important;
span {
background-clip: text !important;
-webkit-background-clip: text !important;
padding-right: 30px;
}
&-tr {
@apply font-normal;
opacity: 0.7;
color: var(--text-color-primary);
}
}
.hover-text {
2021-11-09 11:34:09 +08:00
&:hover {
@apply font-bold opacity-100 rounded-xl;
background-color: var(--hover-bg-color);
span {
color: var(--text-color-active) !important;
}
2021-11-09 11:34:09 +08:00
}
}
}
}
2024-05-23 17:12:35 +08:00
.mobile {
#drawer-target {
@apply flex-col p-4 pt-8 justify-start;
2024-05-23 17:12:35 +08:00
.music-img {
display: none;
}
.music-lrc {
height: calc(100vh - 260px) !important;
width: 100vw;
span {
padding-right: 0px !important;
}
.hover-text {
2025-03-22 15:01:38 +08:00
&:hover {
background-color: transparent;
}
}
2025-03-22 15:01:38 +08:00
.music-lrc-text {
@apply text-xl text-center;
}
2024-05-23 17:12:35 +08:00
}
.music-content {
@apply h-[calc(100vh-120px)];
width: 100vw !important;
}
2024-05-23 17:12:35 +08:00
}
}
.music-drawer {
transition: none; // 移除之前的过渡效果,现在使用 JS 动画
}
// 添加全局字体样式
:root {
--current-font-family:
system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
sans-serif;
}
#drawer-target {
2025-01-18 03:25:21 +08:00
@apply top-0 left-0 absolute overflow-hidden rounded px-24 flex items-center justify-center w-full h-full py-8;
animation-duration: 300ms;
.music-lrc-text {
font-family: var(--current-font-family);
}
}
2025-01-18 03:25:21 +08:00
.close-btn {
opacity: 0.3;
transition: opacity 0.3s ease;
&:hover {
opacity: 1;
}
}
.control-btn {
@apply w-9 h-9 flex items-center justify-center rounded cursor-pointer transition-all duration-300 z-[9999];
2025-01-18 03:25:21 +08:00
background: rgba(142, 142, 142, 0.192);
backdrop-filter: blur(12px);
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;
}
}
}
2023-12-18 15:24:57 +08:00
</style>