IOS开发
This commit is contained in:
+1
-1
@@ -5,7 +5,7 @@ ENV = 'development'
|
|||||||
#VITE_APP_BASE_API_Url = 'https://aits.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:7002'
|
VITE_APP_BASE_API_Url = 'https://aitstest.jlbank.com.cn:7001'
|
||||||
|
|
||||||
# DEV
|
# DEV
|
||||||
#VITE_APP_BASE_API_Url = 'http://25.18.122.65:7001'
|
#VITE_APP_BASE_API_Url = 'http://25.18.122.65:7001'
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"bundleId" : "cn.com.jlbank.aits",
|
"bundleId" : "cn.com.jlbank.aits",
|
||||||
"certificateFile" : "/Users/jlbank/Desktop/tra-app/certificate/ios/开发证书.p12",
|
"certificateFile" : "/Users/jlbank/Desktop/lx_uniapp/tra-app/certificate/ios/开发证书.p12",
|
||||||
"certificateProfileFile" : "/Users/jlbank/Desktop/tra-app/certificate/ios/jiaixuedev.mobileprovision",
|
"certificateProfileFile" : "/Users/jlbank/Desktop/lx_uniapp/tra-app/certificate/ios/jiaixuedev.mobileprovision",
|
||||||
"playground" : "standard",
|
"playground" : "standard",
|
||||||
"runSignStatus" : 1,
|
"runSignStatus" : 1,
|
||||||
"type" : "uni-app:app-ios"
|
"type" : "uni-app:app-ios"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ErrorCode, ShowErrorTipOptions } from './touch-btn.types';
|
import { ErrorMessageEnum, ShowErrorTipOptions } from './touch-btn.types';
|
||||||
import { get_base_url } from '@/api/request.js';
|
import { get_base_url } from '@/api/request.js';
|
||||||
import { getPhoneEnvBool } from '@/common/common.js';
|
import { getPhoneEnvBool } from '@/common/common.js';
|
||||||
const IsAndEnv = getPhoneEnvBool('AND')
|
const IsAndEnv = getPhoneEnvBool('AND')
|
||||||
@@ -69,7 +69,7 @@ export const initWebSocket = (wsUrl : string) : Promise<UniApp.SocketTask> => {
|
|||||||
* @returns 固定返回false(保持原有返回值类型)
|
* @returns 固定返回false(保持原有返回值类型)
|
||||||
*/
|
*/
|
||||||
export function showErrorTip(
|
export function showErrorTip(
|
||||||
errorCode : ErrorCode,
|
errorMsg : ErrorMessageEnum | string,
|
||||||
options : ShowErrorTipOptions = {}
|
options : ShowErrorTipOptions = {}
|
||||||
) : boolean {
|
) : boolean {
|
||||||
// 解构可选参数,设置默认延迟时间为200ms
|
// 解构可选参数,设置默认延迟时间为200ms
|
||||||
@@ -78,7 +78,7 @@ export function showErrorTip(
|
|||||||
// 延迟显示对应的错误提示
|
// 延迟显示对应的错误提示
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: errorCode, // 直接使用枚举值作为提示文本
|
title: errorMsg, // 直接使用枚举值作为提示文本
|
||||||
icon: 'none'
|
icon: 'none'
|
||||||
});
|
});
|
||||||
}, delayTime); // 使用传入的延迟时间(或默认200ms)
|
}, delayTime); // 使用传入的延迟时间(或默认200ms)
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export type VoiceToTextSubStatus =
|
|||||||
export type WorkModeType = 'both' | 'text-only' | 'voice-only' ;
|
export type WorkModeType = 'both' | 'text-only' | 'voice-only' ;
|
||||||
|
|
||||||
/** 错误码枚举 - 直接映射提示文本 */
|
/** 错误码枚举 - 直接映射提示文本 */
|
||||||
export enum ErrorCode {
|
export enum ErrorMessageEnum {
|
||||||
// 网络错误提示
|
// 网络错误提示
|
||||||
NetworkError = '网络连接失败,请稍后再试!',
|
NetworkError = '网络连接失败,请稍后再试!',
|
||||||
// 录音时长过短提示
|
// 录音时长过短提示
|
||||||
|
|||||||
@@ -116,12 +116,13 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import * as TouchBtnImages from './touch-btn.images'; // 导出图片
|
import * as TouchBtnImages from './touch-btn.images'; // 导出图片
|
||||||
import { StatusType, VoiceToTextSubStatus, ErrorCode, WorkModeType } from './touch-btn.types';
|
import { StatusType, VoiceToTextSubStatus, ErrorMessageEnum, WorkModeType } from './touch-btn.types';
|
||||||
import type { TouchBtnEmits } from './touch-btn.types';
|
import type { TouchBtnEmits } from './touch-btn.types';
|
||||||
import { wsUrl, bottomAreaTopBoundary, horizontalSplitX, initWebSocket, showErrorTip, vibrateLight } from './touch-btn';
|
import { wsUrl, bottomAreaTopBoundary, horizontalSplitX, initWebSocket, showErrorTip, vibrateLight } from './touch-btn';
|
||||||
import { ref, computed, onMounted, nextTick, watch, reactive } from 'vue';
|
import { ref, computed, onMounted, nextTick, watch, reactive } from 'vue';
|
||||||
import { onHide, onLaunch } from '@dcloudio/uni-app';
|
import { onHide, onLaunch } from '@dcloudio/uni-app';
|
||||||
import common, { setupKeyboardHeightListener, getToken } from '@/common/common';
|
import common, { setupKeyboardHeightListener, getToken } from '@/common/common';
|
||||||
|
import { goto_login_fun } from '@/api/request.js';
|
||||||
import { startAudioRecord, stopAudioRecord, preRequestRecordPermission } from '@/uni_modules/ty-recording';
|
import { startAudioRecord, stopAudioRecord, preRequestRecordPermission } from '@/uni_modules/ty-recording';
|
||||||
import { ProtocolCodec, MessageType, ControlCommand, ControlCode, ControlBody } from '@/common/protocolCodec';
|
import { ProtocolCodec, MessageType, ControlCommand, ControlCode, ControlBody } from '@/common/protocolCodec';
|
||||||
import permissionApi from '@/common/permission';
|
import permissionApi from '@/common/permission';
|
||||||
@@ -212,14 +213,11 @@
|
|||||||
|
|
||||||
const classes = [];
|
const classes = [];
|
||||||
isOutStatus.value === '02' && classes.push('is-out');
|
isOutStatus.value === '02' && classes.push('is-out');
|
||||||
(isOutStatus.value === '03' || status.value === 'voice_to_text' || status.value === 'send_over') &&
|
(isOutStatus.value === '03' || status.value === 'voice_to_text' || status.value === 'send_over') && classes.push('is-translate');
|
||||||
classes.push('is-translate');
|
|
||||||
// 仅语音模式,01松手后要和03操作一样
|
// 仅语音模式,01松手后要和03操作一样
|
||||||
// isOutStatus.value === '01' && workMode.value === 'text-only' && status.value == 'voice_to_text' && classes.push('is-translate');
|
// 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);
|
voice_to_text_status.value !== '' && classes.push(voice_to_text_status.value);
|
||||||
|
|
||||||
return classes;
|
return classes;
|
||||||
// return ['is-translate','success' ];
|
|
||||||
});
|
});
|
||||||
const recordId = ref('');
|
const recordId = ref('');
|
||||||
const allText = ref('');
|
const allText = ref('');
|
||||||
@@ -271,11 +269,8 @@
|
|||||||
if (props.canAnswer) return common.msg(props.canAnswerText);
|
if (props.canAnswer) return common.msg(props.canAnswerText);
|
||||||
try {
|
try {
|
||||||
const state = await permissionApi.judgeIosPermission('record');
|
const state = await permissionApi.judgeIosPermission('record');
|
||||||
if (state === 'not determined') {
|
console.log('state', state);
|
||||||
// 苹果专用
|
if (state === 'not determined') return preRequestRecordPermission();
|
||||||
preRequestRecordPermission();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('麦克风权限申请失败:', e);
|
console.error('麦克风权限申请失败:', e);
|
||||||
return;
|
return;
|
||||||
@@ -302,8 +297,7 @@
|
|||||||
touchPos.y = changedTouche.clientY;
|
touchPos.y = changedTouche.clientY;
|
||||||
// 记录开始时间戳(毫秒)
|
// 记录开始时间戳(毫秒)
|
||||||
recordStartTime.value = Date.now();
|
recordStartTime.value = Date.now();
|
||||||
// 弹出录音界面
|
popupRef.value.open(); // 弹出录音界面
|
||||||
startRecordingTimer(true); // 启动超长定时器
|
|
||||||
startAudioRecord({
|
startAudioRecord({
|
||||||
onFrame: (pcmData: any) => {
|
onFrame: (pcmData: any) => {
|
||||||
if (wsStatus.value && ws.value) {
|
if (wsStatus.value && ws.value) {
|
||||||
@@ -316,6 +310,7 @@
|
|||||||
// 发送完成后清空队列
|
// 发送完成后清空队列
|
||||||
voiceQueue = [];
|
voiceQueue = [];
|
||||||
}
|
}
|
||||||
|
console.log('再发送当前帧的音频数据', pcmData.length);
|
||||||
// 再发送当前帧的音频数据
|
// 再发送当前帧的音频数据
|
||||||
ws.value.send({
|
ws.value.send({
|
||||||
data: ProtocolCodec.pack(MessageType.AUDIO_DATA, pcmData)
|
data: ProtocolCodec.pack(MessageType.AUDIO_DATA, pcmData)
|
||||||
@@ -327,85 +322,98 @@
|
|||||||
onStart: (res: any) => {
|
onStart: (res: any) => {
|
||||||
console.log('启动', res);
|
console.log('启动', res);
|
||||||
recordId.value = res.recordId ?? '';
|
recordId.value = res.recordId ?? '';
|
||||||
popupRef.value.open();
|
console.log('WS连接成功', isRecording.value);
|
||||||
console.log('开启语音');
|
|
||||||
setTimeout(() => {
|
initWebSocket(wsUrl + '?summary=' + getToken())
|
||||||
initWebSocket(wsUrl + '?summary=' + getToken())
|
.then((socketTask) => {
|
||||||
.then((socketTask) => {
|
ws.value = socketTask;
|
||||||
if (!isRecording.value) {
|
|
||||||
ws.value = null;
|
ws.value.onMessage((res: { data: Uint8Array | ArrayBuffer }) => {
|
||||||
wsStatus.value = false;
|
try {
|
||||||
return;
|
const { msgType, body } = ProtocolCodec.unpack(res.data);
|
||||||
}
|
if (msgType === MessageType.TEXT_MESSAGE) {
|
||||||
ws.value = socketTask;
|
tempText.value = '';
|
||||||
wsStatus.value = true;
|
allText.value = allText.value + body;
|
||||||
console.log('WS连接成功', isRecording.value);
|
} else if (msgType === MessageType.IDENTITY) {
|
||||||
ws.value.onMessage((res: { data: Uint8Array | ArrayBuffer }) => {
|
console.log("错误消息", body);
|
||||||
try {
|
const _errorBody = body as {code:number,msg:string };
|
||||||
const { msgType, body } = ProtocolCodec.unpack(res.data);
|
if(_errorBody?.code === 2) { // 认证错误,实际是登录失效
|
||||||
console.log('msgType', msgType, body);
|
closeWS();
|
||||||
if (msgType === MessageType.TEXT_MESSAGE) {
|
goto_login_fun()
|
||||||
tempText.value = '';
|
closePopup();
|
||||||
allText.value = allText.value + body;
|
return;
|
||||||
} else if (msgType === MessageType.TIP_MESSAGE) {
|
} else if(_errorBody?.code === 1) {
|
||||||
tempText.value = body as string;
|
if (!isRecording.value) {
|
||||||
} else if (msgType === MessageType.CONTROL_CMD) {
|
ws.value = null;
|
||||||
// 返回结束消息
|
wsStatus.value = false;
|
||||||
const controlBody = body as ControlBody;
|
return;
|
||||||
if (controlBody.type === ControlCode.SERVER_TO_FRONT_OVER) {
|
}
|
||||||
console.log(
|
console.log('开启语音');
|
||||||
'识别结束',
|
wsStatus.value = true;
|
||||||
controlBody,
|
startRecordingTimer(true); // 启动超长定时器
|
||||||
voice_to_text_status.value,
|
} else if(_errorBody?.code === 3) { // 路数不够等问题
|
||||||
status.value,
|
closeWS();
|
||||||
voice_to_text_status.value
|
closePopup();
|
||||||
);
|
showErrorTip(_errorBody?.msg ?? '请稍后再试!');
|
||||||
// 如果不等于init说明已经超时,被超时函数处理完了,这里即使返回也不做处理
|
}
|
||||||
if (
|
} else if (msgType === MessageType.TIP_MESSAGE) {
|
||||||
status.value === 'voice_to_text' &&
|
tempText.value = body as string;
|
||||||
voice_to_text_status.value !== 'init'
|
} else if (msgType === MessageType.CONTROL_CMD) {
|
||||||
)
|
// 返回结束消息
|
||||||
return;
|
const controlBody = body as ControlBody;
|
||||||
if (status.value === 'send_over') return;
|
if (controlBody.type === ControlCode.SERVER_TO_FRONT_OVER) {
|
||||||
// 时间太短,关掉的时候就处理了,这里不处理
|
console.log(
|
||||||
const recordDurationMs = Date.now() - recordStartTime.value;
|
'识别结束',
|
||||||
if (recordDurationMs <= props.minRecordDuration) return;
|
controlBody,
|
||||||
closeWS();
|
voice_to_text_status.value,
|
||||||
status.value = 'send_over'; // 设置成发送完成
|
status.value,
|
||||||
if (voiceTimer) clearTimeout(voiceTimer);
|
voice_to_text_status.value
|
||||||
// 设置识别结果
|
);
|
||||||
voice_to_text_status.value = controlBody?.txt === '' ? 'failed' : 'success';
|
// 如果不等于init说明已经超时,被超时函数处理完了,这里即使返回也不做处理
|
||||||
// 取值出来
|
if (
|
||||||
ossKey.value = controlBody?.ossKey || '';
|
status.value === 'voice_to_text' &&
|
||||||
if (typeof controlBody?.txt === 'string') {
|
voice_to_text_status.value !== 'init'
|
||||||
allText.value = controlBody.txt.trim();
|
)
|
||||||
tempText.value = ''
|
return;
|
||||||
}
|
if (status.value === 'send_over') return;
|
||||||
const _isOutStatus =
|
// 时间太短,关掉的时候就处理了,这里不处理
|
||||||
workMode.value === 'text-only' && isOutStatus.value === '01'
|
const recordDurationMs = Date.now() - recordStartTime.value;
|
||||||
? '03'
|
if (recordDurationMs <= props.minRecordDuration) return;
|
||||||
: isOutStatus.value;
|
closeWS();
|
||||||
// 只有模式为01的时候才发送出去
|
status.value = 'send_over'; // 设置成发送完成
|
||||||
console.log('只有模式为01的时候才发送出去', controlBody);
|
if (voiceTimer) clearTimeout(voiceTimer);
|
||||||
if (_isOutStatus === '01') {
|
// 设置识别结果
|
||||||
emit('submitVoice', {
|
voice_to_text_status.value = controlBody?.txt === '' ? 'failed' : 'success';
|
||||||
ossKey: ossKey.value,
|
// 取值出来
|
||||||
text: allText.value,
|
ossKey.value = controlBody?.ossKey || '';
|
||||||
recordId: recordId.value
|
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 (e) {
|
||||||
})
|
console.log('后端发来的信息有问题', e, res.data);
|
||||||
.catch(() => {
|
}
|
||||||
stopRecord('networkError');
|
|
||||||
console.log('网络出现问题');
|
|
||||||
});
|
});
|
||||||
}, 200);
|
})
|
||||||
|
.catch(() => {
|
||||||
|
stopRecord('networkError');
|
||||||
|
console.log('网络出现问题');
|
||||||
|
});
|
||||||
},
|
},
|
||||||
onStop: () => {
|
onStop: () => {
|
||||||
console.log('关闭');
|
console.log('关闭');
|
||||||
@@ -422,6 +430,7 @@
|
|||||||
|
|
||||||
// 结束录音
|
// 结束录音
|
||||||
const closePopup = () => {
|
const closePopup = () => {
|
||||||
|
if (voiceTimer) clearTimeout(voiceTimer);
|
||||||
isRecording.value = false;
|
isRecording.value = false;
|
||||||
popupRef.value.close();
|
popupRef.value.close();
|
||||||
};
|
};
|
||||||
@@ -518,7 +527,7 @@
|
|||||||
} else if (recordDurationMs <= props.minRecordDuration) {
|
} else if (recordDurationMs <= props.minRecordDuration) {
|
||||||
console.log('进入这里了', status.value);
|
console.log('进入这里了', status.value);
|
||||||
if (_isOutStatus !== '02') {
|
if (_isOutStatus !== '02') {
|
||||||
showErrorTip(ErrorCode.ShortRecord);
|
showErrorTip(ErrorMessageEnum.ShortRecord);
|
||||||
}
|
}
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
status.value = 'send_over'; // 设置成发送完成
|
status.value = 'send_over'; // 设置成发送完成
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"deploymentTarget": "35"
|
"deploymentTarget": "13"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,9 +11,6 @@ let isRecording = false;
|
|||||||
let audioEngine: AVAudioEngine | null = null;
|
let audioEngine: AVAudioEngine | null = null;
|
||||||
//音频输入节点(全局变量,避免被释放)
|
//音频输入节点(全局变量,避免被释放)
|
||||||
let inputNode: AVAudioInputNode | null = null;
|
let inputNode: AVAudioInputNode | null = null;
|
||||||
// 新增:缓存录制的所有Int16 PCM数据
|
|
||||||
let recordedPCMData: UInt8[] = [];
|
|
||||||
// 3. 保存完整PCM数据到文件
|
|
||||||
const targetSampleRate:Double = 16000.0; // 和采集时的目标格式一致
|
const targetSampleRate:Double = 16000.0; // 和采集时的目标格式一致
|
||||||
const channels = 1; // 实际以采集时的声道数为准(可从inputFormat获取)
|
const channels = 1; // 实际以采集时的声道数为准(可从inputFormat获取)
|
||||||
// 新增:录音启动时间戳(毫秒)
|
// 新增:录音启动时间戳(毫秒)
|
||||||
@@ -28,11 +25,7 @@ export function preRequestRecordPermission(callback?: (granted: boolean) => void
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. 配置基础音频会话(仅为权限申请,用record类别)
|
// 1. 配置基础音频会话(仅为权限申请,用record类别)
|
||||||
UTSiOS.try(audioSession.setCategory(
|
UTSiOS.try(audioSession.setCategory(AVAudioSession.Category.record));
|
||||||
AVAudioSession.Category.record,
|
|
||||||
mode=AVAudioSession.Mode.default
|
|
||||||
));
|
|
||||||
|
|
||||||
// 2. 激活会话(权限申请建议激活,避免部分设备弹窗不触发)
|
// 2. 激活会话(权限申请建议激活,避免部分设备弹窗不触发)
|
||||||
UTSiOS.try(audioSession.setActive(true));
|
UTSiOS.try(audioSession.setActive(true));
|
||||||
|
|
||||||
@@ -40,12 +33,8 @@ export function preRequestRecordPermission(callback?: (granted: boolean) => void
|
|||||||
audioSession.requestRecordPermission((granted: boolean) => {
|
audioSession.requestRecordPermission((granted: boolean) => {
|
||||||
console.log("预申请麦克风权限结果:", granted);
|
console.log("预申请麦克风权限结果:", granted);
|
||||||
|
|
||||||
// 4. 权限申请完成后,立即停用会话(核心:释放资源)
|
|
||||||
try {
|
try {
|
||||||
UTSiOS.try(audioSession.setActive(
|
UTSiOS.try(audioSession.setActive(false));
|
||||||
false,
|
|
||||||
options= AVAudioSession.SetActiveOptions.notifyOthersOnDeactivation
|
|
||||||
));
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("预申请后停用音频会话失败:", e);
|
console.warn("预申请后停用音频会话失败:", e);
|
||||||
}
|
}
|
||||||
@@ -72,11 +61,7 @@ export function startAudioRecord(options: StartAudioRecordOptions){
|
|||||||
// 获取 iOS 系统的音频会话单例对象。
|
// 获取 iOS 系统的音频会话单例对象。
|
||||||
const audioSession = AVAudioSession.sharedInstance();
|
const audioSession = AVAudioSession.sharedInstance();
|
||||||
// 设置音频会话的类别、模式和附加选项,定义 App 的音频行为。
|
// 设置音频会话的类别、模式和附加选项,定义 App 的音频行为。
|
||||||
UTSiOS.try(audioSession.setCategory(
|
UTSiOS.try(audioSession.setCategory(AVAudioSession.Category.record));
|
||||||
AVAudioSession.Category.playAndRecord,
|
|
||||||
mode=AVAudioSession.Mode.spokenAudio,
|
|
||||||
options= AVAudioSession.CategoryOptions.duckOthers // 可选:其他音频静音
|
|
||||||
));
|
|
||||||
UTSiOS.try(audioSession.setActive(true));
|
UTSiOS.try(audioSession.setActive(true));
|
||||||
isRecording = true;
|
isRecording = true;
|
||||||
audioSession.requestRecordPermission((granted: boolean) => {
|
audioSession.requestRecordPermission((granted: boolean) => {
|
||||||
@@ -84,7 +69,7 @@ export function startAudioRecord(options: StartAudioRecordOptions){
|
|||||||
try {
|
try {
|
||||||
UTSiOS.try(audioSession.setActive(false, options= AVAudioSession.SetActiveOptions.notifyOthersOnDeactivation));
|
UTSiOS.try(audioSession.setActive(false, options= AVAudioSession.SetActiveOptions.notifyOthersOnDeactivation));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("停用音频会话失败:", e);
|
|
||||||
}
|
}
|
||||||
// 2. 重置状态
|
// 2. 重置状态
|
||||||
isRecording = false;
|
isRecording = false;
|
||||||
|
|||||||
Reference in New Issue
Block a user