├── .babelrc
├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .postcssrc.js
├── .travis.yml
├── README.md
├── build
├── build.js
├── check-versions.js
├── logo.png
├── utils.js
├── vue-loader.conf.js
├── webpack.base.conf.js
├── webpack.dev.conf.js
└── webpack.prod.conf.js
├── config
├── dev.env.js
├── index.js
└── prod.env.js
├── gif
└── GIF.gif
├── index.html
├── package-lock.json
├── package.json
├── src
├── App.vue
├── api
│ ├── config.js
│ ├── rank.js
│ ├── recommend.js
│ ├── search.js
│ ├── singer.js
│ └── song.js
├── base
│ ├── dialog
│ │ └── dialog.vue
│ ├── history-list
│ │ └── history-list.vue
│ ├── listView
│ │ └── listView.vue
│ ├── loading
│ │ ├── loading.gif
│ │ └── loading.vue
│ ├── progress-bar
│ │ └── progress-bar.vue
│ ├── progress-circle
│ │ └── progress-circle.vue
│ ├── scroll
│ │ └── scroll.vue
│ ├── search-input
│ │ └── search-input.vue
│ ├── slider
│ │ └── slider.vue
│ ├── song-list
│ │ └── song-list.vue
│ └── top-tip
│ │ └── top-tip.vue
├── common
│ ├── fonts
│ │ ├── music-icon.eot
│ │ ├── music-icon.svg
│ │ ├── music-icon.ttf
│ │ └── music-icon.woff
│ ├── image
│ │ └── default.png
│ ├── js
│ │ ├── cache.js
│ │ ├── config.js
│ │ ├── domUtil.js
│ │ ├── jsonp.js
│ │ ├── mixin.js
│ │ ├── singer.js
│ │ ├── song.js
│ │ ├── uid.js
│ │ └── util.js
│ └── less
│ │ ├── base.less
│ │ ├── icon.less
│ │ ├── index.less
│ │ ├── mixin.less
│ │ ├── reset.less
│ │ └── variable.less
├── components
│ ├── disc-detail
│ │ └── disc-detail.vue
│ ├── header
│ │ └── header.vue
│ ├── music-list
│ │ └── music-list.vue
│ ├── play-list
│ │ └── play-list.vue
│ ├── player
│ │ └── player.vue
│ ├── rank-detail
│ │ └── rank-detail.vue
│ ├── rank
│ │ └── rank.vue
│ ├── recommend
│ │ └── recommend.vue
│ ├── search
│ │ └── search.vue
│ ├── singer-detail
│ │ └── singer-detail.vue
│ ├── singer
│ │ └── singer.vue
│ ├── suggest
│ │ └── suggest.vue
│ └── tab
│ │ └── tab.vue
├── main.js
├── router
│ └── index.js
└── store
│ ├── actions.js
│ ├── getters.js
│ ├── index.js
│ ├── mutation-types.js
│ ├── mutations.js
│ └── state.js
└── static
└── .gitkeep
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | ["env", {
4 | "modules": false,
5 | "targets": {
6 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
7 | }
8 | }],
9 | "stage-2"
10 | ],
11 | "plugins": ["transform-vue-jsx", "transform-runtime"]
12 | }
13 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | /build/
2 | /config/
3 | /dist/
4 | /*.js
5 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | // https://eslint.org/docs/user-guide/configuring
2 |
3 | module.exports = {
4 | root: true,
5 | parserOptions: {
6 | parser: 'babel-eslint'
7 | },
8 | env: {
9 | browser: true,
10 | },
11 | extends: [
12 | // https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention
13 | // consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules.
14 | 'plugin:vue/essential',
15 | // https://github.com/standard/standard/blob/master/docs/RULES-en.md
16 | 'standard'
17 | ],
18 | // required to lint *.vue files
19 | plugins: [
20 | 'vue'
21 | ],
22 | // add your custom rules here
23 | rules: {
24 | // allow async-await
25 | 'generator-star-spacing': 'off',
26 | // allow debugger during development
27 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
28 | 'semi': ['error', 'always'],
29 | 'indent': 0,
30 | 'space-before-function-paren': 0
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules/
3 | /dist/
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Editor directories and files
9 | .idea
10 | .vscode
11 | *.suo
12 | *.ntvs*
13 | *.njsproj
14 | *.sln
15 |
--------------------------------------------------------------------------------
/.postcssrc.js:
--------------------------------------------------------------------------------
1 | // https://github.com/michael-ciniawsky/postcss-load-config
2 |
3 | module.exports = {
4 | "plugins": {
5 | "postcss-import": {},
6 | "postcss-url": {},
7 | // to edit target browsers: use "browserslist" field in package.json
8 | "autoprefixer": {}
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | # nodejs版本
3 | node_js:
4 | - '6'
5 | # Travis-CI Caching
6 | cache:
7 | directories:
8 | - node_modules
9 | # S: Build Lifecycle
10 | install:
11 | - npm install
12 | before_script:
13 | # 无其他依赖项所以执行npm run build 构建就行了
14 | script:
15 | - npm run build
16 | after_script:
17 | - cd ./dist
18 | - git init
19 | - git config user.name "${U_NAME}"
20 | - git config user.email "${U_EMAIL}"
21 | - git add .
22 | - git commit -m "Update tools"
23 | - git push --force --quiet "https://${GH_TOKEN}@${GH_REF}" master:${P_BRANCH}
24 | # E: Build LifeCycle
25 | branches:
26 | only:
27 | - master
28 | env:
29 | global:
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
Cool Music
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | [](https://github.com/anuraghazra/github-readme-stats)
15 |
16 | ## 项目简介
17 |
18 | 入坑 Vue 以来,一直苦没有真正的实战项目来提升自己,所以在工作之余开发了一个基于 Vue2.0+版本的音乐 WebAPP。项目构建初期也在 github 上参考了众多案例,通过学习并阅读他人的成熟代码进而提升自己,历时一个半月完成了 Cool Music 这个项目。我希望把这个项目的源代码开放出来,来帮助更多的前端爱好者们,大家共同进步,共同提升。
19 |
20 | ## 技术栈
21 |
22 | 1. Vue 相关:Vue、Vue-router、Vue-lazyload。
23 | 2. 脚手架:Vue-cli。
24 | 3. 状态管理:Vuex。
25 | 4. 构建工具:Webpack。
26 | 5. CSS 预处理:Less。
27 | 6. 移动端插件:better-scroll、fastclick。
28 | 7. 数据获取:axios。
29 | 8. 代码风格规范工具:ESLint。
30 | 9. JavaScript 语法标准:ECMAScript 2015。
31 | 10. 动画相关:create-keyframe-animation。
32 | 11. 本地存储:Store.js
33 |
34 | ## 实现的功能
35 |
36 | 1. 轮播图功能的实现。
37 | 2. 歌单、歌曲列表的展现。
38 | 3. 音乐的播放、暂停、快进、快退和同步歌词的实现。
39 | 4. 歌手列表通讯录的实现。
40 | 5. 搜索功能、热门搜索和保存历史记录功能的实现。
41 | 6. 收藏歌曲,个人相关功能的实现。
42 | 7. 上拉加载,页面侧滑。播放器收起和弹出等动画的实现。
43 |
44 | ## 构建步骤
45 |
46 | ```bash
47 | # 安装依赖库
48 | npm install
49 |
50 | # 启动webpack内置服务
51 | npm run dev
52 |
53 | # 项目构建
54 | npm run build
55 |
56 | # 生产打包并查看报告
57 | npm run build --report
58 | ```
59 |
60 | ## 项目预览
61 |
62 |
63 |
64 | ## 写在最后
65 |
66 | 如果大家觉得还行,能否帮忙 Star 下?
67 |
68 | :kissing_heart::kissing_heart::kissing_heart:
69 |
--------------------------------------------------------------------------------
/build/build.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | require('./check-versions')()
3 |
4 | process.env.NODE_ENV = 'production'
5 |
6 | const ora = require('ora')
7 | const rm = require('rimraf')
8 | const path = require('path')
9 | const chalk = require('chalk')
10 | const webpack = require('webpack')
11 | const config = require('../config')
12 | const webpackConfig = require('./webpack.prod.conf')
13 |
14 | const spinner = ora('building for production...')
15 | spinner.start()
16 |
17 | rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
18 | if (err) throw err
19 | webpack(webpackConfig, (err, stats) => {
20 | spinner.stop()
21 | if (err) throw err
22 | process.stdout.write(stats.toString({
23 | colors: true,
24 | modules: false,
25 | children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build.
26 | chunks: false,
27 | chunkModules: false
28 | }) + '\n\n')
29 |
30 | if (stats.hasErrors()) {
31 | console.log(chalk.red(' Build failed with errors.\n'))
32 | process.exit(1)
33 | }
34 |
35 | console.log(chalk.cyan(' Build complete.\n'))
36 | console.log(chalk.yellow(
37 | ' Tip: built files are meant to be served over an HTTP server.\n' +
38 | ' Opening index.html over file:// won\'t work.\n'
39 | ))
40 | })
41 | })
42 |
--------------------------------------------------------------------------------
/build/check-versions.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const chalk = require('chalk')
3 | const semver = require('semver')
4 | const packageConfig = require('../package.json')
5 | const shell = require('shelljs')
6 |
7 | function exec (cmd) {
8 | return require('child_process').execSync(cmd).toString().trim()
9 | }
10 |
11 | const versionRequirements = [
12 | {
13 | name: 'node',
14 | currentVersion: semver.clean(process.version),
15 | versionRequirement: packageConfig.engines.node
16 | }
17 | ]
18 |
19 | if (shell.which('npm')) {
20 | versionRequirements.push({
21 | name: 'npm',
22 | currentVersion: exec('npm --version'),
23 | versionRequirement: packageConfig.engines.npm
24 | })
25 | }
26 |
27 | module.exports = function () {
28 | const warnings = []
29 |
30 | for (let i = 0; i < versionRequirements.length; i++) {
31 | const mod = versionRequirements[i]
32 |
33 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
34 | warnings.push(mod.name + ': ' +
35 | chalk.red(mod.currentVersion) + ' should be ' +
36 | chalk.green(mod.versionRequirement)
37 | )
38 | }
39 | }
40 |
41 | if (warnings.length) {
42 | console.log('')
43 | console.log(chalk.yellow('To use this template, you must update following to modules:'))
44 | console.log()
45 |
46 | for (let i = 0; i < warnings.length; i++) {
47 | const warning = warnings[i]
48 | console.log(' ' + warning)
49 | }
50 |
51 | console.log()
52 | process.exit(1)
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/build/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/facebesidewyj/cool-music/2a92f69de9d41fc380e07feda9db93fccb84088e/build/logo.png
--------------------------------------------------------------------------------
/build/utils.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const path = require('path')
3 | const config = require('../config')
4 | const ExtractTextPlugin = require('extract-text-webpack-plugin')
5 | const packageConfig = require('../package.json')
6 |
7 | exports.assetsPath = function (_path) {
8 | const assetsSubDirectory = process.env.NODE_ENV === 'production'
9 | ? config.build.assetsSubDirectory
10 | : config.dev.assetsSubDirectory
11 |
12 | return path.posix.join(assetsSubDirectory, _path)
13 | }
14 |
15 | exports.cssLoaders = function (options) {
16 | options = options || {}
17 |
18 | const cssLoader = {
19 | loader: 'css-loader',
20 | options: {
21 | sourceMap: options.sourceMap
22 | }
23 | }
24 |
25 | const postcssLoader = {
26 | loader: 'postcss-loader',
27 | options: {
28 | sourceMap: options.sourceMap
29 | }
30 | }
31 |
32 | // generate loader string to be used with extract text plugin
33 | function generateLoaders (loader, loaderOptions) {
34 | const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader]
35 |
36 | if (loader) {
37 | loaders.push({
38 | loader: loader + '-loader',
39 | options: Object.assign({}, loaderOptions, {
40 | sourceMap: options.sourceMap
41 | })
42 | })
43 | }
44 |
45 | // Extract CSS when that option is specified
46 | // (which is the case during production build)
47 | if (options.extract) {
48 | return ExtractTextPlugin.extract({
49 | use: loaders,
50 | fallback: 'vue-style-loader'
51 | })
52 | } else {
53 | return ['vue-style-loader'].concat(loaders)
54 | }
55 | }
56 |
57 | // https://vue-loader.vuejs.org/en/configurations/extract-css.html
58 | return {
59 | css: generateLoaders(),
60 | postcss: generateLoaders(),
61 | less: generateLoaders('less'),
62 | sass: generateLoaders('sass', { indentedSyntax: true }),
63 | scss: generateLoaders('sass'),
64 | stylus: generateLoaders('stylus'),
65 | styl: generateLoaders('stylus')
66 | }
67 | }
68 |
69 | // Generate loaders for standalone style files (outside of .vue)
70 | exports.styleLoaders = function (options) {
71 | const output = []
72 | const loaders = exports.cssLoaders(options)
73 |
74 | for (const extension in loaders) {
75 | const loader = loaders[extension]
76 | output.push({
77 | test: new RegExp('\\.' + extension + '$'),
78 | use: loader
79 | })
80 | }
81 |
82 | return output
83 | }
84 |
85 | exports.createNotifierCallback = () => {
86 | const notifier = require('node-notifier')
87 |
88 | return (severity, errors) => {
89 | if (severity !== 'error') return
90 |
91 | const error = errors[0]
92 | const filename = error.file && error.file.split('!').pop()
93 |
94 | notifier.notify({
95 | title: packageConfig.name,
96 | message: severity + ': ' + error.name,
97 | subtitle: filename || '',
98 | icon: path.join(__dirname, 'logo.png')
99 | })
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/build/vue-loader.conf.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const utils = require('./utils')
3 | const config = require('../config')
4 | const isProduction = process.env.NODE_ENV === 'production'
5 | const sourceMapEnabled = isProduction
6 | ? config.build.productionSourceMap
7 | : config.dev.cssSourceMap
8 |
9 | module.exports = {
10 | loaders: utils.cssLoaders({
11 | sourceMap: sourceMapEnabled,
12 | extract: isProduction
13 | }),
14 | cssSourceMap: sourceMapEnabled,
15 | cacheBusting: config.dev.cacheBusting,
16 | transformToRequire: {
17 | video: ['src', 'poster'],
18 | source: 'src',
19 | img: 'src',
20 | image: 'xlink:href'
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/build/webpack.base.conf.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const path = require('path')
3 | const utils = require('./utils')
4 | const config = require('../config')
5 | const vueLoaderConfig = require('./vue-loader.conf')
6 |
7 | function resolve (dir) {
8 | return path.join(__dirname, '..', dir)
9 | }
10 |
11 | const createLintingRule = () => ({
12 | test: /\.(js|vue)$/,
13 | loader: 'eslint-loader',
14 | enforce: 'pre',
15 | include: [resolve('src'), resolve('test')],
16 | options: {
17 | formatter: require('eslint-friendly-formatter'),
18 | emitWarning: !config.dev.showEslintErrorsInOverlay
19 | }
20 | })
21 |
22 | module.exports = {
23 | context: path.resolve(__dirname, '../'),
24 | entry: {
25 | app: './src/main.js'
26 | },
27 | output: {
28 | path: config.build.assetsRoot,
29 | filename: '[name].js',
30 | publicPath: process.env.NODE_ENV === 'production'
31 | ? config.build.assetsPublicPath
32 | : config.dev.assetsPublicPath
33 | },
34 | resolve: {
35 | extensions: ['.js', '.vue', '.json'],
36 | alias: {
37 | 'vue$': 'vue/dist/vue.esm.js',
38 | '@': resolve('src'),
39 | 'common': resolve('src/common'),
40 | 'components': resolve('src/components'),
41 | 'api': resolve('src/api'),
42 | 'base': resolve('src/base')
43 | }
44 | },
45 | module: {
46 | rules: [
47 | ...(config.dev.useEslint ? [createLintingRule()] : []),
48 | {
49 | test: /\.vue$/,
50 | loader: 'vue-loader',
51 | options: vueLoaderConfig
52 | },
53 | {
54 | test: /\.js$/,
55 | loader: 'babel-loader',
56 | include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')]
57 | },
58 | {
59 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
60 | loader: 'url-loader',
61 | options: {
62 | limit: 10000,
63 | name: utils.assetsPath('img/[name].[hash:7].[ext]')
64 | }
65 | },
66 | {
67 | test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
68 | loader: 'url-loader',
69 | options: {
70 | limit: 10000,
71 | name: utils.assetsPath('media/[name].[hash:7].[ext]')
72 | }
73 | },
74 | {
75 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
76 | loader: 'url-loader',
77 | options: {
78 | limit: 10000,
79 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
80 | }
81 | }
82 | ]
83 | },
84 | node: {
85 | // prevent webpack from injecting useless setImmediate polyfill because Vue
86 | // source contains it (although only uses it if it's native).
87 | setImmediate: false,
88 | // prevent webpack from injecting mocks to Node native modules
89 | // that does not make sense for the client
90 | dgram: 'empty',
91 | fs: 'empty',
92 | net: 'empty',
93 | tls: 'empty',
94 | child_process: 'empty'
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/build/webpack.dev.conf.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const utils = require('./utils');
3 | const webpack = require('webpack');
4 | const config = require('../config');
5 | const merge = require('webpack-merge');
6 | const path = require('path');
7 | const baseWebpackConfig = require('./webpack.base.conf');
8 | const CopyWebpackPlugin = require('copy-webpack-plugin');
9 | const HtmlWebpackPlugin = require('html-webpack-plugin');
10 | const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin');
11 | const portfinder = require('portfinder');
12 | const axios = require('axios');
13 |
14 | const HOST = process.env.HOST;
15 | const PORT = process.env.PORT && Number(process.env.PORT);
16 |
17 | const devWebpackConfig = merge(baseWebpackConfig, {
18 | module: {
19 | rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true })
20 | },
21 | // cheap-module-eval-source-map is faster for development
22 | devtool: config.dev.devtool,
23 |
24 | // these devServer options should be customized in /config/index.js
25 | devServer: {
26 | // axios通过node发送https请求欺骗服务器
27 | before(app) {
28 | // 获得推荐歌单
29 | app.get('/api/getDiscList', function(req, res) {
30 | const url = 'https://c.y.qq.com/splcloud/fcgi-bin/fcg_get_diss_by_tag.fcg';
31 | axios.get(url, {
32 | headers: {
33 | referer: 'https://c.y.qq.com/',
34 | host: 'c.y.qq.com'
35 | },
36 | params: req.query
37 | }).then((response) => {
38 | res.json(response.data);
39 | }).catch((e) => {
40 | console.log(e);
41 | });
42 | });
43 |
44 | // 获得轮播图数据
45 | app.get('/api/getRecommend', function(req, res) {
46 | const url = 'https://c.y.qq.com/musichall/fcgi-bin/fcg_yqqhomepagerecommend.fcg';
47 | axios.get(url, {
48 | headers: {
49 | referer: 'https://c.y.qq.com/',
50 | host: 'c.y.qq.com'
51 | },
52 | params: req.query
53 | }).then((response) => {
54 | res.json(response.data);
55 | }).catch((e) => {
56 | console.log(e);
57 | });
58 | });
59 |
60 | // 获得歌手列表数据
61 | app.get('/api/getSingers', function(req, res) {
62 | const url = 'https://c.y.qq.com/v8/fcg-bin/v8.fcg';
63 | axios.get(url, {
64 | headers: {
65 | referer: 'https://c.y.qq.com/',
66 | host: 'c.y.qq.com'
67 | },
68 | params: req.query
69 | }).then((response) => {
70 | res.json(response.data);
71 | }).catch((e) => {
72 | console.log(e);
73 | });
74 | });
75 |
76 | // 获得歌手对应的歌曲列表
77 | app.get('/api/getSingerSongs', function(req, res) {
78 | const url = 'https://c.y.qq.com/v8/fcg-bin/fcg_v8_singer_track_cp.fcg';
79 | axios.get(url, {
80 | headers: {
81 | referer: 'https://c.y.qq.com/',
82 | host: 'c.y.qq.com'
83 | },
84 | params: req.query
85 | }).then((response) => {
86 | res.json(response.data);
87 | }).catch((e) => {
88 | console.log(e);
89 | });
90 | });
91 |
92 | // 获取合法的播放权限
93 | app.get('/api/getVKey', function(req, res) {
94 | const url = 'https://c.y.qq.com/base/fcgi-bin/fcg_music_express_mobile3.fcg';
95 | axios.get(url, {
96 | headers: {
97 | referer: 'https://c.y.qq.com/',
98 | host: 'c.y.qq.com'
99 | },
100 | params: req.query
101 | }).then((response) => {
102 | res.json(response.data);
103 | }).catch((e) => {
104 | console.log(e);
105 | });
106 | });
107 |
108 | // 获取歌词
109 | app.get('/api/getLyric', function(req, res) {
110 | const url = 'https://c.y.qq.com/lyric/fcgi-bin/fcg_query_lyric_new.fcg';
111 | axios.get(url, {
112 | headers: {
113 | referer: 'https://c.y.qq.com/',
114 | host: 'c.y.qq.com'
115 | },
116 | params: req.query
117 | }).then((response) => {
118 | let ret = response.data;
119 | if (typeof ret === 'string') {
120 | const reg = /^\w+\(({.+})\)$/;
121 | const matches = ret.match(reg);
122 | if (matches) {
123 | ret = JSON.parse(matches[1]);
124 | }
125 | }
126 | res.json(ret);
127 | }).catch((e) => {
128 | console.log(e);
129 | });
130 | });
131 |
132 | // 获取歌单详情
133 | app.get('/api/getDiscDetail', function(req, res) {
134 | const url = 'https://c.y.qq.com/qzone/fcg-bin/fcg_ucc_getcdinfo_byids_cp.fcg';
135 | axios.get(url, {
136 | headers: {
137 | referer: 'https://c.y.qq.com/',
138 | host: 'c.y.qq.com'
139 | },
140 | params: req.query
141 | }).then((response) => {
142 | let ret = response.data;
143 | if (typeof ret === 'string') {
144 | const reg = /^\w+\(({.+})\)$/;
145 | const matches = ret.match(reg);
146 | if (matches) {
147 | ret = JSON.parse(matches[1]);
148 | }
149 | }
150 | res.json(ret);
151 | }).catch((e) => {
152 | console.log(e);
153 | });
154 | });
155 |
156 | // 获取榜单列表
157 | app.get('/api/getRankList', function(req, res) {
158 | const url = 'https://c.y.qq.com/v8/fcg-bin/fcg_myqq_toplist.fcg';
159 | axios.get(url, {
160 | headers: {
161 | referer: 'https://c.y.qq.com/',
162 | host: 'c.y.qq.com'
163 | },
164 | params: req.query
165 | }).then((response) => {
166 | let ret = response.data;
167 | if (typeof ret === 'string') {
168 | const reg = /^\w+\(({.+})\)$/;
169 | const matches = ret.match(reg);
170 | if (matches) {
171 | ret = JSON.parse(matches[1]);
172 | }
173 | }
174 | res.json(ret);
175 | }).catch((e) => {
176 | console.log(e);
177 | });
178 | });
179 |
180 | // 获取榜单内的歌曲列表
181 | app.get('/api/getRankDetail', function(req, res) {
182 | const url = 'https://c.y.qq.com/v8/fcg-bin/fcg_v8_toplist_cp.fcg';
183 | axios.get(url, {
184 | headers: {
185 | referer: 'https://c.y.qq.com/',
186 | host: 'c.y.qq.com'
187 | },
188 | params: req.query
189 | }).then((response) => {
190 | let ret = response.data;
191 | if (typeof ret === 'string') {
192 | const reg = /^\w+\(({.+})\)$/;
193 | const matches = ret.match(reg);
194 | if (matches) {
195 | ret = JSON.parse(matches[1]);
196 | }
197 | }
198 | res.json(ret);
199 | }).catch((e) => {
200 | console.log(e);
201 | });
202 | });
203 |
204 | // 获取搜索热词
205 | app.get('/api/getHotWord', function(req, res) {
206 | const url = 'https://c.y.qq.com/splcloud/fcgi-bin/gethotkey.fcg';
207 | axios.get(url, {
208 | headers: {
209 | referer: 'https://c.y.qq.com/',
210 | host: 'c.y.qq.com'
211 | },
212 | params: req.query
213 | }).then((response) => {
214 | let ret = response.data;
215 | if (typeof ret === 'string') {
216 | const reg = /^\w+\(({.+})\)$/;
217 | const matches = ret.match(reg);
218 | if (matches) {
219 | ret = JSON.parse(matches[1]);
220 | }
221 | }
222 | res.json(ret);
223 | }).catch((e) => {
224 | console.log(e);
225 | });
226 | });
227 |
228 | // 搜索接口
229 | app.get('/api/searchWord', function(req, res) {
230 | const url = 'https://c.y.qq.com/soso/fcgi-bin/search_for_qq_cp';
231 | axios.get(url, {
232 | headers: {
233 | referer: 'https://c.y.qq.com/',
234 | host: 'c.y.qq.com'
235 | },
236 | params: req.query
237 | }).then((response) => {
238 | let ret = response.data;
239 | if (typeof ret === 'string') {
240 | const reg = /^\w+\(({.+})\)$/;
241 | const matches = ret.match(reg);
242 | if (matches) {
243 | ret = JSON.parse(matches[1]);
244 | }
245 | }
246 | res.json(ret);
247 | }).catch((e) => {
248 | console.log(e);
249 | });
250 | });
251 | },
252 | clientLogLevel: 'warning',
253 | historyApiFallback: {
254 | rewrites: [
255 | { from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') }
256 | ]
257 | },
258 | hot: true,
259 | contentBase: false, // since we use CopyWebpackPlugin.
260 | compress: true,
261 | host: HOST || config.dev.host,
262 | port: PORT || config.dev.port,
263 | open: config.dev.autoOpenBrowser,
264 | overlay: config.dev.errorOverlay
265 | ? { warnings: false, errors: true }
266 | : false,
267 | publicPath: config.dev.assetsPublicPath,
268 | proxy: config.dev.proxyTable,
269 | quiet: true, // necessary for FriendlyErrorsPlugin
270 | watchOptions: {
271 | poll: config.dev.poll
272 | }
273 | },
274 | plugins: [
275 | new webpack.DefinePlugin({
276 | 'process.env': require('../config/dev.env')
277 | }),
278 | new webpack.HotModuleReplacementPlugin(),
279 | new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update.
280 | new webpack.NoEmitOnErrorsPlugin(),
281 | // https://github.com/ampedandwired/html-webpack-plugin
282 | new HtmlWebpackPlugin({
283 | filename: 'index.html',
284 | template: 'index.html',
285 | inject: true
286 | }),
287 | // copy custom static assets
288 | new CopyWebpackPlugin([{
289 | from: path.resolve(__dirname, '../static'),
290 | to: config.dev.assetsSubDirectory,
291 | ignore: ['.*']
292 | }])
293 | ]
294 | });
295 |
296 | module.exports = new Promise((resolve, reject) => {
297 | portfinder.basePort = process.env.PORT || config.dev.port;
298 | portfinder.getPort((err, port) => {
299 | if (err) {
300 | reject(err);
301 | } else {
302 | // publish the new Port, necessary for e2e tests
303 | process.env.PORT = port;
304 | // add port to devServer config
305 | devWebpackConfig.devServer.port = port;
306 |
307 | // Add FriendlyErrorsPlugin
308 | devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({
309 | compilationSuccessInfo: {
310 | messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`]
311 | },
312 | onErrors: config.dev.notifyOnErrors
313 | ? utils.createNotifierCallback()
314 | : undefined
315 | }));
316 |
317 | resolve(devWebpackConfig);
318 | }
319 | });
320 | });
321 |
--------------------------------------------------------------------------------
/build/webpack.prod.conf.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const path = require('path')
3 | const utils = require('./utils')
4 | const webpack = require('webpack')
5 | const config = require('../config')
6 | const merge = require('webpack-merge')
7 | const baseWebpackConfig = require('./webpack.base.conf')
8 | const CopyWebpackPlugin = require('copy-webpack-plugin')
9 | const HtmlWebpackPlugin = require('html-webpack-plugin')
10 | const ExtractTextPlugin = require('extract-text-webpack-plugin')
11 | const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
12 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
13 |
14 | const env = require('../config/prod.env')
15 |
16 | const webpackConfig = merge(baseWebpackConfig, {
17 | module: {
18 | rules: utils.styleLoaders({
19 | sourceMap: config.build.productionSourceMap,
20 | extract: true,
21 | usePostCSS: true
22 | })
23 | },
24 | devtool: config.build.productionSourceMap ? config.build.devtool : false,
25 | output: {
26 | path: config.build.assetsRoot,
27 | filename: utils.assetsPath('js/[name].[chunkhash].js'),
28 | chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
29 | },
30 | plugins: [
31 | // http://vuejs.github.io/vue-loader/en/workflow/production.html
32 | new webpack.DefinePlugin({
33 | 'process.env': env
34 | }),
35 | new UglifyJsPlugin({
36 | uglifyOptions: {
37 | compress: {
38 | warnings: false
39 | }
40 | },
41 | sourceMap: config.build.productionSourceMap,
42 | parallel: true
43 | }),
44 | // extract css into its own file
45 | new ExtractTextPlugin({
46 | filename: utils.assetsPath('css/[name].[contenthash].css'),
47 | // Setting the following option to `false` will not extract CSS from codesplit chunks.
48 | // Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack.
49 | // It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`,
50 | // increasing file size: https://github.com/vuejs-templates/webpack/issues/1110
51 | allChunks: true,
52 | }),
53 | // Compress extracted CSS. We are using this plugin so that possible
54 | // duplicated CSS from different components can be deduped.
55 | new OptimizeCSSPlugin({
56 | cssProcessorOptions: config.build.productionSourceMap
57 | ? { safe: true, map: { inline: false } }
58 | : { safe: true }
59 | }),
60 | // generate dist index.html with correct asset hash for caching.
61 | // you can customize output by editing /index.html
62 | // see https://github.com/ampedandwired/html-webpack-plugin
63 | new HtmlWebpackPlugin({
64 | filename: config.build.index,
65 | template: 'index.html',
66 | inject: true,
67 | minify: {
68 | removeComments: true,
69 | collapseWhitespace: true,
70 | removeAttributeQuotes: true
71 | // more options:
72 | // https://github.com/kangax/html-minifier#options-quick-reference
73 | },
74 | // necessary to consistently work with multiple chunks via CommonsChunkPlugin
75 | chunksSortMode: 'dependency'
76 | }),
77 | // keep module.id stable when vendor modules does not change
78 | new webpack.HashedModuleIdsPlugin(),
79 | // enable scope hoisting
80 | new webpack.optimize.ModuleConcatenationPlugin(),
81 | // split vendor js into its own file
82 | new webpack.optimize.CommonsChunkPlugin({
83 | name: 'vendor',
84 | minChunks (module) {
85 | // any required modules inside node_modules are extracted to vendor
86 | return (
87 | module.resource &&
88 | /\.js$/.test(module.resource) &&
89 | module.resource.indexOf(
90 | path.join(__dirname, '../node_modules')
91 | ) === 0
92 | )
93 | }
94 | }),
95 | // extract webpack runtime and module manifest to its own file in order to
96 | // prevent vendor hash from being updated whenever app bundle is updated
97 | new webpack.optimize.CommonsChunkPlugin({
98 | name: 'manifest',
99 | minChunks: Infinity
100 | }),
101 | // This instance extracts shared chunks from code splitted chunks and bundles them
102 | // in a separate chunk, similar to the vendor chunk
103 | // see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk
104 | new webpack.optimize.CommonsChunkPlugin({
105 | name: 'app',
106 | async: 'vendor-async',
107 | children: true,
108 | minChunks: 3
109 | }),
110 |
111 | // copy custom static assets
112 | new CopyWebpackPlugin([
113 | {
114 | from: path.resolve(__dirname, '../static'),
115 | to: config.build.assetsSubDirectory,
116 | ignore: ['.*']
117 | }
118 | ])
119 | ]
120 | })
121 |
122 | if (config.build.productionGzip) {
123 | const CompressionWebpackPlugin = require('compression-webpack-plugin')
124 |
125 | webpackConfig.plugins.push(
126 | new CompressionWebpackPlugin({
127 | asset: '[path].gz[query]',
128 | algorithm: 'gzip',
129 | test: new RegExp(
130 | '\\.(' +
131 | config.build.productionGzipExtensions.join('|') +
132 | ')$'
133 | ),
134 | threshold: 10240,
135 | minRatio: 0.8
136 | })
137 | )
138 | }
139 |
140 | if (config.build.bundleAnalyzerReport) {
141 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
142 | webpackConfig.plugins.push(new BundleAnalyzerPlugin())
143 | }
144 |
145 | module.exports = webpackConfig
146 |
--------------------------------------------------------------------------------
/config/dev.env.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const merge = require('webpack-merge')
3 | const prodEnv = require('./prod.env')
4 |
5 | module.exports = merge(prodEnv, {
6 | NODE_ENV: '"development"'
7 | })
8 |
--------------------------------------------------------------------------------
/config/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | // Template version: 1.3.1
3 | // see http://vuejs-templates.github.io/webpack for documentation.
4 |
5 | const path = require('path');
6 |
7 | module.exports = {
8 | dev: {
9 |
10 | // Paths
11 | assetsSubDirectory: 'static',
12 | assetsPublicPath: '/',
13 | proxyTable: {},
14 |
15 | // Various Dev Server settings
16 | host: '0.0.0.0', // can be overwritten by process.env.HOST
17 | port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined
18 | autoOpenBrowser: false,
19 | errorOverlay: true,
20 | notifyOnErrors: true,
21 | poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions-
22 |
23 | // Use Eslint Loader?
24 | // If true, your code will be linted during bundling and
25 | // linting errors and warnings will be shown in the console.
26 | useEslint: true,
27 | // If true, eslint errors and warnings will also be shown in the error overlay
28 | // in the browser.
29 | showEslintErrorsInOverlay: false,
30 |
31 | /**
32 | * Source Maps
33 | */
34 |
35 | // https://webpack.js.org/configuration/devtool/#development
36 | devtool: 'cheap-module-eval-source-map',
37 |
38 | // If you have problems debugging vue-files in devtools,
39 | // set this to false - it *may* help
40 | // https://vue-loader.vuejs.org/en/options.html#cachebusting
41 | cacheBusting: true,
42 |
43 | cssSourceMap: true
44 | },
45 |
46 | build: {
47 | // Template for index.html
48 | index: path.resolve(__dirname, '../dist/index.html'),
49 |
50 | // Paths
51 | assetsRoot: path.resolve(__dirname, '../dist'),
52 | assetsSubDirectory: 'static',
53 | assetsPublicPath: '/',
54 |
55 | /**
56 | * Source Maps
57 | */
58 |
59 | productionSourceMap: true,
60 | // https://webpack.js.org/configuration/devtool/#production
61 | devtool: '#source-map',
62 |
63 | // Gzip off by default as many popular static hosts such as
64 | // Surge or Netlify already gzip all static assets for you.
65 | // Before setting to `true`, make sure to:
66 | // npm install --save-dev compression-webpack-plugin
67 | productionGzip: false,
68 | productionGzipExtensions: ['js', 'css'],
69 |
70 | // Run the build command with an extra argument to
71 | // View the bundle analyzer report after build finishes:
72 | // `npm run build --report`
73 | // Set to `true` or `false` to always turn it on or off
74 | bundleAnalyzerReport: process.env.npm_config_report
75 | }
76 | };
77 |
--------------------------------------------------------------------------------
/config/prod.env.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | module.exports = {
3 | NODE_ENV: '"production"'
4 | }
5 |
--------------------------------------------------------------------------------
/gif/GIF.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/facebesidewyj/cool-music/2a92f69de9d41fc380e07feda9db93fccb84088e/gif/GIF.gif
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | vue-music
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue-music",
3 | "version": "1.0.0",
4 | "description": "基于Vue开发的音乐App",
5 | "author": "facebesidewyj ",
6 | "private": true,
7 | "scripts": {
8 | "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
9 | "start": "npm run dev",
10 | "lint": "eslint --ext .js,.vue src",
11 | "build": "node build/build.js"
12 | },
13 | "dependencies": {
14 | "axios": "^0.18.0",
15 | "babel-runtime": "^6.26.0",
16 | "better-scroll": "^1.8.4",
17 | "fastclick": "^1.0.6",
18 | "jsonp": "^0.2.1",
19 | "vue": "^2.5.13",
20 | "vue-lazyload": "^1.2.1",
21 | "vue-router": "^3.0.1",
22 | "vuex": "^3.0.1",
23 | "create-keyframe-animation": "^0.1.0",
24 | "js-base64": "^2.4.3",
25 | "lyric-parser": "^1.0.1",
26 | "store": "^2.0.12"
27 | },
28 | "devDependencies": {
29 | "autoprefixer": "^8.1.0",
30 | "babel-core": "^6.26.0",
31 | "babel-eslint": "^8.2.2",
32 | "babel-helper-vue-jsx-merge-props": "^2.0.3",
33 | "babel-loader": "^7.1.4",
34 | "babel-plugin-syntax-jsx": "^6.18.0",
35 | "babel-plugin-transform-runtime": "^6.23.0",
36 | "babel-plugin-transform-vue-jsx": "^3.7.0",
37 | "babel-polyfill": "^6.26.0",
38 | "babel-preset-env": "^1.6.1",
39 | "babel-preset-stage-2": "^6.24.1",
40 | "chalk": "^2.3.2",
41 | "copy-webpack-plugin": "^4.5.0",
42 | "css-loader": "^0.28.10",
43 | "eslint": "^4.18.2",
44 | "eslint-config-standard": "^11.0.0",
45 | "eslint-friendly-formatter": "^3.0.0",
46 | "eslint-loader": "^2.0.0",
47 | "eslint-plugin-import": "^2.9.0",
48 | "eslint-plugin-node": "^6.0.1",
49 | "eslint-plugin-promise": "^3.7.0",
50 | "eslint-plugin-standard": "^3.0.1",
51 | "eslint-plugin-vue": "^4.3.0",
52 | "extract-text-webpack-plugin": "^3.0.2",
53 | "file-loader": "^1.1.11",
54 | "friendly-errors-webpack-plugin": "^1.6.1",
55 | "html-webpack-plugin": "^3.0.6",
56 | "less": "^3.0.1",
57 | "less-loader": "^4.0.6",
58 | "node-notifier": "^5.2.1",
59 | "optimize-css-assets-webpack-plugin": "^4.0.0",
60 | "ora": "^2.0.0",
61 | "portfinder": "^1.0.13",
62 | "postcss-import": "^11.1.0",
63 | "postcss-loader": "^2.1.1",
64 | "postcss-url": "^7.3.1",
65 | "rimraf": "^2.6.2",
66 | "semver": "^5.5.0",
67 | "shelljs": "^0.8.1",
68 | "uglifyjs-webpack-plugin": "^1.2.2",
69 | "url-loader": "^1.0.1",
70 | "vue-loader": "^14.2.1",
71 | "vue-style-loader": "^4.0.2",
72 | "vue-template-compiler": "^2.5.13",
73 | "webpack": "^4.1.1",
74 | "webpack-bundle-analyzer": "^2.11.1",
75 | "webpack-dev-server": "^3.1.0",
76 | "webpack-merge": "^4.1.2"
77 | },
78 | "engines": {
79 | "node": ">= 6.0.0",
80 | "npm": ">= 3.0.0"
81 | },
82 | "browserslist": ["> 1%", "last 2 versions", "not ie <= 8"]
83 | }
84 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
25 |
26 |
29 |
--------------------------------------------------------------------------------
/src/api/config.js:
--------------------------------------------------------------------------------
1 | // 固定查询参数
2 | export const commonParams = {
3 | g_tk: 1928093487,
4 | inCharset: 'utf-8',
5 | outCharset: 'utf-8',
6 | notice: 0,
7 | format: 'jsonp'
8 | };
9 |
10 | // 回调参数
11 | export const options = {
12 | param: 'jsonpCallback',
13 | prefix: 'jp'
14 | };
15 |
16 | // 状态码
17 | export const ERR_OK = 0;
18 |
--------------------------------------------------------------------------------
/src/api/rank.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 榜单页相关接口
3 | */
4 | import { commonParams } from './config';
5 | import { createSong, isVaildSong } from 'common/js/song';
6 | import axios from 'axios';
7 |
8 | /**
9 | * 获取榜单列表
10 | */
11 | export function getRankList() {
12 | const url = '/api/getRankList';
13 |
14 | const data = Object.assign({}, commonParams, {
15 | uin: 0,
16 | needNewCode: 1,
17 | platform: 'h5',
18 | format: 'json'
19 | });
20 |
21 | return axios(url, { params: data }).then(res => {
22 | return Promise.resolve(res.data);
23 | });
24 | }
25 |
26 | /**
27 | * 获取榜单歌曲列表
28 | * @param {Number} id 榜单id
29 | */
30 | export function getRankDetail(id) {
31 | const url = '/api/getRankDetail';
32 |
33 | const data = Object.assign({}, commonParams, {
34 | topid: id,
35 | needNewCode: 1,
36 | uin: 0,
37 | tpl: 3,
38 | page: 'detail',
39 | type: 'top',
40 | platform: 'h5',
41 | format: 'json'
42 | });
43 |
44 | return axios.get(url, { params: data }).then(res => {
45 | return Promise.resolve(res.data);
46 | });
47 | }
48 |
49 | export function formatRankSongs(songs) {
50 | let res = [];
51 | songs.forEach(song => {
52 | if (isVaildSong(song.data)) {
53 | res.push(createSong(song.data));
54 | }
55 | });
56 | return res;
57 | }
58 |
--------------------------------------------------------------------------------
/src/api/recommend.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 推荐页相关接口
3 | */
4 | import { commonParams } from './config';
5 | import { createSong, isVaildSong } from 'common/js/song';
6 | import axios from 'axios';
7 |
8 | export function getRecommend() {
9 | const url = '/api/getRecommend';
10 |
11 | // Object.assign对象合并,第一个参数为目标对象,后续两个参数为源对象
12 | const data = Object.assign({}, commonParams, {
13 | platform: 'h5',
14 | uin: 0,
15 | needNewCode: 1
16 | });
17 |
18 | return axios.get(url, { params: data }).then(res => {
19 | return Promise.resolve(res.data);
20 | });
21 | }
22 |
23 | /**
24 | * 获取推荐歌单
25 | */
26 | export function getDiscList() {
27 | const url = '/api/getDiscList';
28 |
29 | // Object.assign对象合并,第一个参数为目标对象,后续两个参数为源对象
30 | const data = Object.assign({}, commonParams, {
31 | platform: 'yqq',
32 | hostUin: 0,
33 | sin: 0,
34 | ein: 29,
35 | sortId: 5,
36 | needNewCode: 0,
37 | categoryId: 10000000,
38 | rnd: Math.random(),
39 | format: 'json'
40 | });
41 |
42 | // axios通过浏览器发送ajax请求获取数据
43 | return axios.get(url, { params: data }).then(res => {
44 | return Promise.resolve(res.data);
45 | });
46 | }
47 |
48 | /**
49 | * 获取歌单详情
50 | */
51 | export function getDiscDetail(id) {
52 | const url = '/api/getDiscDetail';
53 |
54 | const data = Object.assign({}, commonParams, {
55 | disstid: id,
56 | type: 1,
57 | json: 1,
58 | utf8: 1,
59 | onlysong: 0,
60 | platform: 'yqq',
61 | hostUin: 0,
62 | needNewCode: 0
63 | });
64 |
65 | return axios.get(url, { params: data }).then(res => {
66 | return Promise.resolve(res.data);
67 | });
68 | }
69 |
70 | /**
71 | * 将歌曲列表处理成我们需要的数据
72 | * @param {Array} songs 传入的歌曲列表
73 | * @return {Array} 符合要求的歌曲列表
74 | */
75 | export function formatDiscSongs(songs) {
76 | let res = [];
77 | songs.forEach(song => {
78 | if (isVaildSong(song)) {
79 | res.push(createSong(song));
80 | }
81 | });
82 | return res;
83 | }
84 |
--------------------------------------------------------------------------------
/src/api/search.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 搜索页相关接口
3 | */
4 | import { commonParams } from './config';
5 | import axios from 'axios';
6 | import { createSong, isVaildSong } from 'common/js/song';
7 |
8 | /**
9 | * 获取搜索热词
10 | */
11 | export function getHotWord() {
12 | const url = '/api/getHotWord';
13 |
14 | const data = Object.assign({}, commonParams, {
15 | uin: 0,
16 | needNewCode: 1,
17 | platform: 'h5'
18 | });
19 |
20 | return axios.get(url, { params: data }).then(res => {
21 | return Promise.resolve(res.data);
22 | });
23 | }
24 |
25 | /**
26 | * 搜索接口
27 | * @param {String} query 检索词
28 | * @param {Number} page 当前页
29 | * @param {Number} zhida 是否搜索歌手
30 | * @param {Number} perpage 前一页
31 | */
32 | export function searchWord(query, page, zhida, perpage) {
33 | const url = '/api/searchWord';
34 |
35 | const data = Object.assign({}, commonParams, {
36 | w: query,
37 | p: page,
38 | perpage,
39 | n: perpage,
40 | catZhida: zhida ? 1 : 0,
41 | zhidaqu: 1,
42 | t: 0,
43 | flag: 1,
44 | ie: 'utf-8',
45 | sem: 1,
46 | aggr: 0,
47 | remoteplace: 'txt.mqq.all',
48 | uin: 0,
49 | needNewCode: 1,
50 | platform: 'h5'
51 | });
52 |
53 | return axios.get(url, { params: data }).then(res => {
54 | return Promise.resolve(res.data);
55 | });
56 | }
57 |
58 | /**
59 | * 将歌曲列表处理成我们需要的数据
60 | * @param {Array} songs 传入的歌曲列表
61 | * @return {Array} 符合要求的歌曲列表
62 | */
63 | export function formatSongs(songs) {
64 | let res = [];
65 | songs.forEach(song => {
66 | if (isVaildSong(song)) {
67 | res.push(createSong(song));
68 | }
69 | });
70 | return res;
71 | }
72 |
--------------------------------------------------------------------------------
/src/api/singer.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 歌手页相关接口
3 | */
4 | import { commonParams } from './config';
5 | import axios from 'axios';
6 | import Singer from 'common/js/singer';
7 |
8 | // 声明格式化数据需要用到的常量
9 | const HOT_NAME = '热门';
10 | const HOT_LIST_SIZE = 10;
11 |
12 | /**
13 | * 用于获取歌手列表的接口
14 | * @return {Object} 返回Promise
15 | */
16 | export function getSingers() {
17 | const url = '/api/getSingers';
18 | const data = Object.assign(commonParams, {
19 | channel: 'singer',
20 | page: 'list',
21 | key: 'all_all_all',
22 | pagesize: 100,
23 | pagenum: 1,
24 | hostUin: 0,
25 | needNewCode: 0,
26 | platform: 'yqq'
27 | });
28 |
29 | return axios.get(url, { params: data }).then(res => {
30 | return Promise.resolve(res.data);
31 | });
32 | }
33 |
34 | /**
35 | * 格式化传入的数据,将数据转成通讯录需要的
36 | * @param {Array} singerList 传入要格式化的数组
37 | * @return {Array} 格式化好的数组
38 | */
39 | export function formatList(singerList) {
40 | // 封装一个热门歌手列表
41 | let hotList = [
42 | {
43 | title: HOT_NAME,
44 | data: []
45 | }
46 | ];
47 |
48 | // 封装热门歌手列表数据
49 | singerList.forEach((item, index) => {
50 | if (index < HOT_LIST_SIZE) {
51 | hotList[0].data.push(
52 | new Singer({
53 | id: item.Fsinger_mid,
54 | name: item.Fsinger_name
55 | })
56 | );
57 | }
58 | });
59 |
60 | // 初始化字母索引列表
61 | let wordArray = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];
62 |
63 | // 初始化一个存放分类歌手数据的列表
64 | let dataList = [];
65 |
66 | // 根据首字母进行排序和分组
67 | for (let i = 0; i < wordArray.length; i++) {
68 | let singerGroup = {
69 | title: '',
70 | data: []
71 | };
72 | for (let j = 0; j < singerList.length; j++) {
73 | if (wordArray[i] === singerList[j].Findex) {
74 | singerGroup.data.push(
75 | new Singer({
76 | id: singerList[j].Fsinger_mid,
77 | name: singerList[j].Fsinger_name
78 | })
79 | );
80 | }
81 | }
82 |
83 | // 如果数组中有数据,则添加分组字母
84 | if (singerGroup.data.length) {
85 | singerGroup.title = wordArray[i];
86 | dataList.push(singerGroup);
87 | }
88 | }
89 | return hotList.concat(dataList);
90 | }
91 |
--------------------------------------------------------------------------------
/src/api/song.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 歌曲相关接口
3 | */
4 | import { commonParams } from './config';
5 | import axios from 'axios';
6 | import { createSong, isVaildSong } from 'common/js/song';
7 | import { getUid } from 'common/js/uid';
8 |
9 | /**
10 | * 用于获取歌手对应歌曲列表的接口
11 | * @return {Object} 返回Promise
12 | */
13 | export function getSingerSongs(singerId) {
14 | const url = '/api/getSingerSongs';
15 | const data = Object.assign(commonParams, {
16 | hostUin: 0,
17 | needNewCode: 0,
18 | platform: 'yqq',
19 | order: 'listen',
20 | begin: 0,
21 | num: 80,
22 | songstatus: 1,
23 | singermid: singerId
24 | });
25 |
26 | return axios.get(url, { params: data }).then(res => {
27 | return Promise.resolve(res.data);
28 | });
29 | }
30 |
31 | /**
32 | * 用于获取合法key的接口
33 | * @return {Object} 返回Promise
34 | */
35 | export function getVKey(songmid, filename) {
36 | const url = '/api/getVKey';
37 |
38 | const data = Object.assign({}, commonParams, {
39 | cid: 205361747,
40 | format: 'json',
41 | platform: 'yqq',
42 | hostUin: 0,
43 | needNewCode: 0,
44 | uin: 0,
45 | songmid,
46 | filename,
47 | guid: getUid()
48 | });
49 |
50 | return axios.get(url, { params: data }).then(res => {
51 | return Promise.resolve(res.data);
52 | });
53 | }
54 |
55 | /**
56 | * 用于获取歌词的接口
57 | * @return {Object} 返回Promise
58 | */
59 | export function getLyric(mid) {
60 | const url = '/api/getLyric';
61 |
62 | const data = Object.assign({}, commonParams, {
63 | songmid: mid,
64 | platform: 'yqq',
65 | hostUin: 0,
66 | needNewCode: 0,
67 | categoryId: 10000000,
68 | pcachetime: +new Date(),
69 | format: 'json'
70 | });
71 |
72 | return axios
73 | .get(url, {
74 | params: data
75 | })
76 | .then(res => {
77 | return Promise.resolve(res.data);
78 | });
79 | }
80 |
81 | /**
82 | * 将歌曲列表处理成我们需要的数据
83 | * @param {Array} songs 传入的歌曲列表
84 | * @return {Array} 符合要求的歌曲列表
85 | */
86 | export function formatSongs(songs) {
87 | let res = [];
88 | songs.forEach(song => {
89 | if (isVaildSong(song.musicData)) {
90 | res.push(createSong(song.musicData));
91 | }
92 | });
93 | return res;
94 | }
95 |
--------------------------------------------------------------------------------
/src/base/dialog/dialog.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
{{text}}
6 |
7 | {{sure}}
8 | {{cancel}}
9 |
10 |
11 | 确定
12 |
13 |
14 |
15 |
16 |
17 |
18 |
91 |
92 |
157 |
--------------------------------------------------------------------------------
/src/base/history-list/history-list.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {{item}}
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
46 |
47 |
80 |
--------------------------------------------------------------------------------
/src/base/listView/listView.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | -
5 |
{{singerGroup.title}}
6 |
7 | -
8 |
9 | {{singer.name}}
10 |
11 |
12 |
13 |
14 |
15 |
16 | -
17 | {{item}}
18 |
19 |
20 |
21 |
22 | {{wordTip}}
23 |
24 |
25 |
{{fixedTitle}}
26 |
27 |
28 |
29 |
30 |
238 |
239 |
331 |
--------------------------------------------------------------------------------
/src/base/loading/loading.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/facebesidewyj/cool-music/2a92f69de9d41fc380e07feda9db93fccb84088e/src/base/loading/loading.gif
--------------------------------------------------------------------------------
/src/base/loading/loading.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |

