feat: 收藏页面新增歌单

This commit is contained in:
imsyy
2023-12-12 11:03:20 +08:00
parent c5747b6a3e
commit 3c39dbd87f
14 changed files with 636 additions and 710 deletions

1111
LICENSE

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,11 @@
> [!IMPORTANT]
> ## 🎉 当前项目正在重构中 🎉
> ## 严肃警告
>
> - 目前版本进入维护模式,仅在遇到重大问题时会进行修复
> - 支持客户端与网页端
> - 支持现有版本所有功能
> - 新增支持播放与管理本地歌曲
> - 请务必遵守 [CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/) 许可协议
> - 在您的修改、演绎、分发或派生项目中,必须同样采用 **CC BY-NC-SA 4.0** 许可协议,**并在适当的位置包含本项目的许可和版权信息**
> - **禁止用于售卖或其他商业用途**,如若发现,作者保留追究法律责任的权利
> - 若发现未遵守 **CC BY-NC-SA 4.0** 许可协议的行为,**本项目将永久停更**
> - 感谢您的尊重与理解
<div align="center">
<img alt="logo" height="80" src="./public/images/logo/favicon.png" />
@@ -191,16 +192,6 @@ pnpm build:mac
- [BlurLyric](https://github.com/Project-And-Factory/BlurLyric)
- [Vue-mmPlayer](https://github.com/maomao1996/Vue-mmPlayer)
## 📜 开源许可
- **本项目仅供个人学习研究使用,禁止用于商业及非法用途**
- 本项目基于 [GNU General Public License version 3](https://opensource.org/license/gpl-3-0/) 许可进行开源
1. **修改和分发:** 任何对本项目的修改和分发都必须基于 GPL Version 3 进行,源代码必须一并提供
2. **派生作品:** 任何派生作品必须同样采用 GPL Version 3并在适当的地方注明原始项目的许可证
3. **免责声明:** 根据 GPL Version 3本项目不提供任何明示或暗示的担保。请详细阅读 GPL Version 3以了解完整的免责声明内容
4. **社区参与:** 欢迎社区的参与和贡献,我们鼓励开发者一同改进和维护本项目
5. **许可证链接:** 请阅读 [GNU General Public License version 3](https://opensource.org/license/gpl-3-0/) 了解更多详情
## 📢 免责声明
本项目部分功能使用了网易云音乐的第三方 API 服务,**仅供个人学习研究使用,禁止用于商业及非法用途**
@@ -210,3 +201,15 @@ pnpm build:mac
请使用者在使用本项目时遵守相关法律法规,**不要将本项目用于任何商业及非法用途。如有违反,一切后果由使用者自负。** 同时,使用者应该自行承担因使用本项目而带来的风险和责任。本项目开发者不对本项目所提供的服务和内容做出任何保证
感谢您的理解
## 📜 开源许可
- **本项目仅供个人学习研究使用,禁止用于商业及非法用途**
- 本项目基于 [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License](https://creativecommons.org/licenses/by-nc-sa/4.0/) 许可进行开源
1. **修改和分发:** 任何对本项目的修改和分发都必须基于 CC BY-NC-SA 4.0 进行,源代码必须一并提供
2. **派生作品:** 任何派生作品必须同样采用 CC BY-NC-SA 4.0,并在适当的地方注明原始项目的许可证
3. **免责声明:** 根据 CC BY-NC-SA 4.0,本项目不提供任何明示或暗示的担保。请详细阅读 [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License](https://creativecommons.org/licenses/by-nc-sa/4.0/) 以了解完整的免责声明内容
4. **社区参与:** 欢迎社区的参与和贡献,我们鼓励开发者一同改进和维护本项目
5. **许可证链接:** 请阅读 [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License](https://creativecommons.org/licenses/by-nc-sa/4.0/) 了解更多详情
<p xmlns:cc="http://creativecommons.org/ns#" xmlns:dct="http://purl.org/dc/terms/"><a property="dct:title" rel="cc:attributionURL" href="https://github.com/imsyy/SPlayer">SPlayer</a> by <a rel="cc:attributionURL dct:creator" property="cc:attributionName" href="https://github.com/imsyy">imsyy</a> is licensed under <a href="http://creativecommons.org/licenses/by-nc-sa/4.0/?ref=chooser-v1" target="_blank" rel="license noopener noreferrer" style="display:inline-block;">CC BY-NC-SA 4.0<img style="height:22px!important;margin-left:3px;vertical-align:text-bottom;" src="https://mirrors.creativecommons.org/presskit/icons/cc.svg?ref=chooser-v1"><img style="height:22px!important;margin-left:3px;vertical-align:text-bottom;" src="https://mirrors.creativecommons.org/presskit/icons/by.svg?ref=chooser-v1"><img style="height:22px!important;margin-left:3px;vertical-align:text-bottom;" src="https://mirrors.creativecommons.org/presskit/icons/nc.svg?ref=chooser-v1"><img style="height:22px!important;margin-left:3px;vertical-align:text-bottom;" src="https://mirrors.creativecommons.org/presskit/icons/sa.svg?ref=chooser-v1"></a></p>

View File

@@ -76,10 +76,14 @@ class MainProcess {
log.info("主进程初始化");
// 启动网易云 API
this.ncmServer = await startNcmServer({
port: import.meta.env.MAIN_VITE_SERVER_PORT,
host: import.meta.env.MAIN_VITE_SERVER_HOST,
});
try {
this.ncmServer = await startNcmServer({
port: import.meta.env.MAIN_VITE_SERVER_PORT,
host: import.meta.env.MAIN_VITE_SERVER_HOST,
});
} catch (error) {
console.error("启动网易云 API 失败:", error);
}
// 非开发环境启动代理
if (!is.dev) {

View File

@@ -34,6 +34,7 @@ export const getArtistDetail = (id) => {
return axios({
method: "GET",
url: "/artist/detail",
noCookie: true,
params: {
id,
},

View File

@@ -69,7 +69,12 @@
</n-text>
<!-- 歌手 -->
<div v-if="item.artists" class="artists">
<n-text v-for="ar in item.artists" :key="ar.id" class="ar">
<n-text
v-for="ar in item.artists"
:key="ar.id"
class="ar"
@click.stop="router.push(`/artist?id=${ar.id}`)"
>
{{ ar.name || ar.userName }}
</n-text>
</div>

View File

@@ -188,16 +188,36 @@ const openDropdown = (e, data, song, index, sourceId) => {
icon: renderIcon("video"),
},
{
key: "copy",
label: "复制歌曲 ID",
props: {
onClick: () => {
const songId = song?.id?.toString();
copyData(songId);
label: "更多操作",
key: "others",
show: !isLocalSong,
icon: renderIcon("more"),
children: [
{
key: "copy",
label: "复制歌曲 ID",
props: {
onClick: () => {
const songId = song?.id?.toString();
copyData(songId);
},
},
icon: renderIcon("content-copy"),
},
},
icon: renderIcon("content-copy"),
{
key: "share",
label: "分享歌曲链接",
props: {
onClick: () => {
const shareUrl = `https://music.163.com/song?id=${song?.id?.toString()}`;
copyData(shareUrl, "复制歌曲链接");
},
},
icon: renderIcon("share"),
},
],
},
{
key: "line-cloud",
type: "divider",

View File

@@ -244,6 +244,11 @@ const routes = [
name: "like-videos",
component: () => import("@/views/Like/videos.vue"),
},
{
path: "playlists",
name: "like-playlists",
component: () => import("@/views/Like/playlists.vue"),
},
],
},
// 本地歌曲

View File

@@ -47,6 +47,7 @@ const formatData = (data, type = "playlist", noTracks = false) => {
updateTime: v.updateTime || v.trackNumberUpdateTime,
description: v.description,
tags: v.tags || v.algTags,
userId: v.userId,
};
// 歌曲
case "song":

View File

@@ -94,20 +94,20 @@ export const getLocalCoverData = async (path, isAlbum = false) => {
/**
* 内容复制
*/
export const copyData = async (data) => {
export const copyData = async (data, info) => {
try {
const isElectron = checkPlatform.electron();
// electron
if (isElectron) {
const result = await electron.ipcRenderer.invoke("copyData", data);
result ? $message.success("复制成功") : $message.error("复制失败");
result ? $message.success(`${info || "复制"}成功`) : $message.error(`${info || "复制"}失败`);
}
// 浏览器端
else {
if (navigator.clipboard) {
try {
await navigator.clipboard.writeText(data);
$message.success("复制成功");
$message.success(`${info || "复制"}成功`);
} catch (error) {
console.error("复制出错:", error);
$message.error("复制失败");
@@ -120,7 +120,9 @@ export const copyData = async (data) => {
textArea.select();
try {
const successful = document.execCommand("copy");
successful ? $message.success("复制成功") : $message.error("复制失败");
successful
? $message.success(`${info || "复制"}成功`)
: $message.error(`${info || "复制"}失败`);
} catch (err) {
console.error("复制出错:", err);
$message.error("复制失败");

View File

@@ -21,6 +21,10 @@ axios.interceptors.request.use(
if (!request.noCookie && (isLogin() || getCookie("MUSIC_U") !== null)) {
request.params.cookie = `MUSIC_U=${getCookie("MUSIC_U")};`;
}
// 去除 cookie
if (request.noCookie) {
request.params.noCookie = true;
}
// 附加 realIP
if (!checkPlatform.electron()) request.params.realIP = "116.25.146.177";
// 发送请求

View File

@@ -4,6 +4,7 @@
<!-- 标签页 -->
<n-tabs v-model:value="tabValue" class="tabs" type="segment" @update:value="tabChange">
<n-tab name="like-albums"> 专辑 </n-tab>
<n-tab name="like-playlists"> 歌单 </n-tab>
<n-tab name="like-artists"> 歌手 </n-tab>
<n-tab name="like-videos"> 视频 </n-tab>
</n-tabs>

View File

@@ -0,0 +1,106 @@
<template>
<div class="like-playlists">
<Transition name="fade" mode="out-in">
<div v-if="userLikeData.playlists?.length" class="pl-list">
<!-- 分类 -->
<n-space class="type">
<n-tag
v-for="(item, index) in ['我创建的', '我收藏的']"
:key="index"
:bordered="false"
:type="index === plTypeChoose ? 'primary' : 'default'"
class="tag"
round
@click="plTypeChange(index)"
>
{{ item }}
</n-tag>
</n-space>
<!-- 列表 -->
<Transition name="fade" mode="out-in">
<div v-if="plTypeChoose === 0" class="list">
<MainCover
:data="likePlaylistsData.filter((playlist) => playlist.userId === userData?.userId)"
/>
</div>
<div v-else class="list">
<MainCover
:data="likePlaylistsData.filter((playlist) => playlist.userId !== userData?.userId)"
/>
</div>
</Transition>
</div>
<n-empty
v-else
description="当前暂无收藏歌单"
class="tip"
style="margin-top: 60px"
size="large"
/>
</Transition>
</div>
</template>
<script setup>
import { storeToRefs } from "pinia";
import { siteData } from "@/stores";
import { useRouter } from "vue-router";
import formatData from "@/utils/formatData";
const router = useRouter();
const data = siteData();
const { userData, userLikeData } = storeToRefs(data);
// 默认分类
const plTypeChoose = ref(Number(router.currentRoute.value.query?.type) || 0);
// 处理视频数据
const likePlaylistsData = computed(() => {
return formatData(userLikeData.value.playlists);
});
// 默认分类变化
const plTypeChange = (type) => {
router.push({
path: "/like/playlists",
query: {
type,
},
});
};
// 监听路由参数变化
watch(
() => router.currentRoute.value,
(val) => {
if (val.name === "like-playlists") {
plTypeChoose.value = Number(val.query?.type) || 0;
}
},
);
</script>
<style lang="scss" scoped>
.like-playlists {
.type {
margin-bottom: 20px;
.tag {
font-size: 13px;
padding: 0 16px;
line-height: 0;
cursor: pointer;
transition:
transform 0.3s,
background-color 0.3s,
color 0.3s;
&:hover {
background-color: var(--main-second-color);
color: var(--main-color);
}
&:active {
transform: scale(0.95);
}
}
}
}
</style>

View File

@@ -107,6 +107,7 @@
:disabled="albumData === 'empty'"
type="primary"
class="play"
tag="div"
circle
strong
secondary
@@ -118,7 +119,7 @@
</n-icon>
</template>
</n-button>
<n-button size="large" round strong secondary @click="likeOrDislike(albumId)">
<n-button size="large" tag="div" round strong secondary @click="likeOrDislike(albumId)">
<template #icon>
<n-icon>
<SvgIcon
@@ -129,7 +130,7 @@
{{ isLikeOrDislike(albumId) ? "收藏专辑" : "取消收藏" }}
</n-button>
<n-dropdown :options="moreOptions" trigger="hover" placement="bottom-start">
<n-button size="large" circle strong secondary>
<n-button size="large" tag="div" circle strong secondary>
<template #icon>
<n-icon>
<SvgIcon icon="format-list-bulleted" />

View File

@@ -114,6 +114,7 @@
:disabled="playListData === 'empty'"
type="primary"
class="play"
tag="div"
circle
strong
secondary
@@ -128,6 +129,7 @@
<n-button
v-if="isUserPLayList"
size="large"
tag="div"
round
strong
secondary
@@ -140,7 +142,15 @@
</template>
编辑歌单
</n-button>
<n-button v-else size="large" round strong secondary @click="likeOrDislike(playlistId)">
<n-button
v-else
size="large"
tag="div"
round
strong
secondary
@click="likeOrDislike(playlistId)"
>
<template #icon>
<n-icon>
<SvgIcon
@@ -153,7 +163,7 @@
{{ isLikeOrDislike(playlistId) ? "收藏歌单" : "取消收藏" }}
</n-button>
<n-dropdown :options="moreOptions" trigger="hover" placement="bottom-start">
<n-button size="large" circle strong secondary>
<n-button size="large" tag="div" circle strong secondary>
<template #icon>
<n-icon>
<SvgIcon icon="format-list-bulleted" />