diff --git a/.env.development b/.env.development index 6e7704c3..1af6108b 100644 --- a/.env.development +++ b/.env.development @@ -4,10 +4,10 @@ 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: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' diff --git a/package.json b/package.json index 217b2a1a..040ad8a2 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "@dcloudio/vite-plugin-uni": "3.0.0-4060620250520001", "@vue/runtime-core": "^3.4.21", "sass": "1.77.0", + "typescript": "^5.9.3", "vite": "5.2.8" } } diff --git a/src/common/ProtocolCodec.ts b/src/common/ProtocolCodec.ts index 2129c88e..dd6e501d 100644 --- a/src/common/ProtocolCodec.ts +++ b/src/common/ProtocolCodec.ts @@ -47,8 +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, // 后端已经返回本次全部内容 + FRONT_TO_SERVER_OVER = 0b0111, // 语音按钮已经抬起 + SERVER_TO_FRONT_OVER = 0b1000, // 后端已经返回本次全部内容 } @@ -97,6 +97,11 @@ export enum ControlCommand { FINISH_PLAY = 0b0010, // 一句话播放结束 } +export interface ControlBody { + type: ControlCode; // type 是必选属性,关联你的 ControlCode 枚举 + [key: string]: any; // 索引签名:兼容后端返回的任意动态属性(txt/ossKey 等) +} + /** * 解包返回结果接口 */ @@ -108,7 +113,7 @@ export interface UnpackedResult { compression : CompressionType; compressionName : keyof typeof CompressionType; sequence : number; // 消息顺序号(0~65535,默认0) - body : Uint8Array | string | object | unknown[] | null; // PING 消息可能返回 null + body : Uint8Array | string | object | unknown[] | null | ControlBody; // PING 消息可能返回 null } /** diff --git a/src/common/common.js b/src/common/common.js index 45127627..73107d3d 100644 --- a/src/common/common.js +++ b/src/common/common.js @@ -83,12 +83,10 @@ const hideLoading = () => uni.hideLoading() // 根据姓名的长度生成对应的带星号格式 export const formatName = (name) => { - // 处理空值或非字符串情况 if (!name || typeof name !== 'string') { return ''; } - const familyName = name.charAt(0); // 取姓氏(第一个字符) const givenNameLength = name.length - 1; // 名字部分的长度 diff --git a/src/components/touch-btn/assets/close-active.png b/src/components/touch-btn/assets/close-active.png new file mode 100644 index 00000000..f139f096 Binary files /dev/null and b/src/components/touch-btn/assets/close-active.png differ diff --git a/src/components/touch-btn/assets/close.png b/src/components/touch-btn/assets/close.png new file mode 100644 index 00000000..080473f6 Binary files /dev/null and b/src/components/touch-btn/assets/close.png differ diff --git a/src/components/touch-btn/style/index.scss b/src/components/touch-btn/style/index.scss index db0667be..f2d405dc 100644 --- a/src/components/touch-btn/style/index.scss +++ b/src/components/touch-btn/style/index.scss @@ -1,4 +1,4 @@ -.talk-btns { +.talk-btns-components { position: flex; left: 0; bottom: 0; diff --git a/src/components/touch-btn/style/popup.scss b/src/components/touch-btn/style/popup.scss index 2a47c82c..e21164c4 100644 --- a/src/components/touch-btn/style/popup.scss +++ b/src/components/touch-btn/style/popup.scss @@ -3,66 +3,74 @@ flex-direction: column; width: 100vw; height: 100vh; - background-color: rgba(0, 0, 0, 0.8); + background-color: rgba(0, 0, 0, 0.85); position: relative; } /* 1. 定义通用的向右移出动画(核心:添加时长,解决秒没) */ @keyframes moveRightOut { - 0% { - right: 0; /* 初始位置 */ - } - 100% { - right: -400rpx; /* 目标位置:向右移出400rpx */ - } + 0% { + right: 0; /* 初始位置 */ + } + 100% { + right: -400rpx; /* 目标位置:向右移出400rpx */ + } } /* 2. 定义通用的向左移出动画(适配 btn-box-1) */ @keyframes moveLeftOut { - 0% { - left: 0; /* 初始位置 */ - } - 100% { - left: -400rpx; /* 目标位置:向左移出400rpx */ - } + 0% { + left: 0; /* 初始位置 */ + } + 100% { + left: -400rpx; /* 目标位置:向左移出400rpx */ + } } /* 2. 定义通用的向左移出动画(适配 btn-box-1) */ @keyframes moveBottomOut { - 0% { - transform: translateY(0%); - } - 100% { - transform: translateY(100%); - } + 0% { + transform: translateY(0%); + } + 100% { + transform: translateY(130%); + } } @keyframes fadeIn { - 0% { - opacity: 0; - } - 100% { - opacity: 1; - } + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} +.page-box { + position: relative; + height: 500rpx; + transition: margin-bottom 0.2s ease-out; } .page-1 { position: absolute; width: 100%; bottom: 0; - &.voice_to_text{ + &.voice_to_text { .btn-box-1 { - animation: moveLeftOut 0.3s ease-out forwards; - /* 清除冲突样式 */ - transform: none !important; - transition: none !important; - } - - .btn-box-2 { - animation: moveRightOut 0.3s ease-out forwards; - /* 清除冲突样式 */ - transform: none !important; - transition: none !important; - } - .circle-bg-box{ - animation: moveBottomOut 0.3s ease-out forwards; - } + animation: moveLeftOut 0.3s ease-out forwards; + } + .btn-box-2 { + animation: moveRightOut 0.3s ease-out forwards; + } + .circle-bg-box { + animation: moveBottomOut 0.3s ease-out forwards; + } + } + // 仅语音 + &.voice-only { + .btn-box-1, + .btn-box-2 { + display: none; + } + .btn-box-3 { + display: block !important; + } } // 两侧按钮 .btns-box { @@ -73,7 +81,6 @@ z-index: 1200; justify-content: space-around; position: relative; - .btn-box { position: absolute; height: 164rpx; @@ -90,6 +97,13 @@ transition: right 0.3s ease-out; right: -40rpx; } + .btn-box-3 { + position: absolute; + height: 124rpx; + transform: scale(1.2); + bottom: 30rpx; + display: none; + } } // 底部半圆 .circle-bg-box { @@ -110,40 +124,52 @@ } } } -.page-2{ +.page-2 { width: 100%; overflow: hidden; + padding-bottom: 50rpx; font-size: 28rpx; text-align: center; display: none; - z-index: 12000; + z-index: 1200; justify-content: space-around; - margin-bottom:234rpx;; + margin-bottom: 214rpx; position: absolute; bottom: 0; opacity: 0; visibility: hidden; // 初始不可见且不占交互位置 &.voice_to_text { - display: flex !important; - visibility: visible; // 激活后可见 - animation: fadeIn 0.4s ease-out 0.1s forwards; + display: flex !important; + visibility: visible; // 激活后可见 + animation: fadeIn 0.4s ease-out 0.1s forwards; } .transVoiceIng { width: 154rpx; height: 154rpx; - overflow: hidden; + overflow: visible; position: relative; - z-index: 12010; - + z-index: 1200; &.cancel-btn { background: url(@/static/images/course/cancel-btn.png) no-repeat; background-size: cover; } - + &.trans-btn { background: url(@/static/images/course/trans-btn.png) no-repeat; background-size: cover; } + .btn-text { + position: absolute; + top: calc(100% + 15rpx); /* 按钮底部+10rpx间距,避免贴边 */ + left: 50%; + transform: translateX(-50%); + /* 2. 强制显示 + 最高层级,防止被遮挡 */ + display: block !important; + z-index: 10000; + color: rgb(163, 163, 163); + font-size: 28rpx; + white-space: nowrap; + } } .send-btn { position: relative; @@ -154,12 +180,36 @@ background-color: #d8d8d8; border-radius: 77rpx; font-size: 33rpx; - z-index: 12010; + z-index: 1200; + display: flex; + align-items: center; + justify-content: center; + .send-btn-loading, + .send-btn-text { + display: none; + } + } + &.init > .send-btn > .send-btn-loading { + display: block !important; + pointer-events: none; + cursor: not-allowed; + } + &.success > .send-btn > .send-btn-text { + display: block !important; + } + &.failed .send-btn { + pointer-events: none; + cursor: not-allowed; + background-color: rgb(80, 80, 80); + color: rgb(106, 106, 106); + .send-btn-text { + display: block !important; + } } } -.talk-dialog-box{ - position: absolute; - bottom: 520rpx ; +.talk-dialog-box { + position: relative; + // bottom: 560rpx; width: 100%; display: flex; justify-content: center; @@ -173,7 +223,7 @@ // 滑动到离开变红 &.is-out { background-color: #ff3e49; - + &::after { background-color: #ff3e49; } @@ -199,9 +249,11 @@ transition: left 0.3s ease-out; // 时长0.3秒,ease-out缓动(结束时变慢,更自然) } // 控制下边箭头指向发送 - &.init::after { + &.init::after, + &.success::after { left: 78% !important; } + &.failed { width: 506rpx; height: 114rpx; @@ -211,11 +263,15 @@ background-color: #ff3e49; } // 激活错误提示框 - .error-tip-div{ + .error-tip-div { display: flex !important; visibility: visible; // 激活后可见 // animation: fadeIn 0.4s ease-out 0.1s forwards; } + .textarea { + // display: none; + visibility: hidden; // 初始不可见且不占交互位置 + } } } .talk-dialog { @@ -239,29 +295,22 @@ top: 50%; transform: translate(-50%, -50%); } - .error-tip-div{ + .error-tip-div { top: 50%; left: 50%; transform: translate(-50%, -50%); position: absolute; display: none; visibility: hidden; // 初始不可见且不占交互位置 - display: flex; align-items: center; color: #ffffff; font-size: 24rpx; font-weight: 400; - .img{ + .img { margin-right: 10rpx; width: 26rpx; height: 26rpx; } } - - - - - - } diff --git a/src/components/touch-btn/touch-btn.images.ts b/src/components/touch-btn/touch-btn.images.ts index 83c8e11e..c6f901a8 100644 --- a/src/components/touch-btn/touch-btn.images.ts +++ b/src/components/touch-btn/touch-btn.images.ts @@ -4,7 +4,11 @@ export { default as cancelBtn } from './assets/cancel.png'; export { default as cancelActiveBtn } from './assets/cancel-active.png'; export { default as translateBtn } from './assets/translate.png'; export { default as translateActiveBtn } from './assets/translate-active.png'; +export { default as closeBtn } from './assets/close.png'; +export { default as closeActiveBtn } from './assets/close-active.png'; + export { default as voiceWhiteBtn } from './assets/voice-white.gif'; + export const icKeyboardSvg = () : string => { const svgXml = ` diff --git a/src/components/touch-btn/touch-btn.ts b/src/components/touch-btn/touch-btn.ts index 94387d3c..2bda168d 100644 --- a/src/components/touch-btn/touch-btn.ts +++ b/src/components/touch-btn/touch-btn.ts @@ -1,12 +1,15 @@ import { ErrorCode, ShowErrorTipOptions } from './touch-btn.types'; import { get_base_url } from '@/api/request.js'; -import { getToken } from '@/common/common.js'; +import { getToken, getPhoneEnvBool } from '@/common/common.js'; +const IsAndEnv = getPhoneEnvBool('AND') + // const url = '/trapractice/voiceCallSocket/voiceCall' // const url = '/traapp/voiceTransSocket/open'; -const url = '/traapp/voiceAiModelSocket/open'; +// const url = '/traapp/asrAiModelSocket/open'; +const url = '/traapp/asrAiModelSocket/open'; const _baseUrl = get_base_url(url).split('http').join('ws'); -// const _baseUrl = ''; export const wsUrl = _baseUrl + url + '?summary=' + getToken(); +// export const wsUrl = 'wss://ty_chat_test.home.tymas.cn:8000/ws/audio'; const windowInfo = uni.getWindowInfo(); // 安全区域信息 const safeArea = windowInfo.safeArea; @@ -17,58 +20,49 @@ export const bottomAreaTopBoundary = safeArea.bottom - btnHeight; // 屏幕横向分割线 export const horizontalSplitX = safeArea.right / 2; - -// 假设你在 Vue3 环境中,ws、wsStatus 是 ref 响应式变量 -import { ref } from 'vue'; - -const ws = ref(null); -const wsStatus = ref(false); - /** * 封装WebSocket连接为Promise * @param {string} wsUrl - WebSocket连接地址 * @returns {Promise} 成功时返回WS实例,失败时reject错误信息 */ -export const initWebSocket = (wsUrl: string): Promise => { - return new Promise((resolve, reject) => { - // 1. 创建WS连接 - const ws = uni.connectSocket({ - url: wsUrl, - fail: (err) => { - console.error('WS连接创建失败:', err); - // 连接创建失败直接reject - reject(new Error(`WS连接创建失败:${err.errMsg || err.message}`)); - }, - success: () => { - console.log('WS连接创建请求已发送'); - } - }); +export const initWebSocket = (wsUrl : string) : Promise => { + console.log('wsUrl', wsUrl); + return new Promise((resolve, reject) => { + // 1. 创建WS连接 + const ws = uni.connectSocket({ + url: wsUrl, + fail: (err) => { + console.error('WS连接创建失败:', err); + // 连接创建失败直接reject + reject(new Error(`WS连接创建失败:${err.errMsg || err.message}`)); + }, + success: () => { + } + }); + + // 2. 监听连接成功(核心:连接成功时resolve,返回WS实例) + const onOpenHandler = (res) => { + resolve(ws); + }; - // 2. 监听连接成功(核心:连接成功时resolve,返回WS实例) - const onOpenHandler = (res) => { - console.log('ws已连接', res); - // resolve返回WS实例 - resolve(ws); - }; + // 4. 监听连接错误(出错时reject) + const onErrorHandler = (err) => { + console.log('WS连接错误', err, wsUrl); + // reject返回错误信息 + reject(new Error(`WS连接错误:${err.errMsg || err.message}`)); + }; - // 4. 监听连接错误(出错时reject) - const onErrorHandler = (err) => { - console.log('WS连接错误', err); - // reject返回错误信息 - reject(new Error(`WS连接错误:${err.errMsg || err.message}`)); - }; - - // 5. 监听连接关闭(如果在open前关闭,也reject) - const onCloseHandler = (err) => { - console.log('WS连接已关闭', err); - // 连接未成功就关闭,执行reject - reject(new Error(`WS连接已关闭:${err.errMsg || err.message}`)); - }; - // 绑定监听事件 - ws.onOpen(onOpenHandler); - ws.onError(onErrorHandler); - ws.onClose(onCloseHandler); - }); + // 5. 监听连接关闭(如果在open前关闭,也reject) + const onCloseHandler = (err) => { + console.log('WS连接已关闭', err); + // 连接未成功就关闭,执行reject + reject(new Error(`WS连接已关闭:${err.errMsg || err.message}`)); + }; + // 绑定监听事件 + ws.onOpen(onOpenHandler); + ws.onError(onErrorHandler); + ws.onClose(onCloseHandler); + }); }; /** @@ -78,19 +72,48 @@ export const initWebSocket = (wsUrl: string): Promise => { * @returns 固定返回false(保持原有返回值类型) */ export function showErrorTip( - errorCode: ErrorCode, - options: ShowErrorTipOptions = {} -): boolean { - // 解构可选参数,设置默认延迟时间为200ms - const { delayTime = 200 } = options; + errorCode : ErrorCode, + options : ShowErrorTipOptions = {} +) : boolean { + // 解构可选参数,设置默认延迟时间为200ms + const { delayTime = 200 } = options; - // 延迟显示对应的错误提示 - setTimeout(() => { - uni.showToast({ - title: errorCode, // 直接使用枚举值作为提示文本 - icon: 'none' - }); - }, delayTime); // 使用传入的延迟时间(或默认200ms) + // 延迟显示对应的错误提示 + setTimeout(() => { + uni.showToast({ + title: errorCode, // 直接使用枚举值作为提示文本 + icon: 'none' + }); + }, delayTime); // 使用传入的延迟时间(或默认200ms) + + return false; +} + +/** + * 跨平台震动反馈封装(适配安卓/iOS)轻震 + * @returns {Promise} 是否成功触发震动 + */ +export const vibrateLight = () : void => { + // #ifdef APP-PLUS + try { + if (IsAndEnv) { + uni.vibrateShort({ + success: function () { + console.log('success'); + } + }); + } else { + let UIImpactFeedbackGenerator = plus.ios.importClass( + 'UIImpactFeedbackGenerator' + ) + let impact = new UIImpactFeedbackGenerator() + impact.prepare() + impact.init(1) + impact.impactOccurred() + } + } catch (error) { + console.error('震动功能执行异常:', error); + } + // #endif +} - return false; -} \ No newline at end of file diff --git a/src/components/touch-btn/touch-btn.types.ts b/src/components/touch-btn/touch-btn.types.ts index a32303e4..7db8a762 100644 --- a/src/components/touch-btn/touch-btn.types.ts +++ b/src/components/touch-btn/touch-btn.types.ts @@ -6,7 +6,7 @@ * - 'text_input' : 文字输入中状态(文字输入面板展开,用户正在输入) * - 'voice_to_text' : 语音转文字状态(独立的语音转文字页面/面板展开) */ -export type StatusType = 'default' | 'voice_recording' | 'text_input' | 'voice_to_text'; +export type StatusType = 'default' | 'voice_recording' | 'text_input' | 'voice_to_text' | 'voice_send_ing'; /** * 语音转文字的子状态(仅当主状态为 voice_to_text 时生效) @@ -14,7 +14,7 @@ export type StatusType = 'default' | 'voice_recording' | 'text_input' | 'voice_t export type VoiceToTextSubStatus = | '' // 纯空 | 'init' // 初始化(刚进入语音转文字页面) - | 'loading' // 转换中(正在处理语音→文字) + // | 'loading' // 转换中(正在处理语音→文字) | 'success' // 转换完成(成功得到文字结果) | 'failed'; // 转换失败(网络/识别错误等) @@ -31,4 +31,36 @@ export enum ErrorCode { export interface ShowErrorTipOptions { // 提示框延迟显示时间(毫秒),默认200ms delayTime?: number; -} \ No newline at end of file +} + + +/** + * 触摸按钮组件的自定义事件类型定义 + * 包含录音、提交、加载相关的所有事件及参数规范 + */ +export type TouchBtnEmits = { + /** + * 提交语音内容(语音转文字完成后触发) + * @param voiceData - 语音相关数据(包含ossKey、识别文本等核心字段) + * @param duration - 录音时长(单位:毫秒),用于校验最短录音时长 + */ + submitVoice: [params: { ossKey: string; text: string }]; + /** + * 提交文字答案(用户确认识别结果/手动编辑后触发) + * @param answerText - 最终提交的文字答案内容 + * @param isManualEdit - 是否手动编辑过识别结果(true=手动修改,false=纯语音识别) + */ + submitAnswer: [answerText: string, isManualEdit: boolean]; + /** + * 开始录音(用户点击录音按钮时触发) + * @param recordParams - 录音初始化参数(可选,默认使用组件内置配置) + */ + startRecord: [recordParams?: { sampleRate: number; channels: number }]; + /** + * 发送加载状态(提交语音/文字时触发,控制父组件加载动画) + * @param isLoading - 是否处于加载中(true=显示加载,false=隐藏加载) + * @param loadingText - 加载提示文本(可选,默认:"处理中...") + */ + sendLoading: [params: { recordId: string; loadingText?: string }]; +}; + diff --git a/src/components/touch-btn/touch-btn.vue b/src/components/touch-btn/touch-btn.vue index 316c6797..cfd17f89 100644 --- a/src/components/touch-btn/touch-btn.vue +++ b/src/components/touch-btn/touch-btn.vue @@ -1,29 +1,28 @@