├── 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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
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 |
2 |
3 |

4 |
{{title}}
5 |
6 |
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 |
2 |
6 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
36 |
37 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/base/switches/switches.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
28 |
29 |
--------------------------------------------------------------------------------
/src/components/m-header/m-header.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
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 |
2 |
3 |
7 |
8 |
9 |
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 |
2 |
3 |
4 |
5 | {{item}}
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
32 |
33 |
--------------------------------------------------------------------------------
/src/components/disc-detail/disc-detail.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
54 |
--------------------------------------------------------------------------------
/src/components/singer-detail/singer-detail.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
52 |
--------------------------------------------------------------------------------
/src/base/search-box/search-box.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
41 |
--------------------------------------------------------------------------------
/src/components/top-detail/top-detail.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
88 |
--------------------------------------------------------------------------------
/src/base/song-list/song-list.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | -
5 |
6 |
7 |
8 |
9 |
{{song.name}}
10 |
{{song.singer}}·{{song.album}}
11 |
12 |
13 |
14 |
15 |
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 |
2 |
3 |
4 |
5 |
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 |
2 |
3 |
4 |
5 |
6 |
{{text}}
7 |
8 |
{{cancelBtnText}}
9 |
{{confirmBtnText}}
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
56 |
57 |
--------------------------------------------------------------------------------
/src/components/rank/rank.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | -
6 |
7 |
![]()
8 |
9 |
10 | -
11 | {{key + 1}}
12 | {{song.songname}} -- {{song.singername}}
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
71 |
--------------------------------------------------------------------------------
/src/base/progress-bar/progress-bar.vue:
--------------------------------------------------------------------------------
1 |
2 |
16 |
17 |
18 |
78 |
79 |
--------------------------------------------------------------------------------
/src/base/slider/slider.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
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 |
2 |
3 |
4 |
5 |
14 |
15 |
热门歌单推荐
16 |
17 | -
18 |
19 |
![]()
20 |
21 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
100 |
--------------------------------------------------------------------------------
/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 |
108 |
--------------------------------------------------------------------------------
/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 |
111 |
--------------------------------------------------------------------------------
/src/components/suggest/suggest.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | -
5 |
6 |
7 |
8 |
9 |
{{getDisplayName(item)}}
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
162 |
--------------------------------------------------------------------------------
/src/components/add-song/add-song.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
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 | 1首歌曲已经添加到播放列表
35 |
36 |
37 |
38 |
39 |
40 |
41 |
122 |
123 |
--------------------------------------------------------------------------------
/src/components/music-list/music-list.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | 随机播放全部
12 |
13 |
14 |
15 |
16 |
17 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
157 |
--------------------------------------------------------------------------------
/src/base/listview/listview.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 | -
12 |
{{group.title}}
13 |
14 | -
20 |
21 | {{item.name}}
22 |
23 |
24 |
25 |
26 |
29 |
32 |
33 |
34 |
35 |
36 |
37 |
167 |
--------------------------------------------------------------------------------
/src/components/playlist/playlist.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
12 |
13 |
14 |
15 |
16 | {{item.name}}
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | 添加歌曲到队列
30 |
31 |
32 |
33 | 关闭
34 |
35 |
36 |
37 |
38 |
39 |
40 |
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 |
--------------------------------------------------------------------------------
/src/components/player/player.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
11 |
12 |
![]()
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
27 |
28 |
29 |
30 |
![]()
31 |
32 |
33 |
34 |
纯音乐,请欣赏
35 |
{{playingLyric}}
36 |
37 |
38 |
39 |
40 |
41 |
纯音乐,请欣赏
42 |
49 | {{line.txt}}
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
{{formatTime(currentTime)}}
62 |
65 |
{{formatTime(currentSong.duration)}}
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
![]()
92 |
93 |
94 |
98 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 | 歌曲无效
113 |
114 |
115 |
122 |
123 |
124 |
513 |
--------------------------------------------------------------------------------