JLBA202505130001_关于吉林银行新建AI智能培训系统的需求_开发语音采集uts插件
@@ -7,7 +7,7 @@ ENV = '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://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'
|
||||
@@ -16,7 +16,7 @@ ENV = 'development'
|
||||
# dev
|
||||
|
||||
# DEV
|
||||
VITE_APP_BASE_API_Url = 'http://25.18.122.65:7001'
|
||||
#VITE_APP_BASE_API_Url = 'http://25.18.122.65:7001'
|
||||
|
||||
# sit
|
||||
#VITE_APP_BASE_API_Url = 'http://25.18.122.91:9786'
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
<template>
|
||||
<view class="main-div">
|
||||
<view class="main-input-div">
|
||||
<!-- 多行文本 -->
|
||||
<view class="input-div">
|
||||
<uv-textarea
|
||||
class="textarea"
|
||||
:class="{'margin-bottom-12':isShow}"
|
||||
v-model="value"
|
||||
border="none"
|
||||
placeholder="发条评论,和大家一起讨论"
|
||||
:customStyle="customStyle"
|
||||
autoHeight
|
||||
:adjustPosition="false"
|
||||
fixed
|
||||
@focus="focus"
|
||||
@blur="blur"
|
||||
></uv-textarea>
|
||||
</view>
|
||||
<!-- 下方的图片展示框 -->
|
||||
<view class="imgs-div" v-if="isShow">
|
||||
<image class="upload-img" v-for="imgItem in uploadImgs" :src="imgItem" mode=""></image>
|
||||
</view>
|
||||
<!-- 工具框 -->
|
||||
<view class="setting-div" v-if="isShow"></view>
|
||||
</view>
|
||||
<view class="tool-div" v-if="isShow">
|
||||
<view class="tool-icon tool-icon-img" @click="upload">
|
||||
<image class="img" :src="commentInput3" mode="aspectFit"></image>
|
||||
</view>
|
||||
<view class="tool-icon tool-icon-emoji">
|
||||
<image class="img" :src="commentInput2" mode="aspectFit"></image>
|
||||
</view>
|
||||
<view class="fl1"></view>
|
||||
<view class="tool-send">发送</view>
|
||||
</view>
|
||||
<!-- 下方主要表情输入框 -->
|
||||
<view class="bottom-main-input-div">
|
||||
<view class="expression-tool-div">
|
||||
<view class="tool-icon tool-icon-img checked_tool" @click="upload">
|
||||
<image class="img" :src="commentInput1" mode="aspectFit"></image>
|
||||
</view>
|
||||
<view class="tool-icon tool-icon-emoji checked_tool">
|
||||
<image class="img" :src="commentInput4" mode="aspectFit"></image>
|
||||
</view>
|
||||
<view class="fl1"></view>
|
||||
</view>
|
||||
<view class="emoji-list">
|
||||
<view class="emoji-item" v-for="emojiItem in emojiList">
|
||||
{{emojiItem}}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import commentInput1 from '@/static/images/courseDetail/comment-input-1.png'
|
||||
import commentInput2 from '@/static/images/courseDetail/comment-input-2.png'
|
||||
import commentInput3 from '@/static/images/courseDetail/comment-input-3.png'
|
||||
import commentInput4 from '@/static/images/courseDetail/comment-input-4.png'
|
||||
import { reactive, ref } from 'vue';
|
||||
import { emojiList } from '@/emoji';
|
||||
const customStyle = {
|
||||
backgroundColor: 'rgb(227, 230, 237)',
|
||||
padding: '18rpx'
|
||||
};
|
||||
const uploadImgs = ref([
|
||||
// '/src/static/images/test/e7dd89a71493fcc6281700a4864e24cb.jpg'
|
||||
])
|
||||
const isShow = ref(false);
|
||||
const popHeight = ref('0px')
|
||||
const value = ref('');
|
||||
const click_input = () => {
|
||||
if(!isShow.value) isShow.value = true
|
||||
}
|
||||
const focus = (event) => {
|
||||
popHeight.value = '278px'
|
||||
isShow.value = true
|
||||
console.log('event', event.detail.height);
|
||||
}
|
||||
const blur = (event) => {
|
||||
console.log('失去焦点不一定关闭');
|
||||
// popHeight.value = '0px'
|
||||
isShow.value = true
|
||||
}
|
||||
const fileList = reactive([]);
|
||||
// 方法
|
||||
const upload = (limit = 4) => {
|
||||
uni.chooseImage({
|
||||
count: limit,
|
||||
sizeType: ['original', 'compressed'],
|
||||
sourceType: ['camera', 'album'],
|
||||
success: async ({
|
||||
tempFilePaths
|
||||
}) => {
|
||||
const valid = tempFilePaths.every(url => {
|
||||
const extension = url.slice(lastDotIndex + 1).split('?')[0].toLowerCase()
|
||||
return ['png', 'jpg'].includes(extension)
|
||||
})
|
||||
if (!valid) return common.msg('您选择的图片格式有误。')
|
||||
const data = {
|
||||
bizScen: 'S3005'
|
||||
}
|
||||
const upload_map = tempFilePaths.map((file) => uploadFile(file, data))
|
||||
Promise.all(upload_map).then(res => {
|
||||
fileList.push(...res.splice(0, limit - fileList.length))
|
||||
}).catch((err) => {
|
||||
common.msg(err)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.emoji-list{
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: 0 auto;
|
||||
background-color: red;
|
||||
justify-content: center;
|
||||
.emoji-item{
|
||||
font-size: 44rpx;
|
||||
padding: 14rpx;
|
||||
}
|
||||
}
|
||||
// 表情输入框
|
||||
.expression-tool-div{
|
||||
padding: 20rpx 10rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.tool-icon{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 30rpx;
|
||||
padding: 14rpx 28rpx;
|
||||
border-radius: 60rpx;
|
||||
margin: 0rpx 10rpx;
|
||||
.img {
|
||||
width: 50rpx;
|
||||
height: 50rpx;
|
||||
}
|
||||
}
|
||||
.checked_tool{
|
||||
background-color: rgb(243, 243, 243);
|
||||
}
|
||||
}
|
||||
.bottom-main-input-div{
|
||||
width: 100%;
|
||||
height: v-bind(popHeight);
|
||||
transition: height 0.3s ease 0s;
|
||||
|
||||
}
|
||||
.none {
|
||||
display: none;
|
||||
}
|
||||
.margin-bottom-12{
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
.tool-div {
|
||||
padding: 10rpx calc(18rpx + 20rpx);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.tool-icon{
|
||||
// padding: 14rpx 34rpx;
|
||||
// border-radius: 60rpx;
|
||||
// margin: 0rpx 10rpx;
|
||||
|
||||
}
|
||||
.tool-icon-img {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 30rpx;
|
||||
.img {
|
||||
width: 50rpx;
|
||||
height: 50rpx;
|
||||
}
|
||||
}
|
||||
.tool-icon-emoji {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.img {
|
||||
width: 50rpx;
|
||||
height: 50rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.tool-send {
|
||||
border-radius: 50rpx;
|
||||
width: 120rpx;
|
||||
height: 60rpx;
|
||||
line-height: 60rpx;
|
||||
text-align: center;
|
||||
background-color: rgb(254, 44, 85);
|
||||
font-size: 30rpx;
|
||||
color: #fff;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
}
|
||||
.main-div {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
background-color: #ffffff;
|
||||
z-index: 1000;
|
||||
border-top: 2rpx solid rgb(220, 220, 220);
|
||||
box-shadow: 0 -2rpx 20rpx rgb(220, 220, 220);
|
||||
}
|
||||
.main-input-div {
|
||||
width: calc(100% - 40rpx);
|
||||
margin: 20rpx;background-color: red;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 40rpx;
|
||||
overflow: hidden;
|
||||
background-color: rgb(227, 230, 237);
|
||||
.input-div {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
.imgs-div {
|
||||
display: flex;
|
||||
padding: 0 18rpx;
|
||||
.upload-img {
|
||||
margin: 0rpx 20rpx 20rpx 0;
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
}
|
||||
.setting-div {
|
||||
width: 100%;
|
||||
height: 100rpx;
|
||||
padding: 0 18rpx;
|
||||
}
|
||||
}
|
||||
.tool-div {
|
||||
border-bottom: 4rpx solid rgb(206, 206, 206);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,522 @@
|
||||
<template>
|
||||
<view class="talk-btns">
|
||||
<view class="touch-btn" @click="disabled_click">
|
||||
<!-- @touchend="stopRecord(false)" -->
|
||||
<uv-button
|
||||
class="touch-btn"
|
||||
@touchstart="startRecord"
|
||||
@touchmove="handleMove"
|
||||
@touchend="loosenButton"
|
||||
v-if="!keyboardIsShow"
|
||||
:customStyle="{ height: '92rpx' }"
|
||||
>
|
||||
按住说话
|
||||
</uv-button>
|
||||
<uv-input
|
||||
v-if="keyboardIsShow"
|
||||
class="input-box"
|
||||
ref="inputRef"
|
||||
v-model="inputText"
|
||||
:maxlength="props.textValueLength"
|
||||
:adjustPosition="true"
|
||||
placeholderStyle="color:#999999;font-size:26rpx"
|
||||
placeholder="请输入文字"
|
||||
:disabled="props.isDisabled"
|
||||
></uv-input>
|
||||
</view>
|
||||
|
||||
<view class="talk-btn-box">
|
||||
<image
|
||||
class="talk-btn-icon"
|
||||
v-show="!keyboardIsShow"
|
||||
src="@/static/images/course/jianpan.png"
|
||||
mode=""
|
||||
@click="changeBtn"
|
||||
></image>
|
||||
<image
|
||||
class="talk-btn-icon"
|
||||
v-show="keyboardIsShow && !inputText"
|
||||
src="@/static/images/course/record.png"
|
||||
mode=""
|
||||
@click="changeBtn"
|
||||
></image>
|
||||
<image
|
||||
class="talk-btn-icon"
|
||||
v-show="keyboardIsShow && !!inputText"
|
||||
src="@/static/images/course/send.png"
|
||||
mode=""
|
||||
@click.stop="showKeyboard"
|
||||
></image>
|
||||
</view>
|
||||
<uni-popup ref="popupRef" class="popup">
|
||||
<view class="overlay-box">
|
||||
<view class="circle-bg-box">
|
||||
<view class="tips-box">松开发送</view>
|
||||
</view>
|
||||
<view class="btns-box">
|
||||
<image
|
||||
class="btn-box btn-box-1"
|
||||
:src="isOutStatus == '02' ? cancelActiveBtn : cancelBtn"
|
||||
mode="heightFix"
|
||||
@click="closePopup()"
|
||||
></image>
|
||||
<image
|
||||
class="btn-box btn-box-2"
|
||||
:src="isOutStatus == '03' ? translateActiveBtn : translateBtn"
|
||||
mode="heightFix"
|
||||
></image>
|
||||
</view>
|
||||
<view class="talk-dialog" :class="{ 'is-out': isOutStatus === '02', 'is-translate': isOutStatus === '03' }">
|
||||
<image src="@/static/images/course/voice-white.gif" mode="" class="gif-box"></image>
|
||||
<uv-textarea
|
||||
ref="inputRef"
|
||||
v-model="allText"
|
||||
:maxlength="255"
|
||||
@click.stop
|
||||
customStyle="background: #ffffff00; padding-bottom: 50rpx; color: #fff; caret-color: #fff"
|
||||
text-style="color: #fff"
|
||||
border="none"
|
||||
placeholderStyle="color:#999999;font-size:26rpx;"
|
||||
placeholder=""
|
||||
></uv-textarea>
|
||||
<!-- <view class="text-div" v-if="isOutStatus === '03'">
|
||||
{{ allText }}{{ tempText }}
|
||||
</view> -->
|
||||
</view>
|
||||
<view class="fl1"></view>
|
||||
</view>
|
||||
</uni-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import cancelBtn from '@/static/images/course/cancel.png';
|
||||
import cancelActiveBtn from '@/static/images/course/cancel-active.png';
|
||||
import translateBtn from '@/static/images/course/translate.png';
|
||||
import translateActiveBtn from '@/static/images/course/translate-active.png';
|
||||
import { ref, computed, onMounted, nextTick, watch, reactive } from 'vue';
|
||||
import common from '@/common/common';
|
||||
import { onHide } from '@dcloudio/uni-app';
|
||||
import permission from '@/common/permission.js';
|
||||
import { getPhoneEnvBool } from '@/common/common.js';
|
||||
import { initRecord } from '@/composables/useExamSubject.js';
|
||||
import { getToken } from '@/common/common';
|
||||
import { startAudioRecord, stopAudioRecord } from '@/uni_modules/ty-recording';
|
||||
import { get_base_url } from '@/api/request.js';
|
||||
import { ProtocolCodec, MessageType, ControlCommand,ControlCode } from '@/pages/sparring/js/ProtocolCodec';
|
||||
// const url = '/trapractice/voiceCallSocket/voiceCall'
|
||||
// const url = '/traapp/voiceTransSocket/open';
|
||||
const url = '/traapp/voiceAiModelSocket/open';
|
||||
const _baseUrl = get_base_url(url).split('http').join('ws');
|
||||
const emit = defineEmits(['submit']);
|
||||
const ws = ref(null);
|
||||
const props = defineProps({
|
||||
isDisabled: {
|
||||
default: false,
|
||||
type: Boolean
|
||||
},
|
||||
isLoading: {
|
||||
default: false,
|
||||
type: Boolean
|
||||
},
|
||||
loadingText: {
|
||||
default: '发送回答中,请稍后再试!',
|
||||
type: String
|
||||
},
|
||||
disabledText: {
|
||||
default: '请稍后再试!',
|
||||
type: String
|
||||
},
|
||||
canAnswer: {
|
||||
default: true,
|
||||
type: Boolean
|
||||
},
|
||||
canAnswerText: {
|
||||
default: '当前状态不能发送内容!',
|
||||
type: String
|
||||
},
|
||||
textValueLength: {
|
||||
type: Number,
|
||||
default: 500
|
||||
},
|
||||
minRecordDuration: {
|
||||
// 最小录音间隔
|
||||
type: Number,
|
||||
default: 1000
|
||||
}
|
||||
});
|
||||
// 记录 按下移动位置
|
||||
const touchPos = reactive({ x: 0, y: 0 });
|
||||
const windowInfo = uni.getWindowInfo();
|
||||
// 安全区域信息
|
||||
const safeArea = windowInfo.safeArea;
|
||||
console.log('safeArea', safeArea);
|
||||
// 计算是否超出按下盒子位置01代表下方盒子,02代表左上03代表右上
|
||||
const isOutStatus = computed(() => {
|
||||
const btnHeight = (windowInfo.screenWidth / 750) * 234;
|
||||
if (touchPos.y < safeArea.bottom - btnHeight) {
|
||||
if (touchPos.x < safeArea.right / 2) return '02';
|
||||
else return '03';
|
||||
} else {
|
||||
return '01';
|
||||
}
|
||||
});
|
||||
// 监听按下移动
|
||||
const handleMove = (obj) => {
|
||||
const changedTouche = obj.changedTouches[0];
|
||||
touchPos.x = changedTouche.pageX;
|
||||
touchPos.y = changedTouche.pageY;
|
||||
};
|
||||
const allText = ref('');
|
||||
const tempText = ref('');
|
||||
|
||||
// 键盘还是语音
|
||||
const keyboardIsShow = ref(false);
|
||||
const popupRef = ref(null);
|
||||
const inputText = ref('');
|
||||
let startTime = 0;
|
||||
|
||||
const startTimer = ref(null); // 延迟启动的定时器ID
|
||||
const startFlag = ref(1);
|
||||
const startRecord = async (obj) => {
|
||||
const changedTouche = obj.touches[0];
|
||||
touchPos.x = changedTouche.clientX;
|
||||
touchPos.y = changedTouche.clientY;
|
||||
const wsUrl = _baseUrl + url + '?summary=' + getToken();
|
||||
ws.value = uni.connectSocket({
|
||||
url: wsUrl,
|
||||
fail: () => {},
|
||||
success: () => {}
|
||||
});
|
||||
ws.value.onOpen((res) => {
|
||||
console.log('ws已连接');
|
||||
popupRef.value.open();
|
||||
startAudioRecord({
|
||||
onFrame:(pcmData) => {
|
||||
// pcmData 是ByteArray类型的原始音频数据
|
||||
console.log('采集到音频数据长度1:', pcmData.length);
|
||||
ws.value.send({
|
||||
data: ProtocolCodec.pack(MessageType.AUDIO_DATA, pcmData)
|
||||
});
|
||||
// 可在此处将PCM转Base64/编码为MP3等,或直接传输
|
||||
},
|
||||
onStop:(pcmData) => {
|
||||
console.log('关闭');
|
||||
},
|
||||
onError:(err) => {
|
||||
console.log('err', err.errMsg);
|
||||
console.log('err', err.errCode);
|
||||
},
|
||||
});
|
||||
});
|
||||
ws.value.onError((err) => {
|
||||
console.log('err', err);
|
||||
});
|
||||
ws.value.onClose((err) => {
|
||||
console.log('onClose', err);
|
||||
});
|
||||
ws.value.onMessage((res) => {
|
||||
const { msgType, body } = ProtocolCodec.unpack(res.data);
|
||||
console.log('来消息了', msgType, body);
|
||||
if (msgType === MessageType.TEXT_MESSAGE) {
|
||||
tempText.value = '';
|
||||
allText.value = allText.value + body;
|
||||
} else if (msgType === MessageType.TIP_MESSAGE) {
|
||||
tempText.value = body;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 关掉遮罩层
|
||||
const closePopup = () => {
|
||||
nextTick(() => {
|
||||
allText.value = '';
|
||||
tempText.value = '';
|
||||
popupRef.value.close();
|
||||
});
|
||||
ws.value.close({
|
||||
code: 1000,
|
||||
reason: '挂断',
|
||||
success: () => {
|
||||
console.log('挂断success');
|
||||
},
|
||||
fail: () => {
|
||||
console.log('挂断fail');
|
||||
},
|
||||
complete: () => {
|
||||
console.log('挂断complete');
|
||||
}
|
||||
});
|
||||
};
|
||||
// 松开按钮
|
||||
const loosenButton = () => {
|
||||
// 判断一下位置,03的时候是转文字不能关闭
|
||||
if (isOutStatus.value === '03') {
|
||||
// 03转文字
|
||||
console.log('已发送', '03转文字');
|
||||
ws.value.send({
|
||||
data: ProtocolCodec.pack(MessageType.CONTROL_CMD, {type:ControlCode.FRNOT_TO_SERVER_OVER})
|
||||
});
|
||||
} else {
|
||||
closePopup();
|
||||
}
|
||||
stopRecord();
|
||||
};
|
||||
// 结束录音
|
||||
const stopRecord = (status = false) => {
|
||||
console.log('结束录音stopRecord', status);
|
||||
// 判断一下位置
|
||||
if (isOutStatus.value === '01') {
|
||||
// 01发送
|
||||
} else if (isOutStatus.value === '02') {
|
||||
// 02取消
|
||||
} else if (isOutStatus.value === '03') {
|
||||
// 03转文字
|
||||
}
|
||||
|
||||
stopAudioRecord();
|
||||
|
||||
|
||||
};
|
||||
|
||||
onMounted(async () => {});
|
||||
onHide(() => {
|
||||
// closePopup();
|
||||
console.log('onHide');
|
||||
});
|
||||
|
||||
const activeNum = ref(1);
|
||||
|
||||
const clearinputText = (text = '') => {
|
||||
const old_text = inputText.value;
|
||||
inputText.value = text;
|
||||
return old_text;
|
||||
};
|
||||
|
||||
// 发送文本
|
||||
const showKeyboard = () => {
|
||||
if (keyboardIsShow.value) {
|
||||
const inputTextTram = inputText.value.trim();
|
||||
if (inputTextTram) {
|
||||
emit('submit', 'text', inputTextTram);
|
||||
} else {
|
||||
common.msg('不能发送空白消息!');
|
||||
}
|
||||
} else {
|
||||
keyboardIsShow.value = !keyboardIsShow.value;
|
||||
}
|
||||
};
|
||||
// 切换键盘语音
|
||||
const changeBtn = () => {
|
||||
if (props.isDisabled) return;
|
||||
keyboardIsShow.value = !keyboardIsShow.value;
|
||||
inputText.value = '';
|
||||
};
|
||||
// 禁用时点击给的提示
|
||||
const disabled_click = () => {
|
||||
console.log('禁用时点击给的提示');
|
||||
if (props.isDisabled) common.msg(props.disabledText);
|
||||
};
|
||||
|
||||
// 外面调用放弃这次
|
||||
const give_up = () => {
|
||||
console.log('调用结束语音回答');
|
||||
stopRecord(false);
|
||||
};
|
||||
defineExpose({ clearinputText, give_up });
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.talk-btns {
|
||||
height: 150rpx;
|
||||
background-color: #fff;
|
||||
padding: 25rpx 30rpx 0;
|
||||
position: relative;
|
||||
|
||||
.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;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: rgba(0, 0, 0, 0.9);
|
||||
|
||||
.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: 36rpx 0 12rpx 0;
|
||||
font-weight: 600;
|
||||
font-size: 34rpx;
|
||||
color: #000000;
|
||||
line-height: 46px;
|
||||
}
|
||||
|
||||
.btns-box {
|
||||
height: 240rpx;
|
||||
width: 100%;
|
||||
font-size: 28rpx;
|
||||
display: flex;
|
||||
z-index: 1200;
|
||||
justify-content: space-around;
|
||||
position: relative;
|
||||
.btn-box {
|
||||
position: absolute;
|
||||
height: 164rpx;
|
||||
transform: scale(1.2);
|
||||
bottom: -10rpx;
|
||||
}
|
||||
.btn-box-1 {
|
||||
left: -40rpx;
|
||||
}
|
||||
.btn-box-2 {
|
||||
right: -40rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.talk-dialog {
|
||||
width: 506rpx;
|
||||
min-height: 234rpx;
|
||||
margin: 0 auto;
|
||||
background-color: #06f;
|
||||
border-radius: 32rpx;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: width 0.3s ease;
|
||||
.text-div {
|
||||
overflow-y: scroll;
|
||||
max-height: 434rpx;
|
||||
padding: 20rpx;
|
||||
font-size: 32rpx;
|
||||
color: #fff;
|
||||
line-height: 40rpx;
|
||||
}
|
||||
&.is-translate {
|
||||
width: 706rpx;
|
||||
.gif-box {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.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>
|
||||
@@ -0,0 +1,11 @@
|
||||
export const emojiList = [
|
||||
'😀', '😃', '😄', '😁', '😆', '😅', '🤣', '😂', '🙂', '🙃', '😉', '😊', '😇', '🥰', '😍', '🤩', '😘', '😗', '☺',
|
||||
'😚', '😙', '🥲', '😋', '😛', '😜', '🤪', '😝', '🤑', '🤗', '🤭', '🤫', '🤔', '🤐', '🤨', '😐', '😑', '😶',
|
||||
'😏', '😒', '🙄', '😬', '🤥', '😌', '😔', '😪', '🤤', '😴', '😷', '🤒', '🤕', '🤢', '🤮', '🤧', '🥵', '🥶',
|
||||
'🥴', '😵', '🤯', '🤠', '🥳', '🥸', '😎', '🤓', '🧐', '😕', '😟', '🙁', '☹', '😮', '😯', '😲', '😳', '🥺', '😦',
|
||||
'😧', '😨', '😰', '😥', '😢', '😭', '😱', '😖', '😣', '😞', '😓', '😩', '😫', '🥱', '😤', '😡', '😠', '🤬',
|
||||
'😈', '👿', '💀', '☠', '*', '🤡', '👹', '👺', '👻', '👽', '👾', '🤖', '😺', '😸', '😹', '😻', '😼', '😽', '🙀',
|
||||
'😿', '😾', '🙈', '🙉', '🙊', '💋', '💌', '💘', '💝', '💖', '💗', '💓', '💞', '💕', '💟', '❣', '💔', '❤', '🧡',
|
||||
'💛', '💚', '💙', '💜', '🤎', '🖤', '🤍', '💯', '💢', '💥', '💫', '💦', '💨', '🕳', '💣', '💬', '👁🗨', '🗨',
|
||||
'🗯', '💭', '💤'
|
||||
]
|
||||
@@ -84,8 +84,7 @@
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view class="btn-box btn-box-height font_pf">
|
||||
<template v-if="[0, 1].includes(tabAct)">
|
||||
<view class="btn-box btn-box-height font_pf" v-if="[0, 1].includes(tabAct)">
|
||||
<view class="fl1">
|
||||
<uv-button type="primary" @click="goto_study()">去学习</uv-button>
|
||||
</view>
|
||||
@@ -96,13 +95,14 @@
|
||||
<uv-button type="success" :disabled="examinationButtonDisabled">去考试</uv-button>
|
||||
<!-- <uv-button type="success" :disabled="true">去考试</uv-button> -->
|
||||
</view>
|
||||
</template>
|
||||
<template v-if="tabAct === 2">
|
||||
<view class="fl1" @click="goto_submit()">
|
||||
<uv-button type="primary" class="send_evaluate" :disabled="isPreview === 'preview'">发布评价</uv-button>
|
||||
</view>
|
||||
</template>
|
||||
<!-- <template v-if="tabAct === 2"> -->
|
||||
<!-- <view class="fl1" @click="goto_submit()"> -->
|
||||
|
||||
<!-- <uv-button type="primary" class="send_evaluate" :disabled="isPreview === 'preview'">发布评价</uv-button> -->
|
||||
<!-- </view> -->
|
||||
<!-- </template> -->
|
||||
</view>
|
||||
<comment-input v-if="tabAct === 2"></comment-input>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -111,9 +111,13 @@
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
<TouchBtn class="talk-btns" @uploadVoice="uploadRecordNow" @submitAnswer="submitAnswerNow"
|
||||
<!-- <TouchBtn class="talk-btns" @uploadVoice="uploadRecordNow" @submitAnswer="submitAnswerNow"
|
||||
@startRecord="startRecordCallback" :loadingText="sendLoadingText" :isLoading="!answerIsOver"
|
||||
:adjustPosition="true" :isDisabled="isHistory" /> -->
|
||||
<touch-btn class="talk-btns" @uploadVoice="uploadRecordNow" @submitAnswer="submitAnswerNow"
|
||||
@startRecord="startRecordCallback" :loadingText="sendLoadingText" :isLoading="!answerIsOver"
|
||||
:adjustPosition="true" :isDisabled="isHistory" />
|
||||
|
||||
<History ref="historyRef" @showDetail="showHistoryDetail"></History>
|
||||
<PreviewVue ref="previewRef" />
|
||||
</scroll-view>
|
||||
@@ -142,7 +146,7 @@
|
||||
import {
|
||||
setupKeyboardHeightListener
|
||||
} from '@/common/common';
|
||||
import TouchBtn from '@/pages/course/components/touchBtn.vue';
|
||||
// import TouchBtn from '@/pages/course/components/touchBtn.vue';
|
||||
import TalkingItem from './components/talking-item.vue';
|
||||
import PreviewVue from './components/preview.vue';
|
||||
import {
|
||||
|
||||
@@ -1,324 +0,0 @@
|
||||
<template>
|
||||
<view class="live-container" :style="{ top: `${topVal}rpx`, left: `${leftVal}rpx` }">
|
||||
<cover-view class="pusher-container" @click.stop>
|
||||
<live-pusher
|
||||
id="livePusher"
|
||||
class="livePusher"
|
||||
:muted="true"
|
||||
:enable-camera="true"
|
||||
:auto-focus="true"
|
||||
:beauty="0"
|
||||
:whiteness="0"
|
||||
aspect="9:16"
|
||||
mode="SD"
|
||||
></live-pusher>
|
||||
</cover-view>
|
||||
<view
|
||||
class="drag-controller"
|
||||
@touchstart="handleTouchStart"
|
||||
@touchmove="handleTouchMove"
|
||||
@touchend="handleTouchEnd"
|
||||
>
|
||||
<!-- 完全透明,只用于接收触摸事件 -->
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
// 响应式数据
|
||||
const topVal = ref(150) // 初始顶部位置 (rpx)
|
||||
const leftVal = ref(0) // 初始左侧位置 (rpx)
|
||||
const livePusherContext = ref(null)
|
||||
|
||||
// 触摸相关数据
|
||||
const startX = ref(0) // 触摸起始X坐标 (px)
|
||||
const startY = ref(0) // 触摸起始Y坐标 (px)
|
||||
const startLeft = ref(0) // 触摸起始时的left值 (rpx)
|
||||
const startTop = ref(0) // 触摸起始时的top值 (rpx)
|
||||
const isAnimating = ref(false) // 新增:动画锁,防止多次触发
|
||||
|
||||
// 系统信息
|
||||
const systemInfo = uni.getSystemInfoSync()
|
||||
const screenWidthPx = systemInfo.windowWidth // 屏幕宽度(px)
|
||||
const screenHeightPx = systemInfo.windowHeight // 屏幕高度(px)
|
||||
const rpxRatio = 750 / screenWidthPx // rpx 转换比例
|
||||
const SCREEN_WIDTH_RPX = 750 // 新增:统一屏幕宽度(rpx),与设计稿一致
|
||||
|
||||
// 容器尺寸 (rpx)
|
||||
const CONTAINER_WIDTH = 270
|
||||
const CONTAINER_HEIGHT = 480
|
||||
|
||||
console.log('系统信息:', {
|
||||
screenWidthPx,
|
||||
screenHeightPx,
|
||||
rpxRatio,
|
||||
CONTAINER_WIDTH,
|
||||
CONTAINER_HEIGHT,
|
||||
SCREEN_WIDTH_RPX
|
||||
})
|
||||
|
||||
// 初始化
|
||||
const init = (componentProxy) => {
|
||||
livePusherContext.value = uni.createLivePusherContext('livePusher', componentProxy)
|
||||
}
|
||||
|
||||
// 切换摄像头
|
||||
const switchCamera = () => {
|
||||
if (!livePusherContext.value) return
|
||||
|
||||
livePusherContext.value.switchCamera({
|
||||
success: (res) => {
|
||||
console.log('livePusher.switchCamera:', JSON.stringify(res))
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('切换摄像头失败:', err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 触摸开始事件 - 增强错误处理
|
||||
const handleTouchStart = (e) => {
|
||||
console.log('=== 触摸开始 ===')
|
||||
// 新增:动画中不响应新的触摸开始
|
||||
if (isAnimating.value) {
|
||||
console.log('动画中,忽略触摸开始')
|
||||
return
|
||||
}
|
||||
|
||||
console.log('事件对象:', e)
|
||||
console.log('触摸点数组:', e.touches)
|
||||
|
||||
try {
|
||||
if (!e.touches || e.touches.length === 0) {
|
||||
console.error('没有检测到触摸点')
|
||||
return
|
||||
}
|
||||
|
||||
const touch = e.touches[0]
|
||||
console.log('第一个触摸点:', touch)
|
||||
|
||||
// 安全地获取触摸坐标
|
||||
const clientX = touch.clientX || touch.pageX || 0
|
||||
const clientY = touch.clientY || touch.pageY || 0
|
||||
|
||||
startX.value = clientX
|
||||
startY.value = clientY
|
||||
|
||||
// 记录当前容器的位置
|
||||
startLeft.value = leftVal.value
|
||||
startTop.value = topVal.value
|
||||
|
||||
console.log('记录的起始位置:', {
|
||||
startX: startX.value,
|
||||
startY: startY.value,
|
||||
startLeft: startLeft.value,
|
||||
startTop: startTop.value
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('触摸开始事件处理出错:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 触摸移动事件 - 修复计算逻辑
|
||||
const handleTouchMove = (e) => {
|
||||
console.log('=== 触摸移动 ===')
|
||||
|
||||
// 防止事件穿透
|
||||
if (e.stopPropagation) e.stopPropagation()
|
||||
if (e.preventDefault) e.preventDefault()
|
||||
|
||||
// 新增:动画中不响应触摸移动
|
||||
if (isAnimating.value) return
|
||||
|
||||
try {
|
||||
if (!e.touches || e.touches.length === 0) {
|
||||
console.log('触摸移动:没有触摸点')
|
||||
return
|
||||
}
|
||||
|
||||
const touch = e.touches[0]
|
||||
console.log('移动时的触摸点:', touch)
|
||||
|
||||
// 安全地获取当前触摸坐标
|
||||
const currentX = touch.clientX || touch.pageX || startX.value
|
||||
const currentY = touch.clientY || touch.pageY || startY.value
|
||||
|
||||
console.log('坐标比较:', {
|
||||
startX: startX.value,
|
||||
currentX: currentX,
|
||||
startY: startY.value,
|
||||
currentY: currentY
|
||||
})
|
||||
|
||||
// 计算偏移量 (px)
|
||||
const deltaX = currentX - startX.value
|
||||
const deltaY = currentY - startY.value
|
||||
|
||||
console.log('偏移量计算:', { deltaX, deltaY })
|
||||
|
||||
// 检查偏移量是否有效
|
||||
if (isNaN(deltaX) || isNaN(deltaY)) {
|
||||
console.error('计算出 NaN 偏移量:', { deltaX, deltaY })
|
||||
return
|
||||
}
|
||||
|
||||
// 将偏移量转换为 rpx 并更新位置
|
||||
const deltaXInRpx = deltaX * rpxRatio
|
||||
const deltaYInRpx = deltaY * rpxRatio
|
||||
|
||||
const newLeft = startLeft.value + deltaXInRpx
|
||||
const newTop = startTop.value + deltaYInRpx
|
||||
|
||||
// 边界检测 - 修复:使用统一的SCREEN_WIDTH_RPX,而非硬编码750
|
||||
const maxLeft = SCREEN_WIDTH_RPX - CONTAINER_WIDTH
|
||||
const screenHeightInRpx = screenHeightPx * rpxRatio
|
||||
const maxTop = screenHeightInRpx - CONTAINER_HEIGHT
|
||||
|
||||
console.log('最终位置计算:', {
|
||||
deltaX, deltaY,
|
||||
deltaXInRpx, deltaYInRpx,
|
||||
newLeft, newTop,
|
||||
maxLeft, maxTop,
|
||||
screenHeightInRpx
|
||||
})
|
||||
|
||||
// 确保位置在有效范围内且不是 NaN
|
||||
if (!isNaN(newLeft) && !isNaN(newTop)) {
|
||||
leftVal.value = Math.max(0, Math.min(newLeft, maxLeft))
|
||||
topVal.value = Math.max(0, Math.min(newTop, maxTop))
|
||||
|
||||
console.log('更新后的位置:', { left: leftVal.value, top: topVal.value })
|
||||
} else {
|
||||
console.error('计算出 NaN 位置:', { newLeft, newTop })
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('触摸移动事件处理出错:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 触摸结束事件 - 核心:自动吸附到最近边缘 + 平滑动画
|
||||
const handleTouchEnd = (e) => {
|
||||
console.log('=== 触摸结束 ===')
|
||||
if (isAnimating.value) return
|
||||
|
||||
// 1. 计算核心参数
|
||||
const currentLeft = leftVal.value // 松手时小窗的左偏移
|
||||
const windowCenterX = currentLeft + CONTAINER_WIDTH / 2 // 小窗中心的X坐标(rpx)
|
||||
const screenMiddleX = SCREEN_WIDTH_RPX / 2 // 屏幕中线X坐标(rpx)
|
||||
|
||||
// 2. 判断更近的边缘:中心在左半屏 → 吸附左侧;右半屏 → 吸附右侧
|
||||
const targetLeft = windowCenterX < screenMiddleX ? 0 : SCREEN_WIDTH_RPX - CONTAINER_WIDTH
|
||||
const targetTop = topVal.value // Y轴保持松手位置
|
||||
|
||||
// 3. 平滑动画过渡
|
||||
const duration = 140 // 动画时长(ms)
|
||||
const frameCount = 60 // 动画帧数
|
||||
const stepX = (targetLeft - currentLeft) / frameCount // 每帧X偏移量
|
||||
const stepY = (targetTop - topVal.value) / frameCount // Y轴无偏移
|
||||
let currentFrame = 0
|
||||
|
||||
isAnimating.value = true
|
||||
const animationTimer = setInterval(() => {
|
||||
currentFrame++
|
||||
// 更新位置
|
||||
leftVal.value += stepX
|
||||
topVal.value += stepY
|
||||
|
||||
// 动画结束
|
||||
if (currentFrame >= frameCount) {
|
||||
clearInterval(animationTimer)
|
||||
// 强制修正到目标位置(避免浮点误差)
|
||||
leftVal.value = targetLeft
|
||||
topVal.value = targetTop
|
||||
isAnimating.value = false
|
||||
console.log('吸附完成,最终位置:', { left: leftVal.value, top: topVal.value })
|
||||
}
|
||||
}, duration / frameCount)
|
||||
|
||||
// 仅重置触摸起始坐标
|
||||
startX.value = 0
|
||||
startY.value = 0
|
||||
}
|
||||
|
||||
// 开启预览
|
||||
const startPreview = () => {
|
||||
console.log('摄像头开启预览')
|
||||
if (!livePusherContext.value) return
|
||||
|
||||
livePusherContext.value.startPreview({
|
||||
success: (res) => {
|
||||
console.log('摄像头开启成功')
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('开启预览失败:', err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 关闭预览
|
||||
const stopPreview = () => {
|
||||
console.log('关闭预览')
|
||||
if (!livePusherContext.value) return
|
||||
|
||||
livePusherContext.value.stopPreview({
|
||||
success: (res) => {
|
||||
console.log('livePusher.stopPreview:', JSON.stringify(res))
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('关闭预览失败:', err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 组件挂载后的初始化
|
||||
onMounted(() => {
|
||||
console.log('组件挂载完成')
|
||||
|
||||
// 初始化左侧位置为屏幕右侧
|
||||
leftVal.value = SCREEN_WIDTH_RPX - CONTAINER_WIDTH // 修复:使用容器实际宽度,而非硬编码280
|
||||
|
||||
console.log('初始化位置:', {
|
||||
screenWidthPx,
|
||||
rpxRatio,
|
||||
SCREEN_WIDTH_RPX,
|
||||
leftVal: leftVal.value
|
||||
})
|
||||
})
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
init,
|
||||
switchCamera,
|
||||
stopPreview,
|
||||
startPreview
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.live-container {
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
border-radius: 20rpx;
|
||||
width: 270rpx;
|
||||
height: 480rpx;
|
||||
background-color: red;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.pusher-container {
|
||||
width: 270rpx;
|
||||
height: 480rpx;
|
||||
}
|
||||
|
||||
.drag-controller {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(255, 0, 0, 0.1); /* 半透明便于调试 */
|
||||
position: absolute;
|
||||
z-index: 999;
|
||||
}
|
||||
</style>
|
||||
@@ -1,322 +0,0 @@
|
||||
<template>
|
||||
<view class="live-container" :style="{ top: `${topVal}px`, left: `${leftVal}px` }">
|
||||
<cover-view class="pusher-container" @click.stop>
|
||||
<live-pusher
|
||||
id="livePusher"
|
||||
class="livePusher"
|
||||
:muted="true"
|
||||
:enable-camera="true"
|
||||
:auto-focus="true"
|
||||
:beauty="0"
|
||||
:whiteness="0"
|
||||
aspect="9:16"
|
||||
mode="SD"
|
||||
></live-pusher>
|
||||
</cover-view>
|
||||
<view
|
||||
class="drag-controller"
|
||||
@touchstart="handleTouchStart"
|
||||
@touchmove="handleTouchMove"
|
||||
@touchend="handleTouchEnd"
|
||||
>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
// 响应式数据 - 直接使用px单位
|
||||
const topVal = ref(40) // 初始顶部位置 (px)
|
||||
const leftVal = ref(0) // 初始左侧位置 (px)
|
||||
const livePusherContext = ref(null)
|
||||
|
||||
// 触摸相关数据
|
||||
const startX = ref(0) // 触摸起始X坐标 (px)
|
||||
const startY = ref(0) // 触摸起始Y坐标 (px)
|
||||
const startLeft = ref(0) // 触摸起始时的left值 (px)
|
||||
const startTop = ref(0) // 触摸起始时的top值 (px)
|
||||
const isAnimating = ref(false) // 动画锁,防止多次触发
|
||||
|
||||
// 系统信息 - 直接使用px,无需rpx换算
|
||||
const systemInfo = uni.getSystemInfoSync()
|
||||
const screenWidthPx = systemInfo.windowWidth // 屏幕宽度(px)
|
||||
const screenHeightPx = systemInfo.windowHeight // 屏幕高度(px)
|
||||
|
||||
// 容器尺寸 (px) - 直接定义像素尺寸
|
||||
const CONTAINER_WIDTH = 135 // 对应原来的270rpx (750设计稿下 270/2=135px)
|
||||
const CONTAINER_HEIGHT = 240 // 对应原来的480rpx (750设计稿下 480/2=240px)
|
||||
|
||||
console.log('系统信息:', {
|
||||
screenWidthPx,
|
||||
screenHeightPx,
|
||||
CONTAINER_WIDTH,
|
||||
CONTAINER_HEIGHT
|
||||
})
|
||||
|
||||
// 初始化
|
||||
const init = (componentProxy) => {
|
||||
livePusherContext.value = uni.createLivePusherContext('livePusher', componentProxy)
|
||||
}
|
||||
|
||||
// 切换摄像头
|
||||
const switchCamera = () => {
|
||||
if (!livePusherContext.value) return
|
||||
|
||||
livePusherContext.value.switchCamera({
|
||||
success: (res) => {
|
||||
console.log('livePusher.switchCamera:', JSON.stringify(res))
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('切换摄像头失败:', err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 触摸开始事件
|
||||
const handleTouchStart = (e) => {
|
||||
console.log('=== 触摸开始 ===')
|
||||
// 结束动画中和,不能开始
|
||||
if (isAnimating.value) return
|
||||
|
||||
console.log('事件对象:', e)
|
||||
console.log('触摸点数组:', e.touches)
|
||||
|
||||
try {
|
||||
if (!e.touches || e.touches.length === 0) {
|
||||
console.error('没有检测到触摸点')
|
||||
return
|
||||
}
|
||||
const touch = e.touches[0]
|
||||
// 安全地获取触摸坐标
|
||||
const clientX = touch.screenX
|
||||
const clientY = touch.screenY
|
||||
|
||||
startX.value = clientX
|
||||
startY.value = clientY
|
||||
|
||||
// 记录当前容器的位置
|
||||
startLeft.value = leftVal.value
|
||||
startTop.value = topVal.value
|
||||
|
||||
console.log('记录的起始位置:', {
|
||||
startX: startX.value,
|
||||
startY: startY.value,
|
||||
startLeft: startLeft.value,
|
||||
startTop: startTop.value
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('触摸开始事件处理出错:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 触摸移动事件 - 移除rpx换算,直接使用px计算
|
||||
const handleTouchMove = (e) => {
|
||||
console.log('=== 触摸移动 ===')
|
||||
|
||||
// 防止事件穿透
|
||||
if (e.stopPropagation) e.stopPropagation()
|
||||
if (e.preventDefault) e.preventDefault()
|
||||
|
||||
// 动画中不响应触摸移动
|
||||
if (isAnimating.value) return
|
||||
|
||||
try {
|
||||
if (!e.touches || e.touches.length === 0) {
|
||||
console.log('触摸移动:没有触摸点')
|
||||
return
|
||||
}
|
||||
|
||||
const touch = e.touches[0]
|
||||
console.log('移动时的触摸点:', touch)
|
||||
|
||||
// 安全地获取当前触摸坐标
|
||||
const currentX = touch.screenX
|
||||
const currentY = touch.screenY
|
||||
|
||||
console.log('坐标比较:', {
|
||||
startX: startX.value,
|
||||
currentX: currentX,
|
||||
startY: startY.value,
|
||||
currentY: currentY
|
||||
})
|
||||
|
||||
// 计算偏移量 (px) - 直接使用px,无需换算
|
||||
const deltaX = currentX - startX.value
|
||||
const deltaY = currentY - startY.value
|
||||
|
||||
console.log('偏移量计算:', { deltaX, deltaY })
|
||||
|
||||
// 检查偏移量是否有效
|
||||
if (isNaN(deltaX) || isNaN(deltaY)) {
|
||||
console.error('计算出 NaN 偏移量:', { deltaX, deltaY })
|
||||
return
|
||||
}
|
||||
|
||||
// 直接更新位置(px),无需rpx转换
|
||||
const newLeft = startLeft.value + deltaX
|
||||
const newTop = startTop.value + deltaY
|
||||
|
||||
// 边界检测 - 使用px单位计算边界
|
||||
const maxLeft = screenWidthPx - CONTAINER_WIDTH
|
||||
const maxTop = screenHeightPx - CONTAINER_HEIGHT
|
||||
|
||||
console.log('最终位置计算:', {
|
||||
deltaX, deltaY,
|
||||
newLeft, newTop,
|
||||
maxLeft, maxTop,
|
||||
screenWidthPx,
|
||||
screenHeightPx
|
||||
})
|
||||
|
||||
// 确保位置在有效范围内且不是 NaN
|
||||
if (!isNaN(newLeft) && !isNaN(newTop)) {
|
||||
leftVal.value = Math.max(0, Math.min(newLeft, maxLeft))
|
||||
topVal.value = Math.max(0, Math.min(newTop, maxTop))
|
||||
|
||||
console.log('更新后的位置:', { left: leftVal.value, top: topVal.value })
|
||||
} else {
|
||||
console.error('计算出 NaN 位置:', { newLeft, newTop })
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('触摸移动事件处理出错:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 触摸结束事件 - 吸附逻辑改为px计算
|
||||
const handleTouchEnd = (e) => {
|
||||
console.log('=== 触摸结束 ===')
|
||||
if (isAnimating.value) return
|
||||
|
||||
|
||||
// 1. 计算核心参数(px单位)
|
||||
const currentLeft = leftVal.value // 松手时小窗的左偏移
|
||||
const windowCenterX = currentLeft + CONTAINER_WIDTH / 2 // 小窗中心的X坐标(px)
|
||||
const screenMiddleX = screenWidthPx / 2 // 屏幕中线X坐标(px)
|
||||
|
||||
// 2. 判断更近的边缘:中心在左半屏 → 吸附左侧;右半屏 → 吸附右侧
|
||||
const targetLeft = windowCenterX < screenMiddleX ? 0 : screenWidthPx - CONTAINER_WIDTH
|
||||
const targetTop = topVal.value // Y轴保持松手位置
|
||||
|
||||
// 3. 平滑动画过渡
|
||||
const duration = 160 // 动画时长(ms)
|
||||
const frameCount = 60 // 动画帧数
|
||||
const stepX = (targetLeft - currentLeft) / frameCount // 每帧X偏移量
|
||||
const stepY = (targetTop - topVal.value) / frameCount // Y轴无偏移
|
||||
let currentFrame = 0
|
||||
|
||||
isAnimating.value = true
|
||||
const animationTimer = setInterval(() => {
|
||||
currentFrame++
|
||||
// 更新位置
|
||||
leftVal.value += stepX
|
||||
topVal.value += stepY
|
||||
|
||||
// 动画结束
|
||||
if (currentFrame >= frameCount) {
|
||||
clearInterval(animationTimer)
|
||||
// 强制修正到目标位置(避免浮点误差)
|
||||
leftVal.value = targetLeft
|
||||
topVal.value = targetTop
|
||||
isAnimating.value = false
|
||||
console.log('吸附完成,最终位置:', { left: leftVal.value, top: topVal.value })
|
||||
}
|
||||
}, duration / frameCount)
|
||||
|
||||
// 仅重置触摸起始坐标
|
||||
startX.value = 0
|
||||
startY.value = 0
|
||||
}
|
||||
|
||||
// 开启预览
|
||||
const startPreview = () => {
|
||||
console.log('摄像头开启预览')
|
||||
if (!livePusherContext.value) return
|
||||
|
||||
livePusherContext.value.startPreview({
|
||||
success: (res) => {
|
||||
console.log('摄像头开启成功')
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('开启预览失败:', err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 关闭预览
|
||||
const stopPreview = () => {
|
||||
console.log('关闭预览')
|
||||
if (!livePusherContext.value) return
|
||||
|
||||
livePusherContext.value.stopPreview({
|
||||
success: (res) => {
|
||||
console.log('livePusher.stopPreview:', JSON.stringify(res))
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('关闭预览失败:', err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 组件挂载后的初始化
|
||||
onMounted(() => {
|
||||
console.log('组件挂载完成')
|
||||
|
||||
// 初始化左侧位置为屏幕右侧(px单位)
|
||||
leftVal.value = screenWidthPx - CONTAINER_WIDTH
|
||||
|
||||
console.log('初始化位置:', {
|
||||
screenWidthPx,
|
||||
leftVal: leftVal.value
|
||||
})
|
||||
})
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
init,
|
||||
switchCamera,
|
||||
stopPreview,
|
||||
startPreview
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.live-container {
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
border-radius: 10px;
|
||||
width: 135px; /* 对应原来的270rpx */
|
||||
height: 240px; /* 对应原来的480rpx */
|
||||
background-color: red;
|
||||
display: flex;
|
||||
}
|
||||
.livePusher{
|
||||
width: 135px;
|
||||
height: 240px;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
|
||||
}
|
||||
|
||||
.pusher-container {
|
||||
width: 135px; /* 对应原来的270rpx */
|
||||
height: 240px; /* 对应原来的480rpx */
|
||||
border-radius: 20px; /* 圆角大小,根据需求调整 */
|
||||
overflow: hidden; /* 溢出裁剪,必须设置 */
|
||||
/* 可选:添加阴影增强视觉效果 */
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.drag-controller {
|
||||
width: 135px; /* 对应原来的270rpx */
|
||||
height: 240px; /* 对应原来的480rpx */
|
||||
position: absolute;
|
||||
z-index: 550;
|
||||
border-radius: 20px; /* 圆角大小,根据需求调整 */
|
||||
overflow: hidden; /* 溢出裁剪,必须设置 */
|
||||
/* 可选:添加阴影增强视觉效果 */
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
</style>
|
||||
@@ -47,6 +47,8 @@ export enum ControlCode {
|
||||
AI_ANSWER_OVER = 0b0100, // 本轮AI回答的文本内容已经全部返回
|
||||
AI_CLUE = 0b0101, // 需要AI提示命令(发后端)
|
||||
AI_CLUE_OVER = 0b0110, // AI提示命令已经全部返回
|
||||
FRNOT_TO_SERVER_OVER = 0b0111, // 语音按钮已经抬起
|
||||
SERVER_TO_FRNOT_OVER = 0b1000, // 后端已经返回本次全部内容
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 194 KiB |
|
Before Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 117 KiB |
|
After Width: | Height: | Size: 758 KiB |
|
Before Width: | Height: | Size: 746 KiB |
|
Before Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 86 KiB |
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"id": "ty-recording",
|
||||
"displayName": "ty-recording",
|
||||
"version": "1.0.0",
|
||||
"description": "ty-recording",
|
||||
"keywords": [
|
||||
"ty-recording"
|
||||
],
|
||||
"repository": "",
|
||||
"engines": {
|
||||
"HBuilderX": "^3.6.8"
|
||||
},
|
||||
"dcloudext": {
|
||||
"type": "uts",
|
||||
"sale": {
|
||||
"regular": {
|
||||
"price": "0.00"
|
||||
},
|
||||
"sourcecode": {
|
||||
"price": "0.00"
|
||||
}
|
||||
},
|
||||
"contact": {
|
||||
"qq": ""
|
||||
},
|
||||
"declaration": {
|
||||
"ads": "",
|
||||
"data": "",
|
||||
"permissions": ""
|
||||
},
|
||||
"npmurl": ""
|
||||
},
|
||||
"uni_modules": {
|
||||
"dependencies": [],
|
||||
"encrypt": [],
|
||||
"platforms": {
|
||||
"cloud": {
|
||||
"tcb": "u",
|
||||
"aliyun": "u",
|
||||
"alipay": "u"
|
||||
},
|
||||
"client": {
|
||||
"Vue": {
|
||||
"vue2": "u",
|
||||
"vue3": "u"
|
||||
},
|
||||
"App": {
|
||||
"app-android": "u",
|
||||
"app-ios": "u",
|
||||
"app-harmony": "u"
|
||||
},
|
||||
"H5-mobile": {
|
||||
"Safari": "u",
|
||||
"Android Browser": "u",
|
||||
"微信浏览器(Android)": "u",
|
||||
"QQ浏览器(Android)": "u"
|
||||
},
|
||||
"H5-pc": {
|
||||
"Chrome": "u",
|
||||
"IE": "u",
|
||||
"Edge": "u",
|
||||
"Firefox": "u",
|
||||
"Safari": "u"
|
||||
},
|
||||
"小程序": {
|
||||
"微信": "u",
|
||||
"阿里": "u",
|
||||
"百度": "u",
|
||||
"字节跳动": "u",
|
||||
"QQ": "u",
|
||||
"钉钉": "u",
|
||||
"快手": "u",
|
||||
"飞书": "u",
|
||||
"京东": "u"
|
||||
},
|
||||
"快应用": {
|
||||
"华为": "u",
|
||||
"联盟": "u"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
# ty-recording
|
||||
### 开发文档
|
||||
[UTS 语法](https://uniapp.dcloud.net.cn/tutorial/syntax-uts.html)
|
||||
[UTS API插件](https://uniapp.dcloud.net.cn/plugin/uts-plugin.html)
|
||||
[UTS uni-app兼容模式组件](https://uniapp.dcloud.net.cn/plugin/uts-component.html)
|
||||
[UTS 标准模式组件](https://doc.dcloud.net.cn/uni-app-x/plugin/uts-vue-component.html)
|
||||
[Hello UTS](https://gitcode.net/dcloud/hello-uts)
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"minSdkVersion": "35"
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import { StartAudioRecord, StartAudioRecordOptions } from '../interface.uts';
|
||||
import { StartAudioRecordFailImpl,AudioErrorCode } from '../unierror.uts';
|
||||
|
||||
/**
|
||||
* 引用Android音频相关系统库
|
||||
*/
|
||||
import AudioRecord from "android.media.AudioRecord";
|
||||
import MediaRecorder from "android.media.MediaRecorder";
|
||||
import AudioFormat from "android.media.AudioFormat";
|
||||
import { UTSAndroid } from "io.dcloud.uts";
|
||||
import HandlerThread from "android.os.HandlerThread";
|
||||
import Handler from "android.os.Handler";
|
||||
import AtomicBoolean from "java.util.concurrent.atomic.AtomicBoolean";
|
||||
const isRecording = new AtomicBoolean(false); // 原子布尔值,保证线程安全
|
||||
|
||||
// 1. 音频采集参数(极简配置,适配大部分设备)
|
||||
const sampleRate : Int = 16000; // 采样率
|
||||
const channelConfig = AudioFormat.CHANNEL_IN_MONO; // 单声道
|
||||
const audioFormat = AudioFormat.ENCODING_PCM_16BIT; // 16位PCM
|
||||
console.log('channelConfig', channelConfig);
|
||||
console.log('audioFormat', audioFormat);
|
||||
const frameSize = 1024; // 对应JS的frameSize:1024
|
||||
const gain = 1.0; // 对应JS的gain:1.0
|
||||
const fromSampleRate = 48000; // 设备原始采样率(JS的fromSampleRate:48000)
|
||||
const frameByteSize = frameSize * 2 * 1;
|
||||
const minBufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat);
|
||||
// 最终缓冲区大小:取最小缓冲区和帧字节数的最大值(避免缓冲区过小)
|
||||
const bufferSize = minBufferSize * 2;
|
||||
|
||||
console.log('bufferSize', bufferSize);
|
||||
let audioRecord : AudioRecord | null = null;
|
||||
let recordThread : Thread | null = null;
|
||||
// let isRecording : boolean = false;
|
||||
|
||||
|
||||
|
||||
// ========== 2. 对外暴露的便捷方法(符合UTS导出规范) ==========
|
||||
@UTSJS.keepAlive
|
||||
export function startAudioRecord(options : StartAudioRecordOptions) : void {
|
||||
console.log('启动', isRecording.get());
|
||||
if (isRecording.get()) return; // 避免重复启动
|
||||
const _audioRecord = new AudioRecord(
|
||||
MediaRecorder.AudioSource.MIC,
|
||||
sampleRate,
|
||||
channelConfig,
|
||||
audioFormat,
|
||||
bufferSize
|
||||
);
|
||||
|
||||
if (_audioRecord.getState() != AudioRecord.STATE_INITIALIZED) {
|
||||
|
||||
const err = new StartAudioRecordFailImpl(AudioErrorCode.AUDIO_FORMAT_ILLEGAL);
|
||||
options.onError?.(err);
|
||||
return;
|
||||
}
|
||||
|
||||
audioRecord = _audioRecord;
|
||||
isRecording.set(true)
|
||||
const _recordThread = new Thread(() => {
|
||||
_audioRecord.startRecording(); // 开始采集
|
||||
console.log('AudioRecord已启动采集');
|
||||
const buffer = new ByteArray(bufferSize); // 缓冲区只创建一次,避免重复分配内存
|
||||
while (isRecording.get()) {
|
||||
const readSize = _audioRecord.read(buffer, 0, bufferSize);
|
||||
if (readSize > 0) {
|
||||
const data = new ByteArray(readSize);
|
||||
java.lang.System.arraycopy(buffer, 0, data, 0, readSize);
|
||||
options.onFrame?.(data)
|
||||
}
|
||||
}
|
||||
// 停止采集
|
||||
if (_audioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) {
|
||||
console.log('停止采集xxxxxxxxx');
|
||||
_audioRecord.stop();
|
||||
options.onStop?.()
|
||||
}
|
||||
});
|
||||
_recordThread.start();
|
||||
recordThread = _recordThread
|
||||
}
|
||||
export function stopAudioRecord() : void {
|
||||
console.log('停止麦克风采集', isRecording.get());
|
||||
if (!isRecording.get()) return;
|
||||
isRecording.set(false)
|
||||
// 释放AudioRecord(局部常量暂存)
|
||||
const _audioRecord = audioRecord;
|
||||
if (_audioRecord != null) {
|
||||
try {
|
||||
if (_audioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) {
|
||||
_audioRecord.stop();
|
||||
}
|
||||
_audioRecord.release();
|
||||
} catch (e) { /* 忽略异常 */ }
|
||||
}
|
||||
audioRecord = null;
|
||||
// 等待线程结束
|
||||
console.log('等待线程结束');
|
||||
const _recordThread = recordThread;
|
||||
if (_recordThread != null) {
|
||||
try {
|
||||
_recordThread.join(1000); // 等待1秒,超时则放弃(避免卡死)
|
||||
console.log("后台线程已停止");
|
||||
} catch (e) {
|
||||
console.warn("停止后台线程时出现异常:", e);
|
||||
} finally {
|
||||
recordThread = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"deploymentTarget": "13"
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* 引用 iOS 系统库,示例如下:
|
||||
* import { UIDevice } from "UIKit";
|
||||
* [可选实现,按需引入]
|
||||
*/
|
||||
|
||||
/* 引入 interface.uts 文件中定义的变量 */
|
||||
import { MyApiOptions, MyApiResult, MyApi, MyApiSync } from '../interface.uts';
|
||||
|
||||
/* 引入 unierror.uts 文件中定义的变量 */
|
||||
import { MyApiFailImpl } from '../unierror';
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* interface.uts
|
||||
* uts插件接口定义文件,按规范定义接口文件可以在HBuilderX中更好的做到语法提示
|
||||
*/
|
||||
|
||||
import { AudioErrorCode } from "./unierror.uts"
|
||||
|
||||
// 成功开启录音的回调参数:返回录音基础配置
|
||||
export type AudioStartRes = {
|
||||
recordId : string; // 录音唯一标识(方便多录音实例管理)
|
||||
sampleRate : number; // 采样率(如16000)
|
||||
format : string; // 录音格式(如pcm/mp3)
|
||||
startTime : number; // 开启时间戳(毫秒)
|
||||
};
|
||||
|
||||
|
||||
// 录音结束并返回文件的回调参数:核心是录音文件信息
|
||||
export type AudioFileRes = {
|
||||
filePath : string; // 录音文件本地路径(UTS/Uniapp 通用路径)
|
||||
fileSize : number; // 文件大小(字节)
|
||||
duration : number; // 录音时长(秒)
|
||||
format : string; // 文件格式(如pcm/mp3)
|
||||
endTime : number; // 结束时间戳(毫秒)
|
||||
};
|
||||
|
||||
/**
|
||||
* 错误码
|
||||
* 根据uni错误码规范要求,建议错误码以90开头,以下是错误码示例:
|
||||
* - 9010001 错误信息1
|
||||
* - 9010002 错误信息2
|
||||
*/
|
||||
// export type AudioErrorCode = 9010001 | 9010002;
|
||||
|
||||
/**
|
||||
* 录音错误的回调参数:带错误码+错误信息,方便问题定位
|
||||
*/
|
||||
export interface AudioErrorRes extends IUniError {
|
||||
errCode : number
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* 开启录音的完整参数配置
|
||||
* 所有回调职责边界清晰,无模糊命名
|
||||
*/
|
||||
|
||||
export type StartAudioRecordOptions = {
|
||||
autoStopDuration ?: number; // 自动关闭时长,单位【秒】;传正数值则超过该时间自动关闭,不传/非正数不开启
|
||||
|
||||
// 【实时持续回调】- 录制过程中高频触发
|
||||
onFrame ?: (frameData : any) => void; // 实时音频帧回调(每帧一次)
|
||||
onVolume ?: (volume : number) => void; // 实时音量回调(如每秒5次,按设备采集频率)
|
||||
|
||||
// 【一次性状态回调】- 特定节点仅触发一次
|
||||
onStart ?: (res : AudioStartRes) => void; // 成功开启录音的回调(仅触发1次)
|
||||
onError ?: (res : AudioErrorRes) => void; // 录音错误回调(任意阶段出错都触发,仅1次)
|
||||
onStop ?: () => void; // 录音结束的纯状态回调(仅通知结束,无返回,1次)
|
||||
onFile ?: (res : AudioFileRes) => void; // 录音结束后,返回录音文件的回调(1次,核心)
|
||||
};
|
||||
|
||||
|
||||
/* 函数定义 */
|
||||
|
||||
export type StartAudioRecord = (options : StartAudioRecordOptions) => void
|
||||
@@ -0,0 +1,146 @@
|
||||
/* 此规范为 uni 规范,可以按照自己的需要选择是否实现 */
|
||||
import { AudioErrorRes } from "./interface.uts"
|
||||
/**
|
||||
* 错误主题
|
||||
* 注意:错误主题一般为插件名称,每个组件不同,需要使用时请更改。
|
||||
* [可选实现]
|
||||
*/
|
||||
export const UniErrorSubject = 'ty-recording';
|
||||
|
||||
|
||||
/**
|
||||
* 错误信息
|
||||
* @UniError
|
||||
*/
|
||||
/**
|
||||
* 音频录制错误码枚举
|
||||
* 前缀901:专属音频模块错误码
|
||||
* 分段规则:9010001-9010009(权限)、9010010-9010019(初始化)、9010020-9010029(采集)、9010030-9010039(线程)、9010040-9010049(资源)、9010050-9010059(参数)、9010060-9010069(系统/硬件)、9010070+(预留)
|
||||
* @UniError
|
||||
*/
|
||||
export enum AudioErrorCode {
|
||||
// 权限相关(9010001-9010009)
|
||||
MIC_PERMISSION_NONE = 9010001, // 麦克风权限未授予
|
||||
MIC_PERMISSION_DENIED = 9010002, // 麦克风权限被永久拒绝
|
||||
MIC_PERMISSION_GET_FAIL = 9010003,// 获取麦克风权限状态失败
|
||||
|
||||
// 初始化相关(9010010-9010019)
|
||||
AUDIO_RECORD_INIT_FAIL = 9010010, // AudioRecord实例初始化失败
|
||||
AUDIO_RECORD_NOT_INIT = 9010011, // AudioRecord未初始化就调用录制/停止
|
||||
AUDIO_RECORD_CREATE_FAIL = 9010012,// AudioRecord实例创建失败(系统资源不足)
|
||||
|
||||
// 采集过程相关(9010020-9010029)
|
||||
RECORD_READ_DATA_FAIL = 9010020, // 音频采集读取数据失败
|
||||
RECORD_INTERRUPTED = 9010021, // 音频采集被中断(如麦克风被抢占)
|
||||
RECORD_BUFFER_EMPTY = 9010022, // 采集缓冲区无有效数据
|
||||
RECORD_START_FAIL = 9010023, // 调用startRecording启动采集失败
|
||||
|
||||
// 线程相关(9010030-9010039)
|
||||
RECORD_THREAD_CREATE_FAIL = 9010030,// 采集线程创建失败
|
||||
RECORD_THREAD_START_FAIL = 9010031, // 采集线程启动失败
|
||||
RECORD_THREAD_INTERRUPT_FAIL = 9010032,// 采集线程中断失败
|
||||
RECORD_THREAD_CRASH = 9010033, // 采集线程运行崩溃
|
||||
|
||||
// 资源释放相关(9010040-9010049)
|
||||
RECORD_STOP_FAIL = 9010040, // 调用stop停止采集失败
|
||||
AUDIO_RECORD_RELEASE_FAIL = 9010041,// AudioRecord资源释放失败
|
||||
RECORD_REPEAT_RELEASE = 9010042, // 音频资源重复释放
|
||||
RECORD_RESOURCE_LEAK = 9010043, // 音频资源泄漏(未正常释放)
|
||||
|
||||
// 参数配置相关(9010050-9010059)
|
||||
SAMPLE_RATE_NOT_SUPPORT = 9010050, // 采样率不被设备支持
|
||||
CHANNEL_CONFIG_ILLEGAL = 9010051, // 声道配置非法/不支持
|
||||
AUDIO_FORMAT_ILLEGAL = 9010052, // 音频格式非法/不支持
|
||||
BUFFER_SIZE_TOO_SMALL = 9010053, // 采集缓冲区大小过小(低于系统最小值)
|
||||
BUFFER_SIZE_ILLEGAL = 9010054, // 采集缓冲区大小配置非法
|
||||
|
||||
// 系统/硬件/外部冲突(9010060-9010069)
|
||||
MIC_OCCUPIED = 9010060, // 麦克风被其他应用占用
|
||||
AUDIO_HARDWARE_ERROR = 9010061, // 音频硬件故障
|
||||
SYSTEM_RESOURCE_INSUFFICIENT = 9010062,// 系统资源不足(无法创建音频实例)
|
||||
AUDIO_SERVICE_EXCEPTION = 9010063,// 系统音频服务异常/未启动
|
||||
|
||||
// 通用业务异常(9010070-9010079)
|
||||
RECORD_REPEAT_START = 9010070, // 重复启动音频录制
|
||||
RECORD_NOT_RUNNING = 9010071, // 未在录制状态却调用停止/暂停
|
||||
BYTE_ARRAY_COPY_FAIL = 9010072, // 音频数据缓冲区拷贝失败
|
||||
|
||||
// 预留扩展(9010080+)
|
||||
RESERVED_01 = 9010080
|
||||
}
|
||||
|
||||
/**
|
||||
* 音频录制错误码-错误信息映射表
|
||||
* 与AudioErrorCode枚举一一对应,提供友好的用户/开发提示
|
||||
* @UniError
|
||||
*/
|
||||
export const AudioErrors : Map<AudioErrorCode, string> = new Map([
|
||||
// 权限相关
|
||||
[AudioErrorCode.MIC_PERMISSION_NONE, '麦克风权限未授予,请先在设置中开启'],
|
||||
[AudioErrorCode.MIC_PERMISSION_DENIED, '麦克风权限被永久拒绝,请前往应用设置手动开启'],
|
||||
[AudioErrorCode.MIC_PERMISSION_GET_FAIL, '获取麦克风权限状态失败,请检查应用权限配置'],
|
||||
|
||||
// 初始化相关
|
||||
[AudioErrorCode.AUDIO_RECORD_INIT_FAIL, '音频录制实例初始化失败,可能是参数不支持或系统异常'],
|
||||
[AudioErrorCode.AUDIO_RECORD_NOT_INIT, '音频录制实例未初始化,无法执行操作'],
|
||||
[AudioErrorCode.AUDIO_RECORD_CREATE_FAIL, '创建音频录制实例失败,系统音频资源不足'],
|
||||
|
||||
// 采集过程相关
|
||||
[AudioErrorCode.RECORD_READ_DATA_FAIL, '音频采集读取数据失败,建议停止后重新启动'],
|
||||
[AudioErrorCode.RECORD_INTERRUPTED, '音频采集被中断,麦克风可能被其他应用抢占'],
|
||||
[AudioErrorCode.RECORD_BUFFER_EMPTY, '音频采集缓冲区无有效数据,采集异常'],
|
||||
[AudioErrorCode.RECORD_START_FAIL, '启动音频采集失败,实例状态异常'],
|
||||
|
||||
// 线程相关
|
||||
[AudioErrorCode.RECORD_THREAD_CREATE_FAIL, '创建音频采集线程失败,系统资源不足'],
|
||||
[AudioErrorCode.RECORD_THREAD_START_FAIL, '启动音频采集线程失败,线程状态异常'],
|
||||
[AudioErrorCode.RECORD_THREAD_INTERRUPT_FAIL, '中断音频采集线程失败,线程已异常'],
|
||||
[AudioErrorCode.RECORD_THREAD_CRASH, '音频采集线程运行崩溃,具体原因请查看日志'],
|
||||
|
||||
// 资源释放相关
|
||||
[AudioErrorCode.RECORD_STOP_FAIL, '停止音频采集失败,录制实例状态异常'],
|
||||
[AudioErrorCode.AUDIO_RECORD_RELEASE_FAIL, '释放音频录制资源失败,可能存在资源泄漏'],
|
||||
[AudioErrorCode.RECORD_REPEAT_RELEASE, '音频录制资源已释放,请勿重复执行释放操作'],
|
||||
[AudioErrorCode.RECORD_RESOURCE_LEAK, '音频录制资源未正常释放,建议检查停止逻辑'],
|
||||
|
||||
// 参数配置相关
|
||||
[AudioErrorCode.SAMPLE_RATE_NOT_SUPPORT, '当前设备不支持该采样率,建议切换为16000/44100Hz'],
|
||||
[AudioErrorCode.CHANNEL_CONFIG_ILLEGAL, '声道配置非法或当前设备不支持,建议使用单声道'],
|
||||
[AudioErrorCode.AUDIO_FORMAT_ILLEGAL, '音频格式非法或当前设备不支持,建议使用PCM_16BIT'],
|
||||
[AudioErrorCode.BUFFER_SIZE_TOO_SMALL, '采集缓冲区过小,已自动调整为系统最小缓冲区'],
|
||||
[AudioErrorCode.BUFFER_SIZE_ILLEGAL, '采集缓冲区大小配置非法,已使用系统默认值'],
|
||||
|
||||
// 系统/硬件/外部冲突
|
||||
[AudioErrorCode.MIC_OCCUPIED, '麦克风被其他应用占用,请关闭后重新尝试'],
|
||||
[AudioErrorCode.AUDIO_HARDWARE_ERROR, '音频硬件故障,请检查设备麦克风是否正常'],
|
||||
[AudioErrorCode.SYSTEM_RESOURCE_INSUFFICIENT, '系统资源不足,无法创建音频录制实例'],
|
||||
[AudioErrorCode.AUDIO_SERVICE_EXCEPTION, '系统音频服务异常,请重启设备后重新尝试'],
|
||||
|
||||
// 通用业务异常
|
||||
[AudioErrorCode.RECORD_REPEAT_START, '当前正在录制中,请勿重复启动音频录制'],
|
||||
[AudioErrorCode.RECORD_NOT_RUNNING, '未在音频录制状态,无需执行停止操作'],
|
||||
[AudioErrorCode.BYTE_ARRAY_COPY_FAIL, '音频数据缓冲区拷贝失败,采集数据可能损坏'],
|
||||
|
||||
// 预留扩展
|
||||
[AudioErrorCode.RESERVED_01, '预留错误码,暂未定义']
|
||||
]);
|
||||
|
||||
|
||||
/**
|
||||
* 错误对象实现
|
||||
*/
|
||||
export class StartAudioRecordFailImpl extends UniError implements AudioErrorRes {
|
||||
|
||||
/**
|
||||
* 错误对象构造函数
|
||||
*/
|
||||
constructor(errCode : AudioErrorCode) {
|
||||
super();
|
||||
|
||||
this.errSubject = UniErrorSubject;
|
||||
|
||||
// @ts-ignore 忽略"value属性不存在"的假报错(运行时存在)
|
||||
this.errCode = errCode.value as number;
|
||||
this.errMsg = AudioErrors.get(errCode) ?? "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export function startAudioRecord(callback) {}
|
||||
export function stopAudioRecord() {}
|
||||