4 |
{{text}}
5 |
6 |
7 |
8 |
24 |
25 |
42 |
--------------------------------------------------------------------------------
/src/base/progress-bar/progress-bar.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
136 |
137 |
177 |
--------------------------------------------------------------------------------
/src/base/progress-circle/progress-circle.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 |
12 |
37 |
38 |
61 |
--------------------------------------------------------------------------------
/src/base/scroll/scroll.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
141 |
142 |
145 |
--------------------------------------------------------------------------------
/src/base/search-input/search-input.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
51 |
52 |
91 |
--------------------------------------------------------------------------------
/src/base/slider/slider.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
159 |
206 |
--------------------------------------------------------------------------------
/src/base/song-list/song-list.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | -
5 |
{{index + 1}}
6 |
7 |
{{song.name}}
8 |
{{getSongDesc(song)}}
9 |
10 |
11 |
12 |
13 |
14 |
15 |
44 |
45 |
83 |
--------------------------------------------------------------------------------
/src/base/top-tip/top-tip.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {{text}}
7 |
8 |
9 |
10 |
11 |
81 |
116 |
--------------------------------------------------------------------------------
/src/common/fonts/music-icon.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/facebesidewyj/cool-music/2a92f69de9d41fc380e07feda9db93fccb84088e/src/common/fonts/music-icon.eot
--------------------------------------------------------------------------------
/src/common/fonts/music-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/src/common/fonts/music-icon.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/facebesidewyj/cool-music/2a92f69de9d41fc380e07feda9db93fccb84088e/src/common/fonts/music-icon.ttf
--------------------------------------------------------------------------------
/src/common/fonts/music-icon.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/facebesidewyj/cool-music/2a92f69de9d41fc380e07feda9db93fccb84088e/src/common/fonts/music-icon.woff
--------------------------------------------------------------------------------
/src/common/image/default.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/facebesidewyj/cool-music/2a92f69de9d41fc380e07feda9db93fccb84088e/src/common/image/default.png
--------------------------------------------------------------------------------
/src/common/js/cache.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 缓存相关操作
3 | */
4 | import Store from 'store';
5 |
6 | const SEARCH_KEY = '__search__';
7 |
8 | // 最多保留10条历史记录
9 | const MAX_SIZE = 10;
10 |
11 | /**
12 | * 封装一个队列,将检索词插入数组
13 | * @param {Array} arr 搜索历史数组
14 | * @param {String} searchWord 检索词
15 | * @param {Function} compare 比较函数
16 | * @param {Number} size 最大长度限制
17 | */
18 | function insertArray(arr, searchWord, compare, size) {
19 | // 查找是否存在相同的检索词
20 | let index = arr.findIndex(compare);
21 |
22 | // 判断索引位置
23 | if (index === 0) {
24 | return;
25 | }
26 | if (index > 0) {
27 | arr.splice(index, 1);
28 | }
29 |
30 | // 入队
31 | arr.unshift(searchWord);
32 |
33 | // 判断长度
34 | if (size && arr.length > size) {
35 | arr.pop();
36 | }
37 | }
38 |
39 | /**
40 | * 封装一个队列,从数组中删除检索词
41 | * @param {Array} arr 搜索历史数组
42 | * @param {String} searchWord 检索词
43 | * @param {Function} compare 比较函数
44 | */
45 | function deleteFromArray(arr, searchWord, compare) {
46 | let index = arr.findIndex(compare);
47 |
48 | if (index > -1) {
49 | arr.splice(index, 1);
50 | }
51 | }
52 |
53 | /**
54 | * 保存搜索词到本地存储
55 | * @param {String} searchWord 检索词
56 | * @return {Array} 搜索历史数组
57 | */
58 | export function saveSearchHistory(searchWord) {
59 | let res = Store.get(SEARCH_KEY);
60 | if (!res) {
61 | res = [];
62 | }
63 | insertArray(
64 | res,
65 | searchWord,
66 | item => {
67 | return item === searchWord;
68 | },
69 | MAX_SIZE
70 | );
71 |
72 | // 存入本地存储
73 | Store.set(SEARCH_KEY, res);
74 | return res;
75 | }
76 |
77 | /**
78 | * 从本地存储中删除检索词
79 | * @param {String} searchWord 检索词
80 | * @return {Array} 搜索历史数组
81 | */
82 | export function deleteSearchHistory(searchWord) {
83 | let res = Store.get(SEARCH_KEY);
84 | deleteFromArray(res, searchWord, item => {
85 | return item === searchWord;
86 | });
87 | Store.set(SEARCH_KEY, res);
88 | return res;
89 | }
90 |
91 | /**
92 | * 清空本地存储
93 | * @return {Array} 空数组
94 | */
95 | export function clearSearchHistory() {
96 | Store.clearAll();
97 | return [];
98 | }
99 |
100 | /**
101 | * 从本地存储中获取搜索历史
102 | */
103 | export function getSearchHistory() {
104 | let res = Store.get(SEARCH_KEY);
105 | if (!res) {
106 | res = [];
107 | }
108 | return res;
109 | }
110 |
--------------------------------------------------------------------------------
/src/common/js/config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 设置常量的配置文件
3 | */
4 |
5 | /**
6 | * 播放的三种模式
7 | * @type {Object}
8 | */
9 | export const playMode = {
10 | sequence: 0,
11 | loop: 1,
12 | random: 2
13 | };
14 |
--------------------------------------------------------------------------------
/src/common/js/domUtil.js:
--------------------------------------------------------------------------------
1 | /**
2 | * DOM操作相关工具类
3 | * @type {Object}
4 | */
5 | export const domUtil = {
6 | /**
7 | * 添加样式名
8 | * @param {Element} el 传入的元素
9 | * @param {String} className 类名
10 | */
11 | addClass(el, className) {
12 | if (!this.hasClass(el, className)) {
13 | let classNameArray = el.className.split(' ');
14 | classNameArray.push(className);
15 | el.className = classNameArray.join(' ');
16 | }
17 | },
18 | /**
19 | * 判断当前元素是否含有该类名
20 | * @param {Element} el 传入的元素
21 | * @param {String} className 类名
22 | * @return {Boolean} 是否含有类名
23 | */
24 | hasClass(el, className) {
25 | let reg = new RegExp('(^|\\s)' + className + '(\\s|$)');
26 | return reg.test(el.className);
27 | },
28 |
29 | /**
30 | * 元素属性相关操作
31 | * @param {Element} el 要设置属性的元素
32 | * @param {String} name 属性名
33 | * @param {String} val 属性值
34 | * @return {String} 属性值
35 | */
36 | attr(el, name, val) {
37 | if (val) {
38 | return el.setAttribute(name, val);
39 | } else {
40 | return el.getAttribute(name);
41 | }
42 | },
43 |
44 | /**
45 | * 元素样式操作
46 | * @param {Element} el 要设置样式的元素
47 | * @param {String} name 样式名
48 | * @param {String} val 样式值
49 | */
50 | setCss(el, name, val) {
51 | let elementStyle = document.createElement('div').style;
52 |
53 | let transformNames = {
54 | webkit: 'webkitTransform',
55 | Moz: 'MozTransform',
56 | O: 'OTransform',
57 | ms: 'msTransform',
58 | standard: 'transform'
59 | };
60 |
61 | let keyName = '';
62 | for (let key in transformNames) {
63 | if (typeof elementStyle[transformNames[key]] !== 'undefined') {
64 | keyName = key;
65 | }
66 | }
67 |
68 | if (keyName) {
69 | if (keyName !== 'standard') {
70 | name = keyName + name.charAt(0).toUpperCase() + name.substr(1);
71 | }
72 | }
73 |
74 | el.style[name] = val;
75 | }
76 | };
77 |
--------------------------------------------------------------------------------
/src/common/js/jsonp.js:
--------------------------------------------------------------------------------
1 | import OriginalJSONP from 'jsonp';
2 |
3 | export default function jsonp(url, data, option) {
4 | // 判断传入的url是否存在?
5 | url += (url.indexOf('?') < 0 ? '?' : '&') + param(data);
6 |
7 | return new Promise((resolve, reject) => {
8 | OriginalJSONP(url, option, (err, data) => {
9 | // 成功
10 | if (!err) {
11 | resolve(data);
12 | } else {
13 | reject(err);
14 | }
15 | });
16 | });
17 | }
18 |
19 | /**
20 | * 将传入的参数封装成字符串拼接
21 | * @param {Object} data 参数对象
22 | * @return {String} 拼接字符串
23 | */
24 | function param(data) {
25 | let url = '';
26 |
27 | // 遍历data对象
28 | for (var k in data) {
29 | let value = data[k] !== undefined ? data[k] : '';
30 | url += `&${k}=${encodeURIComponent(value)}`;
31 | }
32 | return url ? url.substring(1) : '';
33 | }
34 |
--------------------------------------------------------------------------------
/src/common/js/mixin.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Vue组件调用的mixin
3 | */
4 | import { mapGetters } from 'vuex';
5 |
6 | export const playListMixin = {
7 | computed: {
8 | ...mapGetters(['playList'])
9 | },
10 | mounted() {
11 | this.handlePlayList(this.playList);
12 | },
13 | /**
14 | * keep-alive组件激活时触发
15 | */
16 | activated() {
17 | this.handlePlayList(this.playList);
18 | },
19 | watch: {
20 | playList(newList) {
21 | this.handlePlayList(newList);
22 | }
23 | },
24 | methods: {
25 | handlePlayList(playList) {
26 | throw new Error('error');
27 | }
28 | }
29 | };
30 |
--------------------------------------------------------------------------------
/src/common/js/singer.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 歌手类
3 | */
4 | export default class Singer {
5 | constructor({ id, name }) {
6 | this.id = id;
7 | this.name = name;
8 | this.avatar = `https://y.gtimg.cn/music/photo_new/T001R300x300M000${id}.jpg?max_age=2592000`;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/common/js/song.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 歌曲类
3 | */
4 | import { ERR_OK } from 'api/config';
5 | import { getUid } from './uid';
6 | import { getVKey, getLyric } from 'api/song';
7 | import { Base64 } from 'js-base64';
8 |
9 | let urlMap = {};
10 |
11 | export default class Song {
12 | constructor({ id, mid, singer, name, album, duration, image }) {
13 | this.id = id;
14 | this.mid = mid;
15 | this.singer = singer;
16 | this.name = name;
17 | this.album = album;
18 | this.duration = duration;
19 | this.image = image;
20 | this.filename = `C400${this.mid}.m4a`;
21 |
22 | // 确保一首歌曲的 id 只对应一个 url
23 | if (urlMap[this.id]) {
24 | this.url = urlMap[this.id];
25 | } else {
26 | this._genUrl();
27 | }
28 | }
29 |
30 | /**
31 | * 获取歌词
32 | * @return {Object} Promise
33 | */
34 | getLyric() {
35 | if (this.lyric) {
36 | return Promise.resolve(this.lyric);
37 | }
38 |
39 | return new Promise((resolve, reject) => {
40 | getLyric(this.mid).then(res => {
41 | if (res.retcode === ERR_OK) {
42 | this.lyric = Base64.decode(res.lyric);
43 | resolve(this.lyric);
44 | } else {
45 | reject(new Error('no lyric'));
46 | }
47 | });
48 | });
49 | }
50 |
51 | /**
52 | * 获取歌曲播放地址
53 | */
54 | _genUrl() {
55 | if (this.url) {
56 | return;
57 | }
58 | getVKey(this.mid, this.filename).then(res => {
59 | if (res.code === ERR_OK) {
60 | const vkey = res.data.items[0].vkey;
61 | this.url = `http://dl.stream.qqmusic.qq.com/${this.filename}?vkey=${vkey}&guid=${getUid()}&uin=0&fromtag=66`;
62 | urlMap[this.id] = this.url;
63 | }
64 | });
65 | }
66 | }
67 |
68 | /**
69 | * 工厂方法创建对象
70 | * @param {Object} musicData 传入的对象
71 | * @return {Song} 返回的Song实例
72 | */
73 | export function createSong(song) {
74 | return new Song({
75 | id: song.songid,
76 | mid: song.songmid,
77 | singer: filterSinger(song.singer),
78 | name: song.songname,
79 | album: song.albumname,
80 | duration: song.interval,
81 | image: `https://y.gtimg.cn/music/photo_new/T002R300x300M000${song.albummid}.jpg?max_age=2592000`
82 | });
83 | }
84 |
85 | /**
86 | * 改名歌手名的展示形式
87 | * @param {Object} singer 传入歌手对象
88 | * @return {String} 歌手名
89 | */
90 | export function filterSinger(singer) {
91 | if (singer) {
92 | let res = [];
93 | singer.forEach(item => {
94 | res.push(item.name);
95 | });
96 | return res.join('/');
97 | } else {
98 | return '';
99 | }
100 | }
101 |
102 | export function isVaildSong(song) {
103 | return song.songid && song.albummid && (!song.pay || song.pay.payalbumprice === 0);
104 | }
105 |
--------------------------------------------------------------------------------
/src/common/js/uid.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 获取歌曲随机数
3 | */
4 | let _uid = 0;
5 |
6 | export function getUid() {
7 | if (_uid) {
8 | return _uid;
9 | }
10 | if (!_uid) {
11 | const t = new Date().getUTCMilliseconds();
12 | _uid = (Math.round(2147483647 * Math.random()) * t) % 1e10;
13 | }
14 | return _uid;
15 | }
16 |
--------------------------------------------------------------------------------
/src/common/js/util.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 常用工具类
3 | * @type {Object}
4 | */
5 | export const util = {
6 | /**
7 | * 格式化时间戳
8 | * @param {Number} time 传入的时间戳
9 | * @return {Number} 格式化好的时间戳
10 | */
11 | formatTime(time) {
12 | time = Math.floor(time);
13 |
14 | let min = Math.floor(time / 60);
15 |
16 | let second = time % 60;
17 | if (second.toString().length === 1) {
18 | second = '0' + second;
19 | }
20 |
21 | return `${min}:${second}`;
22 | },
23 |
24 | /**
25 | * 打乱数组
26 | * @param {Array} arr 传入的数组
27 | * @return {Array} 打乱之后的数组
28 | */
29 | randomArray(arr) {
30 | let tempArr = arr.slice();
31 | for (let i = 0; i < tempArr.length; i++) {
32 | // 获取随机数
33 | let randomNum = Math.floor(Math.random() * (i - 0 + 1) + 0);
34 | let temp = tempArr[i];
35 | tempArr[i] = tempArr[randomNum];
36 | tempArr[randomNum] = temp;
37 | }
38 | return tempArr;
39 | },
40 |
41 | /**
42 | * 节流函数
43 | * @param {Function} func 要执行节流的函数
44 | * @param {Number} delay 延迟时间
45 | * @return {Function} 执行节流函数的函数
46 | */
47 | debounce(func, delay) {
48 | let timer = null;
49 |
50 | return function(...args) {
51 | if (timer) {
52 | clearTimeout(timer);
53 | }
54 | timer = setTimeout(() => {
55 | func.apply(this, args);
56 | }, delay);
57 | };
58 | }
59 | };
60 |
--------------------------------------------------------------------------------
/src/common/less/base.less:
--------------------------------------------------------------------------------
1 | @import "variable.less";
2 |
3 | body,
4 | html {
5 | line-height: 1;
6 | font-family: 'PingFang SC', 'STHeitiSC-Light', 'Helvetica-Light', arial, sans-serif, 'Droid Sans Fallback';
7 | user-select: none;
8 | -webkit-tap-highlight-color: transparent;
9 | background: @color-background;
10 | color: @color-text;
11 | }
12 |
--------------------------------------------------------------------------------
/src/common/less/icon.less:
--------------------------------------------------------------------------------
1 | @font-face{
2 | font-family: 'music-icon';
3 | src: url('../fonts/music-icon.eot?2qevqt');
4 | src: url('../fonts/music-icon.eot?2qevqt#iefix') format('embedded-opentype'),
5 | url('../fonts/music-icon.ttf?2qevqt') format('truetype'),
6 | url('../fonts/music-icon.woff?2qevqt') format('woff'),
7 | url('../fonts/music-icon.svg?2qevqt#music-icon') format('svg');
8 | font-weight: normal;
9 | font-style: normal;
10 | }
11 |
12 | [class^="icon-"], [class*=" icon-"] {
13 |
14 | /* use !important to prevent issues with browser extensions that change fonts */
15 | font-family: 'music-icon' !important;
16 | speak: none;
17 | font-style: normal;
18 | font-weight: normal;
19 | font-variant: normal;
20 | text-transform: none;
21 | line-height: 1;
22 |
23 | /* Better Font Rendering =========== */
24 | -webkit-font-smoothing: antialiased;
25 | -moz-osx-font-smoothing: grayscale;
26 | }
27 | .icon-ok:before{
28 | content: "\e900";
29 | }
30 |
31 | .icon-close:before{
32 | content: "\e901";
33 | }
34 |
35 | .icon-add:before{
36 | content: "\e902";
37 | }
38 |
39 | .icon-play-mini:before{
40 | content: "\e903";
41 | }
42 |
43 | .icon-playlist:before{
44 | content: "\e904";
45 | }
46 |
47 | .icon-music:before{
48 | content: "\e905";
49 | }
50 |
51 | .icon-search:before{
52 | content: "\e906";
53 | }
54 |
55 | .icon-clear:before{
56 | content: "\e907";
57 | }
58 |
59 | .icon-delete:before{
60 | content: "\e908";
61 | }
62 |
63 | .icon-favorite:before{
64 | content: "\e909";
65 | }
66 |
67 | .icon-not-favorite:before{
68 | content: "\e90a";
69 | }
70 |
71 | .icon-pause:before{
72 | content: "\e90b";
73 | }
74 |
75 | .icon-play:before{
76 | content: "\e90c";
77 | }
78 |
79 | .icon-prev:before{
80 | content: "\e90d";
81 | }
82 |
83 | .icon-loop:before{
84 | content: "\e90e";
85 | }
86 |
87 | .icon-sequence:before{
88 | content: "\e90f";
89 | }
90 |
91 | .icon-random:before{
92 | content: "\e910";
93 | }
94 |
95 | .icon-back:before{
96 | content: "\e911";
97 | }
98 |
99 | .icon-mine:before{
100 | content: "\e912";
101 | }
102 |
103 | .icon-next:before{
104 | content: "\e913";
105 | }
106 |
107 | .icon-dismiss:before{
108 | content: "\e914";
109 | }
110 |
111 | .icon-pause-mini:before{
112 | content: "\e915";
113 | }
114 |
--------------------------------------------------------------------------------
/src/common/less/index.less:
--------------------------------------------------------------------------------
1 | @import "./reset.less";
2 | @import "./base.less";
3 | @import "./icon.less";
4 |
--------------------------------------------------------------------------------
/src/common/less/mixin.less:
--------------------------------------------------------------------------------
1 | // 根据dpr选择背景图片
2 | .bg-image(@url) {
3 | background-image: url("@{url}@2x.png");
4 | @media (-webkit-min-device-pixel-ratio: 3),
5 | (min-device-pixel-ratio: 3) {
6 | background-image: url("@{url}@3x.png");
7 | }
8 | }
9 |
10 |
11 | // 单行文本溢出显示省略号
12 | .no-wrap {
13 | text-overflow: ellipsis;
14 | overflow: hidden;
15 | white-space: nowrap;
16 | }
17 |
18 | // 扩展点击区域(使用伪类)
19 | .extend-click {
20 | position: relative;
21 | &:before {
22 | content: '';
23 | position: absolute;
24 | top: -10px;
25 | left: -10px;
26 | right: -10px;
27 | bottom: -10px;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/common/less/reset.less:
--------------------------------------------------------------------------------
1 | /* http://meyerweb.com/eric/tools/css/reset/
2 | v2.0 | 20110126
3 | License: none (public domain)
4 | */
5 |
6 | * {
7 | touch-action: none;
8 | }
9 |
10 | a,
11 | abbr,
12 | acronym,
13 | address,
14 | applet,
15 | article,
16 | aside,
17 | audio,
18 | b,
19 | big,
20 | blockquote,
21 | body,
22 | canvas,
23 | caption,
24 | center,
25 | cite,
26 | code,
27 | dd,
28 | del,
29 | details,
30 | dfn,
31 | div,
32 | dl,
33 | dt,
34 | em,
35 | embed,
36 | fieldset,
37 | figcaption,
38 | figure,
39 | footer,
40 | form,
41 | h1,
42 | h2,
43 | h3,
44 | h4,
45 | h5,
46 | h6,
47 | header,
48 | hgroup,
49 | html,
50 | i,
51 | iframe,
52 | img,
53 | ins,
54 | kbd,
55 | label,
56 | legend,
57 | li,
58 | mark,
59 | menu,
60 | nav,
61 | object,
62 | ol,
63 | output,
64 | p,
65 | pre,
66 | q,
67 | ruby,
68 | s,
69 | samp,
70 | section,
71 | small,
72 | span,
73 | strike,
74 | strong,
75 | sub,
76 | summary,
77 | sup,
78 | table,
79 | tbody,
80 | td,
81 | tfoot,
82 | th,
83 | thead,
84 | time,
85 | tr,
86 | tt,
87 | u,
88 | ul,
89 | var,
90 | video {
91 | margin: 0;
92 | padding: 0;
93 | border: 0;
94 | font-size: 100%;
95 | font: inherit;
96 | vertical-align: baseline;
97 | }
98 | /* HTML5 display-role reset for older browsers */
99 |
100 | article,
101 | aside,
102 | details,
103 | figcaption,
104 | figure,
105 | footer,
106 | header,
107 | hgroup,
108 | menu,
109 | nav,
110 | section {
111 | display: block;
112 | }
113 |
114 | body {
115 | line-height: 1;
116 | }
117 |
118 | ol,
119 | ul {
120 | list-style: none;
121 | }
122 |
123 | blockquote,
124 | q {
125 | quotes: none;
126 | }
127 |
128 | blockquote:after,
129 | blockquote:before,
130 | q:after,
131 | q:before {
132 | content: '';
133 | content: none;
134 | }
135 |
136 | table {
137 | border-collapse: collapse;
138 | border-spacing: 0;
139 | }
140 |
141 | a {
142 | text-decoration: none;
143 | }
--------------------------------------------------------------------------------
/src/common/less/variable.less:
--------------------------------------------------------------------------------
1 | /**
2 | * 基础颜色和字体大小变量定义
3 | */
4 | // 颜色定义规范
5 | @color-background: #364f6b;
6 | @color-background-d: rgba(54, 79, 107, 0.3);
7 | @color-highlight-background: #3D84A8;
8 | @color-dialog-background: #666;
9 | @color-theme: #f6416c;
10 | @color-theme-d: rgba(246, 65, 108, 0.5);
11 | @color-sub-theme: #d93f30;
12 | @color-text: #fff;
13 | @color-text-d: rgba(255, 255, 255, 0.3);
14 | @color-text-l: rgba(255, 255, 255, 0.5);
15 | @color-text-ll: rgba(255, 255, 255, 0.8); //字体定义规范
16 | @font-size-small-s: 10px;
17 | @font-size-small: 12px;
18 | @font-size-medium: 14px;
19 | @font-size-medium-x: 16px;
20 | @font-size-large: 18px;
21 | @font-size-large-x: 22px;
--------------------------------------------------------------------------------
/src/components/disc-detail/disc-detail.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
52 |
53 |
65 |
--------------------------------------------------------------------------------
/src/components/header/header.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
11 |
26 |
--------------------------------------------------------------------------------
/src/components/music-list/music-list.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 |
12 |
13 | 随机播放全部
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
188 |
189 |
297 |
--------------------------------------------------------------------------------
/src/components/play-list/play-list.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 播放列表
7 |
8 |
9 |
10 |
11 |
12 |
13 | -
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | 关闭
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
158 |
159 |
257 |
--------------------------------------------------------------------------------
/src/components/player/player.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
![背景图]()
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
![cd]()
21 |
22 |
23 |
{{playingLyric}}
24 |
25 |
26 |
27 |
32 |
33 |
34 |
35 |
36 |
40 |
41 |
{{playTime}}
42 |
45 |
{{totalTime}}
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
![cd]()
71 |
72 |
76 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
662 |
663 |
976 |
--------------------------------------------------------------------------------
/src/components/rank-detail/rank-detail.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
52 |
53 |
65 |
--------------------------------------------------------------------------------
/src/components/rank/rank.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | -
6 |
7 |
![榜单图片]()
8 |
9 |
10 | -
11 | {{index+1}}
12 | {{song.songname}}
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
95 |
96 |
153 |
--------------------------------------------------------------------------------
/src/components/recommend/recommend.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
14 |
15 |
热门歌单推荐
16 |
17 | -
18 |
19 |
![歌单图片]()
20 |
21 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
133 |
134 |
195 |
--------------------------------------------------------------------------------
/src/components/search/search.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
热门搜索
11 |
12 | -
13 | {{key.k}}
14 |
15 |
16 |
17 |
18 |
19 | 搜索历史
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
159 |
160 |
231 |
--------------------------------------------------------------------------------
/src/components/singer-detail/singer-detail.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
56 |
57 |
68 |
--------------------------------------------------------------------------------
/src/components/singer/singer.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
83 |
84 |
98 |
--------------------------------------------------------------------------------
/src/components/suggest/suggest.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | -
5 |
6 |
7 |
8 |
11 |
12 |
13 |
14 |
15 | 抱歉,暂无结果
16 |
17 |
18 |
19 |
20 |
202 |
203 |
249 |
--------------------------------------------------------------------------------
/src/components/tab/tab.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 推荐
5 |
6 |
7 | 榜单
8 |
9 |
10 | 歌手
11 |
12 |
13 | 搜索
14 |
15 |
16 |
17 |
18 |
29 |
30 |
54 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | // The Vue build version to load with the `import` command
2 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias.
3 | import 'babel-polyfill';
4 | import Vue from 'vue';
5 | import App from './App';
6 | import router from './router';
7 | import FastClick from 'fastclick';
8 | import VueLazyload from 'vue-lazyload';
9 | import store from './store';
10 | import 'common/less/index.less';
11 |
12 | Vue.config.productionTip = false;
13 |
14 | // 取消移动端300ms延迟
15 | FastClick.attach(document.body);
16 |
17 | // VueLazyload懒加载,支持require方法
18 | Vue.use(VueLazyload, {
19 | error: require('common/image/default.png'),
20 | loading: require('common/image/default.png')
21 | });
22 |
23 | /* eslint-disable no-new */
24 | new Vue({
25 | el: '#app',
26 | router,
27 | store,
28 | render: h => h(App)
29 | });
30 |
--------------------------------------------------------------------------------
/src/router/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import Router from 'vue-router';
3 |
4 | Vue.use(Router);
5 |
6 | const Recommend = () => import('components/recommend/recommend');
7 | const Rank = () => import('components/rank/rank');
8 | const Singer = () => import('components/singer/singer');
9 | const Search = () => import('components/search/search');
10 | const SingerDetail = () => import('components/singer-detail/singer-detail');
11 | const DiscDetail = () => import('components/disc-detail/disc-detail');
12 | const RankDetail = () => import('components/rank-detail/rank-detail');
13 |
14 | export default new Router({
15 | routes: [
16 | {
17 | path: '/',
18 | redirect: '/recommend'
19 | },
20 | {
21 | path: '/recommend',
22 | component: Recommend,
23 | children: [
24 | {
25 | path: ':id',
26 | component: DiscDetail
27 | }
28 | ]
29 | },
30 | {
31 | path: '/rank',
32 | component: Rank,
33 | children: [
34 | {
35 | path: ':id',
36 | component: RankDetail
37 | }
38 | ]
39 | },
40 | {
41 | path: '/singer',
42 | component: Singer,
43 | children: [
44 | {
45 | path: ':id', // :属性指可以传入属性来当做路径
46 | component: SingerDetail
47 | }
48 | ]
49 | },
50 | {
51 | path: '/search',
52 | component: Search,
53 | children: [
54 | {
55 | path: ':id', // :属性指可以传入属性来当做路径
56 | component: SingerDetail
57 | }
58 | ]
59 | }
60 | ]
61 | });
62 |
--------------------------------------------------------------------------------
/src/store/actions.js:
--------------------------------------------------------------------------------
1 | /**
2 | *封装一系列mutation操作
3 | */
4 | import * as types from './mutation-types';
5 | import { util } from 'common/js/util';
6 | import { playMode } from 'common/js/config';
7 | import { saveSearchHistory, deleteSearchHistory, clearSearchHistory } from 'common/js/cache';
8 |
9 | /**
10 | * 保存搜索历史
11 | */
12 | export function saveHistory({ commit, state }, searchWord) {
13 | commit(types.SET_SEARCH_HISTORY, saveSearchHistory(searchWord));
14 | }
15 |
16 | /**
17 | * 删除一条搜索历史
18 | */
19 | export function deleteHistory({ commit, state }, searchWord) {
20 | commit(types.SET_SEARCH_HISTORY, deleteSearchHistory(searchWord));
21 | }
22 |
23 | /**
24 | * 清空搜索历史
25 | */
26 | export function clearHistory({ commit, state }) {
27 | commit(types.SET_SEARCH_HISTORY, clearSearchHistory());
28 | }
29 |
30 | /**
31 | * 播放歌曲
32 | */
33 | export function selectPlay({ commit, state }, { list, index }) {
34 | commit(types.SET_SEQUENCE_LIST, list);
35 |
36 | // 随机播放,打乱播放列表,并寻找索引
37 | if (state.playMode === playMode.random) {
38 | let randomList = util.randomArray(list);
39 |
40 | index = findIndex(randomList, list[index]);
41 | commit(types.SET_PLAY_LIST, randomList);
42 | } else {
43 | commit(types.SET_PLAY_LIST, list);
44 | }
45 |
46 | commit(types.SET_PLAY_STATE, true);
47 | commit(types.SET_FULL_SCREEN, true);
48 | commit(types.SET_CURRENT_INDEX, index);
49 | }
50 |
51 | /**
52 | * 随机播放
53 | */
54 | export function playRandom({ commit }, { list }) {
55 | commit(types.SET_SEQUENCE_LIST, list);
56 | commit(types.SET_PLAY_MODE, playMode.random);
57 | commit(types.SET_FULL_SCREEN, true);
58 |
59 | // 打乱播放列表
60 | let randomList = util.randomArray(list);
61 | commit(types.SET_PLAY_LIST, randomList);
62 | commit(types.SET_PLAY_STATE, true);
63 | commit(types.SET_CURRENT_INDEX, 0);
64 | }
65 |
66 | /**
67 | * 向列表中插入新歌曲
68 | */
69 | export function insertNewSong({ commit, state }, song) {
70 | let playList = state.playList.slice();
71 | let sequenceList = state.sequenceList.slice();
72 | let currentIndex = state.currentIndex;
73 |
74 | // 获取当前歌曲
75 | let currentSong = playList[currentIndex];
76 |
77 | // 判断当前播放列表中是否已经存在要插入的歌曲
78 | let songInPlayList = findIndex(playList, song);
79 |
80 | // 判断前播放列表中是否存在该歌曲
81 | if (songInPlayList > -1) {
82 | // 将已存在的歌曲去除
83 | playList.splice(songInPlayList, 1);
84 |
85 | // 判断当前索引的位置
86 | if (currentIndex > songInPlayList) {
87 | playList.splice(currentIndex, 0, song);
88 | } else {
89 | playList.splice(currentIndex + 1, 0, song);
90 | }
91 | } else {
92 | currentIndex++;
93 | playList.splice(currentIndex, 0, song);
94 | }
95 |
96 | // 获取原始歌曲列表中的当前索引和该歌曲索引
97 | let currentSongInSequenceList = findIndex(sequenceList, currentSong);
98 | let songInSequenceList = findIndex(sequenceList, song);
99 |
100 | // 判断原始列表中是否存在该歌曲
101 | if (songInSequenceList > -1) {
102 | // 将已存在的歌曲去除
103 | sequenceList.splice(songInSequenceList, 1);
104 |
105 | // 判断当前索引的位置
106 | if (currentSongInSequenceList > songInSequenceList) {
107 | sequenceList.splice(currentSongInSequenceList, 0, song);
108 | } else {
109 | sequenceList.splice(currentSongInSequenceList + 1, 0, song);
110 | }
111 | } else {
112 | sequenceList.splice(currentSongInSequenceList + 1, 0, song);
113 | }
114 |
115 | commit(types.SET_PLAY_LIST, playList);
116 | commit(types.SET_SEQUENCE_LIST, sequenceList);
117 | commit(types.SET_CURRENT_INDEX, currentIndex);
118 | commit(types.SET_PLAY_STATE, true);
119 | commit(types.SET_FULL_SCREEN, true);
120 | }
121 |
122 | /**
123 | * 清空播放列表
124 | */
125 | export function clearPlayList({ commit, state }) {
126 | commit(types.SET_PLAY_LIST, []);
127 | commit(types.SET_SEQUENCE_LIST, []);
128 | commit(types.SET_CURRENT_INDEX, -1);
129 | commit(types.SET_PLAY_STATE, false);
130 | commit(types.SET_FULL_SCREEN, false);
131 | }
132 |
133 | /**
134 | * 将歌曲从播放列表中删除
135 | */
136 | export function deleteSongFromPlayList({ commit, state }, song) {
137 | let playList = state.playList.slice();
138 | let sequenceList = state.sequenceList.slice();
139 | let currentIndex = state.currentIndex;
140 |
141 | let indexInPlay = findIndex(playList, song);
142 |
143 | playList.splice(indexInPlay, 1);
144 |
145 | let indexInSeq = findIndex(sequenceList, song);
146 |
147 | sequenceList.splice(indexInSeq, 1);
148 |
149 | // 判断索引位置
150 | if (indexInPlay < currentIndex || currentIndex === playList.length) {
151 | currentIndex--;
152 | }
153 |
154 | // 判断播放状态
155 | let flag = playList.length > 0;
156 |
157 | commit(types.SET_PLAY_LIST, playList);
158 | commit(types.SET_SEQUENCE_LIST, sequenceList);
159 | commit(types.SET_CURRENT_INDEX, currentIndex);
160 | commit(types.SET_PLAY_STATE, flag);
161 | }
162 |
163 | /**
164 | * 封装查询歌曲在列表中的位置
165 | * @param {Array} list 歌曲列表
166 | * @param {Object} song 歌曲对象
167 | * @return {Number} 索引
168 | */
169 | function findIndex(list, song) {
170 | return list.findIndex(item => {
171 | return item.id === song.id;
172 | });
173 | }
174 |
--------------------------------------------------------------------------------
/src/store/getters.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 封装一些取值方法
3 | */
4 | export const singer = state => state.singer;
5 |
6 | export const disc = state => state.disc;
7 |
8 | export const rank = state => state.rank;
9 |
10 | export const playState = state => state.playState;
11 |
12 | export const fullScreen = state => state.fullScreen;
13 |
14 | export const playList = state => state.playList;
15 |
16 | export const sequenceList = state => state.sequenceList;
17 |
18 | export const playMode = state => state.playMode;
19 |
20 | export const currentIndex = state => state.currentIndex;
21 |
22 | export const searchHistory = state => state.searchHistory;
23 |
24 | /**
25 | * 获取当前播放的歌曲
26 | * @param {Object} state vuex里的数据对象
27 | * @return {Object} 当前播放的歌曲
28 | */
29 | export const currentSong = state => {
30 | // 没有初始化成功时应该赋予空对象,否则v-bind就会报错,因为对象是undefined
31 | return state.playList[state.currentIndex] || {};
32 | };
33 |
--------------------------------------------------------------------------------
/src/store/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import Vuex from 'vuex';
3 | import * as actions from './actions';
4 | import * as getters from './getters';
5 | import state from './state';
6 | import mutations from './mutations';
7 |
8 | // 引入一个日志插件
9 | import createLogger from 'vuex/dist/logger';
10 |
11 | Vue.use(Vuex);
12 |
13 | // 区分生产和开发环境来设置是否能调试
14 | const debug = process.env.NODE_ENV !== 'production';
15 |
16 | export default new Vuex.Store({
17 | actions,
18 | getters,
19 | state,
20 | mutations,
21 | strict: debug,
22 | plugins: debug ? [createLogger()] : []
23 | });
24 |
--------------------------------------------------------------------------------
/src/store/mutation-types.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 设置一些常量
3 | */
4 | export const SET_SINGER = 'SET_SINGER';
5 |
6 | export const SET_DISC = 'SET_DISC';
7 |
8 | export const SET_RANK = 'SET_RANK';
9 |
10 | export const SET_PLAY_STATE = 'SET_PLAY_STATE';
11 |
12 | export const SET_FULL_SCREEN = 'SET_FULL_SCREEN';
13 |
14 | export const SET_PLAY_LIST = 'SET_PLAY_LIST';
15 |
16 | export const SET_SEQUENCE_LIST = 'SET_SEQUENCE_LIST';
17 |
18 | export const SET_PLAY_MODE = 'SET_PLAY_MODE';
19 |
20 | export const SET_CURRENT_INDEX = 'SET_CURRENT_INDEX';
21 |
22 | export const SET_SEARCH_HISTORY = 'SET_SEARCH_HISTORY';
23 |
--------------------------------------------------------------------------------
/src/store/mutations.js:
--------------------------------------------------------------------------------
1 | import * as types from './mutation-types';
2 |
3 | const mutation = {
4 | [types.SET_SINGER](state, singer) {
5 | state.singer = singer;
6 | },
7 | [types.SET_DISC](state, disc) {
8 | state.disc = disc;
9 | },
10 | [types.SET_RANK](state, rank) {
11 | state.rank = rank;
12 | },
13 | [types.SET_PLAY_STATE](state, playState) {
14 | state.playState = playState;
15 | },
16 | [types.SET_FULL_SCREEN](state, fullScreen) {
17 | state.fullScreen = fullScreen;
18 | },
19 | [types.SET_PLAY_LIST](state, playList) {
20 | state.playList = playList;
21 | },
22 | [types.SET_SEQUENCE_LIST](state, sequenceList) {
23 | state.sequenceList = sequenceList;
24 | },
25 | [types.SET_PLAY_MODE](state, playMode) {
26 | state.playMode = playMode;
27 | },
28 | [types.SET_CURRENT_INDEX](state, currentIndex) {
29 | state.currentIndex = currentIndex;
30 | },
31 | [types.SET_SEARCH_HISTORY](state, searchHistory) {
32 | state.searchHistory = searchHistory;
33 | }
34 | };
35 |
36 | export default mutation;
37 |
--------------------------------------------------------------------------------
/src/store/state.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 设置state为数据源
3 | */
4 | import { playMode } from 'common/js/config';
5 | import { getSearchHistory } from 'common/js/cache';
6 |
7 | const state = {
8 | singer: {},
9 | playState: false, // 播放状态(暂停/播放)
10 | fullScreen: false, // 展示状态(全屏/迷你)
11 | playList: [], // 播放列表
12 | sequenceList: [], // 顺序列表
13 | playMode: playMode.sequence, // 播放模式
14 | currentIndex: -1, // 当前播放索引
15 | disc: {}, // 歌单
16 | rank: {}, // 榜单
17 | searchHistory: getSearchHistory() // 搜索历史
18 | };
19 |
20 | export default state;
21 |
--------------------------------------------------------------------------------
/static/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/facebesidewyj/cool-music/2a92f69de9d41fc380e07feda9db93fccb84088e/static/.gitkeep
--------------------------------------------------------------------------------