├── LICENSE ├── README.md ├── img ├── 20220217112332.jpg ├── MV列表页.jpg ├── MV详情页.jpg ├── 右下角播放器.jpg ├── 排行榜.jpg ├── 歌单列表页.jpg ├── 歌单详情页.jpg ├── 歌手列表页.jpg ├── 歌曲详情页.jpg └── 首页.jpg ├── index.html ├── package.json ├── public ├── 1.mp3 ├── 2.mp3 ├── favicon.ico ├── index.html └── ly.html ├── src ├── App.vue ├── apis │ ├── http.js │ └── instance.js ├── assets │ ├── css │ │ └── global.css │ ├── fonts │ │ ├── fonts.css │ │ ├── iconfont.ttf │ │ ├── iconfont.woff │ │ └── iconfont.woff2 │ ├── img │ │ ├── disc.png │ │ ├── disc_bg.png │ │ ├── login_bg2.jpg │ │ ├── logo.jpg │ │ └── stylus.png │ ├── js │ │ ├── common.js │ │ └── index │ │ │ ├── hot_recom.js │ │ │ ├── new_album.js │ │ │ └── new_mv.js │ └── less │ │ ├── main.less │ │ └── reset.less ├── components │ ├── AlbumList.vue │ ├── ArtistItem.vue │ ├── Comments.vue │ ├── Empty.vue │ ├── Header.vue │ ├── Loading.vue │ ├── Login.vue │ ├── Lyrics.vue │ ├── MvList.vue │ ├── PlayBarTmp │ │ ├── AudioBox.vue │ │ ├── Bar.vue │ │ ├── MiniBar.vue │ │ └── PlayBar.vue │ ├── PlayList.vue │ ├── ProgressLine.vue │ ├── Reply.vue │ ├── Search.vue │ ├── Sidebar.vue │ └── SongList.vue ├── main.js ├── router │ └── index.js ├── store │ ├── actions.js │ ├── getters.js │ ├── index.js │ ├── mutation-types.js │ ├── mutations.js │ └── state.js ├── utils │ ├── app.js │ ├── song.js │ └── util.js └── views │ ├── album │ └── Index.vue │ ├── artist │ └── Index.vue │ ├── dj │ └── Index.vue │ ├── index │ ├── ArtistList.vue │ ├── Banner.vue │ ├── DjList.vue │ ├── Index.vue │ └── RankList.vue │ ├── mvlist │ ├── Detail.vue │ └── Index.vue │ ├── playlist │ ├── Detail.vue │ └── Index.vue │ ├── rank │ └── Index.vue │ ├── search │ ├── Index.vue │ └── artist.vue │ ├── singer │ └── Index.vue │ ├── song │ └── Index.vue │ └── user │ └── Index.vue └── vite.config.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 trtst 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue3 网易云音乐 2 | 3 | #### 前言 4 | 5 | 开头还是感谢 [网易云音乐 NodeJS 版 API](https://binaryify.github.io/NeteaseCloudMusicApi/#/)提供的API练练手。 6 | 7 | 目前已完成音乐网站的大部分功能,部分细节有可能需要优化调整,如果你有更好的想法实现或者发现了错误、抑或代码优化建议等等,可以留言,我会回复。你的订阅,就是我的动力。 8 | 9 | 欢迎 **watch** / **star** / **fork** ,一键三连。 :clap: 10 | 11 | **Vue2.x版本** :[基于网易云音乐API实现PC端音乐网站](https://github.com/trtst/vue_pc_music) 12 | 13 | 14 | #### 项目预览 15 | 16 | [trtst_PC_Vue_3.x_网易云音乐](https://m2.trtst.com) 17 | 18 | #### 介绍 19 | 20 | 该项目是基于 Vue 和 ElementUI 组件库 搭建的一个PC端的音乐网站,能登录网易云音乐用户账号,听歌、评论、收藏等功能。 21 | 22 | #### 项目安装 23 | 24 | 25 | ``` 26 | //首先你应当克隆该项目 27 | git clone https://github.com/trtst/vue3-music.git 28 | 29 | // 然后 安装项目依赖 30 | npm install 31 | 32 | // 接下来 部署服务端(二选一) 33 | a) 克隆服务端项目并运行 34 | b)部署服务端项目到Vercel 35 | 36 | // 运行项目 37 | npm run dev 38 | ``` 39 | 40 | 41 | #### 技术栈 42 | 43 | Vue3 / 44 | Vue-router / 45 | Element-UI / 46 | Axios / 47 | Vuex / 48 | Vite 49 | 50 | #### 问题交流群 51 | 欢迎加群一起讨论:233725017 52 | 53 | ![加群:233725017](img/20220217112332.jpg) 54 | 55 | #### 视频展示 56 | 57 | 1、[播放器](https://www.bilibili.com/video/BV173411a7MR) 58 | 59 | 60 | #### 效果图预览展示 61 | 62 | ##### 首页 63 | 64 | ![首页](img/%E9%A6%96%E9%A1%B5.jpg) 65 | 66 | ##### 排行榜 67 | 68 | ![排行榜](img/%E6%8E%92%E8%A1%8C%E6%A6%9C.jpg) 69 | 70 | ##### 歌单列表页 71 | 72 | ![歌单列表页](img/%E6%AD%8C%E5%8D%95%E5%88%97%E8%A1%A8%E9%A1%B5.jpg) 73 | 74 | ##### 歌单详情页 75 | 76 | ![歌单详情页](img/%E6%AD%8C%E5%8D%95%E8%AF%A6%E6%83%85%E9%A1%B5.jpg) 77 | 78 | ##### MV列表页 79 | 80 | ![MV列表页](img/MV%E5%88%97%E8%A1%A8%E9%A1%B5.jpg) 81 | 82 | ##### MV详情页 83 | 84 | ![MV详情页](img/MV%E8%AF%A6%E6%83%85%E9%A1%B5.jpg) 85 | 86 | ##### 歌手列表页 87 | 88 | ![歌手列表页](img/%E6%AD%8C%E6%89%8B%E5%88%97%E8%A1%A8%E9%A1%B5.jpg) 89 | 90 | ##### 歌曲详情页 91 | 92 | ![歌曲详情页](img/%E6%AD%8C%E6%9B%B2%E8%AF%A6%E6%83%85%E9%A1%B5.jpg) 93 | 94 | ##### 右下角播放器 95 | 96 | ![右下角播放器](img/%E5%8F%B3%E4%B8%8B%E8%A7%92%E6%92%AD%E6%94%BE%E5%99%A8.jpg) 97 | 98 | 99 | #### 项目页面功能完成列表 100 | 101 | ✅ 首页 102 | 103 | ✅ 登录/退出登录 104 | 105 | ✅ 排行榜 106 | 107 | ✅ 歌单列表页 108 | 109 | ✅ 歌单详情页 110 | 111 | ✅ MV列表页 112 | 113 | ✅ MV详情页 114 | 115 | ✅ 歌手列表页 116 | 117 | ✅ 专辑详情页 118 | 119 | ✅ 歌曲播放 120 | 121 | ✅ 播放条(有些细节还需要修改优化) 122 | 123 | ⬜️ 评论留言点赞删除等 124 | 125 | ⬜️ 歌手详情页 126 | 127 | ⬜️ 搜索页面 128 | 129 | ⬜️ 歌曲收藏 130 | 131 | ⬜️ 个人主页 132 | 133 | 134 | ...... 135 | -------------------------------------------------------------------------------- /img/20220217112332.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trtst/vue3-music/125626c9f4cad816a06c28e5baebd9d288d25032/img/20220217112332.jpg -------------------------------------------------------------------------------- /img/MV列表页.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trtst/vue3-music/125626c9f4cad816a06c28e5baebd9d288d25032/img/MV列表页.jpg -------------------------------------------------------------------------------- /img/MV详情页.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trtst/vue3-music/125626c9f4cad816a06c28e5baebd9d288d25032/img/MV详情页.jpg -------------------------------------------------------------------------------- /img/右下角播放器.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trtst/vue3-music/125626c9f4cad816a06c28e5baebd9d288d25032/img/右下角播放器.jpg -------------------------------------------------------------------------------- /img/排行榜.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trtst/vue3-music/125626c9f4cad816a06c28e5baebd9d288d25032/img/排行榜.jpg -------------------------------------------------------------------------------- /img/歌单列表页.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trtst/vue3-music/125626c9f4cad816a06c28e5baebd9d288d25032/img/歌单列表页.jpg -------------------------------------------------------------------------------- /img/歌单详情页.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trtst/vue3-music/125626c9f4cad816a06c28e5baebd9d288d25032/img/歌单详情页.jpg -------------------------------------------------------------------------------- /img/歌手列表页.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trtst/vue3-music/125626c9f4cad816a06c28e5baebd9d288d25032/img/歌手列表页.jpg -------------------------------------------------------------------------------- /img/歌曲详情页.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trtst/vue3-music/125626c9f4cad816a06c28e5baebd9d288d25032/img/歌曲详情页.jpg -------------------------------------------------------------------------------- /img/首页.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trtst/vue3-music/125626c9f4cad816a06c28e5baebd9d288d25032/img/首页.jpg -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "v3m", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "dev": "vite", 6 | "build": "vite build", 7 | "preview": "vite preview" 8 | }, 9 | "dependencies": { 10 | "axios": "^0.25.0", 11 | "element-plus": "^1.3.0-beta.5", 12 | "swiper": "^7.4.1", 13 | "vue": "^3.2.25", 14 | "vue-router": "^4.0.12", 15 | "vue3-video-play": "^1.3.1-beta.6", 16 | "vuex": "^4.0.2" 17 | }, 18 | "devDependencies": { 19 | "@vitejs/plugin-vue": "^2.0.0", 20 | "less": "^4.1.2", 21 | "unplugin-auto-import": "^0.5.11", 22 | "unplugin-vue-components": "^0.17.13", 23 | "vite": "^2.7.2" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /public/1.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trtst/vue3-music/125626c9f4cad816a06c28e5baebd9d288d25032/public/1.mp3 -------------------------------------------------------------------------------- /public/2.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trtst/vue3-music/125626c9f4cad816a06c28e5baebd9d288d25032/public/2.mp3 -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trtst/vue3-music/125626c9f4cad816a06c28e5baebd9d288d25032/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/ly.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 可视化音乐播放器 6 | 7 | 8 | 9 | 10 |

