├── .browserslistrc ├── public └── favicon.ico ├── src ├── shims-vue.d.ts ├── assets │ ├── images │ │ ├── logo.png │ │ ├── singer-bg.png │ │ ├── login-avatar.jpg │ │ ├── music-load.jpg │ │ ├── music-error.svg │ │ └── music-ico.svg │ └── less │ │ └── reset.less ├── doc │ └── images │ │ ├── comment.gif │ │ ├── player.gif │ │ ├── search.gif │ │ ├── singer.gif │ │ └── createSong.gif ├── views │ ├── player │ │ ├── image │ │ │ ├── random.png │ │ │ ├── listloop.png │ │ │ ├── singleloop.png │ │ │ └── musiclist.svg │ │ ├── readme.ts │ │ └── childComp │ │ │ └── mini-player.vue │ ├── my │ │ ├── childRouter │ │ │ ├── my_radio.vue │ │ │ ├── my_star.vue │ │ │ ├── watch_new_music.vue │ │ │ └── play_history.vue │ │ ├── index.vue │ │ └── childComp │ │ │ ├── my_music.vue │ │ │ └── head.vue │ ├── searchResult │ │ └── childComp │ │ │ ├── radio.vue │ │ │ ├── album.vue │ │ │ ├── topbar.vue │ │ │ ├── video.vue │ │ │ ├── singer.vue │ │ │ ├── song-sheet.vue │ │ │ ├── user.vue │ │ │ └── single.vue │ ├── video │ │ └── index.vue │ ├── find │ │ ├── image │ │ │ ├── paihang.svg │ │ │ ├── music.svg │ │ │ ├── mv.svg │ │ │ ├── diantai.svg │ │ │ └── jiemu.svg │ │ ├── childComp │ │ │ ├── recommend.vue │ │ │ ├── find-swiper.vue │ │ │ ├── new-album.vue │ │ │ └── recmend-songlist.vue │ │ └── index.vue │ ├── singer │ │ └── childComp │ │ │ └── topbar.vue │ ├── singerDetail │ │ └── childComp │ │ │ ├── mv.vue │ │ │ ├── album.vue │ │ │ ├── home.vue │ │ │ └── head.vue │ ├── search │ │ ├── index.vue │ │ └── childComp │ │ │ ├── search-history.vue │ │ │ └── hot-search-list.vue │ ├── songManage │ │ └── updateSong │ │ │ ├── image │ │ │ ├── emotion.svg │ │ │ ├── style.svg │ │ │ ├── theme.svg │ │ │ ├── language.svg │ │ │ └── scene.svg │ │ │ ├── edit-song-desc.vue │ │ │ └── edit-song-name.vue │ ├── test.vue │ ├── login │ │ ├── index.vue │ │ ├── phone.vue │ │ └── register.vue │ ├── comment │ │ └── childComp │ │ │ └── head.vue │ └── musicList │ │ ├── index.vue │ │ └── childComp │ │ ├── songlist.vue │ │ └── head.vue ├── components │ ├── common │ │ ├── loading │ │ │ ├── loading.gif │ │ │ └── loading.vue │ │ ├── toast │ │ │ ├── index.js │ │ │ ├── toast.vue │ │ │ ├── index.d.ts │ │ │ └── main.js │ │ ├── swiper │ │ │ └── SwiperItem.vue │ │ ├── navbar │ │ │ └── navbar.vue │ │ ├── message │ │ │ └── message.vue │ │ ├── kl-dialog │ │ │ └── kl-dialog.vue │ │ ├── gridview │ │ │ └── grid-view.vue │ │ ├── bottomPopup │ │ │ └── bottom-popup.vue │ │ ├── kl-confirm │ │ │ └── kl-confirm.vue │ │ ├── scroll │ │ │ └── scroll.vue │ │ └── scrollNavBar │ │ │ └── scroll-nav-bar.vue │ └── content │ │ ├── mv-list │ │ ├── mv-list.vue │ │ └── mv-list-items.vue │ │ ├── album-list │ │ ├── album-list.vue │ │ └── album-list-items.vue │ │ ├── single-list │ │ ├── single-list.vue │ │ └── single-list-items.vue │ │ ├── progress-circle │ │ └── progress-circle.vue │ │ ├── tab-bar │ │ └── tab-bar.vue │ │ ├── head-menu │ │ └── head-menu.vue │ │ ├── progress-bar │ │ └── progress-bar.vue │ │ ├── songlist-operation │ │ └── index.vue │ │ └── create-song-dialog │ │ └── index.vue ├── shims-tsx.d.ts ├── utils │ ├── debounce.ts │ ├── dom.ts │ ├── html.ts │ ├── formatDate.ts │ ├── cookie.ts │ ├── longpress.ts │ └── lyric-parser.js ├── store │ ├── interface.ts │ ├── getters.ts │ ├── index.ts │ └── actions.ts ├── service │ ├── player.ts │ ├── user.ts │ ├── service.ts │ ├── search.ts │ ├── find.ts │ ├── musiclist.ts │ ├── singer.ts │ ├── login.ts │ ├── rankinglist.ts │ └── songsheet.ts ├── router │ ├── musiclist.ts │ ├── login.ts │ ├── my.ts │ ├── songManage.ts │ └── index.ts ├── conf │ ├── playlist.ts │ └── songsInfo.ts ├── main.ts ├── vue-content-loader.d.ts └── App.vue ├── babel.config.js ├── .gitignore ├── postcss.config.js ├── tsconfig.json ├── package.json ├── vue.config.js └── README.en.md /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lang1427/vue-typescript-music/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import Vue from 'vue' 3 | export default Vue 4 | } 5 | -------------------------------------------------------------------------------- /src/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lang1427/vue-typescript-music/HEAD/src/assets/images/logo.png -------------------------------------------------------------------------------- /src/doc/images/comment.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lang1427/vue-typescript-music/HEAD/src/doc/images/comment.gif -------------------------------------------------------------------------------- /src/doc/images/player.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lang1427/vue-typescript-music/HEAD/src/doc/images/player.gif -------------------------------------------------------------------------------- /src/doc/images/search.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lang1427/vue-typescript-music/HEAD/src/doc/images/search.gif -------------------------------------------------------------------------------- /src/doc/images/singer.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lang1427/vue-typescript-music/HEAD/src/doc/images/singer.gif -------------------------------------------------------------------------------- /src/doc/images/createSong.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lang1427/vue-typescript-music/HEAD/src/doc/images/createSong.gif -------------------------------------------------------------------------------- /src/assets/images/singer-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lang1427/vue-typescript-music/HEAD/src/assets/images/singer-bg.png -------------------------------------------------------------------------------- /src/assets/images/login-avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lang1427/vue-typescript-music/HEAD/src/assets/images/login-avatar.jpg -------------------------------------------------------------------------------- /src/assets/images/music-load.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lang1427/vue-typescript-music/HEAD/src/assets/images/music-load.jpg -------------------------------------------------------------------------------- /src/views/player/image/random.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lang1427/vue-typescript-music/HEAD/src/views/player/image/random.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ], 5 | plugins: ["lodash"] 6 | } 7 | -------------------------------------------------------------------------------- /src/views/player/image/listloop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lang1427/vue-typescript-music/HEAD/src/views/player/image/listloop.png -------------------------------------------------------------------------------- /src/views/player/image/singleloop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lang1427/vue-typescript-music/HEAD/src/views/player/image/singleloop.png -------------------------------------------------------------------------------- /src/components/common/loading/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lang1427/vue-typescript-music/HEAD/src/components/common/loading/loading.gif -------------------------------------------------------------------------------- /src/components/common/toast/index.js: -------------------------------------------------------------------------------- 1 | import Toast from './main' 2 | 3 | export default{ 4 | install: function(Vue,opts = {}){ 5 | 6 | Vue.prototype.$toast = Toast 7 | 8 | } 9 | } 10 | 11 | -------------------------------------------------------------------------------- /src/assets/images/music-error.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.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/views/my/childRouter/my_radio.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 14 | -------------------------------------------------------------------------------- /src/views/my/childRouter/my_star.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 14 | -------------------------------------------------------------------------------- /src/shims-tsx.d.ts: -------------------------------------------------------------------------------- 1 | import Vue, { VNode } from 'vue' 2 | 3 | declare global { 4 | namespace JSX { 5 | // tslint:disable no-empty-interface 6 | interface Element extends VNode {} 7 | // tslint:disable no-empty-interface 8 | interface ElementClass extends Vue {} 9 | interface IntrinsicElements { 10 | [elem: string]: any 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/components/common/swiper/SwiperItem.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | 23 | -------------------------------------------------------------------------------- /src/utils/debounce.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @param funcName 需要进行防抖的函数名 4 | * @param delay 防抖所需时间 5 | */ 6 | export function debounce(funcName: any, delay: number = 50) { 7 | let timer: any = null 8 | 9 | return function(this: any) { 10 | if (timer) clearTimeout(timer) 11 | timer = window.setTimeout(() => { 12 | funcName.apply(this) 13 | }, delay) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/views/my/childRouter/watch_new_music.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 14 | -------------------------------------------------------------------------------- /src/views/searchResult/childComp/radio.vue: -------------------------------------------------------------------------------- 1 | 2 | 暂无内容 3 | 4 | 5 | 11 | 12 | -------------------------------------------------------------------------------- /src/utils/dom.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 获取 / 设置 自定义属性 3 | * @param el 元素 4 | * @param name 属性名 5 | * @param val 属性值 6 | */ 7 | export function getData(el: HTMLElement, name: string, val?: string) { 8 | const prefix = 'data-' 9 | name = prefix + name 10 | if (val) { 11 | // 如果有val就设置自定义属性 12 | return el.setAttribute(name, val) 13 | } else { 14 | // 获取自定义属性 15 | return el.getAttribute(name) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/store/interface.ts: -------------------------------------------------------------------------------- 1 | export interface IState { 2 | loadingShow: boolean 3 | searchKeyWrold: string | null 4 | searchHistory: string | never[] | null 5 | loginAccount: string | null 6 | account: object 7 | playList: object[] | null 8 | currentPlayIndex: number 9 | playMode: EPlayMode 10 | playHistory: object[] | null 11 | } 12 | // 定义播放的类型 13 | export enum EPlayMode { 14 | listLoop, // 0列表循环 15 | singleLoop, // 1单曲循环 16 | random // 2随机播放 17 | } -------------------------------------------------------------------------------- /src/views/video/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 此功能暂未开放,感谢您的谅解! 3 | 4 | 5 | 12 | -------------------------------------------------------------------------------- /src/service/player.ts: -------------------------------------------------------------------------------- 1 | import { service } from './service' 2 | 3 | // 音乐是否可用 4 | export function isCanMusic(id: number) { 5 | return service({ 6 | url: '/check/music', 7 | params: { 8 | id 9 | } 10 | }) 11 | } 12 | 13 | // 获取音乐url 14 | export function musicUrl(id: number) { 15 | return service({ 16 | url: '/song/url', 17 | params: { 18 | id 19 | } 20 | }) 21 | } 22 | 23 | // 获取音乐歌词 24 | export function musicLyric(id: number) { 25 | return service({ 26 | url: 'lyric', 27 | params: { 28 | id 29 | } 30 | }) 31 | } -------------------------------------------------------------------------------- /src/router/musiclist.ts: -------------------------------------------------------------------------------- 1 | 2 | const album = () => import(/*webpackChunkName:'album'*/'views/musicList/index.vue') 3 | const songsheet = () => import(/*webpackChunkName:'songsheet'*/'views/musicList/index.vue') 4 | const topList = () => import(/*webpackChunkName:'topList'*/'views/musicList/index.vue') // 排行榜 5 | 6 | export default [ 7 | { 8 | path: '/album/:id', 9 | name: 'album', 10 | component: album 11 | }, 12 | { 13 | path: '/songsheet/:id', 14 | name: 'songsheet', 15 | component: songsheet 16 | }, { 17 | path: '/toplist/:id', 18 | name: 'toplist', 19 | component: topList 20 | } 21 | ] -------------------------------------------------------------------------------- /src/views/find/image/paihang.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/common/loading/loading.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 正在努力加载中... 6 | 7 | 8 | 9 | 10 | 16 | 17 | 33 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {}, 4 | "postcss-px-to-viewport": { 5 | viewportWidth: 375, // 视窗的宽度,对应的是我们设计稿的宽度,Iphone6的一般是375 (xx/375*100vw) 6 | viewportHeight: 667, // 视窗的高度,Iphone6的一般是667 7 | unitPrecision: 3, // 指定`px`转换为视窗单位值的小数位数(很多时候无法整除) 8 | viewportUnit: "vw", // 指定需要转换成的视窗单位,建议使用vw 9 | selectorBlackList: ['.ignore', '.hairlines'], // 指定不转换为视窗单位的类,可以自定义,可以无限添加,建议定义一至两个通用的类名 10 | minPixelValue: 1, // 小于或等于`1px`不转换为视窗单位,你也可以设置为你想要的值 11 | mediaQuery: false, // 允许在媒体查询中转换`px` 12 | exclude: [/mini-player/] // 底部迷你版播放器不转化 忽略文件 不转化成视窗单位 (必须是正则) 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /src/views/player/image/musiclist.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/content/mv-list/mv-list.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 29 | 30 | -------------------------------------------------------------------------------- /src/service/user.ts: -------------------------------------------------------------------------------- 1 | import { service } from './service' 2 | 3 | // 登陆状态 4 | export function loginStatus() { 5 | return service({ 6 | url: '/login/status' 7 | }) 8 | } 9 | export class UserBaseInfo { 10 | userId!: number 11 | nickname!: string 12 | avatarUrl!: string 13 | constructor(profile: IProfile) { 14 | this.userId = profile.userId 15 | this.nickname = profile.nickname 16 | this.avatarUrl = profile.avatarUrl 17 | } 18 | } 19 | export interface IProfile { 20 | userId: number, 21 | nickname: string, 22 | avatarUrl: string 23 | } 24 | 25 | // 获取用户详情 26 | export function userDetail(id: number) { 27 | return service({ 28 | url: '/user/detail', 29 | params: { 30 | uid: id 31 | } 32 | }) 33 | } 34 | 35 | -------------------------------------------------------------------------------- /src/components/content/album-list/album-list.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 29 | 30 | 35 | -------------------------------------------------------------------------------- /src/service/service.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | import $store from '@/store/index' 4 | 5 | const baseURL = 6 | process.env.NODE_ENV === 'production' ? '' : 'http://localhost:3000' 7 | 8 | export function service(options: object): any { 9 | return new Promise((resolve, reject) => { 10 | const instance = axios.create({ 11 | baseURL, 12 | withCredentials: true 13 | }) 14 | instance.interceptors.request.use(req => { 15 | $store.state.loadingShow = true 16 | return req 17 | }) 18 | instance.interceptors.response.use(res => { 19 | $store.state.loadingShow = false 20 | return res.data 21 | }) 22 | 23 | instance(options) 24 | .then(res => { 25 | resolve(res) 26 | }) 27 | .catch(err => { 28 | reject(err) 29 | }) 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /src/utils/html.ts: -------------------------------------------------------------------------------- 1 | // html 编码(转义) 2 | export function htmlEncode(str: string) { 3 | var temp = ""; 4 | if (str.length == 0) return ""; 5 | temp = str.replace(/&/g, "&"); 6 | temp = temp.replace(//g, ">"); 8 | temp = temp.replace(/\s/g, " "); 9 | temp = temp.replace(/\'/g, "'"); 10 | temp = temp.replace(/\"/g, """); 11 | return temp; 12 | } 13 | 14 | // html编码(反转义) 15 | export function htmlDecode(str: string) { 16 | var temp = ""; 17 | if (str.length == 0) return ""; 18 | temp = str.replace(/&/g, "&"); 19 | temp = temp.replace(/</g, "<"); 20 | temp = temp.replace(/>/g, ">"); 21 | temp = temp.replace(/ /g, " "); 22 | temp = temp.replace(/'/g, "\'"); 23 | temp = temp.replace(/"/g, "\""); 24 | return temp; 25 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "experimentalDecorators": true, 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "sourceMap": true, 13 | "baseUrl": ".", 14 | "types": [ 15 | "webpack-env" 16 | ], 17 | "paths": { 18 | "@/*": [ 19 | "src/*" 20 | ] 21 | }, 22 | "lib": [ 23 | "esnext", 24 | "dom", 25 | "dom.iterable", 26 | "scripthost" 27 | ] 28 | }, 29 | "include": [ 30 | "src/**/*.ts", 31 | "src/**/*.tsx", 32 | "src/**/*.vue", 33 | "tests/**/*.ts", 34 | "tests/**/*.tsx" 35 | ], 36 | "exclude": [ 37 | "node_modules" 38 | ] 39 | } -------------------------------------------------------------------------------- /src/utils/formatDate.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * date: new Date(date*1000) 3 | * fmt: yyyy-MM-dd hh:mm:ss */ 4 | export function formatDate(date: Date, fmt: string) { 5 | if (/(y+)/.test(fmt)) { 6 | fmt = fmt.replace( 7 | RegExp.$1, 8 | (date.getFullYear() + '').substr(4 - RegExp.$1.length) 9 | ) 10 | } 11 | let o: any = { 12 | 'M+': date.getMonth() + 1, 13 | 'd+': date.getDate(), 14 | 'h+': date.getHours(), 15 | 'm+': date.getMinutes(), 16 | 's+': date.getSeconds() 17 | } 18 | for (let k in o) { 19 | if (new RegExp(`(${k})`).test(fmt)) { 20 | let str = o[k] + '' 21 | fmt = fmt.replace( 22 | RegExp.$1, 23 | RegExp.$1.length === 1 ? str : padLeftZero(str) 24 | ) 25 | } 26 | } 27 | return fmt 28 | } 29 | 30 | function padLeftZero(str: string) { 31 | return ('00' + str).substr(str.length) 32 | } 33 | -------------------------------------------------------------------------------- /src/components/common/navbar/navbar.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 21 | 22 | 42 | -------------------------------------------------------------------------------- /src/views/singer/childComp/topbar.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 歌手分类 8 | 9 | 10 | 11 | 12 | 28 | 29 | 46 | -------------------------------------------------------------------------------- /src/conf/playlist.ts: -------------------------------------------------------------------------------- 1 | interface IPlaylist { 2 | id: number 3 | name: string 4 | imgURL: string 5 | } 6 | export interface ISongs { 7 | songsId: number 8 | songsName: string 9 | imgUrl: string 10 | } 11 | // 播放列表容器的类 12 | export class PlayList implements IPlaylist { 13 | id: number 14 | name: string 15 | imgURL: string 16 | constructor(songs: ISongs) { 17 | this.id = songs.songsId 18 | this.name = songs.songsName 19 | this.imgURL = songs.imgUrl 20 | } 21 | } 22 | 23 | // 当有播放列表时,需要设置一个marginBottom,没有播放列表时去除marginBottom 24 | export function playerSetMarginBottom() { 25 | (document.querySelector('#app')).children[1] ? (document.querySelector('#app')).children[1].style.marginBottom = '50px' : false 26 | } 27 | export function playerRemoveMarginBottom() { 28 | (document.querySelector('#app')).children[1] ? (document.querySelector('#app')).children[1].style.marginBottom = '0px' : false 29 | } -------------------------------------------------------------------------------- /src/utils/cookie.ts: -------------------------------------------------------------------------------- 1 | /* 封装cookie */ 2 | 3 | // 获取cookie 4 | export function getCookie(name:string) { 5 | var arr:any; 6 | var reg = new RegExp('(^| )' + name + '=([^;]*)(;|$)') 7 | 8 | if (document.cookie.match(reg)) { 9 | arr = document.cookie.match(reg); 10 | return (arr[2]) 11 | } else { 12 | return null 13 | } 14 | } 15 | 16 | // 设置cookie,增加到vue实例方便全局调用 17 | export function setCookie(cName:string, value:string, expiredays:number) { 18 | var exdate:any = new Date() 19 | exdate.setDate(exdate.getDate() + expiredays) 20 | document.cookie = cName + '=' + escape(value) + ((expiredays == null) ? '' : ';expires=' + exdate.toGMTString()) 21 | } 22 | 23 | // 删除cookie 24 | export function delCookie(name:string) { 25 | var exp:any = new Date() 26 | exp.setTime(exp.getTime() - 1) 27 | var cval = getCookie(name) 28 | if (cval != null) { 29 | document.cookie = name + '=' + cval + ';expires=' + exp.toGMTString() 30 | } 31 | } -------------------------------------------------------------------------------- /src/components/content/single-list/single-list.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 33 | 34 | -------------------------------------------------------------------------------- /src/service/search.ts: -------------------------------------------------------------------------------- 1 | import { service } from './service' 2 | 3 | /**获取热搜列表数据 */ 4 | export function hotSearch() { 5 | return service({ 6 | url: '/search/hot/detail' 7 | }) 8 | } 9 | 10 | /** 输入框搜索建议 */ 11 | export function searchSuggest(keywords: string, type: string = 'mobile') { 12 | return service({ 13 | url: '/search/suggest', 14 | params: { 15 | keywords, 16 | type 17 | } 18 | }) 19 | } 20 | 21 | /**搜索 (翻页的坑,没有正确理解后台APi接口) 22 | * keywords:搜索关键字 23 | * type:类型:(1.单曲 10.专辑 100.歌手 1000.歌单 1002.用户 1004.MV 1006.歌词 1009.电台 1014.视频 1018.综合) 24 | * limit:返回数量(默认30) 25 | * offset:页数(默认0) 26 | * */ 27 | export function search( 28 | keywords: string, 29 | type: number = 1, 30 | limit: number = 30, 31 | offset: number = 0 // offset: 翻页 offset*limit 32 | ) { 33 | return service({ 34 | url: '/search', 35 | params: { 36 | keywords, 37 | limit, 38 | offset: offset * limit, 39 | type 40 | } 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /src/views/singerDetail/childComp/mv.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ message }} 5 | 6 | 7 | 8 | 9 | 35 | 36 | -------------------------------------------------------------------------------- /src/components/common/message/message.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ message }} 4 | 5 | 6 | 7 | 24 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | import store from './store' 5 | 6 | import 'font-awesome/css/font-awesome.css' 7 | 8 | Vue.config.productionTip = false 9 | 10 | import FastClick from 'fastclick' 11 | ;(FastClick).attach(document.body) 12 | 13 | import LazyLoad from 'vue-lazyload' 14 | Vue.use(LazyLoad, { 15 | loading: require('./assets/images/music-load.jpg'), 16 | error: require('./assets/images/music-error.svg') 17 | }) 18 | 19 | import Toast from './components/common/toast/index.js' 20 | Vue.use(Toast) 21 | 22 | Vue.prototype.$bus = new Vue() 23 | 24 | Vue.filter('finalPlayCount', (playCount: number): number | string => { 25 | if (playCount < 100000) { 26 | return playCount 27 | } else if (playCount >= 100000 && playCount < 100000000) { 28 | return (playCount / 10000).toFixed(0) + '万' 29 | } 30 | return (playCount / 100000000).toFixed(0) + '亿' 31 | }) 32 | 33 | new Vue({ 34 | router, 35 | store, 36 | render: h => h(App) 37 | }).$mount('#app') 38 | -------------------------------------------------------------------------------- /src/views/singerDetail/childComp/album.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ message }} 6 | 7 | 8 | 9 | 10 | 11 | 40 | 41 | 51 | -------------------------------------------------------------------------------- /src/views/search/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 39 | 40 | -------------------------------------------------------------------------------- /src/service/find.ts: -------------------------------------------------------------------------------- 1 | import { service } from './service' 2 | 3 | /**获取轮播图数据 4 | * 0:pc端 5 | * 1.Android端 6 | * 2.iphone 7 | * 3.ipad 8 | * */ 9 | export function getBanner(type: number = 0) { 10 | return service({ 11 | url: '/banner', 12 | params: { 13 | type: type 14 | } 15 | }) 16 | } 17 | export class bannerData { 18 | private readonly bannerId: string 19 | private readonly pic: string 20 | private readonly titleColor: string 21 | private readonly typeTitle: string 22 | private readonly url: string 23 | constructor(bannerlist: any) { 24 | this.bannerId = bannerlist.bannerId 25 | this.pic = bannerlist.pic 26 | this.titleColor = bannerlist.titleColor 27 | this.typeTitle = bannerlist.typeTitle 28 | this.url = bannerlist.url 29 | } 30 | } 31 | 32 | /**获取推荐歌单数据 */ 33 | export function getSonglist(limit: number = 30) { 34 | return service({ 35 | url: '/personalized', 36 | params: { 37 | limit 38 | } 39 | }) 40 | } 41 | 42 | /**获取新碟上线数据 */ 43 | export function getNewAlbum() { 44 | return service({ 45 | url: '/album/newest' 46 | }) 47 | } 48 | 49 | -------------------------------------------------------------------------------- /src/service/musiclist.ts: -------------------------------------------------------------------------------- 1 | import { service } from './service' 2 | 3 | /** 获取专辑内容 (新碟返回的数据为专辑) 需要当前专辑的id*/ 4 | export function albumContent(id: number) { 5 | return service({ 6 | url: '/album', 7 | params: { 8 | id 9 | } 10 | }) 11 | } 12 | interface IAlbum { 13 | picUrl: string 14 | name: string 15 | artist: { 16 | name: string 17 | id: number 18 | } 19 | publishTime: number 20 | description: string 21 | info: { 22 | commentCount: number 23 | shareCount: number 24 | } 25 | } 26 | export class AlbumBaseInfo { 27 | imgUrl: string 28 | title: string 29 | singerName: string 30 | singerId: number 31 | publishTime: number 32 | description: string 33 | commentCount: number 34 | shareCount: number 35 | constructor(album: IAlbum) { 36 | this.imgUrl = album.picUrl 37 | this.title = album.name 38 | this.singerName = album.artist.name 39 | this.singerId = album.artist.id 40 | this.publishTime = album.publishTime 41 | this.description = album.description 42 | this.commentCount = album.info.commentCount // 评论数量 43 | this.shareCount = album.info.shareCount // 分享数量 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/vue-content-loader.d.ts: -------------------------------------------------------------------------------- 1 | import { VueConstructor } from 'vue'; 2 | 3 | export const ContentLoader: ContentLoaderConstructor 4 | export const FacebookLoader: FacebookLoaderConstructor 5 | export const BulletListLoader: BulletListLoaderConstructor 6 | export const InstagramLoader: InstagramLoaderConstructor 7 | export const CodeLoader: CodeLoaderConstructor 8 | export const ListLoader: ListLoaderConstructor 9 | 10 | export interface ContentLoaderProps { 11 | width: number, 12 | height: number, 13 | speed: number, 14 | preserveAspectRatio: string, 15 | primaryColor: string, 16 | secondaryColor: string, 17 | uniqueKey: string, 18 | animate: boolean 19 | } 20 | 21 | export interface ContentLoaderConstructor extends VueConstructor { 22 | props: ContentLoaderProps 23 | } 24 | 25 | export interface FacebookLoaderConstructor extends ContentLoaderConstructor{} 26 | export interface CodeLoaderConstructor extends ContentLoaderConstructor{} 27 | export interface BulletListLoaderConstructor extends ContentLoaderConstructor{} 28 | export interface InstagramLoaderConstructor extends ContentLoaderConstructor{} 29 | export interface ListLoaderConstructor extends ContentLoaderConstructor{} -------------------------------------------------------------------------------- /src/router/login.ts: -------------------------------------------------------------------------------- 1 | const login = () => import(/*webpackChunkName:'login'*/'@/views/login/index.vue') 2 | 3 | const phone = () => import(/*webpackChunkName:'phone'*/'@/views/login/phone.vue') 4 | const loginPhone = ()=>import(/*webpackChunkName:'loginPhone'*/'@/views/login/login-phone.vue') 5 | 6 | const email = () => import(/*webpackChunkName:'email'*/'@/views/login/email.vue') 7 | 8 | const register = ()=>import(/*webpackChunkName:'register'*/'@/views/login/register.vue') 9 | 10 | 11 | export default [ 12 | { 13 | path: '/login', 14 | name: 'login', 15 | component: login, 16 | children: [ 17 | { 18 | path: 'phone', 19 | name: 'phone', 20 | component: phone 21 | }, 22 | { 23 | path:'login-phone', 24 | name:'loginPhone', 25 | component:loginPhone 26 | }, 27 | { 28 | path: 'email', 29 | name: 'email', 30 | component: email 31 | } 32 | ] 33 | }, 34 | { 35 | path:'/register', 36 | name:'register', 37 | component:register 38 | } 39 | 40 | ] -------------------------------------------------------------------------------- /src/store/getters.ts: -------------------------------------------------------------------------------- 1 | import { IState } from './interface' 2 | export default { 3 | encodeLoginAccount(state: IState) { 4 | return (state.loginAccount).replace((state.loginAccount).substr(3, 4), '****') 5 | }, 6 | playListLength(state: IState) { 7 | return (state.playList).length 8 | }, 9 | // state.currentPlayIndex = -1 bug? state获取localstorage时需要使用三目运算符而是逻辑或 10 | // 这里的防错处理的比较多 播放列表发生改变时,如果播放索引大于播放列表时的需要做防止找不到id,name,img的处理 11 | playMusicID(state: IState) { 12 | return state.currentPlayIndex != -1 ? (state.currentPlayIndex < (state.playList).length ? (state).playList[state.currentPlayIndex].id : -1) : -1 13 | }, 14 | playMusicName(state: IState) { 15 | return state.currentPlayIndex != -1 ? (state.currentPlayIndex < (state.playList).length ? (state).playList[state.currentPlayIndex].name : '') : '' 16 | }, 17 | playMusicImg(state: IState) { 18 | return state.currentPlayIndex != -1 ? (state.currentPlayIndex < (state.playList).length ? (state).playList[state.currentPlayIndex].imgURL : '') : '' 19 | }, 20 | playHistoryLength(state:IState){ 21 | return (state.playHistory as object[]).length 22 | } 23 | } -------------------------------------------------------------------------------- /src/router/my.ts: -------------------------------------------------------------------------------- 1 | const my = () => import(/*webpackChunkName:'my'*/'@/views/my/index.vue') 2 | const playHistory = ()=>import(/*webpackChunkName:'playHistory'*/'@/views/my/childRouter/play_history.vue') 3 | const radio = () => import(/*webpackChunkName:'radio'*/'@/views/my/childRouter/my_radio.vue') 4 | const star = () => import(/*webpackChunkName:'star'*/'@/views/my/childRouter/my_star.vue') 5 | const watchNewMusic = () => import(/*webpackChunkName:'watchNewMusic'*/'@/views/my/childRouter/watch_new_music.vue') 6 | 7 | export default [ 8 | { 9 | path: '/my', 10 | name: 'my', 11 | component: my, 12 | children: [ 13 | { 14 | path:'playhistory', 15 | name:'playHistory', 16 | component:playHistory 17 | }, 18 | { 19 | path: 'radio', 20 | name: 'radio', 21 | component: radio 22 | }, { 23 | path: 'star', 24 | name: 'star', 25 | component: star 26 | }, { 27 | path: 'watchnewmusic', 28 | name: 'watchnewmusic', 29 | component: watchNewMusic 30 | } 31 | ] 32 | } 33 | ] 34 | 35 | -------------------------------------------------------------------------------- /src/views/singerDetail/childComp/home.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 播放热门50 6 | 7 | 8 | 9 | 10 | 11 | 12 | 32 | 33 | 49 | -------------------------------------------------------------------------------- /src/views/songManage/updateSong/image/emotion.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/utils/longpress.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | Vue.directive('longpress', { 4 | bind(el, binding) { 5 | 6 | let timer: any = null 7 | 8 | el.addEventListener('touchstart', function () { 9 | if (timer === null) { 10 | timer = window.setTimeout(() => { 11 | binding.value.methods(binding.value.params) 12 | }, 300) 13 | } 14 | }) 15 | 16 | el.addEventListener('touchmove', function () { 17 | window.clearTimeout(timer) 18 | timer = null 19 | }) 20 | 21 | el.addEventListener('touchend', function () { 22 | window.clearTimeout(timer) 23 | timer = null 24 | }) 25 | 26 | // var event = new CustomEvent("longpress", {"detail":{"message":'长按事件'}}) 27 | // el.dispatchEvent(event) 28 | } 29 | }) 30 | 31 | /** 32 | * 33 | 34 | 使用方式: 35 | params可以是 undefined, string, number, boolean, array, object 类型的值 36 | 37 | html: 38 | 39 | 自定义事件 40 | 41 | 42 | script: 43 | 44 | didi(id:string){ 45 | console.log(id) 46 | console.log('didi') 47 | } 48 | 49 | 50 | * 51 | */ -------------------------------------------------------------------------------- /src/views/find/image/music.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/conf/songsInfo.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface ISonginfo { 3 | id: number 4 | name: string 5 | al: { 6 | id: number 7 | picUrl: string 8 | } 9 | album: { 10 | id: number 11 | artist: { 12 | img1v1Url: string 13 | } 14 | } 15 | ar: [{ name: string }] 16 | artists: [{ name: string }] 17 | } 18 | 19 | export class SongsInfoClass { 20 | songsId: number 21 | songsName: string 22 | albumId: number 23 | imgUrl: string 24 | singerName: string 25 | constructor(songsInfo: ISonginfo) { 26 | this.songsId = songsInfo.id 27 | this.songsName = songsInfo.name 28 | this.albumId = songsInfo.al && songsInfo.al.id || songsInfo.album && songsInfo.album.id 29 | this.imgUrl = songsInfo.al && songsInfo.al.picUrl || songsInfo.album && songsInfo.album.artist.img1v1Url 30 | let singers: any[] = [] 31 | if (songsInfo.ar) { 32 | for (let item of songsInfo.ar) { 33 | singers.push(item.name) 34 | } 35 | } else { 36 | for (let item of songsInfo.artists) { 37 | singers.push(item.name) 38 | } 39 | } 40 | this.singerName = singers.length === 1 ? singers[0] : singers.join(' | ') 41 | } 42 | } -------------------------------------------------------------------------------- /src/views/find/image/mv.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/music-ico.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | 4 | Vue.use(Vuex) 5 | 6 | const state: IState = { 7 | loadingShow: false, 8 | searchKeyWrold: window.sessionStorage.getItem('searchKeyWrold') 9 | ? window.sessionStorage.getItem('searchKeyWrold') 10 | : '', 11 | searchHistory: JSON.parse( 12 | window.localStorage.getItem('musicHistorySearch') || '[]' 13 | ), 14 | loginAccount: window.sessionStorage.getItem('loginAccount') ? window.sessionStorage.getItem('loginAccount') : '', 15 | account: JSON.parse((window.localStorage).getItem('account')) || {}, 16 | playList: JSON.parse(window.localStorage.getItem('playlist') || '[]'), // 播放列表中的容器 17 | currentPlayIndex: (window.localStorage).getItem('playIndex') ? parseInt((window.localStorage).getItem('playIndex')) : -1, 18 | // currentPlayIndex: parseInt((window.localStorage).getItem('playIndex')) || -1, // 当前播放容器的索引值 19 | playMode: parseInt((window.localStorage).getItem('mode')) || EPlayMode.listLoop, 20 | playHistory: JSON.parse(window.localStorage.getItem('playHistory') || '[]') 21 | } 22 | import { IState, EPlayMode } from './interface' 23 | import { mutations } from './mutatioins' 24 | import { actions } from './actions' 25 | import getters from './getters' 26 | export default new Vuex.Store({ 27 | state, 28 | mutations, 29 | actions, 30 | getters 31 | }) 32 | -------------------------------------------------------------------------------- /src/service/singer.ts: -------------------------------------------------------------------------------- 1 | import { service } from '@/service/service' 2 | 3 | /** 获取100个热门歌手 */ 4 | export function getSinger() { 5 | return service({ 6 | url: '/top/artists?limit=100' 7 | }) 8 | } 9 | export interface ISinger { 10 | id: number 11 | name: string 12 | picUrl: string 13 | pin: string 14 | } 15 | export class SingerData { 16 | id!: number 17 | name!: string 18 | imgUrl!: string 19 | 20 | constructor(artists: ISinger) { 21 | this.id = artists.id 22 | this.name = artists.name 23 | this.imgUrl = artists.picUrl 24 | } 25 | } 26 | 27 | /** 获取歌手单曲(可获得部分信息和热门歌曲) 用于歌手详情页 */ 28 | export function getSingerDetail(id: number) { 29 | return service({ 30 | url: '/artists', 31 | params: { 32 | id 33 | } 34 | }) 35 | } 36 | export interface ISingerHeadInfo { 37 | musicSize?: number 38 | albumSize?: number 39 | mvSize?: number 40 | img1v1Url?: string 41 | } 42 | 43 | /** 获取歌手专辑 */ 44 | export function getSingerAlbum( 45 | id: number, 46 | offset: number = 0, 47 | limit: number = 50 48 | ) { 49 | return service({ 50 | url: '/artist/album', 51 | params: { 52 | id, 53 | offset: limit * offset, 54 | limit 55 | } 56 | }) 57 | } 58 | 59 | /** 获取歌手MV */ 60 | export function getSingerMv(id: number) { 61 | return service({ 62 | url: '/artist/mv', 63 | params: { 64 | id 65 | } 66 | }) 67 | } 68 | -------------------------------------------------------------------------------- /src/views/find/image/diantai.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-typescript-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 | "@types/better-scroll": "^1.12.1", 11 | "@types/fastclick": "^1.0.28", 12 | "@types/lodash": "^4.14.149", 13 | "axios": "^0.19.0", 14 | "better-scroll": "^1.13.2", 15 | "core-js": "^3.4.3", 16 | "fastclick": "^1.0.6", 17 | "font-awesome": "^4.7.0", 18 | "lodash": "^4.17.15", 19 | "moment": "^2.26.0", 20 | "pinyin": "^2.9.0", 21 | "vue": "^2.6.10", 22 | "vue-class-component": "^7.0.2", 23 | "vue-content-loader": "^0.2.3", 24 | "vue-lazyload": "^1.3.3", 25 | "vue-property-decorator": "^8.3.0", 26 | "vue-router": "^3.1.3", 27 | "vuex": "^3.1.2" 28 | }, 29 | "devDependencies": { 30 | "@vue/cli-plugin-babel": "^4.1.0", 31 | "@vue/cli-plugin-typescript": "^4.1.0", 32 | "@vue/cli-service": "^4.1.0", 33 | "babel-plugin-lodash": "^3.3.4", 34 | "less": "^3.0.4", 35 | "less-loader": "^5.0.0", 36 | "lodash-webpack-plugin": "^0.11.5", 37 | "moment-locales-webpack-plugin": "^1.2.0", 38 | "postcss-px-to-viewport": "^1.1.1", 39 | "style-resources-loader": "^1.3.2", 40 | "typescript": "~3.5.3", 41 | "vue-template-compiler": "^2.6.10", 42 | "webpack-bundle-analyzer": "^3.6.1" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/components/content/single-list/single-list-items.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ order }} 4 | 5 | {{ listItems.songsName }} 6 | {{ listItems.singerName }} 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 31 | 32 | -------------------------------------------------------------------------------- /src/views/my/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 50 | -------------------------------------------------------------------------------- /src/components/content/mv-list/mv-list-items.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ▷{{ mvListItems.playCount | finalPlayCount }} 8 | 9 | 10 | {{ mvListItems.name }} 11 | {{ mvListItems.publishTime }} 12 | 13 | 14 | 15 | 16 | 29 | 30 | 63 | -------------------------------------------------------------------------------- /src/views/find/childComp/recommend.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | {{ item.name }} 11 | 12 | 13 | 14 | 15 | 37 | 38 | 56 | -------------------------------------------------------------------------------- /src/views/singerDetail/childComp/head.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ singerHeadInfo.name }} 5 | {{ singerHeadInfo.briefDesc }} 6 | 7 | 8 | 9 | 10 | 32 | 33 | 63 | -------------------------------------------------------------------------------- /src/service/login.ts: -------------------------------------------------------------------------------- 1 | import {service} from './service' 2 | 3 | // 发送验证码 4 | export function sendVerifyCode(phone:string){ 5 | return service({ 6 | url:'/captcha/sent', 7 | params:{ 8 | phone 9 | } 10 | }) 11 | } 12 | // 验证验证码 13 | export function testVerifyCode(phone:string,captcha:string){ 14 | return service({ 15 | url:'/captcha/verify', 16 | params:{ 17 | phone, 18 | captcha 19 | } 20 | }) 21 | } 22 | // 检测手机号码是否已注册 23 | export function testIsRegister(phone:string){ 24 | return service({ 25 | url:'/cellphone/existence/check', 26 | params:{ 27 | phone 28 | } 29 | }) 30 | } 31 | 32 | // 注册/修改密码 33 | export function registerAccount(captcha:string,phone:string,password:string,nikename:string){ 34 | return service({ 35 | url:'/register/cellphone', 36 | params:{ 37 | captcha, 38 | phone, 39 | password, 40 | nikename 41 | } 42 | }) 43 | } 44 | 45 | // 手机登录 46 | export function phoneLogin(phone:string,password:string){ 47 | return service({ 48 | url:'/login/cellphone', 49 | params:{ 50 | phone, 51 | password 52 | } 53 | }) 54 | } 55 | // 邮箱登录 56 | export function emailLogin(email:string,password:string){ 57 | return service({ 58 | url:'/login', 59 | params:{ 60 | email, 61 | password 62 | } 63 | }) 64 | } -------------------------------------------------------------------------------- /src/router/songManage.ts: -------------------------------------------------------------------------------- 1 | const deleteSong = () => import(/*webpackChunkName:'deleteSong'*/'@/views/songManage/delete-song.vue') 2 | const updateSong = () => import(/*webpackChunkName:'updateSong'*/'@/views/songManage/updateSong/index.vue') 3 | const editSongName = () => import(/*webpackChunkName:'editSongName'*/'@/views/songManage/updateSong/edit-song-name.vue') 4 | const editSongTags = () => import(/*webpackChunkName:'editSongTags'*/'@/views/songManage/updateSong/edit-song-tags.vue') 5 | const editSongDesc = () => import(/*webpackChunkName:'editSongDesc'*/'@/views/songManage/updateSong/edit-song-desc.vue') 6 | const addSong = ()=>import(/*webpackChunkName:'addSong'*/'@/views/songManage/add-song.vue') 7 | 8 | export default [ 9 | { 10 | path: '/songmanage/delete', 11 | name: 'deleteSong', 12 | component: deleteSong 13 | }, 14 | { 15 | path: '/songmanage/update', 16 | name: 'updateSong', 17 | component: updateSong, 18 | children: [ 19 | { 20 | path: 'editname', 21 | name: 'editSongName', 22 | component: editSongName 23 | }, { 24 | path: 'edittags', 25 | name: 'editSongTags', 26 | component: editSongTags 27 | }, { 28 | path: 'editdesc', 29 | name: 'editSongDesc', 30 | component: editSongDesc 31 | } 32 | ] 33 | },{ 34 | path:'/songmanage/add', 35 | name:'addSong', 36 | component:addSong 37 | } 38 | ] -------------------------------------------------------------------------------- /src/views/songManage/updateSong/image/style.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | // const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin 3 | const LodashWebpackPlugin = require('lodash-webpack-plugin') 4 | const MomentLocalesPlugin = require('moment-locales-webpack-plugin') 5 | module.exports = { 6 | 7 | publicPath: process.env.NODE_ENV === 'production' ? './' : '/', 8 | 9 | productionSourceMap: false, 10 | 11 | chainWebpack: confirm => { 12 | const types = ['vue-modules', 'vue', 'normal-module', 'normal'] 13 | types.forEach(type => { 14 | addStyleResource(confirm.module.rule('less').oneOf(type)) 15 | }) 16 | }, 17 | css: { 18 | loaderOptions: { 19 | less: { 20 | javascriptEnabled: true 21 | } 22 | } 23 | }, 24 | 25 | configureWebpack: { 26 | plugins: [ 27 | // new BundleAnalyzerPlugin() 28 | new LodashWebpackPlugin() // 通过 webpack-bundle-analyzer和babel-plugin-lodash 对 lodash 进行按需引入 29 | ,new MomentLocalesPlugin({ 30 | localesToKeep: ['es-us', 'zh-cn'] 31 | }) // 移除 moment 不必要的语言环境 32 | ], 33 | resolve: { 34 | alias: { 35 | 'components': '@/components', 36 | 'utils': '@/utils', 37 | 'views': '@/views' 38 | } 39 | } 40 | } 41 | } 42 | 43 | function addStyleResource(rule) { 44 | rule.use('style-resource').loader('style-resources-loader').options({ 45 | patterns: [ 46 | path.resolve(__dirname, './src/assets/less/reset.less') 47 | ] 48 | }) 49 | } -------------------------------------------------------------------------------- /src/views/find/childComp/find-swiper.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 37 | 38 | 54 | -------------------------------------------------------------------------------- /src/components/content/progress-circle/progress-circle.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 12 | 22 | 23 | 24 | 25 | 26 | 27 | 44 | -------------------------------------------------------------------------------- /src/views/searchResult/childComp/album.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 52 | 53 | 66 | -------------------------------------------------------------------------------- /src/views/songManage/updateSong/image/theme.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/common/kl-dialog/kl-dialog.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 42 | -------------------------------------------------------------------------------- /src/views/searchResult/childComp/topbar.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 43 | 44 | -------------------------------------------------------------------------------- /src/views/test.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | content-loader 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | facebook-loader 13 | 14 | code-loader 15 | 16 | bullet-list-loader 17 | 18 | instagram-loader 19 | 20 | list-loader 21 | 22 | 23 | 24 | 25 | 52 | -------------------------------------------------------------------------------- /src/views/player/readme.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 2020/04/01 音乐播放器的实现思路: 3 | * 4 | * 播放操作: 5 | * 1.当点击音乐播放列表时,将当前的音乐列表存放在一个容器中,并记录当前播放音乐的索引值 6 | * 2.监听当前播放音乐的索引值,一旦发生变化,就去请求 得到音乐的url地址 7 | * 3.当前音乐播放完了 跳入下一首 8 | * 4.播放完成之后判断播放模式(单曲循环、列表循环、随机播放) 9 | * 如果是单曲循环 则添加 loop属性 =====> (思路错误) 10 | * 当播放完成之后 在添加loop属性 已经没有任何意义了 还会导出其他模式下也是循环播放的状态 11 | * 解决的办法则是 设置当前的时长为0 让它回到初始状态 继续播放 12 | * 13 | * 而播放完成之后 则是进行下一首的播放 所以需要一个 next 方法 去调用 14 | * next 只有两种情况 列表循环和随机播放 当去点击下一首时 此时只能是列表循环模式 所以无论如何 只要不是 随机模式 即一定为 列表循环 15 | * prev 方法同上思想 只是currentPlayIndex减1 16 | * 如果是列表循环 则让当前的currentPlayIndex加1,需做超出判断 17 | * 如果是随机播放 则生成一个符合规则的随机数设置在当前的currentPlayIndex 规则:0 ~ playlist.length-1 整数 18 | * 19 | * 暂停/播放操作: 20 | * 默认为暂停状态,当然 如果音乐容器中没有内容是不允许显示播放器来的 21 | * 当音乐有url时,即设置为播放状态,此时容器中已有内容 22 | * 当点击暂停或播放图标时,将当前的播放状态取反向外发射通知, 23 | * 当父级组件收到通知后 立即 将当前播放状态 设置为子级传过来的参数 24 | * 并判断,如果是true 则此时需要从暂停状态 -> 播放状态 否则则相反 25 | * bug? 刷新后,再次点击播放 无效 26 | * 刷新后,默认是暂停状态,当点击播放时,因为没有url所以将会 报错: DOMException: The play() request was interrupted by a call to pause() 27 | * 解决方案: 28 | * 在mounted的时候手动调用一次获取音乐的url的请求,并设置为暂停状态 29 | * 因为mounted中才可以操作DOM ,另外需要注意的是图标的状态,所以采用函数回调的方式已达到暂停情况下时的图标的展示 30 | * 31 | * 32 | * 更新进度条(Peraent)操作: 33 | * audio有一个timeupdate事件(当前播放时间发生改变的时候执行) 34 | * 进度百分比 = 当前播放时长 / 总时长 35 | * 36 | * 37 | * 滑动过程中,将当前进度条的颜色设置到当前滑动的距离,以及时间 38 | * 错误想法 39 | * 如果是在播放状态下,滑动并没有结束而是没有在移动的情况下 进度条颜色会短路 bug? 解决思路:阀门 相同的还有当前时间 40 | * 定义一个正在滑动的布尔值变量isMove,默认为false状态 在timeupdate事件中进度条判断isMove是否为true 41 | * 滑动开始改变时,将isMove设置为true,滑动结束设置为false 42 | * true时:将不在timeupdate事件中对进度值赋值,并在滑动过程中进行赋值 43 | * 44 | * 正确想法 45 | * 同样是阀门的解决思路,不同点在于 改变当前时间 即等同于 改变当前进度 46 | * 47 | * 滑动结束时,拿到滑动的时间赋值给播放器上的当前时间,并判断当时是否为播放状态,进行对应时间段的播放与暂停 48 | * 49 | * 50 | * 播放模式的操作: 51 | * 因fontawesome字体图标库并不能提供完整的图标,所以采用png图片的形式 52 | * 对每一种图片采用不同的类 以此区分 53 | * 点击播放模式的使用,将该类样式设置并commit到state中 54 | * 55 | */ -------------------------------------------------------------------------------- /src/components/common/gridview/grid-view.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 69 | 70 | 76 | -------------------------------------------------------------------------------- /src/components/content/album-list/album-list-items.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ albumListItems.name }} 8 | {{ desc }} 9 | 10 | 11 | 12 | 13 | 49 | 50 | 77 | -------------------------------------------------------------------------------- /src/components/common/bottomPopup/bottom-popup.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 48 | 54 | -------------------------------------------------------------------------------- /src/views/search/childComp/search-history.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 历史记录 5 | 6 | 7 | 8 | 9 | {{ item }} 15 | 16 | 17 | 18 | 19 | 20 | 45 | 46 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 69 | 70 | 72 | -------------------------------------------------------------------------------- /src/components/content/tab-bar/tab-bar.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | {{ item.title }} 12 | {{ item.size }} 13 | 14 | 15 | 18 | 19 | 20 | 21 | 62 | 63 | -------------------------------------------------------------------------------- /src/views/login/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 手机号登陆 6 | 7 | 8 | 9 | 10 | 11 | 同意 12 | 《用户协议》 《隐私政策》 《儿童隐私协议》 13 | 14 | 15 | 16 | 17 | 18 | 41 | -------------------------------------------------------------------------------- /src/views/find/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | © 2019-2021 kanglang.xyz 版权所有 9 | 赣ICP备19003694号-1 10 | 11 | 12 | 13 | 14 | 75 | 76 | 80 | -------------------------------------------------------------------------------- /src/views/comment/childComp/head.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ title }} 8 | 9 | 10 | 评论区 11 | 12 | 最新 13 | 最热 14 | 15 | 16 | 17 | 18 | 19 | 45 | -------------------------------------------------------------------------------- /src/components/common/kl-confirm/kl-confirm.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ title }} 6 | 7 | {{ content }} 8 | 9 | 取消 10 | 确定 11 | 12 | 13 | 14 | 15 | 16 | 42 | -------------------------------------------------------------------------------- /src/store/actions.ts: -------------------------------------------------------------------------------- 1 | import lodash from 'lodash' 2 | 3 | export const actions = { 4 | // 添加历史搜索记录 5 | addHistorySearchArr(context: any, newVal: string) { 6 | let arr = context.state.searchHistory.find((item: string) => { 7 | if (item === newVal) { 8 | return true 9 | } 10 | }) 11 | if (arr) { 12 | return 13 | } else { 14 | context.commit('addHistorySearch', newVal) 15 | } 16 | }, 17 | // 登录方式 18 | loginMode(context: any, newVal: object) { 19 | let router = window.location.href 20 | if (router.match(/\/login\/email/)) { 21 | context.commit('emailLogin', newVal) 22 | } else { 23 | context.commit('phoneLogin', newVal) 24 | } 25 | }, 26 | // 加入音乐播放列表队列中 27 | changePlayList(context: any, newVal: object[]) { 28 | if (context.state.playList.length === 0) { 29 | context.commit('addPlaylist', newVal) 30 | } else { 31 | if (lodash.isEqual(context.state.playList, newVal)) return false // 与当前列表一致的话则不做commit 32 | context.commit('changePlaylist', newVal) 33 | } 34 | }, 35 | changeCurrentPlayIndex(context: any, newVal: number) { 36 | if (context.state.currentPlayIndex === -1) { 37 | context.commit('firstChangePlayIndex', newVal) 38 | } else { 39 | context.commit('changePlayIndex', newVal) 40 | } 41 | }, 42 | // 删除播放列表,如果是-1则清空列表,否则删除对应的歌曲 43 | removePlayList(context: any, newVal: number) { 44 | return new Promise((resolve, reject) => { 45 | if (newVal === -1) { 46 | context.commit('removeAll') 47 | resolve('清空播放列表') 48 | } else { 49 | context.commit('removeCurrent', newVal) 50 | resolve('删除当前选中歌曲') 51 | } 52 | }) 53 | 54 | }, 55 | // 播放历史记录操作 56 | operationPlayHistory(context: any, newVal: object | number) { 57 | 58 | if (lodash.isEqual(context.state.playList, context.state.playHistory)) return false 59 | 60 | if (newVal === -1) { 61 | context.commit('clearPlayHistory') 62 | return false 63 | } 64 | 65 | if (context.state.playHistory.length != 0) { 66 | let res = context.state.playHistory.findIndex((item: any) => { 67 | return item.id === (newVal as any).id 68 | }) 69 | if (res !== -1) { 70 | context.commit('removeCurrentPlayHistory', res) 71 | } 72 | } 73 | 74 | if (context.state.playHistory.length < 200) { 75 | context.commit('unshiftPlayHistory', newVal) 76 | } else { 77 | context.commit('splicePlayHistory', newVal) 78 | } 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /src/components/common/toast/toast.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 74 | -------------------------------------------------------------------------------- /src/views/find/image/jiemu.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/content/head-menu/head-menu.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{ item }} 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 74 | 75 | 102 | -------------------------------------------------------------------------------- /src/views/searchResult/childComp/video.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | {{ item.title }} 12 | {{ item.durationms }} by {{ item.creator[0].userName }} 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 60 | 61 | 99 | -------------------------------------------------------------------------------- /src/views/login/phone.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 手机号登录 8 | 9 | 10 | 未注册手机号登录后将自动创建账号 11 | 12 | +86 13 | 15 | 16 | 下一步 17 | 18 | 19 | 20 | 52 | -------------------------------------------------------------------------------- /src/components/common/scroll/scroll.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /src/components/common/toast/index.d.ts: -------------------------------------------------------------------------------- 1 | import Vue, {VNode,PluginObject} from 'vue' 2 | 3 | // export type Toast = 'success' | 'warning' | 'info' | 'error' 4 | 5 | /** Message Component */ 6 | export declare class KlToastComponent extends Vue { 7 | /** Close the Loading instance */ 8 | close (): void 9 | } 10 | 11 | export interface CloseEventHandler { 12 | /** 13 | * Triggers when a message is being closed 14 | * 15 | * @param instance The message component that is being closed 16 | */ 17 | (instance: KlToastComponent): void 18 | } 19 | 20 | /** Options used in Message */ 21 | export interface ElMessageOptions { 22 | /** Message text */ 23 | message: string | VNode 24 | 25 | /** Message type */ 26 | // type?: Toast 27 | 28 | 29 | /** Display duration, millisecond. If set to 0, it will not turn off automatically */ 30 | duration?: number 31 | 32 | /** Whether to show a close button */ 33 | // showClose?: boolean 34 | 35 | /** Whether to center the text */ 36 | // center?: boolean 37 | 38 | /** Whether message is treated as HTML string */ 39 | // dangerouslyUseHTMLString?: boolean 40 | 41 | /** Callback function when closed with the message instance as the parameter */ 42 | onClose?: CloseEventHandler 43 | 44 | /** Set the distance to the top of viewport. Default is 20 px. */ 45 | offset?: number 46 | } 47 | 48 | export interface KlToast { 49 | /** Show an info message */ 50 | (text: string): KlToastComponent 51 | 52 | /** Show message */ 53 | (options: ElMessageOptions): KlToastComponent 54 | 55 | /** Show a success message */ 56 | // success (text: string): KlToastComponent 57 | 58 | /** Show a success message with options */ 59 | // success (options: ElMessageOptions): KlToastComponent 60 | 61 | /** Show a warning message */ 62 | // warning (text: string): KlToastComponent 63 | 64 | /** Show a warning message with options */ 65 | // warning (options: ElMessageOptions): KlToastComponent 66 | 67 | /** Show an info message */ 68 | // info (text: string): KlToastComponent 69 | 70 | /** Show an info message with options */ 71 | // info (options: ElMessageOptions): KlToastComponent 72 | 73 | /** Show an error message */ 74 | // error (text: string): KlToastComponent 75 | 76 | /** Show an error message with options */ 77 | // error (options: ElMessageOptions): KlToastComponent 78 | } 79 | 80 | 81 | 82 | declare module 'vue/types/vue' { 83 | interface Vue { 84 | /** Used to show feedback after an activity. The difference with Notification is that the latter is often used to show a system level passive notification. */ 85 | $toast: KlToast 86 | } 87 | } 88 | 89 | export interface ToastPluginObject extends PluginObject {} 90 | 91 | declare var Toast: ToastPluginObject; 92 | export default Toast; -------------------------------------------------------------------------------- /src/assets/less/reset.less: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | div, 4 | span, 5 | applet, 6 | object, 7 | iframe, 8 | h1, 9 | h2, 10 | h3, 11 | h4, 12 | h5, 13 | h6, 14 | p, 15 | blockquote, 16 | pre, 17 | a, 18 | abbr, 19 | acronym, 20 | address, 21 | big, 22 | cite, 23 | code, 24 | del, 25 | dfn, 26 | em, 27 | img, 28 | ins, 29 | kbd, 30 | q, 31 | s, 32 | samp, 33 | small, 34 | strike, 35 | strong, 36 | sub, 37 | sup, 38 | tt, 39 | var, 40 | b, 41 | u, 42 | i, 43 | center, 44 | dl, 45 | dt, 46 | dd, 47 | ol, 48 | ul, 49 | li, 50 | fieldset, 51 | form, 52 | label, 53 | legend, 54 | table, 55 | caption, 56 | tbody, 57 | tfoot, 58 | thead, 59 | tr, 60 | th, 61 | td, 62 | article, 63 | aside, 64 | canvas, 65 | details, 66 | embed, 67 | figure, 68 | figcaption, 69 | footer, 70 | header, 71 | hgroup, 72 | main, 73 | menu, 74 | nav, 75 | output, 76 | ruby, 77 | section, 78 | summary, 79 | time, 80 | mark, 81 | audio, 82 | video { 83 | margin: 0; 84 | padding: 0; 85 | border: 0; 86 | vertical-align: baseline; 87 | box-sizing: border-box; 88 | font-family: "Microsoft YaHei", "微软雅黑"; 89 | color: #333; 90 | font-size: 14px; 91 | // touch-action: none; /*解决新版本谷歌浏览器的Unable to preventDefault inside passive event listener due to target being treated as passive.*/ 92 | touch-action: pan-y pinch-zoom; 93 | } 94 | 95 | /* HTML5 display-role reset for older browsers */ 96 | article, 97 | aside, 98 | details, 99 | figcaption, 100 | figure, 101 | footer, 102 | header, 103 | hgroup, 104 | main, 105 | menu, 106 | nav, 107 | section { 108 | display: block; 109 | } 110 | 111 | /* HTML5 hidden-attribute fix for newer browsers */ 112 | ol, 113 | ul { 114 | list-style: none; 115 | } 116 | 117 | blockquote, 118 | q { 119 | quotes: none; 120 | } 121 | 122 | blockquote:before, 123 | blockquote:after, 124 | q:before, 125 | q:after { 126 | content: ''; 127 | content: none; 128 | } 129 | 130 | /* 自定义更改统一样式 */ 131 | input[type="checkbox"]{ 132 | margin: 0; 133 | padding: 0; 134 | vertical-align: middle; 135 | } 136 | 137 | table { 138 | border-collapse: collapse; 139 | border-spacing: 0; 140 | } 141 | 142 | .fl { 143 | float: left !important; 144 | } 145 | 146 | .fr { 147 | float: right !important; 148 | } 149 | 150 | .clearfix::before, 151 | .clearfix::after { 152 | content: ""; 153 | display: table; 154 | } 155 | 156 | .clearfix::after { 157 | clear: both; 158 | } 159 | 160 | .clearfix { 161 | *zoom: 1; 162 | } 163 | 164 | .none { 165 | display: none; 166 | } 167 | 168 | 169 | [class^="fa-"] { 170 | font-family: FontAwesome !important; 171 | font-style: normal; 172 | } 173 | 174 | @klColor: rgb(244, 0, 0); 175 | 176 | input{ 177 | // 设置input光标颜色 178 | caret-color:@klColor!important; 179 | } -------------------------------------------------------------------------------- /src/service/rankinglist.ts: -------------------------------------------------------------------------------- 1 | // 排行榜相关 2 | 3 | import { service } from './service' 4 | 5 | interface IRankData { 6 | id: number 7 | name: string 8 | coverImgUrl: string 9 | updateFrequency: string 10 | tracks: [ 11 | { 12 | first: string 13 | second: string 14 | } 15 | ] 16 | } 17 | export class RankData { 18 | id: number 19 | name: string 20 | coverImgUrl: string 21 | updateFrequency: string 22 | songsInfo: object 23 | constructor(list: IRankData) { 24 | this.id = list.id 25 | this.name = list.name 26 | this.coverImgUrl = list.coverImgUrl 27 | this.updateFrequency = list.updateFrequency 28 | this.songsInfo = list.tracks 29 | } 30 | } 31 | // 所有榜单摘要 32 | export function topListDetail() { 33 | return service({ 34 | url: '/toplist/detail' 35 | }) 36 | } 37 | 38 | 39 | // 排行榜对应的idx 40 | export const rankIdx = [ 41 | "云音乐新歌榜", 42 | "云音乐热歌榜", 43 | "网易原创歌曲榜", 44 | "云音乐飙升榜", 45 | "云音乐电音榜", 46 | "UK排行榜周榜", 47 | "美国Billboard周榜", 48 | "KTV嗨榜", 49 | "iTunes榜", 50 | "Hit FM Top榜", 51 | "日本Oricon周榜", 52 | "韩国Melon排行榜周榜", 53 | "韩国Mnet排行榜周榜", 54 | "韩国Melon原声周榜", 55 | "中国TOP排行榜(港台榜)", 56 | "中国TOP排行榜(内地榜)", 57 | "香港电台中文歌曲龙虎榜", 58 | "华语金曲榜", 59 | "中国嘻哈榜", 60 | "法国 NRJ EuroHot 30周榜", 61 | "台湾Hito排行榜", 62 | "Beatport全球电子舞曲榜", 63 | "云音乐ACG音乐榜", 64 | "云音乐说唱榜", 65 | "云音乐古典音乐榜", 66 | "云音乐电音榜", 67 | "抖音排行榜", 68 | "新声榜", 69 | "云音乐韩语榜", 70 | "英国Q杂志中文版周榜", 71 | "电竞音乐榜", 72 | "云音乐欧美热歌榜", 73 | "云音乐欧美新歌榜", 74 | "说唱TOP榜" 75 | ] 76 | // 获取当前点击的排行榜数据 77 | export function topList(idx: number) { 78 | return service({ 79 | url: '/top/list', 80 | params: { 81 | idx 82 | } 83 | }) 84 | } 85 | interface IRankbaseinfo { 86 | coverImgUrl: string 87 | name: string 88 | tags: string[] 89 | id: number 90 | description: string 91 | updateTime:number 92 | commentCount: number 93 | shareCount: number 94 | } 95 | // 排行榜数据基本信息 96 | export class RankBaseInfo { 97 | imgUrl: string 98 | title: string 99 | tags: string[] 100 | singerId: number 101 | description: string 102 | publishTime:number 103 | commentCount: number 104 | shareCount: number 105 | constructor(rank: IRankbaseinfo) { 106 | this.imgUrl = rank.coverImgUrl 107 | this.title = rank.name 108 | this.tags = rank.tags 109 | this.singerId = rank.id 110 | this.description = rank.description 111 | this.publishTime = rank.updateTime 112 | this.commentCount = rank.commentCount // 评论数量 113 | this.shareCount = rank.shareCount // 分享数量 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/views/my/childComp/my_music.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 我的音乐 5 | 6 | 7 | 8 | 9 | {{ item.title }} 10 | {{ item.mes }} 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 42 | -------------------------------------------------------------------------------- /src/views/login/register.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 手机号注册 8 | 9 | 10 | 下一步 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 手机号注册 19 | 20 | 21 | 注册 22 | 23 | 24 | 25 | 26 | 68 | -------------------------------------------------------------------------------- /src/views/find/childComp/new-album.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 新碟 5 | 更多新碟 6 | 7 | 8 | 9 | 15 | 16 | {{ item.name }} 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 60 | 61 | -------------------------------------------------------------------------------- /src/views/player/childComp/mini-player.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ $store.getters.playMusicName }} 8 | 滑动可以切换上下首哦 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 49 | -------------------------------------------------------------------------------- /src/views/songManage/updateSong/image/language.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/views/searchResult/childComp/singer.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 13 | 14 | 15 | {{ item.name }} 16 | 17 | 已入驻 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 70 | 71 | 111 | -------------------------------------------------------------------------------- /src/views/my/childRouter/play_history.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ title }} 8 | 9 | 10 | 11 | 12 | 13 | 14 | {{ index+1 }} 15 | {{ item.name }} 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 63 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | 4 | Vue.use(VueRouter) 5 | 6 | // 解决 vue-router 新版本 重复点击路由 浏览器 Console 输出的异常 7 | const originalPush = VueRouter.prototype.push 8 | VueRouter.prototype.push = function push(location: string) { 9 | return (originalPush).call(this, location).catch((err: string) => err) 10 | } 11 | 12 | import myRoutes from './my' 13 | import loginRouters from './login' 14 | import musicListRouters from './musiclist' 15 | import songManage from './songManage' 16 | const find = () => import(/*webpackChunkName:'find'*/ 'views/find/index.vue') 17 | const rankingList = () => import(/*webpackChunkName:'rankingList'*/'views/rankingList/index.vue') 18 | const video = () => import(/*webpackChunkName:'video'*/'views/video/index.vue') 19 | 20 | const search = () => 21 | import(/*webpackChunkName:'search'*/ 'views/search/index.vue') 22 | const searchResult = () => 23 | import(/*webpackChunkName:'searchResult'*/ 'views/searchResult/index.vue') 24 | const singer = () => 25 | import(/*webpackChunkName:'singer'*/ 'views/singer/index.vue') 26 | const singerDetail = () => 27 | import(/*webpackChunkName:'singerDetail'*/ 'views/singerDetail/index.vue') 28 | 29 | const comment = () => import(/*webpackChunkName:'comment'*/'@/views/comment/index.vue') 30 | 31 | const test = () => import('views/test.vue') 32 | 33 | const routes = [ 34 | ...myRoutes, 35 | ...loginRouters, 36 | ...musicListRouters, 37 | ...songManage, 38 | { 39 | path: '/', 40 | redirect: '/find' 41 | }, 42 | { 43 | path: '/find', 44 | name: 'find', 45 | component: find 46 | }, 47 | { 48 | path: '/rankingList', 49 | name: 'rankingList', 50 | component: rankingList 51 | }, 52 | { 53 | path: '/video', 54 | name: 'video', 55 | component: video 56 | }, 57 | { 58 | path: '/search', 59 | name: 'search', 60 | component: search 61 | }, 62 | { 63 | path: '/searchresult', 64 | name: 'searchresult', 65 | component: searchResult 66 | }, 67 | { 68 | path: '/singer', 69 | name: 'singer', 70 | component: singer, 71 | /** 72 | * 子路由所带来的无穷问题? 73 | * 1.当请求歌手详情时,还会跟着请求热门歌手的数据 74 | 对location.href进行正则匹配,过滤多余请求 75 | * 2.歌手详情页滚动时,滚动到最底部 可见 热门歌手,需对其隐藏(问题最大的地方) 76 | 在歌手详情页对热门歌手数据中的元素添加隐藏类, 77 | 离开歌手详情页发送bus事件,通知热门歌手页去除隐藏类,这里并不能使用keep-alive提供的activated生命钩子函数 78 | * 3.歌手详情返回到热门歌手时,没有热门歌手数据可见 79 | 对热门歌手页进行keep-alive 80 | */ 81 | children: [ 82 | { 83 | path: 'detail/:id', 84 | name: 'singerDetail', 85 | component: singerDetail 86 | } 87 | ] 88 | }, 89 | { 90 | path: '/comment/:type', 91 | name: 'comment', 92 | component: comment 93 | }, 94 | 95 | { 96 | path: '/test', 97 | component: test 98 | } 99 | ] 100 | 101 | const router = new VueRouter({ 102 | routes 103 | }) 104 | 105 | export default router 106 | -------------------------------------------------------------------------------- /src/components/common/toast/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import toast from './toast.vue' 3 | 4 | /** 5 | * 可传入的参数: 6 | * message: 提示消息 7 | * offset: 顶部偏移量 8 | */ 9 | 10 | const ToastConstructor = Vue.extend(toast) // 创建一个Toast构造器 11 | 12 | 13 | let instance; // 实例对象 14 | let instances = [] // 存放所有的实例对象 15 | 16 | 17 | const interval = 16 // 两个相连Toast弹出消息的DOM元素间隔 18 | let seed = 1 19 | let zIndex = 2020 20 | const offsetTop = 60 // 默认 距离顶部的偏移量 21 | 22 | const Toast = function (options = options || {}) { 23 | 24 | if (Vue.prototype.$isServer) return // 判断是否为服务端渲染,如果是则不往后执行了 25 | 26 | if (typeof options === 'string') { // 如果当前用户调用 $toast() 时 传入的是一个字符串类型的数据 只直接让它等同于 message 27 | options = { 28 | message: options 29 | } 30 | } 31 | 32 | let id = 'toast_message_' + seed++ // 记录每一个toast的id (id递增) 33 | let userOnClose = options.onClose 34 | 35 | options.onClose = function () { // 关闭方法 36 | Toast.close(id, userOnClose) 37 | } 38 | 39 | instance = new ToastConstructor({ 40 | data: options // 实例化时需要把options放进去,否则toast组件中data的值都为初始默认值 41 | }) // 实例化构造器 42 | 43 | instance.id = id; // 将当前id挂载在当前实例中,在后续通过id进行操作 44 | 45 | instance.$mount() // 将该实例挂载到 新创建的 div中 46 | 47 | document.body.appendChild(instance.$el) // 将该 div 放在body元素内 48 | 49 | instance.message = options.message ? options.message : instance.message; 50 | 51 | 52 | let verticalOffset = options.offset || offsetTop 53 | instances.forEach(item => { 54 | verticalOffset += item.$el.offsetHeight + interval 55 | }) 56 | 57 | instance.offsetTop = verticalOffset 58 | instance.visible = true 59 | instance.$el.style.zIndex = zIndex++ 60 | 61 | instances.push(instance) 62 | 63 | return instance 64 | } 65 | 66 | // 关闭当前元素 67 | Toast.close = function (id, userOnClose) { 68 | let len = instances.length // 得到实例中的个数 69 | let index = -1 // 初始化当前index 70 | let removeHeight; 71 | for (let i = 0; i < len; i++) { 72 | if (id === instances[i].id) { // 如果toast的id等于当前实例对象的id 73 | removeHeight = instances[i].$el.offsetHeight // 得到当前实例的dom对象距离顶部的高度 74 | index = i 75 | if (typeof userOnClose === 'function') { 76 | userOnClose(instances[i]) // 关闭当前元素 77 | } 78 | instances.splice(i, 1) // 删除当前元素 79 | break; 80 | } 81 | } 82 | if (len <= 1 || index === - 1 || index > instances.length - 1) return // 不满足条件 则不往下执行 83 | // 因为移除了某个元素,需要统一对后面的元素设置为上一个元素的高度 84 | for (let i = index; i < len - 1; i++) { 85 | 86 | let dom = instances[i].$el 87 | 88 | dom.style['top'] = parseInt(dom.style['top'], 10) - removeHeight - interval + 'px' 89 | } 90 | } 91 | // 关闭所有元素 92 | Toast.closeAll = function () { 93 | for (let i = instanceps.length - 1; i >= 0; i--) { 94 | instanceps[i].close() 95 | } 96 | } 97 | 98 | 99 | export default Toast 100 | 101 | -------------------------------------------------------------------------------- /src/views/songManage/updateSong/edit-song-desc.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 歌单介绍 6 | 保存 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 | 17 | 51 | -------------------------------------------------------------------------------- /src/views/musicList/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 95 | -------------------------------------------------------------------------------- /src/views/searchResult/childComp/song-sheet.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 17 | 18 | 19 | 20 | 21 | {{ item.name }} 22 | 23 | {{ item.trackCount }}首 by {{ item.creator.nickname }} 播放{{ 24 | item.playCount | finalPlayCount 25 | }}次 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 78 | 79 | 115 | -------------------------------------------------------------------------------- /src/views/searchResult/childComp/user.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | {{ item.nickname }} 12 | {{ item.signature }} 13 | 14 | + 关注 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 61 | 62 | 113 | -------------------------------------------------------------------------------- /src/components/content/progress-bar/progress-bar.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 75 | -------------------------------------------------------------------------------- /src/views/search/childComp/hot-search-list.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 热搜榜 4 | 5 | 11 | {{ 12 | index + 1 13 | }} 14 | 15 | 16 | {{ item.searchWord }} 17 | 18 | 19 | {{ item.content }} 20 | 21 | {{ item.score }} 22 | 23 | 24 | 25 | 26 | 27 | 28 | 66 | 67 | 106 | -------------------------------------------------------------------------------- /src/views/find/childComp/recmend-songlist.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 推荐歌单 5 | 歌单广场 6 | 7 | 8 | 9 | 15 | 16 | 17 | {{ item.playCount | finalPlayCount }} 18 | 19 | 20 | {{ item.name }} 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 64 | 65 | -------------------------------------------------------------------------------- /src/components/content/songlist-operation/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {{ curSongInfo.songsName }} 10 | {{ curSongInfo.singerName }} 11 | 12 | 13 | 14 | 15 | 16 | 下一首播放 17 | 18 | 19 | 20 | 评论 21 | 22 | 23 | 24 | 25 | 26 | 27 | 64 | -------------------------------------------------------------------------------- /src/views/searchResult/childComp/single.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 播放全部 10 | 11 | 12 | 13 | 多选 14 | 15 | 16 | 17 | 23 | {{ item.songsName }} 24 | {{ item.singerName }} 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 79 | 80 | -------------------------------------------------------------------------------- /src/views/songManage/updateSong/edit-song-name.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 歌单名称 6 | 保存 7 | 8 | 9 | 10 | 17 | x 18 | 19 | 20 | 21 | 22 | 23 | 67 | -------------------------------------------------------------------------------- /src/views/my/childComp/head.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ isLogin.Nikename }} 8 | 9 | 立即登录 10 | 11 | 12 | 13 | 14 | 20 | 21 | {{ item.title }} 22 | 23 | 24 | 25 | 26 | 27 | 28 | 69 | -------------------------------------------------------------------------------- /src/views/songManage/updateSong/image/scene.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/content/create-song-dialog/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 新建歌单 5 | 6 | 7 | {{ inputLimit }} 8 | 9 | 10 | 取消 11 | 提交 12 | 13 | 14 | 15 | 16 | 17 | 89 | -------------------------------------------------------------------------------- /src/components/common/scrollNavBar/scroll-nav-bar.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | {{ item }} 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 88 | 89 | -------------------------------------------------------------------------------- /src/service/songsheet.ts: -------------------------------------------------------------------------------- 1 | // 歌单相关 网络请求 2 | import { service } from './service' 3 | 4 | // 获取用户歌单 5 | export function userSongsheet(id: number) { 6 | return service({ 7 | url: '/user/playlist?timestamp=' + Date.now(), 8 | params: { 9 | uid: id 10 | } 11 | }) 12 | } 13 | export interface IUserSongList { 14 | id: number 15 | name: string 16 | coverImgUrl: string 17 | trackCount: number 18 | } 19 | export class UserSongsheetInfo { 20 | private id: number 21 | private name: string 22 | private imgUrl: string 23 | private count: number 24 | constructor(userSongList: IUserSongList) { 25 | this.id = userSongList.id 26 | this.name = userSongList.name 27 | this.imgUrl = userSongList.coverImgUrl 28 | this.count = userSongList.trackCount 29 | } 30 | } 31 | 32 | /** 获取歌单详情 */ 33 | export function songsDetail(id: number) { 34 | return service({ 35 | url: '/playlist/detail?timestamp=' + Date.now(), 36 | params: { 37 | id 38 | } 39 | }) 40 | } 41 | interface ISongs { 42 | coverImgUrl: string 43 | name: string 44 | tags: string[] 45 | id: number 46 | description: string 47 | commentCount: number 48 | shareCount: number 49 | } 50 | export class SongsBaseInfo { 51 | imgUrl: string 52 | title: string 53 | tags: string[] 54 | singerId: number 55 | description: string 56 | commentCount: number 57 | shareCount: number 58 | constructor(songs: ISongs) { 59 | this.imgUrl = songs.coverImgUrl 60 | this.title = songs.name 61 | this.tags = songs.tags 62 | this.singerId = songs.id 63 | this.description = songs.description 64 | this.commentCount = songs.commentCount // 评论数量 65 | this.shareCount = songs.shareCount // 分享数量 66 | } 67 | } 68 | 69 | 70 | // 新建歌单 71 | export function createSongSheet(name: string) { 72 | return service({ 73 | url: "/playlist/create", 74 | params: { 75 | name 76 | } 77 | }) 78 | } 79 | 80 | /* 对歌单中添加或删除歌曲 81 | op: 从歌单增加单曲为 add, 删除为 del 82 | pid: 歌单 id 83 | tracks: 歌曲 id,可多个,用逗号隔开 84 | */ 85 | export function songsheetOperation(op: string, pid: number, tracks: string) { 86 | return service({ 87 | url: '/playlist/tracks', 88 | params: { 89 | op, 90 | pid, 91 | tracks 92 | } 93 | }) 94 | } 95 | 96 | // 删除歌单 97 | export function deleteSongsheet(id: string) { 98 | return service({ 99 | url: '/playlist/delete', 100 | params: { 101 | id 102 | } 103 | }) 104 | } 105 | 106 | 107 | /** 更新歌单操作 */ 108 | 109 | // 更新歌单名 110 | export function updateSongName(id: number, name: string) { 111 | return service({ 112 | url: '/playlist/name/update?timestamp=' + Date.now(), 113 | params: { 114 | id, 115 | name 116 | } 117 | }) 118 | } 119 | // 更新歌单标签 120 | export function updateSongTags(id: number, tags: string) { 121 | return service({ 122 | url: '/playlist/tags/update?timestamp=' + Date.now(), 123 | params: { 124 | id, 125 | tags 126 | } 127 | }) 128 | } 129 | // 更新歌单描述 130 | export function updateSongDesc(id: number, desc: string) { 131 | return service({ 132 | url: '/playlist/desc/update?timestamp=' + Date.now(), 133 | params: { 134 | id, 135 | desc 136 | } 137 | }) 138 | } -------------------------------------------------------------------------------- /src/views/musicList/childComp/songlist.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 播放全部 7 | {{ totalCount }} 8 | 9 | 10 | 16 | {{ index+1 }} 17 | 18 | 19 | 20 | 21 | {{ item.songsName }} 22 | {{ item.singerName }} 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 64 | -------------------------------------------------------------------------------- /README.en.md: -------------------------------------------------------------------------------- 1 | # vue-typescript-music 2 | 3 | ## Vue+TypeScript better-music-webapp (Music Project) 4 | 5 | [中文](./README.md) 6 | 7 | **Online access address** [online address](http://47.93.187.37/) 8 | 9 | **Project analysis online** [Source resolution address](https://blog.csdn.net/weixin_42661283/article/details/106552202) 10 | 11 | > vue-content-loader [TypeScript Support](https://github.com/egoist/vue-content-loader/pull/13) 12 | 13 | > Continuous improvement of projects and ongoing updates... 14 | 15 | ##### Please Star,Issues 16 | 17 | 1. **Your star is my driving force for continuous updating and maintenance!!!** 18 | 2. **If there are some problems during use, please issue** 19 | 20 | > Detailed notes perfect interpretation you deserve 21 | > Zero UI component library, hand-made 22 | 23 | ### Back end API dependency 24 | 25 | `NeteaseCloudMusicApi 3.29.0` 26 | 27 | 1. [Interface download address](https://github.com/Binaryify/NeteaseCloudMusicApi) 28 | 2. [Interface API document address](https://binaryify.github.io/NeteaseCloudMusicApi/#/?id=neteasecloudmusicapi) 29 | 30 | ### Example renderings 31 | 32 | Direct access to online address is recommended (the picture file is large and may not be loaded) 33 | 34 |  35 |  36 |  37 |  38 |  39 | 40 | 41 | 42 | 43 | ### Introduction to interface and function module 44 | 45 | 46 | 47 | **Personal page** 48 | 49 | - [x] login 50 | - [x] Play history 51 | - [ ] My radio station 52 | - [ ] My star 53 | - [ ] Focus on new songs 54 | - [ ] My music 55 | - [ ] My favorite music 56 | 57 | 58 | **Default page(Music Hall)** 59 | 60 | - [x] banner Rotation chart 61 | - [x] Recommended song list 62 | - [x] New dish 63 | - [ ] Recommended new music 64 | - [ ] Recommended radio station 65 | - [x] Ranking List 66 | - [ ] Recommended programs 67 | - [ ] Recommended MV 68 | 69 | 70 | **Search page** 71 | 72 | - [x] Hot search list 73 | - [x] History 74 | - [x] Singer classification 75 | - [x] Search input box function 76 | 77 | **Search result** 78 | 79 | - [x] Search results navigation 80 | - [x] Comprehensive 81 | - [x] Single 82 | - [x] Video 83 | - [x] Singer 84 | - [x] Album 85 | - [x] Song sheet 86 | - [x] Radio station 87 | - [x] Uiser 88 | 89 | **Singer details** 90 | 91 | - [x] Home page 92 | - [x] Album 93 | - [x] Mv 94 | 95 | **Comment interface** 96 | - [x] Comment like, cancel like 97 | - [x] Comment 98 | - [x] Reply to comments 99 | - [x] Copy comments 100 | - [x] Delete comment 101 | 102 | **Play music** 103 | 104 | - [x] Play 105 | - [x] Play up and down 106 | - [x] Slide to switch playback 107 | - [x] Play mode 108 | - [x] Play list 109 | - [x] Star Song sheet 110 | - [x] Delete playlist 111 | - [x] Download currently playing music 112 | - [x] Lyric 113 | 114 | **Video page** 115 | 116 | `Not yet open` 117 | 118 | 119 | 120 | 121 | --- 122 | 123 | ## Project setup 124 | 125 | ``` 126 | npm install 127 | ``` 128 | 129 | ### Compiles and hot-reloads for development 130 | 131 | ``` 132 | npm run serve 133 | ``` 134 | 135 | ### Compiles and minifies for production 136 | 137 | ``` 138 | npm run build 139 | ``` 140 | 141 | ### Run your tests 142 | 143 | ``` 144 | npm run test 145 | ``` 146 | 147 | ### Lints and fixes files 148 | 149 | ``` 150 | npm run lint 151 | ``` 152 | -------------------------------------------------------------------------------- /src/utils/lyric-parser.js: -------------------------------------------------------------------------------- 1 | const timeExp = /\[(\d{2,}):(\d{2})(?:\.(\d{2,3}))?]/g 2 | 3 | const STATE_PAUSE = 0 4 | const STATE_PLAYING = 1 5 | 6 | const tagRegMap = { 7 | title: 'ti', 8 | artist: 'ar', 9 | album: 'al', 10 | offset: 'offset', 11 | by: 'by' 12 | } 13 | 14 | function noop() { 15 | } 16 | // export default class Lyric{ 17 | module.exports = class Lyric { 18 | constructor(lrc, hanlder = noop) { 19 | this.lrc = lrc 20 | this.tags = {} 21 | this.lines = [] 22 | this.handler = hanlder 23 | this.state = STATE_PAUSE 24 | this.curLine = 0 25 | 26 | this._init() 27 | } 28 | 29 | _init() { 30 | this._initTag() 31 | 32 | this._initLines() 33 | } 34 | 35 | _initTag() { 36 | for (let tag in tagRegMap) { 37 | const matches = this.lrc.match(new RegExp(`\\[${tagRegMap[tag]}:([^\\]]*)]`, 'i')) 38 | this.tags[tag] = matches && matches[1] || '' 39 | } 40 | } 41 | 42 | _initLines() { 43 | const lines = this.lrc.split('\n') 44 | for (let i = 0; i < lines.length; i++) { 45 | const line = lines[i] 46 | let result = timeExp.exec(line) 47 | if (result) { 48 | const txt = line.replace(timeExp, '').trim() 49 | if (txt) { 50 | let tirdResult = result[3] || '0' 51 | let length = tirdResult.length 52 | let __tirdResult = parseInt(tirdResult, 10) 53 | __tirdResult = length > 2 && __tirdResult < 100 ? __tirdResult : __tirdResult > 99 ? __tirdResult : __tirdResult * 10 54 | this.lines.push({ 55 | time: result[1] * 60 * 1000 + result[2] * 1000 + __tirdResult, 56 | txt 57 | }) 58 | } 59 | } 60 | } 61 | 62 | this.lines.sort((a, b) => { 63 | return a.time - b.time 64 | }) 65 | } 66 | 67 | _findCurNum(time) { 68 | for (let i = 0; i < this.lines.length; i++) { 69 | if (time <= this.lines[i].time) { 70 | return i 71 | } 72 | } 73 | return this.lines.length - 1 74 | } 75 | 76 | _callHandler(i) { 77 | if (i < 0) { 78 | return 79 | } 80 | this.handler({ 81 | txt: this.lines[i].txt, 82 | lineNum: i 83 | }) 84 | } 85 | 86 | _playRest() { 87 | let line = this.lines[this.curNum] 88 | let delay = line.time - (+new Date() - this.startStamp) 89 | 90 | this.timer = setTimeout(() => { 91 | this._callHandler(this.curNum++) 92 | if (this.curNum < this.lines.length && this.state === STATE_PLAYING) { 93 | this._playRest() 94 | } 95 | }, delay) 96 | } 97 | 98 | play(startTime = 0, skipLast) { 99 | if (!this.lines.length) { 100 | return 101 | } 102 | this.state = STATE_PLAYING 103 | 104 | this.curNum = this._findCurNum(startTime) 105 | this.startStamp = +new Date() - startTime 106 | 107 | if (!skipLast) { 108 | this._callHandler(this.curNum - 1) 109 | } 110 | 111 | if (this.curNum < this.lines.length) { 112 | clearTimeout(this.timer) 113 | this._playRest() 114 | } 115 | } 116 | 117 | togglePlay() { 118 | var now = +new Date() 119 | if (this.state === STATE_PLAYING) { 120 | this.stop() 121 | this.pauseStamp = now 122 | } else { 123 | this.state = STATE_PLAYING 124 | this.play((this.pauseStamp || now) - (this.startStamp || now), true) 125 | this.pauseStamp = 0 126 | } 127 | } 128 | 129 | stop() { 130 | this.state = STATE_PAUSE 131 | clearTimeout(this.timer) 132 | } 133 | 134 | seek(offset) { 135 | this.play(offset) 136 | } 137 | } -------------------------------------------------------------------------------- /src/views/musicList/childComp/head.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{ topTitle }} 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 22 | 23 | 24 | 25 | 添加歌曲 26 | 27 | 28 | 编辑歌单信息 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 80 | --------------------------------------------------------------------------------
{{ listItems.songsName }}
{{ listItems.singerName }}
{{ item.name }}
{{ singerHeadInfo.briefDesc }}
{{ albumListItems.name }}
{{ desc }}
{{ item.title }}
{{ item.durationms }} by {{ item.creator[0].userName }}
{{ $store.getters.playMusicName }}
滑动可以切换上下首哦
16 | {{ item.searchWord }} 17 | 18 |
{{ item.content }}
{{ curSongInfo.songsName }}
{{ curSongInfo.singerName }}
{{ item.songsName }}
{{ item.singerName }}