├── .all-contributorsrc ├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .postcssrc.js ├── .prettierignore ├── .prettierrc ├── .travis.yml ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── docs ├── .vuepress │ ├── components │ │ ├── aplayer-fixed.vue │ │ ├── aplayer-hls.vue │ │ ├── aplayer-lrc.vue │ │ ├── aplayer-mini.vue │ │ ├── aplayer-playlist.vue │ │ ├── aplayer-theme.vue │ │ └── aplayer-vnode.vue │ ├── config.js │ ├── enhanceApp.js │ ├── public │ │ ├── favicon.ico │ │ └── hero.png │ └── styles │ │ ├── index.styl │ │ └── palette.styl ├── README.md ├── api │ └── README.md ├── guide │ ├── README.md │ ├── api.md │ ├── cdn.md │ ├── events.md │ ├── faq.md │ ├── fixed.md │ ├── hls.md │ ├── lrc.md │ ├── mini.md │ ├── options.md │ ├── playlist.md │ └── theme.md └── options │ └── README.md ├── example ├── App.scss ├── App.tsx ├── main.ts └── public │ ├── favicon.ico │ ├── index.html │ └── music │ └── data.json ├── logo ├── Icon.png ├── Logo.png ├── design.png ├── icon-blue.png ├── icon.png ├── logo-blue.png ├── logo-type-horizontal.png ├── logo-type-vertical.png └── logo.png ├── package.json ├── packages └── @moefe │ ├── vue-aplayer │ ├── assets │ │ ├── style │ │ │ └── aplayer.scss │ │ └── svg │ │ │ ├── loading.svg │ │ │ ├── loop-all.svg │ │ │ ├── loop-none.svg │ │ │ ├── loop-one.svg │ │ │ ├── lrc.svg │ │ │ ├── menu.svg │ │ │ ├── order-list.svg │ │ │ ├── order-random.svg │ │ │ ├── pause.svg │ │ │ ├── play.svg │ │ │ ├── right.svg │ │ │ ├── skip.svg │ │ │ ├── volume-down.svg │ │ │ ├── volume-off.svg │ │ │ └── volume-up.svg │ ├── components │ │ ├── APlayer.tsx │ │ ├── Button.tsx │ │ ├── Controller.tsx │ │ ├── Cover.tsx │ │ ├── Icon.tsx │ │ ├── Lyric.tsx │ │ ├── Main.tsx │ │ ├── PlayList.tsx │ │ ├── Player.tsx │ │ └── Progress.tsx │ ├── index.ts │ ├── shims.d.ts │ └── utils │ │ └── index.ts │ ├── vue-audio │ ├── events.ts │ └── index.ts │ ├── vue-store │ └── index.ts │ └── vue-touch │ └── index.tsx ├── tsconfig.json ├── types ├── aplayer.d.ts ├── index.d.ts ├── test.tsx ├── tsconfig.json └── tslint.json ├── utils ├── index.ts └── mixin.ts ├── vue.config.js └── yarn.lock /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "vue-aplayer", 3 | "projectOwner": "MoePlayer", 4 | "badgeTemplate": "[![All Contributors](https://img.shields.io/badge/all_contributors-<%= contributors.length %>-orange.svg?style=for-the-badge)](#contributors)", 5 | "commit": false, 6 | "contributors": [ 7 | { 8 | "login": "u3u", 9 | "name": "さくら", 10 | "avatar_url": "https://avatars2.githubusercontent.com/u/20062482?v=4", 11 | "profile": "https://qwq.cat", 12 | "contributions": [ 13 | "code", 14 | "doc", 15 | "example" 16 | ] 17 | }, 18 | { 19 | "login": "DIYgod", 20 | "name": "DIYgod", 21 | "avatar_url": "https://avatars2.githubusercontent.com/u/8266075?v=4", 22 | "profile": "https://diygod.me", 23 | "contributions": [ 24 | "design", 25 | "ideas" 26 | ] 27 | }, 28 | { 29 | "login": "RexSkz", 30 | "name": "Rex Zeng", 31 | "avatar_url": "https://avatars3.githubusercontent.com/u/27483702?v=4", 32 | "profile": "https://forkmeongithub.com/", 33 | "contributions": [ 34 | "bug" 35 | ] 36 | }, 37 | { 38 | "login": "nunojesus", 39 | "name": "Nuno Jesus", 40 | "avatar_url": "https://avatars0.githubusercontent.com/u/34600369?v=4", 41 | "profile": "https://github.com/nunojesus", 42 | "contributions": [ 43 | "design" 44 | ] 45 | } 46 | ], 47 | "repoType": "github" 48 | } 49 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@vue/app"] 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | # Use 4 spaces for the Python files 13 | [*.py] 14 | indent_size = 4 15 | max_line_length = 80 16 | 17 | # The JSON files contain newlines inconsistently 18 | [*.json] 19 | insert_final_newline = ignore 20 | 21 | # Minified JavaScript files shouldn't be changed 22 | [**.min.js] 23 | indent_style = ignore 24 | insert_final_newline = ignore 25 | 26 | # Makefiles always use tabs for indentation 27 | [Makefile] 28 | indent_style = tab 29 | 30 | # Batch files use tabs for indentation 31 | [*.bat] 32 | indent_style = tab 33 | 34 | [*.md] 35 | trim_trailing_whitespace = false 36 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /example/public 2 | /dist 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['plugin:vue/essential', '@vue/airbnb', '@vue/typescript'], 4 | rules: { 5 | // https://github.com/vuejs/vue-cli/issues/1672 6 | indent: 'off', 7 | 'space-infix-ops': 'off', 8 | 'object-curly-newline': 'off', 9 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 10 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 11 | 'no-plusplus': ['error', { allowForLoopAfterthoughts: true }], 12 | 'import/extensions': ['off'], 13 | 'import/no-unresolved': ['off'], 14 | 'import/no-duplicates': ['off'], 15 | 'import/no-named-as-default': ['off'], 16 | 'import/no-named-as-default-member': ['off'], 17 | 'import/no-extraneous-dependencies': ['off'], 18 | 'function-paren-newline': ['off'], 19 | 'implicit-arrow-linebreak': ['off'], 20 | 'no-confusing-arrow': ['off'], 21 | 'class-methods-use-this': [ 22 | 'error', 23 | { 24 | exceptMethods: [ 25 | 'render', 26 | 'beforeCreate', 27 | 'created', 28 | 'beforeMount', 29 | 'mounted', 30 | 'beforeUpdate', 31 | 'updated', 32 | 'activated', 33 | 'deactivated', 34 | 'beforeDestroy', 35 | 'destroyed', 36 | 'errorCaptured', 37 | ], 38 | }, 39 | ], 40 | 'max-len': [ 41 | 'error', 42 | { 43 | code: 80, 44 | ignoreUrls: true, 45 | ignoreStrings: true, 46 | ignoreComments: true, 47 | ignoreTrailingComments: true, 48 | ignoreTemplateLiterals: true, 49 | }, 50 | ], 51 | }, 52 | }; 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | /demo 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | 15 | # Editor directories and files 16 | .idea 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw* 22 | -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {}, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /demo 3 | /node_modules 4 | package.json 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5", 4 | "arrowParens": "always" 5 | } 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 9 4 | 5 | install: 6 | - yarn 7 | 8 | script: 9 | - yarn lint 10 | - yarn lint:prettier 11 | - yarn test:types 12 | 13 | cache: 14 | yarn: true 15 | directories: 16 | - node_modules 17 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "emmet.excludeLanguages": ["typescriptreact"], 4 | "eslint.autoFixOnSave": true, 5 | "eslint.enable": true, 6 | "eslint.validate": [ 7 | "javascript", 8 | "javascriptreact", 9 | "typescript", 10 | "typescriptreact" 11 | ], 12 | "files.exclude": { 13 | "demo": true, 14 | "dist": true, 15 | "logo": true 16 | }, 17 | "prettier.eslintIntegration": true, 18 | "tslint.autoFixOnSave": true, 19 | "tslint.enable": true, 20 | "typescript.tsdk": "node_modules/typescript/lib" 21 | } 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) u3u (https://qwq.cat) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | vue-aplayer 4 | 5 |

6 | 7 |

8 | NPM version 9 | Build Status 10 | LICENSE MIT 11 | Code Style: Prettier 12 |

13 | 14 | > This is the branch for `@moefe/vue-aplayer` 2.0. 15 | 16 | ## Status: Beta 17 | 18 | Most of the planned features are in place but there may still be bugs. 19 | 20 | ## Documentation 21 | 22 | Docs are available at [aplayer.moefe.org/docs/](https://aplayer.moefe.org/docs/) 23 | 24 | ## Install 25 | 26 | ```bash 27 | yarn add @moefe/vue-aplayer 28 | ``` 29 | 30 | ## Usage 31 | 32 | [![Edit vue-aplayer](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/xrylkkp27w?fontsize=12&module=%2Fsrc%2FApp.vue) 33 | 34 | ```js 35 | import Vue from 'vue'; 36 | import APlayer from '@moefe/vue-aplayer'; 37 | 38 | Vue.use(APlayer, { 39 | defaultCover: 'https://github.com/u3u.png', // set the default cover 40 | productionTip: false, // disable console output 41 | }); 42 | ``` 43 | 44 | ## Related Projects 45 | 46 | - [APlayer](https://github.com/MoePlayer/APlayer): Original project, thanks [@DIYgod](https://github.com/DIYgod) 47 | - [Vue-Aplayer](https://github.com/SevenOutman/vue-aplayer): Another Vue implementation of APlayer by [@SevenOutman](https://github.com/SevenOutman) 48 | 49 | ## Contributing 50 | 51 | 1. Fork it! 52 | 2. Create your feature branch: `git checkout -b my-new-feature` 53 | 3. Commit your changes: `git commit -am 'Add some feature'` 54 | 4. Push to the branch: `git push origin my-new-feature` 55 | 5. Submit a pull request :D 56 | 57 | ## Contributors 58 | 59 | Thanks goes to these wonderful people ([emoji key](https://github.com/kentcdodds/all-contributors#emoji-key)): 60 | 61 | 62 | 63 | 64 | | [
さくら](https://qwq.cat)
[💻](https://github.com/MoePlayer/vue-aplayer/commits?author=u3u "Code") [📖](https://github.com/MoePlayer/vue-aplayer/commits?author=u3u "Documentation") [💡](#example-u3u "Examples") | [
DIYgod](https://diygod.me)
[🎨](#design-DIYgod "Design") [🤔](#ideas-DIYgod "Ideas, Planning, & Feedback") | [
Rex Zeng](https://forkmeongithub.com/)
[🐛](https://github.com/MoePlayer/vue-aplayer/issues?q=author%3ARexSkz "Bug reports") | [
Nuno Jesus](https://github.com/nunojesus)
[🎨](#design-nunojesus "Design") | 65 | | :---: | :---: | :---: | :---: | 66 | 67 | 68 | 69 | This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind are welcome! 70 | 71 | ## Author 72 | 73 | **vue-aplayer** © [u3u](https://github.com/u3u), Released under the [MIT](./LICENSE) License.
74 | Authored and maintained by u3u with help from contributors ([list](https://github.com/MoePlayer/vue-aplayer/contributors)). 75 | 76 | > [qwq.cat](https://qwq.cat) · GitHub [@u3u](https://github.com/u3u) 77 | -------------------------------------------------------------------------------- /docs/.vuepress/components/aplayer-fixed.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 40 | -------------------------------------------------------------------------------- /docs/.vuepress/components/aplayer-hls.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 46 | -------------------------------------------------------------------------------- /docs/.vuepress/components/aplayer-lrc.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 29 | -------------------------------------------------------------------------------- /docs/.vuepress/components/aplayer-mini.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 23 | -------------------------------------------------------------------------------- /docs/.vuepress/components/aplayer-playlist.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 40 | -------------------------------------------------------------------------------- /docs/.vuepress/components/aplayer-theme.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 48 | -------------------------------------------------------------------------------- /docs/.vuepress/components/aplayer-vnode.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 71 | -------------------------------------------------------------------------------- /docs/.vuepress/config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | base: '/docs/', 5 | title: 'vue-aplayer', 6 | description: '🍰 A beautiful HTML5 music player for Vue.js', 7 | head: [ 8 | ['link', { rel: 'icon', href: '/favicon.ico' }], 9 | ['script', { src: 'https://cdn.jsdelivr.net/npm/hls.js' }], 10 | ['script', { src: 'https://cdn.jsdelivr.net/npm/colorthief' }], // prettier-ignore 11 | ], 12 | dest: 'demo/docs', 13 | plugins: [ 14 | [ 15 | '@vuepress/pwa', 16 | { 17 | serviceWorker: true, 18 | updatePopup: { 19 | message: '发现新内容可用', 20 | buttonText: '刷新', 21 | }, 22 | }, 23 | ], 24 | [ 25 | '@vuepress/last-updated', 26 | { 27 | transformer: (timestamp) => { 28 | const dayjs = require('dayjs'); 29 | return dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss'); 30 | }, 31 | }, 32 | ], 33 | ], 34 | themeConfig: { 35 | nav: [ 36 | { text: '指南', link: '/guide/' }, 37 | { text: '选项', link: '/options/' }, 38 | { text: 'API', link: '/api/' }, 39 | ], 40 | sidebar: { 41 | '/guide/': [ 42 | '', 43 | 'options', 44 | 'api', 45 | 'events', 46 | 'lrc', 47 | 'playlist', 48 | 'fixed', 49 | 'mini', 50 | 'hls', 51 | 'theme', 52 | 'cdn', 53 | 'faq', 54 | ], 55 | }, 56 | repo: 'MoePlayer/vue-aplayer', 57 | docsDir: 'docs', 58 | docsBranch: 'dev', 59 | editLinks: true, 60 | editLinkText: '在 GitHub 上编辑此页', 61 | lastUpdated: '上次更新', 62 | }, 63 | chainWebpack: (config) => { 64 | config.resolve.set('symlinks', false); 65 | }, 66 | }; 67 | -------------------------------------------------------------------------------- /docs/.vuepress/enhanceApp.js: -------------------------------------------------------------------------------- 1 | export default ({ 2 | Vue, // VuePress 正在使用的 Vue 构造函数 3 | options, // 附加到根实例的一些选项 4 | router, // 当前应用的路由实例 5 | siteData, // 站点元数据 6 | }) => { 7 | if (typeof window !== 'undefined') { 8 | localStorage.setItem('aplayer-setting', '[]'); 9 | Vue.use(require('@moefe/vue-aplayer').default); 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /docs/.vuepress/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoePlayer/vue-aplayer/dd10c503001179dec4fed6f6644e50768a938e54/docs/.vuepress/public/favicon.ico -------------------------------------------------------------------------------- /docs/.vuepress/public/hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoePlayer/vue-aplayer/dd10c503001179dec4fed6f6644e50768a938e54/docs/.vuepress/public/hero.png -------------------------------------------------------------------------------- /docs/.vuepress/styles/index.styl: -------------------------------------------------------------------------------- 1 | .home 2 | .hero 3 | img 4 | width: 230px; 5 | 6 | .guide 7 | table 8 | tr 9 | th:first-child, td:first-child 10 | min-width: 149px; 11 | 12 | th:last-child, td:last-child 13 | width: 100%; 14 | 15 | .content:not(.custom) 16 | > h2 17 | + 18 | .button 19 | margin-top: 1rem; 20 | 21 | .aplayer 22 | margin-top: calc(1rem + 5px); 23 | 24 | .button 25 | display: block; 26 | padding: 0 14px; 27 | outline: none; 28 | border: 2px solid #42b983; 29 | border-radius: 17px; 30 | font-size: 14px; 31 | font-weight: 500; 32 | line-height: 16px; 33 | height: 32px; 34 | color: #42b983; 35 | background: transparent; 36 | cursor: pointer; 37 | box-sizing: border-box; 38 | transition: 0.3s; 39 | 40 | &:hover 41 | color: #fff; 42 | background: #42b983; 43 | -------------------------------------------------------------------------------- /docs/.vuepress/styles/palette.styl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoePlayer/vue-aplayer/dd10c503001179dec4fed6f6644e50768a938e54/docs/.vuepress/styles/palette.styl -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | heroImage: /hero.png 4 | actionText: 快速上手 → 5 | actionLink: /guide/ 6 | features: 7 | - title: 原汁原味 8 | details: 保持完整的功能和相同的API,与最新 APlayer 保持同步更新,最小化差异。 9 | - title: Vue 驱动 10 | details: 使用 Vue 重写了所有逻辑,不是简单的封装,所有属性都是响应式的。 11 | - title: TypeScript 支持 12 | details: 提供完整的 TypeScript 类型定义支持,对于喜欢使用 JSX 的用户非常友好。 13 | footer: MIT Licensed | Copyright © 2018-present MoePlayer 14 | --- 15 | 16 | ### 安装 17 | 18 | ```bash 19 | yarn add @moefe/vue-aplayer # OR npm install @moefe/vue-aplayer --save 20 | ``` 21 | 22 | ### 用法 23 | 24 | ```js 25 | import Vue from 'vue'; 26 | import APlayer from '@moefe/vue-aplayer'; 27 | 28 | Vue.use(APlayer, { 29 | defaultCover: 'https://github.com/u3u.png', // 设置播放器默认封面图片 30 | productionTip: false, // 是否在控制台输出版本信息 31 | }); 32 | ``` 33 | 34 | ::: warning 兼容性说明 35 | vue-aplayer 要求 Vue.js >= 2.2.0 36 | ::: 37 | -------------------------------------------------------------------------------- /docs/api/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar: auto 3 | --- 4 | 5 | # API 6 | 7 | ## version 8 | 9 | - **类型**:`string` 10 | - **描述**:只读属性,返回 APlayer 的版本号 11 | - **用法**: 12 | 13 | ```js 14 | import { APlayer } from '@moefe/vue-aplayer'; 15 | 16 | console.log(APlayer.version); 17 | ``` 18 | 19 | ## media 20 | 21 | - **类型**:`APlayer.Media` 22 | - **描述**:只读的原生 [`Media`](https://www.w3schools.com/tags/ref_av_dom.asp) 对象 23 | - **用法**: 24 | 25 | ```js 26 | const { media } = this.$refs.aplayer; 27 | 28 | console.log(media.currentTime); // 获取音频当前播放时间 29 | console.log(media.duration); // 获取音频总时间 30 | console.log(media.paused); // 获取音频是否暂停 31 | ``` 32 | 33 | ## currentMusic 34 | 35 | ::: warning 警告 36 | 如果你想切换到播放列表中的其他音频请使用 [`switch`](#switch) 方法,而不要直接设置它 37 | ::: 38 | 39 | - **类型**:`APlayer.Audio` 40 | - **描述**:获取当前正在播放的音频 41 | - **用法**: 42 | 43 | ```js 44 | console.log(this.$refs.aplayer.currentMusic); 45 | ``` 46 | 47 | ## play() 48 | 49 | - **类型**:`Function` 50 | - **返回值**:`Promise` 51 | - **描述**:播放音频 52 | - **用法**: 53 | 54 | ```js 55 | this.$refs.aplayer.play(); 56 | ``` 57 | 58 | ## pause() 59 | 60 | - **类型**:`Function` 61 | - **返回值**:`void` 62 | - **描述**:暂停音频 63 | - **用法**: 64 | 65 | ```js 66 | this.$refs.aplayer.pause(); 67 | ``` 68 | 69 | ## toggle() 70 | 71 | - **类型**:`Function` 72 | - **返回值**:`void` 73 | - **描述**:切换播放和暂停 74 | - **用法**: 75 | 76 | ```js 77 | this.$refs.aplayer.toggle(); 78 | ``` 79 | 80 | ## seek() 81 | 82 | - **类型**:`Function` 83 | - **参数**: 84 | - `time` 85 | - **类型**:`number` 86 | - **描述**:时间(秒) 87 | - **返回值**:`void` 88 | - **描述**:跳到特定时间 89 | - **用法**: 90 | 91 | ```js 92 | this.$refs.aplayer.seek(100); 93 | ``` 94 | 95 | ## switch() 96 | 97 | - **类型**:`Function` 98 | - **参数**: 99 | - `audio` 100 | - **类型**:`number` | `string` 101 | - **描述**:音频索引或音频的部分名称 102 | - **返回值**:`void` 103 | - **描述**:切换到播放列表中的其他音频 104 | - **用法**: 105 | 106 | ```js 107 | this.$refs.aplayer.switch(1); // 切换到播放列表中的第二首歌 108 | this.$refs.aplayer.switch('东西'); // 切换到播放列表中歌曲名包含“东西”的第一首歌 109 | ``` 110 | 111 | ## skipBack() 112 | 113 | - **类型**:`Function` 114 | - **返回值**:`void` 115 | - **描述**:切换到上一首音频 116 | - **用法**: 117 | 118 | ```js 119 | this.$refs.aplayer.skipBack(); 120 | ``` 121 | 122 | ## skipForward() 123 | 124 | - **类型**:`Function` 125 | - **返回值**:`void` 126 | - **描述**:切换到下一首音频 127 | - **用法**: 128 | 129 | ```js 130 | this.$refs.aplayer.skipForward(); 131 | ``` 132 | 133 | ## showNotice() 134 | 135 | - **类型**:`Function` 136 | - **参数**: 137 | - `text` 138 | - **类型**:`string` 139 | - **描述**:通知文本 140 | - `time` 141 | - **类型**:`number?` 142 | - **默认值**:2000 143 | - **描述**:显示时间(毫秒) 144 | - `opacity` 145 | - **类型**:`number?` 146 | - **默认值**:0.8 147 | - **描述**:通知透明度 (0 ~ 1) 148 | - **返回值**:`Promise` 149 | - **描述**:显示通知,设置时间为 0 可以取消通知自动隐藏 150 | - **用法**: 151 | 152 | ```js 153 | this.$refs.aplayer.showNotice('喵喵喵'); 154 | ``` 155 | 156 | ## showLrc() 157 | 158 | - **类型**:`Function` 159 | - **返回值**:`void` 160 | - **描述**:显示歌词 161 | - **用法**: 162 | 163 | ```js 164 | this.$refs.aplayer.showLrc(); 165 | ``` 166 | 167 | ## hideLrc() 168 | 169 | - **类型**:`Function` 170 | - **返回值**:`void` 171 | - **描述**:隐藏歌词 172 | - **用法**: 173 | 174 | ```js 175 | this.$refs.aplayer.hideLrc(); 176 | ``` 177 | 178 | ## toggleLrc() 179 | 180 | - **类型**:`Function` 181 | - **返回值**:`void` 182 | - **描述**:显示/隐藏歌词 183 | - **用法**: 184 | 185 | ```js 186 | this.$refs.aplayer.toggleLrc(); 187 | ``` 188 | 189 | ## showList() 190 | 191 | - **类型**:`Function` 192 | - **返回值**:`void` 193 | - **描述**:显示播放列表 194 | - **用法**: 195 | 196 | ```js 197 | this.$refs.aplayer.showList(); 198 | ``` 199 | 200 | ## hideList() 201 | 202 | - **类型**:`Function` 203 | - **返回值**:`void` 204 | - **描述**:隐藏播放列表 205 | - **用法**: 206 | 207 | ```js 208 | this.$refs.aplayer.hideList(); 209 | ``` 210 | 211 | ## toggleList() 212 | 213 | - **类型**:`Function` 214 | - **返回值**:`void` 215 | - **描述**:显示/隐藏播放列表 216 | - **用法**: 217 | 218 | ```js 219 | this.$refs.aplayer.toggleList(); 220 | ``` 221 | -------------------------------------------------------------------------------- /docs/guide/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebarDepth: 2 3 | --- 4 | 5 | # 入门 6 | 7 | ::: warning 注意 8 | 请确保你的 Vue.js 版本 >= 2.2.0 9 | ::: 10 | 11 | ## 安装 12 | 13 | ### 使用 npm 14 | 15 | ```bash 16 | npm install @moefe/vue-aplayer --save 17 | ``` 18 | 19 | ### 使用 yarn 20 | 21 | ```bash 22 | yarn add @moefe/vue-aplayer 23 | ``` 24 | 25 | 推荐使用 yarn 安装并提交 yarn.lock 锁定版本号 26 | 27 | ## 快速开始 28 | 29 | ### 传统方式 30 | 31 | 📝 index.html 32 | 33 | ```html 34 | 35 | 36 | 37 |
38 | 39 |
40 | 41 | 42 | 43 | 44 | 63 | 64 | ``` 65 | 66 | ### 单文件组件 67 | 68 | 📝 main.js 69 | 70 | ```js 71 | import Vue from 'vue'; 72 | import APlayer from '@moefe/vue-aplayer'; 73 | 74 | Vue.use(APlayer, { 75 | defaultCover: 'https://github.com/u3u.png', 76 | productionTip: true, 77 | }); 78 | ``` 79 | 80 | 📝 app.vue 81 | 82 | ```vue 83 | 84 | 89 | 90 | 105 | ``` 106 | 107 | ::: warning 提示 108 | 109 | 这种方式是官方推荐的,也是大家熟知的使用最多、上手最快的。但是开发体验不是很友好。 110 | 虽然官方提供了 [Vetur](https://github.com/vuejs/vetur) 扩展来强化开发体验,但依然无法做到以下几点: 111 | 112 | 1. ~~目前 Prettier 还不支持格式化模版部分[(正在进行中)](https://github.com/prettier/prettier/pull/4753)~~ 113 | 2. 模版部分没有强大的智能感知功能 114 | 3. 对 TypeScript 不友好 115 | 4. 无法批量传递 `props` 116 | 5. 不能使用 HTML 内置标签名 117 | 118 | ::: 119 | 120 | ### vue-class-component 121 | 122 | 如果你熟悉 React,或是 JSX 爱好者,那么我推荐你使用这种方式。 123 | 你依然可以使用 `@Component` 装饰器以传统方式传递组件的属性,但我不推荐这么做。 124 | 125 | #### JavaScript 126 | 127 | ::: tip 提示 128 | `@vue/cli` 3.0 默认配置了 [JSX 预设](https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/babel-preset-app),所以你无需做任何额外的配置。 129 | 安装 [vue-tsx-support](https://github.com/wonderful-panda/vue-tsx-support#install-and-enable) 130 | 并配置 [jsconfig.json](https://code.visualstudio.com/docs/languages/jsconfig) 131 | 和 [vue-jsx-hot-loader](https://github.com/skyrpex/vue-jsx-hot-loader) 获得最佳开发体验。 132 | ::: 133 | 134 | 📝 main.js 135 | 136 | ```js 137 | import Vue from 'vue'; 138 | import APlayer from '@moefe/vue-aplayer'; 139 | 140 | Vue.use(APlayer, { 141 | defaultCover: 'https://github.com/u3u.png', 142 | productionTip: true, 143 | }); 144 | ``` 145 | 146 | 📝 app.js 147 | 148 | ```jsx 149 | import Vue from 'vue'; 150 | import Component from 'vue-class-component'; 151 | import { APlayer } from '@moefe/vue-aplayer'; 152 | 153 | @Component 154 | export default class App extends Vue { 155 | audio = { 156 | name: '东西(Cover:林俊呈)', 157 | artist: '纳豆', 158 | url: 'https://cdn.moefe.org/music/mp3/thing.mp3', 159 | cover: 'https://p1.music.126.net/5zs7IvmLv7KahY3BFzUmrg==/109951163635241613.jpg?param=300y300', // prettier-ignore 160 | lrc: 'https://cdn.moefe.org/music/lrc/thing.lrc', 161 | }; 162 | 163 | render() { 164 | return ( 165 |
166 | 167 |
168 | ); 169 | } 170 | } 171 | ``` 172 | 173 | #### TypeScript 174 | 175 | ::: danger 注意 176 | TypeScript 用户必须安装 [vue-tsx-support](https://github.com/wonderful-panda/vue-tsx-support#install-and-enable) 177 | 并配置 [tsconfig.json](https://www.tslang.cn/docs/handbook/tsconfig-json.html) 178 | 同样,你也可以配置 [vue-jsx-hot-loader](https://github.com/skyrpex/vue-jsx-hot-loader) 获得最佳开发体验。 179 | ::: 180 | 181 | 📝 main.ts 182 | 183 | ```ts 184 | import Vue from 'vue'; 185 | import APlayer from '@moefe/vue-aplayer'; 186 | 187 | Vue.use(APlayer, { 188 | defaultCover: 'https://github.com/u3u.png', 189 | productionTip: true, 190 | }); 191 | ``` 192 | 193 | 📝 app.tsx 194 | 195 | ```tsx 196 | import Vue from 'vue'; 197 | import Comopnent from 'vue-class-component'; 198 | import { APlayer } from '@moefe/vue-aplayer'; 199 | 200 | @Comopnent 201 | export default class App extends Vue { 202 | private audio: APlayer.Audio | APlayer.Audio[] = { 203 | name: '东西(Cover:林俊呈)', 204 | artist: '纳豆', 205 | url: 'https://cdn.moefe.org/music/mp3/thing.mp3', 206 | cover: 'https://p1.music.126.net/5zs7IvmLv7KahY3BFzUmrg==/109951163635241613.jpg?param=300y300', // prettier-ignore 207 | lrc: 'https://cdn.moefe.org/music/lrc/thing.lrc', 208 | }; 209 | 210 | render() { 211 | return ( 212 |
213 | 214 |
215 | ); 216 | } 217 | } 218 | ``` 219 | -------------------------------------------------------------------------------- /docs/guide/api.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | ## version 4 | 5 | - **类型**:`string` 6 | - **描述**:只读属性,返回 APlayer 的版本号 7 | - **用法**: 8 | 9 | ```js 10 | import { APlayer } from '@moefe/vue-aplayer'; 11 | 12 | console.log(APlayer.version); 13 | ``` 14 | 15 | ## media 16 | 17 | - **类型**:`APlayer.Media` 18 | - **描述**:只读的原生 [`Media`](https://www.w3schools.com/tags/ref_av_dom.asp) 对象 19 | - **用法**: 20 | 21 | ```js 22 | const { media } = this.$refs.aplayer; 23 | 24 | console.log(media.currentTime); // 获取音频当前播放时间 25 | console.log(media.duration); // 获取音频总时间 26 | console.log(media.paused); // 获取音频是否暂停 27 | ``` 28 | 29 | ## currentMusic 30 | 31 | ::: warning 警告 32 | 如果你想切换到播放列表中的其他音频请使用 [`switch`](#switch) 方法,而不要直接设置它 33 | ::: 34 | 35 | - **类型**:`APlayer.Audio` 36 | - **描述**:获取当前正在播放的音频 37 | - **用法**: 38 | 39 | ```js 40 | console.log(this.$refs.aplayer.currentMusic); 41 | ``` 42 | 43 | ## play() 44 | 45 | - **类型**:`Function` 46 | - **返回值**:`Promise` 47 | - **描述**:播放音频 48 | - **用法**: 49 | 50 | ```js 51 | this.$refs.aplayer.play(); 52 | ``` 53 | 54 | ## pause() 55 | 56 | - **类型**:`Function` 57 | - **返回值**:`void` 58 | - **描述**:暂停音频 59 | - **用法**: 60 | 61 | ```js 62 | this.$refs.aplayer.pause(); 63 | ``` 64 | 65 | ## toggle() 66 | 67 | - **类型**:`Function` 68 | - **返回值**:`void` 69 | - **描述**:切换播放和暂停 70 | - **用法**: 71 | 72 | ```js 73 | this.$refs.aplayer.toggle(); 74 | ``` 75 | 76 | ## seek() 77 | 78 | - **类型**:`Function` 79 | - **参数**: 80 | - `time` 81 | - **类型**:`number` 82 | - **描述**:时间(秒) 83 | - **返回值**:`void` 84 | - **描述**:跳到特定时间 85 | - **用法**: 86 | 87 | ```js 88 | this.$refs.aplayer.seek(100); 89 | ``` 90 | 91 | ## switch() 92 | 93 | - **类型**:`Function` 94 | - **参数**: 95 | - `audio` 96 | - **类型**:`number` | `string` 97 | - **描述**:音频索引或音频的部分名称 98 | - **返回值**:`void` 99 | - **描述**:切换到播放列表中的其他音频 100 | - **用法**: 101 | 102 | ```js 103 | this.$refs.aplayer.switch(1); // 切换到播放列表中的第二首歌 104 | this.$refs.aplayer.switch('东西'); // 切换到播放列表中歌曲名包含“东西”的第一首歌 105 | ``` 106 | 107 | ## skipBack() 108 | 109 | - **类型**:`Function` 110 | - **返回值**:`void` 111 | - **描述**:切换到上一首音频 112 | - **用法**: 113 | 114 | ```js 115 | this.$refs.aplayer.skipBack(); 116 | ``` 117 | 118 | ## skipForward() 119 | 120 | - **类型**:`Function` 121 | - **返回值**:`void` 122 | - **描述**:切换到下一首音频 123 | - **用法**: 124 | 125 | ```js 126 | this.$refs.aplayer.skipForward(); 127 | ``` 128 | 129 | ## showNotice() 130 | 131 | - **类型**:`Function` 132 | - **参数**: 133 | - `text` 134 | - **类型**:`string` 135 | - **描述**:通知文本 136 | - `time` 137 | - **类型**:`number?` 138 | - **默认值**:2000 139 | - **描述**:显示时间(毫秒) 140 | - `opacity` 141 | - **类型**:`number?` 142 | - **默认值**:0.8 143 | - **描述**:通知透明度 (0 ~ 1) 144 | - **返回值**:`Promise` 145 | - **描述**:显示通知,设置时间为 0 可以取消通知自动隐藏 146 | - **用法**: 147 | 148 | ```js 149 | this.$refs.aplayer.showNotice('喵喵喵'); 150 | ``` 151 | 152 | ## showLrc() 153 | 154 | - **类型**:`Function` 155 | - **返回值**:`void` 156 | - **描述**:显示歌词 157 | - **用法**: 158 | 159 | ```js 160 | this.$refs.aplayer.showLrc(); 161 | ``` 162 | 163 | ## hideLrc() 164 | 165 | - **类型**:`Function` 166 | - **返回值**:`void` 167 | - **描述**:隐藏歌词 168 | - **用法**: 169 | 170 | ```js 171 | this.$refs.aplayer.hideLrc(); 172 | ``` 173 | 174 | ## toggleLrc() 175 | 176 | - **类型**:`Function` 177 | - **返回值**:`void` 178 | - **描述**:显示/隐藏歌词 179 | - **用法**: 180 | 181 | ```js 182 | this.$refs.aplayer.toggleLrc(); 183 | ``` 184 | 185 | ## showList() 186 | 187 | - **类型**:`Function` 188 | - **返回值**:`void` 189 | - **描述**:显示播放列表 190 | - **用法**: 191 | 192 | ```js 193 | this.$refs.aplayer.showList(); 194 | ``` 195 | 196 | ## hideList() 197 | 198 | - **类型**:`Function` 199 | - **返回值**:`void` 200 | - **描述**:隐藏播放列表 201 | - **用法**: 202 | 203 | ```js 204 | this.$refs.aplayer.hideList(); 205 | ``` 206 | 207 | ## toggleList() 208 | 209 | - **类型**:`Function` 210 | - **返回值**:`void` 211 | - **描述**:显示/隐藏播放列表 212 | - **用法**: 213 | 214 | ```js 215 | this.$refs.aplayer.toggleList(); 216 | ``` 217 | -------------------------------------------------------------------------------- /docs/guide/cdn.md: -------------------------------------------------------------------------------- 1 | # CDN 2 | 3 | - [jsDelivr](https://cdn.jsdelivr.net/npm/@moefe/vue-aplayer/dist/VueAPlayer.umd.min.js) 4 | - [unpkg](https://unpkg.com/@moefe/vue-aplayer/dist/VueAPlayer.umd.min.js) 5 | - [bundle.run](https://bundle.run/@moefe/vue-aplayer/dist/VueAPlayer.umd.min.js) 6 | -------------------------------------------------------------------------------- /docs/guide/events.md: -------------------------------------------------------------------------------- 1 | --- 2 | pageClass: guide 3 | --- 4 | 5 | # 事件绑定 6 | 7 | ::: tip 提示 8 | 跟组件和 prop 不同,事件名不会被用作一个 JavaScript 变量名或属性名,所以就没有理由使用 camelCase 或 PascalCase 了。 9 | 并且 v-on 事件监听器在 DOM 模板中会被自动转换为全小写 (因为 HTML 是大小写不敏感的),所以在 DOM 模板中请始终使用全小写监听事件。 10 | ::: 11 | 12 | ## 原生 Media 事件 13 | 14 | | 事件名称 | 描述 | 15 | | :----------------- | :--------------------------------------------------------- | 16 | | onAbort | 在退出时触发 | 17 | | onCanplay | 当文件就绪可以开始播放时触发(缓冲已足够开始时) | 18 | | onCanplaythrough | 当媒介能够无需因缓冲而停止即可播放至结尾时触发 | 19 | | onDurationchange | 当媒介长度改变时触发 | 20 | | onEmptied | 当发生故障并且文件突然不可用时触发(比如连接意外断开时) | 21 | | onEnded | 当媒介已到达结尾时触发(可发送类似“感谢观看”之类的消息) | 22 | | onError | 当在文件加载期间发生错误时触发 | 23 | | onLoadeddata | 当媒介数据已加载时触发 | 24 | | onLoadedmetadata | 当元数据(比如分辨率和时长)被加载时触发 | 25 | | onLoadstart | 在文件开始加载且未实际加载任何数据前触发 | 26 | | onPause | 当媒介被用户或程序暂停时触发 | 27 | | onPlay | 当媒介已就绪可以开始播放时触发 | 28 | | onPlaying | 当媒介已开始播放时触发 | 29 | | onProgress | 当浏览器正在获取媒介数据时触发 | 30 | | onRatechange | 每当回放速率改变时触发(比如当用户切换到慢动作或快进模式) | 31 | | onReadystatechange | 每当就绪状态改变时触发(就绪状态监测媒介数据的状态) | 32 | | onSeeked | 当 seeking 属性设置为 false(指示定位已结束)时触发 | 33 | | onSeeking | 当 seeking 属性设置为 true(指示定位是活动的)时触发 | 34 | | onStalled | 在浏览器不论何种原因未能取回媒介数据时触发 | 35 | | onSuspend | 在媒介数据完全加载之前不论何种原因终止取回媒介数据时触发 | 36 | | onTimeupdate | 当播放位置改变时触发 | 37 | | onVolumechange | 每当音量改变时(包括将音量设置为静音)时触发 | 38 | | onWaiting | 当媒介已停止播放但打算继续播放时触发 | 39 | 40 | 📝 example.vue 41 | 42 | ```vue 43 | 71 | 72 | 97 | ``` 98 | 99 | ## 播放器事件 100 | 101 | | 事件名称 | 描述 | 102 | | :----------- | :--------------------- | 103 | | onListShow | 播放列表显示时触发 | 104 | | onListHide | 播放列表隐藏时触发 | 105 | | onListAdd | 播放列表新增音频时触发 | 106 | | onListRemove | 播放列表删除音频时触发 | 107 | | onListClear | 播放列表清空时触发 | 108 | | onListSwitch | 切换播放的音频时触发 | 109 | | onNoticeShow | 通知消息显示时触发 | 110 | | onNoticeHide | 通知消息隐藏时触发 | 111 | | onLrcShow | 歌词面板显示时触发 | 112 | | onLrcHide | 歌词面板隐藏时触发 | 113 | 114 | ::: warning 注意 115 | 由于某些选项会通过用户的操作直接修改,如果你传递了它们,会导致双向绑定的值不一致。 116 | 如果你想同步它们,可以通过监下面的事件来操作。你也可以使用 117 | [.sync 修饰符](https://cn.vuejs.org/v2/guide/components-custom-events.html#sync-%E4%BF%AE%E9%A5%B0%E7%AC%A6) 来同步。 118 | ::: 119 | 120 | | 事件名称 | 描述 | 121 | | :---------------- | :----------------------------------------------------------------------------- | 122 | | update:volume | 修改音量时触发,用于同步 [`volume`](options.html#volume) 选项 | 123 | | update:mini | 修改迷你模式时触发,用于同步 [`mini`](options.html#mini) 选项 | 124 | | update:loop | 修改循环模式时触发,用于同步 [`loop`](options.html#loop) 选项 | 125 | | update:order | 修改顺序模式时触发,用于同步 [`order`](options.html#order) 选项 | 126 | | update:listFolded | 播放列表展开/隐藏时触发,用于同步 [`listFolded`](options.html#listfolded) 选项 | 127 | 128 | 📝 example.vue 129 | 130 | ```vue 131 | 151 | 152 | 182 | ``` 183 | -------------------------------------------------------------------------------- /docs/guide/faq.md: -------------------------------------------------------------------------------- 1 | # FAQ 2 | 3 | ## 为什么播放器不能在手机上自动播放? 4 | 5 | 因为大多数移动端浏览器禁止了音频自动播放。 6 | 7 | ## 为什么在 Safari 中无法切换歌曲? 8 | 9 | 出现这个问题是因为 Vue.js 2.5+ 重写了 `nextTick`, 10 | Vue.js 优先检测是否支持原生 `setImmediate`,这是一个高版本 IE 和 Edge 才支持的特性,不支持的话再去检测是否支持原生的 `MessageChannel`,如果也不支持的话就会降级为 `setTimeout 0`。 11 | 12 | 解决方案是在引入 Vue.js 之前将 `setImmediate` 和 `MessageChannel` 设置为 `undefined` 13 | 14 | 参考资料: 15 | -------------------------------------------------------------------------------- /docs/guide/fixed.md: -------------------------------------------------------------------------------- 1 | # 吸底模式 2 | 3 | 4 | 5 | 📝 example.vue 6 | 7 | ```vue 8 | 11 | 12 | 48 | ``` 49 | -------------------------------------------------------------------------------- /docs/guide/hls.md: -------------------------------------------------------------------------------- 1 | # HLS 支持 2 | 3 | 4 | 5 | 📝 example.vue 6 | 7 | ```vue 8 | 11 | 12 | 49 | ``` 50 | -------------------------------------------------------------------------------- /docs/guide/lrc.md: -------------------------------------------------------------------------------- 1 | # 歌词 2 | 3 | ## LRC 格式 4 | 5 | ``` 6 | [ti:歌词(歌曲)的标题] 7 | [al:本歌所在的唱片集] 8 | [ar:演出者-歌手] 9 | [au:歌词作者-作曲家] 10 | [by:此LRC文件的创建者] 11 | [offset:+/- 以毫秒为单位加快或延后歌词的播放] 12 | 13 | [re:创建此LRC文件的播放器或编辑器] 14 | [ve:程序的版本] 15 | 16 | [mm:ss.ms] 我们一起学猫叫 17 | [mm:ss.ms][mm:ss:ms] 一起喵喵喵喵喵 18 | ... 19 | ``` 20 | 21 | 查看维基百科了解更多: 22 | 23 | ## LRC 文件 24 | 25 | 26 | 27 | 📝 example.vue 28 | 29 | ```vue 30 | 37 | 38 | 58 | ``` 59 | 60 | ## LRC 字符串 61 | 62 | 63 | 64 | 📝 example.vue 65 | 66 | ```vue 67 | 71 | 72 | 92 | ``` 93 | 94 | ## 禁用歌词 95 | 96 | 97 | 98 | 📝 example.vue 99 | 100 | ```vue 101 | 105 | 106 | 125 | ``` 126 | -------------------------------------------------------------------------------- /docs/guide/mini.md: -------------------------------------------------------------------------------- 1 | # 迷你模式 2 | 3 | 4 | 5 | 📝 example.vue 6 | 7 | ```vue 8 | 11 | 12 | 31 | ``` 32 | -------------------------------------------------------------------------------- /docs/guide/options.md: -------------------------------------------------------------------------------- 1 | # 选项 2 | 3 | ::: tip 提示 4 | HTML 中的特性名是大小写不敏感的,所以浏览器会把所有大写字符解释为小写字符。 5 | 这意味着当你使用 DOM 中的模板时,camelCase (驼峰命名法) 的 prop 名需要使用其等价的 kebab-case (短横线分隔命名) 命名。如果你使用字符串模板,那么这个限制就不存在了。 6 | ::: 7 | 8 | ## fixed 9 | 10 | - **类型**:`boolean?` 11 | - **默认值**:`false` 12 | - **描述**:是否开启吸底模式 13 | 14 | ## mini 15 | 16 | ::: tip 提示 17 | 如果开启吸底模式,该选项可以控制播放器展开或收起 18 | ::: 19 | 20 | - **类型**:`boolean?` 21 | - **默认值**:`false` 22 | - **描述**:是否开启迷你模式 23 | 24 | ## autoplay 25 | 26 | ::: warning 注意 27 | 由于大多数移动端浏览器禁止了音频自动播放,所以该选项在移动端无效 28 | ::: 29 | 30 | - **类型**:`boolean?` 31 | - **默认值**:`false` 32 | - **描述**:是否开启自动播放 33 | 34 | ## theme 35 | 36 | ::: tip 提示 37 | 你可以选择引入 [color-thief](https://cdn.jsdelivr.net/npm/colorthief@2.0.2/dist/) 让播放器根据封面图片自动获取主题颜色 38 | ::: 39 | 40 | - **类型**:`string?` 41 | - **默认值**:`#b7daff` 42 | - **描述**:设置播放器默认主题颜色 43 | 44 | ## loop 45 | 46 | ::: warning 注意 47 | 由于播放器会保存用户的使用习惯,所以播放器首次初始化之后该选项将失效 48 | ::: 49 | 50 | - **类型**:`APlayer.LoopMode?` 51 | - **默认值**:`all` 52 | - **描述**:设置播放器的初始循环模式 53 | 54 | ```ts 55 | declare namespace APlayer { 56 | export type LoopMode = 'all' | 'one' | 'none'; 57 | } 58 | ``` 59 | 60 | ## order 61 | 62 | ::: warning 注意 63 | 由于播放器会保存用户的使用习惯,所以播放器首次初始化之后该选项将失效 64 | ::: 65 | 66 | - **类型**:`APlayer.OrderMode?` 67 | - **默认值**:`list` 68 | - **描述**:设置播放器的初始顺序模式 69 | 70 | ```ts 71 | declare namespace APlayer { 72 | export type OrderMode = 'list' | 'random'; 73 | } 74 | ``` 75 | 76 | ## preload 77 | 78 | - **类型**:`APlayer.Preload?` 79 | - **默认值**:`auto` 80 | - **描述**:设置音频的预加载模式 81 | 82 | ```ts 83 | declare namespace APlayer { 84 | export type Preload = 'none' | 'metadata' | 'auto'; 85 | } 86 | ``` 87 | 88 | ## volume 89 | 90 | - **类型**:`number?` 91 | - **默认值**:`0.7` 92 | - **描述**:设置播放器的音量 93 | 94 | ## audio 95 | 96 | - **类型**:`APlayer.Audio | Array` 97 | - **默认值**:`undefined` 98 | - **描述**:设置要播放的音频对象或播放列表 99 | 100 | ```ts 101 | declare namespace APlayer { 102 | export type AudioType = 'auto' | 'hls' | 'normal'; 103 | export interface Audio { 104 | id?: number; // 音频 id 105 | name: string | VNode; // 音频名称 106 | artist: string | VNode; // 音频艺术家 107 | url: string; // 音频播放地址 108 | cover: string; // 音频封面 109 | lrc?: string; // lrc 歌词 110 | theme?: string; // 单曲主题色,它将覆盖全局的默认主题色 111 | type?: AudioType; // 指定音频的类型 112 | speed?: number; // 单曲播放速度 113 | } 114 | } 115 | ``` 116 | 117 | 118 | 119 | 这里与 [APlayer](https://github.com/MoePlayer/APlayer) 不同的是新增了 `id` 和 `speed` 属性。 120 | `id` 默认情况下由播放器自动生成,你也可以手动传一个 `id` 来覆盖它。 121 | `speed` 属性可以指定该音频的播放速度。 122 | 123 | ::: warning 注意 124 | `id` 是用来区分音频的唯一标识,不允许重复,如果出现重复可能会导致播放器出现异常。 125 | 默认情况下 `id` 是根据播放列表的索引生成,当播放列表发生变化时 (新增/删除) 会重新生成。 126 | 当你从播放列表中删除音频时,由于播放列表发生了变化,所以会导致当前音频的 `id` 与删除后的播放列表不匹配。 127 | 出现这种情况时,会降级根据 `url` 更新当前音频的信息,如果播放列表中每一项的 `url` 都是唯一的,那么不会有问题。 128 | 如果有重复的 `url`,你必须设置音频的 `id` 属性,以确保每一项都是唯一的,否则播放器可能出现异常。 129 | ::: 130 | 131 | ## customAudioType 132 | 133 | - **类型**:`{ [index: string]: Function }?` 134 | - **默认值**:`undefined` 135 | - **描述**:自定义音频类型 136 | 137 | 📝 [example.vue](/guide/hls.html) 138 | 139 | ```vue 140 | 143 | 144 | 181 | ``` 182 | 183 | ## mutex 184 | 185 | - **类型**:`boolean?` 186 | - **默认值**:`true` 187 | - **描述**:是否开启互斥模式 188 | 189 | 如果开启则会阻止多个播放器同时播放,当前播放器播放时暂停其他播放器 190 | 191 | ## lrcType 192 | 193 | - **类型**:`APlayer.LrcType?` 194 | - **默认值**:`0` 195 | - **描述**:设置 lrc 歌词解析模式 196 | 197 | ```ts 198 | declare namespace APlayer { 199 | export enum LrcType { 200 | file = 3, // 表示 audio.lrc 的值是 lrc 文件地址,将通过 `fetch` 获取 lrc 歌词文本 201 | html = 2, // 不支持 html 用法 202 | string = 1, // 表示 audio.lrc 的值是 lrc 格式的字符串,将直接通过它解析歌词 203 | disabled = 0, // 禁用 lrc 歌词 204 | } 205 | } 206 | ``` 207 | 208 | ## listFolded 209 | 210 | ::: warning 注意 211 | 由于播放器会保存用户的使用习惯,所以播放器首次初始化之后该选项将失效 212 | ::: 213 | 214 | - **类型**:`boolean?` 215 | - **默认值**:`false` 216 | - **描述**:是否折叠播放列表 217 | 218 | ## listMaxHeight 219 | 220 | - **类型**:`number?` 221 | - **默认值**:`250` 222 | - **描述**:设置播放列表最大高度,单位为像素 223 | 224 | ## storageName 225 | 226 | - **类型**:`string?` 227 | - **默认值**:`aplayer-setting` 228 | - **描述**:设置存储播放器设置的 `localStorage` key 229 | 230 | 这里与 [APlayer](https://github.com/MoePlayer/APlayer) 有所不同,在 `localStorage` 中保存的是对象数组 231 | 不同的实例之间互不影响,一般情况下你不需要修改此项。 232 | 233 | ```ts 234 | declare namespace APlayer { 235 | export type LoopMode = 'all' | 'one' | 'none'; 236 | export type OrderMode = 'list' | 'random'; 237 | export interface Settings { 238 | currentTime: number; // 当前音频的播放时间 239 | duration: number | null; // 当前音频的长度 240 | paused: boolean; // 当前播放器是否暂停 241 | mini: boolean; // 是否是 mini 模式 242 | lrc: boolean; // 当前歌词 243 | list: boolean; // 当前列表是否展开 244 | volume: number; // 当前播放器音量 245 | loop: LoopMode; // 当前循环模式 246 | order: OrderMode; // 当前顺序模式 247 | music: Audio | null; // 当前播放的音频对象 248 | } 249 | } 250 | ``` 251 | 252 | ```js 253 | // 你可以使用实例的 `currentSettings` 属性获取当前实例的播放器设置 254 | console.log(this.$refs.aplayer.currentSettings); 255 | ``` 256 | -------------------------------------------------------------------------------- /docs/guide/playlist.md: -------------------------------------------------------------------------------- 1 | # 播放列表 2 | 3 | 4 | 5 | 📝 example.vue 6 | 7 | ```vue 8 | 11 | 12 | 48 | ``` 49 | -------------------------------------------------------------------------------- /docs/guide/theme.md: -------------------------------------------------------------------------------- 1 | # 主题 2 | 3 | ## 单曲主题颜色 4 | 5 | 6 | 7 | 📝 example.vue 8 | 9 | ```vue 10 | 13 | 14 | 58 | ``` 59 | 60 | ## 根据封面自适应主题颜色 61 | 62 | 63 | 64 | 📝 example.vue 65 | 66 | ```vue 67 | 70 | 71 | 107 | ``` 108 | -------------------------------------------------------------------------------- /docs/options/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar: auto 3 | --- 4 | 5 | # 选项 6 | 7 | ::: tip 提示 8 | HTML 中的特性名是大小写不敏感的,所以浏览器会把所有大写字符解释为小写字符。 9 | 这意味着当你使用 DOM 中的模板时,camelCase (驼峰命名法) 的 prop 名需要使用其等价的 kebab-case (短横线分隔命名) 命名。如果你使用字符串模板,那么这个限制就不存在了。 10 | ::: 11 | 12 | ## fixed 13 | 14 | - **类型**:`boolean?` 15 | - **默认值**:`false` 16 | - **描述**:是否开启吸底模式 17 | 18 | ## mini 19 | 20 | ::: tip 提示 21 | 如果开启吸底模式,该选项可以控制播放器展开或收起 22 | ::: 23 | 24 | - **类型**:`boolean?` 25 | - **默认值**:`false` 26 | - **描述**:是否开启迷你模式 27 | 28 | ## autoplay 29 | 30 | ::: warning 注意 31 | 由于大多数移动端浏览器禁止了音频自动播放,所以该选项在移动端无效 32 | ::: 33 | 34 | - **类型**:`boolean?` 35 | - **默认值**:`false` 36 | - **描述**:是否开启自动播放 37 | 38 | ## theme 39 | 40 | ::: tip 提示 41 | 你可以选择引入 [color-thief](https://cdn.jsdelivr.net/npm/colorthief@2.0.2/dist/) 让播放器根据封面图片自动获取主题颜色 42 | ::: 43 | 44 | - **类型**:`string?` 45 | - **默认值**:`#b7daff` 46 | - **描述**:设置播放器默认主题颜色 47 | 48 | ## loop 49 | 50 | ::: warning 注意 51 | 由于播放器会保存用户的使用习惯,所以播放器首次初始化之后该选项将失效 52 | ::: 53 | 54 | - **类型**:`APlayer.LoopMode?` 55 | - **默认值**:`all` 56 | - **描述**:设置播放器的初始循环模式 57 | 58 | ```ts 59 | declare namespace APlayer { 60 | export type LoopMode = 'all' | 'one' | 'none'; 61 | } 62 | ``` 63 | 64 | ## order 65 | 66 | ::: warning 注意 67 | 由于播放器会保存用户的使用习惯,所以播放器首次初始化之后该选项将失效 68 | ::: 69 | 70 | - **类型**:`APlayer.OrderMode?` 71 | - **默认值**:`list` 72 | - **描述**:设置播放器的初始顺序模式 73 | 74 | ```ts 75 | declare namespace APlayer { 76 | export type OrderMode = 'list' | 'random'; 77 | } 78 | ``` 79 | 80 | ## preload 81 | 82 | - **类型**:`APlayer.Preload?` 83 | - **默认值**:`auto` 84 | - **描述**:设置音频的预加载模式 85 | 86 | ```ts 87 | declare namespace APlayer { 88 | export type Preload = 'none' | 'metadata' | 'auto'; 89 | } 90 | ``` 91 | 92 | ## volume 93 | 94 | - **类型**:`number?` 95 | - **默认值**:`0.7` 96 | - **描述**:设置播放器的音量 97 | 98 | ## audio 99 | 100 | - **类型**:`APlayer.Audio | Array` 101 | - **默认值**:`undefined` 102 | - **描述**:设置要播放的音频对象或播放列表 103 | 104 | ```ts 105 | declare namespace APlayer { 106 | export type AudioType = 'auto' | 'hls' | 'normal'; 107 | export interface Audio { 108 | id?: number; // 音频 id 109 | name: string | VNode; // 音频名称 110 | artist: string | VNode; // 音频艺术家 111 | url: string; // 音频播放地址 112 | cover: string; // 音频封面 113 | lrc?: string; // lrc 歌词 114 | theme?: string; // 单曲主题色,它将覆盖全局的默认主题色 115 | type?: AudioType; // 指定音频的类型 116 | speed?: number; // 单曲播放速度 117 | } 118 | } 119 | ``` 120 | 121 | 122 | 123 | 这里与 [APlayer](https://github.com/MoePlayer/APlayer) 不同的是新增了 `id` 和 `speed` 属性。 124 | `id` 默认情况下由播放器自动生成,你也可以手动传一个 `id` 来覆盖它。 125 | `speed` 属性可以指定该音频的播放速度。 126 | 127 | ::: warning 注意 128 | `id` 是用来区分音频的唯一标识,不允许重复,如果出现重复可能会导致播放器出现异常。 129 | 默认情况下 `id` 是根据播放列表的索引生成,当播放列表发生变化时 (新增/删除) 会重新生成。 130 | 当你从播放列表中删除音频时,由于播放列表发生了变化,所以会导致当前音频的 `id` 与删除后的播放列表不匹配。 131 | 出现这种情况时,会降级根据 `url` 更新当前音频的信息,如果播放列表中每一项的 `url` 都是唯一的,那么不会有问题。 132 | 如果有重复的 `url`,你必须设置音频的 `id` 属性,以确保每一项都是唯一的,否则播放器可能出现异常。 133 | ::: 134 | 135 | ## customAudioType 136 | 137 | - **类型**:`{ [index: string]: Function }?` 138 | - **默认值**:`undefined` 139 | - **描述**:自定义音频类型 140 | 141 | 📝 [example.vue](/guide/hls.html) 142 | 143 | ```vue 144 | 147 | 148 | 185 | ``` 186 | 187 | ## mutex 188 | 189 | - **类型**:`boolean?` 190 | - **默认值**:`true` 191 | - **描述**:是否开启互斥模式 192 | 193 | 如果开启则会阻止多个播放器同时播放,当前播放器播放时暂停其他播放器 194 | 195 | ## lrcType 196 | 197 | - **类型**:`APlayer.LrcType?` 198 | - **默认值**:`0` 199 | - **描述**:设置 lrc 歌词解析模式 200 | 201 | ```ts 202 | declare namespace APlayer { 203 | export enum LrcType { 204 | file = 3, // 表示 audio.lrc 的值是 lrc 文件地址,将通过 `fetch` 获取 lrc 歌词文本 205 | html = 2, // 不支持 html 用法 206 | string = 1, // 表示 audio.lrc 的值是 lrc 格式的字符串,将直接通过它解析歌词 207 | disabled = 0, // 禁用 lrc 歌词 208 | } 209 | } 210 | ``` 211 | 212 | ## listFolded 213 | 214 | ::: warning 注意 215 | 由于播放器会保存用户的使用习惯,所以播放器首次初始化之后该选项将失效 216 | ::: 217 | 218 | - **类型**:`boolean?` 219 | - **默认值**:`false` 220 | - **描述**:是否折叠播放列表 221 | 222 | ## listMaxHeight 223 | 224 | - **类型**:`number?` 225 | - **默认值**:`250` 226 | - **描述**:设置播放列表最大高度,单位为像素 227 | 228 | ## storageName 229 | 230 | - **类型**:`string?` 231 | - **默认值**:`aplayer-setting` 232 | - **描述**:设置存储播放器设置的 `localStorage` key 233 | 234 | 这里与 [APlayer](https://github.com/MoePlayer/APlayer) 有所不同,在 `localStorage` 中保存的是对象数组 235 | 不同的实例之间互不影响,一般情况下你不需要修改此项。 236 | 237 | ```ts 238 | declare namespace APlayer { 239 | export type LoopMode = 'all' | 'one' | 'none'; 240 | export type OrderMode = 'list' | 'random'; 241 | export interface Settings { 242 | currentTime: number; // 当前音频的播放时间 243 | duration: number | null; // 当前音频的长度 244 | paused: boolean; // 当前播放器是否暂停 245 | mini: boolean; // 是否是 mini 模式 246 | lrc: boolean; // 当前歌词 247 | list: boolean; // 当前列表是否展开 248 | volume: number; // 当前播放器音量 249 | loop: LoopMode; // 当前循环模式 250 | order: OrderMode; // 当前顺序模式 251 | music: Audio | null; // 当前播放的音频对象 252 | } 253 | } 254 | ``` 255 | 256 | ```js 257 | // 你可以使用实例的 `currentSettings` 属性获取当前实例的播放器设置 258 | console.log(this.$refs.aplayer.currentSettings); 259 | ``` 260 | -------------------------------------------------------------------------------- /example/App.scss: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | body { 6 | margin: 0; 7 | font: 14px/1.4 -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 8 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 9 | } 10 | 11 | html, 12 | body, 13 | #app { 14 | height: 100%; 15 | } 16 | 17 | h1 { 18 | margin: 0; 19 | margin-top: -50px; 20 | font-weight: normal; 21 | font-size: 40px; 22 | letter-spacing: 1px; 23 | text-transform: uppercase; 24 | } 25 | 26 | h3 { 27 | margin-top: 20px; 28 | color: #999; 29 | font-weight: normal; 30 | letter-spacing: 1px; 31 | } 32 | 33 | .landing { 34 | position: relative; 35 | z-index: 1; 36 | padding: 10px; 37 | display: flex; 38 | align-items: center; 39 | justify-content: center; 40 | flex-direction: column; 41 | height: 100%; 42 | user-select: none; 43 | > * { 44 | opacity: 0.8; 45 | } 46 | } 47 | 48 | .landing-button { 49 | border: 1px solid #ccc; 50 | border-radius: 33px; 51 | padding: 10px 30px; 52 | background-color: white; 53 | display: inline-block; 54 | margin-right: 20px; 55 | color: #333; 56 | text-decoration: none; 57 | transition: 0.3s; 58 | } 59 | 60 | .landing-button:hover { 61 | border-color: #42b983; 62 | color: #42b983; 63 | text-decoration: none; 64 | } 65 | 66 | .aplayer-wrap { 67 | width: 600px; 68 | max-width: 100%; 69 | margin: 20px 0 40px; 70 | } 71 | -------------------------------------------------------------------------------- /example/App.tsx: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Comopnent from 'vue-class-component'; 3 | import APlayerPlugin, { APlayer } from '@moefe/vue-aplayer'; 4 | import { sleep } from 'utils'; 5 | import './App.scss'; 6 | 7 | Vue.use(APlayerPlugin, { 8 | productionTip: process.env.NODE_ENV !== 'development', 9 | }); 10 | 11 | @Comopnent 12 | export default class App extends Vue { 13 | private readonly aplayer0: APlayer.Options = { 14 | fixed: true, 15 | lrcType: 3, 16 | listMaxHeight: 100, 17 | preload: 'auto', 18 | audio: [], 19 | }; 20 | 21 | private readonly aplayer1: APlayer.Options = { 22 | lrcType: 3, 23 | listMaxHeight: 98, 24 | preload: 'auto', 25 | audio: [], 26 | }; 27 | 28 | async created() { 29 | const data: Array = await fetch('/music/data.json').then( 30 | res => res.json(), 31 | ); 32 | const isSafari = /apple/i.test(navigator.vendor); 33 | if (isSafari) { 34 | for (let i = 0; i < data.length; i++) { 35 | data[i].speed = 1; 36 | } 37 | } 38 | await sleep(1500); 39 | this.aplayer1.audio = data.splice(0, 4); 40 | await sleep(1500); 41 | this.aplayer0.audio = data; 42 | } 43 | 44 | render() { 45 | const { aplayer0, aplayer1 } = this; 46 | 47 | return ( 48 |
49 | 50 |
51 |

Vue-Aplayer

52 |

🍰 A beautiful HTML5 music player for Vue.js.

53 |
54 | 55 |
56 |
57 | 62 | GitHub 63 | 64 | {/* eslint-disable-next-line no-script-url */} 65 | 66 | Docs 67 | 68 |
69 |
70 |
71 | ); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /example/main.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | 3 | Vue.config.devtools = true; 4 | Vue.config.productionTip = false; 5 | 6 | // hotfix 7 | if (localStorage.getItem('GIT_HASH') !== GIT_HASH) { 8 | localStorage.clear(); 9 | localStorage.setItem('GIT_HASH', GIT_HASH); 10 | } 11 | 12 | new Vue({ 13 | // eslint-disable-next-line global-require 14 | render: h => h(require('./App').default), 15 | }).$mount('#app'); 16 | -------------------------------------------------------------------------------- /example/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoePlayer/vue-aplayer/dd10c503001179dec4fed6f6644e50768a938e54/example/public/favicon.ico -------------------------------------------------------------------------------- /example/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | vue-aplayer 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /example/public/music/data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "东西(Cover:林俊呈)", 4 | "artist": "纳豆", 5 | "url": "https://cdn.moefe.org/music/mp3/thing.mp3", 6 | "cover": "https://p1.music.126.net/5zs7IvmLv7KahY3BFzUmrg==/109951163635241613.jpg?param=300y300", 7 | "lrc": "https://cdn.moefe.org/music/lrc/thing.lrc" 8 | }, 9 | { 10 | "name": "响喜乱舞(Cover:MARiA)", 11 | "artist": "泠鸢yousa", 12 | "url": "https://cdn.moefe.org/music/mp3/kyoukiranbu.mp3", 13 | "cover": "https://p1.music.126.net/AUGVPQ_rVrngDH9ocQrn3Q==/109951163613037822.jpg?param=300y300", 14 | "lrc": "https://cdn.moefe.org/music/lrc/kyoukiranbu.lrc" 15 | }, 16 | { 17 | "name": "啵唧", 18 | "artist": "Hanser", 19 | "url": "https://cdn.moefe.org/music/mp3/kiss.mp3", 20 | "cover": "https://p1.music.126.net/K0-IPcIQ9QFvA0jXTBqoWQ==/109951163636756693.jpg?param=300y300", 21 | "lrc": "https://cdn.moefe.org/music/lrc/kiss.lrc" 22 | }, 23 | { 24 | "name": "伴宅日记", 25 | "artist": "Hanser", 26 | "url": "https://cdn.moefe.org/music/mp3/diary.mp3", 27 | "cover": "https://p1.music.126.net/oW7TW0VjK5PoNjhzdPm1lw==/109951163626390573.jpg?param=300y300", 28 | "lrc": "https://cdn.moefe.org/music/lrc/diary.lrc" 29 | }, 30 | { 31 | "name": "童遊", 32 | "artist": "めらみぽっぷ", 33 | "url": "https://cdn.moefe.org/music/mp3/innocenttreasures.mp3", 34 | "cover": "https://p1.music.126.net/tkazmUdvztqtaL-XDN4D2A==/5947258394962501.jpg?param=300y300", 35 | "lrc": "https://cdn.moefe.org/music/lrc/innocenttreasures.lrc" 36 | }, 37 | { 38 | "name": "The Party We Have Never Seen", 39 | "artist": "Nana Takahashi", 40 | "url": "https://cdn.moefe.org/music/mp3/thepartywehaveneverseen.mp3", 41 | "cover": "https://p1.music.126.net/IwclpJu4gaqhSZrKunEFWg==/3297435379408525.jpg?param=300y300", 42 | "lrc": "https://cdn.moefe.org/music/lrc/thepartywehaveneverseen.lrc" 43 | }, 44 | { 45 | "name": "Let It Go.m3u8", 46 | "artist": "Idina Menzel", 47 | "url": "https://cdn.moefe.org/music/hls/frozen.m3u8", 48 | "cover": "https://p1.music.126.net/n72JJkPg2-ENxhB-DsZ2AA==/109951163115400390.jpg?param=300y300", 49 | "lrc": "https://cdn.moefe.org/music/lrc/frozen.lrc" 50 | }, 51 | { 52 | "name": "Star Sky", 53 | "artist": "Two Steps From Hell", 54 | "url": "https://cdn.moefe.org/music/mp3/starsky.mp3", 55 | "cover": "https://p2.music.126.net/nJROWeZiEp1TUv27amRguQ==/18195817928618786.jpg?param=300y300", 56 | "lrc": "https://cdn.moefe.org/music/lrc/starsky.lrc" 57 | } 58 | ] 59 | -------------------------------------------------------------------------------- /logo/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoePlayer/vue-aplayer/dd10c503001179dec4fed6f6644e50768a938e54/logo/Icon.png -------------------------------------------------------------------------------- /logo/Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoePlayer/vue-aplayer/dd10c503001179dec4fed6f6644e50768a938e54/logo/Logo.png -------------------------------------------------------------------------------- /logo/design.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoePlayer/vue-aplayer/dd10c503001179dec4fed6f6644e50768a938e54/logo/design.png -------------------------------------------------------------------------------- /logo/icon-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoePlayer/vue-aplayer/dd10c503001179dec4fed6f6644e50768a938e54/logo/icon-blue.png -------------------------------------------------------------------------------- /logo/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoePlayer/vue-aplayer/dd10c503001179dec4fed6f6644e50768a938e54/logo/icon.png -------------------------------------------------------------------------------- /logo/logo-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoePlayer/vue-aplayer/dd10c503001179dec4fed6f6644e50768a938e54/logo/logo-blue.png -------------------------------------------------------------------------------- /logo/logo-type-horizontal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoePlayer/vue-aplayer/dd10c503001179dec4fed6f6644e50768a938e54/logo/logo-type-horizontal.png -------------------------------------------------------------------------------- /logo/logo-type-vertical.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoePlayer/vue-aplayer/dd10c503001179dec4fed6f6644e50768a938e54/logo/logo-type-vertical.png -------------------------------------------------------------------------------- /logo/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoePlayer/vue-aplayer/dd10c503001179dec4fed6f6644e50768a938e54/logo/logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@moefe/vue-aplayer", 3 | "description": "A beautiful HTML5 music player for Vue.js", 4 | "version": "2.0.0-beta.5", 5 | "author": "u3u ", 6 | "license": "MIT", 7 | "homepage": "https://github.com/MoePlayer/vue-aplayer#readme", 8 | "main": "dist/VueAPlayer.common.js", 9 | "unpkg": "dist/VueAPlayer.umd.min.js", 10 | "jsdelivr": "dist/VueAPlayer.umd.min.js", 11 | "types": "types/index.d.ts", 12 | "files": [ 13 | "dist", 14 | "types" 15 | ], 16 | "repository": { 17 | "url": "git+https://github.com/MoePlayer/vue-aplayer.git", 18 | "type": "git" 19 | }, 20 | "bugs": { 21 | "url": "https://github.com/MoePlayer/vue-aplayer/issues" 22 | }, 23 | "keywords": [ 24 | "player", 25 | "aplayer", 26 | "vue-aplayer", 27 | "music", 28 | "html5", 29 | "audio", 30 | "media" 31 | ], 32 | "scripts": { 33 | "prepublishOnly": "yarn build", 34 | "serve": "vue-cli-service serve --open", 35 | "build": "vue-cli-service build --report --target lib --name VueAPlayer packages/@moefe/vue-aplayer/index.ts", 36 | "build:example": "vue-cli-service build --dest demo", 37 | "docs:link": "yarn link && yarn link \"@moefe/vue-aplayer\"", 38 | "docs:dev": "yarn docs:link && vuepress dev docs --debug --port 3000", 39 | "docs:build": "yarn docs:prebuild && vuepress build docs", 40 | "docs:prebuild": "yarn build && yarn docs:link", 41 | "lint": "vue-cli-service lint packages", 42 | "lint:prettier": "prettier-eslint-check \"**/*.{js,jsx,json,ts,tsx,scss,vue,md}\"", 43 | "format": "prettier-eslint \"**/*.{js,jsx,json,ts,tsx,scss,vue,md}\" --write", 44 | "test:types": "dtslint types", 45 | "contributors": "all-contributors" 46 | }, 47 | "peerDependencies": { 48 | "vue": "^2.2.0" 49 | }, 50 | "dependencies": { 51 | "vue": "^2.5.17" 52 | }, 53 | "devDependencies": { 54 | "@types/classnames": "^2.2.6", 55 | "@types/hls.js": "^0.10.2", 56 | "@types/node": "^10.12.12", 57 | "@vue/cli-plugin-babel": "^3.2.0", 58 | "@vue/cli-plugin-eslint": "^3.2.1", 59 | "@vue/cli-plugin-typescript": "^3.2.0", 60 | "@vue/cli-service": "^3.2.0", 61 | "@vue/eslint-config-airbnb": "^4.0.0", 62 | "@vue/eslint-config-typescript": "^3.2.0", 63 | "@vuepress/plugin-pwa": "^1.0.0-alpha.0", 64 | "all-contributors-cli": "^5.4.1", 65 | "classnames": "^2.2.6", 66 | "dayjs": "^1.7.7", 67 | "dtslint": "^0.3.0", 68 | "git-revision-webpack-plugin": "^3.0.3", 69 | "lint-staged": "^8.1.0", 70 | "node-sass": "^4.11.0", 71 | "prettier": "^1.15.3", 72 | "prettier-eslint-check": "^1.0.2", 73 | "prettier-eslint-cli": "^4.7.1", 74 | "sass-loader": "^7.1.0", 75 | "typescript": "^3.2.2", 76 | "vue-class-component": "^6.3.2", 77 | "vue-property-decorator": "^7.2.0", 78 | "vue-svg-loader": "^0.11.0", 79 | "vue-template-compiler": "^2.5.17", 80 | "vue-tsx-support": "^2.2.1", 81 | "vuepress": "^1.0.0-alpha.27" 82 | }, 83 | "browserslist": [ 84 | "> 1%", 85 | "last 2 versions", 86 | "not ie <= 8" 87 | ], 88 | "gitHooks": { 89 | "pre-commit": "lint-staged" 90 | }, 91 | "lint-staged": { 92 | "*.{js,jsx,ts,tsx,vue}": "vue-cli-service lint", 93 | "*.{js,jsx,json,ts,tsx,scss,vue,md}": "prettier-eslint-check" 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /packages/@moefe/vue-aplayer/assets/style/aplayer.scss: -------------------------------------------------------------------------------- 1 | $aplayer-height: 66px; 2 | $lrc-height: 30px; 3 | $aplayer-height-lrc: $aplayer-height + $lrc-height - 6; 4 | 5 | .aplayer { 6 | background: #fff; 7 | font-family: Arial, Helvetica, sans-serif; 8 | margin: 5px; 9 | box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.07), 0 1px 5px 0 rgba(0, 0, 0, 0.1); 10 | border-radius: 2px; 11 | overflow: hidden; 12 | user-select: none; 13 | line-height: initial; 14 | position: relative; 15 | 16 | * { 17 | box-sizing: content-box; 18 | } 19 | 20 | svg { 21 | width: 100%; 22 | height: 100%; 23 | 24 | path, 25 | circle { 26 | fill: #fff; 27 | } 28 | } 29 | 30 | &.aplayer-withlist { 31 | .aplayer-info { 32 | border-bottom: 1px solid #e9e9e9; 33 | } 34 | .aplayer-list { 35 | display: block; 36 | width: 100%; // don't remove this property 37 | } 38 | .aplayer-info 39 | .aplayer-controller 40 | .aplayer-time 41 | .aplayer-icon.aplayer-icon-menu { 42 | display: inline; 43 | } 44 | .aplayer-icon-order { 45 | display: inline; 46 | } 47 | } 48 | 49 | &.aplayer-withlrc { 50 | .aplayer-pic { 51 | height: $aplayer-height-lrc; 52 | width: $aplayer-height-lrc; 53 | } 54 | .aplayer-info { 55 | margin-left: $aplayer-height-lrc; 56 | height: $aplayer-height-lrc; 57 | padding: 10px 7px 0 7px; 58 | } 59 | .aplayer-lrc { 60 | display: block; 61 | } 62 | } 63 | 64 | &.aplayer-narrow { 65 | width: $aplayer-height; 66 | 67 | .aplayer-info { 68 | display: none; 69 | } 70 | .aplayer-list { 71 | display: none; 72 | } 73 | .aplayer-pic, 74 | .aplayer-body { 75 | height: $aplayer-height; 76 | width: $aplayer-height; 77 | } 78 | } 79 | 80 | &.aplayer-fixed { 81 | position: fixed; 82 | bottom: 0; 83 | left: 0; 84 | right: 0; 85 | margin: 0; 86 | z-index: 99; 87 | overflow: visible; 88 | max-width: 400px; 89 | box-shadow: none; 90 | 91 | .aplayer-list { 92 | margin-bottom: 65px; 93 | border: 1px solid #eee; 94 | border-bottom: none; 95 | box-sizing: border-box; 96 | } 97 | 98 | .aplayer-body { 99 | position: fixed; 100 | bottom: 0; 101 | left: 0; 102 | right: 0; 103 | margin: 0; 104 | z-index: 99; 105 | background: #fff; 106 | padding-right: 18px; 107 | transition: width 0.3s ease; 108 | max-width: 400px; 109 | width: calc(100% - 18px); 110 | } 111 | 112 | .aplayer-lrc { 113 | display: block; 114 | position: fixed; 115 | bottom: 10px; 116 | left: 0; 117 | right: 0; 118 | margin: 0; 119 | z-index: 98; 120 | pointer-events: none; 121 | text-shadow: -1px -1px 0 #fff; 122 | 123 | &:before, 124 | &:after { 125 | display: none; 126 | } 127 | } 128 | 129 | .aplayer-info { 130 | transform: scaleX(1); 131 | transform-origin: 0 0; 132 | transition: all 0.3s ease; 133 | border-bottom: none; 134 | border-top: 1px solid #e9e9e9; 135 | 136 | .aplayer-music { 137 | width: calc(100% - 105px); 138 | } 139 | } 140 | 141 | .aplayer-miniswitcher { 142 | display: block; 143 | } 144 | 145 | &.aplayer-narrow { 146 | .aplayer-info { 147 | display: block; 148 | transform: scaleX(0); 149 | } 150 | .aplayer-body { 151 | width: $aplayer-height !important; 152 | } 153 | 154 | .aplayer-miniswitcher .aplayer-icon { 155 | transform: rotateY(0); 156 | } 157 | } 158 | 159 | .aplayer-icon-back, 160 | .aplayer-icon-play, 161 | .aplayer-icon-forward, 162 | .aplayer-icon-lrc { 163 | display: inline-block; 164 | } 165 | 166 | .aplayer-icon-back, 167 | .aplayer-icon-play, 168 | .aplayer-icon-forward, 169 | .aplayer-icon-menu { 170 | position: absolute; 171 | bottom: 27px; 172 | width: 20px; 173 | height: 20px; 174 | } 175 | 176 | .aplayer-icon-back { 177 | right: 75px; 178 | } 179 | 180 | .aplayer-icon-play { 181 | right: 50px; 182 | } 183 | 184 | .aplayer-icon-forward { 185 | right: 25px; 186 | } 187 | 188 | .aplayer-icon-menu { 189 | right: 0; 190 | } 191 | } 192 | 193 | &.aplayer-mobile { 194 | .aplayer-icon-volume-up, 195 | .aplayer-icon-volume-down { 196 | display: none; 197 | } 198 | } 199 | 200 | &.aplayer-arrow { 201 | .aplayer-icon-order, 202 | .aplayer-icon-loop { 203 | display: none; 204 | } 205 | } 206 | 207 | &.aplayer-loading { 208 | .aplayer-info .aplayer-controller .aplayer-loading-icon { 209 | display: block; 210 | } 211 | 212 | .aplayer-info 213 | .aplayer-controller 214 | .aplayer-bar-wrap 215 | .aplayer-bar 216 | .aplayer-played 217 | .aplayer-thumb { 218 | transform: scale(1); 219 | } 220 | } 221 | 222 | .aplayer-body { 223 | position: relative; 224 | } 225 | 226 | .aplayer-icon { 227 | width: 15px; 228 | height: 15px; 229 | border: none; 230 | background-color: transparent; 231 | outline: none; 232 | cursor: pointer; 233 | opacity: 0.8; 234 | vertical-align: middle; 235 | padding: 0; 236 | font-size: 12px; 237 | margin: 0; 238 | display: inline-block; 239 | 240 | path { 241 | transition: all 0.2s ease-in-out; 242 | } 243 | } 244 | 245 | .aplayer-icon-order, 246 | .aplayer-icon-back, 247 | .aplayer-icon-play, 248 | .aplayer-icon-forward, 249 | .aplayer-icon-lrc { 250 | display: none; 251 | } 252 | 253 | .aplayer-icon-lrc-inactivity { 254 | svg { 255 | opacity: 0.4; 256 | } 257 | } 258 | 259 | .aplayer-icon-forward { 260 | transform: rotate(180deg); 261 | } 262 | 263 | .aplayer-lrc-content { 264 | display: none; 265 | } 266 | 267 | .aplayer-pic { 268 | position: relative; 269 | float: left; 270 | height: $aplayer-height; 271 | width: $aplayer-height; 272 | background-size: cover; 273 | background-position: center; 274 | transition: all 0.3s ease; 275 | cursor: pointer; 276 | 277 | &:hover .aplayer-button { 278 | opacity: 1; 279 | } 280 | 281 | .aplayer-button { 282 | position: absolute; 283 | border-radius: 50%; 284 | opacity: 0.8; 285 | text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2); 286 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2); 287 | background: rgba(0, 0, 0, 0.2); 288 | transition: all 0.1s ease; 289 | 290 | path { 291 | fill: #fff; 292 | } 293 | } 294 | 295 | .aplayer-hide { 296 | display: none; 297 | } 298 | 299 | .aplayer-play { 300 | width: 26px; 301 | height: 26px; 302 | border: 2px solid #fff; 303 | bottom: 50%; 304 | right: 50%; 305 | margin: 0 -15px -15px 0; 306 | svg { 307 | position: absolute; 308 | top: 3px; 309 | left: 4px; 310 | height: 20px; 311 | width: 20px; 312 | } 313 | } 314 | 315 | .aplayer-pause { 316 | width: 16px; 317 | height: 16px; 318 | border: 2px solid #fff; 319 | bottom: 4px; 320 | right: 4px; 321 | svg { 322 | position: absolute; 323 | top: 2px; 324 | left: 2px; 325 | height: 12px; 326 | width: 12px; 327 | } 328 | } 329 | } 330 | 331 | .aplayer-info { 332 | margin-left: $aplayer-height; 333 | padding: 14px 7px 0 10px; 334 | height: $aplayer-height; 335 | box-sizing: border-box; 336 | 337 | .aplayer-music { 338 | overflow: hidden; 339 | white-space: nowrap; 340 | text-overflow: ellipsis; 341 | margin: 0 0 13px 5px; 342 | user-select: text; 343 | cursor: default; 344 | padding-bottom: 2px; 345 | height: 20px; 346 | 347 | .aplayer-title { 348 | font-size: 14px; 349 | } 350 | 351 | .aplayer-author { 352 | font-size: 12px; 353 | color: #666; 354 | } 355 | } 356 | 357 | .aplayer-controller { 358 | position: relative; 359 | display: flex; 360 | 361 | .aplayer-bar-wrap { 362 | margin: 0 0 0 5px; 363 | padding: 4px 0; 364 | cursor: pointer !important; 365 | flex: 1; 366 | 367 | &:hover { 368 | .aplayer-bar .aplayer-played .aplayer-thumb { 369 | transform: scale(1); 370 | } 371 | } 372 | 373 | .aplayer-bar { 374 | position: relative; 375 | height: 2px; 376 | width: 100%; 377 | background: #cdcdcd; 378 | 379 | .aplayer-loaded { 380 | position: absolute; 381 | left: 0; 382 | top: 0; 383 | bottom: 0; 384 | background: #aaa; 385 | height: 2px; 386 | transition: all 0.5s ease; 387 | } 388 | 389 | .aplayer-played { 390 | position: absolute; 391 | left: 0; 392 | top: 0; 393 | bottom: 0; 394 | height: 2px; 395 | transition: background-color 0.3s ease; 396 | 397 | .aplayer-thumb { 398 | position: absolute; 399 | top: 0; 400 | right: 5px; 401 | margin-top: -4px; 402 | margin-right: -10px; 403 | height: 10px; 404 | width: 10px; 405 | border-radius: 50%; 406 | cursor: pointer; 407 | transition: all 0.3s ease-in-out; 408 | transform: scale(0); 409 | } 410 | } 411 | } 412 | } 413 | 414 | .aplayer-time { 415 | position: relative; 416 | right: 0; 417 | bottom: 4px; 418 | height: 17px; 419 | color: #999; 420 | font-size: 11px; 421 | padding-left: 7px; 422 | 423 | .aplayer-time-inner { 424 | vertical-align: middle; 425 | } 426 | 427 | .aplayer-icon { 428 | cursor: pointer; 429 | transition: all 0.2s ease; 430 | 431 | path { 432 | fill: #666; 433 | } 434 | 435 | &.aplayer-icon-loop { 436 | margin-right: 2px; 437 | } 438 | 439 | &:hover { 440 | path { 441 | fill: #000; 442 | } 443 | } 444 | 445 | &.aplayer-icon-menu { 446 | display: none; 447 | } 448 | } 449 | 450 | &.aplayer-time-narrow { 451 | .aplayer-icon-mode { 452 | display: none; 453 | } 454 | 455 | .aplayer-icon-menu { 456 | display: none; 457 | } 458 | } 459 | } 460 | 461 | .aplayer-volume-wrap { 462 | position: relative; 463 | display: inline-block; 464 | margin-left: 3px; 465 | cursor: pointer !important; 466 | 467 | &:hover .aplayer-volume-bar-wrap { 468 | height: 40px; 469 | } 470 | 471 | .aplayer-volume-bar-wrap { 472 | position: absolute; 473 | bottom: 15px; 474 | right: -3px; 475 | width: 25px; 476 | height: 0; 477 | z-index: 99; 478 | overflow: hidden; 479 | transition: all 0.2s ease-in-out; 480 | 481 | &.aplayer-volume-bar-wrap-active { 482 | height: 40px; 483 | } 484 | 485 | .aplayer-volume-bar { 486 | position: absolute; 487 | bottom: 0; 488 | right: 10px; 489 | width: 5px; 490 | height: 35px; 491 | background: #aaa; 492 | border-radius: 2.5px; 493 | overflow: hidden; 494 | 495 | .aplayer-volume { 496 | position: absolute; 497 | bottom: 0; 498 | right: 0; 499 | width: 5px; 500 | transition: all 0.1s ease; 501 | } 502 | } 503 | } 504 | } 505 | 506 | .aplayer-loading-icon { 507 | display: none; 508 | 509 | svg { 510 | position: absolute; 511 | animation: rotate 1s linear infinite; 512 | } 513 | } 514 | } 515 | } 516 | 517 | .aplayer-lrc { 518 | display: none; 519 | position: relative; 520 | height: $lrc-height; 521 | text-align: center; 522 | overflow: hidden; 523 | margin: -10px 0 7px; 524 | 525 | &:before { 526 | position: absolute; 527 | top: 0; 528 | z-index: 1; 529 | display: block; 530 | overflow: hidden; 531 | width: 100%; 532 | height: 10%; 533 | content: ' '; 534 | background: -moz-linear-gradient( 535 | top, 536 | rgba(255, 255, 255, 1) 0%, 537 | rgba(255, 255, 255, 0) 100% 538 | ); 539 | background: -webkit-linear-gradient( 540 | top, 541 | rgba(255, 255, 255, 1) 0%, 542 | rgba(255, 255, 255, 0) 100% 543 | ); 544 | background: linear-gradient( 545 | to bottom, 546 | rgba(255, 255, 255, 1) 0%, 547 | rgba(255, 255, 255, 0) 100% 548 | ); 549 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#00ffffff', GradientType=0); 550 | } 551 | 552 | &:after { 553 | position: absolute; 554 | bottom: 0; 555 | z-index: 1; 556 | display: block; 557 | overflow: hidden; 558 | width: 100%; 559 | height: 33%; 560 | content: ' '; 561 | background: -moz-linear-gradient( 562 | top, 563 | rgba(255, 255, 255, 0) 0%, 564 | rgba(255, 255, 255, 0.8) 100% 565 | ); 566 | background: -webkit-linear-gradient( 567 | top, 568 | rgba(255, 255, 255, 0) 0%, 569 | rgba(255, 255, 255, 0.8) 100% 570 | ); 571 | background: linear-gradient( 572 | to bottom, 573 | rgba(255, 255, 255, 0) 0%, 574 | rgba(255, 255, 255, 0.8) 100% 575 | ); 576 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00ffffff', endColorstr='#ccffffff', GradientType=0); 577 | } 578 | 579 | p { 580 | font-size: 12px; 581 | color: #666; 582 | line-height: 16px !important; 583 | height: 16px !important; 584 | padding: 0 !important; 585 | margin: 0 !important; 586 | transition-property: font-size, color, opacity; 587 | transition-timing-function: ease-out; 588 | transition-duration: 0.5s; 589 | opacity: 0.4; 590 | overflow: hidden; 591 | 592 | &.aplayer-lrc-current { 593 | opacity: 1; 594 | overflow: visible; 595 | height: initial !important; 596 | min-height: 16px; 597 | } 598 | } 599 | 600 | &.aplayer-lrc-hide { 601 | display: none; 602 | } 603 | 604 | .aplayer-lrc-contents { 605 | width: 100%; 606 | transition: all 0.5s ease-out; 607 | user-select: text; 608 | cursor: default; 609 | } 610 | } 611 | 612 | .aplayer-list { 613 | overflow: auto; 614 | transition: all 0.5s ease; 615 | will-change: height; 616 | display: none; 617 | overflow: hidden; 618 | list-style-type: none; 619 | margin: 0; 620 | padding: 0; 621 | overflow-y: auto; 622 | 623 | &::-webkit-scrollbar { 624 | width: 5px; 625 | } 626 | 627 | &::-webkit-scrollbar-thumb { 628 | border-radius: 3px; 629 | background-color: #eee; 630 | } 631 | 632 | &::-webkit-scrollbar-thumb:hover { 633 | background-color: #ccc; 634 | } 635 | 636 | li { 637 | position: relative; 638 | height: 32px; 639 | line-height: 32px; 640 | padding: 0 15px; 641 | font-size: 12px; 642 | border-top: 1px solid #e9e9e9; 643 | cursor: pointer; 644 | transition: all 0.2s ease; 645 | overflow: hidden; 646 | margin: 0; 647 | 648 | &:first-child { 649 | border-top: none; 650 | } 651 | 652 | &:hover { 653 | background: #efefef; 654 | } 655 | 656 | &.aplayer-list-light { 657 | background: #e9e9e9; 658 | 659 | .aplayer-list-cur { 660 | display: inline-block; 661 | } 662 | } 663 | 664 | .aplayer-list-cur { 665 | display: none; 666 | width: 3px; 667 | height: 22px; 668 | position: absolute; 669 | left: 0; 670 | top: 5px; 671 | transition: background-color 0.3s ease; 672 | cursor: pointer; 673 | } 674 | .aplayer-list-index { 675 | color: #666; 676 | margin-right: 12px; 677 | cursor: pointer; 678 | } 679 | .aplayer-list-author { 680 | color: #666; 681 | float: right; 682 | cursor: pointer; 683 | } 684 | } 685 | } 686 | 687 | .aplayer-notice { 688 | opacity: 0; 689 | position: absolute; 690 | z-index: 1; 691 | top: 50%; 692 | left: 50%; 693 | transform: translate(-50%, -50%); 694 | font-size: 12px; 695 | border-radius: 4px; 696 | padding: 5px 10px; 697 | transition: all 0.3s ease-in-out; 698 | overflow: hidden; 699 | color: #fff; 700 | pointer-events: none; 701 | background-color: #f4f4f5; 702 | color: #909399; 703 | } 704 | 705 | .aplayer-miniswitcher { 706 | display: none; 707 | position: absolute; 708 | top: 0; 709 | right: 0; 710 | bottom: 0; 711 | height: 100%; 712 | background: #e6e6e6; 713 | width: 18px; 714 | border-radius: 0 2px 2px 0; 715 | 716 | .aplayer-icon { 717 | height: 100%; 718 | width: 100%; 719 | transform: rotateY(180deg); 720 | transition: all 0.3s ease; 721 | 722 | path { 723 | fill: #666; 724 | } 725 | 726 | &:hover { 727 | path { 728 | fill: #000; 729 | } 730 | } 731 | } 732 | } 733 | } 734 | 735 | @keyframes aplayer-roll { 736 | 0% { 737 | left: 0; 738 | } 739 | 100% { 740 | left: -100%; 741 | } 742 | } 743 | 744 | @keyframes rotate { 745 | 0% { 746 | transform: rotate(0); 747 | } 748 | 100% { 749 | transform: rotate(360deg); 750 | } 751 | } 752 | -------------------------------------------------------------------------------- /packages/@moefe/vue-aplayer/assets/svg/loading.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/@moefe/vue-aplayer/assets/svg/loop-all.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/@moefe/vue-aplayer/assets/svg/loop-none.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/@moefe/vue-aplayer/assets/svg/loop-one.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/@moefe/vue-aplayer/assets/svg/lrc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/@moefe/vue-aplayer/assets/svg/menu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/@moefe/vue-aplayer/assets/svg/order-list.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/@moefe/vue-aplayer/assets/svg/order-random.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/@moefe/vue-aplayer/assets/svg/pause.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/@moefe/vue-aplayer/assets/svg/play.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/@moefe/vue-aplayer/assets/svg/right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/@moefe/vue-aplayer/assets/svg/skip.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/@moefe/vue-aplayer/assets/svg/volume-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/@moefe/vue-aplayer/assets/svg/volume-off.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/@moefe/vue-aplayer/assets/svg/volume-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/@moefe/vue-aplayer/components/APlayer.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-nested-ternary */ 2 | /* eslint-disable no-underscore-dangle */ 3 | import { VNode } from 'vue'; 4 | import * as Vue from 'vue-tsx-support'; 5 | import Component from 'vue-class-component'; 6 | import { Prop, Provide, Watch } from 'vue-property-decorator'; 7 | import classNames from 'classnames'; 8 | import _Hls from 'hls.js'; 9 | import Audio, { ReadyState, events } from '@moefe/vue-audio'; 10 | import Store from '@moefe/vue-store'; 11 | import Mixin from 'utils/mixin'; 12 | import Player, { Notice } from './Player'; 13 | import PlayList from './PlayList'; 14 | import Lyric from './Lyric'; 15 | import { shuffle, HttpRequest } from '../utils'; 16 | import '../assets/style/aplayer.scss'; 17 | 18 | declare global { 19 | const Hls: typeof _Hls; 20 | } 21 | 22 | const instances: APlayer[] = []; 23 | const store = new Store(); 24 | let channel: BroadcastChannel | null = null; 25 | 26 | if (typeof BroadcastChannel !== 'undefined') { 27 | channel = new BroadcastChannel('aplayer'); 28 | } 29 | 30 | @Component({ mixins: [Mixin] }) 31 | export default class APlayer extends Vue.Component< 32 | APlayer.Options, 33 | APlayer.Events 34 | > { 35 | public static readonly version: string = APLAYER_VERSION; 36 | 37 | public readonly $refs!: { 38 | container: HTMLDivElement; 39 | }; 40 | 41 | // #region [只读] 播放器选项 42 | @Prop({ type: Boolean, required: false, default: false }) 43 | private readonly fixed!: boolean; 44 | 45 | @Prop({ type: Boolean, required: false, default: null }) 46 | private readonly mini!: boolean; 47 | 48 | @Prop({ type: Boolean, required: false, default: false }) 49 | private readonly autoplay!: boolean; 50 | 51 | @Prop({ type: String, required: false, default: '#b7daff' }) 52 | private readonly theme!: string; 53 | 54 | @Prop({ type: String, required: false, default: 'all' }) 55 | private readonly loop!: APlayer.LoopMode; 56 | 57 | @Prop({ type: String, required: false, default: 'list' }) 58 | private readonly order!: APlayer.OrderMode; 59 | 60 | @Prop({ type: String, required: false, default: 'auto' }) 61 | private readonly preload!: APlayer.Preload; 62 | 63 | @Prop({ type: Number, required: false, default: 0.7 }) 64 | private readonly volume!: number; 65 | 66 | @Prop({ type: [Object, Array], required: true }) 67 | private readonly audio!: APlayer.Audio | Array; 68 | 69 | @Prop({ type: Object, required: false }) 70 | private readonly customAudioType?: any; 71 | 72 | @Prop({ type: Boolean, required: false, default: true }) 73 | private readonly mutex!: boolean; 74 | 75 | @Prop({ type: Number, required: false, default: 0 }) 76 | private readonly lrcType!: APlayer.LrcType; 77 | 78 | @Prop({ type: Boolean, required: false, default: false }) 79 | private readonly listFolded!: boolean; 80 | 81 | @Prop({ type: Number, required: false, default: 250 }) 82 | private readonly listMaxHeight!: number; 83 | 84 | @Prop({ type: String, required: false, default: 'aplayer-setting' }) 85 | private readonly storageName!: string; 86 | // #endregion 87 | 88 | // 提供当前实例的引用,让子组件获取该实例的可响应数据 89 | @Provide() 90 | private get aplayer() { 91 | return this; 92 | } 93 | 94 | private get settings(): APlayer.Settings[] { 95 | return this.store.store; 96 | } 97 | 98 | public get currentSettings(): APlayer.Settings { 99 | return this.settings[instances.indexOf(this)]; 100 | } 101 | 102 | // 当前播放模式对应的播放列表 103 | private get currentList() { 104 | return this.currentOrder === 'list' ? this.orderList : this.randomList; 105 | } 106 | 107 | // 数据源,自动生成 ID 作为播放列表项的 key 108 | private get dataSource(): APlayer.Audio[] { 109 | return (Array.isArray(this.audio) ? this.audio : [this.audio]) 110 | .filter(x => x) 111 | .map((item, index) => ({ 112 | id: index + 1, 113 | ...item, 114 | })); 115 | } 116 | 117 | // 根据数据源生成顺序播放列表(处理 VNode) 118 | private get orderList(): APlayer.Audio[] { 119 | const text = (vnode: string | VNode, key: string) => 120 | typeof vnode === 'string' 121 | ? vnode 122 | : vnode.data && vnode.data.attrs && vnode.data.attrs[`data-${key}`]; 123 | 124 | return this.dataSource.map(({ name, artist, ...item }) => ({ 125 | ...item, 126 | name: text(name, 'name'), 127 | artist: text(artist, 'artist'), 128 | })); 129 | } 130 | 131 | // 根据顺序播放列表生成随机播放列表 132 | private get randomList(): APlayer.Audio[] { 133 | return shuffle([...this.orderList]); 134 | } 135 | 136 | // 是否正在缓冲 137 | private get isLoading(): boolean { 138 | const { preload, currentPlayed, currentLoaded } = this; 139 | const { src, paused, duration } = this.media; 140 | const loading = !!src && (currentPlayed > currentLoaded || !duration); 141 | return preload === 'none' ? !paused && loading : loading; 142 | } 143 | 144 | private readonly options!: APlayer.InstallOptions; 145 | 146 | private readonly isMobile!: boolean; 147 | 148 | // 是否正在拖动进度条(防止抖动) 149 | private isDraggingProgressBar = false; 150 | 151 | // 是否正在等待进度条更新(防止抖动) 152 | private isAwaitChangeProgressBar = false; 153 | 154 | // 是否是迷你模式 155 | private isMini = this.mini !== null ? this.mini : this.fixed; 156 | 157 | // 是否是 arrow 模式 158 | private isArrow = false; 159 | 160 | // 当 currentMusic 改变时是否允许播放 161 | private canPlay = !this.isMobile && this.autoplay; 162 | 163 | // 播放列表是否可见 164 | private listVisible = !this.listFolded; 165 | 166 | private get listScrollTop(): number { 167 | return this.currentOrderIndex * 33; 168 | } 169 | 170 | // 控制迷你模式下的歌词是否可见 171 | private lyricVisible = true; 172 | 173 | // 封面图片对象 174 | private img = new Image(); 175 | 176 | // 封面下载对象 177 | private xhr = new HttpRequest(); 178 | 179 | // 响应式媒体对象 180 | private media = new Audio(); 181 | 182 | // 核心音频对象 183 | private player = this.media.audio; 184 | 185 | // 播放器设置存储对象 186 | private store = store; 187 | 188 | // 当前播放的音乐 189 | private currentMusic: APlayer.Audio = { 190 | id: NaN, 191 | name: '未加载音频', 192 | artist: '(ಗ ‸ ಗ )', 193 | url: '', 194 | }; 195 | 196 | // 当前播放的音乐索引 197 | public get currentIndex(): number { 198 | return this.currentOrder === 'list' 199 | ? this.currentOrderIndex 200 | : this.currentRandomIndex; 201 | } 202 | 203 | private get currentOrderIndex(): number { 204 | const { id, url } = this.currentMusic; 205 | return this.orderList.findIndex( 206 | item => item.id === id || item.url === url, 207 | ); 208 | } 209 | 210 | private get currentRandomIndex() { 211 | const { id, url } = this.currentMusic; 212 | return this.randomList.findIndex( 213 | item => item.id === id || item.url === url, 214 | ); 215 | } 216 | 217 | // 当前已缓冲比例 218 | private get currentLoaded(): number { 219 | if (this.media.readyState < ReadyState.HAVE_FUTURE_DATA) return 0; 220 | const { length } = this.media.buffered; 221 | return length > 0 222 | ? this.media.buffered.end(length - 1) / this.media.duration 223 | : 1; 224 | } 225 | 226 | // 当前已播放比例 227 | private currentPlayed = 0; 228 | 229 | // 当前音量 230 | private currentVolume = this.volume; 231 | 232 | // 当前循环模式 233 | private currentLoop = this.loop; 234 | 235 | // 当前顺序模式 236 | private currentOrder = this.order; 237 | 238 | // 当前主题,通过封面自适应主题 > 当前播放的音乐指定的主题 > 主题选项 239 | private currentTheme = this.currentMusic.theme || this.theme; 240 | 241 | // 通知对象 242 | private notice: Notice = { text: '', time: 2000, opacity: 0 }; 243 | 244 | // #region 监听属性 245 | 246 | @Watch('orderList', { immediate: true, deep: true }) 247 | private async handleChangePlayList( 248 | newList: APlayer.Audio[], 249 | oldList?: APlayer.Audio[], 250 | ) { 251 | if (oldList) { 252 | const newLength = newList.length; 253 | const oldLength = oldList.length; 254 | if (newLength !== oldLength) { 255 | if (newLength <= 0) this.$emit('listClear'); 256 | else if (newLength > oldLength) this.$emit('listAdd'); 257 | else { 258 | if (this.currentOrderIndex < 0) { 259 | const { id, url } = this.currentMusic; 260 | const oldIndex = oldList.findIndex( 261 | item => item.id === id || item.url === url, 262 | ); 263 | Object.assign(this.currentMusic, oldList[oldIndex - 1]); 264 | } 265 | this.canPlay = !this.player.paused; 266 | this.$emit('listRemove'); 267 | } 268 | } 269 | } 270 | 271 | // 播放列表初始化 272 | if (this.orderList.length > 0) { 273 | if (!this.currentMusic.id) { 274 | [this.currentMusic] = this.currentList; 275 | } else { 276 | this.canPlay = !this.player.paused; 277 | const music = this.orderList[this.currentOrderIndex] || this.orderList[0]; // eslint-disable-line max-len 278 | Object.assign(this.currentMusic, music); 279 | } 280 | 281 | await this.$nextTick(); 282 | this.canPlay = true; 283 | } 284 | } 285 | 286 | @Watch('currentMusic', { immediate: true, deep: true }) 287 | private async handleChangeCurrentMusic( 288 | newMusic: APlayer.Audio, 289 | oldMusic?: APlayer.Audio, 290 | ) { 291 | if (newMusic.theme) { 292 | this.currentTheme = newMusic.theme; 293 | } else { 294 | const cover = newMusic.cover || this.options.defaultCover; 295 | if (cover) { 296 | setTimeout(async () => { 297 | try { 298 | this.currentTheme = await this.getThemeColorFromCover(cover); 299 | } catch (e) { 300 | this.currentTheme = newMusic.theme || this.theme; 301 | } 302 | }); 303 | } 304 | } 305 | 306 | if (newMusic.url) { 307 | if ( 308 | (oldMusic !== undefined && oldMusic.url) !== newMusic.url 309 | || this.player.src !== newMusic.url 310 | ) { 311 | this.currentPlayed = 0; 312 | if (oldMusic && oldMusic.id) { 313 | // 首次初始化时不要触发事件 314 | this.handleChangeSettings(); 315 | this.$emit('listSwitch', newMusic); 316 | } 317 | const src = await this.getAudioUrl(newMusic); 318 | if (src) this.player.src = src; 319 | this.player.playbackRate = newMusic.speed || 1; 320 | this.player.preload = this.preload; 321 | this.player.volume = this.currentVolume; 322 | this.player.currentTime = 0; 323 | this.player.onerror = (e: Event | string) => { 324 | this.showNotice(e.toString()); 325 | }; 326 | } 327 | // **请勿移动此行**,否则当歌曲结束播放时如果歌单中只有一首歌曲将无法重复播放 328 | if (this.canPlay) this.play(); 329 | } 330 | } 331 | 332 | @Watch('volume') 333 | private handleChangeVolume(volume: number) { 334 | this.currentVolume = volume; 335 | } 336 | 337 | @Watch('currentVolume') 338 | private handleChangeCurrentVolume() { 339 | this.player.volume = this.currentVolume; 340 | this.$emit('update:volume', this.currentVolume); 341 | } 342 | 343 | @Watch('media.currentTime') 344 | private handleChangeCurrentTime() { 345 | if (!this.isDraggingProgressBar && !this.isAwaitChangeProgressBar) { 346 | this.currentPlayed = this.media.currentTime / this.media.duration || 0; 347 | } 348 | } 349 | 350 | @Watch('media.$data', { deep: true }) 351 | private handleChangeSettings() { 352 | const settings: APlayer.Settings = { 353 | currentTime: this.media.currentTime, 354 | duration: this.media.duration, 355 | paused: this.media.paused, 356 | mini: this.isMini, 357 | lrc: this.lyricVisible, 358 | list: this.listVisible, 359 | volume: this.currentVolume, 360 | loop: this.currentLoop, 361 | order: this.currentOrder, 362 | music: this.currentMusic, 363 | }; 364 | 365 | if (settings.volume <= 0) { 366 | settings.volume = this.currentSettings.volume; 367 | } 368 | 369 | this.saveSettings(settings); 370 | } 371 | 372 | @Watch('media.ended') 373 | private handleChangeEnded() { 374 | if (!this.media.ended) return; 375 | this.currentPlayed = 0; 376 | switch (this.currentLoop) { 377 | default: 378 | case 'all': 379 | this.handleSkipForward(); 380 | break; 381 | case 'one': 382 | this.play(); 383 | break; 384 | case 'none': 385 | if (this.currentIndex === this.currentList.length - 1) { 386 | [this.currentMusic] = this.currentList; 387 | this.pause(); 388 | this.canPlay = false; 389 | } else this.handleSkipForward(); 390 | break; 391 | } 392 | } 393 | 394 | @Watch('mini') 395 | private handleChangeMini() { 396 | this.isMini = this.mini; 397 | } 398 | 399 | @Watch('isMini', { immediate: true }) 400 | private async handleChangeCurrentMini(newVal: boolean, oldVal?: boolean) { 401 | await this.$nextTick(); 402 | const { container } = this.$refs; 403 | this.isArrow = container && container.offsetWidth <= 300; 404 | if (oldVal !== undefined) { 405 | this.$emit('update:mini', this.isMini); 406 | this.handleChangeSettings(); 407 | } 408 | } 409 | 410 | @Watch('loop') 411 | private handleChangeLoop() { 412 | this.currentLoop = this.loop; 413 | } 414 | 415 | @Watch('currentLoop') 416 | private handleChangeCurrentLoop() { 417 | this.$emit('update:loop', this.currentLoop); 418 | this.handleChangeSettings(); 419 | } 420 | 421 | @Watch('order') 422 | private handleChangeOrder() { 423 | this.currentOrder = this.order; 424 | } 425 | 426 | @Watch('currentOrder') 427 | private handleChangeCurrentOrder() { 428 | this.$emit('update:order', this.currentOrder); 429 | this.handleChangeSettings(); 430 | } 431 | 432 | @Watch('listVisible') 433 | private handleChangeListVisible() { 434 | this.$emit(this.listVisible ? 'listShow' : 'listHide'); 435 | this.$emit('update:listFolded', this.listVisible); 436 | this.handleChangeSettings(); 437 | } 438 | 439 | @Watch('lyricVisible') 440 | private handleChangeLyricVisible() { 441 | this.$emit(this.lyricVisible ? 'lrcShow' : 'lrcHide'); 442 | this.handleChangeSettings(); 443 | } 444 | 445 | // #endregion 446 | 447 | // #region 公开 API 448 | 449 | public async play() { 450 | try { 451 | if (this.mutex) this.pauseOtherInstances(); 452 | await this.player.play(); 453 | } catch (e) { 454 | this.showNotice(e.message); 455 | this.player.pause(); 456 | } 457 | } 458 | 459 | public pause() { 460 | this.player.pause(); 461 | } 462 | 463 | public toggle() { 464 | if (this.media.paused) this.play(); 465 | else this.pause(); 466 | } 467 | 468 | private async seeking(percent: number, paused: boolean = true) { 469 | try { 470 | this.isAwaitChangeProgressBar = true; 471 | if (this.preload === 'none') { 472 | if (!this.player.src) await this.media.srcLoaded(); 473 | const oldPaused = this.player.paused; 474 | await this.play(); // preload 为 none 的情况下必须先 play 475 | if (paused && oldPaused) this.pause(); 476 | } 477 | if (paused) this.pause(); 478 | await this.media.loaded(); 479 | this.player.currentTime = percent * this.media.duration; 480 | if (!paused) { 481 | this.play(); 482 | if (channel && this.mutex) { 483 | channel.postMessage('mutex'); 484 | } 485 | } 486 | } catch (e) { 487 | this.showNotice(e.message); 488 | } finally { 489 | this.isAwaitChangeProgressBar = false; 490 | } 491 | } 492 | 493 | public seek(time: number) { 494 | this.seeking(time / this.media.duration, this.media.paused); 495 | } 496 | 497 | public switch(audio: number | string) { 498 | switch (typeof audio) { 499 | case 'number': 500 | this.currentMusic = this.orderList[ 501 | Math.min(Math.max(0, audio), this.orderList.length - 1) 502 | ]; 503 | break; 504 | // eslint-disable-next-line no-case-declarations 505 | default: 506 | const music = this.orderList.find( 507 | item => typeof item.name === 'string' && item.name.includes(audio), 508 | ); 509 | if (music) this.currentMusic = music; 510 | break; 511 | } 512 | } 513 | 514 | public skipBack() { 515 | const playIndex = this.getPlayIndexByMode('skipBack'); 516 | this.currentMusic = { ...this.currentList[playIndex] }; 517 | } 518 | 519 | public skipForward() { 520 | const playIndex = this.getPlayIndexByMode('skipForward'); 521 | this.currentMusic = { ...this.currentList[playIndex] }; 522 | } 523 | 524 | public showLrc() { 525 | this.lyricVisible = true; 526 | } 527 | 528 | public hideLrc() { 529 | this.lyricVisible = false; 530 | } 531 | 532 | public toggleLrc() { 533 | this.lyricVisible = !this.lyricVisible; 534 | } 535 | 536 | public showList() { 537 | this.listVisible = true; 538 | } 539 | 540 | public hideList() { 541 | this.listVisible = false; 542 | } 543 | 544 | public toggleList() { 545 | this.listVisible = !this.listVisible; 546 | } 547 | 548 | public showNotice( 549 | text: string, 550 | time: number = 2000, 551 | opacity: number = 0.8, 552 | ): Promise { 553 | return new Promise((resolve) => { 554 | if (this.isMini) { 555 | // eslint-disable-next-line no-console 556 | console.warn('aplayer notice:', text); 557 | resolve(); 558 | } else { 559 | this.notice = { text, time, opacity }; 560 | this.$emit('noticeShow'); 561 | if (time > 0) { 562 | setTimeout(() => { 563 | this.notice.opacity = 0; 564 | this.$emit('noticeHide'); 565 | resolve(); 566 | }, time); 567 | } 568 | } 569 | }); 570 | } 571 | 572 | // #endregion 573 | 574 | // #region 私有 API 575 | 576 | // 从封面中获取主题颜色 577 | private getThemeColorFromCover(url: string): Promise { 578 | return new Promise(async (resolve, reject) => { 579 | try { 580 | if (typeof ColorThief !== 'undefined') { 581 | const image = await this.xhr.download(url, 'blob'); 582 | const reader = new FileReader(); 583 | reader.onload = () => { 584 | this.img.src = reader.result as string; 585 | this.img.onload = () => { 586 | const [r, g, b] = new ColorThief().getColor(this.img); 587 | const theme = `rgb(${r}, ${g}, ${b})`; 588 | resolve(theme || this.currentMusic.theme || this.theme); 589 | }; 590 | this.img.onabort = reject; 591 | this.img.onerror = reject; 592 | }; 593 | reader.onabort = reject; 594 | reader.onerror = reject; 595 | reader.readAsDataURL(image); 596 | } else resolve(this.currentMusic.theme || this.theme); 597 | } catch (e) { 598 | resolve(this.currentMusic.theme || this.theme); 599 | } 600 | }); 601 | } 602 | 603 | private getAudioUrl(music: APlayer.Audio): Promise { 604 | return new Promise((resolve, reject) => { 605 | let { type } = music; 606 | if (type && this.customAudioType && this.customAudioType[type]) { 607 | if (typeof this.customAudioType[type] === 'function') { 608 | this.customAudioType[type](this.player, music, this); 609 | } else { 610 | // eslint-disable-next-line no-console 611 | console.error(`Illegal customType: ${type}`); 612 | } 613 | resolve(); 614 | } else { 615 | if (!type || type === 'auto') { 616 | type = /m3u8(#|\?|$)/i.test(music.url) ? 'hls' : 'normal'; 617 | } 618 | if (type === 'hls') { 619 | try { 620 | if (Hls.isSupported()) { 621 | const hls: Hls = new Hls(); 622 | hls.loadSource(music.url); 623 | hls.attachMedia(this.player as HTMLVideoElement); 624 | resolve(); 625 | } else if ( 626 | this.player.canPlayType('application/x-mpegURL') 627 | || this.player.canPlayType('application/vnd.apple.mpegURL') 628 | ) { 629 | resolve(music.url); 630 | } else { 631 | reject(new Error('HLS is not supported.')); 632 | } 633 | } catch (e) { 634 | reject(new Error('HLS is not supported.')); 635 | } 636 | } else { 637 | resolve(music.url); 638 | } 639 | } 640 | }); 641 | } 642 | 643 | private getPlayIndexByMode(type: 'skipBack' | 'skipForward'): number { 644 | const { length } = this.currentList; 645 | const index = this.currentIndex; 646 | return (type === 'skipBack' ? length + (index - 1) : index + 1) % length; 647 | } 648 | 649 | private pauseOtherInstances() { 650 | instances.filter(inst => inst !== this).forEach(inst => inst.pause()); 651 | } 652 | 653 | private saveSettings(settings: APlayer.Settings | null) { 654 | const instanceIndex = instances.indexOf(this); 655 | if (settings === null) delete instances[instanceIndex]; 656 | this.store.set( 657 | this.settings[instanceIndex] !== undefined 658 | ? this.settings.map((item, index) => 659 | index === instanceIndex ? settings : item, 660 | ) 661 | : [...this.settings, settings], 662 | ); 663 | } 664 | 665 | // #endregion 666 | 667 | // #region 事件处理 668 | 669 | // 切换上一曲 670 | private handleSkipBack() { 671 | this.skipBack(); 672 | } 673 | 674 | // 切换下一曲 675 | private handleSkipForward() { 676 | this.skipForward(); 677 | } 678 | 679 | // 切换播放 680 | private handleTogglePlay() { 681 | this.toggle(); 682 | } 683 | 684 | // 处理切换顺序模式 685 | private handleToggleOrderMode() { 686 | this.currentOrder = this.currentOrder === 'list' ? 'random' : 'list'; 687 | } 688 | 689 | // 处理切换循环模式 690 | private handleToggleLoopMode() { 691 | this.currentLoop = this.currentLoop === 'all' 692 | ? 'one' 693 | : this.currentLoop === 'one' 694 | ? 'none' 695 | : 'all'; 696 | } 697 | 698 | // 处理切换播放/暂停事件 699 | private handleTogglePlaylist() { 700 | this.toggleList(); 701 | } 702 | 703 | // 处理切换歌词显隐事件 704 | private handleToggleLyric() { 705 | this.toggleLrc(); 706 | } 707 | 708 | // 处理进度条改变事件 709 | private handleChangeProgress(e: MouseEvent | TouchEvent, percent: number) { 710 | this.currentPlayed = percent; 711 | this.isDraggingProgressBar = e.type.includes('move'); 712 | if (['touchend', 'mouseup'].includes(e.type)) { 713 | this.seeking(percent, this.media.paused); // preload 为 none 的情况下无法获取到 duration 714 | } 715 | } 716 | 717 | // 处理切换迷你模式事件 718 | private handleMiniSwitcher() { 719 | this.isMini = !this.isMini; 720 | } 721 | 722 | // 处理播放曲目改变事件 723 | private handleChangePlaylist(music: APlayer.Audio, index: number) { 724 | if (music.id === this.currentMusic.id) this.handleTogglePlay(); 725 | else this.currentMusic = this.orderList[index]; 726 | } 727 | // #endregion 728 | 729 | beforeMount() { 730 | this.store.key = this.storageName; 731 | 732 | const emptyIndex = instances.findIndex(x => !x); 733 | if (emptyIndex > -1) instances[emptyIndex] = this; 734 | else instances.push(this); 735 | 736 | if (this.currentSettings) { 737 | const { 738 | mini, 739 | lrc, 740 | list, 741 | volume, 742 | loop, 743 | order, 744 | music, 745 | currentTime, 746 | duration, 747 | paused, 748 | } = this.currentSettings; 749 | this.isMini = mini; 750 | this.lyricVisible = lrc; 751 | this.listVisible = list; 752 | this.currentVolume = volume; 753 | this.currentLoop = loop; 754 | this.currentOrder = order; 755 | if (music) { 756 | this.currentMusic = music; 757 | if (!this.isMobile && duration) { 758 | this.seeking(currentTime / duration, paused); 759 | } 760 | } 761 | } 762 | 763 | // 处理多页面互斥 764 | if (channel) { 765 | if (this.mutex) { 766 | channel.addEventListener('message', ({ data }) => { 767 | if (data === 'mutex') this.pause(); 768 | }); 769 | } 770 | } else { 771 | // 不支持 BroadcastChannel,暂不处理 772 | } 773 | 774 | events.forEach((event) => { 775 | this.player.addEventListener(event, e => this.$emit(event, e)); 776 | }); 777 | } 778 | 779 | beforeDestroy() { 780 | this.pause(); 781 | this.saveSettings(null); 782 | this.$emit('destroy'); 783 | this.$el.remove(); 784 | } 785 | 786 | render() { 787 | const { 788 | dataSource, 789 | fixed, 790 | lrcType, 791 | isMini, 792 | isMobile, 793 | isArrow, 794 | isLoading, 795 | notice, 796 | listVisible, 797 | listScrollTop, 798 | currentMusic, 799 | lyricVisible, 800 | } = this; 801 | 802 | return ( 803 |
1, 808 | 'aplayer-withlrc': !fixed && (lrcType !== 0 && lyricVisible), 809 | 'aplayer-narrow': isMini, 810 | 'aplayer-fixed': fixed, 811 | 'aplayer-mobile': isMobile, 812 | 'aplayer-arrow': isArrow, 813 | 'aplayer-loading': isLoading, 814 | })} 815 | > 816 | 829 | 836 | {fixed && lrcType !== 0 ? : null} 837 |
838 | ); 839 | } 840 | } 841 | -------------------------------------------------------------------------------- /packages/@moefe/vue-aplayer/components/Button.tsx: -------------------------------------------------------------------------------- 1 | import * as Vue from 'vue-tsx-support'; 2 | import Component from 'vue-class-component'; 3 | import { Prop } from 'vue-property-decorator'; 4 | import Icon from './Icon'; 5 | 6 | export interface ButtonProps { 7 | type: string; 8 | icon: string; 9 | } 10 | 11 | export interface ButtonEvents { 12 | onClick?: MouseEvent; 13 | } 14 | 15 | @Component 16 | export default class Button extends Vue.Component { 17 | @Prop({ type: String, required: true }) 18 | private readonly type!: string; 19 | 20 | @Prop({ type: String, required: true }) 21 | private readonly icon!: string; 22 | 23 | private handleClick() { 24 | this.$emit('click'); 25 | } 26 | 27 | render() { 28 | return ( 29 | 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/@moefe/vue-aplayer/components/Controller.tsx: -------------------------------------------------------------------------------- 1 | import * as Vue from 'vue-tsx-support'; 2 | import Component from 'vue-class-component'; 3 | import { Inject } from 'vue-property-decorator'; 4 | import Touch from '@moefe/vue-touch'; 5 | import Icon from './Icon'; 6 | import Button from './Button'; 7 | import Progress from './Progress'; 8 | 9 | export interface ControllerEvents { 10 | onTogglePlay: void; 11 | onSkipBack: void; 12 | onSkipForward: void; 13 | onToggleOrderMode: void; 14 | onToggleLoopMode: void; 15 | onTogglePlaylist: void; 16 | onToggleLyric: void; 17 | onChangeVolume: number; 18 | onChangeProgress: (e: MouseEvent | TouchEvent, percent: number) => void; 19 | onMiniSwitcher: void; 20 | } 21 | 22 | @Component 23 | export default class Controller extends Vue.Component<{}, ControllerEvents> { 24 | public readonly $refs!: { 25 | volumeBar: HTMLElement; 26 | }; 27 | 28 | @Inject() 29 | private readonly aplayer!: APlayer.Options & { 30 | media: APlayer.Media; 31 | currentTheme: string; 32 | currentVolume: number; 33 | currentPlayed: number; 34 | currentLoop: APlayer.LoopMode; 35 | currentOrder: APlayer.OrderMode; 36 | currentSettings: APlayer.Settings; 37 | }; 38 | 39 | @Inject() 40 | private handleSkipBack!: () => void; 41 | 42 | @Inject() 43 | private handleSkipForward!: () => void; 44 | 45 | @Inject() 46 | private handleTogglePlay!: () => void; 47 | 48 | @Inject() 49 | private handleToggleOrderMode!: () => void; 50 | 51 | @Inject() 52 | private handleToggleLoopMode!: () => void; 53 | 54 | @Inject() 55 | private handleTogglePlaylist!: () => void; 56 | 57 | @Inject() 58 | private handleToggleLyric!: () => void; 59 | 60 | @Inject() 61 | private handleChangeVolume!: (volume: number) => void; 62 | 63 | private get playIcon(): string { 64 | return this.aplayer.media.paused ? 'play' : 'pause'; 65 | } 66 | 67 | private get volumeIcon(): string { 68 | const { currentVolume } = this.aplayer; 69 | return currentVolume <= 0 ? 'off' : currentVolume >= 0.95 ? 'up' : 'down'; // eslint-disable-line no-nested-ternary 70 | } 71 | 72 | private get ptime(): string { 73 | const { media, currentPlayed } = this.aplayer; 74 | return this.timeSecondsFormat(currentPlayed * media.duration); 75 | } 76 | 77 | private get dtime(): string { 78 | return this.timeSecondsFormat(this.aplayer.media.duration); 79 | } 80 | 81 | // eslint-disable-next-line class-methods-use-this 82 | private timeSecondsFormat(time: number = 0): string { 83 | const minutes = Math.floor(time / 60) || 0; 84 | const seconds = Math.floor(time % 60) || 0; 85 | return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; // prettier-ignore 86 | } 87 | 88 | private handleToggleVolume() { 89 | const { currentVolume, currentSettings } = this.aplayer; 90 | this.handleChangeVolume(currentVolume > 0 ? 0 : currentSettings.volume); 91 | } 92 | 93 | private handleClickVolumeBar(e: MouseEvent) { 94 | this.handlePanMove(e); 95 | } 96 | 97 | private handlePanMove(e: MouseEvent | TouchEvent) { 98 | const target = this.$refs.volumeBar; 99 | const targetTop = target.getBoundingClientRect().bottom; 100 | if (targetTop <= 0) return; // 音量控制面板已隐藏 101 | const clientY = !e.type.startsWith('touch') 102 | ? (e as MouseEvent).clientY 103 | : (e as TouchEvent).changedTouches[0].clientY; 104 | const offsetTop = Math.round(targetTop - clientY); 105 | let volume = offsetTop / target.offsetHeight; 106 | volume = Math.min(volume, 1); 107 | volume = Math.max(volume, 0); 108 | this.handleChangeVolume(volume); 109 | } 110 | 111 | render() { 112 | const { ptime, dtime, volumeIcon } = this; 113 | const { 114 | lrcType, 115 | currentTheme, 116 | currentVolume, 117 | currentOrder, 118 | currentLoop, 119 | } = this.aplayer; 120 | 121 | return ( 122 |
123 | 124 |
125 | 126 | {ptime} /{' '} 127 | {dtime}{' '} 128 | 129 | 133 | 134 | 135 | 139 | 140 | 141 | 145 | 146 | 147 |
148 |
188 |
189 | ); 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /packages/@moefe/vue-aplayer/components/Cover.tsx: -------------------------------------------------------------------------------- 1 | import * as Vue from 'vue-tsx-support'; 2 | import Component from 'vue-class-component'; 3 | import { Inject } from 'vue-property-decorator'; 4 | 5 | export interface CoverEvents { 6 | onClick?: MouseEvent; 7 | } 8 | 9 | @Component 10 | export default class Cover extends Vue.Component<{}, CoverEvents> { 11 | @Inject() 12 | private readonly aplayer!: APlayer.Options & { 13 | options: APlayer.InstallOptions; 14 | currentTheme: string; 15 | currentMusic: APlayer.Audio; 16 | }; 17 | 18 | private get style() { 19 | const { options, currentTheme, currentMusic } = this.aplayer; 20 | const cover = currentMusic.cover || options.defaultCover; 21 | return { 22 | backgroundImage: cover && `url("${cover}")`, 23 | backgroundColor: currentTheme, 24 | }; 25 | } 26 | 27 | private handleClick(e: MouseEvent) { 28 | this.$emit('click', e); 29 | } 30 | 31 | render() { 32 | return ( 33 |
34 | {this.$slots.default} 35 |
36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/@moefe/vue-aplayer/components/Icon.tsx: -------------------------------------------------------------------------------- 1 | import * as Vue from 'vue-tsx-support'; 2 | import Component from 'vue-class-component'; 3 | import { Prop } from 'vue-property-decorator'; 4 | 5 | export interface IconProps { 6 | type: string; 7 | } 8 | 9 | export const icon = (type: string) => 10 | require(`../assets/svg/${type}.svg`).default; // eslint-disable-line 11 | 12 | @Component 13 | export default class Icon extends Vue.Component { 14 | @Prop({ type: String, required: true }) 15 | private readonly type!: string; 16 | 17 | render() { 18 | const I = icon(this.type); 19 | return ; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/@moefe/vue-aplayer/components/Lyric.tsx: -------------------------------------------------------------------------------- 1 | import * as Vue from 'vue-tsx-support'; 2 | import Component from 'vue-class-component'; 3 | import { Prop, Inject, Watch } from 'vue-property-decorator'; 4 | import classNames from 'classnames'; 5 | import { HttpRequest } from '../utils'; 6 | 7 | interface LRC { 8 | time: number; 9 | text: string; 10 | } 11 | 12 | export interface LyricProps { 13 | visible?: boolean; 14 | } 15 | 16 | @Component 17 | export default class Lyric extends Vue.Component { 18 | @Prop({ type: Boolean, required: false, default: true }) 19 | private readonly visible?: boolean; 20 | 21 | @Inject() 22 | private readonly aplayer!: APlayer.Options & { 23 | media: APlayer.Media; 24 | currentMusic: APlayer.Audio; 25 | currentPlayed: number; 26 | }; 27 | 28 | private lrc = ''; 29 | 30 | private xhr = new HttpRequest(); 31 | 32 | private isLoading = false; 33 | 34 | private get noLyric(): string { 35 | /* eslint-disable no-nested-ternary */ 36 | const { currentMusic } = this.aplayer; 37 | return !currentMusic.id 38 | ? '(ಗ ‸ ಗ ) 未加载音频' 39 | : this.isLoading 40 | ? '(*ゝω・) 少女祈祷中..' 41 | : this.lrc 42 | ? '(・∀・*) 抱歉,该歌词格式不支持' 43 | : '(,,•́ . •̀,,) 抱歉,当前歌曲暂无歌词'; 44 | /* eslint-enable no-nested-ternary */ 45 | } 46 | 47 | private get parsed(): Array { 48 | return this.parseLRC(this.lrc); 49 | } 50 | 51 | private get current(): LRC { 52 | const { media, currentPlayed } = this.aplayer; 53 | const match = this.parsed.filter( 54 | x => x.time < currentPlayed * media.duration * 1000, 55 | ); 56 | if (match && match.length > 0) return match[match.length - 1]; 57 | return this.parsed[0]; 58 | } 59 | 60 | private get transitionDuration(): number { 61 | return this.parsed.length > 1 ? 500 : 0; 62 | } 63 | 64 | private get translateY(): number { 65 | const { current, parsed } = this; 66 | if (parsed.length <= 0) return 0; 67 | const index = parsed.indexOf(current); 68 | const isLast = index === parsed.length - 1; 69 | return (isLast ? (index - 1) * 16 : index * 16) * -1; 70 | } 71 | 72 | private get style() { 73 | return { 74 | transitionDuration: `${this.transitionDuration}ms`, 75 | transform: `translate3d(0, ${this.translateY}px, 0)`, 76 | }; 77 | } 78 | 79 | private getLyricFromCurrentMusic() { 80 | return new Promise((resolve, reject) => { 81 | const { lrcType, currentMusic } = this.aplayer; 82 | switch (lrcType) { 83 | case 0: 84 | resolve(''); 85 | break; 86 | case 1: 87 | resolve(currentMusic.lrc); 88 | break; 89 | case 3: 90 | resolve(currentMusic.lrc ? this.xhr.download(currentMusic.lrc) : ''); 91 | break; 92 | default: 93 | reject(new Error(`Illegal lrcType: ${lrcType}`)); 94 | break; 95 | } 96 | }); 97 | } 98 | 99 | private parseLRC(lrc: string): Array { 100 | const reg = /\[(\d+):(\d+)[.|:](\d+)\](.+)/; 101 | const regTime = /\[(\d+):(\d+)[.|:](\d+)\]/g; 102 | const regCompatible = /\[(\d+):(\d+)]()(.+)/; 103 | const regTimeCompatible = /\[(\d+):(\d+)]/g; 104 | const regOffset = /\[offset:\s*(-?\d+)\]/; 105 | const offsetMatch = this.lrc.match(regOffset); 106 | const offset = offsetMatch ? Number(offsetMatch[1]) : 0; 107 | const parsed: Array = []; 108 | 109 | const matchAll = (line: string) => { 110 | const match = line.match(reg) || line.match(regCompatible); 111 | if (!match || match.length !== 5) return; 112 | const minutes = Number(match[1]) || 0; 113 | const seconds = Number(match[2]) || 0; 114 | const milliseconds = Number(match[3]) || 0; 115 | 116 | const time = minutes * 60 * 1000 + seconds * 1000 + milliseconds + offset; // eslint-disable-line no-mixed-operators 117 | const text = (match[4] as string) 118 | .replace(regTime, '') 119 | .replace(regTimeCompatible, ''); 120 | 121 | // 优化:不要显示空行 122 | if (!text) return; 123 | parsed.push({ time, text }); 124 | matchAll(match[4]); // 递归匹配多个时间标签 125 | }; 126 | 127 | lrc 128 | .replace(/\\n/g, '\n') 129 | .split('\n') 130 | .forEach(line => matchAll(line)); 131 | 132 | if (parsed.length > 0) { 133 | parsed.sort((a, b) => a.time - b.time); 134 | } 135 | 136 | return parsed; 137 | } 138 | 139 | @Watch('aplayer.lrcType', { immediate: true }) 140 | @Watch('aplayer.currentMusic.lrc', { immediate: true }) 141 | private async handleChange() { 142 | try { 143 | this.isLoading = true; 144 | this.lrc = ''; 145 | this.lrc = await this.getLyricFromCurrentMusic(); 146 | } finally { 147 | this.isLoading = false; 148 | } 149 | } 150 | 151 | render() { 152 | const { visible, style, parsed, current, noLyric } = this; 153 | 154 | return ( 155 |
161 |
162 | {parsed.length > 0 ? ( 163 | parsed.map((item, index) => ( 164 |

170 | {item.text} 171 |

172 | )) 173 | ) : ( 174 |

{noLyric}

175 | )} 176 |
177 |
178 | ); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /packages/@moefe/vue-aplayer/components/Main.tsx: -------------------------------------------------------------------------------- 1 | import * as Vue from 'vue-tsx-support'; 2 | import Component from 'vue-class-component'; 3 | import { Inject } from 'vue-property-decorator'; 4 | import Lyric from './Lyric'; 5 | 6 | @Component 7 | export default class Main extends Vue.Component<{}> { 8 | @Inject() 9 | private readonly aplayer!: APlayer.Options & { 10 | currentMusic: APlayer.Audio; 11 | }; 12 | 13 | private get music() { 14 | const { currentMusic } = this.aplayer; 15 | return { 16 | name: currentMusic.name, 17 | artist: currentMusic.artist ? ` - ${currentMusic.artist}` : '', 18 | }; 19 | } 20 | 21 | render() { 22 | const { music } = this; 23 | const { fixed } = this.aplayer; 24 | 25 | return ( 26 |
27 |
28 | {music.name} 29 | {music.artist} 30 |
31 | {!fixed ? : null} 32 | {this.$slots.default} 33 |
34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/@moefe/vue-aplayer/components/PlayList.tsx: -------------------------------------------------------------------------------- 1 | import * as Vue from 'vue-tsx-support'; 2 | import Component from 'vue-class-component'; 3 | import { Prop, Inject, Watch } from 'vue-property-decorator'; 4 | import classNames from 'classnames'; 5 | 6 | export interface PlayListProps { 7 | visible?: boolean; 8 | currentMusic: APlayer.Audio; 9 | dataSource: APlayer.Audio[]; 10 | scrollTop: number; 11 | } 12 | 13 | export interface PlayListEvents { 14 | onChange: (music: APlayer.Audio, index: number) => void; 15 | } 16 | 17 | @Component 18 | export default class PlayList extends Vue.Component< 19 | PlayListProps, 20 | PlayListEvents 21 | > { 22 | public readonly $refs!: { 23 | list: HTMLOListElement; 24 | }; 25 | 26 | @Prop({ type: Boolean, required: false, default: true }) 27 | private readonly visible?: boolean; 28 | 29 | @Prop({ type: Object, required: true }) 30 | private readonly currentMusic!: APlayer.Audio; 31 | 32 | @Prop({ type: Array, required: true }) 33 | private readonly dataSource!: Array; 34 | 35 | @Prop({ type: Number, required: true }) 36 | private readonly scrollTop!: number; 37 | 38 | @Inject() 39 | private readonly aplayer!: APlayer.Options & { 40 | currentTheme: string; 41 | }; 42 | 43 | private get listHeight(): number { 44 | const { visible, dataSource } = this; 45 | return visible 46 | ? Math.min(dataSource.length * 33, Number(this.aplayer.listMaxHeight)) 47 | : 0; 48 | } 49 | 50 | @Watch('scrollTop', { immediate: true }) 51 | @Watch('dataSource', { immediate: true, deep: true }) 52 | @Watch('visible') 53 | private async handleChangeScrollTop() { 54 | await this.$nextTick(); 55 | if (this.visible) { 56 | this.$refs.list.scrollTop = this.scrollTop; 57 | } 58 | } 59 | 60 | render() { 61 | const { listHeight, dataSource, currentMusic } = this; 62 | const { currentTheme } = this.aplayer; 63 | 64 | return ( 65 |
    66 | {dataSource.map((item, index) => ( 67 |
  1. this.$emit('change', item, index)} 73 | > 74 | 80 | {index + 1}{' '} 81 | {item.name} 82 | {item.artist} 83 |
  2. 84 | ))} 85 |
86 | ); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /packages/@moefe/vue-aplayer/components/Player.tsx: -------------------------------------------------------------------------------- 1 | import * as Vue from 'vue-tsx-support'; 2 | import Component from 'vue-class-component'; 3 | import { Prop, Provide, Inject } from 'vue-property-decorator'; 4 | import Cover from './Cover'; 5 | import Icon from './Icon'; 6 | import Main from './Main'; 7 | import Controller, { ControllerEvents } from './Controller'; 8 | import Button from './Button'; 9 | 10 | export interface Notice { 11 | text: string; 12 | time: number; 13 | opacity: number; 14 | } 15 | 16 | export interface PlayerProps { 17 | notice?: Notice; 18 | } 19 | 20 | @Component 21 | export default class Player extends Vue.Component< 22 | PlayerProps, 23 | ControllerEvents 24 | > { 25 | @Prop({ type: Object, required: true }) 26 | private readonly notice!: Notice; 27 | 28 | @Inject() 29 | private readonly aplayer!: { media: APlayer.Media }; 30 | 31 | private get playIcon(): string { 32 | return this.aplayer.media.paused ? 'play' : 'pause'; 33 | } 34 | 35 | @Provide() 36 | private handleTogglePlay() { 37 | this.$emit('togglePlay'); 38 | } 39 | 40 | @Provide() 41 | private handleSkipBack() { 42 | this.$emit('skipBack'); 43 | } 44 | 45 | @Provide() 46 | private handleSkipForward() { 47 | this.$emit('skipForward'); 48 | } 49 | 50 | @Provide() 51 | private handleToggleOrderMode() { 52 | this.$emit('toggleOrderMode'); 53 | } 54 | 55 | @Provide() 56 | private handleToggleLoopMode() { 57 | this.$emit('toggleLoopMode'); 58 | } 59 | 60 | @Provide() 61 | private handleTogglePlaylist() { 62 | this.$emit('togglePlaylist'); 63 | } 64 | 65 | @Provide() 66 | private handleToggleLyric() { 67 | this.$emit('toggleLyric'); 68 | } 69 | 70 | @Provide() 71 | private handleChangeVolume(volume: number) { 72 | this.$emit('changeVolume', volume); 73 | } 74 | 75 | @Provide() 76 | private handleChangeProgress(e: MouseEvent | TouchEvent, percent: number) { 77 | this.$emit('changeProgress', e, percent); 78 | } 79 | 80 | private handleMiniSwitcher() { 81 | this.$emit('miniSwitcher'); 82 | } 83 | 84 | render() { 85 | const { playIcon, notice } = this; 86 | 87 | return ( 88 |
89 | 90 |
91 | 92 |
93 |
94 |
95 | 106 |
107 |
108 | {notice.text} 109 |
110 |
111 |
113 |
114 | ); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /packages/@moefe/vue-aplayer/components/Progress.tsx: -------------------------------------------------------------------------------- 1 | import * as Vue from 'vue-tsx-support'; 2 | import Comopnent from 'vue-class-component'; 3 | import { Inject } from 'vue-property-decorator'; 4 | import Touch from '@moefe/vue-touch'; 5 | import Icon from './Icon'; 6 | 7 | @Comopnent 8 | export default class Progress extends Vue.Component<{}> { 9 | public $refs!: { 10 | progressBar: HTMLElement; 11 | }; 12 | 13 | @Inject() 14 | private readonly aplayer!: APlayer.Options & { 15 | currentTheme: string; 16 | currentLoaded: number; 17 | currentPlayed: number; 18 | }; 19 | 20 | @Inject() 21 | private readonly handleChangeProgress!: ( 22 | e: MouseEvent | TouchEvent, 23 | percent: number 24 | ) => void; 25 | 26 | private handleChange(e: MouseEvent | TouchEvent) { 27 | const target = this.$refs.progressBar; 28 | const targetLeft = target.getBoundingClientRect().left; 29 | const clientX = !e.type.startsWith('touch') 30 | ? (e as MouseEvent).clientX 31 | : (e as TouchEvent).changedTouches[0].clientX; 32 | const offsetLeft = clientX - targetLeft; 33 | let percent = offsetLeft / target.offsetWidth; 34 | percent = Math.min(percent, 1); 35 | percent = Math.max(percent, 0); 36 | this.handleChangeProgress(e, percent); 37 | } 38 | 39 | render() { 40 | const { currentTheme, currentLoaded, currentPlayed } = this.aplayer; 41 | 42 | return ( 43 | 48 |
49 |
55 |
62 | 68 | 69 | 70 | 71 | 72 |
73 |
74 | 75 | ); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /packages/@moefe/vue-aplayer/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import _Vue from 'vue'; 3 | import APlayer from './components/APlayer'; 4 | 5 | export { APlayer }; 6 | 7 | export default function install( 8 | Vue: typeof _Vue, 9 | options?: APlayer.InstallOptions, 10 | ) { 11 | const defaultOptions: APlayer.InstallOptions = { 12 | productionTip: true, 13 | defaultCover: 'https://avatars2.githubusercontent.com/u/20062482?s=270', 14 | }; 15 | const opts = { ...defaultOptions, ...options }; 16 | Object.assign(APlayer.prototype, { options: opts }); 17 | 18 | Vue.component('aplayer', APlayer); 19 | Vue.component('APlayer', APlayer); 20 | 21 | if (opts.productionTip) { 22 | // eslint-disable-next-line no-console 23 | console.log( 24 | `%c vue-aplayer %c v${APLAYER_VERSION} ${GIT_HASH} %c`, 25 | 'background: #35495e; padding: 1px; border-radius: 3px 0 0 3px; color: #fff', 26 | 'background: #41b883; padding: 1px; border-radius: 0 3px 3px 0; color: #fff', 27 | 'background: transparent', 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/@moefe/vue-aplayer/shims.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/export */ 2 | 3 | declare const APLAYER_VERSION: string; 4 | declare const GIT_HASH: string; 5 | 6 | declare module '*.json' { 7 | const value: any; 8 | export default value; 9 | } 10 | 11 | declare class ColorThief { 12 | getColor(sourceImage: HTMLImageElement, quality?: number): number[]; 13 | 14 | getPalette( 15 | sourceImage: HTMLImageElement, 16 | colorCount?: number, 17 | quality?: number 18 | ): number[][]; 19 | } 20 | -------------------------------------------------------------------------------- /packages/@moefe/vue-aplayer/utils/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign */ 2 | 3 | export function shuffle(arr: any[]) { 4 | for (let i = arr.length - 1; i >= 0; i--) { 5 | const randomIndex = Math.floor(Math.random() * (i + 1)); 6 | const itemAtIndex = arr[randomIndex]; 7 | arr[randomIndex] = arr[i]; 8 | arr[i] = itemAtIndex; 9 | } 10 | return arr; 11 | } 12 | 13 | export class HttpRequest { 14 | private xhr = new XMLHttpRequest(); 15 | 16 | public download( 17 | url: string, 18 | responseType: XMLHttpRequestResponseType = '', 19 | ) { 20 | return new Promise((resolve, reject) => { 21 | this.xhr.open('get', url); 22 | this.xhr.responseType = responseType; 23 | this.xhr.onload = () => { 24 | const { status } = this.xhr; 25 | if ((status >= 200 && status < 300) || status === 304) { 26 | resolve(this.xhr.response); 27 | } 28 | }; 29 | this.xhr.onabort = reject; 30 | this.xhr.onerror = reject; 31 | this.xhr.ontimeout = reject; 32 | this.xhr.send(); 33 | }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/@moefe/vue-audio/events.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | 'abort', 3 | 'canplay', 4 | 'canplaythrough', 5 | 'durationchange', 6 | 'emptied', 7 | 'ended', 8 | 'error', 9 | 'loadeddata', 10 | 'loadedmetadata', 11 | 'loadstart', 12 | 'pause', 13 | 'play', 14 | 'playing', 15 | 'progress', 16 | 'ratechange', 17 | 'readystatechange', 18 | 'seeked', 19 | 'seeking', 20 | 'stalled', 21 | 'suspend', 22 | 'timeupdate', 23 | 'volumechange', 24 | 'waiting', 25 | ]; 26 | -------------------------------------------------------------------------------- /packages/@moefe/vue-audio/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Component from 'vue-class-component'; 3 | import { eventLoop } from 'utils'; 4 | import events from './events'; 5 | 6 | export { events }; 7 | 8 | export enum ReadyState { 9 | /** 没有关于音频是否就绪的信息 */ 10 | HAVE_NOTHING = 0, 11 | /** 关于音频就绪的元数据 */ 12 | HAVE_METADATA = 1, 13 | /** 关于当前播放位置的数据是可用的,但没有足够的数据来播放下一帧/毫秒 */ 14 | HAVE_CURRENT_DATA = 2, 15 | /** 当前及至少下一帧的数据是可用的 */ 16 | HAVE_FUTURE_DATA = 3, 17 | /** 可用数据足以开始播放 */ 18 | HAVE_ENOUGH_DATA = 4, 19 | } 20 | 21 | @Component 22 | export default class VueAudio extends Vue implements APlayer.Media { 23 | [index: string]: any; 24 | 25 | constructor() { 26 | super(); 27 | events.forEach((event) => { 28 | this.audio.addEventListener(event, (e) => { 29 | this.sync(); 30 | }); 31 | }); 32 | } 33 | 34 | private sync() { 35 | Object.keys(this.$data).forEach((key) => { 36 | if (key === 'audio') return; 37 | this[key] = (this.audio as any)[key]; 38 | }); 39 | } 40 | 41 | public loaded() { 42 | return eventLoop(() => this.readyState >= ReadyState.HAVE_FUTURE_DATA, 0); 43 | } 44 | 45 | public srcLoaded() { 46 | return eventLoop(() => this.src, 0); 47 | } 48 | 49 | public readonly audio: HTMLAudioElement = new Audio(); 50 | 51 | public readonly audioTracks: AudioTrackList = this.audio.audioTracks; 52 | 53 | public readonly autoplay: boolean = this.audio.autoplay; 54 | 55 | public readonly buffered: TimeRanges = this.audio.buffered; 56 | 57 | public readonly controls: boolean = this.audio.controls; 58 | 59 | public readonly crossOrigin: string | null = this.audio.crossOrigin; 60 | 61 | public readonly currentSrc: string = this.audio.currentSrc; 62 | 63 | public readonly currentTime: number = this.audio.currentTime; 64 | 65 | public readonly defaultMuted: boolean = this.audio.defaultMuted; 66 | 67 | public readonly defaultPlaybackRate: number = this.audio.defaultPlaybackRate; 68 | 69 | public readonly duration: number = this.audio.duration; 70 | 71 | public readonly ended: boolean = this.audio.ended; 72 | 73 | public readonly error: MediaError | null = this.audio.error; 74 | 75 | public readonly loop: boolean = this.audio.loop; 76 | 77 | public readonly mediaKeys: MediaKeys | null = this.audio.mediaKeys; 78 | 79 | public readonly muted: boolean = this.audio.muted; 80 | 81 | public readonly networkState: number = this.audio.networkState; 82 | 83 | public readonly paused: boolean = this.audio.paused; 84 | 85 | public readonly playbackRate: number = this.audio.playbackRate; 86 | 87 | public readonly played: TimeRanges = this.audio.played; 88 | 89 | public readonly preload: string = this.audio.preload; 90 | 91 | public readonly readyState: number = this.audio.readyState; 92 | 93 | public readonly seekable: TimeRanges = this.audio.seekable; 94 | 95 | public readonly seeking: boolean = this.audio.seeking; 96 | 97 | public readonly src: string = this.audio.src; 98 | 99 | public readonly textTracks: TextTrackList = this.audio.textTracks; 100 | 101 | public readonly volume: number = this.audio.volume; 102 | 103 | render() { 104 | return null; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /packages/@moefe/vue-store/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Component from 'vue-class-component'; 3 | 4 | @Component 5 | export default class VueStore extends Vue { 6 | public key: string = 'aplayer-setting'; 7 | 8 | public store: any[] = this.get(this.key); 9 | 10 | // eslint-disable-next-line class-methods-use-this 11 | public get(key: string): any[] { 12 | return JSON.parse(localStorage.getItem(key) || '[]'); 13 | } 14 | 15 | public set(val: any[]) { 16 | this.store = val; 17 | localStorage.setItem(this.key, JSON.stringify(val)); 18 | } 19 | 20 | render() { 21 | return null; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/@moefe/vue-touch/index.tsx: -------------------------------------------------------------------------------- 1 | import * as Vue from 'vue-tsx-support'; 2 | import Component from 'vue-class-component'; 3 | import { Prop } from 'vue-property-decorator'; 4 | import Mixin from 'utils/mixin'; 5 | 6 | export interface TouchProps { 7 | panMoveClass?: string; 8 | } 9 | 10 | export interface TouchEvents { 11 | onPanStart: MouseEvent | TouchEvent; 12 | onPanMove: MouseEvent | TouchEvent; 13 | onPanEnd: MouseEvent | TouchEvent; 14 | } 15 | 16 | @Component({ mixins: [Mixin] }) 17 | export default class Touch extends Vue.Component { 18 | @Prop({ type: String, required: false }) 19 | private readonly panMoveClass!: string; 20 | 21 | private readonly isMobile!: boolean; 22 | 23 | private isDragMove: boolean = false; 24 | 25 | private get classNames() { 26 | const { panMoveClass, isDragMove } = this; 27 | return { [panMoveClass]: isDragMove }; 28 | } 29 | 30 | private get dragStart(): 'touchstart' | 'mousedown' { 31 | return this.isMobile ? 'touchstart' : 'mousedown'; 32 | } 33 | 34 | private get dragMove(): 'touchmove' | 'mousemove' { 35 | return this.isMobile ? 'touchmove' : 'mousemove'; 36 | } 37 | 38 | private get dragEnd(): 'touchend' | 'mouseup' { 39 | return this.isMobile ? 'touchend' : 'mouseup'; 40 | } 41 | 42 | private thumbMove(e: MouseEvent | TouchEvent) { 43 | this.isDragMove = true; 44 | this.$emit('panMove', e); 45 | } 46 | 47 | private thumbUp(e: MouseEvent | TouchEvent) { 48 | document.removeEventListener(this.dragMove, this.thumbMove); 49 | document.removeEventListener(this.dragEnd, this.thumbUp); 50 | this.isDragMove = false; 51 | this.$emit('panEnd', e); 52 | } 53 | 54 | mounted() { 55 | this.$el.addEventListener(this.dragStart, (e: MouseEvent | TouchEvent) => { 56 | this.$emit('panStart', e); 57 | document.addEventListener(this.dragMove, this.thumbMove); 58 | document.addEventListener(this.dragEnd, this.thumbUp); 59 | }); 60 | } 61 | 62 | render() { 63 | return ( 64 |
73 | {this.$slots.default} 74 |
75 | ); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "moduleResolution": "node", 8 | "experimentalDecorators": true, 9 | "emitDecoratorMetadata": true, 10 | "allowSyntheticDefaultImports": true, 11 | "sourceMap": true, 12 | "baseUrl": ".", 13 | "types": ["node", "hls.js"], 14 | "paths": { 15 | "utils/*": ["utils/*"], 16 | "@moefe/vue-audio": ["packages/@moefe/vue-audio/index.ts"], 17 | "@moefe/vue-store": ["packages/@moefe/vue-store/index.ts"], 18 | "@moefe/vue-touch": ["packages/@moefe/vue-touch/index.tsx"], 19 | "@moefe/vue-aplayer": ["packages/@moefe/vue-aplayer/index.ts"] 20 | } 21 | }, 22 | "include": [ 23 | "types/**/*.ts", 24 | "example/**/*.ts", 25 | "example/**/*.tsx", 26 | "packages/**/*.ts", 27 | "packages/**/*.tsx", 28 | "utils/**/*.ts", 29 | "node_modules/@types/", 30 | "node_modules/vue-tsx-support/enable-check.d.ts" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /types/aplayer.d.ts: -------------------------------------------------------------------------------- 1 | import { VNode } from 'vue'; 2 | 3 | declare global { 4 | namespace APlayer { 5 | export interface InstallOptions { 6 | defaultCover?: string; 7 | productionTip?: boolean; 8 | } 9 | 10 | export type LoopMode = 'all' | 'one' | 'none'; 11 | export type OrderMode = 'list' | 'random'; 12 | export type Preload = 'none' | 'metadata' | 'auto'; 13 | export type AudioType = 'auto' | 'hls' | 'normal'; 14 | 15 | export enum LrcType { 16 | file = 3, 17 | html = 2, // not support 18 | string = 1, 19 | disabled = 0, 20 | } 21 | 22 | export interface Audio { 23 | [index: number]: this; 24 | 25 | id?: number; 26 | name: string | VNode; // eslint-disable-line no-restricted-globals 27 | artist: string | VNode; 28 | url: string; 29 | cover?: string; 30 | lrc?: string; 31 | theme?: string; 32 | type?: AudioType; 33 | speed?: number; 34 | } 35 | 36 | export interface Options { 37 | fixed?: boolean; 38 | mini?: boolean; 39 | autoplay?: boolean; 40 | theme?: string; 41 | loop?: LoopMode; 42 | order?: OrderMode; 43 | preload?: Preload; 44 | volume?: number; 45 | audio: Audio | Audio[]; 46 | customAudioType?: any; 47 | mutex?: boolean; 48 | lrcType?: LrcType; 49 | listFolded?: boolean; 50 | listMaxHeight?: number; 51 | storageName?: string; 52 | } 53 | 54 | export interface Events { 55 | onAbort: Event; 56 | onCanplay: Event; 57 | onCanplaythrough: Event; 58 | onDurationchange: Event; 59 | onEmptied: Event; 60 | onEnded: Event; 61 | onError: Event; 62 | onLoadeddata: Event; 63 | onLoadedmetadata: Event; 64 | onLoadstart: Event; 65 | onPause: Event; 66 | onPlay: Event; 67 | onPlaying: Event; 68 | onProgress: Event; 69 | onRatechange: Event; 70 | onReadystatechange: Event; 71 | onSeeked: Event; 72 | onSeeking: Event; 73 | onStalled: Event; 74 | onSuspend: Event; 75 | onTimeupdate: Event; 76 | onVolumechange: Event; 77 | onWaiting: Event; 78 | 79 | onListSwitch: Audio; 80 | onListShow: void; 81 | onListHide: void; 82 | onListAdd: void; 83 | onListRemove: void; 84 | onListClear: void; 85 | onNoticeShow: void; 86 | onNoticeHide: void; 87 | onLrcShow: void; 88 | onLrcHide: void; 89 | onDestroy: void; 90 | } 91 | 92 | export interface Settings { 93 | currentTime: number; 94 | duration: number | null; 95 | paused: boolean; 96 | mini: boolean; 97 | lrc: boolean; 98 | list: boolean; 99 | volume: number; 100 | loop: LoopMode; 101 | order: OrderMode; 102 | music: Audio | null; 103 | } 104 | 105 | export interface Media { 106 | /** 返回表示可用音频轨道的 AudioTrackList 对象。 */ 107 | readonly audioTracks: AudioTrackList; 108 | /** 设置或返回是否在就绪(加载完成)后随即播放音频。 */ 109 | readonly autoplay: boolean; 110 | /** 返回表示音频已缓冲部分的 TimeRanges 对象。 */ 111 | readonly buffered: TimeRanges; 112 | /** 设置或返回音频是否应该显示控件(比如播放/暂停等)。 */ 113 | readonly controls: boolean; 114 | /** 设置或返回音频的 CORS 设置。 */ 115 | readonly crossOrigin: string | null; 116 | /** 返回当前音频的 URL。 */ 117 | readonly currentSrc: string; 118 | /** 设置或返回音频中的当前播放位置(以秒计)。 */ 119 | readonly currentTime: number; 120 | /** 设置或返回音频默认是否静音。 */ 121 | readonly defaultMuted: boolean; 122 | /** 设置或返回音频的默认播放速度。 */ 123 | readonly defaultPlaybackRate: number; 124 | /** 返回音频的长度(以秒计)。 */ 125 | readonly duration: number; 126 | /** 返回音频的播放是否已结束。 */ 127 | readonly ended: boolean; 128 | /** 返回表示音频错误状态的 MediaError 对象。 */ 129 | readonly error: MediaError | null; 130 | /** 设置或返回音频是否应在结束时再次播放。 */ 131 | readonly loop: boolean; 132 | /** 设置或返回音频所属媒介组合的名称。 */ 133 | readonly mediaKeys: MediaKeys | null; 134 | /** 设置或返回是否关闭声音。 */ 135 | readonly muted: boolean; 136 | /** 返回音频的当前网络状态。 */ 137 | readonly networkState: number; 138 | /** 设置或返回音频是否暂停。 */ 139 | readonly paused: boolean; 140 | /** 设置或返回音频播放的速度。 */ 141 | readonly playbackRate: number; 142 | /** 返回表示音频已播放部分的 TimeRanges 对象。 */ 143 | readonly played: TimeRanges; 144 | /** 设置或返回音频的 preload 属性的值。 */ 145 | readonly preload: string; 146 | /** 返回音频当前的就绪状态。 */ 147 | readonly readyState: number; 148 | /** 返回表示音频可寻址部分的 TimeRanges 对象。 */ 149 | readonly seekable: TimeRanges; 150 | /** 返回用户当前是否正在音频中进行查找。 */ 151 | readonly seeking: boolean; 152 | /** 设置或返回音频的 src 属性的值。 */ 153 | readonly src: string; 154 | /** 返回表示可用文本轨道的 TextTrackList 对象。 */ 155 | readonly textTracks: TextTrackList; 156 | /** 设置或返回音频的音量。 */ 157 | readonly volume: number; 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | // TypeScript Version: 2.8 2 | import _Vue from 'vue'; 3 | import * as Vue from 'vue-tsx-support'; 4 | 5 | export class APlayer extends Vue.Component { 6 | static readonly version: string; 7 | 8 | readonly $refs: { 9 | container: HTMLDivElement; 10 | }; 11 | 12 | readonly currentMusic: APlayer.Audio; 13 | 14 | readonly currentSettings: APlayer.Settings; 15 | 16 | readonly media: APlayer.Media; 17 | 18 | play(): Promise; 19 | 20 | pause(): void; 21 | 22 | toggle(): void; 23 | 24 | seek(time: number): void; 25 | 26 | switch(audio: number | string): void; 27 | 28 | skipBack(): void; 29 | 30 | skipForward(): void; 31 | 32 | showLrc(): void; 33 | 34 | hideLrc(): void; 35 | 36 | toggleLrc(): void; 37 | 38 | showList(): void; 39 | 40 | hideList(): void; 41 | 42 | toggleList(): void; 43 | 44 | showNotice(text: string, time?: number, opacity?: number): void; 45 | } 46 | 47 | export default function install( 48 | Vue: typeof _Vue, 49 | options?: APlayer.InstallOptions 50 | ): void; 51 | -------------------------------------------------------------------------------- /types/test.tsx: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Comopnent from 'vue-class-component'; 3 | import APlayerPlugin, { APlayer } from './'; // eslint-disable-line 4 | 5 | Vue.use(APlayerPlugin, { 6 | defaultCover: '', 7 | productionTip: true, 8 | }); 9 | 10 | console.log(APlayer.version); 11 | 12 | @Comopnent 13 | export default class App extends Vue { 14 | readonly $refs!: { 15 | aplayer: APlayer; 16 | }; 17 | 18 | async created() { 19 | const { aplayer } = this.$refs; 20 | console.log(aplayer.$refs.container); 21 | console.log(aplayer.media.currentTime); 22 | console.log(aplayer.media.duration); 23 | console.log(aplayer.media.paused); 24 | console.log(aplayer.currentMusic); 25 | console.log(aplayer.currentSettings); 26 | await aplayer.play(); 27 | aplayer.toggle(); 28 | aplayer.pause(); 29 | aplayer.seek(0); 30 | aplayer.switch(0); 31 | aplayer.switch(''); 32 | aplayer.skipBack(); 33 | aplayer.skipForward(); 34 | aplayer.showLrc(); 35 | aplayer.hideList(); 36 | aplayer.toggleLrc(); 37 | aplayer.showList(); 38 | aplayer.hideList(); 39 | aplayer.toggleList(); 40 | aplayer.showNotice(''); 41 | aplayer.showNotice('', 1e3); 42 | aplayer.showNotice('', 1e3, 0.8); 43 | } 44 | 45 | render() { 46 | return ( 47 |
48 | 62 | [HOT] 光るなら 63 | 64 | ), 65 | artist: 'Goose house', 66 | url: 'https://moeplayer.b0.upaiyun.com/aplayer/hikarunara.mp3', 67 | cover: 'https://moeplayer.b0.upaiyun.com/aplayer/hikarunara.jpg', 68 | lrc: 'https://moeplayer.b0.upaiyun.com/aplayer/hikarunara.lrc', 69 | theme: '#ebd0c2', 70 | }} 71 | lrcType={2} 72 | listFolded={false} 73 | listMaxHeight={250} 74 | storageName="aplayer-setting" 75 | onAbort={() => {}} 76 | onCanplay={() => {}} 77 | onCanplaythrough={() => {}} 78 | onDurationchange={() => {}} 79 | onEmptied={() => {}} 80 | onEnded={() => {}} 81 | onError={() => {}} 82 | onLoadeddata={() => {}} 83 | onLoadedmetadata={() => {}} 84 | onLoadstart={() => {}} 85 | onPause={() => {}} 86 | onPlay={() => {}} 87 | onPlaying={() => {}} 88 | onProgress={() => {}} 89 | onRatechange={() => {}} 90 | onReadystatechange={() => {}} 91 | onSeeked={() => {}} 92 | onSeeking={() => {}} 93 | onStalled={() => {}} 94 | onSuspend={() => {}} 95 | onTimeupdate={() => {}} 96 | onVolumechange={() => {}} 97 | onWaiting={() => {}} 98 | onListSwitch={() => {}} 99 | onListShow={() => {}} 100 | onListHide={() => {}} 101 | onListAdd={() => {}} 102 | onListRemove={() => {}} 103 | onListClear={() => {}} 104 | onNoticeShow={() => {}} 105 | onNoticeHide={() => {}} 106 | onLrcShow={() => {}} 107 | onLrcHide={() => {}} 108 | onDestroy={() => {}} 109 | /> 110 |
111 | ); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /types/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "lib": ["es6", "dom"], 6 | "declaration": true, 7 | "strict": true, 8 | "jsx": "preserve", 9 | "moduleResolution": "node", 10 | "experimentalDecorators": true, 11 | "emitDecoratorMetadata": true, 12 | "allowSyntheticDefaultImports": true, 13 | "noImplicitAny": true, 14 | "noImplicitThis": true, 15 | "strictNullChecks": true, 16 | "strictFunctionTypes": true 17 | }, 18 | "include": [ 19 | "./*.ts", 20 | "./*.tsx", 21 | "../node_modules/vue-tsx-support/enable-check.d.ts" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /types/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "dtslint/dtslint.json", 3 | "rules": { 4 | "strict-export-declare-modifiers": false, 5 | "no-empty-interface": false, 6 | "no-relative-import-in-test": false, 7 | "file-name-casing": false, 8 | "void-return": false 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /utils/index.ts: -------------------------------------------------------------------------------- 1 | export function sleep(delay: number = 0): Promise { 2 | return new Promise(resolve => setTimeout(resolve, delay)); 3 | } 4 | 5 | export function eventLoop( 6 | target: () => any, 7 | timeout: number = 3000, 8 | ): Promise { 9 | return new Promise((resolve, reject) => { 10 | const startTime = new Date().getTime(); 11 | const timerId = setInterval(() => { 12 | if (!target()) { 13 | if (timeout > 0 && new Date().getTime() - startTime > timeout) { 14 | reject(); 15 | clearInterval(timerId); 16 | } 17 | return; 18 | } 19 | resolve(); 20 | clearInterval(timerId); 21 | }, 100); 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /utils/mixin.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Component from 'vue-class-component'; 3 | 4 | @Component 5 | export default class Mixin extends Vue { 6 | private get isMobile(): boolean { 7 | const ua = this.$ssrContext 8 | ? this.$ssrContext.userAgent 9 | : window.navigator.userAgent; 10 | return /mobile/i.test(ua); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable strict */ 2 | /* eslint-disable global-require */ 3 | // https://cli.vuejs.org/config/ 4 | // https://cli.vuejs.org/guide/build-targets.html 5 | const path = require('path'); 6 | const gitRevisionPlugin = new (require('git-revision-webpack-plugin'))(); 7 | 8 | module.exports = { 9 | css: { extract: false }, 10 | pages: { 11 | app: { 12 | entry: path.resolve(__dirname, 'example/main.ts'), 13 | template: path.resolve(__dirname, 'example/public/index.html'), 14 | filename: 'index.html', 15 | }, 16 | }, 17 | devServer: { 18 | // https://webpack.docschina.org/configuration/dev-server/#devserver-contentbase 19 | contentBase: path.resolve(__dirname, 'example/public'), 20 | }, 21 | chainWebpack: (config) => { 22 | if (process.env.NODE_ENV === 'production') { 23 | // https://github.com/vuejs/vue-cli/blob/5a8abe029e0c9a16f575983f76d51c569145b0b0/packages/%40vue/cli-service-global/lib/globalConfigPlugin.js#L128-L131 24 | // https://github.com/vuejs/vue-cli/blob/5a8abe029e0c9a16f575983f76d51c569145b0b0/packages/%40vue/cli-service/lib/commands/build/resolveAppConfig.js#L6-L12 25 | // https://github.com/vuejs/vue-cli/blob/5a8abe029e0c9a16f575983f76d51c569145b0b0/packages/%40vue/cli-service/lib/config/app.js#L211-L221 26 | // https://github.com/vuejs/vue-cli/issues/1550#issuecomment-401786406 27 | config.plugin('copy').use(require('copy-webpack-plugin'), [ 28 | [ 29 | { 30 | from: path.resolve(__dirname, 'example/public'), 31 | to: path.resolve(__dirname, 'demo'), 32 | ignore: ['index.html', '.DS_Store'], 33 | }, 34 | ], 35 | ]); 36 | } else { 37 | // https://github.com/vuejs/vue-cli/issues/1132 38 | config.output.filename('[name].[hash].js').end(); 39 | } 40 | 41 | config 42 | .entry('app') 43 | .clear() 44 | .add('./example/main.ts'); 45 | 46 | config.plugin('define').tap((args) => { 47 | Object.assign(args[0], { 48 | APLAYER_VERSION: JSON.stringify(require('./package.json').version), 49 | GIT_HASH: JSON.stringify(gitRevisionPlugin.commithash().substr(0, 7)), 50 | }); 51 | return args; 52 | }); 53 | 54 | // https://cli.vuejs.org/guide/webpack.html#replacing-loaders-of-a-rule 55 | config.module 56 | .rule('svg') 57 | .use('file-loader') 58 | .loader('vue-svg-loader'); 59 | 60 | // https://github.com/mozilla-neutrino/webpack-chain#config-resolve-alias 61 | config.resolve.alias 62 | .set('utils', path.resolve(__dirname, 'utils')) 63 | .set('@moefe/vue-audio', path.resolve(__dirname, 'packages/@moefe/vue-audio')) 64 | .set('@moefe/vue-store', path.resolve(__dirname, 'packages/@moefe/vue-store')) 65 | .set('@moefe/vue-touch', path.resolve(__dirname, 'packages/@moefe/vue-touch')) 66 | .set('@moefe/vue-aplayer', path.resolve(__dirname, 'packages/@moefe/vue-aplayer')); // prettier-ignore 67 | }, 68 | }; 69 | --------------------------------------------------------------------------------