mirror of
https://github.com/kevin-DL/vue-audio-recorder.git
synced 2026-01-11 19:04:28 +00:00
Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e6d6260b1b | |||
| adf3fba1b3 | |||
| eb674f00f0 | |||
| b1ff2cd677 | |||
| f680d5f47d | |||
| 4cde0ea657 | |||
| d836371eaa | |||
|
|
84fb2fd6f6 | ||
|
|
2b12f06640 | ||
|
|
cf7df625ca | ||
|
|
a51fa4fdf2 | ||
|
|
94fc582eef | ||
|
|
1f8aad013a | ||
|
|
22d1b524a4 | ||
|
|
a9a98d2e33 | ||
|
|
e482c3bea7 | ||
|
|
0c4c0cd091 | ||
|
|
77f3460825 | ||
|
|
7fdf3f745b | ||
|
|
87f9703529 | ||
|
|
f4bf0c503a | ||
|
|
4cfac6de85 | ||
|
|
685a4b76e6 | ||
|
|
d8d3d1491e | ||
|
|
db90e87dff | ||
|
|
7e89d8a33a | ||
|
|
420cf2e194 | ||
|
|
52b7dfe958 | ||
|
|
5d8139d674 |
73
README.md
73
README.md
@@ -14,8 +14,9 @@
|
||||
- Records limit
|
||||
- A lot of callbacks
|
||||
- Individual an audio player
|
||||
- MP3/WAV support
|
||||
|
||||
### Tested in
|
||||
### Tested in (desktop)
|
||||
|
||||
- Chrome
|
||||
- Firefox
|
||||
@@ -29,21 +30,28 @@ npm i vue-audio-recorder --save
|
||||
|
||||
## AudioRecorder props
|
||||
|
||||
| Prop | Type | Description |
|
||||
| --------------------- | -------- | --------------------------------------------------------------- |
|
||||
| attempts | Number | Number of recording attempts |
|
||||
| compact | Boolean | Hide the download and upload buttons |
|
||||
| time | Number | Time limit for the record (minutes) |
|
||||
| upload-url | String | URL for uploading |
|
||||
| start-record | Function | Fires after click the record button |
|
||||
| stop-record | Function | Fires after click the stop button or exceeding the time limit |
|
||||
| start-upload | Function | Fires after start uploading |
|
||||
| attempts-limit | Function | Fires after exceeding the attempts |
|
||||
| failed-upload | Function | Fires after a failure uploading |
|
||||
| mic-failed | Function | Fires if your microphone doesn't work |
|
||||
| successful-upload | Function | Fires after a successful uploading |
|
||||
| successful-upload-msg | String | Displays the message after a successful uploading |
|
||||
| failed-upload-msg | String | Displays the message after a failure uploading |
|
||||
| Prop | Type | Description |
|
||||
| --------------------- | -------- | ------------------------------------------------------------------------ |
|
||||
| attempts | Number | Number of recording attempts |
|
||||
| headers | Object | HTTP headers |
|
||||
| time | Number | Time limit for the record (minutes) |
|
||||
| bit-rate | Number | Default: 128 (only for MP3) |
|
||||
| sample-rate | Number | Default: 44100 |
|
||||
| filename | String | Download/Upload filename |
|
||||
| format | String | WAV/MP3. Default: mp3 |
|
||||
| upload-url | String | URL for uploading |
|
||||
| show-download-button | Boolean | If it is true show a download button. Default: true |
|
||||
| show-upload-button | Boolean | If it is true show an upload button. Default: true |
|
||||
| show-custom-button | Boolean | If true show another button linked to specific action. Default: true |
|
||||
| before-upload | Function | Callback fires before uploading |
|
||||
| successful-upload | Function | Callback fires after successful uploading |
|
||||
| failed-upload | Function | Callback fires after failure uploading |
|
||||
| mic-failed | Function | Callback fires if your microphone doesn't work |
|
||||
| before-recording | Function | Callback fires after click the record button |
|
||||
| pause-recording | Function | Callback fires after pause recording |
|
||||
| after-recording | Function | Callback fires after click the stop button or exceeding the time limit |
|
||||
| select-record | Function | Callback fires after choise a record. Returns the record |
|
||||
| custom-callback | Function | Callback fires when clicking on the custom button |
|
||||
|
||||
## AudioPlayer props
|
||||
| Prop | Type | Description |
|
||||
@@ -52,31 +60,31 @@ npm i vue-audio-recorder --save
|
||||
|
||||
## Usage
|
||||
|
||||
The most common use case is to register the component globally
|
||||
|
||||
```js
|
||||
import {AudioRecorder, AudioPlayer} from 'vue-audio-recorder'
|
||||
|
||||
Vue.component(AudioPlayer)
|
||||
Vue.component(AudioRecorder)
|
||||
```
|
||||
|
||||
Alternatively you can do this to register the components
|
||||
|
||||
```js
|
||||
import AudioRecorder from 'vue-audio-recorder'
|
||||
|
||||
Vue.use(AudioRecorder)
|
||||
```
|
||||
|
||||
```js
|
||||
methods: {
|
||||
callback (data) {
|
||||
console.debug(data)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```html
|
||||
<audio-recorder
|
||||
upload-url="YOUR_API_URL"
|
||||
:attempts="3"
|
||||
:time="2"
|
||||
:start-record="callback"
|
||||
:stop-record="callback"
|
||||
:start-upload="callback"
|
||||
:headers="headers"
|
||||
:before-recording="callback"
|
||||
:pause-recording="callback"
|
||||
:after-recording="callback"
|
||||
:select-record="callback"
|
||||
:before-upload="callback"
|
||||
:successful-upload="callback"
|
||||
:failed-upload="callback"/>
|
||||
```
|
||||
@@ -98,6 +106,11 @@ npm run dev
|
||||
npm run build
|
||||
```
|
||||
|
||||
## TODO
|
||||
|
||||
- Clear record list
|
||||
- Responsive design
|
||||
|
||||
## Authors
|
||||
|
||||
[Gennady Grishkovtsov](https://www.linkedin.com/in/grishkovtsov/) - Developer
|
||||
|
||||
26
demo/app.vue
26
demo/app.vue
@@ -11,32 +11,34 @@
|
||||
|
||||
<audio-recorder v-if="showRecorder"
|
||||
upload-url="some url"
|
||||
filename="ninja"
|
||||
format="wav"
|
||||
:attempts="3"
|
||||
:time="2"
|
||||
:start-record="callback"
|
||||
:stop-record="callback"
|
||||
:start-upload="callback"
|
||||
:headers="headers"
|
||||
:before-recording="callback"
|
||||
:pause-recording="callback"
|
||||
:after-recording="callback"
|
||||
:select-record="callback"
|
||||
:before-upload="callback"
|
||||
:successful-upload="callback"
|
||||
:failed-upload="callback"/>
|
||||
:failed-upload="callback"
|
||||
:bit-rate="192"/>
|
||||
|
||||
<audio-player :src="mp3" v-if="!showRecorder"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AudioPlayer from '../src/components/player'
|
||||
import AudioRecorder from '../src/components/recorder'
|
||||
|
||||
export default {
|
||||
name: 'app',
|
||||
components: {
|
||||
AudioPlayer,
|
||||
AudioRecorder
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
mp3: '/demo/example.mp3',
|
||||
showRecorder: true
|
||||
showRecorder: true,
|
||||
headers: {
|
||||
'X-Custom-Header': 'some data'
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -2,8 +2,12 @@ import Vue from 'vue'
|
||||
import axios from 'axios'
|
||||
import app from './app'
|
||||
|
||||
import AudioRecorder from '@/index'
|
||||
|
||||
Vue.prototype.$http = axios
|
||||
|
||||
Vue.use(AudioRecorder)
|
||||
|
||||
new Vue({
|
||||
el: '#app',
|
||||
render: h => h(app)
|
||||
|
||||
2
dist/vue-audio-recorder.min.js
vendored
2
dist/vue-audio-recorder.min.js
vendored
File diff suppressed because one or more lines are too long
2
dist/vue-audio-recorder.min.js.map
vendored
2
dist/vue-audio-recorder.min.js.map
vendored
File diff suppressed because one or more lines are too long
18
package.json
18
package.json
@@ -1,14 +1,16 @@
|
||||
{
|
||||
"name": "vue-audio-recorder",
|
||||
"description": "Audio recorder for Vue.js. It allows to create, play, download and store records on a server",
|
||||
"version": "2.1.0",
|
||||
"version": "4.0.1",
|
||||
"author": "Gennady Grishkovtsov <grishkovelli@gmail.com>",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "cross-env NODE_ENV=development webpack-dev-server --open --hot --https",
|
||||
"build": "cross-env NODE_ENV=production webpack --progress --hide-modules"
|
||||
"dev": "webpack-dev-server --env.NODE_ENV=development --mode development --open --hot --https",
|
||||
"build": "webpack --env.NODE_ENV=production --mode production --progress --hide-modules"
|
||||
},
|
||||
"dependencies": {
|
||||
"lamejs": "^1.2.0"
|
||||
},
|
||||
"dependencies": {},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
@@ -26,11 +28,13 @@
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"node-sass": "^4.5.3",
|
||||
"sass-loader": "^6.0.6",
|
||||
"uglifyjs-webpack-plugin": "^2.0.1",
|
||||
"vue": "^2.5.16",
|
||||
"vue-loader": "^13.0.5",
|
||||
"vue-loader": "^14.2.2",
|
||||
"vue-template-compiler": "^2.4.4",
|
||||
"webpack": "^3.6.0",
|
||||
"webpack-dev-server": "^2.9.1",
|
||||
"webpack": "^4.17.1",
|
||||
"webpack-cli": "^3.1.2",
|
||||
"webpack-dev-server": "^3.1.9",
|
||||
"webpack-merge": "^4.1.3"
|
||||
},
|
||||
"main": "dist/vue-audio-recorder.min.js",
|
||||
|
||||
BIN
screenshot.png
BIN
screenshot.png
Binary file not shown.
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 44 KiB |
38
src/components/downloader.vue
Normal file
38
src/components/downloader.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<style lang="scss">
|
||||
@import '../scss/icons';
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<icon-button
|
||||
id="download"
|
||||
class="ar-icon ar-icon__xs ar-icon--no-border"
|
||||
name="download"
|
||||
@click.native="download"/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import IconButton from './icon-button'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
record : { type: Object },
|
||||
filename : { type: String }
|
||||
},
|
||||
components: {
|
||||
IconButton
|
||||
},
|
||||
methods: {
|
||||
download () {
|
||||
if (!this.record.url) {
|
||||
return
|
||||
}
|
||||
|
||||
const type = this.record.blob.type.split('/')[1]
|
||||
const link = document.createElement('a')
|
||||
link.href = this.record.url
|
||||
link.download = `${this.filename}.${type}`
|
||||
link.click()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -10,6 +10,9 @@
|
||||
data: function () {
|
||||
return {
|
||||
icons: {
|
||||
upload: '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">\n' +
|
||||
' <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />\n' +
|
||||
'</svg>',
|
||||
download: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 12v7H5v-7H3v7c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2v-7h-2zm-6 .67l2.59-2.58L17 11.5l-5 5-5-5 1.41-1.41L11 12.67V3h2z"/><path fill="none" d="M0 0h24v24H0z"/></svg>',
|
||||
mic: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 14c1.66 0 2.99-1.34 2.99-3L15 5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm5.3-3c0 3-2.54 5.1-5.3 5.1S6.7 14 6.7 11H5c0 3.41 2.72 6.23 6 6.72V21h2v-3.28c3.28-.48 6-3.3 6-6.72h-1.7z"/><path d="M0 0h24v24H0z" fill="none"/></svg>',
|
||||
pause: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/><path d="M0 0h24v24H0z" fill="none"/></svg>',
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { calculateLineHeadPosition } from '@/lib/utils.js'
|
||||
import { calculateLineHeadPosition } from '@/lib/utils'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
@@ -35,7 +35,7 @@
|
||||
},
|
||||
methods: {
|
||||
onMouseDown (ev) {
|
||||
let seekPos = calculateLineHeadPosition(ev, this.$refs[this.refId])
|
||||
const seekPos = calculateLineHeadPosition(ev, this.$refs[this.refId])
|
||||
this.$emit('change-linehead', seekPos)
|
||||
document.addEventListener('mousemove', this.onMouseMove)
|
||||
document.addEventListener('mouseup', this.onMouseUp)
|
||||
@@ -43,17 +43,17 @@
|
||||
onMouseUp (ev) {
|
||||
document.removeEventListener('mouseup', this.onMouseUp)
|
||||
document.removeEventListener('mousemove', this.onMouseMove)
|
||||
let seekPos = calculateLineHeadPosition(ev, this.$refs[this.refId])
|
||||
const seekPos = calculateLineHeadPosition(ev, this.$refs[this.refId])
|
||||
this.$emit('change-linehead', seekPos)
|
||||
},
|
||||
onMouseMove (ev) {
|
||||
let seekPos = calculateLineHeadPosition(ev, this.$refs[this.refId])
|
||||
const seekPos = calculateLineHeadPosition(ev, this.$refs[this.refId])
|
||||
this.$emit('change-linehead', seekPos)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
calculateSize () {
|
||||
let value = this.percentage < 1 ? this.percentage * 100 : this.percentage
|
||||
const value = this.percentage < 1 ? this.percentage * 100 : this.percentage
|
||||
return `${this.rowDirection ? 'width' : 'height'}: ${value}%`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,26 @@
|
||||
<style lang="scss">
|
||||
.ar-player {
|
||||
width: 380px;
|
||||
height: 120px;
|
||||
border: 1px solid #E8E8E8;
|
||||
border-radius: 24px;
|
||||
height: unset;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #FAFAFA;
|
||||
background-color: unset;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
|
||||
& > .ar-player-bar {
|
||||
border: 1px solid #E8E8E8;
|
||||
border-radius: 24px;
|
||||
margin: 0 0 0 5px;
|
||||
|
||||
& > .ar-player__progress {
|
||||
width: 125px;
|
||||
}
|
||||
}
|
||||
|
||||
&-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -26,33 +36,6 @@
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
&--compact {
|
||||
height: unset;
|
||||
flex-direction: row;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background-color: unset;
|
||||
|
||||
& > .ar-player-actions {
|
||||
width: unset;
|
||||
|
||||
& > #download,
|
||||
& > #upload {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
& > .ar-player-bar {
|
||||
border: 1px solid #E8E8E8;
|
||||
border-radius: 24px;
|
||||
margin: 0 0 0 5px;
|
||||
|
||||
& > .ar-player__progress {
|
||||
width: 125px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__progress {
|
||||
width: 160px;
|
||||
margin: 0 8px;
|
||||
@@ -73,6 +56,10 @@
|
||||
&--active {
|
||||
fill: white !important;
|
||||
background-color: #05CBCD !important;
|
||||
|
||||
&:hover {
|
||||
fill: #505050 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -81,27 +68,14 @@
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div class="ar-player" :class="{'ar-player--compact': compact}">
|
||||
|
||||
<div class="ar-player">
|
||||
<div class="ar-player-actions">
|
||||
<icon-button
|
||||
id="download"
|
||||
class="ar-icon ar-icon__sm"
|
||||
name="download"
|
||||
@click.native="decorator(download)"/>
|
||||
|
||||
<icon-button
|
||||
id="play"
|
||||
class="ar-icon ar-icon__lg ar-player__play"
|
||||
:name="playBtnIcon"
|
||||
:class="{'ar-player__play--active': isPlaying}"
|
||||
@click.native="decorator(playback)"/>
|
||||
|
||||
<icon-button
|
||||
id="upload"
|
||||
class="ar-icon ar-icon__sm"
|
||||
name="save"
|
||||
@click.native="decorator(upload)"/>
|
||||
@click.native="playback"/>
|
||||
</div>
|
||||
|
||||
<div class="ar-player-bar">
|
||||
@@ -120,27 +94,23 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import IconButton from './icon-button'
|
||||
import LineControl from './line-control'
|
||||
import IconButton from './icon-button'
|
||||
import LineControl from './line-control'
|
||||
import VolumeControl from './volume-control'
|
||||
import { convertTimeMMSS } from '@/lib/utils.js'
|
||||
import { convertTimeMMSS } from '@/lib/utils'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
src : { type: String },
|
||||
uploadUrl : { type: String },
|
||||
record : { type: Object },
|
||||
compact : { type: Boolean, default: true },
|
||||
startUpload : { type: Function },
|
||||
successfulUpload : { type: Function },
|
||||
failedUpload : { type: Function }
|
||||
src : { type: String },
|
||||
record : { type: Object },
|
||||
filename : { type: String }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
isPlaying: false,
|
||||
duration: convertTimeMMSS(0),
|
||||
playedTime: convertTimeMMSS(0),
|
||||
progress: 0
|
||||
isPlaying : false,
|
||||
duration : convertTimeMMSS(0),
|
||||
playedTime : convertTimeMMSS(0),
|
||||
progress : 0
|
||||
}
|
||||
},
|
||||
components: {
|
||||
@@ -161,10 +131,14 @@
|
||||
})
|
||||
|
||||
this.player.addEventListener('timeupdate', this._onTimeUpdate)
|
||||
|
||||
this.$eventBus.$on('remove-record', () => {
|
||||
this._resetProgress()
|
||||
})
|
||||
},
|
||||
computed: {
|
||||
audioSource () {
|
||||
let url = this.src || this.record.url
|
||||
const url = this.src || this.record.url
|
||||
if (url) {
|
||||
return url
|
||||
} else {
|
||||
@@ -180,6 +154,10 @@
|
||||
},
|
||||
methods: {
|
||||
playback () {
|
||||
if (!this.audioSource) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.isPlaying) {
|
||||
this.player.pause()
|
||||
} else {
|
||||
@@ -188,52 +166,15 @@
|
||||
|
||||
this.isPlaying = !this.isPlaying
|
||||
},
|
||||
upload () {
|
||||
if (this.startUpload) {
|
||||
this.startUpload()
|
||||
}
|
||||
|
||||
this.$emit('on-start-upload')
|
||||
|
||||
let data = new FormData()
|
||||
data.append('audio', this.record.blob, 'my-record')
|
||||
|
||||
this.$http.post(this.uploadUrl, data, {
|
||||
headers: {'Content-Type': `multipart/form-data; boundary=${data._boundary}`}
|
||||
}).then(resp => {
|
||||
this.$emit('on-end-upload', 'success')
|
||||
if (this.successfulUpload) {
|
||||
this.successfulUpload(resp)
|
||||
}
|
||||
}).catch(error => {
|
||||
this.$emit('on-end-upload', 'fail')
|
||||
if (this.failedUpload) {
|
||||
this.failedUpload(error)
|
||||
}
|
||||
})
|
||||
},
|
||||
download () {
|
||||
let link = document.createElement('a')
|
||||
link.href = this.record.url
|
||||
link.download = 'record.wav'
|
||||
link.click()
|
||||
},
|
||||
decorator (func) {
|
||||
if (!this.audioSource) {
|
||||
return
|
||||
}
|
||||
func()
|
||||
},
|
||||
_resetProgress () {
|
||||
if (this.isPlaying) {
|
||||
this.player.pause()
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.duration = convertTimeMMSS(0)
|
||||
this.playedTime = convertTimeMMSS(0)
|
||||
this.progress = 0
|
||||
this.isPlaying = false
|
||||
}, 0)
|
||||
|
||||
this.duration = convertTimeMMSS(0)
|
||||
this.playedTime = convertTimeMMSS(0)
|
||||
this.progress = 0
|
||||
this.isPlaying = false
|
||||
},
|
||||
_onTimeUpdate () {
|
||||
this.playedTime = convertTimeMMSS(this.player.currentTime)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<style lang="scss">
|
||||
@import '../scss/icons';
|
||||
.ar {
|
||||
width: 420px;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
@@ -72,7 +73,7 @@
|
||||
&__records-limit {
|
||||
position: absolute;
|
||||
color: #AEAEAE;
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
top: 78px;
|
||||
}
|
||||
}
|
||||
@@ -160,6 +161,22 @@
|
||||
top: 0;
|
||||
color: rgb(244, 120, 90);
|
||||
}
|
||||
|
||||
&__downloader,
|
||||
&__uploader {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
&__downloader {
|
||||
right: 115px;
|
||||
}
|
||||
|
||||
&__uploader {
|
||||
right: 85px;
|
||||
}
|
||||
}
|
||||
|
||||
@import '../scss/icons';
|
||||
@@ -168,7 +185,6 @@
|
||||
<template>
|
||||
<div class="ar">
|
||||
<div class="ar__overlay" v-if="isUploading"></div>
|
||||
|
||||
<div class="ar-spinner" v-if="isUploading">
|
||||
<div class="ar-spinner__dot"></div>
|
||||
<div class="ar-spinner__dot"></div>
|
||||
@@ -201,77 +217,102 @@
|
||||
:class="{'ar-records__record--selected': record.id === selected.id}"
|
||||
:key="record.id"
|
||||
v-for="(record, idx) in recordList"
|
||||
@click="selected = record">
|
||||
@click="choiceRecord(record)">
|
||||
<div
|
||||
class="ar__rm"
|
||||
v-if="record.id === selected.id"
|
||||
@click="removeRecord(idx)">×</div>
|
||||
<div class="ar__text">Record {{idx + 1}}</div>
|
||||
<div class="ar__text">{{record.duration}}</div>
|
||||
|
||||
<downloader
|
||||
v-if="record.id === selected.id && showDownloadButton"
|
||||
class="ar__downloader"
|
||||
:record="record"
|
||||
:filename="filename"/>
|
||||
|
||||
<uploader
|
||||
v-if="record.id === selected.id && showUploadButton"
|
||||
class="ar__uploader"
|
||||
:record="record"
|
||||
:filename="filename"
|
||||
:headers="headers"
|
||||
:upload-url="uploadUrl"/>
|
||||
<icon-button name="upload" class="ar__uploader ar-icon ar-icon__xs" v-if="showCustomButton" @click.native="customAction(record)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<audio-player
|
||||
:compact="compact"
|
||||
:record="selected"
|
||||
:upload-url="uploadUrl"
|
||||
:start-upload="startUpload"
|
||||
:successful-upload="successfulUpload"
|
||||
:failed-upload="failedUpload"
|
||||
@start-upload="onStartUpload"
|
||||
@end-upload="onEndUpload"/>
|
||||
|
||||
<div :class="uploadStatusClasses" v-if="uploadStatus">{{message}}</div>
|
||||
<audio-player :record="selected"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AudioPlayer from './player.vue'
|
||||
import IconButton from './icon-button.vue'
|
||||
import Recorder from '@/lib/recorder.js'
|
||||
import { convertTimeMMSS } from '@/lib/utils.js'
|
||||
import AudioPlayer from './player'
|
||||
import Downloader from './downloader'
|
||||
import IconButton from './icon-button'
|
||||
import Recorder from '@/lib/recorder'
|
||||
import Uploader from './uploader'
|
||||
import UploaderPropsMixin from '@/mixins/uploader-props'
|
||||
import { convertTimeMMSS } from '@/lib/utils'
|
||||
|
||||
export default {
|
||||
mixins: [UploaderPropsMixin],
|
||||
props: {
|
||||
attempts : { type: Number },
|
||||
compact : { type: Boolean, default: false },
|
||||
time : { type: Number },
|
||||
uploadUrl : { type: String },
|
||||
attempts : { type: Number },
|
||||
time : { type: Number },
|
||||
|
||||
bitRate : { type: Number, default: 128 },
|
||||
sampleRate : { type: Number, default: 44100 },
|
||||
|
||||
showDownloadButton : { type: Boolean, default: true },
|
||||
showUploadButton : { type: Boolean, default: true },
|
||||
showCustomButton : {type: Boolean, default: false},
|
||||
|
||||
attemptsLimit : { type: Function },
|
||||
failedUpload : { type: Function },
|
||||
micFailed : { type: Function },
|
||||
startRecord : { type: Function },
|
||||
startUpload : { type: Function },
|
||||
stopRecord : { type: Function },
|
||||
beforeRecording : { type: Function },
|
||||
pauseRecording : { type: Function },
|
||||
afterRecording : { type: Function },
|
||||
failedUpload : { type: Function },
|
||||
beforeUpload : { type: Function },
|
||||
successfulUpload : { type: Function },
|
||||
|
||||
successfulUploadMsg : { type: String, default: 'Upload successful' },
|
||||
failedUploadMsg : { type: String, default: 'Upload fail' }
|
||||
selectRecord : { type: Function },
|
||||
customCallback : { type: Function },
|
||||
format : { type: String }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
isUploading: false,
|
||||
recorder: new Recorder({
|
||||
afterStop: () => {
|
||||
this.recordList = this.recorder.recordList()
|
||||
|
||||
if (this.stopRecord) {
|
||||
this.stopRecord('stop record')
|
||||
}
|
||||
},
|
||||
attempts: this.attempts,
|
||||
time: this.time
|
||||
}),
|
||||
recordList: [],
|
||||
selected: {},
|
||||
uploadStatus: null
|
||||
isUploading : false,
|
||||
recorder : this._initRecorder(),
|
||||
recordList : [],
|
||||
selected : {},
|
||||
uploadStatus : null,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
AudioPlayer,
|
||||
IconButton
|
||||
Downloader,
|
||||
IconButton,
|
||||
Uploader
|
||||
},
|
||||
mounted () {
|
||||
this.$eventBus.$on('start-upload', () => {
|
||||
this.isUploading = true
|
||||
this.beforeUpload && this.beforeUpload('before upload')
|
||||
})
|
||||
|
||||
this.$eventBus.$on('end-upload', (msg) => {
|
||||
this.isUploading = false
|
||||
|
||||
if (msg.status === 'success') {
|
||||
this.successfulUpload && this.successfulUpload(msg.response)
|
||||
} else {
|
||||
this.failedUpload && this.failedUpload(msg.response)
|
||||
}
|
||||
})
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.stopRecorder()
|
||||
},
|
||||
methods: {
|
||||
toggleRecorder () {
|
||||
@@ -281,14 +322,8 @@
|
||||
|
||||
if (!this.isRecording || (this.isRecording && this.isPause)) {
|
||||
this.recorder.start()
|
||||
if (this.startRecord) {
|
||||
this.startRecord('start record')
|
||||
}
|
||||
} else {
|
||||
this.recorder.pause()
|
||||
if (this.startRecord) {
|
||||
this.startRecord('pause record')
|
||||
}
|
||||
}
|
||||
},
|
||||
stopRecorder () {
|
||||
@@ -297,23 +332,42 @@
|
||||
}
|
||||
|
||||
this.recorder.stop()
|
||||
this.recordList = this.recorder.recordList()
|
||||
},
|
||||
removeRecord (idx) {
|
||||
this.recordList.splice(idx, 1)
|
||||
this.$set(this.selected, 'url', null)
|
||||
this.$eventBus.$emit('remove-record')
|
||||
},
|
||||
onStartUpload () {
|
||||
this.isUploading = true
|
||||
choiceRecord (record) {
|
||||
if (this.selected === record) {
|
||||
return
|
||||
}
|
||||
this.selected = record
|
||||
this.selectRecord && this.selectRecord(record)
|
||||
},
|
||||
onEndUpload (status) {
|
||||
this.isUploading = false
|
||||
this.uploadStatus = status
|
||||
setTimeout(() => {this.uploadStatus = null}, 1500)
|
||||
_initRecorder () {
|
||||
return new Recorder({
|
||||
beforeRecording : this.beforeRecording,
|
||||
afterRecording : this.afterRecording,
|
||||
pauseRecording : this.pauseRecording,
|
||||
micFailed : this.micFailed,
|
||||
bitRate : this.bitRate,
|
||||
sampleRate : this.sampleRate,
|
||||
format : this.format
|
||||
})
|
||||
},
|
||||
customAction(record) {
|
||||
if (this.customCallback) {
|
||||
this.customCallback(record)
|
||||
} else {
|
||||
console.log(record)
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
attemptsLeft () {
|
||||
return this.attempts - this.recorder.records.length
|
||||
return this.attempts - this.recordList.length
|
||||
},
|
||||
iconButtonType () {
|
||||
return this.isRecording && this.isPause ? 'mic' : this.isRecording ? 'pause' : 'mic'
|
||||
@@ -324,20 +378,12 @@
|
||||
isRecording () {
|
||||
return this.recorder.isRecording
|
||||
},
|
||||
message () {
|
||||
return this.uploadStatus === 'success' ? this.successfulUploadMsg : this.failedUploadMsg
|
||||
},
|
||||
recordedTime () {
|
||||
if (this.time && this.recorder.duration >= this.time * 60) {
|
||||
this.stopRecorder()
|
||||
}
|
||||
return convertTimeMMSS(this.recorder.duration)
|
||||
},
|
||||
uploadStatusClasses () {
|
||||
let classes = ['ar__upload-status']
|
||||
classes.push(this.uploadStatus === 'success' ? 'ar__upload-status--success' : 'ar__upload-status--fail')
|
||||
return classes.join(' ')
|
||||
},
|
||||
volume () {
|
||||
return parseFloat(this.recorder.volume)
|
||||
}
|
||||
|
||||
43
src/components/uploader.vue
Normal file
43
src/components/uploader.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<style lang="scss">
|
||||
@import '../scss/icons';
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<icon-button name="save" class="ar-icon ar-icon__xs ar-icon--no-border" @click.native="upload"/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import IconButton from './icon-button'
|
||||
import UploaderPropsMixin from '@/mixins/uploader-props'
|
||||
|
||||
export default {
|
||||
mixins: [UploaderPropsMixin],
|
||||
props: {
|
||||
record: { type: Object }
|
||||
},
|
||||
components: {
|
||||
IconButton
|
||||
},
|
||||
methods: {
|
||||
upload () {
|
||||
if (!this.record.url) {
|
||||
return
|
||||
}
|
||||
|
||||
this.$eventBus.$emit('start-upload')
|
||||
|
||||
const data = new FormData()
|
||||
data.append('audio', this.record.blob, `${this.filename}.mp3`)
|
||||
|
||||
const headers = Object.assign(this.headers, {})
|
||||
headers['Content-Type'] = `multipart/form-data; boundary=${data._boundary}`
|
||||
|
||||
this.$http.post(this.uploadUrl, data, { headers: headers }).then(resp => {
|
||||
this.$eventBus.$emit('end-upload', { status: 'success', response: resp })
|
||||
}).catch(error => {
|
||||
this.$eventBus.$emit('end-upload', { status: 'fail', response: error })
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -37,8 +37,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import IconButton from './icon-button.vue'
|
||||
import LineControl from './line-control.vue'
|
||||
import IconButton from './icon-button'
|
||||
import LineControl from './line-control'
|
||||
|
||||
export default {
|
||||
data () {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import AudioPlayer from './components/player.vue'
|
||||
import AudioRecorder from './components/recorder.vue'
|
||||
import AudioPlayer from '@/components/player.vue'
|
||||
import AudioRecorder from '@/components/recorder.vue'
|
||||
|
||||
const components = {
|
||||
AudioPlayer,
|
||||
@@ -12,6 +12,8 @@ const components = {
|
||||
|
||||
this.installed = true
|
||||
|
||||
Vue.prototype.$eventBus = Vue.prototype.$eventBus || new Vue
|
||||
|
||||
Vue.component('audio-player', AudioPlayer)
|
||||
Vue.component('audio-recorder', AudioRecorder)
|
||||
}
|
||||
|
||||
49
src/lib/mp3-encoder.js
Normal file
49
src/lib/mp3-encoder.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import { Mp3Encoder } from 'lamejs'
|
||||
|
||||
export default class {
|
||||
constructor(config) {
|
||||
this.bitRate = config.bitRate
|
||||
this.sampleRate = config.sampleRate
|
||||
this.dataBuffer = []
|
||||
this.encoder = new Mp3Encoder(1, this.sampleRate, this.bitRate)
|
||||
}
|
||||
|
||||
encode(arrayBuffer) {
|
||||
const maxSamples = 1152
|
||||
const samples = this._convertBuffer(arrayBuffer)
|
||||
let remaining = samples.length
|
||||
|
||||
for (let i = 0; remaining >= 0; i += maxSamples) {
|
||||
const left = samples.subarray(i, i + maxSamples)
|
||||
const buffer = this.encoder.encodeBuffer(left)
|
||||
this.dataBuffer.push(new Int8Array(buffer))
|
||||
remaining -= maxSamples
|
||||
}
|
||||
}
|
||||
|
||||
finish() {
|
||||
this.dataBuffer.push(this.encoder.flush())
|
||||
const blob = new Blob(this.dataBuffer, { type: 'audio/mp3' })
|
||||
this.dataBuffer = []
|
||||
|
||||
return {
|
||||
id : Date.now(),
|
||||
blob : blob,
|
||||
url : URL.createObjectURL(blob)
|
||||
}
|
||||
}
|
||||
|
||||
_floatTo16BitPCM(input, output) {
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
const s = Math.max(-1, Math.min(1, input[i]))
|
||||
output[i] = (s < 0 ? s * 0x8000 : s * 0x7FFF)
|
||||
}
|
||||
}
|
||||
|
||||
_convertBuffer(arrayBuffer) {
|
||||
const data = new Float32Array(arrayBuffer)
|
||||
const out = new Int16Array(arrayBuffer.length)
|
||||
this._floatTo16BitPCM(data, out)
|
||||
return out
|
||||
}
|
||||
}
|
||||
@@ -1,30 +1,56 @@
|
||||
import Mp3Encoder from './mp3-encoder'
|
||||
import WavEncoder from './wav-encoder'
|
||||
import { convertTimeMMSS } from './utils'
|
||||
|
||||
export default class {
|
||||
constructor (options = {}) {
|
||||
this.afterStop = options.afterStop
|
||||
this.micFailed = options.micFailed
|
||||
this.beforeRecording = options.beforeRecording
|
||||
this.pauseRecording = options.pauseRecording
|
||||
this.afterRecording = options.afterRecording
|
||||
this.micFailed = options.micFailed
|
||||
this.format = options.format
|
||||
|
||||
this.encoderOptions = {
|
||||
bitRate : options.bitRate,
|
||||
sampleRate : options.sampleRate
|
||||
}
|
||||
|
||||
this.bufferSize = 4096
|
||||
this.records = []
|
||||
this.samples = []
|
||||
|
||||
this.isPause = false
|
||||
this.isRecording = false
|
||||
this.isPause = false
|
||||
this.isRecording = false
|
||||
|
||||
this.duration = 0
|
||||
this.volume = 0
|
||||
|
||||
this.wavSamples = []
|
||||
|
||||
this._duration = 0
|
||||
}
|
||||
|
||||
start () {
|
||||
navigator.mediaDevices.getUserMedia({audio: true})
|
||||
.then(this._micCaptured.bind(this))
|
||||
.catch(this._micError.bind(this))
|
||||
const constraints = {
|
||||
video: false,
|
||||
audio: {
|
||||
channelCount: 1,
|
||||
echoCancellation: false
|
||||
}
|
||||
}
|
||||
|
||||
this.beforeRecording && this.beforeRecording('start recording')
|
||||
|
||||
navigator.mediaDevices
|
||||
.getUserMedia(constraints)
|
||||
.then(this._micCaptured.bind(this))
|
||||
.catch(this._micError.bind(this))
|
||||
|
||||
this.isPause = false
|
||||
this.isRecording = true
|
||||
|
||||
if (this._isMp3() && !this.lameEncoder) {
|
||||
this.lameEncoder = new Mp3Encoder(this.encoderOptions)
|
||||
}
|
||||
}
|
||||
|
||||
stop () {
|
||||
@@ -33,23 +59,22 @@ export default class {
|
||||
this.processor.disconnect()
|
||||
this.context.close()
|
||||
|
||||
let encoder = new WavEncoder({
|
||||
bufferSize : this.bufferSize,
|
||||
sampleRate : this.context.sampleRate,
|
||||
samples : this.samples
|
||||
})
|
||||
let record = null
|
||||
|
||||
let audioBlob = encoder.getData()
|
||||
let audioUrl = URL.createObjectURL(audioBlob)
|
||||
if (this._isMp3()) {
|
||||
record = this.lameEncoder.finish()
|
||||
} else {
|
||||
let wavEncoder = new WavEncoder({
|
||||
bufferSize : this.bufferSize,
|
||||
sampleRate : this.encoderOptions.sampleRate,
|
||||
samples : this.wavSamples
|
||||
})
|
||||
record = wavEncoder.finish()
|
||||
this.wavSamples = []
|
||||
}
|
||||
|
||||
this.samples = []
|
||||
|
||||
this.records.push({
|
||||
id : Date.now(),
|
||||
blob : audioBlob,
|
||||
duration : convertTimeMMSS(this.duration),
|
||||
url : audioUrl
|
||||
})
|
||||
record.duration = convertTimeMMSS(this.duration)
|
||||
this.records.push(record)
|
||||
|
||||
this._duration = 0
|
||||
this.duration = 0
|
||||
@@ -57,19 +82,18 @@ export default class {
|
||||
this.isPause = false
|
||||
this.isRecording = false
|
||||
|
||||
if (this.afterStop) {
|
||||
this.afterStop()
|
||||
}
|
||||
this.afterRecording && this.afterRecording(record)
|
||||
}
|
||||
|
||||
pause () {
|
||||
this.stream.getTracks().forEach((track) => track.stop())
|
||||
this.input.disconnect()
|
||||
this.processor.disconnect()
|
||||
this.context.close()
|
||||
|
||||
this._duration = this.duration
|
||||
this.isPause = true
|
||||
|
||||
this.pauseRecording && this.pauseRecording('pause recording')
|
||||
}
|
||||
|
||||
recordList () {
|
||||
@@ -77,7 +101,7 @@ export default class {
|
||||
}
|
||||
|
||||
lastRecord () {
|
||||
return this.records.slice(-1)
|
||||
return this.records.slice(-1).pop()
|
||||
}
|
||||
|
||||
_micCaptured (stream) {
|
||||
@@ -88,8 +112,14 @@ export default class {
|
||||
this.stream = stream
|
||||
|
||||
this.processor.onaudioprocess = (ev) => {
|
||||
let sample = ev.inputBuffer.getChannelData(0)
|
||||
let sum = 0.0
|
||||
const sample = ev.inputBuffer.getChannelData(0)
|
||||
let sum = 0.0
|
||||
|
||||
if (this._isMp3()) {
|
||||
this.lameEncoder.encode(sample)
|
||||
} else {
|
||||
this.wavSamples.push(new Float32Array(sample))
|
||||
}
|
||||
|
||||
for (let i = 0; i < sample.length; ++i) {
|
||||
sum += sample[i] * sample[i]
|
||||
@@ -97,7 +127,6 @@ export default class {
|
||||
|
||||
this.duration = parseFloat(this._duration) + parseFloat(this.context.currentTime.toFixed(2))
|
||||
this.volume = Math.sqrt(sum / sample.length).toFixed(2)
|
||||
this.samples.push(new Float32Array(sample))
|
||||
}
|
||||
|
||||
this.input.connect(this.processor)
|
||||
@@ -105,8 +134,10 @@ export default class {
|
||||
}
|
||||
|
||||
_micError (error) {
|
||||
if (this.micFailed) {
|
||||
this.micFailed(error)
|
||||
}
|
||||
this.micFailed && this.micFailed(error)
|
||||
}
|
||||
|
||||
_isMp3 () {
|
||||
return this.format.toLowerCase() === 'mp3'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export function calculateLineHeadPosition (ev, element) {
|
||||
let progressWidth = element.getBoundingClientRect().width
|
||||
let leftPosition = ev.target.getBoundingClientRect().left
|
||||
const progressWidth = element.getBoundingClientRect().width
|
||||
const leftPosition = ev.target.getBoundingClientRect().left
|
||||
let pos = (ev.clientX - leftPosition) / progressWidth
|
||||
|
||||
try {
|
||||
|
||||
@@ -5,7 +5,7 @@ export default class {
|
||||
this.samples = options.samples
|
||||
}
|
||||
|
||||
getData () {
|
||||
finish () {
|
||||
this._joinSamples()
|
||||
|
||||
let buffer = new ArrayBuffer(44 + this.samples.length * 2)
|
||||
@@ -27,7 +27,13 @@ export default class {
|
||||
|
||||
this._floatTo16BitPCM(view, 44, this.samples)
|
||||
|
||||
return new Blob([view], {type: 'audio/wav'})
|
||||
const blob = new Blob([view], {type: 'audio/wav'})
|
||||
|
||||
return {
|
||||
id : Date.now(),
|
||||
blob : blob,
|
||||
url : URL.createObjectURL(blob)
|
||||
}
|
||||
}
|
||||
|
||||
_floatTo16BitPCM (output, offset, input) {
|
||||
|
||||
8
src/mixins/uploader-props.js
Normal file
8
src/mixins/uploader-props.js
Normal file
@@ -0,0 +1,8 @@
|
||||
export default {
|
||||
props: {
|
||||
filename : { type: String, default: 'record' },
|
||||
format : { type: String, default: 'mp3' },
|
||||
headers : { type: Object, default: () => ({}) },
|
||||
uploadUrl : { type: String }
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,16 @@
|
||||
cursor: pointer;
|
||||
transition: .2s;
|
||||
|
||||
&:hover {
|
||||
fill: #505050;
|
||||
}
|
||||
|
||||
&--no-border {
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&--rec {
|
||||
fill: white;
|
||||
background-color: #FF6B64;
|
||||
@@ -15,7 +25,6 @@
|
||||
|
||||
&--pulse {
|
||||
animation: ripple .5s linear infinite;
|
||||
|
||||
@keyframes ripple {
|
||||
0% {
|
||||
box-shadow:
|
||||
@@ -32,14 +41,22 @@
|
||||
}
|
||||
}
|
||||
|
||||
&__xs {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
&__sm {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
&__lg {
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
line-height: 45px;
|
||||
box-shadow: 0 2px 5px 1px rgba(158,158,158,0.5);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,67 +1,70 @@
|
||||
const webpack = require('webpack')
|
||||
const merge = require('webpack-merge')
|
||||
const env = `./webpack.${process.env.NODE_ENV === 'production' ? 'prod' : 'dev'}.js`
|
||||
const path = require('path')
|
||||
|
||||
module.exports = merge(require(env), {
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.scss$/,
|
||||
use: [
|
||||
'vue-style-loader',
|
||||
'css-loader',
|
||||
'sass-loader'
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.vue$/,
|
||||
loader: 'vue-loader',
|
||||
options: {
|
||||
loaders: {
|
||||
'scss': [
|
||||
'vue-style-loader',
|
||||
'css-loader',
|
||||
'sass-loader'
|
||||
]
|
||||
module.exports = (env, args) => {
|
||||
let conf = `./webpack.${env.NODE_ENV === 'production' ? 'prod' : 'dev'}.js`
|
||||
|
||||
return merge(require(conf), {
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.scss$/,
|
||||
use: [
|
||||
'vue-style-loader',
|
||||
'css-loader',
|
||||
'sass-loader'
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.vue$/,
|
||||
loader: 'vue-loader',
|
||||
options: {
|
||||
loaders: {
|
||||
'scss': [
|
||||
'vue-style-loader',
|
||||
'css-loader',
|
||||
'sass-loader'
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
loader: 'babel-loader',
|
||||
exclude: /node_modules/
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpg|gif|svg)$/,
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
name: '[name].[ext]?[hash]'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
loader: 'babel-loader',
|
||||
exclude: /node_modules/
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpg|gif|svg)$/,
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
name: '[name].[ext]?[hash]'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'vue$': 'vue/dist/vue.esm.js',
|
||||
'@': path.resolve(__dirname, 'src')
|
||||
]
|
||||
},
|
||||
extensions: ['*', '.js', '.vue', '.json']
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': {
|
||||
NODE_ENV: `"${process.env.NODE_ENV}"`
|
||||
resolve: {
|
||||
alias: {
|
||||
'vue$': 'vue/dist/vue.esm.js',
|
||||
'@': path.resolve(__dirname, 'src')
|
||||
},
|
||||
VERSION: JSON.stringify(require("./package.json").version)
|
||||
}),
|
||||
],
|
||||
devServer: {
|
||||
historyApiFallback: true,
|
||||
noInfo: true,
|
||||
overlay: true
|
||||
},
|
||||
performance: {
|
||||
hints: false
|
||||
}
|
||||
})
|
||||
extensions: ['*', '.js', '.vue', '.json']
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': {
|
||||
NODE_ENV: `"${process.env.NODE_ENV}"`
|
||||
},
|
||||
VERSION: JSON.stringify(require("./package.json").version)
|
||||
}),
|
||||
],
|
||||
devServer: {
|
||||
historyApiFallback: true,
|
||||
noInfo: true,
|
||||
overlay: true
|
||||
},
|
||||
performance: {
|
||||
hints: false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ const path = require('path')
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||
|
||||
module.exports = {
|
||||
devtool: '#eval-source-map',
|
||||
entry: './demo/index.js',
|
||||
output: {
|
||||
path: path.resolve(__dirname, './demo')
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
const path = require('path')
|
||||
const webpack = require('webpack')
|
||||
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
|
||||
|
||||
module.exports = {
|
||||
devtool: '#source-map',
|
||||
entry: './src/index.js',
|
||||
entry: {
|
||||
main: './src/index.js'
|
||||
},
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
filename: 'vue-audio-recorder.min.js',
|
||||
@@ -12,18 +15,21 @@ module.exports = {
|
||||
libraryExport: 'default',
|
||||
umdNamedDefine: true
|
||||
},
|
||||
optimization: {
|
||||
minimizer: [
|
||||
new UglifyJsPlugin({
|
||||
cache: true,
|
||||
parallel: true,
|
||||
sourceMap: true
|
||||
})
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': {
|
||||
NODE_ENV: '"production"'
|
||||
}
|
||||
}),
|
||||
new webpack.optimize.UglifyJsPlugin({
|
||||
sourceMap: true,
|
||||
compress: {
|
||||
warnings: true
|
||||
}
|
||||
}),
|
||||
new webpack.LoaderOptionsPlugin({
|
||||
minimize: false
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user