├── babel.config.js ├── .eslintignore ├── public ├── favicon.ico └── index.html ├── src ├── assets │ ├── hot_icon.png │ └── hot_music_bg_2x.jpg ├── store │ ├── state.js │ ├── mutation-types.js │ ├── getters.js │ ├── index.js │ ├── mutations.js │ └── actions.js ├── permission.js ├── views │ ├── Home.vue │ ├── 404.vue │ ├── MusicPlayr.vue │ └── SongMenu.vue ├── main.js ├── App.vue ├── router.js ├── components │ ├── Tab │ │ ├── components │ │ │ ├── searchHot │ │ │ │ └── index.vue │ │ │ ├── searchBar │ │ │ │ └── index.vue │ │ │ └── searchResult │ │ │ │ └── index.vue │ │ ├── TabSearch.vue │ │ ├── TabIndex.vue │ │ └── TabRank.vue │ ├── HomeTop │ │ ├── index.vue │ │ └── logo.svg │ └── SongsList │ │ └── index.vue ├── utils │ ├── request.js │ └── index.js └── api │ └── api.js ├── .env.production ├── .env.staging ├── .gitignore ├── .env.development ├── vue.config.js ├── README.md ├── .github └── workflows │ └── deploy.yml ├── package.json └── .eslintrc.js /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["@vue/app"] 3 | }; 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # 不检测的文件夹 2 | build/*.js 3 | src/assets 4 | public 5 | dist 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Francis1024/NeteaseMusic/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/assets/hot_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Francis1024/NeteaseMusic/HEAD/src/assets/hot_icon.png -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | # just a flag 2 | ENV = 'production' 3 | 4 | # base api 5 | VUE_APP_BASE_API = '/api/' 6 | 7 | -------------------------------------------------------------------------------- /src/assets/hot_music_bg_2x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Francis1024/NeteaseMusic/HEAD/src/assets/hot_music_bg_2x.jpg -------------------------------------------------------------------------------- /.env.staging: -------------------------------------------------------------------------------- 1 | NODE_ENV = production 2 | 3 | # just a flag 4 | ENV = 'staging' 5 | 6 | # base api 7 | VUE_APP_BASE_API = '/api/' 8 | 9 | -------------------------------------------------------------------------------- /src/store/state.js: -------------------------------------------------------------------------------- 1 | const state = { 2 | // 推荐歌单 3 | remdList: [], 4 | // 最新音乐 5 | newSongList: [], 6 | // 当前播放音乐的地址 7 | currentMusicUrl: '' 8 | }; 9 | 10 | export default state; 11 | -------------------------------------------------------------------------------- /src/store/mutation-types.js: -------------------------------------------------------------------------------- 1 | // 推荐歌单 2 | export const SET_REMDLIST = 'SET_REMDLIST'; 3 | // 最新音乐 4 | export const SET_NEWSONGLIST = 'SET_NEWSONGLIST'; 5 | // 音乐地址 6 | export const SET_MUSICURL = 'SET_MUSICURL'; 7 | -------------------------------------------------------------------------------- /src/permission.js: -------------------------------------------------------------------------------- 1 | import router from './router'; 2 | 3 | router.beforeEach(async(to, from, next) => { 4 | // set page title 5 | next(); 6 | }); 7 | 8 | router.afterEach(() => { 9 | // finish progress bar 10 | 11 | }); 12 | -------------------------------------------------------------------------------- /src/store/getters.js: -------------------------------------------------------------------------------- 1 | const getters = { 2 | // 截取6条歌单数据 3 | remdList: state => { 4 | return state.remdList.slice(0, 6); 5 | }, 6 | // 获取最新歌曲列表 7 | newSongList: state => { 8 | return state.newSongList; 9 | } 10 | }; 11 | export default getters; 12 | -------------------------------------------------------------------------------- /src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 11 | 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /deploy 4 | /dist 5 | package-lock.json 6 | 7 | # local env files 8 | .env.local 9 | .env.*.local 10 | 11 | # Log files 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuex from 'vuex'; 3 | import state from './state'; 4 | import getters from './getters'; 5 | import mutations from './mutations'; 6 | import actions from './actions'; 7 | Vue.use(Vuex); 8 | 9 | export default new Vuex.Store({ 10 | state, 11 | getters, 12 | mutations, 13 | actions 14 | }); 15 | -------------------------------------------------------------------------------- /src/views/404.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 19 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import App from '@/App.vue'; 3 | import router from '@/router'; 4 | import store from '@/store/index'; 5 | import 'lib-flexible'; 6 | import Vant from 'vant'; 7 | import 'vant/lib/index.css'; 8 | 9 | Vue.use(Vant); 10 | Vue.config.productionTip = false; 11 | 12 | // console.log('环境变量', process.env); 13 | 14 | new Vue({ 15 | router, 16 | store, 17 | render: h => h(App) 18 | }).$mount('#app'); 19 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 11 | 12 | 13 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/store/mutations.js: -------------------------------------------------------------------------------- 1 | import * as types from './mutation-types'; 2 | 3 | const mutations = { 4 | // 推荐歌单 5 | [types.SET_REMDLIST](state, remdList) { 6 | state.remdList = remdList; 7 | }, 8 | // 最新音乐 9 | [types.SET_NEWSONGLIST](state, newSongList) { 10 | state.newSongList = newSongList; 11 | }, 12 | // 音乐地址 13 | [types.SET_MUSICURL](state, currentMusicUrl) { 14 | console.log(state); 15 | console.log(currentMusicUrl.url); 16 | state.currentMusicUrl = currentMusicUrl.url; 17 | } 18 | }; 19 | export default mutations; 20 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | # just a flag 2 | ENV = 'development' 3 | 4 | # base api 5 | VUE_APP_BASE_API = '/api/' 6 | 7 | # vue-cli uses the VUE_CLI_BABEL_TRANSPILE_MODULES environment variable, 8 | # to control whether the babel-plugin-dynamic-import-node plugin is enabled. 9 | # It only does one thing by converting all import() to require(). 10 | # This configuration can significantly increase the speed of hot updates, 11 | # when you have a large number of pages. 12 | # Detail: https://github.com/vuejs/vue-cli/blob/dev/packages/@vue/babel-preset-app/index.js 13 | 14 | VUE_CLI_BABEL_TRANSPILE_MODULES = true 15 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | function resolve(dir) { 4 | return path.join(__dirname, dir) 5 | } 6 | module.exports = { 7 | devServer: { 8 | proxy: { 9 | '/api': { 10 | // 需要代理的接口,一般会加前缀来区分 11 | target: 'https://music.ztyuu.com', // 接口域名 12 | changeOrigin: true // 是否跨域 13 | } 14 | } 15 | }, 16 | configureWebpack: { 17 | // provide the app's title in webpack's name field, so that 18 | // it can be accessed in index.html to inject the correct title. 19 | resolve: { 20 | alias: { 21 | '@': resolve('src') 22 | } 23 | } 24 | }, 25 | 26 | // 将构建好的文件输出到哪里 27 | outputDir: 'dist' 28 | }; 29 | -------------------------------------------------------------------------------- /src/router.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Router from 'vue-router'; 3 | 4 | Vue.use(Router); 5 | // 路由懒加载 6 | // const getComponent = name => () => import(`@/views/${name}.vue`); 7 | export default new Router({ 8 | mode: 'history', 9 | routes: [ 10 | // 主页面 11 | { 12 | path: '/', 13 | component: () => import('@/views/Home') 14 | }, 15 | // 歌单详情页面 16 | { 17 | path: '/songMenuDetail', 18 | component: () => import('@/views/SongMenu') 19 | }, 20 | // 歌曲播放页面 21 | { 22 | path: '/MusicPlayr', 23 | component: () => import('@/views/MusicPlayr') 24 | }, 25 | { 26 | path: '*', 27 | component: () => import('@/views/404') 28 | } 29 | ] 30 | }); 31 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 网易云音乐 7 | 8 | 9 | 12 |
13 | 14 | 15 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![NeteaseMusic](https://socialify.git.ci/Francis1024/NeteaseMusic/image?description=1&font=Inter&forks=1&issues=1&language=1&owner=1&pattern=Floating%20Cogs&pulls=1&stargazers=1&theme=Light) 2 | 3 | ### 项目描述 4 | 5 | [![996.icu](https://img.shields.io/badge/link-996.icu-red.svg)](https://996.icu)[![LICENSE](https://img.shields.io/badge/license-Anti%20996-blue.svg)](https://github.com/996icu/996.ICU/blob/master/LICENSE) 6 | 7 | 一个基于 Vue-Cli 3 + Vuex +Vant UI 的网易云音乐移动端 8 | 9 | ### 项目运行 10 | 11 | ``` 12 | #克隆到本地 13 | git clone https://github.com/Francis1024/NeteaseMusic.git 14 | 15 | #进入项目文件夹 16 | cd Netease-Music 17 | 18 | #安装依赖 19 | npm install 20 | 21 | #运行项目 22 | npm run serve 23 | 24 | #项目部署 25 | npm run build 26 | ``` 27 | 28 | 29 | 30 | ### Todo 31 | 32 | - [x] 推荐页面 33 | - [x] 热歌榜页面 34 | - [x] 搜索页面 35 | - [x] 音乐播放页面 36 | - [x] 歌单信息页面 37 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy NeteaseMusic 2 | on: 3 | push: 4 | branches: 5 | - master # 只在master上push触发部署 6 | paths-ignore: # 下列文件的变更不触发部署,可以自行添加 7 | - README.md 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest # 使用ubuntu系统镜像运行自动化脚本 13 | strategy: 14 | matrix: 15 | node-version: [12.x] 16 | steps: # 自动化步骤 17 | - uses: actions/checkout@master # 第一步,下载代码仓库 18 | - name: Build 19 | run: | 20 | npm install && npm run build 21 | - name: Deploy # 第二步,rsync推文件 22 | uses: AEnterprise/rsync-deploy@v1.0 # 使用别人包装好的步骤镜像 23 | env: 24 | DEPLOY_KEY: ${{ secrets.ACCESS_TOKEN }} # 引用配置,SSH私钥 25 | ARGS: -avz --delete --exclude='*.pyc' # rsync参数,排除.pyc文件 26 | SERVER_PORT: ${{ secrets.SERVER_PORT }} # SSH端口 27 | FOLDER: ./dist/ # 要推送的文件夹,路径相对于代码仓库的根目录 28 | SERVER_IP: ${{ secrets.REMOTE_HOST }} # 引用配置,服务器的host名(IP或者域名domain.com) 29 | USERNAME: ${{ secrets.REMOTE_USER }} # 引用配置,服务器登录名 30 | SERVER_DESTINATION: ${{ secrets.TARGET }} # 部署到目标文件夹 31 | -------------------------------------------------------------------------------- /src/components/Tab/components/searchHot/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 28 | 29 | 32 | 55 | -------------------------------------------------------------------------------- /src/store/actions.js: -------------------------------------------------------------------------------- 1 | import { getRemdSongList, getNewSongList, getMusicUrl } from '@/api/api'; 2 | 3 | const actions = { 4 | // 推荐歌单 5 | getRemdSongList({ commit }) { 6 | return new Promise((resolve, reject) => { 7 | getRemdSongList().then(res => { 8 | console.log('推荐歌单数据', res); 9 | if (res.code === 200) { 10 | commit('SET_REMDLIST', res.result); 11 | resolve(); 12 | } else { 13 | reject(); 14 | } 15 | }); 16 | }); 17 | }, 18 | getNewSongList({ commit }) { 19 | return new Promise((resolve, reject) => { 20 | getNewSongList().then(res => { 21 | console.log('最新歌曲数据', res); 22 | if (res.code === 200) { 23 | commit('SET_NEWSONGLIST', res.result); 24 | resolve(); 25 | } else { 26 | reject(); 27 | } 28 | }); 29 | }); 30 | }, 31 | getMusicUrl({ commit }, id) { 32 | return new Promise((resolve, reject) => { 33 | getMusicUrl(id).then(res => { 34 | console.log('当前歌曲数据', res); 35 | if (res.code === 200) { 36 | commit('SET_MUSICURL', res.data[0]); 37 | resolve(); 38 | } else { 39 | reject(); 40 | } 41 | }); 42 | }); 43 | } 44 | }; 45 | export default actions; 46 | -------------------------------------------------------------------------------- /src/utils/request.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | // create an axios instance 4 | const service = axios.create({ 5 | baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url 6 | // withCredentials: true, // send cookies when cross-domain requests 7 | timeout: 5000 // request timeout 8 | 9 | }); 10 | 11 | // request interceptor 12 | service.interceptors.request.use( 13 | config => { 14 | return config 15 | }, 16 | error => { 17 | // do something with request error 18 | console.log(error) // for debug 19 | return Promise.reject(error) 20 | } 21 | ) 22 | 23 | // response interceptor 24 | service.interceptors.response.use( 25 | /** 26 | * If you want to get http information such as headers or status 27 | * Please return response => response 28 | */ 29 | 30 | /** 31 | * Determine the request status by custom code 32 | * Here is just an example 33 | * You can also judge the status by HTTP Status Code 34 | */ 35 | response => { 36 | const res = response.data; 37 | if (res.code !== 200) { 38 | return Promise.reject(new Error(res.message || 'Error')); 39 | } else { 40 | return res; 41 | } 42 | }, 43 | error => { 44 | console.log('err' + error) // for debug 45 | return Promise.reject(error) 46 | } 47 | ) 48 | 49 | export default service 50 | -------------------------------------------------------------------------------- /src/api/api.js: -------------------------------------------------------------------------------- 1 | // 引入axios 2 | import request from '@/utils/request.js'; 3 | // 获取推荐歌单 4 | export function getRemdSongList() { 5 | return request({ 6 | url: '/personalized' 7 | }) 8 | } 9 | // 获取最新歌曲 10 | export function getNewSongList() { 11 | return request({ 12 | url: '/personalized/newsong' 13 | }) 14 | } 15 | // 获取歌曲地址 16 | export function getMusicUrl(id) { 17 | return request({ 18 | url: '/song/url', 19 | params: { id: id } 20 | }) 21 | } 22 | // 获取音乐详情 23 | export function getMusicDetail(id) { 24 | return request({ 25 | url: '/song/detail', 26 | params: { ids: id } 27 | }) 28 | } 29 | 30 | // 排行榜 31 | // https://binaryify.github.io/NeteaseCloudMusicApi/#/?id=排行榜 32 | export function getMusicTopList(params) { 33 | return request({ 34 | url: '/top/list', 35 | params 36 | }) 37 | } 38 | 39 | // 热门搜索 40 | export function getSearchHot() { 41 | return request({ 42 | url: '/search/hot' 43 | }) 44 | } 45 | // 搜索多重匹配 46 | export function getSearchMultimatch(params) { 47 | return request({ 48 | url: '/search/multimatch', 49 | params 50 | }) 51 | } 52 | // 搜索接口 53 | // /search?keywords= 海阔天空 54 | export function getSearchMusic(params) { 55 | return request({ 56 | url: '/search', 57 | params 58 | }) 59 | } 60 | // 获取歌单详情 61 | export function getPlaylistDetail(params) { 62 | return request({ 63 | url: '/playlist/detail', 64 | params 65 | }) 66 | } 67 | -------------------------------------------------------------------------------- /src/components/HomeTop/index.vue: -------------------------------------------------------------------------------- 1 | 15 | 33 | 34 | 55 | -------------------------------------------------------------------------------- /src/components/Tab/TabSearch.vue: -------------------------------------------------------------------------------- 1 | 8 | 55 | 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "netease-music", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "dev": "vue-cli-service serve", 8 | "build": "vue-cli-service build", 9 | "lint": "vue-cli-service lint" 10 | }, 11 | "dependencies": { 12 | "axios": "^0.21.2", 13 | "core-js": "^2.6.5", 14 | "lib-flexible": "^0.3.2", 15 | "postcss-pxtorem": "^4.0.1", 16 | "vant": "^2.1.8", 17 | "vue": "^2.6.10", 18 | "vue-router": "^3.0.3", 19 | "vuex": "^3.0.1" 20 | }, 21 | "devDependencies": { 22 | "@vue/cli-plugin-babel": "^3.12.1", 23 | "@vue/cli-plugin-eslint": "^3.6.0", 24 | "@vue/cli-service": "^5.0.8", 25 | "@vue/eslint-config-prettier": "^4.0.1", 26 | "babel-eslint": "^10.0.1", 27 | "eslint": "^5.16.0", 28 | "eslint-plugin-vue": "^5.0.0", 29 | "sass": "^1.18.0", 30 | "sass-loader": "^7.1.0", 31 | "vue-template-compiler": "^2.5.21" 32 | }, 33 | "eslintConfig": { 34 | "root": true, 35 | "env": { 36 | "node": true 37 | }, 38 | "extends": [ 39 | "plugin:vue/essential", 40 | "@vue/prettier" 41 | ], 42 | "rules": { 43 | "no-console": "off", 44 | "no-debugger": "off" 45 | }, 46 | "parserOptions": { 47 | "parser": "babel-eslint" 48 | } 49 | }, 50 | "postcss": { 51 | "plugins": { 52 | "autoprefixer": { 53 | "browsers": [ 54 | "Android >= 4.0", 55 | "iOS >= 7" 56 | ] 57 | }, 58 | "postcss-pxtorem": { 59 | "rootValue": 37.5, 60 | "propList": [ 61 | "*" 62 | ] 63 | } 64 | } 65 | }, 66 | "browserslist": [ 67 | "> 1%", 68 | "last 2 versions", 69 | "not ie <= 8" 70 | ] 71 | } 72 | -------------------------------------------------------------------------------- /src/components/Tab/components/searchBar/index.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 45 | 46 | 83 | -------------------------------------------------------------------------------- /src/components/SongsList/index.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 36 | 37 | 94 | -------------------------------------------------------------------------------- /src/components/Tab/components/searchResult/index.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 37 | 98 | -------------------------------------------------------------------------------- /src/views/MusicPlayr.vue: -------------------------------------------------------------------------------- 1 | 24 | 76 | 128 | -------------------------------------------------------------------------------- /src/components/Tab/TabIndex.vue: -------------------------------------------------------------------------------- 1 | 35 | 78 | 79 | 141 | -------------------------------------------------------------------------------- /src/components/Tab/TabRank.vue: -------------------------------------------------------------------------------- 1 | 32 | 61 | 149 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by PanJiaChen on 16/11/18. 3 | */ 4 | 5 | import axios from 'axios'; 6 | import store from '@/store'; 7 | 8 | /** 9 | * Parse the time to string 10 | * @param {(Object|string|number)} time 11 | * @param {string} cFormat 12 | * @returns {string | null} 13 | */ 14 | export function parseTime(time, cFormat) { 15 | if (arguments.length === 0) { 16 | return null 17 | } 18 | const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}' 19 | let date 20 | if (typeof time === 'object') { 21 | date = time 22 | } else { 23 | if ((typeof time === 'string') && (/^[0-9]+$/.test(time))) { 24 | time = parseInt(time) 25 | } 26 | if ((typeof time === 'number') && (time.toString().length === 10)) { 27 | time = time * 1000 28 | } 29 | date = new Date(time) 30 | } 31 | const formatObj = { 32 | y: date.getFullYear(), 33 | m: date.getMonth() + 1, 34 | d: date.getDate(), 35 | h: date.getHours(), 36 | i: date.getMinutes(), 37 | s: date.getSeconds(), 38 | a: date.getDay() 39 | } 40 | const time_str = format.replace(/{([ymdhisa])+}/g, (result, key) => { 41 | const value = formatObj[key] 42 | // Note: getDay() returns 0 on Sunday 43 | if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value ] } 44 | return value.toString().padStart(2, '0') 45 | }) 46 | return time_str 47 | } 48 | 49 | /** 50 | * @param {number} time 51 | * @param {string} option 52 | * @returns {string} 53 | */ 54 | export function formatTime(time, option) { 55 | if (('' + time).length === 10) { 56 | time = parseInt(time) * 1000 57 | } else { 58 | time = +time 59 | } 60 | const d = new Date(time) 61 | const now = Date.now() 62 | 63 | const diff = (now - d) / 1000 64 | 65 | if (diff < 30) { 66 | return '刚刚' 67 | } else if (diff < 3600) { 68 | // less 1 hour 69 | return Math.ceil(diff / 60) + '分钟前' 70 | } else if (diff < 3600 * 24) { 71 | return Math.ceil(diff / 3600) + '小时前' 72 | } else if (diff < 3600 * 24 * 2) { 73 | return '1天前' 74 | } 75 | if (option) { 76 | return parseTime(time, option) 77 | } else { 78 | return ( 79 | d.getMonth() + 80 | 1 + 81 | '月' + 82 | d.getDate() + 83 | '日' + 84 | d.getHours() + 85 | '时' + 86 | d.getMinutes() + 87 | '分' 88 | ) 89 | } 90 | } 91 | 92 | /** 93 | * @param {string} pageTitle 94 | * @param {string} singerName 95 | * @returns {string} 96 | */ 97 | export function getPageTitle(pageTitle, singerName) { 98 | const title = `${pageTitle} - ${singerName} ` 99 | document.title = title 100 | 101 | // var s = title.split(''); 102 | // setInterval(function() { 103 | // s.push(s[0]); 104 | // s.shift(); 105 | // document.title = s.join(''); 106 | // }, 500); 107 | } 108 | /** 109 | * @param {string} url 110 | * @returns {Object} 111 | */ 112 | export function param2Obj(url) { 113 | const search = url.split('?')[1] 114 | if (!search) { 115 | return {} 116 | } 117 | return JSON.parse( 118 | '{"' + 119 | decodeURIComponent(search) 120 | .replace(/"/g, '\\"') 121 | .replace(/&/g, '","') 122 | .replace(/=/g, '":"') 123 | .replace(/\+/g, ' ') + 124 | '"}' 125 | ) 126 | } 127 | 128 | /** 129 | * @param content 130 | */ 131 | export function copy(content) { 132 | const aux = document.createElement('textarea'); 133 | aux.value = content; 134 | document.body.appendChild(aux); 135 | aux.select(); 136 | document.execCommand('copy'); 137 | document.body.removeChild(aux); 138 | console.log('复制成功'); 139 | } 140 | 141 | export function exportExcel(url, filename) { 142 | return new Promise((resolve, reject) => { 143 | axios(url, { 144 | headers: { 'token': store.getters.token }, 145 | responseType: 'blob' 146 | } 147 | ).then(response => { 148 | const url = window.URL.createObjectURL(new Blob([response.data])); 149 | const link = document.createElement('a'); 150 | link.href = url; 151 | link.setAttribute('download', filename); 152 | document.body.appendChild(link); 153 | link.click(); 154 | resolve() 155 | } 156 | ).catch(err => { 157 | const response = err.response; 158 | const msg = response.statusText; 159 | console.error(msg); 160 | reject() 161 | }) 162 | }) 163 | } 164 | 165 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parserOptions: { 4 | parser: 'babel-eslint', 5 | sourceType: 'module' 6 | }, 7 | env: { 8 | browser: true, 9 | node: true, 10 | es6: true, 11 | }, 12 | extends: ['plugin:vue/recommended', 'eslint:recommended'], 13 | 14 | // add your custom rules here 15 | //it is base on https://github.com/vuejs/eslint-config-vue 16 | rules: { 17 | "vue/max-attributes-per-line": [2, { 18 | "singleline": 10, 19 | "multiline": { 20 | "max": 1, 21 | "allowFirstLine": false 22 | } 23 | }], 24 | "vue/singleline-html-element-content-newline": "off", 25 | "vue/multiline-html-element-content-newline":"off", 26 | "vue/name-property-casing": ["error", "PascalCase"], 27 | "vue/no-v-html": "off", 28 | 'accessor-pairs': 2, 29 | 'arrow-spacing': [2, { 30 | 'before': true, 31 | 'after': true 32 | }], 33 | 'block-spacing': [2, 'always'], 34 | 'brace-style': [2, '1tbs', { 35 | 'allowSingleLine': true 36 | }], 37 | 'camelcase': [0, { 38 | 'properties': 'always' 39 | }], 40 | 'comma-dangle': [2, 'never'], 41 | 'comma-spacing': [2, { 42 | 'before': false, 43 | 'after': true 44 | }], 45 | 'comma-style': [2, 'last'], 46 | 'constructor-super': 2, 47 | 'curly': [2, 'multi-line'], 48 | 'dot-location': [2, 'property'], 49 | 'eol-last': 2, 50 | 'eqeqeq': ["error", "always", {"null": "ignore"}], 51 | 'generator-star-spacing': [2, { 52 | 'before': true, 53 | 'after': true 54 | }], 55 | 'handle-callback-err': [2, '^(err|error)$'], 56 | 'indent': [2, 2, { 57 | 'SwitchCase': 1 58 | }], 59 | 'jsx-quotes': [2, 'prefer-single'], 60 | 'key-spacing': [2, { 61 | 'beforeColon': false, 62 | 'afterColon': true 63 | }], 64 | 'keyword-spacing': [2, { 65 | 'before': true, 66 | 'after': true 67 | }], 68 | 'new-cap': [2, { 69 | 'newIsCap': true, 70 | 'capIsNew': false 71 | }], 72 | 'new-parens': 2, 73 | 'no-array-constructor': 2, 74 | 'no-caller': 2, 75 | 'no-console': 'off', 76 | 'no-class-assign': 2, 77 | 'no-cond-assign': 2, 78 | 'no-const-assign': 2, 79 | 'no-control-regex': 0, 80 | 'no-delete-var': 2, 81 | 'no-dupe-args': 2, 82 | 'no-dupe-class-members': 2, 83 | 'no-dupe-keys': 2, 84 | 'no-duplicate-case': 2, 85 | 'no-empty-character-class': 2, 86 | 'no-empty-pattern': 2, 87 | 'no-eval': 2, 88 | 'no-ex-assign': 2, 89 | 'no-extend-native': 2, 90 | 'no-extra-bind': 2, 91 | 'no-extra-boolean-cast': 2, 92 | 'no-extra-parens': [2, 'functions'], 93 | 'no-fallthrough': 2, 94 | 'no-floating-decimal': 2, 95 | 'no-func-assign': 2, 96 | 'no-implied-eval': 2, 97 | 'no-inner-declarations': [2, 'functions'], 98 | 'no-invalid-regexp': 2, 99 | 'no-irregular-whitespace': 2, 100 | 'no-iterator': 2, 101 | 'no-label-var': 2, 102 | 'no-labels': [2, { 103 | 'allowLoop': false, 104 | 'allowSwitch': false 105 | }], 106 | 'no-lone-blocks': 2, 107 | 'no-mixed-spaces-and-tabs': 2, 108 | 'no-multi-spaces': 2, 109 | 'no-multi-str': 2, 110 | 'no-multiple-empty-lines': [2, { 111 | 'max': 1 112 | }], 113 | 'no-native-reassign': 2, 114 | 'no-negated-in-lhs': 2, 115 | 'no-new-object': 2, 116 | 'no-new-require': 2, 117 | 'no-new-symbol': 2, 118 | 'no-new-wrappers': 2, 119 | 'no-obj-calls': 2, 120 | 'no-octal': 2, 121 | 'no-octal-escape': 2, 122 | 'no-path-concat': 2, 123 | 'no-proto': 2, 124 | 'no-redeclare': 2, 125 | 'no-regex-spaces': 2, 126 | 'no-return-assign': [2, 'except-parens'], 127 | 'no-self-assign': 2, 128 | 'no-self-compare': 2, 129 | 'no-sequences': 2, 130 | 'no-shadow-restricted-names': 2, 131 | 'no-spaced-func': 2, 132 | 'no-sparse-arrays': 2, 133 | 'no-this-before-super': 2, 134 | 'no-throw-literal': 2, 135 | 'no-trailing-spaces': 2, 136 | 'no-undef': 2, 137 | 'no-undef-init': 2, 138 | 'no-unexpected-multiline': 2, 139 | 'no-unmodified-loop-condition': 2, 140 | 'no-unneeded-ternary': [2, { 141 | 'defaultAssignment': false 142 | }], 143 | 'no-unreachable': 2, 144 | 'no-unsafe-finally': 2, 145 | 'no-unused-vars': [2, { 146 | 'vars': 'all', 147 | 'args': 'none' 148 | }], 149 | 'no-useless-call': 2, 150 | 'no-useless-computed-key': 2, 151 | 'no-useless-constructor': 2, 152 | 'no-useless-escape': 0, 153 | 'no-whitespace-before-property': 2, 154 | 'no-with': 2, 155 | 'one-var': [2, { 156 | 'initialized': 'never' 157 | }], 158 | 'operator-linebreak': [2, 'after', { 159 | 'overrides': { 160 | '?': 'before', 161 | ':': 'before' 162 | } 163 | }], 164 | 'padded-blocks': [2, 'never'], 165 | 'quotes': [2, 'single', { 166 | 'avoidEscape': true, 167 | 'allowTemplateLiterals': true 168 | }], 169 | // 'semi': [2, 'never'], 170 | // 'semi-spacing': [2, { 171 | // 'before': false, 172 | // 'after': true 173 | // }], 174 | 'space-before-blocks': [2, 'always'], 175 | 'space-before-function-paren': [2, 'never'], 176 | 'space-in-parens': [2, 'never'], 177 | 'space-infix-ops': 2, 178 | 'space-unary-ops': [2, { 179 | 'words': true, 180 | 'nonwords': false 181 | }], 182 | 'spaced-comment': [2, 'always', { 183 | 'markers': ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ','] 184 | }], 185 | 'template-curly-spacing': [2, 'never'], 186 | 'use-isnan': 2, 187 | 'valid-typeof': 2, 188 | 'wrap-iife': [2, 'any'], 189 | 'yield-star-spacing': [2, 'both'], 190 | 'yoda': [2, 'never'], 191 | 'prefer-const': 2, 192 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, 193 | 'object-curly-spacing': [2, 'always', { 194 | objectsInObjects: false 195 | }], 196 | 'array-bracket-spacing': [2, 'never'] 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/views/SongMenu.vue: -------------------------------------------------------------------------------- 1 | 72 | 73 | 93 | 278 | -------------------------------------------------------------------------------- /src/components/HomeTop/logo.svg: -------------------------------------------------------------------------------- 1 | --------------------------------------------------------------------------------