Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
水玉婷
ai-wechat
Commits
a4d5ca91
Commit
a4d5ca91
authored
Nov 25, 2025
by
水玉婷
Browse files
feat:添加语音输入功能
parent
b1f619b0
Changes
6
Hide whitespace changes
Inline
Side-by-side
localhost-key.pem
0 → 100644
View file @
a4d5ca91
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCmrZ80Y5XKfRZG
utsLbDeQhBFhpAqvoE9FnN614BsF4lVVNPYR6Oxowa/XYne56AVEleGpFxXVoRw5
j0s3mT8MIpUpK20WKczO3f88Q9WVQFi68pkBVLUigp78fYH8u3ZSv37tG0SMwlle
NlYDQGo66T/a7fybulWNdtmWmr4pz+BO7cLbodCRglVkrg2aRZTIbjVbWWBoyKXO
yRwzgGgzDpbzesgnlrtvAX1cV+oCtt/ixI+qrrkXtpVkKSmqzwrGzDx8h1QP2qU+
ZhfpNqig+ANtNd7a7Gnhsy0+q3pYSY/JHGikJLSSW5hZFgWqXBhSkDG25O2BSwyz
dy2uGFmPAgMBAAECggEAMfMmGtUdNql124xzyGCN5kktzE0Uxr0MBJiWRXr7ni/N
0tMkSwm6j0o8IBfqOVRG/97K2ZmJeZPmmXlP2UGbm09h1AynjFTKg9QTgUPy5d96
t8ur/rIb9lOewZv7MHodY37v0q6xRF2Z2pn9/Mt5Cl6MPFfFtAWLTfGoE3IcOvsN
Br4/XGYKi0zkd3biqLAEJDL1wHL5J+5VqtL8ZmV6qGBQXw0PBI3ZZO8pV5lsIL1Z
qB/2LMeKCv96KFCfpanK3XdBep9KVUdWIxsjJhgi1ETtLdD/8OmBu+YJp78/Xdqv
io7ux2+Bj3gqEqTIdMySRrRg3oFe/zX0/bkHzScJgQKBgQDaO5lXIjg0OGwdCn81
YeS8z9uqjkFlxvqQe8Xkub57Wrvcva4+f2VRVvXM/xOmFhfMIFMqjTQ6OVwol3Nd
pEvDeiy67bozGle++A/RvPEgt1HpWha79CiJ8V4KCc8sonu5GgLZGCRTmwZ3m0ru
aSR9OwQb7y4ttNsk0UoSLmnBhwKBgQDDhf3lRtiTQic4Zlb2rLdvU4OAOYhDsT6h
JQSVGSwrcGyuv3pkg2yQ8/VlZ8M54rIg7m0+/gRCxZ6owvnj7V0AgUFbk5aG/JSV
Atpqcf12pkoBo1xFmcozf5y0E3rrn5D5jGU/lnOXhDAxmP+bJP80O2+ffhCrJOda
OjY2N6RJuQKBgQChbYCqIZftmObwPHmIpVcsC51z9jKN9LgX9FaYMIWkfaOFT5H6
jQYHOworj2ubabBEwIyEZ1sAzrlLFWyzEfsxJ8i6pWscrhnGG3yoKtk62B/xO0Ch
26O5Fh/30PW9EJvwejstF1yXs47/HpI49PGW6PbLKwu/p46LF31xIX/9NQKBgGVK
IdjIFeRbrfPC2KRbn3+1tPcVVukyhi52/eO7sa0jRbpVibNOfkythWAuG+396aez
vLaYY16v/9yPfWM9kSN00oX9dEqjyNlVLA9e1B7GUKp+lYuc+yoonuaO/OvZswIE
YGNLrsA8g7b9+tTFmsvVSqNGbJ4stQmCBJmbw6lJAoGBAL0/LehG2sKvsNDoXT72
2HOYH2jz5w5/20ptEjV4I6XvdRw4tfQDe1casNS1RuE7pNTcSKnN8J0vQ4BImHyR
hX2SAd8gTg6UDVCEpjwRveIhnvR+dbIgoIF8b8EdMIeepzP7Bs5fHcIXS3ikXl9t
eTWeKLyu4ce/Lz7caIlkp6tk
-----END PRIVATE KEY-----
localhost.pem
0 → 100644
View file @
a4d5ca91
-----BEGIN CERTIFICATE-----
MIICpDCCAYwCCQDzfHp3ypuCvDANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAls
b2NhbGhvc3QwHhcNMjUxMTI1MDgzNDUyWhcNMjYxMTI1MDgzNDUyWjAUMRIwEAYD
VQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCm
rZ80Y5XKfRZGutsLbDeQhBFhpAqvoE9FnN614BsF4lVVNPYR6Oxowa/XYne56AVE
leGpFxXVoRw5j0s3mT8MIpUpK20WKczO3f88Q9WVQFi68pkBVLUigp78fYH8u3ZS
v37tG0SMwlleNlYDQGo66T/a7fybulWNdtmWmr4pz+BO7cLbodCRglVkrg2aRZTI
bjVbWWBoyKXOyRwzgGgzDpbzesgnlrtvAX1cV+oCtt/ixI+qrrkXtpVkKSmqzwrG
zDx8h1QP2qU+ZhfpNqig+ANtNd7a7Gnhsy0+q3pYSY/JHGikJLSSW5hZFgWqXBhS
kDG25O2BSwyzdy2uGFmPAgMBAAEwDQYJKoZIhvcNAQELBQADggEBADmQg8xLPZ2R
+sZcSEK6jxxvq6lImC0o+vd664jcZb3wG5YMI7gskpiz7uDluH7Tqt+8DjvA5anr
Yvyt6VGYLsCvFq150j4/nRPajlLWM4y3Ulz9vuVSAqY3HXlA55n8ab19HbmpgR0g
QmWDKI4uJ6gLXpQYvp0UAyMwuD1JDf/SPm1kZOQl1gWO2s8rF7fxZD66a4UbjloB
IwVk7oASGcYk+IN9SS3iF1PKHyaoOMA5BnyZvkWS3h0Q++AgYWXzFTvN/lq4g088
lTvsOwA0mMGIRek0zMkrlVB7exhJpRHEhqS576q2O7H9JsZRwlRxc1G7kWimlT0L
pscjZR5TtQo=
-----END CERTIFICATE-----
src/views/components/AiChat.vue
View file @
a4d5ca91
...
@@ -6,7 +6,7 @@
...
@@ -6,7 +6,7 @@
<
img :src="props.logoUrl || defaultAvatar" alt="avatar" class="avatar-image" />
<
img :src="props.logoUrl || defaultAvatar" alt="avatar" class="avatar-image" />
</div>
</div>
<
div class="header-info">
<
div class="header-info">
<
h2>
{{
props
.
dialogSessionId
?
props
?.
detailData
?.
title
||
'
继续对话
'
:
'
新建对
333
话
'
}}
</h2>
<
h2>
{{
props
.
dialogSessionId
?
props
?.
detailData
?.
title
||
'
继续对话
'
:
'
新建对话
'
}}
</h2>
</div>
</div>
</div>
</div>
...
@@ -69,6 +69,16 @@
...
@@ -69,6 +69,16 @@
<!-- 输入区域 - 始终显示 -->
<!-- 输入区域 - 始终显示 -->
<div class="chat-input-container">
<div class="chat-input-container">
<div class="chat-input">
<div class="chat-input">
<!-- 语音识别按钮 -->
<VoiceRecognition
ref="voiceRecognitionRef"
:disabled="loading"
:debug="true"
@audio="handleVoiceAudio"
@error="handleVoiceError"
class="voice-recognition-wrapper"
/>
<textarea ref="textarea" v-model="messageText" placeholder="输入消息..." @keypress="handleKeyPress"
<textarea ref="textarea" v-model="messageText" placeholder="输入消息..." @keypress="handleKeyPress"
@input="adjustTextareaHeight" :disabled="loading"></textarea>
@input="adjustTextareaHeight" :disabled="loading"></textarea>
<button @click="sendMessage" :disabled="loading">
<button @click="sendMessage" :disabled="loading">
...
@@ -88,6 +98,7 @@ import tableTemplate from './tableTemplate';
...
@@ -88,6 +98,7 @@ import tableTemplate from './tableTemplate';
import { SendOutlined, UserOutlined } from '@ant-design/icons-vue';
import { SendOutlined, UserOutlined } from '@ant-design/icons-vue';
import defaultAvatar from '@/assets/logo.png';
import defaultAvatar from '@/assets/logo.png';
import ChartComponent from './ChartComponent.vue'; // 导入独立的图表组件
import ChartComponent from './ChartComponent.vue'; // 导入独立的图表组件
import VoiceRecognition from './VoiceRecognition.vue'; // 导入语音识别组件
// 组件属性
// 组件属性
const props = withDefaults(
const props = withDefaults(
...
@@ -192,13 +203,28 @@ const contentTemplates = {
...
@@ -192,13 +203,28 @@ const contentTemplates = {
onerror="console.error('iframe加载失败:', this.src)"
onerror="console.error('iframe加载失败:', this.src)"
></iframe>
></iframe>
</div>`;
</div>`;
},
// 音频消息模板
audio: (audioData: any) => {
const { audioUrl, audioBlob } = audioData;
let src = audioUrl;
// 如果提供了Blob对象,创建对象URL
if (audioBlob && !audioUrl) {
src = URL.createObjectURL(audioBlob);
}
return `<div class="audio-message">
<audio controls src="${src}">
您的浏览器不支持音频播放
</audio>
</div>`;
}
}
};
};
// 定义消息类型 - 更新接口添加图表相关字段
// 定义消息类型 - 更新接口添加图表相关字段
interface Message {
interface Message {
messageType: 'received' | 'sent';
messageType: 'received' | 'sent';
type?: number | string;
avatar: string;
avatar: string;
recordId: string;
recordId: string;
promptTokens: number;
promptTokens: number;
...
@@ -216,6 +242,13 @@ interface Message {
...
@@ -216,6 +242,13 @@ interface Message {
}[];
}[];
}
}
// 检查是否为音频消息的辅助函数
const isAudioMessage = (messageData: any): boolean => {
return messageData.questionType === 'audio' ||
(messageData.question && typeof messageData.question === 'object' &&
(messageData.question.audioUrl || messageData.question.audioData));
};
interface SSEData {
interface SSEData {
message: any;
message: any;
status: number | string;
status: number | string;
...
@@ -243,6 +276,86 @@ const isReconnecting = ref(false);
...
@@ -243,6 +276,86 @@ const isReconnecting = ref(false);
const timeArr = ref([]);
const timeArr = ref([]);
const hasStartedConversation = ref(false); // 添加对话开始状态
const hasStartedConversation = ref(false); // 添加对话开始状态
// 语音事件处理函数
const handleVoiceAudio = (audioBlob: Blob) => {
console.log('收到音频数据:', audioBlob);
// 开始对话
startConversation();
// 添加音频消息到聊天记录
messages.value.push({
messageType: 'sent',
avatar: '我',
recordId: '',
promptTokens: 0,
completionTokens: 0,
totalTokens: 0,
date: dayjs().format('HH:mm'),
contentBlocks: [
{
content: contentTemplates.audio({ audioBlob }),
thinkContent: '',
hasThinkBox: false,
thinkBoxExpanded: false,
}
],
});
// 滚动到底部
nextTick(() => {
scrollToBottom();
});
// 发送音频到AI
sendAudioMessage(audioBlob);
};
const handleVoiceError = (error: string) => {
console.error('语音识别错误:', error);
// 可以添加错误提示
};
// 发送音频消息
const sendAudioMessage = async (audioBlob: Blob) => {
loading.value = true;
try {
// 创建FormData来发送音频文件
const formData = new FormData();
formData.append('audio', audioBlob, 'recording.wav');
formData.append('dialogSessionId', dialogSessionId.value);
formData.append('appId', props.params?.appId || '');
formData.append('stage', props.params?.stage || '');
// 调用外部传入的消息发送函数
if (props.onMessageSend) {
console.log('调用外部音频发送函数');
// 这里需要根据实际情况调整,可能需要将音频转换为base64或其他格式
await props.onMessageSend(audioBlob);
} else {
// 默认的API调用逻辑 - 发送音频
console.log('默认音频API调用逻辑');
const response = await post(`${props.apiBaseUrl}/aiService/ask/audio/app/${props.params?.appId}`, formData, {
headers: {
Token: props.token || '',
'x-session-id': props.token || '',
'x-app-code': props.appCode || '',
'Content-Type': 'multipart/form-data',
}
});
const data = response.data;
if (data.code === 0) {
console.log('音频发送成功');
}
}
} catch (e) {
console.error('发送音频消息失败:', e);
} finally {
loading.value = false;
}
};
// 开始对话函数 - 修改为在发送消息时调用
// 开始对话函数 - 修改为在发送消息时调用
const startConversation = () => {
const startConversation = () => {
hasStartedConversation.value = true;
hasStartedConversation.value = true;
...
@@ -264,7 +377,7 @@ const simulateOptionData = () => {
...
@@ -264,7 +377,7 @@ const simulateOptionData = () => {
}
}
};
};
// 第二个消息:展示一个options(会走iframe逻辑)
// 第二个消息:展示一个options(会走iframe逻辑)
const secondOptionData = {
const secondOptionData = {
status: 3,
status: 3,
type: 'option',
type: 'option',
...
@@ -314,7 +427,7 @@ const simulateOptionData = () => {
...
@@ -314,7 +427,7 @@ const simulateOptionData = () => {
messages.value.push(secondResult.updatedResponse);
messages.value.push(secondResult.updatedResponse);
nextTick(() => {
nextTick(() => {
scrollToBottom();
scrollToBottom();
});
});
}
}
};
};
...
@@ -765,81 +878,92 @@ onBeforeUnmount(() => {
...
@@ -765,81 +878,92 @@ onBeforeUnmount(() => {
});
});
// 处理历史记录数据
// 处理历史记录数据
const processHistoryData = (data
Array
: any[]
)
=> {
const processHistoryData = (data: any
): Message
[] => {
const result: Message[] = [];
const result: Message[] = [];
dataArray.forEach((data) => {
const date = dayjs(data.createTime).format('HH:mm');
let date = dayjs(data.startTime).format('YYYY-MM-DD HH:mm:ss');
if (data.question) {
// 处理问题消息
result.push({
if (data.question) {
messageType: 'sent',
let questionContent = '';
avatar: '我',
recordId: '',
// 检查是否为音频消息
promptTokens: 0,
if (isAudioMessage(data)) {
completionTokens: 0,
// 处理音频消息
totalTokens: 0,
const audioData = data.question;
date,
questionContent = contentTemplates.audio({
contentBlocks: [
audioUrl: audioData.audioUrl,
{
audioBlob: audioData.audioBlob
content: contentTemplates.text(data.question),
thinkContent: '',
hasThinkBox: false,
thinkBoxExpanded: false,
},
],
});
});
} else {
// 处理文本消息
questionContent = contentTemplates.text(
typeof data.question === 'string' ? data.question : data.question.content || ''
);
}
}
if (data.answerInfoList && Array.isArray(data.answerInfoList)) {
result.push({
const aiMessage: Message = {
messageType: 'sent',
messageType: 'received',
avatar: '我',
avatar: 'AI',
recordId: '',
recordId: '',
promptTokens: 0,
promptTokens: 0,
completionTokens: 0,
completionTokens: 0,
totalTokens: 0,
totalTokens: 0,
date,
contentBlocks: [],
contentBlocks: [
date,
{
};
content: questionContent,
thinkContent: '',
hasThinkBox: false,
thinkBoxExpanded: false,
},
],
});
}
let currentThinkingMode = false;
// 处理AI回答消息
let currentBlockIdx = -1;
if (data.answerInfoList && Array.isArray(data.answerInfoList)) {
const aiMessage: Message = {
// 历史数据处理,isHistoryData设为true,思考框折叠
messageType: 'received',
data.answerInfoList.forEach((answer) => {
avatar: 'AI',
const sseData: SSEData = {
recordId: '',
message: answer.message || '',
promptTokens: 0,
status: answer.status || 0,
completionTokens: 0,
type: answer.type || 0,
totalTokens: 0,
};
contentBlocks: [],
date,
const processResult = processSSEMessage(
};
sseData,
aiMessage,
currentThinkingMode,
currentBlockIdx,
true,
);
currentThinkingMode = processResult.updatedIsThinking;
let currentThinkingMode = false;
currentBlockIdx = processResult.updatedBlockIndex;
let currentBlockIdx = -1;
aiMessage.recordId = processResult.recordId;
aiMessage.promptTokens = processResult.promptTokens;
aiMessage.completionTokens = processResult.completionTokens;
aiMessage.totalTokens = processResult.totalTokens;
});
// 确保历史记录中的思考框默认折叠
// 历史数据处理,isHistoryData设为true,思考框折叠
aiMessage.contentBlocks.forEach((block) => {
data.answerInfoList.forEach((answer) => {
if (block.hasThinkBox) {
const sseData: SSEData = {
block.thinkBoxExpanded = false;
message: answer.message || '',
}
status: answer.status || 0,
});
type: answer.type || '',
};
if (aiMessage.contentBlocks.length > 0) {
const processResult = processSSEMessage(
result.push(aiMessage);
sseData,
}
aiMessage,
currentThinkingMode,
currentBlockIdx,
true,
);
currentThinkingMode = processResult.updatedIsThinking;
currentBlockIdx = processResult.updatedBlockIndex;
aiMessage.recordId = processResult.recordId;
aiMessage.promptTokens = processResult.promptTokens;
aiMessage.completionTokens = processResult.completionTokens;
aiMessage.totalTokens = processResult.totalTokens;
});
if (aiMessage.contentBlocks.length > 0) {
result.push(aiMessage);
}
}
}
);
}
return result;
return result;
};
};
...
...
src/views/components/VoiceRecognition.vue
0 → 100644
View file @
a4d5ca91
<
template
>
<
div class="voice-recognition">
<!-- 语音按钮 -->
<
button
class="voice-btn"
:class="
{ 'recording': isRecording, 'disabled': disabled }"
@click="toggleRecording"
:disabled="disabled"
:title="isRecording ? '停止录音' : '开始录音'"
>
<!-- 语音图标始终显示 -->
<
span class="voice-icon">
<
AudioOutlined />
</span>
<!-- 录音时显示指示器 -->
<
span v-if="isRecording" class="recording-indicator">
<
span class="pulse">
</span>
<
span class="pulse">
</span>
<
span class="pulse">
</span>
</span>
</button>
<!-- 语音识别状态提示 -->
<
div v-if="showStatus" class="voice-status" :class="statusClass">
{{
statusText
}}
</div>
</div>
</
template
>
<
script
setup
lang=
"ts"
>
import
{
ref
,
computed
,
onMounted
,
onUnmounted
,
nextTick
}
from
'
vue
'
import
{
AudioOutlined
}
from
'
@ant-design/icons-vue
'
// 组件属性
interface
Props
{
disabled
?:
boolean
debug
?:
boolean
}
const
props
=
withDefaults
(
defineProps
<
Props
>
(),
{
disabled
:
false
,
debug
:
false
})
// 组件事件
const
emit
=
defineEmits
<
{
audio
:
[
audioBlob
:
Blob
]
error
:
[
error
:
string
]
}
>
()
// 响应式数据
const
isRecording
=
ref
(
false
)
const
showStatus
=
ref
(
false
)
const
statusText
=
ref
(
''
)
// MediaRecorder相关
const
mediaRecorder
=
ref
<
MediaRecorder
|
null
>
(
null
)
const
audioChunks
=
ref
<
Blob
[]
>
([])
const
audioStream
=
ref
<
MediaStream
|
null
>
(
null
)
// 计算属性
const
statusClass
=
computed
(()
=>
{
return
isRecording
.
value
?
'
recording
'
:
'
idle
'
})
// 检查浏览器是否支持MediaRecorder
const
isMediaRecorderSupported
=
()
=>
{
const
supported
=
'
MediaRecorder
'
in
window
;
if
(
props
.
debug
)
{
console
.
log
(
'
MediaRecorder支持检查:
'
,
supported
);
}
return
supported
;
}
// 显示状态消息
const
showStatusMessage
=
(
message
:
string
)
=>
{
statusText
.
value
=
message
showStatus
.
value
=
true
setTimeout
(()
=>
{
showStatus
.
value
=
false
},
3000
)
}
// 检查麦克风权限
const
checkMicrophonePermission
=
async
()
=>
{
try
{
const
permissionStatus
=
await
navigator
.
permissions
.
query
({
name
:
'
microphone
'
as
PermissionName
});
if
(
props
.
debug
)
{
console
.
log
(
'
麦克风权限状态:
'
,
permissionStatus
.
state
);
}
if
(
permissionStatus
.
state
===
'
denied
'
)
{
showStatusMessage
(
'
麦克风权限被拒绝,请在浏览器设置中允许访问
'
);
return
false
;
}
return
true
;
}
catch
(
error
)
{
if
(
props
.
debug
)
{
console
.
warn
(
'
无法检查麦克风权限:
'
,
error
);
}
return
true
;
// 如果无法检查权限,继续尝试
}
}
// 显示权限引导提示
const
showPermissionGuide
=
()
=>
{
const
guideMessage
=
`
麦克风权限被拒绝,请按以下步骤操作:
1. 点击浏览器地址栏左侧的"锁形图标"或"不安全"标识
2. 选择"网站设置"
3. 找到"麦克风"权限,选择"允许"
4. 刷新页面后重试
`
;
showStatusMessage
(
'
麦克风权限被拒绝,请检查浏览器设置
'
);
// 在调试模式下显示详细引导
if
(
props
.
debug
)
{
console
.
warn
(
'
麦克风权限被拒绝,用户需要手动授权
'
);
console
.
log
(
guideMessage
);
}
}
// 开始录音
const
startRecording
=
async
()
=>
{
if
(
props
.
disabled
)
return
// 检查权限
const
hasPermission
=
await
checkMicrophonePermission
();
if
(
!
hasPermission
)
{
showPermissionGuide
();
return
;
}
// 检查浏览器支持
if
(
!
isMediaRecorderSupported
())
{
const
errorMsg
=
'
您的浏览器不支持音频录制功能
'
;
showStatusMessage
(
errorMsg
);
emit
(
'
error
'
,
errorMsg
);
return
;
}
try
{
// 获取麦克风权限
const
stream
=
await
navigator
.
mediaDevices
.
getUserMedia
({
audio
:
{
echoCancellation
:
true
,
noiseSuppression
:
true
,
sampleRate
:
44100
}
});
audioStream
.
value
=
stream
;
audioChunks
.
value
=
[];
// 创建MediaRecorder实例
mediaRecorder
.
value
=
new
MediaRecorder
(
stream
,
{
mimeType
:
'
audio/webm;codecs=opus
'
});
// 设置数据可用事件处理
mediaRecorder
.
value
.
ondataavailable
=
(
event
)
=>
{
if
(
event
.
data
.
size
>
0
)
{
audioChunks
.
value
.
push
(
event
.
data
);
}
};
// 设置停止事件处理 - 录制完成后直接发送
mediaRecorder
.
value
.
onstop
=
()
=>
{
sendRecordedAudio
();
};
// 开始录制
mediaRecorder
.
value
.
start
(
100
);
// 每100ms收集一次数据
isRecording
.
value
=
true
;
showStatusMessage
(
'
正在录音...
'
);
if
(
props
.
debug
)
{
console
.
log
(
'
开始录音,MediaRecorder状态:
'
,
mediaRecorder
.
value
.
state
);
}
}
catch
(
error
)
{
console
.
error
(
'
启动录音失败:
'
,
error
);
let
errorMessage
=
'
无法启动录音
'
;
if
(
error
&&
error
.
name
===
'
NotAllowedError
'
)
{
errorMessage
=
'
麦克风权限被拒绝
'
;
showPermissionGuide
();
}
else
if
(
error
&&
error
.
name
===
'
NotFoundError
'
)
{
errorMessage
=
'
未找到麦克风设备
'
;
}
showStatusMessage
(
errorMessage
);
emit
(
'
error
'
,
errorMessage
);
}
}
// 停止录音
const
stopRecording
=
()
=>
{
if
(
mediaRecorder
.
value
&&
mediaRecorder
.
value
.
state
===
'
recording
'
)
{
mediaRecorder
.
value
.
stop
();
isRecording
.
value
=
false
;
// 停止所有音频轨道
if
(
audioStream
.
value
)
{
audioStream
.
value
.
getTracks
().
forEach
(
track
=>
track
.
stop
());
audioStream
.
value
=
null
;
}
if
(
props
.
debug
)
{
console
.
log
(
'
停止录音,MediaRecorder状态:
'
,
mediaRecorder
.
value
.
state
);
}
}
}
// 发送录制的音频
const
sendRecordedAudio
=
()
=>
{
if
(
audioChunks
.
value
.
length
===
0
)
{
showStatusMessage
(
'
录音数据为空
'
);
return
;
}
const
audioBlob
=
new
Blob
(
audioChunks
.
value
,
{
type
:
'
audio/webm;codecs=opus
'
});
// 发送音频数据
emit
(
'
audio
'
,
audioBlob
);
showStatusMessage
(
'
音频已发送
'
);
// 清理录音数据
audioChunks
.
value
=
[];
if
(
props
.
debug
)
{
console
.
log
(
'
音频发送完成,大小:
'
,
Math
.
round
(
audioBlob
.
size
/
1024
),
'
KB
'
);
}
}
// 切换录音状态
const
toggleRecording
=
()
=>
{
if
(
isRecording
.
value
)
{
stopRecording
();
}
else
{
startRecording
();
}
}
// 组件卸载时清理资源
onUnmounted
(()
=>
{
if
(
isRecording
.
value
)
{
stopRecording
();
}
if
(
audioStream
.
value
)
{
audioStream
.
value
.
getTracks
().
forEach
(
track
=>
track
.
stop
());
}
})
// 暴露方法给父组件
defineExpose
({
startRecording
,
stopRecording
,
isRecording
:
()
=>
isRecording
.
value
})
</
script
>
<
style
scoped
lang=
"less"
>
@import './style.less';
.voice-recognition {
position: relative;
display: inline-block;
}
.voice-btn {
width: 40px;
height: 40px;
border: none;
border-radius: 50%;
background: @primary-color;
color: @white;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
position: relative;
z-index: 100; /* 大幅提高按钮的z-index,确保始终在最上层 */
&:hover:not(.disabled) {
background: @primary-hover;
transform: scale(1.05);
}
&:active:not(.disabled) {
transform: scale(0.95);
}
&.recording {
background: @error-color;
animation: pulse 1.5s infinite;
}
&.disabled {
background: @gray-4;
cursor: not-allowed;
opacity: 0.6;
}
}
.voice-icon {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
.recording-indicator {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
gap: 2px;
.pulse {
width: 4px;
height: 4px;
background: @white;
border-radius: 50%;
animation: pulse 1.5s infinite;
&:nth-child(2) {
animation-delay: 0.2s;
}
&:nth-child(3) {
animation-delay: 0.4s;
}
}
}
.voice-status {
position: absolute;
top: -45px; /* 进一步增加距离,确保完全不会遮挡按钮 */
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.8);
color: @white;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
white-space: nowrap;
z-index: 1; /* 保持较低的z-index */
pointer-events: none; /* 禁止状态提示框接收点击事件 */
&.recording {
background: rgba(255, 0, 0, 0.8);
}
}
@keyframes pulse {
0% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.5;
transform: scale(0.8);
}
100% {
opacity: 1;
transform: scale(1);
}
}
</
style
>
\ No newline at end of file
src/views/components/style.less
View file @
a4d5ca91
...
@@ -18,6 +18,7 @@
...
@@ -18,6 +18,7 @@
@gray-6: #666666;
@gray-6: #666666;
@gray-7: #333333;
@gray-7: #333333;
@success-color: #52c41a;
@success-color: #52c41a;
@success-hover: #46a51a; // 添加缺失的变量定义
@error-color: #f5222d;
@error-color: #f5222d;
@warning-color: #faad14;
@warning-color: #faad14;
...
@@ -333,9 +334,18 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li {
...
@@ -333,9 +334,18 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li {
align-items: flex-end;
align-items: flex-end;
position: relative;
position: relative;
// 语音识别按钮容器 - 移动到右边
.voice-recognition-wrapper {
position: absolute;
right: 40px; // 放在发送按钮左边
top: 50%;
transform: translateY(-50%);
z-index: 10;
}
textarea {
textarea {
flex: 1;
flex: 1;
padding: 14px
7
0px 14px 1
8
px;
padding: 14px
11
0px 14px 1
6
px;
// 调整内边距:右侧为两个按钮留空间,左侧恢复正常
border-radius: 12px;
border-radius: 12px;
outline: none;
outline: none;
resize: none;
resize: none;
...
@@ -378,13 +388,7 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li {
...
@@ -378,13 +388,7 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li {
transition: color 0.2s, border-color 0.2s, background-color 0.2s;
transition: color 0.2s, border-color 0.2s, background-color 0.2s;
z-index: 10;
z-index: 10;
transform: translateY(-50%);
transform: translateY(-50%);
&:hover {
color: @primary-hover;
border-color: @primary-hover;
background-color: rgba(91, 138, 254, 0.05);
}
&:active {
&:active {
background-color: rgba(91, 138, 254, 0.1);
background-color: rgba(91, 138, 254, 0.1);
transform: translateY(-50%) scale(0.95);
transform: translateY(-50%) scale(0.95);
...
@@ -842,4 +846,93 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li {
...
@@ -842,4 +846,93 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li {
}
}
}
}
}
}
}
// 音频消息样式
.message-audio {
background: linear-gradient(135deg, #f0f9ff, #e6f7ff);
border: 1px solid #91d5ff;
border-radius: 12px;
padding: 12px;
margin: 8px 0;
.audio-indicator {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
.audio-icon {
font-size: 16px;
}
.audio-text {
font-size: 14px;
font-weight: 500;
color: #1890ff;
}
}
.audio-transcript {
font-size: 14px;
line-height: 1.4;
color: #595959;
background: rgba(255, 255, 255, 0.7);
padding: 8px;
border-radius: 6px;
border-left: 3px solid #1890ff;
}
}
// 语音识别组件样式调整
.chat-input {
display: flex;
align-items: flex-end;
gap: 8px;
textarea {
flex: 1;
padding: 14px 110px 14px 16px; // 调整内边距:右侧为两个按钮留空间,左侧恢复正常
border-radius: 12px;
outline: none;
resize: none;
height: 52px;
font-size: 15px;
transition: border-color 0.3s, box-shadow 0.3s;
background-color: @blue-light-2;
border: 1px solid @gray-3;
overflow: hidden;
position: relative;
box-shadow: 0px 2px 8px 0px rgba(0, 0, 0, 0.12);
&:focus {
border-color: @primary-color;
box-shadow: 0 0 0 2px rgba(91, 138, 254, 0.1);
}
&:disabled {
background-color: @gray-2;
border-color: @gray-3;
color: @gray-5;
cursor: not-allowed;
}
}
// 语音识别按钮样式
.voice-recognition {
margin: 0;
}
}
.operation-box {
margin-top: 6px;
p {
color: @gray-5;
font-size: 12px;
span {
margin-right: 15px;
}
}
}
}
\ No newline at end of file
vite.config.js
View file @
a4d5ca91
import
{
defineConfig
}
from
'
vite
'
import
{
defineConfig
}
from
'
vite
'
import
vue
from
'
@vitejs/plugin-vue
'
import
vue
from
'
@vitejs/plugin-vue
'
import
{
resolve
}
from
'
path
'
;
import
{
resolve
}
from
'
path
'
;
import
fs
from
'
fs
'
;
export
default
defineConfig
({
export
default
defineConfig
({
base
:
'
/ai/
'
,
// 添加基础路径前缀
base
:
'
/ai/
'
,
// 添加基础路径前缀
plugins
:
[
vue
()],
plugins
:
[
vue
()],
server
:
{
server
:
{
host
:
'
0.0.0.0
'
,
host
:
'
0.0.0.0
'
,
port
:
3000
,
port
:
3000
,
https
:
{
key
:
fs
.
readFileSync
(
'
./localhost-key.pem
'
),
cert
:
fs
.
readFileSync
(
'
./localhost.pem
'
)
},
// 添加history fallback配置
// 添加history fallback配置
historyApiFallback
:
{
historyApiFallback
:
{
rewrites
:
[
rewrites
:
[
...
@@ -40,24 +44,6 @@ export default defineConfig({
...
@@ -40,24 +44,6 @@ export default defineConfig({
console
.
log
(
'
发送请求到:
'
,
options
.
target
);
console
.
log
(
'
发送请求到:
'
,
options
.
target
);
});
});
}
}
},
// 修复pedService代理配置
'
/pedService
'
:
{
target
:
'
http://10.17.86.37:8630
'
,
changeOrigin
:
true
,
// 解决跨域问题
secure
:
false
,
// 允许不安全的SSL连接
rewrite
:
(
path
)
=>
path
.
replace
(
/^
\/
pedService/
,
'
/pedService
'
),
// 保留前缀
configure
:
(
proxy
,
options
)
=>
{
proxy
.
on
(
'
error
'
,
(
err
,
req
,
res
)
=>
{
console
.
log
(
'
pedService代理错误:
'
,
err
);
});
proxy
.
on
(
'
proxyReq
'
,
(
proxyReq
,
req
,
res
)
=>
{
console
.
log
(
'
pedService发送请求到本地服务:
'
,
options
.
target
+
req
.
url
);
});
proxy
.
on
(
'
proxyRes
'
,
(
proxyRes
,
req
,
res
)
=>
{
console
.
log
(
'
pedService收到响应,状态码:
'
,
proxyRes
.
statusCode
);
});
}
}
}
},
},
},
},
...
...
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment