mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-04-04 15:00:49 +08:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06bffe7618 | ||
|
|
7abc087d70 | ||
|
|
eb2ea1981d | ||
|
|
6dc14ec51b | ||
|
|
36f8257a3e | ||
|
|
c55544df46 | ||
|
|
008f2183de | ||
|
|
dd3a3c3bbb | ||
|
|
941eb2e66e | ||
|
|
a98fcb43d6 | ||
|
|
791121ae06 | ||
|
|
0c156e2708 | ||
|
|
017b47fded | ||
|
|
e27ed22c16 | ||
|
|
904d8744ef | ||
|
|
800e0b7360 | ||
|
|
b6a5461a1d | ||
|
|
a4eda61a86 | ||
|
|
a79d0712a4 | ||
|
|
8f782cdc9d | ||
|
|
2f851f3172 |
@@ -42,6 +42,9 @@
|
||||
"no-console": "off",
|
||||
"no-continue": "off",
|
||||
"no-restricted-syntax": "off",
|
||||
"no-return-assign": "off",
|
||||
"no-unused-expressions": "off",
|
||||
"no-return-await": "off",
|
||||
"no-plusplus": "off",
|
||||
"no-param-reassign": "off",
|
||||
"no-shadow": "off",
|
||||
|
||||
2
app.js
2
app.js
@@ -20,7 +20,7 @@ function createWindow() {
|
||||
win.setMinimumSize(1200, 780);
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
win.webContents.openDevTools({ mode: 'detach' });
|
||||
win.loadURL('http://localhost:4678/');
|
||||
win.loadURL('http://localhost:4488/');
|
||||
} else {
|
||||
win.loadURL(`file://${__dirname}/dist/index.html`);
|
||||
}
|
||||
|
||||
4
components.d.ts
vendored
4
components.d.ts
vendored
@@ -1,10 +1,10 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
export {}
|
||||
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
MPop: typeof import('./src/components/common/MPop.vue')['default']
|
||||
@@ -23,9 +23,9 @@ declare module 'vue' {
|
||||
NPopover: typeof import('naive-ui')['NPopover']
|
||||
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
||||
NSlider: typeof import('naive-ui')['NSlider']
|
||||
NSpin: typeof import('naive-ui')['NSpin']
|
||||
NSwitch: typeof import('naive-ui')['NSwitch']
|
||||
NTooltip: typeof import('naive-ui')['NTooltip']
|
||||
NVirtualList: typeof import('naive-ui')['NVirtualList']
|
||||
PlayBottom: typeof import('./src/components/common/PlayBottom.vue')['default']
|
||||
PlayListsItem: typeof import('./src/components/common/PlayListsItem.vue')['default']
|
||||
PlaylistType: typeof import('./src/components/PlaylistType.vue')['default']
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.5.0",
|
||||
"version": "1.5.1",
|
||||
"isProxy": false,
|
||||
"author": "alger"
|
||||
}
|
||||
|
||||
46
package.json
46
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "alger-music",
|
||||
"version": "1.5.0",
|
||||
"version": "2.0.0",
|
||||
"description": "这是一个用于音乐播放的应用程序。",
|
||||
"author": "Alger <algerkc@qq.com>",
|
||||
"main": "app.js",
|
||||
@@ -21,16 +21,16 @@
|
||||
"@tailwindcss/postcss7-compat": "^2.2.4",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"@vitejs/plugin-vue": "^4.2.3",
|
||||
"@vue/compiler-sfc": "^3.3.4",
|
||||
"@vue/eslint-config-typescript": "^12.0.0",
|
||||
"@vue/runtime-core": "^3.3.4",
|
||||
"@vueuse/core": "^10.7.1",
|
||||
"@vueuse/electron": "^10.9.0",
|
||||
"autoprefixer": "^9.8.6",
|
||||
"axios": "^0.21.1",
|
||||
"electron": "^30.0.0",
|
||||
"electron-builder": "^24.13.0",
|
||||
"@vitejs/plugin-vue": "^5.1.3",
|
||||
"@vue/compiler-sfc": "^3.5.0",
|
||||
"@vue/eslint-config-typescript": "^13.0.0",
|
||||
"@vue/runtime-core": "^3.5.0",
|
||||
"@vueuse/core": "^11.0.3",
|
||||
"@vueuse/electron": "^11.0.3",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"axios": "^1.7.7",
|
||||
"electron": "^32.0.1",
|
||||
"electron-builder": "^25.0.5",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-airbnb-base": "^15.0.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
@@ -40,22 +40,22 @@
|
||||
"eslint-plugin-vue": "^9.21.1",
|
||||
"eslint-plugin-vue-scoped-css": "^2.7.2",
|
||||
"lodash": "^4.17.21",
|
||||
"naive-ui": "^2.38.2",
|
||||
"postcss": "^7.0.36",
|
||||
"prettier": "^3.2.5",
|
||||
"naive-ui": "^2.39.0",
|
||||
"postcss": "^8.4.44",
|
||||
"prettier": "^3.3.3",
|
||||
"remixicon": "^4.2.0",
|
||||
"sass": "^1.35.2",
|
||||
"sass": "^1.78.0",
|
||||
"tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.2.4",
|
||||
"typescript": "^4.3.2",
|
||||
"unplugin-auto-import": "^0.17.2",
|
||||
"unplugin-vue-components": "^0.26.0",
|
||||
"typescript": "^5.5.4",
|
||||
"unplugin-auto-import": "^0.18.2",
|
||||
"unplugin-vue-components": "^0.27.4",
|
||||
"vfonts": "^0.1.0",
|
||||
"vite": "^4.4.7",
|
||||
"vite": "^5.4.3",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-vue-devtools": "1.0.0-beta.5",
|
||||
"vue": "^3.3.4",
|
||||
"vue-router": "^4.2.4",
|
||||
"vue-tsc": "^0.0.24",
|
||||
"vite-plugin-vue-devtools": "7.4.0",
|
||||
"vue": "^3.5.0",
|
||||
"vue-router": "^4.4.3",
|
||||
"vue-tsc": "^2.1.4",
|
||||
"vuex": "^4.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
body{
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
.n-popover:has(.music-play){
|
||||
border-radius: 1.5rem !important;
|
||||
}
|
||||
@@ -3,9 +3,7 @@
|
||||
<audio id="MusicAudio" ref="audioRef" :src="playMusicUrl" :autoplay="play"></audio>
|
||||
<n-config-provider :theme="darkTheme">
|
||||
<n-dialog-provider>
|
||||
<keep-alive>
|
||||
<router-view></router-view>
|
||||
</keep-alive>
|
||||
<router-view></router-view>
|
||||
</n-dialog-provider>
|
||||
</n-config-provider>
|
||||
</div>
|
||||
|
||||
@@ -43,7 +43,7 @@ export const getRecommendMusic = (params: IRecommendMusicParams) => {
|
||||
|
||||
// 获取每日推荐
|
||||
export const getDayRecommend = () => {
|
||||
return request.get<IData<IDayRecommend>>('/recommend/songs');
|
||||
return request.get<IData<IData<IDayRecommend>>>('/recommend/songs');
|
||||
};
|
||||
|
||||
// 获取最新专辑推荐
|
||||
|
||||
@@ -3,24 +3,47 @@
|
||||
:show="show"
|
||||
:height="isMobile ? '100vh' : '70vh'"
|
||||
placement="bottom"
|
||||
:drawer-style="{ backgroundColor: 'transparent' }"
|
||||
block-scroll
|
||||
mask-closable
|
||||
:style="{ backgroundColor: 'transparent' }"
|
||||
@mask-click="close"
|
||||
>
|
||||
<div class="music-page">
|
||||
<i class="iconfont icon-icon_error music-close" @click="close"></i>
|
||||
<div class="music-close">
|
||||
<i class="icon ri-layout-column-line" @click="doubleDisply = !doubleDisply"></i>
|
||||
<i class="icon iconfont icon-icon_error" @click="close"></i>
|
||||
</div>
|
||||
<div class="music-title text-el">{{ name }}</div>
|
||||
<!-- 歌单歌曲列表 -->
|
||||
<div class="music-list">
|
||||
<n-scrollbar>
|
||||
<n-scrollbar @scroll="handleScroll">
|
||||
<div
|
||||
v-for="(item, index) in songList"
|
||||
:key="item.id"
|
||||
:class="setAnimationClass('animate__bounceInUp')"
|
||||
:style="setAnimationDelay(index, 50)"
|
||||
v-loading="loading || !songList.length"
|
||||
class="music-list-content"
|
||||
:class="{ 'double-list': doubleDisply }"
|
||||
>
|
||||
<song-item :item="formatDetail(item)" @play="handlePlay" />
|
||||
<div
|
||||
v-for="(item, index) in displayedSongs"
|
||||
:key="item.id"
|
||||
class="double-item"
|
||||
:class="setAnimationClass('animate__bounceInUp')"
|
||||
:style="setAnimationDelay(index, 5)"
|
||||
>
|
||||
<song-item :item="formatDetail(item)" @play="handlePlay" />
|
||||
</div>
|
||||
<div v-if="isLoadingMore" class="loading-more">加载更多...</div>
|
||||
</div>
|
||||
<play-bottom />
|
||||
</n-scrollbar>
|
||||
|
||||
<!-- <n-virtual-list :item-size="42" :items="displayedSongs" item-resizable @scroll="handleScroll">
|
||||
<template #default="{ item, index }">
|
||||
<div :key="item.id" class="double-item">
|
||||
<song-item :item="formatDetail(item)" @play="handlePlay" />
|
||||
</div>
|
||||
</template>
|
||||
</n-virtual-list>
|
||||
<play-bottom /> -->
|
||||
</div>
|
||||
</div>
|
||||
</n-drawer>
|
||||
@@ -29,6 +52,7 @@
|
||||
<script setup lang="ts">
|
||||
import { useStore } from 'vuex';
|
||||
|
||||
import { getMusicDetail } from '@/api/music';
|
||||
import SongItem from '@/components/common/SongItem.vue';
|
||||
import { isMobile, setAnimationClass, setAnimationDelay } from '@/utils';
|
||||
|
||||
@@ -36,12 +60,27 @@ import PlayBottom from './common/PlayBottom.vue';
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const props = defineProps<{
|
||||
const {
|
||||
songList,
|
||||
loading = false,
|
||||
listInfo,
|
||||
} = defineProps<{
|
||||
show: boolean;
|
||||
name: string;
|
||||
songList: any[];
|
||||
loading?: boolean;
|
||||
listInfo?: any;
|
||||
}>();
|
||||
const emit = defineEmits(['update:show']);
|
||||
const emit = defineEmits(['update:show', 'update:loading']);
|
||||
|
||||
const page = ref(0);
|
||||
const pageSize = 20;
|
||||
const total = ref(0);
|
||||
const isLoadingMore = ref(false);
|
||||
const displayedSongs = ref<any[]>([]);
|
||||
|
||||
// 双排显示开关
|
||||
const doubleDisply = ref(false);
|
||||
|
||||
const formatDetail = computed(() => (detail: any) => {
|
||||
const song = {
|
||||
@@ -56,13 +95,57 @@ const formatDetail = computed(() => (detail: any) => {
|
||||
});
|
||||
|
||||
const handlePlay = () => {
|
||||
const tracks = props.songList || [];
|
||||
store.commit('setPlayList', tracks);
|
||||
const tracks = songList || [];
|
||||
store.commit(
|
||||
'setPlayList',
|
||||
tracks.map((item) => ({
|
||||
...item,
|
||||
picUrl: item.al.picUrl,
|
||||
song: {
|
||||
artists: item.ar,
|
||||
},
|
||||
})),
|
||||
);
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
emit('update:show', false);
|
||||
};
|
||||
|
||||
const loadMoreSongs = async () => {
|
||||
if (displayedSongs.value.length >= total.value) return;
|
||||
|
||||
isLoadingMore.value = true;
|
||||
try {
|
||||
const trackIds = listInfo.trackIds
|
||||
.slice(page.value * pageSize, (page.value + 1) * pageSize)
|
||||
.map((item: any) => item.id);
|
||||
const reslist = await getMusicDetail(trackIds);
|
||||
// displayedSongs.value = displayedSongs.value.concat(reslist.data.songs);
|
||||
displayedSongs.value = JSON.parse(JSON.stringify([...displayedSongs.value, ...reslist.data.songs]));
|
||||
page.value++;
|
||||
} catch (error) {
|
||||
console.error('error', error);
|
||||
} finally {
|
||||
isLoadingMore.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleScroll = (e: any) => {
|
||||
const { scrollTop, scrollHeight, clientHeight } = e.target;
|
||||
if (scrollTop + clientHeight >= scrollHeight - 50 && !isLoadingMore.value) {
|
||||
loadMoreSongs();
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => songList,
|
||||
(newSongs) => {
|
||||
displayedSongs.value = JSON.parse(JSON.stringify(newSongs));
|
||||
total.value = listInfo ? listInfo.trackIds.length : displayedSongs.value.length;
|
||||
},
|
||||
{ deep: true, immediate: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -71,16 +154,24 @@ const close = () => {
|
||||
@apply px-8 w-full h-full bg-black bg-opacity-75 rounded-t-2xl;
|
||||
backdrop-filter: blur(20px);
|
||||
}
|
||||
|
||||
&-title {
|
||||
@apply text-lg font-bold text-white p-4;
|
||||
}
|
||||
|
||||
&-close {
|
||||
@apply absolute top-4 right-8 cursor-pointer text-white text-3xl;
|
||||
@apply absolute top-4 right-8 cursor-pointer text-white flex gap-2 items-center;
|
||||
.icon {
|
||||
@apply text-3xl;
|
||||
}
|
||||
}
|
||||
|
||||
&-list {
|
||||
height: calc(100% - 60px);
|
||||
|
||||
&-content {
|
||||
min-height: 400px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,4 +180,20 @@ const close = () => {
|
||||
@apply px-4;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-more {
|
||||
@apply text-center text-white py-10;
|
||||
}
|
||||
|
||||
.double-list {
|
||||
@apply flex flex-wrap gap-5;
|
||||
|
||||
.double-item {
|
||||
width: calc(50% - 10px);
|
||||
}
|
||||
|
||||
.song-item {
|
||||
background-color: #191919;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<div class="recommend-singer">
|
||||
<div class="recommend-singer-list">
|
||||
<div
|
||||
v-if="dayRecommendData"
|
||||
class="recommend-singer-item relative"
|
||||
:class="setAnimationClass('animate__backInRight')"
|
||||
:style="setAnimationDelay(0, 100)"
|
||||
@@ -27,7 +28,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-for="(item, index) in hotSingerData?.artists.slice(0, 4)"
|
||||
v-for="(item, index) in hotSingerData?.artists"
|
||||
:key="item.id"
|
||||
class="recommend-singer-item relative"
|
||||
:class="setAnimationClass('animate__backInRight')"
|
||||
@@ -59,6 +60,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useStore } from 'vuex';
|
||||
|
||||
import { getDayRecommend, getHotSinger } from '@/api/home';
|
||||
import router from '@/router';
|
||||
@@ -66,34 +68,42 @@ import { IDayRecommend } from '@/type/day_recommend';
|
||||
import type { IHotSinger } from '@/type/singer';
|
||||
import { getImgUrl, setAnimationClass, setAnimationDelay, setBackgroundImg } from '@/utils';
|
||||
|
||||
const store = useStore();
|
||||
|
||||
// 歌手信息
|
||||
const hotSingerData = ref<IHotSinger>();
|
||||
const dayRecommendData = ref<IDayRecommend>();
|
||||
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 () => {
|
||||
await loadData();
|
||||
});
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const [{ data: singerData }, { data: dayRecommend }] = await Promise.all([
|
||||
getHotSinger({ offset: 0, limit: 5 }),
|
||||
getDayRecommend(),
|
||||
]);
|
||||
// 第一个请求:获取热门歌手
|
||||
const { data: singerData } = await getHotSinger({ offset: 0, limit: 5 });
|
||||
|
||||
// 第二个请求:获取每日推荐
|
||||
try {
|
||||
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;
|
||||
dayRecommendData.value = dayRecommend.data;
|
||||
} catch (error) {
|
||||
console.error('error', error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const toSearchSinger = (keyword: string) => {
|
||||
router.push({
|
||||
@@ -103,6 +113,13 @@ const toSearchSinger = (keyword: string) => {
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 监听登录状态
|
||||
watchEffect(() => {
|
||||
if (store.state.user) {
|
||||
loadData();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
<template>
|
||||
<div class="recommend-music">
|
||||
<div class="title" :class="setAnimationClass('animate__fadeInLeft')">本周最热音乐</div>
|
||||
<div v-show="recommendMusic?.result" class="recommend-music-list" :class="setAnimationClass('animate__bounceInUp')">
|
||||
<div
|
||||
v-show="recommendMusic?.result"
|
||||
v-loading="loading"
|
||||
class="recommend-music-list"
|
||||
:class="setAnimationClass('animate__bounceInUp')"
|
||||
>
|
||||
<!-- 推荐音乐列表 -->
|
||||
<template v-for="(item, index) in recommendMusic?.result" :key="item.id">
|
||||
<div :class="setAnimationClass('animate__bounceInUp')" :style="setAnimationDelay(index, 100)">
|
||||
@@ -24,11 +29,14 @@ import SongItem from './common/SongItem.vue';
|
||||
const store = useStore();
|
||||
// 推荐歌曲
|
||||
const recommendMusic = ref<IRecommendMusic>();
|
||||
const loading = ref(false);
|
||||
|
||||
// 加载推荐歌曲
|
||||
const loadRecommendMusic = async () => {
|
||||
loading.value = true;
|
||||
const { data } = await getRecommendMusic({ limit: 10 });
|
||||
recommendMusic.value = data;
|
||||
loading.value = false;
|
||||
};
|
||||
|
||||
// 页面初始化
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
<template>
|
||||
<div class="search-item" @click="handleClick">
|
||||
<div class="search-item" :class="item.type" @click="handleClick">
|
||||
<div class="search-item-img">
|
||||
<n-image :src="getImgUrl(item.picUrl, '200y200')" lazy preview-disabled />
|
||||
<n-image :src="getImgUrl(item.picUrl, '320y180')" lazy preview-disabled />
|
||||
<div v-if="item.type === 'mv'" class="play">
|
||||
<i class="iconfont icon icon-play"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="search-item-info">
|
||||
<div class="search-item-name">{{ item.name }}</div>
|
||||
<div class="search-item-artist">{{ item.desc }}</div>
|
||||
<p class="search-item-name">{{ item.name }}</p>
|
||||
<p class="search-item-artist">{{ item.desc }}</p>
|
||||
</div>
|
||||
|
||||
<MusicList
|
||||
@@ -13,6 +16,7 @@
|
||||
v-model:show="showPop"
|
||||
:name="item.name"
|
||||
:song-list="songList"
|
||||
:list-info="listInfo"
|
||||
/>
|
||||
|
||||
<PlayVideo v-if="item.type === 'mv'" v-model:show="showPop" :title="item.name" :url="url" />
|
||||
@@ -39,8 +43,10 @@ const url = ref('');
|
||||
const songList = ref<any[]>([]);
|
||||
|
||||
const showPop = ref(false);
|
||||
const listInfo = ref<any>(null);
|
||||
|
||||
const handleClick = async () => {
|
||||
listInfo.value = null;
|
||||
if (props.item.type === '专辑') {
|
||||
showPop.value = true;
|
||||
const res = await getAlbum(props.item.id);
|
||||
@@ -54,6 +60,7 @@ const handleClick = async () => {
|
||||
showPop.value = true;
|
||||
const res = await getListDetail(props.item.id);
|
||||
songList.value = res.data.playlist.tracks;
|
||||
listInfo.value = res.data.playlist;
|
||||
}
|
||||
|
||||
if (props.item.type === 'mv') {
|
||||
@@ -72,6 +79,7 @@ const handleClick = async () => {
|
||||
@apply w-12 h-12 mr-4 rounded-2xl overflow-hidden;
|
||||
}
|
||||
.search-item-info {
|
||||
@apply flex-1 overflow-hidden;
|
||||
&-name {
|
||||
@apply text-white text-sm text-center;
|
||||
}
|
||||
@@ -80,4 +88,23 @@ const handleClick = async () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mv {
|
||||
&:hover {
|
||||
.play {
|
||||
@apply opacity-60;
|
||||
}
|
||||
}
|
||||
.search-item-img {
|
||||
width: 160px;
|
||||
height: 90px;
|
||||
@apply rounded-lg relative;
|
||||
}
|
||||
.play {
|
||||
@apply absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 opacity-0 transition-opacity;
|
||||
.icon {
|
||||
@apply text-white text-5xl;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,14 +1,24 @@
|
||||
<template>
|
||||
<div class="song-item" :class="{ 'song-mini': mini }">
|
||||
<n-image v-if="item.picUrl" :src="getImgUrl(item.picUrl, '40y40')" class="song-item-img" lazy preview-disabled />
|
||||
<n-image
|
||||
v-if="item.picUrl"
|
||||
ref="songImg"
|
||||
:src="getImgUrl(item.picUrl, '40y40')"
|
||||
class="song-item-img"
|
||||
preview-disabled
|
||||
:img-props="{
|
||||
crossorigin: 'anonymous',
|
||||
}"
|
||||
@load="imageLoad"
|
||||
/>
|
||||
<div class="song-item-content">
|
||||
<div class="song-item-content-title">
|
||||
<n-ellipsis class="text-ellipsis" line-clamp="1">{{ item.name }}</n-ellipsis>
|
||||
</div>
|
||||
<div class="song-item-content-name">
|
||||
<n-ellipsis class="text-ellipsis" line-clamp="1">
|
||||
<span v-for="(artists, artistsindex) in item.song.artists" :key="artistsindex"
|
||||
>{{ artists.name }}{{ artistsindex < item.song.artists.length - 1 ? ' / ' : '' }}</span
|
||||
<span v-for="(artists, artistsindex) in item.ar || item.song.artists" :key="artistsindex"
|
||||
>{{ artists.name }}{{ artistsindex < (item.ar || item.song.artists).length - 1 ? ' / ' : '' }}</span
|
||||
>
|
||||
</n-ellipsis>
|
||||
</div>
|
||||
@@ -30,10 +40,12 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useTemplateRef } from 'vue';
|
||||
import { useStore } from 'vuex';
|
||||
|
||||
import type { SongResult } from '@/type/music';
|
||||
import { getImgUrl } from '@/utils';
|
||||
import { getImageBackground } from '@/utils/linearColor';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -60,9 +72,27 @@ const isPlaying = computed(() => {
|
||||
|
||||
const emits = defineEmits(['play']);
|
||||
|
||||
const songImageRef = useTemplateRef('songImg');
|
||||
|
||||
const imageLoad = async () => {
|
||||
if (!songImageRef.value) {
|
||||
return;
|
||||
}
|
||||
const { backgroundColor } = await getImageBackground(
|
||||
(songImageRef.value as any).imageRef as unknown as HTMLImageElement,
|
||||
);
|
||||
// eslint-disable-next-line vue/no-mutating-props
|
||||
props.item.backgroundColor = backgroundColor;
|
||||
};
|
||||
|
||||
// 播放音乐 设置音乐详情 打开音乐底栏
|
||||
const playMusicEvent = async (item: SongResult) => {
|
||||
if (playMusic.value.id === item.id) {
|
||||
if (play.value) {
|
||||
store.commit('setPlayMusic', false);
|
||||
} else {
|
||||
store.commit('setPlayMusic', true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
await store.commit('setPlay', item);
|
||||
|
||||
7
src/directive/index.ts
Normal file
7
src/directive/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { vLoading } from './loading/index';
|
||||
|
||||
const directives = {
|
||||
loading: vLoading,
|
||||
};
|
||||
|
||||
export default directives;
|
||||
40
src/directive/loading/index.ts
Normal file
40
src/directive/loading/index.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { createVNode, render, VNode } from 'vue';
|
||||
|
||||
import Loading from './index.vue';
|
||||
|
||||
const vnode: VNode = createVNode(Loading) as VNode;
|
||||
|
||||
export const vLoading = {
|
||||
// 在绑定元素的父组件 及他自己的所有子节点都挂载完成后调用
|
||||
mounted: (el: HTMLElement, binding: any) => {
|
||||
render(vnode, el);
|
||||
},
|
||||
// 在绑定元素的父组件 及他自己的所有子节点都更新后调用
|
||||
updated: (el: HTMLElement, binding: any) => {
|
||||
if (binding.value) {
|
||||
vnode?.component?.exposed.show();
|
||||
} else {
|
||||
vnode?.component?.exposed.hide();
|
||||
}
|
||||
// 动态添加删除自定义class: loading-parent
|
||||
formatterClass(el, binding);
|
||||
},
|
||||
// 绑定元素的父组件卸载后调用
|
||||
unmounted: () => {
|
||||
vnode?.component?.exposed.hide();
|
||||
},
|
||||
};
|
||||
|
||||
function formatterClass(el: HTMLElement, binding: any) {
|
||||
const classStr = el.getAttribute('class');
|
||||
const tagetClass: number = classStr?.indexOf('loading-parent') as number;
|
||||
if (binding.value) {
|
||||
if (tagetClass === -1) {
|
||||
el.setAttribute('class', `${classStr} loading-parent`);
|
||||
}
|
||||
} else if (tagetClass > -1) {
|
||||
const classArray: Array<string> = classStr?.split('') as string[];
|
||||
classArray.splice(tagetClass - 1, tagetClass + 15);
|
||||
el.setAttribute('class', classArray?.join(''));
|
||||
}
|
||||
}
|
||||
92
src/directive/loading/index.vue
Normal file
92
src/directive/loading/index.vue
Normal file
@@ -0,0 +1,92 @@
|
||||
<!-- -->
|
||||
<template>
|
||||
<div v-if="isShow" class="loading-box">
|
||||
<div class="mask" :style="{ background: maskBackground }"></div>
|
||||
<div class="loading-content-box">
|
||||
<n-spin size="small" />
|
||||
<div :style="{ color: textColor }" class="tip">{{ tip }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { NSpin } from 'naive-ui';
|
||||
import { ref } from 'vue';
|
||||
|
||||
defineProps({
|
||||
tip: {
|
||||
type: String,
|
||||
default() {
|
||||
return '加载中...';
|
||||
},
|
||||
},
|
||||
maskBackground: {
|
||||
type: String,
|
||||
default() {
|
||||
return 'rgba(0, 0, 0, 0.8)';
|
||||
},
|
||||
},
|
||||
loadingColor: {
|
||||
type: String,
|
||||
default() {
|
||||
return 'rgba(255, 255, 255, 1)';
|
||||
},
|
||||
},
|
||||
textColor: {
|
||||
type: String,
|
||||
default() {
|
||||
return 'rgba(255, 255, 255, 1)';
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const isShow = ref(false);
|
||||
const show = () => {
|
||||
isShow.value = true;
|
||||
};
|
||||
const hide = () => {
|
||||
isShow.value = false;
|
||||
};
|
||||
defineExpose({
|
||||
show,
|
||||
hide,
|
||||
isShow,
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.loading-box {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
z-index: 9999;
|
||||
.n-spin {
|
||||
// color: #ccc;
|
||||
}
|
||||
.mask {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.loading-content-box {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.tip {
|
||||
font-size: 14px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,147 +1,177 @@
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { getMusicLrc } from '@/api/music';
|
||||
import store from '@/store';
|
||||
import { ILyric } from '@/type/lyric';
|
||||
import type { ILyricText, SongResult } from '@/type/music';
|
||||
|
||||
interface ILrcData {
|
||||
text: string;
|
||||
trText: string;
|
||||
}
|
||||
const windowData = window as any;
|
||||
|
||||
export const lrcData = ref<ILyric>();
|
||||
export const newLrcIndex = ref<number>(0);
|
||||
export const lrcArray = ref<Array<ILrcData>>([]);
|
||||
export const lrcTimeArray = ref<Array<Number>>([]);
|
||||
export const isElectron = computed(() => !!windowData.electronAPI);
|
||||
|
||||
export const parseTime = (timeString: string) => {
|
||||
const [minutes, seconds] = timeString.split(':');
|
||||
return Number(minutes) * 60 + Number(seconds);
|
||||
};
|
||||
export const lrcArray = ref<ILyricText[]>([]); // 歌词数组
|
||||
export const lrcTimeArray = ref<number[]>([]); // 歌词时间数组
|
||||
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); // 当前播放歌曲
|
||||
|
||||
const TIME_REGEX = /(\d{2}:\d{2}(\.\d*)?)/g;
|
||||
const LRC_REGEX = /(\[(\d{2}):(\d{2})(\.(\d*))?\])/g;
|
||||
|
||||
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);
|
||||
watch(
|
||||
() => store.state.playMusic,
|
||||
() => {
|
||||
nextTick(() => {
|
||||
lrcArray.value = playMusic.value.lyric?.lrcArray || [];
|
||||
lrcTimeArray.value = playMusic.value.lyric?.lrcTimeArray || [];
|
||||
});
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
},
|
||||
);
|
||||
const isPlaying = computed(() => store.state.play as boolean);
|
||||
|
||||
// 增加矫正时间
|
||||
export const addCorrectionTime = (time: number) => {
|
||||
correctionTime.value += time;
|
||||
};
|
||||
export const addCorrectionTime = (time: number) => (correctionTime.value += time);
|
||||
|
||||
// 减少矫正时间
|
||||
export const reduceCorrectionTime = (time: number) => {
|
||||
correctionTime.value -= time;
|
||||
};
|
||||
export const reduceCorrectionTime = (time: number) => (correctionTime.value -= time);
|
||||
|
||||
export const isCurrentLrc = (index: number, time: number) => {
|
||||
const currentTime = Number(lrcTimeArray.value[index]);
|
||||
const nextTime = Number(lrcTimeArray.value[index + 1]);
|
||||
// 获取当前播放歌词
|
||||
export const isCurrentLrc = (index: number, time: number): boolean => {
|
||||
const currentTime = lrcTimeArray.value[index];
|
||||
const nextTime = lrcTimeArray.value[index + 1];
|
||||
const nowTime = time + correctionTime.value;
|
||||
const isTrue = nowTime > currentTime && nowTime < nextTime;
|
||||
if (isTrue) {
|
||||
newLrcIndex.value = index;
|
||||
}
|
||||
return isTrue;
|
||||
};
|
||||
|
||||
export const nowTime = ref(0);
|
||||
export const allTime = ref(0);
|
||||
export const nowIndex = ref(0);
|
||||
|
||||
export const getLrcIndex = (time: number) => {
|
||||
// 获取当前播放歌词INDEX
|
||||
export const getLrcIndex = (time: number): number => {
|
||||
for (let i = 0; i < lrcTimeArray.value.length; i++) {
|
||||
if (isCurrentLrc(i, time)) {
|
||||
nowIndex.value = i || nowIndex.value;
|
||||
nowIndex.value = i;
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return nowIndex.value;
|
||||
};
|
||||
|
||||
// 设置当前播放时间
|
||||
export const setAudioTime = (index: number, audio: HTMLAudioElement) => {
|
||||
audio.currentTime = lrcTimeArray.value[index] as number;
|
||||
audio.play();
|
||||
// 获取当前播放歌词进度
|
||||
const currentLrcTiming = computed(() => {
|
||||
const start = lrcTimeArray.value[nowIndex.value] || 0;
|
||||
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 {};
|
||||
};
|
||||
|
||||
// 计算这个歌词的播放时间
|
||||
const getLrcTime = (index: number) => {
|
||||
return Number(lrcTimeArray.value[index]);
|
||||
watch(nowTime, (newTime) => {
|
||||
const newIndex = getLrcIndex(newTime);
|
||||
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 = () => {
|
||||
const index = getLrcIndex(nowTime.value);
|
||||
const currentLrc = lrcArray.value[index];
|
||||
const nextLrc = lrcArray.value[index + 1];
|
||||
return { currentLrc, nextLrc };
|
||||
return {
|
||||
currentLrc: lrcArray.value[index],
|
||||
nextLrc: lrcArray.value[index + 1],
|
||||
};
|
||||
};
|
||||
|
||||
// 获取一句歌词播放时间是 几秒到几秒
|
||||
export const getLrcTimeRange = (index: number) => {
|
||||
const currentTime = Number(lrcTimeArray.value[index]);
|
||||
const nextTime = Number(lrcTimeArray.value[index + 1]);
|
||||
return { currentTime, nextTime };
|
||||
};
|
||||
export const getLrcTimeRange = (index: number) => ({
|
||||
currentTime: lrcTimeArray.value[index],
|
||||
nextTime: lrcTimeArray.value[index + 1],
|
||||
});
|
||||
|
||||
export const sendLyricToWin = (isPlay: boolean = true) => {
|
||||
if (!isElectron.value) return;
|
||||
|
||||
try {
|
||||
// 设置lyricWinData 获取 当前播放的两句歌词 和歌词时间
|
||||
let lyricWinData = null;
|
||||
if (lrcArray.value.length > 0) {
|
||||
const nowIndex = getLrcIndex(nowTime.value);
|
||||
const { currentLrc, nextLrc } = getCurrentLrc();
|
||||
const { currentTime, nextTime } = getLrcTimeRange(nowIndex);
|
||||
lyricWinData = {
|
||||
// 设置lyricWinData 获取 当前播放的两句歌词 和歌词时间
|
||||
const lyricWinData = {
|
||||
currentLrc,
|
||||
nextLrc,
|
||||
currentTime,
|
||||
@@ -151,20 +181,18 @@ export const sendLyricToWin = (isPlay: boolean = true) => {
|
||||
lrcArray: lrcArray.value,
|
||||
nowTime: nowTime.value,
|
||||
allTime: allTime.value,
|
||||
startCurrentTime: getLrcTime(nowIndex),
|
||||
startCurrentTime: lrcTimeArray.value[nowIndex],
|
||||
isPlay,
|
||||
};
|
||||
|
||||
const windowData = window as any;
|
||||
windowData.electronAPI.sendLyric(JSON.stringify(lyricWinData));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('error', error);
|
||||
console.error('Error sending lyric to window:', error);
|
||||
}
|
||||
};
|
||||
|
||||
export const openLyric = () => {
|
||||
const windowData = window as any;
|
||||
if (!isElectron.value) return;
|
||||
windowData.electronAPI.openLyric();
|
||||
sendLyricToWin();
|
||||
};
|
||||
|
||||
175
src/hooks/MusicListHook.ts
Normal file
175
src/hooks/MusicListHook.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="layout-page">
|
||||
<div class="layout-main">
|
||||
<div class="layout-main" :style="{ background: backgroundColor }">
|
||||
<title-bar v-if="isElectron" />
|
||||
<div class="layout-main-page" :class="isElectron ? '' : 'pt-6'">
|
||||
<!-- 侧边菜单栏 -->
|
||||
@@ -9,7 +9,7 @@
|
||||
<!-- 搜索栏 -->
|
||||
<search-bar />
|
||||
<!-- 主页面路由 -->
|
||||
<div class="main-content bg-black" :native-scrollbar="false">
|
||||
<div class="main-content" :native-scrollbar="false">
|
||||
<n-message-provider>
|
||||
<router-view
|
||||
v-slot="{ Component }"
|
||||
@@ -37,6 +37,7 @@ import { useRoute } from 'vue-router';
|
||||
import { useStore } from 'vuex';
|
||||
|
||||
import PlayBottom from '@/components/common/PlayBottom.vue';
|
||||
import { isElectron } from '@/hooks/MusicHook';
|
||||
import homeRouter from '@/router/home';
|
||||
import { isMobile } from '@/utils';
|
||||
|
||||
@@ -69,11 +70,18 @@ const audio = {
|
||||
value: document.querySelector('#MusicAudio') as HTMLAudioElement,
|
||||
};
|
||||
|
||||
const windowData = window as any;
|
||||
const isElectron = computed(() => {
|
||||
return !!windowData.electronAPI;
|
||||
});
|
||||
|
||||
const backgroundColor = ref('#000');
|
||||
// watch(
|
||||
// () => store.state.playMusic,
|
||||
// () => {
|
||||
// backgroundColor.value = store.state.playMusic.backgroundColor;
|
||||
// console.log('backgroundColor.value', backgroundColor.value);
|
||||
// },
|
||||
// {
|
||||
// immediate: true,
|
||||
// deep: true,
|
||||
// },
|
||||
// );
|
||||
onMounted(() => {
|
||||
// 监听音乐是否播放
|
||||
watch(
|
||||
@@ -95,14 +103,6 @@ onMounted(() => {
|
||||
default:
|
||||
}
|
||||
};
|
||||
// 按下键盘按钮监听
|
||||
document.onkeydown = (e) => {
|
||||
switch (e.code) {
|
||||
case 'Space':
|
||||
return false;
|
||||
default:
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const audioPlay = () => {
|
||||
@@ -130,11 +130,11 @@ const playMusicEvent = async () => {
|
||||
.layout-page {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
@apply flex justify-center items-center overflow-hidden;
|
||||
@apply flex justify-center items-center overflow-hidden bg-black;
|
||||
}
|
||||
|
||||
.layout-main {
|
||||
@apply bg-black text-white shadow-xl flex flex-col relative;
|
||||
@apply text-white shadow-xl flex flex-col relative transition-all;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -1,66 +1,72 @@
|
||||
<template>
|
||||
<n-drawer :show="musicFull" height="100vh" placement="bottom" :drawer-style="{ backgroundColor: 'transparent' }">
|
||||
<n-drawer :show="musicFull" height="100vh" placement="bottom" :style="{ background: background }">
|
||||
<div id="drawer-target">
|
||||
<div
|
||||
class="drawer-back"
|
||||
:class="{ paused: !isPlaying }"
|
||||
:style="{ backgroundImage: `url(${getImgUrl(playMusic?.picUrl, '300y300')})` }"
|
||||
></div>
|
||||
<div class="drawer-back"></div>
|
||||
<div class="music-img">
|
||||
<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 class="music-content">
|
||||
<div class="music-content-name">{{ playMusic.song.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
|
||||
ref="lrcSider"
|
||||
class="music-lrc"
|
||||
style="height: 55vh"
|
||||
style="height: 60vh"
|
||||
:native-scrollbar="false"
|
||||
@mouseover="mouseOverLayout"
|
||||
@mouseleave="mouseLeaveLayout"
|
||||
>
|
||||
<template v-for="(item, index) in lrcArray" :key="index">
|
||||
<div ref="lrcContainer">
|
||||
<div
|
||||
v-for="(item, index) in lrcArray"
|
||||
:id="`music-lrc-text-${index}`"
|
||||
:key="index"
|
||||
class="music-lrc-text"
|
||||
:class="{ 'now-text': isCurrentLrc(index, nowTime) }"
|
||||
:class="{ 'now-text': index === nowIndex }"
|
||||
@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>
|
||||
</template>
|
||||
</div>
|
||||
</n-layout>
|
||||
<!-- 时间矫正 -->
|
||||
<div class="music-content-time">
|
||||
<!-- <div class="music-content-time">
|
||||
<n-button @click="reduceCorrectionTime(0.2)">-</n-button>
|
||||
<n-button @click="addCorrectionTime(0.2)">+</n-button>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</n-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useStore } from 'vuex';
|
||||
import { useDebounceFn } from '@vueuse/core';
|
||||
|
||||
import {
|
||||
addCorrectionTime,
|
||||
isCurrentLrc,
|
||||
lrcArray,
|
||||
newLrcIndex,
|
||||
nowTime,
|
||||
nowIndex,
|
||||
playMusic,
|
||||
reduceCorrectionTime,
|
||||
setAudioTime,
|
||||
useLyricProgress,
|
||||
} from '@/hooks/MusicHook';
|
||||
import type { SongResult } from '@/type/music';
|
||||
import { getImgUrl } from '@/utils';
|
||||
|
||||
const store = useStore();
|
||||
const { getLrcStyle } = useLyricProgress();
|
||||
|
||||
// const isPlaying = computed(() => store.state.play as boolean);
|
||||
// 获取歌词滚动dom
|
||||
const lrcSider = ref<any>(null);
|
||||
const isMouse = ref(false);
|
||||
const lrcContainer = ref<HTMLElement | null>(null);
|
||||
|
||||
const props = defineProps({
|
||||
musicFull: {
|
||||
@@ -71,30 +77,51 @@ const props = defineProps({
|
||||
type: HTMLAudioElement,
|
||||
default: null,
|
||||
},
|
||||
background: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
// 播放的音乐信息
|
||||
const playMusic = computed(() => store.state.playMusic as SongResult);
|
||||
const isPlaying = computed(() => store.state.play as boolean);
|
||||
// 获取歌词滚动dom
|
||||
const lrcSider = ref<any>(null);
|
||||
const isMouse = ref(false);
|
||||
// 歌词滚动方法
|
||||
const lrcScroll = () => {
|
||||
if (props.musicFull && !isMouse.value) {
|
||||
const top = newLrcIndex.value * 60 - 225;
|
||||
lrcSider.value.scrollTo({ top, behavior: 'smooth' });
|
||||
const lrcScroll = (behavior = 'smooth') => {
|
||||
const nowEl = document.querySelector(`#music-lrc-text-${nowIndex.value}`);
|
||||
if (props.musicFull && !isMouse.value && nowEl && lrcContainer.value) {
|
||||
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 = () => {
|
||||
isMouse.value = true;
|
||||
};
|
||||
const mouseLeaveLayout = () => {
|
||||
setTimeout(() => {
|
||||
isMouse.value = false;
|
||||
}, 3000);
|
||||
lrcScroll();
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
watch(nowIndex, () => {
|
||||
debouncedLrcScroll();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.musicFull,
|
||||
() => {
|
||||
if (props.musicFull) {
|
||||
nextTick(() => {
|
||||
lrcScroll('instant');
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
defineExpose({
|
||||
lrcScroll,
|
||||
});
|
||||
@@ -110,14 +137,12 @@ defineExpose({
|
||||
}
|
||||
}
|
||||
.drawer-back {
|
||||
@apply absolute bg-cover bg-center opacity-70;
|
||||
filter: blur(80px) brightness(80%);
|
||||
@apply absolute bg-cover bg-center;
|
||||
z-index: -1;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
animation: round 20s linear infinite;
|
||||
}
|
||||
|
||||
.drawer-back.paused {
|
||||
@@ -125,30 +150,28 @@ defineExpose({
|
||||
}
|
||||
|
||||
#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);
|
||||
background-color: rgba(0, 0, 0, 0.747);
|
||||
animation-duration: 300ms;
|
||||
|
||||
.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 {
|
||||
width: 350px;
|
||||
height: 350px;
|
||||
@apply rounded-xl;
|
||||
@apply rounded-xl w-full h-full shadow-2xl;
|
||||
}
|
||||
}
|
||||
|
||||
.music-content {
|
||||
@apply flex flex-col justify-center items-center;
|
||||
@apply flex flex-col justify-center items-center relative;
|
||||
|
||||
&-name {
|
||||
@apply font-bold text-3xl py-2;
|
||||
@apply font-bold text-xl pb-1 pt-4;
|
||||
}
|
||||
|
||||
&-singer {
|
||||
@apply text-base py-2;
|
||||
@apply text-base;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,25 +179,25 @@ defineExpose({
|
||||
display: none;
|
||||
@apply flex justify-center items-center;
|
||||
}
|
||||
|
||||
.music-lrc {
|
||||
background-color: inherit;
|
||||
width: 500px;
|
||||
height: 550px;
|
||||
.now-text {
|
||||
@apply text-red-500;
|
||||
}
|
||||
&-text {
|
||||
@apply text-white text-lg flex flex-col justify-center items-center cursor-pointer font-bold;
|
||||
height: 60px;
|
||||
transition: all 0.2s ease-out;
|
||||
|
||||
@apply text-2xl cursor-pointer font-bold px-2 py-4;
|
||||
color: #ffffff8a;
|
||||
// transition: all 0.5s ease;
|
||||
span {
|
||||
padding-right: 100px;
|
||||
}
|
||||
&:hover {
|
||||
@apply font-bold text-red-500;
|
||||
@apply font-bold opacity-100 rounded-xl;
|
||||
background-color: #ffffff26;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&-tr {
|
||||
@apply text-sm font-normal;
|
||||
@apply font-normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
<template>
|
||||
<!-- 展开全屏 -->
|
||||
<music-full ref="MusicFullRef" v-model:musicFull="musicFull" :audio="audio.value as HTMLAudioElement" />
|
||||
<music-full
|
||||
ref="MusicFullRef"
|
||||
v-model:music-full="musicFullVisible"
|
||||
:audio="audio.value as HTMLAudioElement"
|
||||
:background="background"
|
||||
/>
|
||||
<!-- 底部播放栏 -->
|
||||
<div class="music-play-bar" :class="setAnimationClass('animate__bounceInUp')">
|
||||
<div
|
||||
class="music-play-bar"
|
||||
:class="setAnimationClass('animate__bounceInUp') + ' ' + (musicFullVisible ? 'play-bar-opcity' : '')"
|
||||
>
|
||||
<n-image
|
||||
:src="getImgUrl(playMusic?.picUrl, '300y300')"
|
||||
class="play-bar-img"
|
||||
@@ -59,13 +67,22 @@
|
||||
</template>
|
||||
解析播放
|
||||
</n-tooltip> -->
|
||||
<n-tooltip class="music-lyric" trigger="hover" :z-index="9999999">
|
||||
<n-tooltip v-if="isElectron" class="music-lyric" trigger="hover" :z-index="9999999">
|
||||
<template #trigger>
|
||||
<i class="iconfont ri-netease-cloud-music-line" @click="openLyric"></i>
|
||||
</template>
|
||||
歌词
|
||||
</n-tooltip>
|
||||
<n-popover trigger="click" :z-index="99999999" content-class="music-play" raw :show-arrow="false" :delay="200">
|
||||
<n-popover
|
||||
trigger="click"
|
||||
:z-index="99999999"
|
||||
content-class="music-play"
|
||||
raw
|
||||
:show-arrow="false"
|
||||
:delay="200"
|
||||
arrow-wrapper-style=" border-radius:1.5rem"
|
||||
@update-show="scrollToPlayList"
|
||||
>
|
||||
<template #trigger>
|
||||
<n-tooltip trigger="manual" :z-index="9999999">
|
||||
<template #trigger>
|
||||
@@ -76,11 +93,13 @@
|
||||
</template>
|
||||
<div class="music-play-list">
|
||||
<div class="music-play-list-back"></div>
|
||||
<n-scrollbar>
|
||||
<div class="music-play-list-content">
|
||||
<song-item v-for="item in playList" :key="item.id" :item="item" mini></song-item>
|
||||
</div>
|
||||
</n-scrollbar>
|
||||
<n-virtual-list ref="palyListRef" :item-size="62" item-resizable :items="playList">
|
||||
<template #default="{ item }">
|
||||
<div class="music-play-list-content">
|
||||
<song-item :key="item.id" :item="item" mini></song-item>
|
||||
</div>
|
||||
</template>
|
||||
</n-virtual-list>
|
||||
</div>
|
||||
</n-popover>
|
||||
</div>
|
||||
@@ -89,10 +108,11 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useTemplateRef } from 'vue';
|
||||
import { useStore } from 'vuex';
|
||||
|
||||
import SongItem from '@/components/common/SongItem.vue';
|
||||
import { allTime, loadLrc, nowTime, openLyric, sendLyricToWin } from '@/hooks/MusicHook';
|
||||
import { allTime, getCurrentLrc, isElectron, nowTime, openLyric, sendLyricToWin } from '@/hooks/MusicHook';
|
||||
import type { SongResult } from '@/type/music';
|
||||
import { getImgUrl, secondToMinute, setAnimationClass } from '@/utils';
|
||||
|
||||
@@ -110,13 +130,14 @@ const playList = computed(() => store.state.playList as SongResult[]);
|
||||
const audio = {
|
||||
value: document.querySelector('#MusicAudio') as HTMLAudioElement,
|
||||
};
|
||||
const background = ref('#000');
|
||||
|
||||
watch(
|
||||
() => store.state.playMusicUrl,
|
||||
() => {
|
||||
loadLrc(playMusic.value.id);
|
||||
() => store.state.playMusic,
|
||||
async () => {
|
||||
background.value = playMusic.value.backgroundColor as string;
|
||||
},
|
||||
{ immediate: true },
|
||||
{ immediate: true, deep: true },
|
||||
);
|
||||
|
||||
const audioPlay = () => {
|
||||
@@ -188,13 +209,16 @@ function handleGetAudioTime(this: HTMLAudioElement) {
|
||||
// 监听音频播放的实时时间事件
|
||||
const audio = this as HTMLAudioElement;
|
||||
// 获取当前播放时间
|
||||
nowTime.value = Math.floor(audio.currentTime);
|
||||
nowTime.value = audio.currentTime;
|
||||
getCurrentLrc();
|
||||
// 获取总时间
|
||||
allTime.value = audio.duration;
|
||||
// 获取音量
|
||||
audioVolume.value = audio.volume;
|
||||
sendLyricToWin(store.state.isPlay);
|
||||
MusicFullRef.value?.lrcScroll();
|
||||
// if (musicFullVisible.value) {
|
||||
// MusicFullRef.value?.lrcScroll();
|
||||
// }
|
||||
}
|
||||
|
||||
// 播放暂停按钮事件
|
||||
@@ -206,11 +230,20 @@ const playMusicEvent = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const musicFull = ref(false);
|
||||
const musicFullVisible = ref(false);
|
||||
|
||||
// 设置musicFull
|
||||
const setMusicFull = () => {
|
||||
musicFull.value = !musicFull.value;
|
||||
musicFullVisible.value = !musicFullVisible.value;
|
||||
};
|
||||
|
||||
const palyListRef = useTemplateRef('palyListRef');
|
||||
|
||||
const scrollToPlayList = (val: boolean) => {
|
||||
if (!val) return;
|
||||
setTimeout(() => {
|
||||
palyListRef.value?.scrollTo({ top: store.state.playListIndex * 62 });
|
||||
}, 50);
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -223,7 +256,7 @@ const setMusicFull = () => {
|
||||
@apply h-20 w-full absolute bottom-0 left-0 flex items-center rounded-t-2xl overflow-hidden box-border px-6 py-2;
|
||||
z-index: 9999;
|
||||
box-shadow: 0px 0px 10px 2px rgba(203, 203, 203, 0.034);
|
||||
background-color: rgba(0, 0, 0, 0.747);
|
||||
background-color: #212121;
|
||||
animation-duration: 0.5s !important;
|
||||
.music-content {
|
||||
width: 140px;
|
||||
@@ -240,6 +273,11 @@ const setMusicFull = () => {
|
||||
}
|
||||
}
|
||||
|
||||
.play-bar-opcity {
|
||||
@apply bg-transparent;
|
||||
box-shadow: 0 0 20px 5px #0000001d;
|
||||
}
|
||||
|
||||
.play-bar-img {
|
||||
@apply w-14 h-14 rounded-2xl;
|
||||
}
|
||||
@@ -262,8 +300,8 @@ const setMusicFull = () => {
|
||||
}
|
||||
|
||||
&-play {
|
||||
@apply flex justify-center items-center w-12 h-12 rounded-full mx-4 hover:bg-green-500 transition;
|
||||
background: #383838;
|
||||
@apply flex justify-center items-center w-12 h-12 rounded-full mx-4 hover:bg-green-500 transition bg-opacity-40;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,13 +333,14 @@ const setMusicFull = () => {
|
||||
.music-play {
|
||||
&-list {
|
||||
height: 50vh;
|
||||
@apply relative rounded-3xl overflow-hidden;
|
||||
width: 300px;
|
||||
@apply relative rounded-3xl overflow-hidden py-2;
|
||||
&-back {
|
||||
backdrop-filter: blur(20px);
|
||||
@apply absolute top-0 left-0 w-full h-full bg-gray-800 bg-opacity-75;
|
||||
}
|
||||
&-content {
|
||||
padding: 10px;
|
||||
@apply mx-2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
</template>
|
||||
<template #suffix>
|
||||
<div class="w-20 px-3 flex justify-between items-center">
|
||||
<div>{{ searchTypeOptions.find((item) => item.key === searchType)?.label }}</div>
|
||||
<div>{{ searchTypeOptions.find((item) => item.key === store.state.searchType)?.label }}</div>
|
||||
<n-dropdown trigger="hover" :options="searchTypeOptions" @select="selectSearchType">
|
||||
<i class="iconfont icon-xiasanjiaoxing"></i>
|
||||
</n-dropdown>
|
||||
@@ -90,7 +90,6 @@ onMounted(() => {
|
||||
|
||||
// 搜索词
|
||||
const searchValue = ref('');
|
||||
const searchType = ref(1);
|
||||
const search = () => {
|
||||
const { value } = searchValue;
|
||||
if (value === '') {
|
||||
@@ -98,17 +97,21 @@ const search = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (router.currentRoute.value.path === '/search') {
|
||||
store.state.searchValue = value;
|
||||
return;
|
||||
}
|
||||
|
||||
router.push({
|
||||
path: '/search',
|
||||
query: {
|
||||
keyword: value,
|
||||
type: searchType.value,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const selectSearchType = (key: number) => {
|
||||
searchType.value = key;
|
||||
store.state.searchType = key;
|
||||
};
|
||||
|
||||
const searchTypeOptions = ref(SEARCH_TYPES);
|
||||
|
||||
@@ -10,8 +10,13 @@ import router from '@/router';
|
||||
import store from '@/store';
|
||||
|
||||
import App from './App.vue';
|
||||
import directives from './directive';
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
Object.keys(directives).forEach((key: string) => {
|
||||
app.directive(key, directives[key]);
|
||||
});
|
||||
app.use(router);
|
||||
app.use(store);
|
||||
app.mount('#app');
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { createStore } from 'vuex';
|
||||
|
||||
import { getMusicUrl, getParsingMusicUrl } from '@/api/music';
|
||||
import { useMusicHistory } from '@/hooks/MusicHistoryHook';
|
||||
import { useMusicListHook } from '@/hooks/MusicListHook';
|
||||
import homeRouter from '@/router/home';
|
||||
import { SongResult } from '@/type/music';
|
||||
import { getMusicProxyUrl } from '@/utils';
|
||||
import type { SongResult } from '@/type/music';
|
||||
|
||||
interface State {
|
||||
menus: any[];
|
||||
@@ -18,6 +16,8 @@ interface State {
|
||||
setData: any;
|
||||
lyric: any;
|
||||
isMobile: boolean;
|
||||
searchValue: string;
|
||||
searchType: number;
|
||||
}
|
||||
|
||||
const state: State = {
|
||||
@@ -32,22 +32,19 @@ const state: State = {
|
||||
setData: null,
|
||||
lyric: {},
|
||||
isMobile: false,
|
||||
searchValue: '',
|
||||
searchType: 1,
|
||||
};
|
||||
|
||||
const windowData = window as any;
|
||||
|
||||
const musicHistory = useMusicHistory();
|
||||
const { handlePlayMusic, nextPlay, prevPlay } = useMusicListHook();
|
||||
|
||||
const mutations = {
|
||||
setMenus(state: State, menus: any[]) {
|
||||
state.menus = menus;
|
||||
},
|
||||
async setPlay(state: State, playMusic: SongResult) {
|
||||
state.playMusic = { ...playMusic, playLoading: true };
|
||||
state.playMusicUrl = await getSongUrl(playMusic.id);
|
||||
state.play = true;
|
||||
musicHistory.addMusic(playMusic);
|
||||
state.playMusic.playLoading = false;
|
||||
await handlePlayMusic(state, playMusic);
|
||||
},
|
||||
setIsPlay(state: State, isPlay: boolean) {
|
||||
state.isPlay = isPlay;
|
||||
@@ -60,49 +57,17 @@ const mutations = {
|
||||
state.playList = playList;
|
||||
},
|
||||
async nextPlay(state: State) {
|
||||
if (state.playList.length === 0) {
|
||||
state.play = true;
|
||||
return;
|
||||
}
|
||||
state.playListIndex = (state.playListIndex + 1) % state.playList.length;
|
||||
await updatePlayMusic(state);
|
||||
await nextPlay(state);
|
||||
},
|
||||
async prevPlay(state: State) {
|
||||
if (state.playList.length === 0) {
|
||||
state.play = true;
|
||||
return;
|
||||
}
|
||||
state.playListIndex = (state.playListIndex - 1 + state.playList.length) % state.playList.length;
|
||||
await updatePlayMusic(state);
|
||||
await prevPlay(state);
|
||||
},
|
||||
async setSetData(state: State, setData: any) {
|
||||
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 store = createStore({
|
||||
state,
|
||||
mutations,
|
||||
|
||||
@@ -3,6 +3,14 @@ export interface IRecommendMusic {
|
||||
category: number;
|
||||
result: SongResult[];
|
||||
}
|
||||
export interface ILyricText {
|
||||
text: string;
|
||||
trText: string;
|
||||
}
|
||||
export interface ILyric {
|
||||
lrcTimeArray: number[];
|
||||
lrcArray: ILyricText[];
|
||||
}
|
||||
|
||||
export interface SongResult {
|
||||
id: number;
|
||||
@@ -16,9 +24,15 @@ export interface SongResult {
|
||||
alg: string;
|
||||
count?: number;
|
||||
playLoading?: boolean;
|
||||
ar?: Artist[];
|
||||
al?: Album;
|
||||
backgroundColor?: string;
|
||||
primaryColor?: string;
|
||||
playMusicUrl?: string;
|
||||
lyric?: ILyric;
|
||||
}
|
||||
|
||||
interface Song {
|
||||
export interface Song {
|
||||
name: string;
|
||||
id: number;
|
||||
position: number;
|
||||
@@ -64,6 +78,9 @@ interface Song {
|
||||
lMusic: BMusic;
|
||||
exclusive: boolean;
|
||||
privilege: Privilege;
|
||||
count?: number;
|
||||
playLoading?: boolean;
|
||||
picUrl?: string;
|
||||
}
|
||||
|
||||
interface Privilege {
|
||||
|
||||
@@ -28,16 +28,21 @@ export const secondToMinute = (s: number) => {
|
||||
};
|
||||
|
||||
// 格式化数字 千,万, 百万, 千万,亿
|
||||
const units = [
|
||||
{ value: 1e8, symbol: '亿' },
|
||||
{ value: 1e4, symbol: '万' },
|
||||
];
|
||||
|
||||
export const formatNumber = (num: string | number) => {
|
||||
num = Number(num);
|
||||
if (num < 10000) {
|
||||
return num;
|
||||
for (let i = 0; i < units.length; i++) {
|
||||
if (num >= units[i].value) {
|
||||
return `${(num / units[i].value).toFixed(1)}${units[i].symbol}`;
|
||||
}
|
||||
}
|
||||
if (num < 100000000) {
|
||||
return `${(num / 10000).toFixed(1)}万`;
|
||||
}
|
||||
return `${(num / 100000000).toFixed(1)}亿`;
|
||||
return num.toString();
|
||||
};
|
||||
|
||||
const windowData = window as any;
|
||||
export const getIsMc = () => {
|
||||
if (!windowData.electron) {
|
||||
@@ -46,6 +51,7 @@ export const getIsMc = () => {
|
||||
if (windowData.electron.ipcRenderer.getStoreValue('set').isProxy) {
|
||||
return true;
|
||||
}
|
||||
if(window.location.origin.includes('localhost')){}
|
||||
return false;
|
||||
};
|
||||
const ProxyUrl = import.meta.env.VITE_API_PROXY || 'http://110.42.251.190:9856';
|
||||
@@ -58,11 +64,14 @@ export const getMusicProxyUrl = (url: string) => {
|
||||
return `${ProxyUrl}/mc?url=${PUrl}`;
|
||||
};
|
||||
|
||||
export const getImgUrl = computed(() => (url: string | undefined, size: string = '') => {
|
||||
export const getImgUrl = (url: string | undefined, size: string = '') => {
|
||||
const bdUrl = 'https://image.baidu.com/search/down?url=';
|
||||
const imgUrl = encodeURIComponent(`${url}?param=${size}`);
|
||||
return `${bdUrl}${imgUrl}`;
|
||||
});
|
||||
const imgUrl = `${url}?param=${size}`;
|
||||
if (!getIsMc()) {
|
||||
return imgUrl;
|
||||
}
|
||||
return `${bdUrl}${encodeURIComponent(imgUrl)}`;
|
||||
};
|
||||
|
||||
export const isMobile = computed(() => {
|
||||
const flag = navigator.userAgent.match(
|
||||
|
||||
183
src/utils/linearColor.ts
Normal file
183
src/utils/linearColor.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
interface IColor {
|
||||
backgroundColor: string;
|
||||
primaryColor: string;
|
||||
}
|
||||
|
||||
export const getImageLinearBackground = async (imageSrc: string): Promise<IColor> => {
|
||||
try {
|
||||
const primaryColor = await getImagePrimaryColor(imageSrc);
|
||||
return {
|
||||
backgroundColor: generateGradientBackground(primaryColor),
|
||||
primaryColor,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('error', error);
|
||||
return {
|
||||
backgroundColor: '',
|
||||
primaryColor: '',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const getImageBackground = async (img: HTMLImageElement): Promise<IColor> => {
|
||||
try {
|
||||
const primaryColor = await getImageColor(img);
|
||||
return {
|
||||
backgroundColor: generateGradientBackground(primaryColor),
|
||||
primaryColor,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('error', error);
|
||||
return {
|
||||
backgroundColor: '',
|
||||
primaryColor: '',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const getImageColor = (img: HTMLImageElement): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
reject(new Error('无法获取canvas上下文'));
|
||||
return;
|
||||
}
|
||||
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||
|
||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
const color = getAverageColor(imageData.data);
|
||||
resolve(`rgb(${color.join(',')})`);
|
||||
});
|
||||
};
|
||||
|
||||
const getImagePrimaryColor = (imageSrc: string): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.crossOrigin = 'Anonymous';
|
||||
img.src = imageSrc;
|
||||
|
||||
img.onload = () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
reject(new Error('无法获取canvas上下文'));
|
||||
return;
|
||||
}
|
||||
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||
|
||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
const color = getAverageColor(imageData.data);
|
||||
resolve(`rgb(${color.join(',')})`);
|
||||
};
|
||||
|
||||
img.onerror = () => reject(new Error('图片加载失败'));
|
||||
});
|
||||
};
|
||||
|
||||
const getAverageColor = (data: Uint8ClampedArray): number[] => {
|
||||
let r = 0;
|
||||
let g = 0;
|
||||
let b = 0;
|
||||
let count = 0;
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
r += data[i];
|
||||
g += data[i + 1];
|
||||
b += data[i + 2];
|
||||
count++;
|
||||
}
|
||||
return [Math.round(r / count), Math.round(g / count), Math.round(b / count)];
|
||||
};
|
||||
|
||||
const generateGradientBackground = (color: string): string => {
|
||||
const [r, g, b] = color.match(/\d+/g)?.map(Number) || [0, 0, 0];
|
||||
const [h, s, l] = rgbToHsl(r, g, b);
|
||||
|
||||
// 增加亮度和暗度的差异
|
||||
const lightL = Math.min(l + 0.2, 0.95);
|
||||
const darkL = Math.max(l - 0.3, 0.05);
|
||||
const midL = (lightL + darkL) / 2;
|
||||
|
||||
// 调整饱和度以增强效果
|
||||
const lightS = Math.min(s * 0.8, 1);
|
||||
const darkS = Math.min(s * 1.2, 1);
|
||||
|
||||
const [lightR, lightG, lightB] = hslToRgb(h, lightS, lightL);
|
||||
const [midR, midG, midB] = hslToRgb(h, s, midL);
|
||||
const [darkR, darkG, darkB] = hslToRgb(h, darkS, darkL);
|
||||
|
||||
const lightColor = `rgb(${lightR}, ${lightG}, ${lightB})`;
|
||||
const midColor = `rgb(${midR}, ${midG}, ${midB})`;
|
||||
const darkColor = `rgb(${darkR}, ${darkG}, ${darkB})`;
|
||||
|
||||
// 使用三个颜色点创建更丰富的渐变
|
||||
return `linear-gradient(to bottom, ${lightColor} 0%, ${midColor} 50%, ${darkColor} 100%)`;
|
||||
};
|
||||
|
||||
// Helper functions (unchanged)
|
||||
function rgbToHsl(r: number, g: number, b: number): [number, number, number] {
|
||||
r /= 255;
|
||||
g /= 255;
|
||||
b /= 255;
|
||||
const max = Math.max(r, g, b);
|
||||
const min = Math.min(r, g, b);
|
||||
let h = 0;
|
||||
let s;
|
||||
const l = (max + min) / 2;
|
||||
|
||||
if (max === min) {
|
||||
h = s = 0;
|
||||
} else {
|
||||
const d = max - min;
|
||||
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
||||
switch (max) {
|
||||
case r:
|
||||
h = (g - b) / d + (g < b ? 6 : 0);
|
||||
break;
|
||||
case g:
|
||||
h = (b - r) / d + 2;
|
||||
break;
|
||||
case b:
|
||||
h = (r - g) / d + 4;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
h /= 6;
|
||||
}
|
||||
|
||||
return [h, s, l];
|
||||
}
|
||||
|
||||
function hslToRgb(h: number, s: number, l: number): [number, number, number] {
|
||||
let r;
|
||||
let g;
|
||||
let b;
|
||||
|
||||
if (s === 0) {
|
||||
r = g = b = l;
|
||||
} else {
|
||||
const hue2rgb = (p: number, q: number, t: number) => {
|
||||
if (t < 0) t += 1;
|
||||
if (t > 1) t -= 1;
|
||||
if (t < 1 / 6) return p + (q - p) * 6 * t;
|
||||
if (t < 1 / 2) return q;
|
||||
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
||||
return p;
|
||||
};
|
||||
|
||||
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
||||
const p = 2 * l - q;
|
||||
r = hue2rgb(p, q, h + 1 / 3);
|
||||
g = hue2rgb(p, q, h);
|
||||
b = hue2rgb(p, q, h - 1 / 3);
|
||||
}
|
||||
|
||||
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
|
||||
}
|
||||
@@ -10,7 +10,7 @@
|
||||
:class="setAnimationClass('animate__bounceIn')"
|
||||
:style="setAnimationDelay(index, 30)"
|
||||
>
|
||||
<song-item class="history-item-content" :item="item" />
|
||||
<song-item class="history-item-content" :item="item" @play="handlePlay" />
|
||||
<div class="history-item-count">
|
||||
{{ item.count }}
|
||||
</div>
|
||||
@@ -24,6 +24,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useStore } from 'vuex';
|
||||
|
||||
import { useMusicHistory } from '@/hooks/MusicHistoryHook';
|
||||
import { setAnimationClass, setAnimationDelay } from '@/utils';
|
||||
|
||||
@@ -31,7 +33,12 @@ defineOptions({
|
||||
name: 'History',
|
||||
});
|
||||
|
||||
const store = useStore();
|
||||
const { delMusic, musicList } = useMusicHistory();
|
||||
|
||||
const handlePlay = () => {
|
||||
store.commit('setPlayList', musicList.value);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -16,19 +16,24 @@ const showMusic = ref(false);
|
||||
|
||||
const recommendItem = ref<IRecommendItem | null>();
|
||||
const listDetail = ref<IListDetail | null>();
|
||||
const listLoading = ref(true);
|
||||
const selectRecommendItem = async (item: IRecommendItem) => {
|
||||
listLoading.value = true;
|
||||
recommendItem.value = null;
|
||||
listDetail.value = null;
|
||||
showMusic.value = true;
|
||||
const { data } = await getListDetail(item.id);
|
||||
recommendItem.value = item;
|
||||
const { data } = await getListDetail(item.id);
|
||||
listDetail.value = data;
|
||||
listLoading.value = false;
|
||||
};
|
||||
|
||||
const route = useRoute();
|
||||
const listTitle = ref(route.query.type || '歌单列表');
|
||||
|
||||
const loading = ref(false);
|
||||
const loadList = async (type: string) => {
|
||||
loading.value = true;
|
||||
const params = {
|
||||
cat: type || '',
|
||||
limit: 30,
|
||||
@@ -36,6 +41,7 @@ const loadList = async (type: string) => {
|
||||
};
|
||||
const { data } = await getListByCat(params);
|
||||
recommendList.value = data.playlists;
|
||||
loading.value = false;
|
||||
};
|
||||
|
||||
if (route.query.type) {
|
||||
@@ -51,6 +57,7 @@ watch(
|
||||
async (newParams) => {
|
||||
if (newParams.type) {
|
||||
recommendList.value = null;
|
||||
listTitle.value = newParams.type || '歌单列表';
|
||||
loadList(newParams.type as string);
|
||||
}
|
||||
},
|
||||
@@ -62,7 +69,7 @@ watch(
|
||||
<div class="recommend-title" :class="setAnimationClass('animate__bounceInLeft')">{{ listTitle }}</div>
|
||||
<!-- 歌单列表 -->
|
||||
<n-scrollbar class="recommend" :size="100" @click="showMusic = false">
|
||||
<div v-if="recommendList" class="recommend-list">
|
||||
<div v-loading="loading" class="recommend-list">
|
||||
<div
|
||||
v-for="(item, index) in recommendList"
|
||||
:key="item.id"
|
||||
@@ -90,10 +97,11 @@ watch(
|
||||
</div>
|
||||
</n-scrollbar>
|
||||
<music-list
|
||||
v-if="listDetail?.playlist"
|
||||
v-model:show="showMusic"
|
||||
:name="listDetail?.playlist.name"
|
||||
:song-list="listDetail?.playlist.tracks"
|
||||
v-model:loading="listLoading"
|
||||
:name="recommendItem?.name || ''"
|
||||
:song-list="listDetail?.playlist.tracks || []"
|
||||
:list-info="listDetail?.playlist"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -70,6 +70,14 @@ const timerIsQr = (key: string) => {
|
||||
return timer;
|
||||
};
|
||||
|
||||
// 离开页面时
|
||||
onBeforeUnmount(() => {
|
||||
if (timerRef.value) {
|
||||
clearInterval(timerRef.value);
|
||||
timerRef.value = null;
|
||||
}
|
||||
});
|
||||
|
||||
// 是否扫码登陆
|
||||
const isQr = ref(!isMobile.value);
|
||||
const chooseQr = () => {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<h2>推荐MV</h2>
|
||||
</div>
|
||||
<n-scrollbar :size="100">
|
||||
<div class="mv-list-content" :class="setAnimationClass('animate__bounceInLeft')">
|
||||
<div v-loading="loading" class="mv-list-content" :class="setAnimationClass('animate__bounceInLeft')">
|
||||
<div
|
||||
v-for="(item, index) in mvList"
|
||||
:key="item.id"
|
||||
@@ -32,7 +32,7 @@
|
||||
</n-scrollbar>
|
||||
|
||||
<n-drawer :show="showMv" height="100vh" placement="bottom" :z-index="999999999">
|
||||
<div class="mv-detail">
|
||||
<div v-loading="mvLoading" class="mv-detail">
|
||||
<video :src="playMvUrl" controls autoplay></video>
|
||||
<div class="mv-detail-title">
|
||||
<div class="title">{{ playMvItem?.name }}</div>
|
||||
@@ -62,19 +62,25 @@ const mvList = ref<Array<IMvItem>>([]);
|
||||
const playMvItem = ref<IMvItem>();
|
||||
const playMvUrl = ref<string>();
|
||||
const store = useStore();
|
||||
const loading = ref(false);
|
||||
|
||||
onMounted(async () => {
|
||||
loading.value = true;
|
||||
const res = await getTopMv(30);
|
||||
mvList.value = res.data.data;
|
||||
loading.value = false;
|
||||
});
|
||||
|
||||
const mvLoading = ref(false);
|
||||
const handleShowMv = async (item: IMvItem) => {
|
||||
mvLoading.value = true;
|
||||
store.commit('setIsPlay', false);
|
||||
store.commit('setPlayMusic', false);
|
||||
showMv.value = true;
|
||||
const res = await getMvUrl(item.id);
|
||||
playMvItem.value = item;
|
||||
playMvUrl.value = res.data.data.url;
|
||||
mvLoading.value = false;
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
:class="setAnimationClass('animate__bounceInLeft')"
|
||||
:style="setAnimationDelay(index, 10)"
|
||||
class="hot-search-item"
|
||||
@click.stop="clickHotKeyword(item.searchWord)"
|
||||
@click.stop="loadSearch(item.searchWord, 1)"
|
||||
>
|
||||
<span class="hot-search-item-count" :class="{ 'hot-search-item-count-3': index < 3 }">{{ index + 1 }}</span>
|
||||
{{ item.searchWord }}
|
||||
@@ -29,32 +29,30 @@
|
||||
:native-scrollbar="false"
|
||||
>
|
||||
<div class="title">{{ hotKeyword }}</div>
|
||||
<n-spin :show="searchDetailLoading">
|
||||
<div class="search-list-box">
|
||||
<template v-if="searchDetail">
|
||||
<div
|
||||
v-for="(item, index) in searchDetail?.songs"
|
||||
:key="item.id"
|
||||
:class="setAnimationClass('animate__bounceInRight')"
|
||||
:style="setAnimationDelay(index, 50)"
|
||||
>
|
||||
<song-item :item="item" @play="handlePlay" />
|
||||
</div>
|
||||
<template v-for="(list, key) in searchDetail">
|
||||
<template v-if="key.toString() !== 'songs'">
|
||||
<div
|
||||
v-for="(item, index) in list"
|
||||
:key="item.id"
|
||||
:class="setAnimationClass('animate__bounceInRight')"
|
||||
:style="setAnimationDelay(index, 50)"
|
||||
>
|
||||
<SearchItem :item="item" />
|
||||
</div>
|
||||
</template>
|
||||
<div v-loading="searchDetailLoading" class="search-list-box">
|
||||
<template v-if="searchDetail">
|
||||
<div
|
||||
v-for="(item, index) in searchDetail?.songs"
|
||||
:key="item.id"
|
||||
:class="setAnimationClass('animate__bounceInRight')"
|
||||
:style="setAnimationDelay(index, 50)"
|
||||
>
|
||||
<song-item :item="item" @play="handlePlay" />
|
||||
</div>
|
||||
<template v-for="(list, key) in searchDetail">
|
||||
<template v-if="key.toString() !== 'songs'">
|
||||
<div
|
||||
v-for="(item, index) in list"
|
||||
:key="item.id"
|
||||
:class="setAnimationClass('animate__bounceInRight')"
|
||||
:style="setAnimationDelay(index, 50)"
|
||||
>
|
||||
<SearchItem :item="item" />
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</n-spin>
|
||||
</template>
|
||||
</div>
|
||||
</n-layout>
|
||||
</div>
|
||||
</template>
|
||||
@@ -62,7 +60,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { useDateFormat } from '@vueuse/core';
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useStore } from 'vuex';
|
||||
|
||||
import { getHotSearch } from '@/api/home';
|
||||
@@ -76,9 +74,10 @@ defineOptions({
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const store = useStore();
|
||||
|
||||
const searchDetail = ref<any>();
|
||||
const searchType = ref(Number(route.query.type) || 1);
|
||||
const searchType = computed(() => store.state.searchType as number);
|
||||
const searchDetailLoading = ref(false);
|
||||
|
||||
// 热搜列表
|
||||
@@ -90,29 +89,26 @@ const loadHotSearch = async () => {
|
||||
|
||||
onMounted(() => {
|
||||
loadHotSearch();
|
||||
loadSearch(route.query.keyword);
|
||||
});
|
||||
|
||||
const hotKeyword = ref(route.query.keyword || '搜索列表');
|
||||
const clickHotKeyword = (keyword: string) => {
|
||||
hotKeyword.value = keyword;
|
||||
router.push({
|
||||
path: '/search',
|
||||
query: {
|
||||
keyword,
|
||||
type: 1,
|
||||
},
|
||||
});
|
||||
// isHotSearchList.value = false;
|
||||
};
|
||||
|
||||
watch(
|
||||
() => store.state.searchValue,
|
||||
(value) => {
|
||||
loadSearch(value);
|
||||
},
|
||||
);
|
||||
|
||||
const dateFormat = (time: any) => useDateFormat(time, 'YYYY.MM.DD').value;
|
||||
const loadSearch = async (keywords: any) => {
|
||||
const loadSearch = async (keywords: any, type: any = null) => {
|
||||
hotKeyword.value = keywords;
|
||||
searchDetail.value = undefined;
|
||||
if (!keywords) return;
|
||||
|
||||
searchDetailLoading.value = true;
|
||||
const { data } = await getSearch({ keywords, type: searchType.value });
|
||||
const { data } = await getSearch({ keywords, type: type || searchType.value });
|
||||
|
||||
const songs = data.result.songs || [];
|
||||
const albums = data.result.albums || [];
|
||||
@@ -135,7 +131,6 @@ const loadSearch = async (keywords: any) => {
|
||||
// songs map 替换属性
|
||||
songs.forEach((item: any) => {
|
||||
item.picUrl = item.al.picUrl;
|
||||
item.song = item;
|
||||
item.artists = item.ar;
|
||||
});
|
||||
albums.forEach((item: any) => {
|
||||
@@ -151,18 +146,15 @@ const loadSearch = async (keywords: any) => {
|
||||
searchDetailLoading.value = false;
|
||||
};
|
||||
|
||||
loadSearch(route.query.keyword);
|
||||
|
||||
watch(
|
||||
() => route.query,
|
||||
async (newParams) => {
|
||||
searchType.value = Number(newParams.type || 1);
|
||||
loadSearch(newParams.keyword);
|
||||
() => route.path,
|
||||
async (path) => {
|
||||
if (path === '/search') {
|
||||
store.state.searchValue = route.query.keyword;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const handlePlay = () => {
|
||||
const tracks = searchDetail.value?.songs || [];
|
||||
store.commit('setPlayList', tracks);
|
||||
|
||||
@@ -49,7 +49,9 @@ const windowData = window as any;
|
||||
|
||||
const handleSave = () => {
|
||||
store.commit('setSetData', setData.value);
|
||||
windowData.electronAPI.restart();
|
||||
if (windowData.electronAPI) {
|
||||
windowData.electronAPI.restart();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { onBeforeRouteUpdate, useRouter } from 'vue-router';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useStore } from 'vuex';
|
||||
|
||||
import { getListDetail } from '@/api/list';
|
||||
@@ -21,6 +21,7 @@ const router = useRouter();
|
||||
const userDetail = ref<IUserDetail>();
|
||||
const playList = ref<any[]>([]);
|
||||
const recordList = ref();
|
||||
const infoLoading = ref(false);
|
||||
|
||||
const user = computed(() => store.state.user);
|
||||
|
||||
@@ -29,6 +30,7 @@ const loadPage = async () => {
|
||||
router.push('/login');
|
||||
return;
|
||||
}
|
||||
infoLoading.value = true;
|
||||
|
||||
const { data: userData } = await getUserDetail(user.value.userId);
|
||||
userDetail.value = userData;
|
||||
@@ -37,7 +39,12 @@ const loadPage = async () => {
|
||||
playList.value = playlistData.playlist;
|
||||
|
||||
const { data: recordData } = await getUserRecord(user.value.userId);
|
||||
recordList.value = recordData.allData;
|
||||
recordList.value = recordData.allData.map((item: any) => ({
|
||||
...item,
|
||||
...item.song,
|
||||
picUrl: item.song.al.picUrl,
|
||||
}));
|
||||
infoLoading.value = false;
|
||||
};
|
||||
|
||||
onActivated(() => {
|
||||
@@ -52,24 +59,12 @@ const isShowList = ref(false);
|
||||
const list = ref<Playlist>();
|
||||
// 展示歌单
|
||||
const showPlaylist = async (id: number) => {
|
||||
const { data } = await getListDetail(id);
|
||||
isShowList.value = true;
|
||||
list.value = {};
|
||||
const { data } = await getListDetail(id);
|
||||
list.value = data.playlist;
|
||||
};
|
||||
|
||||
// 格式化歌曲列表项
|
||||
const formatDetail = computed(() => (detail: any) => {
|
||||
const song = {
|
||||
artists: detail.ar,
|
||||
name: detail.al.name,
|
||||
id: detail.al.id,
|
||||
};
|
||||
|
||||
detail.song = song;
|
||||
detail.picUrl = detail.al.picUrl;
|
||||
return detail;
|
||||
});
|
||||
|
||||
const handlePlay = () => {
|
||||
const tracks = recordList.value || [];
|
||||
store.commit('setPlayList', tracks);
|
||||
@@ -77,7 +72,7 @@ const handlePlay = () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="user-page" @click.stop="isShowList = false">
|
||||
<div class="user-page">
|
||||
<div
|
||||
v-if="userDetail"
|
||||
class="left"
|
||||
@@ -106,7 +101,7 @@ const handlePlay = () => {
|
||||
<div class="uesr-signature">{{ userDetail.profile.signature }}</div>
|
||||
|
||||
<div class="play-list" :class="setAnimationClass('animate__fadeInLeft')">
|
||||
<div class=" ">创建的歌单</div>
|
||||
<div class="title">创建的歌单</div>
|
||||
<n-scrollbar>
|
||||
<div v-for="(item, index) in playList" :key="index" class="play-list-item" @click="showPlaylist(item.id)">
|
||||
<n-image :src="getImgUrl(item.coverImgUrl, '50y50')" class="play-list-item-img" lazy preview-disabled />
|
||||
@@ -120,25 +115,25 @@ const handlePlay = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!isMobile" class="right" :class="setAnimationClass('animate__fadeInRight')">
|
||||
<div v-if="!isMobile" v-loading="infoLoading" class="right" :class="setAnimationClass('animate__fadeInRight')">
|
||||
<div class="title">听歌排行</div>
|
||||
<div class="record-list">
|
||||
<n-scrollbar>
|
||||
<div
|
||||
v-for="(item, index) in recordList"
|
||||
:key="item.song.id"
|
||||
:key="item.id"
|
||||
class="record-item"
|
||||
:class="setAnimationClass('animate__bounceInUp')"
|
||||
:style="setAnimationDelay(index, 50)"
|
||||
:style="setAnimationDelay(index, 25)"
|
||||
>
|
||||
<song-item class="song-item" :item="formatDetail(item.song)" @play="handlePlay" />
|
||||
<song-item class="song-item" :item="item" @play="handlePlay" />
|
||||
<div class="play-count">{{ item.playCount }}次</div>
|
||||
</div>
|
||||
<play-bottom />
|
||||
</n-scrollbar>
|
||||
</div>
|
||||
</div>
|
||||
<music-list v-if="list" v-model:show="isShowList" :name="list.name" :song-list="list.tracks" />
|
||||
<music-list v-model:show="isShowList" :name="list?.name || ''" :song-list="list?.tracks || []" :list-info="list" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ export default defineConfig({
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
// 指定端口
|
||||
port: 4678,
|
||||
port: 4488,
|
||||
proxy: {
|
||||
// with options
|
||||
'/api': {
|
||||
|
||||
Reference in New Issue
Block a user