├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── README.md ├── build ├── build.js ├── logo.png ├── utils.js ├── webpack.base.conf.js ├── webpack.dev.conf.js └── webpack.prod.conf.js ├── dist ├── favicon.ico ├── icons │ ├── music-120.png │ ├── music-144.png │ ├── music-152.png │ ├── music-192.png │ ├── music-256.png │ ├── music-384.png │ ├── music-48.png │ └── music-512.png ├── index.html ├── manifest.json └── static │ ├── css │ ├── app.4aae7266452030a68c992e7a644689e2.css │ └── app.4aae7266452030a68c992e7a644689e2.css.map │ ├── img │ ├── default.9710131.png │ ├── empty.4c4c2c3.png │ └── loading.1f26c50.gif │ └── js │ ├── app.f2498bb9c13212717c50.js │ ├── app.f2498bb9c13212717c50.js.map │ ├── manifest.2ae2e69a05c33dfc65f8.js │ ├── manifest.2ae2e69a05c33dfc65f8.js.map │ ├── vendor.ebd99a3ce2f05a6feefe.js │ └── vendor.ebd99a3ce2f05a6feefe.js.map ├── favicon.ico ├── index.html ├── package-lock.json ├── package.json ├── postcss.config.js ├── src ├── api │ ├── config.js │ ├── recommends.js │ ├── search.js │ ├── singers.js │ ├── song.js │ └── top-list.js ├── app.vue ├── assets │ ├── icons │ │ ├── back.svg │ │ ├── calendar.svg │ │ ├── clear.svg │ │ ├── clock.svg │ │ ├── comment.svg │ │ ├── delete.svg │ │ ├── disc.svg │ │ ├── down.svg │ │ ├── download.svg │ │ ├── favorite.svg │ │ ├── fm.svg │ │ ├── headphones.svg │ │ ├── like.svg │ │ ├── logo.svg │ │ ├── loop.svg │ │ ├── menu.svg │ │ ├── mini-pause.svg │ │ ├── mini-pause2.svg │ │ ├── mini-play.svg │ │ ├── mini-play2.svg │ │ ├── more.svg │ │ ├── music.svg │ │ ├── next.svg │ │ ├── once.svg │ │ ├── pause.svg │ │ ├── play.svg │ │ ├── playlist.svg │ │ ├── prev.svg │ │ ├── random.svg │ │ ├── rank.svg │ │ ├── repeat.svg │ │ ├── search.svg │ │ ├── unfavorite.svg │ │ └── volume.svg │ └── images │ │ ├── default.png │ │ ├── empty.png │ │ └── loading.gif ├── components │ ├── confirm.vue │ ├── empty.vue │ ├── loading.vue │ ├── progress-bar.vue │ ├── progress-circle.vue │ ├── scroll.vue │ ├── swiper.vue │ ├── tab.vue │ └── toast.vue ├── icon.vue ├── main.js ├── pages │ ├── comment │ │ ├── comment-item.vue │ │ └── comment.vue │ ├── entry │ │ └── entry.vue │ ├── header │ │ └── header.vue │ ├── music-list │ │ ├── music-list.vue │ │ └── song-list.vue │ ├── player │ │ └── player.vue │ ├── playlist │ │ └── playlist.vue │ ├── recommends │ │ ├── recommend-detail.vue │ │ └── recommends.vue │ ├── search │ │ ├── search-box.vue │ │ └── search.vue │ ├── singers │ │ ├── singer-detail.vue │ │ └── singers.vue │ ├── top-list │ │ ├── top-list-detail.vue │ │ └── top-list.vue │ └── user │ │ └── user.vue ├── route.js ├── services │ ├── cache.js │ ├── config.js │ └── song.js ├── styles │ ├── base.styl │ ├── highlight.styl │ ├── index.styl │ ├── lib │ │ └── variables-bootstrap.styl │ ├── mixins.styl │ ├── reset.styl │ └── variables.styl ├── utils │ ├── axios.js │ ├── cache.js │ ├── console.js │ ├── directives.js │ ├── filters.js │ ├── index.js │ └── mixins.js └── vuex │ ├── actions.js │ ├── getters.js │ ├── mutation-types.js │ ├── mutations.js │ ├── state.js │ └── store.js └── static ├── icons ├── music-120.png ├── music-144.png ├── music-152.png ├── music-192.png ├── music-256.png ├── music-384.png ├── music-48.png └── music-512.png └── manifest.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "env", 4 | "stage-2" 5 | ], 6 | "plugins": [ 7 | [ 8 | "transform-runtime" 9 | ] 10 | ] 11 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | indent_style = space 16 | indent_size = 4 17 | 18 | [*.js] 19 | indent_style = space 20 | indent_size = 2 21 | 22 | [*.coffee] 23 | indent_style = space 24 | indent_size = 2 25 | 26 | [*.html] 27 | indent_style = space 28 | indent_size = 4 29 | 30 | [*.ejs] 31 | indent_style = space 32 | indent_size = 4 33 | 34 | [*.css] 35 | indent_style = space 36 | indent_size = 2 37 | 38 | [*.less] 39 | indent_style = space 40 | indent_size = 2 41 | 42 | [*.styl] 43 | indent_style = space 44 | indent_size = 2 45 | 46 | [*.vue] 47 | indent_style = space 48 | indent_size = 2 49 | 50 | [*.eslintrc] 51 | indent_style = space 52 | indent_size = 2 53 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /dist -------------------------------------------------------------------------------- /.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 | 'plugin:vue/essential', 13 | 'standard' 14 | ], 15 | plugins: [ 16 | 'vue' 17 | ], 18 | rules: { 19 | 'semi': [2, 'always'], 20 | 'generator-star-spacing': 0, 21 | 'space-before-function-paren': 0, 22 | 'prefer-promise-reject-errors': 0, 23 | 'eol-last': 0, 24 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off' 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependency directory 2 | node_modules 3 | 4 | # Logs 5 | logs -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-music-webapp 网易云音乐 2 | 3 | 技术栈: vue、vuex、better-sroll、vue-lazyload、webpack3 4 | 5 | ## 网易云音乐接口地址 6 | [网易云接口仓库地址](https://github.com/Binaryify/NeteaseCloudMusicApi) 7 | 8 | ## Live demo 9 | http://music.ipeihan.top 10 | 11 | 手机扫码,体验更加 12 | 13 | ![二维码](http://images.ipeihan.top/20191021164510.png) 14 | 15 | 16 | ## Typescript 版 17 | Typescript 版请移步https://github.com/lpeihan/netease 18 | 19 | ## Usage 20 | ```shell 21 | # 开发环境 22 | npm run dev 23 | 24 | # 生产环境 25 | npm run build 26 | ``` 27 | 28 | ## Finished 29 | * 推荐页面 30 | 31 | * 音乐排行 32 | * 歌手页表 33 | * 我的收藏 34 | * 最近播放 35 | * 搜索 36 | * 歌曲列表 37 | * 播放列表 38 | * 歌词展示 39 | * 评论展示 40 | * 切换播放模式 41 | 42 | ## TodoList 43 | 44 | * ~~加入 `pwa`,支持添加图标到桌面~~ 45 | * ~~兼容`safari`以及部分不能播放音频的安卓手机~~ 46 | -------------------------------------------------------------------------------- /build/build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | process.env.NODE_ENV = 'production'; 4 | 5 | const ora = require('ora'); 6 | const rm = require('rimraf'); 7 | const chalk = require('chalk'); 8 | const webpack = require('webpack'); 9 | const webpackConfig = require('./webpack.prod.conf'); 10 | const { resolve } = require('./utils'); 11 | 12 | const spinner = ora('building for production...'); 13 | spinner.start(); 14 | 15 | rm(resolve('dist'), err => { 16 | if (err) { 17 | throw err; 18 | } 19 | webpack(webpackConfig, (err, stats) => { 20 | spinner.stop(); 21 | if (err) { 22 | throw err; 23 | } 24 | process.stdout.write(stats.toString({ 25 | colors: true, 26 | modules: false, 27 | children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build. 28 | chunks: false, 29 | chunkModules: false 30 | }) + '\n\n'); 31 | 32 | if (stats.hasErrors()) { 33 | console.log(chalk.red(' Build failed with errors.\n')); 34 | process.exit(1); 35 | } 36 | 37 | console.log(chalk.cyan(' Build complete.\n')); 38 | console.log(chalk.yellow( 39 | ' Tip: The build folder is ready to be deployed.\n' + 40 | ' You may serve it with a static server:\n' 41 | )); 42 | console.log(chalk.cyan( 43 | ' npm install -g serve\n' + 44 | ' serve -s dist\n' 45 | )); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /build/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lpeihan/vue-music-webapp/e058cf522d731c80881c93f3364335136203b33e/build/logo.png -------------------------------------------------------------------------------- /build/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 5 | 6 | const isProduction = process.env.NODE_ENV === 'production'; 7 | 8 | exports.resolve = function(dir = '') { 9 | return path.join(__dirname, '..', dir); 10 | }; 11 | 12 | exports.assetsPath = function(_path) { 13 | return path.posix.join('static', _path); 14 | }; 15 | 16 | exports.cssLoader = function(loader) { 17 | const loaders = [ 18 | { 19 | loader: 'css-loader', 20 | options: { sourceMap: true } 21 | }, 22 | { 23 | loader: 'postcss-loader', 24 | options: { sourceMap: true } 25 | } 26 | ]; 27 | 28 | if (loader) { 29 | loaders.push({ 30 | loader: `${loader}-loader`, 31 | options: { sourceMap: true } 32 | }); 33 | } 34 | 35 | if (isProduction) { 36 | return ExtractTextPlugin.extract({ 37 | use: loaders, 38 | fallback: 'vue-style-loader' 39 | }); 40 | } else { 41 | return ['vue-style-loader'].concat(loaders); 42 | } 43 | }; 44 | 45 | exports.vueLoaderConf = { 46 | loaders: { 47 | css: exports.cssLoader(), 48 | stylus: exports.cssLoader('stylus') 49 | }, 50 | cssSourceMap: isProduction 51 | }; 52 | -------------------------------------------------------------------------------- /build/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { resolve, assetsPath, cssLoader, vueLoaderConf } = require('./utils'); 4 | 5 | module.exports = { 6 | context: resolve(), 7 | entry: { 8 | app: './src/main.js' 9 | }, 10 | output: { 11 | path: resolve('dist'), 12 | filename: assetsPath('js/[name].js'), 13 | publicPath: '/' 14 | }, 15 | resolve: { 16 | extensions: ['.js', '.vue', '.json'], 17 | alias: { 18 | 'vue$': 'vue/dist/vue.esm.js' 19 | } 20 | }, 21 | module: { 22 | rules: [ 23 | { 24 | test: /\.js$/, 25 | use: 'babel-loader', 26 | include: [ 27 | resolve('src'), 28 | resolve('node_modules/_pinyin@2.8.3@pinyin') 29 | ] 30 | }, 31 | { 32 | test: /\.vue$/, 33 | loader: 'vue-loader', 34 | options: vueLoaderConf 35 | }, 36 | { 37 | test: /\.(js|vue)$/, 38 | loader: 'eslint-loader', 39 | enforce: 'pre', 40 | include: [resolve('src')], 41 | options: { 42 | emitWarning: true 43 | } 44 | }, 45 | { 46 | test: /assets[\\/]+?\S+\.svg$/, 47 | use: 'svg-inline-loader' 48 | }, 49 | { 50 | test: /\.css$/, 51 | use: cssLoader() 52 | }, 53 | { 54 | test: /\.styl$/, 55 | use: cssLoader('stylus') 56 | }, 57 | { 58 | test: /\.(png|jpe?g|gif)(\?.*)?$/, 59 | loader: 'url-loader', 60 | options: { 61 | limit: 1000, 62 | name: assetsPath('img/[name].[hash:7].[ext]') 63 | } 64 | }, 65 | { 66 | test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, 67 | loader: 'url-loader', 68 | options: { 69 | limit: 10000, 70 | name: assetsPath('media/[name].[hash:7].[ext]') 71 | } 72 | }, 73 | { 74 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 75 | loader: 'url-loader', 76 | options: { 77 | limit: 10000, 78 | name: assetsPath('fonts/[name].[hash:7].[ext]') 79 | } 80 | } 81 | ] 82 | } 83 | }; 84 | -------------------------------------------------------------------------------- /build/webpack.dev.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const webpack = require('webpack'); 4 | const path = require('path'); 5 | const merge = require('webpack-merge'); 6 | const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin'); 7 | const notifier = require('node-notifier'); 8 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 9 | 10 | const packageConfig = require('../package.json'); 11 | const baseWebpackConf = require('./webpack.base.conf'); 12 | const { 13 | resolve 14 | } = require('./utils'); 15 | 16 | const host = 'localhost'; 17 | const port = 8302; 18 | const proxy = {}; 19 | 20 | module.exports = merge(baseWebpackConf, { 21 | devServer: { 22 | host, 23 | port, 24 | proxy, 25 | hot: true, 26 | inline: true, 27 | open: true, 28 | compress: true, 29 | quiet: true, 30 | clientLogLevel: 'warning', 31 | contentBase: resolve('static'), 32 | overlay: { 33 | errors: true, 34 | warnings: false 35 | }, 36 | historyApiFallback: { 37 | rewrites: [ 38 | { from: /.*/, to: path.posix.join('/', 'index.html') } 39 | ] 40 | } 41 | }, 42 | devtool: 'cheap-module-eval-source-map', 43 | plugins: [ 44 | new webpack.DefinePlugin({ 45 | 'process.env': { 46 | 'NODE_ENV': '"development"' 47 | } 48 | }), 49 | new HtmlWebpackPlugin({ 50 | filename: 'index.html', 51 | template: 'index.html', 52 | title: packageConfig.name, 53 | inject: true, 54 | favicon: resolve('favicon.ico') 55 | }), 56 | new webpack.HotModuleReplacementPlugin(), 57 | new webpack.NamedModulesPlugin(), 58 | new FriendlyErrorsPlugin({ 59 | compilationSuccessInfo: { 60 | messages: [ 61 | `Your application is running here http://${host}:${port}` 62 | ] 63 | }, 64 | onErrors: function(severity, errors) { 65 | if (severity !== 'error') { 66 | return; 67 | } 68 | const error = errors[0]; 69 | const filename = error.file.split('!').pop(); 70 | notifier.notify({ 71 | title: packageConfig.name, 72 | message: severity + ': ' + error.name, 73 | subtitle: filename || '', 74 | icon: path.resolve(__dirname, 'logo.png') 75 | }); 76 | } 77 | }) 78 | ] 79 | }); 80 | -------------------------------------------------------------------------------- /build/webpack.prod.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const webpack = require('webpack'); 4 | const merge = require('webpack-merge'); 5 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 6 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 7 | const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin'); 8 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 9 | 10 | const packageConfig = require('../package.json'); 11 | const baseWebpackConf = require('./webpack.base.conf'); 12 | const { 13 | resolve, 14 | assetsPath 15 | } = require('./utils'); 16 | 17 | module.exports = merge(baseWebpackConf, { 18 | output: { 19 | publicPath: '/', 20 | filename: assetsPath('js/[name].[chunkhash].js') 21 | }, 22 | devtool: '#source-map', 23 | plugins: [ 24 | new webpack.DefinePlugin({ 25 | 'process.env': { 26 | 'NODE_ENV': '"production"' 27 | } 28 | }), 29 | new HtmlWebpackPlugin({ 30 | filename: 'index.html', 31 | template: 'index.html', 32 | title: packageConfig.name, 33 | favicon: resolve('favicon.ico'), 34 | inject: true, 35 | minify: { 36 | removeComments: true, 37 | collapseWhitespace: true, 38 | removeAttributeQuotes: true 39 | }, 40 | chunksSortMode: 'dependency' 41 | }), 42 | new webpack.optimize.UglifyJsPlugin({ 43 | parallel: true, 44 | sourceMap: true, 45 | compress: { 46 | warnings: false 47 | } 48 | }), 49 | new ExtractTextPlugin({ 50 | filename: assetsPath('css/[name].[contenthash].css'), 51 | allChunks: true 52 | }), 53 | new OptimizeCSSPlugin({ 54 | cssProcessorOptions: { safe: true, map: { inline: false } } 55 | }), 56 | new webpack.HashedModuleIdsPlugin(), 57 | new webpack.optimize.CommonsChunkPlugin({ 58 | name: 'vendor', 59 | minChunks: function(module) { 60 | return ( 61 | module.resource && 62 | /\.js$/.test(module.resource) && 63 | module.resource.indexOf(resolve('node_modules')) === 0 64 | ); 65 | } 66 | }), 67 | new webpack.optimize.CommonsChunkPlugin({ 68 | name: 'manifest', 69 | minChunks: Infinity 70 | }), 71 | new CopyWebpackPlugin([ 72 | { 73 | from: resolve('static'), 74 | to: '' 75 | } 76 | ]) 77 | ] 78 | }); 79 | -------------------------------------------------------------------------------- /dist/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lpeihan/vue-music-webapp/e058cf522d731c80881c93f3364335136203b33e/dist/favicon.ico -------------------------------------------------------------------------------- /dist/icons/music-120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lpeihan/vue-music-webapp/e058cf522d731c80881c93f3364335136203b33e/dist/icons/music-120.png -------------------------------------------------------------------------------- /dist/icons/music-144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lpeihan/vue-music-webapp/e058cf522d731c80881c93f3364335136203b33e/dist/icons/music-144.png -------------------------------------------------------------------------------- /dist/icons/music-152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lpeihan/vue-music-webapp/e058cf522d731c80881c93f3364335136203b33e/dist/icons/music-152.png -------------------------------------------------------------------------------- /dist/icons/music-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lpeihan/vue-music-webapp/e058cf522d731c80881c93f3364335136203b33e/dist/icons/music-192.png -------------------------------------------------------------------------------- /dist/icons/music-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lpeihan/vue-music-webapp/e058cf522d731c80881c93f3364335136203b33e/dist/icons/music-256.png -------------------------------------------------------------------------------- /dist/icons/music-384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lpeihan/vue-music-webapp/e058cf522d731c80881c93f3364335136203b33e/dist/icons/music-384.png -------------------------------------------------------------------------------- /dist/icons/music-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lpeihan/vue-music-webapp/e058cf522d731c80881c93f3364335136203b33e/dist/icons/music-48.png -------------------------------------------------------------------------------- /dist/icons/music-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lpeihan/vue-music-webapp/e058cf522d731c80881c93f3364335136203b33e/dist/icons/music-512.png -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | Netease
-------------------------------------------------------------------------------- /dist/manifest.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "name": "Netease", 4 | "short_name": "Netease", 5 | "icons": [{ 6 | "src": "/icons/music-120.png", 7 | "sizes": "120x120", 8 | "type": "image/png" 9 | }, { 10 | "src": "/icons/music-144.png", 11 | "sizes": "144x144", 12 | "type": "image/png" 13 | }, { 14 | "src": "/icons/music-152.png", 15 | "sizes": "152x152", 16 | "type": "image/png" 17 | }, { 18 | "src": "/icons/music-192.png", 19 | "sizes": "192x192", 20 | "type": "image/png" 21 | }, { 22 | "src": "/icons/music-256.png", 23 | "sizes": "256x256", 24 | "type": "image/png" 25 | }, { 26 | "src": "/icons/music-384.png", 27 | "sizes": "384x384", 28 | "type": "image/png" 29 | }, { 30 | "src": "/icons/music-512.png", 31 | "sizes": "512x512", 32 | "type": "image/png" 33 | }], 34 | "start_url": "/", 35 | "background_color": "#f2f3f5", 36 | "display": "standalone", 37 | "theme_color": "#d44439" 38 | } -------------------------------------------------------------------------------- /dist/static/img/default.9710131.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lpeihan/vue-music-webapp/e058cf522d731c80881c93f3364335136203b33e/dist/static/img/default.9710131.png -------------------------------------------------------------------------------- /dist/static/img/empty.4c4c2c3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lpeihan/vue-music-webapp/e058cf522d731c80881c93f3364335136203b33e/dist/static/img/empty.4c4c2c3.png -------------------------------------------------------------------------------- /dist/static/img/loading.1f26c50.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lpeihan/vue-music-webapp/e058cf522d731c80881c93f3364335136203b33e/dist/static/img/loading.1f26c50.gif -------------------------------------------------------------------------------- /dist/static/js/manifest.2ae2e69a05c33dfc65f8.js: -------------------------------------------------------------------------------- 1 | !function(r){function n(e){if(o[e])return o[e].exports;var t=o[e]={i:e,l:!1,exports:{}};return r[e].call(t.exports,t,t.exports,n),t.l=!0,t.exports}var e=window.webpackJsonp;window.webpackJsonp=function(o,u,c){for(var f,i,p,a=0,l=[];a 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | <%= htmlWebpackPlugin.options.title %> 16 | 17 | 18 |
19 | 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Netease", 3 | "version": "1.0.0", 4 | "homepage": "https://lpeihan.github.io/vue-music-webapp", 5 | "description": "", 6 | "main": "index.js", 7 | "scripts": { 8 | "build": "node build/build.js", 9 | "dev": "webpack-dev-server --config build/webpack.dev.conf.js", 10 | "predeploy": "npm run build", 11 | "deploy": "gh-pages -d dist" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "ISC", 16 | "devDependencies": { 17 | "autoprefixer": "^8.3.0", 18 | "babel-core": "^6.26.0", 19 | "babel-eslint": "^8.2.3", 20 | "babel-loader": "^7.1.4", 21 | "babel-plugin-transform-runtime": "^6.23.0", 22 | "babel-preset-env": "^1.6.1", 23 | "babel-preset-stage-2": "^6.24.1", 24 | "chalk": "^2.4.0", 25 | "copy-webpack-plugin": "^4.5.1", 26 | "css-loader": "^0.28.11", 27 | "eslint": "^4.19.1", 28 | "eslint-config-standard": "^11.0.0", 29 | "eslint-loader": "^2.0.0", 30 | "eslint-plugin-import": "^2.11.0", 31 | "eslint-plugin-node": "^6.0.1", 32 | "eslint-plugin-promise": "^3.7.0", 33 | "eslint-plugin-standard": "^3.0.1", 34 | "eslint-plugin-vue": "^4.5.0", 35 | "extract-text-webpack-plugin": "^3.0.2", 36 | "file-loader": "^1.1.11", 37 | "friendly-errors-webpack-plugin": "^1.7.0", 38 | "gh-pages": "^1.1.0", 39 | "html-webpack-plugin": "^3.2.0", 40 | "node-notifier": "^5.2.1", 41 | "optimize-css-assets-webpack-plugin": "^3.2.0", 42 | "ora": "^2.0.0", 43 | "postcss-loader": "^2.1.4", 44 | "rimraf": "^2.6.2", 45 | "stylus": "^0.54.5", 46 | "stylus-loader": "^3.0.2", 47 | "svg-inline-loader": "^0.8.0", 48 | "url-loader": "^1.0.1", 49 | "vconsole": "^3.2.0", 50 | "vue-lazyload": "^1.2.6", 51 | "vue-loader": "^14.2.2", 52 | "vue-style-loader": "^4.1.0", 53 | "vue-template-compiler": "^2.5.16", 54 | "webpack": "^3.8.1", 55 | "webpack-dev-server": "^2.9.7", 56 | "webpack-merge": "^4.1.2" 57 | }, 58 | "dependencies": { 59 | "axios": "^0.18.0", 60 | "better-scroll": "^1.15.1", 61 | "create-keyframe-animation": "^0.1.0", 62 | "lyric-parser": "^1.0.1", 63 | "pinyin": "2.8.3", 64 | "vue": "^2.5.16", 65 | "vue-lazyload": "^1.2.3", 66 | "vue-router": "^3.0.1", 67 | "vue-swiper-component": "^2.1.0", 68 | "vuex": "^3.0.1" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('autoprefixer') 4 | ] 5 | }; 6 | -------------------------------------------------------------------------------- /src/api/config.js: -------------------------------------------------------------------------------- 1 | // export const host = 'http://120.79.162.149:3000'; 2 | 3 | // export const host = 'http://192.168.31.170:3000'; 4 | 5 | export const host = 'http://47.98.144.117:3000'; 6 | -------------------------------------------------------------------------------- /src/api/recommends.js: -------------------------------------------------------------------------------- 1 | import { host } from './config'; 2 | import axios from 'axios'; 3 | 4 | export function getBanners () { 5 | return axios.get(host + '/banner'); 6 | } 7 | 8 | export function getRecommends() { 9 | return axios.get(host + '/personalized'); 10 | } 11 | 12 | export function getRecommendDetail(id) { 13 | return axios.get(host + `/playlist/detail?id=${id}`); 14 | } -------------------------------------------------------------------------------- /src/api/search.js: -------------------------------------------------------------------------------- 1 | import { host } from './config'; 2 | import axios from 'axios'; 3 | 4 | export function getSearchHot() { 5 | return axios.get(host + `/search/hot`); 6 | } 7 | 8 | export function getSearchSuggest(keywords) { 9 | return axios.get(host + `/search/suggest?keywords=${keywords}`); 10 | } 11 | 12 | export function getSearchSinger(keywords) { 13 | return axios.get(host + `/search?keywords=${keywords}&type=100&limit=1`); 14 | } 15 | 16 | export function getSearchSongs(keywords, offset = 0, limit = 20) { 17 | return axios.get(host + `/search?keywords=${keywords}&offset=${offset}&limit=${limit}`); 18 | } 19 | 20 | export function getSearchMusicList(keywords) { 21 | return axios.get(host + `/search?keywords=${keywords}&type=1000&limit=1`); 22 | } 23 | 24 | export function getSongDetail (id) { 25 | return axios.get(host + `/song/detail?ids=${id}`); 26 | } 27 | -------------------------------------------------------------------------------- /src/api/singers.js: -------------------------------------------------------------------------------- 1 | import { host } from './config'; 2 | import axios from 'axios'; 3 | 4 | export function getSingers () { 5 | return axios.get(host + '/top/artists?limit=100'); 6 | } 7 | 8 | export function getSingerDetail(id) { 9 | return axios.get(host + `/artists?id=${id}`); 10 | } -------------------------------------------------------------------------------- /src/api/song.js: -------------------------------------------------------------------------------- 1 | import { host } from './config'; 2 | import axios from 'axios'; 3 | 4 | export function getSong(id) { 5 | return axios.get(host + `/song/url?id=${id}`); 6 | } 7 | 8 | export function getLyric (id) { 9 | return axios.get(host + `/lyric?id=${id}`); 10 | } 11 | 12 | export function getComments(id, offset = 0) { 13 | return axios.get(host + `/comment/music?id=${id}&offset=${offset}`); 14 | } 15 | -------------------------------------------------------------------------------- /src/api/top-list.js: -------------------------------------------------------------------------------- 1 | import { host } from './config'; 2 | import axios from 'axios'; 3 | 4 | export function getTopList() { 5 | return axios.get(host + `/toplist/detail`); 6 | } 7 | 8 | export function getTopListDetail(id) { 9 | return axios.get(host + `/playlist/detail?id=${id}`); 10 | } -------------------------------------------------------------------------------- /src/app.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 62 | 63 | 65 | -------------------------------------------------------------------------------- /src/assets/icons/back.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/calendar.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/clear.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/clock.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/comment.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/delete.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/disc.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/down.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/download.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/favorite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/fm.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/headphones.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/like.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/loop.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/menu.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/mini-pause.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/mini-pause2.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/mini-play.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/mini-play2.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/more.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/music.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/once.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/pause.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/play.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/playlist.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/prev.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/random.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/rank.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/repeat.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/search.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/unfavorite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/volume.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lpeihan/vue-music-webapp/e058cf522d731c80881c93f3364335136203b33e/src/assets/images/default.png -------------------------------------------------------------------------------- /src/assets/images/empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lpeihan/vue-music-webapp/e058cf522d731c80881c93f3364335136203b33e/src/assets/images/empty.png -------------------------------------------------------------------------------- /src/assets/images/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lpeihan/vue-music-webapp/e058cf522d731c80881c93f3364335136203b33e/src/assets/images/loading.gif -------------------------------------------------------------------------------- /src/components/confirm.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 42 | 43 | 89 | -------------------------------------------------------------------------------- /src/components/empty.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 18 | -------------------------------------------------------------------------------- /src/components/loading.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 18 | 19 | 30 | -------------------------------------------------------------------------------- /src/components/progress-bar.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 70 | 71 | 100 | -------------------------------------------------------------------------------- /src/components/progress-circle.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 51 | 52 | 80 | -------------------------------------------------------------------------------- /src/components/scroll.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 86 | 87 | 91 | -------------------------------------------------------------------------------- /src/components/swiper.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 132 | 133 | 166 | -------------------------------------------------------------------------------- /src/components/tab.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 104 | 105 | 154 | -------------------------------------------------------------------------------- /src/components/toast.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 31 | 32 | 54 | -------------------------------------------------------------------------------- /src/icon.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 52 | 53 | 65 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import App from './app'; 3 | import router from './route'; 4 | import Icon from './icon'; 5 | import store from './vuex/store'; 6 | import VueLazyload from 'vue-lazyload'; 7 | 8 | /* eslint-disable no-unused-vars */ 9 | // import VConsole from 'vconsole'; 10 | import './utils/console'; 11 | 12 | import './utils/axios'; 13 | import './utils/directives'; 14 | import './utils/filters'; 15 | import './styles/index.styl'; 16 | 17 | // const vConsole = new VConsole(); 18 | 19 | Vue.component('icon', Icon); 20 | 21 | Vue.use(VueLazyload, { 22 | loading: require('./assets/images/default.png') 23 | }); 24 | 25 | Vue.config.productionTip = false; 26 | 27 | /* eslint-disable no-new */ 28 | new Vue({ 29 | el: '#app', 30 | router, 31 | store, 32 | components: { App }, 33 | template: '' 34 | }); 35 | -------------------------------------------------------------------------------- /src/pages/comment/comment-item.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 30 | 31 | -------------------------------------------------------------------------------- /src/pages/comment/comment.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 108 | 109 | 171 | -------------------------------------------------------------------------------- /src/pages/entry/entry.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 38 | 39 | 68 | -------------------------------------------------------------------------------- /src/pages/header/header.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 33 | -------------------------------------------------------------------------------- /src/pages/music-list/music-list.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 109 | 110 | 198 | -------------------------------------------------------------------------------- /src/pages/music-list/song-list.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 32 | 33 | 81 | -------------------------------------------------------------------------------- /src/pages/player/player.vue: -------------------------------------------------------------------------------- 1 | 111 | 112 | 430 | 431 | 621 | -------------------------------------------------------------------------------- /src/pages/playlist/playlist.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 117 | 118 | 188 | -------------------------------------------------------------------------------- /src/pages/recommends/recommend-detail.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 58 | -------------------------------------------------------------------------------- /src/pages/recommends/recommends.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 113 | 114 | 172 | -------------------------------------------------------------------------------- /src/pages/search/search-box.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 55 | 56 | 89 | -------------------------------------------------------------------------------- /src/pages/search/search.vue: -------------------------------------------------------------------------------- 1 | 87 | 88 | 270 | 271 | 411 | -------------------------------------------------------------------------------- /src/pages/singers/singer-detail.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 56 | -------------------------------------------------------------------------------- /src/pages/singers/singers.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 188 | 189 | 245 | -------------------------------------------------------------------------------- /src/pages/top-list/top-list-detail.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 52 | -------------------------------------------------------------------------------- /src/pages/top-list/top-list.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 83 | 84 | 120 | -------------------------------------------------------------------------------- /src/pages/user/user.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 72 | 73 | 113 | -------------------------------------------------------------------------------- /src/route.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Router from 'vue-router'; 3 | import RecommendDetail from './pages/recommends/recommend-detail'; 4 | import SingerDetail from './pages/singers/singer-detail'; 5 | import TopListDetail from './pages/top-list/top-list-detail'; 6 | import Search from './pages/search/search'; 7 | import User from './pages/user/user'; 8 | import Comments from './pages/comment/comment'; 9 | 10 | Vue.use(Router); 11 | 12 | export default new Router({ 13 | mode: 'history', 14 | routes: [ 15 | { 16 | path: '/recommends/:id', 17 | name: 'recommendsDetail', 18 | component: RecommendDetail 19 | }, 20 | { 21 | path: '/singers/:id', 22 | name: 'singerDetail', 23 | component: SingerDetail 24 | }, 25 | { 26 | path: '/top-list/:id', 27 | name: 'topListDetail', 28 | component: TopListDetail 29 | }, 30 | { 31 | path: '/search', 32 | name: 'search', 33 | component: Search, 34 | children: [ 35 | { 36 | path: 'singers/:id', 37 | component: SingerDetail 38 | }, 39 | { 40 | path: 'recommends/:id', 41 | component: RecommendDetail 42 | } 43 | ] 44 | }, 45 | { 46 | path: '/user', 47 | name: 'user', 48 | component: User 49 | }, 50 | { 51 | path: '/music/comment/:id/full-screen', 52 | name: 'comment', 53 | component: Comments 54 | } 55 | ] 56 | }); 57 | -------------------------------------------------------------------------------- /src/services/cache.js: -------------------------------------------------------------------------------- 1 | import storage from '../utils/cache'; 2 | 3 | const SEARCH_SHITORY = '__SEARCH_HISTORY__'; 4 | const FAVORITE_LIST = '__FAVORITE_LIST__'; 5 | const PLAY_HISTORY = '__PLAY_HISTORY__'; 6 | 7 | export function loadSearchHistory() { 8 | return storage.getItem(SEARCH_SHITORY); 9 | } 10 | 11 | export function cacheSearchHistory(history) { 12 | return storage.setItem(SEARCH_SHITORY, history); 13 | } 14 | 15 | export function loadFavoriteList() { 16 | return storage.getItem(FAVORITE_LIST); 17 | } 18 | 19 | export function cacheFavoriteList(list) { 20 | return storage.setItem(FAVORITE_LIST, list); 21 | } 22 | 23 | export function loadPlayHistory() { 24 | return storage.getItem(PLAY_HISTORY); 25 | } 26 | 27 | export function cachePlayHistory(history) { 28 | return storage.setItem(PLAY_HISTORY, history); 29 | } 30 | -------------------------------------------------------------------------------- /src/services/config.js: -------------------------------------------------------------------------------- 1 | export const mode = { 2 | loop: 0, 3 | random: 1, 4 | once: 2 5 | }; -------------------------------------------------------------------------------- /src/services/song.js: -------------------------------------------------------------------------------- 1 | class Song { 2 | constructor({id, singer, name, image, desc, alias}) { 3 | this.id = id; 4 | this.singer = singer; 5 | this.name = name; 6 | this.image = image; 7 | this.desc = desc; 8 | this.alias = alias; 9 | } 10 | } 11 | 12 | function singerName(names) { 13 | let name = []; 14 | name = names.map((item) => { 15 | return item.name; 16 | }); 17 | 18 | return name.join('/'); 19 | } 20 | 21 | export function createSong(song) { 22 | return new Song({ 23 | id: song.id, 24 | name: `${song.name}`, 25 | singer: singerName(song.ar), 26 | image: song.al.picUrl, 27 | desc: `${song.al.name}` 28 | }); 29 | } 30 | 31 | export function createSearchSong(song) { 32 | return new Song({ 33 | id: song.id, 34 | name: song.name, 35 | singer: singerName(song.artists), 36 | alias: song.alias ? song.alias[0] : '', 37 | image: song.artists[0].img1v1Url 38 | }); 39 | } -------------------------------------------------------------------------------- /src/styles/base.styl: -------------------------------------------------------------------------------- 1 | @import "./variables.styl" 2 | 3 | body 4 | line-height: 1 5 | font-family: 'PingFang SC', 'STHeitiSC-Light', 'Helvetica-Light', arial, sans-serif, 'Droid Sans Fallback' 6 | user-select: none 7 | background: $color-background 8 | color: $color-text -------------------------------------------------------------------------------- /src/styles/highlight.styl: -------------------------------------------------------------------------------- 1 | @import "./variables" 2 | 3 | .highlight-text 4 | color: $color-primary -------------------------------------------------------------------------------- /src/styles/index.styl: -------------------------------------------------------------------------------- 1 | @import "./reset.styl" 2 | @import "./base.styl" 3 | @import "./highlight.styl" -------------------------------------------------------------------------------- /src/styles/mixins.styl: -------------------------------------------------------------------------------- 1 | // 2 | // Mixins 3 | // -------------------------------------------------- 4 | 5 | @import "./variables" 6 | 7 | size() 8 | if length(arguments) == 1 9 | width: arguments[0] 10 | height: arguments[0] 11 | else 12 | width: arguments[0] 13 | height: arguments[1] 14 | 15 | no-wrap() 16 | text-overflow: ellipsis 17 | overflow: hidden 18 | white-space: nowrap 19 | 20 | -pos(type, args) 21 | i = 0 22 | position: unquote(type) 23 | for j in (1..4) 24 | if length(args) > i 25 | {args[i]}: args[i + 1] is a 'unit' ? args[i += 1] : 0 26 | i += 1 27 | 28 | fixed() 29 | -pos('fixed', arguments) 30 | 31 | absolute() 32 | -pos('absolute', arguments) 33 | 34 | relative() 35 | -pos('relative', arguments) 36 | 37 | 38 | border-1px($color = $color-border, $left = 0, $right = 0) 39 | position: relative 40 | &::after 41 | content: '' 42 | absolute: left $left bottom 0 right $right 43 | border-top: 1px solid $color 44 | transform: scaleY(0.5) 45 | 46 | background-filter($blur = 80px, $scale = 1.5, $background = rgba(7, 17, 27, 0.2)) 47 | absolute: top 0 left 0 right 0 bottom 0 48 | background-size: cover 49 | filter: blur($blur) 50 | transform: scale($scale) 51 | z-index: -1 52 | &::before 53 | content: '' 54 | absolute: top 0 left 0 right 0 bottom 0 55 | background: $background 56 | -------------------------------------------------------------------------------- /src/styles/reset.styl: -------------------------------------------------------------------------------- 1 | /** 2 | * Eric Meyer's Reset CSS v2.0 (http://meyerweb.com/eric/tools/css/reset/) 3 | * http://cssreset.com 4 | */ 5 | html, body, div, span, applet, object, iframe, 6 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 7 | a, abbr, acronym, address, big, cite, code, 8 | del, dfn, em, img, ins, kbd, q, s, samp, 9 | small, strike, strong, sub, sup, tt, var, 10 | b, u, i, center, 11 | dl, dt, dd, ol, ul, li, 12 | fieldset, form, label, legend, 13 | table, caption, tbody, tfoot, thead, tr, th, td, 14 | article, aside, canvas, details, embed, 15 | figure, figcaption, footer, header, 16 | menu, nav, output, ruby, section, summary, 17 | time, mark, audio, video, input 18 | margin: 0 19 | padding: 0 20 | border: 0 21 | font-size: 100% 22 | font-weight: normal 23 | vertical-align: baseline 24 | 25 | /* HTML5 display-role reset for older browsers */ 26 | article, aside, details, figcaption, figure, 27 | footer, header, menu, nav, section 28 | display: block 29 | 30 | body 31 | line-height: 1 32 | 33 | blockquote, q 34 | quotes: none 35 | 36 | blockquote:before, blockquote:after, 37 | q:before, q:after 38 | content: none 39 | 40 | table 41 | border-collapse: collapse 42 | border-spacing: 0 43 | 44 | /* custom */ 45 | 46 | a 47 | color: #7e8c8d 48 | -webkit-backface-visibility: hidden 49 | text-decoration: none 50 | 51 | li 52 | list-style: none 53 | 54 | body 55 | -webkit-text-size-adjust: none 56 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0) 57 | -------------------------------------------------------------------------------- /src/styles/variables.styl: -------------------------------------------------------------------------------- 1 | // 2 | // Variables 3 | // -------------------------------------------------- 4 | 5 | $color-background = white 6 | $color-highlight-background = rgb(253, 108, 98) 7 | $color-theme = rgb(212, 68, 57) 8 | $color-sub-theme = rgb(240, 116, 107) 9 | $color-text = #2E3030 10 | $color-text-l = #757575 11 | $color-text-g = #c7c7c7 12 | $color-text-ll = rgba(255, 255, 255, 0.8) 13 | $color-border = #dddddd 14 | $color-overlay = rgba(7, 17, 27, 0.4) 15 | 16 | $white = #ffffff 17 | $color-primary = #337ab7 18 | 19 | $font-size-small-s = 10px 20 | $font-size-small = 12px 21 | $font-size-medium = 14px 22 | $font-size-medium-x = 16px 23 | $font-size-large = 18px 24 | $font-size-large-x = 24px 25 | $font-size-default = $font-size-medium-x 26 | 27 | $font-size-icon = 28px -------------------------------------------------------------------------------- /src/utils/axios.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import axios from 'axios'; 3 | 4 | Vue.prototype.$http = axios; 5 | -------------------------------------------------------------------------------- /src/utils/cache.js: -------------------------------------------------------------------------------- 1 | function serialize(val) { 2 | return JSON.stringify(val); 3 | } 4 | 5 | function deserialize(val) { 6 | return JSON.parse(val); 7 | } 8 | 9 | const storage = { 10 | store: window.localStorage, 11 | 12 | getItem(key) { 13 | return deserialize(this.store.getItem(key)); 14 | }, 15 | setItem(key, val) { 16 | this.store.setItem(key, serialize(val)); 17 | 18 | return val; 19 | }, 20 | removeItem(key) { 21 | this.store.removeItem(key); 22 | } 23 | }; 24 | 25 | export default storage; -------------------------------------------------------------------------------- /src/utils/console.js: -------------------------------------------------------------------------------- 1 | export function loadScript(src, cb) { 2 | const script = document.createElement('script'); 3 | script.type = 'text/javascript'; 4 | script.src = src; 5 | 6 | let flag = false; // 防止 IE9/10 中执行两次 7 | 8 | script.onload = script.onreadystatechange = function() { 9 | if ( 10 | flag === false && 11 | (!this.readyState || this.readyState === 'complete') 12 | ) { 13 | flag = true; 14 | cb && cb(); 15 | } 16 | }; 17 | 18 | const s = document.getElementsByTagName('script')[0]; 19 | s.parentNode.insertBefore(script, s); 20 | } 21 | 22 | let vconsole; 23 | let count = 0; 24 | const url = 25 | 'https://res.wx.qq.com/mmbizwap/zh_CN/htmledition/js/vconsole/3.0.0/vconsole.min.js'; 26 | 27 | // 通过 url 唤醒 28 | if (/console/g.test(location.href)) { 29 | loadScript(url, function() { 30 | if (typeof vconsole === 'undefined') { 31 | /* eslint-disable */ 32 | vconsole = new VConsole(); 33 | } 34 | }); 35 | } 36 | 37 | // 通过点击事件 38 | window.addEventListener('load', function() { 39 | const entry = document.querySelector('#vconsole-secret'); 40 | 41 | // 点击 #vconsole-secret 的元素唤起 42 | entry && 43 | entry.addEventListener('click', function() { 44 | count++; 45 | 46 | if (count > 5) { 47 | count = -10000; 48 | loadScript(url, function() { 49 | if (typeof vconsole === 'undefined') { 50 | /* eslint-disable */ 51 | vconsole = new VConsole(); 52 | } 53 | }); 54 | } 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /src/utils/directives.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | 3 | Vue.directive('focus', { 4 | inserted(el) { 5 | el.focus(); 6 | } 7 | }); -------------------------------------------------------------------------------- /src/utils/filters.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | 3 | import { leftpad } from './index'; 4 | 5 | function fillZero(str) { 6 | return leftpad(str, 2, '0'); 7 | } 8 | 9 | Vue.filter('date', (value) => { 10 | const date = new Date(parseInt(value, 10)); 11 | return value ? `${date.getFullYear()}年${fillZero(date.getMonth() + 1)}月\ 12 | ${fillZero(date.getDate())}日` : ''; 13 | }); 14 | 15 | Vue.filter('highlight', (value, key) => { 16 | const reg = new RegExp(key, 'g'); 17 | return value.replace(reg, `${key}`); 18 | }); 19 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | export function leftpad(str, len, ch = ' ') { 2 | str = `${str}`; 3 | 4 | for (let i = str.length; i < len; i++) { 5 | str = ch + str; 6 | } 7 | 8 | return str; 9 | }; 10 | 11 | export function shuffle(array) { 12 | const items = array.slice(); 13 | let t, r, i; 14 | 15 | for (i = items.length - 1; i > 0; i--) { 16 | r = Math.round(Math.random() * i); 17 | 18 | t = items[i]; 19 | items[i] = items[r]; 20 | items[r] = t; 21 | } 22 | 23 | return items; 24 | } 25 | 26 | export function debounce(func, delay) { 27 | let timer; 28 | 29 | return function(...args) { 30 | if (timer) { 31 | clearTimeout(timer); 32 | } 33 | 34 | timer = setTimeout(() => { 35 | func.apply(this, args); 36 | }, delay); 37 | }; 38 | } -------------------------------------------------------------------------------- /src/utils/mixins.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import { mapGetters } from 'vuex'; 3 | 4 | Vue.mixin({}); 5 | 6 | const PLAYLIST = 'playlist'; 7 | 8 | export const playlistMixin = { 9 | computed: { 10 | ...mapGetters([ 11 | 'playlist' 12 | ]) 13 | }, 14 | methods: { 15 | appendBottom(playlist = this.playlist) { 16 | this.$nextTick(() => { 17 | const el = this.$refs.scroll.$el.children[0]; 18 | const lastChild = el.lastChild; 19 | 20 | if (playlist.length && lastChild && lastChild.nodeName !== '#text' && lastChild.getAttribute(PLAYLIST) !== PLAYLIST) { 21 | const bottom = document.createElement('div'); 22 | bottom.style.height = '60px'; 23 | bottom.setAttribute(PLAYLIST, PLAYLIST); 24 | 25 | el.appendChild(bottom); 26 | this.$refs.scroll.refresh(); 27 | } else if (!playlist.length && lastChild && lastChild.nodeName !== '#text' && lastChild.getAttribute(PLAYLIST) === PLAYLIST) { 28 | el.removeChild(lastChild); 29 | this.$refs.scroll.refresh(); 30 | } 31 | }); 32 | } 33 | }, 34 | watch: { 35 | playlist(val) { 36 | this.appendBottom(val); 37 | } 38 | }, 39 | mounted() { 40 | this.appendBottom(this.playlist); 41 | } 42 | }; 43 | 44 | export const showMixin = { 45 | data() { 46 | return { 47 | show: false, 48 | name: 'default' 49 | }; 50 | }, 51 | methods: { 52 | open(name = this.name) { 53 | this.show = true; 54 | this.name = name; 55 | const hash = location.hash ? `&${name}` : `#${name}`; 56 | history.pushState({ page: name }, name, `${location.href}${hash}`); 57 | }, 58 | back() { 59 | history.go(-1); 60 | }, 61 | close() { 62 | if (location.hash.indexOf(this.name) === -1) { 63 | this.show = false; 64 | } 65 | } 66 | }, 67 | created() { 68 | addEventListener('popstate', this.close); 69 | }, 70 | beforeDestroy() { 71 | removeEventListener('popstate', this.close); 72 | } 73 | }; 74 | -------------------------------------------------------------------------------- /src/vuex/actions.js: -------------------------------------------------------------------------------- 1 | import * as types from './mutation-types'; 2 | import { cacheSearchHistory, cacheFavoriteList, cachePlayHistory } from '../services/cache'; 3 | 4 | const actions = { 5 | selectPlay({ commit, state }, { list, index }) { 6 | commit(types.SET_PLAYLIST, list); 7 | commit(types.SET_SEQUENCE_LIST, list); 8 | commit(types.SET_CURRENT_INDEX, index); 9 | commit(types.SET_FULL_SCREEN, true); 10 | commit(types.SET_PLAYING, true); 11 | }, 12 | 13 | deleteSong({ commit, state, dispatch }, { song }) { 14 | const playlist = state.playlist.slice(); 15 | const sequenceList = state.sequenceList.slice(); 16 | let currentIndex = state.currentIndex; 17 | 18 | if (playlist.length === 1) { 19 | dispatch('clearPlaylist'); 20 | return; 21 | } 22 | 23 | const pIndex = playlist.findIndex(item => item.id === song.id); 24 | const sIndex = sequenceList.findIndex(item => item.id === song.id); 25 | 26 | playlist.splice(pIndex, 1); 27 | sequenceList.splice(sIndex, 1); 28 | 29 | if (currentIndex > pIndex || currentIndex === playlist.length) { 30 | currentIndex--; 31 | } 32 | 33 | commit(types.SET_PLAYLIST, playlist); 34 | commit(types.SET_SEQUENCE_LIST, sequenceList); 35 | commit(types.SET_CURRENT_INDEX, currentIndex); 36 | }, 37 | 38 | clearPlaylist({ commit }) { 39 | commit(types.SET_FULL_SCREEN, false); 40 | commit(types.SET_PLAYLIST, []); 41 | commit(types.SET_SEQUENCE_LIST, []); 42 | commit(types.SET_CURRENT_INDEX, -1); 43 | commit(types.SET_PLAYING, false); 44 | }, 45 | 46 | insertSong({ commit, state }, song) { 47 | const playlist = state.playlist.slice(); 48 | const sequenceList = state.sequenceList.slice(); 49 | 50 | const fIndex = playlist.findIndex(item => { 51 | return item.id === song.id; 52 | }); 53 | 54 | let index = 0; 55 | 56 | if (fIndex > -1) { 57 | index = fIndex; 58 | } else { 59 | playlist.unshift(song); 60 | sequenceList.unshift(song); 61 | } 62 | 63 | commit(types.SET_PLAYLIST, playlist); 64 | commit(types.SET_SEQUENCE_LIST, sequenceList); 65 | commit(types.SET_CURRENT_INDEX, index); 66 | commit(types.SET_FULL_SCREEN, true); 67 | commit(types.SET_PLAYING, true); 68 | }, 69 | 70 | saveSearchHistory({ commit, state }, query) { 71 | const searchHistory = state.searchHistory.slice(); 72 | 73 | const index = searchHistory.findIndex(item => item === query); 74 | 75 | if (index > -1) { 76 | searchHistory.splice(index, 1); 77 | } 78 | 79 | searchHistory.unshift(query); 80 | 81 | if (searchHistory.length > 10) { 82 | searchHistory.pop(); 83 | } 84 | 85 | commit(types.SET_SEARCH_HISTORY, cacheSearchHistory(searchHistory)); 86 | }, 87 | 88 | deleteSearchHitory({ commit, state }, index) { 89 | const searchHistory = state.searchHistory.slice(); 90 | 91 | searchHistory.splice(index, 1); 92 | 93 | commit(types.SET_SEARCH_HISTORY, cacheSearchHistory(searchHistory)); 94 | }, 95 | 96 | clearSearchHistory({ commit }) { 97 | commit(types.SET_SEARCH_HISTORY, cacheSearchHistory([])); 98 | }, 99 | 100 | saveFavoriteList({ commit, state }, song) { 101 | const favoriteList = state.favoriteList.slice(); 102 | 103 | const index = favoriteList.findIndex(item => item.id === song.id); 104 | 105 | if (index > -1) { 106 | favoriteList.splice(index, 1); 107 | } 108 | 109 | favoriteList.unshift(song); 110 | 111 | if (favoriteList.length > 100) { 112 | favoriteList.pop(); 113 | } 114 | 115 | commit(types.SET_FAVORITE_LIST, cacheFavoriteList(favoriteList)); 116 | }, 117 | 118 | deleteFavoriteList({ commit, state }, song) { 119 | const favoriteList = state.favoriteList.slice(); 120 | 121 | const index = favoriteList.findIndex(item => song.id === item.id); 122 | 123 | favoriteList.splice(index, 1); 124 | commit(types.SET_FAVORITE_LIST, cacheFavoriteList(favoriteList)); 125 | }, 126 | 127 | savePlayHistory({ commit, state }, song) { 128 | const playHistory = state.playHistory.slice(); 129 | 130 | const index = playHistory.findIndex(item => item.id === song.id); 131 | 132 | if (index > -1) { 133 | playHistory.splice(index, 1); 134 | } 135 | 136 | playHistory.unshift(song); 137 | 138 | if (playHistory.length > 100) { 139 | playHistory.pop(); 140 | } 141 | 142 | commit(types.SET_PLAY_HISTORY, cachePlayHistory(playHistory)); 143 | } 144 | }; 145 | 146 | export default actions; 147 | -------------------------------------------------------------------------------- /src/vuex/getters.js: -------------------------------------------------------------------------------- 1 | const getters = { 2 | musicList: state => state.musicList, 3 | 4 | playlist: state => state.playlist, 5 | 6 | currentIndex: state => state.currentIndex, 7 | 8 | currentSong: state => state.playlist[state.currentIndex] || {}, 9 | 10 | fullScreen: state => state.fullScreen, 11 | 12 | playing: state => state.playing, 13 | 14 | sequenceList: state => state.sequenceList, 15 | 16 | mode: state => state.mode, 17 | 18 | tabIndex: state => state.tabIndex, 19 | 20 | singer: state => state.singer, 21 | 22 | topList: state => state.topList, 23 | 24 | searchHistory: state => state.searchHistory, 25 | 26 | favoriteList: state => state.favoriteList, 27 | 28 | playHistory: state => state.playHistory 29 | }; 30 | 31 | export default getters; 32 | -------------------------------------------------------------------------------- /src/vuex/mutation-types.js: -------------------------------------------------------------------------------- 1 | export const SET_MUSIC_LIST = 'SET_MUSIC_LIST'; 2 | 3 | export const SET_PLAYLIST = 'SET_PLAYLIST'; 4 | 5 | export const SET_CURRENT_INDEX = 'SET_CURRENT_INDEX'; 6 | 7 | export const SET_FULL_SCREEN = 'SET_FULL_SCREEN'; 8 | 9 | export const SET_PLAYING = 'SET_PLAYING'; 10 | 11 | export const SET_SEQUENCE_LIST = 'SET_SEQUENCE_LIST'; 12 | 13 | export const SET_MODE = 'SET_MODE'; 14 | 15 | export const SET_SINGER = 'SET_SINGER'; 16 | 17 | export const SET_TOP_LIST = 'SET_TOP_LIST'; 18 | 19 | export const SET_SEARCH_HISTORY = 'SET_SEARCH_HISTORY'; 20 | 21 | export const SET_FAVORITE_LIST = 'SET_FAVORITE_LIST'; 22 | 23 | export const SET_PLAY_HISTORY = 'SET_PLAY_HISTORY'; -------------------------------------------------------------------------------- /src/vuex/mutations.js: -------------------------------------------------------------------------------- 1 | import * as types from './mutation-types'; 2 | 3 | const mutations = { 4 | [types.SET_MUSIC_LIST](state, musicList) { 5 | state.musicList = musicList; 6 | }, 7 | 8 | [types.SET_PLAYLIST](state, playlist) { 9 | state.playlist = playlist; 10 | }, 11 | 12 | [types.SET_CURRENT_INDEX](state, index) { 13 | state.currentIndex = index; 14 | }, 15 | 16 | [types.SET_FULL_SCREEN](state, flag) { 17 | state.fullScreen = flag; 18 | }, 19 | 20 | [types.SET_PLAYING](state, flag) { 21 | state.playing = flag; 22 | }, 23 | 24 | [types.SET_SEQUENCE_LIST](state, list) { 25 | state.sequenceList = list; 26 | }, 27 | 28 | [types.SET_MODE](state, mode) { 29 | state.mode = mode; 30 | }, 31 | 32 | [types.SET_SINGER](state, singer) { 33 | state.singer = singer; 34 | }, 35 | 36 | [types.SET_TOP_LIST](state, list) { 37 | state.topList = list; 38 | }, 39 | 40 | [types.SET_SEARCH_HISTORY](state, history) { 41 | state.searchHistory = history; 42 | }, 43 | 44 | [types.SET_FAVORITE_LIST](state, list) { 45 | state.favoriteList = list; 46 | }, 47 | 48 | [types.SET_PLAY_HISTORY](state, history) { 49 | state.playHistory = history; 50 | } 51 | }; 52 | 53 | export default mutations; 54 | -------------------------------------------------------------------------------- /src/vuex/state.js: -------------------------------------------------------------------------------- 1 | import { mode } from '../services/config'; 2 | import { loadSearchHistory, loadFavoriteList, loadPlayHistory } from '../services/cache'; 3 | 4 | const state = { 5 | musicList: {}, 6 | 7 | playlist: [], 8 | 9 | sequenceList: [], 10 | 11 | currentIndex: -1, 12 | 13 | fullScreen: false, 14 | 15 | playing: false, 16 | 17 | mode: mode.loop, 18 | 19 | singer: {}, 20 | 21 | topList: {}, 22 | 23 | searchHistory: loadSearchHistory() || [], 24 | 25 | favoriteList: loadFavoriteList() || [], 26 | 27 | playHistory: loadPlayHistory() || [] 28 | }; 29 | 30 | export default state; 31 | -------------------------------------------------------------------------------- /src/vuex/store.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuex from 'vuex'; 3 | import state from './state'; 4 | import getters from './getters'; 5 | import mutations from './mutations'; 6 | import actions from './actions'; 7 | 8 | import createLogger from 'vuex/dist/logger'; 9 | 10 | Vue.use(Vuex); 11 | 12 | export default new Vuex.Store({ 13 | state, 14 | mutations, 15 | getters, 16 | actions, 17 | 18 | plugins: process.env.NODE_ENV !== 'production' ? [createLogger()] : [] 19 | }); 20 | -------------------------------------------------------------------------------- /static/icons/music-120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lpeihan/vue-music-webapp/e058cf522d731c80881c93f3364335136203b33e/static/icons/music-120.png -------------------------------------------------------------------------------- /static/icons/music-144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lpeihan/vue-music-webapp/e058cf522d731c80881c93f3364335136203b33e/static/icons/music-144.png -------------------------------------------------------------------------------- /static/icons/music-152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lpeihan/vue-music-webapp/e058cf522d731c80881c93f3364335136203b33e/static/icons/music-152.png -------------------------------------------------------------------------------- /static/icons/music-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lpeihan/vue-music-webapp/e058cf522d731c80881c93f3364335136203b33e/static/icons/music-192.png -------------------------------------------------------------------------------- /static/icons/music-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lpeihan/vue-music-webapp/e058cf522d731c80881c93f3364335136203b33e/static/icons/music-256.png -------------------------------------------------------------------------------- /static/icons/music-384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lpeihan/vue-music-webapp/e058cf522d731c80881c93f3364335136203b33e/static/icons/music-384.png -------------------------------------------------------------------------------- /static/icons/music-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lpeihan/vue-music-webapp/e058cf522d731c80881c93f3364335136203b33e/static/icons/music-48.png -------------------------------------------------------------------------------- /static/icons/music-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lpeihan/vue-music-webapp/e058cf522d731c80881c93f3364335136203b33e/static/icons/music-512.png -------------------------------------------------------------------------------- /static/manifest.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "name": "Netease", 4 | "short_name": "Netease", 5 | "icons": [{ 6 | "src": "/icons/music-120.png", 7 | "sizes": "120x120", 8 | "type": "image/png" 9 | }, { 10 | "src": "/icons/music-144.png", 11 | "sizes": "144x144", 12 | "type": "image/png" 13 | }, { 14 | "src": "/icons/music-152.png", 15 | "sizes": "152x152", 16 | "type": "image/png" 17 | }, { 18 | "src": "/icons/music-192.png", 19 | "sizes": "192x192", 20 | "type": "image/png" 21 | }, { 22 | "src": "/icons/music-256.png", 23 | "sizes": "256x256", 24 | "type": "image/png" 25 | }, { 26 | "src": "/icons/music-384.png", 27 | "sizes": "384x384", 28 | "type": "image/png" 29 | }, { 30 | "src": "/icons/music-512.png", 31 | "sizes": "512x512", 32 | "type": "image/png" 33 | }], 34 | "start_url": "/", 35 | "background_color": "#f2f3f5", 36 | "display": "standalone", 37 | "theme_color": "#d44439" 38 | } --------------------------------------------------------------------------------