├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── babel.config.js ├── package-lock.json ├── package.json ├── public ├── favicon.ico └── index.html ├── release.sh ├── src ├── App.vue ├── api │ ├── album.js │ ├── artist.js │ ├── comment.js │ ├── discovery.js │ ├── index.js │ ├── mv.js │ ├── playlist.js │ ├── search.js │ ├── song-list.js │ ├── song.js │ └── user.js ├── assets │ └── image │ │ ├── play-bar-support.png │ │ └── play-bar.png ├── base │ ├── button.vue │ ├── card.vue │ ├── confirm.vue │ ├── empty.vue │ ├── highlight-text.vue │ ├── icon.vue │ ├── loading.vue │ ├── pagination.vue │ ├── play-icon.vue │ ├── progress-bar.vue │ ├── scroller.vue │ ├── striped-list.vue │ ├── tabs.vue │ ├── title.vue │ ├── toggle.vue │ ├── video-player.vue │ └── volume.vue ├── components │ ├── comment.vue │ ├── comments.vue │ ├── mini-player.vue │ ├── mv-card.vue │ ├── player.vue │ ├── playlist-card.vue │ ├── playlist.vue │ ├── routes-history.vue │ ├── search.vue │ ├── share-reader.vue │ ├── share.vue │ ├── song-card.vue │ ├── song-table.vue │ ├── theme.vue │ ├── top-playlist-card.vue │ ├── user.vue │ └── with-pagination.vue ├── layout │ ├── header.vue │ ├── index.vue │ └── menu.vue ├── main.js ├── page │ ├── discovery │ │ ├── banner.vue │ │ ├── index.vue │ │ ├── new-mvs.vue │ │ ├── new-playlists.vue │ │ └── new-songs.vue │ ├── mv │ │ └── index.vue │ ├── mvs │ │ └── index.vue │ ├── playlist-detail │ │ ├── header.vue │ │ └── index.vue │ ├── playlists │ │ └── index.vue │ ├── search │ │ ├── index.vue │ │ ├── mvs.vue │ │ ├── playlists.vue │ │ └── songs.vue │ └── songs │ │ └── index.vue ├── router.js ├── store │ ├── helper │ │ ├── global.js │ │ ├── music.js │ │ └── user.js │ ├── index.js │ └── modules │ │ ├── global │ │ ├── index.js │ │ ├── mutations.js │ │ └── state.js │ │ ├── music │ │ ├── actions.js │ │ ├── getters.js │ │ ├── index.js │ │ ├── mutations.js │ │ └── state.js │ │ └── user │ │ ├── actions.js │ │ ├── getters.js │ │ ├── index.js │ │ ├── mutations.js │ │ └── state.js ├── style │ ├── app.scss │ ├── element-overwrite.scss │ ├── index.scss │ ├── mixin.scss │ ├── reset.scss │ ├── themes │ │ ├── variables-red.js │ │ ├── variables-white.js │ │ └── variables.js │ └── variables.scss └── utils │ ├── axios.js │ ├── business.js │ ├── common.js │ ├── config.js │ ├── dom.js │ ├── global.js │ ├── index.js │ ├── lrcparse.js │ ├── mixin.js │ └── rem.js └── vue.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | 23 | # custom 24 | music -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🎵 基于 Vue2、Vue-CLI3 的高仿网易云 mac 客户端播放器(PC) Online Music Player 2 | 3 | 音乐播放器虽然烂大街了,但是作为前端没自己撸一个一直是个遗憾。 4 | 5 | 偶然间发现 PC 端 Web 版的网易云音乐做的实在是太简陋了,社区仿 PC 客户端的网易云也不多见。 6 | 7 | 为了弥补这个遗憾,就用 Vue 全家桶模仿 mac 客户端的 UI 实现了一个,欢迎提出意见和 star🌟~ 8 | 9 | 💐[预览地址](https://ssh-music.vercel.app) 10 | 11 | 💐[源码地址](https://github.com/sl1673495/vue-netease-music) 12 | 13 | ### 进度 14 | 15 | - [x] mv 页(3.0 新增) 16 | - [x] cd 页 (2.0 新增) 17 | - [x] 搜索建议 18 | - [x] 搜索详情 19 | - [x] 播放(版权歌曲无法播放) 20 | - [x] 发现页 21 | - [x] 播放列表 22 | - [x] 播放记录 23 | - [x] 全部歌单 24 | - [x] 歌单详情 25 | - [x] 最新音乐 26 | - [x] 主题换肤功能 27 | - [x] 登录(网易云 uid 登录) 28 | 29 | ### 后端接口 30 | 31 | https://binaryify.github.io/NeteaseCloudMusicApi 32 | 33 | ### 技术栈 34 | 35 | - **_Vue_** 全家桶 通过 Vue-CLI3 初始化生成。 36 | - **_ElementUI_** 魔改样式。 37 | - **_better-scroll_** 歌词滚动部分用了黄轶老大的 (贼爽) 38 | - **_CSS Variables_** 主题换肤。 39 | - **_ESNext_** (JavaScript 语言的下一代标准) 40 | - **_Sass_**(CSS 预处理器) 41 | - **_postcss-pxtorem_**(自动处理 rem,妈妈再也不用担心屏幕太大太小了) 42 | - **_workbox-webpack-plugin_** 谷歌开发的利用 Service Worker 实现页面预缓存的插件。 43 | 44 | ### Screenshots 45 | 46 | ![首页](https://user-images.githubusercontent.com/23615778/62509203-da358580-b83c-11e9-97b3-367fb06a8347.png) 47 | 48 | ![歌单列表](https://user-images.githubusercontent.com/23615778/62509204-dace1c00-b83c-11e9-8d3f-0bcb93e3aab7.png) 49 | 50 | ![歌单详情](https://user-images.githubusercontent.com/23615778/62509201-d99cef00-b83c-11e9-8e4b-b122b8b94468.png) 51 | 52 | ![音乐播放](https://user-images.githubusercontent.com/23615778/62509202-da358580-b83c-11e9-98e1-530e5741ff56.png) 53 | 54 | ### 安装与使用 55 | 56 | ``` 57 | npm i 58 | npm run dev 59 | ``` 60 | 61 | ### 友情链接 62 | 63 | [mmPlayer](https://github.com/maomao1996/Vue-mmPlayer) 64 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@vue/app'], 3 | plugins: [ 4 | [ 5 | 'component', 6 | { 7 | libraryName: 'element-ui', 8 | styleLibraryName: 'theme-chalk' 9 | } 10 | ] 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-netease-muisc", 3 | "version": "3.4.2", 4 | "private": true, 5 | "scripts": { 6 | "dev": "npm run serve", 7 | "serve": "vue-cli-service serve", 8 | "build": "vue-cli-service build", 9 | "publish": "npm run build && cd music && now", 10 | "lint": "vue-cli-service lint", 11 | "release-f": "standard-version -f", 12 | "release-major": "standard-version -r major", 13 | "release-minor": "standard-version -r minor", 14 | "release-patch": "standard-version -r patch", 15 | "release": "sh release.sh" 16 | }, 17 | "dependencies": { 18 | "@better-scroll/core": "^2.0.0-alpha.20", 19 | "@better-scroll/mouse-wheel": "^2.0.0-alpha.20", 20 | "@better-scroll/scroll-bar": "^2.0.0-alpha.20", 21 | "axios": "^0.21.1", 22 | "better-scroll": "^1.15.2", 23 | "clipboard": "^2.0.4", 24 | "core-js": "^2.6.5", 25 | "element-ui": "^2.10.1", 26 | "good-storage": "^1.1.0", 27 | "lodash-es": "^4.17.15", 28 | "sass": "^1.56.0", 29 | "vue": "^2.6.10", 30 | "vue-lazyload": "^1.3.3", 31 | "vue-meta": "^2.2.1", 32 | "vue-router": "^3.0.3", 33 | "vuex": "^3.0.1", 34 | "xgplayer": "^2.1.9" 35 | }, 36 | "devDependencies": { 37 | "@vue/cli-plugin-babel": "^3.9.0", 38 | "@vue/cli-plugin-eslint": "^3.9.0", 39 | "@vue/cli-service": "^3.9.0", 40 | "babel-eslint": "^10.0.1", 41 | "babel-plugin-component": "^1.1.1", 42 | "commitizen": "^4.0.3", 43 | "concurrently": "^4.1.2", 44 | "cross-env": "^7.0.3", 45 | "cz-conventional-changelog": "^3.0.2", 46 | "eslint": "^5.16.0", 47 | "eslint-plugin-vue": "^5.0.0", 48 | "now": "^17.0.4", 49 | "postcss-pxtorem": "^4.0.1", 50 | "sass-loader": "^7.1.0", 51 | "standard-changelog": "^2.0.13", 52 | "standard-version": "^8.0.1", 53 | "vue-template-compiler": "^2.6.10", 54 | "workbox-webpack-plugin": "^5.1.3" 55 | }, 56 | "eslintConfig": { 57 | "root": true, 58 | "env": { 59 | "node": true 60 | }, 61 | "extends": [ 62 | "plugin:vue/essential", 63 | "eslint:recommended" 64 | ], 65 | "parserOptions": { 66 | "parser": "babel-eslint" 67 | } 68 | }, 69 | "postcss": { 70 | "plugins": { 71 | "autoprefixer": {}, 72 | "postcss-pxtorem": { 73 | "rootValue": 14, 74 | "propList": [ 75 | "*" 76 | ] 77 | } 78 | } 79 | }, 80 | "browserslist": [ 81 | "> 1%", 82 | "last 2 versions" 83 | ], 84 | "config": { 85 | "commitizen": { 86 | "path": "./node_modules/cz-conventional-changelog" 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sl1673495/vue-netease-music/b288c3b6791f206bab3508295348ecf9a9b33bbf/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 17 | 18 | 19 | 28 | 29 | 30 | 31 |
32 | 33 | <% if ( NODE_ENV === 'production' ) { %> 34 | 35 | 36 | 37 | 38 | <%} %> 39 | 40 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echoCommon() 4 | { 5 | echo -e "\033[33m $1 \033[0m" 6 | } 7 | 8 | echoFail() 9 | { 10 | echo -e "\033[31m $1 \033[0m" 11 | } 12 | 13 | echoSuccess() 14 | { 15 | echo -e "\033[32m $1 \033[0m" 16 | } 17 | 18 | git pull 19 | echoSuccess 'git pull success' 20 | git add . 21 | echoSuccess 'git add success' 22 | echo 23 | git cz 24 | echoSuccess 'commit success' 25 | echo 26 | echoCommon "选择要发布的方式?" 27 | select var in release-major release-minor release-patch; 28 | do 29 | break 30 | done 31 | 32 | npm run $var 33 | 34 | if [ $? -eq 0 ]; then 35 | echoSuccess "release success" 36 | echoCommon "start building" 37 | git push --follow-tags origin master 38 | npm run build 39 | else 40 | echoFail "release failed" 41 | fi 42 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 27 | 28 | 35 | -------------------------------------------------------------------------------- /src/api/album.js: -------------------------------------------------------------------------------- 1 | import { request } from '@/utils' 2 | 3 | export const getAlbum = id => request.get(`/album?id=${id}`) -------------------------------------------------------------------------------- /src/api/artist.js: -------------------------------------------------------------------------------- 1 | import { request } from '@/utils' 2 | 3 | export const getArtists = id => request.get(`/artists?id=${id}`) 4 | -------------------------------------------------------------------------------- /src/api/comment.js: -------------------------------------------------------------------------------- 1 | import { requestWithoutLoading } from '@/utils' 2 | 3 | // 歌曲评论 4 | export const getSongComment = params => 5 | requestWithoutLoading.get(`/comment/music`, { params }) 6 | // 歌单评论 7 | export const getPlaylistComment = params => 8 | requestWithoutLoading.get(`/comment/playlist`, { params }) 9 | // 热门评论 10 | export const getHotComment = params => 11 | requestWithoutLoading.get(`/comment/hot`, { params }) 12 | // mv评论 13 | export const getMvComment = params => 14 | requestWithoutLoading.get('/comment/mv', { params }) 15 | -------------------------------------------------------------------------------- /src/api/discovery.js: -------------------------------------------------------------------------------- 1 | import { request } from '@/utils' 2 | 3 | export const getBanner = () => request.get('/banner?type=0') 4 | 5 | export const getNewSongs = () => request.get('/personalized/newsong') 6 | 7 | export const getPersonalized = params => 8 | request.get(`/personalized`, { params }) 9 | 10 | export const getPersonalizedMv = () => request.get(`/personalized/mv`) -------------------------------------------------------------------------------- /src/api/index.js: -------------------------------------------------------------------------------- 1 | export * from './album' 2 | export * from './comment' 3 | export * from './discovery' 4 | export * from './mv' 5 | export * from './playlist' 6 | export * from './search' 7 | export * from './song-list' 8 | export * from './song' 9 | export * from './user' 10 | export * from './artist' 11 | -------------------------------------------------------------------------------- /src/api/mv.js: -------------------------------------------------------------------------------- 1 | import { request } from '@/utils' 2 | 3 | export const getMvDetail = id => request.get(`/mv/detail?mvid=${id}`) 4 | 5 | export const getMvUrl = id => request.get(`/mv/url?id=${id}`) 6 | 7 | export const getSimiMv = id => request.get(`/simi/mv?mvid=${id}`) 8 | 9 | export const getAllMvs = (params) => request.get(`/mv/all`, {params}) -------------------------------------------------------------------------------- /src/api/playlist.js: -------------------------------------------------------------------------------- 1 | import { request, requestWithoutLoading } from '@/utils' 2 | 3 | // 获取歌单 4 | export const getPlaylists = (params) => request.get('/top/playlist', { params }) 5 | // 精品歌单 6 | export const getTopPlaylists = (params) => request.get('/top/playlist/highquality', { params }) 7 | // 获取相似歌单 8 | export const getSimiPlaylists = (id, option) => requestWithoutLoading.get(`/simi/playlist?id=${id}`, option) -------------------------------------------------------------------------------- /src/api/search.js: -------------------------------------------------------------------------------- 1 | import { request } from "@/utils"; 2 | 3 | export const getSearchHot = () => request.get('/search/hot') 4 | 5 | export const getSearchSuggest = (keywords) => request.get('/search/suggest', { params: { keywords } }) 6 | 7 | export const getSearch = (params) => request.get(`/search`, { params }) -------------------------------------------------------------------------------- /src/api/song-list.js: -------------------------------------------------------------------------------- 1 | import { request } from '@/utils' 2 | 3 | export const getListDetail = params => 4 | request.get('/playlist/detail', { params }) 5 | -------------------------------------------------------------------------------- /src/api/song.js: -------------------------------------------------------------------------------- 1 | import { request, requestWithoutLoading } from '@/utils' 2 | 3 | // 获取音乐url 4 | export const getSongUrl = id => request.get(`/song/url?id=${id}`) 5 | 6 | // 获取音乐详情 7 | export const getSongDetail = ids => request.get(`/song/detail?ids=${ids}`) 8 | 9 | // 新歌速递 10 | export const getTopSongs = type => request.get(`/top/song?type=${type}`) 11 | 12 | // 相似音乐 13 | export const getSimiSongs = (id, option) => 14 | requestWithoutLoading.get(`/simi/song?id=${id}`, option) 15 | 16 | // 歌词 17 | export const getLyric = id => request.get(`/lyric?id=${id}`) 18 | -------------------------------------------------------------------------------- /src/api/user.js: -------------------------------------------------------------------------------- 1 | import { requestWithoutLoading } from '@/utils' 2 | 3 | export const getUserDetail = (uid) => requestWithoutLoading.get("/user/detail", { params: { uid } }) 4 | 5 | const PLAYLIST_LIMIT = 1000 6 | export const getUserPlaylist = (uid) => requestWithoutLoading.get("/user/playlist", { params: { uid, limit: PLAYLIST_LIMIT } }) -------------------------------------------------------------------------------- /src/assets/image/play-bar-support.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sl1673495/vue-netease-music/b288c3b6791f206bab3508295348ecf9a9b33bbf/src/assets/image/play-bar-support.png -------------------------------------------------------------------------------- /src/assets/image/play-bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sl1673495/vue-netease-music/b288c3b6791f206bab3508295348ecf9a9b33bbf/src/assets/image/play-bar.png -------------------------------------------------------------------------------- /src/base/button.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 26 | 27 | 41 | -------------------------------------------------------------------------------- /src/base/card.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 32 | 33 | 73 | -------------------------------------------------------------------------------- /src/base/confirm.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 76 | 77 | 91 | -------------------------------------------------------------------------------- /src/base/empty.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 20 | -------------------------------------------------------------------------------- /src/base/highlight-text.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 40 | -------------------------------------------------------------------------------- /src/base/icon.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 71 | 72 | 101 | -------------------------------------------------------------------------------- /src/base/loading.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 18 | 19 | 24 | -------------------------------------------------------------------------------- /src/base/pagination.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 22 | 23 | 28 | -------------------------------------------------------------------------------- /src/base/play-icon.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 34 | 35 | 46 | -------------------------------------------------------------------------------- /src/base/progress-bar.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 95 | 96 | 145 | -------------------------------------------------------------------------------- /src/base/scroller.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 65 | 66 | 78 | -------------------------------------------------------------------------------- /src/base/striped-list.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 42 | 43 | -------------------------------------------------------------------------------- /src/base/tabs.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 142 | 143 | 225 | -------------------------------------------------------------------------------- /src/base/title.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /src/base/toggle.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 61 | 62 | 64 | -------------------------------------------------------------------------------- /src/base/video-player.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 51 | 52 | -------------------------------------------------------------------------------- /src/base/volume.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 65 | 66 | 82 | -------------------------------------------------------------------------------- /src/components/comment.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 35 | 36 | 94 | -------------------------------------------------------------------------------- /src/components/comments.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 151 | 152 | 171 | -------------------------------------------------------------------------------- /src/components/mini-player.vue: -------------------------------------------------------------------------------- 1 | // 底部播放栏组件 2 | 103 | 104 | 281 | 282 | 427 | -------------------------------------------------------------------------------- /src/components/mv-card.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 35 | 36 | 107 | -------------------------------------------------------------------------------- /src/components/player.vue: -------------------------------------------------------------------------------- 1 | 151 | 152 | 385 | 386 | 616 | -------------------------------------------------------------------------------- /src/components/playlist-card.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 24 | 25 | 94 | -------------------------------------------------------------------------------- /src/components/playlist.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 90 | 91 | 142 | -------------------------------------------------------------------------------- /src/components/routes-history.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 30 | 31 | 41 | -------------------------------------------------------------------------------- /src/components/search.vue: -------------------------------------------------------------------------------- 1 | 89 | 90 | 217 | 218 | 284 | -------------------------------------------------------------------------------- /src/components/share-reader.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/share.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 33 | -------------------------------------------------------------------------------- /src/components/song-card.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 22 | 23 | 76 | -------------------------------------------------------------------------------- /src/components/song-table.vue: -------------------------------------------------------------------------------- 1 | 231 | 232 | 293 | -------------------------------------------------------------------------------- /src/components/theme.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 90 | 91 | 110 | -------------------------------------------------------------------------------- /src/components/top-playlist-card.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 30 | 31 | 91 | -------------------------------------------------------------------------------- /src/components/user.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 109 | 110 | 163 | -------------------------------------------------------------------------------- /src/components/with-pagination.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 78 | 79 | 84 | -------------------------------------------------------------------------------- /src/layout/header.vue: -------------------------------------------------------------------------------- 1 | 60 | 61 | 95 | 96 | 186 | -------------------------------------------------------------------------------- /src/layout/index.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 45 | 46 | 73 | -------------------------------------------------------------------------------- /src/layout/menu.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 63 | 64 | 120 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | import App from './App.vue' 4 | import router from './router' 5 | 6 | import '@/style/index.scss' 7 | import '@/utils/rem' 8 | import '@/utils/axios' 9 | import store from './store/index' 10 | import global from './utils/global' 11 | 12 | Vue.config.productionTip = false 13 | Vue.use(global) 14 | 15 | new Vue({ 16 | router, 17 | store, 18 | render: h => h(App), 19 | }).$mount('#app') 20 | -------------------------------------------------------------------------------- /src/page/discovery/banner.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 26 | 27 | 40 | -------------------------------------------------------------------------------- /src/page/discovery/index.vue: -------------------------------------------------------------------------------- 1 | // 发现音乐页面 2 | 12 | 13 | 23 | 24 | 29 | -------------------------------------------------------------------------------- /src/page/discovery/new-mvs.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 46 | 47 | 52 | -------------------------------------------------------------------------------- /src/page/discovery/new-playlists.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 37 | 38 | -------------------------------------------------------------------------------- /src/page/discovery/new-songs.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 92 | 93 | -------------------------------------------------------------------------------- /src/page/mv/index.vue: -------------------------------------------------------------------------------- 1 | // mv详情页面 2 | 62 | 63 | 132 | 133 | 212 | -------------------------------------------------------------------------------- /src/page/mvs/index.vue: -------------------------------------------------------------------------------- 1 | 60 | 61 | 108 | 109 | 127 | -------------------------------------------------------------------------------- /src/page/playlist-detail/header.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 63 | 64 | 152 | -------------------------------------------------------------------------------- /src/page/playlist-detail/index.vue: -------------------------------------------------------------------------------- 1 | // 歌单详情页面 2 | 34 | 35 | 132 | 133 | 171 | -------------------------------------------------------------------------------- /src/page/playlists/index.vue: -------------------------------------------------------------------------------- 1 | // 推荐歌单页面 2 | 46 | 47 | 123 | 124 | 144 | -------------------------------------------------------------------------------- /src/page/search/index.vue: -------------------------------------------------------------------------------- 1 | // 搜索详情页面 2 | 24 | 25 | 72 | 73 | 102 | -------------------------------------------------------------------------------- /src/page/search/mvs.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 65 | 66 | 75 | -------------------------------------------------------------------------------- /src/page/search/playlists.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 59 | 60 | 72 | -------------------------------------------------------------------------------- /src/page/search/songs.vue: -------------------------------------------------------------------------------- 1 | // 搜索详情页面 2 | 23 | 24 | 88 | 89 | 106 | -------------------------------------------------------------------------------- /src/page/songs/index.vue: -------------------------------------------------------------------------------- 1 | // 最新音乐页面 2 | 19 | 20 | 71 | 72 | 81 | -------------------------------------------------------------------------------- /src/router.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | 4 | const Discovery = () => import(/* webpackChunkName: "Discovery" */ '@/page/discovery') 5 | const PlaylistDetail = () => import(/* webpackChunkName: "PlaylistDetail" */ '@/page/playlist-detail') 6 | const Playlists = () => import(/* webpackChunkName: "Playlists" */ '@/page/playlists') 7 | const Songs = () => import(/* webpackChunkName: "Songs" */ '@/page/songs') 8 | const Search = () => import(/* webpackChunkName: "Search" */ '@/page/search') 9 | const SearchSongs = () => import(/* webpackChunkName: "SearchSongs" */ '@/page/search/songs') 10 | const SearchPlaylists = () => import(/* webpackChunkName: "SearchPlaylists" */ '@/page/search/playlists') 11 | const SearchMvs = () => import(/* webpackChunkName: "SearchMvs" */ '@/page/search/mvs') 12 | 13 | const Mvs = () => import(/* webpackChunkName: "Mvs" */ '@/page/mvs') 14 | const Mv = () => import(/* webpackChunkName: "Mv" */ '@/page/mv') 15 | 16 | // 内容需要居中的页面 17 | export const layoutCenterNames = ['discovery', 'playlists', 'songs', 'mvs'] 18 | 19 | // 需要显示在侧边栏菜单的页面 20 | export const menuRoutes = [ 21 | { 22 | path: '/discovery', 23 | name: 'discovery', 24 | component: Discovery, 25 | meta: { 26 | title: '发现音乐', 27 | icon: 'music', 28 | }, 29 | }, 30 | { 31 | path: '/playlists', 32 | name: 'playlists', 33 | component: Playlists, 34 | meta: { 35 | title: '推荐歌单', 36 | icon: 'playlist-menu', 37 | }, 38 | }, 39 | { 40 | path: '/songs', 41 | name: 'songs', 42 | component: Songs, 43 | meta: { 44 | title: '最新音乐', 45 | icon: 'yinyue', 46 | }, 47 | }, 48 | { 49 | path: '/mvs', 50 | name: 'mvs', 51 | component: Mvs, 52 | meta: { 53 | title: '最新MV', 54 | icon: 'mv', 55 | }, 56 | }, 57 | ] 58 | 59 | Vue.use(Router) 60 | 61 | export default new Router({ 62 | mode: 'hash', 63 | routes: [ 64 | { 65 | path: '/', 66 | redirect: '/discovery', 67 | }, 68 | { 69 | path: '/playlist/:id', 70 | name: 'playlist', 71 | component: PlaylistDetail, 72 | }, 73 | { 74 | path: '/search/:keywords', 75 | name: 'search', 76 | component: Search, 77 | props: true, 78 | children: [ 79 | { 80 | path: '/', 81 | redirect: 'songs', 82 | }, 83 | { 84 | path: 'songs', 85 | name: 'searchSongs', 86 | component: SearchSongs, 87 | }, 88 | { 89 | path: 'playlists', 90 | name: 'searchPlaylists', 91 | component: SearchPlaylists, 92 | }, 93 | { 94 | path: 'mvs', 95 | name: 'searchMvs', 96 | component: SearchMvs, 97 | }, 98 | ], 99 | }, 100 | { 101 | path: '/mv/:id', 102 | name: 'mv', 103 | component: Mv, 104 | props: (route) => ({id: +route.params.id}), 105 | }, 106 | ...menuRoutes, 107 | ], 108 | }) 109 | -------------------------------------------------------------------------------- /src/store/helper/global.js: -------------------------------------------------------------------------------- 1 | import { createNamespacedHelpers } from 'vuex' 2 | 3 | export const { mapState, mapMutations, mapGetters, mapActions } = createNamespacedHelpers('global') -------------------------------------------------------------------------------- /src/store/helper/music.js: -------------------------------------------------------------------------------- 1 | import { createNamespacedHelpers } from 'vuex' 2 | 3 | export const { mapState, mapMutations, mapGetters, mapActions } = createNamespacedHelpers('music') -------------------------------------------------------------------------------- /src/store/helper/user.js: -------------------------------------------------------------------------------- 1 | import { createNamespacedHelpers } from 'vuex' 2 | 3 | export const { mapState, mapMutations, mapGetters, mapActions } = createNamespacedHelpers('user') -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import musicModule from './modules/music' 4 | import userModule from './modules/user' 5 | import globalModule from './modules/global' 6 | import createLogger from 'vuex/dist/logger' 7 | 8 | Vue.use(Vuex) 9 | const debug = process.env.NODE_ENV !== 'production' 10 | 11 | export default new Vuex.Store({ 12 | modules: { 13 | music: musicModule, 14 | user: userModule, 15 | global: globalModule, 16 | }, 17 | plugins: debug ? [createLogger()] : [] 18 | }) 19 | -------------------------------------------------------------------------------- /src/store/modules/global/index.js: -------------------------------------------------------------------------------- 1 | import state from './state' 2 | import mutations from './mutations' 3 | 4 | export default { 5 | namespaced: true, 6 | state, 7 | mutations, 8 | } -------------------------------------------------------------------------------- /src/store/modules/global/mutations.js: -------------------------------------------------------------------------------- 1 | export default { 2 | setAxiosLoading(state, loading) { 3 | state.axiosLoading = loading 4 | }, 5 | } 6 | -------------------------------------------------------------------------------- /src/store/modules/global/state.js: -------------------------------------------------------------------------------- 1 | export default { 2 | axiosLoading: false, 3 | } 4 | -------------------------------------------------------------------------------- /src/store/modules/music/actions.js: -------------------------------------------------------------------------------- 1 | import storage from 'good-storage' 2 | import { PLAY_HISTORY_KEY, getSongImg } from '@/utils' 3 | 4 | export default { 5 | // 整合歌曲信息 并且开始播放 6 | async startSong({ commit, state }, rawSong) { 7 | // 浅拷贝一份 改变引用 8 | // 1.不污染元数据 9 | // 2.单曲循环为了触发watch 10 | const song = Object.assign({}, rawSong) 11 | if (!song.img) { 12 | if (song.albumId) { 13 | song.img = await getSongImg(song.id, song.albumId) 14 | } 15 | } 16 | commit('setCurrentSong', song) 17 | commit('setPlayingState', true) 18 | // 历史记录 19 | const { playHistory } = state 20 | const playHistoryCopy = playHistory.slice() 21 | const findedIndex = playHistoryCopy.findIndex(({ id }) => song.id === id) 22 | if (findedIndex !== -1) { 23 | // 删除旧那一项, 插入到最前面 24 | playHistoryCopy.splice(findedIndex, 1) 25 | } 26 | playHistoryCopy.unshift(song) 27 | commit('setPlayHistory', playHistoryCopy) 28 | storage.set(PLAY_HISTORY_KEY, playHistoryCopy) 29 | }, 30 | clearCurrentSong({ commit }) { 31 | commit('setCurrentSong', {}) 32 | commit('setPlayingState', false) 33 | commit('setCurrentTime', 0) 34 | }, 35 | clearPlaylist({ commit, dispatch }) { 36 | commit('setPlaylist', []) 37 | dispatch('clearCurrentSong') 38 | }, 39 | clearHistory({ commit }) { 40 | const history = [] 41 | commit('setPlayHistory', history) 42 | storage.set(PLAY_HISTORY_KEY, history) 43 | }, 44 | addToPlaylist({ commit, state }, song) { 45 | const { playlist } = state 46 | const copy = playlist.slice() 47 | if (!copy.find(({ id }) => id === song.id)) { 48 | copy.unshift(song) 49 | commit('setPlaylist', copy) 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /src/store/modules/music/getters.js: -------------------------------------------------------------------------------- 1 | import { isDef, playModeMap } from '@/utils' 2 | 3 | export const currentIndex = (state) => { 4 | const { currentSong, playlist } = state 5 | return playlist.findIndex(({ id }) => id === currentSong.id) 6 | } 7 | 8 | export const nextSong = (state, getters) => { 9 | const { playlist, playMode } = state 10 | const nextStratMap = { 11 | [playModeMap.sequence.code]: getSequenceNextIndex, 12 | [playModeMap.loop.code]: getLoopNextIndex, 13 | [playModeMap.random.code]: getRandomNextIndex 14 | } 15 | const getNextStrat = nextStratMap[playMode] 16 | const index = getNextStrat() 17 | 18 | return playlist[index] 19 | 20 | // 顺序 21 | function getSequenceNextIndex() { 22 | let nextIndex = getters.currentIndex + 1 23 | if (nextIndex > playlist.length - 1) { 24 | nextIndex = 0 25 | } 26 | return nextIndex 27 | } 28 | 29 | // 随机 30 | function getRandomNextIndex() { 31 | return getRandomIndex(playlist, getters.currentIndex) 32 | } 33 | 34 | // 单曲 35 | function getLoopNextIndex() { 36 | return getters.currentIndex 37 | } 38 | } 39 | 40 | // 上一首歌 41 | export const prevSong = (state, getters) => { 42 | const { playlist, playMode } = state 43 | const prevStratMap = { 44 | [playModeMap.sequence.code]: genSequencePrevIndex, 45 | [playModeMap.loop.code]: getLoopPrevIndex, 46 | [playModeMap.random.code]: getRandomPrevIndex 47 | } 48 | const getPrevStrat = prevStratMap[playMode] 49 | const index = getPrevStrat() 50 | 51 | return playlist[index] 52 | 53 | function genSequencePrevIndex() { 54 | let prevIndex = getters.currentIndex - 1 55 | if (prevIndex < 0) { 56 | prevIndex = playlist.length - 1 57 | } 58 | return prevIndex 59 | } 60 | 61 | function getLoopPrevIndex() { 62 | return getters.currentIndex 63 | } 64 | 65 | function getRandomPrevIndex() { 66 | return getRandomIndex(playlist, getters.currentIndex) 67 | } 68 | } 69 | 70 | // 当前是否有歌曲在播放 71 | export const hasCurrentSong = (state) => { 72 | return isDef(state.currentSong.id) 73 | } 74 | 75 | function getRandomIndex(playlist, currentIndex) { 76 | // 防止无限循环 77 | if (playlist.length === 1) { 78 | return currentIndex 79 | } 80 | let index = Math.round(Math.random() * (playlist.length - 1)) 81 | if (index === currentIndex) { 82 | index = getRandomIndex(playlist, currentIndex) 83 | } 84 | return index 85 | } -------------------------------------------------------------------------------- /src/store/modules/music/index.js: -------------------------------------------------------------------------------- 1 | import state from './state' 2 | import * as getters from './getters' 3 | import mutations from './mutations' 4 | import actions from './actions' 5 | 6 | export default { 7 | namespaced: true, 8 | state, 9 | getters, 10 | mutations, 11 | actions, 12 | } -------------------------------------------------------------------------------- /src/store/modules/music/mutations.js: -------------------------------------------------------------------------------- 1 | import { shallowEqual } from '@/utils' 2 | export default { 3 | setCurrentSong(state, song) { 4 | state.currentSong = song 5 | }, 6 | setCurrentTime(state, time) { 7 | state.currentTime = time 8 | }, 9 | setPlayingState(state, playing) { 10 | state.playing = playing 11 | }, 12 | setPlayMode(state, mode) { 13 | state.playMode = mode 14 | }, 15 | setPlaylistShow(state, show) { 16 | state.isPlaylistShow = show 17 | }, 18 | setPlayerShow(state, show) { 19 | state.isPlayerShow = show 20 | }, 21 | setPlaylistPromptShow(state, show) { 22 | state.isPlaylistPromptShow = show 23 | }, 24 | setPlaylist(state, playlist) { 25 | const { isPlaylistShow, playlist: oldPlaylist } = state 26 | state.playlist = playlist 27 | // 播放列表未显示 并且两次播放列表的不一样 则弹出提示 28 | if (!isPlaylistShow && !shallowEqual(oldPlaylist, playlist, 'id')) { 29 | state.isPlaylistPromptShow = true 30 | setTimeout(() => { 31 | state.isPlaylistPromptShow = false 32 | }, 2000) 33 | } 34 | }, 35 | setPlayHistory(state, history) { 36 | state.playHistory = history 37 | }, 38 | setMenuShow(state, show) { 39 | state.isMenuShow = show 40 | }, 41 | } 42 | -------------------------------------------------------------------------------- /src/store/modules/music/state.js: -------------------------------------------------------------------------------- 1 | import storage from 'good-storage' 2 | import { PLAY_HISTORY_KEY } from '@/utils' 3 | import { playModeMap } from '@/utils/config' 4 | 5 | export default { 6 | // 当前播放歌曲 7 | currentSong: {}, 8 | // 当前播放时长 9 | currentTime: 0, 10 | // 播放状态 11 | playing: false, 12 | // 播放模式 13 | playMode: playModeMap.sequence.code, 14 | // 播放列表显示 15 | isPlaylistShow: false, 16 | // 播放提示显示 17 | isPlaylistPromptShow: false, 18 | // 歌曲详情页显示 19 | isPlayerShow: false, 20 | // 播放列表数据 21 | playlist: [], 22 | // 播放历史数据 23 | playHistory: storage.get(PLAY_HISTORY_KEY, []), 24 | // 菜单显示 25 | isMenuShow: true, 26 | } 27 | -------------------------------------------------------------------------------- /src/store/modules/user/actions.js: -------------------------------------------------------------------------------- 1 | import storage from 'good-storage' 2 | import { UID_KEY } from '@/utils' 3 | import { notify, isDef } from '@/utils' 4 | import { getUserDetail, getUserPlaylist } from "@/api" 5 | 6 | export default { 7 | async login({ commit }, uid) { 8 | const error = () => { 9 | notify.error('登录失败,请输入正确的uid。') 10 | return false 11 | } 12 | 13 | if (!isDef(uid)) { 14 | return error() 15 | } 16 | 17 | try { 18 | const user = await getUserDetail(uid) 19 | const { profile } = user 20 | commit('setUser', profile) 21 | storage.set(UID_KEY, profile.userId) 22 | } catch (e) { 23 | return error() 24 | } 25 | 26 | const { playlist } = await getUserPlaylist(uid) 27 | commit('setUserPlaylist', playlist) 28 | return true 29 | }, 30 | logout({ commit }) { 31 | commit('setUser', {}) 32 | commit('setUserPlaylist', []) 33 | storage.set(UID_KEY, null) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/store/modules/user/getters.js: -------------------------------------------------------------------------------- 1 | import { isDef } from "@/utils"; 2 | 3 | export const isLogin = (state) => isDef(state.user.userId) 4 | 5 | // 根据用户请求的数据整合出菜单 6 | export const userMenus = (state) => { 7 | const {user,userPlaylist } = state 8 | const retMenus = [] 9 | const userCreateList = [] 10 | const userCollectList = [] 11 | 12 | userPlaylist.forEach(playlist => { 13 | const { userId } = playlist 14 | if (user.userId === userId) { 15 | userCreateList.push(playlist) 16 | } else { 17 | userCollectList.push(playlist) 18 | } 19 | }) 20 | 21 | const genPlaylistChildren = playlist => 22 | playlist.map(({ id, name }) => ({ 23 | path: `/playlist/${id}`, 24 | meta: { 25 | title: name, 26 | icon: "playlist-menu" 27 | }, 28 | })) 29 | 30 | if (userCreateList.length) { 31 | retMenus.push({ 32 | type: "playlist", 33 | title: "创建的歌单", 34 | children: genPlaylistChildren(userCreateList) 35 | }) 36 | } 37 | 38 | if (userCollectList.length) { 39 | retMenus.push({ 40 | type: "playlist", 41 | title: "收藏的歌单", 42 | children: genPlaylistChildren(userCollectList) 43 | }) 44 | } 45 | 46 | return retMenus 47 | } -------------------------------------------------------------------------------- /src/store/modules/user/index.js: -------------------------------------------------------------------------------- 1 | import state from './state' 2 | import * as getters from './getters' 3 | import mutations from './mutations' 4 | import actions from './actions' 5 | 6 | export default { 7 | namespaced: true, 8 | state, 9 | getters, 10 | mutations, 11 | actions, 12 | } -------------------------------------------------------------------------------- /src/store/modules/user/mutations.js: -------------------------------------------------------------------------------- 1 | export default { 2 | setUser(state, user) { 3 | state.user = user 4 | }, 5 | setUserPlaylist(state, playlist) { 6 | state.userPlaylist = playlist 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/store/modules/user/state.js: -------------------------------------------------------------------------------- 1 | export default { 2 | // 登录用户 3 | user: {}, 4 | // 登录用户歌单 5 | userPlaylist: [] 6 | } 7 | -------------------------------------------------------------------------------- /src/style/app.scss: -------------------------------------------------------------------------------- 1 | /** 2 | ** 应用级别的一些样式 3 | **/ 4 | body { 5 | color: var(--font-color); 6 | } 7 | 8 | ::selection { 9 | background-color: $theme-color; 10 | color: white; 11 | } 12 | 13 | // 滚动条 14 | ::-webkit-scrollbar-track { 15 | background-color: var(--menu-bgcolor); 16 | } 17 | 18 | ::-webkit-scrollbar { 19 | width: 5px; 20 | height: 5px; 21 | background-color: #f5f5f5; 22 | } 23 | 24 | ::-webkit-scrollbar-thumb { 25 | background-color: var(--scrollbar-color); 26 | } 27 | 28 | .slide-enter-active, 29 | .slide-leave-active { 30 | transition: all 0.5s; 31 | transform: none; 32 | } 33 | .slide-enter, 34 | .slide-leave-to { 35 | transform: translateY(100%); 36 | } 37 | 38 | .fade-enter-active, 39 | .fade-leave-active { 40 | transition: all 0.5s; 41 | opacity: 1; 42 | } 43 | .fade-enter, 44 | .fade-leave-to { 45 | opacity: 0; 46 | } 47 | 48 | // 转化为rem 49 | .iconfont { 50 | font-size: 16px; 51 | } 52 | 53 | a { 54 | color: $theme-color; 55 | 56 | &:hover, 57 | &:focus, 58 | &:visited { 59 | color: $theme-color; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/style/element-overwrite.scss: -------------------------------------------------------------------------------- 1 | // table 2 | .el-table th, 3 | .el-table td { 4 | padding: 4px !important; 5 | font-size: $font-size-sm !important; 6 | } 7 | .el-table::before { 8 | height: 0 !important; 9 | } 10 | .el-table--enable-row-hover .el-table__body tr:hover > td { 11 | background-color: var(--playlist-hover-bgcolor) !important; 12 | } 13 | // 空数据 14 | .el-table__empty-block { 15 | background: var(--body-bgcolor); 16 | color: var(--font-color); 17 | } 18 | .el-table__header-wrapper th { 19 | color: var(--font-color); 20 | } 21 | .el-table { 22 | background-color: var(--body-bgcolor) !important; 23 | } 24 | 25 | // 表格单元格的通用样式 26 | @mixin el-td-style($color) { 27 | td, 28 | th, 29 | tr { 30 | background-color: $color !important; 31 | transition: background-color 0s !important; 32 | border-bottom: none !important; 33 | 34 | .cell { 35 | white-space: nowrap !important; 36 | } 37 | } 38 | } 39 | .el-table, 40 | .el-table { 41 | @include el-td-style(var(--body-bgcolor)); 42 | 43 | tr.el-table__row--striped { 44 | @include el-td-style(var(--stripe-bg)); 45 | } 46 | } 47 | // 允许外部在某个类下面覆写table样式 48 | @mixin el-table-theme($color, $stripe-color: var(--stripe-bg)) { 49 | ::v-deep .el-table { 50 | @include el-td-style($color); 51 | 52 | tr.el-table__row--striped { 53 | @include el-td-style(#{$stripe-color}); 54 | } 55 | } 56 | } 57 | // carosel 58 | .el-carousel--horizontal { 59 | overflow: hidden; 60 | } 61 | 62 | // popover 63 | @each $direction in 'bottom' 'top' 'left' 'right' { 64 | .el-popper[x-placement^='#{$direction}'] .popper__arrow, 65 | .el-popper[x-placement^='#{$direction}'] .popper__arrow::after { 66 | border-#{$direction}-color: var(--prompt-bg-color) !important; 67 | } 68 | } 69 | .el-popover { 70 | background: var(--prompt-bg-color) !important; 71 | border: none !important; 72 | text-align: left; 73 | @include box-shadow; 74 | } 75 | 76 | // input 77 | $input-height: 24px; 78 | @mixin el-input-style($color, $bg-color, $placeholder-color) { 79 | .el-input__inner { 80 | height: $input-height !important; 81 | line-height: $input-height !important; 82 | background: #{$bg-color} !important; 83 | border: none !important; 84 | color: #{$color} !important; 85 | 86 | &:hover { 87 | border: none !important; 88 | } 89 | } 90 | .el-input__prefix { 91 | i { 92 | line-height: $input-height + 1px !important; 93 | color: #{$color} !important; 94 | transition: none !important; 95 | } 96 | } 97 | 98 | input::-webkit-input-placeholder { 99 | color: #{$placeholder-color} !important; 100 | } 101 | } 102 | 103 | // 外部覆写input-theme样式 104 | @mixin el-input-theme($color, $bg-color, $placeholder-color: $color) { 105 | ::v-deep .el-input { 106 | @include el-input-style($color, $bg-color, $placeholder-color); 107 | } 108 | } 109 | 110 | .el-input { 111 | @include el-input-style( 112 | var(--input-color), 113 | var(--input-bgcolor), 114 | var(--font-color-grey-shallow) 115 | ); 116 | } 117 | 118 | // pagination 119 | .el-pagination, 120 | .el-pagination button, 121 | .el-pager li { 122 | background: inherit !important; 123 | color: var(--font-color) !important; 124 | 125 | .active { 126 | color: $theme-color !important; 127 | } 128 | } 129 | 130 | // dialog 131 | .el-dialog { 132 | background: var(--modal-bg-color) !important; 133 | @include box-shadow; 134 | 135 | .el-dialog__body { 136 | color: var(--font-color) !important; 137 | } 138 | 139 | // 右上角图标 140 | .el-dialog__headerbtn:focus .el-dialog__close, 141 | .el-dialog__headerbtn:hover .el-dialog__close { 142 | color: $theme-color; 143 | } 144 | } 145 | 146 | // button 147 | .el-button--primary { 148 | background: $theme-color !important; 149 | border-color: $theme-color !important; 150 | } 151 | 152 | // loading 153 | .el-loading-spinner { 154 | circle { 155 | stroke: $theme-color !important; 156 | } 157 | .el-loading-text { 158 | color: $theme-color !important; 159 | } 160 | .el-icon-loading { 161 | color: $theme-color !important; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/style/index.scss: -------------------------------------------------------------------------------- 1 | @import "./element-overwrite.scss"; 2 | @import "./reset.scss"; 3 | @import "./app.scss"; 4 | -------------------------------------------------------------------------------- /src/style/mixin.scss: -------------------------------------------------------------------------------- 1 | @mixin text-ellipsis() { 2 | overflow: hidden; 3 | text-overflow: ellipsis; 4 | white-space: nowrap; 5 | } 6 | 7 | @mixin text-ellipsis-multi($line) { 8 | display: -webkit-box; 9 | overflow: hidden; 10 | text-overflow: ellipsis; 11 | -webkit-line-clamp: $line; 12 | -webkit-box-orient: vertical; 13 | } 14 | 15 | @mixin flex-center($direction: row) { 16 | display: flex; 17 | flex-direction: $direction; 18 | align-items: center; 19 | justify-content: center; 20 | } 21 | 22 | @mixin box-shadow { 23 | box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.2); 24 | } 25 | 26 | @mixin img-wrap($width, $height: $width) { 27 | width: $width; 28 | height: $height; 29 | flex-shrink: 0; 30 | 31 | img { 32 | width: 100%; 33 | height: 100%; 34 | } 35 | } 36 | 37 | @mixin abs-stretch { 38 | position: absolute; 39 | left: 0; 40 | bottom: 0; 41 | top: 0; 42 | right: 0; 43 | } 44 | 45 | @mixin abs-center { 46 | position: absolute; 47 | left: 50%; 48 | top: 50%; 49 | transform: translate(-50%, -50%); 50 | } 51 | 52 | @mixin round($d) { 53 | width: $d; 54 | height: $d; 55 | border-radius: 50%; 56 | } 57 | 58 | @mixin list($item-width) { 59 | .list-wrap { 60 | display: flex; 61 | flex-wrap: wrap; 62 | margin: 0 -12px; 63 | 64 | .list-item { 65 | width: $item-width; 66 | margin-bottom: 36px; 67 | padding: 0 12px; 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /src/style/reset.scss: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | height: 100%; 4 | overflow: hidden; 5 | font-weight: 400; 6 | font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial, 7 | sans-serif; 8 | } 9 | 10 | html, 11 | body, 12 | div, 13 | span, 14 | object, 15 | iframe, 16 | h1, 17 | h2, 18 | h3, 19 | h4, 20 | h5, 21 | h6, 22 | p, 23 | blockquote, 24 | pre, 25 | abbr, 26 | address, 27 | cite, 28 | code, 29 | del, 30 | dfn, 31 | em, 32 | img, 33 | ins, 34 | kbd, 35 | q, 36 | samp, 37 | small, 38 | strong, 39 | sub, 40 | sup, 41 | var, 42 | b, 43 | i, 44 | dl, 45 | dt, 46 | dd, 47 | ol, 48 | ul, 49 | li, 50 | fieldset, 51 | form, 52 | label, 53 | legend, 54 | table, 55 | caption, 56 | tbody, 57 | tfoot, 58 | thead, 59 | tr, 60 | th, 61 | td, 62 | article, 63 | aside, 64 | canvas, 65 | details, 66 | figcaption, 67 | figure, 68 | footer, 69 | header, 70 | hgroup, 71 | menu, 72 | nav, 73 | section, 74 | summary, 75 | time, 76 | mark, 77 | audio, 78 | video { 79 | margin: 0; 80 | padding: 0; 81 | border: 0; 82 | outline: 0; 83 | font-size: 100%; 84 | vertical-align: baseline; 85 | background: transparent; 86 | list-style: none; 87 | box-sizing: border-box; 88 | } 89 | 90 | body { 91 | line-height: 1.2; 92 | } 93 | 94 | :focus { 95 | outline: 1; 96 | } 97 | 98 | article, 99 | aside, 100 | canvas, 101 | details, 102 | figcaption, 103 | figure, 104 | footer, 105 | header, 106 | hgroup, 107 | menu, 108 | nav, 109 | section, 110 | summary { 111 | display: block; 112 | } 113 | 114 | nav ul { 115 | list-style: none; 116 | } 117 | 118 | blockquote, 119 | q { 120 | quotes: none; 121 | } 122 | 123 | blockquote:before, 124 | blockquote:after, 125 | q:before, 126 | q:after { 127 | content: ""; 128 | content: none; 129 | } 130 | 131 | a { 132 | margin: 0; 133 | padding: 0; 134 | border: 0; 135 | font-size: 100%; 136 | vertical-align: baseline; 137 | background: transparent; 138 | } 139 | 140 | ins { 141 | background-color: #ff9; 142 | color: #000; 143 | text-decoration: none; 144 | } 145 | 146 | mark { 147 | background-color: #ff9; 148 | color: #000; 149 | font-style: italic; 150 | font-weight: bold; 151 | } 152 | 153 | del { 154 | text-decoration: line-through; 155 | } 156 | 157 | abbr[title], 158 | dfn[title] { 159 | border-bottom: 1px dotted #000; 160 | cursor: help; 161 | } 162 | 163 | table { 164 | border-collapse: collapse; 165 | border-spacing: 0; 166 | } 167 | 168 | hr { 169 | display: block; 170 | height: 1px; 171 | border: 0; 172 | border-top: 1px solid #cccccc; 173 | margin: 1em 0; 174 | padding: 0; 175 | } 176 | 177 | input, 178 | select { 179 | vertical-align: middle; 180 | } 181 | 182 | img[src=""], 183 | img:not([src]) { 184 | opacity: 0; 185 | } 186 | -------------------------------------------------------------------------------- /src/style/themes/variables-red.js: -------------------------------------------------------------------------------- 1 | import variablesWhite from './variables-white' 2 | 3 | export default { 4 | ...variablesWhite, 5 | ['--header-bgcolor']: '#D74D45', 6 | ['--header-font-color']: '#EFB6B2', 7 | ['--header-input-color']: '#EFB6B2', 8 | ['--header-input-bgcolor']: '#DD6861', 9 | ['--header-input-placeholder-color']: '#EFB6B2', 10 | ['--round-hover-bgcolor']: '#CA4841', 11 | } -------------------------------------------------------------------------------- /src/style/themes/variables-white.js: -------------------------------------------------------------------------------- 1 | export default { 2 | ['--body-bgcolor']: '#fff', 3 | ['--light-bgcolor']: '#f5f5f5', 4 | 5 | ['--font-color']: '#4a4a4a', 6 | ['--font-color-shallow']: '#404040', 7 | ['--font-color-white']: '#333333', 8 | ['--font-color-grey']: '#5c5c5c', 9 | ['--font-color-grey2']: '#909090', 10 | ['--font-color-grey-shallow']: '#BEBEBE', 11 | ['--border']: '#f2f2f1', 12 | ['--scrollbar-color']: '#D0D0D0', 13 | ['--round-hover-bgcolor']: '#EBEBEB', 14 | ['--stripe-bg']: '#FAFAFA', 15 | ['--shallow-theme-bgcolor']: '#fdf6f5', 16 | ['--shallow-theme-bgcolor-hover']: '#FBEDEC', 17 | 18 | //header 19 | ['--header-bgcolor']: '#F9F9F9', 20 | ['--header-font-color']: '#4a4a4a', 21 | ['--header-input-color']: '#4a4a4a', 22 | ['--header-input-bgcolor']: '#EDEDED', 23 | ['--header-input-placeholder-color']: '#BEBEBE', 24 | 25 | // menu 26 | ['--menu-bgcolor']: '#ededed', 27 | ['--menu-item-hover-bg']: '#e7e7e7', 28 | ['--menu-item-active-bg']: '#e2e2e2', 29 | 30 | //player 31 | ['--player-bgcolor']: '#F9F9F9', 32 | 33 | //playlist 34 | ['--playlist-bgcolor']: '#fff', 35 | ['--playlist-hover-bgcolor']: '#EFEFEF', 36 | 37 | //search 38 | ['--search-bgcolor']: '#fff', 39 | //progress 40 | ['--progress-bgcolor']: '#F5F5F5', 41 | 42 | //input 43 | ['--input-color']: '#4a4a4a', 44 | ['--input-bgcolor']: '#EDEDED', 45 | 46 | //button 47 | ['--button-border-color']: '#D9D9D9', 48 | ['--button-hover-bgcolor']: '#F5F5F5', 49 | 50 | //tab 51 | ['--tab-item-color']: '#7F7F81', 52 | ['--tab-item-hover-color']: '#343434', 53 | ['--tab-item-active-color']: '#000', 54 | 55 | //modal 56 | ['--modal-bg-color']: '#F9F9F9', 57 | // prompt 58 | ['--prompt-bg-color']: '#fff', 59 | //song-detail 60 | ['--song-shallow-grey-bg']: '#E6E5E6', 61 | } 62 | -------------------------------------------------------------------------------- /src/style/themes/variables.js: -------------------------------------------------------------------------------- 1 | export default { 2 | ['--body-bgcolor']: '#252525', 3 | ['--light-bgcolor']: '#2e2e2e', 4 | 5 | ['--font-color']: '#b1b1b1', 6 | ['--font-color-shallow']: '#6f6f6f', 7 | ['--font-color-white']: '#dcdde4', 8 | ['--font-color-grey']: '#5C5C5C', 9 | ['--font-color-grey2']: '#808080', 10 | ['--font-color-grey-shallow']: '#727272', 11 | ['--border']: '#3F3F3F', 12 | ['--scrollbar-color']: '#3a3a3a', 13 | ['--round-hover-bgcolor']: '#373737', 14 | ['--stripe-bg']: '#323232', 15 | ['--shallow-theme-bgcolor']: '#2D2625', 16 | ['--shallow-theme-bgcolor-hover']: '#352726', 17 | 18 | //header 19 | ['--header-bgcolor']: '#252525', 20 | ['--header-font-color']: '#b1b1b1', 21 | ['--header-input-color']: '#b1b1b1', 22 | ['--header-input-bgcolor']: '#4B4B4B', 23 | ['--header-input-placeholder-color']: '#727272', 24 | 25 | //menu 26 | ['--menu-bgcolor']: '#202020', 27 | ['--menu-item-hover-bg']: '#1d1d1d', 28 | ['--menu-item-active-bg']: '#1b1b1b', 29 | 30 | //player 31 | ['--player-bgcolor']: '#252525', 32 | 33 | //playlist 34 | ['--playlist-bgcolor']: '#363636', 35 | ['--playlist-hover-bgcolor']: '#2e2e2e', 36 | 37 | //search 38 | ['--search-bgcolor']: '#363636', 39 | 40 | //progress 41 | ['--progress-bgcolor']: '#232323', 42 | 43 | //input 44 | ['--input-color']: '#b1b1b1', 45 | ['--input-bgcolor']: '#4B4B4B', 46 | 47 | //button 48 | ['--button-border-color']: '#454545', 49 | ['--button-hover-bgcolor']: '#3E3E3E', 50 | //tab 51 | ['--tab-item-color']: '#797979', 52 | ['--tab-item-hover-color']: '#B4B4B4', 53 | ['--tab-item-active-color']: '#fff', 54 | //modal 55 | ['--modal-bg-color']: '#202020', 56 | // prompt 57 | ['--prompt-bg-color']: '#363636', 58 | //song-detail 59 | ['--song-shallow-grey-bg']: '#2A2A2A', 60 | } -------------------------------------------------------------------------------- /src/style/variables.scss: -------------------------------------------------------------------------------- 1 | $theme-color: #d33a31; 2 | $black: #000; 3 | $white: #fff; 4 | $gold: #e7aa5a; 5 | $blue: #517eaf; 6 | 7 | $font-size: 14px; 8 | $font-size-medium-sm: 13px; 9 | $font-size-lg: 16px; 10 | $font-size-sm: 12px; 11 | $font-size-xs: 10px; 12 | $font-size-medium: 15px; 13 | $font-size-title: 18px; 14 | $font-size-title-lg: 24px; 15 | 16 | $font-weight-bold: 700; 17 | 18 | $font-color-transparent: rgba(255, 255, 255, 0.5); 19 | 20 | $border: 1px solid #3f3f3f; 21 | 22 | $page-padding: 16px 32px; 23 | // layout 24 | $layout-content-min-width: 700px; 25 | // content 26 | $center-content-max-width: 1000px; 27 | // header 28 | $header-height: 50px; 29 | 30 | // mini-player 31 | $mini-player-height: 60px; 32 | $mini-player-z-index: 1002; 33 | 34 | //playlist 35 | $playlist-z-index: 1001; 36 | 37 | //search 38 | $search-panel-z-index: 1001; 39 | 40 | //song-detail 41 | $song-detail-z-index: 1000; 42 | -------------------------------------------------------------------------------- /src/utils/axios.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { Loading } from 'element-ui' 3 | import { confirm } from '@/base/confirm' 4 | import store from '@/store' 5 | 6 | const BASE_URL = 'https://mu-api.yuk0.com/' 7 | // 不带全局loading的请求实例 8 | export const requestWithoutLoading = createBaseInstance() 9 | // 带全局loading的请求实例 10 | // 传入函数是因为需要在处理请求结果handleResponse之前处理loading 11 | // 所以要在内部插入loading拦截器的处理逻辑 12 | export const request = createBaseInstance() 13 | mixinLoading(request.interceptors) 14 | // 通用的axios实例 15 | function createBaseInstance() { 16 | const instance = axios.create({ 17 | baseURL: BASE_URL, 18 | }) 19 | 20 | instance.interceptors.response.use(handleResponse, handleError) 21 | return instance 22 | } 23 | 24 | function handleError(e) { 25 | confirm(e.message, '出错啦~') 26 | throw e 27 | } 28 | 29 | function handleResponse(response) { 30 | return response.data 31 | } 32 | 33 | let loading 34 | let loadingCount = 0 35 | const SET_AXIOS_LOADING = 'global/setAxiosLoading' 36 | function mixinLoading(interceptors) { 37 | interceptors.request.use(loadingRequestInterceptor) 38 | interceptors.response.use( 39 | loadingResponseInterceptor, 40 | loadingResponseErrorInterceptor 41 | ) 42 | 43 | function loadingRequestInterceptor(config) { 44 | if (!loading) { 45 | loading = Loading.service({ 46 | target: 'body', 47 | background: 'transparent', 48 | text: '载入中', 49 | }) 50 | store.commit(SET_AXIOS_LOADING, true) 51 | } 52 | loadingCount++ 53 | 54 | return config 55 | } 56 | 57 | function handleResponseLoading() { 58 | loadingCount-- 59 | if (loadingCount === 0) { 60 | loading.close() 61 | loading = null 62 | store.commit(SET_AXIOS_LOADING, false) 63 | } 64 | } 65 | 66 | function loadingResponseInterceptor(response) { 67 | handleResponseLoading() 68 | return response 69 | } 70 | 71 | function loadingResponseErrorInterceptor(e) { 72 | handleResponseLoading() 73 | throw e 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/utils/business.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 业务工具方法 3 | */ 4 | import { getAlbum, getMvDetail } from "@/api" 5 | import router from '@/router' 6 | import { isDef, notify } from './common'; 7 | 8 | export function createSong(song) { 9 | const { id, name, img, artists, duration, albumId, albumName,mvId, ...rest } = song 10 | 11 | return { 12 | id, 13 | name, 14 | img, 15 | artists, 16 | duration, 17 | albumName, 18 | url: genSongPlayUrl(song.id), 19 | artistsText: genArtistisText(artists), 20 | durationSecond: duration / 1000, 21 | // 专辑 如果需要额外请求封面的话必须加上 22 | albumId, 23 | // mv的id 如果有的话 会在songTable组件中加上mv链接。 24 | mvId, 25 | ...rest 26 | } 27 | } 28 | 29 | export async function getSongImg(id, albumId) { 30 | if (!isDef(albumId)) { 31 | throw new Error('need albumId') 32 | } 33 | const { songs } = await getAlbum(albumId) 34 | const { 35 | al: { picUrl } 36 | } = songs.find(({ id: songId }) => songId === id) || {} 37 | return picUrl 38 | } 39 | 40 | export function genArtistisText(artists) { 41 | return (artists || []).map(({ name }) => name).join('/') 42 | } 43 | 44 | // 有时候虽然有mvId 但是请求却404 所以跳转前先请求一把 45 | export async function goMvWithCheck(id) { 46 | try { 47 | await getMvDetail(id) 48 | goMv(id) 49 | } catch (error) { 50 | notify("mv获取失败") 51 | } 52 | } 53 | 54 | export function goMv(id) { 55 | router.push(`/mv/${id}`) 56 | } 57 | 58 | function genSongPlayUrl(id) { 59 | return `https://music.163.com/song/media/outer/url?id=${id}.mp3` 60 | } -------------------------------------------------------------------------------- /src/utils/common.js: -------------------------------------------------------------------------------- 1 | import { Notification } from 'element-ui' 2 | 3 | export { debounce, throttle } from 'lodash-es' 4 | 5 | export function pad(num, n = 2) { 6 | let len = num.toString().length 7 | while (len < n) { 8 | num = '0' + num 9 | len++ 10 | } 11 | return num 12 | } 13 | 14 | export function formatDate(date, fmt = 'yyyy-MM-dd hh:mm:ss') { 15 | date = date instanceof Date ? date : new Date(date) 16 | if (/(y+)/.test(fmt)) { 17 | fmt = fmt.replace( 18 | RegExp.$1, 19 | (date.getFullYear() + '').substr(4 - RegExp.$1.length) 20 | ) 21 | } 22 | let o = { 23 | 'M+': date.getMonth() + 1, 24 | 'd+': date.getDate(), 25 | 'h+': date.getHours(), 26 | 'm+': date.getMinutes(), 27 | 's+': date.getSeconds() 28 | } 29 | for (let k in o) { 30 | if (new RegExp(`(${k})`).test(fmt)) { 31 | let str = o[k] + '' 32 | fmt = fmt.replace( 33 | RegExp.$1, 34 | RegExp.$1.length === 1 ? str : padLeftZero(str) 35 | ) 36 | } 37 | } 38 | return fmt 39 | } 40 | 41 | function padLeftZero(str) { 42 | return ('00' + str).substr(str.length) 43 | } 44 | 45 | export function formatTime(interval) { 46 | interval = interval | 0 47 | const minute = pad((interval / 60) | 0) 48 | const second = pad(interval % 60) 49 | return `${minute}:${second}` 50 | } 51 | 52 | export function formatNumber(number) { 53 | number = Number(number) || 0 54 | return number > 100000 ? `${Math.round(number / 10000)}万` : number 55 | } 56 | 57 | export function genImgUrl(url, w, h) { 58 | if (!h) { 59 | h = w 60 | } 61 | url += `?param=${w}y${h}` 62 | return url 63 | } 64 | 65 | 66 | export function isLast(index, arr) { 67 | return index === arr.length - 1 68 | } 69 | 70 | export function shallowEqual(a, b, compareKey) { 71 | if (a.length !== b.length) { 72 | return false 73 | } 74 | for (let i = 0; i < a.length; i++) { 75 | let compareA = a[i] 76 | let compareB = b[i] 77 | if (compareKey) { 78 | compareA = compareA[compareKey] 79 | compareB = compareB[compareKey] 80 | } 81 | if (!Object.is(a[i], b[i])) { 82 | return false 83 | } 84 | } 85 | return true 86 | } 87 | 88 | export function notify(message, type) { 89 | const params = { 90 | message, 91 | duration: 1500 92 | } 93 | const fn = type ? Notification[type] : Notification 94 | return fn(params) 95 | } 96 | ['success', 'warning', 'info', 'error'].forEach(key => { 97 | notify[key] = (message) => { 98 | return notify(message, key) 99 | } 100 | }) 101 | 102 | export function requestFullScreen(element) { 103 | const docElm = element; 104 | if (docElm.requestFullscreen) { 105 | docElm.requestFullscreen(); 106 | } else if (docElm.msRequestFullscreen) { 107 | docElm.msRequestFullscreen(); 108 | } else if (docElm.mozRequestFullScreen) { 109 | docElm.mozRequestFullScreen(); 110 | } else if (docElm.webkitRequestFullScreen) { 111 | docElm.webkitRequestFullScreen(); 112 | } 113 | } 114 | 115 | export function exitFullscreen() { 116 | const de = window.parent.document; 117 | 118 | if (de.exitFullscreen) { 119 | de.exitFullscreen(); 120 | } else if (de.mozCancelFullScreen) { 121 | de.mozCancelFullScreen(); 122 | } else if (de.webkitCancelFullScreen) { 123 | de.webkitCancelFullScreen(); 124 | } else if (de.msExitFullscreen) { 125 | de.msExitFullscreen() 126 | } 127 | } 128 | 129 | export function isFullscreen() { 130 | return document.fullScreen || 131 | document.mozFullScreen || 132 | document.webkitIsFullScreen 133 | } 134 | 135 | export function isUndef(v) { 136 | return v === undefined || v === null 137 | } 138 | 139 | export function isDef(v) { 140 | return v !== undefined && v !== null 141 | } 142 | 143 | export function isTrue(v) { 144 | return v === true 145 | } 146 | 147 | export function isFalse(v) { 148 | return v === false 149 | } 150 | 151 | export function getPageOffset(page, limit) { 152 | return page * limit 153 | } -------------------------------------------------------------------------------- /src/utils/config.js: -------------------------------------------------------------------------------- 1 | export const playModeMap = { 2 | sequence: { 3 | code: 'sequence', 4 | icon: 'sequence', 5 | name: '顺序播放' 6 | }, 7 | loop: { 8 | code: 'loop', 9 | icon: 'loop', 10 | name: '单曲循环' 11 | }, 12 | random: { 13 | code: 'random', 14 | icon: 'random', 15 | name: '随机播放' 16 | } 17 | } 18 | 19 | // 存储播放记录 20 | export const PLAY_HISTORY_KEY = '__play_history__' 21 | 22 | // 用户id 23 | export const UID_KEY = '__uid__' 24 | 25 | // 音量 26 | export const VOLUME_KEY = '__volume__' 27 | -------------------------------------------------------------------------------- /src/utils/dom.js: -------------------------------------------------------------------------------- 1 | export function hasClass(el, className) { 2 | return el.classList.contains(className) 3 | } 4 | 5 | export function addClass(el, className) { 6 | el.classList.add(className) 7 | } 8 | 9 | export function getData(el, name, val) { 10 | const prefix = 'data-' 11 | if (val) { 12 | return el.setAttribute(prefix + name, val) 13 | } 14 | return el.getAttribute(prefix + name) 15 | } 16 | 17 | let elementStyle = document.createElement('div').style 18 | 19 | let vendor = ( () => { 20 | let transformNames = { 21 | webkit: 'webkitTransform', 22 | Moz: 'MozTransform', 23 | O: 'OTransform', 24 | ms: 'msTransform', 25 | standard: 'transform' 26 | } 27 | 28 | for (let key in transformNames) { 29 | if (elementStyle[transformNames[key]] !== undefined) { 30 | return key 31 | } 32 | } 33 | 34 | return false 35 | })() 36 | 37 | export function prefixStyle(style) { 38 | if (vendor === false) { 39 | return false 40 | } 41 | 42 | if (vendor === 'standard') { 43 | return style 44 | } 45 | 46 | return vendor + style.charAt(0).toUpperCase() + style.substr(1) 47 | } 48 | 49 | export function hasParent(dom, parentDom) { 50 | parentDom = Array.isArray(parentDom) ? parentDom: [parentDom] 51 | while(dom) { 52 | if (parentDom.find(p => p === dom)) { 53 | return true 54 | }else { 55 | dom = dom.parentNode 56 | } 57 | } 58 | } 59 | 60 | export function scrollInto(dom) { 61 | dom.scrollIntoView({ behavior: "smooth" }) 62 | } 63 | 64 | export const EMPTY_IMG = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" -------------------------------------------------------------------------------- /src/utils/global.js: -------------------------------------------------------------------------------- 1 | import { 2 | Input, 3 | Dialog, 4 | Button, 5 | Loading, 6 | Carousel, 7 | CarouselItem, 8 | Table, 9 | TableColumn, 10 | Popover, 11 | Pagination, 12 | } from "element-ui" 13 | import VueLazyload from "vue-lazyload" 14 | import Meta from 'vue-meta' 15 | import * as utils from "./index" 16 | import { EMPTY_IMG } from "./dom" 17 | 18 | export default { 19 | install(Vue) { 20 | const requireComponent = require.context( 21 | "@/base", 22 | true, 23 | /[a-z0-9]+\.(jsx?|vue)$/i, 24 | ) 25 | // 批量注册base组件 26 | requireComponent.keys().forEach(fileName => { 27 | const componentConfig = requireComponent(fileName) 28 | const componentName = componentConfig.default.name 29 | if (componentName) { 30 | Vue.component(componentName, componentConfig.default || componentConfig) 31 | } 32 | }) 33 | 34 | Vue.prototype.$ELEMENT = { size: "small" } 35 | Vue.prototype.$utils = utils 36 | 37 | Vue.use(Input) 38 | Vue.use(Carousel) 39 | Vue.use(CarouselItem) 40 | Vue.use(Table) 41 | Vue.use(TableColumn) 42 | Vue.use(Popover) 43 | Vue.use(Pagination) 44 | Vue.use(Loading) 45 | Vue.use(Dialog) 46 | Vue.use(Button) 47 | 48 | Vue.use(Meta) 49 | 50 | Vue.use(VueLazyload, { 51 | loading: EMPTY_IMG, 52 | error: EMPTY_IMG, 53 | }) 54 | }, 55 | } 56 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | export * from './common' 2 | 3 | export * from './rem' 4 | 5 | export * from './config' 6 | 7 | export * from './dom' 8 | 9 | export * from './business' 10 | 11 | export * from './axios' 12 | 13 | export * from './mixin' 14 | -------------------------------------------------------------------------------- /src/utils/lrcparse.js: -------------------------------------------------------------------------------- 1 | export default function lyricParser(lrc) { 2 | return { 3 | 'lyric': parseLyric(lrc.lrc.lyric || ''), 4 | 'tlyric': parseLyric(lrc.tlyric.lyric || ''), 5 | 'lyricuser': lrc.lyricUser, 6 | 'transuser': lrc.transUser, 7 | } 8 | } 9 | 10 | export function parseLyric(lrc) { 11 | const lyrics = lrc.split('\n') 12 | const lrcObj = [] 13 | for (let i = 0; i < lyrics.length; i++) { 14 | const lyric = decodeURIComponent(lyrics[i]) 15 | const timeReg = /\[\d*:\d*((\.|:)\d*)*\]/g 16 | const timeRegExpArr = lyric.match(timeReg) 17 | if (!timeRegExpArr) continue 18 | const content = lyric.replace(timeReg, '') 19 | for (let k = 0, h = timeRegExpArr.length; k < h; k++) { 20 | const t = timeRegExpArr[k] 21 | const min = Number(String(t.match(/\[\d*/i)).slice(1)) 22 | const sec = Number(String(t.match(/:\d*/i)).slice(1)) 23 | const time = min * 60 + sec 24 | if (content !== '') { 25 | lrcObj.push({ time: time, content }) 26 | } 27 | } 28 | } 29 | return lrcObj 30 | } -------------------------------------------------------------------------------- /src/utils/mixin.js: -------------------------------------------------------------------------------- 1 | import store from '@/store' 2 | 3 | export const hideMenuMixin = { 4 | created() { 5 | store.commit('music/setMenuShow', false) 6 | }, 7 | beforeDestroy() { 8 | store.commit('music/setMenuShow', true) 9 | }, 10 | } 11 | 12 | export const hideMiniPlayerMixin = { 13 | created() { 14 | store.commit('music/setMiniPlayerShow', false) 15 | }, 16 | beforeDestroy() { 17 | store.commit('music/setMiniPlayerShow', true) 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /src/utils/rem.js: -------------------------------------------------------------------------------- 1 | import { throttle } from './common' 2 | 3 | export const remBase = 14 4 | 5 | let htmlFontSize 6 | !(function() { 7 | const calc = function() { 8 | const maxFontSize = 18 9 | const minFontSize = 14 10 | const html = document.getElementsByTagName('html')[0] 11 | const width = html.clientWidth 12 | let size = remBase * (width / 1440) 13 | size = Math.min(maxFontSize, size) 14 | size = Math.max(minFontSize, size) 15 | html.style.fontSize = size + 'px' 16 | htmlFontSize = size 17 | } 18 | calc() 19 | window.addEventListener('resize', throttle(calc, 500)) 20 | })() 21 | 22 | // 根据基准字号计算 23 | // 用于静态样式 24 | export function toRem(px) { 25 | return `${px / remBase}rem` 26 | } 27 | 28 | // 根据当前的html根字体大小计算 29 | // 用于某些js的动态计算 30 | export function toCurrentRem(px) { 31 | return `${px / htmlFontSize}rem` 32 | } 33 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | const WorkboxPlugin = require("workbox-webpack-plugin"); 2 | 3 | const isProd = process.env.NODE_ENV === "production"; 4 | 5 | module.exports = { 6 | outputDir: "music", 7 | configureWebpack: { 8 | devtool: isProd ? false : "source-map", 9 | devServer: { 10 | open: true, 11 | proxy: { 12 | "/netease-api": { 13 | target: "http://localhost:3000", 14 | pathRewrite: { "^/netease-api": "" }, 15 | changeOrigin: true, 16 | secure: false, 17 | }, 18 | }, 19 | port: 8888, 20 | }, 21 | externals: isProd 22 | ? { 23 | vue: "Vue", 24 | "vue-router": "VueRouter", 25 | vuex: "Vuex", 26 | axios: "axios", 27 | } 28 | : {}, 29 | plugins: [ 30 | new WorkboxPlugin.GenerateSW({ 31 | skipWaiting: true, 32 | clientsClaim: true, 33 | }), 34 | ], 35 | }, 36 | css: { 37 | loaderOptions: { 38 | sass: { 39 | implementation: require("sass"), 40 | data: ` 41 | @import "~@/style/variables.scss"; 42 | @import "~@/style/mixin.scss"; 43 | `, 44 | }, 45 | }, 46 | }, 47 | }; 48 | --------------------------------------------------------------------------------