feat: 初始化项目
3
.browserslistrc
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
> 1%
|
||||||
|
last 2 versions
|
||||||
|
not ie <= 8
|
13
.editorconfig
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# http://editorconfig.org
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
7
.env
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# 前端实际请求的API
|
||||||
|
VUE_APP_BASE_API_URL=/api
|
||||||
|
# dev环境使用的api
|
||||||
|
VUE_APP_DEV_API_URL=http://192.168.31.208:3002
|
||||||
|
|
||||||
|
# 访客统计 id (默认不开启,设置值后展示)
|
||||||
|
VUE_APP_VISITOR_BADGE_ID=
|
39
.eslintrc.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: {
|
||||||
|
node: true,
|
||||||
|
},
|
||||||
|
parserOptions: {
|
||||||
|
parser: '@babel/eslint-parser',
|
||||||
|
},
|
||||||
|
extends: ['plugin:vue/recommended', 'prettier', 'plugin:prettier/recommended'],
|
||||||
|
rules: {
|
||||||
|
'vue/multi-word-component-names': 'off',
|
||||||
|
'vue/max-attributes-per-line': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
singleline: 10,
|
||||||
|
multiline: {
|
||||||
|
max: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'vue/singleline-html-element-content-newline': 'off',
|
||||||
|
'vue/html-self-closing': [
|
||||||
|
'warn',
|
||||||
|
{
|
||||||
|
html: {
|
||||||
|
void: 'always',
|
||||||
|
normal: 'never',
|
||||||
|
},
|
||||||
|
svg: 'never',
|
||||||
|
math: 'never',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'vue/multiline-html-element-content-newline': 'off',
|
||||||
|
'vue/name-property-casing': ['error', 'PascalCase'],
|
||||||
|
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||||
|
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||||
|
'no-sequences': 2,
|
||||||
|
},
|
||||||
|
}
|
25
.gitignore
vendored
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# lock files
|
||||||
|
yarn.lock
|
||||||
|
package-lock.json
|
||||||
|
|
||||||
|
# log files
|
||||||
|
*-debug.log
|
||||||
|
*-error.log
|
||||||
|
|
||||||
|
# compile
|
||||||
|
dist
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
4
.husky/commit-msg
Executable file
@ -0,0 +1,4 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
. "$(dirname -- "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
npx femm-verify-commit $1
|
4
.npmrc
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
auto-install-peers=true
|
||||||
|
strict-peer-dependencies=false
|
||||||
|
|
||||||
|
registry=https://registry.npmmirror.com
|
6
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"EditorConfig.EditorConfig",
|
||||||
|
"esbenp.prettier-vscode"
|
||||||
|
]
|
||||||
|
}
|
3
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"files.eol": "\n"
|
||||||
|
}
|
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2018 yigencong
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
196
README.md
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
# wangyiyun(网抑云)
|
||||||
|
|
||||||
|
> wangyiyun 是一款web端的"缝合怪"播放器,具有音乐搜索、播放、歌词显示、播放历史、查看歌曲评论、网易云用户歌单播放同步等功能,此项目是对本人毕设项目的重新修改再发布,之前的git记录太乱了,并且上传过许多乱七八糟的大文件,使得clone时会把大文件的上传记录给clone下来,从2024年1月开始,网易云开始严查NeteaseCloudMusicApi,此项目已放入私人仓库。
|
||||||
|
|
||||||
|
模仿 QQ 音乐网页版界面,采用 `flexbox` 和 `position` 布局;<br />
|
||||||
|
wangyiyun 虽然是响应式,但主要以 PC 端为主,移动端只做相应适配;<br />
|
||||||
|
本项目没有做兼容性的处理,基本上只要是现代浏览器都能访问,IE除外
|
||||||
|
|
||||||
|
- [在线演示地址](https://music.icoding.fun/)
|
||||||
|
|
||||||
|
## 免责声明
|
||||||
|
|
||||||
|
1. 本项目是一个**毕业设计项目**,旨在**只用于完成毕业设计**。
|
||||||
|
|
||||||
|
2. 本项目**不提供任何音频存储和贩卖服务**。所有音频内容均由网易云音乐的第三方 API 提供,**仅供个人学习研究使用,严禁将其用于任何商业及非法用途**,版权归原始平台所有。
|
||||||
|
|
||||||
|
3. 使用本项目造成的任何纠纷、责任或损失**由使用者自行承担**。本项目开发者不对因使用本项目而产生的任何直接或间接责任承担责任,并保留追究使用者违法行为的权利。
|
||||||
|
|
||||||
|
4. **请使用者在使用本项目时遵守相关法律法规,不得将本项目用于任何商业及非法用途**。如有违反,一切后果由使用者自负。同时,使用者应该自行承担因使用本项目而带来的风险和责任。
|
||||||
|
|
||||||
|
5. 本项目使用了网易云音乐的[第三方 API 服务](https://github.com/Binaryify/NeteaseCloudMusicApi),对于该第三方 API 服务造成的任何问题,本项目开发者不承担责任。
|
||||||
|
|
||||||
|
6. 自2024年1月开始,网易云已经将NeteaseCloudMusicApi封停,之后再也不会更新,此项目后期可能无法使用,或者改为一款本地播放源的项目。
|
||||||
|
|
||||||
|
请在使用本项目之前仔细阅读以上免责声明,并确保您已完全理解并接受其中的所有条款和条件。如果您不同意或无法遵守这些规定,请不要使用本项目。
|
||||||
|
|
||||||
|
## 安装与使用
|
||||||
|
|
||||||
|
### 检查 node 版本
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# 查看 node 版本,确保 node 版本高于 12 版本
|
||||||
|
node -v
|
||||||
|
```
|
||||||
|
|
||||||
|
### wangyiyun
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# 下载 wangyiyun
|
||||||
|
git clone https://git.icoding.fun/yigencong/wangyiyun
|
||||||
|
|
||||||
|
# 进入 wangyiyun 播放器目录
|
||||||
|
cd wangyiyun
|
||||||
|
|
||||||
|
# 安装依赖 推荐使用 pnpm
|
||||||
|
pnpm install
|
||||||
|
# 或者
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# 本地运行 wangyiyun
|
||||||
|
npm run serve
|
||||||
|
|
||||||
|
# 编译打包
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### 后台 api 服务(本地开发)
|
||||||
|
|
||||||
|
[网易云音乐 NodeJS 版 API](https://binaryify.github.io/NeteaseCloudMusicApi)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# 下载 NeteaseCloudMusicApi
|
||||||
|
git clone --depth=1 https://github.com/Binaryify/NeteaseCloudMusicApi
|
||||||
|
|
||||||
|
# 进入 NeteaseCloudMusicApi 后台服务目录
|
||||||
|
cd NeteaseCloudMusicApi
|
||||||
|
|
||||||
|
# 安装依赖
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# 运行后台 api 服务 访问 http://localhost:3000
|
||||||
|
node app.js
|
||||||
|
```
|
||||||
|
|
||||||
|
### 注意点
|
||||||
|
|
||||||
|
**运行 wangyiyun 后无法获取音乐请检查后台 `api` 服务是否启动(即控制台请求报 404)**<br />
|
||||||
|
**线上部署不是直接将整个项目丢到服务器,再去运行 `npm run serve` 命令**<br />
|
||||||
|
**项目打包前 `VUE_APP_BASE_API_URL` 必须改后台 `api` 服务地址为线上地址,不能是本地地址**
|
||||||
|
|
||||||
|
### docker一键化部署
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker pull yigencong/wangyiyun:v1.0
|
||||||
|
docker run -d -p 3001:3001 --name wangyiyun yigencong/wangyiyun:v1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
- [Vue Cli](https://cli.vuejs.org/zh/) Vue 脚手架工具
|
||||||
|
- [Vue 2.x](https://v2.cn.vuejs.org/) 核心框架
|
||||||
|
- [Vue Router](https://router.vuejs.org/zh/) 页面路由
|
||||||
|
- [Vuex](https://vuex.vuejs.org/zh/) 状态管理
|
||||||
|
- ES6 (JavaScript 语言的下一代标准)
|
||||||
|
- Less(CSS 预处理器)
|
||||||
|
- Axios(网络请求)
|
||||||
|
- FastClick(解决移动端 300ms 点击延迟)
|
||||||
|
|
||||||
|
## 项目结构目录图(使用 tree 生成)
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>展开查看</summary>
|
||||||
|
<pre><code>
|
||||||
|
├── public // 静态资源目录
|
||||||
|
│ └─index.html // 入口 html 文件
|
||||||
|
├── screenshots // 项目截图
|
||||||
|
├── src // 项目源码目录
|
||||||
|
│ ├── api // 数据交互目录
|
||||||
|
│ │ └── index.js // 获取数据
|
||||||
|
│ ├── assets // 资源目录
|
||||||
|
│ │ └── background // 启动背景图目录
|
||||||
|
│ │ └── img // 静态图片目录
|
||||||
|
│ ├── base // 公共基础组件目录
|
||||||
|
│ │ ├── wyy-dialog
|
||||||
|
│ │ │ └── wyy-dialog.vue // 对话框组件
|
||||||
|
│ │ ├── wyy-icon
|
||||||
|
│ │ │ └── wyy-icon.vue // icon 组件
|
||||||
|
│ │ ├── wyy-loading
|
||||||
|
│ │ │ └── wyy-loading.vue // 加载动画组件
|
||||||
|
│ │ ├── wyy-no-result
|
||||||
|
│ │ │ └── wyy-no-result.vue // 暂无数据提示组件
|
||||||
|
│ │ ├── wyy-progress
|
||||||
|
│ │ │ └── wyy-progress.vue // 进度条拖动组件
|
||||||
|
│ │ └── wyy-toast
|
||||||
|
│ │ ├── index.js // wyy-toast 组件插件化配置
|
||||||
|
│ │ └── wyy-toast.vue // 弹出层提示组件
|
||||||
|
│ ├── components // 公共项目组件目录
|
||||||
|
│ │ ├── lyric
|
||||||
|
│ │ │ └── lyric // 歌词和封面组件
|
||||||
|
│ │ └── wyy-header
|
||||||
|
│ │ │ └── wyy-header.vue // 头部组件
|
||||||
|
│ │ ├── music-btn
|
||||||
|
│ │ │ └── music-btn.vue // 按钮组件
|
||||||
|
│ │ ├── music-list
|
||||||
|
│ │ │ └── music-list.vue // 列表组件
|
||||||
|
│ │ └── volume
|
||||||
|
│ │ └── volume.vue // 音量控制组件
|
||||||
|
│ ├── pages // 页面组件目录
|
||||||
|
│ │ ├── comment
|
||||||
|
│ │ │ └── comment.vue // 评论
|
||||||
|
│ │ ├── details
|
||||||
|
│ │ │ └── details.vue // 排行榜详情
|
||||||
|
│ │ ├── historyList
|
||||||
|
│ │ │ └── historyList.vue // 我听过的(播放历史)
|
||||||
|
│ │ ├── playList
|
||||||
|
│ │ │ └── playList.vue // 正在播放
|
||||||
|
│ │ ├── search
|
||||||
|
│ │ │ └── search.vue // 搜索
|
||||||
|
│ │ ├── topList
|
||||||
|
│ │ │ └── topList.vue // 排行榜页面
|
||||||
|
│ │ ├── userList
|
||||||
|
│ │ │ └── userList.vue // 我的歌单
|
||||||
|
│ │ ├── wangyiyun.js // 播放器事相关件绑定
|
||||||
|
│ │ └── music.vue // 播放器主页面
|
||||||
|
│ ├── router
|
||||||
|
│ │ └── index.js // 路由配置
|
||||||
|
│ ├── store // vuex 的状态管理
|
||||||
|
│ │ ├── actions.js // 配置 actions
|
||||||
|
│ │ ├── getters.js // 配置 getters
|
||||||
|
│ │ ├── index.js // 引用 vuex,创建 store
|
||||||
|
│ │ ├── mutation-types.js // 定义常量 mutations 名
|
||||||
|
│ │ ├── mutations.js // 配置 mutations
|
||||||
|
│ │ └── state.js // 配置 state
|
||||||
|
│ ├── styles // 样式文件目录
|
||||||
|
│ │ ├── index.less // wangyiyun 相关基础样式
|
||||||
|
│ │ ├── mixin.less // 样式混合
|
||||||
|
│ │ ├── reset.less // 样式重置
|
||||||
|
│ │ └── var.less // 样式变量(字体大小、字体颜色、背景颜色)
|
||||||
|
│ ├── js // 数据交互目录
|
||||||
|
│ │ ├── axios.js // axios 简单封装
|
||||||
|
│ │ ├── hack.js // 修改 nextTick
|
||||||
|
│ │ ├── mixin.js // 组件混合
|
||||||
|
│ │ ├── song.js // 数据处理
|
||||||
|
│ │ ├── storage.js // localStorage 配置
|
||||||
|
│ │ └── util.js // 公用 js 方法
|
||||||
|
│ ├── App.vue // 根组件
|
||||||
|
│ ├── config.js // 配置文件(播放器默认配置、版本号等)
|
||||||
|
│ └── main.js // 入口主文件
|
||||||
|
└── vue.config.js // vue-cli 配置文件
|
||||||
|
|
||||||
|
</code></pre>
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## 功能与界面
|
||||||
|
|
||||||
|
- 播放器
|
||||||
|
- 快捷键操作
|
||||||
|
- 歌词滚动
|
||||||
|
- 正在播放
|
||||||
|
- 排行榜
|
||||||
|
- 歌单详情
|
||||||
|
- 搜索
|
||||||
|
- 播放历史
|
||||||
|
- 查看评论
|
||||||
|
- 同步网易云歌单
|
3
babel.config.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
module.exports = {
|
||||||
|
presets: ['@vue/cli-plugin-babel/preset'],
|
||||||
|
}
|
34
jsconfig.json
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"baseUrl": "./",
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"src/*"
|
||||||
|
],
|
||||||
|
"api/*": [
|
||||||
|
"src/api/*"
|
||||||
|
],
|
||||||
|
"assets/*": [
|
||||||
|
"src/assets/*"
|
||||||
|
],
|
||||||
|
"base/*": [
|
||||||
|
"src/base/*"
|
||||||
|
],
|
||||||
|
"components/*": [
|
||||||
|
"src/components/*"
|
||||||
|
],
|
||||||
|
"pages/*": [
|
||||||
|
"src/pages/*"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"lib": [
|
||||||
|
"esnext",
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"scripthost"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
53
package.json
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"name": "wangyiyun",
|
||||||
|
"version": "1.8.3",
|
||||||
|
"private": true,
|
||||||
|
"description": "Online music player",
|
||||||
|
"author": "yigencong <yigencong@yahoo.com>",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://git.icoding.fun/yigencong/wangyiyun/issues"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://git.icoding.fun/yigencong/wangyiyun"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vue-cli-service serve",
|
||||||
|
"serve": "vue-cli-service serve",
|
||||||
|
"build": "vue-cli-service build",
|
||||||
|
"lint": "vue-cli-service lint",
|
||||||
|
"prepare": "husky install"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.6.2",
|
||||||
|
"core-js": "^3.33.3",
|
||||||
|
"fastclick": "^1.0.6",
|
||||||
|
"vue": "^2.6.14",
|
||||||
|
"vue-lazyload": "^1.3.4",
|
||||||
|
"vue-router": "^3.5.1",
|
||||||
|
"vuex": "^3.6.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.23.5",
|
||||||
|
"@babel/eslint-parser": "^7.23.3",
|
||||||
|
"@femm/prettier": "^1.1.0",
|
||||||
|
"@femm/verify-commit": "^1.0.1",
|
||||||
|
"@vue/cli-plugin-babel": "~5.0.8",
|
||||||
|
"@vue/cli-plugin-eslint": "~5.0.8",
|
||||||
|
"@vue/cli-service": "~5.0.8",
|
||||||
|
"dayjs": "^1.11.10",
|
||||||
|
"eslint": "^7.32.0",
|
||||||
|
"eslint-config-prettier": "^8.5.0",
|
||||||
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
|
"eslint-plugin-vue": "^8.7.1",
|
||||||
|
"husky": "^8.0.3",
|
||||||
|
"less": "^4.2.0",
|
||||||
|
"less-loader": "^8.0.0",
|
||||||
|
"prettier": "^2.8.1",
|
||||||
|
"style-resources-loader": "^1.5.0",
|
||||||
|
"vue-cli-plugin-style-resources-loader": "^0.1.5",
|
||||||
|
"vue-template-compiler": "^2.6.14"
|
||||||
|
},
|
||||||
|
"prettier": "@femm/prettier"
|
||||||
|
}
|
7243
pnpm-lock.yaml
generated
Normal file
5
postcss.config.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
BIN
public/favicon.ico
Executable file
After Width: | Height: | Size: 853 B |
BIN
public/img/warn.png
Normal file
After Width: | Height: | Size: 4.0 KiB |
162
public/index.html
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="renderer" content="webkit" />
|
||||||
|
<meta name="force-rendering" content="webkit" />
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width,initial-scale=1,maximum-scale=1,minimum-scale=1,user-scalable=no"
|
||||||
|
/>
|
||||||
|
<title>网抑云 在线音乐播放器</title>
|
||||||
|
<meta
|
||||||
|
name="keywords"
|
||||||
|
content="网抑云,播放器,在线音乐,在线播放器,音乐播放器,在线音乐播放器,wangyiyun 在线音乐播放器"
|
||||||
|
/>
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="网抑云 是由yigencong开源的一款在线音乐播放器,具有音乐搜索、播放、歌词显示、播放历史、查看歌曲评论、网易云用户歌单播放同步等功能"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="//at.alicdn.com/t/font_1367495_eza6utwbiqn.css"
|
||||||
|
/>
|
||||||
|
<style type="text/css">
|
||||||
|
noscript {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 1996520;
|
||||||
|
background: #fff;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 34px;
|
||||||
|
line-height: 100px;
|
||||||
|
}
|
||||||
|
#appLoading {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 1996;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
font-size: 20px;
|
||||||
|
background: rgba(255, 255, 255, 1);
|
||||||
|
}
|
||||||
|
#appLoading.removeAnimate {
|
||||||
|
animation: removeAnimate 0.3s 0.5s 1 both;
|
||||||
|
}
|
||||||
|
#appLoading .loader {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 5em;
|
||||||
|
height: 5em;
|
||||||
|
transform: translate(-50%, -50%) rotate(165deg);
|
||||||
|
}
|
||||||
|
#appLoading .loader::before,
|
||||||
|
#appLoading .loader::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
display: block;
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
border-radius: 0.5em;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
#appLoading .loader::before {
|
||||||
|
animation: before 2s infinite;
|
||||||
|
}
|
||||||
|
#appLoading .loader::after {
|
||||||
|
animation: after 2s infinite;
|
||||||
|
}
|
||||||
|
@keyframes before {
|
||||||
|
0% {
|
||||||
|
width: 1em;
|
||||||
|
box-shadow: 2em -1em rgba(225, 20, 98, 0.75),
|
||||||
|
-2em 1em rgba(111, 202, 220, 0.75);
|
||||||
|
}
|
||||||
|
35% {
|
||||||
|
width: 5em;
|
||||||
|
box-shadow: 0 -1em rgba(225, 20, 98, 0.75),
|
||||||
|
0 1em rgba(111, 202, 220, 0.75);
|
||||||
|
}
|
||||||
|
70% {
|
||||||
|
width: 1em;
|
||||||
|
box-shadow: -2em -1em rgba(225, 20, 98, 0.75),
|
||||||
|
2em 1em rgba(111, 202, 220, 0.75);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
box-shadow: 2em -1em rgba(225, 20, 98, 0.75),
|
||||||
|
-2em 1em rgba(111, 202, 220, 0.75);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes after {
|
||||||
|
0% {
|
||||||
|
height: 1em;
|
||||||
|
box-shadow: 1em 2em rgba(61, 184, 143, 0.75),
|
||||||
|
-1em -2em rgba(233, 169, 32, 0.75);
|
||||||
|
}
|
||||||
|
35% {
|
||||||
|
height: 5em;
|
||||||
|
box-shadow: 1em 0 rgba(61, 184, 143, 0.75),
|
||||||
|
-1em 0 rgba(233, 169, 32, 0.75);
|
||||||
|
}
|
||||||
|
70% {
|
||||||
|
height: 1em;
|
||||||
|
box-shadow: 1em -2em rgba(61, 184, 143, 0.75),
|
||||||
|
-1em 2em rgba(233, 169, 32, 0.75);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
box-shadow: 1em 2em rgba(61, 184, 143, 0.75),
|
||||||
|
-1em -2em rgba(233, 169, 32, 0.75);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes removeAnimate {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
;(function () {
|
||||||
|
if (!!window.ActiveXObject || 'ActiveXObject' in window) {
|
||||||
|
window.location = './prompt.html'
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
</script>
|
||||||
|
<% if ( NODE_ENV === 'production' ) { %>
|
||||||
|
<script>
|
||||||
|
var _hmt = _hmt || []
|
||||||
|
window._hmt = _hmt
|
||||||
|
;(function () {
|
||||||
|
var hm = document.createElement('script')
|
||||||
|
hm.src = 'https://hm.baidu.com/hm.js?71e62b6d09afa9deac7bfa5c60ad06dd'
|
||||||
|
var s = document.getElementsByTagName('script')[0]
|
||||||
|
s.parentNode.insertBefore(hm, s)
|
||||||
|
})()
|
||||||
|
</script>
|
||||||
|
<% } %>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>
|
||||||
|
网抑云 在线音乐播放器<br />为了更好的体验请开启 script
|
||||||
|
</noscript>
|
||||||
|
<div id="appLoading">
|
||||||
|
<div class="loader"></div>
|
||||||
|
</div>
|
||||||
|
<div id="wangyiyun">
|
||||||
|
网抑云
|
||||||
|
是由yigencong开源的一款在线音乐播放器,具有音乐搜索、播放、歌词显示、播放历史、查看歌曲评论、网易云用户歌单播放同步等功能
|
||||||
|
</div>
|
||||||
|
<!-- built files will be auto injected -->
|
||||||
|
</body>
|
||||||
|
</html>
|
31
public/prompt.html
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="robots" content="noindex,nofollow"/>
|
||||||
|
<title>wangyiyun | 温馨提示</title>
|
||||||
|
<style>
|
||||||
|
body{font-size: 14px;font-family: 'helvetica neue',tahoma,arial,'hiragino sans gb','microsoft yahei','Simsun',sans-serif; background-color:#fff; color:#808080;}
|
||||||
|
.wrap{margin:200px auto;width:510px;}
|
||||||
|
td{text-align:left; padding:2px 10px;}
|
||||||
|
td.header{font-size:22px; padding-bottom:10px; color:#000;}
|
||||||
|
td.check-info{padding-top:20px;}
|
||||||
|
a{color:#328ce5; text-decoration:none;}
|
||||||
|
a:hover{text-decoration:underline;}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="5"><img src="./img/warn.png"></td>
|
||||||
|
<td class="header">wangyiyun | 温馨提示</td>
|
||||||
|
</tr>
|
||||||
|
<tr><td></td></tr>
|
||||||
|
<tr><td>很抱歉!为了更好的体验,本站限制以下浏览器访问:</td></tr>
|
||||||
|
<tr><td>IE浏览器和使用IE内核的浏览器</td></tr>
|
||||||
|
<tr><td>解决办法:下载其他主流浏览器或者切换浏览器内核为极速内核</td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
BIN
screenshots/1.jpg
Normal file
After Width: | Height: | Size: 378 KiB |
BIN
screenshots/2.jpg
Normal file
After Width: | Height: | Size: 598 KiB |
BIN
screenshots/3.jpg
Normal file
After Width: | Height: | Size: 392 KiB |
BIN
screenshots/4.jpg
Normal file
After Width: | Height: | Size: 658 KiB |
BIN
screenshots/5.jpg
Normal file
After Width: | Height: | Size: 376 KiB |
BIN
screenshots/6.jpg
Normal file
After Width: | Height: | Size: 414 KiB |
BIN
screenshots/7.jpg
Normal file
After Width: | Height: | Size: 845 KiB |
BIN
screenshots/8.jpg
Normal file
After Width: | Height: | Size: 912 KiB |
109
src/App.vue
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
<template>
|
||||||
|
<div id="app">
|
||||||
|
<!--主体-->
|
||||||
|
<wyy-header />
|
||||||
|
<router-view />
|
||||||
|
<!--更新说明-->
|
||||||
|
<wyy-dialog ref="versionDialog" type="alert" head-text="更新提示" :body-text="versionInfo" />
|
||||||
|
<!--播放器-->
|
||||||
|
<audio ref="wangyiyun"></audio>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapMutations, mapActions } from 'vuex'
|
||||||
|
import { getPlaylistDetail } from 'api'
|
||||||
|
import { WYYPLAYER_CONFIG, VERSION } from '@/config'
|
||||||
|
import WyyHeader from 'components/wyy-header/wyy-header'
|
||||||
|
import WyyDialog from 'base/wyy-dialog/wyy-dialog'
|
||||||
|
import { getVersion, setVersion } from '@/utils/storage'
|
||||||
|
|
||||||
|
const VERSION_INFO = `<div class="wyy-dialog-text text-left">
|
||||||
|
版本号:V1.0 2024-2-20<br/>
|
||||||
|
重要通知:binaryify/netease_cloud_music_api<br/>
|
||||||
|
已被网易和谐,以后不再维护和更新,下个版本会改为nas的web端播放器。
|
||||||
|
docker镜像获取方式 docker pull yigencong/wangyiyun:v1.0(当前版本)
|
||||||
|
</div>`
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'App',
|
||||||
|
components: {
|
||||||
|
WyyHeader,
|
||||||
|
WyyDialog,
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
// 设置版本更新信息
|
||||||
|
this.versionInfo = VERSION_INFO
|
||||||
|
|
||||||
|
// 获取正在播放列表
|
||||||
|
getPlaylistDetail(WYYPLAYER_CONFIG.PLAYLIST_ID).then((playlist) => {
|
||||||
|
const list = playlist.tracks.slice(0, 100)
|
||||||
|
this.setPlaylist({ list })
|
||||||
|
})
|
||||||
|
|
||||||
|
// 设置title
|
||||||
|
let OriginTitile = document.title
|
||||||
|
let titleTime
|
||||||
|
document.addEventListener('visibilitychange', function () {
|
||||||
|
if (document.hidden) {
|
||||||
|
document.title = '网抑云需要你点歌!'
|
||||||
|
clearTimeout(titleTime)
|
||||||
|
} else {
|
||||||
|
document.title = '点首歌吧!'
|
||||||
|
titleTime = setTimeout(function () {
|
||||||
|
document.title = OriginTitile
|
||||||
|
}, 2000)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 设置audio元素
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.setAudioele(this.$refs.wangyiyun)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 首次加载完成后移除动画
|
||||||
|
let loadDOM = document.querySelector('#appLoading')
|
||||||
|
if (loadDOM) {
|
||||||
|
const animationendFunc = function () {
|
||||||
|
loadDOM.removeEventListener('animationend', animationendFunc)
|
||||||
|
loadDOM.removeEventListener('webkitAnimationEnd', animationendFunc)
|
||||||
|
document.body.removeChild(loadDOM)
|
||||||
|
loadDOM = null
|
||||||
|
const version = getVersion()
|
||||||
|
if (version !== null) {
|
||||||
|
setVersion(VERSION)
|
||||||
|
if (version !== VERSION) {
|
||||||
|
this.$refs.versionDialog.show()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setVersion(VERSION)
|
||||||
|
this.$refs.versionDialog.show()
|
||||||
|
}
|
||||||
|
}.bind(this)
|
||||||
|
loadDOM.addEventListener('animationend', animationendFunc)
|
||||||
|
loadDOM.addEventListener('webkitAnimationEnd', animationendFunc)
|
||||||
|
loadDOM.classList.add('removeAnimate')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapMutations({
|
||||||
|
setAudioele: 'SET_AUDIOELE',
|
||||||
|
}),
|
||||||
|
...mapActions(['setPlaylist']),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
#app {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
color: @text_color;
|
||||||
|
font-size: @font_size_medium;
|
||||||
|
|
||||||
|
audio {
|
||||||
|
position: fixed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
119
src/api/index.js
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
import axios from '@/utils/axios'
|
||||||
|
import { DEFAULT_LIMIT } from '@/config'
|
||||||
|
import { formatSongs } from '@/utils/song'
|
||||||
|
|
||||||
|
// 排行榜列表
|
||||||
|
export function getToplistDetail() {
|
||||||
|
return axios.get('/toplist/detail')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 推荐歌单
|
||||||
|
export function getPersonalized() {
|
||||||
|
return axios.get('/personalized')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 歌单详情
|
||||||
|
export function getPlaylistDetail(id) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
axios
|
||||||
|
.get('/playlist/detail', {
|
||||||
|
params: { id },
|
||||||
|
})
|
||||||
|
.then(({ playlist }) => playlist || {})
|
||||||
|
.then((playlist) => {
|
||||||
|
const { trackIds, tracks } = playlist
|
||||||
|
if (!Array.isArray(trackIds)) {
|
||||||
|
reject(new Error('获取歌单详情失败'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 过滤完整歌单 如排行榜
|
||||||
|
if (tracks.length === trackIds.length) {
|
||||||
|
playlist.tracks = formatSongs(playlist.tracks)
|
||||||
|
resolve(playlist)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 限制歌单详情最大 500
|
||||||
|
const ids = trackIds
|
||||||
|
.slice(0, 500)
|
||||||
|
.map((v) => v.id)
|
||||||
|
.toString()
|
||||||
|
getMusicDetail(ids).then(({ songs }) => {
|
||||||
|
playlist.tracks = formatSongs(songs)
|
||||||
|
resolve(playlist)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
export function search(keywords, page = 0, limit = DEFAULT_LIMIT) {
|
||||||
|
return axios.get('/search', {
|
||||||
|
params: {
|
||||||
|
offset: page * limit,
|
||||||
|
limit: limit,
|
||||||
|
keywords,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 热搜
|
||||||
|
export function searchHot() {
|
||||||
|
return axios.get('/search/hot')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户歌单详情
|
||||||
|
export function getUserPlaylist(uid) {
|
||||||
|
return axios.get('/user/playlist', {
|
||||||
|
params: {
|
||||||
|
uid,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取歌曲详情
|
||||||
|
export function getMusicDetail(ids) {
|
||||||
|
return axios.get('/song/detail', {
|
||||||
|
params: {
|
||||||
|
ids,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取音乐是否可以用
|
||||||
|
export function getCheckMusic(id) {
|
||||||
|
return axios.get('/check/music', {
|
||||||
|
params: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取音乐地址
|
||||||
|
export function getMusicUrl(id) {
|
||||||
|
return axios.get('/song/url', {
|
||||||
|
params: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取歌词
|
||||||
|
export function getLyric(id) {
|
||||||
|
const url = '/lyric'
|
||||||
|
return axios.get(url, {
|
||||||
|
params: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取音乐评论
|
||||||
|
export function getComment(id, page, limit = DEFAULT_LIMIT) {
|
||||||
|
return axios.get('/comment/music', {
|
||||||
|
params: {
|
||||||
|
offset: page * limit,
|
||||||
|
limit: limit,
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
BIN
src/assets/background/bg-1.jpg
Normal file
After Width: | Height: | Size: 33 KiB |
BIN
src/assets/background/bg-2.jpg
Normal file
After Width: | Height: | Size: 46 KiB |
BIN
src/assets/background/bg-3.jpg
Normal file
After Width: | Height: | Size: 38 KiB |
BIN
src/assets/img/album_cover_player.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
src/assets/img/default.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
BIN
src/assets/img/player_cover.png
Normal file
After Width: | Height: | Size: 5.2 KiB |
BIN
src/assets/img/wave.gif
Normal file
After Width: | Height: | Size: 622 B |
224
src/base/wyy-dialog/wyy-dialog.vue
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
<template>
|
||||||
|
<!--对话框-->
|
||||||
|
<transition name="wyy-dialog-fade">
|
||||||
|
<div v-show="dialogShow" class="wyy-dialog-box">
|
||||||
|
<div class="wyy-dialog-wrapper">
|
||||||
|
<div class="wyy-dialog-content">
|
||||||
|
<div class="wyy-dialog-head" v-text="headText"></div>
|
||||||
|
<slot>
|
||||||
|
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||||
|
<div class="wyy-dialog-text" v-html="bodyText"></div>
|
||||||
|
</slot>
|
||||||
|
<div class="wyy-dialog-btns">
|
||||||
|
<div
|
||||||
|
v-if="dialogType !== 'alert'"
|
||||||
|
class="wyy-btn-cancel"
|
||||||
|
@click="cancel"
|
||||||
|
v-text="cancelBtnText"
|
||||||
|
></div>
|
||||||
|
<slot name="btn"></slot>
|
||||||
|
<div class="wyy-btn-confirm" @click="confirm" v-text="confirmBtnText"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'WyyDialog',
|
||||||
|
props: {
|
||||||
|
// type:confirm、alert
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: 'confirm',
|
||||||
|
},
|
||||||
|
// 标题文本
|
||||||
|
headText: {
|
||||||
|
type: String,
|
||||||
|
default: '提示',
|
||||||
|
},
|
||||||
|
// 内容文本(可以是html)
|
||||||
|
bodyText: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
// 取消按钮文本
|
||||||
|
cancelBtnText: {
|
||||||
|
type: String,
|
||||||
|
default: '取消',
|
||||||
|
},
|
||||||
|
// 确定按钮文本
|
||||||
|
confirmBtnText: {
|
||||||
|
type: String,
|
||||||
|
default: '确定',
|
||||||
|
},
|
||||||
|
// Dialog 是否插入至 body 元素下
|
||||||
|
appendToBody: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
dialogShow: false, // 是否显示对话框
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
dialogType() {
|
||||||
|
return this.type.toLowerCase()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
dialogShow(val) {
|
||||||
|
if (val && this.appendToBody) {
|
||||||
|
document.body.appendChild(this.$el)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
if (this.dialogShow && this.appendToBody) {
|
||||||
|
document.body.appendChild(this.$el)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
destroyed() {
|
||||||
|
if (this.appendToBody && this.$el && this.$el.parentNode) {
|
||||||
|
this.$el.parentNode.removeChild(this.$el)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// 显示事件
|
||||||
|
show() {
|
||||||
|
this.dialogShow = true
|
||||||
|
},
|
||||||
|
// 隐藏事件
|
||||||
|
hide() {
|
||||||
|
this.dialogShow = false
|
||||||
|
},
|
||||||
|
// 取消事件
|
||||||
|
cancel() {
|
||||||
|
this.hide()
|
||||||
|
this.$emit('cancel')
|
||||||
|
},
|
||||||
|
// 确定事件
|
||||||
|
confirm() {
|
||||||
|
this.hide()
|
||||||
|
this.$emit('confirm')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
@dialog-prefix-cls: wyy-dialog;
|
||||||
|
|
||||||
|
.@{dialog-prefix-cls}-box {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 1996;
|
||||||
|
background-color: @dialog_bg_color;
|
||||||
|
user-select: none;
|
||||||
|
backdrop-filter: @backdrop_filter;
|
||||||
|
&.@{dialog-prefix-cls}-fade-enter-active {
|
||||||
|
animation: wyy-dialog-fadein 0.3s;
|
||||||
|
.@{dialog-prefix-cls}-content {
|
||||||
|
animation: wyy-dialog-zoom 0.3s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.@{dialog-prefix-cls}-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
z-index: 1996;
|
||||||
|
.@{dialog-prefix-cls}-content {
|
||||||
|
width: 420px;
|
||||||
|
border-radius: @dialog_border_radius;
|
||||||
|
background: @dialog_content_bg_color;
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
width: 270px;
|
||||||
|
border-radius: @dialog_mobile_border_radius;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.@{dialog-prefix-cls}-head {
|
||||||
|
padding: 15px;
|
||||||
|
padding-bottom: 0;
|
||||||
|
font-size: @font_size_large;
|
||||||
|
color: @text_color_active;
|
||||||
|
}
|
||||||
|
.@{dialog-prefix-cls}-text {
|
||||||
|
padding: 20px 15px;
|
||||||
|
line-height: 22px;
|
||||||
|
font-size: @font_size_medium;
|
||||||
|
color: @dialog_text_color;
|
||||||
|
}
|
||||||
|
.@{dialog-prefix-cls}-btns {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 15px 10px;
|
||||||
|
text-align: center;
|
||||||
|
color: @dialog_text_color;
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
justify-content: flex-end;
|
||||||
|
div {
|
||||||
|
display: block;
|
||||||
|
padding: 8px 15px;
|
||||||
|
border-radius: @dialog_btn_mobile_border_radius;
|
||||||
|
border: 1px solid @btn_color;
|
||||||
|
font-size: @font_size_medium;
|
||||||
|
cursor: pointer;
|
||||||
|
&:not(:nth-of-type(1)) {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
color: @text_color_active;
|
||||||
|
border: 1px solid @btn_color_active;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
& {
|
||||||
|
padding: 0;
|
||||||
|
justify-content: center;
|
||||||
|
div {
|
||||||
|
flex: 1;
|
||||||
|
line-height: 22px;
|
||||||
|
padding: 10px 0;
|
||||||
|
border-top: 1px solid @dialog_line_color;
|
||||||
|
font-size: @font_size_large;
|
||||||
|
&:not(:nth-of-type(1)) {
|
||||||
|
border-left: 1px solid @dialog_line_color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes wyy-dialog-fadein {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes wyy-dialog-zoom {
|
||||||
|
0% {
|
||||||
|
transform: scale(0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
53
src/base/wyy-icon/wyy-icon.vue
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<!-- icon 组件 -->
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'WyyIcon',
|
||||||
|
props: {
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: Number,
|
||||||
|
default: 16,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getIconCls() {
|
||||||
|
return `icon-${this.type}`
|
||||||
|
},
|
||||||
|
getIconStyle() {
|
||||||
|
return { fontSize: this.size + 'px' }
|
||||||
|
},
|
||||||
|
onClick(e) {
|
||||||
|
this.$emit('click', e)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
render() {
|
||||||
|
const Icon = (
|
||||||
|
<i
|
||||||
|
onClick={this.onClick}
|
||||||
|
class={`iconfont ${this.getIconCls()}`}
|
||||||
|
style={this.getIconStyle()}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
return Icon
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
.iconfont {
|
||||||
|
display: inline-block;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: normal;
|
||||||
|
font-variant: normal;
|
||||||
|
line-height: 1;
|
||||||
|
vertical-align: baseline;
|
||||||
|
text-transform: none;
|
||||||
|
speak: none;
|
||||||
|
/* Better Font Rendering =========== */
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
</style>
|
82
src/base/wyy-loading/wyy-loading.vue
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
<template>
|
||||||
|
<!--加载动画-->
|
||||||
|
<div v-show="value" class="wyy-loading" :style="{ backgroundColor: loadingBgColor }">
|
||||||
|
<div class="wyy-loading-content">
|
||||||
|
<svg class="circular" viewBox="25 25 50 50">
|
||||||
|
<circle class="path" cx="50" cy="50" r="20" fill="none"></circle>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'WyyLoading',
|
||||||
|
props: {
|
||||||
|
// 是否显示
|
||||||
|
value: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
// 加载动画背景颜色
|
||||||
|
loadingBgColor: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
.wyy-loading {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 1996;
|
||||||
|
background: @load_bg_color;
|
||||||
|
.wyy-loading-content {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
width: 100%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
text-align: center;
|
||||||
|
.circular {
|
||||||
|
height: 50px;
|
||||||
|
width: 50px;
|
||||||
|
animation: loading-rotate 2s linear infinite;
|
||||||
|
.path {
|
||||||
|
animation: loading-dash 1.5s ease-in-out infinite;
|
||||||
|
stroke-dasharray: 90, 150;
|
||||||
|
stroke-dashoffset: 0;
|
||||||
|
stroke-width: 2;
|
||||||
|
stroke: @text_color;
|
||||||
|
stroke-linecap: round;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//动画函数
|
||||||
|
@keyframes loading-rotate {
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes loading-dash {
|
||||||
|
0% {
|
||||||
|
stroke-dasharray: 1, 200;
|
||||||
|
stroke-dashoffset: 0;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
stroke-dasharray: 90, 150;
|
||||||
|
stroke-dashoffset: -40px;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
stroke-dasharray: 90, 150;
|
||||||
|
stroke-dashoffset: -120px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
34
src/base/wyy-no-result/wyy-no-result.vue
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<template>
|
||||||
|
<!--暂无数据提示-->
|
||||||
|
<div class="wyy-no-result">
|
||||||
|
<p class="wyy-no-result-text">{{ title }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'WyyNoResult',
|
||||||
|
props: {
|
||||||
|
// 无数据提示文本
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
.wyy-no-result {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
&-text {
|
||||||
|
margin-top: 30px;
|
||||||
|
font-size: @font_size_medium;
|
||||||
|
color: @text_color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
174
src/base/wyy-progress/wyy-progress.vue
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
<template>
|
||||||
|
<!--进度条拖动-->
|
||||||
|
<div ref="wyyProgress" class="wyyProgress" @click="barClick">
|
||||||
|
<div class="wyyProgress-bar"></div>
|
||||||
|
<div ref="wyyPercentProgress" class="wyyProgress-outer"></div>
|
||||||
|
<div ref="wyyProgressInner" class="wyyProgress-inner">
|
||||||
|
<div class="wyyProgress-dot" @mousedown="barDown" @touchstart.prevent="barDown"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const dotWidth = 10
|
||||||
|
export default {
|
||||||
|
name: 'WyyProgress',
|
||||||
|
props: {
|
||||||
|
// 进度值一
|
||||||
|
percent: {
|
||||||
|
type: [Number],
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
// 进度值二(歌曲缓冲进度用)
|
||||||
|
percentProgress: {
|
||||||
|
type: [Number],
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
move: {
|
||||||
|
status: false, // 是否可拖动
|
||||||
|
startX: 0, // 记录最开始点击的X坐标
|
||||||
|
left: 0, // 记录当前已经移动的距离
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
percent(newPercent) {
|
||||||
|
if (newPercent >= 0 && !this.move.status) {
|
||||||
|
const barWidth = this.$refs.wyyProgress.clientWidth - dotWidth
|
||||||
|
const offsetWidth = newPercent * barWidth
|
||||||
|
this.moveSilde(offsetWidth)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
percentProgress(newValue) {
|
||||||
|
let offsetWidth = this.$refs.wyyProgress.clientWidth * newValue
|
||||||
|
this.$refs.wyyPercentProgress.style.width = `${offsetWidth}px`
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.bindEvents()
|
||||||
|
const barWidth = this.$refs.wyyProgress.clientWidth - dotWidth
|
||||||
|
const offsetWidth = this.percent * barWidth
|
||||||
|
this.moveSilde(offsetWidth)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.unbindEvents()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// 添加绑定事件
|
||||||
|
bindEvents() {
|
||||||
|
document.addEventListener('mousemove', this.barMove)
|
||||||
|
document.addEventListener('mouseup', this.barUp)
|
||||||
|
|
||||||
|
document.addEventListener('touchmove', this.barMove)
|
||||||
|
document.addEventListener('touchend', this.barUp)
|
||||||
|
},
|
||||||
|
// 移除绑定事件
|
||||||
|
unbindEvents() {
|
||||||
|
document.removeEventListener('mousemove', this.barMove)
|
||||||
|
document.removeEventListener('mouseup', this.barUp)
|
||||||
|
|
||||||
|
document.removeEventListener('touchmove', this.barMove)
|
||||||
|
document.removeEventListener('touchend', this.barUp)
|
||||||
|
},
|
||||||
|
// 点击事件
|
||||||
|
barClick(e) {
|
||||||
|
let rect = this.$refs.wyyProgress.getBoundingClientRect()
|
||||||
|
let offsetWidth = Math.min(
|
||||||
|
this.$refs.wyyProgress.clientWidth - dotWidth,
|
||||||
|
Math.max(0, e.clientX - rect.left),
|
||||||
|
)
|
||||||
|
this.moveSilde(offsetWidth)
|
||||||
|
this.commitPercent(true)
|
||||||
|
},
|
||||||
|
// 鼠标按下事件
|
||||||
|
barDown(e) {
|
||||||
|
this.move.status = true
|
||||||
|
this.move.startX = e.clientX || e.touches[0].pageX
|
||||||
|
this.move.left = this.$refs.wyyProgressInner.clientWidth
|
||||||
|
},
|
||||||
|
// 鼠标/触摸移动事件
|
||||||
|
barMove(e) {
|
||||||
|
if (!this.move.status) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
e.preventDefault()
|
||||||
|
let endX = e.clientX || e.touches[0].pageX
|
||||||
|
let dist = endX - this.move.startX
|
||||||
|
let offsetWidth = Math.min(
|
||||||
|
this.$refs.wyyProgress.clientWidth - dotWidth,
|
||||||
|
Math.max(0, this.move.left + dist),
|
||||||
|
)
|
||||||
|
this.moveSilde(offsetWidth)
|
||||||
|
this.commitPercent()
|
||||||
|
},
|
||||||
|
// 鼠标/触摸释放事件
|
||||||
|
barUp(e) {
|
||||||
|
if (this.move.status) {
|
||||||
|
this.commitPercent(true)
|
||||||
|
this.move.status = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 移动滑块
|
||||||
|
moveSilde(offsetWidth) {
|
||||||
|
this.$refs.wyyProgressInner.style.width = `${offsetWidth}px`
|
||||||
|
},
|
||||||
|
// 修改 percent
|
||||||
|
commitPercent(isEnd = false) {
|
||||||
|
const { wyyProgress, wyyProgressInner } = this.$refs
|
||||||
|
const lineWidth = wyyProgress.clientWidth - dotWidth
|
||||||
|
const percent = wyyProgressInner.clientWidth / lineWidth
|
||||||
|
this.$emit(isEnd ? 'percentChangeEnd' : 'percentChange', percent)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
.wyyProgress {
|
||||||
|
position: relative;
|
||||||
|
padding: 5px;
|
||||||
|
user-select: none;
|
||||||
|
cursor: pointer;
|
||||||
|
overflow: hidden;
|
||||||
|
.wyyProgress-bar {
|
||||||
|
height: 2px;
|
||||||
|
width: 100%;
|
||||||
|
background: @bar_color;
|
||||||
|
}
|
||||||
|
.wyyProgress-outer {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 5px;
|
||||||
|
display: inline-block;
|
||||||
|
width: 0;
|
||||||
|
height: 2px;
|
||||||
|
margin-top: -1px;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
.wyyProgress-inner {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 5px;
|
||||||
|
display: inline-block;
|
||||||
|
width: 0;
|
||||||
|
height: 2px;
|
||||||
|
margin-top: -1px;
|
||||||
|
background: @line_color;
|
||||||
|
.wyyProgress-dot {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
right: -5px;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: @dot_color;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
39
src/base/wyy-toast/index.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import TempToast from './wyy-toast.vue'
|
||||||
|
|
||||||
|
let instance
|
||||||
|
let showToast = false
|
||||||
|
let time // 存储toast显示状态
|
||||||
|
const wyyToast = {
|
||||||
|
install(Vue, options = {}) {
|
||||||
|
let opt = TempToast.data() // 获取组件中的默认配置
|
||||||
|
Object.assign(opt, options) // 合并配置
|
||||||
|
Vue.prototype.$wyyToast = (message, position) => {
|
||||||
|
if (showToast) {
|
||||||
|
clearTimeout(time)
|
||||||
|
instance.vm.visible = showToast = false
|
||||||
|
document.body.removeChild(instance.vm.$el)
|
||||||
|
// return;// 如果toast还在,则不再执行
|
||||||
|
}
|
||||||
|
if (message) {
|
||||||
|
opt.message = message // 如果有传message,则使用所传的message
|
||||||
|
}
|
||||||
|
if (position) {
|
||||||
|
opt.position = position // 如果有传type,则使用所传的type
|
||||||
|
}
|
||||||
|
let TempToastConstructor = Vue.extend(TempToast)
|
||||||
|
instance = new TempToastConstructor({
|
||||||
|
data: opt,
|
||||||
|
})
|
||||||
|
instance.vm = instance.$mount()
|
||||||
|
document.body.appendChild(instance.vm.$el)
|
||||||
|
instance.vm.visible = showToast = true
|
||||||
|
|
||||||
|
time = setTimeout(function () {
|
||||||
|
instance.vm.visible = showToast = false
|
||||||
|
document.body.removeChild(instance.vm.$el)
|
||||||
|
}, opt.duration)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default wyyToast
|
75
src/base/wyy-toast/wyy-toast.vue
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
<template>
|
||||||
|
<!--弹出层提示-->
|
||||||
|
<transition name="toast-fade">
|
||||||
|
<div v-show="visible" class="wyy-toast" :class="positionClasss">
|
||||||
|
{{ message }}
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'WyyToast',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
position: 'center', // 默认显示位置
|
||||||
|
message: '', // 默认显示文本
|
||||||
|
duration: 1500, // 显示时间, 毫秒
|
||||||
|
visible: false, // 是否显示
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
positionClasss() {
|
||||||
|
return 'wyy-toast-' + this.position
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
@prefix-cls: wyy-toast;
|
||||||
|
|
||||||
|
.@{prefix-cls} {
|
||||||
|
position: fixed;
|
||||||
|
left: 50%;
|
||||||
|
z-index: 1996;
|
||||||
|
max-width: 80%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: @border_radius;
|
||||||
|
padding: 10px 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-align: center;
|
||||||
|
min-height: 40px;
|
||||||
|
line-height: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #fff;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
user-select: none;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
&&-top {
|
||||||
|
top: 10%;
|
||||||
|
}
|
||||||
|
&&-center {
|
||||||
|
top: 50%;
|
||||||
|
margin-top: -20px;
|
||||||
|
}
|
||||||
|
&&-bottom {
|
||||||
|
bottom: 10%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-fade-enter {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translate3d(-50%, -10px, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-fade-enter-active {
|
||||||
|
will-change: transform;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-fade-enter-to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate3d(-50%, 0, 0);
|
||||||
|
}
|
||||||
|
</style>
|
171
src/components/lyric/lyric.vue
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<!--封面-->
|
||||||
|
<dl class="music-info">
|
||||||
|
<dt>
|
||||||
|
<img :src="musicPicUrl" />
|
||||||
|
</dt>
|
||||||
|
<template v-if="currentMusic.id">
|
||||||
|
<dd>歌曲名:{{ currentMusic.name }}</dd>
|
||||||
|
<dd>歌手名:{{ currentMusic.singer }}</dd>
|
||||||
|
<dd>专辑名:{{ currentMusic.album }}</dd>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<dd>网抑云在线音乐播放器</dd>
|
||||||
|
<dd>
|
||||||
|
<a class="hover" target="_blank" href="https://github.com/yigencong">
|
||||||
|
<wyy-icon type="github" :size="14" />
|
||||||
|
yigencong
|
||||||
|
</a>
|
||||||
|
</dd>
|
||||||
|
</template>
|
||||||
|
</dl>
|
||||||
|
<!--歌词-->
|
||||||
|
<div ref="musicLyric" class="music-lyric">
|
||||||
|
<div class="music-lyric-items" :style="lyricTop">
|
||||||
|
<p v-if="!currentMusic.id">还没有播放音乐哦!</p>
|
||||||
|
<p v-else-if="nolyric">暂无歌词!</p>
|
||||||
|
<template v-else-if="lyric.length > 0">
|
||||||
|
<p v-for="(item, index) in lyric" :key="index" :class="{ on: lyricIndex === index }">
|
||||||
|
{{ item.text }}
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
<p v-else>歌词加载失败!</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Lyric',
|
||||||
|
props: {
|
||||||
|
// 歌词数据
|
||||||
|
lyric: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
// 是否无歌词
|
||||||
|
nolyric: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
// 当前歌词下标
|
||||||
|
lyricIndex: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
top: 0, // 歌词居中
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
musicPicUrl() {
|
||||||
|
return this.currentMusic.id
|
||||||
|
? `${this.currentMusic.image}?param=300y300`
|
||||||
|
: require('../../assets/img/player_cover.png')
|
||||||
|
},
|
||||||
|
lyricTop() {
|
||||||
|
return `transform :translate3d(0, ${-34 * (this.lyricIndex - this.top)}px, 0)`
|
||||||
|
},
|
||||||
|
...mapGetters(['currentMusic']),
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
clearTimeout(this.resizeTimer)
|
||||||
|
this.resizeTimer = setTimeout(() => this.clacTop(), 60)
|
||||||
|
})
|
||||||
|
this.$nextTick(() => this.clacTop())
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// 计算歌词居中的 top值
|
||||||
|
clacTop() {
|
||||||
|
const dom = this.$refs.musicLyric
|
||||||
|
const { display = '' } = window.getComputedStyle(dom)
|
||||||
|
if (display === 'none') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const height = dom.offsetHeight
|
||||||
|
this.top = Math.floor(height / 34 / 2)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.music-info {
|
||||||
|
padding-bottom: 20px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: @font_size_medium;
|
||||||
|
dt {
|
||||||
|
position: relative;
|
||||||
|
width: 186px;
|
||||||
|
height: 186px;
|
||||||
|
margin: 0 auto 15px;
|
||||||
|
&:after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 9px;
|
||||||
|
top: 0;
|
||||||
|
width: 201px;
|
||||||
|
height: 180px;
|
||||||
|
background: url('~assets/img/album_cover_player.png') 0 0 no-repeat;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
vertical-align: middle;
|
||||||
|
width: 186px;
|
||||||
|
height: 186px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dd {
|
||||||
|
height: 30px;
|
||||||
|
line-height: 30px;
|
||||||
|
.no-wrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*歌词部分*/
|
||||||
|
.music-lyric {
|
||||||
|
position: absolute;
|
||||||
|
top: 315px;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-align: center;
|
||||||
|
mask-image: linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
rgba(255, 255, 255, 0) 0,
|
||||||
|
rgba(255, 255, 255, 0.6) 15%,
|
||||||
|
rgba(255, 255, 255, 1) 25%,
|
||||||
|
rgba(255, 255, 255, 1) 75%,
|
||||||
|
rgba(255, 255, 255, 0.6) 85%,
|
||||||
|
rgba(255, 255, 255, 0) 100%
|
||||||
|
);
|
||||||
|
.music-lyric-items {
|
||||||
|
text-align: center;
|
||||||
|
line-height: 34px;
|
||||||
|
font-size: @font_size_small;
|
||||||
|
transform: translate3d(0, 0, 0);
|
||||||
|
transition: transform 0.6s ease-out;
|
||||||
|
.no-wrap();
|
||||||
|
.on {
|
||||||
|
color: @lyric_color_active;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当屏幕小于 960 时
|
||||||
|
@media (max-width: 960px) {
|
||||||
|
.music-info {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.music-lyric {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
67
src/components/music-btn/music-btn.vue
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
<template>
|
||||||
|
<!--选项-->
|
||||||
|
<div class="music-btn">
|
||||||
|
<router-link to="/music/playlist" tag="span">正在播放</router-link>
|
||||||
|
<router-link to="/music/toplist" tag="span">推荐</router-link>
|
||||||
|
<router-link to="/music/search" tag="span">搜索</router-link>
|
||||||
|
<router-link to="/music/userlist" tag="span">我的歌单</router-link>
|
||||||
|
<span class="show-960" @click="$emit('onClickLyric')">歌词</span>
|
||||||
|
<router-link to="/music/historylist" tag="span">我听过的</router-link>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.music-btn {
|
||||||
|
width: 100%;
|
||||||
|
height: 60px;
|
||||||
|
font-size: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow-x: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
span {
|
||||||
|
display: inline-block;
|
||||||
|
height: 40px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin-right: 8px;
|
||||||
|
padding: 0 23px;
|
||||||
|
border: 1px solid @btn_color;
|
||||||
|
color: @btn_color;
|
||||||
|
border-radius: @btn_border_radius;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 40px;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
&:nth-last-of-type(1) {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
&:hover,
|
||||||
|
&.active {
|
||||||
|
border-color: @btn_color_active;
|
||||||
|
color: @btn_color_active;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (min-width: 960px) {
|
||||||
|
span.show-960 {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 960px) {
|
||||||
|
span.show-960 {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
height: 50px;
|
||||||
|
span {
|
||||||
|
height: 35px;
|
||||||
|
padding: 0 10px;
|
||||||
|
margin-right: 6px;
|
||||||
|
line-height: 35px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
379
src/components/music-list/music-list.vue
Normal file
@ -0,0 +1,379 @@
|
|||||||
|
<template>
|
||||||
|
<!--歌曲列表-->
|
||||||
|
<div class="music-list flex-col">
|
||||||
|
<template v-if="list.length > 0">
|
||||||
|
<div class="list-item list-header">
|
||||||
|
<span class="list-name">歌曲</span>
|
||||||
|
<span class="list-artist">歌手</span>
|
||||||
|
<span v-if="isDuration" class="list-time">时长</span>
|
||||||
|
<span v-else class="list-album">专辑</span>
|
||||||
|
</div>
|
||||||
|
<div ref="listContent" class="list-content" @scroll="listScroll($event)">
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in list"
|
||||||
|
:key="item.id"
|
||||||
|
class="list-item"
|
||||||
|
:class="{ on: playing && currentMusic.id === item.id }"
|
||||||
|
@dblclick="selectItem(item, index, $event)"
|
||||||
|
>
|
||||||
|
<span class="list-num" v-text="index + 1"></span>
|
||||||
|
<div class="list-name">
|
||||||
|
<span>{{ item.name }}</span>
|
||||||
|
<div class="list-menu">
|
||||||
|
<wyy-icon
|
||||||
|
class="hover"
|
||||||
|
:type="getPlayIconType(item)"
|
||||||
|
:size="40"
|
||||||
|
@click.stop="selectItem(item, index)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="list-artist">{{ item.singer }}</span>
|
||||||
|
<span v-if="isDuration" class="list-time">
|
||||||
|
{{ item.duration % 3600 | format }}
|
||||||
|
<wyy-icon
|
||||||
|
class="hover list-menu-icon-del"
|
||||||
|
type="delete-mini"
|
||||||
|
:size="40"
|
||||||
|
@click.stop="deleteItem(index)"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span v-else class="list-album">{{ item.album }}</span>
|
||||||
|
</div>
|
||||||
|
<slot name="listBtn"></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<wyy-no-result v-else title="弄啥呢,怎么啥也没有!!!" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// import {getCheckMusic} from 'api'
|
||||||
|
import { mapGetters, mapMutations } from 'vuex'
|
||||||
|
import { format } from '@/utils/util'
|
||||||
|
import WyyNoResult from 'base/wyy-no-result/wyy-no-result'
|
||||||
|
|
||||||
|
const LIST_TYPE_ALBUM = 'album'
|
||||||
|
const LIST_TYPE_DURATION = 'duration'
|
||||||
|
const LIST_TYPE_PULLUP = 'pullup'
|
||||||
|
|
||||||
|
// 触发滚动加载的阈值
|
||||||
|
const THRESHOLD = 100
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'MusicList',
|
||||||
|
components: {
|
||||||
|
WyyNoResult,
|
||||||
|
},
|
||||||
|
filters: {
|
||||||
|
format,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
// 歌曲数据
|
||||||
|
list: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 列表类型
|
||||||
|
* album: 显示专辑栏目(默认)
|
||||||
|
* duration: 显示时长栏目
|
||||||
|
* pullup: 开启上拉加载
|
||||||
|
*/
|
||||||
|
listType: {
|
||||||
|
type: String,
|
||||||
|
default: LIST_TYPE_ALBUM,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
lockUp: true, // 是否锁定滚动加载事件,默认锁定
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isDuration() {
|
||||||
|
return this.listType === LIST_TYPE_DURATION
|
||||||
|
},
|
||||||
|
...mapGetters(['playing', 'currentMusic']),
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
list(newList, oldList) {
|
||||||
|
if (this.listType !== LIST_TYPE_PULLUP) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (newList.length !== oldList.length) {
|
||||||
|
this.lockUp = false
|
||||||
|
} else if (newList[newList.length - 1].id !== oldList[oldList.length - 1].id) {
|
||||||
|
this.lockUp = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
activated() {
|
||||||
|
this.scrollTop && this.$refs.listContent && (this.$refs.listContent.scrollTop = this.scrollTop)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// 滚动事件
|
||||||
|
listScroll(e) {
|
||||||
|
const scrollTop = e.target.scrollTop
|
||||||
|
this.scrollTop = scrollTop
|
||||||
|
if (this.listType !== LIST_TYPE_PULLUP || this.lockUp) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const { scrollHeight, offsetHeight } = e.target
|
||||||
|
if (scrollTop + offsetHeight >= scrollHeight - THRESHOLD) {
|
||||||
|
this.lockUp = true // 锁定滚动加载
|
||||||
|
this.$emit('pullUp') // 触发滚动加载事件
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 回到顶部
|
||||||
|
scrollTo() {
|
||||||
|
this.$refs.listContent.scrollTop = 0
|
||||||
|
},
|
||||||
|
// 播放暂停事件
|
||||||
|
selectItem(item, index, e) {
|
||||||
|
if (e && /list-menu-icon-del/.test(e.target.className)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (this.currentMusic.id && item.id === this.currentMusic.id) {
|
||||||
|
this.setPlaying(!this.playing)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为了修复 safari、 ios 微信、安卓 UC 无法播放问题,暂时移除接口校验直接播放
|
||||||
|
*/
|
||||||
|
this.$emit('select', item, index) // 触发点击播放事件
|
||||||
|
|
||||||
|
// getMusicUrl(item.id)
|
||||||
|
// .then(res => {
|
||||||
|
// if (!res.data.data[0].url) {
|
||||||
|
// this.$wyyToast('当前音乐无法播放,请播放其他音乐')
|
||||||
|
// } else {
|
||||||
|
// this.$emit('select', item, index)//触发点击播放事件
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// getCheckMusic(item.id)
|
||||||
|
// .then(res => {
|
||||||
|
// if (res.data.message !== 'ok') {
|
||||||
|
// this.$wyyToast('当前音乐无法播放,请播放其他音乐')
|
||||||
|
// } else {
|
||||||
|
// this.$emit('select', item, index)//触发点击播放事件
|
||||||
|
// }
|
||||||
|
// }).catch(error => {
|
||||||
|
// this.$wyyToast(error.response.data.message)
|
||||||
|
// })
|
||||||
|
},
|
||||||
|
// 获取播放状态 type
|
||||||
|
getPlayIconType({ id: itemId }) {
|
||||||
|
const {
|
||||||
|
playing,
|
||||||
|
currentMusic: { id },
|
||||||
|
} = this
|
||||||
|
return playing && id === itemId ? 'pause-mini' : 'play-mini'
|
||||||
|
},
|
||||||
|
// 删除事件
|
||||||
|
deleteItem(index) {
|
||||||
|
this.$emit('del', index) // 触发删除事件
|
||||||
|
},
|
||||||
|
...mapMutations({
|
||||||
|
setPlaying: 'SET_PLAYING',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.music-list {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-header {
|
||||||
|
border-bottom: 1px solid @list_head_line_color;
|
||||||
|
color: @text_color_active;
|
||||||
|
|
||||||
|
.list-name {
|
||||||
|
padding-left: 40px;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-no {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
color: @text_color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 50px;
|
||||||
|
border-bottom: 1px solid @list_item_line_color;
|
||||||
|
line-height: 50px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&.list-item-no {
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.on {
|
||||||
|
color: #fff;
|
||||||
|
|
||||||
|
.list-num {
|
||||||
|
font-size: 0;
|
||||||
|
background: url('~assets/img/wave.gif') no-repeat center center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.list-name {
|
||||||
|
padding-right: 80px;
|
||||||
|
|
||||||
|
.list-menu {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not([class*='list-header']):hover {
|
||||||
|
.list-name {
|
||||||
|
padding-right: 80px;
|
||||||
|
|
||||||
|
.list-menu {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-time {
|
||||||
|
font-size: 0;
|
||||||
|
|
||||||
|
.list-menu-icon-del {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-num {
|
||||||
|
display: block;
|
||||||
|
width: 30px;
|
||||||
|
margin-right: 10px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-name {
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
& > span {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 1;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
small {
|
||||||
|
margin-left: 5px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*hover菜单*/
|
||||||
|
|
||||||
|
.list-menu {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
right: 10px;
|
||||||
|
height: 40px;
|
||||||
|
font-size: 0;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-artist,
|
||||||
|
.list-album {
|
||||||
|
display: block;
|
||||||
|
width: 300px;
|
||||||
|
.no-wrap();
|
||||||
|
@media (max-width: 1440px) {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
width: 150px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-time {
|
||||||
|
display: block;
|
||||||
|
width: 60px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.list-menu-icon-del {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 0;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-btn {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 50px;
|
||||||
|
span {
|
||||||
|
padding: 5px 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
&:hover {
|
||||||
|
color: @text_color_active;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 960px) {
|
||||||
|
.list-item .list-name {
|
||||||
|
padding-right: 70px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.list-item {
|
||||||
|
.list-name .list-menu {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-artist,
|
||||||
|
.list-album {
|
||||||
|
width: 20%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.list-item {
|
||||||
|
.list-artist {
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-album,
|
||||||
|
.list-time {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
83
src/components/volume/volume.vue
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
<!-- 音量控制组件 -->
|
||||||
|
<template>
|
||||||
|
<div class="volume">
|
||||||
|
<wyy-icon
|
||||||
|
class="pointer volume-icon"
|
||||||
|
:type="getVolumeIconType()"
|
||||||
|
:size="30"
|
||||||
|
@click="handleToggleVolume"
|
||||||
|
/>
|
||||||
|
<div class="volume-progress-wrapper">
|
||||||
|
<wyy-progress
|
||||||
|
:percent="volumeProgress"
|
||||||
|
@percentChangeEnd="handleVolumeChange"
|
||||||
|
@percentChange="handleVolumeChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import WyyProgress from 'base/wyy-progress/wyy-progress'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Volume',
|
||||||
|
components: {
|
||||||
|
WyyProgress,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
volume: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
volumeProgress() {
|
||||||
|
return this.volume
|
||||||
|
},
|
||||||
|
isMute: {
|
||||||
|
get() {
|
||||||
|
return this.volumeProgress === 0
|
||||||
|
},
|
||||||
|
set(newMute) {
|
||||||
|
const volume = newMute ? 0 : this.lastVolume
|
||||||
|
if (newMute) {
|
||||||
|
this.lastVolume = this.volumeProgress
|
||||||
|
}
|
||||||
|
this.handleVolumeChange(volume)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getVolumeIconType() {
|
||||||
|
return this.isMute ? 'volume-off' : 'volume'
|
||||||
|
},
|
||||||
|
// 是否静音
|
||||||
|
handleToggleVolume() {
|
||||||
|
this.isMute = !this.isMute
|
||||||
|
},
|
||||||
|
handleVolumeChange(percent) {
|
||||||
|
this.$emit('volumeChange', percent)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.volume {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 150px;
|
||||||
|
&-icon {
|
||||||
|
margin-right: 5px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
&-progress-wrapper {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
top: 2px;
|
||||||
|
width: 36px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
247
src/components/wyy-header/wyy-header.vue
Normal file
@ -0,0 +1,247 @@
|
|||||||
|
<template>
|
||||||
|
<!--头部-->
|
||||||
|
<header class="wyy-header">
|
||||||
|
<h1 class="header">
|
||||||
|
<a href="https://git.icoding.fun/yigencong/wangyiyun" target="_blank">
|
||||||
|
网抑云 在线音乐播放器
|
||||||
|
</a>
|
||||||
|
<img
|
||||||
|
v-if="visitorBadge"
|
||||||
|
:src="visitorBadge"
|
||||||
|
alt="累计访问数"
|
||||||
|
class="visitor"
|
||||||
|
onerror="this.style.display='none'"
|
||||||
|
/>
|
||||||
|
</h1>
|
||||||
|
<dl class="user">
|
||||||
|
<template v-if="user.userId">
|
||||||
|
<router-link class="user-info" to="/music/userlist" tag="dt">
|
||||||
|
<img class="avatar" :src="`${user.avatarUrl}?param=50y50`" />
|
||||||
|
<span>{{ user.nickname }}</span>
|
||||||
|
</router-link>
|
||||||
|
<dd class="user-btn" @click="openDialog(2)">退出</dd>
|
||||||
|
</template>
|
||||||
|
<dd v-else class="user-btn" @click="openDialog(0)">登录</dd>
|
||||||
|
</dl>
|
||||||
|
<!--登录-->
|
||||||
|
<wyy-dialog
|
||||||
|
ref="loginDialog"
|
||||||
|
head-text="登录"
|
||||||
|
confirm-btn-text="登录"
|
||||||
|
cancel-btn-text="关闭"
|
||||||
|
@confirm="login"
|
||||||
|
>
|
||||||
|
<div class="wyy-dialog-text">
|
||||||
|
<input
|
||||||
|
v-model.trim="uidValue"
|
||||||
|
class="wyy-dialog-input"
|
||||||
|
type="number"
|
||||||
|
autofocus
|
||||||
|
placeholder="请输入您的网易云 UID"
|
||||||
|
@keyup.enter="login"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div slot="btn" @click="openDialog(1)">帮助</div>
|
||||||
|
</wyy-dialog>
|
||||||
|
<!--帮助-->
|
||||||
|
<wyy-dialog
|
||||||
|
ref="helpDialog"
|
||||||
|
head-text="登录帮助"
|
||||||
|
confirm-btn-text="去登录"
|
||||||
|
cancel-btn-text="关闭"
|
||||||
|
@confirm="openDialog(0)"
|
||||||
|
>
|
||||||
|
<div class="wyy-dialog-text">
|
||||||
|
<p>
|
||||||
|
1、
|
||||||
|
<a target="_blank" href="https://music.163.com">点我(https://music.163.com)</a>
|
||||||
|
打开网易云音乐官网
|
||||||
|
</p>
|
||||||
|
<p>2、点击页面右上角的“登录”</p>
|
||||||
|
<p>3、点击您的头像,进入我的主页</p>
|
||||||
|
<p>4、复制浏览器地址栏 /user/home?id= 后面的数字(网易云 UID)</p>
|
||||||
|
</div>
|
||||||
|
</wyy-dialog>
|
||||||
|
<!--退出-->
|
||||||
|
<wyy-dialog ref="outDialog" body-text="确定退出当前用户吗?" @confirm="out" />
|
||||||
|
</header>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { getUserPlaylist } from 'api'
|
||||||
|
import { mapGetters, mapActions } from 'vuex'
|
||||||
|
import WyyDialog from 'base/wyy-dialog/wyy-dialog'
|
||||||
|
import { toHttps } from '@/utils/util'
|
||||||
|
import { VISITOR_BADGE_ID } from '@/config'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'WyyHeader',
|
||||||
|
components: {
|
||||||
|
WyyDialog,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
user: {}, // 用户数据
|
||||||
|
uidValue: '', // 记录用户 UID
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
visitorBadge() {
|
||||||
|
if (VISITOR_BADGE_ID) {
|
||||||
|
return `https://visitor-badge.laobi.icu/badge?left_color=transparent&right_color=transparent&page_id=${VISITOR_BADGE_ID}`
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
},
|
||||||
|
...mapGetters(['uid']),
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.uid && this._getUserPlaylist(this.uid)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// 打开对话框
|
||||||
|
openDialog(key) {
|
||||||
|
switch (key) {
|
||||||
|
case 0:
|
||||||
|
this.$refs.loginDialog.show()
|
||||||
|
break
|
||||||
|
case 1:
|
||||||
|
this.$refs.loginDialog.hide()
|
||||||
|
this.$refs.helpDialog.show()
|
||||||
|
break
|
||||||
|
case 2:
|
||||||
|
this.$refs.outDialog.show()
|
||||||
|
break
|
||||||
|
case 3:
|
||||||
|
this.$refs.loginDialog.hide()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 退出登录
|
||||||
|
out() {
|
||||||
|
this.user = {}
|
||||||
|
this.setUid(null)
|
||||||
|
this.$wyyToast('退出成功!')
|
||||||
|
},
|
||||||
|
// 登录
|
||||||
|
login() {
|
||||||
|
if (this.uidValue === '') {
|
||||||
|
this.$wyyToast('UID 不能为空')
|
||||||
|
this.openDialog(0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.openDialog(3)
|
||||||
|
this._getUserPlaylist(this.uidValue)
|
||||||
|
},
|
||||||
|
// 获取用户数据
|
||||||
|
_getUserPlaylist(uid) {
|
||||||
|
getUserPlaylist(uid).then(({ playlist = [] }) => {
|
||||||
|
this.uidValue = ''
|
||||||
|
if (playlist.length === 0 || !playlist[0].creator) {
|
||||||
|
this.$wyyToast(`未查询找 UID 为 ${uid} 的用户信息`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const creator = playlist[0].creator
|
||||||
|
this.setUid(uid)
|
||||||
|
creator.avatarUrl = toHttps(creator.avatarUrl)
|
||||||
|
this.user = creator
|
||||||
|
setTimeout(() => {
|
||||||
|
this.$wyyToast(`${this.user.nickname} 欢迎使用 wangyiyun`)
|
||||||
|
}, 200)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
...mapActions(['setUid']),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
.wyy-header {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 60px;
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
background: @header_bg_color;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
.flex-center;
|
||||||
|
line-height: 60px;
|
||||||
|
color: @text_color_active;
|
||||||
|
font-size: @font_size_large;
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
padding-left: 15px;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
@media (max-width: 414px) {
|
||||||
|
font-size: @font_size_medium;
|
||||||
|
}
|
||||||
|
.visitor {
|
||||||
|
margin-left: 6px;
|
||||||
|
height: 20px;
|
||||||
|
@media (max-width: 414px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.user {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
right: 15px;
|
||||||
|
line-height: 30px;
|
||||||
|
text-align: right;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
&-info {
|
||||||
|
float: left;
|
||||||
|
margin-right: 15px;
|
||||||
|
cursor: pointer;
|
||||||
|
.avatar {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border-radius: 50%;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
span {
|
||||||
|
margin-left: 10px;
|
||||||
|
color: @text_color_active;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&-btn {
|
||||||
|
float: left;
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
color: @text_color_active;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
&-info {
|
||||||
|
margin-right: 10px;
|
||||||
|
span {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.wyy-dialog-text {
|
||||||
|
text-align: left;
|
||||||
|
.wyy-dialog-input {
|
||||||
|
width: 100%;
|
||||||
|
height: 40px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 0 15px;
|
||||||
|
border: 1px solid @btn_color;
|
||||||
|
outline: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: @text_color_active;
|
||||||
|
font-size: @font_size_medium;
|
||||||
|
box-shadow: 0 0 1px 0 #fff inset;
|
||||||
|
&::placeholder {
|
||||||
|
color: @text_color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: #d43c33;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
46
src/config.js
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
/* 版本号 */
|
||||||
|
export const VERSION = process.env.VUE_APP_VERSION
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 访客统计 id
|
||||||
|
* 具体使用文档 https://github.com/jwenjian/visitor-badge
|
||||||
|
*/
|
||||||
|
export const VISITOR_BADGE_ID = process.env.VUE_APP_VISITOR_BADGE_ID
|
||||||
|
|
||||||
|
/* 背景图(可引入网络图或本地静态图) */
|
||||||
|
const requireAll = (requireContext) => requireContext.keys().map(requireContext)
|
||||||
|
const BACKGROUNDS = requireAll(require.context('./assets/background', false))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 播放模式
|
||||||
|
* LIST_LOOP: 列表循环
|
||||||
|
* ORDER: 顺序播放
|
||||||
|
* RANDOM: 随机播放
|
||||||
|
* LOOP: 单曲循环
|
||||||
|
*/
|
||||||
|
export const PLAY_MODE = {
|
||||||
|
LIST_LOOP: 0,
|
||||||
|
ORDER: 1,
|
||||||
|
RANDOM: 2,
|
||||||
|
LOOP: 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 播放器默认配置
|
||||||
|
*/
|
||||||
|
export const WYYPLAYER_CONFIG = {
|
||||||
|
/**
|
||||||
|
* 默认歌单ID (正在播放列表)
|
||||||
|
* 默认为云音乐热歌榜 https://music.163.com/#/discover/toplist?id=3778678
|
||||||
|
*/
|
||||||
|
PLAYLIST_ID: 3778678,
|
||||||
|
/* 默认播放模式 */
|
||||||
|
PLAY_MODE: PLAY_MODE.LIST_LOOP,
|
||||||
|
/* 默认音量 */
|
||||||
|
VOLUME: 0.8,
|
||||||
|
/* 默认背景 */
|
||||||
|
BACKGROUND: BACKGROUNDS[Math.floor(Math.random() * BACKGROUNDS.length)],
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 默认分页数量 */
|
||||||
|
export const DEFAULT_LIMIT = 30
|
62
src/main.js
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
// The Vue build version to load with the `import` command
|
||||||
|
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
|
||||||
|
// import 'babel-polyfill'
|
||||||
|
// import '@/utils/hack'
|
||||||
|
import Vue from 'vue'
|
||||||
|
import store from './store'
|
||||||
|
import router from './router'
|
||||||
|
import App from './App'
|
||||||
|
import fastclick from 'fastclick'
|
||||||
|
import WyyToast from 'base/wyy-toast'
|
||||||
|
import Icon from 'base/wyy-icon/wyy-icon'
|
||||||
|
import VueLazyload from 'vue-lazyload'
|
||||||
|
import { VERSION } from './config'
|
||||||
|
|
||||||
|
import '@/styles/index.less'
|
||||||
|
|
||||||
|
// 优化移动端300ms点击延迟
|
||||||
|
fastclick.attach(document.body)
|
||||||
|
|
||||||
|
// 弹出层
|
||||||
|
Vue.use(WyyToast)
|
||||||
|
|
||||||
|
// icon 组件
|
||||||
|
Vue.component(Icon.name, Icon)
|
||||||
|
|
||||||
|
// 懒加载
|
||||||
|
Vue.use(VueLazyload, {
|
||||||
|
preLoad: 1,
|
||||||
|
loading: require('assets/img/default.png'),
|
||||||
|
})
|
||||||
|
|
||||||
|
// 访问版本统计
|
||||||
|
window._hmt && window._hmt.push(['_setCustomVar', 1, 'version', VERSION, 1])
|
||||||
|
|
||||||
|
const redirectList = ['/music/details', '/music/comment']
|
||||||
|
router.beforeEach((to, from, next) => {
|
||||||
|
window._hmt && to.path && window._hmt.push(['_trackPageview', '/#' + to.fullPath])
|
||||||
|
if (redirectList.includes(to.path)) {
|
||||||
|
next()
|
||||||
|
} else {
|
||||||
|
document.title =
|
||||||
|
(to.meta.title && `${to.meta.title} - 网抑云在线音乐播放器`) || 'wangyiyun在线音乐播放器'
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 版权信息
|
||||||
|
window.wangyiyun = window.wangyiyun = `欢迎使用 wangyiyun!
|
||||||
|
当前版本为:V${VERSION}
|
||||||
|
作者:yigencong
|
||||||
|
Github:https://git.icoding.fun/yigencong/wangyiyun
|
||||||
|
歌曲来源于网易云音乐 (https://music.163.com)`
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.info(`%c${window.wangyiyun}`, `color:blue`)
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-new
|
||||||
|
new Vue({
|
||||||
|
el: '#wangyiyun',
|
||||||
|
store,
|
||||||
|
router,
|
||||||
|
render: (h) => h(App),
|
||||||
|
})
|
244
src/pages/comment/comment.vue
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
<template>
|
||||||
|
<!--评论-->
|
||||||
|
<div class="comment" @scroll="listScroll($event)">
|
||||||
|
<wyy-loading v-model="wyyLoadShow" />
|
||||||
|
<dl v-if="hotComments.length > 0" class="comment-list">
|
||||||
|
<!--精彩评论-->
|
||||||
|
<dt class="comment-title">精彩评论</dt>
|
||||||
|
<dd v-for="item in hotComments" :key="item.commentId" class="comment-item">
|
||||||
|
<a target="_blank" :href="`https://music.163.com/#/user/home?id=${item.user.userId}`">
|
||||||
|
<img v-lazy="`${item.user.avatarUrl}?param=50y50`" class="comment-item-pic" />
|
||||||
|
<h2 class="comment-item-title">{{ item.user.nickname }}</h2>
|
||||||
|
</a>
|
||||||
|
<p class="comment-item-disc">{{ item.content }}</p>
|
||||||
|
<div class="comment-item-opt">
|
||||||
|
<span class="comment-opt-date">{{ item.time | format }}</span>
|
||||||
|
<span class="comment-opt-liked">
|
||||||
|
<wyy-icon type="good" />
|
||||||
|
{{ item.likedCount }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
<!--最新评论-->
|
||||||
|
<dl v-if="commentList.length > 0" class="comment-list">
|
||||||
|
<dt class="comment-title">最新评论({{ total }})</dt>
|
||||||
|
<dd v-for="item in commentList" :key="item.commentId" class="comment-item">
|
||||||
|
<a
|
||||||
|
class="comment-item-pic"
|
||||||
|
target="_blank"
|
||||||
|
:href="`https://music.163.com/#/user/home?id=${item.user.userId}`"
|
||||||
|
>
|
||||||
|
<img v-lazy="`${item.user.avatarUrl}?param=50y50`" class="cover-img" />
|
||||||
|
</a>
|
||||||
|
<h2 class="comment-item-title">
|
||||||
|
<a target="_blank" :href="`https://music.163.com/#/user/home?id=${item.user.userId}`">
|
||||||
|
{{ item.user.nickname }}
|
||||||
|
</a>
|
||||||
|
</h2>
|
||||||
|
<p class="comment-item-disc">{{ item.content }}</p>
|
||||||
|
<div
|
||||||
|
v-for="beReplied in item.beReplied"
|
||||||
|
:key="beReplied.user.userId"
|
||||||
|
class="comment-item-replied"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
:href="`https://music.163.com/#/user/home?id=${beReplied.user.userId}`"
|
||||||
|
>
|
||||||
|
{{ beReplied.user.nickname }}
|
||||||
|
</a>
|
||||||
|
:{{ beReplied.content }}
|
||||||
|
</div>
|
||||||
|
<div class="comment-item-opt">
|
||||||
|
<span class="comment-opt-date">{{ item.time | format }}</span>
|
||||||
|
<span v-if="item.likedCount > 0" class="comment-opt-liked">
|
||||||
|
<wyy-icon type="good" />
|
||||||
|
{{ item.likedCount }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { getComment } from 'api'
|
||||||
|
import { addZero } from '@/utils/util'
|
||||||
|
import WyyLoading from 'base/wyy-loading/wyy-loading'
|
||||||
|
import { loadMixin } from '@/utils/mixin'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Comment',
|
||||||
|
components: {
|
||||||
|
WyyLoading,
|
||||||
|
},
|
||||||
|
filters: {
|
||||||
|
// 格式化时间
|
||||||
|
format(time) {
|
||||||
|
let formatTime
|
||||||
|
const date = new Date(time)
|
||||||
|
const dateObj = {
|
||||||
|
year: date.getFullYear(),
|
||||||
|
month: date.getMonth(),
|
||||||
|
date: date.getDate(),
|
||||||
|
hours: date.getHours(),
|
||||||
|
minutes: date.getMinutes(),
|
||||||
|
}
|
||||||
|
const newTime = new Date()
|
||||||
|
const diff = newTime.getTime() - time
|
||||||
|
if (newTime.getDate() === dateObj.date && diff < 60000) {
|
||||||
|
formatTime = '刚刚'
|
||||||
|
} else if (newTime.getDate() === dateObj.date && diff > 60000 && diff < 3600000) {
|
||||||
|
formatTime = `${Math.floor(diff / 60000)}分钟前`
|
||||||
|
} else if (newTime.getDate() === dateObj.date && diff > 3600000 && diff < 86400000) {
|
||||||
|
formatTime = `${addZero(dateObj.hours)}:${addZero(dateObj.minutes)}`
|
||||||
|
} else if (newTime.getDate() !== dateObj.date && diff < 86400000) {
|
||||||
|
formatTime = `昨天${addZero(dateObj.hours)}:${addZero(dateObj.minutes)}`
|
||||||
|
} else if (newTime.getFullYear() === dateObj.year) {
|
||||||
|
formatTime = `${dateObj.month + 1}月${dateObj.date}日`
|
||||||
|
} else {
|
||||||
|
formatTime = `${dateObj.year}年${dateObj.month + 1}月${dateObj.date}日`
|
||||||
|
}
|
||||||
|
return formatTime
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mixins: [loadMixin],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
lockUp: true, // 是否锁定滚动加载事件,默认锁定
|
||||||
|
page: 0, // 分页
|
||||||
|
hotComments: [], // 精彩评论
|
||||||
|
commentList: [], // 最新评论
|
||||||
|
total: null, // 评论总数
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
commentList(newList, oldList) {
|
||||||
|
if (newList.length !== oldList.length) {
|
||||||
|
this.lockUp = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.initData()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// 初始化数据
|
||||||
|
initData() {
|
||||||
|
getComment(this.$route.params.id, this.page).then(
|
||||||
|
(res) => {
|
||||||
|
this.hotComments = res.hotComments
|
||||||
|
this.commentList = res.comments
|
||||||
|
this.total = res.total
|
||||||
|
this.lockUp = true
|
||||||
|
this._hideLoad()
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
this._hideLoad()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
// 列表滚动事件
|
||||||
|
listScroll(e) {
|
||||||
|
if (this.lockUp) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const { scrollTop, scrollHeight, offsetHeight } = e.target
|
||||||
|
if (scrollTop + offsetHeight >= scrollHeight - 100) {
|
||||||
|
this.lockUp = true // 锁定滚动加载
|
||||||
|
this.page += 1
|
||||||
|
this.pullUp() // 触发滚动加载事件
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 滚动加载事件
|
||||||
|
pullUp() {
|
||||||
|
getComment(this.$route.params.id, this.page).then(({ comments }) => {
|
||||||
|
this.commentList = [...this.commentList, ...comments]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.comment {
|
||||||
|
.comment-list {
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-title {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1;
|
||||||
|
margin: 0 -10px;
|
||||||
|
padding: 10px;
|
||||||
|
height: 34px;
|
||||||
|
line-height: 34px;
|
||||||
|
color: @text_color_active;
|
||||||
|
background: @header_bg_color;
|
||||||
|
backdrop-filter: @backdrop_filter;
|
||||||
|
}
|
||||||
|
.comment-item {
|
||||||
|
position: relative;
|
||||||
|
padding: 15px 0 15px 55px;
|
||||||
|
& + .comment-item {
|
||||||
|
border-top: 1px solid @comment_item_line_color;
|
||||||
|
}
|
||||||
|
&-pic {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 20px;
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
border-radius: 50%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
&-title {
|
||||||
|
height: 20px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
font-weight: 400;
|
||||||
|
.no-wrap();
|
||||||
|
color: @text_color_active;
|
||||||
|
}
|
||||||
|
&-disc {
|
||||||
|
overflow: hidden;
|
||||||
|
word-break: break-all;
|
||||||
|
word-wrap: break-word;
|
||||||
|
line-height: 25px;
|
||||||
|
text-align: justify;
|
||||||
|
color: @text_color;
|
||||||
|
img {
|
||||||
|
position: relative;
|
||||||
|
vertical-align: middle;
|
||||||
|
top: -2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&-replied {
|
||||||
|
padding: 8px 19px;
|
||||||
|
margin-top: 10px;
|
||||||
|
line-height: 20px;
|
||||||
|
border: 1px solid @comment_replied_line_color;
|
||||||
|
a {
|
||||||
|
color: @text_color_active;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&-opt {
|
||||||
|
margin-top: 10px;
|
||||||
|
line-height: 25px;
|
||||||
|
text-align: right;
|
||||||
|
overflow: hidden;
|
||||||
|
.comment-opt-date {
|
||||||
|
float: left;
|
||||||
|
line-height: 28px;
|
||||||
|
}
|
||||||
|
.comment-opt-liked {
|
||||||
|
display: inline-block;
|
||||||
|
height: 20px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
59
src/pages/details/details.vue
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
<template>
|
||||||
|
<!--歌单详情-->
|
||||||
|
<div class="details">
|
||||||
|
<wyy-loading v-model="wyyLoadShow" />
|
||||||
|
<music-list :list="list" @select="selectItem" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapActions } from 'vuex'
|
||||||
|
import { getPlaylistDetail } from 'api'
|
||||||
|
import WyyLoading from 'base/wyy-loading/wyy-loading'
|
||||||
|
import MusicList from 'components/music-list/music-list'
|
||||||
|
import { loadMixin } from '@/utils/mixin'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Detail',
|
||||||
|
components: {
|
||||||
|
WyyLoading,
|
||||||
|
MusicList,
|
||||||
|
},
|
||||||
|
mixins: [loadMixin],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
list: [], // 列表
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
// 获取歌单详情
|
||||||
|
getPlaylistDetail(this.$route.params.id)
|
||||||
|
.then((playlist) => {
|
||||||
|
document.title = `${playlist.name} - 网抑云在线音乐播放器`
|
||||||
|
this.list = playlist.tracks
|
||||||
|
this._hideLoad()
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this._hideLoad()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// 播放暂停事件
|
||||||
|
selectItem(item, index) {
|
||||||
|
this.selectPlay({
|
||||||
|
list: this.list,
|
||||||
|
index,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
...mapActions(['selectPlay']),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.details {
|
||||||
|
.music-list {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
58
src/pages/historyList/historyList.vue
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<template>
|
||||||
|
<!--我听过的(播放历史)-->
|
||||||
|
<div class="historyList">
|
||||||
|
<music-list :list="historyList" list-type="duration" @select="selectItem" @del="deleteItem">
|
||||||
|
<div slot="listBtn" class="list-btn">
|
||||||
|
<span @click="$refs.dialog.show()">清空列表</span>
|
||||||
|
</div>
|
||||||
|
</music-list>
|
||||||
|
<wyy-dialog
|
||||||
|
ref="dialog"
|
||||||
|
body-text="是否清空播放历史列表"
|
||||||
|
confirm-btn-text="清空"
|
||||||
|
@confirm="clearList"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters, mapMutations, mapActions } from 'vuex'
|
||||||
|
import MusicList from 'components/music-list/music-list'
|
||||||
|
import WyyDialog from 'base/wyy-dialog/wyy-dialog'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HistoryList',
|
||||||
|
components: {
|
||||||
|
MusicList,
|
||||||
|
WyyDialog,
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters(['historyList', 'playing', 'currentMusic']),
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// 清空列表事件
|
||||||
|
clearList() {
|
||||||
|
this.clearHistory()
|
||||||
|
this.$wyyToast('列表清空成功')
|
||||||
|
},
|
||||||
|
// 播放事件
|
||||||
|
selectItem(item, index) {
|
||||||
|
this.selectPlay({
|
||||||
|
list: this.historyList,
|
||||||
|
index,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// 删除事件
|
||||||
|
deleteItem(index) {
|
||||||
|
let list = [...this.historyList]
|
||||||
|
list.splice(index, 1)
|
||||||
|
this.removeHistory(list)
|
||||||
|
this.$wyyToast('删除成功')
|
||||||
|
},
|
||||||
|
...mapMutations({
|
||||||
|
setPlaying: 'SET_PLAYING',
|
||||||
|
}),
|
||||||
|
...mapActions(['selectPlay', 'clearHistory', 'removeHistory']),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
600
src/pages/music.vue
Normal file
@ -0,0 +1,600 @@
|
|||||||
|
<template>
|
||||||
|
<div class="music flex-col">
|
||||||
|
<div class="music-content">
|
||||||
|
<div class="music-left flex-col">
|
||||||
|
<music-btn @onClickLyric="handleOpenLyric" />
|
||||||
|
<keep-alive>
|
||||||
|
<router-view v-if="$route.meta.keepAlive" class="router-view" />
|
||||||
|
</keep-alive>
|
||||||
|
<router-view v-if="!$route.meta.keepAlive" :key="$route.path" class="router-view" />
|
||||||
|
</div>
|
||||||
|
<div class="music-right" :class="{ show: lyricVisible }">
|
||||||
|
<div class="close-lyric" @click="handleCloseLyric">关闭歌词</div>
|
||||||
|
<lyric ref="lyric" :lyric="lyric" :nolyric="nolyric" :lyric-index="lyricIndex" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--播放器-->
|
||||||
|
<div class="music-bar" :class="{ disable: !musicReady || !currentMusic.id }">
|
||||||
|
<div class="music-bar-btns">
|
||||||
|
<wyy-icon class="pointer" type="prev" :size="36" title="上一曲 Ctrl + Left" @click="prev" />
|
||||||
|
<div class="control-play pointer" title="播放暂停 Ctrl + Space" @click="play">
|
||||||
|
<wyy-icon :type="playing ? 'pause' : 'play'" :size="24" />
|
||||||
|
</div>
|
||||||
|
<wyy-icon
|
||||||
|
class="pointer"
|
||||||
|
type="next"
|
||||||
|
:size="36"
|
||||||
|
title="下一曲 Ctrl + Right"
|
||||||
|
@click="next"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="music-music">
|
||||||
|
<div class="music-bar-info">
|
||||||
|
<template v-if="currentMusic && currentMusic.id">
|
||||||
|
{{ currentMusic.name }}
|
||||||
|
<span>- {{ currentMusic.singer }}</span>
|
||||||
|
</template>
|
||||||
|
<template v-else>欢迎使用网抑云在线音乐播放器</template>
|
||||||
|
</div>
|
||||||
|
<div v-if="currentMusic.id" class="music-bar-time">
|
||||||
|
{{ currentTime | format }}/{{ currentMusic.duration % 3600 | format }}
|
||||||
|
</div>
|
||||||
|
<wyy-progress
|
||||||
|
class="music-progress"
|
||||||
|
:percent="percentMusic"
|
||||||
|
:percent-progress="currentProgress"
|
||||||
|
@percentChange="progressMusic"
|
||||||
|
@percentChangeEnd="progressMusicEnd"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 播放模式 -->
|
||||||
|
<wyy-icon
|
||||||
|
class="icon-color pointer mode"
|
||||||
|
:type="getModeIconType()"
|
||||||
|
:title="getModeIconTitle()"
|
||||||
|
:size="30"
|
||||||
|
@click="modeChange"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 评论 -->
|
||||||
|
<wyy-icon class="icon-color pointer comment" type="comment" :size="30" @click="openComment" />
|
||||||
|
|
||||||
|
<!-- 音量控制 -->
|
||||||
|
<div class="music-bar-volume" title="音量加减 [Ctrl + Up / Down]">
|
||||||
|
<volume :volume="volume" @volumeChange="volumeChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--遮罩-->
|
||||||
|
<div class="wangyiyun-bg" :style="{ backgroundImage: picUrl }"></div>
|
||||||
|
<div class="wangyiyun-mask"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { getLyric } from 'api'
|
||||||
|
import wangyiyunMusic from './wyyPlayer'
|
||||||
|
import { randomSortArray, parseLyric, format, silencePromise } from '@/utils/util'
|
||||||
|
import { PLAY_MODE, WYYPLAYER_CONFIG } from '@/config'
|
||||||
|
import { getVolume, setVolume } from '@/utils/storage'
|
||||||
|
import { mapGetters, mapMutations, mapActions } from 'vuex'
|
||||||
|
|
||||||
|
import WyyProgress from 'base/wyy-progress/wyy-progress'
|
||||||
|
import MusicBtn from 'components/music-btn/music-btn'
|
||||||
|
import Lyric from 'components/lyric/lyric'
|
||||||
|
import Volume from 'components/volume/volume'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Music',
|
||||||
|
components: {
|
||||||
|
WyyProgress,
|
||||||
|
MusicBtn,
|
||||||
|
Lyric,
|
||||||
|
Volume,
|
||||||
|
},
|
||||||
|
filters: {
|
||||||
|
// 时间格式化
|
||||||
|
format,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
const volume = getVolume()
|
||||||
|
return {
|
||||||
|
musicReady: false, // 是否可以使用播放器
|
||||||
|
currentTime: 0, // 当前播放时间
|
||||||
|
currentProgress: 0, // 当前缓冲进度
|
||||||
|
lyricVisible: false, // 移动端歌词显示
|
||||||
|
lyric: [], // 歌词
|
||||||
|
nolyric: false, // 是否有歌词
|
||||||
|
lyricIndex: 0, // 当前播放歌词下标
|
||||||
|
isMute: false, // 是否静音
|
||||||
|
volume, // 音量大小
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
picUrl() {
|
||||||
|
return this.currentMusic.id && this.currentMusic.image
|
||||||
|
? `url(${this.currentMusic.image}?param=300y300)`
|
||||||
|
: `url(${WYYPLAYER_CONFIG.BACKGROUND})`
|
||||||
|
},
|
||||||
|
percentMusic() {
|
||||||
|
const duration = this.currentMusic.duration
|
||||||
|
return this.currentTime && duration ? this.currentTime / duration : 0
|
||||||
|
},
|
||||||
|
...mapGetters([
|
||||||
|
'audioEle',
|
||||||
|
'mode',
|
||||||
|
'playing',
|
||||||
|
'playlist',
|
||||||
|
'orderList',
|
||||||
|
'currentIndex',
|
||||||
|
'currentMusic',
|
||||||
|
'historyList',
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
currentMusic(newMusic, oldMusic) {
|
||||||
|
if (!newMusic.id) {
|
||||||
|
this.lyric = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (newMusic.id === oldMusic.id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.audioEle.src = newMusic.url
|
||||||
|
// 重置相关参数
|
||||||
|
this.lyricIndex = this.currentTime = this.currentProgress = 0
|
||||||
|
silencePromise(this.audioEle.play())
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this._getLyric(newMusic.id)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
playing(newPlaying) {
|
||||||
|
const audio = this.audioEle
|
||||||
|
this.$nextTick(() => {
|
||||||
|
newPlaying ? silencePromise(audio.play()) : audio.pause()
|
||||||
|
this.musicReady = true
|
||||||
|
})
|
||||||
|
},
|
||||||
|
currentTime(newTime) {
|
||||||
|
if (this.nolyric) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let lyricIndex = 0
|
||||||
|
for (let i = 0; i < this.lyric.length; i++) {
|
||||||
|
if (newTime > this.lyric[i].time) {
|
||||||
|
lyricIndex = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.lyricIndex = lyricIndex
|
||||||
|
},
|
||||||
|
$route() {
|
||||||
|
this.lyricVisible = false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
wangyiyunMusic.initAudio(this)
|
||||||
|
this.initKeyDown()
|
||||||
|
this.volumeChange(this.volume)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// 按键事件
|
||||||
|
initKeyDown() {
|
||||||
|
document.onkeydown = (e) => {
|
||||||
|
switch (e.ctrlKey && e.keyCode) {
|
||||||
|
case 32: // 播放暂停Ctrl + Space
|
||||||
|
this.play()
|
||||||
|
break
|
||||||
|
case 37: // 上一曲Ctrl + Left
|
||||||
|
this.prev()
|
||||||
|
break
|
||||||
|
case 38: // 音量加Ctrl + Up
|
||||||
|
let plus = Number((this.volume += 0.1).toFixed(1))
|
||||||
|
if (plus > 1) {
|
||||||
|
plus = 1
|
||||||
|
}
|
||||||
|
this.volumeChange(plus)
|
||||||
|
break
|
||||||
|
case 39: // 下一曲Ctrl + Right
|
||||||
|
this.next()
|
||||||
|
break
|
||||||
|
case 40: // 音量减Ctrl + Down
|
||||||
|
let reduce = Number((this.volume -= 0.1).toFixed(1))
|
||||||
|
if (reduce < 0) {
|
||||||
|
reduce = 0
|
||||||
|
}
|
||||||
|
this.volumeChange(reduce)
|
||||||
|
break
|
||||||
|
case 79: // 切换播放模式Ctrl + O
|
||||||
|
this.modeChange()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 上一曲
|
||||||
|
prev() {
|
||||||
|
if (!this.musicReady) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (this.playlist.length === 1) {
|
||||||
|
this.loop()
|
||||||
|
} else {
|
||||||
|
let index = this.currentIndex - 1
|
||||||
|
if (index < 0) {
|
||||||
|
index = this.playlist.length - 1
|
||||||
|
}
|
||||||
|
this.setCurrentIndex(index)
|
||||||
|
if (!this.playing && this.musicReady) {
|
||||||
|
this.setPlaying(true)
|
||||||
|
}
|
||||||
|
this.musicReady = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 播放暂停
|
||||||
|
play() {
|
||||||
|
if (!this.musicReady) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.setPlaying(!this.playing)
|
||||||
|
},
|
||||||
|
// 下一曲
|
||||||
|
// 当 flag 为 true 时,表示上一曲播放失败
|
||||||
|
next(flag = false) {
|
||||||
|
if (!this.musicReady) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const {
|
||||||
|
playlist: { length },
|
||||||
|
} = this
|
||||||
|
if (
|
||||||
|
(length - 1 === this.currentIndex && this.mode === PLAY_MODE.ORDER) ||
|
||||||
|
(length === 1 && flag)
|
||||||
|
) {
|
||||||
|
this.setCurrentIndex(-1)
|
||||||
|
this.setPlaying(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (length === 1) {
|
||||||
|
this.loop()
|
||||||
|
} else {
|
||||||
|
let index = this.currentIndex + 1
|
||||||
|
if (index === length) {
|
||||||
|
index = 0
|
||||||
|
}
|
||||||
|
if (!this.playing && this.musicReady) {
|
||||||
|
this.setPlaying(true)
|
||||||
|
}
|
||||||
|
this.setCurrentIndex(index)
|
||||||
|
this.musicReady = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 循环
|
||||||
|
loop() {
|
||||||
|
this.audioEle.currentTime = 0
|
||||||
|
silencePromise(this.audioEle.play())
|
||||||
|
this.setPlaying(true)
|
||||||
|
if (this.lyric.length > 0) {
|
||||||
|
this.lyricIndex = 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 修改音乐显示时长
|
||||||
|
progressMusic(percent) {
|
||||||
|
this.currentTime = this.currentMusic.duration * percent
|
||||||
|
},
|
||||||
|
// 修改音乐进度
|
||||||
|
progressMusicEnd(percent) {
|
||||||
|
this.audioEle.currentTime = this.currentMusic.duration * percent
|
||||||
|
},
|
||||||
|
// 切换播放顺序
|
||||||
|
modeChange() {
|
||||||
|
const mode = (this.mode + 1) % 4
|
||||||
|
this.setPlayMode(mode)
|
||||||
|
if (mode === PLAY_MODE.LOOP) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let list = []
|
||||||
|
switch (mode) {
|
||||||
|
case PLAY_MODE.LIST_LOOP:
|
||||||
|
case PLAY_MODE.ORDER:
|
||||||
|
list = this.orderList
|
||||||
|
break
|
||||||
|
case PLAY_MODE.RANDOM:
|
||||||
|
list = randomSortArray(this.orderList)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
this.resetCurrentIndex(list)
|
||||||
|
this.setPlaylist(list)
|
||||||
|
},
|
||||||
|
// 修改当前歌曲索引
|
||||||
|
resetCurrentIndex(list) {
|
||||||
|
const index = list.findIndex((item) => {
|
||||||
|
return item.id === this.currentMusic.id
|
||||||
|
})
|
||||||
|
this.setCurrentIndex(index)
|
||||||
|
},
|
||||||
|
// 打开音乐评论
|
||||||
|
openComment() {
|
||||||
|
if (!this.currentMusic.id) {
|
||||||
|
this.$wyyToast('还没有播放歌曲哦!')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
this.$router.push(`/music/comment/${this.currentMusic.id}`)
|
||||||
|
},
|
||||||
|
// 修改音量大小
|
||||||
|
volumeChange(percent) {
|
||||||
|
percent === 0 ? (this.isMute = true) : (this.isMute = false)
|
||||||
|
this.volume = percent
|
||||||
|
this.audioEle.volume = percent
|
||||||
|
setVolume(percent)
|
||||||
|
},
|
||||||
|
// 获取播放模式 icon
|
||||||
|
getModeIconType() {
|
||||||
|
return {
|
||||||
|
[PLAY_MODE.LIST_LOOP]: 'loop',
|
||||||
|
[PLAY_MODE.ORDER]: 'sequence',
|
||||||
|
[PLAY_MODE.RANDOM]: 'random',
|
||||||
|
[PLAY_MODE.LOOP]: 'loop-one',
|
||||||
|
}[this.mode]
|
||||||
|
},
|
||||||
|
// 获取播放模式 title
|
||||||
|
getModeIconTitle() {
|
||||||
|
const key = 'Ctrl + O'
|
||||||
|
return {
|
||||||
|
[PLAY_MODE.LIST_LOOP]: `列表循环 ${key}`,
|
||||||
|
[PLAY_MODE.ORDER]: `顺序播放 ${key}`,
|
||||||
|
[PLAY_MODE.RANDOM]: `随机播放 ${key}`,
|
||||||
|
[PLAY_MODE.LOOP]: `单曲循环 ${key}`,
|
||||||
|
}[this.mode]
|
||||||
|
},
|
||||||
|
// 查看歌词
|
||||||
|
handleOpenLyric() {
|
||||||
|
this.lyricVisible = true
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.lyric.clacTop()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// 关闭歌词
|
||||||
|
handleCloseLyric() {
|
||||||
|
this.lyricVisible = false
|
||||||
|
},
|
||||||
|
// 获取歌词
|
||||||
|
_getLyric(id) {
|
||||||
|
getLyric(id).then((res) => {
|
||||||
|
if (res.lrc && res.lrc.lyric) {
|
||||||
|
this.nolyric = false
|
||||||
|
this.lyric = parseLyric(res.lrc.lyric)
|
||||||
|
} else {
|
||||||
|
this.nolyric = true
|
||||||
|
}
|
||||||
|
silencePromise(this.audioEle.play())
|
||||||
|
})
|
||||||
|
},
|
||||||
|
...mapMutations({
|
||||||
|
setPlaying: 'SET_PLAYING',
|
||||||
|
setPlaylist: 'SET_PLAYLIST',
|
||||||
|
setCurrentIndex: 'SET_CURRENTINDEX',
|
||||||
|
}),
|
||||||
|
...mapActions(['setHistory', 'setPlayMode']),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
.router-view {
|
||||||
|
flex: 1;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.music {
|
||||||
|
padding: 75px 25px 25px 25px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1750px;
|
||||||
|
margin: 0 auto;
|
||||||
|
height: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
|
.music-content {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
.music-left {
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.music-right {
|
||||||
|
position: relative;
|
||||||
|
width: 310px;
|
||||||
|
margin-left: 10px;
|
||||||
|
.close-lyric {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*底部wangyiyun-bar*/
|
||||||
|
.music-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
padding: 15px 0;
|
||||||
|
color: #fff;
|
||||||
|
&.disable {
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
.icon-color {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.music-bar-btns {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
width: 180px;
|
||||||
|
.control-play {
|
||||||
|
.flex-center;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
color: #fff;
|
||||||
|
background-color: rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-center;
|
||||||
|
.btn-prev {
|
||||||
|
width: 19px;
|
||||||
|
min-width: 19px;
|
||||||
|
height: 20px;
|
||||||
|
background-position: 0 -30px;
|
||||||
|
}
|
||||||
|
.btn-play {
|
||||||
|
width: 21px;
|
||||||
|
min-width: 21px;
|
||||||
|
height: 29px;
|
||||||
|
margin: 0 50px;
|
||||||
|
background-position: 0 0;
|
||||||
|
&.btn-play-pause {
|
||||||
|
background-position: -30px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.btn-next {
|
||||||
|
width: 19px;
|
||||||
|
min-width: 19px;
|
||||||
|
height: 20px;
|
||||||
|
background-position: 0 -52px;
|
||||||
|
}
|
||||||
|
.music-music {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
flex: 1;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding-left: 40px;
|
||||||
|
font-size: @font_size_small;
|
||||||
|
color: @text_color_active;
|
||||||
|
.music-bar-info {
|
||||||
|
height: 15px;
|
||||||
|
padding-right: 80px;
|
||||||
|
line-height: 15px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 1;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
}
|
||||||
|
.music-bar-time {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.mode,
|
||||||
|
.comment,
|
||||||
|
.music-bar-volume {
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 音量控制
|
||||||
|
.volume-wrapper {
|
||||||
|
margin-left: 20px;
|
||||||
|
width: 150px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*遮罩*/
|
||||||
|
.wangyiyun-mask,
|
||||||
|
.wangyiyun-bg {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wangyiyun-mask {
|
||||||
|
z-index: -1;
|
||||||
|
background-color: @mask_color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wangyiyun-bg {
|
||||||
|
z-index: -2;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: 50%;
|
||||||
|
filter: blur(12px);
|
||||||
|
opacity: 0.7;
|
||||||
|
transition: all 0.8s;
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 960px) {
|
||||||
|
.close-lyric {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//当屏幕小于960时
|
||||||
|
@media (max-width: 960px) {
|
||||||
|
.music-right {
|
||||||
|
display: none;
|
||||||
|
&.show {
|
||||||
|
display: block;
|
||||||
|
margin-left: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//当屏幕小于768时
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
padding: 75px 15px 5px 15px;
|
||||||
|
|
||||||
|
.music-bar {
|
||||||
|
padding-top: 10px;
|
||||||
|
.music-bar-info span,
|
||||||
|
.music-bar-volume .mmProgress {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//当屏幕小于520时
|
||||||
|
@media (max-width: 520px) {
|
||||||
|
.music-bar {
|
||||||
|
position: relative;
|
||||||
|
flex-direction: column;
|
||||||
|
.music-bar-btns {
|
||||||
|
width: 60%;
|
||||||
|
margin-top: 10px;
|
||||||
|
order: 2;
|
||||||
|
}
|
||||||
|
.music-music {
|
||||||
|
padding-left: 0;
|
||||||
|
order: 1;
|
||||||
|
}
|
||||||
|
.mode,
|
||||||
|
.comment {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 20px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.mode {
|
||||||
|
left: 5px;
|
||||||
|
}
|
||||||
|
.comment {
|
||||||
|
right: 5px;
|
||||||
|
}
|
||||||
|
.music-bar-volume {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
65
src/pages/playList/playList.vue
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
<template>
|
||||||
|
<!--正在播放-->
|
||||||
|
<div class="playList">
|
||||||
|
<music-list :list="playlist" list-type="duration" @select="selectItem" @del="deleteItem">
|
||||||
|
<div slot="listBtn" class="list-btn">
|
||||||
|
<span @click="$refs.dialog.show()">清空列表</span>
|
||||||
|
</div>
|
||||||
|
</music-list>
|
||||||
|
<wyy-dialog
|
||||||
|
ref="dialog"
|
||||||
|
body-text="是否清空正在播放列表"
|
||||||
|
confirm-btn-text="清空"
|
||||||
|
@confirm="clearList"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters, mapMutations, mapActions } from 'vuex'
|
||||||
|
import MusicList from 'components/music-list/music-list'
|
||||||
|
import WyyDialog from 'base/wyy-dialog/wyy-dialog'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'PlayList',
|
||||||
|
components: {
|
||||||
|
MusicList,
|
||||||
|
WyyDialog,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
show: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters(['playing', 'playlist', 'currentMusic']),
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// 清空列表事件
|
||||||
|
clearList() {
|
||||||
|
this.clearPlayList()
|
||||||
|
this.$wyyToast('列表清空成功')
|
||||||
|
},
|
||||||
|
// 播放暂停事件
|
||||||
|
selectItem(item, index) {
|
||||||
|
if (item.id !== this.currentMusic.id) {
|
||||||
|
this.setCurrentIndex(index)
|
||||||
|
this.setPlaying(true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 删除事件
|
||||||
|
deleteItem(index) {
|
||||||
|
let list = [...this.playlist]
|
||||||
|
list.splice(index, 1)
|
||||||
|
this.removerPlayListItem({ list, index })
|
||||||
|
this.$wyyToast('删除成功')
|
||||||
|
},
|
||||||
|
...mapMutations({
|
||||||
|
setPlaying: 'SET_PLAYING',
|
||||||
|
setCurrentIndex: 'SET_CURRENTINDEX',
|
||||||
|
clearPlaylist: 'CLEAR_PLAYLIST',
|
||||||
|
}),
|
||||||
|
...mapActions(['removerPlayListItem', 'clearPlayList']),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
168
src/pages/search/search.vue
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
<template>
|
||||||
|
<!--搜索-->
|
||||||
|
<div class="search flex-col">
|
||||||
|
<wyy-loading v-model="wyyLoadShow" />
|
||||||
|
<div class="search-head">
|
||||||
|
<span v-for="(item, index) in Artists" :key="index" @click="clickHot(item.first)">
|
||||||
|
{{ item.first }}
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
v-model.trim="searchValue"
|
||||||
|
class="search-input"
|
||||||
|
type="text"
|
||||||
|
placeholder="音乐/歌手"
|
||||||
|
@keyup.enter="onEnter"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 overflow-hidden">
|
||||||
|
<music-list
|
||||||
|
ref="musicList"
|
||||||
|
:list="list"
|
||||||
|
list-type="pullup"
|
||||||
|
@select="selectItem"
|
||||||
|
@pullUp="pullUpLoad"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters, mapActions, mapMutations } from 'vuex'
|
||||||
|
import { search, searchHot, getMusicDetail } from 'api'
|
||||||
|
import { formatSongs } from '@/utils/song'
|
||||||
|
import WyyLoading from 'base/wyy-loading/wyy-loading'
|
||||||
|
import MusicList from 'components/music-list/music-list'
|
||||||
|
import { loadMixin } from '@/utils/mixin'
|
||||||
|
import { toHttps } from '@/utils/util'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Search',
|
||||||
|
components: {
|
||||||
|
WyyLoading,
|
||||||
|
MusicList,
|
||||||
|
},
|
||||||
|
mixins: [loadMixin],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
searchValue: '', // 搜索关键词
|
||||||
|
Artists: [], // 热搜数组
|
||||||
|
list: [], // 搜索数组
|
||||||
|
page: 0, // 分页
|
||||||
|
lockUp: true, // 是否锁定上拉加载事件,默认锁定
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters(['playing', 'currentMusic']),
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
list(newList, oldList) {
|
||||||
|
if (newList.length !== oldList.length) {
|
||||||
|
this.lockUp = false
|
||||||
|
} else if (newList[newList.length - 1].id !== oldList[oldList.length - 1].id) {
|
||||||
|
this.lockUp = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
// 获取热搜
|
||||||
|
searchHot().then(({ result }) => {
|
||||||
|
this.Artists = result.hots.slice(0, 5)
|
||||||
|
this.wyyLoadShow = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// 点击热搜
|
||||||
|
clickHot(name) {
|
||||||
|
this.searchValue = name
|
||||||
|
this.onEnter()
|
||||||
|
},
|
||||||
|
// 搜索事件
|
||||||
|
onEnter() {
|
||||||
|
if (this.searchValue.replace(/(^\s+)|(\s+$)/g, '') === '') {
|
||||||
|
this.$wyyToast('搜索内容不能为空!')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.wyyLoadShow = true
|
||||||
|
this.page = 0
|
||||||
|
if (this.list.length > 0) {
|
||||||
|
this.$refs.musicList.scrollTo()
|
||||||
|
}
|
||||||
|
search(this.searchValue).then(({ result }) => {
|
||||||
|
this.list = formatSongs(result.songs)
|
||||||
|
this._hideLoad()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// 滚动加载事件
|
||||||
|
pullUpLoad() {
|
||||||
|
this.page += 1
|
||||||
|
search(this.searchValue, this.page).then(({ result }) => {
|
||||||
|
if (!result.songs) {
|
||||||
|
this.$wyyToast('没有更多歌曲啦!')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.list = [...this.list, ...formatSongs(result.songs)]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// 播放歌曲
|
||||||
|
async selectItem(music) {
|
||||||
|
try {
|
||||||
|
const image = await this._getMusicDetail(music.id)
|
||||||
|
music.image = toHttps(image)
|
||||||
|
this.selectAddPlay(music)
|
||||||
|
} catch (error) {
|
||||||
|
this.$wyyToast('哎呀,出错啦~')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 获取歌曲详情
|
||||||
|
_getMusicDetail(id) {
|
||||||
|
return getMusicDetail(id).then((res) => res.songs[0].al.picUrl)
|
||||||
|
},
|
||||||
|
...mapMutations({
|
||||||
|
setPlaying: 'SET_PLAYING',
|
||||||
|
}),
|
||||||
|
...mapActions(['selectAddPlay']),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.search {
|
||||||
|
overflow: hidden;
|
||||||
|
height: 100%;
|
||||||
|
.search-head {
|
||||||
|
display: flex;
|
||||||
|
height: 40px;
|
||||||
|
padding: 10px 15px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: @search_bg_color;
|
||||||
|
span {
|
||||||
|
line-height: 40px;
|
||||||
|
margin-right: 15px;
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
color: @text_color_active;
|
||||||
|
}
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
& {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.search-input {
|
||||||
|
flex: 1;
|
||||||
|
height: 40px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 0 15px;
|
||||||
|
border: 1px solid @btn_color;
|
||||||
|
outline: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: @text_color_active;
|
||||||
|
font-size: @font_size_medium;
|
||||||
|
box-shadow: 0 0 1px 0 #fff inset;
|
||||||
|
&::placeholder {
|
||||||
|
color: @text_color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
128
src/pages/topList/topList.vue
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
<template>
|
||||||
|
<!--排行榜-->
|
||||||
|
<div class="topList">
|
||||||
|
<wyy-loading v-model="wyyLoadShow" />
|
||||||
|
<template v-if="!wyyLoadShow">
|
||||||
|
<div class="topList-head">云音乐特色榜</div>
|
||||||
|
<div class="topList-content">
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in list"
|
||||||
|
:key="index"
|
||||||
|
class="list-item"
|
||||||
|
:title="`${item.name}-${item.updateFrequency}`"
|
||||||
|
>
|
||||||
|
<router-link :to="{ path: `/music/details/${item.id}` }" tag="div" class="topList-item">
|
||||||
|
<div class="topList-img">
|
||||||
|
<img v-lazy="`${item.coverImgUrl}?param=300y300`" class="cover-img" />
|
||||||
|
</div>
|
||||||
|
<h3 class="name">{{ item.name }}</h3>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="topList-head">热门歌单</div>
|
||||||
|
<div class="topList-content">
|
||||||
|
<div v-for="(item, index) in hotList" :key="index" class="list-item" :title="item.name">
|
||||||
|
<router-link :to="{ path: `/music/details/${item.id}` }" tag="div" class="topList-item">
|
||||||
|
<div class="topList-img">
|
||||||
|
<img v-lazy="`${item.picUrl}?param=300y300`" class="cover-img" />
|
||||||
|
</div>
|
||||||
|
<h3 class="name">{{ item.name }}</h3>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { getToplistDetail, getPersonalized } from 'api'
|
||||||
|
import WyyLoading from 'base/wyy-loading/wyy-loading'
|
||||||
|
import { loadMixin } from '@/utils/mixin'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'PlayList',
|
||||||
|
components: {
|
||||||
|
WyyLoading,
|
||||||
|
},
|
||||||
|
mixins: [loadMixin],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
list: [], // 云音乐特色榜
|
||||||
|
hotList: [], // 热门歌单
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
Promise.all([getToplistDetail(), getPersonalized()])
|
||||||
|
.then(([topList, hotList]) => {
|
||||||
|
this.list = topList.list.filter((v) => v.ToplistType)
|
||||||
|
this.hotList = hotList.result.slice()
|
||||||
|
this._hideLoad()
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.topList {
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
&-head {
|
||||||
|
width: 100%;
|
||||||
|
height: 34px;
|
||||||
|
line-height: 34px;
|
||||||
|
padding: 20px 0;
|
||||||
|
font-size: @font_size_large;
|
||||||
|
color: @text_color_active;
|
||||||
|
}
|
||||||
|
&-content {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.list-item {
|
||||||
|
float: left;
|
||||||
|
width: calc(~'100% / 7');
|
||||||
|
.topList-item {
|
||||||
|
width: 130px;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 0 auto 20px;
|
||||||
|
&:hover {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.name {
|
||||||
|
height: 30px;
|
||||||
|
line-height: 30px;
|
||||||
|
font-size: @font_size_medium;
|
||||||
|
.no-wrap();
|
||||||
|
}
|
||||||
|
@media (max-width: 1100px) {
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 1500px) {
|
||||||
|
width: calc(~'100% / 6');
|
||||||
|
}
|
||||||
|
@media (max-width: 1400px), (max-width: 960px) {
|
||||||
|
width: calc(~'100% / 5');
|
||||||
|
}
|
||||||
|
@media (max-width: 1280px), (max-width: 768px) {
|
||||||
|
width: calc(~'100% / 4');
|
||||||
|
}
|
||||||
|
@media (max-width: 540px) {
|
||||||
|
width: calc(~'100% / 3');
|
||||||
|
}
|
||||||
|
.topList-img {
|
||||||
|
position: relative;
|
||||||
|
padding-top: 100%;
|
||||||
|
width: 100%;
|
||||||
|
height: 0;
|
||||||
|
.cover-img {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
125
src/pages/userList/userList.vue
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
<template>
|
||||||
|
<!--我的歌单-->
|
||||||
|
<div class="userList">
|
||||||
|
<wyy-loading v-model="wyyLoadShow" />
|
||||||
|
<template v-if="list.length > 0">
|
||||||
|
<div v-for="item in formatList" :key="item.id" class="list-item" :title="item.name">
|
||||||
|
<router-link :to="{ path: `/music/details/${item.id}` }" tag="div" class="userList-item">
|
||||||
|
<img v-lazy="`${item.coverImgUrl}?param=200y200`" class="cover-img" />
|
||||||
|
<h3 class="name">{{ item.name }}</h3>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<wyy-no-result v-else title="啥也没有哦,快去登录看看吧!" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex'
|
||||||
|
|
||||||
|
import { getUserPlaylist } from 'api'
|
||||||
|
import { loadMixin } from '@/utils/mixin'
|
||||||
|
|
||||||
|
import WyyLoading from 'base/wyy-loading/wyy-loading'
|
||||||
|
import WyyNoResult from 'base/wyy-no-result/wyy-no-result'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'PlayList',
|
||||||
|
components: {
|
||||||
|
WyyLoading,
|
||||||
|
WyyNoResult,
|
||||||
|
},
|
||||||
|
mixins: [loadMixin],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
list: [], // 列表
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
formatList() {
|
||||||
|
return this.list.filter((item) => item.trackCount > 0)
|
||||||
|
},
|
||||||
|
...mapGetters(['uid']),
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
uid(newUid) {
|
||||||
|
if (newUid) {
|
||||||
|
this.wyyLoadShow = true
|
||||||
|
this._getUserPlaylist(newUid)
|
||||||
|
} else {
|
||||||
|
this.list = []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
if (!this.uid) {
|
||||||
|
this.wyyLoadShow = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
activated() {
|
||||||
|
if (this.uid && this.list.length === 0) {
|
||||||
|
this.wyyLoadShow = true
|
||||||
|
this._getUserPlaylist(this.uid)
|
||||||
|
} else if (!this.uid && this.list.length !== 0) {
|
||||||
|
this.list = []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// 获取我的歌单详情
|
||||||
|
_getUserPlaylist(uid) {
|
||||||
|
getUserPlaylist(uid).then((res) => {
|
||||||
|
if (res.playlist.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.list = res.playlist.slice(1)
|
||||||
|
this._hideLoad()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.userList {
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
&-head {
|
||||||
|
height: 100px;
|
||||||
|
}
|
||||||
|
.list-item {
|
||||||
|
float: left;
|
||||||
|
width: calc(~'100% / 7');
|
||||||
|
.userList-item {
|
||||||
|
width: 130px;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 0 auto 20px;
|
||||||
|
&:hover {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.name {
|
||||||
|
height: 30px;
|
||||||
|
line-height: 30px;
|
||||||
|
font-size: @font_size_medium;
|
||||||
|
.no-wrap();
|
||||||
|
}
|
||||||
|
@media (max-width: 1100px) {
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 1500px) {
|
||||||
|
width: calc(~'100% / 6');
|
||||||
|
}
|
||||||
|
@media (max-width: 1400px), (max-width: 960px) {
|
||||||
|
width: calc(~'100% / 5');
|
||||||
|
}
|
||||||
|
@media (max-width: 1280px), (max-width: 768px) {
|
||||||
|
width: calc(~'100% / 4');
|
||||||
|
}
|
||||||
|
@media (max-width: 540px) {
|
||||||
|
width: calc(~'100% / 3');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
93
src/pages/wyyPlayer.js
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import { PLAY_MODE } from '@/config'
|
||||||
|
|
||||||
|
// 重试次数
|
||||||
|
let retry = 1
|
||||||
|
|
||||||
|
const wangyiyunMusic = {
|
||||||
|
initAudio(that) {
|
||||||
|
const ele = that.audioEle
|
||||||
|
// 音频缓冲事件
|
||||||
|
ele.onprogress = () => {
|
||||||
|
try {
|
||||||
|
if (ele.buffered.length > 0) {
|
||||||
|
const duration = that.currentMusic.duration
|
||||||
|
let buffered = 0
|
||||||
|
ele.buffered.end(0)
|
||||||
|
buffered = ele.buffered.end(0) > duration ? duration : ele.buffered.end(0)
|
||||||
|
that.currentProgress = buffered / duration
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
// 开始播放音乐
|
||||||
|
ele.onplay = () => {
|
||||||
|
let timer
|
||||||
|
clearTimeout(timer)
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
that.musicReady = true
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
// 获取当前播放时间
|
||||||
|
ele.ontimeupdate = () => {
|
||||||
|
that.currentTime = ele.currentTime
|
||||||
|
}
|
||||||
|
// 当前音乐播放完毕
|
||||||
|
ele.onended = () => {
|
||||||
|
if (that.mode === PLAY_MODE.LOOP) {
|
||||||
|
that.loop()
|
||||||
|
} else {
|
||||||
|
that.next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 音乐播放出错
|
||||||
|
ele.onerror = () => {
|
||||||
|
if (retry === 0) {
|
||||||
|
let toastText = '当前音乐不可播放,已自动播放下一曲'
|
||||||
|
if (that.playlist.length === 1) {
|
||||||
|
toastText = '没有可播放的音乐哦~'
|
||||||
|
}
|
||||||
|
that.$wyyToast(toastText)
|
||||||
|
that.next(true)
|
||||||
|
} else {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log('重试一次')
|
||||||
|
retry -= 1
|
||||||
|
ele.url = that.currentMusic.url
|
||||||
|
ele.load()
|
||||||
|
}
|
||||||
|
// console.log('播放出错啦!')
|
||||||
|
}
|
||||||
|
// 音乐进度拖动大于加载时重载音乐
|
||||||
|
ele.onstalled = () => {
|
||||||
|
ele.load()
|
||||||
|
that.setPlaying(false)
|
||||||
|
let timer
|
||||||
|
clearTimeout(timer)
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
that.setPlaying(true)
|
||||||
|
}, 10)
|
||||||
|
}
|
||||||
|
// 将能播放的音乐加入播放历史
|
||||||
|
ele.oncanplay = () => {
|
||||||
|
retry = 1
|
||||||
|
if (that.historyList.length === 0 || that.currentMusic.id !== that.historyList[0].id) {
|
||||||
|
that.setHistory(that.currentMusic)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 音频数据不可用时
|
||||||
|
ele.onstalled = () => {
|
||||||
|
ele.load()
|
||||||
|
that.setPlaying(false)
|
||||||
|
let timer
|
||||||
|
clearTimeout(timer)
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
that.setPlaying(true)
|
||||||
|
}, 10)
|
||||||
|
}
|
||||||
|
// 当音频已暂停时
|
||||||
|
ele.onpause = () => {
|
||||||
|
that.setPlaying(false)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default wangyiyunMusic
|
72
src/router/index.js
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
import Router from 'vue-router'
|
||||||
|
Vue.use(Router)
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
redirect: '/music',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/music',
|
||||||
|
component: () => import('pages/music'),
|
||||||
|
redirect: '/music/playlist',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/music/playlist', // 正在播放列表
|
||||||
|
component: () => import('pages/playList/playList'),
|
||||||
|
meta: {
|
||||||
|
keepAlive: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/music/userlist', // 我的歌单
|
||||||
|
component: () => import('pages/userList/userList'),
|
||||||
|
meta: {
|
||||||
|
title: '我的歌单',
|
||||||
|
keepAlive: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/music/toplist', // 排行榜列表
|
||||||
|
component: () => import('pages/topList/topList'),
|
||||||
|
meta: {
|
||||||
|
title: '排行榜',
|
||||||
|
keepAlive: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/music/details/:id', // 音乐详情列表
|
||||||
|
component: () => import('pages/details/details'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/music/historylist', // 我听过的列表
|
||||||
|
component: () => import('pages/historyList/historyList'),
|
||||||
|
meta: {
|
||||||
|
title: '我听过的',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/music/search', // 搜索
|
||||||
|
component: () => import('pages/search/search'),
|
||||||
|
meta: {
|
||||||
|
title: '搜索',
|
||||||
|
keepAlive: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/music/comment/:id', // 音乐评论
|
||||||
|
component: () => import('pages/comment/comment'),
|
||||||
|
meta: {
|
||||||
|
title: '评论详情',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export default new Router({
|
||||||
|
linkActiveClass: 'active',
|
||||||
|
linkExactActiveClass: 'active',
|
||||||
|
routes,
|
||||||
|
})
|
88
src/store/actions.js
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import {
|
||||||
|
clearHistoryList,
|
||||||
|
setHistoryList,
|
||||||
|
removeHistoryList,
|
||||||
|
setMode,
|
||||||
|
setUserId,
|
||||||
|
} from '@/utils/storage'
|
||||||
|
import * as types from './mutation-types'
|
||||||
|
|
||||||
|
function findIndex(list, music) {
|
||||||
|
return list.findIndex((item) => {
|
||||||
|
return item.id === music.id
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置播放列表
|
||||||
|
export const setPlaylist = function ({ commit }, { list }) {
|
||||||
|
commit(types.SET_PLAYLIST, list)
|
||||||
|
commit(types.SET_ORDERLIST, list)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择播放(会更新整个播放列表)
|
||||||
|
export const selectPlay = function ({ commit }, { list, index }) {
|
||||||
|
commit(types.SET_PLAYLIST, list)
|
||||||
|
commit(types.SET_ORDERLIST, list)
|
||||||
|
commit(types.SET_CURRENTINDEX, index)
|
||||||
|
commit(types.SET_PLAYING, true)
|
||||||
|
}
|
||||||
|
// 选择播放(会插入一条到播放列表)
|
||||||
|
export const selectAddPlay = function ({ commit, state }, music) {
|
||||||
|
let list = [...state.playlist]
|
||||||
|
// 查询当前播放列表是否有代插入的音乐,并返回其索引值
|
||||||
|
let index = findIndex(list, music)
|
||||||
|
// 当前播放列表有待插入的音乐时,直接改变当前播放音乐的索引值
|
||||||
|
if (index > -1) {
|
||||||
|
commit(types.SET_CURRENTINDEX, index)
|
||||||
|
} else {
|
||||||
|
list.unshift(music)
|
||||||
|
commit(types.SET_PLAYLIST, list)
|
||||||
|
commit(types.SET_ORDERLIST, list)
|
||||||
|
commit(types.SET_CURRENTINDEX, 0)
|
||||||
|
}
|
||||||
|
commit(types.SET_PLAYING, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空播放列表
|
||||||
|
export const clearPlayList = function ({ commit }) {
|
||||||
|
commit(types.SET_PLAYING, false)
|
||||||
|
commit(types.SET_CURRENTINDEX, -1)
|
||||||
|
commit(types.SET_PLAYLIST, [])
|
||||||
|
commit(types.SET_ORDERLIST, [])
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除正在播放列表中的歌曲
|
||||||
|
export const removerPlayListItem = function ({ commit, state }, { list, index }) {
|
||||||
|
let currentIndex = state.currentIndex
|
||||||
|
if (index < state.currentIndex || list.length === state.currentIndex) {
|
||||||
|
currentIndex--
|
||||||
|
commit(types.SET_CURRENTINDEX, currentIndex)
|
||||||
|
}
|
||||||
|
commit(types.SET_PLAYLIST, list)
|
||||||
|
commit(types.SET_ORDERLIST, list)
|
||||||
|
if (!list.length) {
|
||||||
|
commit(types.SET_PLAYING, false)
|
||||||
|
} else {
|
||||||
|
commit(types.SET_PLAYING, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 设置播放历史
|
||||||
|
export const setHistory = function ({ commit }, music) {
|
||||||
|
commit(types.SET_HISTORYLIST, setHistoryList(music))
|
||||||
|
}
|
||||||
|
// 删除播放历史
|
||||||
|
export const removeHistory = function ({ commit }, music) {
|
||||||
|
commit(types.SET_HISTORYLIST, removeHistoryList(music))
|
||||||
|
}
|
||||||
|
// 清空播放历史
|
||||||
|
export const clearHistory = function ({ commit }) {
|
||||||
|
commit(types.SET_HISTORYLIST, clearHistoryList())
|
||||||
|
}
|
||||||
|
// 设置播放模式
|
||||||
|
export const setPlayMode = function ({ commit }, mode) {
|
||||||
|
commit(types.SET_PLAYMODE, setMode(mode))
|
||||||
|
}
|
||||||
|
// 设置网易云用户UID
|
||||||
|
export const setUid = function ({ commit }, uid) {
|
||||||
|
commit(types.SET_UID, setUserId(uid))
|
||||||
|
}
|
20
src/store/getters.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
// audio元素
|
||||||
|
export const audioEle = (state) => state.audioEle
|
||||||
|
// 播放模式
|
||||||
|
export const mode = (state) => state.mode
|
||||||
|
// 播放状态
|
||||||
|
export const playing = (state) => state.playing
|
||||||
|
// 播放列表
|
||||||
|
export const playlist = (state) => state.playlist
|
||||||
|
// 顺序列表
|
||||||
|
export const orderList = (state) => state.orderList
|
||||||
|
// 当前音乐索引
|
||||||
|
export const currentIndex = (state) => state.currentIndex
|
||||||
|
// 当前音乐
|
||||||
|
export const currentMusic = (state) => {
|
||||||
|
return state.playlist[state.currentIndex] || {}
|
||||||
|
}
|
||||||
|
// 播放历史列表
|
||||||
|
export const historyList = (state) => state.historyList
|
||||||
|
// 网易云用户UID
|
||||||
|
export const uid = (state) => state.uid
|
21
src/store/index.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
import Vuex from 'vuex'
|
||||||
|
import state from './state'
|
||||||
|
import * as getters from './getters'
|
||||||
|
import * as actions from './actions'
|
||||||
|
import mutations from './mutations'
|
||||||
|
// vuex调试
|
||||||
|
import createLogger from 'vuex/dist/logger'
|
||||||
|
const debug = process.env.NODE_ENV !== 'production'
|
||||||
|
|
||||||
|
Vue.use(Vuex)
|
||||||
|
|
||||||
|
export default new Vuex.Store({
|
||||||
|
state,
|
||||||
|
getters,
|
||||||
|
mutations,
|
||||||
|
actions,
|
||||||
|
// vuex调试
|
||||||
|
strict: debug,
|
||||||
|
plugins: debug ? [createLogger()] : [],
|
||||||
|
})
|
8
src/store/mutation-types.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export const SET_AUDIOELE = 'SET_AUDIOELE' // 修改audio元素
|
||||||
|
export const SET_PLAYMODE = 'SET_PLAYMODE' // 修改播放模式
|
||||||
|
export const SET_PLAYING = 'SET_PLAYING' // 修改播放状态
|
||||||
|
export const SET_PLAYLIST = 'SET_PLAYLIST' // 修改播放列表
|
||||||
|
export const SET_ORDERLIST = 'SET_ORDERLIST' // 修改顺序列表
|
||||||
|
export const SET_CURRENTINDEX = 'SET_CURRENTINDEX' // 修改当前音乐索引
|
||||||
|
export const SET_HISTORYLIST = 'SET_HISTORYLIST' // 修改播放历史列表
|
||||||
|
export const SET_UID = 'SET_UID' // 修改网易云用户UID
|
38
src/store/mutations.js
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import * as types from './mutation-types'
|
||||||
|
|
||||||
|
const mutations = {
|
||||||
|
// 修改audio元素
|
||||||
|
[types.SET_AUDIOELE](state, audioEle) {
|
||||||
|
state.audioEle = audioEle
|
||||||
|
},
|
||||||
|
// 修改播放模式
|
||||||
|
[types.SET_PLAYMODE](state, mode) {
|
||||||
|
state.mode = mode
|
||||||
|
},
|
||||||
|
// 修改播放状态
|
||||||
|
[types.SET_PLAYING](state, playing) {
|
||||||
|
state.playing = playing
|
||||||
|
},
|
||||||
|
// 修改播放列表
|
||||||
|
[types.SET_PLAYLIST](state, playlist) {
|
||||||
|
state.playlist = playlist
|
||||||
|
},
|
||||||
|
// 修改顺序列表
|
||||||
|
[types.SET_ORDERLIST](state, orderList) {
|
||||||
|
state.orderList = orderList
|
||||||
|
},
|
||||||
|
// 修改当前音乐索引
|
||||||
|
[types.SET_CURRENTINDEX](state, currentIndex) {
|
||||||
|
state.currentIndex = currentIndex
|
||||||
|
},
|
||||||
|
// 修改播放历史列表
|
||||||
|
[types.SET_HISTORYLIST](state, historyList) {
|
||||||
|
state.historyList = historyList
|
||||||
|
},
|
||||||
|
// 修改网易云用户UID
|
||||||
|
[types.SET_UID](state, uid) {
|
||||||
|
state.uid = uid
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default mutations
|
14
src/store/state.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { getHistoryList, getMode, getUserId } from '@/utils/storage'
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
audioEle: null, // audio元素
|
||||||
|
mode: getMode(), // 播放模式,默认列表循环
|
||||||
|
playing: false, // 播放状态
|
||||||
|
playlist: [], // 播放列表
|
||||||
|
orderList: [], // 顺序列表
|
||||||
|
currentIndex: -1, // 当前音乐索引
|
||||||
|
historyList: getHistoryList() || [], // 播放历史列表
|
||||||
|
uid: getUserId() || null, // 网易云用户UID
|
||||||
|
}
|
||||||
|
|
||||||
|
export default state
|
117
src/styles/index.less
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
@import 'reset';
|
||||||
|
@import 'var';
|
||||||
|
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
#app {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
min-width: 320px;
|
||||||
|
font-family: Arial;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
//浮动
|
||||||
|
.fl {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fr {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pointer {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover {
|
||||||
|
color: @text_color;
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
color: @text_color_active;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-left {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clearfix {
|
||||||
|
&:after {
|
||||||
|
display: block;
|
||||||
|
content: '';
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-col {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-1 {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overflow-hidden {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 滚动条相关样式 */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
/*滚动条整体部分,其中的属性有width,height,background,border(就和一个块级元素一样)等*/
|
||||||
|
background-color: @scrollbar_bg;
|
||||||
|
width: @scrollbar_size; // 纵向滚动条
|
||||||
|
height: @scrollbar_size; // 横向滚动条
|
||||||
|
border-radius: @scrollbar_border_radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-button {
|
||||||
|
/*滚动条两端的按钮。可以用display:none让其不显示,也可以添加背景图片,颜色改变显示效果。*/
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
/*外层轨道。可以用display:none让其不显示,也可以添加背景图片,颜色改变显示效果。*/
|
||||||
|
display: none;
|
||||||
|
//background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: @scrollbar_border_radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track-piece {
|
||||||
|
/*内层轨道,滚动条中间部分(除去)。*/
|
||||||
|
//background-color: rgba(255, 255, 255, .1);
|
||||||
|
border-radius: @scrollbar_border_radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
/*滚动条里面可以拖动的那部分*/
|
||||||
|
background-color: @scrollbar_thumb;
|
||||||
|
border-radius: @scrollbar_border_radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-corner {
|
||||||
|
border-radius: @scrollbar_border_radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-resizer {
|
||||||
|
/*定义右下角拖动块的样式*/
|
||||||
|
border-radius: @scrollbar_border_radius;
|
||||||
|
}
|
13
src/styles/mixin.less
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
// 显示省略号
|
||||||
|
.no-wrap() {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-center(@direction: row) {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: @direction;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
141
src/styles/reset.less
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
html,
|
||||||
|
body,
|
||||||
|
div,
|
||||||
|
span,
|
||||||
|
applet,
|
||||||
|
object,
|
||||||
|
iframe,
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6,
|
||||||
|
p,
|
||||||
|
blockquote,
|
||||||
|
pre,
|
||||||
|
a,
|
||||||
|
abbr,
|
||||||
|
acronym,
|
||||||
|
address,
|
||||||
|
big,
|
||||||
|
cite,
|
||||||
|
code,
|
||||||
|
del,
|
||||||
|
dfn,
|
||||||
|
em,
|
||||||
|
img,
|
||||||
|
ins,
|
||||||
|
kbd,
|
||||||
|
q,
|
||||||
|
s,
|
||||||
|
samp,
|
||||||
|
small,
|
||||||
|
strike,
|
||||||
|
strong,
|
||||||
|
sub,
|
||||||
|
sup,
|
||||||
|
tt,
|
||||||
|
var,
|
||||||
|
b,
|
||||||
|
u,
|
||||||
|
i,
|
||||||
|
center,
|
||||||
|
dl,
|
||||||
|
dt,
|
||||||
|
dd,
|
||||||
|
ol,
|
||||||
|
ul,
|
||||||
|
li,
|
||||||
|
fieldset,
|
||||||
|
form,
|
||||||
|
label,
|
||||||
|
legend,
|
||||||
|
table,
|
||||||
|
caption,
|
||||||
|
tbody,
|
||||||
|
tfoot,
|
||||||
|
thead,
|
||||||
|
tr,
|
||||||
|
th,
|
||||||
|
td,
|
||||||
|
article,
|
||||||
|
aside,
|
||||||
|
canvas,
|
||||||
|
details,
|
||||||
|
embed,
|
||||||
|
figure,
|
||||||
|
figcaption,
|
||||||
|
footer,
|
||||||
|
header,
|
||||||
|
hgroup,
|
||||||
|
menu,
|
||||||
|
nav,
|
||||||
|
output,
|
||||||
|
ruby,
|
||||||
|
section,
|
||||||
|
summary,
|
||||||
|
time,
|
||||||
|
mark,
|
||||||
|
audio,
|
||||||
|
video {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
font-size: 100%;
|
||||||
|
font: inherit;
|
||||||
|
vertical-align: baseline;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* HTML5 display-role reset for older browsers */
|
||||||
|
article,
|
||||||
|
aside,
|
||||||
|
details,
|
||||||
|
figcaption,
|
||||||
|
figure,
|
||||||
|
footer,
|
||||||
|
header,
|
||||||
|
hgroup,
|
||||||
|
menu,
|
||||||
|
nav,
|
||||||
|
section {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol,
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote,
|
||||||
|
q {
|
||||||
|
quotes: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote:before,
|
||||||
|
blockquote:after,
|
||||||
|
q:before,
|
||||||
|
q:after {
|
||||||
|
content: '';
|
||||||
|
content: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
border-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='number']::-webkit-inner-spin-button,
|
||||||
|
input[type='number']::-webkit-outer-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
71
src/styles/var.less
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
/* 颜色规范定义 */
|
||||||
|
@text_color: rgba(255, 255, 255, 0.6);
|
||||||
|
@text_color_active: #fff; //重点部分
|
||||||
|
|
||||||
|
// active 颜色
|
||||||
|
@active_color: #fff;
|
||||||
|
|
||||||
|
// 遮罩层颜色
|
||||||
|
@mask_color: rgba(0, 0, 0, 0.4);
|
||||||
|
// 背景滤镜
|
||||||
|
@backdrop_filter: blur(6px);
|
||||||
|
|
||||||
|
/* 字体大小规范定义*/
|
||||||
|
@font_size_small: 12px;
|
||||||
|
@font_size_medium: 14px;
|
||||||
|
@font_size_medium_x: 16px;
|
||||||
|
@font_size_large: 18px;
|
||||||
|
@font_size_large_x: 22px;
|
||||||
|
|
||||||
|
/* 圆角规范定义 */
|
||||||
|
@border_radius_base: 2px;
|
||||||
|
@border_radius: 4px;
|
||||||
|
|
||||||
|
/* 滚动条相关 */
|
||||||
|
// 滚动条圆角
|
||||||
|
@scrollbar_border_radius: 10px;
|
||||||
|
// 滚动条大小
|
||||||
|
@scrollbar_size: 5px;
|
||||||
|
@scrollbar_bg: rgba(0, 0, 0, 0.3);
|
||||||
|
@scrollbar_thumb: rgba(255, 255, 255, 0.5);
|
||||||
|
|
||||||
|
/* loading */
|
||||||
|
@load_bg_color: rgba(0, 0, 0, 0.2);
|
||||||
|
|
||||||
|
/* header */
|
||||||
|
@header_bg_color: rgba(0, 0, 0, 0.3);
|
||||||
|
|
||||||
|
/* search-head */
|
||||||
|
@search_bg_color: rgba(0, 0, 0, 0.2);
|
||||||
|
|
||||||
|
/* dialog 相关 */
|
||||||
|
@dialog_bg_color: rgba(0, 0, 0, 0.5);
|
||||||
|
@dialog_content_bg_color: rgba(0, 0, 0, 0.6);
|
||||||
|
@dialog_text_color: rgba(255, 255, 255, 0.7);
|
||||||
|
@dialog_line_color: rgba(0, 0, 0, 0.35);
|
||||||
|
@dialog_border_radius: @border_radius;
|
||||||
|
@dialog_btn_border_radius: @border_radius;
|
||||||
|
@dialog_mobile_border_radius: 10px;
|
||||||
|
@dialog_btn_mobile_border_radius: @border_radius_base;
|
||||||
|
|
||||||
|
/* btn 相关 */
|
||||||
|
@btn_color: rgba(255, 255, 255, 0.6);
|
||||||
|
@btn_color_active: #fff;
|
||||||
|
@btn_border_radius: @border_radius_base;
|
||||||
|
|
||||||
|
/* 歌词高亮颜色 */
|
||||||
|
@lyric_color_active: #40ce8f;
|
||||||
|
|
||||||
|
/* 进度条 */
|
||||||
|
@bar_color: rgba(255, 255, 255, 0.15);
|
||||||
|
@line_color: #fff;
|
||||||
|
@dot_color: #fff;
|
||||||
|
|
||||||
|
/* 列表 */
|
||||||
|
@list_head_line_color: rgba(255, 255, 255, 0.8);
|
||||||
|
@list_item_line_color: rgba(255, 255, 255, 0.1);
|
||||||
|
|
||||||
|
/* 评论 */
|
||||||
|
@comment_head_line_color: rgba(255, 255, 255, 0.8);
|
||||||
|
@comment_item_line_color: rgba(255, 255, 255, 0.1);
|
||||||
|
@comment_replied_line_color: rgba(255, 255, 255, 0.3);
|
23
src/utils/axios.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import Vue from 'vue'
|
||||||
|
|
||||||
|
const request = axios.create({
|
||||||
|
baseURL: process.env.VUE_APP_BASE_API_URL,
|
||||||
|
})
|
||||||
|
|
||||||
|
request.interceptors.response.use(
|
||||||
|
(response) => {
|
||||||
|
window.response = response
|
||||||
|
|
||||||
|
if (response.status === 200 && response.data.code === 200) {
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
return Promise.reject(response)
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
Vue.prototype.$wyyToast(error.response ? error.response.data.message : error.message)
|
||||||
|
return Promise.reject(error)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
export default request
|
6
src/utils/hack.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
// hack for global nextTick
|
||||||
|
|
||||||
|
function noop() {}
|
||||||
|
|
||||||
|
window.MessageChannel = noop
|
||||||
|
window.setImmediate = noop
|
47
src/utils/mixin.js
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { mapGetters, mapMutations, mapActions } from 'vuex'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 歌曲列表
|
||||||
|
*/
|
||||||
|
export const listMixin = {
|
||||||
|
computed: {
|
||||||
|
...mapGetters(['playing', 'currentMusic']),
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
selectItem(item, index) {
|
||||||
|
if (item.id === this.currentMusic.id && this.playing) {
|
||||||
|
this.setPlaying(false)
|
||||||
|
} else {
|
||||||
|
this.selectPlay({
|
||||||
|
list: this.list,
|
||||||
|
index,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
...mapMutations({
|
||||||
|
setPlaying: 'SET_PLAYING',
|
||||||
|
}),
|
||||||
|
...mapActions(['selectPlay']),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* loading状态
|
||||||
|
* @type {{data(): *, methods: {_hideLoad(): void}}}
|
||||||
|
*/
|
||||||
|
export const loadMixin = {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
wyyLoadShow: true, // loading状态
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
_hideLoad() {
|
||||||
|
let timer
|
||||||
|
clearTimeout(timer)
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
this.wyyLoadShow = false
|
||||||
|
}, 200)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
50
src/utils/song.js
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { toHttps } from './util'
|
||||||
|
|
||||||
|
function filterSinger(singers) {
|
||||||
|
if (!Array.isArray(singers) || !singers.length) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
let arr = []
|
||||||
|
singers.forEach((item) => {
|
||||||
|
arr.push(item.name)
|
||||||
|
})
|
||||||
|
return arr.join('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Song {
|
||||||
|
constructor({ id, name, singer, album, image, duration, url }) {
|
||||||
|
this.id = id
|
||||||
|
this.name = name
|
||||||
|
this.singer = singer
|
||||||
|
this.album = album
|
||||||
|
this.image = image
|
||||||
|
this.duration = duration
|
||||||
|
this.url = url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createSong(music) {
|
||||||
|
const album = music.album || music.al || {}
|
||||||
|
const duration = music.duration || music.dt
|
||||||
|
return new Song({
|
||||||
|
id: music.id,
|
||||||
|
name: music.name,
|
||||||
|
singer: filterSinger(music.ar || music.artists),
|
||||||
|
album: album.name,
|
||||||
|
image: toHttps(album.picUrl) || null,
|
||||||
|
duration: duration / 1000,
|
||||||
|
url: `https://music.163.com/song/media/outer/url?id=${music.id}.mp3`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 歌曲数据格式化
|
||||||
|
export function formatSongs(list) {
|
||||||
|
const Songs = []
|
||||||
|
list.forEach((item) => {
|
||||||
|
const musicData = item
|
||||||
|
if (musicData.id) {
|
||||||
|
Songs.push(createSong(musicData))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return Songs
|
||||||
|
}
|
131
src/utils/storage.js
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
import { WYYPLAYER, WYYPLAYER_CONFIG } from '@/config'
|
||||||
|
|
||||||
|
const STORAGE = window.localStorage
|
||||||
|
const storage = {
|
||||||
|
get(key, data = []) {
|
||||||
|
if (STORAGE) {
|
||||||
|
return STORAGE.getItem(key)
|
||||||
|
? Array.isArray(data)
|
||||||
|
? JSON.parse(STORAGE.getItem(key))
|
||||||
|
: STORAGE.getItem(key)
|
||||||
|
: data
|
||||||
|
}
|
||||||
|
},
|
||||||
|
set(key, val) {
|
||||||
|
if (STORAGE) {
|
||||||
|
STORAGE.setItem(key, val)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
clear(key) {
|
||||||
|
if (STORAGE) {
|
||||||
|
STORAGE.removeItem(key)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 播放历史
|
||||||
|
* @type HISTORYLIST_KEY:key值
|
||||||
|
* HistoryListMAX:最大长度
|
||||||
|
*/
|
||||||
|
const HISTORYLIST_KEY = '__wangyiyun_historyList__'
|
||||||
|
const HistoryListMAX = 200
|
||||||
|
// 获取播放历史
|
||||||
|
export function getHistoryList() {
|
||||||
|
return storage.get(HISTORYLIST_KEY)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新播放历史
|
||||||
|
export function setHistoryList(music) {
|
||||||
|
let list = storage.get(HISTORYLIST_KEY)
|
||||||
|
const index = list.findIndex((item) => {
|
||||||
|
return item.id === music.id
|
||||||
|
})
|
||||||
|
if (index === 0) {
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
if (index > 0) {
|
||||||
|
list.splice(index, 1)
|
||||||
|
}
|
||||||
|
list.unshift(music)
|
||||||
|
if (HistoryListMAX && list.length > HistoryListMAX) {
|
||||||
|
list.pop()
|
||||||
|
}
|
||||||
|
storage.set(HISTORYLIST_KEY, JSON.stringify(list))
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除一条播放历史
|
||||||
|
export function removeHistoryList(music) {
|
||||||
|
storage.set(HISTORYLIST_KEY, JSON.stringify(music))
|
||||||
|
return music
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空播放历史
|
||||||
|
export function clearHistoryList() {
|
||||||
|
storage.clear(HISTORYLIST_KEY)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 播放模式
|
||||||
|
* @type MODE_KEY:key值
|
||||||
|
* HistoryListMAX:最大长度
|
||||||
|
*/
|
||||||
|
const MODE_KEY = '__wangyiyun_mode__'
|
||||||
|
// 获取播放模式
|
||||||
|
export function getMode() {
|
||||||
|
return Number(storage.get(MODE_KEY, WYYPLAYER_CONFIG.PLAY_MODE))
|
||||||
|
}
|
||||||
|
// 修改播放模式
|
||||||
|
export function setMode(mode) {
|
||||||
|
storage.set(MODE_KEY, mode)
|
||||||
|
return mode
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 网易云用户uid
|
||||||
|
* @type USERID_KEY:key值
|
||||||
|
*/
|
||||||
|
const USERID_KEY = '__wangyiyun_userID__'
|
||||||
|
// 获取用户uid
|
||||||
|
export function getUserId() {
|
||||||
|
return Number(storage.get(USERID_KEY, null))
|
||||||
|
}
|
||||||
|
// 修改用户uid
|
||||||
|
export function setUserId(uid) {
|
||||||
|
storage.set(USERID_KEY, uid)
|
||||||
|
return uid
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 版本号
|
||||||
|
* @type VERSION_KEY:key值
|
||||||
|
*/
|
||||||
|
const VERSION_KEY = '__wangyiyun_version__'
|
||||||
|
// 获取版本号
|
||||||
|
export function getVersion() {
|
||||||
|
let version = storage.get(VERSION_KEY, null)
|
||||||
|
return Array.isArray(version) ? null : version
|
||||||
|
}
|
||||||
|
// 修改版本号
|
||||||
|
export function setVersion(version) {
|
||||||
|
storage.set(VERSION_KEY, version)
|
||||||
|
return version
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 音量
|
||||||
|
* @type VOLUME_KEY:key值
|
||||||
|
*/
|
||||||
|
const VOLUME_KEY = '__wangyiyun_volume__'
|
||||||
|
// 获取音量
|
||||||
|
export function getVolume() {
|
||||||
|
const volume = storage.get(VOLUME_KEY, WYYPLAYER_CONFIG.VOLUME)
|
||||||
|
return Number(volume)
|
||||||
|
}
|
||||||
|
// 修改音量
|
||||||
|
export function setVolume(volume) {
|
||||||
|
storage.set(VOLUME_KEY, volume)
|
||||||
|
return volume
|
||||||
|
}
|
107
src/utils/util.js
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
// 随机排序数组/洗牌函数 https://github.com/lodash/lodash/blob/master/shuffle.js
|
||||||
|
function copyArray(source, array) {
|
||||||
|
let index = -1
|
||||||
|
const length = source.length
|
||||||
|
array || (array = new Array(length))
|
||||||
|
while (++index < length) {
|
||||||
|
array[index] = source[index]
|
||||||
|
}
|
||||||
|
return array
|
||||||
|
}
|
||||||
|
|
||||||
|
export const randomSortArray = function shuffle(array) {
|
||||||
|
const length = array == null ? 0 : array.length
|
||||||
|
if (!length) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
let index = -1
|
||||||
|
const lastIndex = length - 1
|
||||||
|
const result = copyArray(array)
|
||||||
|
while (++index < length) {
|
||||||
|
const rand = index + Math.floor(Math.random() * (lastIndex - index + 1))
|
||||||
|
const value = result[rand]
|
||||||
|
result[rand] = result[index]
|
||||||
|
result[index] = value
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// 防抖函数
|
||||||
|
export function debounce(func, delay) {
|
||||||
|
let timer
|
||||||
|
return function (...args) {
|
||||||
|
if (timer) {
|
||||||
|
clearTimeout(timer)
|
||||||
|
}
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
func.apply(this, args)
|
||||||
|
}, delay)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 补0函数
|
||||||
|
export function addZero(s) {
|
||||||
|
return s < 10 ? '0' + s : s
|
||||||
|
}
|
||||||
|
|
||||||
|
// 歌词解析
|
||||||
|
const timeExp = /\[(\d{2,}):(\d{2})(?:\.(\d{2,3}))?]/g
|
||||||
|
export function parseLyric(lrc) {
|
||||||
|
const lines = lrc.split('\n')
|
||||||
|
const lyric = []
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = lines[i]
|
||||||
|
const result = timeExp.exec(line)
|
||||||
|
if (!result) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const text = line.replace(timeExp, '').trim()
|
||||||
|
if (text) {
|
||||||
|
lyric.push({
|
||||||
|
time: (result[1] * 6e4 + result[2] * 1e3 + (result[3] || 0) * 1) / 1e3,
|
||||||
|
text,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lyric
|
||||||
|
}
|
||||||
|
|
||||||
|
// 时间格式化
|
||||||
|
export function format(value) {
|
||||||
|
let minute = Math.floor(value / 60)
|
||||||
|
let second = Math.floor(value % 60)
|
||||||
|
return `${addZero(minute)}:${addZero(second)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://github.com/videojs/video.js/blob/master/src/js/utils/promise.js
|
||||||
|
* Silence a Promise-like object.
|
||||||
|
*
|
||||||
|
* This is useful for avoiding non-harmful, but potentially confusing "uncaught
|
||||||
|
* play promise" rejection error messages.
|
||||||
|
*
|
||||||
|
* @param {Object} value
|
||||||
|
* An object that may or may not be `Promise`-like.
|
||||||
|
*/
|
||||||
|
export function isPromise(v) {
|
||||||
|
return v !== undefined && v !== null && typeof v.then === 'function'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function silencePromise(value) {
|
||||||
|
if (isPromise(value)) {
|
||||||
|
value.then(null, () => {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断 string 类型
|
||||||
|
export function isString(v) {
|
||||||
|
return typeof v === 'string'
|
||||||
|
}
|
||||||
|
|
||||||
|
// http 链接转化成 https
|
||||||
|
export function toHttps(url) {
|
||||||
|
if (!isString(url)) {
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
return url.replace('http://', 'https://')
|
||||||
|
}
|
62
vue.config.js
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
const { defineConfig } = require('@vue/cli-service')
|
||||||
|
const path = require('path')
|
||||||
|
const dayjs = require('dayjs')
|
||||||
|
|
||||||
|
function resolve(dir) {
|
||||||
|
return path.join(__dirname, dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
const isEnvProduction = process.env.NODE_ENV === 'production'
|
||||||
|
|
||||||
|
// 注入版本信息
|
||||||
|
process.env.VUE_APP_VERSION = require('./package.json').version
|
||||||
|
// 注入版本更新时间
|
||||||
|
process.env.VUE_APP_UPDATE_TIME = dayjs().locale('zh-cn').format('YYYY-mm-DD')
|
||||||
|
|
||||||
|
module.exports = defineConfig({
|
||||||
|
publicPath: '',
|
||||||
|
chainWebpack(config) {
|
||||||
|
config.resolve.alias
|
||||||
|
.set('api', resolve('src/api'))
|
||||||
|
.set('assets', resolve('src/assets'))
|
||||||
|
.set('base', resolve('src/base'))
|
||||||
|
.set('components', resolve('src/components'))
|
||||||
|
.set('pages', resolve('src/pages'))
|
||||||
|
config.plugin('html').tap((args) => {
|
||||||
|
if (isEnvProduction) {
|
||||||
|
if (!args[0].minify) {
|
||||||
|
/* 参考 https://github.com/jantimon/html-webpack-plugin#minification */
|
||||||
|
args[0].minify = {
|
||||||
|
collapseWhitespace: true,
|
||||||
|
keepClosingSlash: true,
|
||||||
|
removeComments: true,
|
||||||
|
removeRedundantAttributes: true,
|
||||||
|
removeScriptTypeAttributes: true,
|
||||||
|
removeStyleLinkTypeAttributes: true,
|
||||||
|
useShortDoctype: true,
|
||||||
|
trimCustomFragments: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
args[0].minify.minifyJS = true
|
||||||
|
args[0].minify.minifyCSS = true
|
||||||
|
}
|
||||||
|
return args
|
||||||
|
})
|
||||||
|
},
|
||||||
|
pluginOptions: {
|
||||||
|
'style-resources-loader': {
|
||||||
|
preProcessor: 'less',
|
||||||
|
patterns: [resolve('src/styles/var.less'), resolve('src/styles/mixin.less')],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
devServer: {
|
||||||
|
port: 3001,
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: process.env.VUE_APP_DEV_API_URL,
|
||||||
|
changeOrigin: true,
|
||||||
|
pathRewrite: { '^/api': '' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|