├── static └── .gitkeep ├── .eslintignore ├── config ├── prod.env.js ├── dev.env.js └── index.js ├── src ├── common │ ├── js │ │ ├── config.js │ │ ├── singFactory.js │ │ ├── jsonp.js │ │ ├── utils.js │ │ ├── songFactory.js │ │ ├── dom.js │ │ ├── mixin.js │ │ └── cache.js │ ├── less │ │ ├── index.less │ │ ├── base.less │ │ ├── mixin.less │ │ ├── variable.less │ │ ├── reset.less │ │ └── icon.less │ ├── 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 │ ├── disc-detail │ │ └── disc-detail.vue │ ├── singer-detail │ │ └── singer-detail.vue │ ├── top-detail │ │ └── top-detail.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 │ ├── getLyric.js │ ├── rank.js │ ├── search.js │ ├── singer.js │ ├── recommend.js │ └── handlesongurl.js ├── store │ ├── state.js │ ├── index.js │ ├── mutation-types.js │ ├── getters.js │ ├── mutations.js │ └── actions.js ├── App.vue ├── main.js └── router │ └── index.js ├── .editorconfig ├── .gitignore ├── .babelrc ├── .postcssrc.js ├── index.html ├── .eslintrc.js ├── package.json ├── prod.server.js └── README.md /static/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /config/ 3 | /dist/ 4 | /*.js 5 | -------------------------------------------------------------------------------- /config/prod.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | NODE_ENV: '"production"' 4 | } 5 | -------------------------------------------------------------------------------- /src/common/js/config.js: -------------------------------------------------------------------------------- 1 | export const playMode = { 2 | sequence: 0, 3 | loop: 1, 4 | random: 2 5 | }; -------------------------------------------------------------------------------- /src/common/less/index.less: -------------------------------------------------------------------------------- 1 | @import './reset.less'; 2 | @import './base.less'; 3 | @import "./icon.less"; 4 | -------------------------------------------------------------------------------- /src/base/loading/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helloforrestworld/Vue-music-/HEAD/src/base/loading/loading.gif -------------------------------------------------------------------------------- /src/common/image/default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helloforrestworld/Vue-music-/HEAD/src/common/image/default.png -------------------------------------------------------------------------------- /src/base/song-list/first@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helloforrestworld/Vue-music-/HEAD/src/base/song-list/first@2x.png -------------------------------------------------------------------------------- /src/base/song-list/first@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helloforrestworld/Vue-music-/HEAD/src/base/song-list/first@3x.png -------------------------------------------------------------------------------- /src/base/song-list/second@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helloforrestworld/Vue-music-/HEAD/src/base/song-list/second@2x.png -------------------------------------------------------------------------------- /src/base/song-list/second@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helloforrestworld/Vue-music-/HEAD/src/base/song-list/second@3x.png -------------------------------------------------------------------------------- /src/base/song-list/third@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helloforrestworld/Vue-music-/HEAD/src/base/song-list/third@2x.png -------------------------------------------------------------------------------- /src/base/song-list/third@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helloforrestworld/Vue-music-/HEAD/src/base/song-list/third@3x.png -------------------------------------------------------------------------------- /src/common/fonts/music-icon.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helloforrestworld/Vue-music-/HEAD/src/common/fonts/music-icon.eot -------------------------------------------------------------------------------- /src/common/fonts/music-icon.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helloforrestworld/Vue-music-/HEAD/src/common/fonts/music-icon.ttf -------------------------------------------------------------------------------- /src/common/fonts/music-icon.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helloforrestworld/Vue-music-/HEAD/src/common/fonts/music-icon.woff -------------------------------------------------------------------------------- /src/base/no-result/no-result@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helloforrestworld/Vue-music-/HEAD/src/base/no-result/no-result@2x.png -------------------------------------------------------------------------------- /src/base/no-result/no-result@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helloforrestworld/Vue-music-/HEAD/src/base/no-result/no-result@3x.png -------------------------------------------------------------------------------- /src/components/m-header/logo@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helloforrestworld/Vue-music-/HEAD/src/components/m-header/logo@2x.png -------------------------------------------------------------------------------- /src/components/m-header/logo@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helloforrestworld/Vue-music-/HEAD/src/components/m-header/logo@3x.png -------------------------------------------------------------------------------- /config/dev.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const merge = require('webpack-merge') 3 | const prodEnv = require('./prod.env') 4 | 5 | module.exports = merge(prodEnv, { 6 | NODE_ENV: '"development"' 7 | }) 8 | -------------------------------------------------------------------------------- /.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 | .vscode 11 | *.suo 12 | *.ntvs* 13 | *.njsproj 14 | *.sln 15 | -------------------------------------------------------------------------------- /src/common/js/singFactory.js: -------------------------------------------------------------------------------- 1 | export class SingFactory { 2 | constructor({name, id}) { 3 | this.name = name; 4 | this.id = id; 5 | this.avatar = `https://y.gtimg.cn/music/photo_new/T001R300x300M000${id}.jpg?max_age=2592000`; 6 | } 7 | }; -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false, 5 | "targets": { 6 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] 7 | } 8 | }], 9 | "stage-2" 10 | ], 11 | "plugins": ["transform-vue-jsx", "transform-runtime"] 12 | } 13 | -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | "plugins": { 5 | "postcss-import": {}, 6 | "postcss-url": {}, 7 | // to edit target browsers: use "browserslist" field in package.json 8 | "autoprefixer": {} 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/api/config.js: -------------------------------------------------------------------------------- 1 | export const ERR_OK = 0; 2 | 3 | export const commonParams = { // 通用的请求参数 4 | g_tk: 1928093487, 5 | inCharset: 'utf-8', 6 | outCharset: 'utf-8', 7 | notice: 0, 8 | format: 'jsonp' 9 | }; 10 | 11 | export const option = { // Jsonp 选项参数 12 | param: 'jsonpCallback' 13 | }; -------------------------------------------------------------------------------- /src/common/less/base.less: -------------------------------------------------------------------------------- 1 | @import "./variable.less"; 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-ll; 10 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | vue-music 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/store/state.js: -------------------------------------------------------------------------------- 1 | import {playMode} from 'common/js/config'; 2 | import {loadSearch, loadPlay, loadFavorite} from 'common/js/cache'; 3 | const state = { 4 | singer: {}, 5 | playing: false, 6 | fullScreen: false, 7 | playlist: [], 8 | sequenceList: [], 9 | mode: playMode.sequence, 10 | currentIndex: -1, 11 | disc: {}, 12 | top: {}, 13 | searchHistory: loadSearch(), 14 | playHistory: loadPlay(), 15 | favoriteList: loadFavorite() 16 | }; 17 | export default state; -------------------------------------------------------------------------------- /src/api/getLyric.js: -------------------------------------------------------------------------------- 1 | import {commonParams} from 'api/config'; 2 | import axios from 'axios'; 3 | 4 | export function getLyric(mid) { 5 | let url = '/api/getLyric'; 6 | const data = Object.assign({}, commonParams, { 7 | songmid: mid, 8 | pcachetime: +new Date(), 9 | platform: 'yqq', 10 | hostUin: 0, 11 | needNewCode: 0, 12 | g_tk: 67232076, 13 | format: 'json' 14 | }); 15 | return axios.get(url, { 16 | params: data 17 | }).then((res) => { 18 | return Promise.resolve(res.data.lyric); 19 | }); 20 | }; -------------------------------------------------------------------------------- /src/common/less/mixin.less: -------------------------------------------------------------------------------- 1 | .bg-image(@url){ 2 | background-image: ~"url(@{url}@2x.png)"; 3 | @media (-webkit-min-device-pixel-ratio: 3),(min-device-pixel-ratio: 3){ 4 | background-image: ~"url(@{url}@3x.png)"; 5 | } 6 | } 7 | .no-wrap(){ 8 | text-overflow: ellipsis; 9 | overflow: hidden; 10 | white-space: nowrap; 11 | } 12 | .extend-click(){ 13 | position: relative; 14 | &:before{ 15 | content: ''; 16 | position: absolute; 17 | top: -10px; 18 | left: -10px; 19 | right: -10px; 20 | bottom: -10px; 21 | } 22 | } -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuex from 'vuex'; 3 | import state from './state'; 4 | import mutations from './mutations'; 5 | import * as getters from './getters'; 6 | import * as actions from './actions'; 7 | import createLogger from 'vuex/dist/logger'; 8 | const debug = process.env.NODE_ENV !== 'production'; 9 | 10 | Vue.use(Vuex); 11 | let store = new Vuex.Store({ 12 | state, 13 | mutations, 14 | actions, 15 | getters, 16 | strict: debug, 17 | plugins: debug ? [createLogger()] : [] 18 | }); 19 | export default store; -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 25 | 26 | 29 | -------------------------------------------------------------------------------- /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 | return new Promise((resolve, reject) => { 6 | originJsonp(url, option, (err, data) => { 7 | if (!err) { 8 | resolve(data); 9 | } else { 10 | reject(err); 11 | }; 12 | }); 13 | }); 14 | }; 15 | 16 | export function param(data) { 17 | let url = ''; 18 | for (let k in data) { 19 | let value = data[k] !== undefined ? data[k] : ''; 20 | url += `&${k}=${encodeURIComponent(value)}`; 21 | }; 22 | return url.substr(1); 23 | }; -------------------------------------------------------------------------------- /src/common/less/variable.less: -------------------------------------------------------------------------------- 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/utils.js: -------------------------------------------------------------------------------- 1 | 2 | function getRomdomInt(min, max) { // 生成min - max 的随机整数 3 | return Math.floor(Math.random() * (max - min + 1) + min); 4 | }; 5 | 6 | export function shuffle(arr) { // 随机打乱数组 7 | let _arr = arr.slice(); 8 | for (let i = 0; i < _arr.length; i++) { 9 | let randomI = getRomdomInt(0, i); 10 | [_arr[i], _arr[randomI]] = [_arr[randomI], _arr[i]]; 11 | }; 12 | return _arr; 13 | }; 14 | 15 | export function debounce(fn, delay) { // 截流函数 16 | let timer; 17 | return function(...args) { 18 | if (timer) { 19 | clearTimeout(timer); 20 | }; 21 | timer = setTimeout(() => { 22 | fn.apply(this, args); 23 | }, delay); 24 | }; 25 | }; -------------------------------------------------------------------------------- /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 = 'SET_TOP'; 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_FAVORITELIST = 'SET_FAVORITELIST'; -------------------------------------------------------------------------------- /src/base/loading/loading.vue: -------------------------------------------------------------------------------- 1 | 7 | 17 | 30 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill'; // es6一些API polyfill 2 | import Vue from 'vue'; 3 | import App from './App'; 4 | import router from './router'; 5 | import store from '@/store'; 6 | import 'common/less/index'; 7 | import fastclick from 'fastclick'; // 消除移动端点击延迟 8 | import VueLazyLoad from 'vue-lazyload'; 9 | 10 | /* eslint-disable no-unused-vars */ 11 | // import VConsole from 'vconsole'; 12 | // let console1 = new VConsole(); 13 | // 移动端console工具 14 | 15 | Vue.config.productionTip = false; 16 | 17 | fastclick.attach(document.body); 18 | 19 | Vue.use(VueLazyLoad, { 20 | loading: require('common/image/default.png') 21 | }); 22 | 23 | /* eslint-disable no-new */ 24 | new Vue({ 25 | el: '#app', 26 | router, 27 | store, 28 | render: h => h(App) 29 | }); 30 | -------------------------------------------------------------------------------- /src/api/rank.js: -------------------------------------------------------------------------------- 1 | import jsonp from 'common/js/jsonp'; 2 | import {option, commonParams} from 'api/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 | uin: 0, 9 | needNewCode: 1, 10 | platform: 'h5' 11 | }); 12 | 13 | return jsonp(url, data, option); 14 | }; 15 | 16 | export function getMusicList(topid) { 17 | const url = 'https://c.y.qq.com/v8/fcg-bin/fcg_v8_toplist_cp.fcg'; 18 | const data = Object.assign({}, commonParams, { 19 | topid, 20 | needNewCode: 1, 21 | uin: 0, 22 | tpl: 3, 23 | page: 'detail', 24 | type: 'top', 25 | platform: 'h5' 26 | }); 27 | 28 | return jsonp(url, data, option); 29 | }; -------------------------------------------------------------------------------- /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 currentSong = (state) => { 16 | return state.playlist[state.currentIndex] || {}; 17 | }; 18 | 19 | export const disc = state => state.disc; 20 | 21 | export const top = state => state.top; 22 | 23 | export const searchHistory = state => state.searchHistory; 24 | 25 | export const playHistory = state => state.playHistory; 26 | 27 | export const favoriteList = state => state.favoriteList; -------------------------------------------------------------------------------- /src/base/no-result/no-result.vue: -------------------------------------------------------------------------------- 1 | 7 | 21 | -------------------------------------------------------------------------------- /src/api/search.js: -------------------------------------------------------------------------------- 1 | import jsonp from 'common/js/jsonp'; 2 | import {commonParams, option} from './config'; 3 | 4 | export function getHotKey() { 5 | let url = 'https://c.y.qq.com/splcloud/fcgi-bin/gethotkey.fcg'; 6 | const data = Object.assign({}, commonParams, { 7 | uin: 0, 8 | needNewCode: 1, 9 | platform: 'h5' 10 | }); 11 | return jsonp(url, data, option); 12 | }; 13 | 14 | export function getSearchResult(query, page, zhida, perpage) { 15 | let url = 'https://c.y.qq.com/soso/fcgi-bin/search_for_qq_cp'; 16 | const data = Object.assign({}, commonParams, { 17 | w: query, 18 | p: page, 19 | perpage, 20 | n: perpage, 21 | catZhida: zhida ? 1 : 0, 22 | zhidaqu: 1, 23 | t: 0, 24 | flag: 1, 25 | ie: 'utf-8', 26 | sem: 1, 27 | aggr: 0, 28 | remoteplace: 'txt.mqq.all', 29 | uin: 0, 30 | needNewCode: 1, 31 | platform: 'h5' 32 | }); 33 | return jsonp(url, data, option); 34 | }; -------------------------------------------------------------------------------- /src/api/singer.js: -------------------------------------------------------------------------------- 1 | import jsonp from 'common/js/jsonp'; 2 | import {option, commonParams} from 'api/config'; 3 | 4 | export function getSingerList() { 5 | let 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 | hostUin: 0, 13 | needNewCode: 0, 14 | platform: 'yqq', 15 | g_tk: 1664029744 16 | }); 17 | return jsonp(url, data, option); 18 | }; 19 | 20 | export function getSingerDetail(id) { 21 | let url = 'https://c.y.qq.com/v8/fcg-bin/fcg_v8_singer_track_cp.fcg'; 22 | const data = Object.assign({}, commonParams, { 23 | g_tk: 5381, 24 | loginUin: 0, 25 | hostUin: 0, 26 | platform: 'yqq', 27 | needNewCode: 0, 28 | order: 'listen', 29 | begin: 0, 30 | num: 100, 31 | songstatus: 1, 32 | singermid: id 33 | }); 34 | return jsonp(url, data, option); 35 | } 36 | -------------------------------------------------------------------------------- /src/common/js/songFactory.js: -------------------------------------------------------------------------------- 1 | export class Song { 2 | constructor({id, mid, singer, name, album, duration, image, url}) { 3 | this.id = id; 4 | this.mid = mid; 5 | this.singer = singer; 6 | this.name = name; 7 | this.album = album; 8 | this.duration = duration; 9 | this.image = image; 10 | this.url = url; 11 | } 12 | }; 13 | 14 | export function createSong(musicData) { 15 | return new Song({ 16 | id: musicData.songid, 17 | mid: musicData.songmid, 18 | singer: _normalizeSinger(musicData.singer), 19 | name: musicData.songname, 20 | album: musicData.albumname, 21 | duration: musicData.interval, 22 | image: `https://y.gtimg.cn/music/photo_new/T002R300x300M000${musicData.albummid}.jpg?max_age=2592000`, 23 | url: `http://ws.stream.qqmusic.qq.com/${musicData.songid}.m4a?fromtag=46` 24 | }); 25 | }; 26 | 27 | function _normalizeSinger(singer) { 28 | if (!singer || !Array.isArray(singer)) return; 29 | let ret = []; 30 | singer.forEach((item) => { 31 | ret.push(item.name); 32 | }); 33 | return ret.join('/'); 34 | }; -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // https://eslint.org/docs/user-guide/configuring 2 | 3 | module.exports = { 4 | root: true, 5 | parserOptions: { 6 | parser: 'babel-eslint' 7 | }, 8 | env: { 9 | browser: true, 10 | }, 11 | extends: [ 12 | // https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention 13 | // consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules. 14 | 'plugin:vue/essential', 15 | // https://github.com/standard/standard/blob/master/docs/RULES-en.md 16 | 'standard' 17 | ], 18 | // required to lint *.vue files 19 | plugins: [ 20 | 'vue' 21 | ], 22 | // add your custom rules here 23 | rules: { 24 | // allow async-await 25 | 'generator-star-spacing': 'off', 26 | // allow debugger during development 27 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 28 | semi: ["error", "always"], 29 | 'indent': 0, 30 | "space-before-function-paren":0, 31 | 'eol-last':0, 32 | 'new-parens': 0, 33 | 'no-trailing-spaces': 0 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/store/mutations.js: -------------------------------------------------------------------------------- 1 | import * as types from './mutation-types'; 2 | const mutations = { 3 | [types.SET_SINGER](state, singer) { 4 | state.singer = singer; 5 | }, 6 | [types.SET_PLAYING_STATE](state, flag) { 7 | state.playing = flag; 8 | }, 9 | [types.SET_FULL_SCREEN](state, flag) { 10 | state.fullScreen = flag; 11 | }, 12 | [types.SET_PLAYLIST](state, list) { 13 | state.playlist = list; 14 | }, 15 | [types.SET_SEQUENCE_LIST](state, list) { 16 | state.sequenceList = list; 17 | }, 18 | [types.SET_PLAY_MODE](state, mode) { 19 | state.mode = mode; 20 | }, 21 | [types.SET_CURRENT_INDEX](state, index) { 22 | state.currentIndex = index; 23 | }, 24 | [types.SET_DISC](state, disc) { 25 | state.disc = disc; 26 | }, 27 | [types.SET_TOP](state, top) { 28 | state.top = top; 29 | }, 30 | [types.SET_SEARCH_HISTORY](state, history) { 31 | state.searchHistory = history; 32 | }, 33 | [types.SET_PLAY_HISTORY](state, history) { 34 | state.playHistory = history; 35 | }, 36 | [types.SET_FAVORITELIST](state, list) { 37 | state.favoriteList = list; 38 | } 39 | }; 40 | export default mutations; -------------------------------------------------------------------------------- /src/common/js/dom.js: -------------------------------------------------------------------------------- 1 | export function addClass(el, className) { 2 | if (hasClass(el, className)) { 3 | return; 4 | }; 5 | let classArr = el.className.split(' '); 6 | classArr.push(className); 7 | el.className = classArr.join(' '); 8 | }; 9 | 10 | export function hasClass(el, className) { 11 | let reg = new RegExp('\\b' + className + '\\b'); 12 | return reg.test(el.className); 13 | }; 14 | 15 | export function getData(el, key, val) { 16 | key = 'data-' + key; 17 | if (val === undefined) { 18 | return el.getAttribute(key); 19 | } else { 20 | el.setAttribute(key, val); 21 | }; 22 | }; 23 | 24 | let elementStyle = document.createElement('div').style; 25 | 26 | let vendor = (() => { 27 | let o = { 28 | webkit: 'webkitTransform', 29 | Moz: 'MozTransform', 30 | O: 'OTransform', 31 | ms: 'msTransform', 32 | standard: 'transform' 33 | }; 34 | for (let k in o) { 35 | if (elementStyle[o[k]] !== undefined) { 36 | return k; 37 | }; 38 | }; 39 | return false; 40 | })(); 41 | 42 | export function prefixStyle(style) { 43 | if (!vendor) return false; 44 | if (vendor === 'standard') { 45 | return style; 46 | }; 47 | return vendor + style.substr(0, 1).toUpperCase() + style.substr(1); 48 | }; 49 | -------------------------------------------------------------------------------- /src/base/top-tip/top-tip.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 36 | 37 | -------------------------------------------------------------------------------- /src/components/tab/tab.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 22 | 23 | -------------------------------------------------------------------------------- /src/base/switches/switches.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 28 | 29 | -------------------------------------------------------------------------------- /src/components/m-header/m-header.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 14 | 15 | -------------------------------------------------------------------------------- /src/api/recommend.js: -------------------------------------------------------------------------------- 1 | import jsonp from 'common/js/jsonp'; 2 | import {option, commonParams} from 'api/config'; 3 | import axios from 'axios'; 4 | 5 | export function getRecommend() { // 推荐页面数据获取 6 | let url = 'https://c.y.qq.com/musichall/fcgi-bin/fcg_yqqhomepagerecommend.fcg'; 7 | const data = Object.assign({}, commonParams, { 8 | platform: 'h5', 9 | uin: 0, 10 | needNewCode: 1 11 | }); 12 | return jsonp(url, data, option); 13 | }; 14 | 15 | export function getDiscList() { // 歌单列表数据获取 16 | const data = Object.assign({}, commonParams, { 17 | platform: 'yqq', 18 | hostUin: 0, 19 | sin: 0, 20 | ein: 29, 21 | sortId: 5, 22 | needNewCode: 0, 23 | categoryId: 10000000, 24 | rnd: Math.random(), 25 | format: 'json' 26 | }); 27 | return axios.get('/api/getDiscList', { 28 | params: data 29 | }).then((res) => { 30 | return Promise.resolve(res.data); 31 | }); 32 | }; 33 | 34 | export function getSongList(disstid) { 35 | let url = '/api/getCdInfo'; 36 | 37 | const data = Object.assign({}, commonParams, { 38 | disstid, 39 | type: 1, 40 | json: 1, 41 | utf8: 1, 42 | onlysong: 0, 43 | platform: 'yqq', 44 | hostUin: 0, 45 | needNewCode: 0 46 | }); 47 | 48 | return axios.get(url, { 49 | params: data 50 | }).then((res) => { 51 | return Promise.resolve(res.data); 52 | }); 53 | }; 54 | -------------------------------------------------------------------------------- /src/base/progress-circle/progress-circle.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 35 | 36 | -------------------------------------------------------------------------------- /src/common/less/reset.less: -------------------------------------------------------------------------------- 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 | 13 | 14 | 32 | 33 | -------------------------------------------------------------------------------- /src/components/disc-detail/disc-detail.vue: -------------------------------------------------------------------------------- 1 | 6 | 54 | -------------------------------------------------------------------------------- /src/components/singer-detail/singer-detail.vue: -------------------------------------------------------------------------------- 1 | 6 | 52 | -------------------------------------------------------------------------------- /src/base/search-box/search-box.vue: -------------------------------------------------------------------------------- 1 | 8 | 41 | -------------------------------------------------------------------------------- /src/components/top-detail/top-detail.vue: -------------------------------------------------------------------------------- 1 | 6 | 63 | -------------------------------------------------------------------------------- /src/common/less/icon.less: -------------------------------------------------------------------------------- 1 | @font-face{ 2 | font-family: 'music-icon'; 3 | src: url('../fonts/music-icon.eot?2qevqt'); 4 | src: url('../fonts/music-icon.eot?2qevqt#iefix') format('embedded-opentype'), 5 | url('../fonts/music-icon.ttf?2qevqt') format('truetype'), 6 | url('../fonts/music-icon.woff?2qevqt') format('woff'), 7 | url('../fonts/music-icon.svg?2qevqt#music-icon') format('svg'); 8 | font-weight: normal; 9 | font-style: normal; 10 | } 11 | [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/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Router from 'vue-router'; 3 | 4 | Vue.use(Router); 5 | const Recommend = (resolve) => { 6 | import('components/recommend/recommend').then((module) => { 7 | resolve(module); 8 | }); 9 | }; 10 | const DiscDetail = (resolve) => { 11 | import('components/disc-detail/disc-detail').then((module) => { 12 | resolve(module); 13 | }); 14 | }; 15 | const Singer = (resolve) => { 16 | import('components/singer/singer').then((module) => { 17 | resolve(module); 18 | }); 19 | }; 20 | const SingerDetail = (resolve) => { 21 | import('components/singer-detail/singer-detail').then((module) => { 22 | resolve(module); 23 | }); 24 | }; 25 | const Rank = (resolve) => { 26 | import('components/rank/rank').then((module) => { 27 | resolve(module); 28 | }); 29 | }; 30 | const topDetail = (resolve) => { 31 | import('components/top-detail/top-detail').then((module) => { 32 | resolve(module); 33 | }); 34 | }; 35 | const Search = (resolve) => { 36 | import('components/search/search').then((module) => { 37 | resolve(module); 38 | }); 39 | }; 40 | const UserCenter = (resolve) => { 41 | import('components/user-center/user-center').then((module) => { 42 | resolve(module); 43 | }); 44 | }; 45 | 46 | export default new Router({ 47 | mode: 'history', 48 | routes: [ 49 | { 50 | path: '/', 51 | redirect: '/recommend' 52 | }, 53 | { 54 | path: '/recommend', 55 | component: Recommend, 56 | children: [ 57 | { 58 | path: ':id', 59 | component: DiscDetail 60 | } 61 | ] 62 | }, 63 | { 64 | path: '/singer', 65 | component: Singer, 66 | children: [ 67 | { 68 | path: ':id', 69 | component: SingerDetail 70 | } 71 | ] 72 | }, 73 | { 74 | path: '/rank', 75 | component: Rank, 76 | children: [ 77 | { 78 | path: ':id', 79 | component: topDetail 80 | } 81 | ] 82 | }, 83 | { 84 | path: '/search', 85 | component: Search, 86 | children: [ 87 | { 88 | path: ':id', 89 | component: SingerDetail 90 | } 91 | ] 92 | }, 93 | { 94 | path: '/user', 95 | component: UserCenter 96 | } 97 | ] 98 | }); 99 | -------------------------------------------------------------------------------- /src/components/singer/singer.vue: -------------------------------------------------------------------------------- 1 | 7 | 88 | -------------------------------------------------------------------------------- /src/base/song-list/song-list.vue: -------------------------------------------------------------------------------- 1 | 16 | 51 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | // Template version: 1.3.1 3 | // see http://vuejs-templates.github.io/webpack for documentation. 4 | 5 | const path = require('path') 6 | 7 | module.exports = { 8 | dev: { 9 | 10 | // Paths 11 | assetsSubDirectory: 'static', 12 | assetsPublicPath: '/', 13 | proxyTable: {}, 14 | 15 | // Various Dev Server settings 16 | host: '0.0.0.0', // can be overwritten by process.env.HOST 17 | port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined 18 | autoOpenBrowser: false, 19 | errorOverlay: true, 20 | notifyOnErrors: true, 21 | poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions- 22 | 23 | // Use Eslint Loader? 24 | // If true, your code will be linted during bundling and 25 | // linting errors and warnings will be shown in the console. 26 | useEslint: true, 27 | // If true, eslint errors and warnings will also be shown in the error overlay 28 | // in the browser. 29 | showEslintErrorsInOverlay: false, 30 | 31 | /** 32 | * Source Maps 33 | */ 34 | 35 | // https://webpack.js.org/configuration/devtool/#development 36 | devtool: 'cheap-module-eval-source-map', 37 | 38 | // If you have problems debugging vue-files in devtools, 39 | // set this to false - it *may* help 40 | // https://vue-loader.vuejs.org/en/options.html#cachebusting 41 | cacheBusting: true, 42 | 43 | cssSourceMap: true 44 | }, 45 | 46 | build: { 47 | // Template for index.html 48 | index: path.resolve(__dirname, '../dist/index.html'), 49 | port: 9090, 50 | // Paths 51 | assetsRoot: path.resolve(__dirname, '../dist'), 52 | assetsSubDirectory: 'static', 53 | assetsPublicPath: '/', 54 | 55 | /** 56 | * Source Maps 57 | */ 58 | 59 | productionSourceMap: true, 60 | // https://webpack.js.org/configuration/devtool/#production 61 | devtool: '#source-map', 62 | 63 | // Gzip off by default as many popular static hosts such as 64 | // Surge or Netlify already gzip all static assets for you. 65 | // Before setting to `true`, make sure to: 66 | // npm install --save-dev compression-webpack-plugin 67 | productionGzip: false, 68 | productionGzipExtensions: ['js', 'css'], 69 | 70 | // Run the build command with an extra argument to 71 | // View the bundle analyzer report after build finishes: 72 | // `npm run build --report` 73 | // Set to `true` or `false` to always turn it on or off 74 | bundleAnalyzerReport: process.env.npm_config_report 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/base/scroll/scroll.vue: -------------------------------------------------------------------------------- 1 | 6 | 99 | -------------------------------------------------------------------------------- /src/api/handlesongurl.js: -------------------------------------------------------------------------------- 1 | // 此模块用于获取正确的歌曲url地址 2 | import {ERR_OK, commonParams} from 'api/config'; 3 | import axios from 'axios'; 4 | let _uid = ''; 5 | 6 | export function processSongsUrl(songs) { 7 | if (!songs.length) { 8 | return Promise.resolve(songs); 9 | }; 10 | return getSongsUrl(songs).then((res) => { 11 | if (res.code === ERR_OK) { 12 | let midUrlInfo = res.url_mid.data.midurlinfo; 13 | midUrlInfo.forEach((info, index) => { 14 | let song = songs[index]; 15 | song.url = `http://dl.stream.qqmusic.qq.com/${info.purl}`; 16 | }); 17 | }; 18 | return songs; 19 | }); 20 | }; 21 | 22 | function getSongsUrl(songs) { 23 | const url = '/api/getPurlUrl'; 24 | 25 | let mids = []; 26 | let types = []; 27 | 28 | songs.forEach((song) => { 29 | mids.push(song.mid); 30 | types.push(0); 31 | }); 32 | 33 | const urlMid = genUrlMid(mids, types); 34 | 35 | const data = Object.assign({}, commonParams, { 36 | g_tk: 5381, 37 | format: 'json', 38 | platform: 'h5', 39 | needNewCode: 1, 40 | uin: 0 41 | }); 42 | 43 | return new Promise((resolve, reject) => { 44 | let tryTime = 3; 45 | 46 | function request() { 47 | return axios.post(url, { 48 | comm: data, 49 | url_mid: urlMid 50 | }).then((response) => { 51 | const res = response.data; 52 | if (res.code === ERR_OK) { 53 | let urlMid = res.url_mid; 54 | if (urlMid && urlMid.code === ERR_OK) { 55 | const info = urlMid.data.midurlinfo[0]; 56 | if (info && info.purl) { 57 | resolve(res); 58 | } else { 59 | retry(); 60 | } 61 | } else { 62 | retry(); 63 | } 64 | } else { 65 | retry(); 66 | } 67 | }); 68 | } 69 | 70 | function retry() { 71 | if (--tryTime >= 0) { 72 | request(); 73 | } else { 74 | reject(new Error('Can not get the songs url')); 75 | } 76 | } 77 | 78 | request(); 79 | }); 80 | } 81 | 82 | function genUrlMid(mids, types) { 83 | const guid = getUid(); 84 | return { 85 | module: 'vkey.GetVkeyServer', 86 | method: 'CgiGetVkey', 87 | param: { 88 | guid, 89 | songmid: mids, 90 | songtype: types, 91 | uin: '0', 92 | loginflag: 0, 93 | platform: '23' 94 | } 95 | }; 96 | } 97 | 98 | export function getUid() { 99 | if (_uid) { 100 | return _uid; 101 | }; 102 | if (!_uid) { 103 | const t = (new Date).getUTCMilliseconds(); 104 | _uid = '' + Math.round(2147483647 * Math.random()) * t % 1e10; 105 | }; 106 | return _uid; 107 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-music", 3 | "version": "1.0.0", 4 | "description": "仿QQ音乐", 5 | "author": "helloforrestworld ", 6 | "private": true, 7 | "scripts": { 8 | "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js", 9 | "start": "npm run dev", 10 | "lint": "eslint --ext .js,.vue src", 11 | "build": "node build/build.js" 12 | }, 13 | "dependencies": { 14 | "axios": "^0.18.0", 15 | "babel-runtime": "^6.0.0", 16 | "better-scroll": "^1.9.1", 17 | "compression": "^1.7.2", 18 | "create-keyframe-animation": "^0.1.0", 19 | "express": "^4.16.3", 20 | "fastclick": "^1.0.6", 21 | "js-base64": "^2.4.3", 22 | "jsonp": "^0.2.1", 23 | "lyric-parser": "^1.0.1", 24 | "vconsole": "^3.2.0", 25 | "vue": "^2.5.2", 26 | "vue-lazyload": "1.0.3", 27 | "vue-router": "^3.0.1", 28 | "vuex": "^3.0.1" 29 | }, 30 | "devDependencies": { 31 | "autoprefixer": "^7.1.2", 32 | "babel-core": "^6.22.1", 33 | "babel-eslint": "^8.2.1", 34 | "babel-helper-vue-jsx-merge-props": "^2.0.3", 35 | "babel-loader": "^7.1.1", 36 | "babel-plugin-syntax-jsx": "^6.18.0", 37 | "babel-plugin-transform-runtime": "^6.22.0", 38 | "babel-plugin-transform-vue-jsx": "^3.5.0", 39 | "babel-preset-env": "^1.3.2", 40 | "babel-preset-stage-2": "^6.22.0", 41 | "babel-polyfill": "^6.2.0", 42 | "chalk": "^2.0.1", 43 | "copy-webpack-plugin": "^4.0.1", 44 | "css-loader": "^0.28.0", 45 | "eslint": "^4.15.0", 46 | "eslint-config-standard": "^10.2.1", 47 | "eslint-friendly-formatter": "^3.0.0", 48 | "eslint-loader": "^1.7.1", 49 | "eslint-plugin-import": "^2.7.0", 50 | "eslint-plugin-node": "^5.2.0", 51 | "eslint-plugin-promise": "^3.4.0", 52 | "eslint-plugin-standard": "^3.0.1", 53 | "eslint-plugin-vue": "^4.0.0", 54 | "extract-text-webpack-plugin": "^3.0.0", 55 | "file-loader": "^1.1.4", 56 | "friendly-errors-webpack-plugin": "^1.6.1", 57 | "html-webpack-plugin": "^2.30.1", 58 | "less": "^3.0.1", 59 | "less-loader": "^4.1.0", 60 | "node-notifier": "^5.1.2", 61 | "optimize-css-assets-webpack-plugin": "^3.2.0", 62 | "ora": "^1.2.0", 63 | "portfinder": "^1.0.13", 64 | "postcss-import": "^11.0.0", 65 | "postcss-loader": "^2.0.8", 66 | "postcss-url": "^7.2.1", 67 | "rimraf": "^2.6.0", 68 | "semver": "^5.3.0", 69 | "shelljs": "^0.7.6", 70 | "uglifyjs-webpack-plugin": "^1.1.1", 71 | "url-loader": "^0.5.8", 72 | "vue-loader": "^13.3.0", 73 | "vue-style-loader": "^3.0.1", 74 | "vue-template-compiler": "^2.5.2", 75 | "webpack": "^3.6.0", 76 | "webpack-bundle-analyzer": "^2.9.0", 77 | "webpack-dev-server": "^2.9.1", 78 | "webpack-merge": "^4.1.0" 79 | }, 80 | "engines": { 81 | "node": ">= 6.0.0", 82 | "npm": ">= 3.0.0" 83 | }, 84 | "browserslist": [ 85 | "> 1%", 86 | "last 2 versions", 87 | "not ie <= 8" 88 | ] 89 | } 90 | -------------------------------------------------------------------------------- /prod.server.js: -------------------------------------------------------------------------------- 1 | var express = require('express') 2 | var compression = require('compression') 3 | var config = require('./config/index') 4 | var axios = require('axios') 5 | const bodyParser = require('body-parser') 6 | 7 | var port = process.env.PORT || config.build.port 8 | 9 | var app = express() 10 | 11 | var apiRoutes = express.Router() 12 | 13 | // 歌单列表代理 14 | app.get('/api/getDiscList', function(req, res) { 15 | const url = 'https://c.y.qq.com/splcloud/fcgi-bin/fcg_get_diss_by_tag.fcg'; 16 | axios.get(url, { 17 | headers: { 18 | referer: 'https://c.y.qq.com', 19 | host: 'c.y.qq.com' 20 | }, 21 | params: req.query 22 | }).then((response) => { 23 | res.json(response.data) 24 | }).catch((err) => { 25 | console.log(err) 26 | }) 27 | }) 28 | 29 | // 音乐文件url处理信息获取 30 | app.post('/api/getPurlUrl', bodyParser.json(), function (req, res) { 31 | const url = 'https://u.y.qq.com/cgi-bin/musicu.fcg' 32 | axios.post(url, req.body, { 33 | headers: { 34 | referer: 'https://y.qq.com/', 35 | origin: 'https://y.qq.com', 36 | 'Content-type': 'application/x-www-form-urlencoded' 37 | } 38 | }).then((response) => { 39 | res.json(response.data) 40 | }).catch((e) => { 41 | console.log(e) 42 | }) 43 | }) 44 | 45 | // 歌词获取 46 | app.get('/api/getLyric', function(req, res) { 47 | const url = 'https://c.y.qq.com/lyric/fcgi-bin/fcg_query_lyric_new.fcg'; 48 | axios.get(url, { 49 | headers: { 50 | referer: 'https://c.y.qq.com', 51 | host: 'c.y.qq.com' 52 | }, 53 | params: req.query 54 | }).then((response) => { 55 | let ret = response.data 56 | if (typeof ret === 'string') { 57 | let reg = /^\w+\(({[^()]+})\)$/ 58 | let matches = ret.match(reg) 59 | if (matches) { 60 | ret = JSON.parse(matches[1]) 61 | } 62 | } 63 | res.json(ret) 64 | }).catch((err) => { 65 | console.log(err) 66 | }) 67 | }) 68 | 69 | // 歌单详情数据 70 | app.get('/api/getCdInfo', function (req, res) { 71 | const url = 'https://c.y.qq.com/qzone/fcg-bin/fcg_ucc_getcdinfo_byids_cp.fcg' 72 | axios.get(url, { 73 | headers: { 74 | referer: 'https://c.y.qq.com/', 75 | host: 'c.y.qq.com' 76 | }, 77 | params: req.query 78 | }).then((response) => { 79 | let ret = response.data 80 | if (typeof ret === 'string') { 81 | const reg = /^\w+\(({.+})\)$/ 82 | const matches = ret.match(reg) 83 | if (matches) { 84 | ret = JSON.parse(matches[1]) 85 | } 86 | } 87 | res.json(ret) 88 | }).catch((e) => { 89 | console.log(e) 90 | }) 91 | }) 92 | 93 | app.use('/api', apiRoutes) 94 | 95 | app.use(compression()) // response压缩 96 | 97 | app.use(express.static('./dist')) // 静态资源目录 98 | 99 | module.exports = app.listen(port, function(err) { 100 | if (err) { 101 | console.log(err) 102 | return 103 | } 104 | console.log('Listening at http://localhost:'+ port + '\n') 105 | }) -------------------------------------------------------------------------------- /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/utils'; 4 | 5 | export const playlistMixin = { // 迷你播放器弹出后 调整列表位置 6 | computed: { 7 | ...mapGetters(['playlist']) 8 | }, 9 | mounted() { 10 | this.playlistHandler(this.playlist); 11 | }, 12 | activated() { 13 | this.playlistHandler(this.playlist); 14 | }, 15 | watch: { 16 | playlist(newPlaylist) { 17 | this.playlistHandler(newPlaylist); 18 | } 19 | }, 20 | methods: { 21 | playlistHandler(playlist) { 22 | throw new Error('a playlistHandler must be implyed [mixin.js]'); 23 | } 24 | } 25 | }; 26 | 27 | export const playerMixin = { // player playlist组件 共用 28 | computed: { 29 | iconMode() { 30 | return this.mode === playMode.sequence ? 'icon-sequence' : this.mode === playMode.loop ? 'icon-loop' : 'icon-random'; 31 | }, 32 | ...mapGetters([ 33 | 'mode', 34 | 'sequenceList', 35 | 'favoriteList' 36 | ]) 37 | }, 38 | methods: { 39 | changeMode() { // 切换播放模式 40 | let mode = (this.mode + 1) % 3; 41 | let list = this.sequenceList; 42 | if (mode === playMode.random) { 43 | list = shuffle(list); 44 | }; 45 | let index = this.findIndex(list); 46 | this.setPlaylist(list); 47 | this.setCurrentIndex(index); 48 | this.setPlayMode(mode); 49 | }, 50 | findIndex(list) { 51 | let index = list.findIndex((item) => { 52 | return this.currentSong.id === item.id; 53 | }); 54 | return index; 55 | }, 56 | getFavoriteIcon(song) { 57 | if (this.isFavorite(song)) { 58 | return 'icon-favorite'; 59 | } else { 60 | return 'icon-not-favorite'; 61 | } 62 | }, 63 | toggleFavorite(song) { 64 | if (this.isFavorite(song)) { 65 | this.deleteFavoriteList(song); 66 | } else { 67 | this.saveFavoriteList(song); 68 | } 69 | }, 70 | isFavorite(song) { 71 | let index = this.favoriteList.findIndex((item) => { 72 | return song.id === item.id; 73 | }); 74 | return index > -1; 75 | }, 76 | ...mapMutations({ 77 | setPlaylist: 'SET_PLAYLIST', 78 | setCurrentIndex: 'SET_CURRENT_INDEX', 79 | setPlayMode: 'SET_PLAY_MODE' 80 | }), 81 | ...mapActions([ 82 | 'saveFavoriteList', 83 | 'deleteFavoriteList' 84 | ]) 85 | } 86 | }; 87 | 88 | export const searchMixin = { // add-song搜索部分 和 search组件 共用 89 | data() { 90 | return { 91 | query: '' 92 | }; 93 | }, 94 | methods: { 95 | onQueryChange(query) { // 搜索框query变化 96 | this.query = query; 97 | }, 98 | addKey(key) { // 填充 搜索框 99 | this.$refs.searchBox.fillInput(key); 100 | }, 101 | blurInput() { // 滚动前搜索框失去焦点 收起键盘 102 | this.$refs.searchBox.blur(); 103 | }, 104 | deleteOneSearch(item) { // 删除某条搜索记录 105 | this.deleteSearchHistory(item); 106 | }, 107 | saveSearch() { // 保存搜索历史 108 | this.saveSearchHistory(this.query); 109 | }, 110 | ...mapActions([ 111 | 'saveSearchHistory', 112 | 'deleteSearchHistory' 113 | ]) 114 | }, 115 | computed: { 116 | ...mapGetters(['searchHistory']) 117 | } 118 | }; 119 | -------------------------------------------------------------------------------- /src/base/confirm/confirm.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 56 | 57 | -------------------------------------------------------------------------------- /src/components/rank/rank.vue: -------------------------------------------------------------------------------- 1 | 24 | 71 | -------------------------------------------------------------------------------- /src/base/progress-bar/progress-bar.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 78 | 79 | -------------------------------------------------------------------------------- /src/base/slider/slider.vue: -------------------------------------------------------------------------------- 1 | 11 | 107 | -------------------------------------------------------------------------------- /src/store/actions.js: -------------------------------------------------------------------------------- 1 | import * as types from './mutation-types'; 2 | import {playMode} from 'common/js/config'; 3 | import {shuffle} from 'common/js/utils'; 4 | import {saveSearch, deleteSearch, clearSearch, savePlay, saveFavorite, deleteFavorite} from 'common/js/cache'; 5 | 6 | export const selectPlay = ({commit, state}, {list, index}) => { // 播放整个列表 7 | let playlist = list; 8 | if (state.mode === playMode.random) { 9 | playlist = shuffle(list); 10 | index = findIndex(playlist, list[index]); 11 | }; 12 | commit(types.SET_SEQUENCE_LIST, list); 13 | commit(types.SET_PLAYLIST, playlist); 14 | commit(types.SET_CURRENT_INDEX, index); 15 | commit(types.SET_FULL_SCREEN, true); 16 | commit(types.SET_PLAYING_STATE, true); 17 | }; 18 | 19 | export const selectAllRandom = ({commit}, {list}) => { // 随机播放全部 20 | commit(types.SET_PLAY_MODE, playMode.random); 21 | commit(types.SET_SEQUENCE_LIST, list); 22 | commit(types.SET_PLAYLIST, shuffle(list)); 23 | commit(types.SET_CURRENT_INDEX, 0); 24 | commit(types.SET_FULL_SCREEN, true); 25 | commit(types.SET_PLAYING_STATE, true); 26 | }; 27 | 28 | export const insertSong = ({commit, state}, song) => { // 在播放列表中插入一首歌曲 29 | let playlist = state.playlist.slice(); 30 | let sequenceList = state.sequenceList.slice(); 31 | let currentIndex = state.currentIndex; 32 | let currentSong = playlist[currentIndex]; 33 | let fpIndex = findIndex(playlist, song); 34 | currentIndex++; 35 | playlist.splice(currentIndex, 0, song); 36 | if (fpIndex > -1) { // 有相同的歌曲 37 | if (fpIndex < currentIndex) { 38 | playlist.splice(fpIndex, 1); 39 | currentIndex--; 40 | } else { // 前面插入了一首歌曲 索引位置需要调整 41 | playlist.splice(fpIndex + 1, 1); 42 | } 43 | } 44 | let fsIndex = findIndex(sequenceList, song); 45 | let currentSIndex = findIndex(sequenceList, currentSong); 46 | currentSIndex++; 47 | sequenceList.splice(currentSIndex, 0, song); 48 | if (fsIndex > -1) { 49 | if (fsIndex < currentSIndex) { 50 | sequenceList.splice(fsIndex, 1); 51 | } else { 52 | sequenceList.splice(fsIndex + 1, 1); 53 | } 54 | } 55 | commit(types.SET_PLAYLIST, playlist); 56 | commit(types.SET_SEQUENCE_LIST, sequenceList); 57 | commit(types.SET_CURRENT_INDEX, currentIndex); 58 | commit(types.SET_FULL_SCREEN, true); 59 | commit(types.SET_PLAYING_STATE, true); 60 | }; 61 | 62 | export const saveSearchHistory = ({commit}, query) => { 63 | commit(types.SET_SEARCH_HISTORY, saveSearch(query)); 64 | }; 65 | 66 | export const deleteSearchHistory = ({commit}, query) => { 67 | commit(types.SET_SEARCH_HISTORY, deleteSearch(query)); 68 | }; 69 | 70 | export const clearSearchHistory = ({commit}) => { 71 | commit(types.SET_SEARCH_HISTORY, clearSearch()); 72 | }; 73 | 74 | export const deleteSong = ({commit, state}, song) => { 75 | let playlist = state.playlist.slice(); 76 | let sequenceList = state.sequenceList.slice(); 77 | let currentIndex = state.currentIndex; 78 | 79 | let pIndex = findIndex(playlist, song); 80 | let sIndex = findIndex(sequenceList, song); 81 | 82 | playlist.splice(pIndex, 1); 83 | sequenceList.splice(sIndex, 1); 84 | 85 | if (currentIndex > pIndex || currentIndex === playlist.length) { 86 | currentIndex--; 87 | }; 88 | 89 | commit(types.SET_SEQUENCE_LIST, sequenceList); 90 | commit(types.SET_PLAYLIST, playlist); 91 | commit(types.SET_CURRENT_INDEX, currentIndex); 92 | 93 | let playingState = playlist.length > 0; 94 | commit(types.SET_PLAYING_STATE, playingState); 95 | commit(types.SET_PLAYING_STATE, playingState); 96 | }; 97 | 98 | export const clearPlaylist = ({commit}) => { 99 | commit(types.SET_SEQUENCE_LIST, []); 100 | commit(types.SET_PLAYLIST, []); 101 | commit(types.SET_CURRENT_INDEX, -1); 102 | commit(types.SET_PLAYING_STATE, false); 103 | }; 104 | 105 | export const savePlayHistory = ({commit}, history) => { 106 | commit(types.SET_PLAY_HISTORY, savePlay(history)); 107 | }; 108 | 109 | export const saveFavoriteList = ({commit}, song) => { 110 | commit(types.SET_FAVORITELIST, saveFavorite(song)); 111 | }; 112 | 113 | export const deleteFavoriteList = ({commit}, song) => { 114 | commit(types.SET_FAVORITELIST, deleteFavorite(song)); 115 | }; 116 | 117 | function findIndex(list, tar) { 118 | let index = list.findIndex((item) => { 119 | return tar.id === item.id; 120 | }); 121 | return index; 122 | }; -------------------------------------------------------------------------------- /src/common/js/cache.js: -------------------------------------------------------------------------------- 1 | // localstroage相关 2 | function saveLoadcal(opts) { 3 | let {module, id, value} = opts; 4 | module = '__' + module + '__'; 5 | let tarModule = window.localStorage[module]; 6 | if (!tarModule) { 7 | tarModule = {}; 8 | } else { 9 | tarModule = JSON.parse(tarModule); 10 | }; 11 | tarModule[id] = value; 12 | window.localStorage[module] = JSON.stringify(tarModule); 13 | }; 14 | 15 | function loadLoadcal(opts) { 16 | let {module, id, def} = opts; 17 | module = '__' + module + '__'; 18 | let tarModule = window.localStorage[module]; 19 | if (!tarModule) { 20 | return def; 21 | } else { 22 | tarModule = JSON.parse(tarModule); 23 | }; 24 | if (!tarModule[id]) { 25 | return def; 26 | }; 27 | return tarModule[id]; 28 | }; 29 | 30 | // 数组插入数据 31 | // 需求 : 1.去除相同的 2.超出限定长度移除最后一个 32 | const SEARCH_MAX_LEN = 15; 33 | const PLAY_MAX_LEN = 200; 34 | const FAVORITE_MAX_LEN = 200; 35 | 36 | function insertArray(arr, value, compare, maxLen) { 37 | let findIndex = arr.findIndex(compare); 38 | if (findIndex === 0) { 39 | return; 40 | }; 41 | if (findIndex > 0) { 42 | arr.splice(findIndex, 1); 43 | }; 44 | arr.unshift(value); 45 | if (maxLen && arr.length > maxLen) { 46 | arr.pop(); 47 | }; 48 | }; 49 | 50 | // 从数组里面删除一个 51 | function deleteFromArray(arr, value, compare) { 52 | let findIndex = arr.findIndex(compare); 53 | if (findIndex > -1) { 54 | arr.splice(findIndex, 1); 55 | }; 56 | } 57 | 58 | // 保存搜索历史 59 | export function saveSearch(value) { 60 | let ret = loadLoadcal({ 61 | module: 'chickmusic', 62 | id: 'searchHistory', 63 | def: [] 64 | }); 65 | insertArray(ret, value, (item) => { 66 | return item === value; 67 | }, SEARCH_MAX_LEN); 68 | saveLoadcal({ 69 | module: 'chickmusic', 70 | id: 'searchHistory', 71 | value: ret 72 | }); 73 | return ret; 74 | }; 75 | 76 | // 读取搜索历史 77 | export function loadSearch() { 78 | return loadLoadcal({ 79 | module: 'chickmusic', 80 | id: 'searchHistory', 81 | def: [] 82 | }); 83 | }; 84 | 85 | // 删除某条搜索历史 86 | export function deleteSearch(value) { 87 | let ret = loadLoadcal({ 88 | module: 'chickmusic', 89 | id: 'searchHistory', 90 | def: [] 91 | }); 92 | deleteFromArray(ret, value, (item) => { 93 | return item === value; 94 | }); 95 | saveLoadcal({ 96 | module: 'chickmusic', 97 | id: 'searchHistory', 98 | value: ret 99 | }); 100 | return ret; 101 | }; 102 | 103 | // 清空搜索历史 104 | export function clearSearch() { 105 | let ret = []; 106 | saveLoadcal({ 107 | module: 'chickmusic', 108 | id: 'searchHistory', 109 | value: ret 110 | }); 111 | return ret; 112 | }; 113 | 114 | export function savePlay(value) { // 存播放历史 115 | let ret = loadLoadcal({ 116 | module: 'chickmusic', 117 | id: 'playHistory', 118 | def: [] 119 | }); 120 | insertArray(ret, value, (item) => { 121 | return item.id === value.id; 122 | }, PLAY_MAX_LEN); 123 | saveLoadcal({ 124 | module: 'chickmusic', 125 | id: 'playHistory', 126 | value: ret 127 | }); 128 | return ret; 129 | }; 130 | 131 | export function loadPlay() { // 读播放历史 132 | let ret = loadLoadcal({ 133 | module: 'chickmusic', 134 | id: 'playHistory', 135 | def: [] 136 | }); 137 | return ret; 138 | }; 139 | 140 | export function saveFavorite(song) { // 存储收藏歌曲 141 | let ret = loadLoadcal({ 142 | module: 'chickmusic', 143 | id: 'favoriteList', 144 | def: [] 145 | }); 146 | insertArray(ret, song, (item) => { 147 | return item.id === song.id; 148 | }, FAVORITE_MAX_LEN); 149 | saveLoadcal({ 150 | module: 'chickmusic', 151 | id: 'favoriteList', 152 | value: ret 153 | }); 154 | return ret; 155 | }; 156 | 157 | export function deleteFavorite(song) { // 删除某首收藏歌曲 158 | let ret = loadLoadcal({ 159 | module: 'chickmusic', 160 | id: 'favoriteList', 161 | def: [] 162 | }); 163 | deleteFromArray(ret, song, (item) => { 164 | return item.id === song.id; 165 | }); 166 | saveLoadcal({ 167 | module: 'chickmusic', 168 | id: 'favoriteList', 169 | value: ret 170 | }); 171 | return ret; 172 | }; 173 | 174 | export function loadFavorite() { // 读取收藏歌曲列表 175 | let ret = loadLoadcal({ 176 | module: 'chickmusic', 177 | id: 'favoriteList', 178 | def: [] 179 | }); 180 | return ret; 181 | }; -------------------------------------------------------------------------------- /src/components/recommend/recommend.vue: -------------------------------------------------------------------------------- 1 | 36 | 100 | -------------------------------------------------------------------------------- /src/components/search/search.vue: -------------------------------------------------------------------------------- 1 | 36 | 108 | -------------------------------------------------------------------------------- /src/components/user-center/user-center.vue: -------------------------------------------------------------------------------- 1 | 32 | 111 | -------------------------------------------------------------------------------- /src/components/suggest/suggest.vue: -------------------------------------------------------------------------------- 1 | 19 | 162 | -------------------------------------------------------------------------------- /src/components/add-song/add-song.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 122 | 123 | -------------------------------------------------------------------------------- /src/components/music-list/music-list.vue: -------------------------------------------------------------------------------- 1 | 34 | 157 | -------------------------------------------------------------------------------- /src/base/listview/listview.vue: -------------------------------------------------------------------------------- 1 | 37 | 167 | -------------------------------------------------------------------------------- /src/components/playlist/playlist.vue: -------------------------------------------------------------------------------- 1 | 41 | 136 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-music 2 | 3 | > Vue全家桶音乐播放器 4 | 5 | ## 电脑端预览: 切换调式模式 刷新后才能正常滚动 6 | 7 | ## Build Setup 8 | 9 | ``` bash 10 | # install dependencies 11 | npm install 12 | 13 | # serve with hot reload at localhost:8080 14 | npm run dev 15 | 16 | # build for production with minification 17 | npm run build 18 | 19 | # build for production and view the bundle analyzer report 20 | npm run build --report 21 | 22 | # 切换移动端调试 刷新后滚动正常 23 | ``` 24 | 25 | ## 预览 26 | ![此处输入图片的描述][1] 27 | ![此处输入图片的描述][2] 28 | ![此处输入图片的描述][3] 29 | 30 | ## 目录结构 31 | ``` 32 | . 33 | ├── build // webpack配置文件 34 | ├── config // 项目打包路径 35 | ├── src // 项目核心文件 36 | │ ├── api // 数据抓取API 37 | | └── config // 请求公共配置 38 | | └── getLyric // 歌词抓取 39 | | └── handlesongurl // 歌曲url处理 40 | | └── rank // 排行榜数据抓取 41 | | └── recommend // 推荐数据抓取 42 | | └── search // 搜索数据抓取 43 | | └── singer // 歌手数据抓取 44 | │ ├── base // 基础组件 45 | | └── confirm // 对话框组件 46 | | └── listview // 歌手页面子组件 47 | | └── loading // 加载中样式展示组件 48 | | └── no-result // 无内容样式展示组件 49 | | └── progress-bar // 播放器进度条 50 | | └── progress-circle // 迷你播放器圆环进度条 51 | | └── scroll // 页面滚动组件 52 | | └── search-box // 搜索框组件 53 | | └── search-list // 历史搜索记录列表 54 | | └── slide // 轮播图组件 55 | | └── song-list // 歌曲列表子组件 56 | | └── switches // 选项卡按钮 57 | | └── top-tip // 头部通知框 58 | │ ├── common // 公共静态资源 59 | | └── fonts // 字体图标 60 | | └── image // 静态图片资源 61 | | └── js // 公共js 62 | | └── cache // localstorage相关 63 | | └── config // 公共配置文件 64 | | └── dom // Dom相关方法 65 | | └── jsonp // promise版本jsonp封装 66 | | └── mixin // 组件mixin 67 | | └── singFactory // 处理歌手格式 68 | | └── songFactory // 处理歌曲格式 69 | | └── utils // js工具库 70 | | └── less // 公共less 71 | │ ├── components // 业务组件 72 | | └── add-song // 播放列表添加歌曲组件 73 | | └── disc-detail // 歌单详情 74 | | └── m-header // 头部 75 | | └── music-list // 歌曲列表 76 | | └── player // 播放器 77 | | └── playlist // 播放列表 78 | | └── rank // 排行榜 79 | | └── recommend // 推荐 80 | | └── search // 搜索 81 | | └── singer // 歌手 82 | | └── singer-detail // 歌手详情 83 | | └── suggest // 搜素结果 84 | | └── tab // 导航 85 | | └── top-detail // 排行榜详情 86 | | └── user-center // 用户中心 87 | | ├── router // 路由 88 | | ├── store // vuex 89 | | └── actions // actions 90 | | └── getters // getters 91 | | └── index // store入口 92 | | └── mutation-types // mutation-types 93 | | └── mutations // mutations 94 | | └── state // state 95 | │ ├── App.vue // 组件入口 96 | │ ├── main.js // 入口文件 97 | ├── index.html // 模板html文件 98 | ├── prod.server.js // 测试打包后的服务器 99 | . 100 | 101 | ``` 102 | ### 组件关系图 103 | ![组件][4] 104 | ### 总结 105 | #### 准备工作 106 | 1.icomoon制作字体图标 107 | 108 | 2.基础less 109 | 110 | - a. 颜色规范 111 | - b. mixin 112 | - c. reset 113 | - d. icon 114 | 115 | 3.eslint规则改写 116 | babel-runtime babel-ployfill // es6一些API polyfill 117 | 118 | 4.fastclick 119 | ```javascript 120 | import fastclick from 'fastclick'; // 消除移动端点击延迟 121 | fastclick.attach(document.body); 122 | ``` 123 | 5.vue-lazyload // 图片懒加载 124 | ```javascript 125 | Vue.use(VueLazyLoad, { 126 | loading: require('common/image/default.png') 127 | }); 128 | ``` 129 | 6.目录结构 130 | > src => api base common components router store 131 | 132 | 7.webpack配置别名路径 133 | ```javascript 134 | extensions: ['.js', '.vue', '.json', '.less'], 135 | alias: { 136 | '@': resolve('src'), 137 | 'common': resolve('src/common'), 138 | 'components': resolve('src/components'), 139 | 'api': resolve('src/api'), 140 | 'base': resolve('src/base') 141 | } 142 | ``` 143 | 144 | #### 知识点 145 | *. slider基础组件封装 146 | ```javascript 147 | // 基于better-scroll 148 | props: 149 | 1.loop 是否无缝 150 | 2.autoPlay: 是否自动播放 151 | 3.interval: 播放间隔 152 | slot: 幻灯片内容列表 153 | 实现: 154 | 1.初始化slide-item宽度和外层容器宽度,并加上组件内写好的slide-item类名 155 | 2.初始化better-scroll 156 | 3.监听scrollEnd通过this.slider.getCurrentPage().pageX 获取当前索引 157 | 4.自动播放实现: scrollEnd时开始setimeout beforeScrollStart时清除定时器 158 | 5.组件初始化:mouted时初始化 resize时 重新计算宽度 this.slider.refresh() 159 | ``` 160 | *. scroll基础组件封装 161 | ```javascript 162 | // 基于better-scroll1 163 | props: 164 | 1.probeType 滚动监听的间隔 默认为1 一定时间间隔监听滚动 165 | 2.click: 是否能点击 166 | 3.data: 渲染列表的数据 监控数据的变化刷新scroll 167 | 4.listenScroll:是否监听滚动 168 | 5.pullup:上滑到底部是否派发事件(上滑加载) 169 | 6.scrollBefore: 滚动开始前是否派发事件 170 | 7.refreshDelay: 数据变化 => scroll刷新的时间间隔(防止有过渡动画 自定义刷新时间能正确计算高度) 171 | 172 | slot: 滚动的整个列表 173 | 174 | methods: 175 | 1.refresh 176 | 2.scrollTo 177 | 3.scrollToElement 178 | emit: 179 | 1.scroll // 滚动时 180 | 2.scrollToEnd // 上滑至底部 181 | 3.scrollBefore // 滚动开始前 182 | 实现: 183 | 1.mouted时初始化better-scroll 根据probeType click 进行配置 184 | 2.根据listenScroll pullup scrollBefore 判断初始化时是否监听scroll的状态 185 | 3.watch:data变化 按refreshDelay时间刷新scroll 186 | 187 | ``` 188 | *. listview(歌手列表)基础组件封装 189 | ```javascript 190 | // 基于封装的scroll组件 191 | // proboType = 3 实时监控滚动位置 左右结构对应 192 | 193 | emit : 1.select (歌手被点击时) 194 | 实现: 195 | 导航关联歌手列表 196 | 1.shortcut触摸记录位置和当前index 歌手列表滚动到第index个 197 | 2.shortcut Move的时候 用 当前坐标 - 初始坐标 / shortcutItem的高度 计算出移动了多少个index 198 | 3.currentIndex = disIndex + startIndex 歌手列表滚动到第currentIndex个 199 | 200 | 歌手列表关联导航 201 | 4.mounted时记录每个列表的高度区间_calculateHeight 202 | 5.歌手列表滚动监听nowY的变化判断落在哪个区间改变currentIndex 203 | 204 | 细节 205 | 歌手列表固定的列表头 206 | 1.绝对定位层 207 | 2.判断currentIndex 获取 当前title 208 | 3.当 height[index] - nowY <= 列表头的高度时 固定的列表头往上偏移 209 | ``` 210 | *. music-list(歌手/歌单 详情列表)和song-list(music-list子组件)基础组件封装 211 | ```javascript 212 | music-list 实现细节 213 | 1. bgimage层 因为请求图片是异步的 所以可以height:0 width:100% padding-top:70%预留位置 214 | 2. list下拉图片放大 上滑覆盖图片 并且有最小值 215 | // 监控scrollY变化 216 | a. 当scrollY > 0 时 percent = Math.abs(newY / this.imageHeight) scale = 1 + percent; 217 | b. bg-layer跟随列表滚动并且滚动的最小值为 -this.imageHeight + TOP_HEIHGT(头部title高度); 218 | c. 当bg-layer滚动到阈值时 图片高度缩小至TOP_HEIHGT 219 | 220 | song-list 221 | props: 222 | songs [] 歌曲列表 223 | rank: 是否排名 Boolean 224 | emit: 225 | this.$emit('select', song, index) 选中一首歌曲 226 | 227 | 实现: 228 | 1.普通的ul列表 229 | 2.排行榜页面用到排名 230 | 3.rank为true显示 231 |
232 | 233 |
234 | 4.前三名有奖杯图片 235 | 5.后面按序号 236 | ``` 237 | *.player(播放器)组件封装 238 | ```javascript 239 | state(vuex): 240 | playing: false // 播放状态 241 | fullScreen: false // 是否全屏 242 | playlist: [] // 播放列表 243 | sequenceList: [] // 顺序列表 244 | mode: 'random'/'sequence'/'loop' // 播放模式 245 | currentIndex: -1 // 播放index 246 | getters: currentSong(state){return state.playlist[state.currentIndex]} 247 | mutations: 每个state对应的修改方法 248 | 249 | 250 | 播放器过渡效果 251 | 全屏播放器: 252 | &.normal-enter-active, &.normal-leave-active{ 253 | transition: all 0.4s; 254 | .top, .bottom{ 255 | transition: all 0.4s cubic-bezier(0.86, 0.18, 0.82, 1.32); 256 | } 257 | } 258 | &.normal-enter, &.normal-leave-to{ 259 | opacity: 0; 260 | .top{ 261 | transform: translate3d(0, -100px, 0); 262 | } 263 | .bottom{ 264 | transform: translate3d(0, 100px, 0); 265 | } 266 | } 267 | 268 | 另外cdWrapper还有一个 从 迷你播放器icon进入 先放大再缩小的过程, 这个过程通过transitions的js钩子里 引入 create-keyframe-animtion实现 269 | 270 | 迷你播放器: 271 | &.mini-enter-active, &.mini-leave-active{ 272 | transition: all 0.4s; 273 | } 274 | &.mini-enter, &.mini-leave-to{ 275 | opacity: 0; 276 | } 277 | 278 | 关于迷你播放器圆环进度条实现原理 279 | 1.引入svg 标签 280 | 2.两个circle fill都为透明 里面放入真正的按钮 281 | 3.一个circle作为bg 另一个则利用 stroke-dasharray stroke-dashoffset按比例描边 282 | 4.dashArray: Math.PI * 100(100为viewbox宽度) 283 | 5.dashOffset() { 284 | return (1 - this.percent) * this.dashArray; 285 | } 286 | 287 | 关于cd的旋转 288 | 1. 理想情况下直接通过css animation infinite 旋转就可以 289 | 2. 暂定直接应用 animation-play-state: pause 290 | 3. 但是ios并不支持animition-play-state 291 | 解决办法: 292 | 1. 通过cd外层容器记录旋转角度 293 | 2. 每次移除animition时记录旋转角度 294 | 3. 开始旋转前将外层容器旋转到相应的角度 295 | 296 | 关于ios safari 和 微信浏览器 audio无法播放歌曲的问题 297 | 1. 如果在微信浏览器, 需要在 WeixinJSBridgeReady 后 动态添加audio标签 298 | 2. 如果在safari, 只能在用用户点击文档后动态添加audio标签 299 | 300 | ``` 301 | *.css前缀补全函数prefixStyle 302 | ```javascript 303 | let eleStyle = document.createElement('div').style; 304 | let o = { 305 | webkit: 'webkitTransform', 306 | Moz: 'MozTransform', 307 | O: 'OTransform, 308 | ms: 'msTransform, 309 | standard: 'transform' 310 | } 311 | cosnt vendor = (() => { 312 | for( let k in o ) { 313 | if (eleStyle[o[k]]] !== undefined) { 314 | return k 315 | } 316 | } 317 | return false 318 | })() 319 | 320 | function prefixStyle(style) { 321 | if (!vendor) return false 322 | if (vendor === 'standard') { 323 | return style 324 | } 325 | return vendor + style.substr(0,1).toUpperCase + style.substr(1) 326 | } 327 | ``` 328 | *. 随机播放算法 329 | ```javascript 330 | function getRomdomInt(min, max) { // 生成min - max 的随机整数 331 | return Math.floor(Math.random() * (max - min + 1) + min); 332 | }; 333 | 334 | export function shuffle(arr) { // 随机打乱数组 335 | let _arr = arr.slice(); 336 | for (let i = 0; i < _arr.length; i++) { 337 | let randomI = getRomdomInt(0, i); 338 | [_arr[i], _arr[randomI]] = [_arr[randomI], _arr[i]]; 339 | }; 340 | return _arr; 341 | }; 342 | ``` 343 | *.截留函数封装(搜索模块用到) 344 | ```javascript 345 | export function debounce(fn, delay) { // 截流函数 346 | let timer; 347 | return function(...args) { 348 | if (timer) { 349 | clearTimeout(timer); 350 | }; 351 | timer = setTimeout(() => { 352 | fn.apply(this, args); 353 | }, delay); 354 | }; 355 | }; 356 | ``` 357 | *. jsonp Promise版封装 358 | ```javascript 359 | import originJsonp from 'jsonp' 360 | export function jsonp(url, data, options) { 361 | url = (url.indexOf('?') < 0 ? '?' : '') + params(data) 362 | return new Promise((resolve, reject) => { 363 | originJsonp(url, options, (err, data) => { 364 | if (err) { 365 | reject(err) 366 | } else { 367 | resolve(data) 368 | } 369 | }) 370 | }) 371 | } 372 | function params(data) { 373 | let ret = '' 374 | for (let k in data) { 375 | var value = data[k] === undefined ? '' : data[k] 376 | ret += `&${k}=${encodeURIComponent(value)}` 377 | } 378 | return ret.substr(1) 379 | } 380 | ``` 381 | *.小细节 382 | ```javascript 383 | 1. 幻灯片图片加载进来后刷新外层scroll 384 | 385 | loadImage() { // 图片渲染后撑开刷新scroll 386 | if (!this.checkloaded) { 387 | this.$refs.scroll.refresh(); 388 | this.checkloaded = true; 389 | }; 390 | } 391 | 2. better-scroll需要在宽高变化时refresh 392 | ``` 393 | *.api处理 394 | ```javascript 395 | // qq音乐某些接口可以直接通过jsonp获取 396 | // 某些接口做了限制,需要后端代理 397 | 398 | 1.普通jsonp 399 | export function getRecommend() { // 获取幻灯片数据 400 | let url = 'https://c.y.qq.com/musichall/fcgi-bin/fcg_yqqhomepagerecommend.fcg' 401 | const data = { 402 | g_tk: 1928093487, 403 | inCharset: 'utf-8', 404 | outCharset: 'utf-8', 405 | notice: 0, 406 | format: 'jsonp', 407 | platform: 'h5', 408 | uin: 0, 409 | needNewCode: 1 410 | } 411 | const options = { 412 | param: 'jsonpCallback' 413 | } 414 | return jsonp(url, data, options); 415 | } 416 | 2.后端代理 417 | // 后端 418 | app.get('/api/getDiscList', (req, res) => { // 获取歌单信息 419 | const url = 'https://c.y.qq.com/splcloud/fcgi-bin/fcg_get_diss_by_tag.fcg'; 420 | axios.get(url, { 421 | header: { 422 | referer: 'https://c.y.qq.com', 423 | host: 'c.y.qq.com' 424 | }, 425 | params: req.query 426 | }).then((response) => { 427 | res.json(response.data) 428 | }).catch((err) => { 429 | console.log(err) 430 | }) 431 | }) 432 | 433 | // 前端 434 | export funtion getDiscList() { 435 | const data = { 436 | platform: 'yqq', 437 | hostUin: 0, 438 | sin: 0, 439 | ein: 29, 440 | sortId: 5, 441 | needNewCode: 0, 442 | categoryId: 10000000, 443 | rnd: Math.random(), 444 | format: 'json', 445 | g_tk: 1928093487, 446 | inCharset: 'utf-8', 447 | outCharset: 'utf-8', 448 | notice: 0 449 | } 450 | axios.get('/api/getDiscList', { 451 | params: data 452 | }).then((res) => { 453 | return Promise.resolve(res.data) 454 | }) 455 | } 456 | 457 | 3. qq音乐歌曲媒体url 需要 mid处理 然后拼成 458 | // 前端 459 | // url_mid获取 460 | let _uid = '' 461 | function genUrlMid(mids, types) { 462 | function getUid() { 463 | if (_uid) { 464 | return _uid; 465 | } 466 | if (!_uid) { 467 | const t = (new Date).getUTCMilliseconds(); 468 | _uid = '' + Math.round(2147483647 * Math.random()) * t % 1e10; 469 | } 470 | return _uid 471 | } 472 | const guid = getUid() 473 | return { 474 | module: 'vkey.GetVkeyServer', 475 | method: 'CgiGetVkey', 476 | param: { 477 | guid, 478 | songmid: mids, 479 | songtype: types, 480 | uin: '0', 481 | loginflag: 0, 482 | platform: '23' 483 | } 484 | } 485 | } 486 | // 参数mids:[song1.mid, song2.mid, song3.mid....] types[0, 0 ,0, ..song.length] 487 | 488 | // 获取purl 489 | const urlMid = genUrlMid(mids, types); 490 | const data = { 491 | g_tk: 5381, 492 | format: 'json', 493 | platform: 'h5', 494 | needNewCode: 1, 495 | uin: 0, 496 | inCharset: 'utf-8', 497 | outCharset: 'utf-8', 498 | notice: 0 499 | } 500 | axios.post('/api/getPurlUrl', { 501 | comm: data, 502 | url_mid: urlMid 503 | }).then((res) => { 504 | let infos = res.url_mid.data.midurlinfo; 505 | songs.forEach((song, index) => { 506 | song.url = `http://dl.stream.qqmusic.qq.com/${info[index].purl}`; // 拼接真正有效的url 507 | }) 508 | }) 509 | 510 | // 后端 511 | app.post('/api/getPurlUrl', bodyParser.json(), function (req, res) { 512 | const url = 'https://u.y.qq.com/cgi-bin/musicu.fcg' 513 | axios.post(url, req.body, { 514 | headers: { 515 | referer: 'https://y.qq.com/', 516 | origin: 'https://y.qq.com', 517 | 'Content-type': 'application/x-www-form-urlencoded' 518 | } 519 | }).then((response) => { 520 | res.json(response.data) 521 | }).catch((e) => { 522 | console.log(e) 523 | }) 524 | }) 525 | ``` 526 | ### 参考 527 | > 慕课网黄佚老师Vue Music App 528 | 529 | 530 | [1]: https://ws1.sinaimg.cn/large/e8323205gy1fqkjptsymkg20qk0hkx6p.jpg 531 | [2]: https://ws1.sinaimg.cn/large/e8323205gy1fqkknfftvog20qi0hkx6s.jpg 532 | [3]: https://ws1.sinaimg.cn/large/e8323205gy1fqkkiz79q2g20qi0hkb2a.jpg 533 | [4]: https://ws1.sinaimg.cn/large/e8323205gy1fqoc72qbdmj21451bjtbc.jpg -------------------------------------------------------------------------------- /src/common/fonts/music-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Generated by IcoMoon 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 | -------------------------------------------------------------------------------- /src/components/player/player.vue: -------------------------------------------------------------------------------- 1 | 124 | 513 | --------------------------------------------------------------------------------