676 lines
22 KiB
Vue
676 lines
22 KiB
Vue
<template>
|
||
<view>
|
||
<view :class="['talk-btns-components', props.theme, { action: isRecording, inputMode: !isInputVisible }]">
|
||
<view class="touch-btn" @click="disabled_click">
|
||
<button
|
||
v-if="isInputVisible"
|
||
class="voice-btn"
|
||
@touchstart="startRecord"
|
||
@touchend="stopRecord()"
|
||
@touchmove="handleMove"
|
||
@touchcancel="stopRecord()"
|
||
:plain="true"
|
||
>
|
||
<slot>
|
||
<text class="voice-text">{{ isRecording ? '松开发送' : '按住说话' }}</text>
|
||
</slot>
|
||
</button>
|
||
<input
|
||
v-if="!isVoiceOnlyMode && !isInputVisible"
|
||
class="input-box"
|
||
ref="inputRef"
|
||
v-model="inputText"
|
||
:maxlength="props.maxlength"
|
||
:adjustPosition="props.adjustPosition"
|
||
placeholderStyle="color:#999999;font-size:26rpx"
|
||
placeholder="请输入文字"
|
||
:disabled="props.isDisabled"
|
||
@click="disabled_click"
|
||
/>
|
||
</view>
|
||
<view class="talk-right-icon-div" v-if="!isVoiceOnlyMode">
|
||
<image class="talk-right-icon" :src="talkRightIcon" mode="widthFix" @click="iconClick()"></image>
|
||
</view>
|
||
</view>
|
||
<uni-popup ref="popupRef" class="popup" @touchmove.stop.prevent="false">
|
||
|
||
<view class="overlay-box">
|
||
<view class="fl1"></view>
|
||
<view class="talk-dialog-box">
|
||
<view class="talk-dialog-div" :class="dialogDynamicClasses">
|
||
<view class="talk-dialog">
|
||
<image :src="TouchBtnImages.voiceWhiteBtn" mode="" class="gif-box"></image>
|
||
<uv-textarea
|
||
class="textarea"
|
||
ref="inputRef"
|
||
v-model="textAreaText"
|
||
:maxlength="props.maxlength"
|
||
@click.stop
|
||
customStyle=" background: #ffffff00;color: #fff; caret-color: #fff"
|
||
text-style="color: #fff"
|
||
border="none"
|
||
placeholder=""
|
||
:autoHeight="true"
|
||
:adjustPosition="false"
|
||
></uv-textarea>
|
||
<!-- autoHeight -->
|
||
<view class="error-tip-div">
|
||
<image :src="TouchBtnImages.iconRecognizeErrorSvg()" mode="aspectFill" class="img"></image>
|
||
<view class="" v-if="voice_to_text_status === 'failed'">未识别到文字</view>
|
||
<view class="" v-if="voice_to_text_status === 'networkError'">网络识别超时</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<!-- 第一形态两侧按钮 -->
|
||
<view class="page-box" :style="{ marginBottom: pageBoxMarginBottom }">
|
||
<view class="page-1" :class="[status, workMode]">
|
||
<view class="btns-box">
|
||
<!-- 左侧取消按钮 -->
|
||
<image
|
||
class="btn-box btn-box-1"
|
||
:src="isOutStatus == '02' ? TouchBtnImages.cancelActiveBtn : TouchBtnImages.cancelBtn"
|
||
mode="heightFix"
|
||
@click="closePopup()"
|
||
></image>
|
||
<!-- 右侧转文字按钮 -->
|
||
<image
|
||
class="btn-box btn-box-2"
|
||
:src="isOutStatus == '03' ? TouchBtnImages.translateActiveBtn : TouchBtnImages.translateBtn"
|
||
mode="heightFix"
|
||
></image>
|
||
<!-- 当仅语音的时候,取消按钮图案 -->
|
||
<image
|
||
class="btn-box-3"
|
||
:src="isOutStatus == '02' ? TouchBtnImages.closeActiveBtn : TouchBtnImages.closeBtn"
|
||
mode="heightFix"
|
||
@click="closePopup()"
|
||
></image>
|
||
</view>
|
||
<!-- 下方按钮 -->
|
||
<view class="circle-bg-box">
|
||
<view class="tips-box">{{ workMode === 'text-only' ? '松开转文字' : '松开发送' }}</view>
|
||
</view>
|
||
</view>
|
||
<!-- 第二形态 -->
|
||
<view class="page-2" :class="[status, voice_to_text_status, workMode]">
|
||
<!-- 左侧XX -->
|
||
<view class="transVoiceIng cancel-btn" @click="closePopup()">
|
||
<text class="btn-text">取消</text>
|
||
</view>
|
||
<!-- 中间发送原语音 -->
|
||
<view class="transVoiceIng trans-btn" @click="originalSend()">
|
||
<text class="btn-text">发送原语音</text>
|
||
</view>
|
||
<!-- 右侧发送按钮(椭圆) -->
|
||
<view class="send-btn" @click="sendMsg()">
|
||
<view class="send-btn-text">发送</view>
|
||
<CommonLoading class="send-btn-loading" color="#444" />
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</uni-popup>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import * as TouchBtnImages from './touch-btn.images'; // 导出图片
|
||
import { StatusType, VoiceToTextSubStatus, ErrorCode, WorkModeType } from './touch-btn.types';
|
||
import type { TouchBtnEmits } from './touch-btn.types';
|
||
import { wsUrl, bottomAreaTopBoundary, horizontalSplitX, initWebSocket, showErrorTip, vibrateLight } from './touch-btn';
|
||
import { ref, computed, onMounted, nextTick, watch, reactive } from 'vue';
|
||
import { onHide, onLaunch } from '@dcloudio/uni-app';
|
||
import common, { setupKeyboardHeightListener, getToken } from '@/common/common';
|
||
import { startAudioRecord, stopAudioRecord, preRequestRecordPermission } from '@/uni_modules/ty-recording';
|
||
import { ProtocolCodec, MessageType, ControlCommand, ControlCode, ControlBody } from '@/common/protocolCodec';
|
||
import permissionApi from '@/common/permission';
|
||
const emit = defineEmits<TouchBtnEmits>();
|
||
const pageBoxMarginBottom = ref('0px'); // 适配输入法
|
||
const ws = ref(null);
|
||
const wsStatus = ref(false);
|
||
const status = ref<StatusType>('default');
|
||
const voice_to_text_status = ref<VoiceToTextSubStatus>('init');
|
||
const popupRef = ref(null);
|
||
const props = defineProps({
|
||
isDisabled: {
|
||
default: false,
|
||
type: Boolean
|
||
},
|
||
// 禁用时候点击反馈的文本
|
||
disabledText: {
|
||
default: '请稍后再试!',
|
||
type: String
|
||
},
|
||
isLoading: {
|
||
default: false,
|
||
type: Boolean
|
||
},
|
||
loadingText: {
|
||
default: '发送回答中,请稍后再试!',
|
||
type: String
|
||
},
|
||
// 设置为true则不能发送消息
|
||
canAnswer: {
|
||
default: false,
|
||
type: Boolean
|
||
},
|
||
// 不能发送消息的提示
|
||
canAnswerText: {
|
||
default: '当前状态不能发送内容!',
|
||
type: String
|
||
},
|
||
maxlength: {
|
||
type: Number,
|
||
default: 500
|
||
},
|
||
// 最小录音时长
|
||
minRecordDuration: {
|
||
type: Number,
|
||
default: 1000
|
||
},
|
||
// 最大录音时长
|
||
maxRecordDuration: {
|
||
type: Number,
|
||
default: 60000
|
||
},
|
||
adjustPosition: {
|
||
default: false,
|
||
type: Boolean
|
||
},
|
||
answerMethod: {
|
||
type: String,
|
||
default: '01',
|
||
validator: (value: string) => {
|
||
/* '01': '不限制回答方式', '02': '仅文字回答', '03': '仅语音回答'*/
|
||
return ['01', '02', '03'].includes(value);
|
||
}
|
||
},
|
||
theme: {
|
||
type: String,
|
||
default: ''
|
||
}
|
||
});
|
||
// 兼容以前的按钮参数
|
||
const workMode = computed<WorkModeType>(() => {
|
||
const modeMap = {
|
||
'01': 'both',
|
||
'02': 'text-only',
|
||
'03': 'voice-only'
|
||
};
|
||
return modeMap[props.answerMethod] || 'both';
|
||
});
|
||
const textAreaText = computed({
|
||
get() {
|
||
return allText.value + tempText.value;
|
||
},
|
||
set(newValue) {
|
||
allText.value = newValue;
|
||
}
|
||
});
|
||
const dialogDynamicClasses = computed(() => {
|
||
|
||
const classes = [];
|
||
isOutStatus.value === '02' && classes.push('is-out');
|
||
(isOutStatus.value === '03' || status.value === 'voice_to_text' || status.value === 'send_over') &&
|
||
classes.push('is-translate');
|
||
// 仅语音模式,01松手后要和03操作一样
|
||
// isOutStatus.value === '01' && workMode.value === 'text-only' && status.value == 'voice_to_text' && classes.push('is-translate');
|
||
voice_to_text_status.value !== '' && classes.push(voice_to_text_status.value);
|
||
|
||
return classes;
|
||
// return ['is-translate','success' ];
|
||
});
|
||
const recordId = ref('');
|
||
const allText = ref('');
|
||
const tempText = ref('');
|
||
const inputText = ref('');
|
||
const ossKey = ref('');
|
||
let voiceQueue = [];
|
||
// 记录 按下移动位置
|
||
const touchPos = reactive({ x: 0, y: 0 });
|
||
// 是否录音中
|
||
const isRecording = ref(false);
|
||
let voiceTimer = null; // 兜底关闭的定时器全局唯一
|
||
// 键盘还是语音
|
||
const isInputVisible = ref(true); // true=显示录音按钮,false=显示输入框
|
||
const recordStartTime = ref(0); // 录音开始时间戳
|
||
let pressTimer = null;
|
||
// 时长自动关闭定时器
|
||
let durationTimer = null;
|
||
// 计算属性:判断是否仅语音/仅文字模式(简化模板逻辑)
|
||
const isVoiceOnlyMode = computed(() => workMode.value === 'voice-only');
|
||
const isTextOnlyMode = computed(() => workMode.value === 'text-only');
|
||
const talkRightIcon = computed(() => {
|
||
if (isInputVisible.value) return TouchBtnImages.icKeyboardSvg();
|
||
if (inputText.value) return TouchBtnImages.iconSendSvg();
|
||
return TouchBtnImages.iconMicSvg();
|
||
});
|
||
// 计算是否超出按下盒子位置01代表下方盒子,02代表左上03代表右上
|
||
const isOutStatus = computed(() => {
|
||
const { y: touchY, x: touchX } = touchPos;
|
||
if (touchY >= bottomAreaTopBoundary) return '01';
|
||
// 仅语音模式只能返回取消(02)
|
||
if (workMode.value === 'voice-only') return '02';
|
||
return touchX < horizontalSplitX ? '02' : '03';
|
||
});
|
||
// 监听按下移动
|
||
const handleMove = (obj) => {
|
||
const { pageX, pageY } = obj.changedTouches[0];
|
||
touchPos.x = pageX;
|
||
touchPos.y = pageY;
|
||
};
|
||
const testFun = () => {
|
||
voice_to_text_status.value = 'failed';
|
||
};
|
||
// 开启语音
|
||
const startRecord = async (obj: { touches: any[] }) => {
|
||
if (props.isDisabled) return;
|
||
if (props.isLoading) return common.msg(props.loadingText);
|
||
if (pressTimer || isRecording.value) return;
|
||
if (props.canAnswer) return common.msg(props.canAnswerText);
|
||
try {
|
||
const state = await permissionApi.judgeIosPermission('record');
|
||
if (state === 'not determined') {
|
||
// 苹果专用
|
||
preRequestRecordPermission();
|
||
return;
|
||
}
|
||
} catch (e) {
|
||
console.error('麦克风权限申请失败:', e);
|
||
return;
|
||
}
|
||
// 标记录音开始
|
||
isRecording.value = true;
|
||
status.value = 'default';
|
||
voice_to_text_status.value = '';
|
||
tempText.value = '';
|
||
allText.value = '';
|
||
ossKey.value = '';
|
||
pressTimer = setTimeout(() => {
|
||
if (!isRecording.value) {
|
||
console.log('定时器被取消还是进入了');
|
||
return;
|
||
}
|
||
pressTimer = null;
|
||
status.value = 'voice_recording';
|
||
console.log('触感震动');
|
||
vibrateLight(); // 触感震动
|
||
// 重设按下位置
|
||
const changedTouche = obj.touches[0];
|
||
touchPos.x = changedTouche.clientX;
|
||
touchPos.y = changedTouche.clientY;
|
||
// 记录开始时间戳(毫秒)
|
||
recordStartTime.value = Date.now();
|
||
// 弹出录音界面
|
||
startRecordingTimer(true); // 启动超长定时器
|
||
startAudioRecord({
|
||
onFrame: (pcmData: any) => {
|
||
if (wsStatus.value && ws.value) {
|
||
if (voiceQueue.length > 0) {
|
||
voiceQueue.forEach((cachedData) => {
|
||
ws.value.send({
|
||
data: ProtocolCodec.pack(MessageType.AUDIO_DATA, cachedData)
|
||
});
|
||
});
|
||
// 发送完成后清空队列
|
||
voiceQueue = [];
|
||
}
|
||
// 再发送当前帧的音频数据
|
||
ws.value.send({
|
||
data: ProtocolCodec.pack(MessageType.AUDIO_DATA, pcmData)
|
||
});
|
||
} else {
|
||
voiceQueue.push(pcmData)
|
||
}
|
||
},
|
||
onStart: (res: any) => {
|
||
console.log('启动', res);
|
||
recordId.value = res.recordId ?? '';
|
||
popupRef.value.open();
|
||
console.log('开启语音');
|
||
setTimeout(() => {
|
||
initWebSocket(wsUrl + '?summary=' + getToken())
|
||
.then((socketTask) => {
|
||
if (!isRecording.value) {
|
||
ws.value = null;
|
||
wsStatus.value = false;
|
||
return;
|
||
}
|
||
ws.value = socketTask;
|
||
wsStatus.value = true;
|
||
console.log('WS连接成功', isRecording.value);
|
||
ws.value.onMessage((res: { data: Uint8Array | ArrayBuffer }) => {
|
||
try {
|
||
const { msgType, body } = ProtocolCodec.unpack(res.data);
|
||
console.log('msgType', msgType, body);
|
||
if (msgType === MessageType.TEXT_MESSAGE) {
|
||
tempText.value = '';
|
||
allText.value = allText.value + body;
|
||
} else if (msgType === MessageType.TIP_MESSAGE) {
|
||
tempText.value = body as string;
|
||
} else if (msgType === MessageType.CONTROL_CMD) {
|
||
// 返回结束消息
|
||
const controlBody = body as ControlBody;
|
||
if (controlBody.type === ControlCode.SERVER_TO_FRONT_OVER) {
|
||
console.log(
|
||
'识别结束',
|
||
controlBody,
|
||
voice_to_text_status.value,
|
||
status.value,
|
||
voice_to_text_status.value
|
||
);
|
||
// 如果不等于init说明已经超时,被超时函数处理完了,这里即使返回也不做处理
|
||
if (
|
||
status.value === 'voice_to_text' &&
|
||
voice_to_text_status.value !== 'init'
|
||
)
|
||
return;
|
||
if (status.value === 'send_over') return;
|
||
// 时间太短,关掉的时候就处理了,这里不处理
|
||
const recordDurationMs = Date.now() - recordStartTime.value;
|
||
if (recordDurationMs <= props.minRecordDuration) return;
|
||
closeWS();
|
||
status.value = 'send_over'; // 设置成发送完成
|
||
if (voiceTimer) clearTimeout(voiceTimer);
|
||
// 设置识别结果
|
||
voice_to_text_status.value = controlBody?.txt === '' ? 'failed' : 'success';
|
||
// 取值出来
|
||
ossKey.value = controlBody?.ossKey || '';
|
||
if (typeof controlBody?.txt === 'string') {
|
||
allText.value = controlBody.txt.trim();
|
||
tempText.value = ''
|
||
}
|
||
const _isOutStatus =
|
||
workMode.value === 'text-only' && isOutStatus.value === '01'
|
||
? '03'
|
||
: isOutStatus.value;
|
||
// 只有模式为01的时候才发送出去
|
||
console.log('只有模式为01的时候才发送出去', controlBody);
|
||
if (_isOutStatus === '01') {
|
||
emit('submitVoice', {
|
||
ossKey: ossKey.value,
|
||
text: allText.value,
|
||
recordId: recordId.value
|
||
});
|
||
}
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.log('后端发来的信息有问题', e, res.data);
|
||
}
|
||
});
|
||
})
|
||
.catch(() => {
|
||
stopRecord('networkError');
|
||
console.log('网络出现问题');
|
||
});
|
||
}, 200);
|
||
},
|
||
onStop: () => {
|
||
console.log('关闭');
|
||
},
|
||
onError: ({ errCode, errMsg }) => {
|
||
console.log('onError', errMsg);
|
||
if (errCode === 9010001) {
|
||
console.log(errMsg);
|
||
}
|
||
}
|
||
});
|
||
}, 120);
|
||
};
|
||
|
||
// 结束录音
|
||
const closePopup = () => {
|
||
isRecording.value = false;
|
||
popupRef.value.close();
|
||
};
|
||
// 结束ws
|
||
const closeWS = () => {
|
||
startRecordingTimer(false);
|
||
if (ws.value) {
|
||
ws.value.close({
|
||
code: 1000,
|
||
reason: '挂断',
|
||
complete: () => {
|
||
console.log('挂断Ws');
|
||
wsStatus.value = false;
|
||
ws.value = null;
|
||
}
|
||
});
|
||
}
|
||
};
|
||
|
||
const stopRecord = async (errrStatus = '') => {
|
||
console.log('松手stopRecord', pressTimer, isRecording.value, status.value);
|
||
if (pressTimer) {
|
||
clearTimeout(pressTimer);
|
||
console.log('取消一个存在的定时器');
|
||
pressTimer = null;
|
||
isRecording.value = false;
|
||
setTimeout(() => {
|
||
status.value = 'default';
|
||
}, 50);
|
||
return;
|
||
}
|
||
if (!isRecording.value) return; // 未处于录音状态
|
||
if (status.value !== 'voice_recording') {
|
||
closeWS();
|
||
closePopup();
|
||
return;
|
||
}
|
||
// 核心:计算录音总时长(毫秒转秒,保留1位小数)
|
||
const recordDurationMs = Date.now() - recordStartTime.value;
|
||
console.log('recordDurationMs', recordDurationMs);
|
||
// 拖动到转文字按钮上抬手或者仅文字模式的发送也强行算他转文字
|
||
const _isOutStatus = workMode.value === 'text-only' && isOutStatus.value === '01' ? '03' : isOutStatus.value;
|
||
|
||
// 时间太短03也直接关了
|
||
if (_isOutStatus === '03' && !(recordDurationMs <= props.minRecordDuration)) {
|
||
console.log('时间不短进来了');
|
||
status.value = 'voice_to_text'; //主状态设置为转文字
|
||
voice_to_text_status.value = 'init';
|
||
if (voiceTimer) clearTimeout(voiceTimer);
|
||
voiceTimer = setTimeout(() => {
|
||
voiceTimer = null;
|
||
console.log('3号定时器', voice_to_text_status.value, status.value);
|
||
if (voice_to_text_status.value !== 'init') return;
|
||
if (status.value === 'send_over') return;
|
||
voice_to_text_status.value = 'networkError';
|
||
status.value = 'send_over'; // 设置成发送完成
|
||
}, 60000);
|
||
} else if (_isOutStatus === '01') {
|
||
status.value = 'voice_send_ing'; //主状态设置为语音发送中
|
||
}
|
||
|
||
if ((_isOutStatus !== '03' || recordDurationMs <= props.minRecordDuration) && recordDurationMs < 700) {
|
||
await new Promise<void>((resolve) => {
|
||
setTimeout(() => {
|
||
resolve();
|
||
}, 700 - recordDurationMs);
|
||
});
|
||
}
|
||
|
||
// 取消的时候自己主动关闭websocket,其他的时候需要等待后端的消息关闭
|
||
if (_isOutStatus === '02' || errrStatus === 'networkError') {
|
||
closeWS();
|
||
}
|
||
// 向后端发送抬手动作
|
||
if (['01', '03'].includes(_isOutStatus) && ws.value) {
|
||
console.log('向后端发送抬手动作');
|
||
ws.value.send({
|
||
data: ProtocolCodec.pack(MessageType.CONTROL_CMD, { type: ControlCode.FRONT_TO_SERVER_OVER })
|
||
});
|
||
}
|
||
stopAudioRecord(); // 关掉录音
|
||
isRecording.value = false;
|
||
console.log('_isOutStatus', _isOutStatus);
|
||
if (_isOutStatus !== '03' || errrStatus === 'networkError' || recordDurationMs <= props.minRecordDuration) {
|
||
// 转文字时候不关遮罩层,网络错误必须关遮罩层
|
||
console.log('转文字时候不关遮罩层,网络错误必须关遮罩层');
|
||
closePopup();
|
||
|
||
// 关掉弹窗后的提示
|
||
if (errrStatus === 'networkError' && _isOutStatus === '01') {
|
||
status.value = 'send_over'; // 设置成发送完成
|
||
emit('submitVoice', { ossKey: '', text: '', recordId: recordId.value });
|
||
return;
|
||
} else if (recordDurationMs <= props.minRecordDuration) {
|
||
console.log('进入这里了', status.value);
|
||
if (_isOutStatus !== '02') {
|
||
showErrorTip(ErrorCode.ShortRecord);
|
||
}
|
||
setTimeout(() => {
|
||
status.value = 'send_over'; // 设置成发送完成
|
||
}, 200);
|
||
return;
|
||
}
|
||
}
|
||
if (_isOutStatus === '01' && status.value == 'voice_send_ing') {
|
||
emit('sendLoading', { recordId: recordId.value });
|
||
// 如果socket长时间没有返回,这里兜底一下
|
||
if (voiceTimer) clearTimeout(voiceTimer);
|
||
voiceTimer = setTimeout(() => {
|
||
console.log('1号定时器', voice_to_text_status.value, status.value);
|
||
voiceTimer = null;
|
||
if (workMode.value === 'voice-only' || (workMode.value === 'both' && _isOutStatus === '01')) {
|
||
if (status.value === 'send_over') return;
|
||
status.value = 'send_over'; // 恢复成默认状态
|
||
console.log('1号定时器纠错');
|
||
emit('submitVoice', { ossKey: '', text: '', recordId: recordId.value });
|
||
}
|
||
}, 60000);
|
||
}
|
||
};
|
||
|
||
// 发送文本
|
||
const showKeyboard = () => {
|
||
if (isInputVisible.value) {
|
||
const inputTextTram = inputText.value.trim();
|
||
if (inputTextTram) {
|
||
// emit('submit', 'text', inputTextTram);
|
||
} else {
|
||
common.msg('不能发送空白消息!');
|
||
}
|
||
} else {
|
||
isInputVisible.value = !isInputVisible.value;
|
||
}
|
||
};
|
||
|
||
// 切换键盘语音
|
||
const changeBtn = (_isInputVisible = true) => {
|
||
if (props.isDisabled) return;
|
||
// 只要不是仅录音,就可以切换到录音按钮
|
||
if (workMode.value === 'text-only') {
|
||
isInputVisible.value = false;
|
||
}
|
||
// 否则为true
|
||
isInputVisible.value = _isInputVisible;
|
||
inputText.value = '';
|
||
};
|
||
// 按钮右侧图标(三个功能)
|
||
const iconClick = () => {
|
||
if (props.isDisabled) return;
|
||
// 当是输入框且输入框的内容不为空时候,是发送文本,否则就是切换
|
||
if (!isInputVisible.value && inputText.value !== '') {
|
||
console.log('send_msg');
|
||
sendMsg();
|
||
} else {
|
||
isInputVisible.value = !isInputVisible.value;
|
||
}
|
||
};
|
||
// 禁用时点击给的提示
|
||
const disabled_click = () => {
|
||
if (props.isDisabled && props.disabledText) {
|
||
console.log('禁用时点击给的提示', props.disabledText);
|
||
common.msg(props.disabledText);
|
||
}
|
||
};
|
||
// 发送文字
|
||
const sendMsg = () => {
|
||
if (!isInputVisible.value) {
|
||
emit('submitAnswer', inputText.value, changeBtn);
|
||
} else {
|
||
emit('submitAnswer', textAreaText.value, changeBtn);
|
||
closePopup();
|
||
}
|
||
};
|
||
// 发送原语音
|
||
const originalSend = () => {
|
||
console.log('发送原语音', { ossKey: ossKey.value, text: allText.value, recordId: recordId.value });
|
||
emit('submitVoice', { ossKey: ossKey.value, text: allText.value, recordId: recordId.value });
|
||
closePopup();
|
||
};
|
||
setupKeyboardHeightListener((height: number) => {
|
||
const _height = Math.max(height - 100, 0);
|
||
pageBoxMarginBottom.value = `${_height}px`;
|
||
});
|
||
const startRecordingTimer = (status: boolean) => {
|
||
const clearRecordingTimer = () => {
|
||
if (durationTimer) {
|
||
clearTimeout(durationTimer);
|
||
durationTimer = null; // 重置定时器变量,避免内存泄漏
|
||
console.log('录音时长定时器已清除');
|
||
}
|
||
};
|
||
clearRecordingTimer();
|
||
if (!status) return;
|
||
const maxDuration = props.maxRecordDuration;
|
||
durationTimer = setTimeout(() => {
|
||
console.log(`录音达到最大时长 ${maxDuration} 秒,自动关闭`);
|
||
// 执行关闭录音的逻辑
|
||
stopRecord();
|
||
clearRecordingTimer();
|
||
}, maxDuration);
|
||
};
|
||
// 外面调用放弃这次
|
||
const give_up = () => {
|
||
console.log('调用结束语音回答');
|
||
stopRecord();
|
||
};
|
||
|
||
// 外部调用,清理输入框数据
|
||
const clearInputText = (text = '') => {
|
||
const old_text = inputText.value;
|
||
inputText.value = text;
|
||
return old_text;
|
||
};
|
||
defineExpose({ clearInputText, give_up });
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
@import './style/index.scss';
|
||
@import './style/popup.scss';
|
||
|
||
.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>
|