JC
1 year ago
29 changed files with 1628 additions and 315 deletions
@ -0,0 +1,53 @@ |
|||
<template> |
|||
<view> |
|||
<h-audio |
|||
v-if="audioPath" |
|||
:audio="audioPath" |
|||
:time="duration" |
|||
sliderColor="#f44336" |
|||
activeColor="#f44336" |
|||
backgroundColor="#fff" |
|||
/> |
|||
<slot></slot> |
|||
<nb-voice-record |
|||
@startRecord="start" |
|||
@endRecord="end" |
|||
@cancelRecord="cancel" |
|||
> |
|||
</nb-voice-record> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import HAudio from '@/components/h-custom-audio/index.vue'; |
|||
export default { |
|||
name: 'audio-recorder', |
|||
components: { |
|||
HAudio |
|||
}, |
|||
data() { |
|||
return { |
|||
audioPath: null, |
|||
duration: 0 |
|||
} |
|||
}, |
|||
methods: { |
|||
start() { |
|||
// 开始录音 |
|||
}, |
|||
end(event) { |
|||
// 结束录音并处理得到的录音文件 |
|||
// event中,app端仅有tempFilePath字段,微信小程序还有duration和fileSize两个字段 |
|||
this.audioPath = event.tempFilePath; |
|||
this.duration = event.duration / 1000; |
|||
this.$emit('end', this.audioPath); |
|||
}, |
|||
cancel() { |
|||
// 用户取消录音 |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss"> |
|||
</style> |
@ -0,0 +1,219 @@ |
|||
<template> |
|||
<view :class="['audio-card', 'audio-toolbar']"> |
|||
<view v-if="title" class="title">{{ title }}</view> |
|||
<view class="audio"> |
|||
<image |
|||
v-if="!isPlay" |
|||
class="play-icon" |
|||
mode="scaleToFill" |
|||
:src="playIcon" |
|||
@click="audioPlay"/> |
|||
<image |
|||
v-if="isPlay" |
|||
class="play-icon" |
|||
mode="scaleToFill" |
|||
:src="pauseIcon" |
|||
@click="audioPause"/> |
|||
<view class="audio-main"> |
|||
<slider |
|||
max="100" |
|||
block-size="16" |
|||
class="progress" |
|||
:block-color="sliderColor" |
|||
:activeColor="activeColor" |
|||
:backgroundColor="backgroundColor" |
|||
:value="progress" |
|||
@change="handleChange" |
|||
@changing="handleProgress"/> |
|||
<view class="time"> |
|||
<view>{{ dateFormart(currentTime) }}</view> |
|||
<view>{{ dateFormart(duration) }}</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
props: { |
|||
classname: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
audio: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
time: { |
|||
type: Number, |
|||
default: 0 |
|||
}, |
|||
title: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
playIcon: { |
|||
type: String, |
|||
default: require('@/static/icons/play.svg') |
|||
}, |
|||
pauseIcon: { |
|||
type: String, |
|||
default: require('@/static/icons/pause.svg') |
|||
}, |
|||
sliderColor: { |
|||
type: String, |
|||
default: '#009EFF' |
|||
}, |
|||
activeColor: { |
|||
type: String, |
|||
default: '#009EFF' |
|||
}, |
|||
backgroundColor: { |
|||
type: String, |
|||
default: '#EFEFEF' |
|||
} |
|||
}, |
|||
data () { |
|||
return { |
|||
innerAudioContext: null, |
|||
isPlay: false, |
|||
timer: null, |
|||
currentTime: 0, |
|||
duration: 0, |
|||
progress: 0 |
|||
} |
|||
}, |
|||
watch: { |
|||
audio: { |
|||
handler(val) { |
|||
this.creatAudio(); |
|||
this.progress = 0; |
|||
this.currentTime = 0; |
|||
}, |
|||
deep: true, |
|||
immediate: true |
|||
} |
|||
}, |
|||
destroyed () { |
|||
this.innerAudioContext.destroy() |
|||
}, |
|||
methods: { |
|||
creatAudio () { |
|||
this.innerAudioContext = uni.createInnerAudioContext() |
|||
this.innerAudioContext.src = encodeURI(this.audio) |
|||
this.innerAudioContext.sessionCategory = "soloAmbient" |
|||
this.innerAudioContext.startTime = 0 |
|||
this.innerAudioContext.onTimeUpdate(() => {}) |
|||
this.innerAudioContext.onCanplay(() => { |
|||
this.duration = this.innerAudioContext.duration || this.time |
|||
}) |
|||
this.innerAudioContext.play() |
|||
this.innerAudioContext.volume = 0 |
|||
setTimeout(() => { |
|||
this.innerAudioContext.pause() |
|||
this.innerAudioContext.volume = 1 |
|||
}, 100) |
|||
}, |
|||
// 开始播放 |
|||
audioPlay () { |
|||
this.innerAudioContext.src = encodeURI(this.audio) |
|||
this.innerAudioContext.sessionCategory = "soloAmbient" |
|||
this.innerAudioContext.startTime = Number(this.progress) === 100 ? 0 : (this.currentTime ? this.currentTime : 0) |
|||
this.innerAudioContext.play() |
|||
this.onCanplay() |
|||
this.isPlay = true |
|||
}, |
|||
onCanplay () { |
|||
this.innerAudioContext.onCanplay(() => { |
|||
this.currentTime = this.innerAudioContext.currentTime |
|||
}) |
|||
this.innerAudioContext.onTimeUpdate(() => { |
|||
this.currentTime = this.innerAudioContext.currentTime |
|||
this.progress = (this.innerAudioContext.currentTime / this.innerAudioContext.duration * 100).toFixed(2) |
|||
}) |
|||
this.innerAudioContext.onEnded(() => { |
|||
if (this.isPlay) { |
|||
this.isPlay = false |
|||
} |
|||
}) |
|||
}, |
|||
// 暂停播放 |
|||
audioPause () { |
|||
this.innerAudioContext.pause() |
|||
this.isPlay = !this.isPlay |
|||
}, |
|||
// 拖动进度条 |
|||
handleProgress (event) { |
|||
this.innerAudioContext.pause() |
|||
}, |
|||
// 拖动进度条结束 |
|||
handleChange (event) { |
|||
this.progress = event.detail.value |
|||
this.currentTime = this.duration * (event.detail.value / 100) |
|||
this.audioPlay() |
|||
}, |
|||
// 处理时间格式(总时长装换时分秒) |
|||
dateFormart (value) { |
|||
value = Number(value).toFixed(0) |
|||
let hour = Math.floor(value / 3600) |
|||
let minute = Math.floor((value - hour * 3600) / 60) |
|||
let second = value - hour * 3600 - minute * 60 |
|||
hour = hour < 10 ? `0${hour}` : hour |
|||
minute = minute < 10 ? `0${minute}` : minute |
|||
second = second < 10 ? `0${Math.floor(second)}` : Math.floor(second) |
|||
return `${minute}:${second}` |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
.audio-card { |
|||
height: 100rpx; |
|||
font-family: PingFangSC-Medium, PingFang SC; |
|||
display: flex; |
|||
flex-direction: column; |
|||
padding: 20rpx 24rpx; |
|||
background-color: #F4F5F6; |
|||
border-top: 1px solid #bbb; |
|||
.title { |
|||
color: #333333; |
|||
font-size: 26rpx; |
|||
font-weight: 500; |
|||
} |
|||
.audio { |
|||
margin-top: 15rpx; |
|||
display: flex; |
|||
.play-icon { |
|||
width: 40rpx; |
|||
height: 40rpx; |
|||
} |
|||
.audio-main { |
|||
height: 48rpx; |
|||
flex: 1; |
|||
margin-left: 15rpx; |
|||
} |
|||
.progress { |
|||
margin: 0; |
|||
} |
|||
.time { |
|||
width: 100%; |
|||
height: 50rpx; |
|||
font-size: 24rpx; |
|||
font-weight: 400; |
|||
color: #666666; |
|||
margin-top: -15rpx; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
} |
|||
} |
|||
} |
|||
.audio-toolbar{ |
|||
position: fixed; |
|||
left: 0; |
|||
right: 0; |
|||
bottom: 200rpx; |
|||
} |
|||
</style> |
@ -1,10 +1,14 @@ |
|||
page { |
|||
-webkit-overflow-scrolling: touch; /* 使iOS滚动流畅 */ |
|||
} |
|||
|
|||
.mescroll-body { |
|||
position: relative; /* 下拉刷新区域相对自身定位 */ |
|||
height: auto; /* 不可固定高度,否则overflow: hidden, 可通过设置最小高度使列表不满屏仍可下拉*/ |
|||
height: auto; /* 不可固定高度,否则overflow:hidden导致无法滑动; 同时使设置的最小高生效,实现列表不满屏仍可下拉*/ |
|||
overflow: hidden; /* 遮住顶部下拉刷新区域 */ |
|||
box-sizing: border-box; /* 避免设置padding出现双滚动条的问题 */ |
|||
} |
|||
|
|||
/* 适配 iPhoneX */ |
|||
@supports (bottom: constant(safe-area-inset-bottom)) or (bottom: env(safe-area-inset-bottom)) { |
|||
.mescroll-safearea { |
|||
padding-bottom: constant(safe-area-inset-bottom); |
|||
padding-bottom: env(safe-area-inset-bottom); |
|||
} |
|||
} |
After Width: | Height: | Size: 475 B |
After Width: | Height: | Size: 471 B |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 1.7 KiB |
@ -0,0 +1,34 @@ |
|||
## 1.0.8(2022-10-24) |
|||
### 修复小程序平台报错 |
|||
## 1.0.7(2022-10-14) |
|||
### 修改获取权限时机 |
|||
1.修改获取权限时机,避免由于未获得权限导致首次长按时无法取消录音; |
|||
2.优化APP端提示文案(APP端触发longpress事件后,如果手指没有任何移动,则此时松手无法监听到touchEnd事件,小程序端无此问题) |
|||
## 1.0.6(2022-09-26) |
|||
### 优化震动反馈 |
|||
## 1.0.5(2022-09-25) |
|||
### 新增录音配置recordOptions |
|||
- 该配置各端支持情况不同,请自行查看官方说明 |
|||
- 其中duration为录音时长(最大10分钟),在超限时将自动结束动画并返回录音文件地址 |
|||
## 1.0.4(2022-09-25) |
|||
### 内置发起录音 |
|||
- H5端不支持录音,故无法使用,有需要可自行在gitee拉取老的纯动画版本 |
|||
- 已支持多端录音,不再仅是动画效果 |
|||
- 多端自动判断是否拥有权限(无权限时进行toast提示) |
|||
- endRecord回调附带录音文件临时地址(详见下方示例) |
|||
## 1.0.3(2022-09-25) |
|||
### 增加震动反馈 |
|||
|
|||
- 已条件编译、增加支持微信小程序 |
|||
## 1.0.2(2022-09-25) |
|||
### 增加震动反馈 |
|||
|
|||
- 已条件编译、仅支持app |
|||
## 1.0.1(2022-09-25) |
|||
### 新增主动通知组件结束方法 |
|||
|
|||
- 如:当录音时长超限时,可主动通知组件结束动画,此时组件会自动回调endRecord事件,正常处理即可。 |
|||
## 1.0.0(2022-09-25) |
|||
### 首次发布 |
|||
|
|||
- 下边写不确定的只是没测试,请自行测试 |
@ -0,0 +1,425 @@ |
|||
<template> |
|||
<view> |
|||
<view |
|||
class="record-btn" |
|||
@longpress="startRecord" |
|||
@touchstart="touchStart" |
|||
@touchmove="touchMove" |
|||
@touchend="endRecord" |
|||
hover-class="record-btn-hover" |
|||
hover-start-time="200" |
|||
hover-stay-time="150" |
|||
:style="[btnStyle, { '--btn-hover-fontcolor': btnHoverFontcolor, '--btn-hover-bgcolor': btnHoverBgcolor }]" |
|||
> |
|||
{{ btnTextContent }} |
|||
</view> |
|||
<view |
|||
class="record-popup" |
|||
:style="{ '--popup-height': popupHeight, '--popup-width': upx2px(popupMaxWidth), '--popup-bottom': upx2px(popupFixBottom), '--popup-bg-color': popupBgColor }" |
|||
> |
|||
<view class="inner-content" v-if="recordPopupShow"> |
|||
<view class="title">{{ popupTitle }}</view> |
|||
<view |
|||
class="voice-line-wrap" |
|||
v-if="recording" |
|||
:style="{ '--line-height': upx2px(lineHeight), '--line-start-color': lineStartColor, '--line-end-color': lineEndColor }" |
|||
> |
|||
<view class="voice-line one"></view> |
|||
<view class="voice-line two"></view> |
|||
<view class="voice-line three"></view> |
|||
<view class="voice-line four"></view> |
|||
<view class="voice-line five"></view> |
|||
<view class="voice-line six"></view> |
|||
<view class="voice-line seven"></view> |
|||
<view class="voice-line six"></view> |
|||
<view class="voice-line five"></view> |
|||
<view class="voice-line four"></view> |
|||
<view class="voice-line three"></view> |
|||
<view class="voice-line two"></view> |
|||
<view class="voice-line one"></view> |
|||
</view> |
|||
<view class="cancel-icon" v-if="!recording">+</view> |
|||
<view class="tips">{{ recording ? popupDefaultTips : popupCancelTips }}</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
<script> |
|||
var that; |
|||
const recorderManager = uni.getRecorderManager(); |
|||
// #ifdef APP-PLUS |
|||
// 引入权限判断 |
|||
import permision from '../../js_sdk/wa-permission/permission.js'; |
|||
// #endif |
|||
export default { |
|||
name: 'nbVoiceRecord', |
|||
/** |
|||
* 录音交互动效组件 |
|||
* @property {Object} recordOptions 录音配置 |
|||
* @property {Object} btnStyle 按钮样式 |
|||
* @property {Object} btnHoverFontcolor 按钮长按时字体颜色 |
|||
* @property {String} btnHoverBgcolor 按钮长按时背景颜色 |
|||
* @property {String} btnDefaultText 按钮初始文字 |
|||
* @property {String} btnRecordingText 录制时按钮文字 |
|||
* @property {Boolean} vibrate 弹窗时是否震动 |
|||
* @property {String} popupTitle 弹窗顶部文字 |
|||
* @property {String} popupDefaultTips 录制时弹窗底部提示 |
|||
* @property {String} popupCancelTips 滑动取消时弹窗底部提示 |
|||
* @property {String} popupMaxWidth 弹窗展开后宽度 |
|||
* @property {String} popupMaxHeight 弹窗展开后高度 |
|||
* @property {String} popupFixBottom 弹窗展开后距底部高度 |
|||
* @property {String} popupBgColor 弹窗背景颜色 |
|||
* @property {String} lineHeight 声波高度 |
|||
* @property {String} lineStartColor 声波波谷时颜色色值 |
|||
* @property {String} lineEndColor 声波波峰时颜色色值 |
|||
* @event {Function} startRecord 开始录音回调 |
|||
* @event {Function} endRecord 结束录音回调 |
|||
* @event {Function} cancelRecord 滑动取消录音回调 |
|||
* @event {Function} stopRecord 主动停止录音 |
|||
*/ |
|||
props: { |
|||
recordOptions: { |
|||
type: Object, |
|||
default() { |
|||
return { |
|||
duration: 600000 |
|||
}; // 请自行查看各端的的支持情况,这里全部使用默认配置 |
|||
} |
|||
}, |
|||
btnStyle: { |
|||
type: Object, |
|||
default() { |
|||
return { |
|||
width: '580rpx', |
|||
height: '180rpx', |
|||
borderRadius: '20rpx', |
|||
backgroundColor: '#fff', |
|||
border: '1rpx solid whitesmoke', |
|||
permisionState: false |
|||
}; |
|||
} |
|||
}, |
|||
btnHoverFontcolor: { |
|||
type: String, |
|||
default: '#000' // 颜色名称或16进制色值 |
|||
}, |
|||
btnHoverBgcolor: { |
|||
type: String, |
|||
default: 'whitesmoke' // 颜色名称或16进制色值 |
|||
}, |
|||
btnDefaultText: { |
|||
type: String, |
|||
default: '长按开始录音' |
|||
}, |
|||
btnRecordingText: { |
|||
type: String, |
|||
default: '录音中' |
|||
}, |
|||
vibrate: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
popupTitle: { |
|||
type: String, |
|||
default: '正在录制音频' |
|||
}, |
|||
popupDefaultTips: { |
|||
type: String, |
|||
default: '松手完成录音' |
|||
}, |
|||
popupCancelTips: { |
|||
type: String, |
|||
default: '松手取消录音' |
|||
}, |
|||
popupMaxWidth: { |
|||
type: Number, |
|||
default: 600 // 单位为rpx |
|||
}, |
|||
popupMaxHeight: { |
|||
type: Number, |
|||
default: 180 // 单位为rpx |
|||
}, |
|||
popupFixBottom: { |
|||
type: Number, |
|||
default: 300 // 单位为rpx |
|||
}, |
|||
popupBgColor: { |
|||
type: String, |
|||
default: 'whitesmoke' |
|||
}, |
|||
lineHeight: { |
|||
type: Number, |
|||
default: 50 // 单位为rpx |
|||
}, |
|||
lineStartColor: { |
|||
type: String, |
|||
default: 'royalblue' // 颜色名称或16进制色值 |
|||
}, |
|||
lineEndColor: { |
|||
type: String, |
|||
default: 'indianred' // 颜色名称或16进制色值 |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
stopStatus: false, // 是否已被父页面通知主动结束录音 |
|||
btnTextContent: this.btnDefaultText, |
|||
startTouchData: {}, |
|||
popupHeight: '0px', // 这是初始的高度 |
|||
recording: true, // 录音中 |
|||
recordPopupShow: false, |
|||
recordTimeout: null // 录音定时器 |
|||
}; |
|||
}, |
|||
created() { |
|||
that = this; |
|||
|
|||
// 请求权限 |
|||
this.checkPermission(); |
|||
|
|||
recorderManager.onStop(res => { |
|||
// 判断是否用户主动结束录音 |
|||
if (that.recordTimeout !== null) { |
|||
// 延时未结束,则是主动结束录音 |
|||
clearTimeout(that.recordTimeout); |
|||
that.recordTimeout = null; // 恢复状态 |
|||
} |
|||
|
|||
// 继续判断是否为取消录音 |
|||
if (that.recording) { |
|||
that.$emit('endRecord', res); |
|||
} else { |
|||
// 用户向上滑动,此时松手后响应的是取消录音的回调 |
|||
that.recording = true; // 恢复状态 |
|||
that.$emit('cancelRecord'); |
|||
} |
|||
}); |
|||
|
|||
recorderManager.onError(err => { |
|||
console.log('err:', err); |
|||
}); |
|||
}, |
|||
computed: {}, |
|||
methods: { |
|||
upx2px(upx) { |
|||
return uni.upx2px(upx) + 'px'; |
|||
}, |
|||
async checkPermission() { |
|||
// #ifdef APP-PLUS |
|||
// 先判断os |
|||
let os = uni.getSystemInfoSync().osName; |
|||
if (os == 'ios') { |
|||
this.permisionState = await permision.judgeIosPermission('record'); |
|||
} else { |
|||
this.permisionState = await permision.requestAndroidPermission('android.permission.RECORD_AUDIO'); |
|||
} |
|||
if (this.permisionState !== true && this.permisionState !== 1) { |
|||
uni.showToast({ |
|||
title: '请先授权使用录音', |
|||
icon: 'none' |
|||
}); |
|||
return; |
|||
} |
|||
// #endif |
|||
|
|||
// #ifndef APP-PLUS |
|||
uni.authorize({ |
|||
scope: 'scope.record', |
|||
success: () => { |
|||
this.permisionState = true; |
|||
}, |
|||
fail() { |
|||
uni.showToast({ |
|||
title: '请授权使用录音', |
|||
icon: 'none' |
|||
}); |
|||
} |
|||
}); |
|||
// #endif |
|||
}, |
|||
|
|||
startRecord() { |
|||
if (!this.permisionState) { |
|||
this.checkPermission(); |
|||
return; |
|||
} |
|||
this.stopStatus = false; |
|||
setTimeout(() => { |
|||
this.popupHeight = this.upx2px(this.popupMaxHeight); |
|||
setTimeout(() => { |
|||
this.recordPopupShow = true; |
|||
this.btnTextContent = this.btnRecordingText; |
|||
if (this.vibrate) { |
|||
// #ifdef APP-PLUS |
|||
plus.device.vibrate(35); |
|||
// #endif |
|||
// #ifdef MP-WEIXIN |
|||
uni.vibrateShort(); |
|||
// #endif |
|||
} |
|||
// 开始录音 |
|||
recorderManager.start(this.recordOptions); |
|||
// 设置定时器 |
|||
this.recordTimeout = setTimeout( |
|||
() => { |
|||
// 如果定时器先结束,则说明此时录音时间超限 |
|||
this.stopRecord(); // 结束录音动画(实际上录音的end回调已经先执行) |
|||
this.recordTimeout = null; // 重置 |
|||
}, |
|||
this.recordOptions.duration ? this.recordOptions.duration : 600000 |
|||
); |
|||
|
|||
this.$emit('startRecord'); |
|||
}, 100); |
|||
}, 200); |
|||
}, |
|||
endRecord() { |
|||
if (this.stopStatus) { |
|||
return; |
|||
} |
|||
this.popupHeight = '0px'; |
|||
this.recordPopupShow = false; |
|||
this.btnTextContent = this.btnDefaultText; |
|||
recorderManager.stop(); |
|||
}, |
|||
stopRecord() { |
|||
// 用法如你录音限制了时间,那么将在结束时强制停止组件的显示 |
|||
this.endRecord(); |
|||
this.stopStatus = true; |
|||
}, |
|||
touchStart(e) { |
|||
this.startTouchData.clientX = e.changedTouches[0].clientX; //手指按下时的X坐标 |
|||
this.startTouchData.clientY = e.changedTouches[0].clientY; //手指按下时的Y坐标 |
|||
}, |
|||
touchMove(e) { |
|||
let touchData = e.touches[0]; //滑动过程中,手指滑动的坐标信息 返回的是Objcet对象 |
|||
let moveX = touchData.clientX - this.startTouchData.clientX; |
|||
let moveY = touchData.clientY - this.startTouchData.clientY; |
|||
if (moveY < -50) { |
|||
if (this.vibrate && this.recording) { |
|||
// #ifdef APP-PLUS |
|||
plus.device.vibrate(35); |
|||
// #endif |
|||
// #ifdef MP-WEIXIN |
|||
uni.vibrateShort(); |
|||
// #endif |
|||
} |
|||
this.recording = false; |
|||
} else { |
|||
this.recording = true; |
|||
} |
|||
} |
|||
} |
|||
}; |
|||
</script> |
|||
<style lang="scss"> |
|||
.record-btn { |
|||
color: #000; |
|||
font-size: 24rpx; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
transition: 0.25s all; |
|||
border: 1rpx solid whitesmoke; |
|||
} |
|||
|
|||
.record-btn-hover { |
|||
color: var(--btn-hover-fontcolor) !important; |
|||
background-color: var(--btn-hover-bgcolor) !important; |
|||
} |
|||
|
|||
.record-popup { |
|||
// position: absolute; |
|||
// bottom: var(--popup-bottom); |
|||
// left: calc(50vw - calc(var(--popup-width) / 2)); |
|||
z-index: 1; |
|||
width: var(--popup-width); |
|||
height: var(--popup-height); |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
border-radius: 10rpx; |
|||
// box-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1); |
|||
background: var(--popup-bg-color); |
|||
color: #000; |
|||
transition: 0.2s height; |
|||
position: relative; |
|||
top: -100rpx; |
|||
|
|||
.inner-content { |
|||
height: var(--popup-height); |
|||
font-size: 24rpx; |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
|
|||
.title { |
|||
font-weight: bold; |
|||
padding: 20rpx 0; |
|||
} |
|||
.tips { |
|||
color: #999; |
|||
padding: 20rpx 0; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.cancel-icon { |
|||
width: 100rpx; |
|||
height: 100rpx; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
color: #fff; |
|||
font-size: 44rpx; |
|||
line-height: 44rpx; |
|||
background-color: pink; |
|||
border-radius: 50%; |
|||
transform: rotate(45deg); |
|||
} |
|||
|
|||
.voice-line-wrap { |
|||
display: flex; |
|||
align-items: center; |
|||
.voice-line { |
|||
width: 5rpx; |
|||
height: var(--line-height); |
|||
border-radius: 3rpx; |
|||
margin: 0 5rpx; |
|||
} |
|||
|
|||
.one { |
|||
animation: wave 0.4s 1s linear infinite alternate; |
|||
} |
|||
.two { |
|||
animation: wave 0.4s 0.9s linear infinite alternate; |
|||
} |
|||
.three { |
|||
animation: wave 0.4s 0.8s linear infinite alternate; |
|||
} |
|||
.four { |
|||
animation: wave 0.4s 0.7s linear infinite alternate; |
|||
} |
|||
.five { |
|||
animation: wave 0.4s 0.6s linear infinite alternate; |
|||
} |
|||
.six { |
|||
animation: wave 0.4s 0.5s linear infinite alternate; |
|||
} |
|||
.seven { |
|||
animation: wave 0.4s linear infinite alternate; |
|||
} |
|||
} |
|||
|
|||
@keyframes wave { |
|||
0% { |
|||
transform: scale(1, 1); |
|||
background-color: var(--line-start-color); |
|||
} |
|||
100% { |
|||
transform: scale(1, 0.2); |
|||
background-color: var(--line-end-color); |
|||
} |
|||
} |
|||
</style> |
@ -0,0 +1,272 @@ |
|||
/** |
|||
* 本模块封装了Android、iOS的应用权限判断、打开应用权限设置界面、以及位置系统服务是否开启 |
|||
*/ |
|||
|
|||
var isIos |
|||
// #ifdef APP-PLUS
|
|||
isIos = (plus.os.name == "iOS") |
|||
// #endif
|
|||
|
|||
// 判断推送权限是否开启
|
|||
function judgeIosPermissionPush() { |
|||
var result = false; |
|||
var UIApplication = plus.ios.import("UIApplication"); |
|||
var app = UIApplication.sharedApplication(); |
|||
var enabledTypes = 0; |
|||
if (app.currentUserNotificationSettings) { |
|||
var settings = app.currentUserNotificationSettings(); |
|||
enabledTypes = settings.plusGetAttribute("types"); |
|||
console.log("enabledTypes1:" + enabledTypes); |
|||
if (enabledTypes == 0) { |
|||
console.log("推送权限没有开启"); |
|||
} else { |
|||
result = true; |
|||
console.log("已经开启推送功能!") |
|||
} |
|||
plus.ios.deleteObject(settings); |
|||
} else { |
|||
enabledTypes = app.enabledRemoteNotificationTypes(); |
|||
if (enabledTypes == 0) { |
|||
console.log("推送权限没有开启!"); |
|||
} else { |
|||
result = true; |
|||
console.log("已经开启推送功能!") |
|||
} |
|||
console.log("enabledTypes2:" + enabledTypes); |
|||
} |
|||
plus.ios.deleteObject(app); |
|||
plus.ios.deleteObject(UIApplication); |
|||
return result; |
|||
} |
|||
|
|||
// 判断定位权限是否开启
|
|||
function judgeIosPermissionLocation() { |
|||
var result = false; |
|||
var cllocationManger = plus.ios.import("CLLocationManager"); |
|||
var status = cllocationManger.authorizationStatus(); |
|||
result = (status != 2) |
|||
console.log("定位权限开启:" + result); |
|||
// 以下代码判断了手机设备的定位是否关闭,推荐另行使用方法 checkSystemEnableLocation
|
|||
/* var enable = cllocationManger.locationServicesEnabled(); |
|||
var status = cllocationManger.authorizationStatus(); |
|||
console.log("enable:" + enable); |
|||
console.log("status:" + status); |
|||
if (enable && status != 2) { |
|||
result = true; |
|||
console.log("手机定位服务已开启且已授予定位权限"); |
|||
} else { |
|||
console.log("手机系统的定位没有打开或未给予定位权限"); |
|||
} */ |
|||
plus.ios.deleteObject(cllocationManger); |
|||
return result; |
|||
} |
|||
|
|||
// 判断麦克风权限是否开启
|
|||
function judgeIosPermissionRecord() { |
|||
var result = false; |
|||
var avaudiosession = plus.ios.import("AVAudioSession"); |
|||
var avaudio = avaudiosession.sharedInstance(); |
|||
var permissionStatus = avaudio.recordPermission(); |
|||
console.log("permissionStatus:" + permissionStatus); |
|||
if (permissionStatus == 1684369017 || permissionStatus == 1970168948) { |
|||
console.log("麦克风权限没有开启"); |
|||
} else { |
|||
result = true; |
|||
console.log("麦克风权限已经开启"); |
|||
} |
|||
plus.ios.deleteObject(avaudiosession); |
|||
return result; |
|||
} |
|||
|
|||
// 判断相机权限是否开启
|
|||
function judgeIosPermissionCamera() { |
|||
var result = false; |
|||
var AVCaptureDevice = plus.ios.import("AVCaptureDevice"); |
|||
var authStatus = AVCaptureDevice.authorizationStatusForMediaType('vide'); |
|||
console.log("authStatus:" + authStatus); |
|||
if (authStatus == 3) { |
|||
result = true; |
|||
console.log("相机权限已经开启"); |
|||
} else { |
|||
console.log("相机权限没有开启"); |
|||
} |
|||
plus.ios.deleteObject(AVCaptureDevice); |
|||
return result; |
|||
} |
|||
|
|||
// 判断相册权限是否开启
|
|||
function judgeIosPermissionPhotoLibrary() { |
|||
var result = false; |
|||
var PHPhotoLibrary = plus.ios.import("PHPhotoLibrary"); |
|||
var authStatus = PHPhotoLibrary.authorizationStatus(); |
|||
console.log("authStatus:" + authStatus); |
|||
if (authStatus == 3) { |
|||
result = true; |
|||
console.log("相册权限已经开启"); |
|||
} else { |
|||
console.log("相册权限没有开启"); |
|||
} |
|||
plus.ios.deleteObject(PHPhotoLibrary); |
|||
return result; |
|||
} |
|||
|
|||
// 判断通讯录权限是否开启
|
|||
function judgeIosPermissionContact() { |
|||
var result = false; |
|||
var CNContactStore = plus.ios.import("CNContactStore"); |
|||
var cnAuthStatus = CNContactStore.authorizationStatusForEntityType(0); |
|||
if (cnAuthStatus == 3) { |
|||
result = true; |
|||
console.log("通讯录权限已经开启"); |
|||
} else { |
|||
console.log("通讯录权限没有开启"); |
|||
} |
|||
plus.ios.deleteObject(CNContactStore); |
|||
return result; |
|||
} |
|||
|
|||
// 判断日历权限是否开启
|
|||
function judgeIosPermissionCalendar() { |
|||
var result = false; |
|||
var EKEventStore = plus.ios.import("EKEventStore"); |
|||
var ekAuthStatus = EKEventStore.authorizationStatusForEntityType(0); |
|||
if (ekAuthStatus == 3) { |
|||
result = true; |
|||
console.log("日历权限已经开启"); |
|||
} else { |
|||
console.log("日历权限没有开启"); |
|||
} |
|||
plus.ios.deleteObject(EKEventStore); |
|||
return result; |
|||
} |
|||
|
|||
// 判断备忘录权限是否开启
|
|||
function judgeIosPermissionMemo() { |
|||
var result = false; |
|||
var EKEventStore = plus.ios.import("EKEventStore"); |
|||
var ekAuthStatus = EKEventStore.authorizationStatusForEntityType(1); |
|||
if (ekAuthStatus == 3) { |
|||
result = true; |
|||
console.log("备忘录权限已经开启"); |
|||
} else { |
|||
console.log("备忘录权限没有开启"); |
|||
} |
|||
plus.ios.deleteObject(EKEventStore); |
|||
return result; |
|||
} |
|||
|
|||
// Android权限查询
|
|||
function requestAndroidPermission(permissionID) { |
|||
return new Promise((resolve, reject) => { |
|||
plus.android.requestPermissions( |
|||
[permissionID], // 理论上支持多个权限同时查询,但实际上本函数封装只处理了一个权限的情况。有需要的可自行扩展封装
|
|||
function(resultObj) { |
|||
var result = 0; |
|||
for (var i = 0; i < resultObj.granted.length; i++) { |
|||
var grantedPermission = resultObj.granted[i]; |
|||
console.log('已获取的权限:' + grantedPermission); |
|||
result = 1 |
|||
} |
|||
for (var i = 0; i < resultObj.deniedPresent.length; i++) { |
|||
var deniedPresentPermission = resultObj.deniedPresent[i]; |
|||
console.log('拒绝本次申请的权限:' + deniedPresentPermission); |
|||
result = 0 |
|||
} |
|||
for (var i = 0; i < resultObj.deniedAlways.length; i++) { |
|||
var deniedAlwaysPermission = resultObj.deniedAlways[i]; |
|||
console.log('永久拒绝申请的权限:' + deniedAlwaysPermission); |
|||
result = -1 |
|||
} |
|||
resolve(result); |
|||
// 若所需权限被拒绝,则打开APP设置界面,可以在APP设置界面打开相应权限
|
|||
// if (result != 1) {
|
|||
// gotoAppPermissionSetting()
|
|||
// }
|
|||
}, |
|||
function(error) { |
|||
console.log('申请权限错误:' + error.code + " = " + error.message); |
|||
resolve({ |
|||
code: error.code, |
|||
message: error.message |
|||
}); |
|||
} |
|||
); |
|||
}); |
|||
} |
|||
|
|||
// 使用一个方法,根据参数判断权限
|
|||
function judgeIosPermission(permissionID) { |
|||
if (permissionID == "location") { |
|||
return judgeIosPermissionLocation() |
|||
} else if (permissionID == "camera") { |
|||
return judgeIosPermissionCamera() |
|||
} else if (permissionID == "photoLibrary") { |
|||
return judgeIosPermissionPhotoLibrary() |
|||
} else if (permissionID == "record") { |
|||
return judgeIosPermissionRecord() |
|||
} else if (permissionID == "push") { |
|||
return judgeIosPermissionPush() |
|||
} else if (permissionID == "contact") { |
|||
return judgeIosPermissionContact() |
|||
} else if (permissionID == "calendar") { |
|||
return judgeIosPermissionCalendar() |
|||
} else if (permissionID == "memo") { |
|||
return judgeIosPermissionMemo() |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
// 跳转到**应用**的权限页面
|
|||
function gotoAppPermissionSetting() { |
|||
if (isIos) { |
|||
var UIApplication = plus.ios.import("UIApplication"); |
|||
var application2 = UIApplication.sharedApplication(); |
|||
var NSURL2 = plus.ios.import("NSURL"); |
|||
// var setting2 = NSURL2.URLWithString("prefs:root=LOCATION_SERVICES");
|
|||
var setting2 = NSURL2.URLWithString("app-settings:"); |
|||
application2.openURL(setting2); |
|||
|
|||
plus.ios.deleteObject(setting2); |
|||
plus.ios.deleteObject(NSURL2); |
|||
plus.ios.deleteObject(application2); |
|||
} else { |
|||
// console.log(plus.device.vendor);
|
|||
var Intent = plus.android.importClass("android.content.Intent"); |
|||
var Settings = plus.android.importClass("android.provider.Settings"); |
|||
var Uri = plus.android.importClass("android.net.Uri"); |
|||
var mainActivity = plus.android.runtimeMainActivity(); |
|||
var intent = new Intent(); |
|||
intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); |
|||
var uri = Uri.fromParts("package", mainActivity.getPackageName(), null); |
|||
intent.setData(uri); |
|||
mainActivity.startActivity(intent); |
|||
} |
|||
} |
|||
|
|||
// 检查系统的设备服务是否开启
|
|||
// var checkSystemEnableLocation = async function () {
|
|||
function checkSystemEnableLocation() { |
|||
if (isIos) { |
|||
var result = false; |
|||
var cllocationManger = plus.ios.import("CLLocationManager"); |
|||
var result = cllocationManger.locationServicesEnabled(); |
|||
console.log("系统定位开启:" + result); |
|||
plus.ios.deleteObject(cllocationManger); |
|||
return result; |
|||
} else { |
|||
var context = plus.android.importClass("android.content.Context"); |
|||
var locationManager = plus.android.importClass("android.location.LocationManager"); |
|||
var main = plus.android.runtimeMainActivity(); |
|||
var mainSvr = main.getSystemService(context.LOCATION_SERVICE); |
|||
var result = mainSvr.isProviderEnabled(locationManager.GPS_PROVIDER); |
|||
console.log("系统定位开启:" + result); |
|||
return result |
|||
} |
|||
} |
|||
|
|||
module.exports = { |
|||
judgeIosPermission: judgeIosPermission, |
|||
requestAndroidPermission: requestAndroidPermission, |
|||
checkSystemEnableLocation: checkSystemEnableLocation, |
|||
gotoAppPermissionSetting: gotoAppPermissionSetting |
|||
} |
@ -0,0 +1,81 @@ |
|||
{ |
|||
"id": "nb-voice-record", |
|||
"displayName": "nbVoiceRecord长按录音动画组件,多端权限判断,可监听开始、结束、取消事件", |
|||
"version": "1.0.8", |
|||
"description": "无多余依赖、纯css动画,支持多端权限判断,自动发起录音并返回录音文件地址,自定义项目多达16个,基本满足你所有需求", |
|||
"keywords": [ |
|||
"长按,录音,动画组件" |
|||
], |
|||
"repository": "", |
|||
"engines": { |
|||
"HBuilderX": "^3.1.0" |
|||
}, |
|||
"dcloudext": { |
|||
"type": "component-vue", |
|||
"sale": { |
|||
"regular": { |
|||
"price": "0.00" |
|||
}, |
|||
"sourcecode": { |
|||
"price": "0.00" |
|||
} |
|||
}, |
|||
"contact": { |
|||
"qq": "" |
|||
}, |
|||
"declaration": { |
|||
"ads": "无", |
|||
"data": "无", |
|||
"permissions": "无" |
|||
}, |
|||
"npmurl": "https://gitee.com/imboya/nb-voice-record" |
|||
}, |
|||
"uni_modules": { |
|||
"dependencies": [], |
|||
"encrypt": [], |
|||
"platforms": { |
|||
"cloud": { |
|||
"tcb": "y", |
|||
"aliyun": "y" |
|||
}, |
|||
"client": { |
|||
"Vue": { |
|||
"vue2": "y", |
|||
"vue3": "u" |
|||
}, |
|||
"App": { |
|||
"app-vue": "y", |
|||
"app-nvue": "u" |
|||
}, |
|||
"H5-mobile": { |
|||
"Safari": "n", |
|||
"Android Browser": "n", |
|||
"微信浏览器(Android)": "n", |
|||
"QQ浏览器(Android)": "n" |
|||
}, |
|||
"H5-pc": { |
|||
"Chrome": "n", |
|||
"IE": "n", |
|||
"Edge": "n", |
|||
"Firefox": "n", |
|||
"Safari": "n" |
|||
}, |
|||
"小程序": { |
|||
"微信": "y", |
|||
"阿里": "u", |
|||
"百度": "u", |
|||
"字节跳动": "u", |
|||
"QQ": "u", |
|||
"钉钉": "u", |
|||
"快手": "u", |
|||
"飞书": "u", |
|||
"京东": "u" |
|||
}, |
|||
"快应用": { |
|||
"华为": "u", |
|||
"联盟": "u" |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,86 @@ |
|||
### nbVoiceRecord概述 |
|||
- 这是个基于uni-app 符合uni_modules 的插件 |
|||
- 无任何依赖、纯css动画 |
|||
- nb是NeverBug的意思 |
|||
|
|||
### 主要功能 |
|||
- 长按组件后弹出录音弹窗,松手完成录音,手指向上滑动可取消; |
|||
- 支持各种自定义,如弹窗高度、宽度、各处文字甚至声纹波形的尺寸和颜色; |
|||
- 已完成多端适配,自动根据授权情况提示完成授权、已获得授权才开始录音 |
|||
- endRecord回调事件附带录音文件 |
|||
|
|||
### 动画预览 |
|||
|
|||
- 默认样式 |
|||
|
|||
![默认样式](https://vkceyugu.cdn.bspapp.com/VKCEYUGU-613ff9e2-568b-4845-987f-93626e21bcde/84cf3c4f-f4f2-41e6-bb82-1414465a944d.gif) |
|||
|
|||
- 自定义按钮为圆形(红背景、白字)、弹窗为正方形 |
|||
|
|||
![正方形](https://vkceyugu.cdn.bspapp.com/VKCEYUGU-613ff9e2-568b-4845-987f-93626e21bcde/893bf1d6-593f-40e6-aeff-12afe4ebbc37.gif) |
|||
|
|||
### 基本用法: |
|||
|
|||
``` |
|||
<template> |
|||
<view> |
|||
<nb-voice-record @startRecord="start" @endRecord="end" @cancelRecord="cancel"></nb-voice-record> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
methods: { |
|||
start() { |
|||
// 开始录音 |
|||
}, |
|||
end(event) { |
|||
// 结束录音并处理得到的录音文件 |
|||
// event中,app端仅有tempFilePath字段,微信小程序还有duration和fileSize两个字段 |
|||
}, |
|||
cancel() { |
|||
// 用户取消录音 |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
``` |
|||
|
|||
### 全部支持参数 |
|||
|
|||
| 参数名 | 类型 | 默认值 | 作用 | 注意事项 | |
|||
| ----- | ----- | ------ | ------- | --- | |
|||
| recordOptions | Object | {duration:60000} | 录音配置 |各端支持情况不同,请自行查看[官方说明](https://uniapp.dcloud.net.cn/api/media/record-manager.html#getrecordermanager) | |
|||
| btnStyle | Object | 请查看源码 | 按钮样式 |对象格式 | |
|||
| btnHoverFontcolor | String | #000 | 按钮长按时文字颜色 | | |
|||
| btnHoverBgcolor | String | whitesmoke | 按钮长按时背景颜色 | | |
|||
| btnDefaultText | String | 长按开始录音 | 初始按钮文字 | | |
|||
| btnRecordingText | String | 录音中 | 录制时按钮文字 | | |
|||
| vibrate | Boolean | true | 震动反馈 | 弹窗、滑动取消时 | |
|||
| popupTitle | String | 正在录制音频 | 弹窗顶部文字 | | |
|||
| popupDefaultTips | String | 松手完成录音 | 录制时弹窗底部提示 | | |
|||
| popupCancelTips | String | 松手取消录音 | 滑动取消时弹窗底部提示 | | |
|||
| popupMaxWidth | Number | 600 | 弹窗展开后宽度 |注意这里几个单位都是rpx | |
|||
| popupMaxHeight | Number | 300 | 弹窗展开后高度 | | |
|||
| popupFixBottom | Number | 200 | 弹窗展开后距底部高度 | | |
|||
| popupBgColor | String | whitesmoke | 弹窗背景颜色 | | |
|||
| lineHeight | Number | 50 | 声波高度 | | |
|||
| lineStartColor | String | royalblue | 声波波谷时颜色色值 | 色值或者颜色名均可 | |
|||
| lineEndColor | String | indianred | 声波波峰时颜色色值 | | |
|||
|
|||
### 作者其他插件 |
|||
|
|||
- [bwinBrand多端自适应企业官网、uniCloud云端一体【用户端】](https://ext.dcloud.net.cn/plugin?id=7821) |
|||
- [bwinBrand多端自适应企业官网、uniCloud云端一体【管理端】](https://ext.dcloud.net.cn/plugin?id=7822) |
|||
- [bwinAgent多端、多项目全民经纪人、uniCloud云端一体【经纪人端】](https://ext.dcloud.net.cn/plugin?id=8606) |
|||
- [bwinAgent多端、多项目全民经纪人、uniCloud云端一体【管理员端】](https://ext.dcloud.net.cn/plugin?id=8607) |
|||
- [必闻优学,教育培训机构模板(单校区版,纯模板)](https://ext.dcloud.net.cn/plugin?id=7709) |
|||
|
|||
### 一个有趣的社区 |
|||
|
|||
- [NeverBug.cn 弹幕式互动社区](https://neverbug.cn) |
|||
|
|||
### 联系作者 |
|||
- QQ:123060128 |
|||
- Email:karma.zhao@gmail.com |
|||
- 官网:https://brand.neverbug.cn |
|||
|
Loading…
Reference in new issue