feat: 优化歌词页面样式 添加歌词进度显示 优化歌曲及列表加载方式 大幅提升歌曲歌词播放速度

This commit is contained in:
alger
2024-10-18 18:37:53 +08:00
parent 7abc087d70
commit 06bffe7618
16 changed files with 466 additions and 266 deletions
+3
View File
@@ -42,6 +42,9 @@
"no-console": "off", "no-console": "off",
"no-continue": "off", "no-continue": "off",
"no-restricted-syntax": "off", "no-restricted-syntax": "off",
"no-return-assign": "off",
"no-unused-expressions": "off",
"no-return-await": "off",
"no-plusplus": "off", "no-plusplus": "off",
"no-param-reassign": "off", "no-param-reassign": "off",
"no-shadow": "off", "no-shadow": "off",
+1 -1
View File
@@ -20,7 +20,7 @@ function createWindow() {
win.setMinimumSize(1200, 780); win.setMinimumSize(1200, 780);
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
win.webContents.openDevTools({ mode: 'detach' }); win.webContents.openDevTools({ mode: 'detach' });
win.loadURL('http://localhost:7788/'); win.loadURL('http://localhost:4488/');
} else { } else {
win.loadURL(`file://${__dirname}/dist/index.html`); win.loadURL(`file://${__dirname}/dist/index.html`);
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "alger-music", "name": "alger-music",
"version": "1.7.0", "version": "2.0.0",
"description": "这是一个用于音乐播放的应用程序。", "description": "这是一个用于音乐播放的应用程序。",
"author": "Alger <algerkc@qq.com>", "author": "Alger <algerkc@qq.com>",
"main": "app.js", "main": "app.js",
+4
View File
@@ -1,3 +1,7 @@
body{ body{
background-color: #000; background-color: #000;
}
.n-popover:has(.music-play){
border-radius: 1.5rem !important;
} }
+1 -1
View File
@@ -43,7 +43,7 @@ export const getRecommendMusic = (params: IRecommendMusicParams) => {
// 获取每日推荐 // 获取每日推荐
export const getDayRecommend = () => { export const getDayRecommend = () => {
return request.get<IData<IDayRecommend>>('/recommend/songs'); return request.get<IData<IData<IDayRecommend>>>('/recommend/songs');
}; };
// 获取最新专辑推荐 // 获取最新专辑推荐
+17 -17
View File
@@ -74,32 +74,32 @@ const store = useStore();
const hotSingerData = ref<IHotSinger>(); const hotSingerData = ref<IHotSinger>();
const dayRecommendData = ref<IDayRecommend>(); const dayRecommendData = ref<IDayRecommend>();
const showMusic = ref(false); const showMusic = ref(false);
// // 加载推荐歌手
// const loadSingerList = async () => {
// const { data } = await getHotSinger({ offset: 0, limit: 5 });
// hotSingerData.value = data;
// };
// const loadDayRecommend = async () => {
// const { data } = await getDayRecommend();
// dayRecommendData.value = data.data;
// };
// 页面初始化
onMounted(async () => { onMounted(async () => {
await loadData(); await loadData();
}); });
const loadData = async () => { const loadData = async () => {
try { try {
const [{ data: singerData }, { data: dayRecommend }] = await Promise.all([ // 第一个请求:获取热门歌手
getHotSinger({ offset: 0, limit: 5 }), const { data: singerData } = await getHotSinger({ offset: 0, limit: 5 });
getDayRecommend(),
]); // 第二个请求:获取每日推荐
if (dayRecommend.data) { try {
singerData.artists = singerData.artists.slice(0, 4); const {
data: { data: dayRecommend },
} = await getDayRecommend();
console.log('dayRecommend', dayRecommend);
// 处理数据
if (dayRecommend) {
singerData.artists = singerData.artists.slice(0, 4);
}
dayRecommendData.value = dayRecommend;
} catch (error) {
console.error('error', error);
} }
hotSingerData.value = singerData; hotSingerData.value = singerData;
dayRecommendData.value = dayRecommend.data;
} catch (error) { } catch (error) {
console.error('error', error); console.error('error', error);
} }
+4 -2
View File
@@ -78,9 +78,11 @@ const imageLoad = async () => {
if (!songImageRef.value) { if (!songImageRef.value) {
return; return;
} }
const background = await getImageBackground((songImageRef.value as any).imageRef as unknown as HTMLImageElement); const { backgroundColor } = await getImageBackground(
(songImageRef.value as any).imageRef as unknown as HTMLImageElement,
);
// eslint-disable-next-line vue/no-mutating-props // eslint-disable-next-line vue/no-mutating-props
props.item.backgroundColor = background; props.item.backgroundColor = backgroundColor;
}; };
// 播放音乐 设置音乐详情 打开音乐底栏 // 播放音乐 设置音乐详情 打开音乐底栏
+135 -116
View File
@@ -1,156 +1,177 @@
import { computed, ref } from 'vue';
import { getMusicLrc } from '@/api/music'; import { getMusicLrc } from '@/api/music';
import store from '@/store';
import { ILyric } from '@/type/lyric'; import { ILyric } from '@/type/lyric';
import type { ILyricText, SongResult } from '@/type/music';
const windowData = window as any; const windowData = window as any;
export const isElectron = computed(() => { export const isElectron = computed(() => !!windowData.electronAPI);
return !!windowData.electronAPI;
});
interface ILrcData { export const lrcArray = ref<ILyricText[]>([]); // 歌词数组
text: string; export const lrcTimeArray = ref<number[]>([]); // 歌词时间数组
trText: string; export const nowTime = ref(0); // 当前播放时间
} export const allTime = ref(0); // 总播放时间
export const nowIndex = ref(0); // 当前播放歌词
export const correctionTime = ref(0.4); // 歌词矫正时间Correction time
export const currentLrcProgress = ref(0); // 来存储当前歌词的进度
export const audio = ref<HTMLAudioElement>(); // 音频对象
export const playMusic = computed(() => store.state.playMusic as SongResult); // 当前播放歌曲
export const lrcData = ref<ILyric>(); watch(
export const newLrcIndex = ref<number>(0); () => store.state.playMusic,
export const lrcArray = ref<Array<ILrcData>>([]); () => {
export const lrcTimeArray = ref<Array<Number>>([]); nextTick(() => {
lrcArray.value = playMusic.value.lyric?.lrcArray || [];
export const parseTime = (timeString: string) => { lrcTimeArray.value = playMusic.value.lyric?.lrcTimeArray || [];
const [minutes, seconds] = timeString.split(':'); });
return Number(minutes) * 60 + Number(seconds); },
}; {
deep: true,
const TIME_REGEX = /(\d{2}:\d{2}(\.\d*)?)/g; },
const LRC_REGEX = /(\[(\d{2}):(\d{2})(\.(\d*))?\])/g; );
const isPlaying = computed(() => store.state.play as boolean);
function parseLyricLine(lyricLine: string) {
const timeText = lyricLine.match(TIME_REGEX)?.[0] || '';
const time = parseTime(timeText);
const text = lyricLine.replace(LRC_REGEX, '').trim();
return { time, text };
}
interface ILyricText {
text: string;
trText: string;
}
function parseLyrics(lyricsString: string) {
const lines = lyricsString.split('\n');
const lyrics: Array<ILyricText> = [];
const times: number[] = [];
lines.forEach((line) => {
const { time, text } = parseLyricLine(line);
times.push(time);
lyrics.push({ text, trText: '' });
});
return { lyrics, times };
}
export const loadLrc = async (playMusicId: number): Promise<void> => {
try {
const { data } = await getMusicLrc(playMusicId);
const { lyrics, times } = parseLyrics(data.lrc.lyric);
let tlyric: {
[key: string]: string;
} = {};
if (data.tlyric.lyric) {
const { lyrics: tLyrics, times: tTimes } = parseLyrics(data.tlyric.lyric);
tlyric = tLyrics.reduce((acc: any, cur, index) => {
acc[tTimes[index]] = cur.text;
return acc;
}, {});
}
if (Object.keys(tlyric).length) {
lyrics.forEach((item, index) => {
item.trText = item.text ? tlyric[times[index].toString()] : '';
});
}
lrcTimeArray.value = times;
lrcArray.value = lyrics;
} catch (err) {
console.error('err', err);
}
};
// 歌词矫正时间Correction time
const correctionTime = ref(0.4);
// 增加矫正时间 // 增加矫正时间
export const addCorrectionTime = (time: number) => { export const addCorrectionTime = (time: number) => (correctionTime.value += time);
correctionTime.value += time;
};
// 减少矫正时间 // 减少矫正时间
export const reduceCorrectionTime = (time: number) => { export const reduceCorrectionTime = (time: number) => (correctionTime.value -= time);
correctionTime.value -= time;
};
export const isCurrentLrc = (index: number, time: number) => { // 获取当前播放歌词
const currentTime = Number(lrcTimeArray.value[index]); export const isCurrentLrc = (index: number, time: number): boolean => {
const nextTime = Number(lrcTimeArray.value[index + 1]); const currentTime = lrcTimeArray.value[index];
const nextTime = lrcTimeArray.value[index + 1];
const nowTime = time + correctionTime.value; const nowTime = time + correctionTime.value;
const isTrue = nowTime > currentTime && nowTime < nextTime; const isTrue = nowTime > currentTime && nowTime < nextTime;
if (isTrue) {
newLrcIndex.value = index;
}
return isTrue; return isTrue;
}; };
export const nowTime = ref(0); // 获取当前播放歌词INDEX
export const allTime = ref(0); export const getLrcIndex = (time: number): number => {
export const nowIndex = ref(0);
export const getLrcIndex = (time: number) => {
for (let i = 0; i < lrcTimeArray.value.length; i++) { for (let i = 0; i < lrcTimeArray.value.length; i++) {
if (isCurrentLrc(i, time)) { if (isCurrentLrc(i, time)) {
nowIndex.value = i || nowIndex.value; nowIndex.value = i;
return i; return i;
} }
} }
return nowIndex.value; return nowIndex.value;
}; };
// 设置当前播放时间 // 获取当前播放歌词进度
export const setAudioTime = (index: number, audio: HTMLAudioElement) => { const currentLrcTiming = computed(() => {
audio.currentTime = lrcTimeArray.value[index] as number; const start = lrcTimeArray.value[nowIndex.value] || 0;
audio.play(); const end = lrcTimeArray.value[nowIndex.value + 1] || start + 1;
return { start, end };
});
// 获取歌词样式
export const getLrcStyle = (index: number) => {
if (index === nowIndex.value) {
return {
backgroundImage: `linear-gradient(to right, #ffffff ${currentLrcProgress.value}%, #ffffff8a ${currentLrcProgress.value}%)`,
backgroundClip: 'text',
WebkitBackgroundClip: 'text',
color: 'transparent',
transition: 'background-image 0.1s linear',
};
}
return {};
}; };
// 计算这个歌词的播放时间 watch(nowTime, (newTime) => {
const getLrcTime = (index: number) => { const newIndex = getLrcIndex(newTime);
return Number(lrcTimeArray.value[index]); if (newIndex !== nowIndex.value) {
nowIndex.value = newIndex;
currentLrcProgress.value = 0; // 重置进度
}
});
// 播放进度
export const useLyricProgress = () => {
let animationFrameId: number | null = null;
const updateProgress = () => {
if (!isPlaying.value) return;
audio.value = audio.value || (document.querySelector('#MusicAudio') as HTMLAudioElement);
if (!audio.value) return;
const { start, end } = currentLrcTiming.value;
const duration = end - start;
const elapsed = audio.value.currentTime - start;
currentLrcProgress.value = Math.min(Math.max((elapsed / duration) * 100, 0), 100);
animationFrameId = requestAnimationFrame(updateProgress);
};
const startProgressAnimation = () => {
if (!animationFrameId && isPlaying.value) {
updateProgress();
}
};
const stopProgressAnimation = () => {
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
animationFrameId = null;
}
};
watch(isPlaying, (newIsPlaying) => {
if (newIsPlaying) {
startProgressAnimation();
} else {
stopProgressAnimation();
}
});
onMounted(() => {
if (isPlaying.value) {
startProgressAnimation();
}
});
onUnmounted(() => {
stopProgressAnimation();
});
return {
currentLrcProgress,
getLrcStyle,
};
};
// 设置当前播放时间
export const setAudioTime = (index: number, audio: HTMLAudioElement) => {
audio.currentTime = lrcTimeArray.value[index];
audio.play();
}; };
// 获取当前播放的歌词 // 获取当前播放的歌词
export const getCurrentLrc = () => { export const getCurrentLrc = () => {
const index = getLrcIndex(nowTime.value); const index = getLrcIndex(nowTime.value);
const currentLrc = lrcArray.value[index]; return {
const nextLrc = lrcArray.value[index + 1]; currentLrc: lrcArray.value[index],
return { currentLrc, nextLrc }; nextLrc: lrcArray.value[index + 1],
};
}; };
// 获取一句歌词播放时间是 几秒到几秒 // 获取一句歌词播放时间是 几秒到几秒
export const getLrcTimeRange = (index: number) => { export const getLrcTimeRange = (index: number) => ({
const currentTime = Number(lrcTimeArray.value[index]); currentTime: lrcTimeArray.value[index],
const nextTime = Number(lrcTimeArray.value[index + 1]); nextTime: lrcTimeArray.value[index + 1],
return { currentTime, nextTime }; });
};
export const sendLyricToWin = (isPlay: boolean = true) => { export const sendLyricToWin = (isPlay: boolean = true) => {
if (!isElectron.value) return;
try { try {
if (!isElectron.value) {
return;
}
// 设置lyricWinData 获取 当前播放的两句歌词 和歌词时间
let lyricWinData = null;
if (lrcArray.value.length > 0) { if (lrcArray.value.length > 0) {
const nowIndex = getLrcIndex(nowTime.value); const nowIndex = getLrcIndex(nowTime.value);
const { currentLrc, nextLrc } = getCurrentLrc(); const { currentLrc, nextLrc } = getCurrentLrc();
const { currentTime, nextTime } = getLrcTimeRange(nowIndex); const { currentTime, nextTime } = getLrcTimeRange(nowIndex);
lyricWinData = { // 设置lyricWinData 获取 当前播放的两句歌词 和歌词时间
const lyricWinData = {
currentLrc, currentLrc,
nextLrc, nextLrc,
currentTime, currentTime,
@@ -160,20 +181,18 @@ export const sendLyricToWin = (isPlay: boolean = true) => {
lrcArray: lrcArray.value, lrcArray: lrcArray.value,
nowTime: nowTime.value, nowTime: nowTime.value,
allTime: allTime.value, allTime: allTime.value,
startCurrentTime: getLrcTime(nowIndex), startCurrentTime: lrcTimeArray.value[nowIndex],
isPlay, isPlay,
}; };
windowData.electronAPI.sendLyric(JSON.stringify(lyricWinData)); windowData.electronAPI.sendLyric(JSON.stringify(lyricWinData));
} }
} catch (error) { } catch (error) {
console.error('error', error); console.error('Error sending lyric to window:', error);
} }
}; };
export const openLyric = () => { export const openLyric = () => {
if (!isElectron.value) { if (!isElectron.value) return;
return;
}
windowData.electronAPI.openLyric(); windowData.electronAPI.openLyric();
sendLyricToWin(); sendLyricToWin();
}; };
+175
View File
@@ -0,0 +1,175 @@
import { getMusicLrc, getMusicUrl, getParsingMusicUrl } from '@/api/music';
import { useMusicHistory } from '@/hooks/MusicHistoryHook';
import type { ILyric, ILyricText, SongResult } from '@/type/music';
import { getImgUrl, getMusicProxyUrl } from '@/utils';
import { getImageLinearBackground } from '@/utils/linearColor';
const musicHistory = useMusicHistory();
// 获取歌曲url
const getSongUrl = async (id: number) => {
const { data } = await getMusicUrl(id);
let url = '';
try {
if (data.data[0].freeTrialInfo || !data.data[0].url) {
const res = await getParsingMusicUrl(id);
url = res.data.data.url;
}
} catch (error) {
console.error('error', error);
}
url = url || data.data[0].url;
return getMusicProxyUrl(url);
};
const getSongDetail = async (playMusic: SongResult) => {
if (playMusic.playMusicUrl) {
return playMusic;
}
playMusic.playLoading = true;
const playMusicUrl = await getSongUrl(playMusic.id);
const { backgroundColor, primaryColor } =
playMusic.backgroundColor && playMusic.primaryColor
? playMusic
: await getImageLinearBackground(getImgUrl(playMusic?.picUrl, '30y30'));
playMusic.playLoading = false;
return { ...playMusic, playMusicUrl, backgroundColor, primaryColor };
};
// 加载 当前歌曲 歌曲列表数据 下一首mp3预加载 歌词数据
export const useMusicListHook = () => {
const handlePlayMusic = async (state: any, playMusic: SongResult) => {
const updatedPlayMusic = await getSongDetail(playMusic);
state.playMusic = updatedPlayMusic;
state.playMusicUrl = updatedPlayMusic.playMusicUrl;
state.play = true;
loadLrcAsync(state, updatedPlayMusic.id);
musicHistory.addMusic(state.playMusic);
const playListIndex = state.playList.findIndex((item: SongResult) => item.id === playMusic.id);
state.playListIndex = playListIndex;
// 请求后续五首歌曲的详情
fetchSongs(state, playListIndex + 1, playListIndex + 6);
};
// 用于预加载下一首歌曲的 MP3 数据
const preloadNextSong = (nextSongUrl: string) => {
const audio = new Audio(nextSongUrl);
audio.preload = 'auto'; // 设置预加载
audio.load(); // 手动加载
};
const fetchSongs = async (state: any, startIndex: number, endIndex: number) => {
const songs = state.playList.slice(Math.max(0, startIndex), Math.min(endIndex, state.playList.length));
const detailedSongs = await Promise.all(
songs.map(async (song: SongResult) => {
// 如果歌曲详情已经存在,就不重复请求
if (!song.playMusicUrl) {
return await getSongDetail(song);
}
return song;
}),
);
// 加载下一首的歌词
const nextSong = detailedSongs[0];
if (!(nextSong.lyric && nextSong.lyric.lrcTimeArray.length > 0)) {
nextSong.lyric = await loadLrc(nextSong.id);
}
// 更新播放列表中的歌曲详情
detailedSongs.forEach((song, index) => {
state.playList[startIndex + index] = song;
});
preloadNextSong(nextSong.playMusicUrl);
};
const nextPlay = async (state: any) => {
if (state.playList.length === 0) {
state.play = true;
return;
}
const playListIndex = (state.playListIndex + 1) % state.playList.length;
await handlePlayMusic(state, state.playList[playListIndex]);
};
const prevPlay = async (state: any) => {
if (state.playList.length === 0) {
state.play = true;
return;
}
const playListIndex = (state.playListIndex - 1 + state.playList.length) % state.playList.length;
await handlePlayMusic(state, state.playList[playListIndex]);
await fetchSongs(state, playListIndex - 5, playListIndex);
};
const parseTime = (timeString: string): number => {
const [minutes, seconds] = timeString.split(':');
return Number(minutes) * 60 + Number(seconds);
};
const parseLyricLine = (lyricLine: string): { time: number; text: string } => {
const TIME_REGEX = /(\d{2}:\d{2}(\.\d*)?)/g;
const LRC_REGEX = /(\[(\d{2}):(\d{2})(\.(\d*))?\])/g;
const timeText = lyricLine.match(TIME_REGEX)?.[0] || '';
const time = parseTime(timeText);
const text = lyricLine.replace(LRC_REGEX, '').trim();
return { time, text };
};
const parseLyrics = (lyricsString: string): { lyrics: ILyricText[]; times: number[] } => {
const lines = lyricsString.split('\n');
const lyrics: ILyricText[] = [];
const times: number[] = [];
lines.forEach((line) => {
const { time, text } = parseLyricLine(line);
times.push(time);
lyrics.push({ text, trText: '' });
});
return { lyrics, times };
};
const loadLrc = async (playMusicId: number): Promise<ILyric> => {
try {
const { data } = await getMusicLrc(playMusicId);
const { lyrics, times } = parseLyrics(data.lrc.lyric);
const tlyric: Record<string, string> = {};
if (data.tlyric.lyric) {
const { lyrics: tLyrics, times: tTimes } = parseLyrics(data.tlyric.lyric);
tLyrics.forEach((lyric, index) => {
tlyric[tTimes[index].toString()] = lyric.text;
});
}
lyrics.forEach((item, index) => {
item.trText = item.text ? tlyric[times[index].toString()] || '' : '';
});
return {
lrcTimeArray: times,
lrcArray: lyrics,
};
} catch (err) {
console.error('Error loading lyrics:', err);
return {
lrcTimeArray: [],
lrcArray: [],
};
}
};
// 异步加载歌词的方法
const loadLrcAsync = async (state: any, playMusicId: number) => {
if (state.playMusic.lyric && state.playMusic.lyric.lrcTimeArray.length > 0) {
return;
}
const lyrics = await loadLrc(playMusicId);
state.playMusic.lyric = lyrics;
};
return {
handlePlayMusic,
nextPlay,
prevPlay,
};
};
+72 -52
View File
@@ -1,69 +1,72 @@
<template> <template>
<n-drawer :show="musicFull" height="100vh" placement="bottom" :style="{ backgroundColor: 'transparent' }"> <n-drawer :show="musicFull" height="100vh" placement="bottom" :style="{ background: background }">
<div id="drawer-target"> <div id="drawer-target">
<div class="drawer-back" :style="{ background: background }"></div> <div class="drawer-back"></div>
<div class="music-img"> <div class="music-img">
<n-image ref="PicImgRef" :src="getImgUrl(playMusic?.picUrl, '300y300')" class="img" lazy preview-disabled /> <n-image ref="PicImgRef" :src="getImgUrl(playMusic?.picUrl, '300y300')" class="img" lazy preview-disabled />
<div>
<div class="music-content-name">{{ playMusic.name }}</div>
<div class="music-content-singer">
<span v-for="(item, index) in playMusic.song.artists" :key="index">
{{ item.name }}{{ index < playMusic.song.artists.length - 1 ? ' / ' : '' }}
</span>
</div>
</div>
</div> </div>
<div class="music-content"> <div class="music-content">
<div class="music-content-name">{{ playMusic.name }}</div>
<div class="music-content-singer">
<span v-for="(item, index) in playMusic.song.artists" :key="index">
{{ item.name }}{{ index < playMusic.song.artists.length - 1 ? ' / ' : '' }}
</span>
</div>
<n-layout <n-layout
ref="lrcSider" ref="lrcSider"
class="music-lrc" class="music-lrc"
style="height: 55vh" style="height: 60vh"
:native-scrollbar="false" :native-scrollbar="false"
@mouseover="mouseOverLayout" @mouseover="mouseOverLayout"
@mouseleave="mouseLeaveLayout" @mouseleave="mouseLeaveLayout"
> >
<template v-for="(item, index) in lrcArray" :key="index"> <div ref="lrcContainer">
<div <div
v-for="(item, index) in lrcArray"
:id="`music-lrc-text-${index}`"
:key="index"
class="music-lrc-text" class="music-lrc-text"
:class="{ 'now-text': isCurrentLrc(index, nowTime) }" :class="{ 'now-text': index === nowIndex }"
@click="setAudioTime(index, audio)" @click="setAudioTime(index, audio)"
> >
<div>{{ item.text }}</div> <span :style="getLrcStyle(index)">{{ item.text }}</span>
<div class="music-lrc-text-tr">{{ item.trText }}</div> <div class="music-lrc-text-tr">{{ item.trText }}</div>
</div> </div>
</template> </div>
</n-layout> </n-layout>
<!-- 时间矫正 --> <!-- 时间矫正 -->
<div class="music-content-time"> <!-- <div class="music-content-time">
<n-button @click="reduceCorrectionTime(0.2)">-</n-button> <n-button @click="reduceCorrectionTime(0.2)">-</n-button>
<n-button @click="addCorrectionTime(0.2)">+</n-button> <n-button @click="addCorrectionTime(0.2)">+</n-button>
</div> </div> -->
</div> </div>
</div> </div>
</n-drawer> </n-drawer>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useStore } from 'vuex'; import { useDebounceFn } from '@vueuse/core';
import { import {
addCorrectionTime, addCorrectionTime,
isCurrentLrc,
lrcArray, lrcArray,
newLrcIndex, nowIndex,
nowTime, playMusic,
reduceCorrectionTime, reduceCorrectionTime,
setAudioTime, setAudioTime,
useLyricProgress,
} from '@/hooks/MusicHook'; } from '@/hooks/MusicHook';
import type { SongResult } from '@/type/music';
import { getImgUrl } from '@/utils'; import { getImgUrl } from '@/utils';
const store = useStore(); const { getLrcStyle } = useLyricProgress();
// 播放的音乐信息
const playMusic = computed(() => store.state.playMusic as SongResult);
// const isPlaying = computed(() => store.state.play as boolean); // const isPlaying = computed(() => store.state.play as boolean);
// 获取歌词滚动dom // 获取歌词滚动dom
const lrcSider = ref<any>(null); const lrcSider = ref<any>(null);
const isMouse = ref(false); const isMouse = ref(false);
const lrcContainer = ref<HTMLElement | null>(null);
const props = defineProps({ const props = defineProps({
musicFull: { musicFull: {
@@ -81,21 +84,44 @@ const props = defineProps({
}); });
// 歌词滚动方法 // 歌词滚动方法
const lrcScroll = () => { const lrcScroll = (behavior = 'smooth') => {
if (props.musicFull && !isMouse.value) { const nowEl = document.querySelector(`#music-lrc-text-${nowIndex.value}`);
const top = newLrcIndex.value * 60 - 225; if (props.musicFull && !isMouse.value && nowEl && lrcContainer.value) {
lrcSider.value.scrollTo({ top, behavior: 'smooth' }); const containerRect = lrcContainer.value.getBoundingClientRect();
const nowElRect = nowEl.getBoundingClientRect();
const relativeTop = nowElRect.top - containerRect.top;
const scrollTop = relativeTop - lrcSider.value.$el.getBoundingClientRect().height / 2;
lrcSider.value.scrollTo({ top: scrollTop, behavior });
} }
}; };
const debouncedLrcScroll = useDebounceFn(lrcScroll, 200);
const mouseOverLayout = () => { const mouseOverLayout = () => {
isMouse.value = true; isMouse.value = true;
}; };
const mouseLeaveLayout = () => { const mouseLeaveLayout = () => {
setTimeout(() => { setTimeout(() => {
isMouse.value = false; isMouse.value = false;
}, 3000); lrcScroll();
}, 2000);
}; };
watch(nowIndex, () => {
debouncedLrcScroll();
});
watch(
() => props.musicFull,
() => {
if (props.musicFull) {
nextTick(() => {
lrcScroll('instant');
});
}
},
);
defineExpose({ defineExpose({
lrcScroll, lrcScroll,
}); });
@@ -112,15 +138,11 @@ defineExpose({
} }
.drawer-back { .drawer-back {
@apply absolute bg-cover bg-center; @apply absolute bg-cover bg-center;
// filter: brightness(80%);
z-index: -1; z-index: -1;
width: 200%; width: 200%;
height: 200%; height: 200%;
top: -50%; top: -50%;
left: -50%; left: -50%;
// animation: round 20s linear infinite;
// will-change: transform;
// transform: translateZ(0);
} }
.drawer-back.paused { .drawer-back.paused {
@@ -128,30 +150,28 @@ defineExpose({
} }
#drawer-target { #drawer-target {
@apply top-0 left-0 absolute w-full h-full overflow-hidden rounded px-24 pt-24 pb-48 flex items-center; @apply top-0 left-0 absolute overflow-hidden rounded px-24 flex items-center justify-center w-full h-full pb-8;
backdrop-filter: blur(20px); backdrop-filter: blur(20px);
background-color: rgba(0, 0, 0, 0.747);
animation-duration: 300ms; animation-duration: 300ms;
.music-img { .music-img {
@apply flex-1 flex justify-center mr-24; @apply flex-1 flex justify-center mr-16 flex-col;
max-width: 360px;
max-height: 360px;
.img { .img {
width: 350px; @apply rounded-xl w-full h-full shadow-2xl;
height: 350px;
@apply rounded-xl;
} }
} }
.music-content { .music-content {
@apply flex flex-col justify-center items-center; @apply flex flex-col justify-center items-center relative;
&-name { &-name {
@apply font-bold text-3xl py-2; @apply font-bold text-xl pb-1 pt-4;
} }
&-singer { &-singer {
@apply text-base py-2; @apply text-base;
} }
} }
@@ -159,25 +179,25 @@ defineExpose({
display: none; display: none;
@apply flex justify-center items-center; @apply flex justify-center items-center;
} }
.music-lrc { .music-lrc {
background-color: inherit; background-color: inherit;
width: 500px; width: 500px;
height: 550px; height: 550px;
.now-text {
@apply text-green-500;
}
&-text { &-text {
@apply text-white text-lg flex flex-col justify-center items-center cursor-pointer font-bold; @apply text-2xl cursor-pointer font-bold px-2 py-4;
height: 60px; color: #ffffff8a;
transition: all 0.2s ease-out; // transition: all 0.5s ease;
span {
padding-right: 100px;
}
&:hover { &:hover {
@apply font-bold text-green-500; @apply font-bold opacity-100 rounded-xl;
background-color: #ffffff26;
color: #fff;
} }
&-tr { &-tr {
@apply text-sm font-normal; @apply font-normal;
} }
} }
} }
+9 -11
View File
@@ -80,6 +80,7 @@
raw raw
:show-arrow="false" :show-arrow="false"
:delay="200" :delay="200"
arrow-wrapper-style=" border-radius:1.5rem"
@update-show="scrollToPlayList" @update-show="scrollToPlayList"
> >
<template #trigger> <template #trigger>
@@ -111,7 +112,7 @@ import { useTemplateRef } from 'vue';
import { useStore } from 'vuex'; import { useStore } from 'vuex';
import SongItem from '@/components/common/SongItem.vue'; import SongItem from '@/components/common/SongItem.vue';
import { allTime, isElectron, loadLrc, nowTime, openLyric, sendLyricToWin } from '@/hooks/MusicHook'; import { allTime, getCurrentLrc, isElectron, nowTime, openLyric, sendLyricToWin } from '@/hooks/MusicHook';
import type { SongResult } from '@/type/music'; import type { SongResult } from '@/type/music';
import { getImgUrl, secondToMinute, setAnimationClass } from '@/utils'; import { getImgUrl, secondToMinute, setAnimationClass } from '@/utils';
@@ -134,7 +135,6 @@ const background = ref('#000');
watch( watch(
() => store.state.playMusic, () => store.state.playMusic,
async () => { async () => {
loadLrc(playMusic.value.id);
background.value = playMusic.value.backgroundColor as string; background.value = playMusic.value.backgroundColor as string;
}, },
{ immediate: true, deep: true }, { immediate: true, deep: true },
@@ -209,15 +209,16 @@ function handleGetAudioTime(this: HTMLAudioElement) {
// 监听音频播放的实时时间事件 // 监听音频播放的实时时间事件
const audio = this as HTMLAudioElement; const audio = this as HTMLAudioElement;
// 获取当前播放时间 // 获取当前播放时间
nowTime.value = Math.floor(audio.currentTime); nowTime.value = audio.currentTime;
getCurrentLrc();
// 获取总时间 // 获取总时间
allTime.value = audio.duration; allTime.value = audio.duration;
// 获取音量 // 获取音量
audioVolume.value = audio.volume; audioVolume.value = audio.volume;
sendLyricToWin(store.state.isPlay); sendLyricToWin(store.state.isPlay);
if (musicFullVisible.value) { // if (musicFullVisible.value) {
MusicFullRef.value?.lrcScroll(); // MusicFullRef.value?.lrcScroll();
} // }
} }
// 播放暂停按钮事件 // 播放暂停按钮事件
@@ -273,7 +274,8 @@ const scrollToPlayList = (val: boolean) => {
} }
.play-bar-opcity { .play-bar-opcity {
background-color: rgba(0, 0, 0, 0.218); @apply bg-transparent;
box-shadow: 0 0 20px 5px #0000001d;
} }
.play-bar-img { .play-bar-img {
@@ -374,8 +376,4 @@ const scrollToPlayList = (val: boolean) => {
flex: 1; flex: 1;
} }
} }
:deep(.n-popover) {
box-shadow: none;
}
</style> </style>
+6 -56
View File
@@ -1,11 +1,8 @@
import { createStore } from 'vuex'; import { createStore } from 'vuex';
import { getMusicUrl, getParsingMusicUrl } from '@/api/music'; import { useMusicListHook } from '@/hooks/MusicListHook';
import { useMusicHistory } from '@/hooks/MusicHistoryHook';
import homeRouter from '@/router/home'; import homeRouter from '@/router/home';
import type { SongResult } from '@/type/music'; import type { SongResult } from '@/type/music';
import { getImgUrl, getMusicProxyUrl } from '@/utils';
import { getImageLinearBackground } from '@/utils/linearColor';
interface State { interface State {
menus: any[]; menus: any[];
@@ -38,17 +35,16 @@ const state: State = {
searchValue: '', searchValue: '',
searchType: 1, searchType: 1,
}; };
const windowData = window as any; const windowData = window as any;
const musicHistory = useMusicHistory(); const { handlePlayMusic, nextPlay, prevPlay } = useMusicListHook();
const mutations = { const mutations = {
setMenus(state: State, menus: any[]) { setMenus(state: State, menus: any[]) {
state.menus = menus; state.menus = menus;
}, },
async setPlay(state: State, playMusic: SongResult) { async setPlay(state: State, playMusic: SongResult) {
await getSongDetail(state, playMusic); await handlePlayMusic(state, playMusic);
}, },
setIsPlay(state: State, isPlay: boolean) { setIsPlay(state: State, isPlay: boolean) {
state.isPlay = isPlay; state.isPlay = isPlay;
@@ -61,63 +57,17 @@ const mutations = {
state.playList = playList; state.playList = playList;
}, },
async nextPlay(state: State) { async nextPlay(state: State) {
if (state.playList.length === 0) { await nextPlay(state);
state.play = true;
return;
}
const playListIndex = (state.playListIndex + 1) % state.playList.length;
await getSongDetail(state, state.playList[playListIndex]);
}, },
async prevPlay(state: State) { async prevPlay(state: State) {
if (state.playList.length === 0) { await prevPlay(state);
state.play = true;
return;
}
const playListIndex = (state.playListIndex - 1 + state.playList.length) % state.playList.length;
await getSongDetail(state, state.playList[playListIndex]);
}, },
async setSetData(state: State, setData: any) { async setSetData(state: State, setData: any) {
state.setData = setData; state.setData = setData;
windowData.electron.ipcRenderer.setStoreValue('set', JSON.parse(JSON.stringify(setData))); window.electron && window.electron.ipcRenderer.setStoreValue('set', JSON.parse(JSON.stringify(setData)));
}, },
}; };
const getSongUrl = async (id: number) => {
const { data } = await getMusicUrl(id);
let url = '';
try {
if (data.data[0].freeTrialInfo || !data.data[0].url) {
const res = await getParsingMusicUrl(id);
url = res.data.data.url;
}
} catch (error) {
console.error('error', error);
}
url = url || data.data[0].url;
return getMusicProxyUrl(url);
};
const updatePlayMusic = async (state: State) => {
state.playMusic = state.playList[state.playListIndex];
state.playMusicUrl = await getSongUrl(state.playMusic.id);
state.play = true;
musicHistory.addMusic(state.playMusic);
};
const getSongDetail = async (state: State, playMusic: SongResult) => {
state.playMusic.playLoading = true;
state.playMusicUrl = await getSongUrl(playMusic.id);
const backgroundColor = playMusic.backgroundColor
? playMusic.backgroundColor
: await getImageLinearBackground(getImgUrl(playMusic?.picUrl, '30y30'));
state.playMusic = { ...playMusic, backgroundColor };
// state.playMusic = { ...playMusic };
state.play = true;
musicHistory.addMusic(playMusic);
state.playListIndex = state.playList.findIndex((item) => item.id === playMusic.id);
state.playMusic.playLoading = false;
};
const store = createStore({ const store = createStore({
state, state,
mutations, mutations,
+11
View File
@@ -3,6 +3,14 @@ export interface IRecommendMusic {
category: number; category: number;
result: SongResult[]; result: SongResult[];
} }
export interface ILyricText {
text: string;
trText: string;
}
export interface ILyric {
lrcTimeArray: number[];
lrcArray: ILyricText[];
}
export interface SongResult { export interface SongResult {
id: number; id: number;
@@ -19,6 +27,9 @@ export interface SongResult {
ar?: Artist[]; ar?: Artist[];
al?: Album; al?: Album;
backgroundColor?: string; backgroundColor?: string;
primaryColor?: string;
playMusicUrl?: string;
lyric?: ILyric;
} }
export interface Song { export interface Song {
+1
View File
@@ -51,6 +51,7 @@ export const getIsMc = () => {
if (windowData.electron.ipcRenderer.getStoreValue('set').isProxy) { if (windowData.electron.ipcRenderer.getStoreValue('set').isProxy) {
return true; return true;
} }
if(window.location.origin.includes('localhost')){}
return false; return false;
}; };
const ProxyUrl = import.meta.env.VITE_API_PROXY || 'http://110.42.251.190:9856'; const ProxyUrl = import.meta.env.VITE_API_PROXY || 'http://110.42.251.190:9856';
+25 -8
View File
@@ -1,20 +1,37 @@
export const getImageLinearBackground = async (imageSrc: string): Promise<string> => { interface IColor {
backgroundColor: string;
primaryColor: string;
}
export const getImageLinearBackground = async (imageSrc: string): Promise<IColor> => {
try { try {
const primaryColor = await getImagePrimaryColor(imageSrc); const primaryColor = await getImagePrimaryColor(imageSrc);
return generateGradientBackground(primaryColor); return {
backgroundColor: generateGradientBackground(primaryColor),
primaryColor,
};
} catch (error) { } catch (error) {
console.error('error', error); console.error('error', error);
return ''; return {
backgroundColor: '',
primaryColor: '',
};
} }
}; };
export const getImageBackground = async (img: HTMLImageElement): Promise<string> => { export const getImageBackground = async (img: HTMLImageElement): Promise<IColor> => {
try { try {
const primaryColor = await getImageColor(img); const primaryColor = await getImageColor(img);
return generateGradientBackground(primaryColor); return {
backgroundColor: generateGradientBackground(primaryColor),
primaryColor,
};
} catch (error) { } catch (error) {
console.error('error', error); console.error('error', error);
return ''; return {
backgroundColor: '',
primaryColor: '',
};
} }
}; };
@@ -83,8 +100,8 @@ const generateGradientBackground = (color: string): string => {
const [h, s, l] = rgbToHsl(r, g, b); const [h, s, l] = rgbToHsl(r, g, b);
// 增加亮度和暗度的差异 // 增加亮度和暗度的差异
const lightL = Math.min(l + 0.5, 0.95); const lightL = Math.min(l + 0.2, 0.95);
const darkL = Math.max(l - 0.5, 0.05); const darkL = Math.max(l - 0.3, 0.05);
const midL = (lightL + darkL) / 2; const midL = (lightL + darkL) / 2;
// 调整饱和度以增强效果 // 调整饱和度以增强效果
+1 -1
View File
@@ -33,7 +33,7 @@ export default defineConfig({
server: { server: {
host: '0.0.0.0', host: '0.0.0.0',
// 指定端口 // 指定端口
port: 7788, port: 4488,
proxy: { proxy: {
// with options // with options
'/api': { '/api': {