From acf5de942d518370350900e799d3af75f288657e Mon Sep 17 00:00:00 2001 From: zhangyh Date: Mon, 20 Oct 2025 17:01:37 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A4=87=E7=94=A8=E9=9F=B3=E9=A2=91?= =?UTF-8?q?=E5=BD=95=E5=88=B6=E7=BB=84=E4=BB=B6&=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E9=A1=B5=E5=8F=96=E6=B6=88=E5=8E=9F=E7=94=9Fform=E9=AA=8C?= =?UTF-8?q?=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/stores/modules/userInfo.ts | 60 +- src/utils/audioUtils.ts | 231 ++++++++ .../asistant/components/AudioRecorder.vue | 557 ++++++++++++++++++ src/views/asistant/components/InputArea.vue | 83 ++- src/views/login/LoginPage.vue | 52 +- 5 files changed, 907 insertions(+), 76 deletions(-) create mode 100644 src/utils/audioUtils.ts create mode 100644 src/views/asistant/components/AudioRecorder.vue diff --git a/src/stores/modules/userInfo.ts b/src/stores/modules/userInfo.ts index b8d3198..9af438d 100644 --- a/src/stores/modules/userInfo.ts +++ b/src/stores/modules/userInfo.ts @@ -1,26 +1,42 @@ // 每一个存储的模块,命名规则use开头,store结尾 import { defineStore } from 'pinia' -export const useUserInfoStore = defineStore({ - id: 'userInfo', // 必须指明唯一的pinia仓库的id - state: () => { - return { - num: 0, - name: '张三', - token: '' - } - }, - getters: { - doubleCount: (state) => state.num * 2 - }, - actions: { - changeNum() { - this.num++ - }, - loginOut() { - // 处理退出登录的一些逻辑 - return new Promise((rez) => { - rez('111') - }) - } +import { ref, computed } from 'vue' + +export const useUserInfoStore = defineStore('userInfo', () => { + // state + const num = ref(0) + const name = ref('张三') + const token = ref('') + + // getters + const getUserInfo = computed(() => ({ + num: num.value, + name: name.value, + token: token.value + })) + + // actions + const setUserInfo = (data: any) => { + name.value = data.name + token.value = data.token + } + + const loginOut = () => { + // 处理退出登录的一些逻辑 + return new Promise((rez) => { + rez('111') + }) + } + + return { + // state + num, + name, + token, + // getters + getUserInfo, + // actions + setUserInfo, + loginOut } }) diff --git a/src/utils/audioUtils.ts b/src/utils/audioUtils.ts new file mode 100644 index 0000000..2028469 --- /dev/null +++ b/src/utils/audioUtils.ts @@ -0,0 +1,231 @@ +/** + * 音频处理工具函数 + * 为后续集成第三方TTS方案做准备 + */ + +// 音频文件类型定义 +export interface AudioFile { + blob: Blob + name: string + size: number + type: string + duration?: number +} + +// 音频格式转换选项 +export interface AudioConvertOptions { + format: 'webm' | 'mp3' | 'wav' | 'ogg' + quality?: number + bitrate?: number +} + +/** + * 将Blob转换为指定格式的音频文件 + * @param audioBlob 原始音频Blob + * @param options 转换选项 + * @returns Promise 转换后的音频Blob + */ +export const convertAudioFormat = async ( + audioBlob: Blob, + options: AudioConvertOptions +): Promise => { + return new Promise((resolve, reject) => { + try { + const audio = new Audio() + const url = URL.createObjectURL(audioBlob) + + audio.onloadedmetadata = () => { + // 这里可以添加格式转换逻辑 + // 目前直接返回原始Blob,实际项目中可以使用Web Audio API或第三方库 + URL.revokeObjectURL(url) + resolve(audioBlob) + } + + audio.onerror = () => { + URL.revokeObjectURL(url) + reject(new Error('音频加载失败')) + } + + audio.src = url + } catch (error) { + reject(error) + } + }) +} + +/** + * 获取音频文件信息 + * @param audioBlob 音频Blob + * @returns Promise 音频文件信息 + */ +export const getAudioFileInfo = async (audioBlob: Blob): Promise => { + return new Promise((resolve, reject) => { + const audio = new Audio() + const url = URL.createObjectURL(audioBlob) + + audio.onloadedmetadata = () => { + const audioFile: AudioFile = { + blob: audioBlob, + name: `audio-${Date.now()}.webm`, + size: audioBlob.size, + type: audioBlob.type, + duration: audio.duration + } + + URL.revokeObjectURL(url) + resolve(audioFile) + } + + audio.onerror = () => { + URL.revokeObjectURL(url) + reject(new Error('无法获取音频信息')) + } + + audio.src = url + }) +} + +/** + * 压缩音频文件 + * @param audioBlob 原始音频Blob + * @param quality 压缩质量 (0-1) + * @returns Promise 压缩后的音频Blob + */ +export const compressAudio = async ( + audioBlob: Blob, + quality: number = 0.8 +): Promise => { + // 这里可以实现音频压缩逻辑 + // 目前直接返回原始Blob,实际项目中可以使用Web Audio API + return audioBlob +} + +/** + * 上传音频文件到服务器 + * @param audioFile 音频文件 + * @param uploadUrl 上传地址 + * @returns Promise 服务器返回的文件ID或URL + */ +export const uploadAudioFile = async ( + audioFile: AudioFile, + uploadUrl: string +): Promise => { + const formData = new FormData() + formData.append('audio', audioFile.blob, audioFile.name) + formData.append('duration', audioFile.duration?.toString() || '0') + + try { + const response = await fetch(uploadUrl, { + method: 'POST', + body: formData + }) + + if (!response.ok) { + throw new Error(`上传失败: ${response.statusText}`) + } + + const result = await response.json() + return result.fileId || result.url + } catch (error) { + console.error('音频上传失败:', error) + throw error + } +} + +/** + * 为第三方TTS服务准备音频数据 + * @param audioBlob 音频Blob + * @param serviceType TTS服务类型 + * @returns Promise 准备好的音频数据 + */ +export const prepareAudioForTTS = async ( + audioBlob: Blob, + serviceType: 'openai' | 'azure' | 'aws' | 'google' +): Promise => { + const audioFile = await getAudioFileInfo(audioBlob) + + switch (serviceType) { + case 'openai': + return { + file: audioFile.blob, + model: 'whisper-1', + language: 'zh', + response_format: 'json' + } + + case 'azure': + return { + audio: audioFile.blob, + language: 'zh-CN', + format: 'json' + } + + case 'aws': + return { + Audio: audioFile.blob, + LanguageCode: 'zh-CN', + MediaFormat: 'webm' + } + + case 'google': + return { + audio: { + content: await blobToBase64(audioFile.blob) + }, + config: { + encoding: 'WEBM_OPUS', + languageCode: 'zh-CN', + sampleRateHertz: 16000 + } + } + + default: + throw new Error(`不支持的TTS服务类型: ${serviceType}`) + } +} + +/** + * 将Blob转换为Base64字符串 + * @param blob Blob对象 + * @returns Promise Base64字符串 + */ +export const blobToBase64 = (blob: Blob): Promise => { + return new Promise((resolve, reject) => { + const reader = new FileReader() + reader.onload = () => resolve(reader.result as string) + reader.onerror = reject + reader.readAsDataURL(blob) + }) +} + +/** + * 验证音频文件格式 + * @param blob 音频Blob + * @returns boolean 是否为有效的音频格式 + */ +export const validateAudioFormat = (blob: Blob): boolean => { + const validTypes = [ + 'audio/webm', + 'audio/mp3', + 'audio/wav', + 'audio/ogg', + 'audio/mpeg' + ] + + return validTypes.includes(blob.type) +} + +/** + * 获取音频录制权限 + * @returns Promise 是否获得权限 + */ +export const requestAudioPermission = async (): Promise => { + try { + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }) + stream.getTracks().forEach(track => track.stop()) + return true + } catch (error) { + console.error('获取音频权限失败:', error) + return false + } +} diff --git a/src/views/asistant/components/AudioRecorder.vue b/src/views/asistant/components/AudioRecorder.vue new file mode 100644 index 0000000..02f15ae --- /dev/null +++ b/src/views/asistant/components/AudioRecorder.vue @@ -0,0 +1,557 @@ + + + + + diff --git a/src/views/asistant/components/InputArea.vue b/src/views/asistant/components/InputArea.vue index 331045d..19fbdd0 100644 --- a/src/views/asistant/components/InputArea.vue +++ b/src/views/asistant/components/InputArea.vue @@ -11,16 +11,13 @@
-
-
-
- - +
+ +
+
-
- + + +
@@ -56,13 +63,17 @@