feat: 添加eslint 和 桌面歌词(未完成)

This commit is contained in:
alger
2024-05-16 18:54:30 +08:00
parent 5e8676a039
commit a9e5bb33e4
65 changed files with 2724 additions and 2320 deletions
+19 -13
View File
@@ -1,10 +1,16 @@
<template>
<div class="history-page">
<div class="title">播放历史</div>
<n-button @click="openLyric">打开歌词</n-button>
<n-scrollbar :size="100">
<div class="history-list-content" :class="setAnimationClass('animate__bounceInLeft')">
<div class="history-item" v-for="(item, index) in musicList" :key="item.id"
:class="setAnimationClass('animate__bounceIn')" :style="setAnimationDelay(index, 30)">
<div
v-for="(item, index) in musicList"
:key="item.id"
class="history-item"
:class="setAnimationClass('animate__bounceIn')"
:style="setAnimationDelay(index, 30)"
>
<song-item class="history-item-content" :item="item" />
<div class="history-item-count">
{{ item.count }}
@@ -19,33 +25,33 @@
</template>
<script setup lang="ts">
import { useMusicHistory } from '@/hooks/MusicHistoryHook'
import { setAnimationClass, setAnimationDelay } from "@/utils";
import { useMusicHistory } from '@/hooks/MusicHistoryHook';
import { setAnimationClass, setAnimationDelay } from '@/utils';
const {delMusic, musicList} = useMusicHistory();
const { delMusic, musicList } = useMusicHistory();
</script>
<style scoped lang="scss">
.history-page{
.history-page {
@apply h-full w-full pt-2;
.title{
.title {
@apply text-xl font-bold;
}
.history-list-content{
.history-list-content {
@apply px-4 mt-2;
.history-item{
.history-item {
@apply flex items-center justify-between;
&-content{
&-content {
@apply flex-1;
}
&-count{
&-count {
@apply px-4 text-lg;
}
&-delete{
&-delete {
@apply cursor-pointer rounded-full border-2 border-gray-400 w-8 h-8 flex justify-center items-center;
}
}
}
}
</style>
</style>
+15 -15
View File
@@ -1,27 +1,27 @@
<template>
<div class="main-page">
<!-- 推荐歌手 -->
<recommend-singer />
<div class="main-content">
<!-- 歌单分类列表 -->
<playlist-type />
<!-- 本周最热音乐 -->
<recommend-songlist />
<!-- 推荐最新专辑 -->
<recommend-album />
</div>
<!-- 推荐歌手 -->
<recommend-singer />
<div class="main-content">
<!-- 歌单分类列表 -->
<playlist-type />
<!-- 本周最热音乐 -->
<recommend-songlist />
<!-- 推荐最新专辑 -->
<recommend-album />
</div>
</div>
</template>
<script lang="ts" setup>
const RecommendSinger = defineAsyncComponent(() => import("@/components/RecommendSinger.vue"));
const PlaylistType = defineAsyncComponent(() => import("@/components/PlaylistType.vue"));
const RecommendSonglist = defineAsyncComponent(() => import("@/components/RecommendSonglist.vue"));
const RecommendAlbum = defineAsyncComponent(() => import("@/components/RecommendAlbum.vue"));
const RecommendSinger = defineAsyncComponent(() => import('@/components/RecommendSinger.vue'));
const PlaylistType = defineAsyncComponent(() => import('@/components/PlaylistType.vue'));
const RecommendSonglist = defineAsyncComponent(() => import('@/components/RecommendSonglist.vue'));
const RecommendAlbum = defineAsyncComponent(() => import('@/components/RecommendAlbum.vue'));
</script>
<style lang="scss" scoped>
.main-page{
.main-page {
@apply h-full w-full;
}
.main-content {
+42 -47
View File
@@ -1,75 +1,66 @@
<script lang="ts" setup>
import { getRecommendList, getListDetail, getListByCat } from '@/api/list'
import type { IRecommendItem } from "@/type/list";
import type { IListDetail } from "@/type/listDetail";
import { setAnimationClass, setAnimationDelay, getImgUrl, formatNumber } from "@/utils";
import { useRoute } from 'vue-router';
import MusicList from "@/components/MusicList.vue";
import { getListByCat, getListDetail, getRecommendList } from '@/api/list';
import PlayBottom from '@/components/common/PlayBottom.vue';
import MusicList from '@/components/MusicList.vue';
import type { IRecommendItem } from '@/type/list';
import type { IListDetail } from '@/type/listDetail';
import { formatNumber, getImgUrl, setAnimationClass, setAnimationDelay } from '@/utils';
const recommendList = ref()
const showMusic = ref(false)
const recommendList = ref();
const showMusic = ref(false);
const recommendItem = ref<IRecommendItem>()
const listDetail = ref<IListDetail>()
const recommendItem = ref<IRecommendItem>();
const listDetail = ref<IListDetail>();
const selectRecommendItem = async (item: IRecommendItem) => {
showMusic.value = true
const { data } = await getListDetail(item.id)
recommendItem.value = item
listDetail.value = data
}
showMusic.value = true;
const { data } = await getListDetail(item.id);
recommendItem.value = item;
listDetail.value = data;
};
const route = useRoute();
const listTitle = ref(route.query.type || "歌单列表");
const listTitle = ref(route.query.type || '歌单列表');
const loadList = async (type: any) => {
const params = {
cat: type || '',
limit: 30,
offset: 0
}
offset: 0,
};
const { data } = await getListByCat(params);
recommendList.value = data.playlists
}
recommendList.value = data.playlists;
};
if (route.query.type) {
loadList(route.query.type)
loadList(route.query.type);
} else {
getRecommendList().then((res: { data: { result: any; }; }) => {
recommendList.value = res.data.result
})
getRecommendList().then((res: { data: { result: any } }) => {
recommendList.value = res.data.result;
});
}
watch(
() => route.query,
async newParams => {
if(newParams.type){
const params = {
tag: newParams.type || '',
limit: 30,
before: 0
}
async (newParams) => {
if (newParams.type) {
loadList(newParams.type);
}
}
)
},
);
</script>
<template>
<div class="list-page">
<div
class="recommend-title"
:class="setAnimationClass('animate__bounceInLeft')"
>{{ listTitle }}</div>
<div class="recommend-title" :class="setAnimationClass('animate__bounceInLeft')">{{ listTitle }}</div>
<!-- 歌单列表 -->
<n-scrollbar class="recommend" @click="showMusic = false" :size="100">
<div class="recommend-list" v-if="recommendList">
<n-scrollbar class="recommend" :size="100" @click="showMusic = false">
<div v-if="recommendList" class="recommend-list">
<div
v-for="(item, index) in recommendList"
:key="item.id"
class="recommend-item"
v-for="(item,index) in recommendList"
:class="setAnimationClass('animate__bounceIn')"
:style="setAnimationDelay(index, 30)"
@click.stop="selectRecommendItem(item)"
@@ -77,7 +68,7 @@ watch(
<div class="recommend-item-img">
<n-image
class="recommend-item-img-img"
:src="getImgUrl( (item.picUrl || item.coverImgUrl), '200y200')"
:src="getImgUrl(item.picUrl || item.coverImgUrl, '200y200')"
width="200"
height="200"
lazy
@@ -91,9 +82,14 @@ watch(
<div class="recommend-item-title">{{ item.name }}</div>
</div>
</div>
<PlayBottom/>
<play-bottom />
</n-scrollbar>
<MusicList v-if="listDetail?.playlist" v-model:show="showMusic" :name="listDetail?.playlist.name" :song-list="listDetail?.playlist.tracks" />
<music-list
v-if="listDetail?.playlist"
v-model:show="showMusic"
:name="listDetail?.playlist.name"
:song-list="listDetail?.playlist.tracks"
/>
</div>
</template>
@@ -150,5 +146,4 @@ watch(
}
}
}
</style>
</style>
+165 -170
View File
@@ -1,170 +1,165 @@
<script lang="ts" setup>
import { getQrKey, createQr, checkQr, getLoginStatus } from '@/api/login'
import { onMounted } from '@vue/runtime-core';
import { getUserDetail, loginByCellphone } from '@/api/login';
import { useStore } from 'vuex';
import { useMessage } from 'naive-ui'
import { setAnimationClass, setAnimationDelay } from "@/utils";
import { useRouter } from 'vue-router';
const message = useMessage()
const store = useStore();
const router = useRouter()
const qrUrl = ref<string>()
onMounted(() => {
loadLogin()
})
const loadLogin = async () => {
const qrKey = await getQrKey()
const key = qrKey.data.data.unikey
const { data } = await createQr(key)
qrUrl.value = data.data.qrimg
timerIsQr(key)
}
const timerIsQr = (key: string) => {
const timer = setInterval(async () => {
const { data } = await checkQr(key)
if (data.code === 800) {
clearInterval(timer)
}
if (data.code === 803) {
// 将token存入localStorage
localStorage.setItem('token', data.cookie)
const user = await getUserDetail()
store.state.user = user.data.profile
message.success('登录成功')
await getLoginStatus().then(res => {
console.log(res);
})
clearInterval(timer)
setTimeout(() => {
router.push('/user')
}, 1000);
}
}, 5000);
}
// 是否扫码登陆
const isQr = ref(true)
const chooseQr = () => {
isQr.value = !isQr.value
}
// 手机号登录
const phone = ref('')
const password = ref('')
const loginPhone = async () => {
const { data } = await loginByCellphone(phone.value, password.value)
if (data.code === 200) {
message.success('登录成功')
store.state.user = data.profile
localStorage.setItem('token', data.cookie)
setTimeout(() => {
router.push('/user')
}, 1000);
}
}
</script>
<template>
<div class="login-page">
<div class="phone-login">
<div class="bg"></div>
<div class="content">
<div class="phone" v-if="isQr" :class="setAnimationClass('animate__fadeInUp')">
<div class="login-title">扫码登陆</div>
<img class="qr-img" :src="qrUrl" />
<div class="text">使用网易云APP扫码登录</div>
</div>
<div class="phone" v-else :class="setAnimationClass('animate__fadeInUp')">
<div class="login-title">手机号登录</div>
<div class="phone-page">
<input v-model="phone" class="phone-input" type="text" placeholder="手机号" />
<input v-model="password" class="phone-input" type="password" placeholder="密码" />
</div>
<n-button class="btn-login" @click="loginPhone()">登录</n-button>
</div>
</div>
<div class="bottom">
<div class="title" @click="chooseQr()">{{ isQr ? '手机号登录' : '扫码登录' }}</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.login-page {
@apply flex flex-col items-center justify-center p-20 pt-20;
}
.login-title {
@apply text-2xl font-bold mb-6;
}
.text {
@apply mt-4 text-green-500 text-xs;
}
.phone-login {
width: 350px;
height: 550px;
@apply rounded-2xl rounded-b-none bg-cover bg-no-repeat relative overflow-hidden;
background-image: url(http://tva4.sinaimg.cn/large/006opRgRgy1gw8nf6no7uj30rs15n0x7.jpg);
background-color: #383838;
box-shadow: inset 0px 0px 20px 5px #0000005e;
.bg {
@apply absolute w-full h-full bg-black opacity-30;
}
.bottom {
width: 200%;
height: 250px;
bottom: -180px;
border-radius: 50%;
left: 50%;
padding: 10px;
transform: translateX(-50%);
color: #ffffff99;
@apply absolute bg-black flex justify-center text-lg font-bold cursor-pointer;
box-shadow: 10px 0px 20px #000000a9;
}
.content {
@apply absolute w-full h-full p-4 flex flex-col items-center justify-center pb-20 text-center;
.qr-img {
@apply opacity-80 rounded-2xl cursor-pointer;
}
.phone {
animation-duration: 0.5s;
&-page {
background-color: #ffffffdd;
width: 250px;
@apply rounded-2xl overflow-hidden;
}
&-input {
height: 40px;
border-bottom: 1px solid #e5e5e5;
@apply w-full text-black px-4 outline-none;
}
}
.btn-login {
width: 250px;
height: 40px;
@apply mt-10 text-white rounded-xl bg-black opacity-60;
}
}
}
</style>
<script lang="ts" setup>
import { useMessage } from 'naive-ui';
import { onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { useStore } from 'vuex';
import { checkQr, createQr, getLoginStatus, getQrKey, getUserDetail, loginByCellphone } from '@/api/login';
import { setAnimationClass } from '@/utils';
const message = useMessage();
const store = useStore();
const router = useRouter();
const qrUrl = ref<string>();
onMounted(() => {
loadLogin();
});
const loadLogin = async () => {
const qrKey = await getQrKey();
const key = qrKey.data.data.unikey;
const { data } = await createQr(key);
qrUrl.value = data.data.qrimg;
timerIsQr(key);
};
const timerIsQr = (key: string) => {
const timer = setInterval(async () => {
const { data } = await checkQr(key);
if (data.code === 800) {
clearInterval(timer);
}
if (data.code === 803) {
// 将token存入localStorage
localStorage.setItem('token', data.cookie);
const user = await getUserDetail();
store.state.user = user.data.profile;
message.success('登录成功');
await getLoginStatus().then((res) => {
console.log(res);
});
clearInterval(timer);
setTimeout(() => {
router.push('/user');
}, 1000);
}
}, 5000);
};
// 是否扫码登陆
const isQr = ref(true);
const chooseQr = () => {
isQr.value = !isQr.value;
};
// 手机号登录
const phone = ref('');
const password = ref('');
const loginPhone = async () => {
const { data } = await loginByCellphone(phone.value, password.value);
if (data.code === 200) {
message.success('登录成功');
store.state.user = data.profile;
localStorage.setItem('token', data.cookie);
setTimeout(() => {
router.push('/user');
}, 1000);
}
};
</script>
<template>
<div class="login-page">
<div class="phone-login">
<div class="bg"></div>
<div class="content">
<div v-if="isQr" class="phone" :class="setAnimationClass('animate__fadeInUp')">
<div class="login-title">扫码登陆</div>
<img class="qr-img" :src="qrUrl" />
<div class="text">使用网易云APP扫码登录</div>
</div>
<div v-else class="phone" :class="setAnimationClass('animate__fadeInUp')">
<div class="login-title">手机号登录</div>
<div class="phone-page">
<input v-model="phone" class="phone-input" type="text" placeholder="手机号" />
<input v-model="password" class="phone-input" type="password" placeholder="密码" />
</div>
<n-button class="btn-login" @click="loginPhone()">登录</n-button>
</div>
</div>
<div class="bottom">
<div class="title" @click="chooseQr()">{{ isQr ? '手机号登录' : '扫码登录' }}</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.login-page {
@apply flex flex-col items-center justify-center p-20 pt-20;
}
.login-title {
@apply text-2xl font-bold mb-6;
}
.text {
@apply mt-4 text-green-500 text-xs;
}
.phone-login {
width: 350px;
height: 550px;
@apply rounded-2xl rounded-b-none bg-cover bg-no-repeat relative overflow-hidden;
background-image: url(http://tva4.sinaimg.cn/large/006opRgRgy1gw8nf6no7uj30rs15n0x7.jpg);
background-color: #383838;
box-shadow: inset 0px 0px 20px 5px #0000005e;
.bg {
@apply absolute w-full h-full bg-black opacity-30;
}
.bottom {
width: 200%;
height: 250px;
bottom: -180px;
border-radius: 50%;
left: 50%;
padding: 10px;
transform: translateX(-50%);
color: #ffffff99;
@apply absolute bg-black flex justify-center text-lg font-bold cursor-pointer;
box-shadow: 10px 0px 20px #000000a9;
}
.content {
@apply absolute w-full h-full p-4 flex flex-col items-center justify-center pb-20 text-center;
.qr-img {
@apply opacity-80 rounded-2xl cursor-pointer;
}
.phone {
animation-duration: 0.5s;
&-page {
background-color: #ffffffdd;
width: 250px;
@apply rounded-2xl overflow-hidden;
}
&-input {
height: 40px;
border-bottom: 1px solid #e5e5e5;
@apply w-full text-black px-4 outline-none;
}
}
.btn-login {
width: 250px;
height: 40px;
@apply mt-10 text-white rounded-xl bg-black opacity-60;
}
}
}
</style>
+182
View File
@@ -0,0 +1,182 @@
<template>
<div class="lyric-window" :class="theme">
<div class="lyric-bar" :class="{ 'lyric-bar-hover': isDrag }">
<div class="buttons">
<!-- <div class="music-buttons">
<div @click="handlePrev">
<i class="iconfont icon-prev"></i>
</div>
<div class="music-buttons-play" @click="playMusicEvent">
<i class="iconfont icon" :class="play ? 'icon-stop' : 'icon-play'"></i>
</div>
<div @click="handleEnded">
<i class="iconfont icon-next"></i>
</div>
</div> -->
<div class="check-theme" @click="checkTheme">
<i v-if="theme === 'light'" class="icon ri-sun-line"></i>
<i v-else class="icon ri-moon-line"></i>
</div>
<div class="button-move">
<i class="icon ri-drag-move-2-line"></i>
</div>
</div>
</div>
<div v-if="lyricData.lrcArray[lyricData.nowIndex]" class="lyric-box">
<h2 class="lyric lyric-current">{{ lyricData.lrcArray[lyricData.nowIndex].text }}</h2>
<p class="lyric-current">{{ lyricData.currentLrc.trText }}</p>
<template v-if="lyricData.lrcArray[lyricData.nowIndex + 1]">
<h2 class="lyric lyric-next">
{{ lyricData.lrcArray[lyricData.nowIndex + 1].text }}
</h2>
<p class="lyric-next">{{ lyricData.nextLrc.trText }}</p>
</template>
</div>
</div>
</template>
<script setup lang="ts">
import { useIpcRenderer } from '@vueuse/electron';
const windowData = window as any;
const ipcRenderer = useIpcRenderer();
const lyricData = ref({
currentLrc: {
text: '',
trText: '',
},
nextLrc: {
text: '',
trText: '',
},
currentTime: 0,
nextTime: 0,
nowTime: 0,
allTime: 0,
startCurrentTime: 0,
lrcArray: [] as any,
lrcTimeArray: [] as any,
nowIndex: 0,
});
onMounted(() => {
ipcRenderer.on('receive-lyric', (event, data) => {
try {
lyricData.value = JSON.parse(data);
console.log('lyricData.value', lyricData.value);
} catch (error) {
console.error('error', error);
}
});
});
const theme = ref('dark');
const checkTheme = () => {
if (theme.value === 'light') {
theme.value = 'dark';
} else {
theme.value = 'light';
}
};
// const drag = (event: MouseEvent) => {
// windowData.electronAPI.dragStart(event);
// };
</script>
<style>
body {
background-color: transparent !important;
}
</style>
<style lang="scss" scoped>
.lyric-window {
width: 100vw;
height: 100vh;
@apply overflow-hidden text-gray-600 hover:bg-gray-400 hover:bg-opacity-75;
&:hover .lyric-bar {
opacity: 1;
}
}
.icon {
@apply text-xl hover:text-white;
}
.lyric-bar {
background-color: #b1b1b1;
@apply flex flex-col justify-center items-center;
width: 100vw;
height: 100px;
opacity: 0;
&:hover {
opacity: 1;
}
}
.lyric-bar-hover {
opacity: 1;
}
.buttons {
width: 100vw;
height: 100px;
@apply flex justify-center items-center;
}
.button-move {
-webkit-app-region: drag;
cursor: move;
}
.music-buttons {
@apply mx-6;
-webkit-app-region: no-drag;
.iconfont {
@apply text-2xl hover:text-green-500 transition;
}
@apply flex items-center;
> div {
@apply cursor-pointer;
}
&-play {
@apply flex justify-center items-center w-12 h-12 rounded-full mx-4 hover:bg-green-500 transition;
background: #383838;
}
}
.check-theme {
font-size: 26px;
cursor: pointer;
opacity: 1;
}
.lyric {
text-shadow: 0 0 10px #fff;
font-size: 4vw;
@apply font-bold m-0 p-0 whitespace-nowrap select-none pointer-events-none;
}
.lyric-current {
color: #333;
}
.lyric-next {
color: #999;
margin: 10px;
}
.lyric-window.dark {
.lyric {
text-shadow: none;
}
.lyric-current {
color: #fff;
}
.lyric-next {
color: #cecece;
}
}
// .lyric-box {
// writing-mode: vertical-rl;
// }
</style>
+39 -26
View File
@@ -5,10 +5,22 @@
</div>
<n-scrollbar :size="100">
<div class="mv-list-content" :class="setAnimationClass('animate__bounceInLeft')">
<div class="mv-item" v-for="(item, index) in mvList" :key="item.id"
:class="setAnimationClass('animate__bounceIn')" :style="setAnimationDelay(index, 30)">
<div
v-for="(item, index) in mvList"
:key="item.id"
class="mv-item"
:class="setAnimationClass('animate__bounceIn')"
:style="setAnimationDelay(index, 30)"
>
<div class="mv-item-img" @click="handleShowMv(item)">
<n-image class="mv-item-img-img" :src="getImgUrl((item.cover), '200y112')" lazy preview-disabled width="200" height="112" />
<n-image
class="mv-item-img-img"
:src="getImgUrl(item.cover, '200y112')"
lazy
preview-disabled
width="200"
height="112"
/>
<div class="top">
<div class="play-count">{{ formatNumber(item.playCount) }}</div>
<i class="iconfont icon-videofill"></i>
@@ -35,39 +47,39 @@
<script setup lang="ts">
import { onMounted } from 'vue';
import { getTopMv, getMvUrl } from '@/api/mv';
import { IMvItem } from '@/type/mv';
import { setAnimationClass, setAnimationDelay, getImgUrl, formatNumber } from "@/utils";
import { useStore } from 'vuex';
const showMv = ref(false)
const mvList = ref<Array<IMvItem>>([])
const playMvItem = ref<IMvItem>()
const playMvUrl = ref<string>()
const store = useStore()
import { getMvUrl, getTopMv } from '@/api/mv';
import { IMvItem } from '@/type/mv';
import { formatNumber, getImgUrl, setAnimationClass, setAnimationDelay } from '@/utils';
const showMv = ref(false);
const mvList = ref<Array<IMvItem>>([]);
const playMvItem = ref<IMvItem>();
const playMvUrl = ref<string>();
const store = useStore();
onMounted(async () => {
const res = await getTopMv(30)
mvList.value = res.data.data
console.log('mvList.value', mvList.value)
})
const res = await getTopMv(30);
mvList.value = res.data.data;
console.log('mvList.value', mvList.value);
});
const handleShowMv = async (item: IMvItem) => {
store.commit('setIsPlay', false)
store.commit('setPlayMusic', false)
showMv.value = true
const res = await getMvUrl(item.id)
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
}
playMvUrl.value = res.data.data.url;
};
const close = () => {
showMv.value = false
showMv.value = false;
if (store.state.playMusicUrl) {
store.commit('setIsPlay', true)
store.commit('setIsPlay', true);
}
}
};
</script>
<style scoped lang="scss">
@@ -157,4 +169,5 @@ const close = () => {
.mv-detail-title:hover {
@apply top-0;
}
}</style>
}
</style>
+123 -135
View File
@@ -1,74 +1,63 @@
<template>
<div class="search-page">
<n-layout
class="hot-search"
:class="setAnimationClass('animate__fadeInDown')"
:native-scrollbar="false"
>
<div class="title">热搜列表</div>
<div class="hot-search-list">
<template v-for="(item, index) in hotSearchData?.data">
<div
:class="setAnimationClass('animate__bounceInLeft')"
:style="setAnimationDelay(index, 10)"
class="hot-search-item"
@click.stop="clickHotKeyword(item.searchWord)"
>
<span
class="hot-search-item-count"
:class="{ 'hot-search-item-count-3': index < 3 }"
>{{ index + 1 }}</span>
{{ item.searchWord }}
</div>
<div class="search-page">
<n-layout class="hot-search" :class="setAnimationClass('animate__fadeInDown')" :native-scrollbar="false">
<div class="title">热搜列表</div>
<div class="hot-search-list">
<template v-for="(item, index) in hotSearchData?.data" :key="index">
<div
:class="setAnimationClass('animate__bounceInLeft')"
:style="setAnimationDelay(index, 10)"
class="hot-search-item"
@click.stop="clickHotKeyword(item.searchWord)"
>
<span class="hot-search-item-count" :class="{ 'hot-search-item-count-3': index < 3 }">{{ index + 1 }}</span>
{{ item.searchWord }}
</div>
</template>
</div>
</n-layout>
<!-- 搜索到的歌曲列表 -->
<n-layout class="search-list" :class="setAnimationClass('animate__fadeInUp')" :native-scrollbar="false">
<div class="title">{{ hotKeyword }}</div>
<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>
</n-layout>
<!-- 搜索到的歌曲列表 -->
<n-layout
class="search-list"
:class="setAnimationClass('animate__fadeInUp')"
:native-scrollbar="false"
>
<div class="title">{{ hotKeyword }}</div>
<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)"
>
<SongItem :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>
</template>
</div>
</n-layout>
</div>
</template>
</template>
</div>
</n-layout>
</div>
</template>
<script lang="ts" setup>
import { getSearch } from "@/api/search";
import type { IHotSearch } from "@/type/search";
import { getHotSearch } from "@/api/home";
import { useRoute, useRouter } from "vue-router";
import { setAnimationClass, setAnimationDelay } from "@/utils";
import { onMounted, ref, watch } from "vue";
import SongItem from "@/components/common/SongItem.vue";
import { useStore } from "vuex";
import { useDateFormat } from '@vueuse/core'
import { useDateFormat } from '@vueuse/core';
import { onMounted, ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useStore } from 'vuex';
import { getHotSearch } from '@/api/home';
import { getSearch } from '@/api/search';
import SongItem from '@/components/common/SongItem.vue';
import type { IHotSearch } from '@/type/search';
import { setAnimationClass, setAnimationDelay } from '@/utils';
const route = useRoute();
const router = useRouter();
@@ -78,105 +67,104 @@ const searchType = ref(Number(route.query.type) || 1);
// 热搜列表
const hotSearchData = ref<IHotSearch>();
const loadHotSearch = async () => {
const { data } = await getHotSearch();
hotSearchData.value = data;
const { data } = await getHotSearch();
hotSearchData.value = data;
};
onMounted(() => {
loadHotSearch();
loadHotSearch();
});
const hotKeyword = ref(route.query.keyword || "搜索列表");
const hotKeyword = ref(route.query.keyword || '搜索列表');
const clickHotKeyword = (keyword: string) => {
hotKeyword.value = keyword;
router.push({
path: "/search",
query: {
keyword: keyword,
type: 1
},
});
// isHotSearchList.value = false;
hotKeyword.value = keyword;
router.push({
path: '/search',
query: {
keyword,
type: 1,
},
});
// isHotSearchList.value = false;
};
const dateFormat = (time:any) => useDateFormat(time, 'YYYY.MM.DD').value
const dateFormat = (time: any) => useDateFormat(time, 'YYYY.MM.DD').value;
const loadSearch = async (keywords: any) => {
hotKeyword.value = keywords;
searchDetail.value = undefined;
if (!keywords) return;
const { data } = await getSearch({keywords, type:searchType.value});
hotKeyword.value = keywords;
searchDetail.value = undefined;
if (!keywords) return;
const { data } = await getSearch({ keywords, type: searchType.value });
const songs = data.result.songs || [];
const albums = data.result.albums || [];
const songs = data.result.songs || [];
const albums = data.result.albums || [];
// songs map 替换属性
songs.map((item: any) => {
item.picUrl = item.al.picUrl;
item.song = item;
item.artists = item.ar;
});
albums.map((item: any) => {
item.desc = `${item.artist.name } ${ item.company } ${dateFormat(item.publishTime)}`;
});
searchDetail.value = {
songs,
albums
}
// songs map 替换属性
songs.forEach((item: any) => {
item.picUrl = item.al.picUrl;
item.song = item;
item.artists = item.ar;
});
albums.forEach((item: any) => {
item.desc = `${item.artist.name} ${item.company} ${dateFormat(item.publishTime)}`;
});
searchDetail.value = {
songs,
albums,
};
};
loadSearch(route.query.keyword);
watch(
() => route.query,
async newParams => {
searchType.value = Number(newParams.type || 1)
loadSearch(newParams.keyword);
}
)
() => route.query,
async (newParams) => {
searchType.value = Number(newParams.type || 1);
loadSearch(newParams.keyword);
},
);
const store = useStore()
const store = useStore();
const handlePlay = (item: any) => {
const tracks = searchDetail.value?.songs || []
store.commit('setPlayList', tracks)
}
const handlePlay = () => {
const tracks = searchDetail.value?.songs || [];
store.commit('setPlayList', tracks);
};
</script>
<style lang="scss" scoped>
.search-page {
@apply flex h-full;
@apply flex h-full;
}
.hot-search {
@apply mr-4 rounded-xl flex-1 overflow-hidden;
background-color: #0d0d0d;
animation-duration: 0.2s;
min-width: 400px;
height: 100%;
&-list{
@apply pb-28;
}
&-item {
@apply px-4 py-3 text-lg hover:bg-gray-700 rounded-xl cursor-pointer;
&-count {
@apply text-green-500 inline-block ml-3 w-8;
&-3 {
@apply text-red-600 font-bold inline-block ml-3 w-8;
}
}
@apply mr-4 rounded-xl flex-1 overflow-hidden;
background-color: #0d0d0d;
animation-duration: 0.2s;
min-width: 400px;
height: 100%;
&-list {
@apply pb-28;
}
&-item {
@apply px-4 py-3 text-lg hover:bg-gray-700 rounded-xl cursor-pointer;
&-count {
@apply text-green-500 inline-block ml-3 w-8;
&-3 {
@apply text-red-600 font-bold inline-block ml-3 w-8;
}
}
}
}
.search-list {
@apply flex-1 rounded-xl;
background-color: #0d0d0d;
height: 100%;
&-box{
@apply pb-28;
}
@apply flex-1 rounded-xl;
background-color: #0d0d0d;
height: 100%;
&-box {
@apply pb-28;
}
}
.title {
@apply text-gray-200 text-xl font-bold my-2 mx-4;
@apply text-gray-200 text-xl font-bold my-2 mx-4;
}
</style>
+16 -15
View File
@@ -5,7 +5,7 @@
<div class="set-item-title">代理</div>
<div class="set-item-content">无法听音乐时打开</div>
</div>
<n-switch v-model:value="setData.isProxy"/>
<n-switch v-model:value="setData.isProxy" />
</div>
<div class="set-item">
<div>
@@ -30,35 +30,36 @@
</template>
<script setup lang="ts">
import store from '@/store'
import { useRouter } from 'vue-router';
const setData = ref(store.state.setData)
const router = useRouter()
import store from '@/store';
const setData = ref(store.state.setData);
const router = useRouter();
const handelCancel = () => {
router.back()
}
router.back();
};
const windowData = window as any
const windowData = window as any;
const handleSave = () => {
store.commit('setSetData', setData.value)
windowData.electronAPI.restart()
}
store.commit('setSetData', setData.value);
windowData.electronAPI.restart();
};
</script>
<style scoped lang="scss">
.set-page{
.set-page {
@apply flex flex-col justify-center items-center pt-8;
}
.set-item{
.set-item {
@apply w-3/5 flex justify-between items-center mb-4;
.set-item-title{
.set-item-title {
@apply text-gray-200 text-base;
}
.set-item-content{
.set-item-content {
@apply text-gray-400 text-sm;
}
}
</style>
</style>
+57 -70
View File
@@ -1,89 +1,87 @@
<script lang="ts" setup>
import { useStore } from "vuex";
import { useRouter } from "vue-router";
import { getUserDetail, getUserPlaylist, getUserRecord } from "@/api/user";
import type { IUserDetail } from "@/type/user";
import { computed, ref } from "vue";
import { setAnimationClass, setAnimationDelay, getImgUrl } from "@/utils";
import { getListDetail } from '@/api/list'
import SongItem from "@/components/common/SongItem.vue";
import MusicList from "@/components/MusicList.vue";
import { computed, ref } from 'vue';
import { useRouter } from 'vue-router';
import { useStore } from 'vuex';
import { getListDetail } from '@/api/list';
import { getUserDetail, getUserPlaylist, getUserRecord } from '@/api/user';
import PlayBottom from '@/components/common/PlayBottom.vue';
import SongItem from '@/components/common/SongItem.vue';
import MusicList from '@/components/MusicList.vue';
import type { Playlist } from '@/type/listDetail';
import PlayBottom from "@/components/common/PlayBottom.vue";
import type { IUserDetail } from '@/type/user';
import { getImgUrl, setAnimationClass, setAnimationDelay } from '@/utils';
const store = useStore()
const router = useRouter()
const userDetail = ref<IUserDetail>()
const playList = ref<any[]>([])
const recordList = ref()
let user = store.state.user
const store = useStore();
const router = useRouter();
const userDetail = ref<IUserDetail>();
const playList = ref<any[]>([]);
const recordList = ref();
let { user } = store.state;
const loadPage = async () => {
if (!user) {
router.push("/login")
return
router.push('/login');
return;
}
const { data: userData } = await getUserDetail(user.userId)
userDetail.value = userData
const { data: userData } = await getUserDetail(user.userId);
userDetail.value = userData;
const { data: playlistData } = await getUserPlaylist(user.userId)
playList.value = playlistData.playlist
const { data: playlistData } = await getUserPlaylist(user.userId);
playList.value = playlistData.playlist;
const { data: recordData } = await getUserRecord(user.userId)
recordList.value = recordData.allData
}
const { data: recordData } = await getUserRecord(user.userId);
recordList.value = recordData.allData;
};
watchEffect(() => {
const localUser = localStorage.getItem('user')
store.state.user = localUser ? JSON.parse(localUser) : null
user = store.state.user
loadPage()
})
const localUser = localStorage.getItem('user');
store.state.user = localUser ? JSON.parse(localUser) : null;
user = store.state.user;
loadPage();
});
const isShowList = ref(false)
const list = ref<Playlist>()
const isShowList = ref(false);
const list = ref<Playlist>();
// 展示歌单
const showPlaylist = async (id: number) => {
const { data } = await getListDetail(id)
isShowList.value = true
list.value = data.playlist
}
const { data } = await getListDetail(id);
isShowList.value = true;
list.value = data.playlist;
};
// 格式化歌曲列表项
const formatDetail = computed(() => (detail: any) => {
let song = {
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 = (item: any) => {
const tracks = recordList.value || []
store.commit('setPlayList', tracks)
}
detail.song = song;
detail.picUrl = detail.al.picUrl;
return detail;
});
const handlePlay = () => {
const tracks = recordList.value || [];
store.commit('setPlayList', tracks);
};
</script>
<template>
<div class="user-page" @click.stop="isShowList = false">
<div
class="left"
v-if="userDetail"
class="left"
:class="setAnimationClass('animate__fadeInLeft')"
:style="{ backgroundImage: `url(${getImgUrl(user.backgroundUrl)})` }"
>
<div class="page">
<div class="user-name">{{ user.nickname }}</div>
<div class="user-info">
<n-avatar round :size="50" :src="getImgUrl(user.avatarUrl,'50y50')" />
<n-avatar round :size="50" :src="getImgUrl(user.avatarUrl, '50y50')" />
<div class="user-info-list">
<div class="user-info-item">
<div class="label">{{ userDetail.profile.followeds }}</div>
@@ -104,24 +102,14 @@ const handlePlay = (item: any) => {
<div class="play-list" :class="setAnimationClass('animate__fadeInLeft')">
<div class="play-list-title">创建的歌单</div>
<n-scrollbar>
<div
class="play-list-item"
v-for="(item,index) in playList"
:key="index"
@click="showPlaylist(item.id)"
>
<n-image
:src="getImgUrl( item.coverImgUrl, '50y50')"
class="play-list-item-img"
lazy
preview-disabled
/>
<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 />
<div class="play-list-item-info">
<div class="play-list-item-name">{{ item.name }}</div>
<div class="play-list-item-count">{{ item.trackCount }}播放{{ item.playCount }}</div>
</div>
</div>
<PlayBottom/>
<play-bottom />
</n-scrollbar>
</div>
</div>
@@ -131,20 +119,20 @@ const handlePlay = (item: any) => {
<div class="record-list">
<n-scrollbar>
<div
class="record-item"
v-for="(item, index) in recordList"
:key="item.song.id"
class="record-item"
:class="setAnimationClass('animate__bounceInUp')"
:style="setAnimationDelay(index, 50)"
>
<SongItem class="song-item" :item="formatDetail(item.song)" @play="handlePlay"/>
<song-item class="song-item" :item="formatDetail(item.song)" @play="handlePlay" />
<div class="play-count">{{ item.playCount }}</div>
</div>
<PlayBottom/>
<play-bottom />
</n-scrollbar>
</div>
</div>
<MusicList v-if="list" v-model:show="isShowList" :name="list.name" :song-list="list.tracks" />
<music-list v-if="list" v-model:show="isShowList" :name="list.name" :song-list="list.tracks" />
</div>
</template>
@@ -222,5 +210,4 @@ const handlePlay = (item: any) => {
}
}
}
</style>
</style>