Compare commits

...

17 Commits

Author SHA1 Message Date
底层用户
ddd12364fe Merge pull request #15 from imsyy/dev
fix: 修复逐字歌词导致的一系列问题
2023-04-17 14:28:27 +08:00
imsyy
7592296124 fix: 修复逐字歌词情况下歌词滚动异常 2023-04-17 14:26:34 +08:00
imsyy
0922735b2d fix: 修复逐字歌词导致的一系列问题 2023-04-17 11:57:15 +08:00
底层用户
144955e7c8 feat: 支持逐字歌词显示
feat: 支持逐字歌词显示
2023-04-14 17:26:45 +08:00
imsyy
7495e7af2d feat: 支持逐字歌词显示 2023-04-14 17:26:12 +08:00
底层用户
43fe04b4fc feat: 新增歌曲前奏等待提醒
feat: 新增歌曲前奏等待提醒
2023-04-14 14:58:55 +08:00
imsyy
db5aebbf89 feat: 新增歌曲前奏等待提醒 2023-04-14 14:56:05 +08:00
底层用户
535d0f7493 Merge pull request #12 from imsyy/dev
feat: 更换歌词解析方式
2023-04-13 16:51:32 +08:00
imsyy
7415f591b3 feat: 更换歌词解析方式 2023-04-13 16:50:12 +08:00
imsyy
e138d06e6f feat: 歌词滚动组件抽离 2023-04-13 14:06:08 +08:00
底层用户
cd05376e18 Merge pull request #11 from imsyy/dev
fix: 歌单无法展示
2023-04-12 16:29:23 +08:00
imsyy
45b374a0cb fix: 歌单无法展示 2023-04-12 16:28:56 +08:00
底层用户
418da81738 Merge pull request #10 from imsyy/dev
feat: 站点标题跟随页面内容
2023-04-12 14:57:14 +08:00
imsyy
dd66725d9c feat: 站点标题跟随页面内容 2023-04-12 14:40:01 +08:00
imsyy
55df8f05dc feat: 新增收藏专辑页面 & 部分变量调整 2023-04-11 15:21:18 +08:00
imsyy
18359d69d2 feat: 新增收藏/取消收藏专辑 & 歌手页优化 2023-04-10 18:29:40 +08:00
imsyy
d291e86998 feat: 免责声明 2023-04-10 14:09:29 +08:00
50 changed files with 1388 additions and 481 deletions

View File

