feat: create/delete playlist, add/remove track from playlist

master
qier222 4 years ago
parent 44df6f5531
commit d1a080eb8f

@ -19,10 +19,14 @@
/></transition> /></transition>
<Toast /> <Toast />
<GlobalEvents :filter="globalEventFilter" @keydown.space="play" /> <GlobalEvents :filter="globalEventFilter" @keydown.space="play" />
<ModalAddTrackToPlaylist />
<ModalNewPlaylist />
</div> </div>
</template> </template>
<script> <script>
import ModalAddTrackToPlaylist from "./components/ModalAddTrackToPlaylist.vue";
import ModalNewPlaylist from "./components/ModalNewPlaylist.vue";
import Navbar from "./components/Navbar.vue"; import Navbar from "./components/Navbar.vue";
import Player from "./components/Player.vue"; import Player from "./components/Player.vue";
import Toast from "./components/Toast.vue"; import Toast from "./components/Toast.vue";
@ -36,6 +40,8 @@ export default {
Player, Player,
GlobalEvents, GlobalEvents,
Toast, Toast,
ModalAddTrackToPlaylist,
ModalNewPlaylist,
}, },
data() { data() {
return { return {
@ -163,4 +169,12 @@ a {
.slide-up-enter, .slide-up-leave-to /* .fade-leave-active below version 2.1.8 */ { .slide-up-enter, .slide-up-leave-to /* .fade-leave-active below version 2.1.8 */ {
transform: translateY(100%); transform: translateY(100%);
} }
[data-electron="yes"] {
button,
.navigation-links a,
.playlist-info .description {
cursor: default !important;
}
}
</style> </style>

@ -124,9 +124,10 @@ export function toplists() {
* @param {number} params.id * @param {number} params.id
*/ */
export function subscribePlaylist(params) { export function subscribePlaylist(params) {
params.timestamp = new Date().getTime();
return request({ return request({
url: "/playlist/subscribe", url: "/playlist/subscribe",
method: "get", method: "post",
params, params,
}); });
} }
@ -157,9 +158,28 @@ export function deletePlaylist(id) {
* @param {string} params.type * @param {string} params.type
*/ */
export function createPlaylist(params) { export function createPlaylist(params) {
params.timestamp = new Date().getTime();
return request({ return request({
url: "/playlist/create", url: "/playlist/create",
method: "post", method: "post",
params, params,
}); });
} }
/**
* 对歌单添加或删除歌曲
* 说明 : 调用此接口 , 可以添加歌曲到歌单或者从歌单删除某首歌曲 ( 需要登录 )
* - op: 从歌单增加单曲为 add, 删除为 del
* - pid: 歌单 id tracks: 歌曲 id,可多个,用逗号隔开
* @param {Object} params
* @param {string} params.op
* @param {string} params.pid
*/
export function addOrRemoveTrackFromPlaylist(params) {
params.timestamp = new Date().getTime();
return request({
url: "/playlist/tracks",
method: "post",
params,
});
}

@ -0,0 +1,181 @@
<template>
<Modal
class="add-track-to-playlist-modal"
:show="show"
:close="close"
:showFooter="false"
title="添加到歌单"
width="25vw"
>
<template slot="default">
<div class="new-playlist-button" @click="newPlaylist"
><svg-icon icon-class="plus" />新建歌单</div
>
<div
class="playlist"
v-for="playlist in ownPlaylists"
:key="playlist.id"
@click="addTrackToPlaylist(playlist.id)"
>
<img :src="playlist.coverImgUrl | resizeImage(224)" />
<div class="info">
<div class="title">{{ playlist.name }}</div>
<div class="track-count">{{ playlist.trackCount }} </div>
</div>
</div>
</template>
</Modal>
</template>
<script>
import { mapActions, mapMutations, mapState } from "vuex";
import Modal from "@/components/Modal.vue";
import { userPlaylist } from "@/api/user";
import { addOrRemoveTrackFromPlaylist } from "@/api/playlist";
export default {
name: "ModalAddTrackToPlaylist",
components: {
Modal,
},
data() {
return {
playlists: [],
};
},
computed: {
...mapState(["modals", "data"]),
show: {
get() {
return this.modals.addTrackToPlaylistModal.show;
},
set(value) {
this.updateModal({
modalName: "addTrackToPlaylistModal",
key: "show",
value,
});
},
},
ownPlaylists() {
return this.playlists.filter(
(p) =>
p.creator.userId === this.data.user.userId &&
p.id !== this.data.likedSongPlaylistID
);
},
},
created() {
this.getUserPlaylists();
},
methods: {
...mapMutations(["updateModal"]),
...mapActions(["showToast"]),
close() {
this.show = false;
},
getUserPlaylists() {
userPlaylist({
timestamp: new Date().getTime(),
limit: 1000,
uid: this.data.user.userId,
}).then((data) => {
this.playlists = data.playlist;
});
},
addTrackToPlaylist(playlistID) {
addOrRemoveTrackFromPlaylist({
op: "add",
pid: playlistID,
tracks: this.modals.addTrackToPlaylistModal.selectedTrackID,
}).then((data) => {
if (data.body.code === 200) {
this.show = false;
this.showToast("已添加到歌单");
} else {
this.showToast(data.body.message);
}
});
},
newPlaylist() {
this.updateModal({
modalName: "newPlaylistModal",
key: "afterCreateAddTrackID",
value: this.modals.addTrackToPlaylistModal.selectedTrackID,
});
this.close();
this.updateModal({
modalName: "newPlaylistModal",
key: "show",
value: true,
});
},
},
};
</script>
<style lang="scss" scoped>
.new-playlist-button {
display: flex;
justify-content: center;
align-items: center;
font-size: 16px;
font-weight: 500;
color: var(--color-text);
background: var(--color-secondary-bg-for-transparent);
border-radius: 8px;
height: 48px;
margin-bottom: 16px;
margin-right: 6px;
margin-left: 6px;
cursor: pointer;
transition: 0.2s;
.svg-icon {
width: 16px;
height: 16px;
margin-right: 8px;
}
&:hover {
color: var(--color-primary);
background: var(--color-primary-bg-for-transparent);
}
}
.playlist {
display: flex;
padding: 6px;
border-radius: 8px;
cursor: pointer;
&:hover {
background: var(--color-secondary-bg-for-transparent);
}
img {
border-radius: 8px;
height: 42px;
width: 42px;
margin-right: 12px;
border: 1px solid rgba(0, 0, 0, 0.04);
}
.info {
display: flex;
flex-direction: column;
justify-content: center;
}
.title {
font-size: 16px;
font-weight: 500;
color: var(--color-text);
padding-right: 16px;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
overflow: hidden;
word-break: break-all;
}
.track-count {
margin-top: 2px;
font-size: 13px;
opacity: 0.68;
color: var(--color-text);
}
}
</style>

@ -0,0 +1,144 @@
<template>
<Modal
class="add-playlist-modal"
:show="show"
:close="close"
title="新建歌单"
width="25vw"
>
<template slot="default">
<input
type="text"
placeholder="歌单标题"
maxlength="40"
v-model="title"
/>
<div class="checkbox">
<input
type="checkbox"
id="checkbox-private"
v-model="privatePlaylist"
/>
<label for="checkbox-private">设置为隐私歌单</label>
</div>
</template>
<template slot="footer">
<button class="primary block" @click="createPlaylist"></button>
</template>
</Modal>
</template>
<script>
import Modal from "@/components/Modal.vue";
import { mapMutations, mapState } from "vuex";
import { createPlaylist, addOrRemoveTrackFromPlaylist } from "@/api/playlist";
export default {
name: "ModalNewPlaylist",
components: {
Modal,
},
data() {
return {
title: "",
privatePlaylist: false,
};
},
computed: {
...mapState(["modals"]),
show: {
get() {
return this.modals.newPlaylistModal.show;
},
set(value) {
this.updateModal({
modalName: "newPlaylistModal",
key: "show",
value,
});
},
},
},
methods: {
...mapMutations(["updateModal"]),
close() {
this.show = false;
this.title = "";
this.privatePlaylist = false;
this.resetAfterCreateAddTrackID();
},
createPlaylist() {
let params = { name: this.title };
if (this.private) params.type = 10;
createPlaylist(params).then((data) => {
if (data.code === 200) {
if (this.modals.newPlaylistModal.afterCreateAddTrackID !== 0) {
addOrRemoveTrackFromPlaylist({
op: "add",
pid: data.id,
tracks: this.modals.newPlaylistModal.afterCreateAddTrackID,
}).then((data) => {
if (data.body.code === 200) {
this.showToast("已添加到歌单");
} else {
this.showToast(data.body.message);
}
this.resetAfterCreateAddTrackID();
});
}
this.close();
this.showToast("成功创建歌单");
}
});
},
resetAfterCreateAddTrackID() {
this.updateModal({
modalName: "newPlaylistModal",
key: "AfterCreateAddTrackID",
value: 0,
});
},
},
};
</script>
<style lang="scss" scoped>
.add-playlist-modal {
.content {
display: flex;
flex-direction: column;
input {
margin-bottom: 12px;
}
input[type="text"] {
width: calc(100% - 24px);
flex: 1;
background: var(--color-secondary-bg-for-transparent);
font-size: 16px;
border: none;
font-weight: 600;
padding: 8px 12px;
border-radius: 8px;
margin-top: -1px;
color: var(--color-text);
&:focus {
background: var(--color-primary-bg-for-transparent);
opacity: 1;
}
[data-theme="light"] &:focus {
color: var(--color-primary);
}
}
.checkbox {
input[type="checkbox" i] {
margin: 3px 3px 3px 4px;
}
display: flex;
align-items: center;
label {
font-size: 12px;
}
}
}
}
</style>

@ -17,6 +17,13 @@
<div class="item" @click="like" v-show="isRightClickedTrackLiked"> <div class="item" @click="like" v-show="isRightClickedTrackLiked">
{{ $t("contextMenu.removeFromMyLikedSongs") }} {{ $t("contextMenu.removeFromMyLikedSongs") }}
</div> </div>
<div class="item" @click="addTrackToPlaylist"></div>
<div
v-if="extraContextMenuItem.includes('removeTrackFromPlaylist')"
class="item"
@click="removeTrackFromPlaylist"
>从歌单中删除</div
>
</ContextMenu> </ContextMenu>
<TrackListItem <TrackListItem
v-for="track in tracks" v-for="track in tracks"
@ -37,6 +44,7 @@ import {
playAList, playAList,
appendTrackToPlayerList, appendTrackToPlayerList,
} from "@/utils/play"; } from "@/utils/play";
import { addOrRemoveTrackFromPlaylist } from "@/api/playlist";
import { isAccountLoggedIn } from "@/utils/auth"; import { isAccountLoggedIn } from "@/utils/auth";
import TrackListItem from "@/components/TrackListItem.vue"; import TrackListItem from "@/components/TrackListItem.vue";
@ -70,6 +78,12 @@ export default {
}; };
}, },
}, },
extraContextMenuItem: {
type: Array,
default: () => {
return []; // 'removeTrackFromPlaylist'
},
},
}, },
data() { data() {
return { return {
@ -93,7 +107,7 @@ export default {
}, },
}, },
methods: { methods: {
...mapMutations(["updateLikedSongs"]), ...mapMutations(["updateLikedSongs", "updateModal"]),
...mapActions(["nextTrack", "playTrackOnListByID", "showToast"]), ...mapActions(["nextTrack", "playTrackOnListByID", "showToast"]),
openMenu(e, track) { openMenu(e, track) {
if (!track.playable) { if (!track.playable) {
@ -160,6 +174,39 @@ export default {
} }
}); });
}, },
addTrackToPlaylist() {
if (!isAccountLoggedIn()) {
this.showToast("此操作需要登录网易云账号");
return;
}
this.updateModal({
modalName: "addTrackToPlaylistModal",
key: "show",
value: true,
});
this.updateModal({
modalName: "addTrackToPlaylistModal",
key: "selectedTrackID",
value: this.rightClickedTrack.id,
});
},
removeTrackFromPlaylist() {
if (!isAccountLoggedIn()) {
this.showToast("此操作需要登录网易云账号");
return;
}
let trackID = this.rightClickedTrack.id;
addOrRemoveTrackFromPlaylist({
op: "del",
pid: this.id,
tracks: trackID,
}).then((data) => {
this.showToast(
data.body.code === 200 ? "已从歌单中删除" : data.body.message
);
this.$parent.removeTrack(trackID);
});
},
}, },
}; };
</script> </script>

