JLBA202505130001_关于吉林银行新建AI智能培训系统的需求_开发安卓录音按钮

This commit is contained in:
田岩
2026-03-06 16:36:05 +08:00
parent c877b388bd
commit 416d4eeb3a
5 changed files with 173 additions and 18 deletions
+73 -3
View File
@@ -103,8 +103,25 @@
:canAnswer="testAnswer"
:adjustPosition="true"
:canAnswerText="canAnswerText"
:answerMethod="answerMethod"
:answerMethod="answerMethod"
:isDisabled="(lastTalkingInfo.type || 0) !== 4 || ['SN', 'MU', 'JD'].includes(lastTalkingInfo.qnsTyp)" />
<touch-btn
class="talk-btns"
ref="touchBtnRef"
@sendLoading="sendLoading"
@submitVoice="submitVoice"
@submitAnswer="submitAnswerNow"
@startRecord="changePlayItemId('')"
:isLoading="isUploadingVoice"
:adjustPosition="true"
:canAnswer="testAnswer"
:canAnswerText="canAnswerText"
:isDisabled="(lastTalkingInfo.type || 0) !== 4 || ['SN', 'MU', 'JD'].includes(lastTalkingInfo.qnsTyp)"
loadingText="只能进行一条回答"
:answerMethod="answerMethod"
/>
</view>
<uv-overlay :show="showSeekModel" opacity=".6" @click="showSeekModel = false">
<view class="overlay-box">
@@ -165,8 +182,8 @@ import common from '@/common/common'
import TalkingItem from '@/pages/course/components/talkingItem.vue'
import ImagePreview from '@/components/image-preview/image-preview.vue'
import PreviewDetail from '@/components/image-preview/preview-detail.vue'
import TouchBtn from '@/pages/course/components/touchBtn.vue'
import TouchBtnOnlyVoice from '@/pages/course/components/touchBtnOnlyVoice.vue'
// import TouchBtn from '@/pages/course/components/touchBtn.vue'
// import TouchBtnOnlyVoice from '@/pages/course/components/touchBtnOnlyVoice.vue'
import badge from '@/components/points-and-badge/badge';
import points from '@/components/points-and-badge/points';
import usePointsAndBadge from '@/components/points-and-badge/usePointsAndBadge';
@@ -885,6 +902,59 @@ const testAnswer = computed(() => {
let _num = _arr.map(t => [1, 2, 6].indexOf(Number(t.type)) >= 0).filter(t => !!t)
return _num.length < 5
})
const sendLoading = () => {
if (!testAnswer.value) {
common.msg('每个问题最多连续发送五次答案!')
return
}
isUploadingVoice.value = true
let _data = {
stdyId: unref(stdyId),
qnsId: unref(lastTalkingInfo).qnsId, // 问题ID 必输项:true 类型:String
pgrphId: unref(lastTalkingInfo).pgrphId, // 段落ID 必输项:true 类型:String
qstLogId: unref(qstLogId), // 段落ID 必输项:true 类型:String
crsId: crsId.value, // 课程ID 必输项:true 类型:String
stdyBatch: stdyBatch.value, // 学习批次 必输项:true 类型:String
ansterContent: '', // 回答文本内容 必输项:false 类型:String
asrStartTm: unref(lastTalkingInfo).asrStartTm
}
let _id = new Date().getTime() + 'id'
//#ifndef APP-PLUS
// h5 假数据
talkingList.value.push({
crsId: crsId.value,
pgrphId: unref(lastTalkingInfo).pgrphId,
qnsId: unref(lastTalkingInfo).qnsId,
talkingContent: '',
talkingVoice: voicePath,
type: 1,
userInfo: {
...choiceTeacherInfo.value
},
id: _id
})
setIsUploadingVoice()
scrollToBottomNow()
// #endif
}
// 新录音组件语音提交
const submitVoice = ({ossKey,text}) => {
if(!ossKey) {
} else if (text === '') {
} else {
unref(ajaxApi)({
processType: 'ANSER-TEXT',
requestBody: JSON.stringify(_data)
})
}
}
const uploadRecordNow = (voicePath, voiceTime) => {
if (!testAnswer.value) {
common.msg('每个问题最多连续发送五次答案!')
+33 -6
View File
@@ -47,6 +47,7 @@
<script setup>
import { computed, unref, ref, toRefs, watch, onMounted } from "vue"
import { onUnload } from '@dcloudio/uni-app';
import { downloadFile } from '@/api/common.js';
const props = defineProps({
dataInfo: {
default: () => ({}),
@@ -78,7 +79,7 @@ import { onUnload } from '@dcloudio/uni-app';
onUnload(()=>{
pauseVoice()
})
watch(() => props.dataInfo.talkingVoice,(val)=>{
watch(() => props.dataInfo.bucketKey,(val)=>{
if(val){
initVoice()
}
@@ -93,7 +94,9 @@ import { onUnload } from '@dcloudio/uni-app';
emit('changePlay', '')
}
const playVoice = () =>{
emit('changePlay', unref(dataInfo)?.id)
if(talkVoiceObj.value && talkVoiceObj.value.src) {
talkVoiceObj.value.play()
return
@@ -106,6 +109,7 @@ import { onUnload } from '@dcloudio/uni-app';
isPlaying.value = true
},100)
}
}
}
const initVoice = () => {
@@ -118,11 +122,34 @@ import { onUnload } from '@dcloudio/uni-app';
talkVoiceObj.value.src = ''
})
if(unref(dataInfo).intrctWay === '02'){
itemVoice.value = unref(dataInfo).talkingVoice
talkVoiceObj.value.src = itemVoice.value
setTimeout(()=>{
setVoiceTime(0)
},100)
downloadFile(unref(dataInfo).bucketKey)
.then((res) => {
if (res?.data) {
common.msg(res.data.message);
return;
}
voiceLoading.value = false;
itemVoice.value = res.tempFilePath;
talkVoiceObj.value.src = res.tempFilePath;
if (voiceIsPlaying.value) return
if(unref(dataInfo)?.id !== props.activeVoiceId) return
talkVoiceObj.value.play();
storageStore.setStorage('audio', _id, itemVoice.value);
setTimeout(()=>{
setVoiceTime(0)
},100)
})
.catch((err) => {
voiceLoading.value = false;
emit('changePlay', '');
});
// itemVoice.value = unref(dataInfo).talkingVoice
// talkVoiceObj.value.src = itemVoice.value
// setTimeout(()=>{
// setVoiceTime(0)
// },100)
}
}
const setVoiceTime = (sum) => {
+1 -1
View File
@@ -1,6 +1,6 @@
<template>
<view class="talking-item">
<questionVue v-if="talkingType === 'question'" @changePlay="changePlay" :dataInfo="talkingInfo" :isLast="isLast"
<questionVue v-if="talkingType === 'question'" @changePlay="changePlay" :dataInfo="props.talkingInfo" :isLast="isLast"
:activeVoiceId="activeVoiceId"></questionVue>
<answerVue v-else-if="talkingType === 'answer'" :dataInfo="props.talkingInfo" :isLast="isLast"
:isLoading="!!props.talkingInfo.isLoading" :isAnswering="props.isAnswering" @handleEvents="handleEvents">
+28 -6
View File
@@ -145,7 +145,7 @@
:adjustPosition="true"
:isDisabled="isHistory"
/> -->
<touch-btn @submitVoice="submitVoice" @sendLoading="sendLoading" @submitAnswer="submitAnswerNow"
<touch-btn @submitVoice="submitVoice" @sendLoading="sendLoading" @submitAnswer="submitAnswer"
@startRecord="startRecordCallback" :loadingText="sendLoadingText" :isLoading="!answerIsOver"
:adjustPosition="true" :isDisabled="isHistory" />
@@ -322,14 +322,16 @@
seqNo.value = body.seqNo;
touchBtnCallBack.value && touchBtnCallBack.value();
touchBtnCallBack.value = null;
console.log('body',body);
talkingList.value.push({
type: 'question',
talkingText: body.intrctContent,
isLoading: false,
id: _id,
...body,
talkingVoice: askData.value.talkingVoice
...askData.value
});
console.log('talkingList',talkingList.value);
talkingList.value.push({
type: 'answer',
isLoading: true,
@@ -701,7 +703,7 @@
}
// 新发送语音
const submitVoice = ({ossKey,text}) => {
const submitVoice = ({ossKey, text}) => {
console.log('新发送语音',ossKey,text);
if(!ossKey) {
talkingList.value.pop();
@@ -716,9 +718,6 @@
intrctWay: '02', //01-文本;02-语音;03-图片
intrctContent: text,
bucketKey: ossKey,
// ossFileAddr: _res.body.ossFileAddr,
// talkingVoice: voicePath,
// voiceTime: voiceTime
};
sendMessage({
commond: 'ASK',
@@ -772,6 +771,29 @@
});
// #endif
};
// 新录音组件文本提交
const submitAnswer = (val, cb = null) => {
if (!answerIsOver.value) {
// 问题回答中
return;
}
touchBtnCallBack.value = cb;
askData.value = {
reqBatch: reqBatch.value,
intrctWay: '01', //01-文本;02-语音;03-图片
intrctContent: val,
bucketKey: ''
};
sendMessage({
commond: 'ASK',
body: askData.value
});
setHeight({
height: 0
});
}
// 发送文本
const submitAnswerNow = (val, cb = null) => {
if (!answerIsOver.value) {
@@ -14,7 +14,7 @@ import AtomicBoolean from "java.util.concurrent.atomic.AtomicBoolean";
const isRecording = new AtomicBoolean(false); // 原子布尔值,保证线程安全
// 1. 音频采集参数(极简配置,适配大部分设备)
const sampleRate : Int = 48000; // 采样率
const sampleRate : Int = 16000; // 采样率
const channelConfig = AudioFormat.CHANNEL_IN_MONO; // 单声道
const audioFormat = AudioFormat.ENCODING_PCM_16BIT; // 16位PCM
const minBufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat);
@@ -58,6 +58,11 @@ export function startAudioRecord(options : StartAudioRecordOptions) : void {
if (readSize > 0) {
const data = new ByteArray(readSize);
java.lang.System.arraycopy(buffer, 0, data, 0, readSize);
// const lastVolume = calculateVolumeLight(buffer, readSize);
// console.log('lastVolume', lastVolume);
options.onFrame?.(data)
}
}
@@ -100,4 +105,35 @@ export function stopAudioRecord() : void {
}
}
}
export function preRequestRecordPermission(callback ?: (granted : boolean) => void) { callback?.(true); }
export function preRequestRecordPermission(callback ?: (granted : boolean) => void) { callback?.(true); }
/**
* 轻量化音量计算(低资源消耗版)
* @param buffer 音频缓冲区
* @param readSize 实际读取的字节数
* @returns 音量值(0-100
*/
function calculateVolumeLight(buffer: ByteArray, readSize: number): number {
try {
let maxAmplitude = 0;
// 1. 抽样计算:每4个采样点取1个(减少75%的计算量)
for (let i:Int = 0; i < readSize; i += 8) { // 2字节/采样点 * 4 = 8字节步长
if (i + 1 >= readSize) break; // 防止越界
// 2. 简化:只取绝对值最大的振幅(替代均方根
const shortVal = (buffer[i + 1] << 8) | (buffer[i] & 0xff);
const absVal = Math.abs(shortVal);
if (absVal > maxAmplitude) {
maxAmplitude = absVal;
}
}
// 3. 直接映射到0-100(放弃分贝计算,减少Math.log10等耗时操作)
const volume = (maxAmplitude / 32767) * 100;
return Math.round(Math.max(0, Math.min(100, volume))); // 限制0-100
} catch (e) {
console.error('轻量化音量计算失败:', e);
return 0;
}
}