├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── babel.config.js
├── package-lock.json
├── package.json
├── public
├── favicon.ico
└── index.html
├── release.sh
├── src
├── App.vue
├── api
│ ├── album.js
│ ├── artist.js
│ ├── comment.js
│ ├── discovery.js
│ ├── index.js
│ ├── mv.js
│ ├── playlist.js
│ ├── search.js
│ ├── song-list.js
│ ├── song.js
│ └── user.js
├── assets
│ └── image
│ │ ├── play-bar-support.png
│ │ └── play-bar.png
├── base
│ ├── button.vue
│ ├── card.vue
│ ├── confirm.vue
│ ├── empty.vue
│ ├── highlight-text.vue
│ ├── icon.vue
│ ├── loading.vue
│ ├── pagination.vue
│ ├── play-icon.vue
│ ├── progress-bar.vue
│ ├── scroller.vue
│ ├── striped-list.vue
│ ├── tabs.vue
│ ├── title.vue
│ ├── toggle.vue
│ ├── video-player.vue
│ └── volume.vue
├── components
│ ├── comment.vue
│ ├── comments.vue
│ ├── mini-player.vue
│ ├── mv-card.vue
│ ├── player.vue
│ ├── playlist-card.vue
│ ├── playlist.vue
│ ├── routes-history.vue
│ ├── search.vue
│ ├── share-reader.vue
│ ├── share.vue
│ ├── song-card.vue
│ ├── song-table.vue
│ ├── theme.vue
│ ├── top-playlist-card.vue
│ ├── user.vue
│ └── with-pagination.vue
├── layout
│ ├── header.vue
│ ├── index.vue
│ └── menu.vue
├── main.js
├── page
│ ├── discovery
│ │ ├── banner.vue
│ │ ├── index.vue
│ │ ├── new-mvs.vue
│ │ ├── new-playlists.vue
│ │ └── new-songs.vue
│ ├── mv
│ │ └── index.vue
│ ├── mvs
│ │ └── index.vue
│ ├── playlist-detail
│ │ ├── header.vue
│ │ └── index.vue
│ ├── playlists
│ │ └── index.vue
│ ├── search
│ │ ├── index.vue
│ │ ├── mvs.vue
│ │ ├── playlists.vue
│ │ └── songs.vue
│ └── songs
│ │ └── index.vue
├── router.js
├── store
│ ├── helper
│ │ ├── global.js
│ │ ├── music.js
│ │ └── user.js
│ ├── index.js
│ └── modules
│ │ ├── global
│ │ ├── index.js
│ │ ├── mutations.js
│ │ └── state.js
│ │ ├── music
│ │ ├── actions.js
│ │ ├── getters.js
│ │ ├── index.js
│ │ ├── mutations.js
│ │ └── state.js
│ │ └── user
│ │ ├── actions.js
│ │ ├── getters.js
│ │ ├── index.js
│ │ ├── mutations.js
│ │ └── state.js
├── style
│ ├── app.scss
│ ├── element-overwrite.scss
│ ├── index.scss
│ ├── mixin.scss
│ ├── reset.scss
│ ├── themes
│ │ ├── variables-red.js
│ │ ├── variables-white.js
│ │ └── variables.js
│ └── variables.scss
└── utils
│ ├── axios.js
│ ├── business.js
│ ├── common.js
│ ├── config.js
│ ├── dom.js
│ ├── global.js
│ ├── index.js
│ ├── lrcparse.js
│ ├── mixin.js
│ └── rem.js
└── vue.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 |
5 | # local env files
6 | .env.local
7 | .env.*.local
8 |
9 | # Log files
10 | npm-debug.log*
11 | yarn-debug.log*
12 | yarn-error.log*
13 |
14 | # Editor directories and files
15 | .idea
16 | .vscode
17 | *.suo
18 | *.ntvs*
19 | *.njsproj
20 | *.sln
21 | *.sw?
22 |
23 | # custom
24 | music
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 🎵 基于 Vue2、Vue-CLI3 的高仿网易云 mac 客户端播放器(PC) Online Music Player
2 |
3 | 音乐播放器虽然烂大街了,但是作为前端没自己撸一个一直是个遗憾。
4 |
5 | 偶然间发现 PC 端 Web 版的网易云音乐做的实在是太简陋了,社区仿 PC 客户端的网易云也不多见。
6 |
7 | 为了弥补这个遗憾,就用 Vue 全家桶模仿 mac 客户端的 UI 实现了一个,欢迎提出意见和 star🌟~
8 |
9 | 💐[预览地址](https://ssh-music.vercel.app)
10 |
11 | 💐[源码地址](https://github.com/sl1673495/vue-netease-music)
12 |
13 | ### 进度
14 |
15 | - [x] mv 页(3.0 新增)
16 | - [x] cd 页 (2.0 新增)
17 | - [x] 搜索建议
18 | - [x] 搜索详情
19 | - [x] 播放(版权歌曲无法播放)
20 | - [x] 发现页
21 | - [x] 播放列表
22 | - [x] 播放记录
23 | - [x] 全部歌单
24 | - [x] 歌单详情
25 | - [x] 最新音乐
26 | - [x] 主题换肤功能
27 | - [x] 登录(网易云 uid 登录)
28 |
29 | ### 后端接口
30 |
31 | https://binaryify.github.io/NeteaseCloudMusicApi
32 |
33 | ### 技术栈
34 |
35 | - **_Vue_** 全家桶 通过 Vue-CLI3 初始化生成。
36 | - **_ElementUI_** 魔改样式。
37 | - **_better-scroll_** 歌词滚动部分用了黄轶老大的 (贼爽)
38 | - **_CSS Variables_** 主题换肤。
39 | - **_ESNext_** (JavaScript 语言的下一代标准)
40 | - **_Sass_**(CSS 预处理器)
41 | - **_postcss-pxtorem_**(自动处理 rem,妈妈再也不用担心屏幕太大太小了)
42 | - **_workbox-webpack-plugin_** 谷歌开发的利用 Service Worker 实现页面预缓存的插件。
43 |
44 | ### Screenshots
45 |
46 | 
47 |
48 | 
49 |
50 | 
51 |
52 | 
53 |
54 | ### 安装与使用
55 |
56 | ```
57 | npm i
58 | npm run dev
59 | ```
60 |
61 | ### 友情链接
62 |
63 | [mmPlayer](https://github.com/maomao1996/Vue-mmPlayer)
64 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: ['@vue/app'],
3 | plugins: [
4 | [
5 | 'component',
6 | {
7 | libraryName: 'element-ui',
8 | styleLibraryName: 'theme-chalk'
9 | }
10 | ]
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue-netease-muisc",
3 | "version": "3.4.2",
4 | "private": true,
5 | "scripts": {
6 | "dev": "npm run serve",
7 | "serve": "vue-cli-service serve",
8 | "build": "vue-cli-service build",
9 | "publish": "npm run build && cd music && now",
10 | "lint": "vue-cli-service lint",
11 | "release-f": "standard-version -f",
12 | "release-major": "standard-version -r major",
13 | "release-minor": "standard-version -r minor",
14 | "release-patch": "standard-version -r patch",
15 | "release": "sh release.sh"
16 | },
17 | "dependencies": {
18 | "@better-scroll/core": "^2.0.0-alpha.20",
19 | "@better-scroll/mouse-wheel": "^2.0.0-alpha.20",
20 | "@better-scroll/scroll-bar": "^2.0.0-alpha.20",
21 | "axios": "^0.21.1",
22 | "better-scroll": "^1.15.2",
23 | "clipboard": "^2.0.4",
24 | "core-js": "^2.6.5",
25 | "element-ui": "^2.10.1",
26 | "good-storage": "^1.1.0",
27 | "lodash-es": "^4.17.15",
28 | "sass": "^1.56.0",
29 | "vue": "^2.6.10",
30 | "vue-lazyload": "^1.3.3",
31 | "vue-meta": "^2.2.1",
32 | "vue-router": "^3.0.3",
33 | "vuex": "^3.0.1",
34 | "xgplayer": "^2.1.9"
35 | },
36 | "devDependencies": {
37 | "@vue/cli-plugin-babel": "^3.9.0",
38 | "@vue/cli-plugin-eslint": "^3.9.0",
39 | "@vue/cli-service": "^3.9.0",
40 | "babel-eslint": "^10.0.1",
41 | "babel-plugin-component": "^1.1.1",
42 | "commitizen": "^4.0.3",
43 | "concurrently": "^4.1.2",
44 | "cross-env": "^7.0.3",
45 | "cz-conventional-changelog": "^3.0.2",
46 | "eslint": "^5.16.0",
47 | "eslint-plugin-vue": "^5.0.0",
48 | "now": "^17.0.4",
49 | "postcss-pxtorem": "^4.0.1",
50 | "sass-loader": "^7.1.0",
51 | "standard-changelog": "^2.0.13",
52 | "standard-version": "^8.0.1",
53 | "vue-template-compiler": "^2.6.10",
54 | "workbox-webpack-plugin": "^5.1.3"
55 | },
56 | "eslintConfig": {
57 | "root": true,
58 | "env": {
59 | "node": true
60 | },
61 | "extends": [
62 | "plugin:vue/essential",
63 | "eslint:recommended"
64 | ],
65 | "parserOptions": {
66 | "parser": "babel-eslint"
67 | }
68 | },
69 | "postcss": {
70 | "plugins": {
71 | "autoprefixer": {},
72 | "postcss-pxtorem": {
73 | "rootValue": 14,
74 | "propList": [
75 | "*"
76 | ]
77 | }
78 | }
79 | },
80 | "browserslist": [
81 | "> 1%",
82 | "last 2 versions"
83 | ],
84 | "config": {
85 | "commitizen": {
86 | "path": "./node_modules/cz-conventional-changelog"
87 | }
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sl1673495/vue-netease-music/b288c3b6791f206bab3508295348ecf9a9b33bbf/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
17 |
18 |
19 |
28 |
29 |
30 |
31 |
32 |
33 | <% if ( NODE_ENV === 'production' ) { %>
34 |
35 |
36 |
37 |
38 | <%} %>
39 |
40 |
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/release.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | echoCommon()
4 | {
5 | echo -e "\033[33m $1 \033[0m"
6 | }
7 |
8 | echoFail()
9 | {
10 | echo -e "\033[31m $1 \033[0m"
11 | }
12 |
13 | echoSuccess()
14 | {
15 | echo -e "\033[32m $1 \033[0m"
16 | }
17 |
18 | git pull
19 | echoSuccess 'git pull success'
20 | git add .
21 | echoSuccess 'git add success'
22 | echo
23 | git cz
24 | echoSuccess 'commit success'
25 | echo
26 | echoCommon "选择要发布的方式?"
27 | select var in release-major release-minor release-patch;
28 | do
29 | break
30 | done
31 |
32 | npm run $var
33 |
34 | if [ $? -eq 0 ]; then
35 | echoSuccess "release success"
36 | echoCommon "start building"
37 | git push --follow-tags origin master
38 | npm run build
39 | else
40 | echoFail "release failed"
41 | fi
42 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
27 |
28 |
35 |
--------------------------------------------------------------------------------
/src/api/album.js:
--------------------------------------------------------------------------------
1 | import { request } from '@/utils'
2 |
3 | export const getAlbum = id => request.get(`/album?id=${id}`)
--------------------------------------------------------------------------------
/src/api/artist.js:
--------------------------------------------------------------------------------
1 | import { request } from '@/utils'
2 |
3 | export const getArtists = id => request.get(`/artists?id=${id}`)
4 |
--------------------------------------------------------------------------------
/src/api/comment.js:
--------------------------------------------------------------------------------
1 | import { requestWithoutLoading } from '@/utils'
2 |
3 | // 歌曲评论
4 | export const getSongComment = params =>
5 | requestWithoutLoading.get(`/comment/music`, { params })
6 | // 歌单评论
7 | export const getPlaylistComment = params =>
8 | requestWithoutLoading.get(`/comment/playlist`, { params })
9 | // 热门评论
10 | export const getHotComment = params =>
11 | requestWithoutLoading.get(`/comment/hot`, { params })
12 | // mv评论
13 | export const getMvComment = params =>
14 | requestWithoutLoading.get('/comment/mv', { params })
15 |
--------------------------------------------------------------------------------
/src/api/discovery.js:
--------------------------------------------------------------------------------
1 | import { request } from '@/utils'
2 |
3 | export const getBanner = () => request.get('/banner?type=0')
4 |
5 | export const getNewSongs = () => request.get('/personalized/newsong')
6 |
7 | export const getPersonalized = params =>
8 | request.get(`/personalized`, { params })
9 |
10 | export const getPersonalizedMv = () => request.get(`/personalized/mv`)
--------------------------------------------------------------------------------
/src/api/index.js:
--------------------------------------------------------------------------------
1 | export * from './album'
2 | export * from './comment'
3 | export * from './discovery'
4 | export * from './mv'
5 | export * from './playlist'
6 | export * from './search'
7 | export * from './song-list'
8 | export * from './song'
9 | export * from './user'
10 | export * from './artist'
11 |
--------------------------------------------------------------------------------
/src/api/mv.js:
--------------------------------------------------------------------------------
1 | import { request } from '@/utils'
2 |
3 | export const getMvDetail = id => request.get(`/mv/detail?mvid=${id}`)
4 |
5 | export const getMvUrl = id => request.get(`/mv/url?id=${id}`)
6 |
7 | export const getSimiMv = id => request.get(`/simi/mv?mvid=${id}`)
8 |
9 | export const getAllMvs = (params) => request.get(`/mv/all`, {params})
--------------------------------------------------------------------------------
/src/api/playlist.js:
--------------------------------------------------------------------------------
1 | import { request, requestWithoutLoading } from '@/utils'
2 |
3 | // 获取歌单
4 | export const getPlaylists = (params) => request.get('/top/playlist', { params })
5 | // 精品歌单
6 | export const getTopPlaylists = (params) => request.get('/top/playlist/highquality', { params })
7 | // 获取相似歌单
8 | export const getSimiPlaylists = (id, option) => requestWithoutLoading.get(`/simi/playlist?id=${id}`, option)
--------------------------------------------------------------------------------
/src/api/search.js:
--------------------------------------------------------------------------------
1 | import { request } from "@/utils";
2 |
3 | export const getSearchHot = () => request.get('/search/hot')
4 |
5 | export const getSearchSuggest = (keywords) => request.get('/search/suggest', { params: { keywords } })
6 |
7 | export const getSearch = (params) => request.get(`/search`, { params })
--------------------------------------------------------------------------------
/src/api/song-list.js:
--------------------------------------------------------------------------------
1 | import { request } from '@/utils'
2 |
3 | export const getListDetail = params =>
4 | request.get('/playlist/detail', { params })
5 |
--------------------------------------------------------------------------------
/src/api/song.js:
--------------------------------------------------------------------------------
1 | import { request, requestWithoutLoading } from '@/utils'
2 |
3 | // 获取音乐url
4 | export const getSongUrl = id => request.get(`/song/url?id=${id}`)
5 |
6 | // 获取音乐详情
7 | export const getSongDetail = ids => request.get(`/song/detail?ids=${ids}`)
8 |
9 | // 新歌速递
10 | export const getTopSongs = type => request.get(`/top/song?type=${type}`)
11 |
12 | // 相似音乐
13 | export const getSimiSongs = (id, option) =>
14 | requestWithoutLoading.get(`/simi/song?id=${id}`, option)
15 |
16 | // 歌词
17 | export const getLyric = id => request.get(`/lyric?id=${id}`)
18 |
--------------------------------------------------------------------------------
/src/api/user.js:
--------------------------------------------------------------------------------
1 | import { requestWithoutLoading } from '@/utils'
2 |
3 | export const getUserDetail = (uid) => requestWithoutLoading.get("/user/detail", { params: { uid } })
4 |
5 | const PLAYLIST_LIMIT = 1000
6 | export const getUserPlaylist = (uid) => requestWithoutLoading.get("/user/playlist", { params: { uid, limit: PLAYLIST_LIMIT } })
--------------------------------------------------------------------------------
/src/assets/image/play-bar-support.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sl1673495/vue-netease-music/b288c3b6791f206bab3508295348ecf9a9b33bbf/src/assets/image/play-bar-support.png
--------------------------------------------------------------------------------
/src/assets/image/play-bar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sl1673495/vue-netease-music/b288c3b6791f206bab3508295348ecf9a9b33bbf/src/assets/image/play-bar.png
--------------------------------------------------------------------------------
/src/base/button.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
10 |
26 |
27 |
41 |
--------------------------------------------------------------------------------
/src/base/card.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
![]()
6 |
7 |
8 |
9 |
10 |
{{ name }}
11 |
12 | {{ desc }}
13 |
14 |
15 |
16 |
17 |
18 |
32 |
33 |
73 |
--------------------------------------------------------------------------------
/src/base/confirm.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 | {{title || '提示'}}
9 | {{text}}
10 |
20 |
21 |
22 |
23 |
76 |
77 |
91 |
--------------------------------------------------------------------------------
/src/base/empty.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | 暂无内容
4 |
5 |
6 |
7 |
12 |
13 |
20 |
--------------------------------------------------------------------------------
/src/base/highlight-text.vue:
--------------------------------------------------------------------------------
1 |
34 |
35 |
40 |
--------------------------------------------------------------------------------
/src/base/icon.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
71 |
72 |
101 |
--------------------------------------------------------------------------------
/src/base/loading.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
18 |
19 |
24 |
--------------------------------------------------------------------------------
/src/base/pagination.vue:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
16 |
22 |
23 |
28 |
--------------------------------------------------------------------------------
/src/base/play-icon.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
11 |
12 |
13 |
14 |
34 |
35 |
46 |
--------------------------------------------------------------------------------
/src/base/progress-bar.vue:
--------------------------------------------------------------------------------
1 |
2 |
23 |
24 |
25 |
95 |
96 |
145 |
--------------------------------------------------------------------------------
/src/base/scroller.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
10 |
65 |
66 |
78 |
--------------------------------------------------------------------------------
/src/base/striped-list.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
42 |
43 |
--------------------------------------------------------------------------------
/src/base/tabs.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
17 | {{tab.title}}
18 |
19 |
20 |
21 | -
29 | {{tab.title}}
30 |
31 |
32 |
33 |
34 |
35 |
142 |
143 |
225 |
--------------------------------------------------------------------------------
/src/base/title.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
14 |
15 |
--------------------------------------------------------------------------------
/src/base/toggle.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
61 |
62 |
64 |
--------------------------------------------------------------------------------
/src/base/video-player.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
51 |
52 |
--------------------------------------------------------------------------------
/src/base/volume.vue:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
65 |
66 |
82 |
--------------------------------------------------------------------------------
/src/components/comment.vue:
--------------------------------------------------------------------------------
1 |
2 |
28 |
29 |
30 |
35 |
36 |
94 |
--------------------------------------------------------------------------------
/src/components/comments.vue:
--------------------------------------------------------------------------------
1 |
2 |
47 |
48 |
49 |
151 |
152 |
171 |
--------------------------------------------------------------------------------
/src/components/mini-player.vue:
--------------------------------------------------------------------------------
1 | // 底部播放栏组件
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
![]()
10 |
11 |
12 |
13 |
14 |
15 |
16 |
{{ currentSong.name }}
17 |
-
18 |
{{ currentSong.artistsText }}
19 |
20 |
21 | {{
22 | $utils.formatTime(currentTime)
23 | }}
24 | /
25 | {{
26 | $utils.formatTime(currentSong.duration / 1000)
27 | }}
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
41 | 请点击开始播放。
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | {{ playModeText }}
56 |
63 |
64 |
65 |
71 | 已更新歌单
72 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
94 |
101 |
102 |
103 |
104 |
281 |
282 |
427 |
--------------------------------------------------------------------------------
/src/components/mv-card.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
![]()
5 |
6 |
7 | {{ $utils.formatNumber(playCount) }}
8 |
9 |
12 |
13 | {{ $utils.formatTime(duration / 1000) }}
14 |
15 |
16 |
{{ name }}
17 |
{{ author }}
18 |
19 |
20 |
21 |
35 |
36 |
107 |
--------------------------------------------------------------------------------
/src/components/player.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 |

15 |

20 |
24 |
29 |
30 |
![]()
31 |
32 |
33 |
34 |
35 |
36 |
37 |
{{currentSong.name}}
38 |
MV
43 |
44 |
45 |
46 |
歌手:
47 |
{{currentSong.artistsText}}
48 |
49 |
50 |
还没有歌词哦~
51 |
59 |
74 |
75 |
76 |
77 |
78 |
79 |
84 |
85 |
89 |
93 |
94 |
98 |
包含这首歌的歌单
99 |
104 |
109 |
110 |
111 |
116 |
{{$utils.formatNumber(simiPlaylist.playCount)}}
117 |
118 |
119 |
120 |
121 |
122 |
126 |
相似歌曲
127 |
132 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
385 |
386 |
616 |
--------------------------------------------------------------------------------
/src/components/playlist-card.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
![]()
5 |
6 | {{ desc }}
7 |
8 |
9 |
10 |
{{ name }}
11 |
12 |
13 |
14 |
24 |
25 |
94 |
--------------------------------------------------------------------------------
/src/components/playlist.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
12 |
17 |
28 |
29 |
33 |
37 |
38 | 你还没有添加任何歌曲
42 |
43 |
44 |
45 |
46 |
47 |
90 |
91 |
142 |
--------------------------------------------------------------------------------
/src/components/routes-history.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
14 |
15 |
16 |
17 |
30 |
31 |
41 |
--------------------------------------------------------------------------------
/src/components/search.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
12 |
16 |
20 |
24 |
29 |
30 |
34 | {{normalizedSuggest.title}}
35 |
36 |
49 |
50 |
51 |
55 |
56 |
热门搜索
57 |
58 | {{hot.first}}
64 |
65 |
66 |
67 |
搜索历史
68 |
72 | {{history.first}}
78 |
79 |
暂无搜索历史
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
217 |
218 |
284 |
--------------------------------------------------------------------------------
/src/components/share-reader.vue:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/share.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
12 |
13 |
14 |
33 |
--------------------------------------------------------------------------------
/src/components/song-card.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ $utils.pad(order) }}
5 |
6 |
7 |
![]()
8 |
9 |
10 |
11 |
{{ name }}
12 |
{{ artistsText }}
13 |
14 |
15 |
16 |
17 |
22 |
23 |
76 |
--------------------------------------------------------------------------------
/src/components/song-table.vue:
--------------------------------------------------------------------------------
1 |
231 |
232 |
293 |
--------------------------------------------------------------------------------
/src/components/theme.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
15 |
19 |
{{themeValue.title}}
20 |
21 |
22 |
27 |
28 |
29 |
30 |
31 |
90 |
91 |
110 |
--------------------------------------------------------------------------------
/src/components/top-playlist-card.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
![]()
6 |
7 |
8 |
9 | 精品歌单
10 |
11 |
{{ name }}
12 |
{{ desc }}
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
30 |
31 |
91 |
--------------------------------------------------------------------------------
/src/components/user.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
10 |
![]()
11 |
{{ user.nickname }}
12 |
13 |
14 |
15 |
20 | 登录
21 |
22 |
27 |
28 |
29 | 1、请
30 | 点我(http://music.163.com)打开网易云音乐官网
33 |
34 |
2、点击页面右上角的“登录”
35 |
3、点击您的头像,进入我的主页
36 |
37 | 4、复制浏览器地址栏 /user/home?id= 后面的数字(网易云 UID)
38 |
39 |
40 |
41 |
50 |
51 |
52 |
53 |
54 |
109 |
110 |
163 |
--------------------------------------------------------------------------------
/src/components/with-pagination.vue:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
16 |
78 |
79 |
84 |
--------------------------------------------------------------------------------
/src/layout/header.vue:
--------------------------------------------------------------------------------
1 |
2 |
59 |
60 |
61 |
95 |
96 |
186 |
--------------------------------------------------------------------------------
/src/layout/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
19 |
20 |
21 |
45 |
46 |
73 |
--------------------------------------------------------------------------------
/src/layout/menu.vue:
--------------------------------------------------------------------------------
1 |
2 |
27 |
28 |
29 |
63 |
64 |
120 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 |
3 | import App from './App.vue'
4 | import router from './router'
5 |
6 | import '@/style/index.scss'
7 | import '@/utils/rem'
8 | import '@/utils/axios'
9 | import store from './store/index'
10 | import global from './utils/global'
11 |
12 | Vue.config.productionTip = false
13 | Vue.use(global)
14 |
15 | new Vue({
16 | router,
17 | store,
18 | render: h => h(App),
19 | }).$mount('#app')
20 |
--------------------------------------------------------------------------------
/src/page/discovery/banner.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
10 |
11 |
12 |
26 |
27 |
40 |
--------------------------------------------------------------------------------
/src/page/discovery/index.vue:
--------------------------------------------------------------------------------
1 | // 发现音乐页面
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
23 |
24 |
29 |
--------------------------------------------------------------------------------
/src/page/discovery/new-mvs.vue:
--------------------------------------------------------------------------------
1 |
2 |
23 |
24 |
25 |
46 |
47 |
52 |
--------------------------------------------------------------------------------
/src/page/discovery/new-playlists.vue:
--------------------------------------------------------------------------------
1 |
2 |
18 |
19 |
20 |
37 |
38 |
--------------------------------------------------------------------------------
/src/page/discovery/new-songs.vue:
--------------------------------------------------------------------------------
1 |
2 |
24 |
25 |
26 |
92 |
93 |
--------------------------------------------------------------------------------
/src/page/mv/index.vue:
--------------------------------------------------------------------------------
1 | // mv详情页面
2 |
3 |
4 |
5 |
6 |
mv详情
7 |
8 |
9 |
14 |
15 |
16 |
17 |
18 |
![]()
19 |
20 |
{{ artist.name }}
21 |
22 |
23 |
{{ mvDetail.name }}
24 |
25 |
26 | 发布:{{
28 | $utils.formatDate(mvDetail.publishTime, "yyyy-MM-dd")
29 | }}
31 | 播放:{{ mvDetail.playCount }}次
32 |
33 |
34 |
37 |
38 |
39 |
相关推荐
40 |
41 |
49 |
50 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
132 |
133 |
212 |
--------------------------------------------------------------------------------
/src/page/mvs/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 | 地区:
8 |
14 |
15 |
16 | 类型:
17 |
23 |
24 |
25 | 排序:
26 |
32 |
33 |
41 |
57 |
58 |
59 |
60 |
61 |
108 |
109 |
127 |
--------------------------------------------------------------------------------
/src/page/playlist-detail/header.vue:
--------------------------------------------------------------------------------
1 |
2 |
33 |
34 |
35 |
63 |
64 |
152 |
--------------------------------------------------------------------------------
/src/page/playlist-detail/index.vue:
--------------------------------------------------------------------------------
1 | // 歌单详情页面
2 |
3 |
4 |
5 |
6 |
7 |
17 |
18 |
19 | 未能找到和
20 | “{{ searchValue }}”
21 | 相关的任何音乐
22 |
23 |
29 |
32 |
33 |
34 |
35 |
132 |
133 |
171 |
--------------------------------------------------------------------------------
/src/page/playlists/index.vue:
--------------------------------------------------------------------------------
1 | // 推荐歌单页面
2 |
3 |
7 |
11 |
17 |
18 |
19 |
26 |
27 |
37 |
44 |
45 |
46 |
47 |
123 |
124 |
144 |
--------------------------------------------------------------------------------
/src/page/search/index.vue:
--------------------------------------------------------------------------------
1 | // 搜索详情页面
2 |
3 |
4 |
11 |
12 |
16 |
17 |
暂无结果
21 |
22 |
23 |
24 |
25 |
72 |
73 |
102 |
--------------------------------------------------------------------------------
/src/page/search/mvs.vue:
--------------------------------------------------------------------------------
1 |
2 |
29 |
30 |
31 |
65 |
66 |
75 |
--------------------------------------------------------------------------------
/src/page/search/playlists.vue:
--------------------------------------------------------------------------------
1 |
2 |
23 |
24 |
25 |
59 |
60 |
72 |
--------------------------------------------------------------------------------
/src/page/search/songs.vue:
--------------------------------------------------------------------------------
1 | // 搜索详情页面
2 |
3 |
4 |
12 |
13 |
19 |
20 |
21 |
22 |
23 |
24 |
88 |
89 |
106 |
--------------------------------------------------------------------------------
/src/page/songs/index.vue:
--------------------------------------------------------------------------------
1 | // 最新音乐页面
2 |
3 |
18 |
19 |
20 |
71 |
72 |
81 |
--------------------------------------------------------------------------------
/src/router.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Router from 'vue-router'
3 |
4 | const Discovery = () => import(/* webpackChunkName: "Discovery" */ '@/page/discovery')
5 | const PlaylistDetail = () => import(/* webpackChunkName: "PlaylistDetail" */ '@/page/playlist-detail')
6 | const Playlists = () => import(/* webpackChunkName: "Playlists" */ '@/page/playlists')
7 | const Songs = () => import(/* webpackChunkName: "Songs" */ '@/page/songs')
8 | const Search = () => import(/* webpackChunkName: "Search" */ '@/page/search')
9 | const SearchSongs = () => import(/* webpackChunkName: "SearchSongs" */ '@/page/search/songs')
10 | const SearchPlaylists = () => import(/* webpackChunkName: "SearchPlaylists" */ '@/page/search/playlists')
11 | const SearchMvs = () => import(/* webpackChunkName: "SearchMvs" */ '@/page/search/mvs')
12 |
13 | const Mvs = () => import(/* webpackChunkName: "Mvs" */ '@/page/mvs')
14 | const Mv = () => import(/* webpackChunkName: "Mv" */ '@/page/mv')
15 |
16 | // 内容需要居中的页面
17 | export const layoutCenterNames = ['discovery', 'playlists', 'songs', 'mvs']
18 |
19 | // 需要显示在侧边栏菜单的页面
20 | export const menuRoutes = [
21 | {
22 | path: '/discovery',
23 | name: 'discovery',
24 | component: Discovery,
25 | meta: {
26 | title: '发现音乐',
27 | icon: 'music',
28 | },
29 | },
30 | {
31 | path: '/playlists',
32 | name: 'playlists',
33 | component: Playlists,
34 | meta: {
35 | title: '推荐歌单',
36 | icon: 'playlist-menu',
37 | },
38 | },
39 | {
40 | path: '/songs',
41 | name: 'songs',
42 | component: Songs,
43 | meta: {
44 | title: '最新音乐',
45 | icon: 'yinyue',
46 | },
47 | },
48 | {
49 | path: '/mvs',
50 | name: 'mvs',
51 | component: Mvs,
52 | meta: {
53 | title: '最新MV',
54 | icon: 'mv',
55 | },
56 | },
57 | ]
58 |
59 | Vue.use(Router)
60 |
61 | export default new Router({
62 | mode: 'hash',
63 | routes: [
64 | {
65 | path: '/',
66 | redirect: '/discovery',
67 | },
68 | {
69 | path: '/playlist/:id',
70 | name: 'playlist',
71 | component: PlaylistDetail,
72 | },
73 | {
74 | path: '/search/:keywords',
75 | name: 'search',
76 | component: Search,
77 | props: true,
78 | children: [
79 | {
80 | path: '/',
81 | redirect: 'songs',
82 | },
83 | {
84 | path: 'songs',
85 | name: 'searchSongs',
86 | component: SearchSongs,
87 | },
88 | {
89 | path: 'playlists',
90 | name: 'searchPlaylists',
91 | component: SearchPlaylists,
92 | },
93 | {
94 | path: 'mvs',
95 | name: 'searchMvs',
96 | component: SearchMvs,
97 | },
98 | ],
99 | },
100 | {
101 | path: '/mv/:id',
102 | name: 'mv',
103 | component: Mv,
104 | props: (route) => ({id: +route.params.id}),
105 | },
106 | ...menuRoutes,
107 | ],
108 | })
109 |
--------------------------------------------------------------------------------
/src/store/helper/global.js:
--------------------------------------------------------------------------------
1 | import { createNamespacedHelpers } from 'vuex'
2 |
3 | export const { mapState, mapMutations, mapGetters, mapActions } = createNamespacedHelpers('global')
--------------------------------------------------------------------------------
/src/store/helper/music.js:
--------------------------------------------------------------------------------
1 | import { createNamespacedHelpers } from 'vuex'
2 |
3 | export const { mapState, mapMutations, mapGetters, mapActions } = createNamespacedHelpers('music')
--------------------------------------------------------------------------------
/src/store/helper/user.js:
--------------------------------------------------------------------------------
1 | import { createNamespacedHelpers } from 'vuex'
2 |
3 | export const { mapState, mapMutations, mapGetters, mapActions } = createNamespacedHelpers('user')
--------------------------------------------------------------------------------
/src/store/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Vuex from 'vuex'
3 | import musicModule from './modules/music'
4 | import userModule from './modules/user'
5 | import globalModule from './modules/global'
6 | import createLogger from 'vuex/dist/logger'
7 |
8 | Vue.use(Vuex)
9 | const debug = process.env.NODE_ENV !== 'production'
10 |
11 | export default new Vuex.Store({
12 | modules: {
13 | music: musicModule,
14 | user: userModule,
15 | global: globalModule,
16 | },
17 | plugins: debug ? [createLogger()] : []
18 | })
19 |
--------------------------------------------------------------------------------
/src/store/modules/global/index.js:
--------------------------------------------------------------------------------
1 | import state from './state'
2 | import mutations from './mutations'
3 |
4 | export default {
5 | namespaced: true,
6 | state,
7 | mutations,
8 | }
--------------------------------------------------------------------------------
/src/store/modules/global/mutations.js:
--------------------------------------------------------------------------------
1 | export default {
2 | setAxiosLoading(state, loading) {
3 | state.axiosLoading = loading
4 | },
5 | }
6 |
--------------------------------------------------------------------------------
/src/store/modules/global/state.js:
--------------------------------------------------------------------------------
1 | export default {
2 | axiosLoading: false,
3 | }
4 |
--------------------------------------------------------------------------------
/src/store/modules/music/actions.js:
--------------------------------------------------------------------------------
1 | import storage from 'good-storage'
2 | import { PLAY_HISTORY_KEY, getSongImg } from '@/utils'
3 |
4 | export default {
5 | // 整合歌曲信息 并且开始播放
6 | async startSong({ commit, state }, rawSong) {
7 | // 浅拷贝一份 改变引用
8 | // 1.不污染元数据
9 | // 2.单曲循环为了触发watch
10 | const song = Object.assign({}, rawSong)
11 | if (!song.img) {
12 | if (song.albumId) {
13 | song.img = await getSongImg(song.id, song.albumId)
14 | }
15 | }
16 | commit('setCurrentSong', song)
17 | commit('setPlayingState', true)
18 | // 历史记录
19 | const { playHistory } = state
20 | const playHistoryCopy = playHistory.slice()
21 | const findedIndex = playHistoryCopy.findIndex(({ id }) => song.id === id)
22 | if (findedIndex !== -1) {
23 | // 删除旧那一项, 插入到最前面
24 | playHistoryCopy.splice(findedIndex, 1)
25 | }
26 | playHistoryCopy.unshift(song)
27 | commit('setPlayHistory', playHistoryCopy)
28 | storage.set(PLAY_HISTORY_KEY, playHistoryCopy)
29 | },
30 | clearCurrentSong({ commit }) {
31 | commit('setCurrentSong', {})
32 | commit('setPlayingState', false)
33 | commit('setCurrentTime', 0)
34 | },
35 | clearPlaylist({ commit, dispatch }) {
36 | commit('setPlaylist', [])
37 | dispatch('clearCurrentSong')
38 | },
39 | clearHistory({ commit }) {
40 | const history = []
41 | commit('setPlayHistory', history)
42 | storage.set(PLAY_HISTORY_KEY, history)
43 | },
44 | addToPlaylist({ commit, state }, song) {
45 | const { playlist } = state
46 | const copy = playlist.slice()
47 | if (!copy.find(({ id }) => id === song.id)) {
48 | copy.unshift(song)
49 | commit('setPlaylist', copy)
50 | }
51 | }
52 | }
--------------------------------------------------------------------------------
/src/store/modules/music/getters.js:
--------------------------------------------------------------------------------
1 | import { isDef, playModeMap } from '@/utils'
2 |
3 | export const currentIndex = (state) => {
4 | const { currentSong, playlist } = state
5 | return playlist.findIndex(({ id }) => id === currentSong.id)
6 | }
7 |
8 | export const nextSong = (state, getters) => {
9 | const { playlist, playMode } = state
10 | const nextStratMap = {
11 | [playModeMap.sequence.code]: getSequenceNextIndex,
12 | [playModeMap.loop.code]: getLoopNextIndex,
13 | [playModeMap.random.code]: getRandomNextIndex
14 | }
15 | const getNextStrat = nextStratMap[playMode]
16 | const index = getNextStrat()
17 |
18 | return playlist[index]
19 |
20 | // 顺序
21 | function getSequenceNextIndex() {
22 | let nextIndex = getters.currentIndex + 1
23 | if (nextIndex > playlist.length - 1) {
24 | nextIndex = 0
25 | }
26 | return nextIndex
27 | }
28 |
29 | // 随机
30 | function getRandomNextIndex() {
31 | return getRandomIndex(playlist, getters.currentIndex)
32 | }
33 |
34 | // 单曲
35 | function getLoopNextIndex() {
36 | return getters.currentIndex
37 | }
38 | }
39 |
40 | // 上一首歌
41 | export const prevSong = (state, getters) => {
42 | const { playlist, playMode } = state
43 | const prevStratMap = {
44 | [playModeMap.sequence.code]: genSequencePrevIndex,
45 | [playModeMap.loop.code]: getLoopPrevIndex,
46 | [playModeMap.random.code]: getRandomPrevIndex
47 | }
48 | const getPrevStrat = prevStratMap[playMode]
49 | const index = getPrevStrat()
50 |
51 | return playlist[index]
52 |
53 | function genSequencePrevIndex() {
54 | let prevIndex = getters.currentIndex - 1
55 | if (prevIndex < 0) {
56 | prevIndex = playlist.length - 1
57 | }
58 | return prevIndex
59 | }
60 |
61 | function getLoopPrevIndex() {
62 | return getters.currentIndex
63 | }
64 |
65 | function getRandomPrevIndex() {
66 | return getRandomIndex(playlist, getters.currentIndex)
67 | }
68 | }
69 |
70 | // 当前是否有歌曲在播放
71 | export const hasCurrentSong = (state) => {
72 | return isDef(state.currentSong.id)
73 | }
74 |
75 | function getRandomIndex(playlist, currentIndex) {
76 | // 防止无限循环
77 | if (playlist.length === 1) {
78 | return currentIndex
79 | }
80 | let index = Math.round(Math.random() * (playlist.length - 1))
81 | if (index === currentIndex) {
82 | index = getRandomIndex(playlist, currentIndex)
83 | }
84 | return index
85 | }
--------------------------------------------------------------------------------
/src/store/modules/music/index.js:
--------------------------------------------------------------------------------
1 | import state from './state'
2 | import * as getters from './getters'
3 | import mutations from './mutations'
4 | import actions from './actions'
5 |
6 | export default {
7 | namespaced: true,
8 | state,
9 | getters,
10 | mutations,
11 | actions,
12 | }
--------------------------------------------------------------------------------
/src/store/modules/music/mutations.js:
--------------------------------------------------------------------------------
1 | import { shallowEqual } from '@/utils'
2 | export default {
3 | setCurrentSong(state, song) {
4 | state.currentSong = song
5 | },
6 | setCurrentTime(state, time) {
7 | state.currentTime = time
8 | },
9 | setPlayingState(state, playing) {
10 | state.playing = playing
11 | },
12 | setPlayMode(state, mode) {
13 | state.playMode = mode
14 | },
15 | setPlaylistShow(state, show) {
16 | state.isPlaylistShow = show
17 | },
18 | setPlayerShow(state, show) {
19 | state.isPlayerShow = show
20 | },
21 | setPlaylistPromptShow(state, show) {
22 | state.isPlaylistPromptShow = show
23 | },
24 | setPlaylist(state, playlist) {
25 | const { isPlaylistShow, playlist: oldPlaylist } = state
26 | state.playlist = playlist
27 | // 播放列表未显示 并且两次播放列表的不一样 则弹出提示
28 | if (!isPlaylistShow && !shallowEqual(oldPlaylist, playlist, 'id')) {
29 | state.isPlaylistPromptShow = true
30 | setTimeout(() => {
31 | state.isPlaylistPromptShow = false
32 | }, 2000)
33 | }
34 | },
35 | setPlayHistory(state, history) {
36 | state.playHistory = history
37 | },
38 | setMenuShow(state, show) {
39 | state.isMenuShow = show
40 | },
41 | }
42 |
--------------------------------------------------------------------------------
/src/store/modules/music/state.js:
--------------------------------------------------------------------------------
1 | import storage from 'good-storage'
2 | import { PLAY_HISTORY_KEY } from '@/utils'
3 | import { playModeMap } from '@/utils/config'
4 |
5 | export default {
6 | // 当前播放歌曲
7 | currentSong: {},
8 | // 当前播放时长
9 | currentTime: 0,
10 | // 播放状态
11 | playing: false,
12 | // 播放模式
13 | playMode: playModeMap.sequence.code,
14 | // 播放列表显示
15 | isPlaylistShow: false,
16 | // 播放提示显示
17 | isPlaylistPromptShow: false,
18 | // 歌曲详情页显示
19 | isPlayerShow: false,
20 | // 播放列表数据
21 | playlist: [],
22 | // 播放历史数据
23 | playHistory: storage.get(PLAY_HISTORY_KEY, []),
24 | // 菜单显示
25 | isMenuShow: true,
26 | }
27 |
--------------------------------------------------------------------------------
/src/store/modules/user/actions.js:
--------------------------------------------------------------------------------
1 | import storage from 'good-storage'
2 | import { UID_KEY } from '@/utils'
3 | import { notify, isDef } from '@/utils'
4 | import { getUserDetail, getUserPlaylist } from "@/api"
5 |
6 | export default {
7 | async login({ commit }, uid) {
8 | const error = () => {
9 | notify.error('登录失败,请输入正确的uid。')
10 | return false
11 | }
12 |
13 | if (!isDef(uid)) {
14 | return error()
15 | }
16 |
17 | try {
18 | const user = await getUserDetail(uid)
19 | const { profile } = user
20 | commit('setUser', profile)
21 | storage.set(UID_KEY, profile.userId)
22 | } catch (e) {
23 | return error()
24 | }
25 |
26 | const { playlist } = await getUserPlaylist(uid)
27 | commit('setUserPlaylist', playlist)
28 | return true
29 | },
30 | logout({ commit }) {
31 | commit('setUser', {})
32 | commit('setUserPlaylist', [])
33 | storage.set(UID_KEY, null)
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/store/modules/user/getters.js:
--------------------------------------------------------------------------------
1 | import { isDef } from "@/utils";
2 |
3 | export const isLogin = (state) => isDef(state.user.userId)
4 |
5 | // 根据用户请求的数据整合出菜单
6 | export const userMenus = (state) => {
7 | const {user,userPlaylist } = state
8 | const retMenus = []
9 | const userCreateList = []
10 | const userCollectList = []
11 |
12 | userPlaylist.forEach(playlist => {
13 | const { userId } = playlist
14 | if (user.userId === userId) {
15 | userCreateList.push(playlist)
16 | } else {
17 | userCollectList.push(playlist)
18 | }
19 | })
20 |
21 | const genPlaylistChildren = playlist =>
22 | playlist.map(({ id, name }) => ({
23 | path: `/playlist/${id}`,
24 | meta: {
25 | title: name,
26 | icon: "playlist-menu"
27 | },
28 | }))
29 |
30 | if (userCreateList.length) {
31 | retMenus.push({
32 | type: "playlist",
33 | title: "创建的歌单",
34 | children: genPlaylistChildren(userCreateList)
35 | })
36 | }
37 |
38 | if (userCollectList.length) {
39 | retMenus.push({
40 | type: "playlist",
41 | title: "收藏的歌单",
42 | children: genPlaylistChildren(userCollectList)
43 | })
44 | }
45 |
46 | return retMenus
47 | }
--------------------------------------------------------------------------------
/src/store/modules/user/index.js:
--------------------------------------------------------------------------------
1 | import state from './state'
2 | import * as getters from './getters'
3 | import mutations from './mutations'
4 | import actions from './actions'
5 |
6 | export default {
7 | namespaced: true,
8 | state,
9 | getters,
10 | mutations,
11 | actions,
12 | }
--------------------------------------------------------------------------------
/src/store/modules/user/mutations.js:
--------------------------------------------------------------------------------
1 | export default {
2 | setUser(state, user) {
3 | state.user = user
4 | },
5 | setUserPlaylist(state, playlist) {
6 | state.userPlaylist = playlist
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/store/modules/user/state.js:
--------------------------------------------------------------------------------
1 | export default {
2 | // 登录用户
3 | user: {},
4 | // 登录用户歌单
5 | userPlaylist: []
6 | }
7 |
--------------------------------------------------------------------------------
/src/style/app.scss:
--------------------------------------------------------------------------------
1 | /**
2 | ** 应用级别的一些样式
3 | **/
4 | body {
5 | color: var(--font-color);
6 | }
7 |
8 | ::selection {
9 | background-color: $theme-color;
10 | color: white;
11 | }
12 |
13 | // 滚动条
14 | ::-webkit-scrollbar-track {
15 | background-color: var(--menu-bgcolor);
16 | }
17 |
18 | ::-webkit-scrollbar {
19 | width: 5px;
20 | height: 5px;
21 | background-color: #f5f5f5;
22 | }
23 |
24 | ::-webkit-scrollbar-thumb {
25 | background-color: var(--scrollbar-color);
26 | }
27 |
28 | .slide-enter-active,
29 | .slide-leave-active {
30 | transition: all 0.5s;
31 | transform: none;
32 | }
33 | .slide-enter,
34 | .slide-leave-to {
35 | transform: translateY(100%);
36 | }
37 |
38 | .fade-enter-active,
39 | .fade-leave-active {
40 | transition: all 0.5s;
41 | opacity: 1;
42 | }
43 | .fade-enter,
44 | .fade-leave-to {
45 | opacity: 0;
46 | }
47 |
48 | // 转化为rem
49 | .iconfont {
50 | font-size: 16px;
51 | }
52 |
53 | a {
54 | color: $theme-color;
55 |
56 | &:hover,
57 | &:focus,
58 | &:visited {
59 | color: $theme-color;
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/style/element-overwrite.scss:
--------------------------------------------------------------------------------
1 | // table
2 | .el-table th,
3 | .el-table td {
4 | padding: 4px !important;
5 | font-size: $font-size-sm !important;
6 | }
7 | .el-table::before {
8 | height: 0 !important;
9 | }
10 | .el-table--enable-row-hover .el-table__body tr:hover > td {
11 | background-color: var(--playlist-hover-bgcolor) !important;
12 | }
13 | // 空数据
14 | .el-table__empty-block {
15 | background: var(--body-bgcolor);
16 | color: var(--font-color);
17 | }
18 | .el-table__header-wrapper th {
19 | color: var(--font-color);
20 | }
21 | .el-table {
22 | background-color: var(--body-bgcolor) !important;
23 | }
24 |
25 | // 表格单元格的通用样式
26 | @mixin el-td-style($color) {
27 | td,
28 | th,
29 | tr {
30 | background-color: $color !important;
31 | transition: background-color 0s !important;
32 | border-bottom: none !important;
33 |
34 | .cell {
35 | white-space: nowrap !important;
36 | }
37 | }
38 | }
39 | .el-table,
40 | .el-table {
41 | @include el-td-style(var(--body-bgcolor));
42 |
43 | tr.el-table__row--striped {
44 | @include el-td-style(var(--stripe-bg));
45 | }
46 | }
47 | // 允许外部在某个类下面覆写table样式
48 | @mixin el-table-theme($color, $stripe-color: var(--stripe-bg)) {
49 | ::v-deep .el-table {
50 | @include el-td-style($color);
51 |
52 | tr.el-table__row--striped {
53 | @include el-td-style(#{$stripe-color});
54 | }
55 | }
56 | }
57 | // carosel
58 | .el-carousel--horizontal {
59 | overflow: hidden;
60 | }
61 |
62 | // popover
63 | @each $direction in 'bottom' 'top' 'left' 'right' {
64 | .el-popper[x-placement^='#{$direction}'] .popper__arrow,
65 | .el-popper[x-placement^='#{$direction}'] .popper__arrow::after {
66 | border-#{$direction}-color: var(--prompt-bg-color) !important;
67 | }
68 | }
69 | .el-popover {
70 | background: var(--prompt-bg-color) !important;
71 | border: none !important;
72 | text-align: left;
73 | @include box-shadow;
74 | }
75 |
76 | // input
77 | $input-height: 24px;
78 | @mixin el-input-style($color, $bg-color, $placeholder-color) {
79 | .el-input__inner {
80 | height: $input-height !important;
81 | line-height: $input-height !important;
82 | background: #{$bg-color} !important;
83 | border: none !important;
84 | color: #{$color} !important;
85 |
86 | &:hover {
87 | border: none !important;
88 | }
89 | }
90 | .el-input__prefix {
91 | i {
92 | line-height: $input-height + 1px !important;
93 | color: #{$color} !important;
94 | transition: none !important;
95 | }
96 | }
97 |
98 | input::-webkit-input-placeholder {
99 | color: #{$placeholder-color} !important;
100 | }
101 | }
102 |
103 | // 外部覆写input-theme样式
104 | @mixin el-input-theme($color, $bg-color, $placeholder-color: $color) {
105 | ::v-deep .el-input {
106 | @include el-input-style($color, $bg-color, $placeholder-color);
107 | }
108 | }
109 |
110 | .el-input {
111 | @include el-input-style(
112 | var(--input-color),
113 | var(--input-bgcolor),
114 | var(--font-color-grey-shallow)
115 | );
116 | }
117 |
118 | // pagination
119 | .el-pagination,
120 | .el-pagination button,
121 | .el-pager li {
122 | background: inherit !important;
123 | color: var(--font-color) !important;
124 |
125 | .active {
126 | color: $theme-color !important;
127 | }
128 | }
129 |
130 | // dialog
131 | .el-dialog {
132 | background: var(--modal-bg-color) !important;
133 | @include box-shadow;
134 |
135 | .el-dialog__body {
136 | color: var(--font-color) !important;
137 | }
138 |
139 | // 右上角图标
140 | .el-dialog__headerbtn:focus .el-dialog__close,
141 | .el-dialog__headerbtn:hover .el-dialog__close {
142 | color: $theme-color;
143 | }
144 | }
145 |
146 | // button
147 | .el-button--primary {
148 | background: $theme-color !important;
149 | border-color: $theme-color !important;
150 | }
151 |
152 | // loading
153 | .el-loading-spinner {
154 | circle {
155 | stroke: $theme-color !important;
156 | }
157 | .el-loading-text {
158 | color: $theme-color !important;
159 | }
160 | .el-icon-loading {
161 | color: $theme-color !important;
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/src/style/index.scss:
--------------------------------------------------------------------------------
1 | @import "./element-overwrite.scss";
2 | @import "./reset.scss";
3 | @import "./app.scss";
4 |
--------------------------------------------------------------------------------
/src/style/mixin.scss:
--------------------------------------------------------------------------------
1 | @mixin text-ellipsis() {
2 | overflow: hidden;
3 | text-overflow: ellipsis;
4 | white-space: nowrap;
5 | }
6 |
7 | @mixin text-ellipsis-multi($line) {
8 | display: -webkit-box;
9 | overflow: hidden;
10 | text-overflow: ellipsis;
11 | -webkit-line-clamp: $line;
12 | -webkit-box-orient: vertical;
13 | }
14 |
15 | @mixin flex-center($direction: row) {
16 | display: flex;
17 | flex-direction: $direction;
18 | align-items: center;
19 | justify-content: center;
20 | }
21 |
22 | @mixin box-shadow {
23 | box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.2);
24 | }
25 |
26 | @mixin img-wrap($width, $height: $width) {
27 | width: $width;
28 | height: $height;
29 | flex-shrink: 0;
30 |
31 | img {
32 | width: 100%;
33 | height: 100%;
34 | }
35 | }
36 |
37 | @mixin abs-stretch {
38 | position: absolute;
39 | left: 0;
40 | bottom: 0;
41 | top: 0;
42 | right: 0;
43 | }
44 |
45 | @mixin abs-center {
46 | position: absolute;
47 | left: 50%;
48 | top: 50%;
49 | transform: translate(-50%, -50%);
50 | }
51 |
52 | @mixin round($d) {
53 | width: $d;
54 | height: $d;
55 | border-radius: 50%;
56 | }
57 |
58 | @mixin list($item-width) {
59 | .list-wrap {
60 | display: flex;
61 | flex-wrap: wrap;
62 | margin: 0 -12px;
63 |
64 | .list-item {
65 | width: $item-width;
66 | margin-bottom: 36px;
67 | padding: 0 12px;
68 | }
69 | }
70 | }
--------------------------------------------------------------------------------
/src/style/reset.scss:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | height: 100%;
4 | overflow: hidden;
5 | font-weight: 400;
6 | font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial,
7 | sans-serif;
8 | }
9 |
10 | html,
11 | body,
12 | div,
13 | span,
14 | object,
15 | iframe,
16 | h1,
17 | h2,
18 | h3,
19 | h4,
20 | h5,
21 | h6,
22 | p,
23 | blockquote,
24 | pre,
25 | abbr,
26 | address,
27 | cite,
28 | code,
29 | del,
30 | dfn,
31 | em,
32 | img,
33 | ins,
34 | kbd,
35 | q,
36 | samp,
37 | small,
38 | strong,
39 | sub,
40 | sup,
41 | var,
42 | b,
43 | i,
44 | dl,
45 | dt,
46 | dd,
47 | ol,
48 | ul,
49 | li,
50 | fieldset,
51 | form,
52 | label,
53 | legend,
54 | table,
55 | caption,
56 | tbody,
57 | tfoot,
58 | thead,
59 | tr,
60 | th,
61 | td,
62 | article,
63 | aside,
64 | canvas,
65 | details,
66 | figcaption,
67 | figure,
68 | footer,
69 | header,
70 | hgroup,
71 | menu,
72 | nav,
73 | section,
74 | summary,
75 | time,
76 | mark,
77 | audio,
78 | video {
79 | margin: 0;
80 | padding: 0;
81 | border: 0;
82 | outline: 0;
83 | font-size: 100%;
84 | vertical-align: baseline;
85 | background: transparent;
86 | list-style: none;
87 | box-sizing: border-box;
88 | }
89 |
90 | body {
91 | line-height: 1.2;
92 | }
93 |
94 | :focus {
95 | outline: 1;
96 | }
97 |
98 | article,
99 | aside,
100 | canvas,
101 | details,
102 | figcaption,
103 | figure,
104 | footer,
105 | header,
106 | hgroup,
107 | menu,
108 | nav,
109 | section,
110 | summary {
111 | display: block;
112 | }
113 |
114 | nav ul {
115 | list-style: none;
116 | }
117 |
118 | blockquote,
119 | q {
120 | quotes: none;
121 | }
122 |
123 | blockquote:before,
124 | blockquote:after,
125 | q:before,
126 | q:after {
127 | content: "";
128 | content: none;
129 | }
130 |
131 | a {
132 | margin: 0;
133 | padding: 0;
134 | border: 0;
135 | font-size: 100%;
136 | vertical-align: baseline;
137 | background: transparent;
138 | }
139 |
140 | ins {
141 | background-color: #ff9;
142 | color: #000;
143 | text-decoration: none;
144 | }
145 |
146 | mark {
147 | background-color: #ff9;
148 | color: #000;
149 | font-style: italic;
150 | font-weight: bold;
151 | }
152 |
153 | del {
154 | text-decoration: line-through;
155 | }
156 |
157 | abbr[title],
158 | dfn[title] {
159 | border-bottom: 1px dotted #000;
160 | cursor: help;
161 | }
162 |
163 | table {
164 | border-collapse: collapse;
165 | border-spacing: 0;
166 | }
167 |
168 | hr {
169 | display: block;
170 | height: 1px;
171 | border: 0;
172 | border-top: 1px solid #cccccc;
173 | margin: 1em 0;
174 | padding: 0;
175 | }
176 |
177 | input,
178 | select {
179 | vertical-align: middle;
180 | }
181 |
182 | img[src=""],
183 | img:not([src]) {
184 | opacity: 0;
185 | }
186 |
--------------------------------------------------------------------------------
/src/style/themes/variables-red.js:
--------------------------------------------------------------------------------
1 | import variablesWhite from './variables-white'
2 |
3 | export default {
4 | ...variablesWhite,
5 | ['--header-bgcolor']: '#D74D45',
6 | ['--header-font-color']: '#EFB6B2',
7 | ['--header-input-color']: '#EFB6B2',
8 | ['--header-input-bgcolor']: '#DD6861',
9 | ['--header-input-placeholder-color']: '#EFB6B2',
10 | ['--round-hover-bgcolor']: '#CA4841',
11 | }
--------------------------------------------------------------------------------
/src/style/themes/variables-white.js:
--------------------------------------------------------------------------------
1 | export default {
2 | ['--body-bgcolor']: '#fff',
3 | ['--light-bgcolor']: '#f5f5f5',
4 |
5 | ['--font-color']: '#4a4a4a',
6 | ['--font-color-shallow']: '#404040',
7 | ['--font-color-white']: '#333333',
8 | ['--font-color-grey']: '#5c5c5c',
9 | ['--font-color-grey2']: '#909090',
10 | ['--font-color-grey-shallow']: '#BEBEBE',
11 | ['--border']: '#f2f2f1',
12 | ['--scrollbar-color']: '#D0D0D0',
13 | ['--round-hover-bgcolor']: '#EBEBEB',
14 | ['--stripe-bg']: '#FAFAFA',
15 | ['--shallow-theme-bgcolor']: '#fdf6f5',
16 | ['--shallow-theme-bgcolor-hover']: '#FBEDEC',
17 |
18 | //header
19 | ['--header-bgcolor']: '#F9F9F9',
20 | ['--header-font-color']: '#4a4a4a',
21 | ['--header-input-color']: '#4a4a4a',
22 | ['--header-input-bgcolor']: '#EDEDED',
23 | ['--header-input-placeholder-color']: '#BEBEBE',
24 |
25 | // menu
26 | ['--menu-bgcolor']: '#ededed',
27 | ['--menu-item-hover-bg']: '#e7e7e7',
28 | ['--menu-item-active-bg']: '#e2e2e2',
29 |
30 | //player
31 | ['--player-bgcolor']: '#F9F9F9',
32 |
33 | //playlist
34 | ['--playlist-bgcolor']: '#fff',
35 | ['--playlist-hover-bgcolor']: '#EFEFEF',
36 |
37 | //search
38 | ['--search-bgcolor']: '#fff',
39 | //progress
40 | ['--progress-bgcolor']: '#F5F5F5',
41 |
42 | //input
43 | ['--input-color']: '#4a4a4a',
44 | ['--input-bgcolor']: '#EDEDED',
45 |
46 | //button
47 | ['--button-border-color']: '#D9D9D9',
48 | ['--button-hover-bgcolor']: '#F5F5F5',
49 |
50 | //tab
51 | ['--tab-item-color']: '#7F7F81',
52 | ['--tab-item-hover-color']: '#343434',
53 | ['--tab-item-active-color']: '#000',
54 |
55 | //modal
56 | ['--modal-bg-color']: '#F9F9F9',
57 | // prompt
58 | ['--prompt-bg-color']: '#fff',
59 | //song-detail
60 | ['--song-shallow-grey-bg']: '#E6E5E6',
61 | }
62 |
--------------------------------------------------------------------------------
/src/style/themes/variables.js:
--------------------------------------------------------------------------------
1 | export default {
2 | ['--body-bgcolor']: '#252525',
3 | ['--light-bgcolor']: '#2e2e2e',
4 |
5 | ['--font-color']: '#b1b1b1',
6 | ['--font-color-shallow']: '#6f6f6f',
7 | ['--font-color-white']: '#dcdde4',
8 | ['--font-color-grey']: '#5C5C5C',
9 | ['--font-color-grey2']: '#808080',
10 | ['--font-color-grey-shallow']: '#727272',
11 | ['--border']: '#3F3F3F',
12 | ['--scrollbar-color']: '#3a3a3a',
13 | ['--round-hover-bgcolor']: '#373737',
14 | ['--stripe-bg']: '#323232',
15 | ['--shallow-theme-bgcolor']: '#2D2625',
16 | ['--shallow-theme-bgcolor-hover']: '#352726',
17 |
18 | //header
19 | ['--header-bgcolor']: '#252525',
20 | ['--header-font-color']: '#b1b1b1',
21 | ['--header-input-color']: '#b1b1b1',
22 | ['--header-input-bgcolor']: '#4B4B4B',
23 | ['--header-input-placeholder-color']: '#727272',
24 |
25 | //menu
26 | ['--menu-bgcolor']: '#202020',
27 | ['--menu-item-hover-bg']: '#1d1d1d',
28 | ['--menu-item-active-bg']: '#1b1b1b',
29 |
30 | //player
31 | ['--player-bgcolor']: '#252525',
32 |
33 | //playlist
34 | ['--playlist-bgcolor']: '#363636',
35 | ['--playlist-hover-bgcolor']: '#2e2e2e',
36 |
37 | //search
38 | ['--search-bgcolor']: '#363636',
39 |
40 | //progress
41 | ['--progress-bgcolor']: '#232323',
42 |
43 | //input
44 | ['--input-color']: '#b1b1b1',
45 | ['--input-bgcolor']: '#4B4B4B',
46 |
47 | //button
48 | ['--button-border-color']: '#454545',
49 | ['--button-hover-bgcolor']: '#3E3E3E',
50 | //tab
51 | ['--tab-item-color']: '#797979',
52 | ['--tab-item-hover-color']: '#B4B4B4',
53 | ['--tab-item-active-color']: '#fff',
54 | //modal
55 | ['--modal-bg-color']: '#202020',
56 | // prompt
57 | ['--prompt-bg-color']: '#363636',
58 | //song-detail
59 | ['--song-shallow-grey-bg']: '#2A2A2A',
60 | }
--------------------------------------------------------------------------------
/src/style/variables.scss:
--------------------------------------------------------------------------------
1 | $theme-color: #d33a31;
2 | $black: #000;
3 | $white: #fff;
4 | $gold: #e7aa5a;
5 | $blue: #517eaf;
6 |
7 | $font-size: 14px;
8 | $font-size-medium-sm: 13px;
9 | $font-size-lg: 16px;
10 | $font-size-sm: 12px;
11 | $font-size-xs: 10px;
12 | $font-size-medium: 15px;
13 | $font-size-title: 18px;
14 | $font-size-title-lg: 24px;
15 |
16 | $font-weight-bold: 700;
17 |
18 | $font-color-transparent: rgba(255, 255, 255, 0.5);
19 |
20 | $border: 1px solid #3f3f3f;
21 |
22 | $page-padding: 16px 32px;
23 | // layout
24 | $layout-content-min-width: 700px;
25 | // content
26 | $center-content-max-width: 1000px;
27 | // header
28 | $header-height: 50px;
29 |
30 | // mini-player
31 | $mini-player-height: 60px;
32 | $mini-player-z-index: 1002;
33 |
34 | //playlist
35 | $playlist-z-index: 1001;
36 |
37 | //search
38 | $search-panel-z-index: 1001;
39 |
40 | //song-detail
41 | $song-detail-z-index: 1000;
42 |
--------------------------------------------------------------------------------
/src/utils/axios.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import { Loading } from 'element-ui'
3 | import { confirm } from '@/base/confirm'
4 | import store from '@/store'
5 |
6 | const BASE_URL = 'https://mu-api.yuk0.com/'
7 | // 不带全局loading的请求实例
8 | export const requestWithoutLoading = createBaseInstance()
9 | // 带全局loading的请求实例
10 | // 传入函数是因为需要在处理请求结果handleResponse之前处理loading
11 | // 所以要在内部插入loading拦截器的处理逻辑
12 | export const request = createBaseInstance()
13 | mixinLoading(request.interceptors)
14 | // 通用的axios实例
15 | function createBaseInstance() {
16 | const instance = axios.create({
17 | baseURL: BASE_URL,
18 | })
19 |
20 | instance.interceptors.response.use(handleResponse, handleError)
21 | return instance
22 | }
23 |
24 | function handleError(e) {
25 | confirm(e.message, '出错啦~')
26 | throw e
27 | }
28 |
29 | function handleResponse(response) {
30 | return response.data
31 | }
32 |
33 | let loading
34 | let loadingCount = 0
35 | const SET_AXIOS_LOADING = 'global/setAxiosLoading'
36 | function mixinLoading(interceptors) {
37 | interceptors.request.use(loadingRequestInterceptor)
38 | interceptors.response.use(
39 | loadingResponseInterceptor,
40 | loadingResponseErrorInterceptor
41 | )
42 |
43 | function loadingRequestInterceptor(config) {
44 | if (!loading) {
45 | loading = Loading.service({
46 | target: 'body',
47 | background: 'transparent',
48 | text: '载入中',
49 | })
50 | store.commit(SET_AXIOS_LOADING, true)
51 | }
52 | loadingCount++
53 |
54 | return config
55 | }
56 |
57 | function handleResponseLoading() {
58 | loadingCount--
59 | if (loadingCount === 0) {
60 | loading.close()
61 | loading = null
62 | store.commit(SET_AXIOS_LOADING, false)
63 | }
64 | }
65 |
66 | function loadingResponseInterceptor(response) {
67 | handleResponseLoading()
68 | return response
69 | }
70 |
71 | function loadingResponseErrorInterceptor(e) {
72 | handleResponseLoading()
73 | throw e
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/utils/business.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 业务工具方法
3 | */
4 | import { getAlbum, getMvDetail } from "@/api"
5 | import router from '@/router'
6 | import { isDef, notify } from './common';
7 |
8 | export function createSong(song) {
9 | const { id, name, img, artists, duration, albumId, albumName,mvId, ...rest } = song
10 |
11 | return {
12 | id,
13 | name,
14 | img,
15 | artists,
16 | duration,
17 | albumName,
18 | url: genSongPlayUrl(song.id),
19 | artistsText: genArtistisText(artists),
20 | durationSecond: duration / 1000,
21 | // 专辑 如果需要额外请求封面的话必须加上
22 | albumId,
23 | // mv的id 如果有的话 会在songTable组件中加上mv链接。
24 | mvId,
25 | ...rest
26 | }
27 | }
28 |
29 | export async function getSongImg(id, albumId) {
30 | if (!isDef(albumId)) {
31 | throw new Error('need albumId')
32 | }
33 | const { songs } = await getAlbum(albumId)
34 | const {
35 | al: { picUrl }
36 | } = songs.find(({ id: songId }) => songId === id) || {}
37 | return picUrl
38 | }
39 |
40 | export function genArtistisText(artists) {
41 | return (artists || []).map(({ name }) => name).join('/')
42 | }
43 |
44 | // 有时候虽然有mvId 但是请求却404 所以跳转前先请求一把
45 | export async function goMvWithCheck(id) {
46 | try {
47 | await getMvDetail(id)
48 | goMv(id)
49 | } catch (error) {
50 | notify("mv获取失败")
51 | }
52 | }
53 |
54 | export function goMv(id) {
55 | router.push(`/mv/${id}`)
56 | }
57 |
58 | function genSongPlayUrl(id) {
59 | return `https://music.163.com/song/media/outer/url?id=${id}.mp3`
60 | }
--------------------------------------------------------------------------------
/src/utils/common.js:
--------------------------------------------------------------------------------
1 | import { Notification } from 'element-ui'
2 |
3 | export { debounce, throttle } from 'lodash-es'
4 |
5 | export function pad(num, n = 2) {
6 | let len = num.toString().length
7 | while (len < n) {
8 | num = '0' + num
9 | len++
10 | }
11 | return num
12 | }
13 |
14 | export function formatDate(date, fmt = 'yyyy-MM-dd hh:mm:ss') {
15 | date = date instanceof Date ? date : new Date(date)
16 | if (/(y+)/.test(fmt)) {
17 | fmt = fmt.replace(
18 | RegExp.$1,
19 | (date.getFullYear() + '').substr(4 - RegExp.$1.length)
20 | )
21 | }
22 | let o = {
23 | 'M+': date.getMonth() + 1,
24 | 'd+': date.getDate(),
25 | 'h+': date.getHours(),
26 | 'm+': date.getMinutes(),
27 | 's+': date.getSeconds()
28 | }
29 | for (let k in o) {
30 | if (new RegExp(`(${k})`).test(fmt)) {
31 | let str = o[k] + ''
32 | fmt = fmt.replace(
33 | RegExp.$1,
34 | RegExp.$1.length === 1 ? str : padLeftZero(str)
35 | )
36 | }
37 | }
38 | return fmt
39 | }
40 |
41 | function padLeftZero(str) {
42 | return ('00' + str).substr(str.length)
43 | }
44 |
45 | export function formatTime(interval) {
46 | interval = interval | 0
47 | const minute = pad((interval / 60) | 0)
48 | const second = pad(interval % 60)
49 | return `${minute}:${second}`
50 | }
51 |
52 | export function formatNumber(number) {
53 | number = Number(number) || 0
54 | return number > 100000 ? `${Math.round(number / 10000)}万` : number
55 | }
56 |
57 | export function genImgUrl(url, w, h) {
58 | if (!h) {
59 | h = w
60 | }
61 | url += `?param=${w}y${h}`
62 | return url
63 | }
64 |
65 |
66 | export function isLast(index, arr) {
67 | return index === arr.length - 1
68 | }
69 |
70 | export function shallowEqual(a, b, compareKey) {
71 | if (a.length !== b.length) {
72 | return false
73 | }
74 | for (let i = 0; i < a.length; i++) {
75 | let compareA = a[i]
76 | let compareB = b[i]
77 | if (compareKey) {
78 | compareA = compareA[compareKey]
79 | compareB = compareB[compareKey]
80 | }
81 | if (!Object.is(a[i], b[i])) {
82 | return false
83 | }
84 | }
85 | return true
86 | }
87 |
88 | export function notify(message, type) {
89 | const params = {
90 | message,
91 | duration: 1500
92 | }
93 | const fn = type ? Notification[type] : Notification
94 | return fn(params)
95 | }
96 | ['success', 'warning', 'info', 'error'].forEach(key => {
97 | notify[key] = (message) => {
98 | return notify(message, key)
99 | }
100 | })
101 |
102 | export function requestFullScreen(element) {
103 | const docElm = element;
104 | if (docElm.requestFullscreen) {
105 | docElm.requestFullscreen();
106 | } else if (docElm.msRequestFullscreen) {
107 | docElm.msRequestFullscreen();
108 | } else if (docElm.mozRequestFullScreen) {
109 | docElm.mozRequestFullScreen();
110 | } else if (docElm.webkitRequestFullScreen) {
111 | docElm.webkitRequestFullScreen();
112 | }
113 | }
114 |
115 | export function exitFullscreen() {
116 | const de = window.parent.document;
117 |
118 | if (de.exitFullscreen) {
119 | de.exitFullscreen();
120 | } else if (de.mozCancelFullScreen) {
121 | de.mozCancelFullScreen();
122 | } else if (de.webkitCancelFullScreen) {
123 | de.webkitCancelFullScreen();
124 | } else if (de.msExitFullscreen) {
125 | de.msExitFullscreen()
126 | }
127 | }
128 |
129 | export function isFullscreen() {
130 | return document.fullScreen ||
131 | document.mozFullScreen ||
132 | document.webkitIsFullScreen
133 | }
134 |
135 | export function isUndef(v) {
136 | return v === undefined || v === null
137 | }
138 |
139 | export function isDef(v) {
140 | return v !== undefined && v !== null
141 | }
142 |
143 | export function isTrue(v) {
144 | return v === true
145 | }
146 |
147 | export function isFalse(v) {
148 | return v === false
149 | }
150 |
151 | export function getPageOffset(page, limit) {
152 | return page * limit
153 | }
--------------------------------------------------------------------------------
/src/utils/config.js:
--------------------------------------------------------------------------------
1 | export const playModeMap = {
2 | sequence: {
3 | code: 'sequence',
4 | icon: 'sequence',
5 | name: '顺序播放'
6 | },
7 | loop: {
8 | code: 'loop',
9 | icon: 'loop',
10 | name: '单曲循环'
11 | },
12 | random: {
13 | code: 'random',
14 | icon: 'random',
15 | name: '随机播放'
16 | }
17 | }
18 |
19 | // 存储播放记录
20 | export const PLAY_HISTORY_KEY = '__play_history__'
21 |
22 | // 用户id
23 | export const UID_KEY = '__uid__'
24 |
25 | // 音量
26 | export const VOLUME_KEY = '__volume__'
27 |
--------------------------------------------------------------------------------
/src/utils/dom.js:
--------------------------------------------------------------------------------
1 | export function hasClass(el, className) {
2 | return el.classList.contains(className)
3 | }
4 |
5 | export function addClass(el, className) {
6 | el.classList.add(className)
7 | }
8 |
9 | export function getData(el, name, val) {
10 | const prefix = 'data-'
11 | if (val) {
12 | return el.setAttribute(prefix + name, val)
13 | }
14 | return el.getAttribute(prefix + name)
15 | }
16 |
17 | let elementStyle = document.createElement('div').style
18 |
19 | let vendor = ( () => {
20 | let transformNames = {
21 | webkit: 'webkitTransform',
22 | Moz: 'MozTransform',
23 | O: 'OTransform',
24 | ms: 'msTransform',
25 | standard: 'transform'
26 | }
27 |
28 | for (let key in transformNames) {
29 | if (elementStyle[transformNames[key]] !== undefined) {
30 | return key
31 | }
32 | }
33 |
34 | return false
35 | })()
36 |
37 | export function prefixStyle(style) {
38 | if (vendor === false) {
39 | return false
40 | }
41 |
42 | if (vendor === 'standard') {
43 | return style
44 | }
45 |
46 | return vendor + style.charAt(0).toUpperCase() + style.substr(1)
47 | }
48 |
49 | export function hasParent(dom, parentDom) {
50 | parentDom = Array.isArray(parentDom) ? parentDom: [parentDom]
51 | while(dom) {
52 | if (parentDom.find(p => p === dom)) {
53 | return true
54 | }else {
55 | dom = dom.parentNode
56 | }
57 | }
58 | }
59 |
60 | export function scrollInto(dom) {
61 | dom.scrollIntoView({ behavior: "smooth" })
62 | }
63 |
64 | export const EMPTY_IMG = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
--------------------------------------------------------------------------------
/src/utils/global.js:
--------------------------------------------------------------------------------
1 | import {
2 | Input,
3 | Dialog,
4 | Button,
5 | Loading,
6 | Carousel,
7 | CarouselItem,
8 | Table,
9 | TableColumn,
10 | Popover,
11 | Pagination,
12 | } from "element-ui"
13 | import VueLazyload from "vue-lazyload"
14 | import Meta from 'vue-meta'
15 | import * as utils from "./index"
16 | import { EMPTY_IMG } from "./dom"
17 |
18 | export default {
19 | install(Vue) {
20 | const requireComponent = require.context(
21 | "@/base",
22 | true,
23 | /[a-z0-9]+\.(jsx?|vue)$/i,
24 | )
25 | // 批量注册base组件
26 | requireComponent.keys().forEach(fileName => {
27 | const componentConfig = requireComponent(fileName)
28 | const componentName = componentConfig.default.name
29 | if (componentName) {
30 | Vue.component(componentName, componentConfig.default || componentConfig)
31 | }
32 | })
33 |
34 | Vue.prototype.$ELEMENT = { size: "small" }
35 | Vue.prototype.$utils = utils
36 |
37 | Vue.use(Input)
38 | Vue.use(Carousel)
39 | Vue.use(CarouselItem)
40 | Vue.use(Table)
41 | Vue.use(TableColumn)
42 | Vue.use(Popover)
43 | Vue.use(Pagination)
44 | Vue.use(Loading)
45 | Vue.use(Dialog)
46 | Vue.use(Button)
47 |
48 | Vue.use(Meta)
49 |
50 | Vue.use(VueLazyload, {
51 | loading: EMPTY_IMG,
52 | error: EMPTY_IMG,
53 | })
54 | },
55 | }
56 |
--------------------------------------------------------------------------------
/src/utils/index.js:
--------------------------------------------------------------------------------
1 | export * from './common'
2 |
3 | export * from './rem'
4 |
5 | export * from './config'
6 |
7 | export * from './dom'
8 |
9 | export * from './business'
10 |
11 | export * from './axios'
12 |
13 | export * from './mixin'
14 |
--------------------------------------------------------------------------------
/src/utils/lrcparse.js:
--------------------------------------------------------------------------------
1 | export default function lyricParser(lrc) {
2 | return {
3 | 'lyric': parseLyric(lrc.lrc.lyric || ''),
4 | 'tlyric': parseLyric(lrc.tlyric.lyric || ''),
5 | 'lyricuser': lrc.lyricUser,
6 | 'transuser': lrc.transUser,
7 | }
8 | }
9 |
10 | export function parseLyric(lrc) {
11 | const lyrics = lrc.split('\n')
12 | const lrcObj = []
13 | for (let i = 0; i < lyrics.length; i++) {
14 | const lyric = decodeURIComponent(lyrics[i])
15 | const timeReg = /\[\d*:\d*((\.|:)\d*)*\]/g
16 | const timeRegExpArr = lyric.match(timeReg)
17 | if (!timeRegExpArr) continue
18 | const content = lyric.replace(timeReg, '')
19 | for (let k = 0, h = timeRegExpArr.length; k < h; k++) {
20 | const t = timeRegExpArr[k]
21 | const min = Number(String(t.match(/\[\d*/i)).slice(1))
22 | const sec = Number(String(t.match(/:\d*/i)).slice(1))
23 | const time = min * 60 + sec
24 | if (content !== '') {
25 | lrcObj.push({ time: time, content })
26 | }
27 | }
28 | }
29 | return lrcObj
30 | }
--------------------------------------------------------------------------------
/src/utils/mixin.js:
--------------------------------------------------------------------------------
1 | import store from '@/store'
2 |
3 | export const hideMenuMixin = {
4 | created() {
5 | store.commit('music/setMenuShow', false)
6 | },
7 | beforeDestroy() {
8 | store.commit('music/setMenuShow', true)
9 | },
10 | }
11 |
12 | export const hideMiniPlayerMixin = {
13 | created() {
14 | store.commit('music/setMiniPlayerShow', false)
15 | },
16 | beforeDestroy() {
17 | store.commit('music/setMiniPlayerShow', true)
18 | },
19 | }
20 |
--------------------------------------------------------------------------------
/src/utils/rem.js:
--------------------------------------------------------------------------------
1 | import { throttle } from './common'
2 |
3 | export const remBase = 14
4 |
5 | let htmlFontSize
6 | !(function() {
7 | const calc = function() {
8 | const maxFontSize = 18
9 | const minFontSize = 14
10 | const html = document.getElementsByTagName('html')[0]
11 | const width = html.clientWidth
12 | let size = remBase * (width / 1440)
13 | size = Math.min(maxFontSize, size)
14 | size = Math.max(minFontSize, size)
15 | html.style.fontSize = size + 'px'
16 | htmlFontSize = size
17 | }
18 | calc()
19 | window.addEventListener('resize', throttle(calc, 500))
20 | })()
21 |
22 | // 根据基准字号计算
23 | // 用于静态样式
24 | export function toRem(px) {
25 | return `${px / remBase}rem`
26 | }
27 |
28 | // 根据当前的html根字体大小计算
29 | // 用于某些js的动态计算
30 | export function toCurrentRem(px) {
31 | return `${px / htmlFontSize}rem`
32 | }
33 |
--------------------------------------------------------------------------------
/vue.config.js:
--------------------------------------------------------------------------------
1 | const WorkboxPlugin = require("workbox-webpack-plugin");
2 |
3 | const isProd = process.env.NODE_ENV === "production";
4 |
5 | module.exports = {
6 | outputDir: "music",
7 | configureWebpack: {
8 | devtool: isProd ? false : "source-map",
9 | devServer: {
10 | open: true,
11 | proxy: {
12 | "/netease-api": {
13 | target: "http://localhost:3000",
14 | pathRewrite: { "^/netease-api": "" },
15 | changeOrigin: true,
16 | secure: false,
17 | },
18 | },
19 | port: 8888,
20 | },
21 | externals: isProd
22 | ? {
23 | vue: "Vue",
24 | "vue-router": "VueRouter",
25 | vuex: "Vuex",
26 | axios: "axios",
27 | }
28 | : {},
29 | plugins: [
30 | new WorkboxPlugin.GenerateSW({
31 | skipWaiting: true,
32 | clientsClaim: true,
33 | }),
34 | ],
35 | },
36 | css: {
37 | loaderOptions: {
38 | sass: {
39 | implementation: require("sass"),
40 | data: `
41 | @import "~@/style/variables.scss";
42 | @import "~@/style/mixin.scss";
43 | `,
44 | },
45 | },
46 | },
47 | };
48 |
--------------------------------------------------------------------------------
8 | {{ comment.user.nickname }}: 9 | {{ comment.content }} 10 |
11 |13 | {{ comment.beReplied[0].user.nickname }}: 16 | {{ comment.beReplied[0].content }} 17 |
18 |