From 4981aed2dfb3ea662473b132db0dc6a2cf212a0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B0=E5=B2=A9?= Date: Wed, 4 Mar 2026 16:09:22 +0800 Subject: [PATCH] =?UTF-8?q?JLBA202505130001=5F=E5=85=B3=E4=BA=8E=E5=90=89?= =?UTF-8?q?=E6=9E=97=E9=93=B6=E8=A1=8C=E6=96=B0=E5=BB=BAAI=E6=99=BA?= =?UTF-8?q?=E8=83=BD=E5=9F=B9=E8=AE=AD=E7=B3=BB=E7=BB=9F=E7=9A=84=E9=9C=80?= =?UTF-8?q?=E6=B1=82=5F=E5=BC=80=E5=8F=91=E5=BD=95=E9=9F=B3=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=EF=BC=8C=E5=9F=BA=E6=9C=AC=E5=8A=9F=E8=83=BD=E5=BC=80?= =?UTF-8?q?=E5=8F=91=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.development | 14 -- src/common/common.js | 8 +- src/components/touch-btn/style/popup.scss | 22 ++- src/components/touch-btn/touch-btn.types.ts | 11 +- src/components/touch-btn/touch-btn.vue | 161 +++++++--------- src/pages/index/dialog.vue | 200 +++++++++----------- src/pages/practice/index.vue | 14 +- 7 files changed, 190 insertions(+), 240 deletions(-) diff --git a/.env.development b/.env.development index 612cd003..93437482 100644 --- a/.env.development +++ b/.env.development @@ -2,25 +2,11 @@ ENV = 'development' # 'development' - - - #VITE_APP_BASE_API_Url = 'https://aits.jlbank.com.cn:7001' VITE_APP_BASE_API_Url = 'https://aitstest.jlbank.com.cn:7001' -# VITE_APP_BASE_API_Url = 'https://aits.jlbank.com.cn:7001' - - -#VITE_APP_BASE_API_Url = 'https://aitstest.jlbank.com.cn:7001' - -VITE_APP_BASE_API_Url = 'https://aitstest.jlbank.com.cn:7002' -# VITE_APP_BASE_API_Url = 'http://192.168.247.200' - -# VITE_APP_BASE_API_Url = 'http://aitscdn.jlbank.com.cn:7001' - -# dev # DEV #VITE_APP_BASE_API_Url = 'http://25.18.122.65:7001' diff --git a/src/common/common.js b/src/common/common.js index 73107d3d..d9ec4948 100644 --- a/src/common/common.js +++ b/src/common/common.js @@ -335,17 +335,17 @@ export function setupKeyboardHeightListener(updateCallback) { const handleKeyboardHeightChange = (res) => { // 获取系统信息 const systemInfo = uni.getSystemInfoSync(); - console.log('systemInfo', systemInfo); + // console.log('systemInfo', systemInfo); // 计算安全区域底部高度 const safeAreaBottomHeight = systemInfo.screenHeight - systemInfo.safeArea.bottom; - console.log('计算安全区域底部高度', safeAreaBottomHeight); + // console.log('计算安全区域底部高度', safeAreaBottomHeight); let keyboardHeight = res.height; - console.log('软键盘高度', keyboardHeight); + // console.log('软键盘高度', keyboardHeight); // 仅在iOS端补偿安全区域高度 if (systemInfo.platform === 'ios') { keyboardHeight = Math.max(0, keyboardHeight); // 避免负数 } - console.log('keyboardHeight', keyboardHeight); + // console.log('keyboardHeight', keyboardHeight); // 通过回调函数更新外部的响应式变量 updateCallback(keyboardHeight); }; diff --git a/src/components/touch-btn/style/popup.scss b/src/components/touch-btn/style/popup.scss index 94b35bc4..1e65e767 100644 --- a/src/components/touch-btn/style/popup.scss +++ b/src/components/touch-btn/style/popup.scss @@ -51,7 +51,7 @@ position: absolute; width: 100%; bottom: 0; - &.voice_to_text { + &.voice_to_text,&.send_over { .btn-box-1 { animation: moveLeftOut 0.3s ease-out forwards; } @@ -139,7 +139,7 @@ bottom: 0; opacity: 0; visibility: hidden; // 初始不可见且不占交互位置 - &.voice_to_text { + &.voice_to_text,&.send_over { display: flex !important; visibility: visible; // 激活后可见 animation: fadeIn 0.4s ease-out 0.1s forwards; @@ -194,15 +194,19 @@ display: none; } } - &.init > .send-btn > .send-btn-loading { - display: block !important; + &.init > .send-btn { pointer-events: none; cursor: not-allowed; + .send-btn-loading { + display: block !important; + pointer-events: none; + cursor: not-allowed; + } } &.success > .send-btn > .send-btn-text { display: block !important; } - &.failed .send-btn { + &.failed .send-btn,&.networkError .send-btn { pointer-events: none; cursor: not-allowed; background-color: rgb(80, 80, 80); @@ -259,7 +263,7 @@ left: 78% !important; } - &.failed { + &.failed,&.networkError { width: 506rpx; height: 114rpx; min-height: 114rpx; @@ -274,7 +278,11 @@ // animation: fadeIn 0.4s ease-out 0.1s forwards; } .textarea { - // display: none; + visibility: hidden; // 初始不可见且不占交互位置 + } + } + &.init{ + .textarea { visibility: hidden; // 初始不可见且不占交互位置 } } diff --git a/src/components/touch-btn/touch-btn.types.ts b/src/components/touch-btn/touch-btn.types.ts index 71878349..4f1940b3 100644 --- a/src/components/touch-btn/touch-btn.types.ts +++ b/src/components/touch-btn/touch-btn.types.ts @@ -5,8 +5,9 @@ * - 'voice_recording' : 录音中状态(语音录制面板展开,正在录音) * - 'text_input' : 文字输入中状态(文字输入面板展开,用户正在输入) * - 'voice_to_text' : 语音转文字状态(独立的语音转文字页面/面板展开) + * - 'send_over' : 发送完成 */ -export type StatusType = 'default' | 'voice_recording' | 'text_input' | 'voice_to_text' | 'voice_send_ing'; +export type StatusType = 'default' | 'voice_recording' | 'text_input' | 'voice_to_text' | 'voice_send_ing' | 'send_over'; /** * 语音转文字的子状态(仅当主状态为 voice_to_text 时生效) @@ -16,7 +17,8 @@ export type VoiceToTextSubStatus = | 'init' // 初始化(刚进入语音转文字页面) // | 'loading' // 转换中(正在处理语音→文字) | 'success' // 转换完成(成功得到文字结果) - | 'failed'; // 转换失败(网络/识别错误等) + | 'failed' // 转换失败(网络/识别错误等) + | 'networkError'; // 转换失败(网络错误等) /** 错误码枚举 - 直接映射提示文本 */ @@ -33,6 +35,7 @@ export interface ShowErrorTipOptions { delayTime?: number; } +type ChangeBtnCallback = () => void; /** * 触摸按钮组件的自定义事件类型定义 @@ -48,9 +51,9 @@ export type TouchBtnEmits = { /** * 提交文字答案(用户确认识别结果/手动编辑后触发) * @param answerText - 最终提交的文字答案内容 - * @param isManualEdit - 是否手动编辑过识别结果(true=手动修改,false=纯语音识别) + * @param changeBtn - 按钮状态变更的回调函数 */ - submitAnswer: [answerText: string, isManualEdit: boolean]; + submitAnswer: [answerText: string, changeBtn: ChangeBtnCallback]; /** * 开始录音(用户点击录音按钮时触发) * @param recordParams - 录音初始化参数(可选,默认使用组件内置配置) diff --git a/src/components/touch-btn/touch-btn.vue b/src/components/touch-btn/touch-btn.vue index 22fae1b3..3d10a444 100644 --- a/src/components/touch-btn/touch-btn.vue +++ b/src/components/touch-btn/touch-btn.vue @@ -19,7 +19,7 @@ ref="inputRef" v-model="inputText" :maxlength="props.maxlength" - :adjustPosition="true" + :adjustPosition="false" placeholderStyle="color:#999999;font-size:26rpx" placeholder="请输入文字" :disabled="props.isDisabled" @@ -27,7 +27,7 @@ /> - + @@ -51,7 +51,8 @@ > - 未识别到文字 + 未识别到文字 + 网络识别超时 @@ -125,7 +126,6 @@ const status = ref('default'); const voice_to_text_status = ref('init'); const popupRef = ref(null); - const props = defineProps({ // mode控制功能模式 mode: { @@ -181,7 +181,8 @@ const dialogDynamicClasses = computed(() => { const classes = []; isOutStatus.value === '02' && classes.push('is-out'); - (isOutStatus.value === '03' || status.value === 'voice_to_text') && classes.push('is-translate'); + (isOutStatus.value === '03' || status.value === 'voice_to_text' || status.value === 'send_over') && + classes.push('is-translate'); // 仅语音模式,01松手后要和03操作一样 // isOutStatus.value === '01' && props.mode === 'text-only' && status.value == 'voice_to_text' && classes.push('is-translate'); voice_to_text_status.value !== '' && classes.push(voice_to_text_status.value); @@ -195,6 +196,7 @@ const touchPos = reactive({ x: 0, y: 0 }); // 是否录音中 const isRecording = ref(false); + let voiceTimer = null; // 兜底关闭的定时器全局唯一 // 键盘还是语音 const isInputVisible = ref(true); // true=显示录音按钮,false=显示输入框 const recordStartTime = ref(0); // 录音开始时间戳 @@ -262,33 +264,40 @@ // 返回结束消息 const controlBody = body as ControlBody; if (controlBody.type === ControlCode.SERVER_TO_FRONT_OVER) { - console.log('识别结束', controlBody); - // if(status.value === 'default') return; - // status.value = 'default'; // 恢复成默认状态 + console.log('识别结束', controlBody, 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; + if (voiceTimer) clearTimeout(voiceTimer); + // 设置识别结果 if (controlBody?.txt === '') { //说明没有识别到文字 voice_to_text_status.value = 'failed'; - emit('submitVoice', { ossKey:'', text:'', recordId: recordId.value }); - // todo } else { - // 识别成功 voice_to_text_status.value = 'success'; - const ossKey = controlBody?.ossKey; - let text = ''; - if (typeof controlBody?.txt === 'string') { - text = controlBody.txt.trim(); - } + } + // status.value = 'send_over'; // 设置成发送完成 + // 取值出来 + const ossKey = controlBody?.ossKey || ''; + let text = ''; + if (typeof controlBody?.txt === 'string') { + text = controlBody.txt.trim(); + } + const _isOutStatus = + props.mode === 'text-only' && isOutStatus.value === '01' ? '03' : isOutStatus.value; + // 只有模式为01的时候才发送出去 + if (_isOutStatus === '01') { emit('submitVoice', { ossKey, text, recordId: recordId.value }); } } } } catch (e) { - console.log('后端发来的信息有问题'); + console.log('后端发来的信息有问题', e); } }); }) .catch(() => { - stopRecord('networkError'); + // stopRecord('networkError'); console.log('网络出现问题'); }); pressTimer.value = setTimeout(() => { @@ -323,86 +332,28 @@ }); }, 120); }; - + // 结束录音 const closePopup = () => { popupRef.value.close(); }; - const stopRecord2 = async (errrStatus = '') => { - clearTimeout(pressTimer.value); - if (!isRecording.value) return; // 未处于录音状态 - if (isOutStatus.value === '03') { - status.value = 'voice_to_text'; //主状态设置为转文字 - voice_to_text_status.value = 'init'; - } else if (isOutStatus.value === '01') { - status.value = 'voice_send_ing'; //主状态设置为语音发送中 - } - if (['01', '03'].includes(isOutStatus.value)) { - // 向后端发送抬手动作 - ws.value && - ws.value.send({ - data: ProtocolCodec.pack(MessageType.CONTROL_CMD, { type: ControlCode.FRONT_TO_SERVER_OVER }) - }); - } - const recordDurationMs = Date.now() - recordStartTime.value; - if (isOutStatus.value !== '02') { - // 取消的时候,不校验最低时间 - // 核心:计算录音总时长(毫秒转秒,保留1位小数) - if (recordDurationMs < 600) { - await new Promise((resolve) => { - setTimeout(() => { - resolve(); - }, 600 - recordDurationMs); - }); - } - } - isRecording.value = false; - if (isOutStatus.value === '02' || errrStatus === 'networkError') { - // 取消的时候自己主动关闭websocket,其他的时候需要等待后端的消息关闭 - ws.value && - ws.value.close({ - code: 1000, - reason: '挂断', - complete: () => { - console.log('挂断Ws'); - wsStatus.value = false; - } - }); - } - stopAudioRecord(); // 关掉录音 - if (isOutStatus.value !== '03' || errrStatus === 'networkError') { - // 转文字时候不关遮罩层,网络错误必须关遮罩层 - popupRef.value.close(); - // 关掉弹窗后的提示 - if (errrStatus === 'networkError') { - return showErrorTip(ErrorCode.NetworkError); - } else if (isOutStatus.value !== '02' && recordDurationMs <= 1200) { - // 取消的时候,不校验最低时间 - return showErrorTip(ErrorCode.ShortRecord); - } - } - if (isOutStatus.value === '01' && status.value == 'voice_send_ing') { - emit('sendLoading', { recordId: recordId.value }); - // 如果socket长时间没有返回,这里兜底一下 - setTimeout(() => { - if(status.value === 'default') return; - status.value = 'default'; // 恢复成默认状态 - emit('submitVoice', { ossKey:'', text:'', recordId: recordId.value }); - }, 5000) - } - - }; - const stopRecord = async (errrStatus = '') => { clearTimeout(pressTimer.value); if (!isRecording.value) return; // 未处于录音状态 - const _isOutStatus = (props.mode === 'text-only' && isOutStatus.value === '01')?"03":isOutStatus.value; + const _isOutStatus = props.mode === 'text-only' && isOutStatus.value === '01' ? '03' : isOutStatus.value; console.log('_isOutStatus', _isOutStatus); // 拖动到转文字按钮上抬手或者仅文字模式的发送也强行算他转文字 - if (_isOutStatus === '03' ) { + if (_isOutStatus === '03') { status.value = 'voice_to_text'; //主状态设置为转文字 voice_to_text_status.value = 'init'; + if (voiceTimer) clearTimeout(voiceTimer); + voiceTimer = setTimeout(() => { + voiceTimer = null; + if (voice_to_text_status.value !== 'init') return; + voice_to_text_status.value = 'networkError'; + emit('submitVoice', { ossKey: '', text: '', recordId: recordId.value }); + }, 5000); } else if (_isOutStatus === '01') { status.value = 'voice_send_ing'; //主状态设置为语音发送中 } @@ -439,7 +390,6 @@ }); } stopAudioRecord(); // 关掉录音 - if (_isOutStatus !== '03' || errrStatus === 'networkError') { // 转文字时候不关遮罩层,网络错误必须关遮罩层 popupRef.value.close(); @@ -454,13 +404,16 @@ if (_isOutStatus === '01' && status.value == 'voice_send_ing') { emit('sendLoading', { recordId: recordId.value }); // 如果socket长时间没有返回,这里兜底一下 - setTimeout(() => { - if(status.value === 'default') return; - status.value = 'default'; // 恢复成默认状态 - emit('submitVoice', { ossKey:'', text:'', recordId: recordId.value }); - }, 5000) + if (voiceTimer) clearTimeout(voiceTimer); + voiceTimer = setTimeout(() => { + voiceTimer = null; + if (props.mode === 'voice-only') { + if (status.value === 'send_over') return; + status.value = 'send_over'; // 恢复成默认状态 + emit('submitVoice', { ossKey: '', text: '', recordId: recordId.value }); + } + }, 5000); } - }; // 发送文本 @@ -483,16 +436,32 @@ isInputVisible.value = !isInputVisible.value; inputText.value = ''; }; + // 按钮右侧图标(三个功能) + const iconClick = () => { + if (props.isDisabled) return; + // 当是输入框且输入框的内容不为空时候,是发送文本,否则就是切换 + if (!isInputVisible.value && inputText.value !== '') { + console.log('send_msg'); + send_msg(); + } else { + isInputVisible.value = !isInputVisible.value; + } + inputText.value = ''; + }; // 禁用时点击给的提示 const disabled_click = () => { console.log('禁用时点击给的提示'); if (props.isDisabled) common.msg(props.disabledText); }; - + const send_msg = () => { - console.log('点击发送'); - + if (!isInputVisible.value) { + emit('submitAnswer', inputText.value, changeBtn); + } else { + emit('submitAnswer', textAreaText.value, changeBtn); + closePopup(); + } }; setupKeyboardHeightListener((height: number) => { diff --git a/src/pages/index/dialog.vue b/src/pages/index/dialog.vue index 03f220d3..3e8c9059 100644 --- a/src/pages/index/dialog.vue +++ b/src/pages/index/dialog.vue @@ -3,17 +3,31 @@ - + AI问答 内容由AI生成 - - - + + 历史 消息 @@ -65,18 +79,28 @@ --> - - + + {{ item }} - @@ -111,18 +135,25 @@ - - - + - + @@ -133,51 +164,20 @@