The response has been limited to 50k tokens of the smallest files in the repo. You can remove this limitation by removing the max tokens filter.
├── .browserslistrc
├── .editorconfig
├── .env
├── .eslintrc.js
├── .gitignore
├── .husky
    └── commit-msg
├── .npmrc
├── .vscode
    ├── extensions.json
    └── settings.json
├── LICENSE
├── README.md
├── babel.config.js
├── jsconfig.json
├── package.json
├── pnpm-lock.yaml
├── postcss.config.js
├── public
    ├── favicon.ico
    ├── img
    │   └── warn.png
    ├── index.html
    └── prompt.html
├── screenshots
    ├── 1.jpg
    ├── 2.jpg
    ├── 3.jpg
    ├── 4.jpg
    ├── 5.jpg
    ├── 6.jpg
    ├── 7.jpg
    └── 8.jpg
├── src
    ├── App.vue
    ├── api
    │   └── index.js
    ├── assets
    │   ├── background
    │   │   ├── bg-1.jpg
    │   │   ├── bg-2.jpg
    │   │   └── bg-3.jpg
    │   └── img
    │   │   ├── album_cover_player.png
    │   │   ├── default.png
    │   │   ├── player_cover.png
    │   │   └── wave.gif
    ├── base
    │   ├── mm-dialog
    │   │   └── mm-dialog.vue
    │   ├── mm-icon
    │   │   └── mm-icon.vue
    │   ├── mm-loading
    │   │   └── mm-loading.vue
    │   ├── mm-no-result
    │   │   └── mm-no-result.vue
    │   ├── mm-progress
    │   │   └── mm-progress.vue
    │   └── mm-toast
    │   │   ├── index.js
    │   │   └── mm-toast.vue
    ├── components
    │   ├── lyric
    │   │   └── lyric.vue
    │   ├── mm-header
    │   │   └── mm-header.vue
    │   ├── music-btn
    │   │   └── music-btn.vue
    │   ├── music-list
    │   │   └── music-list.vue
    │   └── volume
    │   │   └── volume.vue
    ├── config.js
    ├── main.js
    ├── pages
    │   ├── comment
    │   │   └── comment.vue
    │   ├── details
    │   │   └── details.vue
    │   ├── historyList
    │   │   └── historyList.vue
    │   ├── mmPlayer.js
    │   ├── music.vue
    │   ├── playList
    │   │   └── playList.vue
    │   ├── search
    │   │   └── search.vue
    │   ├── topList
    │   │   └── topList.vue
    │   └── userList
    │   │   └── userList.vue
    ├── router
    │   └── index.js
    ├── store
    │   ├── actions.js
    │   ├── getters.js
    │   ├── index.js
    │   ├── mutation-types.js
    │   ├── mutations.js
    │   └── state.js
    ├── styles
    │   ├── index.less
    │   ├── mixin.less
    │   ├── reset.less
    │   └── var.less
    └── utils
    │   ├── axios.js
    │   ├── hack.js
    │   ├── mixin.js
    │   ├── song.js
    │   ├── storage.js
    │   └── util.js
└── vue.config.js


/.browserslistrc:
--------------------------------------------------------------------------------
1 | > 1%
2 | last 2 versions
3 | not ie <= 8
4 | 


--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
 1 | # http://editorconfig.org
 2 | root = true
 3 | 
 4 | [*]
 5 | indent_style = space
 6 | indent_size = 2
 7 | end_of_line = lf
 8 | charset = utf-8
 9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 | 
12 | [*.md]
13 | trim_trailing_whitespace = false
14 | 


--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | # 后台 api 服务地址
2 | VUE_APP_BASE_API_URL=http://localhost:3000
3 | 
4 | # 访客统计 id (默认不开启,设置值后展示)
5 | VUE_APP_VISITOR_BADGE_ID=
6 | 


--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
 1 | module.exports = {
 2 |   root: true,
 3 |   env: {
 4 |     node: true,
 5 |   },
 6 |   parserOptions: {
 7 |     parser: '@babel/eslint-parser',
 8 |   },
 9 |   extends: ['plugin:vue/recommended', 'prettier', 'plugin:prettier/recommended'],
10 |   rules: {
11 |     'vue/multi-word-component-names': 'off',
12 |     'vue/max-attributes-per-line': [
13 |       'error',
14 |       {
15 |         singleline: 10,
16 |         multiline: {
17 |           max: 1,
18 |         },
19 |       },
20 |     ],
21 |     'vue/singleline-html-element-content-newline': 'off',
22 |     'vue/html-self-closing': [
23 |       'warn',
24 |       {
25 |         html: {
26 |           void: 'always',
27 |           normal: 'never',
28 |         },
29 |         svg: 'never',
30 |         math: 'never',
31 |       },
32 |     ],
33 |     'vue/multiline-html-element-content-newline': 'off',
34 |     'vue/name-property-casing': ['error', 'PascalCase'],
35 |     'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
36 |     'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
37 |     'no-sequences': 2,
38 |   },
39 | }
40 | 


--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
 2 | 
 3 | # Editor directories and files
 4 | .idea
 5 | 
 6 | # dependencies
 7 | node_modules
 8 | 
 9 | # lock files
10 | yarn.lock
11 | package-lock.json
12 | 
13 | # log files
14 | *-debug.log
15 | *-error.log
16 | 
17 | # compile
18 | dist
19 | 
20 | # local env files
21 | .env.local
22 | .env.*.local
23 | 
24 | # misc
25 | .DS_Store
26 | 


--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 | 
4 | npx femm-verify-commit $1
5 | 


--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | auto-install-peers=true
2 | strict-peer-dependencies=false
3 | 
4 | registry=https://registry.npmmirror.com
5 | 


--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 |   "recommendations": [
3 |     "EditorConfig.EditorConfig",
4 |     "esbenp.prettier-vscode"
5 |   ]
6 | }
7 | 


--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 |   "files.eol": "\n"
3 | }
4 | 