11 | 12 | 13 | 14 | 15 | 110 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 14 | 34 | 37 | -------------------------------------------------------------------------------- /src/apis/http.js: -------------------------------------------------------------------------------- 1 | import api from './instance' 2 | // 首页轮播图 3 | const getBanner = () => { return api.get('/banner', {}) } 4 | // 搜索 5 | const search = ({ keywords = '' }) => { return api.get(`/search?keywords=${keywords}`, {}) } 6 | // 搜索类型;默认为 1 即单曲 , 取值意义 : 1: 单曲, 10: 专辑, 100: 歌手, 1000: 歌单, 1002: 用户, 1004: MV, 1006: 歌词, 1009: 电台, 1014: 视频, 1018:综合 7 | const cloudsearch = ({ keywords = '', limit = 30, offset = 0, type = '1' }) => { return api.get(`/cloudsearch?keywords=${keywords}&limit=${limit}&offset=${offset}&type=${type}`, {}) } 8 | // 热门搜索 9 | const serachHot = () => { return api.get('/search/hot', {}) } 10 | // 热门搜索详细 11 | const serachHotDetail = () => { return api.get('/search/hot/detail', {}) } 12 | const serachSuggest = ({ keywords = '' }) => { return api.get(`/search/suggest?keywords=${keywords}`, {}) } 13 | const serachMatch = ({ keywords = '' }) => { return api.get(`/search/multimatch?keywords=${keywords}`, {}) } 14 | // 登录 15 | const login = ({ phone = '', pwd = '', realIP = '43.241.243.255' }) => { return api.post(`/login/cellphone`, {phone, password :pwd, realIP}) } 16 | // 退出登录 17 | const logout = () => { return api.get('/logout', {}) } 18 | // 获取用户详情 19 | const getUserInfo = ({ uid = '' }) => { return api.get(`/user/detail?uid=${uid}`, {}) } 20 | // 音乐是否可用 21 | const checkSong = ({ id = '' }) => { return api.get(`/check/music?id=${id}`, {}) } 22 | 23 | /* ********* 歌单 ********* */ 24 | // 热门歌单分类 25 | const hotList = () => { return api.get('/playlist/hot', {}) } 26 | // 歌单列表 27 | const playList = ({ order = 'hot', cat = '', limit = 50, offset = 0 }) => { return api.get(`/top/playlist?limit=${limit}&order=${order}&cat=${cat}&offset=${offset}`, {}) } 28 | // 推荐歌单 29 | const personalized = (limit = 30) => { return api.get(`/personalized?limit=${limit}`, {}) } 30 | // 精品歌单 31 | const highquality = (limit = 20, before = 0) => { return api.get(`/top/playlist/highquality?limit=${limit}&before=${before}`, {}) } 32 | // 精品歌单标签 33 | const highqualitytag = () => { return api.get('/playlist/highquality/tags', {}) } 34 | // 歌单分类 35 | const catlist = () => { return api.get('/playlist/catlist', {}) } 36 | // 歌单详情 37 | const playlistdetail = ({ id = '', s = 8 }) => { return api.get(`/playlist/detail?id=${id}&s=${s}`, {}) } 38 | // 歌单收藏用户 39 | const playlistSCollect = ({ id = '', limit = 20, offset = 0 }) => { return api.get(`/playlist/subscribers?id=${id}&limit=${limit}&offset=${offset}`, {}) } 40 | // 相关歌单推荐 41 | const playlistRelated = ({ id = '' }) => { return api.get(`/related/playlist?id=${id}`, {}) } 42 | // 歌单评论 43 | const playlistComment = ({ id = '', limit = 20, offset = 0, before = 0 }) => { return api.get(`/comment/playlist?id=${id}&limit=${limit}&offset=${offset}&before=${before}`, {}) } 44 | // 收藏、取消歌单 1:收藏 2取消 45 | const subPlayList = ({ t = 1, id = '' }) => { return api.get(`/playlist/subscribe?t=${t}&id=${id}`, {}) } 46 | // 获取用户歌单 47 | const playlistUser = ({ uid = '', limit = 30, offset = 0 }) => { return api.get(`/user/playlist?uid=${uid}&limit=${limit}&offset=${offset}`, {}) } 48 | // 添加歌曲到歌单 49 | const addPlayList = ({ op = 'add', pid = '', tracks = '' }) => { return api.get(`/playlist/tracks?op=${op}&pid=${pid}&tracks=${tracks}`, {}) } 50 | 51 | /* ********* 歌曲 ********* */ 52 | // 歌曲详情 多个id , 隔开 53 | const songDetail = ({ ids = '', timestamp = 0 }) => { return api.post(`/song/detail?timestamp=${timestamp}`, { ids: ids }) } 54 | // 获取音乐URL 55 | const songUrl = ({ id = '' }) => { return api.get(`/song/url?id=${id}`, {}) } 56 | // 喜欢歌曲 57 | const likeSong = ({ id = '', like = false }) => { return api.get(`/like?id=${id}&like=${like}`, {}) } 58 | // 歌词 59 | const lyrics = ({ id = '' }) => { return api.get(`/lyric?id=${id}`, {}) } 60 | // 获取相似音乐 61 | const simiSong = ({ id = '' }) => { return api.get(`/simi/song?id=${id}`, {}) } 62 | // 包含这首歌的歌单 63 | const simiPlayList = ({ id = '' }) => { return api.get(`/simi/playlist?id=${id}`, {}) } 64 | 65 | /* ********* 歌曲评论 ********* */ 66 | // 歌曲评论 67 | const commentSong = ({ id = '', limit = 20, offset = 0, before = 0, timestamp = 0 }) => { return api.get(`/comment/music?id=${id}&limit=${limit}&offset=${offset}&before=${before}×tamp=${timestamp}`, {}) } 68 | /* 69 | * 发送/删除评论 70 | * t: 0删除 1发送 2回复 71 | * type: 0: 歌曲 1: mv 2: 歌单 3: 专辑 4: 电台 5: 视频 6: 动态 72 | * id: 对应资源id 73 | * content: 发送的内容/对应内容的id 74 | * commentId: 回复的评论id 75 | */ 76 | const comment = ({ t = 1, type = 0, id = '', content = '', commentId = '' }) => { return api.get(`/comment?t=${t}&type=${type}&id=${id}&content=${content}&commentId=${commentId}`, {}) } 77 | /* 78 | * 给评论点赞 79 | * id: 对应资源id 80 | * cid: 评论id 81 | * t: 是否点赞 1: 是 0: 取消 82 | * type: 0: 歌曲 1: mv 2: 歌单 3: 专辑 4: 电台 5: 视频 6: 动态 83 | */ 84 | const commentLike = ({ id = '', cid = '', t = 1, type = 0 }) => { return api.get(`/comment/like?id=${id}&cid=${cid}&t=${t}&type=${type}`, {}) } 85 | 86 | /* ********* 专辑 ********* */ 87 | // 获取专辑内容 88 | const album = ({ id = '' }) => { return api.get(`/album?id=${id}`, {}) } 89 | const albumDynamic = ({ id = '' }) => { return api.get(`/album/detail/dynamic?id=${id}`, {}) } 90 | const albumSub = ({ id = '', t = 1 }) => { return api.get(`/album/sub?id=${id}&t=${t}`, {}) } 91 | // 专辑评论 92 | const albumComment = ({ id = '', limit = 20, offset = 0, before = 0, timestamp = 0 }) => { return api.get(`/comment/album?id=${id}&limit=${limit}&offset=${offset}&before=${before}×tamp=${timestamp}`, {}) } 93 | 94 | /* ********* 歌手 ********* */ 95 | // 歌手介绍 96 | const artistDesc = ({ id = '' }) => { return api.get(`/artist/desc?id=${id}`, {}) } 97 | // 歌手热门歌曲 98 | const artists = ({ id = '' }) => { return api.get(`/artists?id=${id}`, {}) } 99 | // 收藏/取消收藏歌手 100 | const artistSub = ({ id = '', t = '1' }) => { return api.get(`/artist/sub?id=${id}&t=${t}`, {}) } 101 | // 获取歌手专辑 102 | const artistAlbum = ({ id = '', limit = 50, offset = 0 }) => { return api.get(`/artist/album?id=${id}&limit=${limit}&offset=${offset}`, {}) } 103 | // 获取歌手 mv 104 | const artistMv = ({ id = '', limit = 50, offset = 0 }) => { return api.get(`/artist/mv?id=${id}&limit=${limit}&offset=${offset}`, {}) } 105 | // 获取歌手列表 106 | /* 107 | * 给评论点赞 108 | * type: -1:全部; 1:男歌手; 2:女歌手; 3:乐队 109 | * area: -1:全部; 7华语; 96欧美; 8:日本; 16韩国; 0:其他 110 | * initial: 按首字母索引查找参数, 热门传-1, #传0 111 | * limit: 30 112 | * offset: 0 113 | */ 114 | const artistList = ({ type = -1, area = -1, initial = '', limit = 50, offset = 0 }) => { return api.get(`/artist/list?type=${type}&area=${area}&initial=${initial}&limit=${limit}&offset=${offset}`, {}) } 115 | // 收藏的歌手列表 116 | const subArtist = () => { return api.get('/artist/sublist', {}) } 117 | 118 | /* ********* MV ********* */ 119 | // 获取 mv 120 | const mv = ({ area = '', type = '', order = '', limit = 50, offset = 0 }) => { return api.get(`/mv/all?area=${area}&type=${type}&order=${order}&limit=${limit}&offset=${offset}`, {}) } 121 | // 获取 mv详情 122 | const mvDetail = ({ id = '' }) => { return api.get(`/mv/detail?mvid=${id}`, {}) } 123 | // 获取 地址 124 | const mvUrl = ({ id = '', r = 1080 }) => { return api.get(`/mv/url?id=${id}&r=${r}`, {}) } 125 | // 获取mv评论 126 | const commentMv = ({ id = '', limit = 20, offset = 0, before = 0, timestamp = 0 }) => { return api.get(`/comment/mv?id=${id}&limit=${limit}&offset=${offset}&before=${before}×tamp=${timestamp}`, {}) } 127 | // 相似mv 128 | const simiMv = ({ id = '' }) => { return api.get(`/simi/mv?mvid=${id}`, {}) } 129 | 130 | /* ********* 排行榜 ********* */ 131 | // 排行榜 132 | const toplist = () => { return api.get('/toplist', {}) } 133 | // 排行榜歌单列表 134 | const topRankList = ({ id = '', s = 8 }) => { return api.get(`/playlist/detail?id=${id}&s=${s}`, {}) } 135 | // 所有榜单内容摘要 136 | const topListDetail = () => { return api.get('/toplist/detail', {}) } 137 | // 歌单详情 138 | const listDetail = ({ id = '', s = 8 }) => { return api.get(`/playlist/detail?id=${id}&s=${s}`, {}) } 139 | 140 | /* ********* video ********* */ 141 | // 视频播放地址 142 | const videoUrl = ({ id = '', r = 1080 }) => { return api.get(`/video/url?id=${id}&r=${r}`, {}) } 143 | // 获取 video 详情 144 | const videoDetail = ({ id = '' }) => { return api.get(`/video/detail?id=${id}`, {}) } 145 | // 相似video 146 | const simiVideo = ({ id = '' }) => { return api.get(`/related/allvideo?id=${id}`, {}) } 147 | // 获取video评论 148 | const commentVideo = ({ id = '', limit = 20, offset = 0, before = 0, timestamp = 0 }) => { return api.get(`/comment/video?id=${id}&limit=${limit}&offset=${offset}&before=${before}×tamp=${timestamp}`, {}) } 149 | 150 | // 热门话题 151 | const hotTopic = (limit = 20, offset = 0) => { return api.get(`/hot/topic?limit=${limit}&offset=${offset}`, {}) } 152 | // 新碟上架 153 | const topAlbum = ({ limit = 20, offset = 0, area = 'all', type = 'new', year = '', month = '' }) => { return api.get(`/top/album?limit=${limit}&offset=${offset}&area=${area}&type=${type}&year=${year}&month=${month}`, {}) } 154 | // 热门歌手 155 | const topArtists = ({ limit = 30, offset = 0 }) => { return api.get(`/top/artists?limit=${limit}&offset=${offset}`, {}) } 156 | // 最新MV 157 | const getNewMv = ({ limit = 30, area = '' }) => { return api.get(`/mv/first?limit=${limit}&area=${area}`, {}) } 158 | // 热门电台 159 | const getHotDj = ({ limit = 30, offset = 0 }) => { return api.get(`/dj/hot?limit=${limit}&offset=${offset}`, {}) } 160 | 161 | export { 162 | getBanner, 163 | search, 164 | serachHot, 165 | serachHotDetail, 166 | serachSuggest, 167 | serachMatch, 168 | cloudsearch, 169 | login, 170 | logout, 171 | getUserInfo, 172 | checkSong, 173 | hotList, 174 | playList, 175 | catlist, 176 | topRankList, 177 | playlistdetail, 178 | playlistSCollect, 179 | playlistRelated, 180 | playlistComment, 181 | subPlayList, 182 | playlistUser, 183 | addPlayList, 184 | songDetail, 185 | songUrl, 186 | likeSong, 187 | lyrics, 188 | simiSong, 189 | simiPlayList, 190 | commentSong, 191 | comment, 192 | commentLike, 193 | album, 194 | albumSub, 195 | albumDynamic, 196 | albumComment, 197 | artistDesc, 198 | artists, 199 | artistSub, 200 | artistAlbum, 201 | artistMv, 202 | artistList, 203 | mv, 204 | mvDetail, 205 | mvUrl, 206 | commentMv, 207 | simiMv, 208 | personalized, 209 | highquality, 210 | highqualitytag, 211 | videoUrl, 212 | videoDetail, 213 | simiVideo, 214 | commentVideo, 215 | hotTopic, 216 | topAlbum, 217 | toplist, 218 | topListDetail, 219 | listDetail, 220 | getNewMv, 221 | topArtists, 222 | subArtist, 223 | getHotDj 224 | } 225 | -------------------------------------------------------------------------------- /src/apis/instance.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | const instance = axios.create({ 4 | timeout: 1000 * 60, 5 | // `withCredentials` 表示跨域请求时是否需要使用凭证 6 | withCredentials: true, 7 | // `validateStatus` 定义对于给定的HTTP 响应状态码是 resolve 或 reject promise 。 8 | // 如果 `validateStatus` 返回 `true` (或者设置为 `null` 或 `undefined`),promise 将被 resolve; 否则,promise 将被 rejecte 9 | validateStatus: status => { 10 | return status >= 200 && status < 300; // default 11 | }, 12 | baseURL: 'https://api.trtst.com/' 13 | }); 14 | 15 | // 添加请求拦截器 16 | instance.interceptors.request.use(function (config) { 17 | // 在发送请求之前做些什么 18 | return config; 19 | }, function (error) { 20 | // 对请求错误做些什么 21 | return Promise.reject(error); 22 | }); 23 | 24 | // 添加响应拦截器 25 | instance.interceptors.response.use(function (response) { 26 | // 对响应数据做点什么 27 | return response; 28 | }, function (error) { 29 | // 对响应错误做点什么 30 | return Promise.reject(error); 31 | }); 32 | 33 | const ajaxMethod = ['get', 'post'] 34 | const api = {} 35 | ajaxMethod.forEach(method => { 36 | api[method] = function (uri, data, config) { 37 | return new Promise(function (resolve, reject) { 38 | instance[method](uri, data, config) 39 | .then(response => { 40 | resolve(response) 41 | }) 42 | .catch(error => { 43 | reject(error) 44 | }) 45 | }) 46 | } 47 | }); 48 | 49 | export default api; 50 | -------------------------------------------------------------------------------- /src/assets/css/global.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-text-main: #2d2d2d; 3 | --color-text: #909090; 4 | --color-text-height: #ff641e; 5 | } 6 | 7 | * { padding: 0; margin: 0; } 8 | html, body { height: 100%; font-size: 14px;font-family: PingFang SC,Arial,Microsoft YaHei,sans-serif; color: var(--color-text-main); background-color: #fafafa;} 9 | a { text-decoration: none; color: var(--color-text-main);} 10 | *,*:focus,*:hover, input{ outline:none; } 11 | ::-webkit-scrollbar { width: 0px; } 12 | ::-webkit-scrollbar-track { border-radius: 8px; } 13 | ::-webkit-scrollbar-thumb { border-radius: 8px; background: #ccc; } 14 | ::-webkit-scrollbar-thumb:hover { background: #aaa; } 15 | ::-webkit-scrollbar-thumb:active { background: #999; } 16 | 17 | #app, .el-container { width: 100vw; height: 100%; } 18 | 19 | .el-aside { width: 270px;} 20 | 21 | .aside-box { 22 | width: 350px; 23 | padding-bottom: 25px; 24 | flex-shrink: 0; 25 | padding-left: 20px; 26 | } 27 | 28 | @media screen and (max-width: 1500px) { 29 | .aside-box { 30 | width: 300px; 31 | } 32 | } -------------------------------------------------------------------------------- /src/assets/fonts/fonts.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "iconfont"; /* Project id 3155227 */ 3 | src: url('iconfont.woff2?t=1646981610105') format('woff2'), 4 | url('iconfont.woff?t=1646981610105') format('woff'), 5 | url('iconfont.ttf?t=1646981610105') format('truetype'); 6 | } 7 | 8 | .iconfont { 9 | font-family: "iconfont" !important; 10 | font-size: 16px; 11 | font-style: normal; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | color: var(--color-text); 15 | } 16 | 17 | .icon-index:before { 18 | content: "\e6e0"; 19 | } 20 | .icon-rank:before { 21 | content: "\e6df"; 22 | } 23 | .icon-playlist:before { 24 | content: "\e636"; 25 | } 26 | .icon-mvlist:before { 27 | content: "\e6dd"; 28 | } 29 | .icon-artist:before { 30 | content: "\e608"; 31 | } 32 | .icon-my:before { 33 | content: "\e630"; 34 | } 35 | .icon-search:before { 36 | content: "\f0f1"; 37 | } 38 | .icon-set:before { 39 | content: "\e601"; 40 | } 41 | .icon-quit:before { 42 | content: "\e618"; 43 | } 44 | .icon-playnum:before { 45 | content: "\e848"; 46 | } 47 | .icon-play:before { 48 | content: "\e737"; 49 | } 50 | .icon-video-play:before { 51 | content: "\e616"; 52 | } 53 | .icon-pause:before { 54 | content: "\e73b"; 55 | } 56 | .icon-add:before { 57 | content: "\e619"; 58 | } 59 | .icon-fav:before { 60 | content: "\e60a"; 61 | } 62 | .icon-dj:before { 63 | content: "\e7e5"; 64 | } 65 | .icon-placeholder:before { 66 | content: "\e631"; 67 | } 68 | .icon-arrow:before { 69 | content: "\e733"; 70 | } 71 | .icon-closed:before { 72 | content: "\e61a"; 73 | } 74 | .icon-comment:before { 75 | content: "\e63d"; 76 | } 77 | .icon-collect:before { 78 | content: "\e665"; 79 | } 80 | .icon-collect-active:before { 81 | content: "\e6db"; 82 | } 83 | .icon-audio-prev:before { 84 | content: "\e849"; 85 | } 86 | .icon-audio-next:before { 87 | content: "\e6a6"; 88 | } 89 | .icon-audio-pause:before { 90 | content: "\e6a5"; 91 | } 92 | .icon-audio-play:before { 93 | content: "\e6a4"; 94 | } 95 | .icon-playsong:before { 96 | content: "\e6a8"; 97 | } 98 | .icon-volume:before { 99 | content: "\e8b9"; 100 | } 101 | .icon-volume-active:before { 102 | content: "\e8b8"; 103 | } 104 | .icon-lock:before { 105 | content: "\e610"; 106 | } 107 | .icon-shuffle:before { 108 | content: "\e66b"; 109 | } 110 | .icon-loop:before { 111 | content: "\e66c"; 112 | } 113 | .icon-single-cycle:before { 114 | content: "\e66d"; 115 | } 116 | .icon-del:before { 117 | content: "\e68b"; 118 | } 119 | .icon-del:before { 120 | content: "\e68b"; 121 | } 122 | .icon-vip:before { 123 | content: "\e676"; 124 | } 125 | .icon-pip:before { 126 | content: "\e683"; 127 | } 128 | .icon-m:before { 129 | content: "\e600"; 130 | } 131 | .icon-mv:before { 132 | content: "\e615"; 133 | } 134 | .icon-empty:before { 135 | content: "\e632"; 136 | } -------------------------------------------------------------------------------- /src/assets/fonts/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trtst/vue3-music/125626c9f4cad816a06c28e5baebd9d288d25032/src/assets/fonts/iconfont.ttf -------------------------------------------------------------------------------- /src/assets/fonts/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trtst/vue3-music/125626c9f4cad816a06c28e5baebd9d288d25032/src/assets/fonts/iconfont.woff -------------------------------------------------------------------------------- /src/assets/fonts/iconfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trtst/vue3-music/125626c9f4cad816a06c28e5baebd9d288d25032/src/assets/fonts/iconfont.woff2 -------------------------------------------------------------------------------- /src/assets/img/disc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trtst/vue3-music/125626c9f4cad816a06c28e5baebd9d288d25032/src/assets/img/disc.png -------------------------------------------------------------------------------- /src/assets/img/disc_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trtst/vue3-music/125626c9f4cad816a06c28e5baebd9d288d25032/src/assets/img/disc_bg.png -------------------------------------------------------------------------------- /src/assets/img/login_bg2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trtst/vue3-music/125626c9f4cad816a06c28e5baebd9d288d25032/src/assets/img/login_bg2.jpg -------------------------------------------------------------------------------- /src/assets/img/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trtst/vue3-music/125626c9f4cad816a06c28e5baebd9d288d25032/src/assets/img/logo.jpg -------------------------------------------------------------------------------- /src/assets/img/stylus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trtst/vue3-music/125626c9f4cad816a06c28e5baebd9d288d25032/src/assets/img/stylus.png -------------------------------------------------------------------------------- /src/assets/js/common.js: -------------------------------------------------------------------------------- 1 | // 专辑 2 | const ALBUM_AREA = [{ name: '全部', code: 'all' }, { name: '华语', code: 'zh' }, { name: '欧美', code: 'ea' }, { name: '韩国', code: 'kr' }, { name: '日本', code: 'jp' }]; 3 | 4 | // MV 5 | const MV_AREA = ['全部', '内地', '港台', '欧美', '日本', '韩国']; 6 | const MV_TYPE = ['全部', '官方版', '原生', '现场版', '网易出品']; 7 | 8 | // 歌手 9 | const ARTIST_AREA = [{ label: '全部', val: -1 }, { label: '华语', val: 7 }, { label: '欧美', val: 96 }, { label: '日本', val: 8 }, { label: '韩国', val: 16 }, { label: '其他', val: 0 }]; 10 | const ARTIST_TYPE = [{ label: '全部', val: -1 }, { label: '男歌手', val: 1 }, { label: '女歌手', val: 2 }, { label: '乐队', val: 3 }]; 11 | 12 | export default { 13 | ALBUM_AREA, 14 | MV_AREA, 15 | MV_TYPE, 16 | ARTIST_AREA, 17 | ARTIST_TYPE 18 | } -------------------------------------------------------------------------------- /src/assets/js/index/hot_recom.js: -------------------------------------------------------------------------------- 1 | import { getCurrentInstance, reactive, onMounted } from 'vue'; 2 | 3 | export default function hot_recom() { 4 | const { proxy } = getCurrentInstance(); 5 | 6 | // -------------- 推荐歌单 7 | // 热门推荐歌单 8 | const playlist_info = reactive({ 9 | playlist_tags: [], 10 | playlist_list: [], 11 | playlist_index: 0, 12 | playlist_params: { limit: 6, offset: 0 }, 13 | playlist_count: 6, 14 | playlist_loading: true 15 | }); 16 | 17 | // 获取热门推荐歌单标签 18 | const getHotTags = async() => { 19 | const { data: res } = await proxy.$http.hotList() 20 | 21 | if (res.code !== 200) { 22 | return proxy.$msg.error('数据请求失败') 23 | } 24 | 25 | res.tags.unshift({ name: '为您推荐' }) 26 | playlist_info['playlist_tags'] = res.tags.splice(0, 6); 27 | } 28 | // 切换歌单类别 29 | const choosePlayListType = (index) => { 30 | playlist_info['playlist_index'] = index; 31 | playlist_info['playlist_params']['cat'] = index !== 0 ? playlist_info['playlist_tags'][index].name : ''; 32 | playlist_info['playlist_loading'] = true; 33 | getPlayList(playlist_info['playlist_params']); 34 | } 35 | 36 | // 分类歌单列表 37 | const getPlayList = async(params) => { 38 | const { data: res } = await proxy.$http.playList(params) 39 | 40 | if (res.code !== 200) { 41 | return proxy.$msg.error('数据请求失败') 42 | } 43 | 44 | playlist_info['playlist_list'] = res.playlists; 45 | playlist_info['playlist_loading'] = false; 46 | } 47 | 48 | onMounted(() => { 49 | getHotTags(); 50 | getPlayList(playlist_info['playlist_params']); 51 | }); 52 | 53 | return { 54 | playlist_info, 55 | getHotTags, 56 | choosePlayListType 57 | } 58 | } -------------------------------------------------------------------------------- /src/assets/js/index/new_album.js: -------------------------------------------------------------------------------- 1 | import { getCurrentInstance, reactive, onMounted } from 'vue'; 2 | 3 | export default function hot_recom() { 4 | const { proxy } = getCurrentInstance(); 5 | 6 | // -------------- 新碟上架 7 | // 热门卡碟 8 | const album_info = reactive({ 9 | album_area: proxy.$COMMON.ALBUM_AREA, 10 | album_list: [], 11 | album_index: 0, 12 | album_params: { limit: 12 }, 13 | album_count: 12, 14 | album_loading: true 15 | }) 16 | 17 | // 新碟上架 18 | const getAlbumList = async(params) => { 19 | const { data: res } = await proxy.$http.topAlbum(params) 20 | 21 | if (res.code !== 200) { 22 | return proxy.$msg.error('数据请求失败') 23 | } 24 | 25 | album_info['album_list'] = res.monthData.slice(0, album_info.album_count); 26 | album_info['album_loading'] = false; 27 | } 28 | 29 | // 切换卡碟类别 30 | const chooseAlbumType = (index) => { 31 | album_info['album_index'] = index; 32 | album_info['album_params']['area'] = index !== 0 ? album_info['album_area'][index].code : ''; 33 | album_info['album_loading'] = true; 34 | getAlbumList(album_info['album_params']); 35 | } 36 | 37 | 38 | onMounted(() => { 39 | getAlbumList(album_info['album_params']); 40 | }); 41 | 42 | return { 43 | album_info, 44 | chooseAlbumType 45 | } 46 | } -------------------------------------------------------------------------------- /src/assets/js/index/new_mv.js: -------------------------------------------------------------------------------- 1 | import { getCurrentInstance, reactive, onMounted } from 'vue'; 2 | 3 | export default function hot_recom() { 4 | const { proxy } = getCurrentInstance(); 5 | 6 | // MV 7 | const mv_info = reactive({ 8 | mv_area: proxy.$COMMON.MV_AREA, 9 | mv_list: [], 10 | mv_index: 0, 11 | mv_params: { limit: 10 }, 12 | mv_count: 10, 13 | mv_loading: true 14 | }) 15 | 16 | // 最新MV 17 | const getMv = async(params) => { 18 | const { data: res } = await proxy.$http.mv(params) 19 | 20 | if (res.code !== 200) { 21 | return proxy.$msg.error('数据请求失败') 22 | } 23 | 24 | mv_info['mv_list'] = res.data; 25 | mv_info['mv_loading'] = false; 26 | } 27 | 28 | // 切换MV类别 29 | const chooseMvType = (index) => { 30 | mv_info['mv_index'] = index; 31 | mv_info['mv_params']['area'] = index !== 0 ? mv_info['mv_area'][index] : ''; 32 | mv_info['mv_loading'] = true; 33 | getMv(mv_info['mv_params']); 34 | } 35 | 36 | onMounted(() => { 37 | getMv(mv_info['mv_params']); 38 | }); 39 | 40 | return { 41 | mv_info, 42 | chooseMvType, 43 | } 44 | } -------------------------------------------------------------------------------- /src/assets/less/main.less: -------------------------------------------------------------------------------- 1 | @mainWidth: calc(~"100vw - 270px - 20px - 20px"); 2 | 3 | .calcHeight(@w, @ow, @oh) { 4 | height: calc(@w * @oh / @ow); 5 | } -------------------------------------------------------------------------------- /src/assets/less/reset.less: -------------------------------------------------------------------------------- 1 | .el-image { 2 | width: 100%; 3 | height: 100%; 4 | background-color: #f5f7fa; 5 | 6 | .image-slot { 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | width: 100%; 11 | height: 100%; 12 | background: #f5f7fa; 13 | } 14 | 15 | .icon-placeholder { 16 | font-size: 50px; 17 | color: #999; 18 | } 19 | } 20 | 21 | .el-select .el-input.is-focus .el-input__inner { 22 | border-color: var(--color-text-height); 23 | } -------------------------------------------------------------------------------- /src/components/AlbumList.vue: -------------------------------------------------------------------------------- 1 | 38 | 49 | 189 | -------------------------------------------------------------------------------- /src/components/ArtistItem.vue: -------------------------------------------------------------------------------- 1 | 24 | 37 | -------------------------------------------------------------------------------- /src/components/Empty.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 12 | 23 | -------------------------------------------------------------------------------- /src/components/Header.vue: -------------------------------------------------------------------------------- 1 | 22 | 63 | 64 | -------------------------------------------------------------------------------- /src/components/Loading.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 11 | 83 | -------------------------------------------------------------------------------- /src/components/Login.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 89 | 132 | -------------------------------------------------------------------------------- /src/components/Lyrics.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 139 | 195 | -------------------------------------------------------------------------------- /src/components/MvList.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 53 | 191 | -------------------------------------------------------------------------------- /src/components/PlayBarTmp/AudioBox.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 177 | -------------------------------------------------------------------------------- /src/components/PlayBarTmp/MiniBar.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 137 | -------------------------------------------------------------------------------- /src/components/PlayBarTmp/PlayBar.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 93 | 352 | -------------------------------------------------------------------------------- /src/components/PlayList.vue: -------------------------------------------------------------------------------- 1 | 46 | 57 | -------------------------------------------------------------------------------- /src/components/ProgressLine.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 79 | 131 | -------------------------------------------------------------------------------- /src/components/Reply.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 41 | 111 | -------------------------------------------------------------------------------- /src/components/Search.vue: -------------------------------------------------------------------------------- 1 | 56 | 174 | 179 | -------------------------------------------------------------------------------- /src/components/Sidebar.vue: -------------------------------------------------------------------------------- 1 | 21 | 57 | 131 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import app from '@utils/app' 2 | import router from './router/index' 3 | import store from './store/index' 4 | import * as getApi from '@apis/http' 5 | import util from '@utils/util' 6 | import common from '@assets/js/common' 7 | 8 | import '@assets/css/global.css' 9 | import '@assets/less/reset.less' 10 | import '@assets/fonts/fonts.css' 11 | 12 | app.config.globalProperties['$http'] = getApi; 13 | app.config.globalProperties['$utils'] = util; 14 | app.config.globalProperties['$COMMON'] = common; 15 | app.config.globalProperties['$msg'] = ElMessage; 16 | 17 | app.use(router).use(store).mount('#app') 18 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from 'vue-router' 2 | 3 | const routes = [ 4 | { path: '/', redirect: '/index'}, 5 | { path: '/index', name: 'index', component: () => import('@views/index/Index.vue')}, 6 | { path: '/rank', name: 'rank', component: () => import('@views/rank/Index.vue')}, 7 | { path: '/playlist', name: 'playlist', component: () => import('@views/playlist/Index.vue')}, 8 | { path: '/playlist/detail', name: 'playlistdetail', component: () => import('@views/playlist/Detail.vue')}, 9 | { path: '/user', name: 'user', component: () => import('@views/user/Index.vue')}, 10 | { path: '/song', name: 'song', component: () => import('@views/song/Index.vue')}, 11 | { path: '/singer', name: 'singer', component: () => import('@views/singer/Index.vue')}, 12 | { path: '/album', name: 'album', component: () => import('@views/album/Index.vue')}, 13 | { path: '/artist', name: 'artist', component: () => import('@views/artist/Index.vue')}, 14 | { path: '/mvlist', name: 'mvlist', component: () => import('@views/mvlist/Index.vue')}, 15 | { path: '/mvlist/mv', name: 'mv', component: () => import('@views/mvlist/Detail.vue')}, 16 | { path: '/dj', name: 'dj', component: () => import('@views/dj/Index.vue')}, 17 | { path: '/search', name: 'search', component: () => import('@views/search/Index.vue')}, 18 | ] 19 | 20 | const router = createRouter({ 21 | history: createWebHistory(), 22 | routes 23 | }) 24 | 25 | export default router -------------------------------------------------------------------------------- /src/store/actions.js: -------------------------------------------------------------------------------- 1 | import * as types from './mutation-types' 2 | import utils from '@/utils/util' 3 | 4 | // 合并歌曲到播放列表查重 5 | const concatPlayList = (list, playList = []) => { 6 | // filter过滤无版权及vip歌曲 7 | return utils.concatPlayList(list.filter(item => { return !item.license && !item.vip }), playList) 8 | } 9 | // 当前歌曲在播放列表的索引 10 | const findIndex = (list, playList) => { 11 | return playList.findIndex(d => { return d.id === list.id }) 12 | } 13 | export default { 14 | loginSuc (context, val) { 15 | context.commit('setLoginDialog', val) 16 | }, 17 | // 播放歌曲列表里全部歌曲(清空当前播放列表) 18 | playAll ({ commit }, { list }) { 19 | commit(types.SET_PLAYLIST, concatPlayList(list)) 20 | commit(types.SET_PLAYSTATUS, true) 21 | commit(types.SET_PLAYINDEX, 0) 22 | }, 23 | // 播放当前选中的歌曲 24 | selectPlay ({ commit, state }, { list }) { 25 | const playList = concatPlayList(list, state.playList) 26 | 27 | commit(types.SET_PLAYLIST, playList) 28 | commit(types.SET_PLAYSTATUS, true) 29 | commit(types.SET_PLAYINDEX, findIndex(list[0], playList)) 30 | }, 31 | // 添加歌曲到当前播放列表 32 | addList ({ commit, state }, { list }) { 33 | const playList = concatPlayList(list, state.playList) 34 | 35 | commit(types.SET_PLAYLIST, playList) 36 | commit(types.SET_PLAYLISTTIPS, true); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/store/getters.js: -------------------------------------------------------------------------------- 1 | export default { 2 | isLogin (state) { 3 | return state.isLogin || JSON.parse(window.localStorage.getItem('isLogin')); 4 | }, 5 | isPlayed (state) { 6 | return state.isPlayed; 7 | }, 8 | loginDialogVisible (state) { 9 | return state.loginDialogVisible; 10 | }, 11 | userInfo (state) { 12 | return state.userInfo || JSON.parse(window.localStorage.getItem('userInfo') || '{}'); 13 | }, 14 | playList (state) { 15 | return state.playList.length ? state.playList : JSON.parse(window.localStorage.getItem('playList')) || [] 16 | }, 17 | playIndex (state) { 18 | return state.playIndex || JSON.parse(window.localStorage.getItem('playIndex')) || 0 19 | }, 20 | isShowPlayListTips (state) { 21 | return state.isShowPlayListTips 22 | } 23 | } -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore } from 'vuex' 2 | 3 | import state from './state' 4 | import mutations from './mutations' 5 | import getters from './getters' 6 | import actions from './actions' 7 | 8 | const store = createStore({ 9 | state, 10 | mutations, 11 | getters, 12 | actions 13 | }); 14 | 15 | export default store -------------------------------------------------------------------------------- /src/store/mutation-types.js: -------------------------------------------------------------------------------- 1 | // 设置登录状态 2 | export const SET_LOGIN = 'SET_LOGIN' 3 | // 设置播放状态 4 | export const SET_PLAYSTATUS = 'SET_PLAYSTATUS' 5 | // 设置播放索引 6 | export const SET_PLAYLIST = 'SET_PLAYLIST' 7 | // 设置播放列表 8 | export const SET_PLAYINDEX = 'SET_PLAYINDEX' 9 | // 添加歌曲到播放列表时的tips 10 | export const SET_PLAYLISTTIPS = 'SET_PLAYLISTTIPS' -------------------------------------------------------------------------------- /src/store/mutations.js: -------------------------------------------------------------------------------- 1 | import * as types from './mutation-types' 2 | 3 | export default { 4 | [types.SET_LOGIN] (state, val = false) { 5 | state.isLogin = val 6 | }, 7 | setUserInfo (state, val) { 8 | state.userInfo = val 9 | }, 10 | setLoginDialog (state, val) { 11 | state.loginDialogVisible = val 12 | }, 13 | [types.SET_PLAYSTATUS] (state, val = false) { 14 | state.isPlayed = val 15 | }, 16 | [types.SET_PLAYLIST] (state, val = null) { 17 | state.playList = val 18 | window.localStorage.setItem('playList', JSON.stringify(val)) 19 | }, 20 | [types.SET_PLAYINDEX] (state, val = 0) { 21 | state.playIndex = val 22 | window.localStorage.setItem('playIndex', val) 23 | }, 24 | [types.SET_PLAYLISTTIPS] (state, val = false) { 25 | state.isShowPlayListTips = val 26 | }, 27 | } -------------------------------------------------------------------------------- /src/store/state.js: -------------------------------------------------------------------------------- 1 | export default { 2 | isLogin: false, // 是否登录 3 | loginDialogVisible: false, // 登录弹窗显示与隐藏 4 | isPlayed: false, // 当前播放状态 5 | playList: [], // 播放列表 6 | userInfo: null, // 登录用户信息 7 | playIndex: 0, // 当前播放歌曲在播放列表的所有位置 8 | isShowPlayListTips: false, // 添加及播放成功后,播放列表按钮提示的文字 9 | } -------------------------------------------------------------------------------- /src/utils/app.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from '@/App.vue' 3 | 4 | const app = createApp(App) 5 | 6 | export default app -------------------------------------------------------------------------------- /src/utils/song.js: -------------------------------------------------------------------------------- 1 | import utils from '@utils/util' 2 | 3 | export default class Song { 4 | constructor ({ 5 | id, 6 | name, 7 | mvId, 8 | singer, 9 | album, 10 | alia, 11 | duration, 12 | url, 13 | vip, 14 | license, 15 | publishTime 16 | }) { 17 | this.id = id 18 | this.name = name 19 | this.mvId = mvId 20 | this.singer = singer 21 | this.album = album 22 | this.alia = alia 23 | this.duration = duration 24 | this.url = url 25 | this.vip = vip 26 | this.license = license 27 | this.publishTime = publishTime 28 | } 29 | } 30 | 31 | export function formatSongInfo (params) { 32 | return new Song({ 33 | id: String(params.id), 34 | name: params.name, 35 | mvId: params.mv, 36 | singer: params.ar, 37 | album: params.al, 38 | alia: params.alia, 39 | vip: params.fee === 1, 40 | license: params.license, 41 | duration: utils.formatSongTime(params.dt), 42 | url: `https://music.163.com/song/media/outer/url?id=${params.id}.mp3`, 43 | publishTime: utils.formatMsgTime(params.publishTime) 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /src/utils/util.js: -------------------------------------------------------------------------------- 1 | import { formatSongInfo } from '@utils/song'; 2 | 3 | export default { 4 | // 数字过万的处理 5 | formartNum (val) { 6 | let num = 0 7 | if (val > 9999) { 8 | num = Math.round(val / 10000 * 10) / 10 + '万' 9 | } else { 10 | num = val 11 | } 12 | 13 | return num 14 | }, 15 | // 时间毫秒格式化处理 2020-10-30 09:30:00 16 | formartDate (originVal, fmt) { 17 | const dt = new Date(originVal) 18 | const opt = { 19 | yyyy: dt.getFullYear(), 20 | MM: (dt.getMonth() + 1 + '').padStart(2, '0'), 21 | dd: (dt.getDate() + '').padStart(2, '0'), 22 | HH: (dt.getHours() + '').padStart(2, '0'), 23 | mm: (dt.getMinutes() + '').padStart(2, '0'), 24 | ss: (dt.getSeconds() + '').padStart(2, '0') 25 | } 26 | 27 | for (const k in opt) { 28 | const ret = new RegExp('(' + k + ')').exec(fmt) 29 | if (ret) { 30 | fmt = fmt.replace(ret[1], opt[k]) 31 | } 32 | } 33 | 34 | return fmt 35 | }, 36 | // 歌曲转毫秒格式化处理 03:30 => (3*60+30) * 1000 37 | formatSongSecond (duration) { 38 | let arr = duration.split(':'), 39 | second = 0; 40 | 41 | for(let i = 0; i < arr.length; i++) { 42 | second += arr[i] * 60 * (arr.length - 1 - i) 43 | } 44 | 45 | second += arr[arr.length - 1] * 1; 46 | 47 | return second; 48 | }, 49 | // 歌曲毫秒格式化处理 03:30 50 | formatSongTime (duration = 0) { 51 | duration = duration >= 0 ? duration / 1000 : 0; 52 | const m = (Math.floor(duration / 60) + '').padStart(2, '0') 53 | const s = (Math.floor(duration % 60) + '').padStart(2, '0') 54 | return `${m}:${s}` 55 | }, 56 | // 评论时间格式化处理 57 | formatMsgTime (duration) { 58 | let result = '' 59 | const NOW = new Date() 60 | const PAST = new Date(duration) 61 | 62 | // 判断是当天的时间 显示格式 10:30 63 | if (NOW.toDateString() === PAST.toDateString()) { 64 | result = this.formartDate(duration, 'HH:mm') 65 | // 时间为当年 显示月日 时间戳 66 | } else if (PAST.getFullYear() === NOW.getFullYear()) { 67 | result = this.formartDate(duration, 'MM月dd日 HH:mm') 68 | } else { 69 | result = this.formartDate(duration, 'yyyy年MM月dd日') 70 | } 71 | 72 | return result 73 | }, 74 | // 添加歌曲到播放列表,过滤重复的歌曲 75 | concatPlayList (newList = [], oldList = []) { 76 | const arr = [...oldList, ...newList] 77 | const map = new Map() 78 | 79 | for (const item of arr) { 80 | if (!map.has(item.id)) { 81 | map.set(item.id, item) 82 | } 83 | } 84 | 85 | return [...map.values()] 86 | }, 87 | // 处理歌曲 88 | formatSongs (list, privileges) { 89 | const ret = [] 90 | list.map((item, index) => { 91 | if (item.id) { 92 | // 是否有版权播放 93 | item.license = !privileges[index].cp 94 | ret.push(formatSongInfo(item)) 95 | } 96 | }) 97 | return ret 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/views/artist/Index.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 120 | 276 | -------------------------------------------------------------------------------- /src/views/dj/Index.vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/views/index/ArtistList.vue: -------------------------------------------------------------------------------- 1 | 31 | 76 | -------------------------------------------------------------------------------- /src/views/index/Banner.vue: -------------------------------------------------------------------------------- 1 | 2 | 36 | 37 | 106 | 107 | 161 | -------------------------------------------------------------------------------- /src/views/index/DjList.vue: -------------------------------------------------------------------------------- 1 | 38 | 74 | -------------------------------------------------------------------------------- /src/views/index/Index.vue: -------------------------------------------------------------------------------- 1 | 55 | 56 | 103 | -------------------------------------------------------------------------------- /src/views/index/RankList.vue: -------------------------------------------------------------------------------- 1 | 56 | 104 | -------------------------------------------------------------------------------- /src/views/mvlist/Detail.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | 139 | 294 | -------------------------------------------------------------------------------- /src/views/mvlist/Index.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 108 | -------------------------------------------------------------------------------- /src/views/playlist/Index.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 176 | -------------------------------------------------------------------------------- /src/views/search/Index.vue: -------------------------------------------------------------------------------- 1 | 72 | 283 | -------------------------------------------------------------------------------- /src/views/search/artist.vue: -------------------------------------------------------------------------------- 1 | 11 | 34 | -------------------------------------------------------------------------------- /src/views/singer/Index.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 60 | -------------------------------------------------------------------------------- /src/views/user/Index.vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import { resolve } from 'path' 4 | import AutoImport from 'unplugin-auto-import/vite' 5 | import Components from 'unplugin-vue-components/vite' 6 | import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' 7 | 8 | // https://vitejs.dev/config/ 9 | export default defineConfig({ 10 | plugins: [ 11 | vue(), 12 | AutoImport({ 13 | resolvers: [ElementPlusResolver()], 14 | }), 15 | Components({ 16 | resolvers: [ElementPlusResolver()], 17 | }), 18 | ], 19 | css: { 20 | preprocessorOptions: { 21 | less: { 22 | modifyVars: { 23 | hack: `true; @import (reference) "${resolve('src/assets/less/main.less')}";`, 24 | }, 25 | javascriptEnabled: true, 26 | } 27 | } 28 | }, 29 | resolve: { 30 | alias: { 31 | '@': resolve(__dirname, 'src'), 32 | '@components': resolve(__dirname, 'src/components'), 33 | '@apis': resolve(__dirname, 'src/apis'), 34 | '@utils': resolve(__dirname, 'src/utils'), 35 | '@plugins': resolve(__dirname, 'src/plugins'), 36 | '@assets': resolve(__dirname, 'src/assets'), 37 | '@views': resolve(__dirname, 'src/views'), 38 | } 39 | }, 40 | server: { 41 | port: 9999, 42 | host: '0.0.0.0', 43 | https: false 44 | } 45 | }) 46 | --------------------------------------------------------------------------------