├── .browserslistrc ├── .gitignore ├── .postcssrc.js ├── README.md ├── ads.txt ├── babel.config.js ├── deploy.bat ├── jest.config.js ├── manifest.json ├── package-lock.json ├── package.json ├── public ├── data │ └── result.json ├── favicon.ico ├── index.html ├── manifest.json └── res │ └── vcplayer │ ├── arrow.svg │ ├── cd.png │ ├── groove.png │ ├── icon.png │ ├── index.png │ ├── loop.svg │ ├── loopSingle.svg │ └── miku.gif ├── src ├── App.vue ├── api │ └── mainApi.ts ├── assets │ ├── bg.jpg │ ├── bg.png │ ├── like.svg │ ├── logo.png │ └── unlike.svg ├── base.scss ├── components │ ├── Audio.vue │ ├── Common │ │ ├── Confirm.vue │ │ └── DropdownList.vue │ ├── CreatePlayListModal.vue │ ├── FileItem.vue │ ├── HomeBottom.vue │ ├── LikeBtnGroup.vue │ ├── LoginModal.vue │ ├── Modal.vue │ ├── Operation │ │ ├── Loop.vue │ │ ├── Random.vue │ │ ├── SelectBottomTools.vue │ │ ├── TimeSlider.vue │ │ ├── VolumeIcon.vue │ │ └── VolumeSlider.vue │ ├── PlayList │ │ └── PlayListItem.vue │ ├── PlayListContentItem.vue │ ├── PlayListItemBase.vue │ ├── PlayingListContentItem.vue │ ├── PlayingToolBar.vue │ └── VPButton.vue ├── declaration.d.ts ├── main.ts ├── mixins │ ├── selectContainer.ts │ ├── selectItem.ts │ └── test.ts ├── mobile.scss ├── router.ts ├── shims-tsx.d.ts ├── shims-vue.d.ts ├── store │ ├── index.ts │ ├── modules │ │ ├── audio.ts │ │ ├── file.ts │ │ ├── home.ts │ │ ├── playList.ts │ │ ├── playing.ts │ │ └── test.ts │ └── types │ │ └── PlayList.ts ├── types │ └── BaseItem.ts ├── utils │ ├── Message.ts │ ├── Notice.ts │ ├── animatie.ts │ ├── config.ts │ ├── enum │ │ ├── LocalStorageKeys.ts │ │ └── LoopMode.ts │ ├── request.ts │ └── utils.ts ├── var.scss ├── views │ ├── AllPlayList.vue │ ├── Files.vue │ ├── Home.vue │ ├── PlayListView.vue │ ├── Playing.vue │ ├── RecentPlay.vue │ └── Test.vue └── vue.d.ts ├── tests └── unit │ ├── HelloWorld.spec.js │ ├── HelloWorld.spec.js.map │ └── HelloWorld.spec.ts ├── tsconfig.json ├── tslint.json └── vue.config.js /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not ie <= 10 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | test.txt 5 | error.log 6 | ~$error.log 7 | test.html 8 | 9 | # local env files 10 | .env.local 11 | .env.*.local 12 | 13 | # Log files 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | 18 | # Editor directories and files 19 | .vs 20 | .idea 21 | .vscode 22 | *.suo 23 | *.ntvs* 24 | *.njsproj 25 | *.sln 26 | *.sw* 27 | 28 | # command shortcut 29 | dev.bat 30 | build.bat -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {}, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Groove Music Online 2 | 3 | Typescript+vue implementation of windows10 Groove music online version 4 | Preview:https://vue-groove.vercel.app 5 | 6 | ## Project setup 7 | ``` 8 | yarn install 9 | ``` 10 | 11 | ### Compiles and hot-reloads for development 12 | ``` 13 | yarn run serve 14 | ``` 15 | 16 | ### Compiles and minifies for production 17 | ``` 18 | yarn run build 19 | ``` 20 | 21 | ### Lints and fixes files 22 | ``` 23 | yarn run lint 24 | ``` 25 | 26 | ### Run your unit tests 27 | ``` 28 | yarn run test:unit 29 | ``` 30 | -------------------------------------------------------------------------------- /ads.txt: -------------------------------------------------------------------------------- 1 | google.com, pub-5807827725641721, DIRECT, f08c47fec0942fa0 -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ["transform-vue-jsx"], 3 | presets: [ 4 | ["@vue/app", { 5 | useBuiltIns: "entry", 6 | }], 7 | ], 8 | }; 9 | -------------------------------------------------------------------------------- /deploy.bat: -------------------------------------------------------------------------------- 1 | git add public/data/result.json 2 | git commit -m "更新作品" 3 | git push origin master -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: [ 3 | 'js', 4 | 'jsx', 5 | 'json', 6 | 'vue', 7 | 'ts', 8 | 'tsx' 9 | ], 10 | transform: { 11 | '^.+\\.vue$': 'vue-jest', 12 | '.+\\.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub', 13 | '^.+\\.tsx?$': 'ts-jest' 14 | }, 15 | moduleNameMapper: { 16 | '^@/(.*)$': '/src/$1' 17 | }, 18 | snapshotSerializers: [ 19 | 'jest-serializer-vue' 20 | ], 21 | testMatch: [ 22 | '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)' 23 | ], 24 | testURL: 'http://localhost/' 25 | } -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { "name": "test", "short_name": "test", "start_url": "/index.html", "display": "standalone", "theme_color": "#3eaf7c", "background_color": "#fff", "icons": [ { "src": "/res/vcplayer/groove.png", "sizes": "120x120", "type": "image/png" } ] } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vc_player", 3 | "version": "0.2.0", 4 | "private": false, 5 | "scripts": { 6 | "serve": "vue-cli-service serve --host 0.0.0.0", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint", 9 | "test:unit": "vue-cli-service test:unit", 10 | "ui": "vue ui" 11 | }, 12 | "dependencies": { 13 | "@babel/polyfill": "^7.4.3", 14 | "@shopify/draggable": "^1.0.0-beta.7", 15 | "@types/lodash": "^4.14.115", 16 | "@types/qs": "^6.5.1", 17 | "animejs": "^2.2.0", 18 | "async-validator": "^1.10.1", 19 | "axios": "^0.18.0", 20 | "iview": "^3.0.0", 21 | "qs": "^6.5.2", 22 | "vue": "^2.6.10", 23 | "vue-class-component": "^6.0.0", 24 | "vue-property-decorator": "^7.0.0", 25 | "vue-router": "^3.0.1", 26 | "vuex": "^3.0.1", 27 | "vuex-class": "^0.3.1" 28 | }, 29 | "devDependencies": { 30 | "@types/jest": "^23.1.4", 31 | "@vue/cli-plugin-babel": "^3.7.0", 32 | "@vue/cli-plugin-typescript": "^3.7.0", 33 | "@vue/cli-plugin-unit-jest": "^3.7.0", 34 | "@vue/cli-service": "^3.7.0", 35 | "@vue/test-utils": "^1.0.0-beta.20", 36 | "babel-core": "7.0.0-bridge.0", 37 | "babel-plugin-import": "^1.8.0", 38 | "babel-plugin-syntax-jsx": "^6.18.0", 39 | "babel-plugin-transform-vue-jsx": "^3.7.0", 40 | "babel-preset-env": "^1.7.0", 41 | "sass": "^1.42.1", 42 | "sass-loader": "^7.1.0", 43 | "ts-jest": "^23.0.0", 44 | "typescript": "^3.4.5", 45 | "vue-template-compiler": "^2.6.10" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axel10/vue-groove/a0eece972cfa28f6952dead0f919e0dfbca8d36e/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 23 | 26 | 27 | 57 | 58 | 59 | 60 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 168 | V player - V collection试听 169 | 170 | 171 | 175 |
176 | 177 | 178 | 179 | 180 | 199 | 200 | 206 | 207 | 208 | 229 | 230 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { "name": "V player - V collection trial", "short_name": "V player", "start_url": "/index.html", "display": "standalone", "theme_color": "#3eaf7c", "background_color": "#fff", "icons": [ { "src": "/res/vcplayer/groove.png", "sizes": "120x120", "type": "image/png" } ] } -------------------------------------------------------------------------------- /public/res/vcplayer/arrow.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/res/vcplayer/cd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axel10/vue-groove/a0eece972cfa28f6952dead0f919e0dfbca8d36e/public/res/vcplayer/cd.png -------------------------------------------------------------------------------- /public/res/vcplayer/groove.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axel10/vue-groove/a0eece972cfa28f6952dead0f919e0dfbca8d36e/public/res/vcplayer/groove.png -------------------------------------------------------------------------------- /public/res/vcplayer/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axel10/vue-groove/a0eece972cfa28f6952dead0f919e0dfbca8d36e/public/res/vcplayer/icon.png -------------------------------------------------------------------------------- /public/res/vcplayer/index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axel10/vue-groove/a0eece972cfa28f6952dead0f919e0dfbca8d36e/public/res/vcplayer/index.png -------------------------------------------------------------------------------- /public/res/vcplayer/loop.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/res/vcplayer/loopSingle.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/res/vcplayer/miku.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axel10/vue-groove/a0eece972cfa28f6952dead0f919e0dfbca8d36e/public/res/vcplayer/miku.gif -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 146 | 147 | 285 | 286 | 335 | 336 | 351 | -------------------------------------------------------------------------------- /src/api/mainApi.ts: -------------------------------------------------------------------------------- 1 | import request from '../utils/request' 2 | import {randomNum} from '@/utils/utils' 3 | import {apiUrl} from '@/utils/config' 4 | 5 | 6 | interface LikeRecord { 7 | liked: boolean 8 | disliked: boolean 9 | likeCount: number 10 | } 11 | 12 | interface Response { 13 | success: boolean 14 | data: T 15 | } 16 | 17 | export default { 18 | getFiles() { 19 | return request.get('/data/result.json?' + randomNum(10)) 20 | }, 21 | getMusic(path: string) { 22 | return request.get(path) 23 | }, 24 | register(data) { 25 | return request.post(apiUrl + '/user/register', data) 26 | }, 27 | 28 | login(data) { 29 | return request.post(apiUrl + '/user/login', data) 30 | }, 31 | like(data: { 32 | isCancel: boolean, 33 | token: string, 34 | action: string, // like或者dislike 35 | type: number, // 1表示专辑,2表示曲目 36 | album: string, 37 | }) { 38 | return request.post(apiUrl + '/like/like', data) 39 | }, 40 | isLogin() { 41 | return request.get(apiUrl + '/user/isLogin') 42 | }, 43 | /* async getLikeRecord(artist: string, title: string, album: string): Promise> { 44 | // return request.get(apiUrl + '/like/get', {token: `${artist}/${title}`}) 45 | if (!album) { 46 | throw new Error('album未传') 47 | } 48 | return await request.get(apiUrl + '/like/getLikeRecord', {token: `${artist}/${title}`, album}) 49 | }, 50 | addPlayCount(data: { artist: string, title: string, album: string }) { 51 | return request.post(apiUrl + '/count/AddPlayCount', data) 52 | }, */ 53 | /* reportError(message: string) { 54 | return axios.post(apiUrl + '/home/reportError', {message}) 55 | },*/ 56 | } 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/assets/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axel10/vue-groove/a0eece972cfa28f6952dead0f919e0dfbca8d36e/src/assets/bg.jpg -------------------------------------------------------------------------------- /src/assets/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axel10/vue-groove/a0eece972cfa28f6952dead0f919e0dfbca8d36e/src/assets/bg.png -------------------------------------------------------------------------------- /src/assets/like.svg: -------------------------------------------------------------------------------- 1 | 3 | 5 | 6 | 7 | 8 | 10 | 11 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axel10/vue-groove/a0eece972cfa28f6952dead0f919e0dfbca8d36e/src/assets/logo.png -------------------------------------------------------------------------------- /src/assets/unlike.svg: -------------------------------------------------------------------------------- 1 | 3 | 5 | 7 | 8 | -------------------------------------------------------------------------------- /src/base.scss: -------------------------------------------------------------------------------- 1 | .like-group{ 2 | display: flex; 3 | align-items: center; 4 | color: #ffffff; 5 | &>div{ 6 | margin: 0 .2rem; 7 | } 8 | } 9 | 10 | .round-btn { 11 | box-sizing: border-box; 12 | border-radius: 50%; 13 | display: inline-flex; 14 | justify-content: center; 15 | align-items: center; 16 | text-align: center; 17 | width: 41px; 18 | height: 41px; 19 | font-size: 26px; 20 | color: #fff; 21 | border: 2px solid rgba(255, 255, 255, 0); 22 | transition:border .3s; 23 | position: relative; 24 | &.no-border{ 25 | border: none!important; 26 | &:hover{ 27 | transform: scale(1.2); 28 | } 29 | } 30 | &>.bridge{ 31 | height: 1.2rem; 32 | display: flex; 33 | justify-content: center; 34 | align-items: center; 35 | color: #666; 36 | background-color: #ffffff; 37 | border-radius: 2rem; 38 | font-size: .8rem; 39 | position: absolute; 40 | left: 1.2rem; 41 | top: -.5rem; 42 | /*box-sizing: border-box;*/ 43 | padding: .3rem .4rem; 44 | } 45 | 46 | &:hover { 47 | border: 2px solid rgba(255, 255, 255, .5); 48 | } 49 | 50 | &.loop, &.svg { 51 | padding: 6px 7px 8px; 52 | /*padding: 7px;*/ 53 | } 54 | 55 | &.mobile-more { 56 | display: none; 57 | } 58 | 59 | 60 | &.active{ 61 | background-color: rgba(0,0,0,.3); 62 | } 63 | img,svg{ 64 | width: 100%; 65 | height: 100%; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/components/Audio.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 33 | 34 | 39 | -------------------------------------------------------------------------------- /src/components/Common/Confirm.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 43 | 44 | -------------------------------------------------------------------------------- /src/components/Common/DropdownList.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 174 | 175 | 233 | -------------------------------------------------------------------------------- /src/components/CreatePlayListModal.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 77 | 78 | 169 | -------------------------------------------------------------------------------- /src/components/FileItem.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | 225 | 226 | 471 | -------------------------------------------------------------------------------- /src/components/LikeBtnGroup.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 92 | 93 | 145 | -------------------------------------------------------------------------------- /src/components/LoginModal.vue: -------------------------------------------------------------------------------- 1 | 60 | 61 | 201 | 202 | 270 | -------------------------------------------------------------------------------- /src/components/Modal.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 31 | 32 | 70 | -------------------------------------------------------------------------------- /src/components/Operation/Loop.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 23 | 24 | 32 | -------------------------------------------------------------------------------- /src/components/Operation/Random.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | 19 | -------------------------------------------------------------------------------- /src/components/Operation/SelectBottomTools.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 16 | 17 | 21 | 22 | -------------------------------------------------------------------------------- /src/components/Operation/TimeSlider.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 31 | 32 | -------------------------------------------------------------------------------- /src/components/Operation/VolumeIcon.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 28 | 29 | -------------------------------------------------------------------------------- /src/components/Operation/VolumeSlider.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 36 | 37 | -------------------------------------------------------------------------------- /src/components/PlayList/PlayListItem.vue: -------------------------------------------------------------------------------- 1 | 2 | 30 | 31 | 105 | 106 | 248 | -------------------------------------------------------------------------------- /src/components/PlayListContentItem.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 141 | 142 | 178 | -------------------------------------------------------------------------------- /src/components/PlayListItemBase.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 17 | 18 | 125 | -------------------------------------------------------------------------------- /src/components/PlayingListContentItem.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 102 | 103 | 115 | -------------------------------------------------------------------------------- /src/components/VPButton.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 22 | 23 | 40 | -------------------------------------------------------------------------------- /src/declaration.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'iview' { 2 | const iview: any 3 | export default iview 4 | } 5 | 6 | declare module '@shopify/draggable' 7 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import router from '@/router' 4 | import store from './store/index' 5 | import iview from 'iview' 6 | import axios from 'axios' 7 | import VPButton from '@/components/VPButton.vue' 8 | import LikeBtnGroup from '@/components/LikeBtnGroup.vue' 9 | 10 | import "@babel/polyfill"; 11 | 12 | 13 | axios.defaults.withCredentials = true 14 | 15 | Vue.use(iview) 16 | Vue.config.productionTip = false 17 | 18 | 19 | Vue.component('VPButton', VPButton) 20 | Vue.component('LikeBtnGroup', LikeBtnGroup) 21 | 22 | 23 | new Vue({ 24 | router, 25 | store, 26 | render: (h) => h(App), 27 | }).$mount('#app') 28 | -------------------------------------------------------------------------------- /src/mixins/selectContainer.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import {Watch} from 'vue-property-decorator'; 3 | 4 | declare module 'vue/types/vue' { 5 | interface Vue { 6 | // test():void 7 | selectedItems:Array 8 | isSelectMode:boolean 9 | } 10 | } 11 | 12 | 13 | export default class SelectContainer extends Vue { 14 | 15 | @Watch("selectedItems") 16 | onSelectedFilesChanged(val:any) { 17 | const operations:Array = Array.prototype.slice.call(document.querySelectorAll('.operation')) 18 | 19 | if (val.length > 0) { 20 | this.$store.commit("home/setIsHideBottom", true); 21 | operations.forEach(o=>o.style.display='none') 22 | 23 | } else { 24 | this.$store.commit("home/setIsHideBottom", false); 25 | operations.forEach(o=>o.style.display='block') 26 | } 27 | } 28 | 29 | cancelSelect(){ 30 | this.selectedItems = [] 31 | } 32 | 33 | selectedItems:Array = []; 34 | get isSelectMode() { 35 | return this.selectedItems.length > 0; 36 | } 37 | } -------------------------------------------------------------------------------- /src/mixins/selectItem.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import {Prop} from 'vue-property-decorator' 3 | import {BaseItem} from '@/types/BaseItem' 4 | 5 | 6 | 7 | 8 | export default class SelectItem extends Vue { 9 | @Prop(Array) public selectedItems!: BaseItem[] 10 | @Prop(Object) public item!: BaseItem 11 | 12 | 13 | get selected() { 14 | return this.selectedItems.findIndex((o: any) => o.id === this.item.id) !== -1 15 | } 16 | 17 | public toggleSelect(e: MouseEvent) { 18 | e.stopPropagation() 19 | if (this.selected) { 20 | this.$emit('select', this.selectedItems.filter((o: any) => o.id !== this.item.id)) 21 | } else { 22 | this.$emit('select', this.selectedItems.concat([this.item])) 23 | } 24 | } 25 | 26 | get isSelectMode() { 27 | return this.selectedItems.length > 0 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/mixins/test.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Component from 'vue-class-component'; 3 | 4 | declare module 'vue/types/vue' { 5 | 6 | interface Vue { 7 | test():void, 8 | arr:any 9 | } 10 | } 11 | 12 | @Component 13 | export default class testMixin extends Vue { 14 | msg='aaa' 15 | arr=[] 16 | } -------------------------------------------------------------------------------- /src/mobile.scss: -------------------------------------------------------------------------------- 1 | @media screen and (max-width: 450px) { 2 | .loading-mask{ 3 | img{ 4 | width: 5rem!important; 5 | } 6 | } 7 | .RecentPlay > .title > .main-title, 8 | .Files > .title > .main-title, 9 | .AllPlayList > .title > .main-title { 10 | display: none !important; 11 | } 12 | 13 | .Files,.RecentPlay { 14 | padding: 40px .8rem 0; 15 | 16 | & > .content > ul { 17 | justify-content: space-between; 18 | &> li { 19 | margin-right: 0 !important; 20 | width: 48% !important; 21 | } 22 | } 23 | 24 | .FileItem { 25 | .top { 26 | /* $coverSize: 9rem; 27 | width: $coverSize !important; 28 | height: $coverSize !important;*/ 29 | } 30 | &.dir { 31 | margin: 0 !important; 32 | & > .color-wrap { 33 | height: 138px !important; 34 | } 35 | } 36 | 37 | &.file, &.dir { 38 | width: auto !important; 39 | } 40 | } 41 | } 42 | 43 | .PlayLists > .content > ul { 44 | justify-content: space-between; 45 | 46 | & > li { 47 | margin-right: 0 !important; 48 | width: 45% !important; 49 | 50 | .PlayListItem { 51 | margin: 0 !important; 52 | width: auto !important; 53 | 54 | & > .color-wrap { 55 | height: 138px !important; 56 | } 57 | } 58 | } 59 | } 60 | 61 | .home > .top > .right { 62 | padding-top: 49px; 63 | box-sizing: border-box; 64 | } 65 | 66 | .like-group{ 67 | right: 1.5rem !important; 68 | } 69 | 70 | .side { 71 | position: fixed; 72 | top: 0; 73 | bottom: 0; 74 | margin: auto; 75 | z-index: 2001; 76 | transform: translateX(-100%); 77 | transition: transform .3s; 78 | background-color: #eee !important; 79 | 80 | &.show { 81 | transform: translateX(0); 82 | } 83 | } 84 | 85 | .home > .top > .mobile-top { 86 | display: flex; 87 | position: fixed; 88 | top: 0; 89 | left: 0; 90 | width: 100%; 91 | background-color: #eee; 92 | height: 50px; 93 | align-items: center; 94 | z-index: 1000; 95 | 96 | h2 { 97 | font-size: 1rem; 98 | 99 | } 100 | 101 | .fold { 102 | height: 50px; 103 | width: 50px; 104 | font-size: 26px; 105 | display: flex; 106 | justify-content: center; 107 | align-items: center; 108 | } 109 | 110 | &.blue { 111 | background-color: rgb(45, 122, 220); 112 | color: #ffffff; 113 | border-bottom: none; 114 | } 115 | } 116 | .Files { 117 | padding: 20px 1.2rem 0!important; 118 | &>.content > ul { 119 | justify-content: space-between; 120 | 121 | & > li { 122 | margin-right: 0; 123 | } 124 | } 125 | } 126 | .HomeBottom { 127 | .file-info { 128 | display: none !important; 129 | } 130 | 131 | .center { 132 | width: 90% !important; 133 | } 134 | 135 | .right { 136 | display: none !important; 137 | } 138 | 139 | .round-btn { 140 | margin: 0 3px !important; 141 | } 142 | 143 | .mobile-more { 144 | display: flex !important; 145 | } 146 | 147 | .center { 148 | .buttons { 149 | .loop { 150 | display: none; 151 | } 152 | } 153 | } 154 | } 155 | 156 | .Playing { 157 | .file-info { 158 | height: 60px !important; 159 | margin-bottom: 18px; 160 | 161 | .cover { 162 | height: 70px !important; 163 | width: 70px !important; 164 | //position: relative; 165 | //bottom: 10px; 166 | } 167 | 168 | .info { 169 | justify-content: space-between !important; 170 | 171 | h5 { 172 | font-size: 20px !important; 173 | margin-bottom: 10px; 174 | } 175 | } 176 | } 177 | 178 | .operations { 179 | .progress { 180 | margin-top: 0 !important; 181 | } 182 | 183 | .buttons { 184 | .right { 185 | display: none !important; 186 | } 187 | 188 | .left { 189 | .Loop, .Random { 190 | //display: none!important; 191 | } 192 | 193 | .volume { 194 | display: none !important; 195 | } 196 | } 197 | } 198 | } 199 | 200 | .music-list { 201 | & > ul > li > .PlayListContentItem { 202 | padding-left: 0; 203 | border-bottom: 0 !important; 204 | 205 | .artist, .operation { 206 | display: none; 207 | } 208 | 209 | .time { 210 | text-align: right; 211 | position: absolute; 212 | right: 0; 213 | } 214 | } 215 | } 216 | 217 | .no-item { 218 | padding-right: 20px; 219 | } 220 | } 221 | .PlayListItemBase { 222 | margin: 0 !important; 223 | padding: 0 20px !important; 224 | 225 | div { 226 | flex-direction: column !important; 227 | justify-content: center !important; 228 | align-items: flex-start !important; 229 | font-size: 12px !important; 230 | 231 | .title { 232 | padding-left: 0 !important; 233 | width: 70% !important; 234 | flex: 0 1 auto !important; 235 | 236 | } 237 | 238 | .album { 239 | width: 70% !important; 240 | flex: 0 1 auto !important; 241 | } 242 | } 243 | } 244 | .PlayList { 245 | .bottom { 246 | .no-item { 247 | margin-top: 320px !important; 248 | margin-left: 25px !important; 249 | } 250 | 251 | ul { 252 | padding: 1rem 0 !important; 253 | top: 230px !important; 254 | 255 | & > .no-item { 256 | margin-top: 310px; 257 | margin-left: 25px; 258 | } 259 | 260 | & > li { 261 | padding-left: 1rem; 262 | box-sizing: border-box; 263 | /* &:hover{ 264 | background-color: #3591e2!important; 265 | &>.PlayListContentItem{ 266 | color: #ffffff; 267 | } 268 | } 269 | }*/ 270 | &.deep { 271 | background-color: #fff !important; 272 | } 273 | 274 | & > .PlayListContentItem { 275 | padding-left: 0; 276 | background-color: transparent !important; 277 | 278 | .artist, .operation, .album { 279 | display: none !important; 280 | } 281 | } 282 | } 283 | } 284 | } 285 | 286 | & > .top { 287 | flex-direction: column; 288 | padding: 20px 0 20px 0 !important; 289 | position: absolute !important; 290 | top: -1px !important; 291 | height: auto !important; 292 | 293 | & > .left { 294 | height: auto !important; 295 | 296 | & > .cover { 297 | width: 140px !important; 298 | 299 | & > .small-block, .block { 300 | height: 4px !important; 301 | } 302 | 303 | & > .cd { 304 | height: 140px !important; 305 | width: auto !important; 306 | 307 | } 308 | } 309 | } 310 | 311 | & > .right { 312 | align-items: center !important; 313 | padding: 5px 0 0 !important; 314 | flex-direction: column !important; 315 | justify-content: flex-start !important; 316 | 317 | .placeholder { 318 | display: none; 319 | } 320 | 321 | .top-area { 322 | text-align: center; 323 | width: 100%; 324 | 325 | & > .list-info { 326 | font-size: .8rem; 327 | } 328 | 329 | .title { 330 | width: 80%; 331 | text-align: center; 332 | margin: 0 auto !important; 333 | overflow: hidden; 334 | white-space: nowrap; 335 | text-overflow: ellipsis; 336 | } 337 | } 338 | 339 | .buttons { 340 | margin-top: 10px !important; 341 | display: flex; 342 | justify-content: space-around; 343 | width: 90%; 344 | 345 | & > div { 346 | font-size: .8rem; 347 | margin: 0 !important; 348 | } 349 | } 350 | } 351 | } 352 | 353 | &::-webkit-scrollbar { 354 | width: 0 !important; 355 | } 356 | 357 | &::-webkit-scrollbar-thumb { 358 | background-color: #999; 359 | border-radius: 0 !important; 360 | } 361 | } 362 | 363 | .PlayingToolBar { 364 | .operations { 365 | .progress { 366 | padding: 0 !important; 367 | } 368 | } 369 | } 370 | 371 | .g-modal{ 372 | &>.main{ 373 | width: 80% !important; 374 | } 375 | } 376 | 377 | } 378 | -------------------------------------------------------------------------------- /src/router.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Router from 'vue-router'; 3 | import Home from './views/Home.vue'; 4 | 5 | Vue.use(Router); 6 | 7 | export default new Router({ 8 | mode:'history', 9 | base:'/', 10 | routes: [ 11 | { 12 | path: '/', 13 | // name: 'home', 14 | component: Home, 15 | children:[ 16 | { 17 | path: '/playList/:id', 18 | name: 'playList', 19 | component: () => import('./views/PlayListView.vue'), 20 | }, 21 | { 22 | path: '/allPlayList', 23 | name: 'allPlayList', 24 | component: () => import('./views/AllPlayList.vue'), 25 | }, 26 | { 27 | path: '', 28 | name: 'file', 29 | component: () => import('./views/Files.vue'), 30 | }, 31 | { 32 | path: '/path/:path', 33 | name: 'file', 34 | component: () => import('./views/Files.vue'), 35 | }, 36 | { 37 | path: '/recentPlay', 38 | name: 'recentPlay', 39 | component: () => import('./views/RecentPlay.vue'), 40 | }, 41 | ] 42 | }, 43 | { 44 | path: '/playing/:light', 45 | name: 'playing', 46 | component: () => import('./views/Playing.vue'), 47 | }, 48 | { 49 | path:'*', 50 | redirect:'/' 51 | } 52 | 53 | /* { 54 | path: '/test', 55 | name: 'test', 56 | // route level code-splitting 57 | // this generates a separate chunk (about.[hash].js) for this route 58 | // which is lazy-loaded when the route is visited. 59 | component: () => import(/!* webpackChunkName: "about" *!/ './views/Test.vue'), 60 | },*/ 61 | ], 62 | }); 63 | -------------------------------------------------------------------------------- /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/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import Vue from 'vue'; 3 | export default Vue; 4 | } 5 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import Vue from 'vue' 3 | import Vuex, { Commit, Dispatch } from 'vuex' 4 | import test from './modules/test' 5 | import playing from './modules/playing' 6 | import home from '@/store/modules/home' 7 | import audio from '@/store/modules/audio' 8 | import file from '@/store/modules/file' 9 | import playList from '@/store/modules/playList' 10 | import {IState as IAudioState} from './modules/audio' 11 | import {IState as IFileState} from './modules/file' 12 | import {IState as IHomeState} from './modules/home' 13 | import {IState as IPlayingState} from './modules/playing' 14 | import {IState as IPlayListState} from './modules/playList' 15 | 16 | 17 | 18 | Vue.use(Vuex) 19 | 20 | export default new Vuex.Store({ 21 | modules: { 22 | test, playing, home, audio, file, playList, 23 | }, 24 | strict: true, 25 | }) 26 | 27 | 28 | export interface ActionContextBasic { 29 | commit: Commit, 30 | dispatch: Dispatch, 31 | rootState: RootState, 32 | state: any 33 | } 34 | 35 | export interface RootState { 36 | audio: IAudioState, 37 | file: IFileState, 38 | home: IHomeState, 39 | playing: IPlayingState, 40 | playList: IPlayListState 41 | } 42 | -------------------------------------------------------------------------------- /src/store/modules/audio.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash' 2 | import {ActionContextBasic} from '@/store' 3 | import {File} from './file' 4 | import {beginAddTime, beginPlay, convertTimeStrToSecond, pausePlay, setPlayTimer} from '@/utils/utils' 5 | import {LoopMode} from '@/utils/enum/LoopMode' 6 | import Vue from 'vue' 7 | import config from '@/utils/config' 8 | import {LocalStorageKeys} from '@/utils/enum/LocalStorageKeys' 9 | import mainApi from '@/api/mainApi' 10 | 11 | 12 | const initState: IState = { 13 | serverPath: '', 14 | playing: false, 15 | currentTime: 0, 16 | duration: 0, 17 | durationStr: '', 18 | volume: 30, 19 | timer: 0, 20 | fps: 5, 21 | loopMode: LoopMode.close, 22 | isRandom: false, 23 | isMute: false, 24 | isLoading: false, 25 | } 26 | 27 | const audioState: IState = _.cloneDeep(initState) 28 | 29 | export interface IState { 30 | serverPath: string, 31 | playing: boolean, 32 | currentTime: number, 33 | duration: number, 34 | volume: number, 35 | timer: number, 36 | fps: 5, 37 | loopMode: LoopMode, 38 | isRandom: boolean, 39 | isMute: boolean, 40 | isLoading: boolean, 41 | durationStr: string 42 | } 43 | 44 | 45 | export function getTimeStr(time: number) { 46 | const sec = Math.floor(time % 60) 47 | const second = sec < 10 ? '0' + sec : sec 48 | const minute = Math.floor(time / 60) 49 | return `${minute}:${second}` 50 | } 51 | 52 | const getters = { 53 | currentTimeStr(state: IState) { 54 | return getTimeStr(state.currentTime > state.duration ? state.duration : state.currentTime) 55 | }, 56 | durationTimeStr(state: IState) { 57 | return getTimeStr(state.duration) 58 | }, 59 | timePercent(state: IState) { 60 | return state.currentTime / state.duration * 100 61 | }, 62 | } 63 | 64 | const actions = { 65 | init({dispatch}: ActionContextBasic) { 66 | const player = document.getElementById('player') as HTMLAudioElement 67 | player.loop = false 68 | player.addEventListener('ended', () => { 69 | dispatch('handleEnd') 70 | }) 71 | player.volume = audioState.volume / 100 72 | if (navigator.mediaSession) { 73 | navigator.mediaSession.setActionHandler('play', () => { 74 | beginPlay() 75 | }) 76 | navigator.mediaSession.setActionHandler('pause', () => { 77 | pausePlay() 78 | }) 79 | } 80 | }, 81 | 82 | handleEnd({commit, dispatch, rootState, state}: ActionContextBasic) { 83 | if (state.isRandom) { 84 | dispatch('randomPlay') 85 | return 86 | } 87 | 88 | switch (state.loopMode) { 89 | case LoopMode.close: 90 | const playingFile = rootState.home.playingFile 91 | const playingList = rootState.playList.playingList 92 | const index = playingList.findIndex((o) => o.token === playingFile.token) 93 | if (index === playingList.length - 1) { 94 | commit('setPlaying', false) 95 | clearInterval(state.timer) 96 | } else { 97 | dispatch('play', playingList[index + 1]) 98 | } 99 | break 100 | case LoopMode.loopAll: 101 | dispatch('toNext') 102 | break 103 | case LoopMode.loopSingle: 104 | dispatch('play', rootState.home.playingFile) 105 | break 106 | } 107 | }, 108 | 109 | 110 | play({state, commit, rootState, dispatch}: ActionContextBasic, file: File) { 111 | if (state.isLoading) { 112 | return 113 | } 114 | if (!file.p || !file.title) { 115 | return 116 | } 117 | commit('setLoading', true) 118 | // dispatch('home/getLikeRecord', {file}, {root: true}).catch((e) => { 119 | // }) 120 | const musicPath = file.musicUrl.endsWith(config.musicExt) 121 | ? file.musicUrl : file.musicUrl.split('.')[0] + `.${config.musicExt}` 122 | commit('initPlay', musicPath) 123 | setTimeout(() => { 124 | const player = document.getElementById('player') as HTMLAudioElement 125 | /* player.onerror=()=>{ 126 | console.log('error') 127 | debugger 128 | }*/ 129 | 130 | player.load() 131 | clearInterval(state.timer) 132 | const msg = (new Vue()).$Message.loading({ 133 | content: '正在加载...', 134 | duration: 0, 135 | }) 136 | 137 | 138 | player.onloadeddata = () => { 139 | commit('setMusicInfo', file.time) 140 | setPlayTimer() 141 | player.play() 142 | msg() 143 | commit('setLoading', false) 144 | 145 | // 设置当前播放文件 146 | dispatch('home/setPlayingFile', file, {root: true}) 147 | if (!rootState.home.isInRecentPlay) { 148 | dispatch('playList/addRecentPlay', file, {root: true}) 149 | } 150 | // mainApi.addPlayCount({artist: file.p, title: file.title, album: file.album}) 151 | } 152 | 153 | 154 | }) 155 | }, 156 | 157 | abort({commit, dispatch}: ActionContextBasic) { 158 | dispatch('home/setPlayingFile', new File(), {root: true}) 159 | const player = document.getElementById('player') as HTMLAudioElement 160 | player.pause() 161 | commit('clearPlaying') 162 | }, 163 | 164 | togglePlay({commit, rootState, state, dispatch}: ActionContextBasic) { 165 | if (state.isLoading) { 166 | return 167 | } 168 | if (rootState.playList.playingList.length === 0 /*|| rootState.home.playingFile.id === 0*/) { 169 | return 170 | } 171 | const player = document.getElementById('player') as HTMLAudioElement 172 | 173 | if (state.serverPath !== rootState.home.playingFile.musicUrl) { 174 | dispatch('play', rootState.home.playingFile) 175 | return 176 | } 177 | if (player.paused) { 178 | beginPlay() 179 | } else { 180 | pausePlay() 181 | } 182 | }, 183 | 184 | toPrev({dispatch, rootState}: ActionContextBasic) { 185 | const playingFile = rootState.home.playingFile 186 | const playingList = rootState.playList.playingList 187 | 188 | if (playingList.length === 0) { 189 | return 190 | } 191 | 192 | if (audioState.isRandom) { 193 | dispatch('randomPlay') 194 | return 195 | } 196 | let index = playingList.findIndex((o) => o.token === playingFile.token) - 1 197 | if (index < 0) { 198 | index = playingList.length - 1 199 | } 200 | dispatch('play', playingList[index]) 201 | }, 202 | toNext({dispatch, rootState}: ActionContextBasic) { 203 | 204 | 205 | const playingFile = rootState.home.playingFile 206 | const playingList = rootState.playList.playingList 207 | 208 | if (playingList.length === 0) { 209 | return 210 | } 211 | 212 | if (audioState.isRandom) { 213 | dispatch('randomPlay') 214 | return 215 | } 216 | 217 | let index = playingList.findIndex((o) => o.token === playingFile.token) + 1 218 | if (index >= playingList.length) { 219 | index = 0 220 | } 221 | dispatch('play', playingList[index]) 222 | }, 223 | randomPlay({dispatch, rootState}: ActionContextBasic) { 224 | const playingList = rootState.playList.playingList 225 | const index = Math.floor(Math.random() * playingList.length) 226 | dispatch('play', playingList[index]) 227 | }, 228 | stop({commit}: ActionContextBasic) { 229 | const player = document.getElementById('player') as HTMLAudioElement 230 | player.pause() 231 | commit('setPlaying', false) 232 | clearInterval(audioState.timer) 233 | }, 234 | } 235 | 236 | const mutations = { 237 | initPlay(state: IState, path: string) { 238 | state.serverPath = path 239 | }, 240 | initVolume(state: IState, vol: number) { 241 | state.volume = vol 242 | }, 243 | clearPlaying(state: IState) { 244 | clearInterval(state.timer) 245 | state.duration = 0 246 | state.currentTime = 0 247 | state.serverPath = '' 248 | }, 249 | setMusicInfo(state: IState, time: string) { 250 | // const player = document.getElementById('player'); 251 | // state.duration = player.duration; 252 | state.duration = convertTimeStrToSecond(time) 253 | state.currentTime = 0 254 | state.playing = true 255 | }, 256 | setTimer(state: IState, timer: number) { 257 | state.timer = timer 258 | }, 259 | /** 260 | * 当前时间自增 261 | */ 262 | syncCurrentTime(state: IState, time: number) { 263 | // state.currentTime += 1 / state.fps 264 | // state.currentTime += time 265 | const player = document.getElementById('player') as HTMLAudioElement 266 | state.currentTime = Math.floor(player.currentTime) 267 | }, 268 | 269 | setPlaying(state: IState, b: boolean) { 270 | state.playing = b 271 | }, 272 | handleSelectTime(state: IState, val: number) { 273 | state.currentTime = state.duration * val / 100 274 | const player = document.getElementById('player') as HTMLAudioElement 275 | player.currentTime = state.currentTime 276 | 277 | if (state.playing) { 278 | beginAddTime() 279 | } 280 | }, 281 | handleInputTime(state: IState) { 282 | clearInterval(state.timer) 283 | }, 284 | 285 | handleChangeVolume(state: IState, val: number) { 286 | state.isMute = false 287 | state.volume = val 288 | const player = document.getElementById('player') as HTMLAudioElement 289 | player.volume = state.volume / 100 290 | localStorage.setItem(LocalStorageKeys.volume, val.toString()) 291 | }, 292 | 293 | toggleRandom(state: IState) { 294 | state.isRandom = !state.isRandom 295 | }, 296 | 297 | switchLoopMode(state: IState) { 298 | switch (state.loopMode) { 299 | case LoopMode.close: 300 | state.loopMode = LoopMode.loopAll 301 | break 302 | case LoopMode.loopAll: 303 | state.loopMode = LoopMode.loopSingle 304 | break 305 | case LoopMode.loopSingle: 306 | state.loopMode = LoopMode.close 307 | break 308 | } 309 | }, 310 | 311 | toggleMute(state: IState) { 312 | const player = document.getElementById('player') as HTMLAudioElement 313 | state.isMute = !state.isMute 314 | player.muted = state.isMute 315 | }, 316 | addVolume(state: IState, num: number) { 317 | const volume = state.volume 318 | if (volume + num > 100) { 319 | state.volume = 100 320 | return 321 | } 322 | if (volume + num < 0) { 323 | state.volume = 0 324 | return 325 | } 326 | state.volume += num 327 | }, 328 | setLoading(state: IState, isLoading: boolean) { 329 | state.isLoading = isLoading 330 | }, 331 | 332 | onPlayerError(state: IState, {context}: { context: Vue }) { 333 | context.$Message.destroy() 334 | context.$Message.error({ 335 | content: '出错了😭请前往留言板汇报bug', 336 | duration: 3, 337 | }) 338 | state.isLoading = false 339 | }, 340 | } 341 | 342 | export default { 343 | namespaced: true, 344 | state: audioState, 345 | getters, 346 | actions, 347 | mutations, 348 | } 349 | -------------------------------------------------------------------------------- /src/store/modules/file.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash' 2 | import {ActionContextBasic} from '@/store' 3 | import mainApi from '../../api/mainApi' 4 | import {convertFilesToLinearArray, initResourceInfo} from '@/utils/utils' 5 | import {BaseItem} from '@/types/BaseItem' 6 | 7 | 8 | const initState = { 9 | files: [], 10 | allFile: [], 11 | currentFiles: [], 12 | path: [], 13 | // dropDownShow: false, 14 | // dropDownFile: null, 15 | } 16 | 17 | export interface IState { 18 | files: File[], 19 | allFile: File[], 20 | currentFiles: File[], 21 | path: string[], 22 | isDir: boolean, 23 | imgUrl: '', 24 | musicUrl: '' 25 | } 26 | 27 | export class File extends BaseItem { 28 | public id!: string 29 | public title: string 30 | public content: File[] 31 | public p: string 32 | public cdTitle: string 33 | // trck: number; 34 | public sort: number 35 | public imgUrl: string 36 | public musicUrl: string 37 | public time!: string 38 | public token?: string 39 | public album!: string 40 | 41 | constructor() { 42 | super() 43 | this.title = '' 44 | this.content = [] 45 | this.p = '' 46 | this.cdTitle = '' 47 | // this.trck=0 48 | this.sort = 0 49 | this.imgUrl = '' 50 | this.musicUrl = '' 51 | } 52 | } 53 | 54 | const fileState = _.cloneDeep(initState) 55 | 56 | const getters = { 57 | currentFiles(state: IState) { 58 | const path = _.cloneDeep(state.path) 59 | if (path.length === 0) { 60 | return state.files 61 | } else { // 在不是根级目录的情况下 62 | let files = _.cloneDeep(state.files) 63 | let filesTmp: File[] = [] 64 | while (path.length > 0) { 65 | const pathItem = path.shift() 66 | const tmp = files.find((o) => { 67 | return o.title === pathItem 68 | }) 69 | if (tmp) { 70 | filesTmp = tmp.content || [] 71 | files = filesTmp 72 | } 73 | } 74 | // 专辑倒序排序,曲目正序排序 75 | if (filesTmp.length>0 && filesTmp[0].album) { 76 | filesTmp.sort((a, b) => { 77 | return a.sort - b.sort 78 | 79 | }) 80 | }else{ 81 | filesTmp.sort((a, b) => { 82 | return b.sort - a.sort 83 | 84 | }) 85 | } 86 | return filesTmp 87 | } 88 | }, 89 | } 90 | 91 | const actions = { 92 | 93 | async init({commit, dispatch}: ActionContextBasic) { 94 | await dispatch({type: 'getFiles'}) 95 | }, 96 | 97 | async getFiles({commit, dispatch}: ActionContextBasic) { 98 | let i = 0 99 | const delay = 800 100 | for (let j = 0; j < delay / 100; j++) { 101 | setTimeout(() => { 102 | i++ 103 | }, 100) 104 | } 105 | const files = await mainApi.getFiles() 106 | files.sort((e1, e2) => { 107 | if (e1.sort > e2.sort) { 108 | return -1 109 | } 110 | if (e1.sort < e2.sort) { 111 | return 1 112 | } 113 | return 0 114 | }) 115 | commit('setFiles', files) 116 | if (i < delay) { 117 | setTimeout(() => { 118 | commit('home/setData', {key: 'loaded', val: true}, {root: true}) 119 | }, delay - i) 120 | } else { 121 | commit('home/setData', {key: 'loaded', val: true}, {root: true}) 122 | } 123 | }, 124 | 125 | randomPlayAll({dispatch, commit, state}: ActionContextBasic) { 126 | commit('playList/setPlayingList', state.allFile, {root: true}) 127 | const index = Math.floor(state.allFile.length * Math.random()) 128 | dispatch('audio/play', state.allFile[index], {root: true}) 129 | }, 130 | 131 | playDirs({dispatch, commit}: ActionContextBasic, dir: File[]) { 132 | 133 | if (!dir[0].content) { 134 | dispatch('audio/play', dir[0], {root: true}) 135 | dispatch('playList/addToPlayingList', dir, {root: true}) 136 | return 137 | } 138 | 139 | const allFile = dir.reduce((res: File[], item) => { 140 | return res.concat(getAllFileByContent(item.content)) 141 | }, []) 142 | dispatch('audio/play', allFile[0], {root: true}) 143 | commit('playList/setPlayingList', allFile, {root: true}) 144 | }, 145 | } 146 | 147 | function getAllFileByContent(content: File[]) { 148 | const tmp: File[] = [] 149 | 150 | function pushItem(con: File[]) { 151 | /* for (let i = 0; i < content.length; i++) { 152 | if (content[i].content) { 153 | pushItem(content[i].content) 154 | } else { 155 | tmp.push(content[i]) 156 | } 157 | }*/ 158 | for (const item of con) { 159 | if (item.content) { 160 | pushItem(item.content) 161 | } else { 162 | tmp.push(item) 163 | } 164 | } 165 | } 166 | 167 | pushItem(content) 168 | return tmp 169 | } 170 | 171 | 172 | const mutations = { 173 | setFiles(state: IState, files: File[]) { 174 | initResourceInfo(files) 175 | state.allFile = convertFilesToLinearArray(files) 176 | state.files = files 177 | }, 178 | setPath(state: IState, paths: string[]) { 179 | state.path = paths 180 | // sessionStorage.setItem('path', JSON.stringify(state.path)); 181 | }, 182 | toPrev(state: IState) { 183 | if (state.path.length > 0) { 184 | state.path.pop() 185 | } 186 | }, 187 | setCurrentFiles(state: IState, files: File[]) { 188 | state.currentFiles = files 189 | }, 190 | } 191 | 192 | export default { 193 | namespaced: true, 194 | state: fileState, 195 | getters, 196 | actions, 197 | mutations, 198 | } 199 | -------------------------------------------------------------------------------- /src/store/modules/home.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash' 2 | import {File} from '@/store/modules/file' 3 | import {ActionContextBasic} from '@/store' 4 | import {LocalStorageKeys} from '@/utils/enum/LocalStorageKeys' 5 | import mainApi from '@/api/mainApi' 6 | import {assert, getToken} from '@/utils/utils' 7 | import Message from '@/utils/Message' 8 | 9 | const initState: IState = { 10 | playingFile: new File(), 11 | isDark: false, 12 | playingCoverUrl: '', 13 | isHideBottom: false, 14 | isFold: false, 15 | isInRecentPlay: false, 16 | isFullScreen: false, 17 | currentTitle: '', 18 | isMobile: false, 19 | loaded: false, 20 | liked: false, 21 | disliked: false, 22 | isLogin: false, 23 | likeCount: 0, 24 | loading: false, 25 | } 26 | 27 | export interface IState { 28 | playingFile: File 29 | playingCoverUrl: string 30 | isDark: boolean 31 | isHideBottom: boolean 32 | isFold: boolean 33 | isInRecentPlay: boolean 34 | isFullScreen: boolean 35 | currentTitle: string 36 | isMobile: boolean 37 | loaded: boolean 38 | liked: boolean 39 | disliked: boolean 40 | isLogin: boolean 41 | likeCount: number 42 | loading: boolean 43 | } 44 | 45 | export interface ISetPlayingFilePayload { 46 | file: File 47 | imgUrl: string 48 | } 49 | 50 | const homeState = _.cloneDeep(initState) 51 | 52 | const getters = {} 53 | 54 | const actions = { 55 | 56 | async init({commit, dispatch, rootState}: ActionContextBasic) { 57 | await dispatch('file/init', {}, {root: true}) 58 | const recentPlay = JSON.parse(localStorage.getItem(LocalStorageKeys.recentPlay) || '[]') 59 | const playingList = JSON.parse(localStorage.getItem(LocalStorageKeys.playingList) || '[]') 60 | const playLists = JSON.parse(localStorage.getItem(LocalStorageKeys.playLists) || '[]') 61 | const playingFile = JSON.parse(localStorage.getItem(LocalStorageKeys.playingFile) || '{}') as File 62 | const volume = parseInt(localStorage.getItem(LocalStorageKeys.volume) || '', 10) 63 | commit('playList/setRecentPlay', recentPlay, {root: true}) 64 | commit('playList/setPlayingList', playingList, {root: true}) 65 | commit('playList/setPlayLists', playLists, {root: true}) 66 | if (playingFile.title && rootState.file.allFile.some((file) => file.id === playingFile.id)) { 67 | commit('setPlayingFile', playingFile) 68 | // dispatch('getLikeRecord', {file: playingFile}) 69 | } 70 | if (!isNaN(volume)) { 71 | commit('audio/initVolume', volume, {root: true}) 72 | } 73 | 74 | // 验证是否登录 75 | /* mainApi.isLogin().then((o) => { 76 | commit('setData', {key: 'isLogin', val: o.data}) 77 | })*/ 78 | }, 79 | 80 | /* getLikeRecord({commit}: ActionContextBasic, {file}: { file: File }) { 81 | assert(!!file.album, 'album缺失') 82 | mainApi.getLikeRecord(file.p, file.title, file.album).then((o) => { 83 | commit('setData', {key: 'liked', val: o.data.liked}) 84 | commit('setData', {key: 'disliked', val: o.data.disliked}) 85 | commit('setData', {key: 'likeCount', val: o.data.likeCount}) 86 | }) 87 | }, */ 88 | 89 | setPlayingFile({commit, rootState}: ActionContextBasic, file: File) { 90 | commit('setPlayingFile', file) 91 | }, 92 | 93 | async like({commit, state}: ActionContextBasic, {action}) { 94 | const {playingFile, disliked, liked, likeCount, loading} = (state as IState) 95 | if (loading) { 96 | return 97 | } 98 | commit('setData', {key: 'loading', val: true}) 99 | if (action !== 'like' && action !== 'dislike') { 100 | throw new Error('action必须为like或者dislike') 101 | } 102 | 103 | if (!playingFile.title) { 104 | throw new Error('没有正在播放的作品') 105 | } 106 | const token = getToken(playingFile.p, playingFile.title) 107 | if (action === 'like') { 108 | if (liked) { 109 | commit('setData', {key: 'liked', val: false}) 110 | commit('setData', {key: 'likeCount', val: likeCount - 1}) 111 | try { 112 | await mainApi.like({isCancel: true, type: 2, action, token, album: playingFile.album}) 113 | } catch (e) { 114 | commit('setData', {key: 'liked', val: true}) 115 | commit('setData', {key: 'likeCount', val: likeCount}) 116 | } finally { 117 | commit('setData', {key: 'loading', val: false}) 118 | } 119 | } else { 120 | commit('setData', {key: 'liked', val: true}) 121 | commit('setData', {key: 'likeCount', val: likeCount + 1}) 122 | try { 123 | await mainApi.like({isCancel: false, type: 2, action, token, album: playingFile.album}) 124 | } catch (e) { 125 | commit('setData', {key: 'liked', val: false}) 126 | commit('setData', {key: 'likeCount', val: likeCount}) 127 | } finally { 128 | commit('setData', {key: 'loading', val: false}) 129 | } 130 | } 131 | } else { 132 | if (disliked) { 133 | commit('setData', {key: 'disliked', val: false}) 134 | try { 135 | await mainApi.like({isCancel: true, type: 2, action, token, album: playingFile.album}) 136 | } catch (e) { 137 | commit('setData', {key: 'disliked', val: true}) 138 | } finally { 139 | commit('setData', {key: 'loading', val: false}) 140 | } 141 | } else { 142 | commit('setData', {key: 'disliked', val: true}) 143 | try { 144 | await mainApi.like({isCancel: false, type: 2, action, token, album: playingFile.album}) 145 | } catch (e) { 146 | commit('setData', {key: 'disliked', val: false}) 147 | } finally { 148 | commit('setData', {key: 'loading', val: false}) 149 | } 150 | } 151 | } 152 | }, 153 | } 154 | const setData: any = (state: IState, {key, val}) => { 155 | state[key] = val 156 | } 157 | 158 | const mutations = { 159 | /* toggleFold(state: IState) { 160 | state.isFold = !state.isFold 161 | },*/ 162 | setPlayingFile(state: IState, file: File) { 163 | state.playingFile = file 164 | localStorage.setItem('playingFile', JSON.stringify(file)) 165 | }, 166 | setIsDark(state: IState, b: boolean) { 167 | state.isDark = b 168 | }, 169 | setIsHideBottom(state: IState, b: boolean) { 170 | state.isHideBottom = b 171 | }, 172 | isInRecentPlay(state: IState, b: boolean) { 173 | state.isInRecentPlay = b 174 | }, 175 | setIsFullScreen(state: IState, b: boolean) { 176 | state.isFullScreen = b 177 | }, 178 | setCurrentTitle(state: IState, title: string) { 179 | state.currentTitle = title 180 | }, 181 | setIsMobile(state: IState, b: boolean) { 182 | state.isMobile = b 183 | }, 184 | setData, 185 | } 186 | 187 | export default { 188 | namespaced: true, 189 | state: homeState, 190 | getters, 191 | actions, 192 | mutations, 193 | } 194 | -------------------------------------------------------------------------------- /src/store/modules/playList.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash' 2 | import {File} from '@/store/modules/file' 3 | import {ActionContextBasic} from '@/store' 4 | import {getFileId, guid, union, unionFiles} from '@/utils/utils' 5 | 6 | import Notice from '@/utils/Notice' 7 | import {PlayList} from '@/store/types/PlayList' 8 | 9 | export interface IState { 10 | recentPlay: File[] 11 | playingList: File[] 12 | playLists: PlayList[] 13 | } 14 | 15 | export class PlayListContentDataItem { 16 | public title!: string 17 | public p!: string 18 | public album!: string 19 | 20 | constructor(title: string, p: string, album: string) { 21 | this.title = title 22 | this.p = p 23 | this.album = album 24 | } 25 | } 26 | 27 | 28 | 29 | export interface AddPlayListPayload { 30 | playListTitle: string 31 | id: number 32 | } 33 | 34 | export interface CreatePlayListPayload { 35 | name: string 36 | content: PlayListContentDataItem[] 37 | } 38 | 39 | export interface IAddToPlayList { 40 | listId: string 41 | content: PlayListContentDataItem[] 42 | } 43 | 44 | const initState = { 45 | recentPlay: [], 46 | playingList: [], 47 | playLists: [], 48 | } 49 | 50 | const playListState = _.cloneDeep(initState) 51 | 52 | 53 | const getters = {} 54 | 55 | const actions = { 56 | addRecentPlay({commit}: ActionContextBasic, file: File) { 57 | commit('addRecentPlay', file) 58 | }, 59 | createPlayList({commit, state}: ActionContextBasic, {name, /*fileIds*/ content}: CreatePlayListPayload) { 60 | const playLists = state.playLists 61 | if (!name) { 62 | name = '新建播放列表' 63 | } 64 | const repeat = playLists.find((o: PlayList) => o.title === name) 65 | let distName = name 66 | let count = 1 67 | if (repeat) { 68 | while (true) { 69 | distName = repeat.title + `(${count + 1})` 70 | if (!playLists.find((o: PlayList) => o.title === distName)) { 71 | break 72 | } 73 | count += 1 74 | } 75 | } 76 | 77 | if (content.length > 0) { 78 | Notice.open({title: `已向播放列表添加了${content.length}首歌曲`}) 79 | } 80 | 81 | commit('createPlayList', {name: distName, content}) 82 | }, 83 | 84 | addToPlayList({commit}: ActionContextBasic, {listId, content}: IAddToPlayList) { 85 | const playList: PlayList = playListState.playLists.find((o: PlayList) => o.id === listId) || new PlayList() 86 | const playListContent = playList.content 87 | const preCount = playListContent.length 88 | const result = union(playListContent, content) 89 | Notice.open({title: `已向播放列表添加了${result.length - preCount}首歌曲`}) 90 | 91 | commit('setPlayListContent', {listId, content: result}) 92 | }, 93 | 94 | /** 95 | * 添加文件到正在播放列表。 96 | * @param commit 97 | * @param dispatch 98 | * @param rootState 99 | * @param state 100 | * @param files 101 | */ 102 | addToPlayingList({commit, dispatch, rootState, state}: ActionContextBasic, files: File[]) { 103 | if (rootState.playList.playingList.length === 0) { 104 | commit('home/setPlayingFile', files[0], {root: true}) 105 | } 106 | commit('setPlayingList', unionFiles(state.playingList, files)) 107 | }, 108 | renamePlayList({commit, state}: ActionContextBasic, {name, id}: any) { 109 | commit('renamePlayList', {name, id}) 110 | }, 111 | 112 | removePlayList({commit, state}: ActionContextBasic, {context, id}: any) { 113 | 114 | const currentId = context.$route.params.id 115 | if (currentId === id) { 116 | const playLists: PlayList[] = state.playLists 117 | let prePlayListIndex = playLists.findIndex((o) => o.id === id) - 1 118 | if (playLists.length < 0) { 119 | prePlayListIndex = 0 120 | } 121 | if (prePlayListIndex >= 0) { 122 | context.$router.replace('/playList/' + playLists[prePlayListIndex].id) 123 | } else { 124 | context.$router.replace('/allPlayList') 125 | } 126 | } 127 | 128 | commit('removePlayList', id) 129 | }, 130 | 131 | removePlayLists({commit, state}: ActionContextBasic, ids: string[]) { 132 | const playLists = state.playLists.filter((o: PlayList) => ids.indexOf(o.id) === -1) 133 | commit('setPlayLists', playLists) 134 | }, 135 | 136 | removePlayListContents({commit, state}: ActionContextBasic, {listId, ids}: any) { 137 | commit('removePlayListContents', {listId, ids}) 138 | }, 139 | 140 | playPlayList({dispatch, commit, state, rootState}: ActionContextBasic, id: string) { 141 | const currentPlayList = state.playLists.find((o: PlayList) => o.id === id) as PlayList 142 | if (!currentPlayList || Object.keys(currentPlayList).length === 0) { 143 | return 144 | } 145 | const currentPlayListIds = currentPlayList.content 146 | const content = currentPlayListIds.map((o) => { 147 | return rootState.file.allFile.find((file) => file.id === getFileId(o.p, o.title, o.album)) 148 | }).filter((o: File) => o) 149 | 150 | commit('setPlayingList', content) 151 | dispatch('audio/play', content[0], {root: true}) 152 | }, 153 | 154 | removePlayingList({commit, rootState, state, dispatch}: ActionContextBasic, ids: string[]) { 155 | const playingFile = rootState.home.playingFile 156 | const playingList: File[] = state.playingList 157 | if (ids.indexOf(playingFile.id) !== -1) { 158 | ids.sort((e1, e2) => { 159 | if (e1 > e2) { 160 | return 1 161 | } 162 | if (e1 === e2) { 163 | return 0 164 | } 165 | return -1 166 | }) 167 | const newIndex = playingList.findIndex((o) => o.id === ids[0]) 168 | commit('removePlayingList', ids) 169 | if (state.playingList.length === 0) { 170 | return 171 | } 172 | const newFile = state.playingList[newIndex] 173 | commit('home/setPlayingFile', newFile, {root: true}) 174 | dispatch('audio/stop', {}, {root: true}) 175 | } else { 176 | commit('removePlayingList', ids) 177 | } 178 | }, 179 | 180 | 181 | } 182 | 183 | const mutations = { 184 | addRecentPlay(state: IState, file: File) { 185 | const recentPlay = state.recentPlay 186 | 187 | const index = recentPlay.findIndex((o) => o.title === file.title) 188 | 189 | if (index === -1) { 190 | recentPlay.unshift(file) 191 | } else { 192 | recentPlay.splice(0, 0, recentPlay.splice(index, 1)[0]) 193 | } 194 | localStorage.setItem('recentPlay', JSON.stringify(recentPlay)) 195 | }, 196 | 197 | setPlayListContent(state: IState, {listId, content}: { listId: string, content: PlayListContentDataItem[] }) { 198 | const playList = state.playLists.find((o) => o.id === listId) || new PlayList() 199 | playList.content = content 200 | localStorage.setItem('playLists', JSON.stringify(state.playLists)) 201 | }, 202 | 203 | setRecentPlay(state: IState, files: File[]) { 204 | state.recentPlay = files 205 | localStorage.setItem('recentPlay', JSON.stringify(state.recentPlay)) 206 | 207 | }, 208 | 209 | addPlayingList(state: IState, file: File) { 210 | state.playingList.push(file) 211 | localStorage.setItem('playingList', JSON.stringify(state.playingList)) 212 | }, 213 | setPlayingList(state: IState, files: File[]) { 214 | state.playingList = files 215 | localStorage.setItem('playingList', JSON.stringify(state.playingList)) 216 | }, 217 | 218 | createPlayList(state: IState, {name, content}: CreatePlayListPayload) { 219 | state.playLists.push({title: name, content, id: guid()}) 220 | localStorage.setItem('playLists', JSON.stringify(state.playLists)) 221 | }, 222 | 223 | 224 | setPlayLists(state: IState, playLists: PlayList[]) { 225 | state.playLists = playLists 226 | localStorage.setItem('playLists', JSON.stringify(state.playLists)) 227 | 228 | }, 229 | 230 | /** 231 | * 重命名播放列表 232 | * @param state 233 | * @param name 234 | * @param id 235 | */ 236 | renamePlayList(state: IState, {name, id}: any) { 237 | const list = state.playLists.find((o) => o.id === id) || new PlayList() 238 | list.title = name 239 | }, 240 | 241 | removePlayList(state: IState, id: string) { 242 | state.playLists = state.playLists.filter((o) => o.id !== id) 243 | localStorage.setItem('playLists', JSON.stringify(state.playLists)) 244 | }, 245 | 246 | removePlayListContents(state: IState, {listId, ids}: any) { 247 | const playList = state.playLists.find((o) => o.id === listId) || new PlayList() 248 | playList.content = playList.content.filter((o) => ids.indexOf(o) === -1) 249 | localStorage.setItem('playLists', JSON.stringify(state.playLists)) 250 | }, 251 | 252 | sortPlayListContent(state: IState, {listId, oldIndex, newIndex}: any) { 253 | const playList = state.playLists.find((o) => o.id === listId) || new PlayList() 254 | const tmp = playList.content.splice(oldIndex, 1)[0] 255 | playList.content.splice(newIndex, 0, tmp) 256 | localStorage.setItem('playLists', JSON.stringify(state.playLists)) 257 | }, 258 | sortPlayingListContent(state: IState, {oldIndex, newIndex}: any) { 259 | const playingList = state.playingList 260 | const tmp = playingList.splice(oldIndex, 1)[0] 261 | playingList.splice(newIndex, 0, tmp) 262 | localStorage.setItem('playingList', JSON.stringify(state.playingList)) 263 | }, 264 | removePlayingList(state: IState, ids: string[]) { 265 | state.playingList = state.playingList.filter((o) => ids.indexOf(o.id) === -1) 266 | localStorage.setItem('playingList', JSON.stringify(state.playingList)) 267 | }, 268 | removeRecentPlay(state: IState, ids: string[]) { 269 | state.recentPlay = state.recentPlay.filter((o) => ids.indexOf(o.id) === -1) 270 | localStorage.setItem('recentPlay', JSON.stringify(state.recentPlay)) 271 | }, 272 | /* checkRecentPlayValid(state:IState){ 273 | state.recentPlay.filter(o=>o.) 274 | }*/ 275 | } 276 | 277 | export default { 278 | namespaced: true, 279 | state: playListState, 280 | getters, 281 | actions, 282 | mutations, 283 | } 284 | -------------------------------------------------------------------------------- /src/store/modules/playing.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash' 2 | 3 | const initState: IState = { 4 | isShowList: true, 5 | isSelectMode: false, 6 | selectedFileIds: [] 7 | } 8 | 9 | export interface IState { 10 | isShowList: boolean, 11 | isSelectMode: boolean, 12 | selectedFileIds: Array 13 | } 14 | 15 | const state = _.cloneDeep(initState) 16 | 17 | const getters = {} 18 | 19 | const actions = {} 20 | 21 | const mutations = { 22 | setShowList(state: IState, b: boolean) { 23 | state.isShowList = b 24 | }, 25 | removeSelectedFile(state: IState, id: number) { 26 | state.selectedFileIds.splice(state.selectedFileIds.indexOf(id),1) 27 | 28 | }, 29 | addSelectedFile(state: IState, id: number) { 30 | state.selectedFileIds.push(id) 31 | 32 | }, 33 | toSelectMode(state:IState,id:number){ 34 | state.isSelectMode = true 35 | state.selectedFileIds.push(id) 36 | }, 37 | cancelSelect(state:IState){ 38 | state.isSelectMode = false 39 | state.selectedFileIds = [] 40 | } 41 | } 42 | 43 | export default { 44 | namespaced: true, 45 | state, 46 | getters, 47 | actions, 48 | mutations 49 | } 50 | -------------------------------------------------------------------------------- /src/store/modules/test.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash' 2 | import {ActionContextBasic} from "@/store"; 3 | 4 | const initState = { 5 | list:[], 6 | view:{ 7 | text:'' 8 | } 9 | } 10 | 11 | export interface IState { 12 | list:string[], 13 | view:{ 14 | text:string 15 | } 16 | } 17 | 18 | const getters = {} 19 | 20 | 21 | const actions = { 22 | testAction({dispatch,commit}:ActionContextBasic){ 23 | commit('add') 24 | } 25 | } 26 | 27 | const mutations = { 28 | add(state:IState){ 29 | state.list.push(state.view.text) 30 | }, 31 | setText(state:IState,str:string){ 32 | state.view.text = str 33 | } 34 | } 35 | 36 | export default { 37 | namespaced: true, 38 | state:initState, 39 | getters, 40 | actions, 41 | mutations 42 | } 43 | -------------------------------------------------------------------------------- /src/store/types/PlayList.ts: -------------------------------------------------------------------------------- 1 | import {PlayListContentDataItem} from '@/store/modules/playList' 2 | 3 | export class PlayList { 4 | public id: string 5 | public title: string 6 | public content: PlayListContentDataItem[] 7 | 8 | constructor() { 9 | this.id = '' 10 | this.title = '' 11 | this.content = [] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/types/BaseItem.ts: -------------------------------------------------------------------------------- 1 | export class BaseItem { 2 | public id: any 3 | public title!: string 4 | public content!: any[] 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/Message.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | 4 | export default { 5 | info(msg:string){ 6 | // @ts-ignore 7 | (new Vue()).$Message.info(msg) 8 | } 9 | 10 | } -------------------------------------------------------------------------------- /src/utils/Notice.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | interface openOpt { 4 | title:string, 5 | desc?:string, 6 | duration?:number 7 | } 8 | export default { 9 | open(opt:openOpt){ 10 | (new Vue()).$Notice.open(opt) 11 | } 12 | } -------------------------------------------------------------------------------- /src/utils/animatie.ts: -------------------------------------------------------------------------------- 1 | import anime from 'animejs' 2 | 3 | const scale = 1 // 动画的整体缩放 4 | const numberOfParticle = 6 // 粒子数量 5 | const particleColor = ['#4fc3f7', '#00b454', '#d8f800', '#0b61a4', '#4db6ac', '#f06292'] // 粒子颜色 6 | const particleDistance = 100 * scale // 粒子飞行距离 7 | const circleSize = 90 * scale // 圆环大小 8 | const circleBorderWidth = 10 * scale // 圆环宽度 9 | const particleSize = 10 * scale // 粒子大小 10 | const circleOpacity = .5 // 圆环透明度 11 | const duration = 1000 // 动画持续时间 12 | 13 | function createCircle(x, y, color = '#1d72ff') { 14 | const circle = document.createElement('span') 15 | circle.style.position = 'fixed' // 使用fixed以防撑开页面 16 | circle.style.display = 'block' 17 | circle.style.borderRadius = `${circleSize}px` 18 | circle.style.boxSizing = 'border-box' 19 | circle.style.opacity = `${circleOpacity}` 20 | circle.style.width = circleSize + 'px' 21 | circle.style.height = circleSize + 'px' 22 | circle.style.left = x - circleSize / 2 + 'px' 23 | circle.style.top = y - circleSize / 2 + 'px' 24 | circle.style.border = `${circleBorderWidth}px solid ${color}` 25 | circle.style.transform = 'scale(.01)' 26 | circle.style.borderWidth = '10px' 27 | document.body.appendChild(circle) 28 | return circle 29 | } 30 | 31 | /*class Particle extends HTMLElement { 32 | public endPos!: { 33 | x: number, y: number, 34 | } 35 | }*/ 36 | 37 | declare global { 38 | interface HTMLDivElement { 39 | endPos: { 40 | x: number, y: number, 41 | } 42 | } 43 | } 44 | 45 | function createParticle(x, y, index) { 46 | x = x - particleSize / 2 47 | y = y - particleSize / 2 48 | const particle = document.createElement('div') 49 | particle.style.position = 'fixed' 50 | particle.style.left = x + 'px' 51 | particle.style.top = y + 'px' 52 | particle.style.width = particleSize + 'px' 53 | particle.style.height = particleSize + 'px' 54 | particle.style.borderRadius = particleSize + 'px' 55 | if (particleColor instanceof Array) { 56 | particle.style.backgroundColor = particleColor[index % particleColor.length] 57 | } else { 58 | particle.style.backgroundColor = particleColor 59 | } 60 | 61 | const angle = (index * (360 / numberOfParticle) - 90) * Math.PI / 180 62 | particle.endPos = { // 计算出每个粒子消失点的位置 63 | x: Math.cos(angle) * particleDistance, 64 | y: Math.sin(angle) * particleDistance, 65 | } 66 | 67 | document.body.appendChild(particle) 68 | return particle 69 | } 70 | 71 | export function fire(x, y) { 72 | const particles = [] 73 | for (let i = 0; i < numberOfParticle; i++) { 74 | particles.push(createParticle(x, y, i)) 75 | } 76 | const circle = createCircle(x, y) 77 | 78 | anime.timeline() // anime时间线动画。文档:https://github.com/juliangarnier/anime#timeline 79 | .add({ // 粒子飞向各自的结束点 80 | targets: particles, 81 | translateX(p) { // transform动画 82 | return parseFloat(p.endPos.x.toFixed(10)) 83 | }, 84 | translateY(p) { 85 | return parseFloat(p.endPos.y.toFixed(10)) 86 | }, 87 | easing: 'easeOutExpo', 88 | duration, 89 | }) 90 | .add({ // 粒子缩小 91 | translateX(p) { 92 | return parseFloat(p.endPos.x.toFixed(10)) // 由于transform已经被之前translate的动画占用,如果还要加scale动画的话就要确保translate不被覆盖。 93 | }, 94 | translateY(p) { 95 | return parseFloat(p.endPos.y.toFixed(10)) 96 | }, 97 | targets: particles, 98 | scale: 0.01, 99 | easing: 'easeOutQuad', 100 | duration: 900, 101 | offset: duration - 900, // 此动画距离动画开始时间的偏移 102 | }) 103 | .add({ 104 | targets: circle, 105 | scale: 1, 106 | borderWidth: circleBorderWidth, 107 | easing: 'easeOutQuad', 108 | duration: duration / 3.5, 109 | offset: 0, 110 | }) 111 | .add({ 112 | targets: circle, 113 | borderWidth: 0, 114 | easing: 'linear', 115 | duration: duration / 3.5, 116 | offset: duration / 3.5, 117 | }) 118 | setTimeout(() => { // 移除动画dom 119 | particles.forEach((o) => { 120 | document.body.removeChild(o) 121 | }) 122 | document.body.removeChild(circle) 123 | }, duration) 124 | } 125 | 126 | 127 | export function shake(e) { 128 | anime({ 129 | targets: e, 130 | rotate: [ 131 | {value: -30, duration: 200, easing: 'easeOutExpo'}, 132 | {value: 10, duration: 200, easing: 'easeOutExpo'}, 133 | {value: 0, duration: 100, easing: 'linear'}, 134 | ], 135 | scale: [ 136 | {value: 1.2, duration: 200, easing: 'easeOutExpo'}, 137 | {value: 1, duration: 100, easing: 'linear'}, 138 | ], 139 | }) 140 | } 141 | 142 | -------------------------------------------------------------------------------- /src/utils/config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | coverPath: 'https://vc-player-file-vert.vercel.app/cover/', 3 | musicPath: 'https://vc-player-file-vert.vercel.app/mp3convert/', 4 | musicExt: 'mp3', 5 | } 6 | export const apiUrl = process.env.NODE_ENV === 'development' ? 'http://localhost:5010' : 'https://api.vcollection.org' 7 | -------------------------------------------------------------------------------- /src/utils/enum/LocalStorageKeys.ts: -------------------------------------------------------------------------------- 1 | 2 | export enum LocalStorageKeys { 3 | volume='volume', 4 | recentPlay='recentPlay', 5 | playingList='playingList', 6 | playLists='playLists', 7 | playingFile='playingFile' 8 | 9 | } -------------------------------------------------------------------------------- /src/utils/enum/LoopMode.ts: -------------------------------------------------------------------------------- 1 | export enum LoopMode { 2 | close = 0, loopAll = 1, loopSingle = 2 3 | } -------------------------------------------------------------------------------- /src/utils/request.ts: -------------------------------------------------------------------------------- 1 | import axios, {AxiosRequestConfig, AxiosResponse} from 'axios' 2 | import * as qs from 'qs' 3 | import mainApi from '@/api/mainApi' 4 | import {apiUrl} from '@/utils/config' 5 | 6 | export class NetWorkError extends Error { 7 | public response!: AxiosResponse 8 | } 9 | 10 | function checkStatus(error: Error) { 11 | axios.post(apiUrl + '/home/reportError', qs.stringify({message: JSON.stringify(error)})) 12 | throw error 13 | } 14 | 15 | /** 16 | * Requests a URL, returning a promise. 17 | * 18 | * @param {string} url The URL we want to request 19 | * @param {object} [options] The options we want to pass to "fetch" 20 | * @return {object} An object containing either "data" or "err" 21 | */ 22 | 23 | export default { 24 | /* get: function (url:string, params) { 25 | return axios.get(url, params) 26 | .then(checkStatus) 27 | .then(data => data.data) 28 | }, 29 | post: function (url, data, config) { 30 | return axios.post(url, qs.stringify({...data}), config) 31 | .then(checkStatus) 32 | .then(data => data.data) 33 | },*/ 34 | get(url: string, data: object = {}, params: AxiosRequestConfig = {}) { 35 | url = Object.keys(data).length ? url + `?${qs.stringify(data)}` : url 36 | return axios.get(url, {...params}) 37 | .catch(checkStatus) 38 | .then((o: AxiosResponse) => o.data) 39 | }, 40 | post(url: string, data: object = {}, config: AxiosRequestConfig = {}) { 41 | return axios.post(url, qs.stringify({...data}), config) 42 | .catch(checkStatus) 43 | .then((o: AxiosResponse) => o.data) 44 | }, 45 | } 46 | -------------------------------------------------------------------------------- /src/var.scss: -------------------------------------------------------------------------------- 1 | $mainBlue: rgb(0, 120, 215); 2 | 3 | $mobileSize:450px; 4 | 5 | @mixin flexCenter{ 6 | display: flex; 7 | justify-content: center; 8 | align-items: center; 9 | } 10 | -------------------------------------------------------------------------------- /src/views/AllPlayList.vue: -------------------------------------------------------------------------------- 1 | 91 | 92 | 181 | 182 | 281 | -------------------------------------------------------------------------------- /src/views/Files.vue: -------------------------------------------------------------------------------- 1 | 63 | 64 | 154 | 155 | 256 | 257 | 258 | 274 | -------------------------------------------------------------------------------- /src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 102 | 103 | 219 | 220 | 416 | -------------------------------------------------------------------------------- /src/views/Playing.vue: -------------------------------------------------------------------------------- 1 | 81 | 82 | 241 | 242 | 470 | 471 | 476 | -------------------------------------------------------------------------------- /src/views/RecentPlay.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 116 | 117 | 196 | -------------------------------------------------------------------------------- /src/views/Test.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 37 | 38 | 63 | 64 | 69 | -------------------------------------------------------------------------------- /src/vue.d.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | 4 | declare module 'vue/types/vue' { 5 | interface Vue { 6 | $Message: any 7 | $Notice: any 8 | } 9 | } 10 | 11 | 12 | declare global { 13 | interface Navigator { 14 | mediaSession: any 15 | } 16 | 17 | interface String { 18 | myExtendAction: () => void 19 | } 20 | 21 | const myVariable = 1 22 | } 23 | -------------------------------------------------------------------------------- /tests/unit/HelloWorld.spec.js: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils'; 2 | import HomeBottom from '@/components/HomeBottom.vue'; 3 | describe('DropDown', function () { 4 | it('should dropDown show', function () { 5 | const wrapper = mount(HomeBottom); 6 | const fn = jest.fn(x => x); 7 | wrapper.setMethods({ togglePlay: fn }); 8 | wrapper.find('.play').trigger('click'); 9 | expect(fn).toBeCalled(); 10 | }); 11 | }); 12 | //# sourceMappingURL=HelloWorld.spec.js.map -------------------------------------------------------------------------------- /tests/unit/HelloWorld.spec.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"HelloWorld.spec.js","sourceRoot":"","sources":["HelloWorld.spec.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,KAAK,EAAe,MAAM,iBAAiB,CAAC;AACpD,OAAO,UAAU,MAAM,6BAA6B,CAAA;AAGpD,QAAQ,CAAC,UAAU,EAAE;IACnB,EAAE,CAAC,sBAAsB,EAAE;QACzB,MAAM,OAAO,GAAG,KAAK,CAAC,UAAU,CAAC,CAAA;QACjC,MAAM,EAAE,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC,CAAA,EAAE,CAAA,CAAC,CAAC,CAAA;QACxB,OAAO,CAAC,UAAU,CAAC,EAAC,UAAU,EAAC,EAAE,EAAC,CAAC,CAAA;QACnC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAA;QACtC,MAAM,CAAC,EAAE,CAAC,CAAC,UAAU,EAAE,CAAA;IACzB,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"} -------------------------------------------------------------------------------- /tests/unit/HelloWorld.spec.ts: -------------------------------------------------------------------------------- 1 | import {mount, shallowMount} from '@vue/test-utils'; 2 | import HomeBottom from '@/components/HomeBottom.vue' 3 | 4 | 5 | describe('DropDown', function () { 6 | it('should dropDown show', function () { 7 | const wrapper = mount(HomeBottom) 8 | const fn = jest.fn(x=>x) 9 | wrapper.setMethods({togglePlay:fn}) 10 | wrapper.find('.play').trigger('click') 11 | expect(fn).toBeCalled() 12 | }); 13 | }); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "esnext", 5 | "strict": false, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "experimentalDecorators": true, 10 | "esModuleInterop": true, 11 | "sourceMap": true, 12 | "baseUrl": ".", 13 | // "noImplicitAny": false, 14 | "types": [ 15 | "node", 16 | "jest" 17 | ], 18 | "paths": { 19 | "@/*": [ 20 | "src/*" 21 | ] 22 | }, 23 | "lib": [ 24 | "es2015", 25 | "dom", 26 | "dom.iterable", 27 | "scripthost" 28 | ] 29 | }, 30 | "include": [ 31 | "src/**/*.ts", 32 | "src/**/*.tsx", 33 | "src/**/*.vue", 34 | "tests/**/*.ts", 35 | "tests/**/*.tsx", 36 | "node_modules/@types/jquery/index.d.ts", 37 | "node_modules/@types/jquery/index.d.ts" 38 | ], 39 | "exclude": [ 40 | "node_modules" 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "linterOptions": { 7 | "exclude": [ 8 | "node_modules/**", 9 | "**/*.js" 10 | ] 11 | }, 12 | "rules": { 13 | "quotemark": [true, "single"], 14 | "indent": [true, "spaces", 2], 15 | "interface-name": false, 16 | "ordered-imports": false, 17 | "object-literal-sort-keys": false, 18 | "no-consecutive-blank-lines": false, 19 | "semicolon": [true,"never"], 20 | "no-bitwise": false, 21 | "no-unused-expression": false 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | css: { 3 | loaderOptions: { 4 | sass: { 5 | implementation: require('sass'), // This line must in sass option 6 | }, 7 | }, 8 | }, 9 | publicPath: "/", 10 | chainWebpack: (config) => { 11 | config.plugins.delete("fork-ts-checker"); 12 | }, 13 | configureWebpack: { 14 | externals: { 15 | "vue": "Vue", 16 | "lodash": "_", 17 | "iview": "iview", 18 | "@shopify/draggable": "Draggable", 19 | "vue-router": "VueRouter", 20 | }, 21 | resolve: { 22 | alias: { 23 | "vue$": "vue/dist/vue.esm.js", 24 | }, 25 | }, 26 | /* devServer:{ 27 | // historyApiFallback: true 28 | overlay:{ 29 | warnings:false 30 | }, 31 | },*/ 32 | }, 33 | 34 | productionSourceMap: false, 35 | }; 36 | --------------------------------------------------------------------------------