├── src ├── api │ ├── index.js │ ├── home │ │ ├── index.js │ │ └── home.api.js │ ├── player │ │ ├── index.js │ │ └── player.api.js │ ├── config.js │ ├── Api.js │ └── http.js ├── common │ ├── stylus │ │ ├── index.styl │ │ ├── base.styl │ │ ├── variable.styl │ │ ├── mixin.styl │ │ ├── icon.styl │ │ └── reset.styl │ ├── image │ │ └── default.png │ └── js │ │ ├── config.js │ │ ├── singer.js │ │ ├── uid.js │ │ ├── jsonp.js │ │ ├── dom.js │ │ ├── util.js │ │ ├── song.js │ │ └── mixin.js ├── store │ ├── config.js │ ├── index.js │ ├── state.js │ ├── getters.js │ ├── mutations.js │ └── actions.js ├── views │ ├── my.vue │ ├── mv.vue │ ├── Disc.vue │ └── Home.vue ├── main.js ├── components-base │ ├── scroll │ │ ├── use-scroll.js │ │ ├── scroll.vue │ │ └── scrol.v2.vue │ ├── slider │ │ ├── use-slider.js │ │ ├── slider.vue │ │ └── slider.v2.vue │ ├── loading │ │ ├── directive.js │ │ └── loading.vue │ ├── mv │ │ ├── mv.vue │ │ └── mv-item.vue │ ├── music-animation │ │ └── music-animation.vue │ └── progress-bar │ │ └── progress-bar.vue ├── App.vue ├── router │ └── index.js └── components │ ├── nav │ └── nav.vue │ ├── song-list │ └── song-list.vue │ ├── disc-list │ └── disc-list.vue │ └── m-header │ └── m-header.vue ├── v2 ├── .browserslistrc ├── public │ ├── robots.txt │ ├── favicon.ico │ ├── img │ │ └── icons │ │ │ ├── apple-touch-icon.png │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon-60x60.png │ │ │ ├── apple-touch-icon-76x76.png │ │ │ ├── apple-touch-icon-120x120.png │ │ │ ├── apple-touch-icon-152x152.png │ │ │ ├── apple-touch-icon-180x180.png │ │ │ └── msapplication-icon-144x144.png │ └── index.html ├── music.zip ├── src │ ├── common │ │ ├── stylus │ │ │ ├── index.styl │ │ │ ├── base.styl │ │ │ ├── variable.styl │ │ │ ├── mixin.styl │ │ │ ├── icon.styl │ │ │ └── reset.styl │ │ ├── js │ │ │ ├── config.js │ │ │ ├── singer.js │ │ │ ├── uid.js │ │ │ ├── jsonp.js │ │ │ ├── util.js │ │ │ ├── dom.js │ │ │ ├── song.js │ │ │ └── mixin.js │ │ └── image │ │ │ └── default.png │ ├── store │ │ ├── config.js │ │ ├── state.js │ │ ├── index.js │ │ ├── actions.js │ │ ├── getters.js │ │ └── mutations.js │ ├── api │ │ ├── disc.js │ │ ├── song.js │ │ ├── singer.js │ │ ├── config.js │ │ ├── mv.js │ │ └── home.js │ ├── views │ │ ├── my.vue │ │ ├── mv.vue │ │ ├── Disc.vue │ │ ├── singerHome.vue │ │ ├── singer.vue │ │ └── Home.vue │ ├── App.vue │ ├── main.js │ ├── registerServiceWorker.js │ ├── router │ │ └── index.js │ ├── components │ │ ├── nav │ │ │ └── nav.vue │ │ ├── song-list │ │ │ └── song-list.vue │ │ ├── music-list │ │ │ └── music-list.vue │ │ ├── singer-music-list │ │ │ └── singer-music-list.vue │ │ ├── m-header │ │ │ └── m-header.vue │ │ └── player │ │ │ └── player.vue │ └── base │ │ ├── mv │ │ ├── mv.vue │ │ └── mv-item.vue │ │ ├── loading │ │ └── loading.vue │ │ ├── scroll │ │ └── scroll.vue │ │ ├── music-animation │ │ └── music-animation.vue │ │ ├── slider │ │ └── slider.vue │ │ ├── progress-bar │ │ └── progress-bar.vue │ │ └── listview │ │ └── listview.vue ├── babel.config.js ├── .editorconfig ├── .gitignore ├── .eslintrc.js ├── vue.config.js ├── package.json └── README.md ├── music.zip ├── public ├── favicon.ico ├── icons │ ├── apple-touch-icon.png │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon-60x60.png │ ├── apple-touch-icon-76x76.png │ ├── apple-touch-icon-120x120.png │ ├── apple-touch-icon-152x152.png │ ├── apple-touch-icon-180x180.png │ └── msapplication-icon-144x144.png └── index.html ├── babel.config.js ├── .gitignore ├── package.json ├── vue.config.js └── README.md /src/api/index.js: -------------------------------------------------------------------------------- 1 | export * from "./Api"; 2 | -------------------------------------------------------------------------------- /v2/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | -------------------------------------------------------------------------------- /src/api/home/index.js: -------------------------------------------------------------------------------- 1 | export * from './home.api'; 2 | -------------------------------------------------------------------------------- /v2/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /src/api/player/index.js: -------------------------------------------------------------------------------- 1 | export * from './player.api'; 2 | -------------------------------------------------------------------------------- /src/api/config.js: -------------------------------------------------------------------------------- 1 | export const HOST = 'http://47.93.242.149:3300' 2 | -------------------------------------------------------------------------------- /music.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xA1b/Music-For-The-Poor/HEAD/music.zip -------------------------------------------------------------------------------- /v2/music.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xA1b/Music-For-The-Poor/HEAD/v2/music.zip -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xA1b/Music-For-The-Poor/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/common/stylus/index.styl: -------------------------------------------------------------------------------- 1 | @import "./reset.styl" 2 | @import "./base.styl" 3 | @import "./icon.styl" -------------------------------------------------------------------------------- /v2/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xA1b/Music-For-The-Poor/HEAD/v2/public/favicon.ico -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/store/config.js: -------------------------------------------------------------------------------- 1 | export const playMode = { 2 | sequence: 0, 3 | loop: 1, 4 | random: 2 5 | } 6 | -------------------------------------------------------------------------------- /v2/src/common/stylus/index.styl: -------------------------------------------------------------------------------- 1 | @import "./reset.styl" 2 | @import "./base.styl" 3 | @import "./icon.styl" -------------------------------------------------------------------------------- /v2/src/store/config.js: -------------------------------------------------------------------------------- 1 | export const playMode = { 2 | sequence: 0, 3 | loop: 1, 4 | random: 2 5 | } 6 | -------------------------------------------------------------------------------- /v2/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/common/image/default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xA1b/Music-For-The-Poor/HEAD/src/common/image/default.png -------------------------------------------------------------------------------- /src/common/js/config.js: -------------------------------------------------------------------------------- 1 | export const playMode = { 2 | sequence: 0, 3 | loop: 1, 4 | random: 2 5 | } 6 | -------------------------------------------------------------------------------- /v2/src/common/js/config.js: -------------------------------------------------------------------------------- 1 | export const playMode = { 2 | sequence: 0, 3 | loop: 1, 4 | random: 2 5 | } 6 | -------------------------------------------------------------------------------- /v2/src/common/image/default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xA1b/Music-For-The-Poor/HEAD/v2/src/common/image/default.png -------------------------------------------------------------------------------- /public/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xA1b/Music-For-The-Poor/HEAD/public/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /public/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xA1b/Music-For-The-Poor/HEAD/public/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/icons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xA1b/Music-For-The-Poor/HEAD/public/icons/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/icons/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xA1b/Music-For-The-Poor/HEAD/public/icons/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /public/icons/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xA1b/Music-For-The-Poor/HEAD/public/icons/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /public/icons/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xA1b/Music-For-The-Poor/HEAD/public/icons/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /public/icons/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xA1b/Music-For-The-Poor/HEAD/public/icons/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /public/icons/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xA1b/Music-For-The-Poor/HEAD/public/icons/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /v2/public/img/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xA1b/Music-For-The-Poor/HEAD/v2/public/img/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /public/icons/msapplication-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xA1b/Music-For-The-Poor/HEAD/public/icons/msapplication-icon-144x144.png -------------------------------------------------------------------------------- /v2/public/img/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xA1b/Music-For-The-Poor/HEAD/v2/public/img/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /v2/public/img/icons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xA1b/Music-For-The-Poor/HEAD/v2/public/img/icons/android-chrome-512x512.png -------------------------------------------------------------------------------- /v2/public/img/icons/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xA1b/Music-For-The-Poor/HEAD/v2/public/img/icons/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /v2/public/img/icons/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xA1b/Music-For-The-Poor/HEAD/v2/public/img/icons/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /v2/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 2 4 | trim_trailing_whitespace = true 5 | insert_final_newline = true 6 | -------------------------------------------------------------------------------- /v2/public/img/icons/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xA1b/Music-For-The-Poor/HEAD/v2/public/img/icons/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /v2/public/img/icons/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xA1b/Music-For-The-Poor/HEAD/v2/public/img/icons/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /v2/public/img/icons/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xA1b/Music-For-The-Poor/HEAD/v2/public/img/icons/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /v2/public/img/icons/msapplication-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xA1b/Music-For-The-Poor/HEAD/v2/public/img/icons/msapplication-icon-144x144.png -------------------------------------------------------------------------------- /v2/src/api/disc.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { BASE_URL } from './config' 3 | export function getSongList (id) { 4 | return axios({ 5 | url: `${BASE_URL}/playlist?id=${id}` 6 | }) 7 | } 8 | -------------------------------------------------------------------------------- /v2/src/api/song.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { BASE_URL } from './config' 3 | export function getSongUrl (id, cid) { 4 | return axios({ 5 | url: `${BASE_URL}/song/url?id=${id}&cid=${cid}&needPic=1` 6 | }) 7 | } 8 | -------------------------------------------------------------------------------- /v2/src/api/singer.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { BASE_URL } from './config' 3 | export function getSongList (name, page) { 4 | return axios({ 5 | url: `${BASE_URL}/search?keyword=${name}&&pageNo=${page}` 6 | }) 7 | } 8 | -------------------------------------------------------------------------------- /src/common/js/singer.js: -------------------------------------------------------------------------------- 1 | export default class Singer { 2 | constructor ({ id, name }) { 3 | this.id = id 4 | this.name = name 5 | this.avatar = `https://y.gtimg.cn/music/photo_new/T001R300x300M000${id}.jpg?max_age=2592000` 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /v2/src/common/js/singer.js: -------------------------------------------------------------------------------- 1 | export default class Singer { 2 | constructor ({ id, name }) { 3 | this.id = id 4 | this.name = name 5 | this.avatar = `https://y.gtimg.cn/music/photo_new/T001R300x300M000${id}.jpg?max_age=2592000` 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/common/js/uid.js: -------------------------------------------------------------------------------- 1 | let _uid = '' 2 | 3 | export function getUid () { 4 | if (_uid) { 5 | return _uid 6 | } 7 | if (!_uid) { 8 | const t = (new Date()).getUTCMilliseconds() 9 | _uid = '' + Math.round(2147483647 * Math.random()) * t % 1e10 10 | } 11 | return _uid 12 | } 13 | -------------------------------------------------------------------------------- /v2/src/common/js/uid.js: -------------------------------------------------------------------------------- 1 | let _uid = '' 2 | 3 | export function getUid () { 4 | if (_uid) { 5 | return _uid 6 | } 7 | if (!_uid) { 8 | const t = (new Date()).getUTCMilliseconds() 9 | _uid = '' + Math.round(2147483647 * Math.random()) * t % 1e10 10 | } 11 | return _uid 12 | } 13 | -------------------------------------------------------------------------------- /src/api/Api.js: -------------------------------------------------------------------------------- 1 | import { HomeApi } from "./home"; 2 | import { PlayerApi } from './player' 3 | 4 | 5 | 6 | const isSuccess = (res) => { 7 | return res && res.code === 1; 8 | }; 9 | 10 | export const Api = { 11 | isSuccess: isSuccess, 12 | HomeApi: new HomeApi(), 13 | PlayerApi: new PlayerApi(), 14 | }; 15 | -------------------------------------------------------------------------------- /v2/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore } from 'vuex' 2 | import state from './state' 3 | import mutations from './mutations' 4 | import * as getters from './getters' 5 | import * as actions from './actions' 6 | 7 | 8 | export default createStore({ 9 | state, 10 | getters, 11 | mutations, 12 | actions, 13 | }) 14 | -------------------------------------------------------------------------------- /src/views/my.vue: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | 17 | -------------------------------------------------------------------------------- /v2/src/views/my.vue: -------------------------------------------------------------------------------- 1 | 6 | 11 | 12 | 18 | -------------------------------------------------------------------------------- /src/common/stylus/base.styl: -------------------------------------------------------------------------------- 1 | @import "variable.styl" 2 | 3 | body, html 4 | line-height: 1 5 | font-family: 'PingFang SC', 'STHeitiSC-Light', 'Helvetica-Light', arial, sans-serif, 'Droid Sans Fallback' 6 | user-select: none 7 | -webkit-tap-highlight-color: transparent 8 | background: #fff 9 | color: $color-text 10 | touch-action: none -------------------------------------------------------------------------------- /v2/src/common/stylus/base.styl: -------------------------------------------------------------------------------- 1 | @import "variable.styl" 2 | 3 | body, html 4 | line-height: 1 5 | font-family: 'PingFang SC', 'STHeitiSC-Light', 'Helvetica-Light', arial, sans-serif, 'Droid Sans Fallback' 6 | user-select: none 7 | -webkit-tap-highlight-color: transparent 8 | background: #fff 9 | color: $color-text 10 | touch-action: none -------------------------------------------------------------------------------- /src/store/state.js: -------------------------------------------------------------------------------- 1 | import { playMode } from '@/common/js/config' 2 | const state = { 3 | disc: {}, 4 | playerIconColor: 'black', 5 | fullScreen: false, 6 | currentIndex: -1, 7 | playing: false, 8 | playlist: [], 9 | sequenceList: [], 10 | mode: playMode.sequence, 11 | searchBoxFocue: false 12 | } 13 | export default state 14 | -------------------------------------------------------------------------------- /v2/src/store/state.js: -------------------------------------------------------------------------------- 1 | import { playMode } from '@/common/js/config' 2 | const state = { 3 | disc: {}, 4 | playerIconColor: 'black', 5 | fullScreen: false, 6 | currentIndex: -1, 7 | playing: false, 8 | playlist: [], 9 | sequenceList: [], 10 | mode: playMode.sequence, 11 | searchBoxFocue: false 12 | } 13 | export default state 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | music/node_modules 4 | /dist 5 | /music 6 | 7 | 8 | # local env files 9 | .env.local 10 | .env.*.local 11 | 12 | # Log files 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | pnpm-debug.log* 17 | 18 | # Editor directories and files 19 | .idea 20 | .vscode 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | -------------------------------------------------------------------------------- /v2/src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import state from './state' 4 | import * as actions from './actions' 5 | import * as getters from './getters' 6 | import mutations from './mutations' 7 | Vue.use(Vuex) 8 | 9 | export default new Vuex.Store({ 10 | state: state, 11 | mutations: mutations, 12 | actions: actions, 13 | getters: getters 14 | }) 15 | -------------------------------------------------------------------------------- /v2/src/api/config.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | export const BASE_URL = 'http://175.24.131.201:3400' 4 | 5 | // 默认超时设置 6 | axios.defaults.timeout = 10000 7 | 8 | // http request 拦截器 9 | axios.interceptors.response.use( 10 | response => { 11 | // 一些统一code的返回处理 12 | return response 13 | }, 14 | error => { 15 | return Promise.reject(error) 16 | } 17 | ) 18 | -------------------------------------------------------------------------------- /src/common/stylus/variable.styl: -------------------------------------------------------------------------------- 1 | // 颜色定义规范 2 | $color-background = #d43c33 3 | $color-highlight-background = #dd001b 4 | $color-theme = #333 5 | 6 | $color-sub-theme = #d93f30 7 | $color-text = #333 8 | $color-text-light = #444 9 | 10 | 11 | //字体定义规范 12 | $font-size-small-s = 10px 13 | $font-size-small = 12px 14 | $font-size-medium = 14px 15 | $font-size-medium-x = 16px 16 | $font-size-large = 18px 17 | $font-size-large-x = 22px -------------------------------------------------------------------------------- /v2/src/common/stylus/variable.styl: -------------------------------------------------------------------------------- 1 | // 颜色定义规范 2 | $color-background = #d43c33 3 | $color-highlight-background = #dd001b 4 | $color-theme = #333 5 | 6 | $color-sub-theme = #d93f30 7 | $color-text = #333 8 | $color-text-light = #444 9 | 10 | 11 | //字体定义规范 12 | $font-size-small-s = 10px 13 | $font-size-small = 12px 14 | $font-size-medium = 14px 15 | $font-size-medium-x = 16px 16 | $font-size-large = 18px 17 | $font-size-large-x = 22px -------------------------------------------------------------------------------- /v2/src/api/mv.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { BASE_URL } from './config' 3 | export function getMv (id) { 4 | return axios({ 5 | url: `${BASE_URL}/mv` 6 | }) 7 | } 8 | // 服务端逻辑 http://www.xbeibeix.com/api/bilibiliapi.php?url=https://www.bilibili.com/&aid=[视频AID]&cid=[视频CID] 9 | export function getVideoUrl (aid, cid) { 10 | return axios({ 11 | url: `${BASE_URL}/video?aid=${aid}&&cid=${cid}` 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /v2/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | 'extends': [ 7 | 'plugin:vue/essential', 8 | '@vue/standard' 9 | ], 10 | rules: { 11 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 12 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 13 | 'vue/require-v-for-key': 'off' 14 | }, 15 | parserOptions: { 16 | parser: 'babel-eslint' 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/common/stylus/mixin.styl: -------------------------------------------------------------------------------- 1 | // 背景图片 2 | bg-image($url) 3 | background-image: url($url + "@2x.png") 4 | @media (-webkit-min-device-pixel-ratio: 3),(min-device-pixel-ratio: 3) 5 | background-image: url($url + "@3x.png") 6 | 7 | // 不换行 8 | no-wrap() 9 | text-overflow: ellipsis 10 | overflow: hidden 11 | white-space: nowrap 12 | 13 | // 扩展点击区域 14 | extend-click() 15 | position: relative 16 | &:before 17 | content: '' 18 | position: absolute 19 | top: -10px 20 | left: -10px 21 | right: -10px 22 | bottom: -10px -------------------------------------------------------------------------------- /v2/src/App.vue: -------------------------------------------------------------------------------- 1 | 11 | 23 | 24 | -------------------------------------------------------------------------------- /v2/src/common/stylus/mixin.styl: -------------------------------------------------------------------------------- 1 | // 背景图片 2 | bg-image($url) 3 | background-image: url($url + "@2x.png") 4 | @media (-webkit-min-device-pixel-ratio: 3),(min-device-pixel-ratio: 3) 5 | background-image: url($url + "@3x.png") 6 | 7 | // 不换行 8 | no-wrap() 9 | text-overflow: ellipsis 10 | overflow: hidden 11 | white-space: nowrap 12 | 13 | // 扩展点击区域 14 | extend-click() 15 | position: relative 16 | &:before 17 | content: '' 18 | position: absolute 19 | top: -10px 20 | left: -10px 21 | right: -10px 22 | bottom: -10px -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | import store from './store' 5 | import lazyPlugin from 'vue3-lazy'; 6 | import loadingDirective from '@/components-base/loading/directive' 7 | 8 | import '@/common/stylus/index.styl' 9 | 10 | const app = createApp(App) 11 | 12 | app.use(store) 13 | app.use(router) 14 | app.use(lazyPlugin, { 15 | loading: require('@/common/image/default.png'), 16 | error: '' 17 | }) 18 | app.directive('loading', loadingDirective) 19 | 20 | app.mount('#app') 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/common/stylus/icon.styl: -------------------------------------------------------------------------------- 1 | 2 | // [class^="icon-"], [class*=" icon-"] 3 | // /* use !important to prevent issues with browser extensions that change fonts */ 4 | // font-family: 'music-icon' !important 5 | // speak: none 6 | // font-style: normal 7 | // font-weight: normal 8 | // font-variant: normal 9 | // text-transform: none 10 | // line-height: 1 11 | 12 | // /* Better Font Rendering =========== */ 13 | // -webkit-font-smoothing: antialiased 14 | // -moz-osx-font-smoothing: grayscale 15 | 16 | 17 | @import 'http://at.alicdn.com/t/font_1621469_al4hf3dkenk.css' -------------------------------------------------------------------------------- /v2/src/common/stylus/icon.styl: -------------------------------------------------------------------------------- 1 | 2 | // [class^="icon-"], [class*=" icon-"] 3 | // /* use !important to prevent issues with browser extensions that change fonts */ 4 | // font-family: 'music-icon' !important 5 | // speak: none 6 | // font-style: normal 7 | // font-weight: normal 8 | // font-variant: normal 9 | // text-transform: none 10 | // line-height: 1 11 | 12 | // /* Better Font Rendering =========== */ 13 | // -webkit-font-smoothing: antialiased 14 | // -moz-osx-font-smoothing: grayscale 15 | 16 | 17 | @import 'http://at.alicdn.com/t/font_1621469_zfmqpho57t.css' -------------------------------------------------------------------------------- /src/components-base/scroll/use-scroll.js: -------------------------------------------------------------------------------- 1 | import BScroll from '@better-scroll/core' 2 | import ObserveDOM from '@better-scroll/observe-dom' 3 | import {ref, onMounted, onUnmounted} from 'vue'; 4 | 5 | BScroll.use(ObserveDOM) //使用响应式dom插件 检测内部变化自动refresh 6 | 7 | export default function useScroll(wrapperRef,options){ 8 | const scroll = ref(null) 9 | onMounted(()=>{ 10 | scroll.value = new BScroll(wrapperRef.value,{ 11 | observeDOM: true, 12 | ...options 13 | }) 14 | }) 15 | onUnmounted(() => { 16 | scroll.value.destroy() 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /src/api/player/player.api.js: -------------------------------------------------------------------------------- 1 | import { Http } from '../http'; 2 | 3 | 4 | export class PlayerApi extends Http { 5 | 6 | async getSongPlayUrl(songmids) { 7 | const res = await this.get(`/song/urls?id=${songmids}`) 8 | return res 9 | } 10 | 11 | async getMvList() { 12 | const res = await this.get(`/mv/list`) 13 | return res 14 | } 15 | 16 | async getMv(mvid) { 17 | const res = await this.get(`/mv/url?id=${mvid}`) 18 | return res 19 | } 20 | 21 | async getComments(id) { 22 | const res = await this.get(`/comment?id=${id}&biztype=5`) 23 | return res 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/common/js/jsonp.js: -------------------------------------------------------------------------------- 1 | import originJsonp from 'jsonp' 2 | 3 | export default function jsonp2 (url, data, option) { 4 | url += (url.indexOf('?') < 0 ? '?' : '&') + param(data) 5 | 6 | return new Promise((resolve, reject) => { 7 | originJsonp(url, option, (err, data) => { 8 | if (!err) { 9 | resolve(data) 10 | } else { 11 | reject(err) 12 | } 13 | }) 14 | }) 15 | } 16 | 17 | export function param (data) { 18 | let url = '' 19 | for (var k in data) { 20 | let value = data[k] !== undefined ? data[k] : '' 21 | url += '&' + k + '=' + encodeURIComponent(value) 22 | } 23 | return url ? url.substring(1) : '' 24 | } 25 | -------------------------------------------------------------------------------- /v2/src/common/js/jsonp.js: -------------------------------------------------------------------------------- 1 | import originJsonp from 'jsonp' 2 | 3 | export default function jsonp2 (url, data, option) { 4 | url += (url.indexOf('?') < 0 ? '?' : '&') + param(data) 5 | 6 | return new Promise((resolve, reject) => { 7 | originJsonp(url, option, (err, data) => { 8 | if (!err) { 9 | resolve(data) 10 | } else { 11 | reject(err) 12 | } 13 | }) 14 | }) 15 | } 16 | 17 | export function param (data) { 18 | let url = '' 19 | for (var k in data) { 20 | let value = data[k] !== undefined ? data[k] : '' 21 | url += '&' + k + '=' + encodeURIComponent(value) 22 | } 23 | return url ? url.substring(1) : '' 24 | } 25 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | 10 | FreeMusic😊 11 | 12 | 13 | 14 | 18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /v2/src/api/home.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { BASE_URL } from './config' 3 | export function getNewSong () { 4 | return axios({ 5 | url: `${BASE_URL}/new/songs` 6 | }) 7 | } 8 | export function getDiscList () { 9 | return axios({ 10 | url: `${BASE_URL}/recommend/playlist` 11 | }) 12 | } 13 | export function getHotWords () { 14 | return axios({ 15 | url: `${BASE_URL}/new/hotwords` 16 | }) 17 | } 18 | /* 19 | 参数: 20 | keyword: 搜索关键词 必填 21 | type: 默认 song,支持:song, playlist, mv, singer, album, lyric 22 | pageno: 默认 1 23 | */ 24 | export function getHotWordsSearch (keyword) { 25 | return axios({ 26 | url: `${BASE_URL}/search?keyword=${keyword}` 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /v2/src/store/actions.js: -------------------------------------------------------------------------------- 1 | import { playMode } from '@/common/js/config' 2 | import { shuffle } from '@/common/js/util' 3 | 4 | export const selectPlay = function ({ commit, state }, { list, index }) { 5 | commit('SET_SEQUENCE_LIST', list) 6 | if (state.mode === playMode.random) { 7 | let randomList = shuffle(list) 8 | commit('SET_PLAYLIST', randomList) 9 | index = findIndex(randomList, list[index]) 10 | } else { 11 | commit('SET_PLAYLIST', list) 12 | } 13 | commit('SET_CURRENT_INDEX', index) 14 | commit('SET_FULLSCREEN', true) 15 | commit('SET_PLAYING_STATE', true) 16 | } 17 | 18 | function findIndex (list, song) { 19 | return list.findIndex((item) => { 20 | return item.id === song.id 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /v2/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | Music 9 | 10 | 11 | 12 | 13 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 9 | 23 | 33 | -------------------------------------------------------------------------------- /v2/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | import store from './store' 5 | // import fastclick from 'fastclick' 6 | import VueLazyload from 'vue-lazyload' 7 | 8 | import './registerServiceWorker' 9 | 10 | import '@/common/stylus/index.styl' 11 | 12 | // fastclick.attach(document.body) 13 | 14 | Vue.config.productionTip = false 15 | 16 | Vue.use(VueLazyload, { 17 | loading: require('@/common/image/default.png'), 18 | error: 'https://pcache.cmam.migu.cn/prod/cmam_music/storage_1/albummaterial/11004/000002/0127/7283/1100000000255111663/cffb35b03da540a78df6fc5dd50fd0d8_1372844695.jpg' 19 | }) 20 | 21 | new Vue({ 22 | router, 23 | store, 24 | render: h => h(App) 25 | }).$mount('#app') 26 | -------------------------------------------------------------------------------- /src/api/home/home.api.js: -------------------------------------------------------------------------------- 1 | import { Http } from '../http'; 2 | 3 | 4 | export class HomeApi extends Http { 5 | 6 | // 按分类推荐歌单 7 | async getRecommendList() { 8 | const res = await this.get('/recommend/playlist') 9 | return res 10 | } 11 | 12 | // 根据分类获取歌单 13 | async getNewSong() { 14 | const res = await this.get('/songlist/list?sort=2') 15 | return res 16 | } 17 | 18 | // 获取歌单详情 19 | async getSongList(id) { 20 | const res = await this.get(`/songlist?id=${id}`) 21 | return res 22 | } 23 | 24 | 25 | async getHotWordsSearch(key) { 26 | const res = await this.get(`/search?key=${key}`) 27 | return res 28 | } 29 | 30 | async getHotWords() { 31 | const res = await this.get('/search/hot') 32 | return res 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/store/getters.js: -------------------------------------------------------------------------------- 1 | export const disc = (state) => { 2 | return state.disc 3 | } 4 | 5 | export const playerIconColor = state => { 6 | return state.playerIconColor 7 | } 8 | 9 | export const fullScreen = state => { 10 | return state.fullScreen 11 | } 12 | 13 | export const currentIndex = state => state.currentIndex 14 | 15 | export const currentSong = (state) => { 16 | return state.playlist[state.currentIndex] || {} 17 | // 通过计算Index获取当前currentSong 修改index响应 类似计算属性 18 | // 如果没有url和封面信息 在player组件里请求后获取数据后 替换修改currentSong 19 | } 20 | export const playing = state => state.playing 21 | 22 | export const playlist = (state) => state.playlist 23 | 24 | export const mode = state => state.mode 25 | 26 | export const searchBoxFocue = state => state.searchBoxFocue 27 | -------------------------------------------------------------------------------- /v2/src/store/getters.js: -------------------------------------------------------------------------------- 1 | export const disc = (state) => { 2 | return state.disc 3 | } 4 | 5 | export const playerIconColor = state => { 6 | return state.playerIconColor 7 | } 8 | 9 | export const fullScreen = state => { 10 | return state.fullScreen 11 | } 12 | 13 | export const currentIndex = state => state.currentIndex 14 | 15 | export const currentSong = (state) => { 16 | return state.playlist[state.currentIndex] || {} 17 | // 通过计算Index获取当前currentSong 修改index响应 类似计算属性 18 | // 如果没有url和封面信息 在player组件里请求后获取数据后 替换修改currentSong 19 | } 20 | export const playing = state => state.playing 21 | 22 | export const playlist = (state) => state.playlist 23 | 24 | export const mode = state => state.mode 25 | 26 | export const searchBoxFocue = state => state.searchBoxFocue 27 | -------------------------------------------------------------------------------- /src/views/mv.vue: -------------------------------------------------------------------------------- 1 | 6 | 29 | 30 | 39 | -------------------------------------------------------------------------------- /v2/src/store/mutations.js: -------------------------------------------------------------------------------- 1 | const mutations = { 2 | SET_DISC (state, disc) { 3 | state.disc = disc 4 | }, 5 | SET_PLAY_ICON_COLOR (state, color) { 6 | state.playerIconColor = color 7 | }, 8 | SET_FULLSCREEN (state, fullState) { 9 | state.fullScreen = fullState 10 | }, 11 | SET_CURRENT_INDEX (state, index) { 12 | state.currentIndex = index 13 | }, 14 | SET_SEQUENCE_LIST (state, list) { 15 | state.sequenceList = list 16 | }, 17 | SET_PLAY_MODE (state, mode) { 18 | state.mode = mode 19 | }, 20 | SET_PLAYLIST (state, list) { 21 | state.playlist = list 22 | }, 23 | SET_PLAYING_STATE (state, flag) { 24 | state.playing = flag 25 | }, 26 | SET_SEARCHBOX_FOCUS (state, flag) { 27 | state.searchBoxFocue = flag 28 | } 29 | } 30 | export default mutations 31 | -------------------------------------------------------------------------------- /v2/src/common/js/util.js: -------------------------------------------------------------------------------- 1 | function getRandomInt (min, max) { 2 | return Math.floor(Math.random() * (max - min + 1) + min) 3 | } 4 | 5 | export function shuffle (arr) { 6 | let _arr = arr.slice() 7 | for (let i = 0; i < _arr.length; i++) { 8 | let j = getRandomInt(0, i) 9 | let t = _arr[i] 10 | _arr[i] = _arr[j] 11 | _arr[j] = t 12 | } 13 | return _arr 14 | } 15 | 16 | export function debounce (func, delay) { 17 | let timer 18 | return function (...args) { 19 | if (timer) { 20 | clearTimeout(timer) 21 | } 22 | timer = setTimeout(() => { 23 | func.apply(this, args) 24 | }, delay) 25 | } 26 | } 27 | export function isWeiXin () { 28 | var ua = window.navigator.userAgent.toLowerCase() 29 | if (ua.match(/micromessenger/i) == 'micromessenger') { // eslint-disable-line 30 | return true 31 | } else { 32 | return false 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /v2/src/views/mv.vue: -------------------------------------------------------------------------------- 1 | 6 | 30 | 31 | 41 | -------------------------------------------------------------------------------- /src/components-base/slider/use-slider.js: -------------------------------------------------------------------------------- 1 | import BScroll from '@better-scroll/core' 2 | import Slide from '@better-scroll/slide' 3 | import {onMounted,ref,onUnmounted} from 'vue'; 4 | 5 | BScroll.use(Slide) 6 | 7 | export default function useSlider(wrapperRef){ 8 | const slider = ref(null) 9 | const currentPageIndex = ref(0) 10 | 11 | onMounted(() => { 12 | slider.value = new BScroll(wrapperRef.value,{ 13 | scrollX: true, 14 | scrollY: false, 15 | slide: true, 16 | momentum: false, 17 | bounce: false, 18 | probeType: 3 19 | }) 20 | 21 | slider.value.on('slideWillChange', (page) => { 22 | currentPageIndex.value = page.pageX 23 | }) 24 | }) 25 | 26 | onUnmounted(() => { 27 | slider.value.destroy() 28 | }) 29 | 30 | return { 31 | slider, 32 | currentPageIndex 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "music", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build" 8 | }, 9 | "dependencies": { 10 | "@better-scroll/core": "^2.4.2", 11 | "@better-scroll/observe-dom": "^2.4.2", 12 | "@better-scroll/slide": "^2.4.2", 13 | "axios": "^0.21.4", 14 | "core-js": "^3.6.5", 15 | "jsonp": "^0.2.1", 16 | "qs": "^6.10.1", 17 | "vue": "^3.0.0", 18 | "vue-router": "^4.0.0-0", 19 | "vue3-lazy": "^1.0.0-alpha.1", 20 | "vuex": "^4.0.0-0" 21 | }, 22 | "devDependencies": { 23 | "@vue/cli-plugin-babel": "~4.5.0", 24 | "@vue/cli-plugin-router": "~4.5.0", 25 | "@vue/cli-plugin-vuex": "~4.5.0", 26 | "@vue/cli-service": "~4.5.0", 27 | "@vue/compiler-sfc": "^3.0.0", 28 | "style-resources-loader": "^1.4.1", 29 | "stylus": "^0.54.7", 30 | "stylus-loader": "^3.0.2" 31 | } 32 | } -------------------------------------------------------------------------------- /src/components-base/scroll/scroll.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 48 | 49 | 52 | -------------------------------------------------------------------------------- /src/store/mutations.js: -------------------------------------------------------------------------------- 1 | const mutations = { 2 | SET_DISC(state, disc) { 3 | state.disc = disc 4 | }, 5 | SET_PLAY_ICON_COLOR(state, color) { 6 | state.playerIconColor = color 7 | }, 8 | SET_FULLSCREEN(state, fullState) { 9 | state.fullScreen = fullState 10 | }, 11 | SET_CURRENT_INDEX(state, index) { 12 | state.currentIndex = index 13 | }, 14 | SAVE_CURRENT_SONG(state, item) { 15 | state.currentSong = item 16 | }, 17 | SET_SEQUENCE_LIST(state, list) { 18 | state.sequenceList = list 19 | }, 20 | SET_PLAY_MODE(state, mode) { 21 | state.mode = mode 22 | }, 23 | SET_PLAYLIST(state, list) { 24 | state.playlist = list 25 | }, 26 | SET_PLAYLIST_PUSH(state, item) { 27 | state.playlist.push(item) 28 | }, 29 | SET_PLAYING_STATE(state, flag) { 30 | state.playing = flag 31 | }, 32 | SET_SEARCHBOX_FOCUS(state, flag) { 33 | state.searchBoxFocue = flag 34 | }, 35 | DELETE_SONG(state, index) { 36 | 37 | } 38 | } 39 | export default mutations 40 | -------------------------------------------------------------------------------- /v2/src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import { register } from 'register-service-worker' 4 | 5 | if (process.env.NODE_ENV === 'production') { 6 | register(`${process.env.BASE_URL}service-worker.js`, { 7 | ready () { 8 | console.log( 9 | 'App is being served from cache by a service worker.\n' + 10 | 'For more details, visit https://goo.gl/AFskqB' 11 | ) 12 | }, 13 | registered () { 14 | console.log('Service worker has been registered.') 15 | }, 16 | cached () { 17 | console.log('Content has been cached for offline use.') 18 | }, 19 | updatefound () { 20 | console.log('New content is downloading.') 21 | }, 22 | updated () { 23 | console.log('New content is available; please refresh.') 24 | }, 25 | offline () { 26 | console.log('No internet connection found. App is running in offline mode.') 27 | }, 28 | error (error) { 29 | console.error('Error during service worker registration:', error) 30 | } 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /src/components-base/loading/directive.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import Loading from './loading' 3 | 4 | const loadingDirective = { 5 | mounted(el,binding) { 6 | const app = createApp() 7 | const instance = app.mount(document.createElement('div')) 8 | 9 | binding.value.title && instance.setTitle(binding.value.title) 10 | binding.value.color && instance.setColor(binding.value.color) 11 | 12 | el.instance = instance // 存一个 13 | 14 | if(binding.value.loading){ 15 | append(el) 16 | } 17 | }, 18 | updated(el,binding) { 19 | binding.value.title && el.instance.setTitle(binding.value.title) 20 | binding.value.color && el.instance.setColor(binding.value.color) 21 | 22 | if(binding.value.loading !== binding.oldValue.loading){ 23 | binding.value.loading? append(el) : remove(el) 24 | } 25 | }, 26 | } 27 | 28 | function append(el){ 29 | el.appendChild(el.instance.$el) 30 | } 31 | 32 | function remove(el){ 33 | el.removeChild(el.instance.$el) 34 | } 35 | 36 | export default loadingDirective 37 | -------------------------------------------------------------------------------- /v2/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | import Home from '../views/Home.vue' 4 | import Disc from '@/views/Disc.vue' 5 | import Mv from '@/views/mv.vue' 6 | import My from '@/views/my.vue' 7 | import Singer from '@/views/singer.vue' 8 | import singerHome from '@/views/singerHome.vue' 9 | Vue.use(VueRouter) 10 | 11 | const routes = [ 12 | { 13 | path: '/', 14 | redirect: '/home' 15 | }, 16 | { 17 | path: '/home', 18 | name: 'home', 19 | component: Home, 20 | children: [ 21 | { 22 | path: ':id', 23 | component: Disc 24 | } 25 | ] 26 | }, 27 | { 28 | path: '/mv', 29 | name: 'mv', 30 | component: Mv 31 | }, 32 | { 33 | path: '/my', 34 | name: 'my', 35 | component: My 36 | }, 37 | { 38 | path: '/singer', 39 | component: Singer, 40 | children: [ 41 | { 42 | path: '/singer/singerHome', 43 | name: 'singerHome', 44 | component: singerHome 45 | } 46 | ] 47 | } 48 | ] 49 | 50 | const router = new VueRouter({ 51 | routes 52 | }) 53 | 54 | export default router 55 | -------------------------------------------------------------------------------- /v2/vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | publicPath: '/music/', 3 | outputDir: 'music', 4 | assetsDir: './static', 5 | // ...other vue-cli plugin options... 6 | pwa: { 7 | name: 'Music', 8 | themeColor: '#4DBA87', 9 | msTileColor: '#ffffff', 10 | appleMobileWebAppCapable: 'yes', 11 | appleMobileWebAppStatusBarStyle: 'white', 12 | iconPaths: { 13 | appleTouchIcon: 'public/img/icons/apple-touch-icon-152x152.png', 14 | maskIcon: 'public/img/icons/safari-pinned-tab.svg', 15 | msTileImage: 'public/img/icons/msapplication-icon-144x144.png' 16 | }, 17 | 18 | // configure the workbox plugin 19 | workboxPluginMode: 'InjectManifest', 20 | workboxOptions: { 21 | // swSrc is required in InjectManifest mode. 22 | swSrc: 'src/registerServiceWorker.js' 23 | // ...other Workbox options... 24 | } 25 | }, 26 | devServer: { 27 | open: process.platform === 'darwin', 28 | // host: '192.168.199.163', 29 | port: 8082, 30 | https: false, 31 | hotOnly: false, 32 | // proxy: {}, // 设置代理 33 | before: app => {} 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /v2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "music-for-the-poor", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "@babel/core": "^7.13.15", 12 | "@babel/preset-env": "^7.13.15", 13 | "axios": "^0.19.1", 14 | "better-scroll": "^1.15.2", 15 | "core-js": "^3.4.4", 16 | "fastclick": "^1.0.6", 17 | "jsonp": "^0.2.1", 18 | "register-service-worker": "^1.6.2", 19 | "vue": "^2.6.10", 20 | "vue-router": "^3.1.3", 21 | "vue-toasted": "^1.1.27", 22 | "vuex": "^3.1.2" 23 | }, 24 | "devDependencies": { 25 | "@vue/cli-plugin-babel": "^4.1.0", 26 | "@vue/cli-plugin-eslint": "^4.1.0", 27 | "@vue/cli-plugin-pwa": "^4.1.0", 28 | "@vue/cli-service": "^4.1.0", 29 | "@vue/eslint-config-standard": "^4.0.0", 30 | "babel-eslint": "^10.0.3", 31 | "eslint": "^5.16.0", 32 | "eslint-plugin-vue": "^5.0.0", 33 | "stylus": "^0.54.7", 34 | "stylus-loader": "^3.0.2", 35 | "vue-lazyload": "^1.3.3", 36 | "vue-template-compiler": "^2.6.10" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router' 2 | import Home from '../views/Home.vue' 3 | import Disc from '../views/Disc.vue' 4 | import Mv from '../views/mv' 5 | import My from '../views/my' 6 | const routes = [ 7 | { 8 | path: '/', 9 | redirect: '/home' 10 | }, 11 | { 12 | path: '/home', 13 | name: 'Home', 14 | component: Home, 15 | children: [ 16 | { 17 | path: '/home/disc/:id', 18 | name: 'disc', 19 | component: Disc 20 | } 21 | ] 22 | }, 23 | { 24 | path: '/mv', 25 | name: 'mv', 26 | component: Mv 27 | }, 28 | { 29 | path: '/my', 30 | name: 'my', 31 | component: My 32 | }, 33 | // { 34 | // path: '/singer', 35 | // component: Singer, 36 | // children: [ 37 | // // { 38 | // // path: '/singer/singerHome', 39 | // // name: 'singerHome', 40 | // // component: () => import('../views/singerHome.vue') 41 | // // } 42 | // ] 43 | // } 44 | ] 45 | 46 | const router = createRouter({ 47 | history: createWebHistory(process.env.BASE_URL), 48 | routes 49 | }) 50 | 51 | export default router 52 | -------------------------------------------------------------------------------- /src/common/js/dom.js: -------------------------------------------------------------------------------- 1 | export function hasClass (el, className) { 2 | let reg = new RegExp('(^|\\s)' + className + '(\\s|$)') 3 | return reg.test(el.className) 4 | } 5 | 6 | export function addClass (el, className) { 7 | if (hasClass(el, className)) { 8 | return 9 | } 10 | 11 | let newClass = el.className.split(' ') 12 | newClass.push(className) 13 | el.className = newClass.join(' ') 14 | } 15 | /* 如果有val 则为设置属性值 */ 16 | export function getData (el, name, val) { 17 | const prefix = 'data-' 18 | name = prefix + name 19 | if (val) { 20 | return el.setAttribute(name, val) 21 | } else { 22 | return el.getAttribute(name) 23 | } 24 | } 25 | 26 | let elementStyle = document.createElement('div').style 27 | 28 | let vendor = (() => { 29 | let transformNames = { 30 | webkit: 'webkitTransform', 31 | Moz: 'MozTransform', 32 | O: 'OTransform', 33 | ms: 'msTransform', 34 | standard: 'transform' 35 | } 36 | 37 | for (let key in transformNames) { 38 | if (elementStyle[transformNames[key]] !== undefined) { 39 | return key 40 | } 41 | } 42 | 43 | return false 44 | })() 45 | 46 | export function prefixStyle (style) { 47 | if (vendor === false) { 48 | return false 49 | } 50 | 51 | if (vendor === 'standard') { 52 | return style 53 | } 54 | 55 | return vendor + style.charAt(0).toUpperCase() + style.substr(1) 56 | } 57 | -------------------------------------------------------------------------------- /v2/src/common/js/dom.js: -------------------------------------------------------------------------------- 1 | export function hasClass (el, className) { 2 | let reg = new RegExp('(^|\\s)' + className + '(\\s|$)') 3 | return reg.test(el.className) 4 | } 5 | 6 | export function addClass (el, className) { 7 | if (hasClass(el, className)) { 8 | return 9 | } 10 | 11 | let newClass = el.className.split(' ') 12 | newClass.push(className) 13 | el.className = newClass.join(' ') 14 | } 15 | /* 如果有val 则为设置属性值 */ 16 | export function getData (el, name, val) { 17 | const prefix = 'data-' 18 | name = prefix + name 19 | if (val) { 20 | return el.setAttribute(name, val) 21 | } else { 22 | return el.getAttribute(name) 23 | } 24 | } 25 | 26 | let elementStyle = document.createElement('div').style 27 | 28 | let vendor = (() => { 29 | let transformNames = { 30 | webkit: 'webkitTransform', 31 | Moz: 'MozTransform', 32 | O: 'OTransform', 33 | ms: 'msTransform', 34 | standard: 'transform' 35 | } 36 | 37 | for (let key in transformNames) { 38 | if (elementStyle[transformNames[key]] !== undefined) { 39 | return key 40 | } 41 | } 42 | 43 | return false 44 | })() 45 | 46 | export function prefixStyle (style) { 47 | if (vendor === false) { 48 | return false 49 | } 50 | 51 | if (vendor === 'standard') { 52 | return style 53 | } 54 | 55 | return vendor + style.charAt(0).toUpperCase() + style.substr(1) 56 | } 57 | -------------------------------------------------------------------------------- /src/common/stylus/reset.styl: -------------------------------------------------------------------------------- 1 | /** 2 | * Eric Meyer's Reset CSS v2.0 (http://meyerweb.com/eric/tools/css/reset/) 3 | * http://cssreset.com 4 | */ 5 | html, body, div, span, applet, object, iframe, 6 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 7 | a, abbr, acronym, address, big, cite, code, 8 | del, dfn, em, img, ins, kbd, q, s, samp, 9 | small, strike, strong, sub, sup, tt, var, 10 | b, u, i, center, 11 | dl, dt, dd, ol, ul, li, 12 | fieldset, form, label, legend, 13 | table, caption, tbody, tfoot, thead, tr, th, td, 14 | article, aside, canvas, details, embed, 15 | figure, figcaption, footer, header, 16 | menu, nav, output, ruby, section, summary, 17 | time, mark, audio, video, input 18 | margin: 0 19 | padding: 0 20 | border: 0 21 | font-size: 100% 22 | font-weight: normal 23 | vertical-align: baseline 24 | 25 | /* HTML5 display-role reset for older browsers */ 26 | article, aside, details, figcaption, figure, 27 | footer, header, menu, nav, section 28 | display: block 29 | 30 | body 31 | line-height: 1 32 | 33 | blockquote, q 34 | quotes: none 35 | 36 | blockquote:before, blockquote:after, 37 | q:before, q:after 38 | content: none 39 | 40 | table 41 | border-collapse: collapse 42 | border-spacing: 0 43 | 44 | /* custom */ 45 | 46 | a 47 | color: #7e8c8d 48 | -webkit-backface-visibility: hidden 49 | text-decoration: none 50 | 51 | li 52 | list-style: none 53 | 54 | body 55 | -webkit-text-size-adjust: none 56 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0) 57 | -------------------------------------------------------------------------------- /v2/src/common/stylus/reset.styl: -------------------------------------------------------------------------------- 1 | /** 2 | * Eric Meyer's Reset CSS v2.0 (http://meyerweb.com/eric/tools/css/reset/) 3 | * http://cssreset.com 4 | */ 5 | html, body, div, span, applet, object, iframe, 6 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 7 | a, abbr, acronym, address, big, cite, code, 8 | del, dfn, em, img, ins, kbd, q, s, samp, 9 | small, strike, strong, sub, sup, tt, var, 10 | b, u, i, center, 11 | dl, dt, dd, ol, ul, li, 12 | fieldset, form, label, legend, 13 | table, caption, tbody, tfoot, thead, tr, th, td, 14 | article, aside, canvas, details, embed, 15 | figure, figcaption, footer, header, 16 | menu, nav, output, ruby, section, summary, 17 | time, mark, audio, video, input 18 | margin: 0 19 | padding: 0 20 | border: 0 21 | font-size: 100% 22 | font-weight: normal 23 | vertical-align: baseline 24 | 25 | /* HTML5 display-role reset for older browsers */ 26 | article, aside, details, figcaption, figure, 27 | footer, header, menu, nav, section 28 | display: block 29 | 30 | body 31 | line-height: 1 32 | 33 | blockquote, q 34 | quotes: none 35 | 36 | blockquote:before, blockquote:after, 37 | q:before, q:after 38 | content: none 39 | 40 | table 41 | border-collapse: collapse 42 | border-spacing: 0 43 | 44 | /* custom */ 45 | 46 | a 47 | color: #7e8c8d 48 | -webkit-backface-visibility: hidden 49 | text-decoration: none 50 | 51 | li 52 | list-style: none 53 | 54 | body 55 | -webkit-text-size-adjust: none 56 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0) 57 | -------------------------------------------------------------------------------- /v2/src/components/nav/nav.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 35 | 61 | -------------------------------------------------------------------------------- /v2/src/views/Disc.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 60 | 61 | 68 | -------------------------------------------------------------------------------- /v2/src/components/song-list/song-list.vue: -------------------------------------------------------------------------------- 1 | 14 | 39 | 59 | -------------------------------------------------------------------------------- /src/store/actions.js: -------------------------------------------------------------------------------- 1 | import { playMode } from '@/common/js/config' 2 | import { shuffle } from '@/common/js/util' 3 | 4 | // export const selectPlay = function ({ commit, state }, { list, index }) { 5 | // commit('SET_SEQUENCE_LIST', list) 6 | // if (state.mode === playMode.random) { 7 | // let randomList = shuffle(list) 8 | // commit('SET_PLAYLIST', randomList) 9 | // index = findIndex(randomList, list[index]) 10 | // } else { 11 | // commit('SET_PLAYLIST', list) 12 | // } 13 | // commit('SET_CURRENT_INDEX', index) 14 | // commit('SET_FULLSCREEN', true) 15 | // commit('SET_PLAYING_STATE', true) 16 | // } 17 | 18 | export const selectPlay = async function ({ commit, dispatch, state }, { item }) { 19 | 20 | let index = findIndex(state.playlist, item) 21 | if (index == -1) { 22 | // 不存在 23 | let list = JSON.parse(JSON.stringify(state.playlist)) 24 | list.push(item) 25 | commit('SET_SEQUENCE_LIST', list) 26 | commit('SET_PLAYLIST', list) 27 | commit('SET_CURRENT_INDEX', list.length - 1) 28 | } else { 29 | commit('SET_CURRENT_INDEX', index) 30 | } 31 | commit('SET_PLAYING_STATE', true) 32 | commit('SET_FULLSCREEN', true) 33 | 34 | // commit('SET_SEQUENCE_LIST', list) 35 | // if (state.mode === playMode.random) { 36 | // let randomList = shuffle(list) 37 | // commit('SET_PLAYLIST', randomList) 38 | // index = findIndex(randomList, list[index]) 39 | // } else { 40 | // commit('SET_PLAYLIST', list) 41 | // } 42 | // commit('SET_CURRENT_INDEX', index) 43 | // commit('SET_FULLSCREEN', true) 44 | // commit('SET_PLAYING_STATE', true) 45 | } 46 | 47 | function findIndex(list, song) { 48 | return list.findIndex((item) => { 49 | return item.songmid === song.songmid 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /src/views/Disc.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 58 | 59 | 68 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | publicPath: '/music/', 5 | outputDir: 'music', 6 | assetsDir: './static', 7 | // ...other vue-cli plugin options... 8 | pwa: { 9 | name: 'FreeMusic', 10 | themeColor: '#4DBA87', 11 | msTileColor: '#ffffff', 12 | appleMobileWebAppCapable: 'yes', 13 | appleMobileWebAppStatusBarStyle: 'white', 14 | iconPaths: { 15 | appleTouchIcon: 'public/img/icons/apple-touch-icon-152x152.png', 16 | maskIcon: 'public/img/icons/safari-pinned-tab.svg', 17 | msTileImage: 'public/img/icons/msapplication-icon-144x144.png' 18 | }, 19 | 20 | // configure the workbox plugin 21 | workboxPluginMode: 'InjectManifest', 22 | workboxOptions: { 23 | // swSrc is required in InjectManifest mode. 24 | swSrc: 'src/registerServiceWorker.js' 25 | // ...other Workbox options... 26 | } 27 | }, 28 | devServer: { 29 | open: process.platform === 'darwin', 30 | // host: '192.168.199.163', 31 | port: 8082, 32 | https: false, 33 | hotOnly: false, 34 | // proxy: {}, // 设置代理 35 | before: app => { } 36 | }, 37 | // 配置使用stylus全局变量 38 | chainWebpack: config => { 39 | const types = ["vue-modules", "vue", "normal-modules", "normal"]; 40 | types.forEach(type => 41 | addStyleResource(config.module.rule("stylus").oneOf(type)) 42 | ); 43 | } 44 | } 45 | 46 | 47 | // 定义函数addStyleResource 48 | 49 | function addStyleResource(rule) { 50 | rule.use("style-resource") 51 | .loader("style-resources-loader") 52 | .options({ 53 | patterns: [ 54 | path.resolve(__dirname, "./src/common/stylus/variable.styl"), 55 | path.resolve(__dirname, "./src/common/stylus/mixin.styl"), 56 | ] 57 | //后面的路径改成你自己放公共stylus变量的路径 58 | }); 59 | } 60 | -------------------------------------------------------------------------------- /v2/src/base/mv/mv.vue: -------------------------------------------------------------------------------- 1 | 17 | 50 | 75 | -------------------------------------------------------------------------------- /src/common/js/util.js: -------------------------------------------------------------------------------- 1 | function getRandomInt(min, max) { 2 | return Math.floor(Math.random() * (max - min + 1) + min) 3 | } 4 | 5 | export function shuffle(arr) { 6 | let _arr = arr.slice() 7 | for (let i = 0; i < _arr.length; i++) { 8 | let j = getRandomInt(0, i) 9 | let t = _arr[i] 10 | _arr[i] = _arr[j] 11 | _arr[j] = t 12 | } 13 | return _arr 14 | } 15 | 16 | export function debounce(func, delay) { 17 | let timer 18 | return function (...args) { 19 | if (timer) { 20 | clearTimeout(timer) 21 | } 22 | timer = setTimeout(() => { 23 | func.apply(this, args) 24 | }, delay) 25 | } 26 | } 27 | export function isWeiXin() { 28 | var ua = window.navigator.userAgent.toLowerCase() 29 | if (ua.match(/micromessenger/i) == 'micromessenger') { // eslint-disable-line 30 | return true 31 | } else { 32 | return false 33 | } 34 | } 35 | 36 | export const isIphoneX = () => { 37 | if (/iphone/gi.test(window.navigator.userAgent)) { 38 | let x = (window.screen.width === 375 && window.screen.height === 812); 39 | let xsMax = (window.screen.width === 414 && window.screen.height === 896); 40 | let xR = (window.screen.width === 414 && window.screen.height === 896); 41 | if (x || xsMax || xR) { 42 | return true; 43 | } else { 44 | return false; 45 | } 46 | } else { 47 | return false 48 | } 49 | } 50 | 51 | export function IsPC() { 52 | var userAgentInfo = navigator.userAgent; 53 | var Agents = new Array("Android", "iPhone", "SymbianOS", "Windows Phone", "iPad", "iPod"); 54 | var flag = true; 55 | for (var v = 0; v < Agents.length; v++) { 56 | if (userAgentInfo.indexOf(Agents[v]) > 0) { 57 | flag = false; 58 | break; 59 | } 60 | } 61 | return flag; 62 | } -------------------------------------------------------------------------------- /src/api/http.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import qs from 'qs'; 3 | import { HOST } from './config'; 4 | 5 | 6 | axios.interceptors.response.use( 7 | response => { 8 | if (response.data) { 9 | return response; 10 | } 11 | return Promise.reject(response); 12 | }, 13 | error => { 14 | return Promise.reject(error.response); 15 | }, 16 | ); 17 | 18 | export class Http { 19 | 20 | async request(config) { 21 | let response; 22 | 23 | try { 24 | response = await axios( 25 | { 26 | ...config, 27 | url: HOST + config.url, 28 | } 29 | ); 30 | return response.data; 31 | } catch (error) { 32 | console.log(error) 33 | } 34 | } 35 | 36 | async get(url, params) { 37 | const config = { 38 | method: 'GET', 39 | url, 40 | params, 41 | }; 42 | return this.request(config); 43 | } 44 | 45 | async post(url, params) { 46 | const config = { 47 | method: 'POST', 48 | url, 49 | data: params, 50 | }; 51 | return this.request(config); 52 | } 53 | 54 | formPost(url, params) { 55 | const config = { 56 | method: 'POST', 57 | url, 58 | data: qs.stringify(params), 59 | }; 60 | return this.request(config); 61 | } 62 | 63 | patch(url, params) { 64 | const config = { 65 | method: 'PATCH', 66 | url, 67 | data: params, 68 | }; 69 | return this.request(config); 70 | } 71 | 72 | 73 | put(url, params) { 74 | const config = { 75 | method: 'PUT', 76 | url, 77 | data: params, 78 | }; 79 | return this.request(config); 80 | } 81 | 82 | delete(url, params) { 83 | const config = { 84 | method: 'DELETE', 85 | url, 86 | data: params, 87 | }; 88 | return this.request(config); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/components-base/mv/mv.vue: -------------------------------------------------------------------------------- 1 | 17 | 49 | 80 | -------------------------------------------------------------------------------- /src/components/nav/nav.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 40 | 78 | -------------------------------------------------------------------------------- /src/components/song-list/song-list.vue: -------------------------------------------------------------------------------- 1 | 14 | 48 | 78 | -------------------------------------------------------------------------------- /src/common/js/song.js: -------------------------------------------------------------------------------- 1 | // import { getLyric, getSongsUrl } from 'api/song' 2 | // import { ERR_OK } from 'api/config' 3 | // import { Base64 } from 'js-base64' 4 | 5 | export default class Song { 6 | constructor ({ id, cid, singer, name, image, url }) { 7 | this.id = id 8 | this.cid = cid 9 | this.singer = singer 10 | this.name = name 11 | this.image = image 12 | this.url = url 13 | } 14 | 15 | // getLyric () { 16 | // if (this.lyric) { 17 | // return Promise.resolve(this.lyric) 18 | // } 19 | 20 | // return new Promise((resolve, reject) => { 21 | // getLyric(this.mid).then((res) => { 22 | // if (res.retcode === ERR_OK) { 23 | // this.lyric = Base64.decode(res.lyric) 24 | // resolve(this.lyric) 25 | // } else { 26 | // reject(new Error('no lyric')) 27 | // } 28 | // }) 29 | // }) 30 | // } 31 | } 32 | 33 | export function createSong (id, cid, singer, name, image, url) { 34 | return new Song({ 35 | id: id, 36 | cid: cid, 37 | singer: singer, 38 | name: name, 39 | image: image, 40 | url: url 41 | }) 42 | } 43 | export function filterSinger (singer) { 44 | let ret = [] 45 | if (!singer) { 46 | return '' 47 | } 48 | singer.forEach((s) => { 49 | ret.push(s.name) 50 | }) 51 | return ret.join('/') 52 | } 53 | 54 | // export function isValidMusic (musicData) { 55 | // return musicData.songid && musicData.albummid && (!musicData.pay || musicData.pay.payalbumprice === 0) 56 | // } 57 | 58 | // export function processSongsUrl (songs) { 59 | // if (!songs.length) { 60 | // return Promise.resolve(songs) 61 | // } 62 | // return getSongsUrl(songs).then((purlMap) => { 63 | // songs = songs.filter((song) => { 64 | // const purl = purlMap[song.mid] 65 | // if (purl) { 66 | // song.url = purl.indexOf('http') === -1 ? `http://dl.stream.qqmusic.qq.com/${purl}` : purl 67 | // return true 68 | // } 69 | // return false 70 | // }) 71 | // return songs 72 | // }) 73 | // } 74 | -------------------------------------------------------------------------------- /v2/README.md: -------------------------------------------------------------------------------- 1 | # Music For The Poor 2 | 3 | ## 更新: 4 | 5 | --2020.09.17:Vue3已经进入RFC了,估计没几个月就要正式发布了,目前本项目正在使用vue3升级重构中~ 提前感受一波Vue3 6 | 7 | ## 简介: 8 | 打造精美音乐WebAppp,提供优雅的用户体验,且能听付费歌曲(比如周杰伦等),为祖国2020全面脱贫实现小康社会尽一份自己力量,打赢这场脱贫攻坚战。精准扶贫,让穷人也能听到好的音乐,让穷人省下一笔钱来脱贫,愿天下没有穷人! 9 | 10 | [在线体验地址](http://www.iamzfj.cn/music) 11 | 12 | 扫描二维码:![](https://upload-images.jianshu.io/upload_images/2514755-755f1985ad057630.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 13 | 14 | 15 | 当然,以上是开玩笑的,主要还是造轮子玩。 16 | 17 | ## Build Setup 18 | 19 | ``` bash 20 | npm i //安装依赖 21 | npm run dev //开发 22 | npm run build //打包 23 | ``` 24 | ### 项目介绍(为了减少gif大小,掉帧严重,优雅的交互体现不出来==) 25 | 26 | ### 一、首页 27 | 28 | 首页主要由顶部搜索和推荐歌单与推荐单曲构成,图片部分应用了懒加载。 29 | 30 | ![](https://s2.ax1x.com/2020/02/03/10slfs.gif) 31 | 32 | ### 二、 音乐播放器 33 | 34 | 音乐播放器有两种形态,一种mini的小图标,在没有播放或暂停时为静止的icon,当有播放音乐时,为一个有节奏跳动的音律动效,点击后进去播放界面,在缓冲时,音频资源未ready时 ,进度条为一个小小的转圈的loadng,准备就绪,可以拖动进度条对音乐前进或后退,下一首上一首操作播放列表。 35 | 36 | 下一步接着要做的便是歌词同步~ 37 | 38 | ![](https://s2.ax1x.com/2020/02/03/102dzj.png) 39 | 40 | ### 三、搜索 41 | 42 | 首页搜索功能 43 | 44 | ![](https://s2.ax1x.com/2020/02/03/10sqHS.gif) 45 | 46 | ### 四、歌手页面 47 | 48 | 歌手页面,左右可以滑动,当点击歌手时,前往歌手主页,歌手主页下有歌曲列表,上拉加载更多功能, 49 | 50 | ![](https://s2.ax1x.com/2020/02/03/10yAN4.gif) 51 | 52 | ### 一、Mv 53 | 54 | 此处解析了哔哩哔哩(bilibili)音乐区的推荐(演奏类),都是非常的棒的视频,同时费劲周折解析了B站视频的高清视频原地址(1080p),很棒的噢~ 55 | 56 | 此处后期可以考虑扩展封装一个播放器组件,目前只是一个简单的播放基础功能。也是一个很有趣的事情哦~~ 欢迎大家来一起参与造轮子~ 57 | 58 | ![](https://s2.ax1x.com/2020/02/03/106Ni4.gif) 59 | 60 | ### 五、我的 61 | 62 | 此处待开发... 63 | 64 | ![](https://s2.ax1x.com/2020/02/03/10gRKI.png) 65 | 66 | 67 | ### 未来规划和展望 68 | 目前武汉新型肺炎爆发,适逢春节,国家号召宅在家里,口罩难求,怕是怕死懒得出门,一直在家里做该项目,进度飞快,这个项目的核心已经完成,但是还是有很多扩展的余地。关于未来的规划,我是这么安排的: 69 | 70 | - 音乐player播放器 增加歌词等扩展功能 71 | - 视频播放器 72 | - 完成收藏、播放历史功能 73 | - 实现MV模块 评论页更优雅的交互 74 | - 同时撰写拆解文章 75 | - “我的”待开发 76 | - 未来更多功能待补充... 77 | 78 | 这个项目长期维护,希望大家踊跃提issue和pr,把这个项目打造得更加完美,帮助到更多的Vue开发者。 79 | 80 | 最后的最后,万水千山总是情,给个star行不行(你回头也好找这个项目呀 (*^_^*)) 81 | 82 | ![](https://s2.ax1x.com/2020/02/03/10h39S.gif) 83 | 84 | # 声明 85 | 本项目代码仅用学习交流, 请勿商业使用。 86 | -------------------------------------------------------------------------------- /v2/src/common/js/song.js: -------------------------------------------------------------------------------- 1 | // import { getLyric, getSongsUrl } from 'api/song' 2 | // import { ERR_OK } from 'api/config' 3 | // import { Base64 } from 'js-base64' 4 | 5 | export default class Song { 6 | constructor ({ id, cid, singer, name, image, url }) { 7 | this.id = id 8 | this.cid = cid 9 | this.singer = singer 10 | this.name = name 11 | this.image = image 12 | this.url = url 13 | } 14 | 15 | // getLyric () { 16 | // if (this.lyric) { 17 | // return Promise.resolve(this.lyric) 18 | // } 19 | 20 | // return new Promise((resolve, reject) => { 21 | // getLyric(this.mid).then((res) => { 22 | // if (res.retcode === ERR_OK) { 23 | // this.lyric = Base64.decode(res.lyric) 24 | // resolve(this.lyric) 25 | // } else { 26 | // reject(new Error('no lyric')) 27 | // } 28 | // }) 29 | // }) 30 | // } 31 | } 32 | 33 | export function createSong (id, cid, singer, name, image, url) { 34 | return new Song({ 35 | id: id, 36 | cid: cid, 37 | singer: singer, 38 | name: name, 39 | image: image, 40 | url: url 41 | }) 42 | } 43 | export function filterSinger (singer) { 44 | let ret = [] 45 | if (!singer) { 46 | return '' 47 | } 48 | singer.forEach((s) => { 49 | ret.push(s.name) 50 | }) 51 | return ret.join('/') 52 | } 53 | 54 | // export function isValidMusic (musicData) { 55 | // return musicData.songid && musicData.albummid && (!musicData.pay || musicData.pay.payalbumprice === 0) 56 | // } 57 | 58 | // export function processSongsUrl (songs) { 59 | // if (!songs.length) { 60 | // return Promise.resolve(songs) 61 | // } 62 | // return getSongsUrl(songs).then((purlMap) => { 63 | // songs = songs.filter((song) => { 64 | // const purl = purlMap[song.mid] 65 | // if (purl) { 66 | // song.url = purl.indexOf('http') === -1 ? `http://dl.stream.qqmusic.qq.com/${purl}` : purl 67 | // return true 68 | // } 69 | // return false 70 | // }) 71 | // return songs 72 | // }) 73 | // } 74 | -------------------------------------------------------------------------------- /v2/src/base/loading/loading.vue: -------------------------------------------------------------------------------- 1 | 9 | 33 | 93 | -------------------------------------------------------------------------------- /v2/src/views/singerHome.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 74 | 75 | 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Music For The Poor 2 | 3 | ## 2021.09.24 更新 4 | - 1.已经从vue2.x更新vue3.x,并基于vue3的composition Api 抽离了可复用逻辑,做完这个项目,既有了react hooks的灵活也有了vue的直观,Interesting~ 5 | - 2.切换了音乐Api,修改了解析接口并加入了我自己的登录信息(尊贵的vip会员😄)以获取付费资源; 6 | - 3.一小部分部音乐获取到的播放歌曲依然有问题,tm的QQ音乐的规则老换,没那么多时间去研究,顶不住了😭... 等他规则变动不那么频繁了,回来看看; 7 | - 4.移除了没啥卵用的模块,添加了下载音乐资源的功能,改进了mv模版的交换体验,体验更棒啦; 8 | 9 | ## 2020.09.17更新 10 | - Vue3已经进入RFC了,估计没几个月就要正式发布了,目前本项目正在使用vue3升级重构中~,已重构部分 提前感受一波Vue3 11 | 12 | ## 简介: 13 | 打造精美音乐WebAppp,提供优雅的用户体验,且能听付费歌曲(比如周杰伦等),为祖国2020全面脱贫实现小康社会尽一份自己力量,打赢这场脱贫攻坚战。精准扶贫,让穷人也能听到好的音乐,让穷人省下一笔钱来脱贫,愿天下没有穷人! 14 | 15 | [在线体验地址](http://www.iamzfj.cn/music) 16 | 17 | 扫描二维码:![](https://upload-images.jianshu.io/upload_images/2514755-755f1985ad057630.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 18 | 19 | 20 | 当然,以上是开玩笑的,主要还是造轮子玩。 21 | 22 | ## Build Setup 23 | 24 | ``` bash 25 | npm i //安装依赖 26 | npm run serve //开发 27 | npm run build //打包, 记得修改你的publicPath 28 | ``` 29 | ### 项目介绍(为了减少gif大小,掉帧严重,优雅的交互体现不出来==) 30 | 31 | ### 一、首页 32 | 33 | 首页主要由顶部搜索和推荐歌单与推荐单曲构成,图片部分应用了懒加载。 34 | 35 | ![](https://s2.ax1x.com/2020/02/03/10slfs.gif) 36 | 37 | ### 二、 音乐播放器 38 | 39 | 音乐播放器有两种形态,一种mini的小图标,在没有播放或暂停时为静止的icon,当有播放音乐时,为一个有节奏跳动的音律动效,点击后进去播放界面,在缓冲时,音频资源未ready时 ,进度条为一个小小的转圈的loadng,准备就绪,可以拖动进度条对音乐前进或后退,下一首上一首操作播放列表。 40 | 41 | 下一步接着要做的便是歌词同步~ 42 | 43 | ![](https://s2.ax1x.com/2020/02/03/102dzj.png) 44 | 45 | ### 三、搜索 46 | 47 | 首页搜索功能 48 | 49 | ![](https://s2.ax1x.com/2020/02/03/10sqHS.gif) 50 | 51 | ### 四、歌手页面 52 | 53 | 歌手页面,左右可以滑动,当点击歌手时,前往歌手主页,歌手主页下有歌曲列表,上拉加载更多功能, 54 | 55 | ![](https://s2.ax1x.com/2020/02/03/10yAN4.gif) 56 | 57 | ### 一、Mv 58 | 59 | 此处解析了哔哩哔哩(bilibili)音乐区的推荐(演奏类),都是非常的棒的视频,同时费劲周折解析了B站视频的高清视频原地址(1080p),很棒的噢~ 60 | 61 | 此处后期可以考虑扩展封装一个播放器组件,目前只是一个简单的播放基础功能。也是一个很有趣的事情哦~~ 欢迎大家来一起参与造轮子~ 62 | 63 | ![](https://s2.ax1x.com/2020/02/03/106Ni4.gif) 64 | 65 | ### 五、我的 66 | 67 | 此处待开发... 68 | 69 | ![](https://s2.ax1x.com/2020/02/03/10gRKI.png) 70 | 71 | 72 | ### 未来规划和展望 73 | 目前武汉新型肺炎爆发,适逢春节,国家号召宅在家里,口罩难求,怕是怕死懒得出门,一直在家里做该项目,进度飞快,这个项目的核心已经完成,但是还是有很多扩展的余地。关于未来的规划,我是这么安排的: 74 | 75 | - 音乐player播放器 增加歌词等扩展功能 76 | - 视频播放器 77 | - 完成收藏、播放历史功能 78 | - 实现MV模块 评论页更优雅的交互 79 | - 同时撰写拆解文章 80 | - “我的”待开发 81 | - 未来更多功能待补充... 82 | 83 | 这个项目长期维护,希望大家踊跃提issue和pr,把这个项目打造得更加完美,帮助到更多的Vue开发者。 84 | 85 | 最后的最后,万水千山总是情,给个star行不行(你回头也好找这个项目呀 (*^_^*)) 86 | 87 | ![](https://s2.ax1x.com/2020/02/03/10h39S.gif) 88 | 89 | # 声明 90 | 本项目代码仅用学习交流, 请勿商业使用。 91 | -------------------------------------------------------------------------------- /v2/src/base/scroll/scroll.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 100 | 101 | 104 | -------------------------------------------------------------------------------- /src/components-base/scroll/scrol.v2.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 100 | 101 | 104 | -------------------------------------------------------------------------------- /v2/src/base/music-animation/music-animation.vue: -------------------------------------------------------------------------------- 1 | 17 | 37 | 98 | -------------------------------------------------------------------------------- /src/components-base/music-animation/music-animation.vue: -------------------------------------------------------------------------------- 1 | 17 | 37 | 98 | -------------------------------------------------------------------------------- /src/components-base/loading/loading.vue: -------------------------------------------------------------------------------- 1 | 12 | 46 | 111 | -------------------------------------------------------------------------------- /src/components-base/slider/slider.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 44 | 45 | 115 | -------------------------------------------------------------------------------- /v2/src/views/singer.vue: -------------------------------------------------------------------------------- 1 | 9 | 97 | 98 | 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/util' 4 | 5 | export const playlistMixin = { 6 | computed: { 7 | ...mapGetters([ 8 | 'playList' 9 | ]) 10 | }, 11 | mounted () { 12 | this.handlePlaylist(this.playList) 13 | }, 14 | activated () { 15 | this.handlePlaylist(this.playList) 16 | }, 17 | watch: { 18 | playList (newVal) { 19 | this.handlePlaylist(newVal) 20 | } 21 | }, 22 | methods: { 23 | handlePlaylist () { 24 | throw new Error('component must implement handlePlaylist method') 25 | } 26 | } 27 | } 28 | 29 | export const playerMixin = { 30 | computed: { 31 | iconMode () { 32 | return this.mode === playMode.sequence ? 'icon-sequence' : this.mode === playMode.loop ? 'icon-loop' : 'icon-random' 33 | }, 34 | ...mapGetters([ 35 | 'sequenceList', 36 | 'playlist', 37 | 'currentSong', 38 | 'mode', 39 | 'favoriteList' 40 | ]), 41 | favoriteIcon () { 42 | return this.getFavoriteIcon(this.currentSong) 43 | } 44 | }, 45 | methods: { 46 | changeMode () { 47 | const mode = (this.mode + 1) % 3 48 | this.setPlayMode(mode) 49 | let list = null 50 | if (mode === playMode.random) { 51 | list = shuffle(this.sequenceList) 52 | } else { 53 | list = this.sequenceList 54 | } 55 | this.resetCurrentIndex(list) 56 | this.setPlaylist(list) 57 | }, 58 | resetCurrentIndex (list) { 59 | let index = list.findIndex((item) => { 60 | return item.id === this.currentSong.id 61 | }) 62 | this.setCurrentIndex(index) 63 | }, 64 | toggleFavorite (song) { 65 | if (this.isFavorite(song)) { 66 | this.deleteFavoriteList(song) 67 | } else { 68 | this.saveFavoriteList(song) 69 | } 70 | }, 71 | getFavoriteIcon (song) { 72 | if (this.isFavorite(song)) { 73 | return 'icon-favorite' 74 | } 75 | return 'icon-not-favorite' 76 | }, 77 | isFavorite (song) { 78 | const index = this.favoriteList.findIndex((item) => { 79 | return item.id === song.id 80 | }) 81 | return index > -1 82 | }, 83 | ...mapMutations({ 84 | setPlayMode: 'SET_PLAY_MODE', 85 | setPlaylist: 'SET_PLAYLIST', 86 | setCurrentIndex: 'SET_CURRENT_INDEX', 87 | setPlayingState: 'SET_PLAYING_STATE' 88 | }), 89 | ...mapActions([ 90 | 'saveFavoriteList', 91 | 'deleteFavoriteList' 92 | ]) 93 | } 94 | } 95 | 96 | export const searchMixin = { 97 | data () { 98 | return { 99 | query: '', 100 | refreshDelay: 120 101 | } 102 | }, 103 | computed: { 104 | ...mapGetters([ 105 | 'searchHistory' 106 | ]) 107 | }, 108 | methods: { 109 | onQueryChange (query) { 110 | // 处理带空格的情况 111 | this.query = query.trim() 112 | }, 113 | blurInput () { 114 | this.$refs.searchBox.blur() 115 | }, 116 | addQuery (query) { 117 | this.$refs.searchBox.setQuery(query) 118 | }, 119 | saveSearch () { 120 | this.saveSearchHistory(this.query) 121 | }, 122 | ...mapActions([ 123 | 'saveSearchHistory', 124 | 'deleteSearchHistory' 125 | ]) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /v2/src/common/js/mixin.js: -------------------------------------------------------------------------------- 1 | import { mapGetters, mapMutations, mapActions } from 'vuex' 2 | import { playMode } from 'common/js/config' 3 | import { shuffle } from 'common/js/util' 4 | 5 | export const playlistMixin = { 6 | computed: { 7 | ...mapGetters([ 8 | 'playList' 9 | ]) 10 | }, 11 | mounted () { 12 | this.handlePlaylist(this.playList) 13 | }, 14 | activated () { 15 | this.handlePlaylist(this.playList) 16 | }, 17 | watch: { 18 | playList (newVal) { 19 | this.handlePlaylist(newVal) 20 | } 21 | }, 22 | methods: { 23 | handlePlaylist () { 24 | throw new Error('component must implement handlePlaylist method') 25 | } 26 | } 27 | } 28 | 29 | export const playerMixin = { 30 | computed: { 31 | iconMode () { 32 | return this.mode === playMode.sequence ? 'icon-sequence' : this.mode === playMode.loop ? 'icon-loop' : 'icon-random' 33 | }, 34 | ...mapGetters([ 35 | 'sequenceList', 36 | 'playlist', 37 | 'currentSong', 38 | 'mode', 39 | 'favoriteList' 40 | ]), 41 | favoriteIcon () { 42 | return this.getFavoriteIcon(this.currentSong) 43 | } 44 | }, 45 | methods: { 46 | changeMode () { 47 | const mode = (this.mode + 1) % 3 48 | this.setPlayMode(mode) 49 | let list = null 50 | if (mode === playMode.random) { 51 | list = shuffle(this.sequenceList) 52 | } else { 53 | list = this.sequenceList 54 | } 55 | this.resetCurrentIndex(list) 56 | this.setPlaylist(list) 57 | }, 58 | resetCurrentIndex (list) { 59 | let index = list.findIndex((item) => { 60 | return item.id === this.currentSong.id 61 | }) 62 | this.setCurrentIndex(index) 63 | }, 64 | toggleFavorite (song) { 65 | if (this.isFavorite(song)) { 66 | this.deleteFavoriteList(song) 67 | } else { 68 | this.saveFavoriteList(song) 69 | } 70 | }, 71 | getFavoriteIcon (song) { 72 | if (this.isFavorite(song)) { 73 | return 'icon-favorite' 74 | } 75 | return 'icon-not-favorite' 76 | }, 77 | isFavorite (song) { 78 | const index = this.favoriteList.findIndex((item) => { 79 | return item.id === song.id 80 | }) 81 | return index > -1 82 | }, 83 | ...mapMutations({ 84 | setPlayMode: 'SET_PLAY_MODE', 85 | setPlaylist: 'SET_PLAYLIST', 86 | setCurrentIndex: 'SET_CURRENT_INDEX', 87 | setPlayingState: 'SET_PLAYING_STATE' 88 | }), 89 | ...mapActions([ 90 | 'saveFavoriteList', 91 | 'deleteFavoriteList' 92 | ]) 93 | } 94 | } 95 | 96 | export const searchMixin = { 97 | data () { 98 | return { 99 | query: '', 100 | refreshDelay: 120 101 | } 102 | }, 103 | computed: { 104 | ...mapGetters([ 105 | 'searchHistory' 106 | ]) 107 | }, 108 | methods: { 109 | onQueryChange (query) { 110 | // 处理带空格的情况 111 | this.query = query.trim() 112 | }, 113 | blurInput () { 114 | this.$refs.searchBox.blur() 115 | }, 116 | addQuery (query) { 117 | this.$refs.searchBox.setQuery(query) 118 | }, 119 | saveSearch () { 120 | this.saveSearchHistory(this.query) 121 | }, 122 | ...mapActions([ 123 | 'saveSearchHistory', 124 | 'deleteSearchHistory' 125 | ]) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/components-base/slider/slider.v2.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 79 | 80 | 130 | -------------------------------------------------------------------------------- /v2/src/base/slider/slider.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 111 | 112 | 154 | -------------------------------------------------------------------------------- /v2/src/base/progress-bar/progress-bar.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 95 | 96 | 184 | -------------------------------------------------------------------------------- /v2/src/base/mv/mv-item.vue: -------------------------------------------------------------------------------- 1 | 46 | 122 | 185 | -------------------------------------------------------------------------------- /src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 124 | 204 | -------------------------------------------------------------------------------- /src/components-base/progress-bar/progress-bar.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 100 | 101 | 203 | -------------------------------------------------------------------------------- /v2/src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 134 | 186 | -------------------------------------------------------------------------------- /v2/src/components/music-list/music-list.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 119 | 215 | -------------------------------------------------------------------------------- /src/components/disc-list/disc-list.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 118 | 248 | -------------------------------------------------------------------------------- /v2/src/base/listview/listview.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 178 | 179 | 248 | -------------------------------------------------------------------------------- /v2/src/components/singer-music-list/singer-music-list.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 151 | 248 | -------------------------------------------------------------------------------- /src/components-base/mv/mv-item.vue: -------------------------------------------------------------------------------- 1 | 67 | 168 | 328 | -------------------------------------------------------------------------------- /src/components/m-header/m-header.vue: -------------------------------------------------------------------------------- 1 | 36 | 111 | 313 | -------------------------------------------------------------------------------- /v2/src/components/m-header/m-header.vue: -------------------------------------------------------------------------------- 1 | 38 | 113 | 268 | -------------------------------------------------------------------------------- /v2/src/components/player/player.vue: -------------------------------------------------------------------------------- 1 | 62 | 266 | 394 | --------------------------------------------------------------------------------