一、项目预览
在线预览:点击访问
其他项目访问:点击访问
项目使用传统vue项目结构实现,前端采用element实现。
element官网:Element - The world's most popular Vue UI framework
二、 项目效果图
1.首页
2.歌单详情列表
3.歌曲在线播放
4.搜索实时推荐
5.歌曲在线实时搜索
6.歌曲待播放列表
7.扫码登录
8.验证码登录
三、代码实现
1.第三方接口api请求地址:
http://www.codeman.ink:3000
2.歌单详情源码
html
<template>
<div class="musicListDetail" v-loading="!musicListDetail">
<!-- 歌单信息 -->
<div class="listInfo">
<!-- 歌单封面 -->
<div class="listAvatar">
<img :src="musicListDetail?musicListDetail.coverImgUrl:''" alt="" />
</div>
<div class="right">
<!-- 标题 -->
<div class="title">
<div class="titleTag">歌单</div>
<div class="titleContent">{{ musicListDetail?musicListDetail.name:'' }}</div>
</div>
<!-- 用户信息 -->
<div class="user">
<div class="userAvatar">
<img :src="musicListDetail&&musicListDetail.creator?musicListDetail.creator.avatarUrl:''" alt="" />
</div>
<div
class="userName">
{{ musicListDetail&&musicListDetail.creator?musicListDetail.creator.nickname:'' }}
</div>
<div class="createTime">
{{ musicListDetail?musicListDetail.createTime:'' | showDate }}创建
</div>
</div>
<!-- 操作按钮 -->
<div class="buttons">
<div class="buttonItem playAll" @click="playAll">
<i class="iconfont icon-bofang playAll"></i>
<span>播放全部</span>
</div>
<div class="buttonItem">
<i class="iconfont icon-xihuan"></i>
<span>收藏</span>
</div>
<div class="buttonItem">
<i class="iconfont icon-zhuanfa"></i>
<span>分享</span>
</div>
</div>
<!-- 标签 -->
<div class="tags">
标签:
<div
class="tagItem"
v-for="(item, index) in musicListDetail?musicListDetail.tags:[]"
:key="index"
>
{{ item }}
</div>
<div v-if="!musicListDetail||!musicListDetail.tags||musicListDetail.tags.length == 0">暂无标签</div>
</div>
<!-- 歌曲列表的歌曲数量和播放量 -->
<div class="otherInfo">
<div class="musicNum">
歌曲 : {{ musicListDetail.trackCount | handleNum }}
</div>
<div class="playCount">
播放 : {{ musicListDetail.playCount | handleNum }}
</div>
</div>
<div class="desc">
简介 :
{{
musicListDetail.description
? musicListDetail.description
: "暂无简介"
}}
</div>
</div>
</div>
<!-- 歌曲列表 -->
<div class="musicList">
<el-tabs value="first">
<el-tab-pane label="歌曲列表" name="first">
<!-- 表格 -->
<el-table
:data="musicListDetail.tracks"
size="mini"
style="width: 100%"
@row-dblclick="clickRow"
@cell-click="clickCell"
highlight-current-row
stripe
lazy
:row-key="
(row) => {
return row.id;
}
"
v-infinite-scroll="this.$store.state.isLogin ? loadMore : ''"
:infinite-scroll-disabled="scrollLoadDisabled"
:infinite-scroll-distance="1500"
:infinite-scroll-immediate="false"
>
<el-table-column
label=""
width="40"
type="index"
:index="handleIndex"
>
</el-table-column>
<el-table-column label="" width="23">
<!-- 下载按钮 -->
<i class="iconfont icon-download"></i>
</el-table-column>
<el-table-column prop="name" label="音乐标题" min-width="350">
</el-table-column>
<el-table-column prop="ar[0].name" label="歌手" min-width="120">
</el-table-column>
<el-table-column prop="al.name" label="专辑" min-width="170">
</el-table-column>
<el-table-column prop="dt" label="时长" min-width="100">
</el-table-column>
<!-- <el-table-column prop="id"></el-table-column> -->
</el-table>
<div class="loadMore" v-if="isMore && !this.$store.state.isLogin">
登陆后查看更多音乐
</div>
<div class="placeholder" v-else></div>
<!-- <div class="placeholder"></div> -->
</el-tab-pane>
</el-tabs>
</div>
<!-- 返回顶部组件 -->
<go-top scrollObj=".musicListDetail"></go-top>
</div>
</template>
<script>
import { formatDate, handleNum, handleMusicTime } from "@/plugins/utils";
import Comment from "@/components/comment/Comment";
import GoTop from "@/components/goTop/GoTop.vue";
import UserListCard from "@/components/userListCard/UserListCard.vue";
export default {
name: "MusicListDetail",
data() {
return {
musicListDetail: {
trackIds:[],
tracks:[]
},
comments: {},
// 当前评论页数
currentCommentPage: 1,
// 是否还有更多音乐
isMore: false,
// 是否禁止滚动加载
scrollLoadDisabled: false,
};
},
components: {
Comment,
GoTop,
UserListCard,
},
methods: {
// 请求
// 根据传来的 id 查询歌单
async getMusicListDetail() {
var timestamp = Date.parse(new Date());
// console.log(this.$route.params.id);
let result = await this.$request("/playlist/detail", {
id: this.$route.params.id,
timestamp,
});
// console.log(result);
this.musicListDetail = result.data.playlist;
// console.log(this.musicListDetail);
// 判断是否还有更多音乐
if (
this.musicListDetail.tracks.length !=
this.musicListDetail.trackIds.length
) {
this.isMore = true;
}
// 处理播放时间
this.musicListDetail.tracks.forEach((item, index) => {
this.musicListDetail.tracks[index].dt = handleMusicTime(item.dt);
});
},
// 获取歌曲详情
async getMusicDetail(ids) {
if (this.isMore == false) return;
this.scrollLoadDisabled = true;
let res = await this.$request("/song/detail", { ids });
// 处理时间
console.log(res);
res.data.songs.forEach((item, index) => {
res.data.songs[index].dt = handleMusicTime(item.dt);
});
this.musicListDetail.tracks.push(...res.data.songs);
// 判断是否还有更多音乐
if (
this.musicListDetail.tracks.length <
this.musicListDetail.trackIds.length
) {
this.isMore = true;
this.scrollLoadDisabled = false;
} else {
this.isMore = false;
}
},
// 事件函数
handleIndex(index) {
// console.log(index);
index += 1;
if (index < 10) {
return "0" + index;
} else {
return index;
}
},
// 双击table的row的回调
async clickRow(row) {
console.log(row);
// 将musicId提交到vuex中 供bottomControl查询歌曲url和其它操作
this.$store.commit("updateMusicId", row.id);
// 如果歌单发生变化,则提交歌单到vuex
if (this.musicListDetail.id != this.$store.state.musicListId) {
// 将歌单传到vuex
this.$store.commit("updateMusicList", {
musicList: this.musicListDetail.tracks,
musicListId: this.musicListDetail.id,
});
}
// let result = await this.$request("/song/url", { id: row.id, br: 320000 });
// console.log(result.data.data[0].url);
// this.$store.commit("updateMusicUrl", result.data.data[0].url);
},
// 点击播放全部按钮的回调
playAll() {
this.$store.commit("updateMusicId", this.musicListDetail.tracks[0].id);
this.$store.commit("updateMusicList", {
musicList: this.musicListDetail.tracks,
musicListId: this.musicListDetail.id,
});
},
handleDOM(current, last) {
if (document.querySelector(".musicListDetail")) {
let tableRows = document
.querySelector(".musicListDetail")
.querySelectorAll(".el-table__row");
// 遍历当前musicList 找到当前播放的index的行进行渲染
// console.log(tableRows);
let index = this.musicListDetail.tracks.findIndex(
(item) => item.id == current
);
// console.log(index);
if (index != -1) {
// 直接修改dom样式的颜色无效 可能是因为第三方组件的原故
// 通过引入全局样式解决
// 将正在播放的音乐前面的索引换成小喇叭
tableRows[index].children[0].querySelector(
".cell"
).innerHTML = `<div><i class="iconfont icon-yinliang"></i></div>`;
tableRows[index].children[0]
.querySelector(".iconfont")
.classList.add("currentRow");
tableRows[index].children[2]
.querySelector(".cell")
.classList.add("currentRow");
}
// 清除上一首的样式
if (last != -1) {
let lastIndex = this.musicListDetail.tracks.findIndex(
(item) => item.id == last
);
if (lastIndex != -1) {
// 将上一个播放的dom的小喇叭换回索引
tableRows[lastIndex].children[0].querySelector(
".cell"
).innerHTML = `<div>${
lastIndex + 1 < 10 ? "0" + (lastIndex + 1) : lastIndex + 1
}</div>`;
// 将上一首的类名删掉 小喇叭的html已经被替换了,不需要再还原
tableRows[lastIndex].children[2]
.querySelector(".cell")
.classList.remove("currentRow");
}
}
}
},
// 点击加载所有音乐的回调
loadMore() {
if (!this.$store.state.isLogin) {
this.$message.error("请先进行登录操作!");
return;
}
// console.log("加载所有音乐");
// this.isMore = false;
let arr = this.musicListDetail.trackIds.slice(
this.musicListDetail.tracks.length
);
if (arr.length > 100) {
arr = arr.slice(0, 100);
}
// console.log(arr.length);
let ids = "";
arr.forEach((item) => {
ids += item.id + ",";
});
ids = ids.substr(0, ids.length - 1);
// console.log(ids);
this.getMusicDetail(ids);
},
async clickCell(row, column, cell) {
// 判断点击的是下载按钮
if (cell.querySelector(".icon-download")) {
// 请求该歌曲的url
console.log(row);
let res = await this.$request("/song/url", { id: row.id });
console.log(res.data.data[0].url);
console.log(res);
if (res.data.data[0].url == null) {
this.$message.warning("暂时无法获取该资源哦!");
return;
}
// 匹配资源的域名
let url = res.data.data[0].url.match(/\http.*?\.net/);
// 匹配域名名称,并匹配对应的代理
let serve = url[0].match(/http:\/(\S*).music/)[1];
if (
serve != "/m7" &&
serve != "/m701" &&
serve != "/m8" &&
serve != "/m801"
) {
// 没有对应的代理
this.$message.error("匹配不到对应的代理,下载失败!");
return;
}
// 截取后面的参数
let params = res.data.data[0].url.slice(url[0].length);
// console.log(url[0], serve, params);
let downloadMusicInfo = {
url: serve + params,
name:
row.name +
" - " +
row.ar[0].name +
"." +
res.data.data[0].type.toLowerCase(),
};
console.log(downloadMusicInfo);
this.$store.commit("updateDownloadMusicInfo", downloadMusicInfo);
}
},
},
computed: {},
watch: {
// "$store.state.currentIndex"(currentIndex, lastIndex) {
// // 目前没什么好思路 直接操作原生DOM
// console.log(currentIndex, lastIndex);
// // this.handleTableDOM(currentIndex, lastIndex);
// },
"$store.state.musicId"(current, last) {
this.handleDOM(current, last);
},
"$store.state.defaultPlay"(current, last) {
console.info('defaultPlay2');
this.playAll();
},
},
filters: {
showDate(value) {
// 1、先将时间戳转成Date对象
const date = new Date(value);
// 2、将date进行格式化
return formatDate(date, "yyyy-MM-dd");
},
handleNum,
},
created() {},
async mounted() {
await this.getMusicListDetail();
this.$nextTick(() => {
// 判断是否和上一次打开的歌单相同
if (this.$route.params.id == this.$store.state.musicListId) {
this.handleDOM(this.$store.state.musicId);
}
});
},
};
</script>
<style scoped>
.musicListDetail {
overflow-y: scroll;
}
.listInfo {
display: flex;
padding: 25px 15px;
align-items: center;
}
.listAvatar {
width: 150px;
height: 150px;
overflow: hidden;
border-radius: 10px;
margin-right: 15px;
position: relative;
}
.listAvatar::after {
content: "";
position: absolute;
height: 100%;
width: 100%;
left: 0;
top: 0;
background: url("../../assets/img/imgLoading.png") no-repeat;
background-size: contain;
z-index: -1;
}
.listAvatar img {
width: 100%;
}
.right {
width: calc(100% - 200px);
}
.title {
display: flex;
align-items: center;
}
.titleTag {
font-size: 12px;
color: #ec4141;
border: 1px solid #ec4141;
padding: 1px 2px;
border-radius: 2px;
margin-right: 5px;
transform: scale(0.8);
}
.titleContent {
font-size: 20px;
font-weight: 600;
color: #373737;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 90%;
}
.user {
display: flex;
align-items: center;
margin-top: 8px;
font-size: 12px;
}
.userAvatar {
height: 25px;
width: 25px;
margin-right: 8px;
}
.userAvatar img {
width: 100%;
border-radius: 50%;
}
.userName {
color: #6191c2;
margin-right: 8px;
cursor: pointer;
}
.createTime {
transform: scale(0.9);
}
.buttons {
margin: 8px 0 0 -5px;
display: flex;
}
.buttonItem {
font-size: 12px;
padding: 8px 15px;
border: 1px solid #ddd;
border-radius: 20px;
transform: scale(0.9);
}
.buttonItem i {
font-size: 12px;
margin-right: 3px;
transform: scale(0.9);
}
.playAll {
background-color: #ec4141;
color: white;
}
.tags {
margin: 8px 0 0 -30px;
display: flex;
font-size: 12px;
transform: scale(0.9);
}
.tagItem {
color: #6191c2;
margin-right: 5px;
}
.otherInfo {
margin: 5px 0 0 -30px;
display: flex;
font-size: 12px;
transform: scale(0.9);
}
.musicNum {
margin-right: 13px;
}
.desc {
margin: 5px 0 0 -30px;
font-size: 12px;
transform: scale(0.9);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.musicList {
margin: -15px 15px 0;
}
.page {
width: 100%;
text-align: center;
padding-bottom: 20px;
}
.placeholder {
width: 100%;
height: 50px;
}
.loadMore {
width: 100%;
height: 50px;
font-size: 12px;
color: #aaa;
text-align: center;
line-height: 50px;
transform: scale(0.9);
}
.red {
color: #ec4141;
}
.commentList /deep/ .el-loading-spinner {
top: 40px;
}
.tips {
font-size: 14px;
margin: 30px 0;
text-align: center;
}
</style>
四、总结
项目功能完整,后续可能将不断升级。
关注作者,及时了解更多好项目!
作者主页也有更多好项目分享!
获取源码或如需帮助,可通过博客后面名片+作者即可!
其他作品集合(主页更多):
- 《uni-app小程序,基于vue实现电商商城》
- 《uni-app基于vue实现商城小程序》
- 《Springboot+Spring Security+OAuth2+redis+mybatis-plus+mysql+vue+elementui实现请假考勤系统》
- 《vue+element实现电商商城礼品代发网,商品、订单管理》
- 《vue+vant2完美实现香奈儿移动端商城网站》
- 《vue+elementui实现联想购物商城,样式美观大方》
- 《vue+elementui实现英雄联盟道具城》
- 《vue+elementui实现app布局小米商城,样式美观大方,功能完整》
- 《vue完美模拟pc版快手,实现短视频,含短视频详情播放》