@ -123,4 +123,7 @@ export default {
updateToast(state, toast) { updateToast(state, toast) {
state.toast = toast; state.toast = toast;
}, },
updateModal(state, { modalName, key, value }) {
state.modals[modalName][key] = value;
},
}; };

@ -12,6 +12,16 @@ export default {
text: "", text: "",
timer: null, timer: null,
}, },
modals: {
addTrackToPlaylistModal: {
show: false,
selectedTrackID: 0,
},
newPlaylistModal: {
show: false,
afterCreateAddTrackID: 0,
},
},
player: JSON.parse(localStorage.getItem("player")), player: JSON.parse(localStorage.getItem("player")),
settings: JSON.parse(localStorage.getItem("settings")), settings: JSON.parse(localStorage.getItem("settings")),
data: JSON.parse(localStorage.getItem("data")), data: JSON.parse(localStorage.getItem("data")),

@ -76,7 +76,7 @@
class="add-playlist" class="add-playlist"
icon="plus" icon="plus"
v-show="currentTab === 'playlists'" v-show="currentTab === 'playlists'"
@click="showAddPlaylistModal = true" @click="openAddPlaylistModal"
><svg-icon icon-class="plus" />新建歌单</button ><svg-icon icon-class="plus" />新建歌单</button
> >
</div> </div>
@ -109,39 +109,11 @@
<MvRow :mvs="mvs" /> <MvRow :mvs="mvs" />
</div> </div>
</div> </div>
<Modal
class="add-playlist-modal"
:show="showAddPlaylistModal"
:close="closeAddPlaylistModal"
title="新建歌单"
width="25vw"
>
<template slot="default">
<input
type="text"
placeholder="歌单标题"
maxlength="40"
v-model="newPlaylist.title"
/>
<div class="checkbox">
<input
type="checkbox"
id="checkbox-private"
v-model="newPlaylist.private"
/>
<label for="checkbox-private">设置为隐私歌单</label>
</div>
</template>
<template slot="footer">
<button class="primary block" @click="createPlaylist"></button>
</template>
</Modal>
</div> </div>
</template> </template>
<script> <script>
import { mapActions, mapState } from "vuex"; import { mapActions, mapMutations, mapState } from "vuex";
import { getTrackDetail, getLyric } from "@/api/track"; import { getTrackDetail, getLyric } from "@/api/track";
import { import {
userDetail, userDetail,
@ -151,7 +123,8 @@ import {
likedMVs, likedMVs,
} from "@/api/user"; } from "@/api/user";
import { randomNum, dailyTask } from "@/utils/common"; import { randomNum, dailyTask } from "@/utils/common";
import { getPlaylistDetail, createPlaylist } from "@/api/playlist"; import { getPlaylistDetail } from "@/api/playlist";
import { isAccountLoggedIn } from "@/utils/auth";
import { playPlaylistByID } from "@/utils/play"; import { playPlaylistByID } from "@/utils/play";
import NProgress from "nprogress"; import NProgress from "nprogress";
@ -159,11 +132,10 @@ import TrackList from "@/components/TrackList.vue";
import CoverRow from "@/components/CoverRow.vue"; import CoverRow from "@/components/CoverRow.vue";
import SvgIcon from "@/components/SvgIcon.vue"; import SvgIcon from "@/components/SvgIcon.vue";
import MvRow from "@/components/MvRow.vue"; import MvRow from "@/components/MvRow.vue";
import Modal from "@/components/Modal.vue";
export default { export default {
name: "Library", name: "Library",
components: { SvgIcon, CoverRow, TrackList, MvRow, Modal }, components: { SvgIcon, CoverRow, TrackList, MvRow },
data() { data() {
return { return {
show: false, show: false,
@ -180,11 +152,6 @@ export default {
albums: [], albums: [],
artists: [], artists: [],
mvs: [], mvs: [],
showAddPlaylistModal: false,
newPlaylist: {
title: "",
private: false,
},
}; };
}, },
created() { created() {
@ -224,10 +191,15 @@ export default {
}, },
methods: { methods: {
...mapActions(["showToast"]), ...mapActions(["showToast"]),
...mapMutations(["updateModal"]),
playLikedSongs() { playLikedSongs() {
playPlaylistByID(this.playlists[0].id, "first", true); playPlaylistByID(this.playlists[0].id, "first", true);
}, },
updateCurrentTab(tab) { updateCurrentTab(tab) {
if (!isAccountLoggedIn() && tab !== "playlists") {
this.showToast("此操作需要登录网易云账号");
return;
}
this.currentTab = tab; this.currentTab = tab;
document document
.getElementById("liked") .getElementById("liked")
@ -311,25 +283,17 @@ export default {
NProgress.done(); NProgress.done();
}); });
}, },
createPlaylist() { openAddPlaylistModal() {
let params = { name: this.newPlaylist.title }; if (!isAccountLoggedIn()) {
if (this.newPlaylist.private) params.type = 10; this.showToast("此操作需要登录网易云账号");
createPlaylist(params).then((data) => { return;
if (data.code === 200) { }
this.closeAddPlaylistModal(); this.updateModal({
this.showToast("成功创建歌单"); modalName: "newPlaylistModal",
this.playlists = []; key: "show",
this.getUserPlaylists(true); value: true,
}
}); });
}, },
closeAddPlaylistModal() {
this.showAddPlaylistModal = false;
this.newPlaylist = {
title: "",
private: false,
};
},
}, },
watch: { watch: {
likedSongsInState() { likedSongsInState() {
@ -499,43 +463,4 @@ button.add-playlist {
transform: scale(0.92); transform: scale(0.92);
} }
} }
.add-playlist-modal {
.content {
display: flex;
flex-direction: column;
input {
margin-bottom: 12px;
}
input[type="text"] {
width: calc(100% - 24px);
flex: 1;
background: var(--color-secondary-bg-for-transparent);
font-size: 16px;
border: none;
font-weight: 600;
padding: 8px 12px;
border-radius: 8px;
margin-top: -1px;
color: var(--color-text);
&:focus {
background: var(--color-primary-bg-for-transparent);
opacity: 1;
[data-theme="light"] {
color: var(--color-primary);
}
}
}
.checkbox {
input[type="checkbox" i] {
margin: 3px 3px 3px 4px;
}
display: flex;
align-items: center;
label {
font-size: 12px;
}
}
}
}
</style> </style>

@ -131,12 +131,20 @@
</h1> </h1>
</div> </div>
<TrackList :tracks="tracks" :type="'playlist'" :id="playlist.id" /> <TrackList
:tracks="tracks"
:type="'playlist'"
:id="playlist.id"
:extraContextMenuItem="
isUserOwnPlaylist ? ['removeTrackFromPlaylist'] : []
"
/>
<Modal <Modal
:show="showFullDescription" :show="showFullDescription"
:close="() => (showFullDescription = false)" :close="() => (showFullDescription = false)"
:showFooter="false" :showFooter="false"
:clickOutsideHide="true"
title="歌单介绍" title="歌单介绍"
>{{ playlist.description }}</Modal >{{ playlist.description }}</Modal
> >
@ -310,6 +318,12 @@ export default {
specialPlaylistInfo() { specialPlaylistInfo() {
return specialPlaylist[this.playlist.id]; return specialPlaylist[this.playlist.id];
}, },
isUserOwnPlaylist() {
return (
this.playlist.creator.userId === this.data.user.userId &&
this.playlist.id !== this.data.likedSongPlaylistID
);
},
}, },
methods: { methods: {
...mapMutations(["appendTrackToPlayerList"]), ...mapMutations(["appendTrackToPlayerList"]),
@ -396,6 +410,10 @@ export default {
this.$refs.playlistMenu.openMenu(e); this.$refs.playlistMenu.openMenu(e);
}, },
deletePlaylist() { deletePlaylist() {
if (!isAccountLoggedIn()) {
this.showToast("此操作需要登录网易云账号");
return;
}
let confirmation = confirm(`确定要删除歌单 ${this.playlist.name}`); let confirmation = confirm(`确定要删除歌单 ${this.playlist.name}`);
if (confirmation === true) { if (confirmation === true) {
deletePlaylist(this.playlist.id).then((data) => { deletePlaylist(this.playlist.id).then((data) => {
@ -411,6 +429,13 @@ export default {
editPlaylist() { editPlaylist() {
alert("此功能开发中"); alert("此功能开发中");
}, },
removeTrack(trackID) {
if (!isAccountLoggedIn()) {
this.showToast("此操作需要登录网易云账号");
return;
}
this.tracks = this.tracks.filter((t) => t.id !== trackID);
},
}, },
}; };
</script> </script>

Loading…
Cancel
Save