Files
tra-app/src/pages/course/components/touchBtn.vue
T
2025-09-12 14:27:18 +08:00

581 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'
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()
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: 0;
.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>