IOS开发

This commit is contained in:
2026-03-18 17:11:08 +08:00
parent bbc8168086
commit 7c39810338
7 changed files with 109 additions and 115 deletions
+1 -1
View File
@@ -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'
+2 -2
View File
@@ -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"
+3 -3
View File
@@ -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)
+1 -1
View File
@@ -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 = '网络连接失败,请稍后再试!',
// 录音时长过短提示 // 录音时长过短提示
+97 -88
View File
@@ -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;