mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-04-03 14:20:50 +08:00
✨ feat: 优化主题效果 添加展开 menu功能 优化图片清晰度 添加随机播放功能(#20)
This commit is contained in:
3
components.d.ts
vendored
3
components.d.ts
vendored
@@ -27,10 +27,7 @@ declare module 'vue' {
|
||||
NLayout: typeof import('naive-ui')['NLayout']
|
||||
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
|
||||
NModal: typeof import('naive-ui')['NModal']
|
||||
NPagination: typeof import('naive-ui')['NPagination']
|
||||
NPopover: typeof import('naive-ui')['NPopover']
|
||||
NRadioButton: typeof import('naive-ui')['NRadioButton']
|
||||
NRadioGroup: typeof import('naive-ui')['NRadioGroup']
|
||||
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
||||
NSlider: typeof import('naive-ui')['NSlider']
|
||||
NSpin: typeof import('naive-ui')['NSpin']
|
||||
|
||||
@@ -25,24 +25,24 @@
|
||||
<div class="music-info">
|
||||
<div class="music-cover">
|
||||
<n-image
|
||||
:src="getImgUrl(cover ? listInfo?.coverImgUrl : displayedSongs[0]?.picUrl, '300y300')"
|
||||
:src="getImgUrl(cover ? listInfo?.coverImgUrl : displayedSongs[0]?.picUrl, '500y500')"
|
||||
class="cover-img"
|
||||
preview-disabled
|
||||
:class="setAnimationClass('animate__fadeIn')"
|
||||
object-fit="cover"
|
||||
/>
|
||||
</div>
|
||||
<div class="music-detail">
|
||||
<div v-if="listInfo?.creator" class="creator-info">
|
||||
<n-avatar round :size="24" :src="getImgUrl(listInfo.creator.avatarUrl, '50y50')" />
|
||||
<span class="creator-name">{{ listInfo.creator.nickname }}</span>
|
||||
</div>
|
||||
<div v-if="listInfo?.description" class="music-desc">
|
||||
<n-ellipsis :line-clamp="isMobile ? 3 : 10">
|
||||
{{ listInfo.description }}
|
||||
</n-ellipsis>
|
||||
</div>
|
||||
<div v-if="listInfo?.creator" class="creator-info">
|
||||
<n-avatar round :size="24" :src="getImgUrl(listInfo.creator.avatarUrl, '50y50')" />
|
||||
<span class="creator-name">{{ listInfo.creator.nickname }}</span>
|
||||
</div>
|
||||
|
||||
<n-scrollbar style="max-height: 200">
|
||||
<div v-if="listInfo?.description" class="music-desc">
|
||||
{{ listInfo.description }}
|
||||
</div>
|
||||
<play-bottom />
|
||||
</n-scrollbar>
|
||||
</div>
|
||||
|
||||
<!-- 右侧歌曲列表 -->
|
||||
@@ -248,25 +248,21 @@ watch(
|
||||
@apply w-[25%] flex-shrink-0 pr-8 flex flex-col;
|
||||
|
||||
.music-cover {
|
||||
@apply w-full aspect-square rounded-2xl overflow-hidden mb-4;
|
||||
@apply w-full aspect-square rounded-2xl overflow-hidden mb-4 min-h-[250px];
|
||||
.cover-img {
|
||||
@apply w-full h-full object-cover;
|
||||
}
|
||||
}
|
||||
|
||||
.music-detail {
|
||||
@apply flex flex-col flex-grow;
|
||||
|
||||
.creator-info {
|
||||
@apply flex items-center mb-4;
|
||||
.creator-name {
|
||||
@apply ml-2 text-gray-700 dark:text-gray-300;
|
||||
}
|
||||
.creator-info {
|
||||
@apply flex items-center mb-4;
|
||||
.creator-name {
|
||||
@apply ml-2 text-gray-700 dark:text-gray-300;
|
||||
}
|
||||
}
|
||||
|
||||
.music-desc {
|
||||
@apply text-sm text-gray-600 dark:text-gray-400 leading-relaxed;
|
||||
}
|
||||
.music-desc {
|
||||
@apply text-sm text-gray-600 dark:text-gray-400 leading-relaxed pr-4;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -136,7 +136,7 @@ onMounted(() => {
|
||||
}
|
||||
.play-list-type {
|
||||
width: 250px;
|
||||
@apply mx-6;
|
||||
@apply mr-4;
|
||||
&-item,
|
||||
&-showall {
|
||||
@apply bg-light dark:bg-black text-gray-900 dark:text-white;
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
:style="setAnimationDelay(0, 100)"
|
||||
>
|
||||
<div
|
||||
:style="setBackgroundImg(getImgUrl(dayRecommendData?.dailySongs[0].al.picUrl, '300y300'))"
|
||||
:style="setBackgroundImg(getImgUrl(dayRecommendData?.dailySongs[0].al.picUrl, '500y500'))"
|
||||
class="recommend-singer-item-bg"
|
||||
></div>
|
||||
<div
|
||||
@@ -34,7 +34,7 @@
|
||||
:class="setAnimationClass('animate__backInRight')"
|
||||
:style="setAnimationDelay(index + 1, 100)"
|
||||
>
|
||||
<div :style="setBackgroundImg(getImgUrl(item.picUrl, '300y300'))" class="recommend-singer-item-bg"></div>
|
||||
<div :style="setBackgroundImg(getImgUrl(item.picUrl, '500y500'))" class="recommend-singer-item-bg"></div>
|
||||
<div class="recommend-singer-item-count p-2 text-base text-gray-200 z-10">{{ item.musicSize }}首</div>
|
||||
<div class="recommend-singer-item-info z-10">
|
||||
<div class="recommend-singer-item-info-play" @click="toSearchSinger(item.name)">
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
<template>
|
||||
<n-drawer :show="show" height="100vh" placement="bottom" :z-index="999999999">
|
||||
<div class="mv-detail">
|
||||
<video :src="url" controls autoplay></video>
|
||||
<div class="mv-detail-title">
|
||||
<div class="title">{{ title }}</div>
|
||||
<button @click="close">
|
||||
<i class="iconfont icon-xiasanjiaoxing"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</n-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useStore } from 'vuex';
|
||||
|
||||
import { audioService } from '@/services/audioService';
|
||||
|
||||
const props = defineProps<{
|
||||
show: boolean;
|
||||
title: string;
|
||||
url: string;
|
||||
}>();
|
||||
|
||||
const store = useStore();
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
(val) => {
|
||||
if (val) {
|
||||
store.commit('setIsPlay', false);
|
||||
store.commit('setPlayMusic', false);
|
||||
audioService.getCurrentSound()?.pause();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits(['update:show']);
|
||||
|
||||
const close = () => {
|
||||
emit('update:show', false);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.mv-detail {
|
||||
@apply w-full h-full bg-black relative;
|
||||
|
||||
&-title {
|
||||
@apply absolute w-full left-0 flex justify-between h-16 px-6 py-2 text-xl font-bold items-center z-50 transition-all duration-300 ease-in-out -top-24;
|
||||
background: linear-gradient(0, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 1) 100%);
|
||||
button .icon-xiasanjiaoxing {
|
||||
@apply text-3xl;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
@apply text-green-400;
|
||||
}
|
||||
}
|
||||
|
||||
video {
|
||||
@apply w-full h-full;
|
||||
}
|
||||
video:hover + .mv-detail-title {
|
||||
@apply top-0;
|
||||
}
|
||||
|
||||
.mv-detail-title:hover {
|
||||
@apply top-0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="search-item" :class="item.type" @click="handleClick">
|
||||
<div class="search-item-img">
|
||||
<n-image :src="getImgUrl(item.picUrl, '320y180')" lazy preview-disabled />
|
||||
<n-image :src="getImgUrl(item.picUrl, item.type === 'mv' ? '320y180' : '100y100')" lazy preview-disabled />
|
||||
<div v-if="item.type === 'mv'" class="play">
|
||||
<i class="iconfont icon icon-play"></i>
|
||||
</div>
|
||||
@@ -17,6 +17,7 @@
|
||||
:name="item.name"
|
||||
:song-list="songList"
|
||||
:list-info="listInfo"
|
||||
:cover="false"
|
||||
/>
|
||||
<mv-player v-if="item.type === 'mv'" v-model:show="showPop" :current-mv="getCurrentMv()" no-list />
|
||||
</div>
|
||||
@@ -64,6 +65,14 @@ const handleClick = async () => {
|
||||
song.al.picUrl = song.al.picUrl || props.item.picUrl;
|
||||
return song;
|
||||
});
|
||||
listInfo.value = {
|
||||
...res.data.album,
|
||||
creator: {
|
||||
avatarUrl: res.data.album.artist.img1v1Url,
|
||||
nickname: `${res.data.album.artist.name} - ${res.data.album.company}`,
|
||||
},
|
||||
description: res.data.album.description,
|
||||
};
|
||||
}
|
||||
|
||||
if (props.item.type === 'playlist') {
|
||||
@@ -84,7 +93,7 @@ const handleClick = async () => {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.search-item {
|
||||
@apply rounded-3xl p-3 flex items-center hover:bg-gray-800 transition cursor-pointer;
|
||||
@apply rounded-3xl p-3 flex items-center hover:bg-light-200 dark:hover:bg-gray-800 transition cursor-pointer;
|
||||
margin: 0 10px;
|
||||
.search-item-img {
|
||||
@apply w-12 h-12 mr-4 rounded-2xl overflow-hidden;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<n-image
|
||||
v-if="item.picUrl"
|
||||
ref="songImg"
|
||||
:src="getImgUrl(item.picUrl, '40y40')"
|
||||
:src="getImgUrl(item.picUrl, '100y100')"
|
||||
class="song-item-img"
|
||||
preview-disabled
|
||||
:img-props="{
|
||||
|
||||
@@ -111,6 +111,22 @@ export const audioServiceOn = (audio: typeof audioService) => {
|
||||
if (store.state.playMode === 1) {
|
||||
// 单曲循环模式
|
||||
audio.getCurrentSound()?.play();
|
||||
} else if (store.state.playMode === 2) {
|
||||
// 随机播放模式
|
||||
const { playList } = store.state;
|
||||
if (playList.length <= 1) {
|
||||
// 如果播放列表只有一首歌或为空,则重新播放当前歌曲
|
||||
audio.getCurrentSound()?.play();
|
||||
} else {
|
||||
// 随机选择一首不同的歌
|
||||
let randomIndex;
|
||||
do {
|
||||
randomIndex = Math.floor(Math.random() * playList.length);
|
||||
} while (randomIndex === store.state.playListIndex && playList.length > 1);
|
||||
|
||||
store.state.playListIndex = randomIndex;
|
||||
store.commit('setPlay', playList[randomIndex]);
|
||||
}
|
||||
} else {
|
||||
// 列表循环模式
|
||||
store.commit('nextPlay');
|
||||
@@ -229,7 +245,7 @@ export const useLyricProgress = () => {
|
||||
};
|
||||
};
|
||||
|
||||
// 设置<EFBFBD><EFBFBD><EFBFBD>前播放时间
|
||||
// 设置当前播放时间
|
||||
export const setAudioTime = (index: number) => {
|
||||
const currentSound = sound.value;
|
||||
if (!currentSound) return;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- menu -->
|
||||
<div class="app-menu">
|
||||
<div class="app-menu" :class="{ 'app-menu-expanded': isText }">
|
||||
<div class="app-menu-header">
|
||||
<div class="app-menu-logo">
|
||||
<div class="app-menu-logo" @click="isText = !isText">
|
||||
<img src="/icon.png" class="w-9 h-9" alt="logo" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -11,7 +11,9 @@
|
||||
<div v-for="(item, index) in menus" :key="item.path" class="app-menu-item">
|
||||
<router-link class="app-menu-item-link" :to="item.path">
|
||||
<i class="iconfont app-menu-item-icon" :style="iconStyle(index)" :class="item.meta.icon"></i>
|
||||
<span v-if="isText" class="app-menu-item-text ml-3">{{ item.meta.title }}</span>
|
||||
<span v-if="isText" class="app-menu-item-text ml-3" :class="isChecked(index) ? 'text-green-500' : ''">{{
|
||||
item.meta.title
|
||||
}}</span>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
@@ -23,10 +25,6 @@
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
const props = defineProps({
|
||||
isText: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: '26px',
|
||||
@@ -54,24 +52,40 @@ watch(
|
||||
},
|
||||
);
|
||||
|
||||
const isChecked = (index: number) => {
|
||||
return path.value === props.menus[index].path;
|
||||
};
|
||||
|
||||
const iconStyle = (index: number) => {
|
||||
const style = {
|
||||
fontSize: props.size,
|
||||
color: path.value === props.menus[index].path ? props.selectColor : props.color,
|
||||
color: isChecked(index) ? props.selectColor : props.color,
|
||||
};
|
||||
return style;
|
||||
};
|
||||
|
||||
const isText = ref(false);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.app-menu {
|
||||
@apply flex-col items-center justify-center px-6 bg-light dark:bg-black;
|
||||
max-width: 100px;
|
||||
@apply flex-col items-center justify-center bg-light dark:bg-black transition-all duration-300 w-[100px] px-1;
|
||||
}
|
||||
|
||||
.app-menu-expanded {
|
||||
@apply w-[160px];
|
||||
.app-menu-item {
|
||||
@apply hover:bg-gray-100 dark:hover:bg-gray-800 rounded mr-4;
|
||||
}
|
||||
}
|
||||
|
||||
.app-menu-item-link,
|
||||
.app-menu-header {
|
||||
@apply flex items-center justify-center;
|
||||
@apply flex items-center w-[200px] overflow-hidden ml-2 px-5;
|
||||
}
|
||||
|
||||
.app-menu-header {
|
||||
@apply ml-1;
|
||||
}
|
||||
|
||||
.app-menu-item-link {
|
||||
@@ -82,7 +96,7 @@ const iconStyle = (index: number) => {
|
||||
@apply transition-all duration-200 text-gray-500 dark:text-gray-400;
|
||||
|
||||
&:hover {
|
||||
@apply text-green-500 scale-105;
|
||||
@apply text-green-500 scale-105 !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<div id="drawer-target">
|
||||
<div class="drawer-back"></div>
|
||||
<div class="music-img" :style="{ color: textColors.theme === 'dark' ? '#000000' : '#ffffff' }">
|
||||
<n-image ref="PicImgRef" :src="getImgUrl(playMusic?.picUrl, '300y300')" class="img" lazy preview-disabled />
|
||||
<n-image ref="PicImgRef" :src="getImgUrl(playMusic?.picUrl, '500y500')" class="img" lazy preview-disabled />
|
||||
<div>
|
||||
<div class="music-content-name">{{ playMusic.name }}</div>
|
||||
<div class="music-content-singer">
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<n-slider v-model:value="timeSlider" :step="1" :max="allTime" :min="0" :format-tooltip="formatTooltip"></n-slider>
|
||||
</div>
|
||||
<div class="play-bar-img-wrapper" @click="setMusicFull">
|
||||
<n-image :src="getImgUrl(playMusic?.picUrl, '300y300')" class="play-bar-img" lazy preview-disabled />
|
||||
<n-image :src="getImgUrl(playMusic?.picUrl, '500y500')" class="play-bar-img" lazy preview-disabled />
|
||||
<div class="hover-arrow">
|
||||
<div class="hover-content">
|
||||
<!-- <i class="ri-arrow-up-s-line text-3xl" :class="{ 'ri-arrow-down-s-line': musicFullVisible }"></i> -->
|
||||
@@ -202,10 +202,28 @@ const mute = () => {
|
||||
// 播放模式
|
||||
const playMode = computed(() => store.state.playMode);
|
||||
const playModeIcon = computed(() => {
|
||||
return playMode.value === 0 ? 'ri-repeat-2-line' : 'ri-repeat-one-line';
|
||||
switch (playMode.value) {
|
||||
case 0:
|
||||
return 'ri-repeat-2-line';
|
||||
case 1:
|
||||
return 'ri-repeat-one-line';
|
||||
case 2:
|
||||
return 'ri-shuffle-line';
|
||||
default:
|
||||
return 'ri-repeat-2-line';
|
||||
}
|
||||
});
|
||||
const playModeText = computed(() => {
|
||||
return playMode.value === 0 ? '列表循环' : '单曲循环';
|
||||
switch (playMode.value) {
|
||||
case 0:
|
||||
return '列表循环';
|
||||
case 1:
|
||||
return '单曲循环';
|
||||
case 2:
|
||||
return '随机播放';
|
||||
default:
|
||||
return '列表循环';
|
||||
}
|
||||
});
|
||||
|
||||
// 切换播放模式
|
||||
|
||||
@@ -59,7 +59,7 @@ const layoutRouter = [
|
||||
name: 'history',
|
||||
component: () => import('@/views/historyAndFavorite/index.vue'),
|
||||
meta: {
|
||||
title: '我的收藏和历史',
|
||||
title: '收藏历史',
|
||||
icon: 'icon-a-TicketStar',
|
||||
keepAlive: true,
|
||||
},
|
||||
|
||||
@@ -102,7 +102,7 @@ const mutations = {
|
||||
localStorage.setItem('favoriteList', JSON.stringify(state.favoriteList));
|
||||
},
|
||||
togglePlayMode(state: State) {
|
||||
state.playMode = state.playMode === 0 ? 1 : 0;
|
||||
state.playMode = (state.playMode + 1) % 3;
|
||||
localStorage.setItem('playMode', JSON.stringify(state.playMode));
|
||||
},
|
||||
toggleTheme(state: State) {
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
<div class="list-page">
|
||||
<!-- 修改歌单分类部分 -->
|
||||
<div class="play-list-type">
|
||||
<n-scrollbar x-scrollable>
|
||||
<div class="categories-wrapper">
|
||||
<n-scrollbar ref="scrollbarRef" x-scrollable>
|
||||
<div class="categories-wrapper" @wheel.prevent="handleWheel">
|
||||
<span
|
||||
v-for="(item, index) in playlistCategory?.sub"
|
||||
:key="item.name"
|
||||
@@ -122,7 +122,7 @@ const loadList = async (type: string, isLoadMore = false) => {
|
||||
|
||||
try {
|
||||
const params = {
|
||||
cat: type || '',
|
||||
cat: type === '每日推荐' ? '' : type,
|
||||
limit: TOTAL_ITEMS,
|
||||
offset: page.value * TOTAL_ITEMS,
|
||||
};
|
||||
@@ -181,6 +181,16 @@ const handleClickPlaylistType = (type: string) => {
|
||||
loadList(type);
|
||||
};
|
||||
|
||||
const scrollbarRef = ref();
|
||||
|
||||
const handleWheel = (e: WheelEvent) => {
|
||||
const scrollbar = scrollbarRef.value;
|
||||
if (scrollbar) {
|
||||
const delta = e.deltaY || e.detail;
|
||||
scrollbar.scrollBy({ left: delta });
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadPlaylistCategory(); // 添加加载歌单分类
|
||||
currentType.value = (route.query.type as string) || currentType.value;
|
||||
|
||||
@@ -92,9 +92,10 @@ watch(selectedCategory, async () => {
|
||||
await loadMvList();
|
||||
});
|
||||
|
||||
const getAnimationDelay = computed(() => {
|
||||
return (index: number) => setAnimationDelay(index, 30);
|
||||
});
|
||||
const getAnimationDelay = (index: number) => {
|
||||
const currentPageIndex = index % limit.value;
|
||||
return setAnimationDelay(currentPageIndex, 30);
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await loadMvList();
|
||||
|
||||
Reference in New Issue
Block a user