629 lines
14 KiB
Vue
629 lines
14 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
|
|
},
|
|
formatType: {
|
|
default: 'mp3', // 生成语音文件类型
|
|
type: String
|
|
},
|
|
});
|
|
const {
|
|
isDisabled,
|
|
onlyVoice,
|
|
adjustPosition,
|
|
formatType
|
|
} = toRefs(props);
|
|
const isLoadingState = computed(() => props.isLoading);
|
|
|
|
const showTalkingModel = ref(false);
|
|
const keyboardIsShow = ref(false);
|
|
const talkModelRef = ref();
|
|
storageStore.setTouchBtnZIndex(12);
|
|
const btnZindex = computed(() => storageStore.getTouchBtnZIndex);
|
|
|
|
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: formatType.value
|
|
});
|
|
};
|
|
|
|
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?.();
|
|
|
|
});
|
|
// 录音结束
|
|
|
|
// 记录 按下移动位置
|
|
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 hasPermission = ref(false)
|
|
|
|
const getRecordPermission = (obj) => {
|
|
if (isDisabled.value) return;
|
|
if (isTalking.value) {
|
|
common.msg('操作频繁,请稍后重试!');
|
|
setTimeout(() => {
|
|
isTalking.value = false;
|
|
}, 500);
|
|
return;
|
|
}
|
|
if(!hasPermission.value){
|
|
let _res = getPermission()
|
|
if (_res) {
|
|
hasPermission.value = true
|
|
showOverlayTalking(obj);
|
|
}
|
|
}else{
|
|
showOverlayTalking(obj);
|
|
}
|
|
};
|
|
const getPermission = async () => {
|
|
//#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();
|
|
}
|
|
});
|
|
return false
|
|
} else {
|
|
return true
|
|
}
|
|
} else {
|
|
let _recordPerm = uni.getAppAuthorizeSetting();
|
|
let _state = _recordPerm.microphoneAuthorized;
|
|
if (_state === 'denied') {
|
|
common.show('提示信息', '当前页面需要使用的录音权限,是否前往开启?', true, '取消', '确定').then((res) => {
|
|
if (res) {
|
|
// 去开启权限
|
|
uni.openAppAuthorizeSetting();
|
|
} else {
|
|
// 取消
|
|
common.navigateBack();
|
|
}
|
|
});
|
|
return false
|
|
} else if (_state === 'not determined') {
|
|
// startRecord()
|
|
return true
|
|
}
|
|
}
|
|
// #endif
|
|
//#ifndef APP-PLUS
|
|
return true
|
|
// #endif
|
|
}
|
|
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;
|
|
let _recnum = recNum.value
|
|
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(() => {
|
|
console.log(_recnum, recNum.value)
|
|
if (_recnum === recNum.value) {
|
|
hideOverlayTalking(recNum.value);
|
|
}
|
|
}, 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, 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> |