├── .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 | [](https://www.murphysec.com/dr/owvCqXgmI2aJ7qkwsD)
4 | [](https://github.com/Flysky12138/vuetify-cloudmusic/commits/master)
5 | 
6 | [](https://github.com/Flysky12138/vuetify-cloudmusic/blob/master/LICENSE)
7 |
8 | ## 预览
9 |
10 | 
11 |
12 | 
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | 非 PC 端
37 |
未适配,不允显示
38 |
39 |
40 |
41 |
42 |
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 |
2 |
14 | {{ text }}
15 |
16 | Close
17 | {{ button.text }}
18 |
19 |
20 |
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 |
2 |
3 |
4 |
5 | mdi-plus-circle-outline
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
{{ item.name }}
20 |
{{ item.trackCount }} 首
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
93 |
--------------------------------------------------------------------------------
/src/components/Button/ButtonDelete.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | mdi-close-circle-outline
6 |
7 |
8 |
9 | {{ name }}
10 |
11 |
12 | 确定删除
21 |
22 |
23 |
24 |
25 |
26 |
36 |
--------------------------------------------------------------------------------
/src/components/Button/ButtonLove.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | mdi-heart
4 | mdi-heart-plus
5 |
6 |
7 |
8 |
49 |
--------------------------------------------------------------------------------
/src/components/Button/ButtonMatch.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | mdi-swap-horizontal-circle-outline
6 |
7 |
8 |
9 |
10 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | mdi-checkbox-marked-circle-outline
28 |
29 |
30 |
31 |
32 | mdi-checkbox-marked-circle-outline
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
87 |
--------------------------------------------------------------------------------
/src/components/Button/ButtonPlay.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
15 | mdi-motion-play-outline
16 |
17 |
18 | {{ tip }}
19 |
20 |
21 |
22 |
48 |
--------------------------------------------------------------------------------
/src/components/Image/ImageAvatar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
25 |
--------------------------------------------------------------------------------
/src/components/Image/ImageCover.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
19 |
--------------------------------------------------------------------------------
/src/components/Introduce.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {{ showAllDescription ? 'mdi-chevron-up' : 'mdi-chevron-down' }}
7 |
8 |
9 |
10 |
11 |
65 |
66 |
77 |
--------------------------------------------------------------------------------
/src/components/Mv.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
17 |
18 |
19 |
20 | mdi-chevron-left
21 |
22 |
23 |
24 |
25 | mdi-chevron-right
26 |
27 |
28 |
29 |
30 |
31 |
32 |
126 |
127 |
135 |
--------------------------------------------------------------------------------
/src/components/MyAudio.vue:
--------------------------------------------------------------------------------
1 |
2 |
15 |
16 |
17 |
145 |
--------------------------------------------------------------------------------
/src/components/Song/SongCard.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | {{ theplayCount }}
13 |
14 |
15 | {{ value.name }}
16 |
17 |
18 |
19 |
45 |
--------------------------------------------------------------------------------
/src/components/Song/SongChip.vue:
--------------------------------------------------------------------------------
1 |
2 | {{ value }}
3 |
4 |
5 |
33 |
--------------------------------------------------------------------------------
/src/components/Song/SongList.vue:
--------------------------------------------------------------------------------
1 |
2 |
21 |
22 |
23 |
24 |
25 | {{ '"' + title + '"' }}
26 |
27 |
28 | {{ ' • ' + filteredItems.length }}
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | vip
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | {{ ' - ' + item.alia[0] }}
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | /
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | {{ $time.song(item.dt) }}
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
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 |
2 |
3 |
4 |
5 | {{ 'Lv.' + info.level }}
6 |
7 |
8 |
9 | mdi-account-outline
10 |
11 |
12 |
13 |
14 |
26 |
--------------------------------------------------------------------------------
/src/layout/AppBar/ButtonRouter.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ item.name }}
5 |
6 |
7 |
8 |
9 |
21 |
--------------------------------------------------------------------------------
/src/layout/AppBar/History.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | mdi-chevron-left
5 |
6 |
7 | mdi-refresh
8 |
9 |
10 | mdi-chevron-right
11 |
12 |
13 |
14 |
15 |
31 |
32 |
40 |
--------------------------------------------------------------------------------
/src/layout/AppBar/Search.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
18 |
19 |
20 |
21 |
22 | {{ item }}
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
76 |
77 |
92 |
--------------------------------------------------------------------------------
/src/layout/AppBar/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
24 |
--------------------------------------------------------------------------------
/src/layout/Drawer/Options.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ name }}
4 |
5 |
6 | {{ item.icon }}
7 |
8 |
9 | {{ item.name }}
10 |
11 |
12 |
13 |
14 |
15 |
74 |
--------------------------------------------------------------------------------
/src/layout/Drawer/Router.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ name }}
4 |
5 |
6 | {{ item.icon }}
7 |
8 |
9 | {{ item.name }}
10 |
11 |
12 |
13 |
14 |
15 |
27 |
--------------------------------------------------------------------------------
/src/layout/Drawer/Tools.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ name }}
4 |
5 |
6 | {{ item.icon }}
7 |
8 |
9 | {{ item.name }}
10 |
11 |
12 |
13 |
14 |
15 |
46 |
--------------------------------------------------------------------------------
/src/layout/Drawer/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | {{ miniDrawer ? 'mdi-share' : 'mdi-reply' }}
22 |
23 |
24 | 改变样式
25 |
26 |
27 |
28 |
29 |
30 |
31 |
76 |
77 |
85 |
--------------------------------------------------------------------------------
/src/layout/SideBar/GoTop.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | mdi-chevron-up
5 |
6 |
7 |
8 |
9 |
28 |
--------------------------------------------------------------------------------
/src/layout/SideBar/LocateMusic.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | mdi-adjust
4 |
5 |
6 |
7 |
17 |
--------------------------------------------------------------------------------
/src/layout/SideBar/Lyrics.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
31 |
32 |
40 |
--------------------------------------------------------------------------------
/src/layout/SideBar/MusicColumn.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | mdi-music-clef-treble
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | mdi-chevron-up
18 |
19 |
20 |
21 |
22 |
23 |
24 | mdi-chevron-down
25 |
26 |
27 |
28 |
29 |
30 |
47 |
48 |
72 |
--------------------------------------------------------------------------------
/src/layout/SideBar/Player/PlayerComment.vue:
--------------------------------------------------------------------------------
1 |
2 | aaaa
3 |
4 |
5 |
10 |
--------------------------------------------------------------------------------
/src/layout/SideBar/Player/PlayerLists.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | mdi-playlist-music-outline
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | 当前播放({{ indexMusicInLists + 1 }}/{{ lists.length }})
18 |
19 |
20 |
21 | mdi-near-me
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | {{ item.name }}
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
74 |
75 |
80 |
--------------------------------------------------------------------------------
/src/layout/SideBar/Player/PlayerLyrics.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {{ item.lyric }}
7 |
8 |
9 | {{ item.tlyric }}
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
95 |
96 |
104 |
--------------------------------------------------------------------------------
/src/layout/SideBar/Player/PlayerMusic/PlayerMusicMode.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ theIcon }}
4 |
5 |
6 |
7 |
40 |
--------------------------------------------------------------------------------
/src/layout/SideBar/Player/PlayerMusic/PlayerMusicPlay.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | mdi-skip-previous
7 |
8 |
9 |
10 |
11 |
12 | {{ isplay ? 'mdi-pause' : 'mdi-play' }}
13 |
14 |
15 |
16 |
17 |
18 | mdi-skip-next
19 |
20 |
21 |
22 |
23 |
24 |
37 |
--------------------------------------------------------------------------------
/src/layout/SideBar/Player/PlayerMusic/PlayerMusicSound.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | mdi-volume-mute
6 | mdi-volume-high
7 |
8 |
9 |
10 |
11 |
12 |
50 |
--------------------------------------------------------------------------------
/src/layout/SideBar/Player/PlayerMusic/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
90 |
--------------------------------------------------------------------------------
/src/layout/SideBar/Player/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | mdi-chevron-down
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
56 |
57 |
65 |
--------------------------------------------------------------------------------
/src/layout/SideBar/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
21 |
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 |
2 |
3 |
4 |
5 | 1、提供的默认接口搭建在
6 | Vercel
7 | 上,访问速度很慢,建议本地运行接口 ↪
8 | NeteaseCloudMusicApi
9 |
10 | 2、已内置解锁VIP和无版权歌曲功能(由于技术原因,不提供默认接口)↪
11 | UnblockNeteaseMusicApi
12 |
13 | UnblockNeteaseMusicApi 获取到的歌曲链接不能跨域,由于 Vercel 不能反代理单次超过5M的数据,而歌曲基本都超过5M,所以我提供不了线上接口。 若像
14 | NeteaseCloudMusicApi 一样使用本地接口方式,会因为 MIXED CONTENT 原因造成 HTTP 强转 HTTPS 而获取不到资源。
15 | 因此你需要更改网站的设置:地址栏左侧的锁按键 > 网站设置 > 不安全内容(允许)
16 |
17 |
18 |
19 |
33 |
34 | {{ item }}
35 |
36 |
37 |
38 |
39 |
52 |
53 | {{ item }}
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
85 |
--------------------------------------------------------------------------------
/src/views/About/components/AboutGithub.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | 本项目开源在
4 | Github
5 |
6 |
7 |
8 |
13 |
--------------------------------------------------------------------------------
/src/views/About/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
21 |
--------------------------------------------------------------------------------
/src/views/Album.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | 歌手:{{ info.artist.name }}
11 | 艺名:{{ info.artist.alias.join('/') }}
12 | 发行时间:{{ $time.dateSort(info.publishTime) }}
13 | 发行公司:{{ info.company }}
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
63 |
--------------------------------------------------------------------------------
/src/views/Artists.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | 艺名:{{ value.alias.join('/') }}
14 | 歌曲:{{ value.musicSize }}
15 | 专辑:{{ value.albumSize }}
16 | 视频:{{ value.mvSize }}
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
77 |
--------------------------------------------------------------------------------
/src/views/Cloud.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
62 |
--------------------------------------------------------------------------------
/src/views/Css.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | mdi-content-save-check
6 |
7 |
8 |
9 |
10 |
46 |
47 |
52 |
--------------------------------------------------------------------------------
/src/views/Discover/components/DiscoverCatlist.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | {{ $route.query.cat || '全部' }}
19 |
20 |
21 |
22 | {{ isShow ? 'mdi-chevron-up' : 'mdi-chevron-down' }}
23 |
24 |
25 |
26 | 热门
27 |
28 |
29 |
30 |
31 |
32 |
60 |
--------------------------------------------------------------------------------
/src/views/Discover/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
19 |
--------------------------------------------------------------------------------
/src/views/Discover/view/Playlist.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
25 |
26 |
27 |
28 |
72 |
--------------------------------------------------------------------------------
/src/views/Home.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | {{ index === 'feature' ? '云音乐特色榜' : '全球媒体榜' }}
18 |
19 |
20 |
21 |
22 |
23 | {{ item.name }}
24 | {{ item.updateFrequency }}
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | mdi-cards-heart
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
97 |
--------------------------------------------------------------------------------
/src/views/Login/components/LoginPhone.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
16 |
17 |
18 |
19 |
20 |
21 |
33 |
34 |
35 |
36 |
37 |
38 |
132 |
--------------------------------------------------------------------------------
/src/views/Login/components/LoginQR.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
72 |
--------------------------------------------------------------------------------
/src/views/Login/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | 登录
11 |
12 |
13 |
14 | 手机号
15 | 二维码
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
68 |
--------------------------------------------------------------------------------
/src/views/Playlist/components/PlaylistDetail.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | {{ value.name }}
13 |
14 |
15 |
16 |
17 | {{ value.nickname }}
18 | 创建于:{{ $time.date(value.createTime) }}
19 |
20 | 播放:{{ value.playCount }}
21 | 分享:{{ value.shareCount }}
22 | 收藏:{{ value.subscribedCount }}
23 |
24 | {{ value.subscribed ? 'mdi-check' : 'mdi-plus' }}
25 |
26 |
27 |
28 |
29 |
30 | 标签:
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
71 |
--------------------------------------------------------------------------------
/src/views/Playlist/components/SkeletonLoader.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
28 |
--------------------------------------------------------------------------------
/src/views/Playlist/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
76 |
--------------------------------------------------------------------------------
/src/views/Recommend.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
26 |
--------------------------------------------------------------------------------
/src/views/Search.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
50 |
--------------------------------------------------------------------------------
/src/views/Temporary.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
47 |
--------------------------------------------------------------------------------
/src/views/User/components/SkeletonLoader.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
25 |
--------------------------------------------------------------------------------
/src/views/User/components/UserDetail.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | {{ value.profile.nickname }}
16 | Lv.{{ value.level }}
17 | {{ theGender() }}
18 |
19 |
20 |
21 | 关注:{{ value.profile.follows }}
22 | 粉丝:{{ value.profile.followeds }}
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | 所在地区:{{ value.profile.province }} -
34 | {{ value.profile.city }}
35 |
36 | 生日:{{ $time.dateSort(value.profile.birthday) }}
37 | 个人介绍:{{ value.profile.signature }}
38 |
39 |
40 |
41 | 注册时间:{{ value.createDays }} 天
42 | 累计听歌:{{ value.listenSongs }} 首
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
83 |
--------------------------------------------------------------------------------
/src/views/User/components/UserListenRanking.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | 听歌排行
9 | 实际播放时间过短的歌曲将不纳入计算
10 |
11 |
12 |
13 |
14 | 所有时间
15 | 最近一周
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
37 |
--------------------------------------------------------------------------------
/src/views/User/components/UserPlaylist.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ title + '(' + value.length + ')' }}
6 |
7 |
8 |
9 |
10 | mdi-pan-left
11 |
12 | {{ page + ' / ' + maxPage }}
13 |
14 | mdi-pan-right
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | mdi-chevron-left
24 |
25 |
26 |
27 |
28 |
29 | mdi-chevron-right
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
110 |
111 |
134 |
--------------------------------------------------------------------------------
/src/views/User/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
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 |
--------------------------------------------------------------------------------