@@ -44,7 +44,7 @@
- 明暗模式自动 / 手动切换
- 移动端基础适配
- [ ] 主题换肤
- [ ] 逐字歌词支持
- [ ] 发表评论
## 😍 Screenshots
@@ -131,4 +131,16 @@ npm build
- [NeteaseCloudMusicApi](https://github.com/Binaryify/NeteaseCloudMusicApi)
- [YesPlayMusic](https://github.com/qier222/YesPlayMusic)
- [Vue-mmPlayer](https://github.com/maomao1996/Vue-mmPlayer)
- [Vue-mmPlayer](https://github.com/maomao1996/Vue-mmPlayer)
## 📜 开源许可
- **本项目仅供个人学习研究使用,禁止用于商业及非法用途**
- 本项目基于 [MIT license](https://opensource.org/license/mit/) 许可进行开源
## 📢 免责声明
本项目使用了网易云音乐的第三方 API 服务,**仅供个人学习研究使用,禁止用于商业及非法用途。** 本项目旨在提供一个前端练手的实战项目,用于帮助开发者提升技能水平和对前端技术的理解
同时,本项目开发者承诺 **严格遵守相关法律法规和网易云音乐 API 使用协议,不会利用本项目进行任何违法活动。** 如因使用本项目而引起的任何纠纷或责任,均由使用者自行承担。**本项目开发者不承担任何因使用本项目而导致的任何直接或间接责任,并保留追究使用者违法行为的权利**
请使用者在使用本项目时遵守相关法律法规,**不要将本项目用于任何商业及非法用途。如有违反,一切后果由使用者自负。** 同时,使用者应该自行承担因使用本项目而带来的风险和责任。本项目开发者不对本项目所提供的服务和内容做出任何保证

View File

@@ -1,6 +1,6 @@
{
"name": "splayer",
"version": "1.0.3",
"version": "1.1.0",
"private": true,
"author": "imsyy",
"home": "https://imsyy.top",

View File

@@ -58,7 +58,7 @@ const annDuration = Number(import.meta.env.VITE_ANN_DURATION);
const spacePlayOrPause = (e) => {
if (e.code === "Space") {
console.log(e.target.tagName);
if (router.currentRoute.value.name == "video") return false;
if (router.currentRoute.value.name === "video") return false;
if (e.target.tagName === "BODY") {
e.preventDefault();
music.setPlayState(!music.getPlayState);
@@ -68,6 +68,20 @@ const spacePlayOrPause = (e) => {
}
};
// 更改页面标题
const setSiteTitle = (val) => {
const title = val
? val === "SPlayer"
? val
: val + " - SPlayer"
: user.siteTitle;
user.setSiteTitle(title);
sessionStorage.setItem("siteTitle", title);
if (!music.getPlayState) {
window.document.title = title;
}
};
// 刷新登录
const toRefreshLogin = () => {
const today = Date.now();
@@ -129,6 +143,7 @@ onMounted(() => {
window.$mainContent = mainContent.value;
window.$cleanAll = cleanAll;
window.$signIn = signIn;
window.$setSiteTitle = setSiteTitle;
// 公告
if (annShow) {

View File

@@ -48,3 +48,20 @@ export const getToplist = (detail = true) => {
url: `/toplist${detail ? "/detail" : null}`,
});
};
/**
* 收藏/取消收藏专辑
* @param {number} t - 操作类型1为收藏2为取消收藏
* @param {number} id - 专辑id
*/
export const likeAlbum = (t, id) => {
return axios({
method: "GET",
url: "/album/sub",
params: {
t,
id,
timestamp: new Date().getTime(),
},
});
};

View File

@@ -59,6 +59,7 @@ export const getPlayListDetail = (id) => {
return axios({
method: "GET",
url: "/playlist/detail",
withCredentials: false,
params: {
id,
timestamp: new Date().getTime(),

View File

@@ -53,6 +53,21 @@ export const getMusicLyric = (id) => {
});
};
/**
* 获取指定音乐的逐字歌词
* @param {number} id - 要获取逐字歌词的音乐ID
*/
export const getMusicNewLyric = (id) => {
return axios({
method: "GET",
hiddenBar: true,
url: "/lyric/new",
params: {
id,
},
});
};
/**
* 获取指定音乐的详情。
* @param {string} ids - 要获取详情的音乐ID多个ID用逗号分隔

View File

@@ -49,6 +49,23 @@ export const getUserPlaylist = (uid, limit = 30, offset = 0) => {
});
};
/**
* 获取用户的专辑列表
* @param {number} [limit=30] - 返回数量默认30
* @param {number} [offset=0] - 偏移数量默认0
*/
export const getUserAlbum = (limit = 30, offset = 0) => {
return axios({
method: "GET",
url: "/album/sublist",
params: {
limit,
offset,
timestamp: new Date().getTime(),
},
});
};
/**
* 获取用户收藏的歌手列表
*/

View File

@@ -1,67 +1,70 @@
<template>
<n-card class="comment" hoverable>
<div class="user">
<div class="avatar">
<img
class="avatarImg"
:src="
commentData.user.avatarUrl.replace(/^http:/, 'https:') +
'?param=50y50'
"
alt="avatar"
/>
<img
class="musicPackage"
v-if="commentData.user.vipRights?.redVipAnnualCount > 0"
:src="commentData.user.vipRights.musicPackage.iconUrl"
alt="redVipAnnualCount"
title="网易音乐人"
/>
</div>
<div
class="associator"
v-if="commentData.user.vipRights?.redVipLevel > 0"
>
<img
v-if="commentData.user.vipRights.associator"
:src="commentData.user.vipRights.associator.iconUrl"
alt="associator"
title="黑胶会员"
/>
</div>
</div>
<div class="review">
<div class="content">
<n-text class="name">{{ commentData.user.nickname }}</n-text>
<n-text class="text" v-html="commentData.content" />
</div>
<div class="beReplied" v-if="commentData.beReplied[0]">
<n-text class="name">
@{{ commentData.beReplied[0].user.nickname }}
</n-text>
<n-text class="text">{{ commentData.beReplied[0].content }}</n-text>
</div>
<div class="thing">
<div class="item">
<n-icon size="14" :depth="3" :component="Time" />
<n-text :depth="3" v-html="getCommentTime(commentData.time)" />
</div>
<div class="item" v-if="commentData.ipLocation?.location">
<n-icon size="14" :depth="3" :component="Local" />
<n-text :depth="3" v-html="commentData.ipLocation.location" />
<Transition mode="out-in">
<n-card v-if="Object.keys(commentData).length" class="comment" hoverable>
<div class="user">
<div class="avatar">
<img
class="avatarImg"
:src="
commentData.user.avatarUrl.replace(/^http:/, 'https:') +
'?param=50y50'
"
alt="avatar"
/>
<img
class="musicPackage"
v-if="commentData.user.vipRights?.redVipAnnualCount > 0"
:src="commentData.user.vipRights.musicPackage.iconUrl"
alt="redVipAnnualCount"
title="网易音乐人"
/>
</div>
<div
:class="commentData.liked ? 'like liked' : 'like'"
@click="toLikeComment"
class="associator"
v-if="commentData.user.vipRights?.redVipLevel > 0"
>
<n-icon>
<ThumbsUp :theme="commentData.liked ? 'filled' : 'outline'" />
</n-icon>
{{ formatNumber(commentData.likedCount) }}
<img
v-if="commentData.user.vipRights.associator"
:src="commentData.user.vipRights.associator.iconUrl"
alt="associator"
title="黑胶会员"
/>
</div>
</div>
</div>
</n-card>
<div class="review">
<div class="content">
<n-text class="name">{{ commentData.user.nickname }}</n-text>
<n-text class="text" v-html="commentData.content" />
</div>
<div class="beReplied" v-if="commentData.beReplied[0]">
<n-text class="name">
@{{ commentData.beReplied[0].user.nickname }}
</n-text>
<n-text class="text">{{ commentData.beReplied[0].content }}</n-text>
</div>
<div class="thing">
<div class="item">
<n-icon size="14" :depth="3" :component="Time" />
<n-text :depth="3" v-html="getCommentTime(commentData.time)" />
</div>
<div class="item" v-if="commentData.ipLocation?.location">
<n-icon size="14" :depth="3" :component="Local" />
<n-text :depth="3" v-html="commentData.ipLocation.location" />
</div>
<div
:class="commentData.liked ? 'like liked' : 'like'"
@click="toLikeComment"
>
<n-icon>
<ThumbsUp :theme="commentData.liked ? 'filled' : 'outline'" />
</n-icon>
{{ formatNumber(commentData.likedCount) }}
</div>
</div>
</div>
</n-card>
<n-skeleton v-else class="skeleton" />
</Transition>
</template>
<script setup>
@@ -77,7 +80,7 @@ const props = defineProps({
// 评论 数据
commentData: {
type: Object,
default: [],
default: {},
},
});
@@ -105,6 +108,15 @@ const toLikeComment = () => {
</script>
<style lang="scss" scoped>
.v-enter-active,
.v-leave-active {
transition: opacity 0.3s ease;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
}
.comment {
margin-bottom: 12px;
border-radius: 8px;
@@ -235,4 +247,10 @@ const toLikeComment = () => {
}
}
}
.skeleton {
width: 100%;
height: 120px;
border-radius: 8px;
margin-bottom: 12px;
}
</style>

View File

@@ -110,7 +110,7 @@ const openRightMenu = (e, data) => {
{
key: "like",
label: isLikeOrDislike(data.id) ? "收藏歌手" : "取消收藏歌手",
show: user.userLogin && user.getUserArtistlists.has ? true : false,
show: user.userLogin && user.getUserArtistLists.has ? true : false,
props: {
onClick: () => {
toLikeArtist(data);
@@ -166,21 +166,19 @@ const toLikeArtist = (data) => {
// 判断收藏还是取消
const isLikeOrDislike = (id) => {
if (user.getUserArtistlists.list[0]) {
const index = user.getUserArtistlists.list.findIndex(
(item) => item.id === id
);
if (index !== -1) {
return false;
}
return true;
} else {
if (!user.getUserArtistLists.list[0]) {
return true;
}
return !user.getUserArtistLists.list.some((item) => item.id === id);
};
onMounted(() => {
if (user.userLogin && !user.getUserArtistlists.has) user.setUserArtistLists();
if (
user.userLogin &&
!user.getUserArtistLists.has &&
!user.getUserArtistLists.isLoading
)
user.setUserArtistLists();
});
</script>

View File

@@ -27,7 +27,7 @@
<PlayOne theme="filled" />
</n-icon>
<div class="description" v-if="listType != 'topList'">
<div class="num" v-if="listType == 'playList'">
<div class="num" v-if="listType == 'playlist'">
<n-icon>
<Headset theme="filled" />
</n-icon>
@@ -40,7 +40,7 @@
</div>
<div class="title">
<span class="name text-hidden">{{ item.name }}</span>
<span v-if="listType == 'playList' && item.artist" class="by">
<span v-if="listType == 'playlist' && item.artist" class="by">
By {{ item.artist.nickname }}
</span>
<span v-else-if="listType == 'topList' && item.update" class="by">
@@ -88,6 +88,7 @@
<script setup>
import { PlayOne, Headset } from "@icon-park/vue-next";
import { delPlayList, likePlaylist } from "@/api/playlist";
import { likeAlbum } from "@/api/album";
import { musicStore, userStore } from "@/store";
import { useRouter } from "vue-router";
import AllArtists from "./AllArtists.vue";
@@ -105,7 +106,7 @@ const props = defineProps({
// 列表类型
listType: {
type: String,
default: "playList",
default: "playlist",
},
// 自定义列数
columns: {
@@ -145,7 +146,8 @@ const openRightMenu = (e, data) => {
{
key: "update",
label: "编辑歌单",
show: router.currentRoute.value.name === "playlists" ? true : false,
show:
router.currentRoute.value.name === "user-playlists" ? true : false,
props: {
onClick: () => {
playlistUpdateRef.value.openUpdateModel(data);
@@ -155,7 +157,8 @@ const openRightMenu = (e, data) => {
{
key: "del",
label: "删除歌单",
show: router.currentRoute.value.name === "playlists" ? true : false,
show:
router.currentRoute.value.name === "user-playlists" ? true : false,
props: {
onClick: () => {
toDelPlayList(data);
@@ -163,31 +166,53 @@ const openRightMenu = (e, data) => {
},
},
{
key: "like",
key: "likePlaylist",
label: isLikeOrDislike(data.id) ? "收藏歌单" : "取消收藏歌单",
show:
user.userLogin &&
user.getUserPlayLists.has &&
router.currentRoute.value.name != "playlists"
props.listType === "playlist" &&
router.currentRoute.value.name !== "user-playlists"
? true
: false,
props: {
onClick: () => {
toLikePlaylist(data.id);
toChangeLike(data.id);
},
},
},
{
key: "likeAlbum",
label: isLikeOrDislike(data.id) ? "收藏专辑" : "取消收藏专辑",
show:
user.userLogin &&
user.getUserAlbumLists.has &&
props.listType === "album"
? true
: false,
props: {
onClick: () => {
toChangeLike(data.id);
},
},
},
{
key: "copy",
label: "复制歌单链接",
label: `复制${props.listType === "playlist" ? "歌单" : "专辑"}链接`,
props: {
onClick: () => {
if (navigator.clipboard) {
try {
navigator.clipboard.writeText(
`https://music.163.com/#/playlist?id=${data.id}`
`https://music.163.com/#/${
props.listType === "playlist" ? "playlist" : "album"
}?id=${data.id}`
);
$message.success(
`${
props.listType === "playlist" ? "歌单" : "专辑"
}链接复制成功`
);
$message.success("歌单链接复制成功");
} catch (err) {
$message.error("复制失败:", err);
}
@@ -211,7 +236,7 @@ const onClickoutside = () => {
// 链接跳转
const toLink = (id) => {
if (props.listType == "playList" || props.listType == "topList") {
if (props.listType === "playlist" || props.listType === "topList") {
router.push({
path: "/playlist",
query: {
@@ -219,7 +244,7 @@ const toLink = (id) => {
page: 1,
},
});
} else if (props.listType == "album") {
} else if (props.listType === "album") {
router.push({
path: "/album",
query: {
@@ -250,36 +275,65 @@ const toDelPlayList = (data) => {
// 判断收藏还是取消
const isLikeOrDislike = (id) => {
if (user.getUserPlayLists.like[0]) {
const index = user.getUserPlayLists.like.findIndex(
(item) => item.id === id
);
if (index !== -1) {
return false;
const listType = props.listType;
const playlists = user.getUserPlayLists.like;
const albums = user.getUserAlbumLists.list;
if (listType === "playlist" && playlists.length) {
return !playlists.some((item) => item.id === id);
}
if (listType === "album" && albums.length) {
return !albums.some((item) => item.id === id);
}
return true;
};
// 收藏/取消收藏
const toChangeLike = async (id) => {
const listType = props.listType;
const type = isLikeOrDislike(id) ? 1 : 2;
const likeFn = listType === "playlist" ? likePlaylist : likeAlbum;
const likeMsg = listType === "playlist" ? "歌单" : "专辑";
try {
const res = await likeFn(type, id);
if (res.code === 200) {
$message.success(`${likeMsg}${type == 1 ? "收藏成功" : "取消收藏成功"}`);
listType === "playlist"
? user.setUserPlayLists()
: user.setUserAlbumLists();
} else {
$message.error(`${likeMsg}${type == 1 ? "收藏失败" : "取消收藏失败"}`);
}
return true;
} else {
return true;
} catch (err) {
$message.error(`${likeMsg}${type == 1 ? "收藏失败" : "取消收藏失败"}`);
console.error(
`${likeMsg}${type == 1 ? "收藏失败:" : "取消收藏失败:"}` + err
);
}
};
// 收藏/取消收藏歌单
const toLikePlaylist = (id) => {
const type = isLikeOrDislike(id) ? 1 : 2;
likePlaylist(type, id).then((res) => {
if (res.code === 200) {
$message.success(`歌单${type == 1 ? "收藏成功" : "取消收藏成功"}`);
user.setUserPlayLists();
} else {
$message.error(`歌单${type == 1 ? "收藏失败" : "取消收藏失败"}`);
}
});
};
onMounted(() => {
if (router.currentRoute.value.name === "playlists" && !music.catList.sub)
if (
router.currentRoute.value.name === "user-playlists" &&
!music.catList.sub
) {
music.setCatList();
if (user.userLogin && !user.getUserPlayLists.has) user.setUserPlayLists();
}
if (
user.userLogin &&
!user.getUserPlayLists.has &&
!user.getUserPlayLists.isLoading &&
props.listType === "playlist"
) {
user.setUserPlayLists();
}
if (
user.userLogin &&
!user.getUserAlbumLists.has &&
!user.getUserAlbumLists.isLoading &&
props.listType === "album"
) {
user.setUserAlbumLists();
}
});
</script>

View File

@@ -269,7 +269,7 @@
<n-text>专辑:{{ drawerData.album.name }}</n-text>
</div>
<div
v-if="router.currentRoute.value.name == 'cloud'"
v-if="router.currentRoute.value.name === 'user-cloud'"
class="item"
@click="
() => {
@@ -284,7 +284,7 @@
<n-text>歌曲信息纠正</n-text>
</div>
<div
v-if="router.currentRoute.value.name == 'cloud'"
v-if="router.currentRoute.value.name === 'user-cloud'"
class="item"
@click="
() => {
@@ -327,10 +327,12 @@ import {
DeleteFour,
Like,
More,
Search,
} from "@icon-park/vue-next";
import { musicStore, settingStore, userStore } from "@/store";
import { useRouter } from "vue-router";
import { setCloudDel } from "@/api/user";
import { NIcon } from "naive-ui";
import AllArtists from "./AllArtists.vue";
import AddPlaylist from "@/components/DataModel/AddPlaylist.vue";
import CloudMatch from "@/components/DataModel/CloudMatch.vue";
@@ -384,6 +386,19 @@ const copySongData = (id, url = true) => {
}
};
// 图标渲染
const renderIcon = (icon) => {
return () => {
return h(
NIcon,
{ depth: 2, style: { transform: "translateX(2px)" } },
{
default: () => h(icon, { theme: "filled" }),
}
);
};
};
// 打开右键菜单
const openRightMenu = (e, data) => {
e.preventDefault();
@@ -393,6 +408,7 @@ const openRightMenu = (e, data) => {
{
key: "play",
label: "立即播放",
icon: renderIcon(PlayOne),
props: {
onClick: () => {
playSong(props.listData, data);
@@ -402,10 +418,11 @@ const openRightMenu = (e, data) => {
{
key: "nextPlay",
label: "下一首播放",
disabled:
icon: renderIcon(AddMusic),
show:
music.getPersonalFmMode || music.getPlaySongData.id == data.id
? true
: false,
? false
: true,
props: {
onClick: () => {
music.addSongToNext(data);
@@ -415,6 +432,7 @@ const openRightMenu = (e, data) => {
{
key: "add",
label: "添加到歌单",
icon: renderIcon(ListAdd),
show: user.userLogin ? true : false,
props: {
onClick: () => {
@@ -422,9 +440,20 @@ const openRightMenu = (e, data) => {
},
},
},
{
key: "download",
label: "歌曲下载",
icon: renderIcon(DownloadFour),
props: {
onClick: () => {
downloadSongRef.value.openDownloadModel(data);
},
},
},
{
key: "comment",
label: "前往评论区",
icon: renderIcon(Comments),
props: {
onClick: () => {
router.push(`/comment?id=${data.id}`);
@@ -434,6 +463,7 @@ const openRightMenu = (e, data) => {
{
key: "mv",
label: "观看 MV",
icon: renderIcon(Video),
show: data.mv && data.mv != 0 ? true : false,
props: {
onClick: () => {
@@ -444,12 +474,13 @@ const openRightMenu = (e, data) => {
{
key: "line1",
type: "divider",
show: router.currentRoute.value.name == "cloud" ? true : false,
show: router.currentRoute.value.name === "user-cloud" ? true : false,
},
{
key: "delete",
label: "从云盘中删除",
show: router.currentRoute.value.name == "cloud" ? true : false,
icon: renderIcon(DeleteFour),
show: router.currentRoute.value.name === "user-cloud" ? true : false,
props: {
onClick: () => {
delCloudSong(data);
@@ -459,7 +490,8 @@ const openRightMenu = (e, data) => {
{
key: "match",
label: "歌曲信息纠正",
show: router.currentRoute.value.name == "cloud" ? true : false,
icon: renderIcon(FileMusic),
show: router.currentRoute.value.name === "user-cloud" ? true : false,
props: {
onClick: () => {
cloudMatchRef.value.openCloudMatch(data);
@@ -473,6 +505,7 @@ const openRightMenu = (e, data) => {
{
key: "search",
label: "同名搜索",
icon: renderIcon(Search),
props: {
onClick: () => {
router.push({
@@ -488,6 +521,7 @@ const openRightMenu = (e, data) => {
{
key: "copyId",
label: "复制歌曲 ID",
icon: renderIcon(FileMusic),
props: {
onClick: () => {
copySongData(data.id, false);
@@ -496,22 +530,14 @@ const openRightMenu = (e, data) => {
},
{
key: "copy",
label: "复制链接",
label: "复制歌曲链接",
icon: renderIcon(LinkTwo),
props: {
onClick: () => {
copySongData(data.id);
},
},
},
{
key: "line2",
type: "divider",
},
{
label: data.name,
key: "name",
disabled: true,
},
];
rightMenuShow.value = true;
rightMenuX.value = e.clientX;
@@ -560,7 +586,7 @@ const playSong = (data, song) => {
music.setPersonalFmMode(false);
if (router.currentRoute.value.name !== "history") music.setPlaylists(data);
// 检查是否为云盘歌曲
if (router.currentRoute.value.name === "cloud") {
if (router.currentRoute.value.name === "user-cloud") {
music.setPlayListMode("cloud");
} else {
music.setPlayListMode("list");

View File

@@ -64,7 +64,9 @@ const openAddToPlaylist = (id) => {
$message.error("请登录账号后使用");
return false;
}
if (!user.getUserPlayLists.has) user.setUserPlayLists();
if (!user.getUserPlayLists.has && !user.getUserPlayLists.isLoading) {
user.setUserPlayLists();
}
addToPlaylistModel.value = true;
addToPlaylistId.value = id;
console.log("开启", addToPlaylistModel.value, addToPlaylistId.value);

View File

@@ -203,7 +203,7 @@ const getSongSize = (data, type) => {
const openDownloadModel = (data) => {
if (user.userLogin) {
if (
router.currentRoute.value.name === "cloud" ||
router.currentRoute.value.name === "user-cloud" ||
user.userData?.vipType ||
data?.fee === 0 ||
data?.pc

View File

@@ -13,7 +13,6 @@
<router-link class="link" to="/">首页</router-link>
<n-dropdown
trigger="hover"
size="large"
:options="discoverOptions"
@select="menuSelect"
>
@@ -21,7 +20,6 @@
</n-dropdown>
<n-dropdown
trigger="hover"
size="large"
:options="userOptions"
@select="menuSelect"
>
@@ -139,11 +137,13 @@ const userDataRender = () => {
{
height: 4,
type: "line",
percentage: user.getUserOtherData.level.progress * 100,
percentage:
user.getUserOtherData.level.progress * 100,
color: "#f55e55",
},
{
default: () => "Lv." + user.getUserOtherData.level.level,
default: () =>
"Lv." + user.getUserOtherData.level.level,
}
)
: "等级信息获取失败"
@@ -182,6 +182,10 @@ const userOptions = ref(
label: "收藏的歌单",
key: "/user/like",
},
{
label: "收藏的专辑",
key: "/user/album",
},
{
label: "收藏的歌手",
key: "/user/artists",
@@ -191,7 +195,12 @@ const userOptions = ref(
key: "/user/cloud",
},
]
: []
: [
{
label: "登录账号",
key: "/login",
},
]
);
const dropdownOptions = ref([
{
@@ -205,56 +214,85 @@ const dropdownOptions = ref([
},
{
label: () => {
return h(NText, null, {
default: () =>
setting.getSiteTheme == "light" ? "深色模式" : "浅色模式",
});
return h(
NText,
{ style: { transform: "translateX(2px)" } },
{
default: () =>
setting.getSiteTheme == "light" ? "深色模式" : "浅色模式",
}
);
},
key: "changeTheme",
icon: () => {
return h(NIcon, null, {
default: () => (setting.getSiteTheme == "light" ? h(Moon) : h(SunOne)),
});
return h(
NIcon,
{ style: { transform: "translateX(2px)" } },
{
default: () =>
setting.getSiteTheme == "light" ? h(Moon) : h(SunOne),
}
);
},
},
{
label: "播放历史",
key: "history",
icon: () => {
return h(NIcon, null, {
default: () => h(History),
});
return h(
NIcon,
{ style: { transform: "translateX(2px)" } },
{
default: () => h(History),
}
);
},
},
{
label: "全局设置",
key: "setting",
icon: () => {
return h(NIcon, null, {
default: () => h(SettingTwo),
});
return h(
NIcon,
{ style: { transform: "translateX(2px)" } },
{
default: () => h(SettingTwo),
}
);
},
},
{
label: () => {
return h(NText, null, {
default: () => (user.userLogin ? "退出登录" : "登录账号"),
});
return h(
NText,
{ style: { transform: "translateX(2px)" } },
{
default: () => (user.userLogin ? "退出登录" : "登录账号"),
}
);
},
key: "user",
icon: () => {
return h(NIcon, null, {
default: () => (user.userLogin ? h(Logout) : h(Login)),
});
return h(
NIcon,
{ style: { transform: "translateX(2px)" } },
{
default: () => (user.userLogin ? h(Logout) : h(Login)),
}
);
},
},
{
label: "关于本站",
key: "about",
icon: () => {
return h(NIcon, null, {
default: () => h(Info),
});
return h(
NIcon,
{ style: { transform: "translateX(2px)" } },
{
default: () => h(Info),
}
);
},
},
]);

View File

@@ -26,7 +26,7 @@
/>
<div
:class="
music.getPlaySongLyric[0] && music.getPlaySongLyric.length > 4
music.getPlaySongLyric.lrc[0] && music.getPlaySongLyric.lrc.length > 4
? 'all'
: 'all noLrc'
"
@@ -50,7 +50,8 @@
<div
class="lrcShow"
v-if="
music.getPlaySongLyric[0] && music.getPlaySongLyric.length > 4
music.getPlaySongLyric.lrc[0] &&
music.getPlaySongLyric.lrc.length > 4
"
>
<div class="data" v-show="setting.playerStyle === 'record'">
@@ -82,72 +83,13 @@
</span>
</div>
</div>
<div
:class="
setting.playerStyle === 'cover'
? 'lrc-all cover'
: 'lrc-all record'
"
v-if="music.getPlaySongLyric[0]"
:style="
setting.lyricsPosition === 'center'
? { textAlign: 'center', paddingRight: '0' }
: null
"
<RollingLyrics
@mouseenter="
lrcMouseStatus = setting.lrcMousePause ? true : false
"
@mouseleave="lrcAllLeave"
>
<!-- 提示文本 -->
<div class="tip">
<n-text>点击选中的歌词以调整播放进度</n-text>
</div>
<div class="placeholder"></div>
<div
:class="
music.getPlaySongLyricIndex == index ? 'lrc on' : 'lrc'
"
:style="{ marginBottom: setting.lyricsFontSize - 1.6 + 'vh' }"
v-for="(item, index) in music.getPlaySongLyric"
:key="item"
:id="'lrc' + index"
@click="jumpTime(item.time)"
>
<div
:class="setting.lyricsBlur ? 'lrc-text blur' : 'lrc-text'"
:style="{
transformOrigin:
setting.lyricsPosition === 'center' ? 'center' : null,
filter: setting.lyricsBlur
? `blur(${getFilter(
music.getPlaySongLyricIndex,
index
)}px)`
: null,
}"
>
<span
class="lyric"
:style="{ fontSize: setting.lyricsFontSize + 'vh' }"
>
{{ item.lyric }}
</span>
<span
v-show="
music.getPlaySongTransl &&
setting.getShowTransl &&
item.lyricFy
"
:style="{ fontSize: setting.lyricsFontSize - 0.4 + 'vh' }"
class="lyric-fy"
>
{{ item.lyricFy }}</span
>
</div>
</div>
<div class="placeholder"></div>
</div>
@lrcTextClick="lrcTextClick"
/>
<div
:class="menuShow ? 'menu show' : 'menu'"
v-show="setting.playerStyle === 'record'"
@@ -188,6 +130,7 @@ import { useRouter } from "vue-router";
import MusicFrequency from "@/utils/MusicFrequency.js";
import PlayerRecord from "./PlayerRecord.vue";
import PlayerCover from "./PlayerCover.vue";
import RollingLyrics from "./RollingLyrics.vue";
import screenfull from "screenfull";
const router = useRouter();
@@ -201,19 +144,10 @@ const menuShow = ref(false);
const avBars = ref(null);
const musicFrequency = ref(null);
// 歌词模糊数值
const getFilter = (lrcIndex, index) => {
if (lrcIndex >= index) {
return lrcIndex - index;
} else {
return index - lrcIndex;
}
};
// 点击歌词跳转
const jumpTime = (time) => {
lrcMouseStatus.value = false;
// 歌词文本点击事件
const lrcTextClick = (time) => {
if ($player) $player.currentTime = time;
lrcMouseStatus.value = false;
};
// 鼠标移出歌词区域
@@ -253,13 +187,20 @@ const toComment = () => {
// 歌词滚动
const lyricsScroll = (index) => {
const type = setting.lyricsBlock;
const el = document.getElementById(
`lrc${type === "center" ? index : index - 2}`
);
const lrcType =
!music.getPlaySongLyric.hasYrc || !setting.showYrc ? "lrc" : "yrc";
const el = document.getElementById(lrcType + index);
if (el && !lrcMouseStatus.value) {
el.scrollIntoView({
const container = el.parentElement;
const containerHeight = container.clientHeight;
// 调整滚动的距离
const scrollDistance =
el.offsetTop -
container.offsetTop -
(type === "center" ? containerHeight / 2 - el.offsetHeight / 2 : 100);
container.scrollTo({
top: scrollDistance,
behavior: "smooth",
block: type,
});
}
};
@@ -495,100 +436,6 @@ watch(
}
}
}
.lrc-all {
margin-right: 20%;
scrollbar-width: none;
// max-width: 460px;
max-width: 52vh;
overflow: auto;
mask: linear-gradient(
180deg,
hsla(0, 0%, 100%, 0) 0,
hsla(0, 0%, 100%, 0.6) 15%,
#fff 25%,
#fff 75%,
hsla(0, 0%, 100%, 0.6) 85%,
hsla(0, 0%, 100%, 0)
);
-webkit-mask: linear-gradient(
180deg,
hsla(0, 0%, 100%, 0) 0,
hsla(0, 0%, 100%, 0.6) 15%,
#fff 25%,
#fff 75%,
hsla(0, 0%, 100%, 0.6) 85%,
hsla(0, 0%, 100%, 0)
);
&::-webkit-scrollbar {
display: none;
}
&.cover {
height: 80vh;
}
&.record {
height: 60vh;
}
&:hover {
.lrc-text {
&.blur {
filter: blur(0) !important;
}
}
}
.placeholder {
width: 100%;
height: 50%;
}
.lrc {
opacity: 0.4;
transition: all 0.3s;
// display: flex;
// flex-direction: column;
// margin-bottom: 4px;
// padding: 12px 20px;
margin-bottom: 0.8vh;
padding: 1.8vh 3vh;
border-radius: 8px;
transition: all 0.3s;
transform-origin: left center;
cursor: pointer;
.lrc-text {
display: flex;
flex-direction: column;
transition: all 0.35s ease-in-out;
transform: scale(0.95);
transform-origin: left center;
.lyric {
transition: all 0.3s;
// font-size: 2.4vh;
}
.lyric-fy {
margin-top: 2px;
transition: all 0.3s;
opacity: 0.8;
// font-size: 2vh;
}
}
&.on {
opacity: 1;
.lrc-text {
transform: scale(1.05);
.lyric {
font-weight: bold;
text-shadow: 0px 0px 30px #ffffff40;
}
}
}
&:hover {
@media (min-width: 768px) {
background-color: #ffffff20;
}
}
&:active {
transform: scale(0.95);
}
}
}
.menu {
opacity: 0;
padding: 0 20px;

View File

@@ -0,0 +1,83 @@
<template>
<Transition mode="out-in">
<div
class="countdown"
:style="{ animationPlayState: music.getPlayState ? 'running' : 'paused' }"
v-if="remainingPoint <= 2"
>
<span class="point" :class="remainingPoint > 2 ? 'hidden' : null"></span>
<span class="point" :class="remainingPoint > 1 ? 'hidden' : null"></span>
<span class="point" :class="remainingPoint > 0 ? 'hidden' : null"></span>
</div>
</Transition>
</template>
<script setup>
import { musicStore } from "@/store";
const music = musicStore();
// 剩余点数
const remainingPoint = ref(0);
// 总时长
const totalDuration = ref(
music.getPlaySongLyric.hasYrc
? music.getPlaySongLyric?.yrc[0].time
: music.getPlaySongLyric?.lrc[0].time
);
// 监听歌曲时长变化
watch(
() => music.getPlaySongTime.currentTime,
(val) => {
const remainingTime = totalDuration.value - val - 0.5;
const progress = 1 - remainingTime / totalDuration.value;
remainingPoint.value = Number(Math.floor(3 * progress));
}
);
// 监听歌曲改变
watch(
() => music.getPlaySongLyric?.lrc,
(val) => {
totalDuration.value = music.getPlaySongLyric.hasYrc
? music.getPlaySongLyric?.yrc[0].time
: val[0].time;
remainingPoint.value = 0;
}
);
</script>
<style lang="scss" scoped>
.v-enter-active,
.v-leave-active {
transition: all 0.3s ease-in-out;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
transform: scale(0);
}
.countdown {
animation: breathe 5s ease-in-out infinite;
.point {
margin-right: 4px;
transition: all 0.3s;
&.hidden {
opacity: 0.2;
}
}
}
@keyframes breathe {
0% {
transform: scale(0.95);
}
50% {
transform: scale(1.1);
}
100% {
transform: scale(0.95);
}
}
</style>

View File

@@ -0,0 +1,297 @@
<template>
<!-- 歌词滚动 -->
<div
v-if="music.getPlaySongLyric.lrc[0]"
:class="[
setting.playerStyle === 'cover' ? 'lrc-all cover' : 'lrc-all record',
setting.lyricsBlock === 'center' ? 'center' : 'top',
]"
:style="
setting.lyricsPosition === 'center'
? { textAlign: 'center', paddingRight: '0' }
: null
"
>
<div
class="placeholder"
:id="
!music.getPlaySongLyric.hasYrc || !setting.showYrc ? 'lrc-1' : 'yrc-1'
"
>
<CountDown :style="{ fontSize: setting.lyricsFontSize + 'vh' }" />
</div>
<template v-if="!music.getPlaySongLyric.hasYrc || !setting.showYrc">
<div
v-for="(item, index) in music.getPlaySongLyric.lrc"
:class="music.getPlaySongLyricIndex == index ? 'lrc on' : 'lrc'"
:style="{ marginBottom: setting.lyricsFontSize - 1.6 + 'vh' }"
:key="item"
:id="'lrc' + index"
@click="lrcTextClick(item.time)"
>
<div
:class="setting.lyricsBlur ? 'lrc-text blur' : 'lrc-text'"
:style="{
transformOrigin:
setting.lyricsPosition === 'center' ? 'center' : null,
filter: setting.lyricsBlur
? `blur(${getFilter(music.getPlaySongLyricIndex, index)}px)`
: null,
}"
>
<span
class="lyric"
:style="{ fontSize: setting.lyricsFontSize + 'vh' }"
>
{{ item.content }}
</span>
<span
v-show="
music.getPlaySongLyric.hasTran &&
setting.getShowTransl &&
item.tran
"
:style="{ fontSize: setting.lyricsFontSize - 1 + 'vh' }"
class="lyric-fy"
>
{{ item.tran }}</span
>
</div>
</div>
</template>
<template v-else>
<div
v-for="(item, index) in music.getPlaySongLyric.yrc"
:class="music.getPlaySongLyricIndex == index ? 'yrc on' : 'yrc'"
:key="item"
:id="'yrc' + index"
@click="lrcTextClick(item.time)"
>
<div
:class="setting.lyricsBlur ? 'yrc-text blur' : 'yrc-text'"
:style="{
transformOrigin:
setting.lyricsPosition === 'center' ? 'center' : null,
filter: setting.lyricsBlur
? `blur(${getFilter(music.getPlaySongLyricIndex, index)}px)`
: null,
}"
>
<div
class="lyric"
:style="{ fontSize: setting.lyricsFontSize + 'vh' }"
>
<span
v-for="(v, i) in item.content"
:key="i"
:style="{
'--dur': v.duration - 0.1 + 's',
}"
:class="
music.getPlaySongLyricIndex == index &&
music.getPlaySongTime.currentTime + 0.2 > v.time
? 'text fill'
: 'text'
"
>
{{ v.content }}
</span>
</div>
<span
v-show="
music.getPlaySongLyric.hasTran &&
setting.getShowTransl &&
item.tran
"
:style="{ fontSize: setting.lyricsFontSize - 1 + 'vh' }"
class="lyric-fy"
>
{{ item.tran }}</span
>
</div>
</div>
</template>
<div class="placeholder"></div>
</div>
</template>
<script setup>
import { musicStore, settingStore } from "@/store";
import CountDown from "./CountDown.vue";
const music = musicStore();
const setting = settingStore();
// 发送方法
const emit = defineEmits(["lrcTextClick"]);
// 歌词模糊数值
const getFilter = (lrcIndex, index) => {
if (lrcIndex >= index) {
return lrcIndex - index;
} else {
return index - lrcIndex;
}
};
// 歌词文本点击
const lrcTextClick = (time) => {
emit("lrcTextClick", time);
};
</script>
<style lang="scss" scoped>
.lrc-all {
margin-right: 20%;
scrollbar-width: none;
// max-width: 460px;
max-width: 52vh;
overflow: auto;
&::-webkit-scrollbar {
display: none;
}
&.cover {
height: 80vh;
}
&.record {
height: 60vh;
}
&.center {
mask: linear-gradient(
180deg,
hsla(0, 0%, 100%, 0) 0,
hsla(0, 0%, 100%, 0.6) 15%,
#fff 25%,
#fff 75%,
hsla(0, 0%, 100%, 0.6) 85%,
hsla(0, 0%, 100%, 0)
);
-webkit-mask: linear-gradient(
180deg,
hsla(0, 0%, 100%, 0) 0,
hsla(0, 0%, 100%, 0.6) 15%,
#fff 25%,
#fff 75%,
hsla(0, 0%, 100%, 0.6) 85%,
hsla(0, 0%, 100%, 0)
);
}
&.top {
mask: linear-gradient(
180deg,
hsla(0, 0%, 100%, 0) 0,
hsla(0, 0%, 100%, 0.6) 5%,
#fff 10%,
#fff 75%,
hsla(0, 0%, 100%, 0.6) 85%,
hsla(0, 0%, 100%, 0)
);
-webkit-mask: linear-gradient(
180deg,
hsla(0, 0%, 100%, 0) 0,
hsla(0, 0%, 100%, 0.6) 5%,
#fff 10%,
#fff 75%,
hsla(0, 0%, 100%, 0.6) 85%,
hsla(0, 0%, 100%, 0)
);
.placeholder {
width: 100%;
height: 16% !important;
}
}
&:hover {
.lrc-text {
&.blur {
filter: blur(0) !important;
}
}
}
@media (max-width: 768px) {
height: 70vh;
margin-right: 0;
}
.placeholder {
width: 100%;
height: 50%;
&:nth-of-type(1) {
display: flex;
align-items: flex-end;
padding: 0 0 0.8vh 3vh;
}
}
.lrc,
.yrc {
opacity: 0.4;
transition: all 0.3s;
margin-bottom: 0.8vh;
padding: 1.8vh 4vh 1.8vh 3vh;
border-radius: 8px;
transition: all 0.3s;
transform-origin: left bottom;
cursor: pointer;
.lrc-text,
.yrc-text {
display: flex;
flex-direction: column;
transition: all 0.35s ease-in-out;
transform: scale(0.95);
transform-origin: left bottom;
.lyric {
font-weight: bold;
transition: all 0.3s;
.text {
transition: all 0.3s;
color: #ffffff66;
&.fill {
text-shadow: 0px 0px 30px #ffffff40;
background-image: linear-gradient(to right, #fff 0%, #fff 0%);
background-repeat: no-repeat;
background-size: 0% 100%;
background-clip: text;
-webkit-background-clip: text;
color: #ffffff66;
animation: toRight var(--dur) forwards linear;
}
@keyframes toRight {
to {
background-size: 100% 100%;
}
}
}
}
.lyric-fy {
margin-top: 4px;
transition: all 0.3s;
opacity: 0.6;
}
}
&.on {
opacity: 1;
.lrc-text {
transform: scale(1.05);
.lyric {
text-shadow: 0px 0px 30px #ffffff40;
}
}
.yrc-text {
transform: scale(1.05);
.lyric {
font-weight: bold;
}
}
}
&:hover {
@media (min-width: 768px) {
background-color: #ffffff20;
}
}
&:active {
transform: scale(0.95);
}
}
.yrc {
opacity: 0.6;
}
}
</style>

View File

@@ -46,24 +46,45 @@
<template v-if="setting.bottomLyricShow">
<Transition mode="out-in">
<AllArtists
v-if="!music.getPlayState || !music.getPlaySongLyric[0]"
v-if="!music.getPlayState || !music.getPlaySongLyric.lrc[0]"
class="text-hidden"
:artistsData="music.getPlaySongData.artist"
/>
<n-text
v-else-if="
music.getPlaySongLyric[0] &&
music.getPlaySongLyricIndex != -1
setting.showYrc &&
music.getPlaySongLyricIndex != -1 &&
music.getPlaySongLyric.hasYrc
"
class="lrc text-hidden"
>
<n-text
v-for="item in music.getPlaySongLyric.yrc[
music.getPlaySongLyricIndex
].content"
:key="item"
:depth="3"
>
{{ item.content }}
</n-text>
</n-text>
<n-text
v-else-if="
music.getPlaySongLyricIndex != -1 &&
music.getPlaySongLyric.lrc[0]
"
class="lrc text-hidden"
:depth="3"
v-html="
music.getPlaySongLyric[music.getPlaySongLyricIndex].lyric
? music.getPlaySongLyric[music.getPlaySongLyricIndex]
.lyric
: '暂无歌词'
music.getPlaySongLyric.lrc[music.getPlaySongLyricIndex]
.content
"
/>
<AllArtists
v-else
class="text-hidden"
:artistsData="music.getPlaySongData.artist"
/>
</Transition>
</template>
<template v-else>
@@ -199,7 +220,7 @@
</template>
<script setup>
import { checkMusicCanUse, getMusicUrl, getMusicLyric } from "@/api/song";
import { checkMusicCanUse, getMusicUrl, getMusicNewLyric } from "@/api/song";
import { NIcon } from "naive-ui";
import {
KeyboardArrowUpFilled,
@@ -250,7 +271,7 @@ const getPlaySongData = (id, level = setting.songLevel) => {
music.setPlaySongLink(res.data[0].url.replace(/^http:/, "https:"));
});
// 获取歌词
getMusicLyric(id).then((res) => {
getMusicNewLyric(id).then((res) => {
music.setPlaySongLyric(res);
});
} else {
@@ -332,7 +353,14 @@ const songPlay = () => {
// 写入播放历史
music.setPlayHistory(music.getPlaySongData);
// 更改页面标题
window.document.title = music.getPlaySongData.name + " - SPlayer";
// $setSiteTitle(
// music.getPlaySongData.name + " - " + music.getPlaySongData.artist[0].name
// );
window.document.title =
music.getPlaySongData.name +
" - " +
music.getPlaySongData.artist[0].name +
" - SPlayer";
};
// 音乐渐入渐出
@@ -385,7 +413,8 @@ const songPause = () => {
console.log("音乐暂停");
if (!$player.ended) music.setPlayState(false);
// 更改页面标题
window.document.title = "SPlayer";
// window.document.title = "SPlayer";
$setSiteTitle();
};
// 歌曲进度条更新
@@ -397,6 +426,7 @@ const songTimeSliderUpdate = (val) => {
// 歌曲播放失败事件
const songError = () => {
console.error("歌曲播放失败");
$message.error("歌曲播放失败,请重试");
if (music.getPlaylists[0]) getPlaySongData(music.getPlaySongData.id);
if (music.getPlayState) songInOrOut("play");
};

View File

@@ -84,22 +84,27 @@ const routes = [
children: [
{
path: "playlists",
name: "playlists",
name: "user-playlists",
component: () => import("@/views/User/playlists.vue"),
},
{
path: "like",
name: "like",
name: "user-like",
component: () => import("@/views/User/like.vue"),
},
{
path: "album",
name: "user-album",
component: () => import("@/views/User/album.vue"),
},
{
path: "artists",
name: "artists",
name: "user-artists",
component: () => import("@/views/User/artists.vue"),
},
{
path: "cloud",
name: "cloud",
name: "user-cloud",
component: () => import("@/views/User/cloud.vue"),
},
],

View File

@@ -1,4 +1,3 @@
import { defineStore } from "pinia";
import useSettingDataStore from "./settingData";
import useMusicDataStore from "./musicData";
import useUserDataStore from "./userData";

View File

@@ -3,10 +3,10 @@ import { getSongTime, getSongPlayingTime } from "@/utils/timeTools.js";
import { getPersonalFm, setFmTrash } from "@/api/home";
import { getLikelist, setLikeSong } from "@/api/user";
import { getPlayListCatlist } from "@/api/playlist";
import { userStore } from "@/store";
import { userStore, settingStore } from "@/store";
import { NIcon } from "naive-ui";
import { PlayCycle, PlayOnce, ShuffleOne } from "@icon-park/vue-next";
import lyricFormat from "@/utils/lyricFormat";
import parseLyric from "@/utils/parseLyric";
const useMusicDataStore = defineStore("musicData", {
state: () => {
@@ -21,12 +21,16 @@ const useMusicDataStore = defineStore("musicData", {
playState: false,
// 当前歌曲播放链接
playSongLink: null,
// 当前歌曲歌词
playSongLyric: [],
// 当前歌曲歌词数据
playSongLyric: {
lrc: [],
yrc: [],
hasTran: false,
hasTran: false,
hasYrc: false,
},
// 当前歌曲歌词播放索引
playSongLyricIndex: 0,
// 当前歌曲是否拥有翻译
playSongTransl: false,
// 每日推荐
dailySongsData: [],
// 歌单分类
@@ -80,10 +84,6 @@ const useMusicDataStore = defineStore("musicData", {
getPersonalFmData(state) {
return state.persistData.personalFmData;
},
// 获取是否拥有翻译
getPlaySongTransl(state) {
return state.playSongTransl;
},
// 获取每日推荐
getDailySongs(state) {
return state.dailySongsData;
@@ -308,25 +308,14 @@ const useMusicDataStore = defineStore("musicData", {
// 歌词处理
setPlaySongLyric(value) {
if (value.lrc) {
this.playSongLyric = lyricFormat(value.lrc.lyric);
if (value.tlyric && value.tlyric.lyric) {
console.log("歌词有翻译");
this.playSongTransl = true;
let playSongLyric = this.playSongLyric;
let playSongLyricFy = lyricFormat(value.tlyric.lyric);
playSongLyric.forEach((v) => {
playSongLyricFy.forEach((x) => {
if (v.time === x.time) {
v.lyricFy = x.lyric;
}
});
});
this.playSongLyric = playSongLyric;
} else {
this.playSongTransl = false;
try {
this.playSongLyric = parseLyric(value);
} catch (err) {
$message.error("歌词处理出错");
console.error("歌词处理出错:" + err);
}
} else {
console.log("无歌词");
console.log("该歌曲暂无歌词");
this.playSongLyric = [];
}
},
@@ -348,16 +337,11 @@ const useMusicDataStore = defineStore("musicData", {
);
}
// 计算当前歌词播放索引
const index = this.playSongLyric.findIndex(
(v) => v.time >= value.currentTime
);
if (index === -1) {
// 如果没有找到合适的歌词,则返回最后一句歌词
this.playSongLyricIndex =
this.playSongLyric.length - 1;
} else {
this.playSongLyricIndex = (index ? index : index + 1) - 1;
}
const setting = settingStore();
const lrcType = !this.playSongLyric.hasYrc || !setting.showYrc;
const lyrics = lrcType ? this.playSongLyric.lrc : this.playSongLyric.yrc;
const index = lyrics.findIndex((v) => v.time >= value.currentTime);
this.playSongLyricIndex = index === -1 ? lyrics.length - 1 : index - 1;
},
// 设置当前播放模式
setPlaySongMode() {

View File

@@ -20,6 +20,8 @@ const useSettingDataStore = defineStore("settingData", {
playerStyle: "cover",
// 底栏歌词显示
bottomLyricShow: true,
// 是否显示逐字歌词
showYrc: true,
// 是否显示歌词翻译
showTransl: true,
// 歌曲音质
@@ -27,9 +29,9 @@ const useSettingDataStore = defineStore("settingData", {
// 歌词位置
lyricsPosition: "left",
// 歌词滚动位置
lyricsBlock: "center",
lyricsBlock: "start",
// 歌词大小
lyricsFontSize: 2.8,
lyricsFontSize: 3.6,
// 歌词模糊
lyricsBlur: false,
// 音乐频谱

View File

@@ -5,12 +5,15 @@ import {
getUserSubcount,
getUserPlaylist,
getUserArtistlist,
getUserAlbum,
} from "@/api/user";
import { formatNumber } from "@/utils/timeTools.js";
import { formatNumber, getLongTime } from "@/utils/timeTools.js";
const useUserDataStore = defineStore("userData", {
state: () => {
return {
// 站点标题
siteTitle: "SPlayer",
// 用户登录状态
userLogin: false,
// 用户 cookie
@@ -21,12 +24,20 @@ const useUserDataStore = defineStore("userData", {
userOtherData: {},
// 用户歌单
userPlayLists: {
isLoading: false,
has: false,
own: [], // 创建歌单
like: [], // 收藏歌单
},
// 用户专辑
userAlbum: {
isLoading: false,
has: false,
list: [],
},
// 用户收藏歌手
userArtistLists: {
isLoading: false,
has: false,
list: [],
},
@@ -50,11 +61,19 @@ const useUserDataStore = defineStore("userData", {
return state.userPlayLists;
},
// 获取用户收藏歌手
getUserArtistlists(state) {
getUserArtistLists(state) {
return state.userArtistLists;
},
// 获取用户收藏专辑
getUserAlbumLists(state) {
return state.userAlbum;
},
},
actions: {
// 更改站点标题
setSiteTitle(value) {
this.siteTitle = value;
},
// 更改 cookie
setCookie(value) {
window.localStorage.setItem("cookie", value);
@@ -91,50 +110,54 @@ const useUserDataStore = defineStore("userData", {
userLogOut();
},
// 更改用户歌单
setUserPlayLists() {
async setUserPlayLists() {
if (this.userLogin) {
try {
if (!Object.keys(this.userOtherData).length) {
this.setUserOtherData();
} else {
this.userPlayLists.isLoading = true;
const { userId } = this.userData;
const { subcount } = this.userOtherData;
const number =
this.userOtherData.subcount.createdPlaylistCount +
this.userOtherData.subcount.subPlaylistCount;
getUserPlaylist(this.getUserData.userId, number).then((res) => {
if (res.playlist) {
this.userPlayLists = {
own: [],
like: [],
};
this.userPlayLists.has = true;
res.playlist.forEach((v) => {
if (v.creator.userId === this.getUserData.userId) {
this.userPlayLists.own.push({
id: v.id,
cover: v.coverImgUrl,
name: v.name,
artist: v.creator,
desc: v.description,
tags: v.tags,
playCount: formatNumber(v.playCount),
trackCount: v.trackCount,
});
} else {
this.userPlayLists.like.push({
id: v.id,
cover: v.coverImgUrl,
name: v.name,
artist: v.creator,
playCount: formatNumber(v.playCount),
});
}
});
} else {
$message.error("用户歌单为空");
}
});
subcount.createdPlaylistCount + subcount.subPlaylistCount;
const res = await getUserPlaylist(userId, number);
if (res.playlist) {
this.userPlayLists = {
has: true,
own: [],
like: [],
};
res.playlist.forEach((v) => {
if (v.creator.userId === this.getUserData.userId) {
this.userPlayLists.own.push({
id: v.id,
cover: v.coverImgUrl,
name: v.name,
artist: v.creator,
desc: v.description,
tags: v.tags,
playCount: formatNumber(v.playCount),
trackCount: v.trackCount,
});
} else {
this.userPlayLists.like.push({
id: v.id,
cover: v.coverImgUrl,
name: v.name,
artist: v.creator,
playCount: formatNumber(v.playCount),
});
}
});
this.userPlayLists.isLoading = false;
} else {
this.userPlayLists.isLoading = false;
$message.error("用户歌单为空");
}
}
} catch (err) {
this.userPlayLists.isLoading = false;
console.error("获取用户歌单时出现错误:" + err);
$message.error("获取用户歌单时出现错误,请刷新后重试");
}
@@ -143,13 +166,13 @@ const useUserDataStore = defineStore("userData", {
}
},
// 更改用户收藏歌手
setUserArtistLists() {
async setUserArtistLists(callback) {
if (this.userLogin) {
getUserArtistlist().then((res) => {
try {
this.userArtistLists.isLoading = true;
const res = await getUserArtistlist();
if (res.data) {
this.userArtistLists = {
list: [],
};
this.userArtistLists.list = [];
this.userArtistLists.has = true;
res.data.forEach((v) => {
this.userArtistLists.list.push({
@@ -159,10 +182,53 @@ const useUserDataStore = defineStore("userData", {
size: v.musicSize,
});
});
if (typeof callback === "function") {
callback();
}
this.userArtistLists.isLoading = false;
} else {
this.userArtistLists.isLoading = false;
$message.error("用户收藏歌手为空");
}
});
} catch (err) {
this.userArtistLists.isLoading = false;
console.error("用户收藏歌手获取失败:" + err);
$message.error("用户收藏歌手获取失败,请刷新后重试");
}
} else {
$message.error("请登录账号后使用");
}
},
// 更改用户收藏专辑
async setUserAlbumLists() {
if (this.userLogin) {
try {
let offset = 0;
let totalCount = null;
this.userAlbum.isLoading = true;
this.userAlbum.list = [];
while (totalCount === null || offset < totalCount) {
const res = await getUserAlbum(30, offset);
res.data.forEach((v) => {
this.userAlbum.list.push({
id: v.id,
cover: v.picUrl,
name: v.name,
artist: v.artists,
time: getLongTime(v.subTime),
});
});
totalCount = res.count;
offset += 30;
console.log(totalCount, offset, this.userAlbum.list);
}
this.userAlbum.isLoading = false;
this.userAlbum.has = true;
} catch (err) {
this.userAlbum.isLoading = false;
console.error("用户收藏专辑获取失败:" + err);
$message.error("用户收藏专辑获取失败,请刷新后重试");
}
} else {
$message.error("请登录账号后使用");
}

View File

@@ -9,7 +9,7 @@
html,
body,
#app {
-webkit-font-smoothing: subpixel-antialiased;
-webkit-font-smoothing: antialiased;
font-family: "HarmonyOS_Regular", sans-serif !important;
}

193
src/utils/parseLyric.js Normal file
View File

@@ -0,0 +1,193 @@
/**
* 将接口数据解析出对应数据
* @param {string} data 接口数据
* @returns {Array} 对应数据
*/
const parseLyric = (data) => {
// 初始化数据
const { lrc, tlyric, romalrc, yrc, ytlrc } = data;
const lyrics = lrc ? lrc.lyric : null;
const otherLyrics = {
tran: tlyric ? tlyric.lyric : null,
roma: romalrc ? romalrc.lyric : null,
yrc: yrc ? yrc.lyric : null,
ytlrc: ytlrc ? ytlrc.lyric : null,
};
// 初始化输出结果
let result = {
lrc: [], // 歌词数组 {time:时间,content:歌词}
yrc: [], // 逐字歌词数据
// 是否具有翻译
hasTran: tlyric ? (tlyric.lyric ? true : false) : false,
// 是否具有音译
hasRoma: romalrc ? (romalrc.lyric ? true : false) : false,
// 是否具有逐字歌词
hasYrc: yrc ? (yrc.lyric ? true : false) : false,
};
// 普通歌词数据
let lrcData = Lrcsplit(lyrics);
// 翻译歌词数据
let tranLrcData = null;
// 循环遍历 otherLyrics 参数对象
for (let i in otherLyrics) {
const element = otherLyrics[i];
if (element !== null) {
// 若存在逐字歌词
if (i == "yrc" && otherLyrics[i] != null) {
result[i] = parseYrc(otherLyrics[i]);
continue;
}
// 若存在翻译
if (i == "ytlrc" && element != null) {
tranLrcData = Lrcsplit(element);
for (let num in tranLrcData) {
// 翻译文本对齐
let objNum = result["yrc"].findIndex(
(o) => o.time == tranLrcData[num].time
);
if (objNum != -1)
result["yrc"][objNum]["tran"] = tranLrcData[num].content;
}
}
// 若存在其他翻译
tranLrcData = Lrcsplit(element);
if (tranLrcData[0]) {
console.log(`歌曲存在 ${i} 翻译`, tranLrcData);
for (let num in tranLrcData) {
// 翻译文本对齐
let objNum = lrcData.findIndex(
(o) => o.time == tranLrcData[num].time
);
if (objNum != -1) lrcData[objNum][i] = tranLrcData[num].content;
}
}
}
}
// 将歌词按时间排序
result.lrc = lrcData.sort((a, b) => {
return a.t - b.t;
});
return result;
};
/**
* 将歌词字符串解析为歌词对象数组
* @param {string} lrc 歌词字符串
* @returns {Array} 歌词对象数组
*/
const Lrcsplit = (lrc) => {
const lyrics = lrc.split("\n");
const lrcData = [];
lyrics.forEach((lyric) => {
lyric = lyric.replace(/(^\s*)|(\s*$)/g, "");
const time = lyric.substring(lyric.indexOf("[") + 1, lyric.indexOf("]"));
const timeArr = time.split(":");
if (isNaN(parseInt(timeArr[0]))) {
for (let i in lyrics) {
if (i != "lrc" && i == timeArr[0].toLowerCase()) {
lyrics[i] = timeArr[1];
}
}
} else {
const lyricArr = lyric.match(/\[(\d+:.+?)\]/g);
let start = 0;
for (let k in lyricArr) {
start += lyricArr[k].length;
}
const content = lyric.substring(start);
if (content == "") {
return false;
}
lyricArr.forEach((t) => {
let time = t.substring(1, t.length - 1);
let second = time.split(":");
if (
(parseFloat(second[0]) * 60 + parseFloat(second[1])).toFixed(3) == 0
) {
return;
}
lrcData.push({
time: (parseFloat(second[0]) * 60 + parseFloat(second[1])).toFixed(3),
content: content,
});
});
}
});
return lrcData;
};
/**
* 逐字歌词解析
* @param {string} lyrics 歌词字符串
* @returns {Array} 歌词对象数组
*/
const parseYrc = (lyrics) => {
// 若无内容,则返回空数组
if (lyrics == undefined) {
return [];
}
let lines = lyrics.split("\n");
let parsedLyrics = [];
// 解析每一句
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// 创建一个空对象,用于存放当前句的信息
let parsedLine = {
time: undefined, // 开始时间
endTime: undefined, // 结束时间
content: undefined, // 歌词内容
};
// 分离出时间信息,并转换为秒
let timeInfo = line
.substring(line.indexOf("[") + 1, line.indexOf("]"))
.split(",");
// 将开始时间转换为秒
parsedLine.time = Number(timeInfo[0]) / 1000;
// 将结束时间转换为秒
parsedLine.endTime = Number(timeInfo[1]) / 1000;
// 若时间信息不合法,将跳过该句
if (isNaN(parsedLine.time) || isNaN(parsedLine.endTime)) {
continue;
}
// 寻找歌词内容
const lyricArr = line.match(/\[[1-9]\d*,[1-9]\d*]/g);
if (!lyricArr) {
continue;
}
// 去除时间信息,获取歌词内容
let contentArray = [];
// 分离成单个字或词,并解析时间信息
let splitContent = line.split(/(\([1-9]\d*,[1-9]\d*,\d*\)[^\(]*)/g);
for (let j = 0; j < splitContent.length; j++) {
const splitc = splitContent[j];
if (splitc == "") {
continue;
}
// 创建一个对象,用于存放当前字或词的信息,并添加到当前句的歌词内容中
let contentObj = {
time: undefined, // 开始时间
duration: undefined, // 持续时间
content: "", // 字或词的文本内容
};
// 提取时间和文本信息,并转换为秒
let time = splitc.match(/\([1-9]\d*,[1-9]\d*,\d*\)/);
if (!time) {
continue;
}
let timeArray = time[0].slice(1, -1).split(",");
// 将开始时间转换为秒
contentObj.time = Number(timeArray[0]) / 1000;
// 将持续时间转换为秒
contentObj.duration = Number(timeArray[1]) / 1000;
// 获取字或词的文本内容
contentObj.content = splitc.slice(time[0].length);
contentArray.push(contentObj);
}
parsedLine.content = contentArray;
parsedLyrics.push(parsedLine);
}
return parsedLyrics;
};
export default parseLyric;

View File

@@ -7,7 +7,7 @@
:src="
albumDetail.picUrl
? albumDetail.picUrl.replace(/^http:/, 'https:') +
'?param=500y500'
'?param=1024y1024'
: null
"
fallback-src="/images/pic/default.png"
@@ -71,7 +71,7 @@
</div>
</div>
<div class="title" v-else-if="!albumId">
<span class="key">未提供所需数据</span>
<span class="key">参数不完整</span>
<br />
<n-button strong secondary @click="router.go(-1)" style="margin-top: 20px">
返回上一级
@@ -109,6 +109,7 @@ const getAlbumData = (id) => {
console.log(res);
// 专辑信息
albumDetail.value = res.album;
$setSiteTitle(res.album.name + " - 专辑");
// 专辑歌曲
if (res.songs) {
albumData.value = [];

View File

@@ -1,6 +1,6 @@
<template>
<div class="artist">
<div class="artistData" v-if="artistData">
<div class="artistData" v-if="artistId && artistData">
<div class="cover">
<n-avatar
round
@@ -31,6 +31,30 @@
<n-text class="desc text-hidden" @click="artistDescShow = true">
{{ artistData.desc }}
</n-text>
<n-space class="button" v-if="user.userLogin">
<!-- <n-button type="primary" strong secondary>
<template #icon>
<n-icon :component="PlayArrowRound" />
</template>
播放热门歌曲
</n-button> -->
<n-button
:type="artistLikeBtn ? 'primary' : 'default'"
strong
secondary
@click="toLikeArtist(artistData)"
>
<template #icon>
<n-icon
:component="
artistLikeBtn ? PersonAddAlt1Round : PersonRemoveAlt1Round
"
/>
</template>
{{ artistLikeBtn ? "收藏歌手" : "取消收藏歌手" }}
</n-button>
</n-space>
<!-- 歌手介绍 -->
<n-modal
class="s-modal"
v-model:show="artistDescShow"
@@ -44,12 +68,24 @@
</n-modal>
</div>
</div>
<div class="error" v-else-if="!artistId">
<n-text>参数不完整</n-text>
<br />
<n-button
strong
secondary
@click="router.go(-1)"
style="margin-top: 20px"
>
返回上一级
</n-button>
</div>
<n-tabs
class="main-tab"
type="segment"
@update:value="tabChange"
v-model:value="tabValue"
v-if="artistId"
v-if="artistData"
>
<n-tab name="songs"> 热门单曲 </n-tab>
<n-tab name="albums"> 专辑 </n-tab>
@@ -72,14 +108,24 @@
<script setup>
import { useRouter } from "vue-router";
import { getArtistDetail } from "@/api/artist";
import { MusicNoteFilled, AlbumFilled, VideocamRound } from "@vicons/material";
import { userStore } from "@/store";
import { getArtistDetail, likeArtist } from "@/api/artist";
import {
MusicNoteFilled,
AlbumFilled,
VideocamRound,
PersonAddAlt1Round,
PersonRemoveAlt1Round,
} from "@vicons/material";
const router = useRouter();
const user = userStore();
// 歌手数据
const artistId = ref(router.currentRoute.value.query.id);
const artistData = ref(null);
const artistDescShow = ref(false);
const artistLikeBtn = ref(false);
// Tab 默认选中
const tabValue = ref(router.currentRoute.value.path.split("/")[2]);
@@ -87,20 +133,30 @@ const tabValue = ref(router.currentRoute.value.path.split("/")[2]);
// 获取歌手数据
const getArtistDetailData = (id) => {
if (id) {
getArtistDetail(id).then((res) => {
console.log(res);
artistData.value = {
name: res.data.artist.name,
occupation: res.data.identify ? res.data.identify.imageDesc : null,
cover: res.data.artist.cover,
desc: res.data.artist.briefDesc,
albumSize: res.data.artist.albumSize,
musicSize: res.data.artist.musicSize,
mvSize: res.data.artist.mvSize,
};
});
getArtistDetail(id)
.then((res) => {
console.log(res);
artistData.value = {
id: res.data.artist.id,
name: res.data.artist.name,
occupation: res.data.identify ? res.data.identify.imageDesc : null,
cover: res.data.artist.cover,
desc: res.data.artist.briefDesc,
albumSize: res.data.artist.albumSize,
musicSize: res.data.artist.musicSize,
mvSize: res.data.artist.mvSize,
};
$setSiteTitle(res.data.artist.name + " - 歌手");
// 请求后回顶
if ($mainContent) $mainContent.scrollIntoView({ behavior: "smooth" });
})
.catch((err) => {
router.go(-1);
console.error("歌手信息获取失败:" + err);
$message.error("歌手信息获取失败");
});
} else {
$message.error("请提供歌手id");
$message.error("参数不完整");
}
};
@@ -116,8 +172,51 @@ const tabChange = (value) => {
});
};
// 判断收藏还是取消
const isLikeOrDislike = (id) => {
if (user.getUserArtistLists.list[0]) {
const index = user.getUserArtistLists.list.findIndex(
(item) => item.id === Number(id)
);
if (index !== -1) {
return false;
}
return true;
} else {
return true;
}
};
// 收藏/取消收藏歌手
const toLikeArtist = (data) => {
const type = isLikeOrDislike(data.id) ? 1 : 2;
likeArtist(type, data.id).then((res) => {
if (res.code === 200) {
$message.success(
`${data.name}${type == 1 ? "收藏成功" : "取消收藏成功"}`
);
user.setUserArtistLists(() => {
artistLikeBtn.value = isLikeOrDislike(artistId.value);
});
} else {
$message.error(`${data.name}${type == 1 ? "收藏失败" : "取消收藏失败"}`);
}
});
};
onMounted(() => {
getArtistDetailData(artistId.value);
artistLikeBtn.value = isLikeOrDislike(artistId.value);
if (
user.userLogin &&
!user.getUserArtistLists.has &&
!user.getUserArtistLists.isLoading
) {
user.setUserArtistLists(() => {
console.log("执行回调", artistId.value, isLikeOrDislike(artistId.value));
artistLikeBtn.value = isLikeOrDislike(artistId.value);
});
}
});
// 监听路由参数变化
@@ -126,6 +225,7 @@ watch(
(val) => {
artistId.value = val.query.id;
tabValue.value = val.path.split("/")[2];
artistLikeBtn.value = isLikeOrDislike(artistId.value);
if (val.path.split("/")[1] == "artist") {
getArtistDetailData(artistId.value);
}
@@ -136,6 +236,15 @@ watch(
<style lang="scss" scoped>
.artist {
margin-top: 30px;
.error {
margin-top: 30px;
margin-bottom: 20px;
.n-text {
font-size: 40px;
font-weight: bold;
margin-right: 8px;
}
}
.artistData {
display: flex;
align-items: center;
@@ -167,6 +276,8 @@ watch(
}
.cover {
margin-right: 40px;
display: flex;
align-items: center;
.n-avatar {
height: 240px;
width: 240px;
@@ -179,7 +290,7 @@ watch(
.name {
font-size: 40px;
font-weight: bold;
margin-bottom: 8px;
margin-bottom: 4px;
margin-left: 2px;
}
.occupation {
@@ -209,13 +320,16 @@ watch(
}
.desc {
margin-top: 12px;
-webkit-line-clamp: 3;
-webkit-line-clamp: 2;
cursor: pointer;
transition: all 0.3s;
&:hover {
opacity: 0.8;
}
}
.button {
margin-top: 18px;
}
}
}
.content {

View File

@@ -134,6 +134,7 @@ const pageNumberChange = (val) => {
};
onMounted(() => {
$setSiteTitle("全部评论");
// 获取评论数据
if (songId.value) getCommentData(songId.value, (pageNumber.value - 1) * 20);
});

View File

@@ -36,6 +36,7 @@ const getDailySongsData = () => {
};
onMounted(() => {
$setSiteTitle("每日推荐");
if (music.getDailySongs.length === 0) getDailySongsData();
});
</script>

View File

@@ -200,6 +200,7 @@ watch(
);
onMounted(() => {
$setSiteTitle("发现 - 歌手");
// 获取歌手数据
getArtistListData(
artistType[artistTypeNamesChoose.value],

View File

@@ -313,6 +313,7 @@ watch(
);
onMounted(() => {
$setSiteTitle("发现 - 歌单");
// 获取歌单分类
if (!music.catList.sub || !music.highqualityCatList[0])
music.setCatList(true);

View File

@@ -97,6 +97,7 @@ const getToplistData = () => {
};
onMounted(() => {
$setSiteTitle("发现 - 排行榜");
getToplistData();
});
</script>

View File

@@ -32,6 +32,10 @@ import DataLists from "@/components/DataList/DataLists.vue";
const music = musicStore();
const router = useRouter();
onMounted(() => {
$setSiteTitle("播放历史");
});
</script>
<style lang="scss" scoped>

View File

@@ -28,6 +28,10 @@ import PaDailySongs from "@/components/Personalized/PaDailySongs.vue";
import PaPersonalFm from "@/components/Personalized/PaPersonalFm.vue";
const setting = settingStore();
onMounted(() => {
$setSiteTitle("SPlayer");
});
</script>
<style lang="scss" scoped>

View File

@@ -288,6 +288,7 @@ const tabChange = (val) => {
};
onMounted(() => {
$setSiteTitle("登录");
// 获取二维码登录 key
getQrKeyData();
});

View File

@@ -149,6 +149,7 @@ const changeArea = (area) => {
};
onMounted(() => {
$setSiteTitle("全部新碟");
getAlbumNewData(
albumAreaChoose.value,
pagelimit.value,

View File

@@ -7,7 +7,7 @@
:src="
playListDetail.coverImgUrl
? playListDetail.coverImgUrl.replace(/^http:/, 'https:') +
'?param=500y500'
'?param=1024y1024'
: null
"
fallback-src="/images/pic/default.png"
@@ -102,7 +102,7 @@
</div>
</div>
<div class="title" v-else-if="!playListId">
<span class="key">未提供所需数据</span>
<span class="key">参数不完整</span>
<br />
<n-button strong secondary @click="router.go(-1)" style="margin-top: 20px">
返回上一级
@@ -155,6 +155,7 @@ const getPlayListDetailData = (id) => {
totalCount.value = res.playlist.trackCount;
// 歌单信息
playListDetail.value = res.playlist;
$setSiteTitle(res.playlist.name + " - 歌单");
} else {
$message.error("获取歌单信息失败");
}

View File

@@ -71,6 +71,10 @@ const tabChange = (value) => {
},
});
};
onMounted(() => {
$setSiteTitle(searchKeywords.value + "的搜索结果");
});
</script>
<style lang="scss" scoped>

View File

@@ -59,6 +59,13 @@
<div class="name">显示歌词翻译</div>
<n-switch v-model:value="showTransl" :round="false" />
</n-card>
<n-card class="set-item">
<div class="name">
显示逐字歌词
<span class="tip">是否在歌曲具有逐字歌词时显示实验性功能</span>
</div>
<n-switch v-model:value="showYrc" :round="false" />
</n-card>
<n-card class="set-item">
<div class="name">
智能暂停滚动
@@ -85,13 +92,13 @@
<n-slider
v-model:value="lyricsFontSize"
:tooltip="false"
:max="3.4"
:min="2.2"
:max="4"
:min="3"
:step="0.01"
:marks="{
2.2: '最小',
2.8: '默认',
3.4: '最大',
3: '最小',
3.6: '默认',
4: '最大',
}"
/>
<div :class="lyricsBlur ? 'more blur' : 'more'">
@@ -137,14 +144,14 @@
<n-card class="set-item">
<div class="name">
歌词模糊
<span class="tip">实验性功能未播放或已播放歌词模糊显示</span>
<span class="tip">未播放或已播放歌词模糊显示实验性功能</span>
</div>
<n-switch v-model:value="lyricsBlur" :round="false" />
</n-card>
<n-card class="set-item">
<div class="name">
显示音乐频谱
<span class="tip">实验性功能可能会导致一些意想不到的后果</span>
<span class="tip">可能会导致一些意想不到的后果实验性功能</span>
</div>
<n-switch
v-model:value="musicFrequency"
@@ -187,6 +194,7 @@ const {
autoSignIn,
lrcMousePause,
searchHistory,
showYrc,
} = storeToRefs(setting);
// 深浅模式
@@ -318,6 +326,10 @@ const resetApp = () => {
},
});
};
onMounted(() => {
$setSiteTitle("全局设置");
});
</script>
<style lang="scss" scoped>

View File

@@ -131,6 +131,9 @@ const getMusicDetailData = (id) => {
console.log(res);
if (res.songs[0]) {
musicDetail.value = res.songs[0];
$setSiteTitle(
res.songs[0].name + " - " + res.songs[0].ar[0].name + " - 单曲"
);
// 获取相似数据
getSimiData(id);
} else {

View File

@@ -14,6 +14,10 @@
<script setup>
import { useRouter } from "vue-router";
const router = useRouter();
onMounted(() => {
$setSiteTitle("403");
});
</script>
<style lang="scss" scoped>
@@ -24,4 +28,4 @@ const router = useRouter();
align-items: center;
flex-direction: column;
}
</style>
</style>

View File

@@ -14,6 +14,10 @@
<script setup>
import { useRouter } from "vue-router";
const router = useRouter();
onMounted(() => {
$setSiteTitle("404");
});
</script>
<style lang="scss" scoped>
@@ -24,4 +28,4 @@ const router = useRouter();
align-items: center;
flex-direction: column;
}
</style>
</style>

View File

@@ -14,6 +14,10 @@
<script setup>
import { useRouter } from "vue-router";
const router = useRouter();
onMounted(() => {
$setSiteTitle("500");
});
</script>
<style lang="scss" scoped>

18
src/views/User/album.vue Normal file
View File

@@ -0,0 +1,18 @@
<template>
<div class="album">
<CoverLists :listData="user.getUserAlbumLists.list" listType="album" />
</div>
</template>
<script setup>
import { userStore } from "@/store";
import CoverLists from "@/components/DataList/CoverLists.vue";
const user = userStore();
onMounted(() => {
$setSiteTitle("音乐库 - 收藏的专辑");
if (!user.getUserAlbumLists.has && !user.getUserAlbumLists.isLoading)
user.setUserAlbumLists();
});
</script>

View File

@@ -1,6 +1,6 @@
<template>
<div class="artists">
<ArtistLists :listData="user.getUserArtistlists.list" />
<ArtistLists :listData="user.getUserArtistLists.list" />
</div>
</template>
@@ -11,6 +11,8 @@ import ArtistLists from "@/components/DataList/ArtistLists.vue";
const user = userStore();
onMounted(() => {
if (!user.getUserArtistlists.has) user.setUserArtistLists();
$setSiteTitle("音乐库 - 收藏的歌手");
if (!user.getUserArtistLists.has && !user.getUserArtistLists.isLoading)
user.setUserArtistLists();
});
</script>

View File

@@ -69,13 +69,9 @@
processing
/>
<template #footer>
<n-space justify="end">
<n-space justify="end" v-if="upSongType === 'error'">
<n-button @click="closeUpSongModal"> 取消 </n-button>
<n-button
type="primary"
@click="resetUpSongModal"
v-if="upSongType === 'error'"
>
<n-button type="primary" @click="resetUpSongModal">
重新上传
</n-button>
</n-space>
@@ -177,6 +173,7 @@ const upCloudSongData = (e) => {
}
})
.catch((err) => {
upSongType.value = "error";
closeUpSongModal();
$message.error("歌曲上传出现错误");
console.error("歌曲上传出现错误:" + err);
@@ -228,13 +225,14 @@ watch(
() => router.currentRoute.value,
(val) => {
pageNumber.value = Number(val.query.page ? val.query.page : 1);
if (val.name == "cloud") {
if (val.name == "user-cloud") {
getCloudData(pagelimit.value, (pageNumber.value - 1) * pagelimit.value);
}
}
);
onMounted(() => {
$setSiteTitle("音乐库 - 音乐云盘");
getCloudData(pagelimit.value, (pageNumber.value - 1) * pagelimit.value);
});
</script>

View File

@@ -22,6 +22,7 @@
>
<n-tab name="playlists"> 我的歌单 </n-tab>
<n-tab name="like"> 收藏的歌单 </n-tab>
<n-tab name="album"> 收藏的专辑 </n-tab>
<n-tab name="artists"> 收藏的歌手 </n-tab>
<n-tab name="cloud"> 音乐云盘 </n-tab>
</n-tabs>

View File

@@ -1,6 +1,6 @@
<template>
<div class="like">
<CoverLists :listData="user.getUserPlayLists.like" :showMore="true" />
<CoverLists :listData="user.getUserPlayLists.like" />
</div>
</template>
@@ -11,6 +11,8 @@ import CoverLists from "@/components/DataList/CoverLists.vue";
const user = userStore();
onMounted(() => {
if (!user.getUserPlayLists.has) user.setUserPlayLists();
$setSiteTitle("音乐库 - 收藏的歌单");
if (!user.getUserPlayLists.has && !user.getUserPlayLists.isLoading)
user.setUserPlayLists();
});
</script>
</script>

View File

@@ -44,7 +44,7 @@
</template>
</n-modal>
</div>
<CoverLists :listData="user.getUserPlayLists.own" :showMore="true" />
<CoverLists :listData="user.getUserPlayLists.own" />
</div>
</template>
@@ -86,7 +86,9 @@ const createClose = () => {
};
onMounted(() => {
if (!user.getUserPlayLists.has) user.setUserPlayLists();
$setSiteTitle("音乐库 - 我的歌单");
if (!user.getUserPlayLists.has && !user.getUserPlayLists.isLoading)
user.setUserPlayLists();
});
</script>

View File

@@ -123,6 +123,8 @@ const playerOptions = {
pip: "画中画",
enterFullscreen: "开启全屏",
exitFullscreen: "退出全屏",
mute: "音量",
unmute: "静音",
},
tooltips: {
controls: true,
@@ -140,6 +142,7 @@ const commentsCount = ref(0);
const getVideoData = (id) => {
getVideoDetail(id).then((res) => {
videoData.value = res.data;
$setSiteTitle(res.data.name + " - " + res.data.artists[0].name + " - 视频");
const requests = res.data.brs.map((v) => {
return getVideoUrl(id, v.br);
});
@@ -327,7 +330,7 @@ watch(
.simiVideo {
width: 20vw;
min-width: 200px;
max-width: 400px;
max-width: 300px;
margin-left: 30px;
@media (max-width: 990px) {
max-width: 100%;