├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── README.md
├── bin
├── app.js
└── www
├── config
├── build.js
├── config.js
├── dev-client.js
├── dev-server.js
├── routes.js
├── webpack.base.conf.js
├── webpack.dev.conf.js
└── webpack.prod.conf.js
├── controllers
└── music.js
├── lib
├── api.js
└── request.js
├── models
└── kuwo.js
├── package.json
├── public
└── index.html
├── src
├── App.vue
├── api
│ └── music.js
├── components
│ ├── Header.vue
│ ├── PlayList.vue
│ ├── Player.vue
│ ├── ProgressLine.vue
│ ├── SearchHeader.vue
│ └── SearchList.vue
├── index.html
├── less
│ ├── 1px.less
│ ├── base.less
│ ├── icon.less
│ ├── style.less
│ └── variable.less
├── main.js
├── router.js
├── store
│ ├── actions.js
│ ├── index.js
│ └── mutations.js
└── view
│ ├── Main.vue
│ └── Search.vue
└── test
└── unit
├── .eslintrc
├── index.js
├── karma.conf.js
├── specs
├── Header.spec.js
└── Search.spec.js
└── util.js
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_style = space
6 | indent_size = 4
7 | end_of_line = lf
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.{json}]
12 | indent_size = 2
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | public/*.js
2 | config/*.js
3 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | parser: 'babel-eslint',
4 | parserOptions: {
5 | ecmaVersion: 6,
6 | sourceType: 'module'
7 | },
8 | // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style
9 | extends: 'standard',
10 | // required to lint *.vue files
11 | plugins: [
12 | 'html'
13 | ],
14 |
15 | // add your custom rules here
16 | rules: {
17 | // allow paren-less arrow functions
18 | 'arrow-parens': 0,
19 | // allow async-await
20 | 'generator-star-spacing': 0,
21 | //
22 | 'space-before-function-paren': [0, "always"],
23 | // 4 space indent
24 | 'indent': [2, 4, { "SwitchCase": 1 }],
25 | // disable no-new
26 | 'no-new': 0,
27 | // allow debugger during development
28 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 |
6 | # Dependency directory
7 | node_modules/
8 | public/
9 | test/unit/coverage/
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Music
2 |
3 | Webpack、Vue、Express集成,学习之用。
4 |
5 | ## 使用
6 |
7 | ```
8 | # install
9 | npm install
10 |
11 | # dev server, localhost:8080
12 | npm run dev
13 |
14 | # build with compress
15 | npm run build
16 |
17 | # Express app server, localhost:4000
18 | npm start
19 | ```
20 |
21 |
--------------------------------------------------------------------------------
/bin/app.js:
--------------------------------------------------------------------------------
1 | var express = require('express')
2 | var config = require('../config/config')
3 |
4 | module.exports = config(express)
5 |
--------------------------------------------------------------------------------
/bin/www:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * Module dependencies.
5 | */
6 |
7 | var app = require('./app')
8 | var debug = require('debug')('music:server')
9 | var http = require('http')
10 |
11 | /**
12 | * Get port from environment and store in Express.
13 | */
14 |
15 | var port = normalizePort(process.env.PORT || '4000')
16 | app.set('port', port)
17 |
18 | /**
19 | * Create HTTP server.
20 | */
21 |
22 | var server = http.createServer(app)
23 |
24 | /**
25 | * Listen on provided port, on all network interfaces.
26 | */
27 |
28 | server.listen(port)
29 | server.on('error', onError)
30 | server.on('listening', onListening)
31 |
32 | /**
33 | * Normalize a port into a number, string, or false.
34 | */
35 |
36 | function normalizePort(val) {
37 | var port = parseInt(val, 10)
38 |
39 | if (isNaN(port)) {
40 | // named pipe
41 | return val
42 | }
43 |
44 | if (port >= 0) {
45 | // port number
46 | return port
47 | }
48 |
49 | return false
50 | }
51 |
52 | /**
53 | * Event listener for HTTP server "error" event.
54 | */
55 |
56 | function onError(error) {
57 | if (error.syscall !== 'listen') {
58 | throw error
59 | }
60 |
61 | var bind = typeof port === 'string'
62 | ? 'Pipe ' + port
63 | : 'Port ' + port
64 |
65 | // handle specific listen errors with friendly messages
66 | switch (error.code) {
67 | case 'EACCES':
68 | console.error(bind + ' requires elevated privileges')
69 | process.exit(1)
70 | break
71 | case 'EADDRINUSE':
72 | console.error(bind + ' is already in use')
73 | process.exit(1)
74 | break
75 | default:
76 | throw error
77 | }
78 | }
79 |
80 | /**
81 | * Event listener for HTTP server "listening" event.
82 | */
83 |
84 | function onListening() {
85 | var addr = server.address()
86 | var bind = typeof addr === 'string'
87 | ? 'pipe ' + addr
88 | : 'port ' + addr.port
89 | debug('Listening on ' + bind)
90 | }
91 |
--------------------------------------------------------------------------------
/config/build.js:
--------------------------------------------------------------------------------
1 | // https://github.com/shelljs/shelljs
2 | require('shelljs/global')
3 | env.NODE_ENV = 'production'
4 |
5 | var ora = require('ora')
6 | var webpack = require('webpack')
7 | var conf = require('./webpack.prod.conf')
8 |
9 | var spinner = ora('building for production...')
10 | spinner.start()
11 |
12 | rm('-rf', 'public')
13 | mkdir('public')
14 | // cp('-R', 'src', conf.output.path)
15 |
16 | webpack(conf, function(err, stats) {
17 | spinner.stop()
18 | if (err) throw err
19 | process.stdout.write(stats.toString({
20 | colors: true,
21 | modules: false,
22 | children: false,
23 | chunks: false,
24 | chunkModules: false
25 | }) + '\n')
26 | })
27 |
--------------------------------------------------------------------------------
/config/config.js:
--------------------------------------------------------------------------------
1 | var path = require('path')
2 | var favicon = require('serve-favicon')
3 | var logger = require('morgan')
4 | var cookieParser = require('cookie-parser')
5 | var bodyParser = require('body-parser')
6 | var ejs = require('ejs')
7 | var routes = require('./routes')
8 |
9 |
10 | module.exports = function(express) {
11 |
12 | var app = express()
13 |
14 | // view engine setup
15 | //app.engine('.html', ejs.__express);
16 | //app.set('views', path.join(__dirname, '../views'));
17 | //app.set('view engine', 'html');
18 |
19 | // uncomment after placing your favicon in /public
20 | // app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')))
21 | app.use(logger('dev'))
22 | app.use(bodyParser.json())
23 | app.use(bodyParser.urlencoded({
24 | extended: false
25 | }))
26 | app.use(cookieParser())
27 | app.use(express.static(path.join(__dirname, '../public')))
28 |
29 | routes(app)
30 |
31 | app.get('/', function(req, res) {
32 | res.sendFile(path.join(__dirname, '../public/index.html'))
33 | })
34 |
35 | app.get('/favicon.ico', function(req, res) {
36 | res.status(404)
37 | })
38 |
39 | // catch 404 and forward to error handler
40 | app.use(function(req, res, next) {
41 | var err = new Error('Not Found')
42 | err.status = 404
43 | next(err)
44 | })
45 |
46 | // error handlers
47 |
48 | // development error handler
49 | // will print stacktrace
50 | if (app.get('env') === 'development') {
51 | app.use(function(err, req, res, next) {
52 | res.status(err.status || 500)
53 | res.render('error', {
54 | message: err.message,
55 | error: err
56 | })
57 | })
58 | }
59 |
60 | // production error handler
61 | // no stacktraces leaked to user
62 | app.use(function(err, req, res, next) {
63 | res.status(err.status || 500)
64 | res.render('error', {
65 | message: err.message,
66 | error: {}
67 | })
68 | })
69 |
70 | return app
71 |
72 | }
73 |
--------------------------------------------------------------------------------
/config/dev-client.js:
--------------------------------------------------------------------------------
1 | require('eventsource-polyfill')
2 | var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true')
3 |
4 | hotClient.subscribe(function(event) {
5 | if (event.action === 'reload') {
6 | window.location.reload()
7 | }
8 | })
9 |
--------------------------------------------------------------------------------
/config/dev-server.js:
--------------------------------------------------------------------------------
1 | var express = require('express')
2 | var webpack = require('webpack')
3 | var opn = require('opn')
4 | var config = require('./webpack.dev.conf')
5 | var routes = require('./routes')
6 | // var proxyMiddleware = require('http-proxy-middleware')
7 |
8 | var app = express()
9 | var compiler = webpack(config)
10 |
11 | // Define HTTP proxies to your custom API backend
12 | // https://github.com/chimurai/http-proxy-middleware
13 | // var proxyTable = {
14 | // // '/api': {
15 | // // target: 'http://jsonplaceholder.typicode.com',
16 | // // changeOrigin: true,
17 | // // pathRewrite: {
18 | // // '^/api': ''
19 | // // }
20 | // // }
21 | // }
22 |
23 | var devMiddleware = require('webpack-dev-middleware')(compiler, {
24 | publicPath: config.output.publicPath,
25 | noInfo: true,
26 | stats: {
27 | colors: true,
28 | chunks: false
29 | }
30 | })
31 |
32 | var hotMiddleware = require('webpack-hot-middleware')(compiler)
33 | // force page reload when html-webpack-plugin template changes
34 | compiler.plugin('compilation', function(compilation) {
35 | compilation.plugin('html-webpack-plugin-after-emit', function(data, cb) {
36 | hotMiddleware.publish({ action: 'reload' })
37 | cb()
38 | })
39 | })
40 |
41 | // proxy api requests
42 | // Object.keys(proxyTable).forEach(function (context) {
43 | // var options = proxyTable[context]
44 | // if (typeof options === 'string') {
45 | // options = { target: options }
46 | // }
47 | // app.use(proxyMiddleware(context, options))
48 | // })
49 |
50 | // handle fallback for HTML5 history API
51 | app.use(require('connect-history-api-fallback')())
52 |
53 | // serve webpack bundle output
54 | app.use(devMiddleware)
55 |
56 | // enable hot-reload and state-preserving
57 | // compilation error display
58 | app.use(hotMiddleware)
59 |
60 | // serve pure static assets
61 | app.use('/src', express.static('./src'))
62 |
63 | routes(app)
64 |
65 | module.exports = app.listen(8080, function(err) {
66 | if (err) {
67 | console.log(err)
68 | return
69 | }
70 | var uri = 'http://localhost:8080'
71 | console.log('Listening at ' + uri + '\n')
72 | opn(uri)
73 | })
74 |
--------------------------------------------------------------------------------
/config/routes.js:
--------------------------------------------------------------------------------
1 | var music = require('../controllers/music')
2 | var path = require('path')
3 |
4 | module.exports = function(app) {
5 |
6 | app.get('/search/:key/:pn', music.search)
7 |
8 | app.get('/song/:id', music.download)
9 |
10 | }
11 |
--------------------------------------------------------------------------------
/config/webpack.base.conf.js:
--------------------------------------------------------------------------------
1 | var path = require('path')
2 | var projectRoot = path.resolve(__dirname, '../')
3 | var ExtractTextPlugin = require('extract-text-webpack-plugin')
4 | var autoprefixer = require('autoprefixer')
5 |
6 | module.exports = {
7 | entry: {
8 | app: './src/main.js'
9 | },
10 | output: {
11 | path: path.resolve(__dirname, '../public/'),
12 | // publicPath: './public/',
13 | filename: '[name].js'
14 | },
15 | resolve: {
16 | extensions: ['', '.js', '.vue'],
17 | fallback: [path.join(__dirname, '../node_modules')],
18 | alias: {
19 | 'src': path.resolve(__dirname, '../src'),
20 | 'components': 'src/components'
21 | }
22 | },
23 | resolveLoader: {
24 | fallback: [path.join(__dirname, '../node_modules')]
25 | },
26 | module: {
27 | preLoaders: [{
28 | test: /\.vue$/,
29 | loader: 'eslint',
30 | include: projectRoot,
31 | exclude: /node_modules/
32 | }, {
33 | test: /\.js$/,
34 | loader: 'eslint',
35 | include: projectRoot,
36 | exclude: /node_modules/
37 | }],
38 | loaders: [{
39 | test: /\.vue$/,
40 | loader: 'vue'
41 | }, {
42 | test: /\.(css|less)$/,
43 | loader: ExtractTextPlugin.extract('style-loader', 'css!postcss!less')
44 | }, {
45 | test: /\.js$/,
46 | loader: 'babel',
47 | include: projectRoot,
48 | exclude: /node_modules/
49 | }, {
50 | test: /\.json$/,
51 | loader: 'json'
52 | }, {
53 | test: /\.html$/,
54 | loader: 'vue-html'
55 | }, {
56 | test: /\.(png|jpe?g|gif|svg|woff2?|eot|ttf|otf)(\?.*)?$/,
57 | loader: 'url',
58 | query: {
59 | limit: 10000,
60 | name: '[name].[hash:7].[ext]'
61 | }
62 | }]
63 | },
64 | vue: {
65 | loaders: {
66 | css: 'vue-style!css',
67 | less: 'vue-style!css!less'
68 | },
69 | postcss: [autoprefixer({
70 | browsers: ['last 3 versions']
71 | })]
72 | },
73 | eslint: {
74 | formatter: require('eslint-friendly-formatter')
75 | },
76 | babel: {
77 | presets: ['es2015', 'stage-2'],
78 | plugins: ['transform-runtime']
79 | },
80 | postcss: [autoprefixer({
81 | browsers: ['last 3 versions']
82 | })]
83 | }
84 |
--------------------------------------------------------------------------------
/config/webpack.dev.conf.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack')
2 | var merge = require('webpack-merge')
3 | var baseConfig = require('./webpack.base.conf')
4 | var ExtractTextPlugin = require('extract-text-webpack-plugin')
5 | var HtmlWebpackPlugin = require('html-webpack-plugin')
6 |
7 | // add hot-reload related code to entry chunks
8 | Object.keys(baseConfig.entry).forEach(function (name) {
9 | baseConfig.entry[name] = ['./config/dev-client'].concat(baseConfig.entry[name])
10 | })
11 |
12 | module.exports = merge(baseConfig, {
13 | devtool: '#eval-source-map',
14 | output: {
15 | // necessary for the html plugin to work properly
16 | // when serving the html from in-memory
17 | publicPath: '/'
18 | },
19 | plugins: [
20 | new webpack.DefinePlugin({
21 | 'process.env': {
22 | NODE_ENV: '"development"'
23 | }
24 | }),
25 | new webpack.optimize.OccurenceOrderPlugin(),
26 | new webpack.HotModuleReplacementPlugin(),
27 | new ExtractTextPlugin("[name].css"),
28 | // new webpack.NoErrorsPlugin(),
29 | new HtmlWebpackPlugin({
30 | filename: 'index.html',
31 | template: 'src/index.html',
32 | inject: true
33 | })
34 | ]
35 | })
36 |
--------------------------------------------------------------------------------
/config/webpack.prod.conf.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack')
2 | var merge = require('webpack-merge')
3 | var baseConfig = require('./webpack.base.conf')
4 | var ExtractTextPlugin = require('extract-text-webpack-plugin')
5 | var HtmlWebpackPlugin = require('html-webpack-plugin')
6 |
7 | module.exports = merge(baseConfig, {
8 | devtool: '#source-map',
9 | output: {
10 | filename: '[name].[chunkhash].js',
11 | chunkFilename: '[id].[chunkhash].js'
12 | },
13 | vue: {
14 | loaders: {
15 | css: ExtractTextPlugin.extract('vue-style-loader', 'css?sourceMap!postcss?sourceMap?sourceMap'),
16 | less: ExtractTextPlugin.extract('vue-style-loader', 'css?sourceMap!postcss?sourceMap!less?sourceMap')
17 | }
18 | },
19 | plugins: [
20 | new webpack.DefinePlugin({
21 | 'process.env': {
22 | NODE_ENV: '"production"'
23 | }
24 | }),
25 | new webpack.optimize.UglifyJsPlugin({
26 | compress: {
27 | warnings: false
28 | }
29 | }),
30 | new webpack.optimize.OccurenceOrderPlugin(),
31 | new ExtractTextPlugin('[name].[contenthash].css'),
32 | new HtmlWebpackPlugin({
33 | filename: '../public/index.html',
34 | template: 'src/index.html',
35 | inject: true,
36 | minify: {
37 | removeComments: true,
38 | collapseWhitespace: true,
39 | removeAttributeQuotes: true
40 | }
41 | })
42 | ]
43 | })
44 |
--------------------------------------------------------------------------------
/controllers/music.js:
--------------------------------------------------------------------------------
1 | var kuwo = require('../models/kuwo');
2 |
3 | exports.search = function(req, res) {
4 |
5 | kuwo.search(req.params).then(function(data) {
6 |
7 | res.send(data);
8 | }, function(e) {
9 | console.log(e);
10 | });
11 |
12 | };
13 |
14 | exports.download = function(req, res) {
15 |
16 | kuwo.download(req.params.id).then(function(data) {
17 | res.send(data);
18 | }, function(e) {
19 | console.log(e);
20 | });
21 |
22 | };
23 |
--------------------------------------------------------------------------------
/lib/api.js:
--------------------------------------------------------------------------------
1 | var cheerio = require('cheerio');
2 |
3 | // 酷我
4 | exports.kuwo = {
5 | searchUrl: 'http://sou.kuwo.cn/ws/NSearch',
6 | songUrl: 'http://antiserver.kuwo.cn/anti.s?format=mp3|aac&type=convert_url&response=url',
7 | parse: function(body, key, pn) {
8 |
9 | // 解析 html
10 | var $ = cheerio.load(body),
11 | $list = $('.m_list ul li'),
12 | $page = $('.page'),
13 | list = [];
14 |
15 | $list.each(function(i, el) {
16 | var $this = $(el);
17 |
18 | list.push({
19 | id: parseInt($this.find('input[name="musicNum"]').val()),
20 | name: $this.find('.m_name a').attr('title'),
21 | album: $this.find('.a_name a').attr('title'),
22 | singer: $this.find('.s_name a').attr('title')
23 | });
24 | });
25 |
26 | $page.find('a').each(function(i, el) {
27 | var $this = $(el),
28 | href = $this.attr('href');
29 |
30 | if (href !== '#@') {
31 | var num = href.split('=').reverse()[0];
32 | $this.attr('data-option', JSON.stringify({
33 | key: key,
34 | pn: num
35 | }));
36 | }
37 |
38 | $this.attr('href', 'javascript:;');
39 | });
40 |
41 | return {
42 | page: $page.html(),
43 | list: list
44 | };
45 | }
46 | };
47 |
48 | // 网易云,这里挖个坑
49 | exports.cloud = {
50 | // 搜索歌曲 POST
51 | // type 单曲(1),歌手(100),专辑(10),歌单(1000),用户(1002) *(type)*
52 | // limit 获取多少条数据
53 | // http://music.163.com/api/search/get/web?s=%E8%8A%B1%E6%B5%B7&type=1&offset=0&total=true&limit=60
54 |
55 | // 歌曲URL GET
56 | // 单曲 http://music.163.com/api/song/detail/?id=185697&ids=[185697]
57 | // 多曲 http://music.163.com/api/song/detail/?ids=[185697,xxxx]
58 | };
59 |
--------------------------------------------------------------------------------
/lib/request.js:
--------------------------------------------------------------------------------
1 | var http = require('http');
2 | var url = require('url');
3 | var querystring = require('querystring');
4 |
5 | function request(method, urlStr, args) {
6 |
7 | var option = url.parse(urlStr);
8 | option.method = method;
9 |
10 | if(args) {
11 | var query = '?' + querystring.stringify(args) + (option.query ? '&' + option.query : '');
12 | option.path = option.pathname + query;
13 | }
14 |
15 | return new Promise(function(resolve, reject) {
16 |
17 | var req = http.request(option, function(res) {
18 |
19 | var body = '';
20 |
21 | res.setEncoding('utf8');
22 | res.on('data', function(data) {
23 | body += data;
24 | });
25 |
26 | res.on('end', function() {
27 |
28 | resolve(body);
29 | });
30 |
31 | });
32 |
33 | req.on('error', function(e) {
34 | reject(e);
35 | });
36 |
37 | req.end();
38 | });
39 | }
40 |
41 | module.exports = function(url) {
42 |
43 | return {
44 | get: function(args) {
45 | return request('GET', url, args);
46 | },
47 | post: function(args) {
48 | return request('POST', url, args);
49 | },
50 | put: function(args) {
51 | return request('PUT', url, args);
52 | },
53 | delete: function(args) {
54 | return request('DELETE', url, args);
55 | }
56 | };
57 |
58 | };
59 |
--------------------------------------------------------------------------------
/models/kuwo.js:
--------------------------------------------------------------------------------
1 | var request = require('../lib/request');
2 | var api = require('../lib/api').kuwo;
3 |
4 | exports.search = function(params) {
5 |
6 | var key = params.key,
7 | pn = params.pn;
8 |
9 | return request(api.searchUrl + '?type=music')
10 | .get(params)
11 | .then(function(body) {
12 | return api.parse(body, key, pn);
13 | });
14 | };
15 |
16 | exports.download = function(id) {
17 |
18 | return request(api.songUrl)
19 | .get({
20 | rid: 'MUSIC_' + id
21 | })
22 | .then(function(body) {
23 | return body;
24 | });
25 | };
26 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue-music",
3 | "version": "1.0.1",
4 | "private": true,
5 | "author": {
6 | "name": "yusen",
7 | "email": "634206017@qq.com"
8 | },
9 | "scripts": {
10 | "start": "node ./bin/www",
11 | "dev": "node config/dev-server.js",
12 | "build": "node config/build.js",
13 | "unit": "karma start test/unit/karma.conf.js --single-run",
14 | "test": "npm run unit"
15 | },
16 | "dependencies": {
17 | "body-parser": "~1.13.2",
18 | "cheerio": "^0.20.0",
19 | "cookie-parser": "~1.3.5",
20 | "debug": "~2.2.0",
21 | "ejs": "^2.4.1",
22 | "express": "^4.13.4",
23 | "morgan": "^1.7.0",
24 | "serve-favicon": "~2.3.0",
25 | "vue": "^2.0.5",
26 | "vue-directive-waves": "^2.0.2",
27 | "vue-resource": "^1.0.3",
28 | "vue-router": "^2.0.1",
29 | "vuex": "^2.0.0"
30 | },
31 | "devDependencies": {
32 | "autoprefixer": "^6.5.2",
33 | "babel-core": "^6.7.7",
34 | "babel-eslint": "^7.1.0",
35 | "babel-loader": "^6.2.4",
36 | "babel-plugin-transform-runtime": "^6.7.5",
37 | "babel-preset-es2015": "^6.6.0",
38 | "babel-preset-stage-2": "^6.5.0",
39 | "babel-runtime": "^5.8.38",
40 | "chai": "^3.5.0",
41 | "connect-history-api-fallback": "^1.2.0",
42 | "cross-env": "^1.0.7",
43 | "css-loader": "^0.25.0",
44 | "eslint": "^3.9.1",
45 | "eslint-config-standard": "^6.2.1",
46 | "eslint-friendly-formatter": "^2.0.6",
47 | "eslint-loader": "^1.6.1",
48 | "eslint-plugin-html": "^1.6.0",
49 | "eslint-plugin-promise": "^3.3.1",
50 | "eslint-plugin-standard": "^2.0.1",
51 | "eventsource-polyfill": "^0.9.6",
52 | "extract-text-webpack-plugin": "^1.0.1",
53 | "file-loader": "^0.9.0",
54 | "html-webpack-plugin": "^2.14.0",
55 | "http-proxy-middleware": "^0.17.2",
56 | "isparta-loader": "^2.0.0",
57 | "json-loader": "^0.5.4",
58 | "karma": "^1.3.0",
59 | "karma-chrome-launcher": "^2.0.0",
60 | "karma-coverage": "^1.1.1",
61 | "karma-mocha": "^1.2.0",
62 | "karma-sinon-chai": "^1.2.4",
63 | "karma-sourcemap-loader": "^0.3.7",
64 | "karma-spec-reporter": "0.0.26",
65 | "karma-webpack": "^1.8.0",
66 | "less": "^2.7.1",
67 | "less-loader": "^2.2.3",
68 | "mocha": "^3.1.2",
69 | "opn": "^4.0.2",
70 | "ora": "^0.3.0",
71 | "postcss-loader": "^1.1.0",
72 | "shelljs": "^0.7.4",
73 | "sinon": "^1.17.6",
74 | "sinon-chai": "^2.8.0",
75 | "style-loader": "^0.13.1",
76 | "url-loader": "^0.5.7",
77 | "vue-hot-reload-api": "^1.3.2",
78 | "vue-html-loader": "^1.2.2",
79 | "vue-loader": "^9.4.0",
80 | "vue-style-loader": "^1.0.0",
81 | "webpack": "^1.13.2",
82 | "webpack-dev-middleware": "^1.8.3",
83 | "webpack-dev-server": "^1.14.1",
84 | "webpack-hot-middleware": "^2.12.2",
85 | "webpack-merge": "^0.14.1"
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
MUSIC
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
9 |
10 |
11 |
20 |
--------------------------------------------------------------------------------
/src/api/music.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import VueResource from 'vue-resource'
3 |
4 | Vue.use(VueResource)
5 |
6 | export default {
7 | search(key) {
8 | return Vue.http.get(`/search/${key}/1`)
9 | .then(res => res.json(),
10 | res => console.error('error: ', res.status, res.statusText))
11 | },
12 | getSong(id) {
13 | return Vue.http.get(`/song/${id}`)
14 | .then(res => res.text(),
15 | res => console.error('error: ', res.status, res.statusText))
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/components/Header.vue:
--------------------------------------------------------------------------------
1 |
2 |
18 |
19 |
20 |
23 |
--------------------------------------------------------------------------------
/src/components/PlayList.vue:
--------------------------------------------------------------------------------
1 |
2 |
20 |
21 |
39 |
--------------------------------------------------------------------------------
/src/components/Player.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |

7 |
8 |
9 |
12 |
15 |
18 |
19 |
20 |
{{currentSong ? currentSong.name : ''}}
21 |
{{currentSong ? currentSong.singer : ''}}
22 |
23 |
24 |
35 |
36 |
37 |
38 |
39 |
40 |
126 |
--------------------------------------------------------------------------------
/src/components/ProgressLine.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
20 |
--------------------------------------------------------------------------------
/src/components/SearchHeader.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/components/SearchList.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
26 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | MUSIC
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/less/1px.less:
--------------------------------------------------------------------------------
1 | /**
2 | * https://github.com/yscoder/border-1px
3 | */
4 | .border-scale(@s) {
5 | -webkit-transform: scale(@s);
6 | transform: scale(@s);
7 | }
8 | .border-postion(@x, @y) {
9 | -webkit-transform-origin: @x @y;
10 | transform-origin: @x @y;
11 | }
12 |
13 | .bd() {
14 | border-width: 0;
15 | border-color: @color-border;
16 | border-style: solid;
17 | }
18 |
19 | .ratina-bd(@dpr) {
20 |
21 | &::before {
22 | width: 100% * @dpr;
23 | height: 100% * @dpr;
24 | .border-scale(1/@dpr);
25 | }
26 | }
27 |
28 | .bd-t {
29 | .bd;
30 | border-top-width: 1px;
31 | }
32 | .bd-r {
33 | .bd;
34 | border-right-width: 1px;
35 | }
36 | .bd-b {
37 | .bd;
38 | border-bottom-width: 1px;
39 | }
40 | .bd-l {
41 | .bd;
42 | border-left-width: 1px;
43 | }
44 | .bd-all {
45 | .bd;
46 | border-width: 1px;
47 | }
48 | .bd-radius {
49 | .bd-all;
50 | border-radius: @border-radius-size;
51 | }
52 | .bd-dashed {
53 | border-style: dashed!important;
54 | }
55 |
56 | hr {
57 | border: none;
58 | }
59 | hr,
60 | .hr {
61 | margin: .7em 0;
62 | width: 100%;
63 | height: 1px;
64 | background: @color-border;
65 | overflow: hidden;
66 | }
67 |
68 | @media screen and (-webkit-min-device-pixel-ratio: 1.5) {
69 | .ratina-bd {
70 | position: relative;
71 | border: none!important;
72 |
73 | &::before {
74 | content: '';
75 | position: absolute;
76 | top: 0;
77 | left: 0;
78 | .bd;
79 | .border-postion(0, 0);
80 | pointer-events: none;
81 | }
82 |
83 | .ratina-bd(1.5);
84 |
85 | &.bd-t {
86 | &::before {
87 | border-top-width: 1px;
88 | }
89 | }
90 |
91 | &.bd-r {
92 | &::before {
93 | border-right-width: 1px;
94 | }
95 | }
96 |
97 | &.bd-b {
98 | &::before {
99 | border-bottom-width: 1px;
100 | }
101 | }
102 |
103 | &.bd-l {
104 | &::before {
105 | border-left-width: 1px;
106 | }
107 | }
108 |
109 | &.bd-all {
110 | &::before {
111 | border-width: 1px;
112 | }
113 | }
114 |
115 | &.bd-radius {
116 | &::before {
117 | border-width: 1px;
118 | border-radius: @border-radius-size * 1.5;
119 | }
120 | }
121 |
122 | &.bd-dashed {
123 | &::before {
124 | border-style: dashed!important;
125 | }
126 | }
127 |
128 | }
129 |
130 | hr,
131 | .hr {
132 | width: 100% * 1.5;
133 | .border-scale(1/1.5);
134 | .border-postion(0, 0);
135 | }
136 | }
137 |
138 | @media screen and (-webkit-min-device-pixel-ratio: 2) {
139 | .ratina-bd {
140 | .ratina-bd(2);
141 |
142 | &.bd-radius {
143 | &::before {
144 | border-radius: @border-radius-size * 2;
145 | }
146 | }
147 | }
148 |
149 | hr,
150 | .hr {
151 | width: 100% * 2;
152 | .border-scale(1/2);
153 | }
154 | }
155 |
156 | @media screen and (-webkit-min-device-pixel-ratio: 3) {
157 | .ratina-bd {
158 | .ratina-bd(3);
159 |
160 | &.bd-radius {
161 | &::before {
162 | border-radius: @border-radius-size * 3;
163 | }
164 | }
165 | }
166 |
167 | hr,
168 | .hr {
169 | width: 100% * 3;
170 | .border-scale(1/3);
171 | }
172 | }
173 |
--------------------------------------------------------------------------------
/src/less/base.less:
--------------------------------------------------------------------------------
1 |
2 | html {
3 | width: 100%;
4 | max-height: 100%;
5 | background-color: #fff;
6 | color: @color-text;
7 | font-size: 100px;
8 | overflow: hidden;
9 | }
10 | ::-webkit-scrollbar {
11 | display: none;
12 | }
13 |
14 | *, *::before, *::after {
15 | -webkit-box-sizing: border-box;
16 | box-sizing: border-box;
17 | }
18 |
19 | body {
20 | -webkit-tap-highlight-color: rgba(255,255,255,0);
21 | }
22 |
23 | body, input, button, textarea, select, option {
24 | font: normal .14em "Helvetica Neue", Helvetica, Arial, sans-serif;
25 | }
26 |
27 | body, h1, h2, h3, h4, h5, h6, p, figure, pre, dl, dd, blockquote, button, input {
28 | margin: 0;
29 | }
30 |
31 | input, legend, input, textarea, button {
32 | padding: 0;
33 | }
34 |
35 | ul, ol, form, fieldset, th, td {
36 | margin: 0;
37 | padding: 0;
38 | }
39 |
40 | ul, ol, li {
41 | list-style: none;
42 | }
43 |
44 | a {
45 | color: inherit;
46 | text-decoration: none;
47 | }
48 |
49 | article, p {
50 | max-height: 100%;
51 | }
52 |
53 |
54 | img {
55 | width: 100%;
56 | max-width: 100%;
57 | border: none;
58 | }
59 |
60 | em, i, address {
61 | font-style: normal;
62 | }
63 |
64 | input[type="button"],
65 | input[type="submit"],
66 | input[type="search"],
67 | input[type="reset"] {
68 | -webkit-appearance: button; /* iOS下的样式问题 */
69 | }
70 |
71 | input:focus,
72 | select:focus,
73 | button:focus,
74 | textarea:focus {
75 | outline: none;
76 | }
77 |
78 | textarea {
79 | resize: none;
80 | }
81 |
82 | .fl {float: left;}
83 | .fr {float: right;}
84 | .oh {overflow: hidden;}
85 | .tr {
86 | text-align: right;
87 | }
88 | .tc {
89 | text-align: center;
90 | }
91 |
92 | .row:after, .clearfix:after {
93 | content: "";
94 | display: table;
95 | clear: both;
96 | overflow: hidden;
97 | }
98 |
99 | .ellipsis {
100 | overflow: hidden;
101 | text-overflow: ellipsis;
102 | white-space: nowrap;
103 | }
104 |
105 | /* flex布局 */
106 | .flex-row, .flex-row-vertical {
107 | display: -webkit-box;
108 | display: -webkit-flex;
109 | display: -ms-flexbox;
110 | display: flex;
111 | }
112 |
113 | .flex-row-vertical {
114 | -webkit-box-orient: vertical;
115 | -webkit-box-direction: normal;
116 | -webkit-flex-direction: column;
117 | -ms-flex-direction: column;
118 | flex-direction: column;
119 | }
120 |
121 | /* 多行布局 */
122 | .flex-row-wrap {
123 | -webkit-flex-wrap: wrap;
124 | -ms-flex-wrap: wrap;
125 | flex-wrap: wrap;
126 | }
127 |
128 | /* 子元素反向排列 */
129 | .flex-row-reverse {
130 | -webkit-box-orient: horizontal;
131 | -webkit-box-direction: reverse;
132 | -webkit-flex-direction: row-reverse;
133 | -ms-flex-direction: row-reverse;
134 | flex-direction: row-reverse;
135 | }
136 |
137 | /* 子元素默认平均分布 */
138 | .flex-row > * {
139 | display: block; /* hack低版本Android 2.3- UC,QQ浏览器 */
140 | }
141 | .flex-col {
142 | -webkit-box-flex: 1;
143 | -webkit-flex: 1;
144 | -ms-flex: 1;
145 | flex: 1;
146 | }
147 |
148 | /* 垂直居中 */
149 | .flex-middle {
150 | -webkit-box-align: center;
151 | -webkit-align-items: center;
152 | -ms-flex-align: center;
153 | align-items: center;
154 | }
155 |
156 | /* 底部对齐 */
157 | .flex-bottom {
158 | -webkit-box-align: end;
159 | -webkit-align-items: flex-end;
160 | -ms-flex-align: end;
161 | align-items: flex-end;
162 | }
163 |
164 | /* 沿主轴方向排列方式 */
165 | .flex-justify-start {
166 | -webkit-box-pack: start;
167 | -webkit-justify-content: flex-start;
168 | -ms-flex-pack: start;
169 | justify-content: flex-start;
170 | }
171 | .flex-justify-end {
172 | -webkit-box-pack: end;
173 | -webkit-justify-content: flex-end;
174 | -ms-flex-pack: end;
175 | justify-content: flex-end;
176 | }
177 | .flex-justify-between {
178 | -webkit-box-pack: justify;
179 | -webkit-justify-content: space-between;
180 | -ms-flex-pack: justify;
181 | justify-content: space-between;
182 | }
183 |
184 | /* 沿侧轴方向排列方式 */
185 | .flex-align-start {
186 | -webkit-align-content: flex-start;
187 | -ms-flex-line-pack: start;
188 | align-content: flex-start;
189 | }
190 | .flex-align-end {
191 | -webkit-align-content: flex-end;
192 | -ms-flex-line-pack: end;
193 | align-content: flex-end;
194 | }
195 | .flex-align-around {
196 | -webkit-align-content: space-around;
197 | -ms-flex-line-pack: distribute;
198 | align-content: space-around;
199 | }
200 | .flex-align-between {
201 | -webkit-align-content: space-between;
202 | -ms-flex-line-pack: justify;
203 | align-content: space-between;
204 | }
205 | .flex-col-auto { /* 不进行伸缩 */
206 | -webkit-box-flex: 0;
207 | -webkit-flex: 0 auto;
208 | -ms-flex: 0 auto;
209 | flex: 0 auto;
210 | }
211 |
212 | .container, .mr-both {
213 | margin-left: @g-margin;
214 | margin-right: @g-margin;
215 | }
216 |
217 | .ml { margin-left: @g-margin}
218 | .mr { margin-right: @g-margin}
219 |
--------------------------------------------------------------------------------
/src/less/icon.less:
--------------------------------------------------------------------------------
1 |
2 | @font-face {
3 | font-family: "iconfont";
4 | src: url('data:application/x-font-woff;charset=utf-8;base64,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') format('woff'),
5 | url('//at.alicdn.com/t/font_1461831434_5471206.ttf') format('truetype');
6 | }
7 |
8 | .icon {
9 | display: inline-block;
10 | font-family: "iconfont" !important;
11 | font-size: .14rem;
12 | font-style: normal;
13 | font-weight: normal;
14 | text-rendering: auto;
15 | -webkit-font-smoothing: antialiased;
16 | -moz-osx-font-smoothing: grayscale;
17 | }
18 | .icon-lg {
19 | font-size: .2rem;
20 | }
21 |
22 | .icon-2x {
23 | font-size: .28rem;
24 | }
25 | .icon-3x {
26 | font-size: .42rem;
27 | }
28 | .icon-4x {
29 | font-size: .56rem;
30 | }
31 | .icon-5x {
32 | font-size: .7rem;
33 | }
34 | .icon-mode-loop:before { content: "\e602"; }
35 | .icon-mode-single:before { content: "\e603"; }
36 | .icon-mode-random:before { content: "\e604"; }
37 | .icon-search:before { content: "\e605"; }
38 | .icon-pause:before { content: "\e608"; }
39 | .icon-play:before { content: "\e607"; }
40 | .icon-music:before { content: "\e606"; }
41 | .icon-prev:before { content: "\e600"; }
42 | .icon-more:before { content: "\e601"; }
43 | .icon-next:before { content: "\e609"; }
44 |
--------------------------------------------------------------------------------
/src/less/style.less:
--------------------------------------------------------------------------------
1 | @import 'variable';
2 | @import 'base';
3 | @import 'icon';
4 | @import '1px';
5 |
6 | html {
7 | background: #fafafa
8 | }
9 |
10 | .@{css-prefix} {
11 |
12 | // header
13 | &-hd {
14 | position: absolute;
15 | top: 0;
16 | left: 0;
17 | z-index: 10;
18 | width: 100%;
19 | height: 56px;
20 | line-height: 56px;
21 | color: #fff;
22 | background: @color-primary;
23 | font-size: .16rem;
24 | box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 10px 0 rgba(0, 0, 0, 0.12);
25 |
26 | ::-webkit-input-placeholder {
27 | color: #9FD69B;
28 | }
29 |
30 | &-icon {
31 | width: 56px;
32 | height: 56px;
33 | line-height: 56px;
34 | text-align: center;
35 | color: #fff;
36 | }
37 |
38 | }
39 | &-key {
40 | border: none;
41 | display: block;
42 | width: 100%;
43 | height: 36px;
44 | padding: 0 40px 0 8px;
45 | color: #fff;
46 | font-size: .16rem;
47 | background: @color-primary;
48 | border-bottom: 1px solid #fff;
49 |
50 | &-wrap {
51 | position: relative;
52 | margin: .1rem .16rem .1rem 0;
53 | }
54 |
55 | &-icon {
56 | position: absolute;
57 | top: 0;
58 | right: 0;
59 | color: #fff;
60 | width: 36px;
61 | height: 36px;
62 | line-height: 36px;
63 | text-align: center;
64 | }
65 | }
66 | &-so {
67 | color: #C8E6C9;
68 | }
69 |
70 | // player
71 | &-player {
72 | position: absolute;
73 | left: 0;
74 | bottom: 0;
75 | z-index: 66;
76 | width: 100%;
77 | height: 40px;
78 | background: #fff;
79 |
80 | .container {
81 | margin: 8px 0;
82 | }
83 | }
84 |
85 | &-pic-s {
86 | width: 40px;
87 | height: 40px;
88 | margin-right: @g-margin / 2;
89 | overflow: hidden;
90 |
91 | }
92 |
93 | &-txt {
94 | padding: 5px 0;
95 | .singer {
96 | color: @color-gray;
97 | font-size: .12rem;
98 | }
99 | }
100 |
101 | &-act {
102 |
103 | button {
104 | border: none;
105 | width: 40px;
106 | height: 40px;
107 | color: @color-gray;
108 | text-align: center;
109 | background: #fff;
110 | }
111 | }
112 |
113 | // list
114 | &-main {
115 | position: absolute;
116 | top: 0;
117 | left: 0;
118 | z-index: 2;
119 | width: 100%;
120 | height: 100%;
121 | max-height: 100%;
122 | padding: 56px 0 39px;
123 | overflow-y: auto;
124 | }
125 |
126 | &-list {
127 | background: #fff
128 | }
129 |
130 | &-list-item {
131 | line-height: 1.8;
132 | font-size: .16rem;
133 |
134 | .container {
135 | display: block;
136 | height: 56px;
137 | line-height: 56px;
138 | }
139 |
140 | .sub {
141 | color: @color-gray;
142 | }
143 | }
144 |
145 | &-play-item {
146 | position: relative;
147 |
148 | &:after {
149 | position: absolute;
150 | top: 0;
151 | left: 0;
152 | content: attr(data-index);
153 | width: 48px;
154 | line-height: 62px;
155 | text-align: center;
156 | font-size: .2rem;
157 | color: @color-gray;
158 | }
159 |
160 | .container {
161 | margin-left: 48px;
162 | margin-right: 0;
163 | padding: 8px 48px 8px 0;
164 | height: auto;
165 | line-height: 1.7;
166 | font-size: .15rem;
167 | }
168 | .sub {
169 | font-size: .12rem;
170 | }
171 | .icon-more {
172 | position: absolute;
173 | top: 50%;
174 | right: 0;
175 | margin-top: -20px;
176 | width: 40px;
177 | height: 40px;
178 | line-height: 40px;
179 | text-align: center;
180 | color: @color-gray;
181 | &:active {
182 | background: none;
183 | }
184 | }
185 | }
186 |
187 | }
188 |
189 | .progress-line {
190 | position: absolute;
191 | bottom: 0;
192 | left: 0;
193 | height: 3px;
194 | background: @color-border;
195 | transition: width 1s ease;
196 |
197 | &.primary {
198 | background: @color-primary;
199 | z-index: 2;
200 | }
201 | }
202 |
203 | .fade-in-left-enter,
204 | .fade-in-left-leave-active {
205 | transform: translate3d(100%, 0, 0);
206 | box-shadow: none;
207 | }
208 |
209 | .fade-out-left-enter,
210 | .fade-out-left-leave-active {
211 | transform: translate3d(-100%, 0, 0);
212 | box-shadow: none;
213 | }
214 |
215 | .fade-in-bottom-enter,
216 | .fade-in-bottom-leave-active {
217 | transform: translate3d(0, 100%, 0);
218 | box-shadow: none;
219 | }
220 |
221 |
222 | .fade-in-left-leave-active,
223 | .fade-out-left-enter-active,
224 | .fade-in-bottom-enter-active,
225 | .fade-in-bottom-leave-active {
226 | transition: transform .2s ease;
227 | }
228 | .fade-in-left-enter-active,
229 | .fade-out-left-leave-active {
230 | transition: transform .2s ease;
231 | }
232 |
233 |
--------------------------------------------------------------------------------
/src/less/variable.less:
--------------------------------------------------------------------------------
1 | @color-primary: #4CAF50;
2 | @color-primary-dark: #388E3C;
3 | @color-primary-light: #C8E6C9;
4 | @color-text: #212121;
5 | @color-gray: #727272;
6 | @color-border: #DADADA;
7 | @color-active-bg: rgba(0, 0, 0, .26);
8 |
9 | @g-margin: 16px;
10 | @border-radius-size: 4px;
11 |
12 | @css-prefix: m;
13 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Waves from 'vue-directive-waves'
3 | import App from './App'
4 | import router from './router'
5 | import store from './store'
6 |
7 | Vue.use(Waves)
8 |
9 | new Vue({
10 | el: '#app',
11 | router,
12 | store,
13 | render: h => h(App)
14 | })
15 |
--------------------------------------------------------------------------------
/src/router.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import VueRouter from 'vue-router'
3 | import Search from './view/Search'
4 | import Main from './view/Main'
5 |
6 | Vue.use(VueRouter)
7 |
8 | export default new VueRouter({
9 | routes: [
10 | { path: '/', component: Main },
11 | { path: '/search', component: Search }
12 | ]
13 | })
14 |
--------------------------------------------------------------------------------
/src/store/actions.js:
--------------------------------------------------------------------------------
1 | import Music from '../api/music'
2 |
3 | export default {
4 | addPlayList(store, song) {
5 | Music.getSong(song.id).then(data => {
6 | song.url = data
7 | store.commit('PUSHSONG', song)
8 | })
9 | },
10 | cutSongByIndex(store, index) {
11 | store.commit('CUTSONG', index)
12 | },
13 | nextSong(store, mode) {
14 | let len = store.state.playList.length
15 | let index = store.state.songIndex
16 |
17 | switch (mode) {
18 | case 0:
19 | // 顺序
20 | index = index + 1 === len ? 0 : index + 1
21 | break
22 | case 2:
23 | // 随机
24 | index = Math.floor((Math.random() * (len - 1)) + 1)
25 | break
26 | }
27 |
28 | store.commit('CUTSONG', index)
29 | },
30 | search(store, key) {
31 | Music.search(key).then(data => {
32 | store.commit('SEARCHLIST', data.list)
33 | })
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/store/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Vuex from 'vuex'
3 | import mutations from './mutations'
4 | import actions from './actions'
5 |
6 | Vue.use(Vuex)
7 |
8 | export default new Vuex.Store({
9 | state: {
10 | songIndex: 0,
11 | playList: [],
12 | searchList: []
13 | },
14 | mutations,
15 | actions,
16 | getters: {
17 | currentSong(state) {
18 | return state.playList[state.songIndex]
19 | },
20 | playList(state) {
21 | return state.playList
22 | },
23 | searchList(state) {
24 | return state.searchList
25 | }
26 | }
27 | })
28 |
--------------------------------------------------------------------------------
/src/store/mutations.js:
--------------------------------------------------------------------------------
1 | export default {
2 | PUSHSONG(state, song) {
3 | !state.playList.some(item => item.id === song.id) && state.playList.push(song)
4 | },
5 | CUTSONG(state, index) {
6 | state.songIndex = index
7 | },
8 | SEARCHLIST(state, data) {
9 | state.searchList = data
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/view/Main.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
10 |
21 |
--------------------------------------------------------------------------------
/src/view/Search.vue:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
32 |
--------------------------------------------------------------------------------
/test/unit/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "mocha": true
4 | },
5 | "globals": {
6 | "expect": true,
7 | "sinon": true
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/test/unit/index.js:
--------------------------------------------------------------------------------
1 | // require all test files (files that ends with .spec.js)
2 | const testsContext = require.context('./specs', true, /\.spec$/)
3 | testsContext.keys().forEach(testsContext)
4 |
5 | // require all src files except main.js for coverage.
6 | // you can also change this to match only the subset of files that
7 | // you want coverage for.
8 | // const srcContext = require.context('../../src', true, /^\.\/(?!main(\.js)?$)/)
9 | const srcContext = require.context('../../src', true, /\.vue$/)
10 | srcContext.keys().forEach(srcContext)
11 |
--------------------------------------------------------------------------------
/test/unit/karma.conf.js:
--------------------------------------------------------------------------------
1 | // This is a karma config file. For more details see
2 | // http://karma-runner.github.io/0.13/config/configuration-file.html
3 | // we are also using it with karma-webpack
4 | // https://github.com/webpack/karma-webpack
5 |
6 | var path = require('path')
7 | var merge = require('webpack-merge')
8 | var baseConfig = require('../../config/webpack.base.conf')
9 | var webpack = require('webpack')
10 | var ExtractTextPlugin = require('extract-text-webpack-plugin')
11 | var projectRoot = path.resolve(__dirname, '../../')
12 |
13 | var webpackConfig = merge(baseConfig, {
14 | // use inline sourcemap for karma-sourcemap-loader
15 | // module: {
16 | // loaders: [{
17 | // test: /\.(css|less)$/,
18 | // loader: 'vue-style!css!less'
19 | // }]
20 | // },
21 | devtool: '#inline-source-map',
22 | vue: {
23 | loaders: {
24 | js: 'isparta'
25 | }
26 | },
27 | plugins: [
28 | new webpack.DefinePlugin({
29 | 'process.env': '"testing"'
30 | }),
31 | new webpack.optimize.OccurenceOrderPlugin(),
32 | new ExtractTextPlugin('[name].css')
33 | ]
34 | })
35 |
36 | // no need for app entry during tests
37 | delete webpackConfig.entry
38 |
39 | // make sure isparta loader is applied before eslint
40 | webpackConfig.module.preLoaders = webpackConfig.module.preLoaders || []
41 | webpackConfig.module.preLoaders.unshift({
42 | test: /\.js$/,
43 | loader: 'isparta',
44 | include: path.resolve(projectRoot, 'src')
45 | })
46 |
47 | // only apply babel for test files when using isparta
48 | webpackConfig.module.loaders.some(function (loader, i) {
49 | if (loader.loader === 'babel') {
50 | loader.include = path.resolve(projectRoot, 'test/unit')
51 | return true
52 | }
53 | })
54 |
55 | module.exports = function (config) {
56 | config.set({
57 | // to run in additional browsers:
58 | // 1. install corresponding karma launcher
59 | // http://karma-runner.github.io/0.13/config/browsers.html
60 | // 2. add it to the `browsers` array below.
61 | browsers: ['Chrome'],
62 | frameworks: ['mocha', 'sinon-chai'],
63 | reporters: ['spec', 'coverage'],
64 | files: ['./index.js'],
65 | preprocessors: {
66 | './index.js': ['webpack', 'sourcemap']
67 | },
68 | webpack: webpackConfig,
69 | webpackMiddleware: {
70 | noInfo: true
71 | },
72 | coverageReporter: {
73 | dir: './coverage',
74 | reporters: [
75 | { type: 'lcov', subdir: '.' },
76 | { type: 'text-summary' }
77 | ]
78 | }
79 | })
80 | }
81 |
--------------------------------------------------------------------------------
/test/unit/specs/Header.spec.js:
--------------------------------------------------------------------------------
1 | import { createVue, destroyVM } from '../util.js'
2 | import Header from 'components/Header'
3 |
4 | describe('Header.vue', () => {
5 | let vm
6 |
7 | afterEach(() => {
8 | destroyVM(vm)
9 | })
10 |
11 | it('Header is displayed', () => {
12 | vm = createVue(Header)
13 | expect(vm.$el.querySelector('.title').textContent).to.equal('Just Music')
14 | })
15 | })
16 |
17 |
--------------------------------------------------------------------------------
/test/unit/specs/Search.spec.js:
--------------------------------------------------------------------------------
1 | import { createVue, destroyVM } from '../util.js'
2 | import SearchView from 'src/view/Search'
3 | import SearchHeader from 'components/SearchHeader'
4 | import SearchList from 'components/SearchList'
5 |
6 | describe('SearchView.vue', () => {
7 | let vm
8 |
9 | afterEach(() => {
10 | destroyVM(vm)
11 | })
12 |
13 | it('Input is normal', () => {
14 | vm = createVue(SearchView, {
15 | SearchHeader,
16 | SearchList
17 | })
18 |
19 | vm.key = '曾经的你'
20 |
21 | vm.$nextTick(() => {
22 | expect(vm.$el.querySelector('.m-key').textContent).to.equal('曾经的你')
23 | })
24 | })
25 | })
26 |
--------------------------------------------------------------------------------
/test/unit/util.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import router from 'src/router'
3 | import Waves from 'vue-directive-waves'
4 |
5 | Vue.use(Waves)
6 |
7 | let id = 0
8 |
9 | // 创建DOM节点
10 | const createElm = () => {
11 | const elm = document.createElement('div')
12 |
13 | elm.id = 'app' + ++id
14 | document.body.appendChild(elm)
15 |
16 | return elm
17 | }
18 |
19 | // 创建测试实例
20 | exports.createVue = (comp, deps) => {
21 | // 关联依赖
22 | comp.components = deps || {}
23 |
24 | return new Vue({
25 | el: createElm(),
26 | router,
27 | render: (h) => h(comp)
28 | })
29 | }
30 |
31 | // 销毁实例
32 | exports.destroyVM = (vm) => {
33 | vm.$el &&
34 | vm.$el.parentNode &&
35 | vm.$el.parentNode.removeChild(vm.$el)
36 | }
37 |
38 | // 触发事件
39 | exports.triggerEvent = (elm, name, ...opts) => {
40 | let eventName
41 |
42 | if (/^mouse|click/.test(name)) {
43 | eventName = 'MouseEvents'
44 | } else if (/^key/.test(name)) {
45 | eventName = 'KeyboardEvent'
46 | } else {
47 | eventName = 'HTMLEvents'
48 | }
49 | const evt = document.createEvent(eventName)
50 |
51 | evt.initEvent(name, ...opts)
52 | elm.dispatchEvent
53 | ? elm.dispatchEvent(evt)
54 | : elm.fireEvent('on' + name, evt)
55 |
56 | return elm
57 | }
58 |
--------------------------------------------------------------------------------