├── static
├── .gitkeep
└── logo.ico
├── _config.yml
├── .eslintignore
├── config
├── prod.env.js
├── dev.env.js
└── index.js
├── src
├── common
│ ├── stylus
│ │ ├── index.styl
│ │ ├── base.styl
│ │ ├── mixin.styl
│ │ ├── variable.styl
│ │ ├── reset.styl
│ │ └── icon.styl
│ ├── js
│ │ ├── config.js
│ │ ├── singer.js
│ │ ├── util.js
│ │ ├── jsonp.js
│ │ ├── dom.js
│ │ ├── song.js
│ │ ├── cache.js
│ │ └── mixin.js
│ ├── image
│ │ └── default.png
│ └── fonts
│ │ ├── music-icon.eot
│ │ ├── music-icon.ttf
│ │ ├── music-icon.woff
│ │ └── music-icon.svg
├── base
│ ├── loading
│ │ ├── loading.gif
│ │ └── loading.vue
│ ├── song-list
│ │ ├── first@2x.png
│ │ ├── first@3x.png
│ │ ├── second@2x.png
│ │ ├── second@3x.png
│ │ ├── third@2x.png
│ │ ├── third@3x.png
│ │ └── song-list.vue
│ ├── no-result
│ │ ├── no-result@2x.png
│ │ ├── no-result@3x.png
│ │ └── no-result.vue
│ ├── top-tip
│ │ └── top-tip.vue
│ ├── switches
│ │ └── switches.vue
│ ├── progress-circle
│ │ └── progress-circle.vue
│ ├── search-list
│ │ └── search-list.vue
│ ├── search-box
│ │ └── search-box.vue
│ ├── scroll
│ │ └── scroll.vue
│ ├── confirm
│ │ └── confirm.vue
│ ├── progress-bar
│ │ └── progress-bar.vue
│ ├── slider
│ │ └── slider.vue
│ └── listview
│ │ └── listview.vue
├── components
│ ├── m-header
│ │ ├── logo@2x.png
│ │ ├── logo@3x.png
│ │ └── m-header.vue
│ ├── tab
│ │ └── tab.vue
│ ├── singer-detail
│ │ └── singer-detail.vue
│ ├── disc
│ │ └── disc.vue
│ ├── top-list
│ │ └── top-list.vue
│ ├── singer
│ │ └── singer.vue
│ ├── rank
│ │ └── rank.vue
│ ├── recommend
│ │ └── recommend.vue
│ ├── search
│ │ └── search.vue
│ ├── user-center
│ │ └── user-center.vue
│ ├── suggest
│ │ └── suggest.vue
│ ├── add-song
│ │ └── add-song.vue
│ ├── music-list
│ │ └── music-list.vue
│ ├── playlist
│ │ └── playlist.vue
│ └── player
│ │ └── player.vue
├── api
│ ├── config.js
│ ├── song.js
│ ├── rank.js
│ ├── search.js
│ ├── singer.js
│ └── recommend.js
├── store
│ ├── state.js
│ ├── index.js
│ ├── mutation-types.js
│ ├── getters.js
│ ├── mutations.js
│ └── actions.js
├── main.js
├── App.vue
└── router
│ └── index.js
├── .editorconfig
├── .gitignore
├── .postcssrc.js
├── .babelrc
├── index.html
├── .eslintrc.js
├── README.md
└── package.json
/static/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/_config.yml:
--------------------------------------------------------------------------------
1 | theme: jekyll-theme-time-machine
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | build/*.js
2 | config/*.js
3 |
--------------------------------------------------------------------------------
/config/prod.env.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | NODE_ENV: '"production"'
3 | }
4 |
--------------------------------------------------------------------------------
/static/logo.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Hello-Tan/vue-music-webapp/HEAD/static/logo.ico
--------------------------------------------------------------------------------
/src/common/stylus/index.styl:
--------------------------------------------------------------------------------
1 | @import "./reset.styl"
2 | @import "./base.styl"
3 | @import "./icon.styl"
--------------------------------------------------------------------------------
/src/common/js/config.js:
--------------------------------------------------------------------------------
1 | export const playMode = {
2 | sequence: 0,
3 | loop: 1,
4 | random: 2
5 | }
6 |
--------------------------------------------------------------------------------
/src/base/loading/loading.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Hello-Tan/vue-music-webapp/HEAD/src/base/loading/loading.gif
--------------------------------------------------------------------------------
/src/common/image/default.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Hello-Tan/vue-music-webapp/HEAD/src/common/image/default.png
--------------------------------------------------------------------------------
/src/base/song-list/first@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Hello-Tan/vue-music-webapp/HEAD/src/base/song-list/first@2x.png
--------------------------------------------------------------------------------
/src/base/song-list/first@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Hello-Tan/vue-music-webapp/HEAD/src/base/song-list/first@3x.png
--------------------------------------------------------------------------------
/src/base/song-list/second@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Hello-Tan/vue-music-webapp/HEAD/src/base/song-list/second@2x.png
--------------------------------------------------------------------------------
/src/base/song-list/second@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Hello-Tan/vue-music-webapp/HEAD/src/base/song-list/second@3x.png
--------------------------------------------------------------------------------
/src/base/song-list/third@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Hello-Tan/vue-music-webapp/HEAD/src/base/song-list/third@2x.png
--------------------------------------------------------------------------------
/src/base/song-list/third@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Hello-Tan/vue-music-webapp/HEAD/src/base/song-list/third@3x.png
--------------------------------------------------------------------------------
/src/common/fonts/music-icon.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Hello-Tan/vue-music-webapp/HEAD/src/common/fonts/music-icon.eot
--------------------------------------------------------------------------------
/src/common/fonts/music-icon.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Hello-Tan/vue-music-webapp/HEAD/src/common/fonts/music-icon.ttf
--------------------------------------------------------------------------------
/src/common/fonts/music-icon.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Hello-Tan/vue-music-webapp/HEAD/src/common/fonts/music-icon.woff
--------------------------------------------------------------------------------
/src/base/no-result/no-result@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Hello-Tan/vue-music-webapp/HEAD/src/base/no-result/no-result@2x.png
--------------------------------------------------------------------------------
/src/base/no-result/no-result@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Hello-Tan/vue-music-webapp/HEAD/src/base/no-result/no-result@3x.png
--------------------------------------------------------------------------------
/src/components/m-header/logo@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Hello-Tan/vue-music-webapp/HEAD/src/components/m-header/logo@2x.png
--------------------------------------------------------------------------------
/src/components/m-header/logo@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Hello-Tan/vue-music-webapp/HEAD/src/components/m-header/logo@3x.png
--------------------------------------------------------------------------------
/config/dev.env.js:
--------------------------------------------------------------------------------
1 | var merge = require('webpack-merge')
2 | var prodEnv = require('./prod.env')
3 |
4 | module.exports = merge(prodEnv, {
5 | NODE_ENV: '"development"'
6 | })
7 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules/
3 | dist/
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Editor directories and files
9 | .idea
10 | *.suo
11 | *.ntvs*
12 | *.njsproj
13 | *.sln
14 |
--------------------------------------------------------------------------------
/.postcssrc.js:
--------------------------------------------------------------------------------
1 | // https://github.com/michael-ciniawsky/postcss-load-config
2 |
3 | module.exports = {
4 | "plugins": {
5 | // to edit target browsers: use "browserlist" field in package.json
6 | "autoprefixer": {}
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/common/js/singer.js:
--------------------------------------------------------------------------------
1 | export default class Singer {
2 | constructor ({id, name}) {
3 | this.id = id
4 | this.name = name
5 | this.avatar = `https://y.gtimg.cn/music/photo_new/T001R300x300M000${id}.jpg?max_age=2592000`
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/api/config.js:
--------------------------------------------------------------------------------
1 | export const commonParams = {
2 | g_tk: 5381,
3 | inCharset: 'utf-8',
4 | outCharset: 'utf-8',
5 | notice: 0,
6 | format: 'json'
7 | }
8 |
9 | export const options = {
10 | param: 'jsonpCallback'
11 | }
12 |
13 | export const ERR_OK = 0
14 |
--------------------------------------------------------------------------------
/src/common/stylus/base.styl:
--------------------------------------------------------------------------------
1 | @import "variable.styl"
2 |
3 | body, html
4 | line-height: 1
5 | font-family: 'PingFang SC', 'STHeitiSC-Light', 'Helvetica-Light', arial, sans-serif, 'Droid Sans Fallback'
6 | user-select: none
7 | -webkit-tap-highlight-color: transparent
8 | background: $color-background
9 | color: $color-text
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | ["env", {
4 | "modules": false,
5 | "targets": {
6 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
7 | }
8 | }],
9 | "stage-2"
10 | ],
11 | "plugins": ["transform-runtime"],
12 | "env": {
13 | "test": {
14 | "presets": ["env", "stage-2"],
15 | "plugins": ["istanbul"]
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
7 | Pikachu Music
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/store/state.js:
--------------------------------------------------------------------------------
1 | import {playMode} from 'common/js/config'
2 | import {loadSearch, loadPlay, loadFavorite} from 'common/js/cache'
3 |
4 | const state = {
5 | singer: {},
6 | playing: false,
7 | fullScreen: false,
8 | playlist: [],
9 | sequenceList: [],
10 | mode: playMode.sequence,
11 | currentIndex: -1,
12 | disc: {},
13 | topList: {},
14 | searchHistory: loadSearch(),
15 | playHistory: loadPlay(),
16 | favoriteList: loadFavorite()
17 | }
18 |
19 | export default state
20 |
--------------------------------------------------------------------------------
/src/store/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Vuex from 'vuex'
3 | import * as actions from './actions'
4 | import * as getters from './getters'
5 | import state from './state'
6 | import mutations from './mutations'
7 | import createLogger from 'vuex/dist/logger'
8 |
9 | Vue.use(Vuex)
10 |
11 | const debug = process.env.NODE_ENV !== 'production'
12 |
13 | export default new Vuex.Store({
14 | actions,
15 | getters,
16 | state,
17 | mutations,
18 | strict: debug,
19 | plugins: debug ? [createLogger()] : []
20 | })
21 |
--------------------------------------------------------------------------------
/src/common/stylus/mixin.styl:
--------------------------------------------------------------------------------
1 | // 背景图片
2 | bg-image($url)
3 | background-image: url($url + "@2x.png")
4 | @media (-webkit-min-device-pixel-ratio: 3),(min-device-pixel-ratio: 3)
5 | background-image: url($url + "@3x.png")
6 |
7 | // 不换行
8 | no-wrap()
9 | text-overflow: ellipsis
10 | overflow: hidden
11 | white-space: nowrap
12 |
13 | // 扩展点击区域
14 | extend-click()
15 | position: relative
16 | &:before
17 | content: ''
18 | position: absolute
19 | top: -10px
20 | left: -10px
21 | right: -10px
22 | bottom: -10px
--------------------------------------------------------------------------------
/src/api/song.js:
--------------------------------------------------------------------------------
1 | import {commonParams} from 'api/config'
2 | import axios from 'axios'
3 |
4 | export function getLyric (mid) {
5 | const url = '/api/lyric'
6 |
7 | const data = Object.assign({}, commonParams, {
8 | songmid: mid,
9 | pcachetime: +new Date(),
10 | loginUin: 0,
11 | hostUin: 0,
12 | format: 'json',
13 | notice: 0,
14 | platform: 'yqq',
15 | needNewCode: 0
16 | })
17 | return axios.get(url, {
18 | params: data
19 | }).then((res) => {
20 | return Promise.resolve(res.data)
21 | })
22 | }
23 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import fastclick from 'fastclick'
2 | import Vue from 'vue'
3 | import App from './App'
4 | import router from './router'
5 | import store from './store'
6 | import VueLazyload from 'vue-lazyload'
7 |
8 | Vue.config.productionTip = false
9 | fastclick.attach(document.body)
10 | Vue.use(VueLazyload, {
11 | loading: require('common/image/default.png')
12 | })
13 |
14 | import 'common/stylus/index.styl'
15 |
16 | /* eslint-disable no-new */
17 | new Vue({
18 | el: '#app',
19 | router,
20 | store,
21 | render: h => h(App)
22 | })
23 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/src/common/stylus/variable.styl:
--------------------------------------------------------------------------------
1 | // 颜色定义规范
2 | $color-background = #222
3 | $color-background-d = rgba(0, 0, 0, 0.3)
4 | $color-highlight-background = #333
5 | $color-dialog-background = #666
6 | $color-theme = #ffcd32
7 | $color-theme-d = rgba(255, 205, 49, 0.5)
8 | $color-sub-theme = #d93f30
9 | $color-text = #fff
10 | $color-text-d = rgba(255, 255, 255, 0.3)
11 | $color-text-l = rgba(255, 255, 255, 0.5)
12 | $color-text-ll = rgba(255, 255, 255, 0.8)
13 |
14 | //字体定义规范
15 | $font-size-small-s = 10px
16 | $font-size-small = 12px
17 | $font-size-medium = 14px
18 | $font-size-medium-x = 16px
19 | $font-size-large = 18px
20 | $font-size-large-x = 22px
--------------------------------------------------------------------------------
/src/common/js/util.js:
--------------------------------------------------------------------------------
1 | function getRandomInt (min, max) {
2 | return Math.floor(Math.random() * (max - min + 1) + min)
3 | }
4 |
5 | export function shuffle (arr) {
6 | let _arr = arr.slice()
7 | for (let i = 0; i < _arr.length; i++) {
8 | let j = getRandomInt(0, i)
9 | let t = _arr[i]
10 | _arr[i] = _arr[j]
11 | _arr[j] = t
12 | }
13 | return _arr
14 | }
15 |
16 | export function debounce (func, delay) {
17 | let timer
18 |
19 | return function (...args) {
20 | if (timer) {
21 | clearTimeout(timer)
22 | }
23 | timer = setTimeout(() => {
24 | func.apply(this, args)
25 | }, delay)
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/common/js/jsonp.js:
--------------------------------------------------------------------------------
1 | import originJsonp from 'jsonp'
2 |
3 | export default function jsonp (url, data, option) {
4 | url += (url.indexOf('?') < 0 ? '?' : '&') + param(data)
5 |
6 | return new Promise((resolve, reject) => {
7 | originJsonp(url, option, (err, data) => {
8 | if (!err) {
9 | resolve(data)
10 | } else {
11 | reject(err)
12 | }
13 | })
14 | })
15 | }
16 |
17 | export function param (data) {
18 | let url = ''
19 | for (var k in data) {
20 | let value = data[k] !== undefined ? data[k] : ''
21 | url += '&' + k + '=' + encodeURIComponent(value)
22 | }
23 | return url ? url.substring(1) : ''
24 | }
25 |
--------------------------------------------------------------------------------
/src/base/loading/loading.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |

4 |
{{title}}
5 |
6 |
7 |
17 |
28 |
--------------------------------------------------------------------------------
/src/store/mutation-types.js:
--------------------------------------------------------------------------------
1 | export const SET_SINGER = 'SET_SINGER'
2 |
3 | export const SET_PLAYING_STATE = 'SET_PLAYING_STATE'
4 |
5 | export const SET_FULL_SCREEN = 'SET_FULL_SCREEN'
6 |
7 | export const SET_PLAYLIST = 'SET_PLAYLIST'
8 |
9 | export const SET_SEQUENCE_LIST = 'SET_SEQUENCE_LIST'
10 |
11 | export const SET_PLAY_MODE = 'SET_PLAY_MODE'
12 |
13 | export const SET_CURRENT_INDEX = 'SET_CURRENT_INDEX'
14 |
15 | export const SET_DISC = 'SET_DISC'
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_PLAY_HISTORY = 'SET_PLAY_HISTORY'
22 |
23 | export const SET_FAVORITE_LIST = 'SET_FAVORITE_LIST'
24 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | // http://eslint.org/docs/user-guide/configuring
2 |
3 | module.exports = {
4 | root: true,
5 | parser: 'babel-eslint',
6 | parserOptions: {
7 | sourceType: 'module'
8 | },
9 | env: {
10 | browser: true,
11 | },
12 | // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style
13 | extends: 'standard',
14 | // required to lint *.vue files
15 | plugins: [
16 | 'html'
17 | ],
18 | // add your custom rules here
19 | 'rules': {
20 | // allow paren-less arrow functions
21 | 'arrow-parens': 0,
22 | // allow async-await
23 | 'generator-star-spacing': 0,
24 | // allow debugger during development
25 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/api/rank.js:
--------------------------------------------------------------------------------
1 | import jsonp from 'common/js/jsonp'
2 | import {commonParams, options} from './config'
3 |
4 | export function getTopList () {
5 | const url = 'https://c.y.qq.com/v8/fcg-bin/fcg_myqq_toplist.fcg'
6 |
7 | const data = Object.assign({}, commonParams, {
8 | format: 'jsonp',
9 | platform: 'h5',
10 | uin: 0,
11 | needNewCode: 1
12 | })
13 |
14 | return jsonp(url, data, options)
15 | }
16 |
17 | export function getMusicList (topid) {
18 | const url = 'https://c.y.qq.com/v8/fcg-bin/fcg_v8_toplist_cp.fcg'
19 |
20 | const data = Object.assign({}, commonParams, {
21 | topid,
22 | format: 'jsonp',
23 | platform: 'h5',
24 | uin: 0,
25 | needNewCode: 1,
26 | tpl: 3,
27 | page: 'detail',
28 | type: 'top'
29 | })
30 |
31 | return jsonp(url, data, options)
32 | }
33 |
--------------------------------------------------------------------------------
/src/store/getters.js:
--------------------------------------------------------------------------------
1 | export const singer = state => state.singer
2 |
3 | export const playing = state => state.playing
4 |
5 | export const fullScreen = state => state.fullScreen
6 |
7 | export const playlist = state => state.playlist
8 |
9 | export const sequenceList = state => state.sequenceList
10 |
11 | export const mode = state => state.mode
12 |
13 | export const currentIndex = state => state.currentIndex
14 |
15 | export const disc = state => state.disc
16 |
17 | export const topList = state => state.topList
18 |
19 | export const searchHistory = state => state.searchHistory
20 |
21 | export const playHistory = state => state.playHistory
22 |
23 | export const favoriteList = state => state.favoriteList
24 |
25 | export const currentSong = (state) => {
26 | return state.playlist[state.currentIndex] || {}
27 | }
28 |
--------------------------------------------------------------------------------
/src/base/no-result/no-result.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
18 |
19 |
36 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # vue-music
2 |
3 | > 基于vue全家桶开发音乐webapp
4 |
5 | # 在线演示地址:
6 | https://tanrenjie.github.io/vue-music-webapp
7 |
8 |
9 | PC端建议使用chrome手机调试工具查看,移动端推荐使用最新版谷歌浏览器或微信自带浏览器
10 |
11 |
12 | API失效/BUG 请发邮件联系。
13 |
14 | **PS: 因Gitpage只能上传静态网站,因此没有歌词(项目中用的nodejs+Axios获取歌词数据)**
15 |
16 |
17 | ### 技术栈
18 | - Vue2.3:采用最新Vue2.3的语法
19 | - Vuex:管理组件之间状态
20 | - vue-router:单页面应用路由
21 | - jsonp:发起跨域http请求
22 | - stylus:css预处理语言
23 | - Express:解决api接口referer端口限制。
24 | - Webpack:自动化构建工具,主要配置vue-cli脚手架提供。
25 | - ES6:采用ES6语法。
26 | - CSS3:CSS3动画及样式。
27 | ### 实现的功能
28 | 1. 音乐播放、暂停、切换、收藏
29 | 2. 搜索单曲、歌手 / 上拉加载更多
30 | 3. 搜索历史记录
31 | 4. 首页轮播图
32 | 5. 歌曲排行榜
33 | 6. 切换播放模式
34 | 7. 歌词跟随播放进度显示
35 | 8. 图片懒加载/组件懒加载
36 |
37 |
38 | ## Build Setup
39 |
40 | ``` bash
41 | # 安装项目依赖
42 | npm install
43 |
44 | # 启动项目,在浏览器输入localhost:8080访问
45 | npm run dev
46 |
47 | # 打包项目
48 | npm run build
49 |
50 | ```
51 |
52 |
--------------------------------------------------------------------------------
/src/api/search.js:
--------------------------------------------------------------------------------
1 | import jsonp from 'common/js/jsonp'
2 | import {commonParams, options} from './config'
3 |
4 | export function getHotKey () {
5 | const url = 'https://c.y.qq.com/splcloud/fcgi-bin/gethotkey.fcg'
6 |
7 | const data = Object.assign({}, commonParams, {
8 | format: 'jsonp',
9 | platform: 'h5',
10 | uin: 0,
11 | needNewCode: 1
12 | })
13 |
14 | return jsonp(url, data, options)
15 | }
16 |
17 | export function search (query, page, zhida, perpage) {
18 | const url = 'https://c.y.qq.com/soso/fcgi-bin/search_for_qq_cp'
19 |
20 | const data = Object.assign({}, commonParams, {
21 | w: query,
22 | p: page,
23 | catZhida: zhida ? 1 : 0,
24 | perpage: perpage,
25 | n: perpage,
26 | zhidaqu: 1,
27 | t: 0,
28 | flag: 1,
29 | ie: 'utf-8',
30 | sem: 1,
31 | aggr: 0,
32 | remoteplace: 'txt.mqq.all',
33 | format: 'jsonp',
34 | platform: 'h5',
35 | uin: 0,
36 | needNewCode: 1
37 | })
38 |
39 | return jsonp(url, data, options)
40 | }
41 |
--------------------------------------------------------------------------------
/src/api/singer.js:
--------------------------------------------------------------------------------
1 | import jsonp from '../common/js/jsonp'
2 | import {commonParams, options} from './config'
3 |
4 | export function getSingerList () {
5 | const url = 'https://c.y.qq.com/v8/fcg-bin/v8.fcg'
6 | const data = Object.assign({}, commonParams, {
7 | channel: 'singer',
8 | page: 'list',
9 | key: 'all_all_all',
10 | pagesize: 100,
11 | pagenum: 1,
12 | loginUin: 0,
13 | hostUin: 0,
14 | format: 'jsonp',
15 | notice: 0,
16 | platform: 'yqq',
17 | needNewCode: 0
18 | })
19 | return jsonp(url, data, options)
20 | }
21 |
22 | export function getSingerDetail (singerId) {
23 | const url = 'https://c.y.qq.com/v8/fcg-bin/fcg_v8_singer_track_cp.fcg'
24 | const data = Object.assign({}, commonParams, {
25 | loginUin: 0,
26 | hostUin: 0,
27 | notice: 0,
28 | format: 'jsonp',
29 | platform: 'yqq',
30 | needNewCode: 0,
31 | order: 'listen',
32 | begin: 0,
33 | num: 100,
34 | songstatus: 1,
35 | singermid: singerId
36 | })
37 | return jsonp(url, data, options)
38 | }
39 |
--------------------------------------------------------------------------------
/src/store/mutations.js:
--------------------------------------------------------------------------------
1 | import * as types from './mutation-types'
2 |
3 | const mutations = {
4 | [types.SET_SINGER] (state, singer) {
5 | state.singer = singer
6 | },
7 | [types.SET_PLAYING_STATE] (state, flag) {
8 | state.playing = flag
9 | },
10 | [types.SET_FULL_SCREEN] (state, flag) {
11 | state.fullScreen = flag
12 | },
13 | [types.SET_PLAYLIST] (state, list) {
14 | state.playlist = list
15 | },
16 | [types.SET_SEQUENCE_LIST] (state, list) {
17 | state.sequenceList = list
18 | },
19 | [types.SET_PLAY_MODE] (state, mode) {
20 | state.mode = mode
21 | },
22 | [types.SET_CURRENT_INDEX] (state, index) {
23 | state.currentIndex = index
24 | },
25 | [types.SET_DISC] (state, disc) {
26 | state.disc = disc
27 | },
28 | [types.SET_TOP_LIST] (state, list) {
29 | state.topList = list
30 | },
31 | [types.SET_SEARCH_HISTORY] (state, history) {
32 | state.searchHistory = history
33 | },
34 | [types.SET_PLAY_HISTORY] (state, history) {
35 | state.playHistory = history
36 | },
37 | [types.SET_FAVORITE_LIST] (state, list) {
38 | state.favoriteList = list
39 | }
40 | }
41 |
42 | export default mutations
43 |
--------------------------------------------------------------------------------
/src/base/top-tip/top-tip.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
36 |
37 |
51 |
--------------------------------------------------------------------------------
/src/components/tab/tab.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 推荐
5 |
6 |
7 | 歌手
8 |
9 |
10 | 排行
11 |
12 |
13 |
14 | 搜索
15 |
16 |
17 |
18 |
19 |
22 |
23 |
42 |
--------------------------------------------------------------------------------
/src/base/switches/switches.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | -
4 | {{item.name}}
5 |
6 |
7 |
8 |
9 |
28 |
29 |
49 |
--------------------------------------------------------------------------------
/src/components/m-header/m-header.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
16 |
17 |
51 |
--------------------------------------------------------------------------------
/src/common/js/dom.js:
--------------------------------------------------------------------------------
1 | export function hasClass (el, className) {
2 | let reg = new RegExp('(^|\\s)' + className + '(\\s|$)')
3 | return reg.test(el.className)
4 | }
5 |
6 | export function addClass (el, className) {
7 | if (hasClass(el, className)) {
8 | return
9 | }
10 |
11 | let newClass = el.className.split(' ')
12 | newClass.push(className)
13 | el.className = newClass.join(' ')
14 | }
15 |
16 | export function getData (el, name, val) {
17 | const prefix = 'data-'
18 | name = prefix + name
19 | if (val) {
20 | return el.setAttribute(name, val)
21 | }
22 | return el.getAttribute(name)
23 | }
24 |
25 | let elementStyle = document.createElement('div').style
26 |
27 | let vendor = (() => {
28 | let transformNames = {
29 | webkit: 'webkitTransform',
30 | Moz: 'MozTransform',
31 | O: 'OTransform',
32 | ms: 'msTransform',
33 | standard: 'transform'
34 | }
35 |
36 | for (let key in transformNames) {
37 | if (elementStyle[transformNames[key]] !== undefined) {
38 | return key
39 | }
40 | }
41 |
42 | return false
43 | })()
44 |
45 | export function prefixStyle (style) {
46 | if (vendor === false) {
47 | return false
48 | }
49 |
50 | if (vendor === 'standard') {
51 | return style
52 | }
53 |
54 | return vendor + style.charAt(0).toUpperCase() + style.substr(1)
55 | }
56 |
--------------------------------------------------------------------------------
/src/api/recommend.js:
--------------------------------------------------------------------------------
1 | import jsonp from 'common/js/jsonp'
2 | import {commonParams, options} from './config'
3 | import axios from 'axios'
4 |
5 | export function getRecommend () {
6 | const url = 'https://c.y.qq.com/musichall/fcgi-bin/fcg_yqqhomepagerecommend.fcg'
7 |
8 | const data = Object.assign({}, commonParams, {
9 | platform: 'h5',
10 | uin: 0,
11 | needNewCode: 1
12 | })
13 |
14 | return jsonp(url, data, options)
15 | }
16 |
17 | export function getDiscList () {
18 | const url = '/api/getDiscList'
19 |
20 | const data = Object.assign({}, commonParams, {
21 | platform: 'yqq',
22 | hostUin: 0,
23 | sin: 0,
24 | ein: 29,
25 | sortId: 5,
26 | needNewCode: 0,
27 | categoryId: 10000000,
28 | rnd: Math.random(),
29 | format: 'json'
30 | })
31 |
32 | return axios.get(url, {
33 | params: data
34 | }).then((res) => {
35 | return Promise.resolve(res.data)
36 | })
37 | }
38 |
39 | export function getSongList (disstid) {
40 | const url = 'https://c.y.qq.com/qzone/fcg-bin/fcg_ucc_getcdinfo_byids_cp.fcg'
41 |
42 | const data = Object.assign({}, commonParams, {
43 | disstid,
44 | format: 'jsonp',
45 | type: 1,
46 | json: 1,
47 | utf8: 1,
48 | onlysong: 0,
49 | platform: 'yqq',
50 | hostUin: 0,
51 | needNewCode: 0
52 | })
53 |
54 | return jsonp(url, data, options)
55 | }
56 |
--------------------------------------------------------------------------------
/src/base/progress-circle/progress-circle.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 |
12 |
36 |
37 |
52 |
--------------------------------------------------------------------------------
/src/common/stylus/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/base/search-list/search-list.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{item}}
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
32 |
33 |
55 |
--------------------------------------------------------------------------------
/config/index.js:
--------------------------------------------------------------------------------
1 | // see http://vuejs-templates.github.io/webpack for documentation.
2 | var path = require('path')
3 |
4 | module.exports = {
5 | build: {
6 | env: require('./prod.env'),
7 | index: path.resolve(__dirname, '../dist/index.html'),
8 | assetsRoot: path.resolve(__dirname, '../dist'),
9 | assetsSubDirectory: 'static',
10 | assetsPublicPath: '/',
11 | productionSourceMap: true,
12 | // Gzip off by default as many popular static hosts such as
13 | // Surge or Netlify already gzip all static assets for you.
14 | // Before setting to `true`, make sure to:
15 | // npm install --save-dev compression-webpack-plugin
16 | productionGzip: false,
17 | productionGzipExtensions: ['js', 'css'],
18 | // Run the build command with an extra argument to
19 | // View the bundle analyzer report after build finishes:
20 | // `npm run build --report`
21 | // Set to `true` or `false` to always turn it on or off
22 | bundleAnalyzerReport: process.env.npm_config_report
23 | },
24 | dev: {
25 | env: require('./dev.env'),
26 | port: 8080,
27 | autoOpenBrowser: true,
28 | assetsSubDirectory: 'static',
29 | assetsPublicPath: '/',
30 | proxyTable: {},
31 | // CSS Sourcemaps off by default because relative paths are "buggy"
32 | // with this option, according to the CSS-Loader README
33 | // (https://github.com/webpack/css-loader#sourcemaps)
34 | // In our experience, they generally work as expected,
35 | // just be aware of this issue when enabling this option.
36 | cssSourceMap: false
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/common/js/song.js:
--------------------------------------------------------------------------------
1 | import {getLyric} from 'api/song'
2 | import {ERR_OK} from 'api/config'
3 | import {Base64} from 'js-base64'
4 |
5 | export default class Song {
6 | constructor ({id, mid, singer, name, album, duration, image, url}) {
7 | this.id = id
8 | this.mid = mid
9 | this.singer = singer
10 | this.name = name
11 | this.album = album
12 | this.duration = duration
13 | this.image = image
14 | this.url = url
15 | }
16 |
17 | getLyric () {
18 | if (this.lyric) {
19 | return Promise.resolve(this.lyric)
20 | }
21 | return new Promise((resolve, reject) => {
22 | getLyric(this.mid).then((res) => {
23 | if (res.code === ERR_OK) {
24 | this.lyric = Base64.decode(res.lyric)
25 | resolve(this.lyric)
26 | } else {
27 | reject('no-lyric')
28 | }
29 | })
30 | })
31 | }
32 | }
33 |
34 | export function createSong (musicData) {
35 | return new Song({
36 | id: musicData.songid,
37 | mid: musicData.songmid,
38 | singer: filterSinger(musicData.singer),
39 | name: musicData.songname,
40 | album: musicData.albumname,
41 | duration: musicData.interval,
42 | image: `https://y.gtimg.cn/music/photo_new/T002R300x300M000${musicData.albummid}.jpg?max_age=2592000`,
43 | url: `http://ws.stream.qqmusic.qq.com/${musicData.songid}.m4a?fromtag=38`
44 | })
45 | }
46 |
47 | function filterSinger (singer) {
48 | let ret = []
49 | if (!singer) {
50 | return ''
51 | }
52 | singer.forEach((s) => {
53 | ret.push(s.name)
54 | })
55 | return ret.join('/')
56 | }
57 |
--------------------------------------------------------------------------------
/src/base/search-box/search-box.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
42 |
43 |
71 |
--------------------------------------------------------------------------------
/src/components/singer-detail/singer-detail.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
63 |
64 |
70 |
--------------------------------------------------------------------------------
/src/components/disc/disc.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
61 |
62 |
69 |
--------------------------------------------------------------------------------
/src/components/top-list/top-list.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
66 |
67 |
74 |
--------------------------------------------------------------------------------
/src/common/stylus/icon.styl:
--------------------------------------------------------------------------------
1 | @font-face
2 | font-family: 'music-icon'
3 | src: url('../fonts/music-icon.eot?2qevqt')
4 | src: url('../fonts/music-icon.eot?2qevqt#iefix') format('embedded-opentype'),
5 | url('../fonts/music-icon.ttf?2qevqt') format('truetype'),
6 | url('../fonts/music-icon.woff?2qevqt') format('woff'),
7 | url('../fonts/music-icon.svg?2qevqt#music-icon') format('svg')
8 | font-weight: normal
9 | font-style: normal
10 |
11 | [class^="icon-"], [class*=" icon-"]
12 | /* use !important to prevent issues with browser extensions that change fonts */
13 | font-family: 'music-icon' !important
14 | speak: none
15 | font-style: normal
16 | font-weight: normal
17 | font-variant: normal
18 | text-transform: none
19 | line-height: 1
20 |
21 | /* Better Font Rendering =========== */
22 | -webkit-font-smoothing: antialiased
23 | -moz-osx-font-smoothing: grayscale
24 |
25 | .icon-ok:before
26 | content: "\e900"
27 |
28 | .icon-close:before
29 | content: "\e901"
30 |
31 | .icon-add:before
32 | content: "\e902"
33 |
34 | .icon-play-mini:before
35 | content: "\e903"
36 |
37 | .icon-playlist:before
38 | content: "\e904"
39 |
40 | .icon-music:before
41 | content: "\e905"
42 |
43 | .icon-search:before
44 | content: "\e906"
45 |
46 | .icon-clear:before
47 | content: "\e907"
48 |
49 | .icon-delete:before
50 | content: "\e908"
51 |
52 | .icon-favorite:before
53 | content: "\e909"
54 |
55 | .icon-not-favorite:before
56 | content: "\e90a"
57 |
58 | .icon-pause:before
59 | content: "\e90b"
60 |
61 | .icon-play:before
62 | content: "\e90c"
63 |
64 | .icon-prev:before
65 | content: "\e90d"
66 |
67 | .icon-loop:before
68 | content: "\e90e"
69 |
70 | .icon-sequence:before
71 | content: "\e90f"
72 |
73 | .icon-random:before
74 | content: "\e910"
75 |
76 | .icon-back:before
77 | content: "\e911"
78 |
79 | .icon-mine:before
80 | content: "\e912"
81 |
82 | .icon-next:before
83 | content: "\e913"
84 |
85 | .icon-dismiss:before
86 | content: "\e914"
87 |
88 | .icon-pause-mini:before
89 | content: "\e915"
90 |
--------------------------------------------------------------------------------
/src/common/js/cache.js:
--------------------------------------------------------------------------------
1 | import storage from 'good-storage'
2 |
3 | const SEARCH_KEY = '__search__'
4 | const SEARCH_MAX_LEN = 15
5 |
6 | const PLAY_KEY = '__play__'
7 | const PLAY_MAX_LEN = 200
8 |
9 | const FAVORITE_KEY = '__favorite__'
10 | const FAVORITE_MAX_LEN = 200
11 |
12 | function insertArray (arr, val, compare, maxLen) {
13 | const index = arr.findIndex(compare)
14 | if (index === 0) {
15 | return
16 | }
17 | if (index > 0) {
18 | arr.splice(index, 1)
19 | }
20 | arr.unshift(val)
21 | if (maxLen && arr.length > maxLen) {
22 | arr.pop()
23 | }
24 | }
25 |
26 | function deleteFromArray (arr, compare) {
27 | const index = arr.findIndex(compare)
28 | if (index > -1) {
29 | arr.splice(index, 1)
30 | }
31 | }
32 |
33 | export function saveSearch (query) {
34 | let searches = storage.get(SEARCH_KEY, [])
35 | insertArray(searches, query, (item) => {
36 | return item === query
37 | }, SEARCH_MAX_LEN)
38 | storage.set(SEARCH_KEY, searches)
39 | return searches
40 | }
41 |
42 | export function deleteSearch (query) {
43 | let searches = storage.get(SEARCH_KEY, [])
44 | deleteFromArray(searches, (item) => {
45 | return item === query
46 | })
47 | storage.set(SEARCH_KEY, searches)
48 | return searches
49 | }
50 |
51 | export function clearSearch () {
52 | storage.remove(SEARCH_KEY)
53 | return []
54 | }
55 |
56 | export function loadSearch () {
57 | return storage.get(SEARCH_KEY, [])
58 | }
59 |
60 | export function savePlay (song) {
61 | let songs = storage.get(PLAY_KEY, [])
62 | insertArray(songs, song, (item) => {
63 | return song.id === item.id
64 | }, PLAY_MAX_LEN)
65 | storage.set(PLAY_KEY, songs)
66 | return songs
67 | }
68 |
69 | export function loadPlay () {
70 | return storage.get(PLAY_KEY, [])
71 | }
72 |
73 | export function saveFavorite (song) {
74 | let songs = storage.get(FAVORITE_KEY, [])
75 | insertArray(songs, song, (item) => {
76 | return song.id === item.id
77 | }, FAVORITE_MAX_LEN)
78 | storage.set(FAVORITE_KEY, songs)
79 | return songs
80 | }
81 |
82 | export function deleteFavorite (song) {
83 | let songs = storage.get(FAVORITE_KEY, [])
84 | deleteFromArray(songs, (item) => {
85 | return item.id === song.id
86 | })
87 | storage.set(FAVORITE_KEY, songs)
88 | return songs
89 | }
90 |
91 | export function loadFavorite () {
92 | return storage.get(FAVORITE_KEY, [])
93 | }
94 |
95 |
--------------------------------------------------------------------------------
/src/router/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Router from 'vue-router'
3 |
4 | Vue.use(Router)
5 |
6 | const Recommend = (resolve) => {
7 | import('components/recommend/recommend').then((module) => {
8 | resolve(module)
9 | })
10 | }
11 |
12 | const Singer = (resolve) => {
13 | import('components/singer/singer').then((module) => {
14 | resolve(module)
15 | })
16 | }
17 |
18 | const Rank = (resolve) => {
19 | import('components/rank/rank').then((module) => {
20 | resolve(module)
21 | })
22 | }
23 |
24 | const Search = (resolve) => {
25 | import('components/search/search').then((module) => {
26 | resolve(module)
27 | })
28 | }
29 |
30 | const SingerDetail = (resolve) => {
31 | import('components/singer-detail/singer-detail').then((module) => {
32 | resolve(module)
33 | })
34 | }
35 |
36 | const Disc = (resolve) => {
37 | import('components/disc/disc').then((module) => {
38 | resolve(module)
39 | })
40 | }
41 |
42 | const TopList = (resolve) => {
43 | import('components/top-list/top-list').then((module) => {
44 | resolve(module)
45 | })
46 | }
47 |
48 | const UserCenter = (resolve) => {
49 | import('components/user-center/user-center').then((module) => {
50 | resolve(module)
51 | })
52 | }
53 |
54 | export default new Router({
55 | routes: [
56 | {
57 | path: '/',
58 | redirect: '/recommend'
59 | },
60 | {
61 | path: '/recommend',
62 | component: Recommend,
63 | children: [
64 | {
65 | path: ':id',
66 | component: Disc
67 | }
68 | ]
69 | },
70 | {
71 | path: '/singer',
72 | component: Singer,
73 | children: [
74 | {
75 | path: ':id',
76 | component: SingerDetail
77 | }
78 | ]
79 | },
80 | {
81 | path: '/rank',
82 | component: Rank,
83 | children: [
84 | {
85 | path: ':id',
86 | component: TopList
87 | }
88 | ]
89 | },
90 | {
91 | path: '/search',
92 | component: Search,
93 | children: [
94 | {
95 | path: ':id',
96 | component: SingerDetail
97 | }
98 | ]
99 | },
100 | {
101 | path: '/user',
102 | component: UserCenter
103 | }
104 | ]
105 | })
106 |
--------------------------------------------------------------------------------
/src/base/song-list/song-list.vue:
--------------------------------------------------------------------------------
1 |
2 |
15 |
16 |
17 |
47 |
48 |
90 |
--------------------------------------------------------------------------------
/src/base/scroll/scroll.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
95 |
96 |
99 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue-music-webapp",
3 | "version": "1.0.0",
4 | "description": "基于vue开发音乐webapp",
5 | "author": "tanrenjie <583818825@qq.com>",
6 | "private": true,
7 | "scripts": {
8 | "dev": "node build/dev-server.js",
9 | "start": "node build/dev-server.js",
10 | "build": "node build/build.js",
11 | "lint": "eslint --ext .js,.vue src"
12 | },
13 | "dependencies": {
14 | "babel-runtime": "^6.0.0",
15 | "vue": "^2.3.3",
16 | "vue-router": "^2.5.3",
17 | "vuex": "^2.3.1",
18 | "fastclick": "^1.0.6",
19 | "vue-lazyload": "1.0.3",
20 | "axios": "^0.16.1",
21 | "jsonp": "0.2.1",
22 | "better-scroll": "^0.1.15",
23 | "create-keyframe-animation": "^0.1.0",
24 | "js-base64": "^2.1.9",
25 | "lyric-parser": "^1.0.1",
26 | "good-storage": "^1.0.1"
27 | },
28 | "devDependencies": {
29 | "autoprefixer": "^6.7.2",
30 | "babel-core": "^6.22.1",
31 | "babel-eslint": "^7.1.1",
32 | "babel-loader": "^6.2.10",
33 | "babel-plugin-transform-runtime": "^6.22.0",
34 | "babel-preset-env": "^1.3.2",
35 | "babel-preset-stage-2": "^6.22.0",
36 | "babel-register": "^6.22.0",
37 | "babel-polyfill": "^6.2.0",
38 | "chalk": "^1.1.3",
39 | "connect-history-api-fallback": "^1.3.0",
40 | "copy-webpack-plugin": "^4.0.1",
41 | "css-loader": "^0.28.0",
42 | "eslint": "^3.19.0",
43 | "eslint-friendly-formatter": "^2.0.7",
44 | "eslint-loader": "^1.7.1",
45 | "eslint-plugin-html": "^2.0.0",
46 | "eslint-config-standard": "^6.2.1",
47 | "eslint-plugin-promise": "^3.4.0",
48 | "eslint-plugin-standard": "^2.0.1",
49 | "eventsource-polyfill": "^0.9.6",
50 | "express": "^4.14.1",
51 | "extract-text-webpack-plugin": "^2.0.0",
52 | "file-loader": "^0.11.1",
53 | "friendly-errors-webpack-plugin": "^1.1.3",
54 | "html-webpack-plugin": "^2.28.0",
55 | "http-proxy-middleware": "^0.17.3",
56 | "webpack-bundle-analyzer": "^2.2.1",
57 | "semver": "^5.3.0",
58 | "shelljs": "^0.7.6",
59 | "opn": "^4.0.2",
60 | "optimize-css-assets-webpack-plugin": "^1.3.0",
61 | "ora": "^1.2.0",
62 | "rimraf": "^2.6.0",
63 | "url-loader": "^0.5.8",
64 | "vue-loader": "^12.1.0",
65 | "vue-style-loader": "^3.0.1",
66 | "vue-template-compiler": "^2.3.3",
67 | "webpack": "^2.6.1",
68 | "webpack-dev-middleware": "^1.10.0",
69 | "webpack-hot-middleware": "^2.18.0",
70 | "webpack-merge": "^4.1.0",
71 | "stylus": "^0.54.5",
72 | "stylus-loader": "^2.1.1"
73 | },
74 | "engines": {
75 | "node": ">= 4.0.0",
76 | "npm": ">= 3.0.0"
77 | },
78 | "browserslist": [
79 | "> 1%",
80 | "last 2 versions",
81 | "not ie <= 8"
82 | ]
83 | }
84 |
--------------------------------------------------------------------------------
/src/components/singer/singer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
99 |
100 |
107 |
--------------------------------------------------------------------------------
/src/base/confirm/confirm.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
{{text}}
7 |
8 |
{{cancelBtnText}}
9 |
{{confirmBtnText}}
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
56 |
57 |
116 |
--------------------------------------------------------------------------------
/src/common/js/mixin.js:
--------------------------------------------------------------------------------
1 | import {mapGetters, mapMutations, mapActions} from 'vuex'
2 | import {playMode} from 'common/js/config'
3 | import {shuffle} from 'common/js/util'
4 |
5 | export const playlistMixin = {
6 | computed: {
7 | ...mapGetters([
8 | 'playlist'
9 | ])
10 | },
11 | mounted () {
12 | this.handlePlaylist(this.playlist)
13 | },
14 | activated () {
15 | this.handlePlaylist(this.playlist)
16 | },
17 | watch: {
18 | playlist (newVal) {
19 | this.handlePlaylist(newVal)
20 | }
21 | },
22 | methods: {
23 | handlePlaylist () {
24 | throw new Error('component must implement handlePlaylist method')
25 | }
26 | }
27 | }
28 |
29 | export const playerMixin = {
30 | computed: {
31 | iconMode () {
32 | return this.mode === playMode.sequence ? 'icon-sequence' : this.mode === playMode.loop ? 'icon-loop' : 'icon-random'
33 | },
34 | ...mapGetters([
35 | 'sequenceList',
36 | 'playlist',
37 | 'currentSong',
38 | 'mode',
39 | 'favoriteList'
40 | ])
41 | },
42 | methods: {
43 | changeMode () {
44 | const mode = (this.mode + 1) % 3
45 | this.setPlayMode(mode)
46 | let list = null
47 | if (mode === playMode.random) {
48 | list = shuffle(this.sequenceList)
49 | } else {
50 | list = this.sequenceList
51 | }
52 | this.resetCurrentIndex(list)
53 | this.setPlaylist(list)
54 | },
55 | resetCurrentIndex (list) {
56 | let index = list.findIndex((item) => {
57 | return item.id === this.currentSong.id
58 | })
59 | this.setCurrentIndex(index)
60 | },
61 | toggleFavorite (song) {
62 | if (this.isFavorite(song)) {
63 | this.deleteFavoriteList(song)
64 | } else {
65 | this.saveFavoriteList(song)
66 | }
67 | },
68 | getFavoriteIcon (song) {
69 | if (this.isFavorite(song)) {
70 | return 'icon-favorite'
71 | }
72 | return 'icon-not-favorite'
73 | },
74 | isFavorite (song) {
75 | const index = this.favoriteList.findIndex((item) => {
76 | return item.id === song.id
77 | })
78 | return index > -1
79 | },
80 | ...mapMutations({
81 | setPlayMode: 'SET_PLAY_MODE',
82 | setPlaylist: 'SET_PLAYLIST',
83 | setCurrentIndex: 'SET_CURRENT_INDEX',
84 | setPlayingState: 'SET_PLAYING_STATE'
85 | }),
86 | ...mapActions([
87 | 'saveFavoriteList',
88 | 'deleteFavoriteList'
89 | ])
90 | }
91 | }
92 |
93 | export const searchMixin = {
94 | data () {
95 | return {
96 | query: '',
97 | refreshDelay: 120
98 | }
99 | },
100 | computed: {
101 | ...mapGetters([
102 | 'searchHistory',
103 | 'playHistory'
104 | ])
105 | },
106 | methods: {
107 | onQueryChange (query) {
108 | this.query = query
109 | },
110 | blurInput () {
111 | this.$refs.searchBox.blur()
112 | },
113 | addQuery (query) {
114 | this.$refs.searchBox.setQuery(query)
115 | },
116 | saveSearch () {
117 | this.saveSearchHistory(this.query)
118 | },
119 | ...mapActions([
120 | 'saveSearchHistory',
121 | 'deleteSearchHistory'
122 | ])
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/src/components/rank/rank.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | -
6 |
7 |
![]()
8 |
9 |
10 | -
11 | {{index + 1}}
12 | {{song.singername}}-{{song.songname}}
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
74 |
75 |
118 |
--------------------------------------------------------------------------------
/src/base/progress-bar/progress-bar.vue:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
15 |
76 |
77 |
108 |
--------------------------------------------------------------------------------
/src/store/actions.js:
--------------------------------------------------------------------------------
1 | import * as types from './mutation-types'
2 | import {playMode} from 'common/js/config'
3 | import {shuffle} from 'common/js/util'
4 | import {saveSearch, deleteSearch, clearSearch, savePlay, saveFavorite, deleteFavorite} from 'common/js/cache'
5 |
6 | function findIndex (list, song) {
7 | return list.findIndex((item) => {
8 | return item.id === song.id
9 | })
10 | }
11 |
12 | export const selectPlay = function ({commit, state}, {list, index}) {
13 | commit(types.SET_SEQUENCE_LIST, list)
14 | if (state.mode === playMode.random) {
15 | let randomList = shuffle(list)
16 | commit(types.SET_PLAYLIST, randomList)
17 | index = findIndex(randomList, list[index])
18 | } else {
19 | commit(types.SET_PLAYLIST, list)
20 | }
21 | commit(types.SET_CURRENT_INDEX, index)
22 | commit(types.SET_FULL_SCREEN, true)
23 | commit(types.SET_PLAYING_STATE, true)
24 | }
25 |
26 | export const randomPlay = function ({commit}, {list}) {
27 | commit(types.SET_PLAY_MODE, playMode.random)
28 | commit(types.SET_SEQUENCE_LIST, list)
29 | let randomList = shuffle(list)
30 | commit(types.SET_PLAYLIST, randomList)
31 | commit(types.SET_CURRENT_INDEX, 0)
32 | commit(types.SET_FULL_SCREEN, true)
33 | commit(types.SET_PLAYING_STATE, true)
34 | }
35 |
36 | export const insertSong = function ({commit, state}, song) {
37 | let playlist = state.playlist.slice()
38 | let sequenceList = state.sequenceList.slice()
39 | let currentIndex = state.currentIndex
40 | let currentSong = playlist[currentIndex]
41 | let fpIndex = findIndex(playlist, song)
42 | currentIndex++
43 | playlist.splice(currentIndex, 0, song)
44 | if (fpIndex > -1) {
45 | if (currentIndex > fpIndex) {
46 | playlist.splice(fpIndex, 1)
47 | currentIndex--
48 | } else {
49 | playlist.splice(fpIndex + 1, 1)
50 | }
51 | }
52 |
53 | let currentSIndex = findIndex(sequenceList, currentSong) + 1
54 | let fsIndex = findIndex(sequenceList, song)
55 | sequenceList.splice(currentSIndex, 0, song)
56 |
57 | if (fsIndex > -1) {
58 | if (currentSIndex > fsIndex) {
59 | sequenceList.splice(fsIndex, 1)
60 | } else {
61 | sequenceList.splice(fsIndex + 1, 1)
62 | }
63 | }
64 |
65 | commit(types.SET_PLAYLIST, playlist)
66 | commit(types.SET_SEQUENCE_LIST, sequenceList)
67 | commit(types.SET_CURRENT_INDEX, currentIndex)
68 | commit(types.SET_FULL_SCREEN, true)
69 | commit(types.SET_PLAYING_STATE, true)
70 | }
71 |
72 | export const saveSearchHistory = function ({commit}, query) {
73 | commit(types.SET_SEARCH_HISTORY, saveSearch(query))
74 | }
75 |
76 | export const deleteSearchHistory = function ({commit}, query) {
77 | commit(types.SET_SEARCH_HISTORY, deleteSearch(query))
78 | }
79 |
80 | export const clearSearchHistory = function ({commit}) {
81 | commit(types.SET_SEARCH_HISTORY, clearSearch())
82 | }
83 |
84 | export const deleteSong = function ({commit, state}, song) {
85 | let sequenceList = state.sequenceList.slice()
86 | let playlist = state.playlist.slice()
87 | let currentIndex = state.currentIndex
88 | let pIndex = findIndex(playlist, song)
89 | playlist.splice(pIndex, 1)
90 | let sIndex = findIndex(sequenceList, song)
91 | sequenceList.splice(sIndex, 1)
92 | if ((currentIndex > pIndex || currentIndex === playlist.length) && currentIndex !== 0) {
93 | currentIndex--
94 | }
95 | commit(types.SET_PLAYLIST, playlist)
96 | commit(types.SET_SEQUENCE_LIST, sequenceList)
97 | commit(types.SET_CURRENT_INDEX, currentIndex)
98 | const playingState = playlist.length > 0
99 | commit(types.SET_PLAYING_STATE, playingState)
100 | }
101 |
102 | export const deleteSongList = function ({commit}) {
103 | commit(types.SET_PLAYLIST, [])
104 | commit(types.SET_SEQUENCE_LIST, [])
105 | commit(types.SET_CURRENT_INDEX, -1)
106 | commit(types.SET_PLAYING_STATE, false)
107 | }
108 |
109 | export const setPlayHistory = function ({commit}, song) {
110 | commit(types.SET_PLAY_HISTORY, savePlay(song))
111 | }
112 |
113 | export const saveFavoriteList = function ({commit}, song) {
114 | commit(types.SET_FAVORITE_LIST, saveFavorite(song))
115 | }
116 | export const deleteFavoriteList = function ({commit}, song) {
117 | commit(types.SET_FAVORITE_LIST, deleteFavorite(song))
118 | }
119 |
--------------------------------------------------------------------------------
/src/components/recommend/recommend.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
![]()
10 |
11 |
12 |
13 |
14 |
热门歌单推荐
15 |
16 | -
17 |
18 |
![]()
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
100 |
101 |
151 |
--------------------------------------------------------------------------------
/src/base/slider/slider.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
10 |
11 |
12 |
13 |
14 |
15 |
131 |
132 |
173 |
--------------------------------------------------------------------------------
/src/components/search/search.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
热门搜索
11 |
12 | -
13 | {{item.k}}
14 |
15 |
16 |
17 |
18 |
19 | 搜索历史
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
110 |
111 |
162 |
--------------------------------------------------------------------------------
/src/components/user-center/user-center.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | 随机播放全部
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
113 |
114 |
175 |
--------------------------------------------------------------------------------
/src/components/suggest/suggest.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 | -
12 |
13 |
14 |
15 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
162 |
163 |
195 |
--------------------------------------------------------------------------------
/src/components/add-song/add-song.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
18 |
19 |
20 |
21 |
22 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | 1首歌曲已添加到播放列表
39 |
40 |
41 |
42 |
43 |
44 |
45 |
130 |
131 |
194 |
--------------------------------------------------------------------------------
/src/components/music-list/music-list.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | 随机播放全部
12 |
13 |
14 |
15 |
16 |
17 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
148 |
149 |
238 |
--------------------------------------------------------------------------------
/src/base/listview/listview.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 | -
10 |
{{group.title}}
11 |
12 | -
14 |
15 | {{item.name}}
16 |
17 |
18 |
19 |
20 |
27 |
28 |
{{fixedTitle}}
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
162 |
163 |
230 |
--------------------------------------------------------------------------------
/src/components/playlist/playlist.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
12 |
16 |
17 |
18 |
19 | {{item.name}}
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | 添加歌曲到列表
33 |
34 |
35 |
36 | 关闭
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
134 |
135 |
240 |
--------------------------------------------------------------------------------
/src/components/player/player.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
![]()
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
23 |
24 |
25 |
26 |
![]()
27 |
28 |
29 |
30 |
{{playingLyric}}
31 |
32 |
33 |
34 |
35 |
36 |
{{noLrc}}
37 |
{{line.txt}}
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
{{format(currentTime)}}
52 |
57 |
{{format(currentSong.duration)}}
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
![]()
83 |
84 |
88 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
105 |
106 |
107 |
108 |
493 |
494 |
740 |
--------------------------------------------------------------------------------
/src/common/fonts/music-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------