├── .eslintrc ├── .gitignore ├── .husky └── pre-commit ├── .prettierrc ├── LICENSE ├── README.md ├── jsconfig.json ├── package.json ├── public ├── favicon.ico ├── img │ └── icons │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── android-chrome-maskable-192x192.png │ │ ├── android-chrome-maskable-512x512.png │ │ ├── apple-touch-icon-120x120.png │ │ ├── apple-touch-icon-152x152.png │ │ ├── apple-touch-icon-180x180.png │ │ ├── apple-touch-icon-60x60.png │ │ ├── apple-touch-icon-76x76.png │ │ ├── apple-touch-icon.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── msapplication-icon-144x144.png │ │ ├── mstile-150x150.png │ │ └── safari-pinned-tab.png ├── index.html └── robots.txt ├── src ├── App.vue ├── common │ ├── download.js │ ├── keymaster.js │ ├── message │ │ ├── Message.vue │ │ └── index.js │ ├── playCount.js │ ├── province-city-china │ │ ├── city.json │ │ └── province.json │ └── time.js ├── components │ ├── Button │ │ ├── ButtonAdd.vue │ │ ├── ButtonDelete.vue │ │ ├── ButtonLove.vue │ │ ├── ButtonMatch.vue │ │ └── ButtonPlay.vue │ ├── Image │ │ ├── ImageAvatar.vue │ │ └── ImageCover.vue │ ├── Introduce.vue │ ├── Mv.vue │ ├── MyAudio.vue │ └── Song │ │ ├── SongCard.vue │ │ ├── SongChip.vue │ │ └── SongList.vue ├── http │ ├── album.js │ ├── api.js │ ├── artist │ │ ├── detail.js │ │ ├── index.js │ │ └── songs.js │ ├── cloud │ │ ├── del.js │ │ ├── detail.js │ │ ├── index.js │ │ ├── match.js │ │ └── upload.js │ ├── index.js │ ├── login │ │ ├── captcha.js │ │ ├── cellphone.js │ │ ├── check.js │ │ ├── index.js │ │ ├── qr.js │ │ └── status.js │ ├── logout.js │ ├── mv │ │ ├── index.js │ │ ├── rcmd.js │ │ └── url.js │ ├── playlist │ │ ├── catlist.js │ │ ├── detail.js │ │ ├── index.js │ │ ├── recommend.js │ │ ├── subscribe.js │ │ ├── top.js │ │ └── tracks.js │ ├── search │ │ ├── index.js │ │ ├── search.js │ │ └── suggest.js │ ├── siginin.js │ ├── song │ │ ├── detail.js │ │ ├── download.js │ │ ├── index.js │ │ ├── like.js │ │ ├── likelist.js │ │ ├── lyric.js │ │ ├── scrobble.js │ │ └── url.js │ ├── toplist.js │ └── user │ │ ├── detail.js │ │ ├── index.js │ │ ├── playlist.js │ │ └── record.js ├── layout │ ├── AppBar │ │ ├── Avatar.vue │ │ ├── ButtonRouter.vue │ │ ├── History.vue │ │ ├── Search.vue │ │ └── index.vue │ ├── Drawer │ │ ├── Options.vue │ │ ├── Router.vue │ │ ├── Tools.vue │ │ └── index.vue │ └── SideBar │ │ ├── GoTop.vue │ │ ├── LocateMusic.vue │ │ ├── Lyrics.vue │ │ ├── MusicColumn.vue │ │ ├── Player │ │ ├── PlayerComment.vue │ │ ├── PlayerLists.vue │ │ ├── PlayerLyrics.vue │ │ ├── PlayerMusic │ │ │ ├── PlayerMusicMode.vue │ │ │ ├── PlayerMusicPlay.vue │ │ │ ├── PlayerMusicSound.vue │ │ │ └── index.vue │ │ └── index.vue │ │ └── index.vue ├── main.js ├── plugins │ ├── router │ │ ├── index.js │ │ └── routes.js │ ├── store │ │ ├── api.js │ │ ├── index.js │ │ ├── player.js │ │ ├── style.js │ │ └── user.js │ └── vuetify │ │ └── index.js ├── registerServiceWorker.js ├── service-worker.js ├── styles │ ├── main.scss │ └── userCss.js └── views │ ├── About │ ├── components │ │ ├── AboutApi.vue │ │ └── AboutGithub.vue │ └── index.vue │ ├── Album.vue │ ├── Artists.vue │ ├── Cloud.vue │ ├── Css.vue │ ├── Discover │ ├── components │ │ └── DiscoverCatlist.vue │ ├── index.vue │ └── view │ │ └── Playlist.vue │ ├── Home.vue │ ├── Login │ ├── components │ │ ├── LoginPhone.vue │ │ └── LoginQR.vue │ └── index.vue │ ├── Playlist │ ├── components │ │ ├── PlaylistDetail.vue │ │ └── SkeletonLoader.vue │ └── index.vue │ ├── Recommend.vue │ ├── Search.vue │ ├── Temporary.vue │ └── User │ ├── components │ ├── SkeletonLoader.vue │ ├── UserDetail.vue │ ├── UserListenRanking.vue │ └── UserPlaylist.vue │ └── index.vue ├── vue.config.js └── yarn.lock /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": ["eslint:recommended", "plugin:vue/essential", "prettier"], 8 | "parserOptions": { 9 | "ecmaVersion": "latest", 10 | "sourceType": "module" 11 | }, 12 | "plugins": ["vue"], 13 | "rules": { 14 | "vue/multi-word-component-names": 0, 15 | "vue/valid-v-slot": 0 16 | }, 17 | "globals": { 18 | "workbox": "readonly" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.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 | pnpm-debug.log* 14 | 15 | # Editor directories and files 16 | .idea 17 | .vscode 18 | *.suo 19 | *.ntvs* 20 | *.njsproj 21 | *.sln 22 | *.sw? 23 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 160, 3 | "useTabs": false, 4 | "semi": false, 5 | "singleQuote": true, 6 | "arrowParens": "avoid", 7 | "trailingComma": "none" 8 | } 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Flysky12138 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 | # Vuetify CloudMusic 2 | 3 | [![OSCS Status](https://www.oscs1024.com/platform/badge/Flysky12138/vuetify-cloudmusic.svg?size=small)](https://www.murphysec.com/dr/owvCqXgmI2aJ7qkwsD) 4 | [![GitHub last commit](https://img.shields.io/github/last-commit/Flysky12138/vuetify-cloudmusic)](https://github.com/Flysky12138/vuetify-cloudmusic/commits/master) 5 | ![GitHub repo size](https://img.shields.io/github/repo-size/Flysky12138/vuetify-cloudmusic) 6 | [![GitHub license](https://img.shields.io/github/license/Flysky12138/vuetify-cloudmusic)](https://github.com/Flysky12138/vuetify-cloudmusic/blob/master/LICENSE) 7 | 8 | ## 预览 9 | 10 | ![0392a0e95386e6f2507f70120aff0026.webp](https://cdn.jsdelivr.net/gh/Flysky12138/warehouse/PicW/vuetify-cloudmusic/0392a0e95386e6f2507f70120aff0026.webp) 11 | 12 | ![b75a24f1b6d429fab38c78cbaac8e16f.webp](https://cdn.jsdelivr.net/gh/Flysky12138/warehouse/PicW/vuetify-cloudmusic/b75a24f1b6d429fab38c78cbaac8e16f.webp) 13 | 14 | ## 安装 15 | 16 | ```bash 17 | git clone https://github.com/Flysky12138/vuetify-cloudmusic.git 18 | 19 | cd vuetify-cloudmusic 20 | 21 | yarn 22 | ``` 23 | 24 | ## 运行 25 | 26 | ```bash 27 | yarn serve 28 | ``` 29 | 30 | ## 其他 31 | 32 | 调整侧边歌词颜色,代码示例 33 | 34 | ```css 35 | .lyrics { 36 | width: 1em !important; 37 | background-image: linear-gradient(to bottom, red 20%, #8080ff, cyan 80%); 38 | -webkit-background-clip: text; 39 | color: transparent; 40 | } 41 | ``` 42 | 43 | ## 鸣谢 44 | 45 | - [Binaryify/NeteaseCloudMusicApi](https://github.com/Binaryify/NeteaseCloudMusicApi) 46 | - [UnblockNeteaseMusic/server](https://github.com/UnblockNeteaseMusic/server) 47 | - [madrobby/keymaster](https://github.com/madrobby/keymaster) 48 | - [PrismJS/prism](https://github.com/PrismJS/prism) 49 | - [koca/vue-prism-editor](https://github.com/koca/vue-prism-editor) 50 | - [uiwjs/province-city-china](https://github.com/uiwjs/province-city-china) 51 | - [beautify-web/js-beautify](https://github.com/beautify-web/js-beautify) 52 | - [blueimp/JavaScript-MD5](https://github.com/blueimp/JavaScript-MD5) 53 | - [prazdevs/pinia-plugin-persistedstate](https://github.com/prazdevs/pinia-plugin-persistedstate) 54 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src/**/*"], 3 | "exclude": ["node_modules"], 4 | "compilerOptions": { 5 | "baseUrl": ".", 6 | "paths": { 7 | "@/*": ["./src/*"] 8 | } 9 | }, 10 | "vueCompilerOptions": { 11 | "target": 2 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vuetify-cloudmusic", 3 | "version": "2.0.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "eslint --fix src/**/*.{js,vue}", 9 | "format": "prettier --write src/**", 10 | "prepare": "husky install" 11 | }, 12 | "lint-staged": { 13 | "*.{js,vue}": "eslint --fix", 14 | "*.{js,md,scss,json}": "prettier --write" 15 | }, 16 | "dependencies": { 17 | "@vue/composition-api": "^1.4.5", 18 | "axios": "^0.21.4", 19 | "blueimp-md5": "^2.19.0", 20 | "core-js": "^3.9.1", 21 | "js-beautify": "^1.14.0", 22 | "keymaster": "^1.6.2", 23 | "pinia": "^2.0.11", 24 | "pinia-plugin-persistedstate": "^1.2.1", 25 | "prismjs": "^1.26.0", 26 | "register-service-worker": "^1.7.1", 27 | "vue": "^2.6.11", 28 | "vue-prism-editor": "^1.3.0", 29 | "vue-router": "^3.2.0", 30 | "vuetify": "^2.4.5" 31 | }, 32 | "devDependencies": { 33 | "@vue/cli-plugin-pwa": "~4.5.0", 34 | "@vue/cli-plugin-router": "^4.5.9", 35 | "@vue/cli-plugin-vuex": "^4.5.9", 36 | "@vue/cli-service": "~4.5.0", 37 | "eslint": "^8.20.0", 38 | "eslint-config-prettier": "^8.5.0", 39 | "eslint-plugin-vue": "^9.3.0", 40 | "husky": "^8.0.0", 41 | "lint-staged": "^13.0.3", 42 | "prettier": "2.7.1", 43 | "sass": "^1.32.8", 44 | "sass-loader": "^8.0.0", 45 | "vue-cli-plugin-vuetify": "^2.2.2", 46 | "vue-template-compiler": "^2.6.11", 47 | "vuetify-loader": "^1.3.0" 48 | }, 49 | "browserslist": [ 50 | "> 1%", 51 | "last 2 versions", 52 | "not dead" 53 | ] 54 | } 55 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flysky12138/vuetify-cloudmusic/3bd661f11bde42498146080f488f08dad473bbc7/public/favicon.ico -------------------------------------------------------------------------------- /public/img/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flysky12138/vuetify-cloudmusic/3bd661f11bde42498146080f488f08dad473bbc7/public/img/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/img/icons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flysky12138/vuetify-cloudmusic/3bd661f11bde42498146080f488f08dad473bbc7/public/img/icons/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/img/icons/android-chrome-maskable-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flysky12138/vuetify-cloudmusic/3bd661f11bde42498146080f488f08dad473bbc7/public/img/icons/android-chrome-maskable-192x192.png -------------------------------------------------------------------------------- /public/img/icons/android-chrome-maskable-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flysky12138/vuetify-cloudmusic/3bd661f11bde42498146080f488f08dad473bbc7/public/img/icons/android-chrome-maskable-512x512.png -------------------------------------------------------------------------------- /public/img/icons/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flysky12138/vuetify-cloudmusic/3bd661f11bde42498146080f488f08dad473bbc7/public/img/icons/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /public/img/icons/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flysky12138/vuetify-cloudmusic/3bd661f11bde42498146080f488f08dad473bbc7/public/img/icons/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /public/img/icons/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flysky12138/vuetify-cloudmusic/3bd661f11bde42498146080f488f08dad473bbc7/public/img/icons/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /public/img/icons/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flysky12138/vuetify-cloudmusic/3bd661f11bde42498146080f488f08dad473bbc7/public/img/icons/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /public/img/icons/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flysky12138/vuetify-cloudmusic/3bd661f11bde42498146080f488f08dad473bbc7/public/img/icons/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /public/img/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flysky12138/vuetify-cloudmusic/3bd661f11bde42498146080f488f08dad473bbc7/public/img/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /public/img/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flysky12138/vuetify-cloudmusic/3bd661f11bde42498146080f488f08dad473bbc7/public/img/icons/favicon-16x16.png -------------------------------------------------------------------------------- /public/img/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flysky12138/vuetify-cloudmusic/3bd661f11bde42498146080f488f08dad473bbc7/public/img/icons/favicon-32x32.png -------------------------------------------------------------------------------- /public/img/icons/msapplication-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flysky12138/vuetify-cloudmusic/3bd661f11bde42498146080f488f08dad473bbc7/public/img/icons/msapplication-icon-144x144.png -------------------------------------------------------------------------------- /public/img/icons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flysky12138/vuetify-cloudmusic/3bd661f11bde42498146080f488f08dad473bbc7/public/img/icons/mstile-150x150.png -------------------------------------------------------------------------------- /public/img/icons/safari-pinned-tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flysky12138/vuetify-cloudmusic/3bd661f11bde42498146080f488f08dad473bbc7/public/img/icons/safari-pinned-tab.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Vuetify CloudMusic 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 20 |
21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 66 | 67 | 74 | -------------------------------------------------------------------------------- /src/common/download.js: -------------------------------------------------------------------------------- 1 | async function download(url, filename, isText = false) { 2 | let content = isText ? new Blob([JSON.stringify(url, null, ' ')]) : await fetch(url).then(res => res.blob()) 3 | content = window.URL.createObjectURL(content) 4 | const a = document.createElement('a') 5 | a.href = content 6 | a.download = filename 7 | a.click() 8 | window.URL.revokeObjectURL(content) 9 | } 10 | 11 | export default download 12 | -------------------------------------------------------------------------------- /src/common/keymaster.js: -------------------------------------------------------------------------------- 1 | import pinia from '@/plugins/store' 2 | import { playerStore } from '@/plugins/store/player' 3 | import key from 'keymaster' 4 | 5 | // 移除按键默认事件 6 | key.filter = event => { 7 | if (['INPUT', 'SELECT', 'TEXTAREA', 'VIDEO'].includes((event.target || event.srcElement).tagName)) { 8 | return false 9 | } else if ([32, 37, 39].includes(event.keyCode)) { 10 | event.preventDefault() 11 | } 12 | return true 13 | } 14 | 15 | const player = playerStore(pinia) 16 | 17 | export default (() => { 18 | key('left', () => { 19 | player.previous() 20 | }) 21 | key('right', () => { 22 | player.next() 23 | }) 24 | key('space', () => { 25 | player.playORpause() 26 | }) 27 | })() 28 | -------------------------------------------------------------------------------- /src/common/message/Message.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 51 | -------------------------------------------------------------------------------- /src/common/message/index.js: -------------------------------------------------------------------------------- 1 | import vuetify from '@/plugins/vuetify' 2 | import Vue from 'vue' 3 | import Message from './Message.vue' 4 | 5 | const createMessage = options => { 6 | if (!document.querySelector('#message')) { 7 | let div = document.createElement('div') 8 | div.id = 'message' 9 | document.querySelector('#app').appendChild(div) 10 | } 11 | new Vue({ 12 | vuetify, 13 | render: h => 14 | h(Message, { 15 | // 给Message组件中的props赋值 16 | props: { 17 | text: options.text, 18 | color: options.color, 19 | timeout: options.timeout, 20 | button: options.button 21 | } 22 | }) 23 | }).$mount('#message') 24 | } 25 | 26 | export default createMessage 27 | -------------------------------------------------------------------------------- /src/common/playCount.js: -------------------------------------------------------------------------------- 1 | // 歌单播放数 2 | function playCount(params) { 3 | if (params < 1e4) { 4 | return params 5 | } else if (params < 1e8) { 6 | return (params / 1e4).toFixed(2) + '万' 7 | } else { 8 | return (params / 1e8).toFixed(2) + '亿' 9 | } 10 | } 11 | 12 | export default playCount 13 | -------------------------------------------------------------------------------- /src/common/province-city-china/province.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "code": "110000", 4 | "name": "北京市", 5 | "province": "11" 6 | }, 7 | { 8 | "code": "120000", 9 | "name": "天津市", 10 | "province": "12" 11 | }, 12 | { 13 | "code": "130000", 14 | "name": "河北省", 15 | "province": "13" 16 | }, 17 | { 18 | "code": "140000", 19 | "name": "山西省", 20 | "province": "14" 21 | }, 22 | { 23 | "code": "150000", 24 | "name": "内蒙古自治区", 25 | "province": "15" 26 | }, 27 | { 28 | "code": "210000", 29 | "name": "辽宁省", 30 | "province": "21" 31 | }, 32 | { 33 | "code": "220000", 34 | "name": "吉林省", 35 | "province": "22" 36 | }, 37 | { 38 | "code": "230000", 39 | "name": "黑龙江省", 40 | "province": "23" 41 | }, 42 | { 43 | "code": "310000", 44 | "name": "上海市", 45 | "province": "31" 46 | }, 47 | { 48 | "code": "320000", 49 | "name": "江苏省", 50 | "province": "32" 51 | }, 52 | { 53 | "code": "330000", 54 | "name": "浙江省", 55 | "province": "33" 56 | }, 57 | { 58 | "code": "340000", 59 | "name": "安徽省", 60 | "province": "34" 61 | }, 62 | { 63 | "code": "350000", 64 | "name": "福建省", 65 | "province": "35" 66 | }, 67 | { 68 | "code": "360000", 69 | "name": "江西省", 70 | "province": "36" 71 | }, 72 | { 73 | "code": "370000", 74 | "name": "山东省", 75 | "province": "37" 76 | }, 77 | { 78 | "code": "410000", 79 | "name": "河南省", 80 | "province": "41" 81 | }, 82 | { 83 | "code": "420000", 84 | "name": "湖北省", 85 | "province": "42" 86 | }, 87 | { 88 | "code": "430000", 89 | "name": "湖南省", 90 | "province": "43" 91 | }, 92 | { 93 | "code": "440000", 94 | "name": "广东省", 95 | "province": "44" 96 | }, 97 | { 98 | "code": "450000", 99 | "name": "广西壮族自治区", 100 | "province": "45" 101 | }, 102 | { 103 | "code": "460000", 104 | "name": "海南省", 105 | "province": "46" 106 | }, 107 | { 108 | "code": "500000", 109 | "name": "重庆市", 110 | "province": "50" 111 | }, 112 | { 113 | "code": "510000", 114 | "name": "四川省", 115 | "province": "51" 116 | }, 117 | { 118 | "code": "520000", 119 | "name": "贵州省", 120 | "province": "52" 121 | }, 122 | { 123 | "code": "530000", 124 | "name": "云南省", 125 | "province": "53" 126 | }, 127 | { 128 | "code": "540000", 129 | "name": "西藏自治区", 130 | "province": "54" 131 | }, 132 | { 133 | "code": "610000", 134 | "name": "陕西省", 135 | "province": "61" 136 | }, 137 | { 138 | "code": "620000", 139 | "name": "甘肃省", 140 | "province": "62" 141 | }, 142 | { 143 | "code": "630000", 144 | "name": "青海省", 145 | "province": "63" 146 | }, 147 | { 148 | "code": "640000", 149 | "name": "宁夏回族自治区", 150 | "province": "64" 151 | }, 152 | { 153 | "code": "650000", 154 | "name": "新疆维吾尔自治区", 155 | "province": "65" 156 | }, 157 | { 158 | "code": "710000", 159 | "name": "台湾省", 160 | "province": "71" 161 | }, 162 | { 163 | "code": "810000", 164 | "name": "香港特别行政区", 165 | "province": "81" 166 | }, 167 | { 168 | "code": "820000", 169 | "name": "澳门特别行政区", 170 | "province": "82" 171 | } 172 | ] 173 | -------------------------------------------------------------------------------- /src/common/time.js: -------------------------------------------------------------------------------- 1 | const dateTimeFormat = [ 2 | new Intl.DateTimeFormat('zh-CN', { 3 | year: 'numeric', 4 | month: 'numeric', 5 | day: 'numeric', 6 | hour: 'numeric', 7 | minute: 'numeric', 8 | second: 'numeric', 9 | hour12: false 10 | }), 11 | new Intl.DateTimeFormat('zh-CN', { 12 | year: 'numeric', 13 | month: '2-digit', 14 | day: '2-digit', 15 | hour12: false 16 | }), 17 | new Intl.DateTimeFormat('zh-CN', { 18 | minute: 'numeric', 19 | second: 'numeric', 20 | hour12: false 21 | }), 22 | new Intl.DateTimeFormat('zh-CN', { 23 | year: 'numeric', 24 | month: '2-digit', 25 | day: '2-digit' 26 | }) 27 | ] 28 | 29 | // 歌单创建时间 30 | function date(timestamp) { 31 | return dateTimeFormat[0].format(timestamp) 32 | } 33 | // 专辑创建时间 34 | function dateSort(timestamp) { 35 | return dateTimeFormat[1].format(timestamp) 36 | } 37 | // 歌曲时长 38 | function song(timestamp) { 39 | return dateTimeFormat[2].format(timestamp) 40 | } 41 | // 日推标题时间 42 | function nowDate() { 43 | return dateTimeFormat[3].format() 44 | } 45 | 46 | export default { 47 | date, 48 | dateSort, 49 | song, 50 | nowDate 51 | } 52 | -------------------------------------------------------------------------------- /src/components/Button/ButtonAdd.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 93 | -------------------------------------------------------------------------------- /src/components/Button/ButtonDelete.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 36 | -------------------------------------------------------------------------------- /src/components/Button/ButtonLove.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 49 | -------------------------------------------------------------------------------- /src/components/Button/ButtonMatch.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 87 | -------------------------------------------------------------------------------- /src/components/Button/ButtonPlay.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 48 | -------------------------------------------------------------------------------- /src/components/Image/ImageAvatar.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 25 | -------------------------------------------------------------------------------- /src/components/Image/ImageCover.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 19 | -------------------------------------------------------------------------------- /src/components/Introduce.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 65 | 66 | 77 | -------------------------------------------------------------------------------- /src/components/Mv.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 126 | 127 | 135 | -------------------------------------------------------------------------------- /src/components/MyAudio.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 145 | -------------------------------------------------------------------------------- /src/components/Song/SongCard.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 45 | -------------------------------------------------------------------------------- /src/components/Song/SongChip.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 33 | -------------------------------------------------------------------------------- /src/components/Song/SongList.vue: -------------------------------------------------------------------------------- 1 | 96 | 97 | 233 | -------------------------------------------------------------------------------- /src/http/album.js: -------------------------------------------------------------------------------- 1 | import axios from './api' 2 | import detail from './song/detail' 3 | 4 | // 获取专辑内容 5 | function personalized(id) { 6 | return new Promise((resolve, reject) => { 7 | axios 8 | .get('/album', { 9 | params: { 10 | id 11 | } 12 | }) 13 | .then(response => { 14 | detail(response.songs.map(element => element.id)).then(songs => { 15 | resolve({ 16 | songs, 17 | info: { 18 | artist: { 19 | name: response.album.artist.name, 20 | alias: response.album.artist.alias, 21 | img1v1Url: response.album.artist.img1v1Url 22 | }, 23 | id: response.album.id, 24 | name: response.album.name, 25 | picUrl: response.album.picUrl, 26 | publishTime: response.album.publishTime, 27 | company: response.album.company, 28 | description: response.album.description || '' 29 | } 30 | }) 31 | }) 32 | }) 33 | .catch(error => reject(error)) 34 | }) 35 | } 36 | 37 | export default personalized 38 | -------------------------------------------------------------------------------- /src/http/api.js: -------------------------------------------------------------------------------- 1 | import pinia from '@/plugins/store' 2 | import { apiStore } from '@/plugins/store/api' 3 | import { userStore } from '@/plugins/store/user' 4 | import axios from 'axios' 5 | 6 | // 根据环境变量区分接口的默认地址 7 | switch (process.env.NODE_ENV) { 8 | case 'production': 9 | axios.defaults.baseURL = apiStore(pinia).api1 || 'https://netease-cloud-music-api-flysky.vercel.app/' 10 | break 11 | default: 12 | axios.defaults.baseURL = 'http://localhost:3000/' 13 | } 14 | 15 | axios.defaults.timeout = 10000 // 超时时间 16 | axios.defaults.retry = 5 // 超时重复请求次数 17 | axios.defaults.retryDelay = 1000 // 超时重复请求的间隙 18 | axios.defaults.withCredentials = true // 跨域是否允许携带Cookie凭证 19 | 20 | // 设置请求拦截器 21 | axios.interceptors.request.use( 22 | config => { 23 | config.params = { 24 | ...config.params, 25 | timestamp: new Date().getTime() // 添加时间戳 26 | } 27 | const cookie = userStore(pinia).cookie // 手动携带Cookie;Chrome v91开始浏览器默认SameSite=Lax无法修改,导致跨域不携带Cookie 28 | if (cookie) Object.assign(config.params, { cookie }) 29 | return config 30 | }, 31 | error => Promise.reject(error) 32 | ) 33 | 34 | // 自定义响应成功的HTTP状态码 35 | axios.defaults.validateStatus = status => /^(2|3|5)\d{2}$/.test(status) 36 | 37 | // 响应拦截器 38 | axios.interceptors.response.use( 39 | // 把请求结果中的http转https。避免浏览器的混合提示 40 | response => JSON.parse(JSON.stringify(response.data).replace(/http:\/\//g, 'https://')), 41 | error => { 42 | const { response, config } = error 43 | if (response) { 44 | switch (response.status) { 45 | case 401: // 当前请求需要权限(一般是未登录) 46 | break 47 | case 403: // 服务器拒绝请求(token过期) 48 | break 49 | case 404: // 找不到页面 50 | } 51 | } else { 52 | // 断网处理:可以设置跳转到断网页面 53 | if (!window.navigator.onLine) return Promise.reject(error) 54 | // 请求延迟,重新请求 55 | config.__retryCount = config.__retryCount || 0 56 | if (!config || !config.retry || config.__retryCount >= config.retry) return Promise.reject(error) 57 | config.__retryCount += 1 58 | const backoff = new Promise(resolve => { 59 | setTimeout(() => { 60 | resolve() 61 | }, config.retryDelay || 1) 62 | }) 63 | return backoff.then(() => axios(config)) 64 | } 65 | } 66 | ) 67 | 68 | export default axios 69 | -------------------------------------------------------------------------------- /src/http/artist/detail.js: -------------------------------------------------------------------------------- 1 | import axios from '../api' 2 | 3 | // 歌手部分信息 4 | function artists(id) { 5 | return new Promise((resolve, reject) => { 6 | axios 7 | .get('/artists', { 8 | params: { 9 | id 10 | } 11 | }) 12 | .then(response => { 13 | resolve({ 14 | alias: response.artist.alias, 15 | musicSize: response.artist.musicSize, 16 | albumSize: response.artist.albumSize, 17 | mvSize: response.artist.mvSize, 18 | briefDesc: response.artist.briefDesc || '', 19 | img1v1Url: response.artist.img1v1Url, 20 | name: response.artist.name, 21 | accountId: response.artist.accountId || 0 22 | }) 23 | }) 24 | .catch(error => reject(error)) 25 | }) 26 | } 27 | 28 | export default artists 29 | -------------------------------------------------------------------------------- /src/http/artist/index.js: -------------------------------------------------------------------------------- 1 | import detail from './detail' 2 | import songs from './songs' 3 | 4 | export default { 5 | songs, 6 | detail 7 | } 8 | -------------------------------------------------------------------------------- /src/http/artist/songs.js: -------------------------------------------------------------------------------- 1 | import axios from '../api' 2 | import detail from '../song/detail' 3 | 4 | // 歌手全部歌曲 5 | function once(id, page = 0, limit = 200) { 6 | return new Promise((resolve, reject) => { 7 | axios 8 | .get('/artist/songs', { 9 | params: { 10 | id, 11 | order: 'hot', 12 | limit, 13 | offset: limit * page 14 | } 15 | }) 16 | .then(response => { 17 | resolve({ 18 | more: response.more, 19 | ids: response.songs.map(element => element.id) 20 | }) 21 | }) 22 | .catch(error => reject(error)) 23 | }) 24 | } 25 | 26 | async function songs(id) { 27 | let data = { 28 | ids: [], 29 | i: 0, 30 | more: false 31 | } 32 | do { 33 | const res = await once(id, data.i++) 34 | data.more = res.more 35 | data.ids = data.ids.concat(res.ids) 36 | } while (data.more) 37 | return await detail(data.ids) 38 | } 39 | 40 | export default songs 41 | -------------------------------------------------------------------------------- /src/http/cloud/del.js: -------------------------------------------------------------------------------- 1 | import axios from '../api' 2 | 3 | // 云盘歌曲删除 4 | function del(id) { 5 | return new Promise((resolve, reject) => { 6 | axios 7 | .get('/user/cloud/del', { 8 | params: { 9 | id 10 | } 11 | }) 12 | .then(response => { 13 | resolve(response) 14 | }) 15 | .catch(error => reject(error)) 16 | }) 17 | } 18 | 19 | export default del 20 | -------------------------------------------------------------------------------- /src/http/cloud/detail.js: -------------------------------------------------------------------------------- 1 | import axios from '../api' 2 | 3 | // 获取云盘数据 4 | function once(page = 0, limit = 200) { 5 | return new Promise((resolve, reject) => { 6 | axios 7 | .get('/user/cloud', { 8 | params: { 9 | limit, 10 | offset: limit * page 11 | } 12 | }) 13 | .then(response => { 14 | resolve({ 15 | hasMore: response.hasMore, 16 | data: response.data.map((element, index) => ({ 17 | count: limit * page + index + 1, 18 | id: element.simpleSong.id, 19 | name: element.simpleSong.name, 20 | alia: element.simpleSong.alia || [], 21 | artists: element.simpleSong.ar 22 | ? element.simpleSong.ar.map(res => ({ 23 | id: res.id, 24 | name: res.name 25 | })) 26 | : [{ id: 0, name: '' }], 27 | album: element.simpleSong.al 28 | ? { 29 | id: element.simpleSong.al.id, 30 | name: element.simpleSong.al.name 31 | } 32 | : { 33 | id: 0, 34 | name: '' 35 | }, 36 | dt: element.simpleSong.dt, 37 | mv: element.simpleSong.mv, 38 | privilege: { 39 | fee: element.simpleSong.privilege.fee, // 0、8:免费;4:所在专辑需单独付费;1:VIP可听 40 | cs: element.simpleSong.privilege.cs, // boolean:云盘 41 | st: element.simpleSong.privilege.st // -200:无版权 42 | } 43 | })) 44 | }) 45 | }) 46 | .catch(error => reject(error)) 47 | }) 48 | } 49 | 50 | async function detail() { 51 | let data = { 52 | arr: [], 53 | i: 0, 54 | hasMore: false 55 | } 56 | do { 57 | const res = await once(data.i) 58 | data.hasMore = res.hasMore 59 | data.arr = data.arr.concat(res.data) 60 | data.i++ 61 | } while (data.hasMore) 62 | return data.arr 63 | } 64 | 65 | export default detail 66 | -------------------------------------------------------------------------------- /src/http/cloud/index.js: -------------------------------------------------------------------------------- 1 | import del from './del' 2 | import detail from './detail' 3 | import match from './match' 4 | import upload from './upload' 5 | 6 | export default { 7 | del, 8 | upload, 9 | detail, 10 | match 11 | } 12 | -------------------------------------------------------------------------------- /src/http/cloud/match.js: -------------------------------------------------------------------------------- 1 | import axios from '../api' 2 | 3 | // 云盘歌曲信息匹配纠正 4 | function match(uid, sid, asid) { 5 | return new Promise((resolve, reject) => { 6 | axios 7 | .get('/cloud/match', { 8 | params: { 9 | uid, 10 | sid, 11 | asid 12 | } 13 | }) 14 | .then(response => { 15 | resolve(response) 16 | }) 17 | .catch(error => reject(error)) 18 | }) 19 | } 20 | 21 | export default match 22 | -------------------------------------------------------------------------------- /src/http/cloud/upload.js: -------------------------------------------------------------------------------- 1 | import axios from '../api' 2 | 3 | // 上传单首歌曲到云盘 4 | function once(file) { 5 | const formData = new FormData() 6 | formData.append('songFile', file) 7 | return new Promise((resolve, reject) => { 8 | axios({ 9 | method: 'post', 10 | url: '/cloud', 11 | headers: { 12 | 'Content-Type': 'multipart/form-data' 13 | }, 14 | data: formData 15 | }) 16 | .then(response => { 17 | resolve(response) 18 | }) 19 | .catch(error => reject(error)) 20 | }) 21 | } 22 | 23 | function upload(fileArr) { 24 | return new Promise((resolve, reject) => { 25 | // 存放请求函数的数组 26 | let funcHttp = [] 27 | for (let item of fileArr) { 28 | funcHttp.push(once(item)) 29 | } 30 | // 开始请求 31 | axios 32 | .all(funcHttp) 33 | .then(response => { 34 | resolve(response) 35 | }) 36 | .catch(error => reject(error)) 37 | }) 38 | } 39 | 40 | export default upload 41 | -------------------------------------------------------------------------------- /src/http/index.js: -------------------------------------------------------------------------------- 1 | import album from './album' 2 | import artist from './artist' 3 | import cloud from './cloud' 4 | import login from './login' 5 | import logout from './logout' 6 | import mv from './mv' 7 | import playlist from './playlist' 8 | import search from './search' 9 | import siginin from './siginin' 10 | import song from './song' 11 | import toplist from './toplist' 12 | import user from './user' 13 | 14 | export default { 15 | login, 16 | playlist, 17 | song, 18 | user, 19 | artist, 20 | logout, 21 | siginin, 22 | cloud, 23 | search, 24 | album, 25 | toplist, 26 | mv 27 | } 28 | -------------------------------------------------------------------------------- /src/http/login/captcha.js: -------------------------------------------------------------------------------- 1 | import axios from '../api' 2 | 3 | // 发送验证码 4 | function captcha(phone) { 5 | return new Promise((resolve, reject) => { 6 | axios 7 | .get('/captcha/sent', { 8 | params: { 9 | phone 10 | } 11 | }) 12 | .then(response => { 13 | resolve(response || {}) 14 | }) 15 | .catch(error => reject(error)) 16 | }) 17 | } 18 | 19 | export default captcha 20 | -------------------------------------------------------------------------------- /src/http/login/cellphone.js: -------------------------------------------------------------------------------- 1 | import md5 from 'blueimp-md5' 2 | import axios from '../api' 3 | 4 | // 密码登录 5 | function password(phone, password) { 6 | return new Promise((resolve, reject) => { 7 | axios 8 | .get('/login/cellphone', { 9 | params: { 10 | phone, 11 | md5_password: md5(password) 12 | } 13 | }) 14 | .then(response => { 15 | response || reject('操作频繁,请稍候再试') 16 | let obj = { 17 | code: 2, 18 | cookie: response.cookie 19 | } 20 | switch (response.code) { 21 | case 502: // 密码错误 22 | obj.code = 0 23 | break 24 | case 200: // 登录成功 25 | obj.code = 1 26 | break 27 | case 250: // 登录失败 28 | obj.code = 2 29 | break 30 | case 509: // 密码错误超过限制 31 | obj.code = 3 32 | } 33 | resolve(obj) 34 | }) 35 | .catch(error => reject(error)) 36 | }) 37 | } 38 | 39 | // 验证码登录 40 | async function captcha(phone, captcha) { 41 | try { 42 | const res = await axios.get('/captcha/verify', { params: { phone, captcha } }) 43 | return res.data 44 | ? (await axios.get('/login/cellphone', { params: { phone, captcha } })) || Promise.reject('操作频繁,请稍候再试,或换二维码登录') 45 | : Promise.reject(res.message) 46 | } catch (error) { 47 | return Promise.reject(error) 48 | } 49 | } 50 | 51 | export default { 52 | password, 53 | captcha 54 | } 55 | -------------------------------------------------------------------------------- /src/http/login/check.js: -------------------------------------------------------------------------------- 1 | import axios from '../api' 2 | 3 | // 检测手机号码是否已注册 4 | function check(phone) { 5 | return new Promise((resolve, reject) => { 6 | axios 7 | .get('/cellphone/existence/check', { 8 | params: { 9 | phone 10 | } 11 | }) 12 | .then(response => { 13 | resolve(response.hasPassword) 14 | }) 15 | .catch(error => reject(error)) 16 | }) 17 | } 18 | 19 | export default check 20 | -------------------------------------------------------------------------------- /src/http/login/index.js: -------------------------------------------------------------------------------- 1 | import captcha from './captcha' 2 | import cellphone from './cellphone' 3 | import check from './check' 4 | import qr from './qr' 5 | import status from './status' 6 | 7 | export default { 8 | cellphone, 9 | check, 10 | qr, 11 | status, 12 | captcha 13 | } 14 | -------------------------------------------------------------------------------- /src/http/login/qr.js: -------------------------------------------------------------------------------- 1 | import axios from '../api' 2 | 3 | // 二维码key生成接口 4 | function key() { 5 | return new Promise((resolve, reject) => { 6 | axios 7 | .get('/login/qr/key') 8 | .then(res => { 9 | resolve(res.data.unikey) 10 | }) 11 | .catch(error => reject(error)) 12 | }) 13 | } 14 | // 二维码生成接口 15 | function create(key) { 16 | return new Promise((resolve, reject) => { 17 | axios 18 | .get('/login/qr/create', { 19 | params: { 20 | key, 21 | qrimg: true 22 | } 23 | }) 24 | .then(res => { 25 | resolve(res.data.qrimg) 26 | }) 27 | .catch(error => reject(error)) 28 | }) 29 | } 30 | // 二维码检测扫码状态接口 31 | function check(key) { 32 | return new Promise((resolve, reject) => { 33 | axios 34 | .get('/login/qr/check', { 35 | params: { 36 | key 37 | } 38 | }) 39 | .then(response => { 40 | let obj = { 41 | code: 2, 42 | cookie: response.cookie 43 | } 44 | switch (response.code) { 45 | case 800: // 二维码过期 46 | obj.code = 0 47 | break 48 | case 801: // 等待扫码 49 | obj.code = 1 50 | break 51 | case 802: // 待确认 52 | obj.code = 2 53 | break 54 | case 803: // 授权成功 55 | obj.code = 3 56 | break 57 | } 58 | resolve(obj) 59 | }) 60 | .catch(error => reject(error)) 61 | }) 62 | } 63 | 64 | export default { 65 | key, 66 | create, 67 | check 68 | } 69 | -------------------------------------------------------------------------------- /src/http/login/status.js: -------------------------------------------------------------------------------- 1 | import axios from '../api' 2 | 3 | // 返回登录状态等信息 4 | async function status() { 5 | try { 6 | const profile = await axios.get('/login/status').then(response => response.data.profile) 7 | return profile 8 | ? await axios(`/user/detail?uid=${profile.userId}`).then(response => ({ 9 | uid: response.profile.userId, 10 | level: response.level, 11 | avatarUrl: response.profile.avatarUrl 12 | })) 13 | : { 14 | uid: -1, 15 | level: 0, 16 | avatarUrl: '' 17 | } 18 | } catch (error) { 19 | return Promise.reject(error) 20 | } 21 | } 22 | 23 | export default status 24 | -------------------------------------------------------------------------------- /src/http/logout.js: -------------------------------------------------------------------------------- 1 | import axios from './api' 2 | 3 | // 退出登录 4 | function logout() { 5 | return new Promise((resolve, reject) => { 6 | axios 7 | .get('/logout') 8 | .then(response => { 9 | resolve(response) 10 | }) 11 | .catch(error => reject(error)) 12 | }) 13 | } 14 | 15 | export default logout 16 | -------------------------------------------------------------------------------- /src/http/mv/index.js: -------------------------------------------------------------------------------- 1 | import rcmd from './rcmd' 2 | import url from './url' 3 | 4 | export default { 5 | rcmd, 6 | url 7 | } 8 | -------------------------------------------------------------------------------- /src/http/mv/rcmd.js: -------------------------------------------------------------------------------- 1 | import axios from '../api' 2 | 3 | // 获取歌曲相关视频的 mlog id 4 | function rcmd(songid, mvid = 0) { 5 | return new Promise((resolve, reject) => { 6 | axios 7 | .get('/mlog/music/rcmd', { 8 | params: { 9 | songid, 10 | mvid 11 | } 12 | }) 13 | .then(response => { 14 | response.data.feeds.length > 0 ? resolve(response.data.feeds.map(res => res.id)) : reject() 15 | }) 16 | .catch(error => reject(error)) 17 | }) 18 | } 19 | 20 | export default rcmd 21 | -------------------------------------------------------------------------------- /src/http/mv/url.js: -------------------------------------------------------------------------------- 1 | import axios from '../api' 2 | 3 | // 获取 mv 播放地址 4 | function mv(id) { 5 | return new Promise((resolve, reject) => { 6 | axios 7 | .get('/mv/url', { 8 | params: { 9 | id 10 | } 11 | }) 12 | .then(response => { 13 | resolve({ 14 | frameUrl: '', 15 | url: response.data.url 16 | }) 17 | }) 18 | .catch(error => reject(error)) 19 | }) 20 | } 21 | 22 | // 获取 mlog 播放地址 23 | function mlog(id) { 24 | return new Promise((resolve, reject) => { 25 | axios 26 | .get('/mlog/url', { 27 | params: { 28 | id 29 | } 30 | }) 31 | .then(response => { 32 | resolve( 33 | response.data 34 | ? { 35 | frameUrl: response.data.resource.content.video.frameUrl, 36 | url: response.data.resource.content.video.urlInfo.url 37 | } 38 | : {} 39 | ) 40 | }) 41 | .catch(error => reject(error)) 42 | }) 43 | } 44 | 45 | export default { 46 | mv, 47 | mlog 48 | } 49 | -------------------------------------------------------------------------------- /src/http/playlist/catlist.js: -------------------------------------------------------------------------------- 1 | import axios from '../api' 2 | 3 | // 歌单分类 4 | function catlist() { 5 | return new Promise((resolve, reject) => { 6 | axios 7 | .get('/playlist/catlist') 8 | .then(response => { 9 | let arr = [] 10 | Object.values(response.categories).forEach(element => { 11 | arr.push({ 12 | sort: element, 13 | names: [] 14 | }) 15 | }) 16 | response.sub.forEach(element => { 17 | arr[element.category].names.push(element.name) 18 | }) 19 | resolve(arr) 20 | }) 21 | .catch(error => reject(error)) 22 | }) 23 | } 24 | 25 | export default catlist 26 | -------------------------------------------------------------------------------- /src/http/playlist/detail.js: -------------------------------------------------------------------------------- 1 | import axios from '../api' 2 | 3 | // 获取歌单详情 4 | function detail(id) { 5 | return new Promise((resolve, reject) => { 6 | axios 7 | .get('/playlist/detail', { 8 | params: { 9 | id 10 | } 11 | }) 12 | .then(response => { 13 | resolve({ 14 | id: response.playlist.id, 15 | coverImgUrl: response.playlist.coverImgUrl, 16 | createTime: response.playlist.createTime, 17 | updateTime: response.playlist.updateTime, 18 | description: response.playlist.description || '', 19 | name: response.playlist.name, 20 | playCount: response.playlist.playCount, 21 | shareCount: response.playlist.shareCount, 22 | subscribed: response.playlist.subscribed, 23 | subscribedCount: response.playlist.subscribedCount, 24 | tags: response.playlist.tags, 25 | trackCount: response.playlist.trackCount, 26 | trackIds: response.playlist.trackIds.map(res => res.id), 27 | userId: response.playlist.userId, 28 | avatarUrl: response.playlist.creator.avatarUrl, 29 | nickname: response.playlist.creator.nickname 30 | }) 31 | }) 32 | .catch(error => reject(error)) 33 | }) 34 | } 35 | 36 | export default detail 37 | -------------------------------------------------------------------------------- /src/http/playlist/index.js: -------------------------------------------------------------------------------- 1 | import catlist from './catlist' 2 | import detail from './detail' 3 | import recommend from './recommend' 4 | import subscribe from './subscribe' 5 | import top from './top' 6 | import tracks from './tracks' 7 | 8 | export default { 9 | catlist, 10 | detail, 11 | recommend, 12 | subscribe, 13 | top, 14 | tracks 15 | } 16 | -------------------------------------------------------------------------------- /src/http/playlist/recommend.js: -------------------------------------------------------------------------------- 1 | import axios from '../api' 2 | import detail from '../song/detail' 3 | 4 | // 日推 5 | function recommend() { 6 | return new Promise((resolve, reject) => { 7 | axios 8 | .get('/recommend/songs') 9 | .then(response => { 10 | detail(response.data.dailySongs.map(element => element.id)) 11 | .then(resolve) 12 | .catch(reject) 13 | }) 14 | .catch(error => reject(error)) 15 | }) 16 | } 17 | 18 | export default recommend 19 | -------------------------------------------------------------------------------- /src/http/playlist/subscribe.js: -------------------------------------------------------------------------------- 1 | import axios from '../api' 2 | 3 | // 收藏/取消收藏歌单 4 | function subscribe(id, t = true) { 5 | return new Promise((resolve, reject) => { 6 | axios 7 | .get('/playlist/subscribe', { 8 | params: { 9 | id, 10 | t: t ? 1 : 2 11 | } 12 | }) 13 | .then(response => { 14 | resolve(response) 15 | }) 16 | .catch(error => reject(error)) 17 | }) 18 | } 19 | 20 | export default subscribe 21 | -------------------------------------------------------------------------------- /src/http/playlist/top.js: -------------------------------------------------------------------------------- 1 | import axios from '../api' 2 | 3 | // 歌单 ( 网友精选碟 ) 4 | function top({ cat = '全部', limit = 30, offset = 0, order = 'hot' }) { 5 | return new Promise((resolve, reject) => { 6 | axios 7 | .get('/top/playlist', { 8 | params: { 9 | cat, 10 | limit, 11 | offset, 12 | order 13 | } 14 | }) 15 | .then(response => { 16 | let obj = { 17 | total: response.total, 18 | playlists: [] 19 | } 20 | response.playlists.forEach(element => { 21 | obj.playlists.push({ 22 | id: element.id, 23 | name: element.name, 24 | coverImgUrl: element.coverImgUrl, 25 | playCount: element.playCount 26 | }) 27 | }) 28 | resolve(obj) 29 | }) 30 | .catch(error => reject(error)) 31 | }) 32 | } 33 | 34 | export default top 35 | -------------------------------------------------------------------------------- /src/http/playlist/tracks.js: -------------------------------------------------------------------------------- 1 | import axios from '../api' 2 | 3 | // 对歌单添加或删除歌曲 4 | function tracks(pid, tracks, method = true) { 5 | return new Promise((resolve, reject) => { 6 | axios 7 | .get('/playlist/tracks', { 8 | params: { 9 | pid, 10 | tracks, 11 | op: method ? 'add' : 'del' 12 | } 13 | }) 14 | .then(response => { 15 | resolve(response) 16 | }) 17 | .catch(error => reject(error)) 18 | }) 19 | } 20 | 21 | export default tracks 22 | -------------------------------------------------------------------------------- /src/http/search/index.js: -------------------------------------------------------------------------------- 1 | import search from './search' 2 | import suggest from './suggest' 3 | 4 | export default { 5 | search, 6 | suggest 7 | } 8 | -------------------------------------------------------------------------------- /src/http/search/search.js: -------------------------------------------------------------------------------- 1 | import axios from '../api' 2 | import detail from '../song/detail' 3 | 4 | // 搜索 1: 单曲, 10: 专辑, 100: 歌手, 1000: 歌单, 1002: 用户, 1004: MV, 1006: 歌词, 1009: 电台, 1014: 视频, 1018:综合 5 | function cloudsearch(keywords, offset = 0, limit = 36, type = 1) { 6 | return new Promise((resolve, reject) => { 7 | axios 8 | .get('/cloudsearch', { 9 | params: { 10 | keywords, 11 | offset, 12 | limit, 13 | type 14 | } 15 | }) 16 | .then(response => { 17 | let obj = { 18 | hasMore: response.result.hasMore, 19 | songCount: response.result.songCount, 20 | songs: [] 21 | } 22 | obj.songCount 23 | ? detail(response.result.songs.map(element => element.id)).then(res => { 24 | resolve(Object.assign(obj, { songs: res })) 25 | }) 26 | : resolve(obj) 27 | }) 28 | .catch(error => reject(error)) 29 | }) 30 | } 31 | 32 | export default cloudsearch 33 | -------------------------------------------------------------------------------- /src/http/search/suggest.js: -------------------------------------------------------------------------------- 1 | import axios from '../api' 2 | 3 | // 搜索建议 4 | function suggest(keywords) { 5 | return new Promise((resolve, reject) => { 6 | axios 7 | .get('/search/suggest', { 8 | params: { 9 | keywords, 10 | type: 'mobile' 11 | } 12 | }) 13 | .then(response => { 14 | resolve(response.result.allMatch ? response.result.allMatch.filter(res => res.type === 1).map(res => res.keyword) : []) 15 | }) 16 | .catch(error => reject(error)) 17 | }) 18 | } 19 | 20 | export default suggest 21 | -------------------------------------------------------------------------------- /src/http/siginin.js: -------------------------------------------------------------------------------- 1 | import axios from './api' 2 | 3 | // 签到 0、安卓端签到 1、web/PC 签到 4 | function daily(type = 0) { 5 | return new Promise((resolve, reject) => { 6 | axios 7 | .get('/daily_signin', { 8 | params: { 9 | type 10 | } 11 | }) 12 | .then(response => { 13 | console.log(type === 0 ? 'Android端签到,' : 'Web/PC端签到,', response ? '获得经验:' + response.point : '今日已签到!') 14 | resolve(response) 15 | }) 16 | .catch(error => reject(error)) 17 | }) 18 | } 19 | 20 | // 云贝签到 21 | function yunbei() { 22 | return new Promise((resolve, reject) => { 23 | axios 24 | .get('/yunbei/sign') 25 | .then(response => { 26 | console.log('云贝签到,', response ? '获得云贝:' + response.point : '今日已签到!') 27 | resolve(response) 28 | }) 29 | .catch(error => reject(error)) 30 | }) 31 | } 32 | 33 | function siginin() { 34 | return new Promise((resolve, reject) => { 35 | axios 36 | .all([daily(0), daily(1), yunbei()]) 37 | .then(response => { 38 | resolve(response) 39 | }) 40 | .catch(error => reject(error)) 41 | }) 42 | } 43 | 44 | export default siginin 45 | -------------------------------------------------------------------------------- /src/http/song/detail.js: -------------------------------------------------------------------------------- 1 | import axios from '../api' 2 | 3 | // 获取歌曲详情 4 | function once(ids, offset) { 5 | return new Promise((resolve, reject) => { 6 | axios 7 | .get('/song/detail', { 8 | params: { 9 | ids: ids.join(',') 10 | } 11 | }) 12 | .then(response => { 13 | resolve( 14 | response.songs.map((element, index) => ({ 15 | count: index + 1 + offset, 16 | id: element.id, 17 | name: element.name, 18 | alia: element.alia, 19 | picUrl: element.al.picUrl, 20 | artists: element.ar.map(res => ({ 21 | id: res.id, 22 | name: res.name 23 | })), 24 | album: { 25 | id: element.al.id, 26 | name: element.al.name 27 | }, 28 | dt: element.dt, 29 | mv: element.mv, 30 | // https://github.com/Binaryify/NeteaseCloudMusicApi/issues/899#issuecomment-680002883 31 | privilege: { 32 | fee: response.privileges[index].fee, // 0、8:免费;4:所在专辑需单独付费;1:VIP可听 33 | st: response.privileges[index].st, // -200:无版权 34 | cs: response.privileges[index].cs // boolean:云盘 35 | } 36 | })) 37 | ) 38 | }) 39 | .catch(error => reject(error)) 40 | }) 41 | } 42 | 43 | function detail(ids) { 44 | return new Promise((resolve, reject) => { 45 | // 每500首请求一次,数量过多会报错 46 | const count = Math.ceil(ids.length / 500) 47 | // 存放请求函数的数组 48 | let funcHttp = [] 49 | for (let i = 0; i < count; i++) { 50 | funcHttp.push(once(ids.slice(500 * i, 500 * (i + 1)), 500 * i)) 51 | } 52 | // 开始请求 53 | axios 54 | .all(funcHttp) 55 | .then(response => { 56 | resolve(response.flat()) 57 | }) 58 | .catch(error => reject(error)) 59 | }) 60 | } 61 | 62 | export default detail 63 | -------------------------------------------------------------------------------- /src/http/song/download.js: -------------------------------------------------------------------------------- 1 | import axios from '../api' 2 | 3 | // 获取客户端歌曲下载 url 4 | function download(id) { 5 | return new Promise((resolve, reject) => { 6 | axios 7 | .get('/song/download/url', { 8 | params: { 9 | id 10 | } 11 | }) 12 | .then(response => { 13 | resolve(response.data && response.data.url) 14 | }) 15 | .catch(error => reject(error)) 16 | }) 17 | } 18 | 19 | export default download 20 | -------------------------------------------------------------------------------- /src/http/song/index.js: -------------------------------------------------------------------------------- 1 | import detail from './detail' 2 | import download from './download' 3 | import like from './like' 4 | import likelist from './likelist' 5 | import lyric from './lyric' 6 | import scrobble from './scrobble' 7 | import url from './url' 8 | 9 | export default { 10 | detail, 11 | like, 12 | likelist, 13 | lyric, 14 | scrobble, 15 | url, 16 | download 17 | } 18 | -------------------------------------------------------------------------------- /src/http/song/like.js: -------------------------------------------------------------------------------- 1 | import axios from '../api' 2 | 3 | // 添加移除喜欢音乐 4 | function like(id, like) { 5 | return new Promise((resolve, reject) => { 6 | axios 7 | .get('/like', { 8 | params: { 9 | id, 10 | like 11 | } 12 | }) 13 | .then(response => { 14 | resolve(response) 15 | }) 16 | .catch(error => reject(error)) 17 | }) 18 | } 19 | 20 | export default like 21 | -------------------------------------------------------------------------------- /src/http/song/likelist.js: -------------------------------------------------------------------------------- 1 | import axios from '../api' 2 | 3 | // 喜欢音乐列表 4 | function likelist(uid) { 5 | return new Promise((resolve, reject) => { 6 | axios 7 | .get('/likelist', { 8 | params: { 9 | uid 10 | } 11 | }) 12 | .then(response => { 13 | resolve(response.ids) 14 | }) 15 | .catch(error => reject(error)) 16 | }) 17 | } 18 | 19 | export default likelist 20 | -------------------------------------------------------------------------------- /src/http/song/lyric.js: -------------------------------------------------------------------------------- 1 | import axios from '../api' 2 | 3 | // 获取歌词 4 | function lyric(id) { 5 | return new Promise((resolve, reject) => { 6 | axios 7 | .get('/lyric', { 8 | params: { 9 | id 10 | } 11 | }) 12 | .then(response => { 13 | let arr = [] 14 | let user = { 15 | time: 1e6, 16 | lyric: '', 17 | tlyric: '' 18 | } 19 | // 歌词 20 | if (response.lrc) { 21 | const lyric = response.lrc.lyric.split('\n').filter(res => res !== '') 22 | lyric.forEach(element => { 23 | let time = element.match(/\d{2}:\d{2}.(\d{2}|\d{3})/g) 24 | if (time) { 25 | time = Math.round((Number(time[0].substring(0, 2)) * 60 + Number(time[0].substring(3))) * 1000) 26 | arr.push({ 27 | time: time, // 毫秒 28 | lyric: element.substring(element.lastIndexOf(']') + 1) // 歌词 29 | }) 30 | } 31 | }) 32 | // 歌词贡献者 33 | response.lyricUser && (user.lyric = '歌词贡献者: ' + response.lyricUser.nickname) 34 | } else { 35 | arr.push({ 36 | time: 0, 37 | lyric: '纯音乐,请欣赏' 38 | }) 39 | } 40 | // 翻译 41 | if (response.tlyric) { 42 | const tlyric = response.tlyric.lyric.split('\n').filter(res => res !== '') 43 | // 在arr数组中,对应相同time对象中插入翻译项tlyric 44 | tlyric.forEach(element => { 45 | let time = element.match(/\d{2}:\d{2}.(\d{2}|\d{3})/g) 46 | if (time) { 47 | time = Math.round((Number(time[0].substring(0, 2)) * 60 + Number(time[0].substring(3))) * 1000) 48 | const item = arr.find(res => res.time === time) 49 | item && (item.tlyric = element.substring(element.lastIndexOf(']') + 1)) // 翻译 50 | } 51 | }) 52 | // 翻译贡献者 53 | response.transUser && (user.tlyric = '翻译贡献者: ' + response.transUser.nickname) 54 | } 55 | arr.push(user) 56 | resolve(arr.filter(res => res.lyric !== '')) 57 | }) 58 | .catch(error => reject(error)) 59 | }) 60 | } 61 | 62 | export default lyric 63 | -------------------------------------------------------------------------------- /src/http/song/scrobble.js: -------------------------------------------------------------------------------- 1 | import axios from '../api' 2 | 3 | // 听歌打卡 4 | function scrobble(id, sourceid, time = 291) { 5 | return new Promise((resolve, reject) => { 6 | axios 7 | .get('/scrobble', { 8 | params: { 9 | id, 10 | sourceid, 11 | time 12 | } 13 | }) 14 | .then(response => { 15 | resolve(response) 16 | }) 17 | .catch(error => reject(error)) 18 | }) 19 | } 20 | 21 | export default scrobble 22 | -------------------------------------------------------------------------------- /src/http/song/url.js: -------------------------------------------------------------------------------- 1 | import axios from '../api' 2 | 3 | // 获取音乐 url 4 | function url(id, br = 320000) { 5 | return new Promise((resolve, reject) => { 6 | axios 7 | .get('/song/url', { 8 | params: { 9 | id, 10 | br 11 | } 12 | }) 13 | .then(response => { 14 | const element = response.data[0] 15 | resolve({ 16 | freeTrialInfo: element.freeTrialInfo, 17 | size: element.size, 18 | type: element.type, 19 | url: element.url 20 | }) 21 | }) 22 | .catch(error => reject(error)) 23 | }) 24 | } 25 | 26 | export default url 27 | -------------------------------------------------------------------------------- /src/http/toplist.js: -------------------------------------------------------------------------------- 1 | import axios from './api' 2 | 3 | // 所有榜单 4 | function toplist() { 5 | return new Promise((resolve, reject) => { 6 | axios 7 | .get('/toplist') 8 | .then(response => { 9 | const data = response.list.reduce( 10 | (acc, cur) => { 11 | const obj = { 12 | id: cur.id, 13 | name: cur.name, 14 | coverImgUrl: cur.coverImgUrl, 15 | updateFrequency: cur.updateFrequency 16 | } 17 | cur.ToplistType ? acc.feature.push(obj) : acc.media.push(obj) 18 | return acc 19 | }, 20 | { feature: [], media: [] } 21 | ) 22 | resolve(data) 23 | }) 24 | .catch(error => reject(error)) 25 | }) 26 | } 27 | 28 | export default toplist 29 | -------------------------------------------------------------------------------- /src/http/user/detail.js: -------------------------------------------------------------------------------- 1 | import city from '@/common/province-city-china/city.json' 2 | import province from '@/common/province-city-china/province.json' 3 | import axios from '../api' 4 | 5 | // 获取用户详情 6 | function detail(uid) { 7 | return new Promise((resolve, reject) => { 8 | axios 9 | .get('/user/detail', { 10 | params: { 11 | uid 12 | } 13 | }) 14 | .then(response => { 15 | const p = province.find(res => res.code == response.profile.province) 16 | const c = city.find(res => res.code == response.profile.city) 17 | resolve({ 18 | level: response.level, 19 | listenSongs: response.listenSongs, 20 | createTime: response.createTime, 21 | createDays: response.createDays, 22 | profile: { 23 | vipType: response.profile.vipType, 24 | birthday: response.profile.birthday, 25 | gender: response.profile.gender, 26 | province: p ? p.name : '-', 27 | city: c ? c.name : '-', 28 | backgroundUrl: response.profile.backgroundUrl, 29 | nickname: response.profile.nickname, 30 | avatarUrl: response.profile.avatarUrl, 31 | signature: response.profile.signature, 32 | followeds: response.profile.followeds, 33 | follows: response.profile.follows 34 | } 35 | }) 36 | }) 37 | .catch(error => reject(error)) 38 | }) 39 | } 40 | 41 | export default detail 42 | -------------------------------------------------------------------------------- /src/http/user/index.js: -------------------------------------------------------------------------------- 1 | import detail from './detail' 2 | import playlist from './playlist' 3 | import record from './record' 4 | 5 | export default { 6 | detail, // 获取用户详情 7 | playlist, // 获取用户歌单 8 | record // 获取用户播放记录 9 | } 10 | -------------------------------------------------------------------------------- /src/http/user/playlist.js: -------------------------------------------------------------------------------- 1 | import axios from '../api' 2 | 3 | // 获取用户歌单 4 | function once(uid, offset = 0) { 5 | return new Promise((resolve, reject) => { 6 | axios 7 | .get('/user/playlist', { 8 | params: { 9 | uid, 10 | limit: 30, 11 | offset: 30 * offset 12 | } 13 | }) 14 | .then(response => { 15 | let obj = { 16 | more: response.more, 17 | create: [], 18 | collect: [] 19 | } 20 | response.playlist.forEach(element => { 21 | const type = element.creator.userId == uid ? 'create' : 'collect' 22 | obj[type].push({ 23 | id: element.id, 24 | name: element.name, 25 | playCount: element.playCount, 26 | trackCount: element.trackCount, 27 | coverImgUrl: element.coverImgUrl 28 | }) 29 | }) 30 | resolve(obj) 31 | }) 32 | .catch(error => reject(error)) 33 | }) 34 | } 35 | 36 | async function playlist(uid) { 37 | let data = { 38 | obj: { 39 | create: [], 40 | collect: [] 41 | }, 42 | i: 0, 43 | more: false 44 | } 45 | do { 46 | const res = await once(uid, data.i++) 47 | data.more = res.more 48 | data.obj.create = data.obj.create.concat(res.create) 49 | data.obj.collect = data.obj.collect.concat(res.collect) 50 | } while (data.more) 51 | return data.obj 52 | } 53 | 54 | export default playlist 55 | -------------------------------------------------------------------------------- /src/http/user/record.js: -------------------------------------------------------------------------------- 1 | import axios from '../api' 2 | import detail from '../song/detail' 3 | 4 | // 获取用户播放记录 5 | function record(uid, type = 0) { 6 | return new Promise((resolve, reject) => { 7 | axios 8 | .get('/user/record', { 9 | params: { 10 | uid, 11 | type 12 | } 13 | }) 14 | .then(response => { 15 | const key = type === 0 ? 'allData' : 'weekData' 16 | response 17 | ? detail(response[key].map(element => element.song.id)).then(item => { 18 | resolve( 19 | item.map((res, index) => 20 | Object.assign(res, { 21 | count: response[key][index].playCount ? response[key][index].playCount : '-' 22 | }) 23 | ) 24 | ) 25 | }) 26 | : resolve(false) 27 | }) 28 | .catch(error => reject(error)) 29 | }) 30 | } 31 | 32 | export default record 33 | -------------------------------------------------------------------------------- /src/layout/AppBar/Avatar.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 26 | -------------------------------------------------------------------------------- /src/layout/AppBar/ButtonRouter.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 21 | -------------------------------------------------------------------------------- /src/layout/AppBar/History.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 31 | 32 | 40 | -------------------------------------------------------------------------------- /src/layout/AppBar/Search.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 76 | 77 | 92 | -------------------------------------------------------------------------------- /src/layout/AppBar/index.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 24 | -------------------------------------------------------------------------------- /src/layout/Drawer/Options.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 74 | -------------------------------------------------------------------------------- /src/layout/Drawer/Router.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 27 | -------------------------------------------------------------------------------- /src/layout/Drawer/Tools.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 46 | -------------------------------------------------------------------------------- /src/layout/Drawer/index.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 76 | 77 | 85 | -------------------------------------------------------------------------------- /src/layout/SideBar/GoTop.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 28 | -------------------------------------------------------------------------------- /src/layout/SideBar/LocateMusic.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | -------------------------------------------------------------------------------- /src/layout/SideBar/Lyrics.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 31 | 32 | 40 | -------------------------------------------------------------------------------- /src/layout/SideBar/MusicColumn.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 47 | 48 | 72 | -------------------------------------------------------------------------------- /src/layout/SideBar/Player/PlayerComment.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /src/layout/SideBar/Player/PlayerLists.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 74 | 75 | 80 | -------------------------------------------------------------------------------- /src/layout/SideBar/Player/PlayerLyrics.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 95 | 96 | 104 | -------------------------------------------------------------------------------- /src/layout/SideBar/Player/PlayerMusic/PlayerMusicMode.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 40 | -------------------------------------------------------------------------------- /src/layout/SideBar/Player/PlayerMusic/PlayerMusicPlay.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 37 | -------------------------------------------------------------------------------- /src/layout/SideBar/Player/PlayerMusic/PlayerMusicSound.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 50 | -------------------------------------------------------------------------------- /src/layout/SideBar/Player/PlayerMusic/index.vue: -------------------------------------------------------------------------------- 1 | 52 | 53 | 90 | -------------------------------------------------------------------------------- /src/layout/SideBar/Player/index.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 56 | 57 | 65 | -------------------------------------------------------------------------------- /src/layout/SideBar/index.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 41 | 42 | 65 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import '@/common/keymaster' 2 | import message from '@/common/message' 3 | import time from '@/common/time' 4 | import router from '@/plugins/router' 5 | import pinia from '@/plugins/store' 6 | import vuetify from '@/plugins/vuetify' 7 | import '@/styles/main.scss' 8 | import '@/styles/userCss' 9 | import Vue from 'vue' 10 | import App from './App.vue' 11 | import http from './http' 12 | import './registerServiceWorker' 13 | 14 | Vue.config.productionTip = false 15 | Vue.prototype.$http = http 16 | Vue.prototype.$message = message 17 | Vue.prototype.$time = time 18 | Vue.prototype.$bus = new Vue() 19 | 20 | const app = new Vue({ 21 | router, 22 | pinia, 23 | vuetify, 24 | render: h => h(App) 25 | }) 26 | 27 | router.onReady(() => { 28 | app.$mount('#app') 29 | }) 30 | -------------------------------------------------------------------------------- /src/plugins/router/index.js: -------------------------------------------------------------------------------- 1 | import message from '@/common/message' 2 | import { userStore } from '@/plugins/store/user' 3 | import Vue from 'vue' 4 | import VueRouter from 'vue-router' 5 | import goTo from 'vuetify/es5/services/goto' 6 | import pinia from '../store' 7 | import routes from './routes' 8 | 9 | // 解决报错问题:Error: Avoided redundant navigation to current location 10 | const original = VueRouter.prototype.push 11 | VueRouter.prototype.push = function push(location) { 12 | return original.call(this, location).catch(err => err) 13 | } 14 | 15 | Vue.use(VueRouter) 16 | 17 | const router = new VueRouter({ 18 | routes, 19 | // 页面跳转浏览位置定位 20 | scrollBehavior: (to, from, savedPosition) => { 21 | // path不同才滚动 22 | if (to.path !== from.path) { 23 | let scrollTo = 0 24 | if (to.hash) { 25 | scrollTo = to.hash 26 | } else if (savedPosition) { 27 | scrollTo = savedPosition.y 28 | } 29 | return goTo(scrollTo, { 30 | duration: 400, 31 | offset: 0, 32 | easing: 'easeOutQuad' 33 | }) 34 | } 35 | } 36 | }) 37 | 38 | // 路由全局前置守卫 39 | router.beforeEach((to, from, next) => { 40 | let params = true 41 | // 首次加载时from.fullPath=to.fullPath=/ 42 | if (to.fullPath === '/' && from.fullPath === '/') { 43 | const lastAddress = localStorage.getItem('lastAddress') || '/' 44 | if (lastAddress !== '/') { 45 | params = lastAddress 46 | } 47 | } else { 48 | localStorage.setItem('lastAddress', to.fullPath) 49 | } 50 | if (!userStore(pinia).islogin && to.meta.requiresAuth) { 51 | message({ text: '未登录,跳转到登录界面!' }) 52 | params = { path: '/login' } 53 | } 54 | next(params) 55 | }) 56 | 57 | export default router 58 | -------------------------------------------------------------------------------- /src/plugins/router/routes.js: -------------------------------------------------------------------------------- 1 | import message from '@/common/message' 2 | import { userStore } from '@/plugins/store/user' 3 | import pinia from '../store' 4 | 5 | const routes = [ 6 | { 7 | path: '*', 8 | redirect: { 9 | name: 'Home' 10 | } 11 | }, 12 | { 13 | path: '/login', 14 | component: () => import('@/views/Login'), 15 | // 路由独享的守卫 16 | beforeEnter(to, from, next) { 17 | if (userStore(pinia).islogin) { 18 | message({ 19 | text: '已经登录过了!', 20 | color: 'success' 21 | }) 22 | history.back() 23 | } else { 24 | next() 25 | } 26 | } 27 | }, 28 | { 29 | path: '/', 30 | name: 'Home', 31 | component: () => import('@/views/Home'), 32 | meta: { 33 | keepAlive: true, 34 | disShowLocateMusicBtn: true 35 | } 36 | }, 37 | { 38 | path: '/user', 39 | component: () => import('@/views/User'), 40 | meta: { 41 | disShowLocateMusicBtn: true 42 | } 43 | }, 44 | { 45 | path: '/search', 46 | component: () => import('@/views/Search') 47 | }, 48 | { 49 | path: '/playlist', 50 | component: () => import('@/views/Playlist'), 51 | meta: { 52 | keepAlive: true 53 | } 54 | }, 55 | { 56 | path: '/discover', 57 | component: () => import('@/views/Discover'), 58 | children: [ 59 | { 60 | path: 'playlist', 61 | component: () => import('@/views/Discover/view/Playlist'), 62 | meta: { 63 | keepAlive: true, 64 | key: 'discover' // 根 标签的 key 属性值,默认是 $route.fullPath 65 | } 66 | } 67 | ] 68 | }, 69 | { 70 | path: '/about', 71 | component: () => import('@/views/About') 72 | }, 73 | { 74 | path: '/recommend', 75 | component: () => import('@/views/Recommend'), 76 | meta: { 77 | requiresAuth: true, 78 | keepAlive: true 79 | } 80 | }, 81 | { 82 | path: '/artists', 83 | component: () => import('@/views/Artists'), 84 | meta: { 85 | keepAlive: true 86 | } 87 | }, 88 | { 89 | path: '/album', 90 | component: () => import('@/views/Album'), 91 | meta: { 92 | keepAlive: true 93 | } 94 | }, 95 | { 96 | path: '/cloud', 97 | component: () => import('@/views/Cloud'), 98 | meta: { 99 | requiresAuth: true, 100 | keepAlive: true 101 | } 102 | }, 103 | { 104 | path: '/temporary', 105 | component: () => import('@/views/Temporary'), 106 | meta: { 107 | keepAlive: true 108 | } 109 | }, 110 | { 111 | path: '/css', 112 | component: () => import('@/views/Css') 113 | } 114 | ] 115 | 116 | export default routes 117 | -------------------------------------------------------------------------------- /src/plugins/store/api.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | 3 | export const apiStore = defineStore('api', { 4 | state: () => ({ 5 | api1: '', 6 | api2: '' 7 | }), 8 | persist: true 9 | }) 10 | -------------------------------------------------------------------------------- /src/plugins/store/index.js: -------------------------------------------------------------------------------- 1 | import { createPinia, PiniaVuePlugin } from 'pinia' 2 | import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' 3 | import Vue from 'vue' 4 | 5 | Vue.use(PiniaVuePlugin) 6 | const pinia = createPinia() 7 | pinia.use(piniaPluginPersistedstate) 8 | 9 | export default pinia 10 | -------------------------------------------------------------------------------- /src/plugins/store/player.js: -------------------------------------------------------------------------------- 1 | import http from '@/http' 2 | import { defineStore } from 'pinia' 3 | 4 | export const playerStore = defineStore('player', { 5 | state: () => ({ 6 | isplay: true, // 正在播放 7 | isShow: false, // 显示侧边音乐按键 8 | music: {}, // 正在播放的音乐信息 9 | lyric: { 10 | data: [], 11 | index: 0 12 | }, // 歌词 13 | dt: 0, // 播放进度 14 | route: '', // 路由地址 15 | lists: [], // 默认播放列表 16 | randomlists: [], // 随机播放列表 17 | mode: 0, // 播放模式:单曲0、顺序1、随机2 18 | volume: 10, // 音量 0-10 19 | muted: false // 静音 20 | }), 21 | persist: { 22 | paths: ['mode', 'volume', 'muted'], 23 | overwrite: true 24 | }, 25 | actions: { 26 | // 保存添加歌曲列表时的完整路由地址 27 | saveRoute(params) { 28 | this.route = params 29 | }, 30 | // 添加稍后播放歌曲 31 | addID(id) { 32 | http.song.detail(id).then(res => { 33 | this.removeMusic(res[0].id) 34 | this.lists.splice(this.lists.findIndex(res => res === this.music) + 1, 0, ...res) 35 | this.randomlists.splice(this.randomlists.findIndex(res => res === this.music) + 1, 0, ...res) 36 | }) 37 | }, 38 | // 添加歌曲 39 | addIDs(ids) { 40 | this.isShow = false 41 | http.song.detail(ids).then(res => { 42 | this.lists = [...res] 43 | this.randomlists = [...res] 44 | // Fisher-Yates Shuffle 洗牌算法 45 | for (let i = 1; i < this.randomlists.length; i++) { 46 | const ran = Math.floor(Math.random() * (i + 1)) // [0,i] 47 | ;[this.randomlists[i], this.randomlists[ran]] = [this.randomlists[ran], this.randomlists[i]] 48 | } 49 | this.music = this.mode === 2 ? this.randomlists[0] : this.lists[0] 50 | this.isShow = true 51 | }) 52 | }, 53 | // 上一首 54 | previous() { 55 | const arr = this.mode === 2 ? this.randomlists : this.lists 56 | if (arr.length > 1) { 57 | this.music = arr[(arr.indexOf(this.music) + arr.length - 1) % arr.length] 58 | } 59 | }, 60 | // 下一首 61 | next() { 62 | const arr = this.mode === 2 ? this.randomlists : this.lists 63 | if (arr.length > 1) { 64 | this.music = arr[(arr.indexOf(this.music) + 1) % arr.length] 65 | } 66 | }, 67 | // 选择音乐 68 | chooseMusic(id) { 69 | this.music = this.lists.find(res => res.id === id) 70 | }, 71 | // 移除音乐 72 | removeMusic(id) { 73 | const index = this.lists.findIndex(res => res.id === id) 74 | index !== -1 && this.lists.splice(index, 1) 75 | const _index = this.randomlists.findIndex(res => res.id === id) 76 | _index !== -1 && this.randomlists.splice(_index, 1) 77 | }, 78 | // 存放播放进度 79 | setPlayDt(params) { 80 | this.dt = params 81 | }, 82 | // 存放歌词 83 | setlyricData(params) { 84 | this.lyric.data = [...params] 85 | this.lyric.index = 0 86 | }, 87 | // 存放当前播放歌词的索引 88 | setlyricIndex(params) { 89 | this.lyric.index = params 90 | }, 91 | // 播放、暂停 92 | playORpause(params = 2) { 93 | if (Object.keys(this.music).length) { 94 | setTimeout(() => { 95 | switch (params) { 96 | case false: 97 | this.isplay = false 98 | break 99 | case true: 100 | this.isplay = true 101 | break 102 | default: 103 | this.isplay = !this.isplay 104 | } 105 | }, 100) 106 | } 107 | }, 108 | // 播放模式 109 | playmode(params) { 110 | this.mode = params 111 | }, 112 | // 音量 113 | setVolume(params) { 114 | this.volume = params 115 | }, 116 | // 静音 117 | setMuted(params) { 118 | this.muted = params 119 | } 120 | }, 121 | getters: { 122 | indexMusicInLists() { 123 | return this.lists.indexOf(this.music) 124 | } 125 | } 126 | }) 127 | -------------------------------------------------------------------------------- /src/plugins/store/style.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | 3 | export const styleStore = defineStore('style', { 4 | state: () => ({ 5 | css: '', 6 | isDark: true, 7 | blur: 70, 8 | miniDrawer: false 9 | }), 10 | persist: true, 11 | actions: { 12 | setMiniDrawer(param) { 13 | this.miniDrawer = param 14 | }, 15 | setIsDark(param) { 16 | this.isDark = param 17 | }, 18 | // 设置背景模糊程度 [10,100] 19 | setBlur(param) { 20 | this.blur = Math.min(Math.max(10, this.blur + param), 100) 21 | } 22 | } 23 | }) 24 | -------------------------------------------------------------------------------- /src/plugins/store/user.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | 3 | export const userStore = defineStore('user', { 4 | state: () => ({ 5 | islogin: false, // 登录状态 6 | info: {}, // 用户信息 7 | cookie: '' // 凭证 8 | }), 9 | actions: { 10 | login(params) { 11 | this.islogin = true 12 | this.cookie = params 13 | }, 14 | setInfo(params) { 15 | this.info = params 16 | }, 17 | logout() { 18 | this.islogin = false 19 | this.info = {} 20 | this.cookie = '' 21 | } 22 | }, 23 | persist: true 24 | }) 25 | -------------------------------------------------------------------------------- /src/plugins/vuetify/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuetify from 'vuetify/lib' 3 | import { createSimpleTransition } from 'vuetify/lib/components/transitions/createTransition' 4 | import pinia from '../store' 5 | import { styleStore } from '../store/style' 6 | 7 | Vue.use(Vuetify) 8 | // 创建路由动画标签 9 | Vue.component('my-router-transition', createSimpleTransition('router-transition')) 10 | 11 | const vuetify = new Vuetify() 12 | const style = styleStore(pinia) 13 | // 监听主题色变化 14 | Object.defineProperty(vuetify.framework.theme, 'isDark', { 15 | get() { 16 | return style.isDark 17 | }, 18 | set(param) { 19 | style.setIsDark(param) 20 | setTitleColor(param) 21 | } 22 | }) 23 | 24 | // 设置 PWA 标题栏背景色 25 | function setTitleColor(boolean) { 26 | document.querySelector('meta[name="theme-color"]').setAttribute('content', boolean ? '#272727' : '#ffffff') 27 | } 28 | setTitleColor(style.isDark) 29 | 30 | export default vuetify 31 | -------------------------------------------------------------------------------- /src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import message from '@/common/message' 4 | import { register } from 'register-service-worker' 5 | 6 | if ('serviceWorker' in window.navigator && process.env.NODE_ENV === 'production') { 7 | register('./service-worker.js', { 8 | ready() { 9 | console.log('App is being served from cache by a service worker.\n' + 'For more details, visit https://goo.gl/AFskqB') 10 | }, 11 | registered() { 12 | console.log('Service worker has been registered.') 13 | }, 14 | cached() { 15 | console.log('Content has been cached for offline use.') 16 | }, 17 | updatefound() { 18 | console.log('New content is downloading.') 19 | }, 20 | updated() { 21 | console.log('New content is available; please refresh.') 22 | message({ 23 | text: '新内容可用。点击 刷新 进行更新。', 24 | color: 'primary', 25 | timeout: 10000, 26 | button: { 27 | text: '刷新', 28 | f: () => { 29 | window.history.go() 30 | } 31 | } 32 | }) 33 | }, 34 | offline() { 35 | console.log('No internet connection found. App is running in offline mode.') 36 | }, 37 | error(error) { 38 | console.error('Error during service worker registration:', error) 39 | } 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /src/service-worker.js: -------------------------------------------------------------------------------- 1 | workbox.core.skipWaiting() // 强制等待中的 Service Worker 被激活 2 | workbox.core.clientsClaim() // Service Worker 被激活后使其立即获得页面控制权 3 | workbox.precaching.precacheAndRoute(self.__precacheManifest || []) // 设置预加载 4 | 5 | /** 6 | * 设置资源缓存策略 workbox 主要提供了以下几种缓存策略 7 | * 1、Stale-While-Revalidate:使用缓存的内容(如果可用)尽快响应请求,如果未缓存,则使用网络请求。 然后,网络请求得到数据用于更新缓存。 8 | * 2、Cache First:如果请求命中缓存,则将使用缓存的响应来完成请求,不会使用网络。 如果没有命中缓存,将通过网络请求来获取数据,并且将数据缓存,以便下次直接从缓存获取数据。该模式可以为前端提供快速响应的同时,减轻服务器压力。但是数据的时效性就需要开发者通过设置缓存过期时间或更改service-work.js里面的修订标识来完成缓存文件的更新。 9 | * 3、Network First:优先从网络获取最新数据。 如果成功,它会将数据放入缓存。如果网络无法返回响应,则将使用缓存数据。 10 | * 4、Network Only:只使用网络请求数据。 11 | * 5、Cache Only:只使用缓存数据。 12 | */ 13 | 14 | workbox.routing.registerRoute( 15 | /.*\.css/, 16 | workbox.strategies.staleWhileRevalidate({ 17 | cacheName: 'css' 18 | }) 19 | ) 20 | workbox.routing.registerRoute( 21 | /.*\.js/, 22 | workbox.strategies.staleWhileRevalidate({ 23 | cacheName: 'js' 24 | }) 25 | ) 26 | workbox.routing.registerRoute( 27 | /\.(?:png|gif|jpg|jpeg|svg)$/, 28 | workbox.strategies.staleWhileRevalidate({ 29 | cacheName: 'images', 30 | plugins: [ 31 | new workbox.expiration.Plugin({ 32 | maxEntries: 60, 33 | maxAgeSeconds: 30 * 24 * 60 * 60 34 | }) 35 | ] 36 | }) 37 | ) 38 | -------------------------------------------------------------------------------- /src/styles/main.scss: -------------------------------------------------------------------------------- 1 | // 按键边框隐藏 2 | * { 3 | outline: none; 4 | } 5 | // 隐藏滚动条类 6 | .scrollbar-hidden, 7 | html { 8 | scrollbar-width: none; // Firefox 9 | -ms-overflow-style: none; // IE10+ 10 | &::-webkit-scrollbar { 11 | display: none; // Chrome,Safari 12 | } 13 | } 14 | // 正在播放歌曲项的样式 15 | .playItem { 16 | color: #f4511e; 17 | } 18 | // 路由动画,src\plugins\vuetify中导入可在组件的transition属性或直接当标签使用 19 | .router-transition { 20 | &-enter { 21 | opacity: 0; 22 | } 23 | &-leave { 24 | display: none; 25 | } 26 | &-enter-active { 27 | transition: all 1.5s; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/styles/userCss.js: -------------------------------------------------------------------------------- 1 | import pinia from '@/plugins/store' 2 | import { styleStore } from '@/plugins/store/style' 3 | 4 | // 创建标签 5 | export default (() => { 6 | const tag = document.createElement('style') 7 | tag.setAttribute('type', 'text/css') 8 | tag.setAttribute('id', 'user-css') 9 | tag.innerHTML = styleStore(pinia).css 10 | document.querySelector('head').appendChild(tag) 11 | })() 12 | -------------------------------------------------------------------------------- /src/views/About/components/AboutApi.vue: -------------------------------------------------------------------------------- 1 | 60 | 61 | 85 | -------------------------------------------------------------------------------- /src/views/About/components/AboutGithub.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | -------------------------------------------------------------------------------- /src/views/About/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 21 | -------------------------------------------------------------------------------- /src/views/Album.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 63 | -------------------------------------------------------------------------------- /src/views/Artists.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 77 | -------------------------------------------------------------------------------- /src/views/Cloud.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 62 | -------------------------------------------------------------------------------- /src/views/Css.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 46 | 47 | 52 | -------------------------------------------------------------------------------- /src/views/Discover/components/DiscoverCatlist.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 60 | -------------------------------------------------------------------------------- /src/views/Discover/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 19 | -------------------------------------------------------------------------------- /src/views/Discover/view/Playlist.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 72 | -------------------------------------------------------------------------------- /src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 2 | 55 | 56 | 97 | -------------------------------------------------------------------------------- /src/views/Login/components/LoginPhone.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 132 | -------------------------------------------------------------------------------- /src/views/Login/components/LoginQR.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 72 | -------------------------------------------------------------------------------- /src/views/Login/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 33 | 34 | 68 | -------------------------------------------------------------------------------- /src/views/Playlist/components/PlaylistDetail.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 71 | -------------------------------------------------------------------------------- /src/views/Playlist/components/SkeletonLoader.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 28 | -------------------------------------------------------------------------------- /src/views/Playlist/index.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 76 | -------------------------------------------------------------------------------- /src/views/Recommend.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 26 | -------------------------------------------------------------------------------- /src/views/Search.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 50 | -------------------------------------------------------------------------------- /src/views/Temporary.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 47 | -------------------------------------------------------------------------------- /src/views/User/components/SkeletonLoader.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 25 | -------------------------------------------------------------------------------- /src/views/User/components/UserDetail.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 83 | -------------------------------------------------------------------------------- /src/views/User/components/UserListenRanking.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 37 | -------------------------------------------------------------------------------- /src/views/User/components/UserPlaylist.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 110 | 111 | 134 | -------------------------------------------------------------------------------- /src/views/User/index.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 103 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transpileDependencies: ['vuetify'], 3 | devServer: { 4 | port: 8888 5 | }, 6 | pwa: { 7 | name: 'Vuetify CloudMusic', 8 | themeColor: '#ffffff', 9 | msTileColor: '#ffffff', 10 | manifestOptions: { 11 | display: 'standalone', 12 | background_color: '#ffffff', 13 | description: '网易云音乐第三方Web播放器' 14 | }, 15 | workboxPluginMode: 'InjectManifest', 16 | workboxOptions: { 17 | swSrc: 'src/service-worker.js' 18 | }, 19 | iconPaths: { 20 | favicon32: 'img/icons/favicon-32x32.png', 21 | favicon16: 'img/icons/favicon-16x16.png', 22 | appleTouchIcon: 'img/icons/apple-touch-icon-152x152.png', 23 | maskIcon: 'img/icons/safari-pinned-tab.svg', 24 | msTileImage: 'img/icons/msapplication-icon-144x144.png' 25 | } 26 | }, 27 | configureWebpack: { 28 | module: { 29 | rules: [ 30 | { 31 | test: /\.mjs$/, 32 | include: /node_modules/, 33 | type: 'javascript/auto' 34 | } 35 | ] 36 | } 37 | } 38 | } 39 | --------------------------------------------------------------------------------