• vue+element模仿实现PC端网易云,对接第三方接口


    一、项目预览

    在线预览:点击访问

    其他项目访问:点击访问

     项目使用传统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.歌单详情源码

    1. <template>
    2. <div class="musicListDetail" v-loading="!musicListDetail">
    3. <div class="listInfo">
    4. <div class="listAvatar">
    5. <img :src="musicListDetail?musicListDetail.coverImgUrl:''" alt="" />
    6. div>
    7. <div class="right">
    8. <div class="title">
    9. <div class="titleTag">歌单div>
    10. <div class="titleContent">{{ musicListDetail?musicListDetail.name:'' }}div>
    11. div>
    12. <div class="user">
    13. <div class="userAvatar">
    14. <img :src="musicListDetail&&musicListDetail.creator?musicListDetail.creator.avatarUrl:''" alt="" />
    15. div>
    16. <div
    17. class="userName">
    18. {{ musicListDetail&&musicListDetail.creator?musicListDetail.creator.nickname:'' }}
    19. div>
    20. <div class="createTime">
    21. {{ musicListDetail?musicListDetail.createTime:'' | showDate }}创建
    22. div>
    23. div>
    24. <div class="buttons">
    25. <div class="buttonItem playAll" @click="playAll">
    26. <i class="iconfont icon-bofang playAll">i>
    27. <span>播放全部span>
    28. div>
    29. <div class="buttonItem">
    30. <i class="iconfont icon-xihuan">i>
    31. <span>收藏span>
    32. div>
    33. <div class="buttonItem">
    34. <i class="iconfont icon-zhuanfa">i>
    35. <span>分享span>
    36. div>
    37. div>
    38. <div class="tags">
    39. 标签:
    40. <div
    41. class="tagItem"
    42. v-for="(item, index) in musicListDetail?musicListDetail.tags:[]"
    43. :key="index"
    44. >
    45. {{ item }}
    46. div>
    47. <div v-if="!musicListDetail||!musicListDetail.tags||musicListDetail.tags.length == 0">暂无标签div>
    48. div>
    49. <div class="otherInfo">
    50. <div class="musicNum">
    51. 歌曲 : {{ musicListDetail.trackCount | handleNum }}
    52. div>
    53. <div class="playCount">
    54. 播放 : {{ musicListDetail.playCount | handleNum }}
    55. div>
    56. div>
    57. <div class="desc">
    58. 简介 :
    59. {{
    60. musicListDetail.description
    61. ? musicListDetail.description
    62. : "暂无简介"
    63. }}
    64. div>
    65. div>
    66. div>
    67. <div class="musicList">
    68. <el-tabs value="first">
    69. <el-tab-pane label="歌曲列表" name="first">
    70. <el-table
    71. :data="musicListDetail.tracks"
    72. size="mini"
    73. style="width: 100%"
    74. @row-dblclick="clickRow"
    75. @cell-click="clickCell"
    76. highlight-current-row
    77. stripe
    78. lazy
    79. :row-key="
    80. (row) => {
    81. return row.id;
    82. }
    83. "
    84. v-infinite-scroll="this.$store.state.isLogin ? loadMore : ''"
    85. :infinite-scroll-disabled="scrollLoadDisabled"
    86. :infinite-scroll-distance="1500"
    87. :infinite-scroll-immediate="false"
    88. >
    89. <el-table-column
    90. label=""
    91. width="40"
    92. type="index"
    93. :index="handleIndex"
    94. >
    95. el-table-column>
    96. <el-table-column label="" width="23">
    97. <i class="iconfont icon-download">i>
    98. el-table-column>
    99. <el-table-column prop="name" label="音乐标题" min-width="350">
    100. el-table-column>
    101. <el-table-column prop="ar[0].name" label="歌手" min-width="120">
    102. el-table-column>
    103. <el-table-column prop="al.name" label="专辑" min-width="170">
    104. el-table-column>
    105. <el-table-column prop="dt" label="时长" min-width="100">
    106. el-table-column>
    107. el-table>
    108. <div class="loadMore" v-if="isMore && !this.$store.state.isLogin">
    109. 登陆后查看更多音乐
    110. div>
    111. <div class="placeholder" v-else>div>
    112. el-tab-pane>
    113. el-tabs>
    114. div>
    115. <go-top scrollObj=".musicListDetail">go-top>
    116. div>
    117. template>
    118. <script>
    119. import { formatDate, handleNum, handleMusicTime } from "@/plugins/utils";
    120. import Comment from "@/components/comment/Comment";
    121. import GoTop from "@/components/goTop/GoTop.vue";
    122. import UserListCard from "@/components/userListCard/UserListCard.vue";
    123. export default {
    124. name: "MusicListDetail",
    125. data() {
    126. return {
    127. musicListDetail: {
    128. trackIds:[],
    129. tracks:[]
    130. },
    131. comments: {},
    132. // 当前评论页数
    133. currentCommentPage: 1,
    134. // 是否还有更多音乐
    135. isMore: false,
    136. // 是否禁止滚动加载
    137. scrollLoadDisabled: false,
    138. };
    139. },
    140. components: {
    141. Comment,
    142. GoTop,
    143. UserListCard,
    144. },
    145. methods: {
    146. // 请求
    147. // 根据传来的 id 查询歌单
    148. async getMusicListDetail() {
    149. var timestamp = Date.parse(new Date());
    150. // console.log(this.$route.params.id);
    151. let result = await this.$request("/playlist/detail", {
    152. id: this.$route.params.id,
    153. timestamp,
    154. });
    155. // console.log(result);
    156. this.musicListDetail = result.data.playlist;
    157. // console.log(this.musicListDetail);
    158. // 判断是否还有更多音乐
    159. if (
    160. this.musicListDetail.tracks.length !=
    161. this.musicListDetail.trackIds.length
    162. ) {
    163. this.isMore = true;
    164. }
    165. // 处理播放时间
    166. this.musicListDetail.tracks.forEach((item, index) => {
    167. this.musicListDetail.tracks[index].dt = handleMusicTime(item.dt);
    168. });
    169. },
    170. // 获取歌曲详情
    171. async getMusicDetail(ids) {
    172. if (this.isMore == false) return;
    173. this.scrollLoadDisabled = true;
    174. let res = await this.$request("/song/detail", { ids });
    175. // 处理时间
    176. console.log(res);
    177. res.data.songs.forEach((item, index) => {
    178. res.data.songs[index].dt = handleMusicTime(item.dt);
    179. });
    180. this.musicListDetail.tracks.push(...res.data.songs);
    181. // 判断是否还有更多音乐
    182. if (
    183. this.musicListDetail.tracks.length <
    184. this.musicListDetail.trackIds.length
    185. ) {
    186. this.isMore = true;
    187. this.scrollLoadDisabled = false;
    188. } else {
    189. this.isMore = false;
    190. }
    191. },
    192. // 事件函数
    193. handleIndex(index) {
    194. // console.log(index);
    195. index += 1;
    196. if (index < 10) {
    197. return "0" + index;
    198. } else {
    199. return index;
    200. }
    201. },
    202. // 双击table的row的回调
    203. async clickRow(row) {
    204. console.log(row);
    205. // 将musicId提交到vuex中 供bottomControl查询歌曲url和其它操作
    206. this.$store.commit("updateMusicId", row.id);
    207. // 如果歌单发生变化,则提交歌单到vuex
    208. if (this.musicListDetail.id != this.$store.state.musicListId) {
    209. // 将歌单传到vuex
    210. this.$store.commit("updateMusicList", {
    211. musicList: this.musicListDetail.tracks,
    212. musicListId: this.musicListDetail.id,
    213. });
    214. }
    215. // let result = await this.$request("/song/url", { id: row.id, br: 320000 });
    216. // console.log(result.data.data[0].url);
    217. // this.$store.commit("updateMusicUrl", result.data.data[0].url);
    218. },
    219. // 点击播放全部按钮的回调
    220. playAll() {
    221. this.$store.commit("updateMusicId", this.musicListDetail.tracks[0].id);
    222. this.$store.commit("updateMusicList", {
    223. musicList: this.musicListDetail.tracks,
    224. musicListId: this.musicListDetail.id,
    225. });
    226. },
    227. handleDOM(current, last) {
    228. if (document.querySelector(".musicListDetail")) {
    229. let tableRows = document
    230. .querySelector(".musicListDetail")
    231. .querySelectorAll(".el-table__row");
    232. // 遍历当前musicList 找到当前播放的index的行进行渲染
    233. // console.log(tableRows);
    234. let index = this.musicListDetail.tracks.findIndex(
    235. (item) => item.id == current
    236. );
    237. // console.log(index);
    238. if (index != -1) {
    239. // 直接修改dom样式的颜色无效 可能是因为第三方组件的原故
    240. // 通过引入全局样式解决
    241. // 将正在播放的音乐前面的索引换成小喇叭
    242. tableRows[index].children[0].querySelector(
    243. ".cell"
    244. ).innerHTML = `
      `
      ;
    245. tableRows[index].children[0]
    246. .querySelector(".iconfont")
    247. .classList.add("currentRow");
    248. tableRows[index].children[2]
    249. .querySelector(".cell")
    250. .classList.add("currentRow");
    251. }
    252. // 清除上一首的样式
    253. if (last != -1) {
    254. let lastIndex = this.musicListDetail.tracks.findIndex(
    255. (item) => item.id == last
    256. );
    257. if (lastIndex != -1) {
    258. // 将上一个播放的dom的小喇叭换回索引
    259. tableRows[lastIndex].children[0].querySelector(
    260. ".cell"
    261. ).innerHTML = `
      ${
    262. lastIndex + 1 < 10 ? "0" + (lastIndex + 1) : lastIndex + 1
    263. }
      `;
  • // 将上一首的类名删掉 小喇叭的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>
  • 四、总结

    项目功能完整,后续可能将不断升级。

    关注作者,及时了解更多好项目!

    作者主页也有更多好项目分享!

    获取源码或如需帮助,可通过博客后面名片+作者即可!

     其他作品集合(主页更多):

    1. 《uni-app小程序,基于vue实现电商商城》
    2. 《uni-app基于vue实现商城小程序》
    3. 《Springboot+Spring Security+OAuth2+redis+mybatis-plus+mysql+vue+elementui实现请假考勤系统》
    4. 《vue+element实现电商商城礼品代发网,商品、订单管理》
    5. 《vue+vant2完美实现香奈儿移动端商城网站》
    6. 《vue+elementui实现联想购物商城,样式美观大方》
    7. 《vue+elementui实现英雄联盟道具城》
    8. 《vue+elementui实现app布局小米商城,样式美观大方,功能完整》
    9. 《vue完美模拟pc版快手,实现短视频,含短视频详情播放》
  • 相关阅读:
    智云通CRM:读懂客户的五种“成交信号”,恰到好处地收单?
    【CSS】使用 CSS 实现一个宽高自适应的正方形
    uniapp获取视频第一帧作为封面图
    git rebase -i 详解
    虹科新闻 | 虹科电子与 Mend 正式建立合作伙伴关系
    untitle
    驱动开发 基于gpio子系统来实现对stm32开发板的led亮灭实现,附加定时器实现一秒亮灭(软件:vscode)
    worthington丨worthington用于细胞收获的胰蛋白酶
    Spark -- Spark3.2.2集成Hudi 0.11.1并同步Hive 3.1.3
    设计模式-代理模式-静态代理和动态代理在Java中的使用示例
  • 原文地址:https://blog.csdn.net/lucky_fang/article/details/134438401