diff --git a/.env.development b/.env.development
index cf4545bf..f235ef78 100644
--- a/.env.development
+++ b/.env.development
@@ -5,19 +5,17 @@ ENV = 'development'
# 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'
# VITE_APP_BASE_API_Url = 'https://aitstest.jlbank.com.cn:7002'
# VITE_APP_BASE_API_Url = 'http://192.168.247.200'
# VITE_APP_BASE_API_Url = 'http://aitscdn.jlbank.com.cn:7001'
# VITE_APP_BASE_API_Url = 'http://192.168.108.129'
# 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'
+#VITE_APP_BASE_API_Url = 'http://25.18.122.91:9786'
# UAT
# VITE_APP_BASE_API_Url = 'http://25.18.122.78:9786'
diff --git a/src/pages.json b/src/pages.json
index 62ee516d..68ecbebe 100644
--- a/src/pages.json
+++ b/src/pages.json
@@ -516,6 +516,7 @@
"path": "pages/sparring/talk",
"style": {
"navigationBarTitleText": "AI陪练通话",
+ "disableSwipeBack": true,
"app-plus": {
"titleNView": false
}
diff --git a/src/pages/sparring/components/talk-microphone.vue b/src/pages/sparring/components/talk-microphone.vue
new file mode 100644
index 00000000..735be302
--- /dev/null
+++ b/src/pages/sparring/components/talk-microphone.vue
@@ -0,0 +1,358 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/pages/sparring/components/talk-speaker.vue b/src/pages/sparring/components/talk-speaker.vue
new file mode 100644
index 00000000..ed364e38
--- /dev/null
+++ b/src/pages/sparring/components/talk-speaker.vue
@@ -0,0 +1,99 @@
+
+
+
+
+
+
+
+
diff --git a/src/pages/sparring/js/StreamPlayer.js b/src/pages/sparring/js/StreamPlayer.js
index f0340d68..0e1c7bce 100644
--- a/src/pages/sparring/js/StreamPlayer.js
+++ b/src/pages/sparring/js/StreamPlayer.js
@@ -30,7 +30,6 @@ export class StreamPlayer {
}
appendChunk(audioData) {
- console.log('this.audioQueue', this.audioQueue.length);
this.audioQueue.push(audioData);
this._processQueue();
}
diff --git a/src/pages/sparring/js/useMicrophone.js b/src/pages/sparring/js/useMicrophone.js
new file mode 100644
index 00000000..af6863c2
--- /dev/null
+++ b/src/pages/sparring/js/useMicrophone.js
@@ -0,0 +1,149 @@
+import {
+ ref,
+ onUnmounted,
+ watch
+} from 'vue';
+ import {
+ ProtocolCodec,
+ MessageType,
+ SerializationType,
+ CompressionType,
+ ControlCommand,
+ ProtocolConst
+ } from './ProtocolCodec';
+/**
+ * 麦克风操作Hook(封装开始/停止录音、状态管理)
+ * @param {Object} options - 录音配置项
+ * @param {number} options.duration - 最大录音时长(秒,默认60)
+ * @param {string} options.format - 录音格式(mp3/wav/pcm,默认mp3)
+ * @param {number} options.sampleRate - 采样率(默认16000)
+ * @param {number} options.bitRate - 码率(默认16000)
+ * @param {Function} options.onData - 实时音频数据回调(仅H5/PCM格式生效)
+ * @param {Function} options.onStop - 结束回调
+ * @param {Function} options.onError - 错误回调
+ * @returns {Object} 录音控制方法+状态
+ */
+export default function useMicrophone(options = {}) {
+ console.log('options', options);
+ const send = options.send
+ const recordFrame = ref(null)
+ const renderJSModule = ref(null)
+
+ // 默认配置
+ const defaultOptions = {
+ duration: 60,
+ format: 'mp3',
+ sampleRate: 16000,
+ bitRate: 16000,
+ onData: () => {},
+ onStop: () => {},
+ onError: (err) => console.error('录音错误:', err)
+ };
+ const finalOptions = {
+ ...defaultOptions,
+ ...options
+ };
+
+ // 响应式状态
+ const isRecording = ref(false); // 是否正在录音
+ const recordTime = ref(0); // 录音时长(秒)
+ const recordResult = ref(null); // 录音结果(临时文件路径/Blob)
+ const recorderManager = ref(null); // 录音管理器实例(uniapp/小程序)
+ const audioContext = ref(null); // AudioContext(H5)
+ const mediaRecorder = ref(null); // MediaRecorder(H5)
+ const timer = ref(null); // 录音时长定时器
+
+ // 平台判断
+ const platform = uni.getSystemInfoSync().platform;
+
+ // ==================== 核心方法:开始录音 ====================
+ const startRecording = async () => {
+ // 防止重复点击
+ if (isRecording.value) return;
+
+ // 重置状态
+ recordTime.value = 0;
+ recordResult.value = null;
+ console.log('开始', recordFrame.value);
+ recordFrame.value.start({
+ sampleRate: 16000,
+ frameSize: 1024,
+ gain: 1.0,
+ onFrameRecorded: ({ isLastFrame, frameBuffer }) => {
+ // console.log('录音返回', frameBuffer);
+ const data = ProtocolCodec.pack(MessageType.AUDIO_DATA, frameBuffer)
+ // console.log('录音返回', data);
+ send(data)
+ // try {
+ // ws.value.send({
+ // data: ProtocolCodec.pack(MessageType.AUDIO_DATA, frameBuffer)
+ // });
+ // } catch (e) {}
+ },
+ onDecibels: (decibels) => {
+ // this.currentDecibels = decibels;
+ }
+ });
+ // this.isRecording = true;
+ // this.status = '录音中...';
+ // isRecording.value = true;
+
+ };
+
+ // ==================== 核心方法:停止录音 ====================
+ const stopRecord = () => {
+ // if (!isRecording.value) return;
+
+ // try {
+ // if (isH5) {
+ // mediaRecorder.value?.stop();
+ // } else if (isMiniProgram || platform === 'app-plus') {
+ // recorderManager.value?.stop();
+ // }
+ // isRecording.value = false;
+ // clearInterval(timer.value);
+ // } catch (err) {
+ // finalOptions.onError(`停止录音失败:${err.message}`);
+ // }
+ };
+
+ // ==================== 资源清理 ====================
+ const cleanup = () => {
+ // stopRecord();
+ // // H5端额外清理
+ // if (isH5) {
+ // if (mediaRecorder.value) mediaRecorder.value = null;
+ // if (audioContext.value) audioContext.value.close();
+ // // 释放Blob URL
+ // if (recordResult.value?.url) {
+ // URL.revokeObjectURL(recordResult.value.url);
+ // }
+ // }
+ // // 小程序/APP端清理
+ // if (recorderManager.value) recorderManager.value = null;
+ };
+
+ // 组件卸载时自动清理
+ // onUnmounted(() => {
+ // cleanup();
+ // });
+
+ // 监听最大时长,自动停止
+ // watch(
+ // () => recordTime.value,
+ // (val) => {
+ // if (val >= finalOptions.duration) {
+ // stopRecord();
+ // }
+ // }
+ // );
+
+ // 返回控制方法+状态
+ return {
+
+ isRecording,
+ renderJSModule,
+ startRecording,
+ recordFrame
+ };
+};
\ No newline at end of file
diff --git a/src/pages/sparring/js/useSpeaker.js b/src/pages/sparring/js/useSpeaker.js
new file mode 100644
index 00000000..568ca088
--- /dev/null
+++ b/src/pages/sparring/js/useSpeaker.js
@@ -0,0 +1,129 @@
+import {
+ ref,
+ onUnmounted,
+ watch
+} from 'vue';
+
+/**
+ * 扬声器管理Hook(音频输出)
+ * @param {Object} options - 播放配置
+ * @param {number} options.sampleRate - 播放采样率(默认16000,需与音频源一致)
+ * @param {number} options.volume - 初始音量(0~1,默认1)
+ * @param {boolean} options.autoPlay - 是否自动播放(默认false,需用户交互触发)
+ * @param {Function} options.onPlay - 播放开始回调
+ * @param {Function} options.onError - 播放错误回调
+ * @returns {Object} 播放控制方法+状态
+ */
+export const useSpeaker = (options = {}) => {
+ // 默认配置
+ const defaultOptions = {
+ sampleRate: 16000,
+ volume: 1.0,
+ autoPlay: false,
+ onPlay: () => {},
+ onError: (err) => console.error('扬声器播放错误:', err)
+ };
+ const finalOptions = {
+ ...defaultOptions,
+ ...options
+ };
+
+ // 响应式状态
+ const isPlaying = ref(false); // 是否正在播放
+ const volume = ref(finalOptions.volume); // 播放音量(0~1)
+ const audioContext = ref(null); // 音频播放上下文(H5)
+ const audioBufferSource = ref(null); // 音频缓冲源(播放PCM流)
+ const audioElement = ref(null); // 音频元素(播放文件/URL)
+ const playQueue = ref([]); // 音频数据播放队列(适配实时流)
+
+ // 平台判断
+ const isH5 = uni.getSystemInfoSync().platform === 'h5';
+
+ // ==================== 初始化播放上下文 ====================
+ const initAudioContext = () => {
+
+ };
+
+ // ==================== 核心方法:播放PCM音频流(实时) ====================
+ /**
+ * 播放PCM音频数据(适配WebSocket传输的实时流)
+ * @param {ArrayBuffer} pcmData - PCM音频数据(16位单声道)
+ */
+ const playPCM = (pcmData) => {
+
+
+
+ };
+
+ // ==================== 控制方法:暂停/停止/调节音量 ====================
+ const pausePlay = () => {
+ if (!isPlaying.value) return;
+
+ if (isH5) {
+ if (audioBufferSource.value) {
+ audioBufferSource.value.stop();
+ audioBufferSource.value = null;
+ }
+ audioElement.value?.pause();
+ } else {
+ uni.createInnerAudioContext().pause();
+ }
+ isPlaying.value = false;
+ };
+
+ const stopPlay = () => {
+ pausePlay();
+ playQueue.value = []; // 清空播放队列
+ if (audioContext.value) {
+ audioContext.value.close().then(() => {
+ audioContext.value = null;
+ });
+ }
+ audioElement.value = null;
+ };
+
+ const setVolume = (val) => {
+ if (val < 0) val = 0;
+ if (val > 1) val = 1;
+ volume.value = val;
+
+ if (isH5) {
+ audioContext.value?.destination.setVolume(val);
+ audioElement.value.volume = val;
+ } else {
+ uni.createInnerAudioContext().volume = val;
+ }
+ };
+
+ // ==================== 资源清理 ====================
+ const cleanup = () => {
+ stopPlay();
+ isPlaying.value = false;
+ volume.value = finalOptions.volume;
+ playQueue.value = [];
+ };
+
+ // 组件卸载时自动清理
+ onUnmounted(() => {
+ cleanup();
+ });
+
+ // 监听音量变化,同步更新
+ watch(volume, (newVal) => {
+ setVolume(newVal);
+ });
+
+ // 返回控制方法+状态
+ return {
+ // 响应式状态
+ isPlaying, // 是否正在播放
+ volume, // 当前音量(0~1)
+ // 核心方法
+ playPCM, // 播放实时PCM音频流(适配WebSocket)
+ playAudio, // 播放音频文件/URL
+ pausePlay, // 暂停播放
+ stopPlay, // 停止播放
+ setVolume, // 调节音量(0~1)
+ cleanup // 手动清理资源
+ };
+};
\ No newline at end of file
diff --git a/src/pages/sparring/js/useWebSocket.js b/src/pages/sparring/js/useWebSocket.js
new file mode 100644
index 00000000..c5d5d02d
--- /dev/null
+++ b/src/pages/sparring/js/useWebSocket.js
@@ -0,0 +1,118 @@
+import {
+ getToken
+} from '@/common/common'
+import {
+ get_base_url
+} from '@/api/request.js'
+import {
+ ref,
+ onUnmounted,
+ computed,
+ getCurrentInstance
+} from 'vue';
+import {
+ ProtocolCodec,
+ MessageType,
+ SerializationType,
+ CompressionType,
+ ControlCommand,
+ ProtocolConst
+} from './ProtocolCodec';
+const DEFAULT_AUTH_PARAMS = {
+ user_id: '1001', // 默认用户ID(可从缓存/全局状态取)
+ token: '', // 优先从缓存获取,避免硬编码
+ name: '测试用户',
+ device_id: uni.getSystemInfoSync().deviceId || '', // 设备ID(补充)
+ timestamp: Date.now() // 时间戳(防篡改)
+};
+// const wsUrl = 'ws://127.0.0.1:8000/ws/audio'
+const url = '/trapractice/voiceCallSocket/voiceCall'
+const _baseUrl = get_base_url(url).split('http').join('ws')
+export default function useWebSocket(aaas = null, options = {}) {
+ const isRecording = ref(false) //是否接通
+ const ws = ref(null)
+ const initConnection = (authParams = {}, onStartRecord, sssssss) => {
+ const wsUrl = _baseUrl + url + '?summary=' + getToken()
+ console.log('wsUrl', wsUrl);
+ const finalAuthParams = {
+ ...DEFAULT_AUTH_PARAMS,
+ ...authParams,
+ };
+ ws.value = uni.connectSocket({
+ url: wsUrl,
+ fail: () => {
+ console.log('fail');
+ },
+ success: () => {
+ console.log('web');
+ },
+ fail: () => {
+ console.log('fail');
+ }
+ });
+ ws.value.onOpen((res) => {
+ ws.value.send({
+ data: ProtocolCodec.pack(MessageType.IDENTITY, finalAuthParams)
+ });
+ });
+ ws.value.onMessage((res) => {
+ const {
+ msgType,
+ body
+ } = ProtocolCodec.unpack(res.data);
+ switch (msgType) {
+ case MessageType.IDENTITY:
+ console.log('身份校验', body);
+ onStartRecord(body)
+ break;
+ case MessageType.AUDIO_DATA:
+ const buffer = body;
+ if (buffer.byteLength < 2) break;
+ const uint8 = new Uint8Array(buffer);
+ const numArray = Array.from(uint8);
+ // currentPCMChunk.value = numArray;
+ sssssss(numArray)
+ // console.log('音频消息', body);
+ break;
+ case MessageType.ERROR:
+
+ // this.handleErrorMessage(body);
+ break;
+ default:
+ console.log('其他消息', body);
+ break;
+ }
+ });
+ }
+ const send = (data) => {
+ ws.value.send({
+ data
+ });
+ }
+ const close = () => {
+ ws.value.close({
+ code: 1000,
+ reason: '用户挂断',
+ success: () => {
+ console.log('挂断success');
+ },
+ fail: () => {
+ console.log('挂断fail');
+ },
+ complete: () => {
+ console.log('挂断complete');
+ }
+
+ });
+ }
+
+
+
+ return {
+ ws,
+ close,
+ initConnection,
+ send,
+ isRecording
+ };
+}
\ No newline at end of file
diff --git a/src/pages/sparring/talk.vue b/src/pages/sparring/talk.vue
index 7947f860..fcbeecb2 100644
--- a/src/pages/sparring/talk.vue
+++ b/src/pages/sparring/talk.vue
@@ -1,8 +1,8 @@
-
+
场景提示
@@ -10,29 +10,35 @@
- {{ userInfo.userName }}
- {{ timeShow }}
+ {{ dataInfo.userName }}
+ {{ formattedTime }}
+ 正在接通中
- {{ sceneDesc }}
+ {{ dataInfo.sceneDesc }}
-
-
+
+
-
-
diff --git a/src/pages/sparring/tip.vue b/src/pages/sparring/tip.vue
index 38bd37a1..3ccb5315 100644
--- a/src/pages/sparring/tip.vue
+++ b/src/pages/sparring/tip.vue
@@ -234,6 +234,9 @@
// #endif
}
const toTalking = () => {
+ common.setPageCache({
+ sceneDesc:detailInfo.value.sceneDesc
+ })
common.navigateTo('/pages/sparring/talk?traId=' + traId.value + '&chaId=' + chaId.value + '&type=' + talkingType
.value + '&isPreview=' + (isPreview.value ? '1' : '0'))
}
diff --git a/src/pagesB.json b/src/pagesB.json
index 2b2f0445..5fa2084c 100644
--- a/src/pagesB.json
+++ b/src/pagesB.json
@@ -95,6 +95,7 @@
"path": "pages/sparring/talk",
"style": {
"navigationBarTitleText": "AI陪练通话",
+ "disableSwipeBack": true,
"app-plus": {
"titleNView": false
}
diff --git a/src/uni_modules/yao-RecordFrame/components/yao-RecordFrame/yao-RecordFrame.vue b/src/uni_modules/yao-RecordFrame/components/yao-RecordFrame/yao-RecordFrame.vue
index 735be302..fb373ea7 100644
--- a/src/uni_modules/yao-RecordFrame/components/yao-RecordFrame/yao-RecordFrame.vue
+++ b/src/uni_modules/yao-RecordFrame/components/yao-RecordFrame/yao-RecordFrame.vue
@@ -2,6 +2,7 @@
+