587 lines
13 KiB
Vue
587 lines
13 KiB
Vue
<template>
|
|
<view class="talk-btns" ref="talkBtnRef">
|
|
<view class="touch-btn" @touchstart="getRecordPermission" @touchend="hideOverlayTalking" @touchmove="handleMove"
|
|
v-if="!keyboardIsShow">按住说话
|
|
</view>
|
|
<view class="touch-btn" v-if="keyboardIsShow">
|
|
<uv-input class="input-box" ref="inputRef" v-model="answerValue" :maxlength="props.textValueLength"
|
|
:adjustPosition="adjustPosition" placeholderStyle='color:#999999;font-size:26rpx' placeholder="请输入文字">
|
|
</uv-input>
|
|
</view>
|
|
<view class="talk-btn-box" v-show="!onlyVoice">
|
|
<image class="talk-btn-icon" v-show="!keyboardIsShow" src="/src/static/images/course/jianpan.png" mode=""
|
|
@click="changeBtn"></image>
|
|
<image class="talk-btn-icon" v-show="keyboardIsShow && !answerValue" src="/src/static/images/course/record.png"
|
|
mode="" @click="changeBtn"></image>
|
|
<image class="talk-btn-icon" v-show="keyboardIsShow && !!answerValue" src="/src/static/images/course/send.png"
|
|
mode="" @click.stop="showKeyboard"></image>
|
|
</view>
|
|
<uni-popup ref="talkModelRef" type="bottom" @click="showTalkingModel = false" mask-background-color="rgba(0,0,0,0.9)">
|
|
<view class="overlay-box">
|
|
<view class="circle-bg-box">
|
|
<view class="icon-box">
|
|
<image src="/src/static/images/course/talk-icon.png" mode=""></image>
|
|
</view>
|
|
</view>
|
|
<view class="tips-box">松开发送</view>
|
|
<view class="btns-box">
|
|
<view :class="['close-btn',isOut?'is-out':'']">取消
|
|
<image class="btn-box" v-if="!isOut" src="/src/static/images/course/close.png" mode=""></image>
|
|
<image class="btn-box" v-else src="/src/static/images/course/close-active.png" mode=""></image>
|
|
</view>
|
|
</view>
|
|
<view :class="['talk-dialog',isOut?'is-out':'']">
|
|
<image src="@/static/images/course/voice-white.gif" mode="" class="gif-box"></image>
|
|
</view>
|
|
</view>
|
|
</uni-popup>
|
|
</view>
|
|
|
|
</template>
|
|
|
|
<script setup>
|
|
import {
|
|
toRefs,
|
|
ref,
|
|
computed,
|
|
unref,
|
|
onMounted,
|
|
onUnmounted,
|
|
nextTick,
|
|
watch,
|
|
getCurrentInstance
|
|
} from 'vue';
|
|
import common from '../../../common/common';
|
|
import permission from '@/common/permission.js'
|
|
import {
|
|
getPhoneEnvBool
|
|
} from '@/common/common.js'
|
|
import { useStorageStore } from "@/store/storage.js"
|
|
|
|
const storageStore = useStorageStore()
|
|
const proxy = getCurrentInstance()
|
|
const emit = defineEmits(['uploadVoice', 'submitAnswer', 'startRecord'])
|
|
const props = defineProps({
|
|
isDisabled: {
|
|
default: true,
|
|
type: Boolean
|
|
},
|
|
isLoading: {
|
|
default: false,
|
|
type: Boolean
|
|
},
|
|
loadingText: {
|
|
default: '发送回答中,请稍后再试!',
|
|
type: String
|
|
},
|
|
onlyVoice: {
|
|
default: false,
|
|
type: Boolean
|
|
},
|
|
canAnswer: {
|
|
default: true,
|
|
type: Boolean
|
|
},
|
|
canAnswerText: {
|
|
default: '当前状态不能发送内容!',
|
|
type: String
|
|
},
|
|
textValueLength: {
|
|
type: Number,
|
|
default: 500
|
|
},
|
|
adjustPosition: {
|
|
default: false,
|
|
type: Boolean
|
|
},
|
|
})
|
|
const {
|
|
isDisabled,
|
|
onlyVoice,
|
|
adjustPosition
|
|
} = toRefs(props)
|
|
const isLoadingState = computed(() => props.isLoading)
|
|
|
|
const showTalkingModel = ref(false)
|
|
const keyboardIsShow = ref(false)
|
|
const talkModelRef= ref()
|
|
|
|
const btnZindex = computed(()=> storageStore.getTouchBtnZIndex)
|
|
console.log(storageStore)
|
|
watch(() => showTalkingModel.value, (val) => val ? talkModelRef.value.open() : talkModelRef.value.close())
|
|
watch(() => props.isLoading, () => {}, {
|
|
deep: true,
|
|
immediate: true
|
|
})
|
|
watch(() => props.canAnswer, () => {}, {
|
|
deep: true,
|
|
immediate: true
|
|
})
|
|
watch(() => props.onlyVoice, () => {}, {
|
|
deep: true,
|
|
immediate: true
|
|
})
|
|
watch(() => props.isDisabled, (val) => {
|
|
if (val) {
|
|
keyboardIsShow.value = false
|
|
}
|
|
}, {
|
|
deep: true,
|
|
immediate: true
|
|
})
|
|
|
|
|
|
const answerValue = ref('')
|
|
const talkBtnRef = ref()
|
|
// 录音
|
|
const audioPath = ref('')
|
|
const audioObj = uni.createInnerAudioContext()
|
|
//#ifdef APP-PLUS
|
|
// app端
|
|
const recordObjCom = ref({})
|
|
const IsAndEnv = getPhoneEnvBool('AND')
|
|
const isStartNum = ref(0)
|
|
const isStopNum = ref(0)
|
|
const initRecord = () => {
|
|
recordObjCom.value = uni.getRecorderManager()
|
|
unref(recordObjCom)?.onStop((res, duration) => {
|
|
audioPath.value = res.tempFilePath
|
|
if (!isOut.value) {
|
|
if (startFlag.value !== 2) {
|
|
// setTimeout(() => {
|
|
// showTalkingModel.value = false
|
|
// nextTick(() => {
|
|
// // showTalkingModel.value = false
|
|
// common.msg('说话时间太短,没有听清!')
|
|
// })
|
|
// },100)
|
|
closeModel(() => {
|
|
nextTick(() => {
|
|
// showTalkingModel.value = false
|
|
common.msg('说话时间太短,没有听清!')
|
|
})
|
|
})
|
|
} else {
|
|
closeModel()
|
|
emit('uploadVoice', audioPath.value)
|
|
}
|
|
} else {
|
|
closeModel()
|
|
}
|
|
})
|
|
unref(recordObjCom).onStart(() => {
|
|
startFlag.value = 1
|
|
setTimeout(() => {
|
|
startFlag.value = 2
|
|
}, 1000)
|
|
let _recordPerm = uni.getAppAuthorizeSetting()
|
|
let _state = _recordPerm.microphoneAuthorized
|
|
console.log(isStopNum.value === isStartNum.value, _state)
|
|
if (_state === 'authorized') {
|
|
if (isStopNum.value === isStartNum.value) {
|
|
stopRecord()
|
|
} else {
|
|
showTalkingModel.value = true
|
|
}
|
|
}
|
|
})
|
|
}
|
|
const startFlag = ref(1)
|
|
// const canStart = ref(true)
|
|
const startRecord = () => {
|
|
isStartNum.value += 1
|
|
unref(recordObjCom).start({
|
|
format: 'mp3'
|
|
})
|
|
}
|
|
|
|
const stopRecord = () => {
|
|
isStopNum.value = isStartNum.value
|
|
try {
|
|
if (unref(recordObjCom).stop) {
|
|
unref(recordObjCom).stop()
|
|
} else {
|
|
closeModel()
|
|
}
|
|
} catch {
|
|
closeModel()
|
|
}
|
|
// setTimeout(() => {
|
|
// canStart.value = true
|
|
// }, 1000)
|
|
}
|
|
const closeModel = (cb = () => {}) => {
|
|
isStopNum.value = isStartNum.value
|
|
setTimeout(() => {
|
|
showTalkingModel.value = false
|
|
cb && cb()
|
|
}, 350)
|
|
setTimeout(() => {
|
|
isTalking.value = false
|
|
}, 800)
|
|
}
|
|
// #endif
|
|
//#ifndef APP-PLUS
|
|
const recordObjCom = ref({})
|
|
const initRecord = () => {}
|
|
// #endif
|
|
onMounted(async () => {
|
|
initRecord?.()
|
|
//#ifdef APP-PLUS
|
|
// 安卓端
|
|
if (IsAndEnv) {
|
|
let _recordPerm = await permission.requestAndroidPermission('android.permission.RECORD_AUDIO')
|
|
if (_recordPerm !== 1) {
|
|
common.show('提示信息', '当前页面需要使用的录音权限,是否前往开启?', true, '取消', '确定').then((res) => {
|
|
if (res) {
|
|
// 去开启权限
|
|
permission.gotoAppPermissionSetting()
|
|
} else {
|
|
// 取消
|
|
common.navigateBack()
|
|
}
|
|
})
|
|
}
|
|
} else {
|
|
let _recordPerm = uni.getAppAuthorizeSetting()
|
|
let _state = _recordPerm.microphoneAuthorized
|
|
if (_state === 'denied') {
|
|
common.show('提示信息', '当前页面需要使用的录音权限,是否前往开启?', true, '取消', '确定').then((res) => {
|
|
if (res) {
|
|
// 去开启权限
|
|
uni.openAppAuthorizeSetting()
|
|
} else {
|
|
// 取消
|
|
common.navigateBack()
|
|
}
|
|
})
|
|
} else if (_state === 'not determined') {
|
|
// startRecord()
|
|
}
|
|
}
|
|
// #endif
|
|
// setTimeout(()=> {talkModelRef.value.open()}, 2000)
|
|
|
|
})
|
|
// 录音结束
|
|
|
|
// 记录 按下移动位置
|
|
const touchX = ref(0)
|
|
const touchY = ref(0)
|
|
const baseY = ref(0)
|
|
const windowInfo = uni.getWindowInfo()
|
|
// 计算是否超出按下盒子位置
|
|
const isOut = computed(() => {
|
|
let _btnHeight = windowInfo.screenWidth / 750 * 234
|
|
let moveVal = (windowInfo.screenHeight - touchY.value) > _btnHeight
|
|
return moveVal
|
|
})
|
|
const outScreen = computed(() => {
|
|
(windowInfo.screenHeight - touchY.value) <= 0
|
|
})
|
|
const recNum = ref(1)
|
|
const activeNum = ref(1)
|
|
|
|
const getRecordPermission = (obj) => {
|
|
if(isDisabled.value) return
|
|
if (isTalking.value) {
|
|
common.msg('操作频繁,请稍后重试!')
|
|
setTimeout(() => {
|
|
isTalking.value = false
|
|
}, 500)
|
|
return
|
|
}
|
|
showOverlayTalking(obj)
|
|
}
|
|
const isMoving = ref(false)
|
|
const isTalking = ref(false)
|
|
const showOverlayTalking = (obj) => {
|
|
if (showTalkingModel.value) return
|
|
if (isLoadingState.value) {
|
|
uni.showToast({
|
|
title: props.loadingText,
|
|
icon: "none",
|
|
duration: 1000
|
|
})
|
|
return
|
|
}
|
|
if (!props.canAnswer) {
|
|
uni.showToast({
|
|
title: props.canAnswerText,
|
|
icon: "none",
|
|
duration: 1000
|
|
})
|
|
return
|
|
}
|
|
// if(!canStart.value){
|
|
// return
|
|
// }
|
|
isMoving.value = true
|
|
isTalking.value = true
|
|
emit('startRecord')
|
|
recNum.value += 1
|
|
if (!isDisabled.value) {
|
|
touchX.value = obj.changedTouches[0].pageX
|
|
touchY.value = obj.changedTouches[0].pageY
|
|
baseY.value = obj.currentTarget.offsetTop
|
|
//#ifdef APP-PLUS
|
|
// canStart.value = false
|
|
startRecord()
|
|
// #endif
|
|
setTimeout(() => {
|
|
hideOverlayTalking()
|
|
}, 59 * 1010)
|
|
} else {
|
|
showTalkingModel.value = false
|
|
uni.showToast({
|
|
title: '暂无问题需要回答!',
|
|
icon: "none",
|
|
duration: 1000
|
|
})
|
|
}
|
|
}
|
|
const hideOverlayTalking = (obj) => {
|
|
isMoving.value = false
|
|
if (!showTalkingModel.value) return
|
|
//#ifdef APP-PLUS
|
|
stopRecord()
|
|
// #endif
|
|
}
|
|
|
|
const handleMove = (obj) => {
|
|
if (isMoving.value) {
|
|
touchX.value = obj.changedTouches[0].pageX
|
|
touchY.value = obj.changedTouches[0].pageY
|
|
if (outScreen.value) {
|
|
hideOverlayTalking(obj)
|
|
}
|
|
}
|
|
}
|
|
// 发送文本
|
|
const showKeyboard = () => {
|
|
if (isLoadingState.value) {
|
|
uni.showToast({
|
|
title: props.loadingText,
|
|
icon: "none",
|
|
duration: 1000
|
|
})
|
|
return
|
|
}
|
|
if (!isDisabled.value) {
|
|
if (keyboardIsShow.value) {
|
|
if (answerValue.value.trim()) {
|
|
emit(
|
|
'submitAnswer',
|
|
answerValue.value,
|
|
changeBtn
|
|
)
|
|
} else {
|
|
// keyboardIsShow.value = !keyboardIsShow.value
|
|
common.msg("不能发送空白消息!")
|
|
}
|
|
} else {
|
|
keyboardIsShow.value = !keyboardIsShow.value
|
|
}
|
|
} else {
|
|
uni.showToast({
|
|
title: '暂无问题需要回答!',
|
|
icon: "none",
|
|
duration: 1000
|
|
})
|
|
}
|
|
}
|
|
const changeBtn = () => {
|
|
if (props.isDisabled) return
|
|
keyboardIsShow.value = !keyboardIsShow.value
|
|
answerValue.value = ''
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.talk-btns {
|
|
height: 150rpx;
|
|
background-color: #fff;
|
|
padding: 25rpx 30rpx 0;
|
|
position: relative;
|
|
z-index: v-bind(btnZindex);
|
|
.touch-btn {
|
|
height: 92rpx;
|
|
box-shadow: 0 8rpx 20rpx 0 rgba(0, 0, 0, .06);
|
|
text-align: center;
|
|
line-height: 92rpx;
|
|
color: #000;
|
|
font-weight: 400;
|
|
position: relative;
|
|
|
|
.input-box {
|
|
width: 100%;
|
|
height: 92rpx;
|
|
line-height: 92rpx;
|
|
text-align: left;
|
|
box-sizing: border-box;
|
|
padding: 0 40rpx 0 40rpx;
|
|
}
|
|
}
|
|
|
|
.talk-btn-box {
|
|
position: absolute;
|
|
height: 100%;
|
|
width: 120rpx;
|
|
right: 0;
|
|
top: 0;
|
|
|
|
.talk-btn-icon {
|
|
width: 40rpx;
|
|
height: 40rpx;
|
|
position: absolute;
|
|
right: 60rpx;
|
|
top: 50%;
|
|
margin-top: -20rpx;
|
|
}
|
|
}
|
|
}
|
|
|
|
.overlay-box {
|
|
display: flex;
|
|
flex-direction: column-reverse;
|
|
// justify-content: flex-end;
|
|
width: 100%;
|
|
height: 100%;
|
|
|
|
.circle-bg-box {
|
|
height: 234rpx;
|
|
background: url(@/static/images/course/talking.png) no-repeat;
|
|
background-size: cover;
|
|
position: relative;
|
|
|
|
.icon-box {
|
|
position: absolute;
|
|
left: 50%;
|
|
top: 50%;
|
|
margin-left: -17rpx;
|
|
margin-top: -24rpx;
|
|
width: 34rpx;
|
|
height: 48rpx;
|
|
|
|
image {
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
}
|
|
}
|
|
|
|
.tips-box {
|
|
height: 40rpx;
|
|
line-height: 40rpx;
|
|
color: #c0c0c0;
|
|
text-align: center;
|
|
margin: 12rpx 0 24rpx;
|
|
}
|
|
|
|
.btns-box {
|
|
height: 270rpx;
|
|
// padding: 0 120rpx;
|
|
width: 154rpx;
|
|
margin: 0 auto 100rpx;
|
|
overflow: hidden;
|
|
font-size: 28rpx;
|
|
text-align: center;
|
|
line-height: 116rpx;
|
|
|
|
.close-btn {
|
|
float: left;
|
|
height: 100%;
|
|
width: 154rpx;
|
|
overflow: hidden;
|
|
color: #999;
|
|
|
|
&.is-out {
|
|
color: #fff;
|
|
}
|
|
}
|
|
|
|
.translate-btn {
|
|
float: right;
|
|
height: 100%;
|
|
width: 154rpx;
|
|
overflow: hidden;
|
|
color: #999;
|
|
}
|
|
|
|
.btn-box {
|
|
width: 100%;
|
|
height: 154rpx;
|
|
}
|
|
}
|
|
|
|
.talk-dialog {
|
|
width: 506rpx;
|
|
height: 234rpx;
|
|
margin: 0 auto;
|
|
background-color: #06f;
|
|
border-radius: 32rpx;
|
|
position: relative;
|
|
|
|
&.is-out {
|
|
background-color: #ff3e49;
|
|
|
|
&::after {
|
|
background-color: #ff3e49;
|
|
}
|
|
}
|
|
|
|
.gif-box {
|
|
position: absolute;
|
|
width: 200rpx;
|
|
height: 80rpx;
|
|
left: 50%;
|
|
top: 50%;
|
|
transform: translate(-50%, -50%);
|
|
}
|
|
|
|
&::after {
|
|
content: '';
|
|
width: 44rpx;
|
|
height: 44rpx;
|
|
clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%);
|
|
background-color: #06f;
|
|
overflow: hidden;
|
|
position: absolute;
|
|
bottom: -22rpx;
|
|
left: 50%;
|
|
margin-left: -22rpx;
|
|
}
|
|
}
|
|
|
|
.white-bg-box {
|
|
height: 365rpx;
|
|
background-color: #fff;
|
|
overflow: hidden;
|
|
border-radius: 32rpx 32rpx 0 0;
|
|
padding: 58rpx 36rpx;
|
|
|
|
.box-title {
|
|
font-size: 32rpx;
|
|
font-weight: bold;
|
|
line-height: 46rpx;
|
|
margin-bottom: 46rpx;
|
|
}
|
|
|
|
.box-btns {
|
|
height: 46rpx;
|
|
line-height: 46rpx;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
color: #999;
|
|
|
|
.is-active {
|
|
color: #06f;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
::v-deep .uv-input__content__field-wrapper__field {
|
|
padding-right: 60rpx;
|
|
}
|
|
</style> |