--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
 1 | MIT License
 2 | 
 3 | Copyright (c) 2018 maomao1996
 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 | # mmPlayer
  2 | 
  3 | > mmPlayer 是由茂茂开源的一款在线音乐播放器,具有音乐搜索、播放、歌词显示、播放历史、查看歌曲评论、网易云用户歌单播放同步等功能
  4 | 
  5 | 模仿 QQ 音乐网页版界面,采用 `flexbox` 和 `position` 布局;<br />
  6 | mmPlayer 虽然是响应式,但主要以 PC 端为主,移动端只做相应适配;<br />
  7 | 只做主流浏览器兼容(对 IE 说拜拜,想想以前做项目还要兼容 IE7 ,都是泪啊!!!)
  8 | 
  9 | - [在线演示地址](https://netease-music.fe-mm.com/)
 10 | - [React 移动端版本(高仿网易云音乐)](https://github.com/maomao1996/react-music)
 11 | - [交流 QQ 群:529940193](http://shang.qq.com/wpa/qunwpa?idkey=f8be1b627a89108ccfda9308720d2a4d0eb3306f253c5d3e8d58452e20b91129) 本群不解答部署相关问题,如有部署问题请看[关于项目线上部署](#关于项目线上部署)
 12 | - 本播放器由 **[茂茂](https://github.com/maomao1996) 开发**,您可以随意修改、使用、转载。但**使用或转载时请务必保留出处(控制台的注释信息)**!!!
 13 | 
 14 | ## 免责声明
 15 | 
 16 | 1. 本项目是一个**前端练手的实战项目**,旨在**帮助开发者提升技能水平和对前端技术的理解**。
 17 | 
 18 | 2. 本项目**不提供任何音频存储和贩卖服务**。所有音频内容均由网易云音乐的第三方 API 提供,**仅供个人学习研究使用,严禁将其用于任何商业及非法用途**,版权归原始平台所有。
 19 | 
 20 | 3. 使用本项目造成的任何纠纷、责任或损失**由使用者自行承担**。本项目开发者不对因使用本项目而产生的任何直接或间接责任承担责任,并保留追究使用者违法行为的权利。
 21 | 
 22 | 4. **请使用者在使用本项目时遵守相关法律法规,不得将本项目用于任何商业及非法用途**。如有违反,一切后果由使用者自负。同时,使用者应该自行承担因使用本项目而带来的风险和责任。
 23 | 
 24 | 5. 本项目使用了网易云音乐的[第三方 API 服务](https://github.com/Binaryify/NeteaseCloudMusicApi),对于该第三方 API 服务造成的任何问题,本项目开发者不承担责任。
 25 | 
 26 | 请在使用本项目之前仔细阅读以上免责声明,并确保您已完全理解并接受其中的所有条款和条件。如果您不同意或无法遵守这些规定,请不要使用本项目。
 27 | 
 28 | ## 安装与使用
 29 | 
 30 | ### 检查 node 版本
 31 | 
 32 | ```sh
 33 | # 查看 node 版本,确保 node 版本高于 12 版本
 34 | node -v
 35 | ```
 36 | 
 37 | ### mmPlayer
 38 | 
 39 | ```sh
 40 | # 下载 mmPlayer
 41 | git clone https://github.com/maomao1996/Vue-mmPlayer
 42 | 
 43 | # 进入 mmPlayer 播放器目录
 44 | cd Vue-mmPlayer
 45 | 
 46 | # 安装依赖 推荐使用 pnpm
 47 | pnpm install
 48 | # 或者
 49 | npm install
 50 | 
 51 | # 本地运行 mmPlayer
 52 | npm run serve
 53 | 
 54 | # 编译打包
 55 | npm run build
 56 | ```
 57 | 
 58 | ### 后台 api 服务(本地开发)
 59 | 
 60 | [网易云音乐 NodeJS 版 API](https://binaryify.github.io/NeteaseCloudMusicApi)
 61 | 
 62 | ```sh
 63 | # 下载 NeteaseCloudMusicApi
 64 | git clone --depth=1 https://github.com/Binaryify/NeteaseCloudMusicApi
 65 | 
 66 | # 进入 NeteaseCloudMusicApi 后台服务目录
 67 | cd NeteaseCloudMusicApi
 68 | 
 69 | # 安装依赖
 70 | npm install
 71 | 
 72 | # 运行后台 api 服务 访问 http://localhost:3000
 73 | node app.js
 74 | ```
 75 | 
 76 | ### 注意点
 77 | 
 78 | **运行 mmPlayer 后无法获取音乐请检查后台 `api` 服务是否启动(即控制台请求报 404)**<br />
 79 | **线上部署不是直接将整个项目丢到服务器,再去运行 `npm run serve` 命令**<br />
 80 | **项目打包前 `VUE_APP_BASE_API_URL` 必须改后台 `api` 服务地址为线上地址,不能是本地地址**
 81 | 
 82 | ### 关于项目线上部署
 83 | 
 84 | 最近有不少小伙伴部署出了问题,我在这说明下
 85 | 
 86 | - 后台 `api` 服务线上部署
 87 |   - 你需要将 [NeteaseCloudMusicApi](https://github.com/Binaryify/NeteaseCloudMusicApi) 下载
 88 |   - 然后将下载的文件上传至服务器
 89 |   - 再通过 `pm2` 去启动服务(`pm2` 安装和相关命令网上有很多,这里不再赘述)
 90 |   - 最后通过服务器 `ip` + 端口号访问验证 `api` 服务是否启动成功
 91 | - `mmPlayer` 线上部署(推荐使用 [Vercel 部署](#vercel-部署))
 92 |   - 首先要注意的是
 93 |   - 先将 `.env` 文件的 `VUE_APP_BASE_API_URL` 修改成上一步启动的后台 `api` 服务地址(服务器 `ip` + 端口号或者你绑定的域名)
 94 |   - 然后先在本地运行 `npm run build` 命令,会打包在生成一个 `dist` 文件
 95 |   - 最后将打包的 `dist` 文件上传到你的网站服务器目录即可
 96 | - 其他:[在宝塔面板部署 mmPlayer](https://github.com/maomao1996/Blog/issues/1)(不喜欢写文,可能有点烂不要介意哈)
 97 | - 最后:本人已和谷歌、百度达成合作了,如果还有啥不懂的,以后可以直接谷歌、百度
 98 | 
 99 | #### Vercel 部署
100 | 
101 | 1. `fork` 此项目
102 | 2. 在 [Vercel](https://vercel.com) 官网点击 New Project
103 | 3. 点击 `Import Git Repository`
104 |    1. 选择你 `fork` 的此项目
105 |    2. 点击 `import`
106 | 4. `Configure Project` 配置
107 |    1. `Project Name` 自己填
108 |    2. `Framework Preset` 选 `Vue.js` (基本默认就是,不用修改)
109 |    3. 点击 `Environment Variables`,并添加一条
110 |       1. `key` 输入 `VUE_APP_BASE_API_URL`
111 |       2. `value` 输入你后台 `api`([NeteaseCloudMusicApi](https://github.com/Binaryify/NeteaseCloudMusicApi))服务的线上地址
112 | 5. 点击 `Deploy` 等部署完成即可
113 | 
114 | ## 技术栈
115 | 
116 | - [Vue Cli](https://cli.vuejs.org/zh/) Vue 脚手架工具
117 | - [Vue 2.x](https://v2.cn.vuejs.org/) 核心框架
118 | - [Vue Router](https://router.vuejs.org/zh/) 页面路由
119 | - [Vuex](https://vuex.vuejs.org/zh/) 状态管理
120 | - ES6 (JavaScript 语言的下一代标准)
121 | - Less(CSS 预处理器)
122 | - Axios(网络请求)
123 | - FastClick(解决移动端 300ms 点击延迟)
124 | 
125 | ## 项目结构目录图(使用 tree 生成)
126 | 
127 | <details>
128 | <summary>展开查看</summary>
129 | <pre><code>
130 | ├── public                                          // 静态资源目录
131 | │   └─index.html                                    // 入口 html 文件
132 | ├── screenshots                                     // 项目截图
133 | ├── src                                             // 项目源码目录
134 | │   ├── api                                         // 数据交互目录
135 | │   │   └── index.js                                // 获取数据
136 | │   ├── assets                                      // 资源目录
137 | │   │   └── background                              // 启动背景图目录
138 | │   │   └── img                                     // 静态图片目录
139 | │   ├── base                                        // 公共基础组件目录
140 | │   │   ├── mm-dialog
141 | │   │   │   └── mm-dialog.vue                       // 对话框组件
142 | │   │   ├── mm-icon
143 | │   │   │   └── mm-icon.vue                         // icon 组件
144 | │   │   ├── mm-loading
145 | │   │   │   └── mm-loading.vue                      // 加载动画组件
146 | │   │   ├── mm-no-result
147 | │   │   │   └── mm-no-result.vue                    // 暂无数据提示组件
148 | │   │   ├── mm-progress
149 | │   │   │   └── mm-progress.vue                     // 进度条拖动组件
150 | │   │   └── mm-toast
151 | │   │        ├── index.js                           // mm-toast 组件插件化配置
152 | │   │        └── mm-toast.vue                       // 弹出层提示组件
153 | │   ├── components                                  // 公共项目组件目录
154 | │   │   ├── lyric
155 | │   │   │   └── lyric                               // 歌词和封面组件
156 | │   │   └── mm-header
157 | │   │   │   └── mm-header.vue                       // 头部组件
158 | │   │   ├── music-btn
159 | │   │   │   └── music-btn.vue                       // 按钮组件
160 | │   │   ├── music-list
161 | │   │   │    └── music-list.vue                     // 列表组件
162 | │   │   └── volume
163 | │   │        └── volume.vue                         // 音量控制组件
164 | │   ├── pages                                       // 页面组件目录
165 | │   │   ├── comment
166 | │   │   │   └── comment.vue                         // 评论
167 | │   │   ├── details
168 | │   │   │   └── details.vue                         // 排行榜详情
169 | │   │   ├── historyList
170 | │   │   │   └── historyList.vue                     // 我听过的(播放历史)
171 | │   │   ├── playList
172 | │   │   │   └── playList.vue                        // 正在播放
173 | │   │   ├── search
174 | │   │   │   └── search.vue                          // 搜索
175 | │   │   ├── topList
176 | │   │   │   └── topList.vue                         // 排行榜页面
177 | │   │   ├── userList
178 | │   │   │   └── userList.vue                        // 我的歌单
179 | │   │   ├── mmPlayer.js                             // 播放器事相关件绑定
180 | │   │   └── music.vue                               // 播放器主页面
181 | │   ├── router
182 | │   │   └── index.js                                // 路由配置
183 | │   ├── store                                       // vuex 的状态管理
184 | │   │   ├── actions.js                              // 配置 actions
185 | │   │   ├── getters.js                              // 配置 getters
186 | │   │   ├── index.js                                // 引用 vuex,创建 store
187 | │   │   ├── mutation-types.js                       // 定义常量 mutations 名
188 | │   │   ├── mutations.js                            // 配置 mutations
189 | │   │   └── state.js                                // 配置 state
190 | │   ├── styles                                      // 样式文件目录
191 | │   │   ├── index.less                              // mmPlayer 相关基础样式
192 | │   │   ├── mixin.less                              // 样式混合
193 | │   │   ├── reset.less                              // 样式重置
194 | │   │   └── var.less                                // 样式变量(字体大小、字体颜色、背景颜色)
195 | │   ├── js                                          // 数据交互目录
196 | │   │   ├── axios.js                                // axios 简单封装
197 | │   │   ├── hack.js                                 // 修改 nextTick
198 | │   │   ├── mixin.js                                // 组件混合
199 | │   │   ├── song.js                                 // 数据处理
200 | │   │   ├── storage.js                              // localStorage 配置
201 | │   │   └── util.js                                 // 公用 js 方法
202 | │   ├── App.vue                                     // 根组件
203 | │   ├── config.js                                   // 配置文件(播放器默认配置、版本号等)
204 | │   └── main.js                                     // 入口主文件
205 | └── vue.config.js                                   // vue-cli 配置文件
206 | 
207 | </code></pre>
208 | 
209 | </details>
210 | 
211 | ## 功能与界面
212 | 
213 | - 播放器
214 | - 快捷键操作
215 | - 歌词滚动
216 | - 正在播放
217 | - 排行榜
218 | - 歌单详情
219 | - 搜索
220 | - 播放历史
221 | - 查看评论
222 | - 同步网易云歌单
223 | 
224 | ### 界面欣赏
225 | 
226 | PC 端界面自我感觉还行, 就是移动端界面总觉得怪怪的,奈何审美有限,所以又去整了高仿网易云的 `React` 版本(如果小哥哥、小姐姐们有好看的界面,欢迎交流哈)
227 | 
228 | <details>
229 | <summary>点击查看</summary>
230 | 
231 | #### PC
232 | 
233 | ##### 正在播放
234 | 
235 | ![正在播放](https://cdn.jsdelivr.net/gh/maomao1996/Vue-mmPlayer/screenshots/1.jpg)
236 | 
237 | ##### 排行榜
238 | 
239 | ![排行榜](https://cdn.jsdelivr.net/gh/maomao1996/Vue-mmPlayer/screenshots/2.jpg)
240 | 
241 | ##### 搜索
242 | 
243 | ![搜索](https://cdn.jsdelivr.net/gh/maomao1996/Vue-mmPlayer/screenshots/3.jpg)
244 | 
245 | ##### 我的歌单
246 | 
247 | ![我的歌单](https://cdn.jsdelivr.net/gh/maomao1996/Vue-mmPlayer/screenshots/4.jpg)
248 | 
249 | ##### 我听过的
250 | 
251 | ![我听过的](https://cdn.jsdelivr.net/gh/maomao1996/Vue-mmPlayer/screenshots/5.jpg)
252 | 
253 | ##### 歌曲评论
254 | 
255 | ![歌曲评论](https://cdn.jsdelivr.net/gh/maomao1996/Vue-mmPlayer/screenshots/6.jpg)
256 | 
257 | #### 移动端
258 | 
259 | ![移动端一](https://cdn.jsdelivr.net/gh/maomao1996/Vue-mmPlayer/screenshots/7.jpg)
260 | ![移动端二](https://cdn.jsdelivr.net/gh/maomao1996/Vue-mmPlayer/screenshots/8.jpg)
261 | 
262 | </details>
263 | 
264 | ## 更新说明
265 | 
266 | ### V1.8.3(2022.12.01)
267 | 
268 | - 修复音乐搜索
269 | - 修复歌手信息为空
270 | - 优化横向滚动条样式
271 | 
272 | <details>
273 | <summary>查看更多</summary>
274 | 
275 | ### V1.8.2(2021.08.23)
276 | 
277 | - 移除我的歌单喜欢的音乐
278 | - 优化请求错误处理
279 | 
280 | ### V1.8.1(2021.02.02)
281 | 
282 | - 修复音乐进度条点击无效问题
283 | 
284 | ### V1.8.0(2020.08.22)
285 | 
286 | - 适配最新版后台 api
287 | - 修复背景图白边
288 | 
289 | ### V1.7.1(2020.07.11)
290 | 
291 | - 新增 IE 提示页面
292 | - 统一错误处理
293 | 
294 | ### V1.7.0(2020.06.27)
295 | 
296 | - 移动端增加歌词显示
297 | 
298 | ### V1.6.9(2020.06.04)
299 | 
300 | - 修改登录用户头像和网易云跳转地址为 https 协议
301 | 
302 | ### V1.6.8(2020.06.01)
303 | 
304 | - 修复歌单详情获取不到完整歌曲详情问题
305 | 
306 | ### V1.6.7(2020.05.02)
307 | 
308 | - 优化进度条拖动,分离拖动进度和音乐播放进度
309 | 
310 | ### V1.6.6(2020.04.18)
311 | 
312 | - 增加播放失败重试机制
313 | - 优化 `toHttps` 方法和版本更新时间的写入
314 | 
315 | ### V1.6.5(2020.04.09)
316 | 
317 | - 增加对 https 的支持
318 | 
319 | ### V1.6.4(2020.02.04)
320 | 
321 | - 调整默认音量
322 | 
323 | ### V1.6.3(2020.01.09)
324 | 
325 | - 修复快速滚动页面空白问题
326 | - 修复播放失败控制台报错问题
327 | 
328 | ### V1.6.2(2019.11.17)
329 | 
330 | - 提高歌词滚动精度
331 | 
332 | ### V1.6.1(2019.09.28)
333 | 
334 | - 修复歌单列表无数据时 JS 报错问题
335 | - 优化有文字复制选中时进度条拖动异常问题
336 | 
337 | ### V1.6.0(2019.08.26)
338 | 
339 | - 采用字体图标
340 | - 优化歌词滚动处理
341 | - 修复推荐页面样式问题
342 | - 调整封面图的分辨率
343 | - 优化首屏加载动画逻辑
344 | 
345 | ### V1.5.7(2019.08.19)
346 | 
347 | - 增加默认背景图随机展示,同时出除默认背景图,需开发者自行引入网络图 / 本地图
348 | - 调整默认音量
349 | - 优化首屏加载动画样式(提高逼格)
350 | - 优化 loading 遮罩颜色
351 | 
352 | ### V1.5.6(2019.04.04)
353 | 
354 | - 升级 `Vue` 版本
355 | - 优化脚手架配置
356 | - 修复 Safari、IOS 微信、安卓 UC 不能播放问题
357 | 
358 | ### V1.5.5(2019.03.29)
359 | 
360 | - 修改 `Vue` 构建版本
361 | - 优化滚动体验,缓存滚动位置
362 | - 优化暂停 / 播放逻辑,减少重复请求
363 | - 优化代码,提高复用
364 | - 修复 IOS 下滚动卡顿的情况
365 | 
366 | ### V1.5.4(2019.01.08)
367 | 
368 | - 更新后台服务器
369 | - 修复无法播放问题
370 | - 修复歌单详情打开失败问题
371 | - 修改音乐是否可用的判断逻辑
372 | - 优化登录操作体验,增加回车事件监听
373 | - 扩大查看评论者主页点击范围
374 | 
375 | ### V1.5.3(2018.07.30)
376 | 
377 | - 修复列表只有一首歌时的 `BUG`
378 | - 去除无关请求操作
379 | - 优化请求播放列表逻辑
380 | 
381 | ### V1.5.2(2018.05.23)
382 | 
383 | - 新增推荐歌单
384 | - 新增图片懒加载
385 | - 更新获取歌单列表接口
386 | - 优化歌单列表展示
387 | 
388 | ### V1.5.1(2018.05.21)
389 | 
390 | - 更新后台服务器
391 | - 修改热搜展示数据
392 | - 提取基础网络请求中的配置
393 | 
394 | ### V1.5.0(2018.05.05)
395 | 
396 | - 新增评论详情功能(网易云音乐最重要的部分不能漏)
397 | - 新增 `title` 提示
398 | - 新增 `noscript` 提示
399 | - 优化歌词滚动
400 | - 优化图片大小,提升加载速度
401 | - 优化歌曲切换时样式错乱
402 | - 增强模块化
403 | 
404 | ### V1.4.0(2018.04.09)预期功能全部完成
405 | 
406 | - 新增同步网易云歌单功能
407 | - 新增快捷键控制
408 |   - 上一曲 Ctrl + Left
409 |   - 播放暂停 Ctrl + Space
410 |   - 下一曲 Ctrl + Right
411 |   - 切换播放模式 Ctrl + O
412 |   - 音量加 Ctrl + Up
413 |   - 音量减 Ctrl + Down
414 | - 修复 safari 和安卓 UC 不能播放的问题
415 | - 优化 `url` 失效问题和音乐无法播放的提示
416 | - 优化移动端下的样式兼容
417 | 
418 | ### V1.3.2(2018.03.19)
419 | 
420 | - 新增播放链接失效后自动重载当前音乐
421 | - 优化列表循环不会自动下一曲问题
422 | - 优化删除正在播放列表歌曲失效问题
423 | - 优化删除歌曲过快会触发播放问题
424 | - 优化音乐来源错误不能播放问题,并使用 `oncanplay`
425 | - 添加播放历史,避免不能播放的音乐加入播放历史
426 | - 修复不能加入音乐到我听过的问题
427 | 
428 | ### V1.3.1(2018.03.12)
429 | 
430 | - 新增双击播放
431 | - 新增更新提示
432 | - 优化无歌词时的显示
433 | - 优化暂无内容提醒
434 | - 优化列表多位歌手的显示
435 | 
436 | ### V1.3.0(2018.03.07)
437 | 
438 | - 新增随机播放、列表循环、单曲循环、顺序播放功能
439 | - 新增清空正在播放列表功能
440 | - 新增清空列表的提示
441 | - 新增版权信息(控制台输入 mmPlayer )
442 | - 增加背景滤镜的模糊度和透明度
443 | - 增加浏览器访问的限制(兼容主流浏览器,最好全是用 chrome,哈哈)
444 | - 整合 `music-list` 组件
445 | - `CSS` 的 `@import` 使用 `~` 代替相对路径(原理:`css-loader` 会把非根路径的 `url` 解释为相对路径,加 `~` 前缀才会解释成模块路径)
446 | - 优化 Safari 下不能滚动和不能播放的问题
447 | - 优化移动端 300ms 点击延迟
448 | - 优化当播放列表只有一首歌时,点击上(下)一曲导致播放失败的问题
449 | - 优化重复插入音乐的问题
450 | - 优化暂停后播放下一首播放状态图标不改变的问题
451 | 
452 | ### V1.2.1(2018.03.01)
453 | 
454 | - 优化正在播放列表第一次加载
455 | - 优化删除歌曲
456 | - 优化 `Vuex` 模块
457 | - 优化加载 loading
458 | - 优化移动端适配
459 | - 提高代码复用性
460 | 
461 | ### V1.2.0(2018.02.28)
462 | 
463 | - 新增搜索功能
464 | - 新增歌曲删除功能(播放历史列表)
465 | - 使用 `ES6` 的 `class` 对数据进行二次处理
466 | - 优化歌词居中显示
467 | - 优化播放可能出现的错误
468 | 
469 | ### V1.1.0(2018.02.09)
470 | 
471 | - 新增我听过的(播放历史)
472 | - 整合公用列表组件
473 | - 新增 `mmToast` 插件
474 | - 整合字体大小、颜色相关 `CSS`
475 | - 优化清空正在播放列表功能
476 | 
477 | ### V1.0.0(2018.02.05)
478 | 
479 | - 发布正式版(因为一系列原因,mmPlayer V1.0.0 版本在试用版的基础上进行了重构了,并引入了 `Vue Router` 和 `Vuex`
480 | - 当前播放歌曲高亮(感觉一个小 GIF 还不够)
481 | - 优化快速切歌导致歌曲播放失败的问题
482 | - 进度条拖动适配移动端
483 | - 优化点击时可能出现的半透明背景
484 | - 新增排行榜
485 | 
486 | </details>
487 | 
488 | ## 数据统计
489 | 
490 | 因为百度统计现在数据存储时长默认为 1 年,造成前几年的数据都丢了(虽说没啥用,但是也是本作品成长的历史),所以在 [github](https://github.com/maomao1996/picture/tree/main/mmPlayer/stats) 保存下每年的累计访问
491 | 
492 | > 2023 年累计访问
493 | 
494 | ![2023](https://cdn.jsdelivr.net/gh/maomao1996/picture/mmPlayer/stats/2023.png)
495 | 
496 | > 2022 年累计访问
497 | 
498 | ![2022](https://cdn.jsdelivr.net/gh/maomao1996/picture/mmPlayer/stats/2022.png)
499 | 
500 | ## 其他说明
501 | 
502 | - 个人练手项目(本想先做移动端的,但是发现有很多人都做过,就稍微标新立异做个 PC 端)
503 | - 如果您喜欢该作品,您可以点右上角 "Star" "Fork" 表示支持 谢谢!
504 | - 如有问题请直接在 [Issues](https://github.com/maomao1996/Vue-mmPlayer/issues/new) 中提,或者您发现问题并有非常好的解决方案,欢迎 `PR`
505 | 
506 | ## 鸣谢
507 | 
508 | 特别感谢 [JetBrains](https://www.jetbrains.com/) 为开源项目提供免费的 [WebStorm](https://www.jetbrains.com/webstorm/) 授权
509 | 
510 | ## License
511 | 
512 | [MIT](https://github.com/maomao1996/Vue-mmPlayer/blob/LICENSE)
513 | 


--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 |   presets: ['@vue/cli-plugin-babel/preset'],
3 | }
4 | 


--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "es5",
 4 |     "module": "esnext",
 5 |     "moduleResolution": "node",
 6 |     "baseUrl": "./",
 7 |     "paths": {
 8 |       "@/*": [
 9 |         "src/*"
10 |       ],
11 |       "api/*": [
12 |         "src/api/*"
13 |       ],
14 |       "assets/*": [
15 |         "src/assets/*"
16 |       ],
17 |       "base/*": [
18 |         "src/base/*"
19 |       ],
20 |       "components/*": [
21 |         "src/components/*"
22 |       ],
23 |       "pages/*": [
24 |         "src/pages/*"
25 |       ],
26 |     },
27 |     "lib": [
28 |       "esnext",
29 |       "dom",
30 |       "dom.iterable",
31 |       "scripthost"
32 |     ]
33 |   }
34 | }
35 | 


--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "name": "vue-mmplayer",
 3 |   "version": "1.8.3",
 4 |   "private": true,
 5 |   "packageManager": "pnpm@8.0.0",
 6 |   "description": "Online music player",
 7 |   "author": "maomao1996 <1714487678@qq.com>",
 8 |   "bugs": {
 9 |     "url": "https://github.com/maomao1996/Vue-mmPlayer/issues"
10 |   },
11 |   "repository": {
12 |     "type": "git",
13 |     "url": "https://github.com/maomao1996/Vue-mmPlayer"
14 |   },
15 |   "license": "MIT",
16 |   "scripts": {
17 |     "dev": "vue-cli-service serve",
18 |     "serve": "vue-cli-service serve",
19 |     "build": "vue-cli-service build",
20 |     "lint": "vue-cli-service lint",
21 |     "prepare": "husky install"
22 |   },
23 |   "dependencies": {
24 |     "axios": "^1.6.2",
25 |     "core-js": "^3.33.3",
26 |     "fastclick": "^1.0.6",
27 |     "vue": "^2.6.14",
28 |     "vue-lazyload": "^1.3.4",
29 |     "vue-router": "^3.5.1",
30 |     "vuex": "^3.6.2"
31 |   },
32 |   "devDependencies": {
33 |     "@babel/core": "^7.23.5",
34 |     "@babel/eslint-parser": "^7.23.3",
35 |     "@femm/prettier": "^1.1.0",
36 |     "@femm/verify-commit": "^1.0.1",
37 |     "@vue/cli-plugin-babel": "~5.0.8",
38 |     "@vue/cli-plugin-eslint": "~5.0.8",
39 |     "@vue/cli-service": "~5.0.8",
40 |     "dayjs": "^1.11.10",
41 |     "eslint": "^7.32.0",
42 |     "eslint-config-prettier": "^8.5.0",
43 |     "eslint-plugin-prettier": "^4.2.1",
44 |     "eslint-plugin-vue": "^8.7.1",
45 |     "husky": "^8.0.3",
46 |     "less": "^4.2.0",
47 |     "less-loader": "^8.0.0",
48 |     "prettier": "^2.8.1",
49 |     "style-resources-loader": "^1.5.0",
50 |     "vue-cli-plugin-style-resources-loader": "^0.1.5",
51 |     "vue-template-compiler": "^2.6.14"
52 |   },
53 |   "prettier": "@femm/prettier"
54 | }
55 | 


--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 |   plugins: {
3 |     autoprefixer: {},
4 |   },
5 | }
6 | 


--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maomao1996/Vue-mmPlayer/ccba70781bef07fae53f697bca898ec429d326db/public/favicon.ico


--------------------------------------------------------------------------------
/public/img/warn.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maomao1996/Vue-mmPlayer/ccba70781bef07fae53f697bca898ec429d326db/public/img/warn.png


--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
  1 | <!DOCTYPE html>
  2 | <html>
  3 |   <head>
  4 |     <meta charset="utf-8" />
  5 |     <meta name="renderer" content="webkit" />
  6 |     <meta name="force-rendering" content="webkit" />
  7 |     <meta
  8 |       name="viewport"
  9 |       content="width=device-width,initial-scale=1,maximum-scale=1,minimum-scale=1,user-scalable=no"
 10 |     />
 11 |     <title>mmPlayer 在线音乐播放器</title>
 12 |     <meta
 13 |       name="keywords"
 14 |       content="mmPlayer,播放器,在线音乐,在线播放器,音乐播放器,在线音乐播放器,mmPlayer 在线音乐播放器"
 15 |     />
 16 |     <meta
 17 |       name="description"
 18 |       content="mmPlayer 是由茂茂开源的一款在线音乐播放器,具有音乐搜索、播放、歌词显示、播放历史、查看歌曲评论、网易云用户歌单播放同步等功能"
 19 |     />
 20 |     <link
 21 |       rel="stylesheet"
 22 |       href="//at.alicdn.com/t/font_1367495_eza6utwbiqn.css"
 23 |     />
 24 |     <style type="text/css">
 25 |       noscript {
 26 |         position: fixed;
 27 |         top: 0;
 28 |         right: 0;
 29 |         bottom: 0;
 30 |         left: 0;
 31 |         z-index: 1996520;
 32 |         background: #fff;
 33 |         text-align: center;
 34 |         font-weight: 700;
 35 |         font-size: 34px;
 36 |         line-height: 100px;
 37 |       }
 38 |       #appLoading {
 39 |         position: fixed;
 40 |         top: 0;
 41 |         left: 0;
 42 |         z-index: 1996;
 43 |         width: 100%;
 44 |         height: 100%;
 45 |         font-size: 20px;
 46 |         background: rgba(255, 255, 255, 1);
 47 |       }
 48 |       #appLoading.removeAnimate {
 49 |         animation: removeAnimate 0.3s 0.5s 1 both;
 50 |       }
 51 |       #appLoading .loader {
 52 |         position: absolute;
 53 |         top: 50%;
 54 |         left: 50%;
 55 |         width: 5em;
 56 |         height: 5em;
 57 |         transform: translate(-50%, -50%) rotate(165deg);
 58 |       }
 59 |       #appLoading .loader::before,
 60 |       #appLoading .loader::after {
 61 |         content: '';
 62 |         position: absolute;
 63 |         top: 50%;
 64 |         left: 50%;
 65 |         display: block;
 66 |         width: 1em;
 67 |         height: 1em;
 68 |         border-radius: 0.5em;
 69 |         transform: translate(-50%, -50%);
 70 |       }
 71 |       #appLoading .loader::before {
 72 |         animation: before 2s infinite;
 73 |       }
 74 |       #appLoading .loader::after {
 75 |         animation: after 2s infinite;
 76 |       }
 77 |       @keyframes before {
 78 |         0% {
 79 |           width: 1em;
 80 |           box-shadow: 2em -1em rgba(225, 20, 98, 0.75),
 81 |             -2em 1em rgba(111, 202, 220, 0.75);
 82 |         }
 83 |         35% {
 84 |           width: 5em;
 85 |           box-shadow: 0 -1em rgba(225, 20, 98, 0.75),
 86 |             0 1em rgba(111, 202, 220, 0.75);
 87 |         }
 88 |         70% {
 89 |           width: 1em;
 90 |           box-shadow: -2em -1em rgba(225, 20, 98, 0.75),
 91 |             2em 1em rgba(111, 202, 220, 0.75);
 92 |         }
 93 |         100% {
 94 |           box-shadow: 2em -1em rgba(225, 20, 98, 0.75),
 95 |             -2em 1em rgba(111, 202, 220, 0.75);
 96 |         }
 97 |       }
 98 |       @keyframes after {
 99 |         0% {
100 |           height: 1em;
101 |           box-shadow: 1em 2em rgba(61, 184, 143, 0.75),
102 |             -1em -2em rgba(233, 169, 32, 0.75);
103 |         }
104 |         35% {
105 |           height: 5em;
106 |           box-shadow: 1em 0 rgba(61, 184, 143, 0.75),
107 |             -1em 0 rgba(233, 169, 32, 0.75);
108 |         }
109 |         70% {
110 |           height: 1em;
111 |           box-shadow: 1em -2em rgba(61, 184, 143, 0.75),
112 |             -1em 2em rgba(233, 169, 32, 0.75);
113 |         }
114 |         100% {
115 |           box-shadow: 1em 2em rgba(61, 184, 143, 0.75),
116 |             -1em -2em rgba(233, 169, 32, 0.75);
117 |         }
118 |       }
119 |       @keyframes removeAnimate {
120 |         from {
121 |           opacity: 1;
122 |         }
123 |         to {
124 |           opacity: 0;
125 |         }
126 |       }
127 |     </style>
128 |     <script>
129 |       ;(function () {
130 |         if (!!window.ActiveXObject || 'ActiveXObject' in window) {
131 |           window.location = './prompt.html'
132 |           return false
133 |         }
134 |       })()
135 |     </script>
136 |     <% if ( NODE_ENV === 'production' ) { %>
137 |     <script>
138 |       var _hmt = _hmt || []
139 |       window._hmt = _hmt
140 |       ;(function () {
141 |         var hm = document.createElement('script')
142 |         hm.src = 'https://hm.baidu.com/hm.js?71e62b6d09afa9deac7bfa5c60ad06dd'
143 |         var s = document.getElementsByTagName('script')[0]
144 |         s.parentNode.insertBefore(hm, s)
145 |       })()
146 |     </script>
147 |     <% } %>
148 |   </head>
149 |   <body>
150 |     <noscript>
151 |       mmPlayer 在线音乐播放器<br />为了更好的体验请开启 script
152 |     </noscript>
153 |     <div id="appLoading">
154 |       <div class="loader"></div>
155 |     </div>
156 |     <div id="mmPlayer">
157 |       mmPlayer
158 |       是由茂茂开源的一款在线音乐播放器,具有音乐搜索、播放、歌词显示、播放历史、查看歌曲评论、网易云用户歌单播放同步等功能
159 |     </div>
160 |     <!-- built files will be auto injected -->
161 |   </body>
162 | </html>
163 | 


--------------------------------------------------------------------------------
/public/prompt.html:
--------------------------------------------------------------------------------
 1 | <!DOCTYPE html>
 2 | <html>
 3 | <head>
 4 |   <meta charset="UTF-8">
 5 |   <meta name="robots" content="noindex,nofollow"/>
 6 |   <title>mmPlayer | 温馨提示</title>
 7 |   <style>
 8 |     body{font-size: 14px;font-family: 'helvetica neue',tahoma,arial,'hiragino sans gb','microsoft yahei','Simsun',sans-serif; background-color:#fff; color:#808080;}
 9 |     .wrap{margin:200px auto;width:510px;}
10 |     td{text-align:left; padding:2px 10px;}
11 |     td.header{font-size:22px; padding-bottom:10px; color:#000;}
12 |     td.check-info{padding-top:20px;}
13 |     a{color:#328ce5; text-decoration:none;}
14 |     a:hover{text-decoration:underline;}
15 |   </style>
16 | </head>
17 | <body>
18 |   <div class="wrap">
19 |     <table>
20 |       <tr>
21 |         <td rowspan="5"><img src="./img/warn.png"></td>
22 |         <td class="header">mmPlayer | 温馨提示</td>
23 |       </tr>
24 |       <tr><td></td></tr>
25 |       <tr><td>很抱歉!为了更好的体验,本站限制以下浏览器访问:</td></tr>
26 |       <tr><td>IE浏览器和使用IE内核的浏览器</td></tr>
27 |       <tr><td>解决办法:下载其他主流浏览器或者切换浏览器内核为极速内核</td></tr>
28 |     </table>
29 |   </div>
30 | </body>
31 | </html>
32 | 


--------------------------------------------------------------------------------
/screenshots/1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maomao1996/Vue-mmPlayer/ccba70781bef07fae53f697bca898ec429d326db/screenshots/1.jpg


--------------------------------------------------------------------------------
/screenshots/2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maomao1996/Vue-mmPlayer/ccba70781bef07fae53f697bca898ec429d326db/screenshots/2.jpg


--------------------------------------------------------------------------------
/screenshots/3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maomao1996/Vue-mmPlayer/ccba70781bef07fae53f697bca898ec429d326db/screenshots/3.jpg


--------------------------------------------------------------------------------
/screenshots/4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maomao1996/Vue-mmPlayer/ccba70781bef07fae53f697bca898ec429d326db/screenshots/4.jpg


--------------------------------------------------------------------------------
/screenshots/5.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maomao1996/Vue-mmPlayer/ccba70781bef07fae53f697bca898ec429d326db/screenshots/5.jpg


--------------------------------------------------------------------------------
/screenshots/6.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maomao1996/Vue-mmPlayer/ccba70781bef07fae53f697bca898ec429d326db/screenshots/6.jpg


--------------------------------------------------------------------------------
/screenshots/7.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maomao1996/Vue-mmPlayer/ccba70781bef07fae53f697bca898ec429d326db/screenshots/7.jpg


--------------------------------------------------------------------------------
/screenshots/8.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maomao1996/Vue-mmPlayer/ccba70781bef07fae53f697bca898ec429d326db/screenshots/8.jpg


--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
  1 | <template>
  2 |   <div id="app">
  3 |     <!--主体-->
  4 |     <mm-header />
  5 |     <router-view />
  6 |     <!--更新说明-->
  7 |     <mm-dialog ref="versionDialog" type="alert" head-text="更新提示" :body-text="versionInfo" />
  8 |     <!--播放器-->
  9 |     <audio ref="mmPlayer"></audio>
 10 |   </div>
 11 | </template>
 12 | 
 13 | <script>
 14 | import { mapMutations, mapActions } from 'vuex'
 15 | import { getPlaylistDetail } from 'api'
 16 | import { MMPLAYER_CONFIG, VERSION } from '@/config'
 17 | import MmHeader from 'components/mm-header/mm-header'
 18 | import MmDialog from 'base/mm-dialog/mm-dialog'
 19 | import { getVersion, setVersion } from '@/utils/storage'
 20 | 
 21 | const VERSION_INFO = `<div class="mm-dialog-text text-left">
 22 | 版本号:${VERSION}(${process.env.VUE_APP_UPDATE_TIME})<br/>
 23 | 1、 采用新版图标<br>
 24 | 2、 修复音乐搜索<br>
 25 | 3、 优化滚动条样式
 26 | </div>`
 27 | 
 28 | export default {
 29 |   name: 'App',
 30 |   components: {
 31 |     MmHeader,
 32 |     MmDialog,
 33 |   },
 34 |   created() {
 35 |     // 设置版本更新信息
 36 |     this.versionInfo = VERSION_INFO
 37 | 
 38 |     // 获取正在播放列表
 39 |     getPlaylistDetail(MMPLAYER_CONFIG.PLAYLIST_ID).then((playlist) => {
 40 |       const list = playlist.tracks.slice(0, 100)
 41 |       this.setPlaylist({ list })
 42 |     })
 43 | 
 44 |     // 设置title
 45 |     let OriginTitile = document.title
 46 |     let titleTime
 47 |     document.addEventListener('visibilitychange', function () {
 48 |       if (document.hidden) {
 49 |         document.title = '死鬼去哪里了!'
 50 |         clearTimeout(titleTime)
 51 |       } else {
 52 |         document.title = '(つェ⊂)咦!又好了!'
 53 |         titleTime = setTimeout(function () {
 54 |           document.title = OriginTitile
 55 |         }, 2000)
 56 |       }
 57 |     })
 58 | 
 59 |     // 设置audio元素
 60 |     this.$nextTick(() => {
 61 |       this.setAudioele(this.$refs.mmPlayer)
 62 |     })
 63 | 
 64 |     // 首次加载完成后移除动画
 65 |     let loadDOM = document.querySelector('#appLoading')
 66 |     if (loadDOM) {
 67 |       const animationendFunc = function () {
 68 |         loadDOM.removeEventListener('animationend', animationendFunc)
 69 |         loadDOM.removeEventListener('webkitAnimationEnd', animationendFunc)
 70 |         document.body.removeChild(loadDOM)
 71 |         loadDOM = null
 72 |         const version = getVersion()
 73 |         if (version !== null) {
 74 |           setVersion(VERSION)
 75 |           if (version !== VERSION) {
 76 |             this.$refs.versionDialog.show()
 77 |           }
 78 |         } else {
 79 |           setVersion(VERSION)
 80 |           this.$refs.versionDialog.show()
 81 |         }
 82 |       }.bind(this)
 83 |       loadDOM.addEventListener('animationend', animationendFunc)
 84 |       loadDOM.addEventListener('webkitAnimationEnd', animationendFunc)
 85 |       loadDOM.classList.add('removeAnimate')
 86 |     }
 87 |   },
 88 |   methods: {
 89 |     ...mapMutations({
 90 |       setAudioele: 'SET_AUDIOELE',
 91 |     }),
 92 |     ...mapActions(['setPlaylist']),
 93 |   },
 94 | }
 95 | </script>
 96 | 
 97 | <style lang="less">
 98 | #app {
 99 |   position: relative;
100 |   width: 100%;
101 |   height: 100%;
102 |   color: @text_color;
103 |   font-size: @font_size_medium;
104 | 
105 |   audio {
106 |     position: fixed;
107 |   }
108 | }
109 | </style>
110 | 


--------------------------------------------------------------------------------
/src/api/index.js:
--------------------------------------------------------------------------------
  1 | import axios from '@/utils/axios'
  2 | import { DEFAULT_LIMIT } from '@/config'
  3 | import { formatSongs } from '@/utils/song'
  4 | 
  5 | // 排行榜列表
  6 | export function getToplistDetail() {
  7 |   return axios.get('/toplist/detail')
  8 | }
  9 | 
 10 | // 推荐歌单
 11 | export function getPersonalized() {
 12 |   return axios.get('/personalized')
 13 | }
 14 | 
 15 | // 歌单详情
 16 | export function getPlaylistDetail(id) {
 17 |   return new Promise((resolve, reject) => {
 18 |     axios
 19 |       .get('/playlist/detail', {
 20 |         params: { id },
 21 |       })
 22 |       .then(({ playlist }) => playlist || {})
 23 |       .then((playlist) => {
 24 |         const { trackIds, tracks } = playlist
 25 |         if (!Array.isArray(trackIds)) {
 26 |           reject(new Error('获取歌单详情失败'))
 27 |           return
 28 |         }
 29 |         // 过滤完整歌单 如排行榜
 30 |         if (tracks.length === trackIds.length) {
 31 |           playlist.tracks = formatSongs(playlist.tracks)
 32 |           resolve(playlist)
 33 |           return
 34 |         }
 35 |         // 限制歌单详情最大 500
 36 |         const ids = trackIds
 37 |           .slice(0, 500)
 38 |           .map((v) => v.id)
 39 |           .toString()
 40 |         getMusicDetail(ids).then(({ songs }) => {
 41 |           playlist.tracks = formatSongs(songs)
 42 |           resolve(playlist)
 43 |         })
 44 |       })
 45 |   })
 46 | }
 47 | 
 48 | // 搜索
 49 | export function search(keywords, page = 0, limit = DEFAULT_LIMIT) {
 50 |   return axios.get('/search', {
 51 |     params: {
 52 |       offset: page * limit,
 53 |       limit: limit,
 54 |       keywords,
 55 |     },
 56 |   })
 57 | }
 58 | 
 59 | // 热搜
 60 | export function searchHot() {
 61 |   return axios.get('/search/hot')
 62 | }
 63 | 
 64 | // 获取用户歌单详情
 65 | export function getUserPlaylist(uid) {
 66 |   return axios.get('/user/playlist', {
 67 |     params: {
 68 |       uid,
 69 |     },
 70 |   })
 71 | }
 72 | 
 73 | // 获取歌曲详情
 74 | export function getMusicDetail(ids) {
 75 |   return axios.get('/song/detail', {
 76 |     params: {
 77 |       ids,
 78 |     },
 79 |   })
 80 | }
 81 | 
 82 | // 获取音乐是否可以用
 83 | export function getCheckMusic(id) {
 84 |   return axios.get('/check/music', {
 85 |     params: {
 86 |       id,
 87 |     },
 88 |   })
 89 | }
 90 | 
 91 | // 获取音乐地址
 92 | export function getMusicUrl(id) {
 93 |   return axios.get('/song/url', {
 94 |     params: {
 95 |       id,
 96 |     },
 97 |   })
 98 | }
 99 | 
100 | // 获取歌词
101 | export function getLyric(id) {
102 |   const url = '/lyric'
103 |   return axios.get(url, {
104 |     params: {
105 |       id,
106 |     },
107 |   })
108 | }
109 | 
110 | // 获取音乐评论
111 | export function getComment(id, page, limit = DEFAULT_LIMIT) {
112 |   return axios.get('/comment/music', {
113 |     params: {
114 |       offset: page * limit,
115 |       limit: limit,
116 |       id,
117 |     },
118 |   })
119 | }
120 | 


--------------------------------------------------------------------------------
/src/assets/background/bg-1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maomao1996/Vue-mmPlayer/ccba70781bef07fae53f697bca898ec429d326db/src/assets/background/bg-1.jpg


--------------------------------------------------------------------------------
/src/assets/background/bg-2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maomao1996/Vue-mmPlayer/ccba70781bef07fae53f697bca898ec429d326db/src/assets/background/bg-2.jpg


--------------------------------------------------------------------------------
/src/assets/background/bg-3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maomao1996/Vue-mmPlayer/ccba70781bef07fae53f697bca898ec429d326db/src/assets/background/bg-3.jpg


--------------------------------------------------------------------------------
/src/assets/img/album_cover_player.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maomao1996/Vue-mmPlayer/ccba70781bef07fae53f697bca898ec429d326db/src/assets/img/album_cover_player.png


--------------------------------------------------------------------------------
/src/assets/img/default.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maomao1996/Vue-mmPlayer/ccba70781bef07fae53f697bca898ec429d326db/src/assets/img/default.png


--------------------------------------------------------------------------------
/src/assets/img/player_cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maomao1996/Vue-mmPlayer/ccba70781bef07fae53f697bca898ec429d326db/src/assets/img/player_cover.png


--------------------------------------------------------------------------------
/src/assets/img/wave.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maomao1996/Vue-mmPlayer/ccba70781bef07fae53f697bca898ec429d326db/src/assets/img/wave.gif


--------------------------------------------------------------------------------
/src/base/mm-dialog/mm-dialog.vue:
--------------------------------------------------------------------------------
  1 | <template>
  2 |   <!--对话框-->
  3 |   <transition name="mm-dialog-fade">
  4 |     <div v-show="dialogShow" class="mm-dialog-box">
  5 |       <div class="mm-dialog-wrapper">
  6 |         <div class="mm-dialog-content">
  7 |           <div class="mm-dialog-head" v-text="headText"></div>
  8 |           <slot>
  9 |             <!-- eslint-disable-next-line vue/no-v-html -->
 10 |             <div class="mm-dialog-text" v-html="bodyText"></div>
 11 |           </slot>
 12 |           <div class="mm-dialog-btns">
 13 |             <div
 14 |               v-if="dialogType !== 'alert'"
 15 |               class="mm-btn-cancel"
 16 |               @click="cancel"
 17 |               v-text="cancelBtnText"
 18 |             ></div>
 19 |             <slot name="btn"></slot>
 20 |             <div class="mm-btn-confirm" @click="confirm" v-text="confirmBtnText"></div>
 21 |           </div>
 22 |         </div>
 23 |       </div>
 24 |     </div>
 25 |   </transition>
 26 | </template>
 27 | 
 28 | <script>
 29 | export default {
 30 |   name: 'MmDialog',
 31 |   props: {
 32 |     // type:confirm、alert
 33 |     type: {
 34 |       type: String,
 35 |       default: 'confirm',
 36 |     },
 37 |     // 标题文本
 38 |     headText: {
 39 |       type: String,
 40 |       default: '提示',
 41 |     },
 42 |     // 内容文本(可以是html)
 43 |     bodyText: {
 44 |       type: String,
 45 |       default: '',
 46 |     },
 47 |     // 取消按钮文本
 48 |     cancelBtnText: {
 49 |       type: String,
 50 |       default: '取消',
 51 |     },
 52 |     // 确定按钮文本
 53 |     confirmBtnText: {
 54 |       type: String,
 55 |       default: '确定',
 56 |     },
 57 |     // Dialog 是否插入至 body 元素下
 58 |     appendToBody: {
 59 |       type: Boolean,
 60 |       default: true,
 61 |     },
 62 |   },
 63 |   data() {
 64 |     return {
 65 |       dialogShow: false, // 是否显示对话框
 66 |     }
 67 |   },
 68 |   computed: {
 69 |     dialogType() {
 70 |       return this.type.toLowerCase()
 71 |     },
 72 |   },
 73 |   watch: {
 74 |     dialogShow(val) {
 75 |       if (val && this.appendToBody) {
 76 |         document.body.appendChild(this.$el)
 77 |       }
 78 |     },
 79 |   },
 80 |   mounted() {
 81 |     if (this.dialogShow && this.appendToBody) {
 82 |       document.body.appendChild(this.$el)
 83 |     }
 84 |   },
 85 |   destroyed() {
 86 |     if (this.appendToBody && this.$el && this.$el.parentNode) {
 87 |       this.$el.parentNode.removeChild(this.$el)
 88 |     }
 89 |   },
 90 |   methods: {
 91 |     // 显示事件
 92 |     show() {
 93 |       this.dialogShow = true
 94 |     },
 95 |     // 隐藏事件
 96 |     hide() {
 97 |       this.dialogShow = false
 98 |     },
 99 |     // 取消事件
100 |     cancel() {
101 |       this.hide()
102 |       this.$emit('cancel')
103 |     },
104 |     // 确定事件
105 |     confirm() {
106 |       this.hide()
107 |       this.$emit('confirm')
108 |     },
109 |   },
110 | }
111 | </script>
112 | 
113 | <style lang="less">
114 | @dialog-prefix-cls: mm-dialog;
115 | 
116 | .@{dialog-prefix-cls}-box {
117 |   position: fixed;
118 |   left: 0;
119 |   right: 0;
120 |   top: 0;
121 |   bottom: 0;
122 |   z-index: 1996;
123 |   background-color: @dialog_bg_color;
124 |   user-select: none;
125 |   backdrop-filter: @backdrop_filter;
126 |   &.@{dialog-prefix-cls}-fade-enter-active {
127 |     animation: mm-dialog-fadein 0.3s;
128 |     .@{dialog-prefix-cls}-content {
129 |       animation: mm-dialog-zoom 0.3s;
130 |     }
131 |   }
132 |   .@{dialog-prefix-cls}-wrapper {
133 |     position: absolute;
134 |     top: 50%;
135 |     left: 50%;
136 |     transform: translate(-50%, -50%);
137 |     z-index: 1996;
138 |     .@{dialog-prefix-cls}-content {
139 |       width: 420px;
140 |       border-radius: @dialog_border_radius;
141 |       background: @dialog_content_bg_color;
142 |       @media (max-width: 767px) {
143 |         width: 270px;
144 |         border-radius: @dialog_mobile_border_radius;
145 |         text-align: center;
146 |       }
147 |       .@{dialog-prefix-cls}-head {
148 |         padding: 15px;
149 |         padding-bottom: 0;
150 |         font-size: @font_size_large;
151 |         color: @text_color_active;
152 |       }
153 |       .@{dialog-prefix-cls}-text {
154 |         padding: 20px 15px;
155 |         line-height: 22px;
156 |         font-size: @font_size_medium;
157 |         color: @dialog_text_color;
158 |       }
159 |       .@{dialog-prefix-cls}-btns {
160 |         display: flex;
161 |         align-items: center;
162 |         padding: 0 15px 10px;
163 |         text-align: center;
164 |         color: @dialog_text_color;
165 |         @media (min-width: 768px) {
166 |           justify-content: flex-end;
167 |           div {
168 |             display: block;
169 |             padding: 8px 15px;
170 |             border-radius: @dialog_btn_mobile_border_radius;
171 |             border: 1px solid @btn_color;
172 |             font-size: @font_size_medium;
173 |             cursor: pointer;
174 |             &:not(:nth-of-type(1)) {
175 |               margin-left: 10px;
176 |             }
177 |             &:hover {
178 |               color: @text_color_active;
179 |               border: 1px solid @btn_color_active;
180 |             }
181 |           }
182 |         }
183 |         @media (max-width: 767px) {
184 |           & {
185 |             padding: 0;
186 |             justify-content: center;
187 |             div {
188 |               flex: 1;
189 |               line-height: 22px;
190 |               padding: 10px 0;
191 |               border-top: 1px solid @dialog_line_color;
192 |               font-size: @font_size_large;
193 |               &:not(:nth-of-type(1)) {
194 |                 border-left: 1px solid @dialog_line_color;
195 |               }
196 |             }
197 |           }
198 |         }
199 |       }
200 |     }
201 |   }
202 | }
203 | 
204 | @keyframes mm-dialog-fadein {
205 |   0% {
206 |     opacity: 0;
207 |   }
208 |   100% {
209 |     opacity: 1;
210 |   }
211 | }
212 | 
213 | @keyframes mm-dialog-zoom {
214 |   0% {
215 |     transform: scale(0);
216 |   }
217 |   50% {
218 |     transform: scale(1.1);
219 |   }
220 |   100% {
221 |     transform: scale(1);
222 |   }
223 | }
224 | </style>
225 | 


--------------------------------------------------------------------------------
/src/base/mm-icon/mm-icon.vue:
--------------------------------------------------------------------------------
 1 | <!-- icon 组件 -->
 2 | <script>
 3 | export default {
 4 |   name: 'MmIcon',
 5 |   props: {
 6 |     type: {
 7 |       type: String,
 8 |       required: true,
 9 |     },
10 |     size: {
11 |       type: Number,
12 |       default: 16,
13 |     },
14 |   },
15 |   methods: {
16 |     getIconCls() {
17 |       return `icon-${this.type}`
18 |     },
19 |     getIconStyle() {
20 |       return { fontSize: this.size + 'px' }
21 |     },
22 |     onClick(e) {
23 |       this.$emit('click', e)
24 |     },
25 |   },
26 |   render() {
27 |     const Icon = (
28 |       <i
29 |         onClick={this.onClick}
30 |         class={`iconfont ${this.getIconCls()}`}
31 |         style={this.getIconStyle()}
32 |       />
33 |     )
34 |     return Icon
35 |   },
36 | }
37 | </script>
38 | 
39 | <style lang="less">
40 | .iconfont {
41 |   display: inline-block;
42 |   font-style: normal;
43 |   font-weight: normal;
44 |   font-variant: normal;
45 |   line-height: 1;
46 |   vertical-align: baseline;
47 |   text-transform: none;
48 |   speak: none;
49 |   /* Better Font Rendering =========== */
50 |   -webkit-font-smoothing: antialiased;
51 |   -moz-osx-font-smoothing: grayscale;
52 | }
53 | </style>
54 | 


--------------------------------------------------------------------------------
/src/base/mm-loading/mm-loading.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <!--加载动画-->
 3 |   <div v-show="value" class="mm-loading" :style="{ backgroundColor: loadingBgColor }">
 4 |     <div class="mm-loading-content">
 5 |       <svg class="circular" viewBox="25 25 50 50">
 6 |         <circle class="path" cx="50" cy="50" r="20" fill="none"></circle>
 7 |       </svg>
 8 |     </div>
 9 |   </div>
10 | </template>
11 | 
12 | <script>
13 | export default {
14 |   name: 'MmLoading',
15 |   props: {
16 |     // 是否显示
17 |     value: {
18 |       type: Boolean,
19 |       default: true,
20 |     },
21 |     // 加载动画背景颜色
22 |     loadingBgColor: {
23 |       type: String,
24 |       default: '',
25 |     },
26 |   },
27 | }
28 | </script>
29 | 
30 | <style lang="less">
31 | .mm-loading {
32 |   position: absolute;
33 |   top: 0;
34 |   left: 0;
35 |   right: 0;
36 |   bottom: 0;
37 |   z-index: 1996;
38 |   background: @load_bg_color;
39 |   .mm-loading-content {
40 |     position: absolute;
41 |     top: 50%;
42 |     width: 100%;
43 |     transform: translateY(-50%);
44 |     text-align: center;
45 |     .circular {
46 |       height: 50px;
47 |       width: 50px;
48 |       animation: loading-rotate 2s linear infinite;
49 |       .path {
50 |         animation: loading-dash 1.5s ease-in-out infinite;
51 |         stroke-dasharray: 90, 150;
52 |         stroke-dashoffset: 0;
53 |         stroke-width: 2;
54 |         stroke: @text_color;
55 |         stroke-linecap: round;
56 |       }
57 |     }
58 |   }
59 | }
60 | 
61 | //动画函数
62 | @keyframes loading-rotate {
63 |   100% {
64 |     transform: rotate(360deg);
65 |   }
66 | }
67 | 
68 | @keyframes loading-dash {
69 |   0% {
70 |     stroke-dasharray: 1, 200;
71 |     stroke-dashoffset: 0;
72 |   }
73 |   50% {
74 |     stroke-dasharray: 90, 150;
75 |     stroke-dashoffset: -40px;
76 |   }
77 |   100% {
78 |     stroke-dasharray: 90, 150;
79 |     stroke-dashoffset: -120px;
80 |   }
81 | }
82 | </style>
83 | 


--------------------------------------------------------------------------------
/src/base/mm-no-result/mm-no-result.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <!--暂无数据提示-->
 3 |   <div class="mm-no-result">
 4 |     <p class="mm-no-result-text">{{ title }}</p>
 5 |   </div>
 6 | </template>
 7 | 
 8 | <script>
 9 | export default {
10 |   name: 'MmNoResult',
11 |   props: {
12 |     // 无数据提示文本
13 |     title: {
14 |       type: String,
15 |       default: '',
16 |     },
17 |   },
18 | }
19 | </script>
20 | 
21 | <style lang="less">
22 | .mm-no-result {
23 |   display: flex;
24 |   justify-content: center;
25 |   align-items: center;
26 |   width: 100%;
27 |   height: 100%;
28 |   &-text {
29 |     margin-top: 30px;
30 |     font-size: @font_size_medium;
31 |     color: @text_color;
32 |   }
33 | }
34 | </style>
35 | 


--------------------------------------------------------------------------------
/src/base/mm-progress/mm-progress.vue:
--------------------------------------------------------------------------------
  1 | <template>
  2 |   <!--进度条拖动-->
  3 |   <div ref="mmProgress" class="mmProgress" @click="barClick">
  4 |     <div class="mmProgress-bar"></div>
  5 |     <div ref="mmPercentProgress" class="mmProgress-outer"></div>
  6 |     <div ref="mmProgressInner" class="mmProgress-inner">
  7 |       <div class="mmProgress-dot" @mousedown="barDown" @touchstart.prevent="barDown"></div>
  8 |     </div>
  9 |   </div>
 10 | </template>
 11 | 
 12 | <script>
 13 | const dotWidth = 10
 14 | export default {
 15 |   name: 'MmProgress',
 16 |   props: {
 17 |     // 进度值一
 18 |     percent: {
 19 |       type: [Number],
 20 |       default: 0,
 21 |     },
 22 |     // 进度值二(歌曲缓冲进度用)
 23 |     percentProgress: {
 24 |       type: [Number],
 25 |       default: 0,
 26 |     },
 27 |   },
 28 |   data() {
 29 |     return {
 30 |       move: {
 31 |         status: false, // 是否可拖动
 32 |         startX: 0, // 记录最开始点击的X坐标
 33 |         left: 0, // 记录当前已经移动的距离
 34 |       },
 35 |     }
 36 |   },
 37 |   watch: {
 38 |     percent(newPercent) {
 39 |       if (newPercent >= 0 && !this.move.status) {
 40 |         const barWidth = this.$refs.mmProgress.clientWidth - dotWidth
 41 |         const offsetWidth = newPercent * barWidth
 42 |         this.moveSilde(offsetWidth)
 43 |       }
 44 |     },
 45 |     percentProgress(newValue) {
 46 |       let offsetWidth = this.$refs.mmProgress.clientWidth * newValue
 47 |       this.$refs.mmPercentProgress.style.width = `${offsetWidth}px`
 48 |     },
 49 |   },
 50 |   mounted() {
 51 |     this.$nextTick(() => {
 52 |       this.bindEvents()
 53 |       const barWidth = this.$refs.mmProgress.clientWidth - dotWidth
 54 |       const offsetWidth = this.percent * barWidth
 55 |       this.moveSilde(offsetWidth)
 56 |     })
 57 |   },
 58 |   beforeDestroy() {
 59 |     this.unbindEvents()
 60 |   },
 61 |   methods: {
 62 |     // 添加绑定事件
 63 |     bindEvents() {
 64 |       document.addEventListener('mousemove', this.barMove)
 65 |       document.addEventListener('mouseup', this.barUp)
 66 | 
 67 |       document.addEventListener('touchmove', this.barMove)
 68 |       document.addEventListener('touchend', this.barUp)
 69 |     },
 70 |     // 移除绑定事件
 71 |     unbindEvents() {
 72 |       document.removeEventListener('mousemove', this.barMove)
 73 |       document.removeEventListener('mouseup', this.barUp)
 74 | 
 75 |       document.removeEventListener('touchmove', this.barMove)
 76 |       document.removeEventListener('touchend', this.barUp)
 77 |     },
 78 |     // 点击事件
 79 |     barClick(e) {
 80 |       let rect = this.$refs.mmProgress.getBoundingClientRect()
 81 |       let offsetWidth = Math.min(
 82 |         this.$refs.mmProgress.clientWidth - dotWidth,
 83 |         Math.max(0, e.clientX - rect.left),
 84 |       )
 85 |       this.moveSilde(offsetWidth)
 86 |       this.commitPercent(true)
 87 |     },
 88 |     // 鼠标按下事件
 89 |     barDown(e) {
 90 |       this.move.status = true
 91 |       this.move.startX = e.clientX || e.touches[0].pageX
 92 |       this.move.left = this.$refs.mmProgressInner.clientWidth
 93 |     },
 94 |     // 鼠标/触摸移动事件
 95 |     barMove(e) {
 96 |       if (!this.move.status) {
 97 |         return false
 98 |       }
 99 |       e.preventDefault()
100 |       let endX = e.clientX || e.touches[0].pageX
101 |       let dist = endX - this.move.startX
102 |       let offsetWidth = Math.min(
103 |         this.$refs.mmProgress.clientWidth - dotWidth,
104 |         Math.max(0, this.move.left + dist),
105 |       )
106 |       this.moveSilde(offsetWidth)
107 |       this.commitPercent()
108 |     },
109 |     // 鼠标/触摸释放事件
110 |     barUp(e) {
111 |       if (this.move.status) {
112 |         this.commitPercent(true)
113 |         this.move.status = false
114 |       }
115 |     },
116 |     // 移动滑块
117 |     moveSilde(offsetWidth) {
118 |       this.$refs.mmProgressInner.style.width = `${offsetWidth}px`
119 |     },
120 |     // 修改 percent
121 |     commitPercent(isEnd = false) {
122 |       const { mmProgress, mmProgressInner } = this.$refs
123 |       const lineWidth = mmProgress.clientWidth - dotWidth
124 |       const percent = mmProgressInner.clientWidth / lineWidth
125 |       this.$emit(isEnd ? 'percentChangeEnd' : 'percentChange', percent)
126 |     },
127 |   },
128 | }
129 | </script>
130 | 
131 | <style lang="less">
132 | .mmProgress {
133 |   position: relative;
134 |   padding: 5px;
135 |   user-select: none;
136 |   cursor: pointer;
137 |   overflow: hidden;
138 |   .mmProgress-bar {
139 |     height: 2px;
140 |     width: 100%;
141 |     background: @bar_color;
142 |   }
143 |   .mmProgress-outer {
144 |     position: absolute;
145 |     top: 50%;
146 |     left: 5px;
147 |     display: inline-block;
148 |     width: 0;
149 |     height: 2px;
150 |     margin-top: -1px;
151 |     background: rgba(255, 255, 255, 0.2);
152 |   }
153 |   .mmProgress-inner {
154 |     position: absolute;
155 |     top: 50%;
156 |     left: 5px;
157 |     display: inline-block;
158 |     width: 0;
159 |     height: 2px;
160 |     margin-top: -1px;
161 |     background: @line_color;
162 |     .mmProgress-dot {
163 |       position: absolute;
164 |       top: 50%;
165 |       right: -5px;
166 |       width: 10px;
167 |       height: 10px;
168 |       border-radius: 50%;
169 |       background-color: @dot_color;
170 |       transform: translateY(-50%);
171 |     }
172 |   }
173 | }
174 | </style>
175 | 


--------------------------------------------------------------------------------
/src/base/mm-toast/index.js:
--------------------------------------------------------------------------------
 1 | import TempToast from './mm-toast.vue'
 2 | 
 3 | let instance
 4 | let showToast = false
 5 | let time // 存储toast显示状态
 6 | const mmToast = {
 7 |   install(Vue, options = {}) {
 8 |     let opt = TempToast.data() // 获取组件中的默认配置
 9 |     Object.assign(opt, options) // 合并配置
10 |     Vue.prototype.$mmToast = (message, position) => {
11 |       if (showToast) {
12 |         clearTimeout(time)
13 |         instance.vm.visible = showToast = false
14 |         document.body.removeChild(instance.vm.$el)
15 |         // return;// 如果toast还在,则不再执行
16 |       }
17 |       if (message) {
18 |         opt.message = message // 如果有传message,则使用所传的message
19 |       }
20 |       if (position) {
21 |         opt.position = position // 如果有传type,则使用所传的type
22 |       }
23 |       let TempToastConstructor = Vue.extend(TempToast)
24 |       instance = new TempToastConstructor({
25 |         data: opt,
26 |       })
27 |       instance.vm = instance.$mount()
28 |       document.body.appendChild(instance.vm.$el)
29 |       instance.vm.visible = showToast = true
30 | 
31 |       time = setTimeout(function () {
32 |         instance.vm.visible = showToast = false
33 |         document.body.removeChild(instance.vm.$el)
34 |       }, opt.duration)
35 |     }
36 |   },
37 | }
38 | 
39 | export default mmToast
40 | 


--------------------------------------------------------------------------------
/src/base/mm-toast/mm-toast.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <!--弹出层提示-->
 3 |   <transition name="toast-fade">
 4 |     <div v-show="visible" class="mm-toast" :class="positionClasss">
 5 |       {{ message }}
 6 |     </div>
 7 |   </transition>
 8 | </template>
 9 | 
10 | <script>
11 | export default {
12 |   name: 'MmToast',
13 |   data() {
14 |     return {
15 |       position: 'center', // 默认显示位置
16 |       message: '', // 默认显示文本
17 |       duration: 1500, // 显示时间, 毫秒
18 |       visible: false, // 是否显示
19 |     }
20 |   },
21 |   computed: {
22 |     positionClasss() {
23 |       return 'mm-toast-' + this.position
24 |     },
25 |   },
26 | }
27 | </script>
28 | 
29 | <style lang="less">
30 | @prefix-cls: mm-toast;
31 | 
32 | .@{prefix-cls} {
33 |   position: fixed;
34 |   left: 50%;
35 |   z-index: 1996;
36 |   max-width: 80%;
37 |   box-sizing: border-box;
38 |   border-radius: @border_radius;
39 |   padding: 10px 20px;
40 |   overflow: hidden;
41 |   text-align: center;
42 |   min-height: 40px;
43 |   line-height: 20px;
44 |   font-size: 14px;
45 |   color: #fff;
46 |   background: rgba(0, 0, 0, 0.5);
47 |   user-select: none;
48 |   transform: translateX(-50%);
49 |   &&-top {
50 |     top: 10%;
51 |   }
52 |   &&-center {
53 |     top: 50%;
54 |     margin-top: -20px;
55 |   }
56 |   &&-bottom {
57 |     bottom: 10%;
58 |   }
59 | }
60 | 
61 | .toast-fade-enter {
62 |   opacity: 0;
63 |   transform: translate3d(-50%, -10px, 0);
64 | }
65 | 
66 | .toast-fade-enter-active {
67 |   will-change: transform;
68 |   transition: all 0.2s;
69 | }
70 | 
71 | .toast-fade-enter-to {
72 |   opacity: 1;
73 |   transform: translate3d(-50%, 0, 0);
74 | }
75 | </style>
76 | 


--------------------------------------------------------------------------------
/src/components/lyric/lyric.vue:
--------------------------------------------------------------------------------
  1 | <template>
  2 |   <div>
  3 |     <!--封面-->
  4 |     <dl class="music-info">
  5 |       <dt>
  6 |         <img :src="musicPicUrl" />
  7 |       </dt>
  8 |       <template v-if="currentMusic.id">
  9 |         <dd>歌曲名:{{ currentMusic.name }}</dd>
 10 |         <dd>歌手名:{{ currentMusic.singer }}</dd>
 11 |         <dd>专辑名:{{ currentMusic.album }}</dd>
 12 |       </template>
 13 |       <template v-else>
 14 |         <dd>mmPlayer在线音乐播放器</dd>
 15 |         <dd>
 16 |           <a class="hover" target="_blank" href="https://github.com/maomao1996">
 17 |             <mm-icon type="github" :size="14" />
 18 |             &nbsp;茂茂
 19 |           </a>
 20 |         </dd>
 21 |       </template>
 22 |     </dl>
 23 |     <!--歌词-->
 24 |     <div ref="musicLyric" class="music-lyric">
 25 |       <div class="music-lyric-items" :style="lyricTop">
 26 |         <p v-if="!currentMusic.id">还没有播放音乐哦!</p>
 27 |         <p v-else-if="nolyric">暂无歌词!</p>
 28 |         <template v-else-if="lyric.length > 0">
 29 |           <p v-for="(item, index) in lyric" :key="index" :class="{ on: lyricIndex === index }">
 30 |             {{ item.text }}
 31 |           </p>
 32 |         </template>
 33 |         <p v-else>歌词加载失败!</p>
 34 |       </div>
 35 |     </div>
 36 |   </div>
 37 | </template>
 38 | 
 39 | <script>
 40 | import { mapGetters } from 'vuex'
 41 | 
 42 | export default {
 43 |   name: 'Lyric',
 44 |   props: {
 45 |     // 歌词数据
 46 |     lyric: {
 47 |       type: Array,
 48 |       default: () => [],
 49 |     },
 50 |     // 是否无歌词
 51 |     nolyric: {
 52 |       type: Boolean,
 53 |       default: false,
 54 |     },
 55 |     // 当前歌词下标
 56 |     lyricIndex: {
 57 |       type: Number,
 58 |       default: 0,
 59 |     },
 60 |   },
 61 |   data() {
 62 |     return {
 63 |       top: 0, // 歌词居中
 64 |     }
 65 |   },
 66 |   computed: {
 67 |     musicPicUrl() {
 68 |       return this.currentMusic.id
 69 |         ? `${this.currentMusic.image}?param=300y300`
 70 |         : require('../../assets/img/player_cover.png')
 71 |     },
 72 |     lyricTop() {
 73 |       return `transform :translate3d(0, ${-34 * (this.lyricIndex - this.top)}px, 0)`
 74 |     },
 75 |     ...mapGetters(['currentMusic']),
 76 |   },
 77 |   mounted() {
 78 |     window.addEventListener('resize', () => {
 79 |       clearTimeout(this.resizeTimer)
 80 |       this.resizeTimer = setTimeout(() => this.clacTop(), 60)
 81 |     })
 82 |     this.$nextTick(() => this.clacTop())
 83 |   },
 84 |   methods: {
 85 |     // 计算歌词居中的 top值
 86 |     clacTop() {
 87 |       const dom = this.$refs.musicLyric
 88 |       const { display = '' } = window.getComputedStyle(dom)
 89 |       if (display === 'none') {
 90 |         return
 91 |       }
 92 |       const height = dom.offsetHeight
 93 |       this.top = Math.floor(height / 34 / 2)
 94 |     },
 95 |   },
 96 | }
 97 | </script>
 98 | 
 99 | <style lang="less" scoped>
100 | .music-info {
101 |   padding-bottom: 20px;
102 |   text-align: center;
103 |   font-size: @font_size_medium;
104 |   dt {
105 |     position: relative;
106 |     width: 186px;
107 |     height: 186px;
108 |     margin: 0 auto 15px;
109 |     &:after {
110 |       content: '';
111 |       position: absolute;
112 |       left: 9px;
113 |       top: 0;
114 |       width: 201px;
115 |       height: 180px;
116 |       background: url('~assets/img/album_cover_player.png') 0 0 no-repeat;
117 |     }
118 |     img {
119 |       vertical-align: middle;
120 |       width: 186px;
121 |       height: 186px;
122 |     }
123 |   }
124 |   dd {
125 |     height: 30px;
126 |     line-height: 30px;
127 |     .no-wrap();
128 |   }
129 | }
130 | 
131 | /*歌词部分*/
132 | .music-lyric {
133 |   position: absolute;
134 |   top: 315px;
135 |   right: 0;
136 |   bottom: 0;
137 |   left: 0;
138 |   overflow: hidden;
139 |   text-align: center;
140 |   mask-image: linear-gradient(
141 |     to bottom,
142 |     rgba(255, 255, 255, 0) 0,
143 |     rgba(255, 255, 255, 0.6) 15%,
144 |     rgba(255, 255, 255, 1) 25%,
145 |     rgba(255, 255, 255, 1) 75%,
146 |     rgba(255, 255, 255, 0.6) 85%,
147 |     rgba(255, 255, 255, 0) 100%
148 |   );
149 |   .music-lyric-items {
150 |     text-align: center;
151 |     line-height: 34px;
152 |     font-size: @font_size_small;
153 |     transform: translate3d(0, 0, 0);
154 |     transition: transform 0.6s ease-out;
155 |     .no-wrap();
156 |     .on {
157 |       color: @lyric_color_active;
158 |     }
159 |   }
160 | }
161 | 
162 | // 当屏幕小于 960 时
163 | @media (max-width: 960px) {
164 |   .music-info {
165 |     display: none;
166 |   }
167 |   .music-lyric {
168 |     top: 0;
169 |   }
170 | }
171 | </style>
172 | 


--------------------------------------------------------------------------------
/src/components/mm-header/mm-header.vue:
--------------------------------------------------------------------------------
  1 | <template>
  2 |   <!--头部-->
  3 |   <header class="mm-header">
  4 |     <h1 class="header">
  5 |       <a href="https://github.com/maomao1996/Vue-mmPlayer" target="_blank">
  6 |         mmPlayer 在线音乐播放器
  7 |       </a>
  8 |       <img
  9 |         v-if="visitorBadge"
 10 |         :src="visitorBadge"
 11 |         alt="累计访问数"
 12 |         class="visitor"
 13 |         onerror="this.style.display='none'"
 14 |       />
 15 |     </h1>
 16 |     <dl class="user">
 17 |       <template v-if="user.userId">
 18 |         <router-link class="user-info" to="/music/userlist" tag="dt">
 19 |           <img class="avatar" :src="`${user.avatarUrl}?param=50y50`" />
 20 |           <span>{{ user.nickname }}</span>
 21 |         </router-link>
 22 |         <dd class="user-btn" @click="openDialog(2)">退出</dd>
 23 |       </template>
 24 |       <dd v-else class="user-btn" @click="openDialog(0)">登录</dd>
 25 |     </dl>
 26 |     <!--登录-->
 27 |     <mm-dialog
 28 |       ref="loginDialog"
 29 |       head-text="登录"
 30 |       confirm-btn-text="登录"
 31 |       cancel-btn-text="关闭"
 32 |       @confirm="login"
 33 |     >
 34 |       <div class="mm-dialog-text">
 35 |         <input
 36 |           v-model.trim="uidValue"
 37 |           class="mm-dialog-input"
 38 |           type="number"
 39 |           autofocus
 40 |           placeholder="请输入您的网易云 UID"
 41 |           @keyup.enter="login"
 42 |         />
 43 |       </div>
 44 |       <div slot="btn" @click="openDialog(1)">帮助</div>
 45 |     </mm-dialog>
 46 |     <!--帮助-->
 47 |     <mm-dialog
 48 |       ref="helpDialog"
 49 |       head-text="登录帮助"
 50 |       confirm-btn-text="去登录"
 51 |       cancel-btn-text="关闭"
 52 |       @confirm="openDialog(0)"
 53 |     >
 54 |       <div class="mm-dialog-text">
 55 |         <p>
 56 |           1、
 57 |           <a target="_blank" href="https://music.163.com">点我(https://music.163.com)</a>
 58 |           打开网易云音乐官网
 59 |         </p>
 60 |         <p>2、点击页面右上角的“登录”</p>
 61 |         <p>3、点击您的头像,进入我的主页</p>
 62 |         <p>4、复制浏览器地址栏 /user/home?id= 后面的数字(网易云 UID)</p>
 63 |       </div>
 64 |     </mm-dialog>
 65 |     <!--退出-->
 66 |     <mm-dialog ref="outDialog" body-text="确定退出当前用户吗?" @confirm="out" />
 67 |   </header>
 68 | </template>
 69 | 
 70 | <script>
 71 | import { getUserPlaylist } from 'api'
 72 | import { mapGetters, mapActions } from 'vuex'
 73 | import MmDialog from 'base/mm-dialog/mm-dialog'
 74 | import { toHttps } from '@/utils/util'
 75 | import { VISITOR_BADGE_ID } from '@/config'
 76 | 
 77 | export default {
 78 |   name: 'MmHeader',
 79 |   components: {
 80 |     MmDialog,
 81 |   },
 82 |   data() {
 83 |     return {
 84 |       user: {}, // 用户数据
 85 |       uidValue: '', // 记录用户 UID
 86 |     }
 87 |   },
 88 |   computed: {
 89 |     visitorBadge() {
 90 |       if (VISITOR_BADGE_ID) {
 91 |         return `https://visitor-badge.laobi.icu/badge?left_color=transparent&right_color=transparent&page_id=${VISITOR_BADGE_ID}`
 92 |       }
 93 |       return ''
 94 |     },
 95 |     ...mapGetters(['uid']),
 96 |   },
 97 |   created() {
 98 |     this.uid && this._getUserPlaylist(this.uid)
 99 |   },
100 |   methods: {
101 |     // 打开对话框
102 |     openDialog(key) {
103 |       switch (key) {
104 |         case 0:
105 |           this.$refs.loginDialog.show()
106 |           break
107 |         case 1:
108 |           this.$refs.loginDialog.hide()
109 |           this.$refs.helpDialog.show()
110 |           break
111 |         case 2:
112 |           this.$refs.outDialog.show()
113 |           break
114 |         case 3:
115 |           this.$refs.loginDialog.hide()
116 |           break
117 |       }
118 |     },
119 |     // 退出登录
120 |     out() {
121 |       this.user = {}
122 |       this.setUid(null)
123 |       this.$mmToast('退出成功!')
124 |     },
125 |     // 登录
126 |     login() {
127 |       if (this.uidValue === '') {
128 |         this.$mmToast('UID 不能为空')
129 |         this.openDialog(0)
130 |         return
131 |       }
132 |       this.openDialog(3)
133 |       this._getUserPlaylist(this.uidValue)
134 |     },
135 |     // 获取用户数据
136 |     _getUserPlaylist(uid) {
137 |       getUserPlaylist(uid).then(({ playlist = [] }) => {
138 |         this.uidValue = ''
139 |         if (playlist.length === 0 || !playlist[0].creator) {
140 |           this.$mmToast(`未查询找 UID 为 ${uid} 的用户信息`)
141 |           return
142 |         }
143 |         const creator = playlist[0].creator
144 |         this.setUid(uid)
145 |         creator.avatarUrl = toHttps(creator.avatarUrl)
146 |         this.user = creator
147 |         setTimeout(() => {
148 |           this.$mmToast(`${this.user.nickname} 欢迎使用 mmPlayer`)
149 |         }, 200)
150 |       })
151 |     },
152 |     ...mapActions(['setUid']),
153 |   },
154 | }
155 | </script>
156 | 
157 | <style lang="less">
158 | .mm-header {
159 |   position: absolute;
160 |   top: 0;
161 |   left: 0;
162 |   width: 100%;
163 |   height: 60px;
164 |   @media (max-width: 768px) {
165 |     background: @header_bg_color;
166 |   }
167 |   .header {
168 |     .flex-center;
169 |     line-height: 60px;
170 |     color: @text_color_active;
171 |     font-size: @font_size_large;
172 |     @media (max-width: 768px) {
173 |       padding-left: 15px;
174 |       justify-content: flex-start;
175 |     }
176 |     @media (max-width: 414px) {
177 |       font-size: @font_size_medium;
178 |     }
179 |     .visitor {
180 |       margin-left: 6px;
181 |       height: 20px;
182 |       @media (max-width: 414px) {
183 |         display: none;
184 |       }
185 |     }
186 |   }
187 |   .user {
188 |     position: absolute;
189 |     top: 50%;
190 |     right: 15px;
191 |     line-height: 30px;
192 |     text-align: right;
193 |     transform: translateY(-50%);
194 |     &-info {
195 |       float: left;
196 |       margin-right: 15px;
197 |       cursor: pointer;
198 |       .avatar {
199 |         width: 30px;
200 |         height: 30px;
201 |         border-radius: 50%;
202 |         vertical-align: middle;
203 |       }
204 |       span {
205 |         margin-left: 10px;
206 |         color: @text_color_active;
207 |       }
208 |     }
209 |     &-btn {
210 |       float: left;
211 |       cursor: pointer;
212 |       &:hover {
213 |         color: @text_color_active;
214 |       }
215 |     }
216 |     @media (max-width: 768px) {
217 |       &-info {
218 |         margin-right: 10px;
219 |         span {
220 |           display: none;
221 |         }
222 |       }
223 |     }
224 |   }
225 | }
226 | .mm-dialog-text {
227 |   text-align: left;
228 |   .mm-dialog-input {
229 |     width: 100%;
230 |     height: 40px;
231 |     box-sizing: border-box;
232 |     padding: 0 15px;
233 |     border: 1px solid @btn_color;
234 |     outline: 0;
235 |     background: transparent;
236 |     color: @text_color_active;
237 |     font-size: @font_size_medium;
238 |     box-shadow: 0 0 1px 0 #fff inset;
239 |     &::placeholder {
240 |       color: @text_color;
241 |     }
242 |   }
243 |   a:hover {
244 |     color: #d43c33;
245 |   }
246 | }
247 | </style>
248 | 


--------------------------------------------------------------------------------
/src/components/music-btn/music-btn.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <!--选项-->
 3 |   <div class="music-btn">
 4 |     <router-link to="/music/playlist" tag="span">正在播放</router-link>
 5 |     <router-link to="/music/toplist" tag="span">推荐</router-link>
 6 |     <router-link to="/music/search" tag="span">搜索</router-link>
 7 |     <router-link to="/music/userlist" tag="span">我的歌单</router-link>
 8 |     <span class="show-960" @click="$emit('onClickLyric')">歌词</span>
 9 |     <router-link to="/music/historylist" tag="span">我听过的</router-link>
10 |   </div>
11 | </template>
12 | 
13 | <script>
14 | export default {}
15 | </script>
16 | 
17 | <style lang="less" scoped>
18 | .music-btn {
19 |   width: 100%;
20 |   height: 60px;
21 |   font-size: 0;
22 |   white-space: nowrap;
23 |   overflow-x: auto;
24 |   -webkit-overflow-scrolling: touch;
25 |   span {
26 |     display: inline-block;
27 |     height: 40px;
28 |     box-sizing: border-box;
29 |     margin-right: 8px;
30 |     padding: 0 23px;
31 |     border: 1px solid @btn_color;
32 |     color: @btn_color;
33 |     border-radius: @btn_border_radius;
34 |     font-size: 14px;
35 |     line-height: 40px;
36 |     overflow: hidden;
37 |     cursor: pointer;
38 |     &:nth-last-of-type(1) {
39 |       margin: 0;
40 |     }
41 |     &:hover,
42 |     &.active {
43 |       border-color: @btn_color_active;
44 |       color: @btn_color_active;
45 |     }
46 |   }
47 |   @media (min-width: 960px) {
48 |     span.show-960 {
49 |       display: none;
50 |     }
51 |   }
52 |   @media (max-width: 960px) {
53 |     span.show-960 {
54 |       display: inline-block;
55 |     }
56 |   }
57 |   @media (max-width: 768px) {
58 |     height: 50px;
59 |     span {
60 |       height: 35px;
61 |       padding: 0 10px;
62 |       margin-right: 6px;
63 |       line-height: 35px;
64 |     }
65 |   }
66 | }
67 | </style>
68 | 


--------------------------------------------------------------------------------
/src/components/music-list/music-list.vue:
--------------------------------------------------------------------------------
  1 | <template>
  2 |   <!--歌曲列表-->
  3 |   <div class="music-list flex-col">
  4 |     <template v-if="list.length > 0">
  5 |       <div class="list-item list-header">
  6 |         <span class="list-name">歌曲</span>
  7 |         <span class="list-artist">歌手</span>
  8 |         <span v-if="isDuration" class="list-time">时长</span>
  9 |         <span v-else class="list-album">专辑</span>
 10 |       </div>
 11 |       <div ref="listContent" class="list-content" @scroll="listScroll($event)">
 12 |         <div
 13 |           v-for="(item, index) in list"
 14 |           :key="item.id"
 15 |           class="list-item"
 16 |           :class="{ on: playing && currentMusic.id === item.id }"
 17 |           @dblclick="selectItem(item, index, $event)"
 18 |         >
 19 |           <span class="list-num" v-text="index + 1"></span>
 20 |           <div class="list-name">
 21 |             <span>{{ item.name }}</span>
 22 |             <div class="list-menu">
 23 |               <mm-icon
 24 |                 class="hover"
 25 |                 :type="getPlayIconType(item)"
 26 |                 :size="40"
 27 |                 @click.stop="selectItem(item, index)"
 28 |               />
 29 |             </div>
 30 |           </div>
 31 |           <span class="list-artist">{{ item.singer }}</span>
 32 |           <span v-if="isDuration" class="list-time">
 33 |             {{ item.duration % 3600 | format }}
 34 |             <mm-icon
 35 |               class="hover list-menu-icon-del"
 36 |               type="delete-mini"
 37 |               :size="40"
 38 |               @click.stop="deleteItem(index)"
 39 |             />
 40 |           </span>
 41 |           <span v-else class="list-album">{{ item.album }}</span>
 42 |         </div>
 43 |         <slot name="listBtn"></slot>
 44 |       </div>
 45 |     </template>
 46 |     <mm-no-result v-else title="弄啥呢,怎么啥也没有!!!" />
 47 |   </div>
 48 | </template>
 49 | 
 50 | <script>
 51 | // import {getCheckMusic} from 'api'
 52 | import { mapGetters, mapMutations } from 'vuex'
 53 | import { format } from '@/utils/util'
 54 | import MmNoResult from 'base/mm-no-result/mm-no-result'
 55 | 
 56 | const LIST_TYPE_ALBUM = 'album'
 57 | const LIST_TYPE_DURATION = 'duration'
 58 | const LIST_TYPE_PULLUP = 'pullup'
 59 | 
 60 | // 触发滚动加载的阈值
 61 | const THRESHOLD = 100
 62 | 
 63 | export default {
 64 |   name: 'MusicList',
 65 |   components: {
 66 |     MmNoResult,
 67 |   },
 68 |   filters: {
 69 |     format,
 70 |   },
 71 |   props: {
 72 |     // 歌曲数据
 73 |     list: {
 74 |       type: Array,
 75 |       default: () => [],
 76 |     },
 77 |     /**
 78 |      * 列表类型
 79 |      * album: 显示专辑栏目(默认)
 80 |      * duration: 显示时长栏目
 81 |      * pullup: 开启上拉加载
 82 |      */
 83 |     listType: {
 84 |       type: String,
 85 |       default: LIST_TYPE_ALBUM,
 86 |     },
 87 |   },
 88 |   data() {
 89 |     return {
 90 |       lockUp: true, // 是否锁定滚动加载事件,默认锁定
 91 |     }
 92 |   },
 93 |   computed: {
 94 |     isDuration() {
 95 |       return this.listType === LIST_TYPE_DURATION
 96 |     },
 97 |     ...mapGetters(['playing', 'currentMusic']),
 98 |   },
 99 |   watch: {
100 |     list(newList, oldList) {
101 |       if (this.listType !== LIST_TYPE_PULLUP) {
102 |         return
103 |       }
104 |       if (newList.length !== oldList.length) {
105 |         this.lockUp = false
106 |       } else if (newList[newList.length - 1].id !== oldList[oldList.length - 1].id) {
107 |         this.lockUp = false
108 |       }
109 |     },
110 |   },
111 |   activated() {
112 |     this.scrollTop && this.$refs.listContent && (this.$refs.listContent.scrollTop = this.scrollTop)
113 |   },
114 |   methods: {
115 |     // 滚动事件
116 |     listScroll(e) {
117 |       const scrollTop = e.target.scrollTop
118 |       this.scrollTop = scrollTop
119 |       if (this.listType !== LIST_TYPE_PULLUP || this.lockUp) {
120 |         return
121 |       }
122 |       const { scrollHeight, offsetHeight } = e.target
123 |       if (scrollTop + offsetHeight >= scrollHeight - THRESHOLD) {
124 |         this.lockUp = true // 锁定滚动加载
125 |         this.$emit('pullUp') // 触发滚动加载事件
126 |       }
127 |     },
128 |     // 回到顶部
129 |     scrollTo() {
130 |       this.$refs.listContent.scrollTop = 0
131 |     },
132 |     // 播放暂停事件
133 |     selectItem(item, index, e) {
134 |       if (e && /list-menu-icon-del/.test(e.target.className)) {
135 |         return
136 |       }
137 |       if (this.currentMusic.id && item.id === this.currentMusic.id) {
138 |         this.setPlaying(!this.playing)
139 |         return
140 |       }
141 | 
142 |       /**
143 |        * 为了修复 safari、 ios 微信、安卓 UC 无法播放问题,暂时移除接口校验直接播放
144 |        */
145 |       this.$emit('select', item, index) // 触发点击播放事件
146 | 
147 |       // getMusicUrl(item.id)
148 |       // .then(res => {
149 |       //     if (!res.data.data[0].url) {
150 |       //         this.$mmToast('当前音乐无法播放,请播放其他音乐')
151 |       //     } else {
152 |       //         this.$emit('select', item, index)//触发点击播放事件
153 |       //     }
154 |       // });
155 |       // getCheckMusic(item.id)
156 |       // .then(res => {
157 |       //     if (res.data.message !== 'ok') {
158 |       //         this.$mmToast('当前音乐无法播放,请播放其他音乐')
159 |       //     } else {
160 |       //         this.$emit('select', item, index)//触发点击播放事件
161 |       //     }
162 |       // }).catch(error => {
163 |       //     this.$mmToast(error.response.data.message)
164 |       // })
165 |     },
166 |     // 获取播放状态 type
167 |     getPlayIconType({ id: itemId }) {
168 |       const {
169 |         playing,
170 |         currentMusic: { id },
171 |       } = this
172 |       return playing && id === itemId ? 'pause-mini' : 'play-mini'
173 |     },
174 |     // 删除事件
175 |     deleteItem(index) {
176 |       this.$emit('del', index) // 触发删除事件
177 |     },
178 |     ...mapMutations({
179 |       setPlaying: 'SET_PLAYING',
180 |     }),
181 |   },
182 | }
183 | </script>
184 | 
185 | <style lang="less" scoped>
186 | .music-list {
187 |   height: 100%;
188 | }
189 | 
190 | .list-header {
191 |   border-bottom: 1px solid @list_head_line_color;
192 |   color: @text_color_active;
193 | 
194 |   .list-name {
195 |     padding-left: 40px;
196 |     user-select: none;
197 |   }
198 | }
199 | 
200 | .list-content {
201 |   flex: 1;
202 |   overflow-x: hidden;
203 |   overflow-y: auto;
204 |   -webkit-overflow-scrolling: touch;
205 | }
206 | 
207 | .list-no {
208 |   display: flex;
209 |   justify-content: center;
210 |   align-items: center;
211 |   width: 100%;
212 |   height: 100%;
213 |   color: @text_color;
214 | }
215 | 
216 | .list-item {
217 |   display: flex;
218 |   width: 100%;
219 |   height: 50px;
220 |   border-bottom: 1px solid @list_item_line_color;
221 |   line-height: 50px;
222 |   overflow: hidden;
223 | 
224 |   &.list-item-no {
225 |     justify-content: center;
226 |     align-items: center;
227 |   }
228 | 
229 |   &.on {
230 |     color: #fff;
231 | 
232 |     .list-num {
233 |       font-size: 0;
234 |       background: url('~assets/img/wave.gif') no-repeat center center;
235 |     }
236 |   }
237 | 
238 |   &:hover {
239 |     .list-name {
240 |       padding-right: 80px;
241 | 
242 |       .list-menu {
243 |         display: block;
244 |       }
245 |     }
246 |   }
247 | 
248 |   &:not([class*='list-header']):hover {
249 |     .list-name {
250 |       padding-right: 80px;
251 | 
252 |       .list-menu {
253 |         display: block;
254 |       }
255 |     }
256 | 
257 |     .list-time {
258 |       font-size: 0;
259 | 
260 |       .list-menu-icon-del {
261 |         display: block;
262 |       }
263 |     }
264 |   }
265 | 
266 |   .list-num {
267 |     display: block;
268 |     width: 30px;
269 |     margin-right: 10px;
270 |     text-align: center;
271 |   }
272 | 
273 |   .list-name {
274 |     position: relative;
275 |     flex: 1;
276 |     box-sizing: border-box;
277 | 
278 |     & > span {
279 |       text-overflow: ellipsis;
280 |       overflow: hidden;
281 |       display: -webkit-box;
282 |       -webkit-line-clamp: 1;
283 |       -webkit-box-orient: vertical;
284 |     }
285 | 
286 |     small {
287 |       margin-left: 5px;
288 |       font-size: 12px;
289 |       color: rgba(255, 255, 255, 0.5);
290 |     }
291 | 
292 |     /*hover菜单*/
293 | 
294 |     .list-menu {
295 |       display: none;
296 |       position: absolute;
297 |       top: 50%;
298 |       right: 10px;
299 |       height: 40px;
300 |       font-size: 0;
301 |       transform: translateY(-50%);
302 |     }
303 |   }
304 | 
305 |   .list-artist,
306 |   .list-album {
307 |     display: block;
308 |     width: 300px;
309 |     .no-wrap();
310 |     @media (max-width: 1440px) {
311 |       width: 200px;
312 |     }
313 |     @media (max-width: 1200px) {
314 |       width: 150px;
315 |     }
316 |   }
317 | 
318 |   .list-time {
319 |     display: block;
320 |     width: 60px;
321 |     position: relative;
322 | 
323 |     .list-menu-icon-del {
324 |       display: none;
325 |       position: absolute;
326 |       top: 50%;
327 |       left: 0;
328 |       transform: translateY(-50%);
329 |     }
330 |   }
331 | }
332 | 
333 | .list-btn {
334 |   display: flex;
335 |   justify-content: center;
336 |   align-items: center;
337 |   height: 50px;
338 |   span {
339 |     padding: 5px 20px;
340 |     cursor: pointer;
341 |     user-select: none;
342 |     &:hover {
343 |       color: @text_color_active;
344 |     }
345 |   }
346 | }
347 | 
348 | @media (max-width: 960px) {
349 |   .list-item .list-name {
350 |     padding-right: 70px;
351 |   }
352 | }
353 | 
354 | @media (max-width: 768px) {
355 |   .list-item {
356 |     .list-name .list-menu {
357 |       display: block;
358 |     }
359 | 
360 |     .list-artist,
361 |     .list-album {
362 |       width: 20%;
363 |     }
364 |   }
365 | }
366 | 
367 | @media (max-width: 640px) {
368 |   .list-item {
369 |     .list-artist {
370 |       width: 80px;
371 |     }
372 | 
373 |     .list-album,
374 |     .list-time {
375 |       display: none;
376 |     }
377 |   }
378 | }
379 | </style>
380 | 


--------------------------------------------------------------------------------
/src/components/volume/volume.vue:
--------------------------------------------------------------------------------
 1 | <!-- 音量控制组件 -->
 2 | <template>
 3 |   <div class="volume">
 4 |     <mm-icon
 5 |       class="pointer volume-icon"
 6 |       :type="getVolumeIconType()"
 7 |       :size="30"
 8 |       @click="handleToggleVolume"
 9 |     />
10 |     <div class="volume-progress-wrapper">
11 |       <mm-progress
12 |         :percent="volumeProgress"
13 |         @percentChangeEnd="handleVolumeChange"
14 |         @percentChange="handleVolumeChange"
15 |       />
16 |     </div>
17 |   </div>
18 | </template>
19 | 
20 | <script>
21 | import MmProgress from 'base/mm-progress/mm-progress'
22 | 
23 | export default {
24 |   name: 'Volume',
25 |   components: {
26 |     MmProgress,
27 |   },
28 |   props: {
29 |     volume: {
30 |       type: Number,
31 |       required: true,
32 |     },
33 |   },
34 |   computed: {
35 |     volumeProgress() {
36 |       return this.volume
37 |     },
38 |     isMute: {
39 |       get() {
40 |         return this.volumeProgress === 0
41 |       },
42 |       set(newMute) {
43 |         const volume = newMute ? 0 : this.lastVolume
44 |         if (newMute) {
45 |           this.lastVolume = this.volumeProgress
46 |         }
47 |         this.handleVolumeChange(volume)
48 |       },
49 |     },
50 |   },
51 |   methods: {
52 |     getVolumeIconType() {
53 |       return this.isMute ? 'volume-off' : 'volume'
54 |     },
55 |     // 是否静音
56 |     handleToggleVolume() {
57 |       this.isMute = !this.isMute
58 |     },
59 |     handleVolumeChange(percent) {
60 |       this.$emit('volumeChange', percent)
61 |     },
62 |   },
63 | }
64 | </script>
65 | 
66 | <style lang="less" scoped>
67 | .volume {
68 |   display: flex;
69 |   align-items: center;
70 |   width: 150px;
71 |   &-icon {
72 |     margin-right: 5px;
73 |     color: #fff;
74 |   }
75 |   &-progress-wrapper {
76 |     flex: 1;
77 |   }
78 |   @media (max-width: 768px) {
79 |     top: 2px;
80 |     width: 36px;
81 |   }
82 | }
83 | </style>
84 | 


--------------------------------------------------------------------------------
/src/config.js:
--------------------------------------------------------------------------------
 1 | /* 版本号 */
 2 | export const VERSION = process.env.VUE_APP_VERSION
 3 | 
 4 | /**
 5 |  * 访客统计 id
 6 |  * 具体使用文档 https://github.com/jwenjian/visitor-badge
 7 |  */
 8 | export const VISITOR_BADGE_ID = process.env.VUE_APP_VISITOR_BADGE_ID
 9 | 
10 | /* 背景图(可引入网络图或本地静态图) */
11 | const requireAll = (requireContext) => requireContext.keys().map(requireContext)
12 | const BACKGROUNDS = requireAll(require.context('./assets/background', false))
13 | 
14 | /**
15 |  * 播放模式
16 |  * LIST_LOOP: 列表循环
17 |  * ORDER: 顺序播放
18 |  * RANDOM: 随机播放
19 |  * LOOP: 单曲循环
20 |  */
21 | export const PLAY_MODE = {
22 |   LIST_LOOP: 0,
23 |   ORDER: 1,
24 |   RANDOM: 2,
25 |   LOOP: 3,
26 | }
27 | 
28 | /**
29 |  * 播放器默认配置
30 |  */
31 | export const MMPLAYER_CONFIG = {
32 |   /**
33 |    * 默认歌单ID (正在播放列表)
34 |    * 默认为云音乐热歌榜 https://music.163.com/#/discover/toplist?id=3778678
35 |    */
36 |   PLAYLIST_ID: 3778678,
37 |   /* 默认播放模式 */
38 |   PLAY_MODE: PLAY_MODE.LIST_LOOP,
39 |   /* 默认音量 */
40 |   VOLUME: 0.8,
41 |   /* 默认背景 */
42 |   BACKGROUND: BACKGROUNDS[Math.floor(Math.random() * BACKGROUNDS.length)],
43 | }
44 | 
45 | /* 默认分页数量 */
46 | export const DEFAULT_LIMIT = 30
47 | 


--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
 1 | // The Vue build version to load with the `import` command
 2 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias.
 3 | // import 'babel-polyfill'
 4 | // import '@/utils/hack'
 5 | import Vue from 'vue'
 6 | import store from './store'
 7 | import router from './router'
 8 | import App from './App'
 9 | import fastclick from 'fastclick'
10 | import mmToast from 'base/mm-toast'
11 | import Icon from 'base/mm-icon/mm-icon'
12 | import VueLazyload from 'vue-lazyload'
13 | import { VERSION } from './config'
14 | 
15 | import '@/styles/index.less'
16 | 
17 | // 优化移动端300ms点击延迟
18 | fastclick.attach(document.body)
19 | 
20 | // 弹出层
21 | Vue.use(mmToast)
22 | 
23 | // icon 组件
24 | Vue.component(Icon.name, Icon)
25 | 
26 | // 懒加载
27 | Vue.use(VueLazyload, {
28 |   preLoad: 1,
29 |   loading: require('assets/img/default.png'),
30 | })
31 | 
32 | // 访问版本统计
33 | window._hmt && window._hmt.push(['_setCustomVar', 1, 'version', VERSION, 1])
34 | 
35 | const redirectList = ['/music/details', '/music/comment']
36 | router.beforeEach((to, from, next) => {
37 |   window._hmt && to.path && window._hmt.push(['_trackPageview', '/#' + to.fullPath])
38 |   if (redirectList.includes(to.path)) {
39 |     next()
40 |   } else {
41 |     document.title =
42 |       (to.meta.title && `${to.meta.title} - mmPlayer在线音乐播放器`) || 'mmPlayer在线音乐播放器'
43 |     next()
44 |   }
45 | })
46 | 
47 | // 版权信息
48 | window.mmPlayer = window.mmplayer = `欢迎使用 mmPlayer!
49 | 当前版本为:V${VERSION}
50 | 作者:茂茂
51 | Github:https://github.com/maomao1996/Vue-mmPlayer
52 | 歌曲来源于网易云音乐 (https://music.163.com)`
53 | // eslint-disable-next-line no-console
54 | console.info(`%c${window.mmplayer}`, `color:blue`)
55 | 
56 | // eslint-disable-next-line no-new
57 | new Vue({
58 |   el: '#mmPlayer',
59 |   store,
60 |   router,
61 |   render: (h) => h(App),
62 | })
63 | 


--------------------------------------------------------------------------------
/src/pages/comment/comment.vue:
--------------------------------------------------------------------------------
  1 | <template>
  2 |   <!--评论-->
  3 |   <div class="comment" @scroll="listScroll($event)">
  4 |     <mm-loading v-model="mmLoadShow" />
  5 |     <dl v-if="hotComments.length > 0" class="comment-list">
  6 |       <!--精彩评论-->
  7 |       <dt class="comment-title">精彩评论</dt>
  8 |       <dd v-for="item in hotComments" :key="item.commentId" class="comment-item">
  9 |         <a target="_blank" :href="`https://music.163.com/#/user/home?id=${item.user.userId}`">
 10 |           <img v-lazy="`${item.user.avatarUrl}?param=50y50`" class="comment-item-pic" />
 11 |           <h2 class="comment-item-title">{{ item.user.nickname }}</h2>
 12 |         </a>
 13 |         <p class="comment-item-disc">{{ item.content }}</p>
 14 |         <div class="comment-item-opt">
 15 |           <span class="comment-opt-date">{{ item.time | format }}</span>
 16 |           <span class="comment-opt-liked">
 17 |             <mm-icon type="good" />
 18 |             {{ item.likedCount }}
 19 |           </span>
 20 |         </div>
 21 |       </dd>
 22 |     </dl>
 23 |     <!--最新评论-->
 24 |     <dl v-if="commentList.length > 0" class="comment-list">
 25 |       <dt class="comment-title">最新评论({{ total }})</dt>
 26 |       <dd v-for="item in commentList" :key="item.commentId" class="comment-item">
 27 |         <a
 28 |           class="comment-item-pic"
 29 |           target="_blank"
 30 |           :href="`https://music.163.com/#/user/home?id=${item.user.userId}`"
 31 |         >
 32 |           <img v-lazy="`${item.user.avatarUrl}?param=50y50`" class="cover-img" />
 33 |         </a>
 34 |         <h2 class="comment-item-title">
 35 |           <a target="_blank" :href="`https://music.163.com/#/user/home?id=${item.user.userId}`">
 36 |             {{ item.user.nickname }}
 37 |           </a>
 38 |         </h2>
 39 |         <p class="comment-item-disc">{{ item.content }}</p>
 40 |         <div
 41 |           v-for="beReplied in item.beReplied"
 42 |           :key="beReplied.user.userId"
 43 |           class="comment-item-replied"
 44 |         >
 45 |           <a
 46 |             target="_blank"
 47 |             :href="`https://music.163.com/#/user/home?id=${beReplied.user.userId}`"
 48 |           >
 49 |             {{ beReplied.user.nickname }}
 50 |           </a>
 51 |           :{{ beReplied.content }}
 52 |         </div>
 53 |         <div class="comment-item-opt">
 54 |           <span class="comment-opt-date">{{ item.time | format }}</span>
 55 |           <span v-if="item.likedCount > 0" class="comment-opt-liked">
 56 |             <mm-icon type="good" />
 57 |             {{ item.likedCount }}
 58 |           </span>
 59 |         </div>
 60 |       </dd>
 61 |     </dl>
 62 |   </div>
 63 | </template>
 64 | 
 65 | <script>
 66 | import { getComment } from 'api'
 67 | import { addZero } from '@/utils/util'
 68 | import MmLoading from 'base/mm-loading/mm-loading'
 69 | import { loadMixin } from '@/utils/mixin'
 70 | 
 71 | export default {
 72 |   name: 'Comment',
 73 |   components: {
 74 |     MmLoading,
 75 |   },
 76 |   filters: {
 77 |     // 格式化时间
 78 |     format(time) {
 79 |       let formatTime
 80 |       const date = new Date(time)
 81 |       const dateObj = {
 82 |         year: date.getFullYear(),
 83 |         month: date.getMonth(),
 84 |         date: date.getDate(),
 85 |         hours: date.getHours(),
 86 |         minutes: date.getMinutes(),
 87 |       }
 88 |       const newTime = new Date()
 89 |       const diff = newTime.getTime() - time
 90 |       if (newTime.getDate() === dateObj.date && diff < 60000) {
 91 |         formatTime = '刚刚'
 92 |       } else if (newTime.getDate() === dateObj.date && diff > 60000 && diff < 3600000) {
 93 |         formatTime = `${Math.floor(diff / 60000)}分钟前`
 94 |       } else if (newTime.getDate() === dateObj.date && diff > 3600000 && diff < 86400000) {
 95 |         formatTime = `${addZero(dateObj.hours)}:${addZero(dateObj.minutes)}`
 96 |       } else if (newTime.getDate() !== dateObj.date && diff < 86400000) {
 97 |         formatTime = `昨天${addZero(dateObj.hours)}:${addZero(dateObj.minutes)}`
 98 |       } else if (newTime.getFullYear() === dateObj.year) {
 99 |         formatTime = `${dateObj.month + 1}月${dateObj.date}日`
100 |       } else {
101 |         formatTime = `${dateObj.year}年${dateObj.month + 1}月${dateObj.date}日`
102 |       }
103 |       return formatTime
104 |     },
105 |   },
106 |   mixins: [loadMixin],
107 |   data() {
108 |     return {
109 |       lockUp: true, // 是否锁定滚动加载事件,默认锁定
110 |       page: 0, // 分页
111 |       hotComments: [], // 精彩评论
112 |       commentList: [], // 最新评论
113 |       total: null, // 评论总数
114 |     }
115 |   },
116 |   watch: {
117 |     commentList(newList, oldList) {
118 |       if (newList.length !== oldList.length) {
119 |         this.lockUp = false
120 |       }
121 |     },
122 |   },
123 |   created() {
124 |     this.initData()
125 |   },
126 |   methods: {
127 |     // 初始化数据
128 |     initData() {
129 |       getComment(this.$route.params.id, this.page).then((res) => {
130 |         this.hotComments = res.hotComments
131 |         this.commentList = res.comments
132 |         this.total = res.total
133 |         this.lockUp = true
134 |         this._hideLoad()
135 |       })
136 |     },
137 |     // 列表滚动事件
138 |     listScroll(e) {
139 |       if (this.lockUp) {
140 |         return
141 |       }
142 |       const { scrollTop, scrollHeight, offsetHeight } = e.target
143 |       if (scrollTop + offsetHeight >= scrollHeight - 100) {
144 |         this.lockUp = true // 锁定滚动加载
145 |         this.page += 1
146 |         this.pullUp() // 触发滚动加载事件
147 |       }
148 |     },
149 |     // 滚动加载事件
150 |     pullUp() {
151 |       getComment(this.$route.params.id, this.page).then(({ comments }) => {
152 |         this.commentList = [...this.commentList, ...comments]
153 |       })
154 |     },
155 |   },
156 | }
157 | </script>
158 | 
159 | <style lang="less" scoped>
160 | .comment {
161 |   .comment-list {
162 |     padding: 0 10px;
163 |   }
164 | 
165 |   .comment-title {
166 |     position: sticky;
167 |     top: 0;
168 |     z-index: 1;
169 |     margin: 0 -10px;
170 |     padding: 10px;
171 |     height: 34px;
172 |     line-height: 34px;
173 |     color: @text_color_active;
174 |     background: @header_bg_color;
175 |     backdrop-filter: @backdrop_filter;
176 |   }
177 |   .comment-item {
178 |     position: relative;
179 |     padding: 15px 0 15px 55px;
180 |     & + .comment-item {
181 |       border-top: 1px solid @comment_item_line_color;
182 |     }
183 |     &-pic {
184 |       display: block;
185 |       position: absolute;
186 |       left: 0;
187 |       top: 20px;
188 |       width: 38px;
189 |       height: 38px;
190 |       border-radius: 50%;
191 |       overflow: hidden;
192 |     }
193 |     &-title {
194 |       height: 20px;
195 |       margin-bottom: 6px;
196 |       font-weight: 400;
197 |       .no-wrap();
198 |       color: @text_color_active;
199 |     }
200 |     &-disc {
201 |       overflow: hidden;
202 |       word-break: break-all;
203 |       word-wrap: break-word;
204 |       line-height: 25px;
205 |       text-align: justify;
206 |       color: @text_color;
207 |       img {
208 |         position: relative;
209 |         vertical-align: middle;
210 |         top: -2px;
211 |       }
212 |     }
213 |     &-replied {
214 |       padding: 8px 19px;
215 |       margin-top: 10px;
216 |       line-height: 20px;
217 |       border: 1px solid @comment_replied_line_color;
218 |       a {
219 |         color: @text_color_active;
220 |       }
221 |     }
222 |     &-opt {
223 |       margin-top: 10px;
224 |       line-height: 25px;
225 |       text-align: right;
226 |       overflow: hidden;
227 |       .comment-opt-date {
228 |         float: left;
229 |         line-height: 28px;
230 |       }
231 |       .comment-opt-liked {
232 |         display: inline-block;
233 |         height: 20px;
234 |         line-height: 20px;
235 |       }
236 |     }
237 |   }
238 | }
239 | </style>
240 | 


--------------------------------------------------------------------------------
/src/pages/details/details.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <!--歌单详情-->
 3 |   <div class="details">
 4 |     <mm-loading v-model="mmLoadShow" />
 5 |     <music-list :list="list" @select="selectItem" />
 6 |   </div>
 7 | </template>
 8 | 
 9 | <script>
10 | import { mapActions } from 'vuex'
11 | import { getPlaylistDetail } from 'api'
12 | import MmLoading from 'base/mm-loading/mm-loading'
13 | import MusicList from 'components/music-list/music-list'
14 | import { loadMixin } from '@/utils/mixin'
15 | 
16 | export default {
17 |   name: 'Detail',
18 |   components: {
19 |     MmLoading,
20 |     MusicList,
21 |   },
22 |   mixins: [loadMixin],
23 |   data() {
24 |     return {
25 |       list: [], // 列表
26 |     }
27 |   },
28 |   created() {
29 |     // 获取歌单详情
30 |     getPlaylistDetail(this.$route.params.id)
31 |       .then((playlist) => {
32 |         document.title = `${playlist.name} - mmPlayer在线音乐播放器`
33 |         this.list = playlist.tracks
34 |         this._hideLoad()
35 |       })
36 |       .catch(() => {
37 |         this._hideLoad()
38 |       })
39 |   },
40 |   methods: {
41 |     // 播放暂停事件
42 |     selectItem(item, index) {
43 |       this.selectPlay({
44 |         list: this.list,
45 |         index,
46 |       })
47 |     },
48 |     ...mapActions(['selectPlay']),
49 |   },
50 | }
51 | </script>
52 | 
53 | <style lang="less" scoped>
54 | .details {
55 |   .music-list {
56 |     height: 100%;
57 |   }
58 | }
59 | </style>
60 | 


--------------------------------------------------------------------------------
/src/pages/historyList/historyList.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <!--我听过的(播放历史)-->
 3 |   <div class="historyList">
 4 |     <music-list :list="historyList" list-type="duration" @select="selectItem" @del="deleteItem">
 5 |       <div slot="listBtn" class="list-btn">
 6 |         <span @click="$refs.dialog.show()">清空列表</span>
 7 |       </div>
 8 |     </music-list>
 9 |     <mm-dialog
10 |       ref="dialog"
11 |       body-text="是否清空播放历史列表"
12 |       confirm-btn-text="清空"
13 |       @confirm="clearList"
14 |     />
15 |   </div>
16 | </template>
17 | 
18 | <script>
19 | import { mapGetters, mapMutations, mapActions } from 'vuex'
20 | import MusicList from 'components/music-list/music-list'
21 | import MmDialog from 'base/mm-dialog/mm-dialog'
22 | 
23 | export default {
24 |   name: 'HistoryList',
25 |   components: {
26 |     MusicList,
27 |     MmDialog,
28 |   },
29 |   computed: {
30 |     ...mapGetters(['historyList', 'playing', 'currentMusic']),
31 |   },
32 |   methods: {
33 |     // 清空列表事件
34 |     clearList() {
35 |       this.clearHistory()
36 |       this.$mmToast('列表清空成功')
37 |     },
38 |     // 播放事件
39 |     selectItem(item, index) {
40 |       this.selectPlay({
41 |         list: this.historyList,
42 |         index,
43 |       })
44 |     },
45 |     // 删除事件
46 |     deleteItem(index) {
47 |       let list = [...this.historyList]
48 |       list.splice(index, 1)
49 |       this.removeHistory(list)
50 |       this.$mmToast('删除成功')
51 |     },
52 |     ...mapMutations({
53 |       setPlaying: 'SET_PLAYING',
54 |     }),
55 |     ...mapActions(['selectPlay', 'clearHistory', 'removeHistory']),
56 |   },
57 | }
58 | </script>
59 | 


--------------------------------------------------------------------------------
/src/pages/mmPlayer.js:
--------------------------------------------------------------------------------
 1 | import { PLAY_MODE } from '@/config'
 2 | 
 3 | // 重试次数
 4 | let retry = 1
 5 | 
 6 | const mmPlayerMusic = {
 7 |   initAudio(that) {
 8 |     const ele = that.audioEle
 9 |     // 音频缓冲事件
10 |     ele.onprogress = () => {
11 |       try {
12 |         if (ele.buffered.length > 0) {
13 |           const duration = that.currentMusic.duration
14 |           let buffered = 0
15 |           ele.buffered.end(0)
16 |           buffered = ele.buffered.end(0) > duration ? duration : ele.buffered.end(0)
17 |           that.currentProgress = buffered / duration
18 |         }
19 |       } catch (e) {}
20 |     }
21 |     // 开始播放音乐
22 |     ele.onplay = () => {
23 |       let timer
24 |       clearTimeout(timer)
25 |       timer = setTimeout(() => {
26 |         that.musicReady = true
27 |       }, 100)
28 |     }
29 |     // 获取当前播放时间
30 |     ele.ontimeupdate = () => {
31 |       that.currentTime = ele.currentTime
32 |     }
33 |     // 当前音乐播放完毕
34 |     ele.onended = () => {
35 |       if (that.mode === PLAY_MODE.LOOP) {
36 |         that.loop()
37 |       } else {
38 |         that.next()
39 |       }
40 |     }
41 |     // 音乐播放出错
42 |     ele.onerror = () => {
43 |       if (retry === 0) {
44 |         let toastText = '当前音乐不可播放,已自动播放下一曲'
45 |         if (that.playlist.length === 1) {
46 |           toastText = '没有可播放的音乐哦~'
47 |         }
48 |         that.$mmToast(toastText)
49 |         that.next(true)
50 |       } else {
51 |         // eslint-disable-next-line no-console
52 |         console.log('重试一次')
53 |         retry -= 1
54 |         ele.url = that.currentMusic.url
55 |         ele.load()
56 |       }
57 |       // console.log('播放出错啦!')
58 |     }
59 |     // 音乐进度拖动大于加载时重载音乐
60 |     ele.onstalled = () => {
61 |       ele.load()
62 |       that.setPlaying(false)
63 |       let timer
64 |       clearTimeout(timer)
65 |       timer = setTimeout(() => {
66 |         that.setPlaying(true)
67 |       }, 10)
68 |     }
69 |     // 将能播放的音乐加入播放历史
70 |     ele.oncanplay = () => {
71 |       retry = 1
72 |       if (that.historyList.length === 0 || that.currentMusic.id !== that.historyList[0].id) {
73 |         that.setHistory(that.currentMusic)
74 |       }
75 |     }
76 |     // 音频数据不可用时
77 |     ele.onstalled = () => {
78 |       ele.load()
79 |       that.setPlaying(false)
80 |       let timer
81 |       clearTimeout(timer)
82 |       timer = setTimeout(() => {
83 |         that.setPlaying(true)
84 |       }, 10)
85 |     }
86 |     // 当音频已暂停时
87 |     ele.onpause = () => {
88 |       that.setPlaying(false)
89 |     }
90 |   },
91 | }
92 | 
93 | export default mmPlayerMusic
94 | 


--------------------------------------------------------------------------------
/src/pages/music.vue:
--------------------------------------------------------------------------------
  1 | <template>
  2 |   <div class="music flex-col">
  3 |     <div class="music-content">
  4 |       <div class="music-left flex-col">
  5 |         <music-btn @onClickLyric="handleOpenLyric" />
  6 |         <keep-alive>
  7 |           <router-view v-if="$route.meta.keepAlive" class="router-view" />
  8 |         </keep-alive>
  9 |         <router-view v-if="!$route.meta.keepAlive" :key="$route.path" class="router-view" />
 10 |       </div>
 11 |       <div class="music-right" :class="{ show: lyricVisible }">
 12 |         <div class="close-lyric" @click="handleCloseLyric">关闭歌词</div>
 13 |         <lyric ref="lyric" :lyric="lyric" :nolyric="nolyric" :lyric-index="lyricIndex" />
 14 |       </div>
 15 |     </div>
 16 | 
 17 |     <!--播放器-->
 18 |     <div class="music-bar" :class="{ disable: !musicReady || !currentMusic.id }">
 19 |       <div class="music-bar-btns">
 20 |         <mm-icon class="pointer" type="prev" :size="36" title="上一曲 Ctrl + Left" @click="prev" />
 21 |         <div class="control-play pointer" title="播放暂停 Ctrl + Space" @click="play">
 22 |           <mm-icon :type="playing ? 'pause' : 'play'" :size="24" />
 23 |         </div>
 24 |         <mm-icon class="pointer" type="next" :size="36" title="下一曲 Ctrl + Right" @click="next" />
 25 |       </div>
 26 |       <div class="music-music">
 27 |         <div class="music-bar-info">
 28 |           <template v-if="currentMusic && currentMusic.id">
 29 |             {{ currentMusic.name }}
 30 |             <span>- {{ currentMusic.singer }}</span>
 31 |           </template>
 32 |           <template v-else>欢迎使用mmPlayer在线音乐播放器</template>
 33 |         </div>
 34 |         <div v-if="currentMusic.id" class="music-bar-time">
 35 |           {{ currentTime | format }}/{{ currentMusic.duration % 3600 | format }}
 36 |         </div>
 37 |         <mm-progress
 38 |           class="music-progress"
 39 |           :percent="percentMusic"
 40 |           :percent-progress="currentProgress"
 41 |           @percentChange="progressMusic"
 42 |           @percentChangeEnd="progressMusicEnd"
 43 |         />
 44 |       </div>
 45 | 
 46 |       <!-- 播放模式 -->
 47 |       <mm-icon
 48 |         class="icon-color pointer mode"
 49 |         :type="getModeIconType()"
 50 |         :title="getModeIconTitle()"
 51 |         :size="30"
 52 |         @click="modeChange"
 53 |       />
 54 | 
 55 |       <!-- 评论 -->
 56 |       <mm-icon class="icon-color pointer comment" type="comment" :size="30" @click="openComment" />
 57 | 
 58 |       <!-- 音量控制 -->
 59 |       <div class="music-bar-volume" title="音量加减 [Ctrl + Up / Down]">
 60 |         <volume :volume="volume" @volumeChange="volumeChange" />
 61 |       </div>
 62 |     </div>
 63 | 
 64 |     <!--遮罩-->
 65 |     <div class="mmPlayer-bg" :style="{ backgroundImage: picUrl }"></div>
 66 |     <div class="mmPlayer-mask"></div>
 67 |   </div>
 68 | </template>
 69 | 
 70 | <script>
 71 | import { getLyric } from 'api'
 72 | import mmPlayerMusic from './mmPlayer'
 73 | import { randomSortArray, parseLyric, format, silencePromise } from '@/utils/util'
 74 | import { PLAY_MODE, MMPLAYER_CONFIG } from '@/config'
 75 | import { getVolume, setVolume } from '@/utils/storage'
 76 | import { mapGetters, mapMutations, mapActions } from 'vuex'
 77 | 
 78 | import MmProgress from 'base/mm-progress/mm-progress'
 79 | import MusicBtn from 'components/music-btn/music-btn'
 80 | import Lyric from 'components/lyric/lyric'
 81 | import Volume from 'components/volume/volume'
 82 | 
 83 | export default {
 84 |   name: 'Music',
 85 |   components: {
 86 |     MmProgress,
 87 |     MusicBtn,
 88 |     Lyric,
 89 |     Volume,
 90 |   },
 91 |   filters: {
 92 |     // 时间格式化
 93 |     format,
 94 |   },
 95 |   data() {
 96 |     const volume = getVolume()
 97 |     return {
 98 |       musicReady: false, // 是否可以使用播放器
 99 |       currentTime: 0, // 当前播放时间
100 |       currentProgress: 0, // 当前缓冲进度
101 |       lyricVisible: false, // 移动端歌词显示
102 |       lyric: [], // 歌词
103 |       nolyric: false, // 是否有歌词
104 |       lyricIndex: 0, // 当前播放歌词下标
105 |       isMute: false, // 是否静音
106 |       volume, // 音量大小
107 |     }
108 |   },
109 |   computed: {
110 |     picUrl() {
111 |       return this.currentMusic.id && this.currentMusic.image
112 |         ? `url(${this.currentMusic.image}?param=300y300)`
113 |         : `url(${MMPLAYER_CONFIG.BACKGROUND})`
114 |     },
115 |     percentMusic() {
116 |       const duration = this.currentMusic.duration
117 |       return this.currentTime && duration ? this.currentTime / duration : 0
118 |     },
119 |     ...mapGetters([
120 |       'audioEle',
121 |       'mode',
122 |       'playing',
123 |       'playlist',
124 |       'orderList',
125 |       'currentIndex',
126 |       'currentMusic',
127 |       'historyList',
128 |     ]),
129 |   },
130 |   watch: {
131 |     currentMusic(newMusic, oldMusic) {
132 |       if (!newMusic.id) {
133 |         this.lyric = []
134 |         return
135 |       }
136 |       if (newMusic.id === oldMusic.id) {
137 |         return
138 |       }
139 |       this.audioEle.src = newMusic.url
140 |       // 重置相关参数
141 |       this.lyricIndex = this.currentTime = this.currentProgress = 0
142 |       silencePromise(this.audioEle.play())
143 |       this.$nextTick(() => {
144 |         this._getLyric(newMusic.id)
145 |       })
146 |     },
147 |     playing(newPlaying) {
148 |       const audio = this.audioEle
149 |       this.$nextTick(() => {
150 |         newPlaying ? silencePromise(audio.play()) : audio.pause()
151 |         this.musicReady = true
152 |       })
153 |     },
154 |     currentTime(newTime) {
155 |       if (this.nolyric) {
156 |         return
157 |       }
158 |       let lyricIndex = 0
159 |       for (let i = 0; i < this.lyric.length; i++) {
160 |         if (newTime > this.lyric[i].time) {
161 |           lyricIndex = i
162 |         }
163 |       }
164 |       this.lyricIndex = lyricIndex
165 |     },
166 |     $route() {
167 |       this.lyricVisible = false
168 |     },
169 |   },
170 |   mounted() {
171 |     this.$nextTick(() => {
172 |       mmPlayerMusic.initAudio(this)
173 |       this.initKeyDown()
174 |       this.volumeChange(this.volume)
175 |     })
176 |   },
177 |   methods: {
178 |     // 按键事件
179 |     initKeyDown() {
180 |       document.onkeydown = (e) => {
181 |         switch (e.ctrlKey && e.keyCode) {
182 |           case 32: // 播放暂停Ctrl + Space
183 |             this.play()
184 |             break
185 |           case 37: // 上一曲Ctrl + Left
186 |             this.prev()
187 |             break
188 |           case 38: // 音量加Ctrl + Up
189 |             let plus = Number((this.volume += 0.1).toFixed(1))
190 |             if (plus > 1) {
191 |               plus = 1
192 |             }
193 |             this.volumeChange(plus)
194 |             break
195 |           case 39: // 下一曲Ctrl + Right
196 |             this.next()
197 |             break
198 |           case 40: // 音量减Ctrl + Down
199 |             let reduce = Number((this.volume -= 0.1).toFixed(1))
200 |             if (reduce < 0) {
201 |               reduce = 0
202 |             }
203 |             this.volumeChange(reduce)
204 |             break
205 |           case 79: // 切换播放模式Ctrl + O
206 |             this.modeChange()
207 |             break
208 |         }
209 |       }
210 |     },
211 |     // 上一曲
212 |     prev() {
213 |       if (!this.musicReady) {
214 |         return
215 |       }
216 |       if (this.playlist.length === 1) {
217 |         this.loop()
218 |       } else {
219 |         let index = this.currentIndex - 1
220 |         if (index < 0) {
221 |           index = this.playlist.length - 1
222 |         }
223 |         this.setCurrentIndex(index)
224 |         if (!this.playing && this.musicReady) {
225 |           this.setPlaying(true)
226 |         }
227 |         this.musicReady = false
228 |       }
229 |     },
230 |     // 播放暂停
231 |     play() {
232 |       if (!this.musicReady) {
233 |         return
234 |       }
235 |       this.setPlaying(!this.playing)
236 |     },
237 |     // 下一曲
238 |     // 当 flag 为 true 时,表示上一曲播放失败
239 |     next(flag = false) {
240 |       if (!this.musicReady) {
241 |         return
242 |       }
243 |       const {
244 |         playlist: { length },
245 |       } = this
246 |       if (
247 |         (length - 1 === this.currentIndex && this.mode === PLAY_MODE.ORDER) ||
248 |         (length === 1 && flag)
249 |       ) {
250 |         this.setCurrentIndex(-1)
251 |         this.setPlaying(false)
252 |         return
253 |       }
254 |       if (length === 1) {
255 |         this.loop()
256 |       } else {
257 |         let index = this.currentIndex + 1
258 |         if (index === length) {
259 |           index = 0
260 |         }
261 |         if (!this.playing && this.musicReady) {
262 |           this.setPlaying(true)
263 |         }
264 |         this.setCurrentIndex(index)
265 |         this.musicReady = false
266 |       }
267 |     },
268 |     // 循环
269 |     loop() {
270 |       this.audioEle.currentTime = 0
271 |       silencePromise(this.audioEle.play())
272 |       this.setPlaying(true)
273 |       if (this.lyric.length > 0) {
274 |         this.lyricIndex = 0
275 |       }
276 |     },
277 |     // 修改音乐显示时长
278 |     progressMusic(percent) {
279 |       this.currentTime = this.currentMusic.duration * percent
280 |     },
281 |     // 修改音乐进度
282 |     progressMusicEnd(percent) {
283 |       this.audioEle.currentTime = this.currentMusic.duration * percent
284 |     },
285 |     // 切换播放顺序
286 |     modeChange() {
287 |       const mode = (this.mode + 1) % 4
288 |       this.setPlayMode(mode)
289 |       if (mode === PLAY_MODE.LOOP) {
290 |         return
291 |       }
292 |       let list = []
293 |       switch (mode) {
294 |         case PLAY_MODE.LIST_LOOP:
295 |         case PLAY_MODE.ORDER:
296 |           list = this.orderList
297 |           break
298 |         case PLAY_MODE.RANDOM:
299 |           list = randomSortArray(this.orderList)
300 |           break
301 |       }
302 |       this.resetCurrentIndex(list)
303 |       this.setPlaylist(list)
304 |     },
305 |     // 修改当前歌曲索引
306 |     resetCurrentIndex(list) {
307 |       const index = list.findIndex((item) => {
308 |         return item.id === this.currentMusic.id
309 |       })
310 |       this.setCurrentIndex(index)
311 |     },
312 |     // 打开音乐评论
313 |     openComment() {
314 |       if (!this.currentMusic.id) {
315 |         this.$mmToast('还没有播放歌曲哦!')
316 |         return false
317 |       }
318 |       this.$router.push(`/music/comment/${this.currentMusic.id}`)
319 |     },
320 |     // 修改音量大小
321 |     volumeChange(percent) {
322 |       percent === 0 ? (this.isMute = true) : (this.isMute = false)
323 |       this.volume = percent
324 |       this.audioEle.volume = percent
325 |       setVolume(percent)
326 |     },
327 |     // 获取播放模式 icon
328 |     getModeIconType() {
329 |       return {
330 |         [PLAY_MODE.LIST_LOOP]: 'loop',
331 |         [PLAY_MODE.ORDER]: 'sequence',
332 |         [PLAY_MODE.RANDOM]: 'random',
333 |         [PLAY_MODE.LOOP]: 'loop-one',
334 |       }[this.mode]
335 |     },
336 |     // 获取播放模式 title
337 |     getModeIconTitle() {
338 |       const key = 'Ctrl + O'
339 |       return {
340 |         [PLAY_MODE.LIST_LOOP]: `列表循环 ${key}`,
341 |         [PLAY_MODE.ORDER]: `顺序播放 ${key}`,
342 |         [PLAY_MODE.RANDOM]: `随机播放 ${key}`,
343 |         [PLAY_MODE.LOOP]: `单曲循环 ${key}`,
344 |       }[this.mode]
345 |     },
346 |     // 查看歌词
347 |     handleOpenLyric() {
348 |       this.lyricVisible = true
349 |       this.$nextTick(() => {
350 |         this.$refs.lyric.clacTop()
351 |       })
352 |     },
353 |     // 关闭歌词
354 |     handleCloseLyric() {
355 |       this.lyricVisible = false
356 |     },
357 |     // 获取歌词
358 |     _getLyric(id) {
359 |       getLyric(id).then((res) => {
360 |         if (res.lrc && res.lrc.lyric) {
361 |           this.nolyric = false
362 |           this.lyric = parseLyric(res.lrc.lyric)
363 |         } else {
364 |           this.nolyric = true
365 |         }
366 |         silencePromise(this.audioEle.play())
367 |       })
368 |     },
369 |     ...mapMutations({
370 |       setPlaying: 'SET_PLAYING',
371 |       setPlaylist: 'SET_PLAYLIST',
372 |       setCurrentIndex: 'SET_CURRENTINDEX',
373 |     }),
374 |     ...mapActions(['setHistory', 'setPlayMode']),
375 |   },
376 | }
377 | </script>
378 | 
379 | <style lang="less">
380 | .router-view {
381 |   flex: 1;
382 |   overflow-x: hidden;
383 |   overflow-y: auto;
384 |   -webkit-overflow-scrolling: touch;
385 | }
386 | 
387 | .music {
388 |   padding: 75px 25px 25px 25px;
389 |   width: 100%;
390 |   max-width: 1750px;
391 |   margin: 0 auto;
392 |   height: 100%;
393 |   box-sizing: border-box;
394 |   overflow: hidden;
395 |   .music-content {
396 |     display: flex;
397 |     flex: 1;
398 |     overflow: hidden;
399 |     width: 100%;
400 |     .music-left {
401 |       flex: 1;
402 |       width: 100%;
403 |       overflow: hidden;
404 |     }
405 |     .music-right {
406 |       position: relative;
407 |       width: 310px;
408 |       margin-left: 10px;
409 |       .close-lyric {
410 |         position: absolute;
411 |         top: 0;
412 |         z-index: 1;
413 |         cursor: pointer;
414 |       }
415 |     }
416 |   }
417 | 
418 |   /*底部mmPlayer-bar*/
419 |   .music-bar {
420 |     display: flex;
421 |     align-items: center;
422 |     width: 100%;
423 |     padding: 15px 0;
424 |     color: #fff;
425 |     &.disable {
426 |       pointer-events: none;
427 |       opacity: 0.6;
428 |     }
429 |     .icon-color {
430 |       color: #fff;
431 |     }
432 |     .music-bar-btns {
433 |       display: flex;
434 |       justify-content: space-between;
435 |       align-items: center;
436 |       width: 180px;
437 |       .control-play {
438 |         .flex-center;
439 |         border-radius: 50%;
440 |         width: 40px;
441 |         height: 40px;
442 |         color: #fff;
443 |         background-color: rgba(255, 255, 255, 0.3);
444 |       }
445 |     }
446 | 
447 |     .flex-center;
448 |     .btn-prev {
449 |       width: 19px;
450 |       min-width: 19px;
451 |       height: 20px;
452 |       background-position: 0 -30px;
453 |     }
454 |     .btn-play {
455 |       width: 21px;
456 |       min-width: 21px;
457 |       height: 29px;
458 |       margin: 0 50px;
459 |       background-position: 0 0;
460 |       &.btn-play-pause {
461 |         background-position: -30px 0;
462 |       }
463 |     }
464 |     .btn-next {
465 |       width: 19px;
466 |       min-width: 19px;
467 |       height: 20px;
468 |       background-position: 0 -52px;
469 |     }
470 |     .music-music {
471 |       position: relative;
472 |       width: 100%;
473 |       flex: 1;
474 |       box-sizing: border-box;
475 |       padding-left: 40px;
476 |       font-size: @font_size_small;
477 |       color: @text_color_active;
478 |       .music-bar-info {
479 |         height: 15px;
480 |         padding-right: 80px;
481 |         line-height: 15px;
482 |         text-overflow: ellipsis;
483 |         overflow: hidden;
484 |         display: -webkit-box;
485 |         -webkit-line-clamp: 1;
486 |         -webkit-box-orient: vertical;
487 |       }
488 |       .music-bar-time {
489 |         position: absolute;
490 |         top: 0;
491 |         right: 5px;
492 |       }
493 |     }
494 |     .mode,
495 |     .comment,
496 |     .music-bar-volume {
497 |       margin-left: 20px;
498 |     }
499 | 
500 |     // 音量控制
501 |     .volume-wrapper {
502 |       margin-left: 20px;
503 |       width: 150px;
504 |     }
505 |   }
506 | 
507 |   /*遮罩*/
508 |   .mmPlayer-mask,
509 |   .mmPlayer-bg {
510 |     position: absolute;
511 |     top: 0;
512 |     right: 0;
513 |     left: 0;
514 |     bottom: 0;
515 |   }
516 | 
517 |   .mmPlayer-mask {
518 |     z-index: -1;
519 |     background-color: @mask_color;
520 |   }
521 | 
522 |   .mmPlayer-bg {
523 |     z-index: -2;
524 |     background-repeat: no-repeat;
525 |     background-size: cover;
526 |     background-position: 50%;
527 |     filter: blur(12px);
528 |     opacity: 0.7;
529 |     transition: all 0.8s;
530 |     transform: scale(1.1);
531 |   }
532 | 
533 |   @media (min-width: 960px) {
534 |     .close-lyric {
535 |       display: none;
536 |     }
537 |   }
538 | 
539 |   //当屏幕小于960时
540 |   @media (max-width: 960px) {
541 |     .music-right {
542 |       display: none;
543 |       &.show {
544 |         display: block;
545 |         margin-left: 0;
546 |         width: 100%;
547 |       }
548 |     }
549 |   }
550 |   //当屏幕小于768时
551 |   @media (max-width: 768px) {
552 |     padding: 75px 15px 5px 15px;
553 | 
554 |     .music-bar {
555 |       padding-top: 10px;
556 |       .music-bar-info span,
557 |       .music-bar-volume .mmProgress {
558 |         display: none;
559 |       }
560 |     }
561 |   }
562 |   //当屏幕小于520时
563 |   @media (max-width: 520px) {
564 |     .music-bar {
565 |       position: relative;
566 |       flex-direction: column;
567 |       .music-bar-btns {
568 |         width: 60%;
569 |         margin-top: 10px;
570 |         order: 2;
571 |       }
572 |       .music-music {
573 |         padding-left: 0;
574 |         order: 1;
575 |       }
576 |       .mode,
577 |       .comment {
578 |         position: absolute;
579 |         bottom: 20px;
580 |         margin: 0;
581 |       }
582 |       .mode {
583 |         left: 5px;
584 |       }
585 |       .comment {
586 |         right: 5px;
587 |       }
588 |       .music-bar-volume {
589 |         display: none;
590 |       }
591 |     }
592 |   }
593 | }
594 | </style>
595 | 


--------------------------------------------------------------------------------
/src/pages/playList/playList.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <!--正在播放-->
 3 |   <div class="playList">
 4 |     <music-list :list="playlist" list-type="duration" @select="selectItem" @del="deleteItem">
 5 |       <div slot="listBtn" class="list-btn">
 6 |         <span @click="$refs.dialog.show()">清空列表</span>
 7 |       </div>
 8 |     </music-list>
 9 |     <mm-dialog
10 |       ref="dialog"
11 |       body-text="是否清空正在播放列表"
12 |       confirm-btn-text="清空"
13 |       @confirm="clearList"
14 |     />
15 |   </div>
16 | </template>
17 | 
18 | <script>
19 | import { mapGetters, mapMutations, mapActions } from 'vuex'
20 | import MusicList from 'components/music-list/music-list'
21 | import MmDialog from 'base/mm-dialog/mm-dialog'
22 | 
23 | export default {
24 |   name: 'PlayList',
25 |   components: {
26 |     MusicList,
27 |     MmDialog,
28 |   },
29 |   data() {
30 |     return {
31 |       show: false,
32 |     }
33 |   },
34 |   computed: {
35 |     ...mapGetters(['playing', 'playlist', 'currentMusic']),
36 |   },
37 |   methods: {
38 |     // 清空列表事件
39 |     clearList() {
40 |       this.clearPlayList()
41 |       this.$mmToast('列表清空成功')
42 |     },
43 |     // 播放暂停事件
44 |     selectItem(item, index) {
45 |       if (item.id !== this.currentMusic.id) {
46 |         this.setCurrentIndex(index)
47 |         this.setPlaying(true)
48 |       }
49 |     },
50 |     // 删除事件
51 |     deleteItem(index) {
52 |       let list = [...this.playlist]
53 |       list.splice(index, 1)
54 |       this.removerPlayListItem({ list, index })
55 |       this.$mmToast('删除成功')
56 |     },
57 |     ...mapMutations({
58 |       setPlaying: 'SET_PLAYING',
59 |       setCurrentIndex: 'SET_CURRENTINDEX',
60 |       clearPlaylist: 'CLEAR_PLAYLIST',
61 |     }),
62 |     ...mapActions(['removerPlayListItem', 'clearPlayList']),
63 |   },
64 | }
65 | </script>
66 | 


--------------------------------------------------------------------------------
/src/pages/search/search.vue:
--------------------------------------------------------------------------------
  1 | <template>
  2 |   <!--搜索-->
  3 |   <div class="search flex-col">
  4 |     <mm-loading v-model="mmLoadShow" />
  5 |     <div class="search-head">
  6 |       <span v-for="(item, index) in Artists" :key="index" @click="clickHot(item.first)">
  7 |         {{ item.first }}
  8 |       </span>
  9 |       <input
 10 |         v-model.trim="searchValue"
 11 |         class="search-input"
 12 |         type="text"
 13 |         placeholder="音乐/歌手"
 14 |         @keyup.enter="onEnter"
 15 |       />
 16 |     </div>
 17 |     <div class="flex-1 overflow-hidden">
 18 |       <music-list
 19 |         ref="musicList"
 20 |         :list="list"
 21 |         list-type="pullup"
 22 |         @select="selectItem"
 23 |         @pullUp="pullUpLoad"
 24 |       />
 25 |     </div>
 26 |   </div>
 27 | </template>
 28 | 
 29 | <script>
 30 | import { mapGetters, mapActions, mapMutations } from 'vuex'
 31 | import { search, searchHot, getMusicDetail } from 'api'
 32 | import { formatSongs } from '@/utils/song'
 33 | import MmLoading from 'base/mm-loading/mm-loading'
 34 | import MusicList from 'components/music-list/music-list'
 35 | import { loadMixin } from '@/utils/mixin'
 36 | import { toHttps } from '@/utils/util'
 37 | 
 38 | export default {
 39 |   name: 'Search',
 40 |   components: {
 41 |     MmLoading,
 42 |     MusicList,
 43 |   },
 44 |   mixins: [loadMixin],
 45 |   data() {
 46 |     return {
 47 |       searchValue: '', // 搜索关键词
 48 |       Artists: [], // 热搜数组
 49 |       list: [], // 搜索数组
 50 |       page: 0, // 分页
 51 |       lockUp: true, // 是否锁定上拉加载事件,默认锁定
 52 |     }
 53 |   },
 54 |   computed: {
 55 |     ...mapGetters(['playing', 'currentMusic']),
 56 |   },
 57 |   watch: {
 58 |     list(newList, oldList) {
 59 |       if (newList.length !== oldList.length) {
 60 |         this.lockUp = false
 61 |       } else if (newList[newList.length - 1].id !== oldList[oldList.length - 1].id) {
 62 |         this.lockUp = false
 63 |       }
 64 |     },
 65 |   },
 66 |   created() {
 67 |     // 获取热搜
 68 |     searchHot().then(({ result }) => {
 69 |       this.Artists = result.hots.slice(0, 5)
 70 |       this.mmLoadShow = false
 71 |     })
 72 |   },
 73 |   methods: {
 74 |     // 点击热搜
 75 |     clickHot(name) {
 76 |       this.searchValue = name
 77 |       this.onEnter()
 78 |     },
 79 |     // 搜索事件
 80 |     onEnter() {
 81 |       if (this.searchValue.replace(/(^\s+)|(\s+$)/g, '') === '') {
 82 |         this.$mmToast('搜索内容不能为空!')
 83 |         return
 84 |       }
 85 |       this.mmLoadShow = true
 86 |       this.page = 0
 87 |       if (this.list.length > 0) {
 88 |         this.$refs.musicList.scrollTo()
 89 |       }
 90 |       search(this.searchValue).then(({ result }) => {
 91 |         this.list = formatSongs(result.songs)
 92 |         this._hideLoad()
 93 |       })
 94 |     },
 95 |     // 滚动加载事件
 96 |     pullUpLoad() {
 97 |       this.page += 1
 98 |       search(this.searchValue, this.page).then(({ result }) => {
 99 |         if (!result.songs) {
100 |           this.$mmToast('没有更多歌曲啦!')
101 |           return
102 |         }
103 |         this.list = [...this.list, ...formatSongs(result.songs)]
104 |       })
105 |     },
106 |     // 播放歌曲
107 |     async selectItem(music) {
108 |       try {
109 |         const image = await this._getMusicDetail(music.id)
110 |         music.image = toHttps(image)
111 |         this.selectAddPlay(music)
112 |       } catch (error) {
113 |         this.$mmToast('哎呀,出错啦~')
114 |       }
115 |     },
116 |     // 获取歌曲详情
117 |     _getMusicDetail(id) {
118 |       return getMusicDetail(id).then((res) => res.songs[0].al.picUrl)
119 |     },
120 |     ...mapMutations({
121 |       setPlaying: 'SET_PLAYING',
122 |     }),
123 |     ...mapActions(['selectAddPlay']),
124 |   },
125 | }
126 | </script>
127 | 
128 | <style lang="less" scoped>
129 | .search {
130 |   overflow: hidden;
131 |   height: 100%;
132 |   .search-head {
133 |     display: flex;
134 |     height: 40px;
135 |     padding: 10px 15px;
136 |     overflow: hidden;
137 |     background: @search_bg_color;
138 |     span {
139 |       line-height: 40px;
140 |       margin-right: 15px;
141 |       cursor: pointer;
142 |       &:hover {
143 |         color: @text_color_active;
144 |       }
145 |       @media (max-width: 640px) {
146 |         & {
147 |           display: none;
148 |         }
149 |       }
150 |     }
151 |     .search-input {
152 |       flex: 1;
153 |       height: 40px;
154 |       box-sizing: border-box;
155 |       padding: 0 15px;
156 |       border: 1px solid @btn_color;
157 |       outline: 0;
158 |       background: transparent;
159 |       color: @text_color_active;
160 |       font-size: @font_size_medium;
161 |       box-shadow: 0 0 1px 0 #fff inset;
162 |       &::placeholder {
163 |         color: @text_color;
164 |       }
165 |     }
166 |   }
167 | }
168 | </style>
169 | 


--------------------------------------------------------------------------------
/src/pages/topList/topList.vue:
--------------------------------------------------------------------------------
  1 | <template>
  2 |   <!--排行榜-->
  3 |   <div class="topList">
  4 |     <mm-loading v-model="mmLoadShow" />
  5 |     <template v-if="!mmLoadShow">
  6 |       <div class="topList-head">云音乐特色榜</div>
  7 |       <div class="topList-content">
  8 |         <div
  9 |           v-for="(item, index) in list"
 10 |           :key="index"
 11 |           class="list-item"
 12 |           :title="`${item.name}-${item.updateFrequency}`"
 13 |         >
 14 |           <router-link :to="{ path: `/music/details/${item.id}` }" tag="div" class="topList-item">
 15 |             <div class="topList-img">
 16 |               <img v-lazy="`${item.coverImgUrl}?param=300y300`" class="cover-img" />
 17 |             </div>
 18 |             <h3 class="name">{{ item.name }}</h3>
 19 |           </router-link>
 20 |         </div>
 21 |       </div>
 22 |       <div class="topList-head">热门歌单</div>
 23 |       <div class="topList-content">
 24 |         <div v-for="(item, index) in hotList" :key="index" class="list-item" :title="item.name">
 25 |           <router-link :to="{ path: `/music/details/${item.id}` }" tag="div" class="topList-item">
 26 |             <div class="topList-img">
 27 |               <img v-lazy="`${item.picUrl}?param=300y300`" class="cover-img" />
 28 |             </div>
 29 |             <h3 class="name">{{ item.name }}</h3>
 30 |           </router-link>
 31 |         </div>
 32 |       </div>
 33 |     </template>
 34 |   </div>
 35 | </template>
 36 | 
 37 | <script>
 38 | import { getToplistDetail, getPersonalized } from 'api'
 39 | import MmLoading from 'base/mm-loading/mm-loading'
 40 | import { loadMixin } from '@/utils/mixin'
 41 | 
 42 | export default {
 43 |   name: 'PlayList',
 44 |   components: {
 45 |     MmLoading,
 46 |   },
 47 |   mixins: [loadMixin],
 48 |   data() {
 49 |     return {
 50 |       list: [], // 云音乐特色榜
 51 |       hotList: [], // 热门歌单
 52 |     }
 53 |   },
 54 |   created() {
 55 |     Promise.all([getToplistDetail(), getPersonalized()])
 56 |       .then(([topList, hotList]) => {
 57 |         this.list = topList.list.filter((v) => v.ToplistType)
 58 |         this.hotList = hotList.result.slice()
 59 |         this._hideLoad()
 60 |       })
 61 |       .catch(() => {})
 62 |   },
 63 | }
 64 | </script>
 65 | 
 66 | <style lang="less" scoped>
 67 | .topList {
 68 |   overflow-x: hidden;
 69 |   overflow-y: auto;
 70 |   -webkit-overflow-scrolling: touch;
 71 |   &-head {
 72 |     width: 100%;
 73 |     height: 34px;
 74 |     line-height: 34px;
 75 |     padding: 20px 0;
 76 |     font-size: @font_size_large;
 77 |     color: @text_color_active;
 78 |   }
 79 |   &-content {
 80 |     overflow: hidden;
 81 |   }
 82 |   .list-item {
 83 |     float: left;
 84 |     width: calc(~'100% / 7');
 85 |     .topList-item {
 86 |       width: 130px;
 87 |       text-align: center;
 88 |       cursor: pointer;
 89 |       margin: 0 auto 20px;
 90 |       &:hover {
 91 |         color: #fff;
 92 |       }
 93 |       .name {
 94 |         height: 30px;
 95 |         line-height: 30px;
 96 |         font-size: @font_size_medium;
 97 |         .no-wrap();
 98 |       }
 99 |       @media (max-width: 1100px) {
100 |         width: 80%;
101 |       }
102 |     }
103 |     @media (max-width: 1500px) {
104 |       width: calc(~'100% / 6');
105 |     }
106 |     @media (max-width: 1400px), (max-width: 960px) {
107 |       width: calc(~'100% / 5');
108 |     }
109 |     @media (max-width: 1280px), (max-width: 768px) {
110 |       width: calc(~'100% / 4');
111 |     }
112 |     @media (max-width: 540px) {
113 |       width: calc(~'100% / 3');
114 |     }
115 |     .topList-img {
116 |       position: relative;
117 |       padding-top: 100%;
118 |       width: 100%;
119 |       height: 0;
120 |       .cover-img {
121 |         position: absolute;
122 |         top: 0;
123 |         left: 0;
124 |       }
125 |     }
126 |   }
127 | }
128 | </style>
129 | 


--------------------------------------------------------------------------------
/src/pages/userList/userList.vue:
--------------------------------------------------------------------------------
  1 | <template>
  2 |   <!--我的歌单-->
  3 |   <div class="userList">
  4 |     <mm-loading v-model="mmLoadShow" />
  5 |     <template v-if="list.length > 0">
  6 |       <div v-for="item in formatList" :key="item.id" class="list-item" :title="item.name">
  7 |         <router-link :to="{ path: `/music/details/${item.id}` }" tag="div" class="userList-item">
  8 |           <img v-lazy="`${item.coverImgUrl}?param=200y200`" class="cover-img" />
  9 |           <h3 class="name">{{ item.name }}</h3>
 10 |         </router-link>
 11 |       </div>
 12 |     </template>
 13 |     <mm-no-result v-else title="啥也没有哦,快去登录看看吧!" />
 14 |   </div>
 15 | </template>
 16 | 
 17 | <script>
 18 | import { mapGetters } from 'vuex'
 19 | 
 20 | import { getUserPlaylist } from 'api'
 21 | import { loadMixin } from '@/utils/mixin'
 22 | 
 23 | import MmLoading from 'base/mm-loading/mm-loading'
 24 | import MmNoResult from 'base/mm-no-result/mm-no-result'
 25 | 
 26 | export default {
 27 |   name: 'PlayList',
 28 |   components: {
 29 |     MmLoading,
 30 |     MmNoResult,
 31 |   },
 32 |   mixins: [loadMixin],
 33 |   data() {
 34 |     return {
 35 |       list: [], // 列表
 36 |     }
 37 |   },
 38 |   computed: {
 39 |     formatList() {
 40 |       return this.list.filter((item) => item.trackCount > 0)
 41 |     },
 42 |     ...mapGetters(['uid']),
 43 |   },
 44 |   watch: {
 45 |     uid(newUid) {
 46 |       if (newUid) {
 47 |         this.mmLoadShow = true
 48 |         this._getUserPlaylist(newUid)
 49 |       } else {
 50 |         this.list = []
 51 |       }
 52 |     },
 53 |   },
 54 |   created() {
 55 |     if (!this.uid) {
 56 |       this.mmLoadShow = false
 57 |     }
 58 |   },
 59 |   activated() {
 60 |     if (this.uid && this.list.length === 0) {
 61 |       this.mmLoadShow = true
 62 |       this._getUserPlaylist(this.uid)
 63 |     } else if (!this.uid && this.list.length !== 0) {
 64 |       this.list = []
 65 |     }
 66 |   },
 67 |   methods: {
 68 |     // 获取我的歌单详情
 69 |     _getUserPlaylist(uid) {
 70 |       getUserPlaylist(uid).then((res) => {
 71 |         if (res.playlist.length === 0) {
 72 |           return
 73 |         }
 74 |         this.list = res.playlist.slice(1)
 75 |         this._hideLoad()
 76 |       })
 77 |     },
 78 |   },
 79 | }
 80 | </script>
 81 | 
 82 | <style lang="less" scoped>
 83 | .userList {
 84 |   overflow-x: hidden;
 85 |   overflow-y: auto;
 86 |   -webkit-overflow-scrolling: touch;
 87 |   &-head {
 88 |     height: 100px;
 89 |   }
 90 |   .list-item {
 91 |     float: left;
 92 |     width: calc(~'100% / 7');
 93 |     .userList-item {
 94 |       width: 130px;
 95 |       text-align: center;
 96 |       cursor: pointer;
 97 |       margin: 0 auto 20px;
 98 |       &:hover {
 99 |         color: #fff;
100 |       }
101 |       .name {
102 |         height: 30px;
103 |         line-height: 30px;
104 |         font-size: @font_size_medium;
105 |         .no-wrap();
106 |       }
107 |       @media (max-width: 1100px) {
108 |         width: 80%;
109 |       }
110 |     }
111 |     @media (max-width: 1500px) {
112 |       width: calc(~'100% / 6');
113 |     }
114 |     @media (max-width: 1400px), (max-width: 960px) {
115 |       width: calc(~'100% / 5');
116 |     }
117 |     @media (max-width: 1280px), (max-width: 768px) {
118 |       width: calc(~'100% / 4');
119 |     }
120 |     @media (max-width: 540px) {
121 |       width: calc(~'100% / 3');
122 |     }
123 |   }
124 | }
125 | </style>
126 | 


--------------------------------------------------------------------------------
/src/router/index.js:
--------------------------------------------------------------------------------
 1 | import Vue from 'vue'
 2 | import Router from 'vue-router'
 3 | Vue.use(Router)
 4 | 
 5 | const routes = [
 6 |   {
 7 |     path: '/',
 8 |     redirect: '/music',
 9 |   },
10 |   {
11 |     path: '/music',
12 |     component: () => import('pages/music'),
13 |     redirect: '/music/playlist',
14 |     children: [
15 |       {
16 |         path: '/music/playlist', // 正在播放列表
17 |         component: () => import('pages/playList/playList'),
18 |         meta: {
19 |           keepAlive: true,
20 |         },
21 |       },
22 |       {
23 |         path: '/music/userlist', // 我的歌单
24 |         component: () => import('pages/userList/userList'),
25 |         meta: {
26 |           title: '我的歌单',
27 |           keepAlive: true,
28 |         },
29 |       },
30 |       {
31 |         path: '/music/toplist', // 排行榜列表
32 |         component: () => import('pages/topList/topList'),
33 |         meta: {
34 |           title: '排行榜',
35 |           keepAlive: true,
36 |         },
37 |       },
38 |       {
39 |         path: '/music/details/:id', // 音乐详情列表
40 |         component: () => import('pages/details/details'),
41 |       },
42 |       {
43 |         path: '/music/historylist', // 我听过的列表
44 |         component: () => import('pages/historyList/historyList'),
45 |         meta: {
46 |           title: '我听过的',
47 |         },
48 |       },
49 |       {
50 |         path: '/music/search', // 搜索
51 |         component: () => import('pages/search/search'),
52 |         meta: {
53 |           title: '搜索',
54 |           keepAlive: true,
55 |         },
56 |       },
57 |       {
58 |         path: '/music/comment/:id', // 音乐评论
59 |         component: () => import('pages/comment/comment'),
60 |         meta: {
61 |           title: '评论详情',
62 |         },
63 |       },
64 |     ],
65 |   },
66 | ]
67 | 
68 | export default new Router({
69 |   linkActiveClass: 'active',
70 |   linkExactActiveClass: 'active',
71 |   routes,
72 | })
73 | 


--------------------------------------------------------------------------------
/src/store/actions.js:
--------------------------------------------------------------------------------
 1 | import {
 2 |   clearHistoryList,
 3 |   setHistoryList,
 4 |   removeHistoryList,
 5 |   setMode,
 6 |   setUserId,
 7 | } from '@/utils/storage'
 8 | import * as types from './mutation-types'
 9 | 
10 | function findIndex(list, music) {
11 |   return list.findIndex((item) => {
12 |     return item.id === music.id
13 |   })
14 | }
15 | 
16 | // 设置播放列表
17 | export const setPlaylist = function ({ commit }, { list }) {
18 |   commit(types.SET_PLAYLIST, list)
19 |   commit(types.SET_ORDERLIST, list)
20 | }
21 | 
22 | // 选择播放(会更新整个播放列表)
23 | export const selectPlay = function ({ commit }, { list, index }) {
24 |   commit(types.SET_PLAYLIST, list)
25 |   commit(types.SET_ORDERLIST, list)
26 |   commit(types.SET_CURRENTINDEX, index)
27 |   commit(types.SET_PLAYING, true)
28 | }
29 | // 选择播放(会插入一条到播放列表)
30 | export const selectAddPlay = function ({ commit, state }, music) {
31 |   let list = [...state.playlist]
32 |   // 查询当前播放列表是否有代插入的音乐,并返回其索引值
33 |   let index = findIndex(list, music)
34 |   // 当前播放列表有待插入的音乐时,直接改变当前播放音乐的索引值
35 |   if (index > -1) {
36 |     commit(types.SET_CURRENTINDEX, index)
37 |   } else {
38 |     list.unshift(music)
39 |     commit(types.SET_PLAYLIST, list)
40 |     commit(types.SET_ORDERLIST, list)
41 |     commit(types.SET_CURRENTINDEX, 0)
42 |   }
43 |   commit(types.SET_PLAYING, true)
44 | }
45 | 
46 | // 清空播放列表
47 | export const clearPlayList = function ({ commit }) {
48 |   commit(types.SET_PLAYING, false)
49 |   commit(types.SET_CURRENTINDEX, -1)
50 |   commit(types.SET_PLAYLIST, [])
51 |   commit(types.SET_ORDERLIST, [])
52 | }
53 | 
54 | // 删除正在播放列表中的歌曲
55 | export const removerPlayListItem = function ({ commit, state }, { list, index }) {
56 |   let currentIndex = state.currentIndex
57 |   if (index < state.currentIndex || list.length === state.currentIndex) {
58 |     currentIndex--
59 |     commit(types.SET_CURRENTINDEX, currentIndex)
60 |   }
61 |   commit(types.SET_PLAYLIST, list)
62 |   commit(types.SET_ORDERLIST, list)
63 |   if (!list.length) {
64 |     commit(types.SET_PLAYING, false)
65 |   } else {
66 |     commit(types.SET_PLAYING, true)
67 |   }
68 | }
69 | // 设置播放历史
70 | export const setHistory = function ({ commit }, music) {
71 |   commit(types.SET_HISTORYLIST, setHistoryList(music))
72 | }
73 | // 删除播放历史
74 | export const removeHistory = function ({ commit }, music) {
75 |   commit(types.SET_HISTORYLIST, removeHistoryList(music))
76 | }
77 | // 清空播放历史
78 | export const clearHistory = function ({ commit }) {
79 |   commit(types.SET_HISTORYLIST, clearHistoryList())
80 | }
81 | // 设置播放模式
82 | export const setPlayMode = function ({ commit }, mode) {
83 |   commit(types.SET_PLAYMODE, setMode(mode))
84 | }
85 | // 设置网易云用户UID
86 | export const setUid = function ({ commit }, uid) {
87 |   commit(types.SET_UID, setUserId(uid))
88 | }
89 | 


--------------------------------------------------------------------------------
/src/store/getters.js:
--------------------------------------------------------------------------------
 1 | // audio元素
 2 | export const audioEle = (state) => state.audioEle
 3 | // 播放模式
 4 | export const mode = (state) => state.mode
 5 | // 播放状态
 6 | export const playing = (state) => state.playing
 7 | // 播放列表
 8 | export const playlist = (state) => state.playlist
 9 | // 顺序列表
10 | export const orderList = (state) => state.orderList
11 | // 当前音乐索引
12 | export const currentIndex = (state) => state.currentIndex
13 | // 当前音乐
14 | export const currentMusic = (state) => {
15 |   return state.playlist[state.currentIndex] || {}
16 | }
17 | // 播放历史列表
18 | export const historyList = (state) => state.historyList
19 | // 网易云用户UID
20 | export const uid = (state) => state.uid
21 | 


--------------------------------------------------------------------------------
/src/store/index.js:
--------------------------------------------------------------------------------
 1 | import Vue from 'vue'
 2 | import Vuex from 'vuex'
 3 | import state from './state'
 4 | import * as getters from './getters'
 5 | import * as actions from './actions'
 6 | import mutations from './mutations'
 7 | // vuex调试
 8 | import createLogger from 'vuex/dist/logger'
 9 | const debug = process.env.NODE_ENV !== 'production'
10 | 
11 | Vue.use(Vuex)
12 | 
13 | export default new Vuex.Store({
14 |   state,
15 |   getters,
16 |   mutations,
17 |   actions,
18 |   // vuex调试
19 |   strict: debug,
20 |   plugins: debug ? [createLogger()] : [],
21 | })
22 | 


--------------------------------------------------------------------------------
/src/store/mutation-types.js:
--------------------------------------------------------------------------------
1 | export const SET_AUDIOELE = 'SET_AUDIOELE' // 修改audio元素
2 | export const SET_PLAYMODE = 'SET_PLAYMODE' // 修改播放模式
3 | export const SET_PLAYING = 'SET_PLAYING' // 修改播放状态
4 | export const SET_PLAYLIST = 'SET_PLAYLIST' // 修改播放列表
5 | export const SET_ORDERLIST = 'SET_ORDERLIST' // 修改顺序列表
6 | export const SET_CURRENTINDEX = 'SET_CURRENTINDEX' // 修改当前音乐索引
7 | export const SET_HISTORYLIST = 'SET_HISTORYLIST' // 修改播放历史列表
8 | export const SET_UID = 'SET_UID' // 修改网易云用户UID
9 | 


--------------------------------------------------------------------------------
/src/store/mutations.js:
--------------------------------------------------------------------------------
 1 | import * as types from './mutation-types'
 2 | 
 3 | const mutations = {
 4 |   // 修改audio元素
 5 |   [types.SET_AUDIOELE](state, audioEle) {
 6 |     state.audioEle = audioEle
 7 |   },
 8 |   // 修改播放模式
 9 |   [types.SET_PLAYMODE](state, mode) {
10 |     state.mode = mode
11 |   },
12 |   // 修改播放状态
13 |   [types.SET_PLAYING](state, playing) {
14 |     state.playing = playing
15 |   },
16 |   // 修改播放列表
17 |   [types.SET_PLAYLIST](state, playlist) {
18 |     state.playlist = playlist
19 |   },
20 |   // 修改顺序列表
21 |   [types.SET_ORDERLIST](state, orderList) {
22 |     state.orderList = orderList
23 |   },
24 |   // 修改当前音乐索引
25 |   [types.SET_CURRENTINDEX](state, currentIndex) {
26 |     state.currentIndex = currentIndex
27 |   },
28 |   // 修改播放历史列表
29 |   [types.SET_HISTORYLIST](state, historyList) {
30 |     state.historyList = historyList
31 |   },
32 |   // 修改网易云用户UID
33 |   [types.SET_UID](state, uid) {
34 |     state.uid = uid
35 |   },
36 | }
37 | 
38 | export default mutations
39 | 


--------------------------------------------------------------------------------
/src/store/state.js:
--------------------------------------------------------------------------------
 1 | import { getHistoryList, getMode, getUserId } from '@/utils/storage'
 2 | 
 3 | const state = {
 4 |   audioEle: null, // audio元素
 5 |   mode: getMode(), // 播放模式,默认列表循环
 6 |   playing: false, // 播放状态
 7 |   playlist: [], // 播放列表
 8 |   orderList: [], // 顺序列表
 9 |   currentIndex: -1, // 当前音乐索引
10 |   historyList: getHistoryList() || [], // 播放历史列表
11 |   uid: getUserId() || null, // 网易云用户UID
12 | }
13 | 
14 | export default state
15 | 


--------------------------------------------------------------------------------
/src/styles/index.less:
--------------------------------------------------------------------------------
  1 | @import 'reset';
  2 | @import 'var';
  3 | 
  4 | html,
  5 | body,
  6 | #app {
  7 |   width: 100%;
  8 |   height: 100%;
  9 |   overflow: hidden;
 10 | }
 11 | 
 12 | body {
 13 |   min-width: 320px;
 14 |   font-family: Arial;
 15 | }
 16 | 
 17 | #app {
 18 |   position: relative;
 19 | }
 20 | 
 21 | .cover-img {
 22 |   width: 100%;
 23 |   height: 100%;
 24 |   object-fit: cover;
 25 | }
 26 | 
 27 | //浮动
 28 | .fl {
 29 |   float: left;
 30 | }
 31 | 
 32 | .fr {
 33 |   float: right;
 34 | }
 35 | 
 36 | .pointer {
 37 |   cursor: pointer;
 38 | }
 39 | 
 40 | .hover {
 41 |   color: @text_color;
 42 |   cursor: pointer;
 43 |   &:hover {
 44 |     color: @text_color_active;
 45 |   }
 46 | }
 47 | 
 48 | .text-left {
 49 |   text-align: left;
 50 | }
 51 | 
 52 | .clearfix {
 53 |   &:after {
 54 |     display: block;
 55 |     content: '';
 56 |     clear: both;
 57 |   }
 58 | }
 59 | 
 60 | .flex {
 61 |   display: flex;
 62 | }
 63 | 
 64 | .flex-col {
 65 |   display: flex;
 66 |   flex-direction: column;
 67 | }
 68 | 
 69 | .flex-1 {
 70 |   flex: 1;
 71 | }
 72 | 
 73 | .overflow-hidden {
 74 |   overflow: hidden;
 75 | }
 76 | 
 77 | /* 滚动条相关样式 */
 78 | ::-webkit-scrollbar {
 79 |   /*滚动条整体部分,其中的属性有width,height,background,border(就和一个块级元素一样)等*/
 80 |   background-color: @scrollbar_bg;
 81 |   width: @scrollbar_size; // 纵向滚动条
 82 |   height: @scrollbar_size; // 横向滚动条
 83 |   border-radius: @scrollbar_border_radius;
 84 | }
 85 | 
 86 | ::-webkit-scrollbar-button {
 87 |   /*滚动条两端的按钮。可以用display:none让其不显示,也可以添加背景图片,颜色改变显示效果。*/
 88 |   display: none;
 89 | }
 90 | 
 91 | ::-webkit-scrollbar-track {
 92 |   /*外层轨道。可以用display:none让其不显示,也可以添加背景图片,颜色改变显示效果。*/
 93 |   display: none;
 94 |   //background-color: rgba(255, 255, 255, 0.1);
 95 |   border-radius: @scrollbar_border_radius;
 96 | }
 97 | 
 98 | ::-webkit-scrollbar-track-piece {
 99 |   /*内层轨道,滚动条中间部分(除去)。*/
100 |   //background-color: rgba(255, 255, 255, .1);
101 |   border-radius: @scrollbar_border_radius;
102 | }
103 | 
104 | ::-webkit-scrollbar-thumb {
105 |   /*滚动条里面可以拖动的那部分*/
106 |   background-color: @scrollbar_thumb;
107 |   border-radius: @scrollbar_border_radius;
108 | }
109 | 
110 | ::-webkit-scrollbar-corner {
111 |   border-radius: @scrollbar_border_radius;
112 | }
113 | 
114 | ::-webkit-resizer {
115 |   /*定义右下角拖动块的样式*/
116 |   border-radius: @scrollbar_border_radius;
117 | }
118 | 


--------------------------------------------------------------------------------
/src/styles/mixin.less:
--------------------------------------------------------------------------------
 1 | // 显示省略号
 2 | .no-wrap() {
 3 |   text-overflow: ellipsis;
 4 |   overflow: hidden;
 5 |   white-space: nowrap;
 6 | }
 7 | 
 8 | .flex-center(@direction: row) {
 9 |   display: flex;
10 |   flex-direction: @direction;
11 |   justify-content: center;
12 |   align-items: center;
13 | }
14 | 


--------------------------------------------------------------------------------
/src/styles/reset.less:
--------------------------------------------------------------------------------
  1 | html,
  2 | body,
  3 | div,
  4 | span,
  5 | applet,
  6 | object,
  7 | iframe,
  8 | h1,
  9 | h2,
 10 | h3,
 11 | h4,
 12 | h5,
 13 | h6,
 14 | p,
 15 | blockquote,
 16 | pre,
 17 | a,
 18 | abbr,
 19 | acronym,
 20 | address,
 21 | big,
 22 | cite,
 23 | code,
 24 | del,
 25 | dfn,
 26 | em,
 27 | img,
 28 | ins,
 29 | kbd,
 30 | q,
 31 | s,
 32 | samp,
 33 | small,
 34 | strike,
 35 | strong,
 36 | sub,
 37 | sup,
 38 | tt,
 39 | var,
 40 | b,
 41 | u,
 42 | i,
 43 | center,
 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 | embed,
 67 | figure,
 68 | figcaption,
 69 | footer,
 70 | header,
 71 | hgroup,
 72 | menu,
 73 | nav,
 74 | output,
 75 | ruby,
 76 | section,
 77 | summary,
 78 | time,
 79 | mark,
 80 | audio,
 81 | video {
 82 |   margin: 0;
 83 |   padding: 0;
 84 |   border: 0;
 85 |   font-size: 100%;
 86 |   font: inherit;
 87 |   vertical-align: baseline;
 88 |   -webkit-tap-highlight-color: transparent;
 89 | }
 90 | 
 91 | /* HTML5 display-role reset for older browsers */
 92 | article,
 93 | aside,
 94 | details,
 95 | figcaption,
 96 | figure,
 97 | footer,
 98 | header,
 99 | hgroup,
100 | menu,
101 | nav,
102 | section {
103 |   display: block;
104 | }
105 | 
106 | body {
107 |   line-height: 1;
108 | }
109 | 
110 | ol,
111 | ul {
112 |   list-style: none;
113 | }
114 | 
115 | blockquote,
116 | q {
117 |   quotes: none;
118 | }
119 | 
120 | blockquote:before,
121 | blockquote:after,
122 | q:before,
123 | q:after {
124 |   content: '';
125 |   content: none;
126 | }
127 | 
128 | table {
129 |   border-collapse: collapse;
130 |   border-spacing: 0;
131 | }
132 | 
133 | a {
134 |   text-decoration: none;
135 |   color: inherit;
136 | }
137 | 
138 | input[type='number']::-webkit-inner-spin-button,
139 | input[type='number']::-webkit-outer-spin-button {
140 |   -webkit-appearance: none;
141 | }
142 | 


--------------------------------------------------------------------------------
/src/styles/var.less:
--------------------------------------------------------------------------------
 1 | /* 颜色规范定义 */
 2 | @text_color: rgba(255, 255, 255, 0.6);
 3 | @text_color_active: #fff; //重点部分
 4 | 
 5 | // active 颜色
 6 | @active_color: #fff;
 7 | 
 8 | // 遮罩层颜色
 9 | @mask_color: rgba(0, 0, 0, 0.4);
10 | // 背景滤镜
11 | @backdrop_filter: blur(6px);
12 | 
13 | /* 字体大小规范定义*/
14 | @font_size_small: 12px;
15 | @font_size_medium: 14px;
16 | @font_size_medium_x: 16px;
17 | @font_size_large: 18px;
18 | @font_size_large_x: 22px;
19 | 
20 | /* 圆角规范定义 */
21 | @border_radius_base: 2px;
22 | @border_radius: 4px;
23 | 
24 | /* 滚动条相关 */
25 | // 滚动条圆角
26 | @scrollbar_border_radius: 10px;
27 | // 滚动条大小
28 | @scrollbar_size: 5px;
29 | @scrollbar_bg: rgba(0, 0, 0, 0.3);
30 | @scrollbar_thumb: rgba(255, 255, 255, 0.5);
31 | 
32 | /* loading */
33 | @load_bg_color: rgba(0, 0, 0, 0.2);
34 | 
35 | /* header */
36 | @header_bg_color: rgba(0, 0, 0, 0.3);
37 | 
38 | /* search-head */
39 | @search_bg_color: rgba(0, 0, 0, 0.2);
40 | 
41 | /* dialog 相关 */
42 | @dialog_bg_color: rgba(0, 0, 0, 0.5);
43 | @dialog_content_bg_color: rgba(0, 0, 0, 0.6);
44 | @dialog_text_color: rgba(255, 255, 255, 0.7);
45 | @dialog_line_color: rgba(0, 0, 0, 0.35);
46 | @dialog_border_radius: @border_radius;
47 | @dialog_btn_border_radius: @border_radius;
48 | @dialog_mobile_border_radius: 10px;
49 | @dialog_btn_mobile_border_radius: @border_radius_base;
50 | 
51 | /* btn 相关 */
52 | @btn_color: rgba(255, 255, 255, 0.6);
53 | @btn_color_active: #fff;
54 | @btn_border_radius: @border_radius_base;
55 | 
56 | /* 歌词高亮颜色 */
57 | @lyric_color_active: #40ce8f;
58 | 
59 | /* 进度条 */
60 | @bar_color: rgba(255, 255, 255, 0.15);
61 | @line_color: #fff;
62 | @dot_color: #fff;
63 | 
64 | /* 列表 */
65 | @list_head_line_color: rgba(255, 255, 255, 0.8);
66 | @list_item_line_color: rgba(255, 255, 255, 0.1);
67 | 
68 | /* 评论 */
69 | @comment_head_line_color: rgba(255, 255, 255, 0.8);
70 | @comment_item_line_color: rgba(255, 255, 255, 0.1);
71 | @comment_replied_line_color: rgba(255, 255, 255, 0.3);
72 | 


--------------------------------------------------------------------------------
/src/utils/axios.js:
--------------------------------------------------------------------------------
 1 | import axios from 'axios'
 2 | import Vue from 'vue'
 3 | 
 4 | const request = axios.create({
 5 |   baseURL: process.env.VUE_APP_BASE_API_URL,
 6 | })
 7 | 
 8 | request.interceptors.response.use(
 9 |   (response) => {
10 |     window.response = response
11 | 
12 |     if (response.status === 200 && response.data.code === 200) {
13 |       return response.data
14 |     }
15 |     return Promise.reject(response)
16 |   },
17 |   (error) => {
18 |     Vue.prototype.$mmToast(error.response ? error.response.data.message : error.message)
19 |     return error
20 |   },
21 | )
22 | 
23 | export default request
24 | 


--------------------------------------------------------------------------------
/src/utils/hack.js:
--------------------------------------------------------------------------------
1 | // hack for global nextTick
2 | 
3 | function noop() {}
4 | 
5 | window.MessageChannel = noop
6 | window.setImmediate = noop
7 | 


--------------------------------------------------------------------------------
/src/utils/mixin.js:
--------------------------------------------------------------------------------
 1 | import { mapGetters, mapMutations, mapActions } from 'vuex'
 2 | 
 3 | /**
 4 |  * 歌曲列表
 5 |  */
 6 | export const listMixin = {
 7 |   computed: {
 8 |     ...mapGetters(['playing', 'currentMusic']),
 9 |   },
10 |   methods: {
11 |     selectItem(item, index) {
12 |       if (item.id === this.currentMusic.id && this.playing) {
13 |         this.setPlaying(false)
14 |       } else {
15 |         this.selectPlay({
16 |           list: this.list,
17 |           index,
18 |         })
19 |       }
20 |     },
21 |     ...mapMutations({
22 |       setPlaying: 'SET_PLAYING',
23 |     }),
24 |     ...mapActions(['selectPlay']),
25 |   },
26 | }
27 | 
28 | /**
29 |  * loading状态
30 |  * @type {{data(): *, methods: {_hideLoad(): void}}}
31 |  */
32 | export const loadMixin = {
33 |   data() {
34 |     return {
35 |       mmLoadShow: true, // loading状态
36 |     }
37 |   },
38 |   methods: {
39 |     _hideLoad() {
40 |       let timer
41 |       clearTimeout(timer)
42 |       timer = setTimeout(() => {
43 |         this.mmLoadShow = false
44 |       }, 200)
45 |     },
46 |   },
47 | }
48 | 


--------------------------------------------------------------------------------
/src/utils/song.js:
--------------------------------------------------------------------------------
 1 | import { toHttps } from './util'
 2 | 
 3 | function filterSinger(singers) {
 4 |   if (!Array.isArray(singers) || !singers.length) {
 5 |     return ''
 6 |   }
 7 |   let arr = []
 8 |   singers.forEach((item) => {
 9 |     arr.push(item.name)
10 |   })
11 |   return arr.join('/')
12 | }
13 | 
14 | export class Song {
15 |   constructor({ id, name, singer, album, image, duration, url }) {
16 |     this.id = id
17 |     this.name = name
18 |     this.singer = singer
19 |     this.album = album
20 |     this.image = image
21 |     this.duration = duration
22 |     this.url = url
23 |   }
24 | }
25 | 
26 | export function createSong(music) {
27 |   const album = music.album || music.al || {}
28 |   const duration = music.duration || music.dt
29 |   return new Song({
30 |     id: music.id,
31 |     name: music.name,
32 |     singer: filterSinger(music.ar || music.artists),
33 |     album: album.name,
34 |     image: toHttps(album.picUrl) || null,
35 |     duration: duration / 1000,
36 |     url: `https://music.163.com/song/media/outer/url?id=${music.id}.mp3`,
37 |   })
38 | }
39 | 
40 | // 歌曲数据格式化
41 | export function formatSongs(list) {
42 |   const Songs = []
43 |   list.forEach((item) => {
44 |     const musicData = item
45 |     if (musicData.id) {
46 |       Songs.push(createSong(musicData))
47 |     }
48 |   })
49 |   return Songs
50 | }
51 | 


--------------------------------------------------------------------------------
/src/utils/storage.js:
--------------------------------------------------------------------------------
  1 | import { MMPLAYER_CONFIG } from '@/config'
  2 | 
  3 | const STORAGE = window.localStorage
  4 | const storage = {
  5 |   get(key, data = []) {
  6 |     if (STORAGE) {
  7 |       return STORAGE.getItem(key)
  8 |         ? Array.isArray(data)
  9 |           ? JSON.parse(STORAGE.getItem(key))
 10 |           : STORAGE.getItem(key)
 11 |         : data
 12 |     }
 13 |   },
 14 |   set(key, val) {
 15 |     if (STORAGE) {
 16 |       STORAGE.setItem(key, val)
 17 |     }
 18 |   },
 19 |   clear(key) {
 20 |     if (STORAGE) {
 21 |       STORAGE.removeItem(key)
 22 |     }
 23 |   },
 24 | }
 25 | 
 26 | /**
 27 |  * 播放历史
 28 |  * @type    HISTORYLIST_KEY:key值
 29 |  *          HistoryListMAX:最大长度
 30 |  */
 31 | const HISTORYLIST_KEY = '__mmPlayer_historyList__'
 32 | const HistoryListMAX = 200
 33 | // 获取播放历史
 34 | export function getHistoryList() {
 35 |   return storage.get(HISTORYLIST_KEY)
 36 | }
 37 | 
 38 | // 更新播放历史
 39 | export function setHistoryList(music) {
 40 |   let list = storage.get(HISTORYLIST_KEY)
 41 |   const index = list.findIndex((item) => {
 42 |     return item.id === music.id
 43 |   })
 44 |   if (index === 0) {
 45 |     return list
 46 |   }
 47 |   if (index > 0) {
 48 |     list.splice(index, 1)
 49 |   }
 50 |   list.unshift(music)
 51 |   if (HistoryListMAX && list.length > HistoryListMAX) {
 52 |     list.pop()
 53 |   }
 54 |   storage.set(HISTORYLIST_KEY, JSON.stringify(list))
 55 |   return list
 56 | }
 57 | 
 58 | // 删除一条播放历史
 59 | export function removeHistoryList(music) {
 60 |   storage.set(HISTORYLIST_KEY, JSON.stringify(music))
 61 |   return music
 62 | }
 63 | 
 64 | // 清空播放历史
 65 | export function clearHistoryList() {
 66 |   storage.clear(HISTORYLIST_KEY)
 67 |   return []
 68 | }
 69 | 
 70 | /**
 71 |  * 播放模式
 72 |  * @type    MODE_KEY:key值
 73 |  *          HistoryListMAX:最大长度
 74 |  */
 75 | const MODE_KEY = '__mmPlayer_mode__'
 76 | // 获取播放模式
 77 | export function getMode() {
 78 |   return Number(storage.get(MODE_KEY, MMPLAYER_CONFIG.PLAY_MODE))
 79 | }
 80 | // 修改播放模式
 81 | export function setMode(mode) {
 82 |   storage.set(MODE_KEY, mode)
 83 |   return mode
 84 | }
 85 | 
 86 | /**
 87 |  * 网易云用户uid
 88 |  * @type USERID_KEY:key值
 89 |  */
 90 | const USERID_KEY = '__mmPlayer_userID__'
 91 | // 获取用户uid
 92 | export function getUserId() {
 93 |   return Number(storage.get(USERID_KEY, null))
 94 | }
 95 | // 修改用户uid
 96 | export function setUserId(uid) {
 97 |   storage.set(USERID_KEY, uid)
 98 |   return uid
 99 | }
100 | 
101 | /**
102 |  * 版本号
103 |  * @type VERSION_KEY:key值
104 |  */
105 | const VERSION_KEY = '__mmPlayer_version__'
106 | // 获取版本号
107 | export function getVersion() {
108 |   let version = storage.get(VERSION_KEY, null)
109 |   return Array.isArray(version) ? null : version
110 | }
111 | // 修改版本号
112 | export function setVersion(version) {
113 |   storage.set(VERSION_KEY, version)
114 |   return version
115 | }
116 | 
117 | /**
118 |  * 音量
119 |  * @type VOLUME_KEY:key值
120 |  */
121 | const VOLUME_KEY = '__mmPlayer_volume__'
122 | // 获取音量
123 | export function getVolume() {
124 |   const volume = storage.get(VOLUME_KEY, MMPLAYER_CONFIG.VOLUME)
125 |   return Number(volume)
126 | }
127 | // 修改音量
128 | export function setVolume(volume) {
129 |   storage.set(VOLUME_KEY, volume)
130 |   return volume
131 | }
132 | 


--------------------------------------------------------------------------------
/src/utils/util.js:
--------------------------------------------------------------------------------
  1 | // 随机排序数组/洗牌函数 https://github.com/lodash/lodash/blob/master/shuffle.js
  2 | function copyArray(source, array) {
  3 |   let index = -1
  4 |   const length = source.length
  5 |   array || (array = new Array(length))
  6 |   while (++index < length) {
  7 |     array[index] = source[index]
  8 |   }
  9 |   return array
 10 | }
 11 | 
 12 | export const randomSortArray = function shuffle(array) {
 13 |   const length = array == null ? 0 : array.length
 14 |   if (!length) {
 15 |     return []
 16 |   }
 17 |   let index = -1
 18 |   const lastIndex = length - 1
 19 |   const result = copyArray(array)
 20 |   while (++index < length) {
 21 |     const rand = index + Math.floor(Math.random() * (lastIndex - index + 1))
 22 |     const value = result[rand]
 23 |     result[rand] = result[index]
 24 |     result[index] = value
 25 |   }
 26 |   return result
 27 | }
 28 | 
 29 | // 防抖函数
 30 | export function debounce(func, delay) {
 31 |   let timer
 32 |   return function (...args) {
 33 |     if (timer) {
 34 |       clearTimeout(timer)
 35 |     }
 36 |     timer = setTimeout(() => {
 37 |       func.apply(this, args)
 38 |     }, delay)
 39 |   }
 40 | }
 41 | 
 42 | // 补0函数
 43 | export function addZero(s) {
 44 |   return s < 10 ? '0' + s : s
 45 | }
 46 | 
 47 | // 歌词解析
 48 | const timeExp = /\[(\d{2,}):(\d{2})(?:\.(\d{2,3}))?]/g
 49 | export function parseLyric(lrc) {
 50 |   const lines = lrc.split('\n')
 51 |   const lyric = []
 52 |   for (let i = 0; i < lines.length; i++) {
 53 |     const line = lines[i]
 54 |     const result = timeExp.exec(line)
 55 |     if (!result) {
 56 |       continue
 57 |     }
 58 |     const text = line.replace(timeExp, '').trim()
 59 |     if (text) {
 60 |       lyric.push({
 61 |         time: (result[1] * 6e4 + result[2] * 1e3 + (result[3] || 0) * 1) / 1e3,
 62 |         text,
 63 |       })
 64 |     }
 65 |   }
 66 |   return lyric
 67 | }
 68 | 
 69 | // 时间格式化
 70 | export function format(value) {
 71 |   let minute = Math.floor(value / 60)
 72 |   let second = Math.floor(value % 60)
 73 |   return `${addZero(minute)}:${addZero(second)}`
 74 | }
 75 | 
 76 | /**
 77 |  * https://github.com/videojs/video.js/blob/master/src/js/utils/promise.js
 78 |  * Silence a Promise-like object.
 79 |  *
 80 |  * This is useful for avoiding non-harmful, but potentially confusing "uncaught
 81 |  * play promise" rejection error messages.
 82 |  *
 83 |  * @param  {Object} value
 84 |  *         An object that may or may not be `Promise`-like.
 85 |  */
 86 | export function isPromise(v) {
 87 |   return v !== undefined && v !== null && typeof v.then === 'function'
 88 | }
 89 | 
 90 | export function silencePromise(value) {
 91 |   if (isPromise(value)) {
 92 |     value.then(null, () => {})
 93 |   }
 94 | }
 95 | 
 96 | // 判断 string 类型
 97 | export function isString(v) {
 98 |   return typeof v === 'string'
 99 | }
100 | 
101 | // http 链接转化成 https
102 | export function toHttps(url) {
103 |   if (!isString(url)) {
104 |     return url
105 |   }
106 |   return url.replace('http://', 'https://')
107 | }
108 | 


--------------------------------------------------------------------------------
/vue.config.js:
--------------------------------------------------------------------------------
 1 | const { defineConfig } = require('@vue/cli-service')
 2 | const path = require('path')
 3 | const dayjs = require('dayjs')
 4 | 
 5 | function resolve(dir) {
 6 |   return path.join(__dirname, dir)
 7 | }
 8 | 
 9 | const isEnvProduction = process.env.NODE_ENV === 'production'
10 | 
11 | // 注入版本信息
12 | process.env.VUE_APP_VERSION = require('./package.json').version
13 | // 注入版本更新时间
14 | process.env.VUE_APP_UPDATE_TIME = dayjs().locale('zh-cn').format('YYYY-MM-DD')
15 | 
16 | module.exports = defineConfig({
17 |   publicPath: '',
18 |   chainWebpack(config) {
19 |     config.resolve.alias
20 |       .set('api', resolve('src/api'))
21 |       .set('assets', resolve('src/assets'))
22 |       .set('base', resolve('src/base'))
23 |       .set('components', resolve('src/components'))
24 |       .set('pages', resolve('src/pages'))
25 |     config.plugin('html').tap((args) => {
26 |       if (isEnvProduction) {
27 |         if (!args[0].minify) {
28 |           /* 参考 https://github.com/jantimon/html-webpack-plugin#minification */
29 |           args[0].minify = {
30 |             collapseWhitespace: true,
31 |             keepClosingSlash: true,
32 |             removeComments: true,
33 |             removeRedundantAttributes: true,
34 |             removeScriptTypeAttributes: true,
35 |             removeStyleLinkTypeAttributes: true,
36 |             useShortDoctype: true,
37 |             trimCustomFragments: true,
38 |           }
39 |         }
40 |         args[0].minify.minifyJS = true
41 |         args[0].minify.minifyCSS = true
42 |       }
43 |       return args
44 |     })
45 |   },
46 |   pluginOptions: {
47 |     'style-resources-loader': {
48 |       preProcessor: 'less',
49 |       patterns: [resolve('src/styles/var.less'), resolve('src/styles/mixin.less')],
50 |     },
51 |   },
52 | })
53 | 


--------------------------------------------------------------------------------