Commit 1dd66d7e authored by 水玉婷's avatar 水玉婷
Browse files

feat:语音时长调试

parent 6d548eb4
...@@ -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 || '继续对话' : '新建对话' }}</h2> <h2>{{ props.dialogSessionId ? props?.detailData?.title || '继续对话' : '新建对话' }}</h2>
</div> </div>
</div> </div>
...@@ -48,7 +48,7 @@ ...@@ -48,7 +48,7 @@
<div v-if="item.hasThinkBox" class="think-box-wrapper"> <div v-if="item.hasThinkBox" class="think-box-wrapper">
<div class="think-box-toggle" @click="toggleThinkBox(index, i)">{{ <div class="think-box-toggle" @click="toggleThinkBox(index, i)">{{
item.thinkBoxExpanded ? '▲ 收起思考过程' : '▼ 展开思考过程' item.thinkBoxExpanded ? '▲ 收起思考过程' : '▼ 展开思考过程'
}}</div> }}</div>
<div v-if="item.thinkBoxExpanded" class="think-box-content" <div v-if="item.thinkBoxExpanded" class="think-box-content"
v-html="contentTemplates.thinking(item.thinkContent || '')"></div> v-html="contentTemplates.thinking(item.thinkContent || '')"></div>
</div> </div>
...@@ -87,7 +87,7 @@ ...@@ -87,7 +87,7 @@
import { EventSourcePolyfill } from 'event-source-polyfill'; import { EventSourcePolyfill } from 'event-source-polyfill';
import { ref, onMounted, onBeforeUnmount, nextTick, watch } from 'vue'; import { ref, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { post,get} from '@/utils/axios.js'; // 导入axios的post方法 import { post, get } from '@/utils/axios.js'; // 导入axios的post方法
import { tableTemplate } from './tableTemplate'; 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';
...@@ -119,6 +119,11 @@ const props = defineProps({ ...@@ -119,6 +119,11 @@ const props = defineProps({
type: String, type: String,
default: '' default: ''
}, },
// 对话详情数据
detailData: {
type: Object,
default: () => ({})
},
onMessageSend: { onMessageSend: {
type: Function, type: Function,
default: undefined default: undefined
...@@ -218,9 +223,9 @@ const contentTemplates = { ...@@ -218,9 +223,9 @@ const contentTemplates = {
></iframe> ></iframe>
</div>`; </div>`;
}, },
// 音频消息模板 // 音频消息模板 - 简化版本,移除audio-icon
audio: (audioData: any) => { audio: (audioData: any) => {
const { audioUrl, audioBlob } = audioData; const { audioUrl, audioBlob, durationTime } = audioData;
let src = audioUrl; let src = audioUrl;
// 如果提供了Blob对象,创建对象URL // 如果提供了Blob对象,创建对象URL
...@@ -233,10 +238,6 @@ const contentTemplates = { ...@@ -233,10 +238,6 @@ const contentTemplates = {
return `<div class="audio-message" data-audio-id="${audioId}"> return `<div class="audio-message" data-audio-id="${audioId}">
<div class="audio-player" data-audio-src="${src}"> <div class="audio-player" data-audio-src="${src}">
<div class="audio-icon">
<span class="play-icon">▶</span>
<span class="pause-icon" style="display: none;">❚❚</span>
</div>
<div class="audio-wave"> <div class="audio-wave">
<div class="wave-bar"></div> <div class="wave-bar"></div>
<div class="wave-bar"></div> <div class="wave-bar"></div>
...@@ -244,14 +245,14 @@ const contentTemplates = { ...@@ -244,14 +245,14 @@ const contentTemplates = {
<div class="wave-bar"></div> <div class="wave-bar"></div>
<div class="wave-bar"></div> <div class="wave-bar"></div>
</div> </div>
<div class="audio-duration">0:00</div> <div class="audio-duration">${durationTime+'"' || '0"'}</div>
</div> </div>
<audio id="${audioId}" src="${src}" preload="metadata" style="display: none;"></audio> <audio id="${audioId}" src="${src}" preload="metadata" style="display: none;"></audio>
</div>`; </div>`;
}, },
// Markdown模板 // Markdown模板
markdown: (content: any) => { markdown: (content: any) => {
// 类型检查和默认值处理 // 类型检查和默认值处理
if (typeof content !== 'string') { if (typeof content !== 'string') {
// 如果是对象,尝试转换为字符串 // 如果是对象,尝试转换为字符串
if (content && typeof content === 'object') { if (content && typeof content === 'object') {
...@@ -261,51 +262,51 @@ const contentTemplates = { ...@@ -261,51 +262,51 @@ const contentTemplates = {
content = String(content || ''); content = String(content || '');
} }
} }
// 简单的Markdown解析器 // 简单的Markdown解析器
const parseMarkdown = (text: string) => { const parseMarkdown = (text: string) => {
// 确保text是字符串 // 确保text是字符串
if (typeof text !== 'string') { if (typeof text !== 'string') {
text = String(text || ''); text = String(text || '');
} }
// 处理标题 // 处理标题
text = text.replace(/^### (.*$)/gim, '<h3>$1</h3>'); text = text.replace(/^### (.*$)/gim, '<h3>$1</h3>');
text = text.replace(/^## (.*$)/gim, '<h2>$1</h2>'); text = text.replace(/^## (.*$)/gim, '<h2>$1</h2>');
text = text.replace(/^# (.*$)/gim, '<h1>$1</h1>'); text = text.replace(/^# (.*$)/gim, '<h1>$1</h1>');
// 处理粗体 // 处理粗体
text = text.replace(/\*\*(.*?)\*\*/gim, '<strong>$1</strong>'); text = text.replace(/\*\*(.*?)\*\*/gim, '<strong>$1</strong>');
text = text.replace(/\*(.*?)\*/gim, '<em>$1</em>'); text = text.replace(/\*(.*?)\*/gim, '<em>$1</em>');
// 处理代码块 // 处理代码块
text = text.replace(/```([\s\S]*?)```/gim, '<pre><code>$1</code></pre>'); text = text.replace(/```([\s\S]*?)```/gim, '<pre><code>$1</code></pre>');
text = text.replace(/`(.*?)`/gim, '<code>$1</code>'); text = text.replace(/`(.*?)`/gim, '<code>$1</code>');
// 处理链接 // 处理链接
text = text.replace(/\[([^\[]+)\]\(([^\)]+)\)/gim, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>'); text = text.replace(/\[([^\[]+)\]\(([^\)]+)\)/gim, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
// 处理图片 // 处理图片
text = text.replace(/!\[([^\[]+)\]\(([^\)]+)\)/gim, '<img src="$2" alt="$1" style="max-width: 100%; height: auto;" />'); text = text.replace(/!\[([^\[]+)\]\(([^\)]+)\)/gim, '<img src="$2" alt="$1" style="max-width: 100%; height: auto;" />');
// 处理列表 // 处理列表
text = text.replace(/^\s*-\s(.*$)/gim, '<li>$1</li>'); text = text.replace(/^\s*-\s(.*$)/gim, '<li>$1</li>');
text = text.replace(/(<li>.*<\/li>)/gim, '<ul>$1</ul>'); text = text.replace(/(<li>.*<\/li>)/gim, '<ul>$1</ul>');
// 处理换行 // 处理换行
text = text.replace(/\n/gim, '<br>'); text = text.replace(/\n/gim, '<br>');
// 处理段落 // 处理段落
text = text.replace(/<br><br>/gim, '</p><p>'); text = text.replace(/<br><br>/gim, '</p><p>');
text = '<p>' + text + '</p>'; text = '<p>' + text + '</p>';
text = text.replace(/<p><(h[1-6]|ul|pre|img)/gim, '</p><$1'); text = text.replace(/<p><(h[1-6]|ul|pre|img)/gim, '</p><$1');
text = text.replace(/(<\/(h[1-6]|ul|pre|img)>)<p>/gim, '$1</p><p>'); text = text.replace(/(<\/(h[1-6]|ul|pre|img)>)<p>/gim, '$1</p><p>');
return text; return text;
}; };
const htmlContent = parseMarkdown(content); const htmlContent = parseMarkdown(content);
// 清理HTML内容:移除br标签和空p段落 // 清理HTML内容:移除br标签和空p段落
const cleanHtml = htmlContent const cleanHtml = htmlContent
.trim() .trim()
...@@ -321,12 +322,12 @@ const contentTemplates = { ...@@ -321,12 +322,12 @@ const contentTemplates = {
.replace(/^\s*<p[^>]*>\s*<\/p>/gi, '') .replace(/^\s*<p[^>]*>\s*<\/p>/gi, '')
.replace(/<p[^>]*>\s*<\/p>\s*$/gi, '') .replace(/<p[^>]*>\s*<\/p>\s*$/gi, '')
.trim(); .trim();
// 检查内容是否为空或只有空白 // 检查内容是否为空或只有空白
if (!cleanHtml || cleanHtml === '<p></p>' || cleanHtml === '<p>&nbsp;</p>') { if (!cleanHtml || cleanHtml === '<p></p>' || cleanHtml === '<p>&nbsp;</p>') {
return ''; // 如果内容为空,返回空字符串不展示 return ''; // 如果内容为空,返回空字符串不展示
} }
return `<div class="message-markdown"> return `<div class="message-markdown">
<div class="markdown-content">${cleanHtml}</div> <div class="markdown-content">${cleanHtml}</div>
</div>`; </div>`;
...@@ -381,7 +382,7 @@ const timeArr = ref([]); ...@@ -381,7 +382,7 @@ const timeArr = ref([]);
const hasStartedConversation = ref(false); // 添加对话开始状态 const hasStartedConversation = ref(false); // 添加对话开始状态
// 语音事件处理函数 - 修改为接收服务器返回的URL // 语音事件处理函数 - 修改为接收服务器返回的URL
const handleVoiceAudio = (audioUrl: string, audioBlob?: Blob) => { const handleVoiceAudio = (audioUrl: string, audioBlob?: Blob, durationTime?: number) => {
console.log('收到音频URL:', audioUrl); console.log('收到音频URL:', audioUrl);
// 开始对话 // 开始对话
startConversation(); startConversation();
...@@ -397,7 +398,7 @@ const handleVoiceAudio = (audioUrl: string, audioBlob?: Blob) => { ...@@ -397,7 +398,7 @@ const handleVoiceAudio = (audioUrl: string, audioBlob?: Blob) => {
date: dayjs().format('HH:mm'), date: dayjs().format('HH:mm'),
contentBlocks: [ contentBlocks: [
{ {
content: contentTemplates.audio({ audioUrl, audioBlob }), content: contentTemplates.audio({ audioUrl, audioBlob, durationTime }),
thinkContent: '', thinkContent: '',
hasThinkBox: false, hasThinkBox: false,
thinkBoxExpanded: false, thinkBoxExpanded: false,
...@@ -407,7 +408,7 @@ const handleVoiceAudio = (audioUrl: string, audioBlob?: Blob) => { ...@@ -407,7 +408,7 @@ const handleVoiceAudio = (audioUrl: string, audioBlob?: Blob) => {
// 如果有音频Blob,直接发送到服务器 // 如果有音频Blob,直接发送到服务器
if (audioUrl) { if (audioUrl) {
sendAudioMessage(audioUrl); sendAudioMessage(audioUrl, durationTime);
} }
// 滚动到底部 // 滚动到底部
...@@ -422,13 +423,15 @@ const handleVoiceError = (error: string) => { ...@@ -422,13 +423,15 @@ const handleVoiceError = (error: string) => {
}; };
// 发送音频消息 - 简化逻辑,与sendMessage保持一致 // 发送音频消息 - 简化逻辑,与sendMessage保持一致
const sendAudioMessage = async (audioUrl: string) => { const sendAudioMessage = async (audioUrl: string, durationTime?: number) => {
loading.value = true; loading.value = true;
try { try {
// 开始对话 // 开始对话
startConversation(); startConversation();
isAIResponding.value = false;
isInThinkingMode.value = false;
currentAIResponse.value = null;
// 调用外部传入的消息发送函数 // 调用外部传入的消息发送函数
if (props.onMessageSend) { if (props.onMessageSend) {
console.log('调用外部音频发送函数'); console.log('调用外部音频发送函数');
...@@ -437,6 +440,7 @@ const sendAudioMessage = async (audioUrl: string) => { ...@@ -437,6 +440,7 @@ const sendAudioMessage = async (audioUrl: string) => {
// 默认的API调用逻辑 - 使用与sendMessage相同的逻辑,只是参数不同 // 默认的API调用逻辑 - 使用与sendMessage相同的逻辑,只是参数不同
const response = await post(`${props.apiBaseUrl}/aiService/ask/app/${props.params?.appId}`, { const response = await post(`${props.apiBaseUrl}/aiService/ask/app/${props.params?.appId}`, {
questionLocalAudioFilePath: audioUrl, questionLocalAudioFilePath: audioUrl,
audioDuration: durationTime,
...props.params, ...props.params,
}, { }, {
headers: { headers: {
...@@ -583,13 +587,13 @@ const processSSEMessage = ( ...@@ -583,13 +587,13 @@ const processSSEMessage = (
const mergeMarkdownContent = (existingContent: string, newContent: string) => { const mergeMarkdownContent = (existingContent: string, newContent: string) => {
// 从现有的markdown内容中提取内部内容 // 从现有的markdown内容中提取内部内容
const existingInnerContent = existingContent.replace(/<div class="message-markdown">\s*<div class="markdown-content">([\s\S]*?)<\/div>\s*<\/div>/, '$1'); const existingInnerContent = existingContent.replace(/<div class="message-markdown">\s*<div class="markdown-content">([\s\S]*?)<\/div>\s*<\/div>/, '$1');
// 从新的markdown内容中提取内部内容 // 从新的markdown内容中提取内部内容
const newInnerContent = newContent.replace(/<div class="message-markdown">\s*<div class="markdown-content">([\s\S]*?)<\/div>\s*<\/div>/, '$1'); const newInnerContent = newContent.replace(/<div class="message-markdown">\s*<div class="markdown-content">([\s\S]*?)<\/div>\s*<\/div>/, '$1');
// 合并内容并重新包裹 // 合并内容并重新包裹
const mergedInnerContent = existingInnerContent + newInnerContent; const mergedInnerContent = existingInnerContent + newInnerContent;
return `<div class="message-markdown"> return `<div class="message-markdown">
<div class="markdown-content">${mergedInnerContent}</div> <div class="markdown-content">${mergedInnerContent}</div>
</div>`; </div>`;
...@@ -599,7 +603,7 @@ const processSSEMessage = ( ...@@ -599,7 +603,7 @@ const processSSEMessage = (
case -1: // 错误信息 case -1: // 错误信息
if (updatedResponse) { if (updatedResponse) {
updatedResponse.contentBlocks.push({ updatedResponse.contentBlocks.push({
content: contentTemplates.error(messageContent || ''), content: contentTemplates.error(messageContent || '出错了~~'),
hasThinkBox: false, hasThinkBox: false,
thinkContent: '', thinkContent: '',
thinkBoxExpanded: false, thinkBoxExpanded: false,
...@@ -652,32 +656,32 @@ const processSSEMessage = ( ...@@ -652,32 +656,32 @@ const processSSEMessage = (
} }
} }
break; break;
case 4: // MD格式 case 4: // MD格式
if (updatedResponse) { if (updatedResponse) {
const markdownContent = contentTemplates.markdown(messageContent || ''); const markdownContent = contentTemplates.markdown(messageContent || '');
// 检查最后一个块是否是markdown块 // 检查最后一个块是否是markdown块
if (isLastBlockMarkdown()) { if (isLastBlockMarkdown()) {
// 合并到现有的markdown块 // 合并到现有的markdown块
const lastMarkdownIndex = getLastMarkdownBlockIndex(); const lastMarkdownIndex = getLastMarkdownBlockIndex();
if (lastMarkdownIndex !== -1) { if (lastMarkdownIndex !== -1) {
updatedResponse.contentBlocks[lastMarkdownIndex].content = updatedResponse.contentBlocks[lastMarkdownIndex].content =
mergeMarkdownContent( mergeMarkdownContent(
updatedResponse.contentBlocks[lastMarkdownIndex].content, updatedResponse.contentBlocks[lastMarkdownIndex].content,
markdownContent markdownContent
); );
}
} else {
// 创建新的markdown块
updatedResponse.contentBlocks.push({
content: markdownContent,
hasThinkBox: false,
thinkContent: '',
thinkBoxExpanded: false,
});
} }
} else {
// 创建新的markdown块
updatedResponse.contentBlocks.push({
content: markdownContent,
hasThinkBox: false,
thinkContent: '',
thinkBoxExpanded: false,
});
} }
break; }
break;
default: // 默认处理 default: // 默认处理
updatedResponse.contentBlocks.push({ updatedResponse.contentBlocks.push({
content: contentTemplates.text(messageContent || ''), content: contentTemplates.text(messageContent || ''),
...@@ -962,91 +966,92 @@ onBeforeUnmount(() => { ...@@ -962,91 +966,92 @@ onBeforeUnmount(() => {
isInThinkingMode.value = false; isInThinkingMode.value = false;
}); });
// 处理历史记录数据 // 处理历史记录数据
const processHistoryData = (dataArray: any[]) => { const processHistoryData = (dataArray: any[]) => {
const result: Message[] = []; const result: Message[] = [];
dataArray.forEach((data) => { dataArray.forEach((data) => {
let date = dayjs(data.startTime).format('YYYY-MM-DD HH:mm:ss'); let date = dayjs(data.startTime).format('YYYY-MM-DD HH:mm:ss');
// 处理问题消息 // 处理问题消息
if (data.question || data.audioPath) { if (data.question || data.audioPath) {
let questionContent = ''; let questionContent = '';
// 检查是否为音频消息 // 检查是否为音频消息
if (data.audioPath) { if (data.audioPath) {
// 处理音频消息 // 处理音频消息
questionContent = contentTemplates.audio({ questionContent = contentTemplates.audio({
audioUrl: data.audioPath audioUrl: data.audioPath,
durationTime: data.autioTime || '0"',
});
} else {
// 处理文本消息
questionContent = contentTemplates.text(data.question);
}
result.push({
messageType: 'sent',
avatar: '',
recordId: '',
promptTokens: 0,
completionTokens: 0,
totalTokens: 0,
date,
contentBlocks: [
{
content: questionContent,
thinkContent: '',
hasThinkBox: false,
thinkBoxExpanded: false,
},
],
}); });
} else {
// 处理文本消息
questionContent = contentTemplates.text(data.question);
} }
result.push({ // 处理AI回答消息
messageType: 'sent', if (data.answerInfoList && Array.isArray(data.answerInfoList)) {
avatar: '', const aiMessage: Message = {
recordId: '', messageType: 'received',
promptTokens: 0, avatar: 'AI',
completionTokens: 0, recordId: '',
totalTokens: 0, promptTokens: 0,
date, completionTokens: 0,
contentBlocks: [ totalTokens: 0,
{ contentBlocks: [],
content: questionContent, date,
thinkContent: '',
hasThinkBox: false,
thinkBoxExpanded: false,
},
],
});
}
// 处理AI回答消息
if (data.answerInfoList && Array.isArray(data.answerInfoList)) {
const aiMessage: Message = {
messageType: 'received',
avatar: 'AI',
recordId: '',
promptTokens: 0,
completionTokens: 0,
totalTokens: 0,
contentBlocks: [],
date,
};
let currentThinkingMode = false;
let currentBlockIdx = -1;
// 历史数据处理,isHistoryData设为true,思考框折叠
data.answerInfoList.forEach((answer) => {
const sseData: SSEData = {
message: answer.message || '',
status: answer.status || 0,
type: answer.type || '',
}; };
const processResult = processSSEMessage( let currentThinkingMode = false;
sseData, let currentBlockIdx = -1;
aiMessage,
currentThinkingMode, // 历史数据处理,isHistoryData设为true,思考框折叠
currentBlockIdx, data.answerInfoList.forEach((answer) => {
true, const sseData: SSEData = {
); message: answer.message || '',
status: answer.status || 0,
currentThinkingMode = processResult.updatedIsThinking; type: answer.type || '',
currentBlockIdx = processResult.updatedBlockIndex; };
aiMessage.recordId = processResult.recordId;
aiMessage.promptTokens = processResult.promptTokens; const processResult = processSSEMessage(
aiMessage.completionTokens = processResult.completionTokens; sseData,
aiMessage.totalTokens = processResult.totalTokens; aiMessage,
}); currentThinkingMode,
currentBlockIdx,
true,
);
if (aiMessage.contentBlocks.length > 0) { currentThinkingMode = processResult.updatedIsThinking;
result.push(aiMessage); 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;
}; };
// 获取历史会话消息 // 获取历史会话消息
const getChatRecord = async (dialogSessionId: string) => { const getChatRecord = async (dialogSessionId: string) => {
...@@ -1173,15 +1178,9 @@ const setupAudioPlayers = () => { ...@@ -1173,15 +1178,9 @@ const setupAudioPlayers = () => {
return; return;
} }
// 音频播放结束,重置为总时长 - 已移除 // 音频播放结束,重置状态
audioElement.addEventListener('ended', () => { audioElement.addEventListener('ended', () => {
player.classList.remove('playing'); player.classList.remove('playing');
const playIcon = player.querySelector('.play-icon');
const pauseIcon = player.querySelector('.pause-icon');
if (playIcon && pauseIcon) {
playIcon.style.display = 'inline';
pauseIcon.style.display = 'none';
}
}); });
// 设置播放/暂停事件 // 设置播放/暂停事件
...@@ -1204,23 +1203,11 @@ const setupAudioPlayers = () => { ...@@ -1204,23 +1203,11 @@ const setupAudioPlayers = () => {
// 音频播放事件 // 音频播放事件
audioElement.addEventListener('play', () => { audioElement.addEventListener('play', () => {
player.classList.add('playing'); player.classList.add('playing');
const playIcon = player.querySelector('.play-icon');
const pauseIcon = player.querySelector('.pause-icon');
if (playIcon && pauseIcon) {
playIcon.style.display = 'none';
pauseIcon.style.display = 'inline';
}
}); });
// 音频暂停事件 // 音频暂停事件
audioElement.addEventListener('pause', () => { audioElement.addEventListener('pause', () => {
player.classList.remove('playing'); player.classList.remove('playing');
const playIcon = player.querySelector('.play-icon');
const pauseIcon = player.querySelector('.pause-icon');
if (playIcon && pauseIcon) {
playIcon.style.display = 'inline';
pauseIcon.style.display = 'none';
}
}); });
}); });
}; };
...@@ -1234,16 +1221,10 @@ const pauseAllOtherAudios = (currentAudio: HTMLAudioElement) => { ...@@ -1234,16 +1221,10 @@ const pauseAllOtherAudios = (currentAudio: HTMLAudioElement) => {
const player = audio.closest('.audio-message')?.querySelector('.audio-player'); const player = audio.closest('.audio-message')?.querySelector('.audio-player');
if (player) { if (player) {
player.classList.remove('playing'); player.classList.remove('playing');
const playIcon = player.querySelector('.play-icon');
const pauseIcon = player.querySelector('.pause-icon');
if (playIcon && pauseIcon) {
playIcon.style.display = 'inline';
pauseIcon.style.display = 'none';
}
} }
} }
}); });
}; // 移除多余的括号 };
onBeforeUnmount(() => { onBeforeUnmount(() => {
closeSSE(); closeSSE();
......
...@@ -25,11 +25,6 @@ ...@@ -25,11 +25,6 @@
<span class="pulse"></span> <span class="pulse"></span>
</span> </span>
</button> </button>
<!-- 语音识别状态提示 -->
<div v-if="showStatus" class="voice-status" :class="statusClass">
{{ statusText }}
</div>
</div> </div>
</template> </template>
...@@ -59,19 +54,12 @@ const emit = defineEmits<{ ...@@ -59,19 +54,12 @@ const emit = defineEmits<{
// 响应式数据 // 响应式数据
const isRecording = ref(false) const isRecording = ref(false)
const showStatus = ref(false)
const statusText = ref('')
// MediaRecorder相关 // MediaRecorder相关
const mediaRecorder = ref<MediaRecorder | null>(null) const mediaRecorder = ref<MediaRecorder | null>(null)
const audioChunks = ref<Blob[]>([]) const audioChunks = ref<Blob[]>([])
const audioStream = ref<MediaStream | null>(null) const audioStream = ref<MediaStream | null>(null)
// 计算属性
const statusClass = computed(() => {
return isRecording.value ? 'recording' : 'idle'
})
// 检查浏览器是否支持MediaRecorder // 检查浏览器是否支持MediaRecorder
const isMediaRecorderSupported = () => { const isMediaRecorderSupported = () => {
const supported = 'MediaRecorder' in window; const supported = 'MediaRecorder' in window;
...@@ -81,16 +69,6 @@ const isMediaRecorderSupported = () => { ...@@ -81,16 +69,6 @@ const isMediaRecorderSupported = () => {
return supported; return supported;
} }
// 显示状态消息
const showStatusMessage = (message: string) => {
statusText.value = message
showStatus.value = true
setTimeout(() => {
showStatus.value = false
}, 3000)
}
// 检查麦克风权限 // 检查麦克风权限
const checkMicrophonePermission = async () => { const checkMicrophonePermission = async () => {
try { try {
...@@ -101,7 +79,6 @@ const checkMicrophonePermission = async () => { ...@@ -101,7 +79,6 @@ const checkMicrophonePermission = async () => {
} }
if (permissionStatus.state === 'denied') { if (permissionStatus.state === 'denied') {
showStatusMessage('麦克风权限被拒绝,请在浏览器设置中允许访问');
return false; return false;
} }
...@@ -114,25 +91,6 @@ const checkMicrophonePermission = async () => { ...@@ -114,25 +91,6 @@ const checkMicrophonePermission = async () => {
} }
} }
// 显示权限引导提示
const showPermissionGuide = () => {
const guideMessage = `
麦克风权限被拒绝,请按以下步骤操作:
1. 点击浏览器地址栏左侧的"锁形图标"或"不安全"标识
2. 选择"网站设置"
3. 找到"麦克风"权限,选择"允许"
4. 刷新页面后重试
`;
showStatusMessage('麦克风权限被拒绝,请检查浏览器设置');
// 在调试模式下显示详细引导
if (props.debug) {
console.warn('麦克风权限被拒绝,用户需要手动授权');
console.log(guideMessage);
}
}
// 开始录音 // 开始录音
const startRecording = async () => { const startRecording = async () => {
if (props.disabled || isRecording.value) return if (props.disabled || isRecording.value) return
...@@ -140,14 +98,13 @@ const startRecording = async () => { ...@@ -140,14 +98,13 @@ const startRecording = async () => {
// 检查权限 // 检查权限
const hasPermission = await checkMicrophonePermission(); const hasPermission = await checkMicrophonePermission();
if (!hasPermission) { if (!hasPermission) {
showPermissionGuide(); emit('error', '麦克风权限被拒绝');
return; return;
} }
// 检查浏览器支持 // 检查浏览器支持
if (!isMediaRecorderSupported()) { if (!isMediaRecorderSupported()) {
const errorMsg = '您的浏览器不支持音频录制功能'; const errorMsg = '您的浏览器不支持音频录制功能';
showStatusMessage(errorMsg);
emit('error', errorMsg); emit('error', errorMsg);
return; return;
} }
...@@ -185,7 +142,6 @@ const startRecording = async () => { ...@@ -185,7 +142,6 @@ const startRecording = async () => {
// 开始录制 // 开始录制
mediaRecorder.value.start(100); // 每100ms收集一次数据 mediaRecorder.value.start(100); // 每100ms收集一次数据
isRecording.value = true; isRecording.value = true;
showStatusMessage('正在录音...');
// 通知父组件开始录音 // 通知父组件开始录音
emit('recordingStart'); emit('recordingStart');
...@@ -200,12 +156,10 @@ const startRecording = async () => { ...@@ -200,12 +156,10 @@ const startRecording = async () => {
if (error && error.name === 'NotAllowedError') { if (error && error.name === 'NotAllowedError') {
errorMessage = '麦克风权限被拒绝'; errorMessage = '麦克风权限被拒绝';
showPermissionGuide();
} else if (error && error.name === 'NotFoundError') { } else if (error && error.name === 'NotFoundError') {
errorMessage = '未找到麦克风设备'; errorMessage = '未找到麦克风设备';
} }
showStatusMessage(errorMessage);
emit('error', errorMessage); emit('error', errorMessage);
} }
} }
...@@ -234,7 +188,7 @@ const stopRecording = () => { ...@@ -234,7 +188,7 @@ const stopRecording = () => {
} }
// 上传音频文件到服务器 - 修改为使用axios // 上传音频文件到服务器 - 修改为使用axios
const uploadAudioFile = async (audioBlob: Blob): Promise<string> => { const uploadAudioFile = async (audioBlob: Blob): Promise<{filePath: string, durationTime: number}> => {
try { try {
const formData = new FormData(); const formData = new FormData();
formData.append('file', audioBlob, 'recording.wav'); formData.append('file', audioBlob, 'recording.wav');
...@@ -250,7 +204,9 @@ const uploadAudioFile = async (audioBlob: Blob): Promise<string> => { ...@@ -250,7 +204,9 @@ const uploadAudioFile = async (audioBlob: Blob): Promise<string> => {
if (result.data.code === 0) { if (result.data.code === 0) {
const filePath = result.data.data.filePath; const filePath = result.data.data.filePath;
return filePath; // 计算音频时长(秒),四舍五入取整
const durationTime = result.data.data.durationTime;
return {filePath,durationTime};
} else { } else {
throw new Error('上传接口返回数据格式错误'); throw new Error('上传接口返回数据格式错误');
} }
...@@ -263,30 +219,27 @@ const uploadAudioFile = async (audioBlob: Blob): Promise<string> => { ...@@ -263,30 +219,27 @@ const uploadAudioFile = async (audioBlob: Blob): Promise<string> => {
// 发送录制的音频 // 发送录制的音频
const sendRecordedAudio = async () => { const sendRecordedAudio = async () => {
if (audioChunks.value.length === 0) { if (audioChunks.value.length === 0) {
showStatusMessage('录音数据为空'); emit('error', '录音数据为空');
return; return;
} }
const audioBlob = new Blob(audioChunks.value, { type: 'audio/webm;codecs=opus' }); const audioBlob = new Blob(audioChunks.value, { type: 'audio/webm;codecs=opus' });
try { try {
showStatusMessage('正在上传音频...');
// 先调用上传接口获取URL // 先调用上传接口获取URL
const audioUrl = await uploadAudioFile(audioBlob); const {filePath,durationTime} = await uploadAudioFile(audioBlob);
// 上传成功后触发audio事件,传递URL和Blob // 上传成功后触发audio事件,传递URL和Blob
emit('audio', audioUrl, audioBlob); emit('audio', filePath, audioBlob, durationTime);
showStatusMessage('音频已发送');
if (props.debug) { if (props.debug) {
console.log('音频上传成功,URL:', audioUrl); console.log('音频上传成功,URL:', filePath);
console.log('音频发送完成,大小:', Math.round(audioBlob.size / 1024), 'KB'); console.log('音频发送完成,大小:', Math.round(audioBlob.size / 1024), 'KB');
console.log('音频时长:', durationTime, '');
} }
} catch (error) { } catch (error) {
console.error('音频上传失败:', error); console.error('音频上传失败:', error);
const errorMsg = '音频上传失败,请重试'; const errorMsg = '音频上传失败,请重试';
showStatusMessage(errorMsg);
emit('error', errorMsg); emit('error', errorMsg);
} finally { } finally {
// 清理录音数据 // 清理录音数据
...@@ -390,25 +343,6 @@ defineExpose({ ...@@ -390,25 +343,6 @@ defineExpose({
} }
} }
.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 { @keyframes pulse {
0% { 0% {
opacity: 1; opacity: 1;
......
...@@ -27,7 +27,7 @@ ...@@ -27,7 +27,7 @@
padding: 0; padding: 0;
margin: 0; margin: 0;
box-sizing: border-box; box-sizing: border-box;
// Webkit浏览器滚动条样式(Chrome, Safari, Edge) // Webkit浏览器滚动条样式(Chrome, Safari, Edge)
&::-webkit-scrollbar { &::-webkit-scrollbar {
width: 0px; // 垂直滚动条宽度 width: 0px; // 垂直滚动条宽度
...@@ -36,7 +36,16 @@ ...@@ -36,7 +36,16 @@
} }
// 重置基础元素样式 // 重置基础元素样式
p, h1, h2, h3, h4, h5, h6, ul, ol, li { p,
h1,
h2,
h3,
h4,
h5,
h6,
ul,
ol,
li {
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
...@@ -54,7 +63,7 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li { ...@@ -54,7 +63,7 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100vh; height: 100vh;
// 居中介绍页面样式 // 居中介绍页面样式
.chat-intro-center { .chat-intro-center {
flex: 1; flex: 1;
...@@ -62,33 +71,33 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li { ...@@ -62,33 +71,33 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 40px 20px; padding: 40px 20px;
.intro-content { .intro-content {
text-align: center; text-align: center;
max-width: 400px; max-width: 400px;
width: 100%; width: 100%;
.avatar-image { .avatar-image {
width: 180px; width: 180px;
height: 180px; height: 180px;
border-radius: 50%; border-radius: 50%;
margin-bottom: 20px; margin-bottom: 20px;
} }
h3 { h3 {
font-size: 24px; font-size: 24px;
color: @gray-7; color: @gray-7;
margin-bottom: 12px; margin-bottom: 12px;
font-weight: 600; font-weight: 600;
} }
p { p {
font-size: 16px; font-size: 16px;
color: @gray-6; color: @gray-6;
line-height: 1.5; line-height: 1.5;
margin-bottom: 30px; margin-bottom: 30px;
} }
.start-chat-btn { .start-chat-btn {
background: linear-gradient(135deg, @primary-color 0%, @primary-light 100%); background: linear-gradient(135deg, @primary-color 0%, @primary-light 100%);
color: @white; color: @white;
...@@ -100,24 +109,24 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li { ...@@ -100,24 +109,24 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li {
cursor: pointer; cursor: pointer;
transition: all 0.3s ease; transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(91, 138, 254, 0.3); box-shadow: 0 4px 15px rgba(91, 138, 254, 0.3);
&:hover { &:hover {
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(91, 138, 254, 0.4); box-shadow: 0 6px 20px rgba(91, 138, 254, 0.4);
} }
&:active { &:active {
transform: translateY(0); transform: translateY(0);
} }
} }
} }
} }
// 聊天头部样式 // 聊天头部样式
.chat-header { .chat-header {
display: flex; display: flex;
align-items: center; align-items: center;
.header-avatar { .header-avatar {
width: 45px; width: 45px;
height: 45px; height: 45px;
...@@ -129,28 +138,29 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li { ...@@ -129,28 +138,29 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li {
font-size: 20px; font-size: 20px;
margin-right: 15px; margin-right: 15px;
border: 2px solid rgba(255, 255, 255, 0.3); border: 2px solid rgba(255, 255, 255, 0.3);
img { img {
width: 100%; width: 100%;
height: auto; height: auto;
} }
} }
.header-info { .header-info {
flex: 1; flex: 1;
h2 { h2 {
font-size: 18px; font-size: 18px;
} }
} }
} }
// 消息区域 // 消息区域
.chat-messages { .chat-messages {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
padding: 12px; padding: 12px;
} }
// 输入容器 // 输入容器
.chat-input-container { .chat-input-container {
padding: 20px; padding: 20px;
...@@ -175,19 +185,19 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li { ...@@ -175,19 +185,19 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li {
margin-bottom: 20px; margin-bottom: 20px;
flex-direction: column; flex-direction: column;
align-items: baseline; align-items: baseline;
&.sent { &.sent {
align-items: flex-end; align-items: flex-end;
.message-time { .message-time {
text-align: right; text-align: right;
} }
.avatar-container { .avatar-container {
flex-direction: row-reverse; flex-direction: row-reverse;
justify-content: flex-end; justify-content: flex-end;
} }
} }
} }
.avatar-container { .avatar-container {
...@@ -209,7 +219,7 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li { ...@@ -209,7 +219,7 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li {
color: @white; color: @white;
border: 2px solid @white; border: 2px solid @white;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
img { img {
width: 100%; width: 100%;
height: auto; height: auto;
...@@ -229,7 +239,7 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li { ...@@ -229,7 +239,7 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li {
max-width: 100%; max-width: 100%;
margin-top: 8px; margin-top: 8px;
min-width: 150px; min-width: 150px;
// 当包含图表、表格或iframe时,宽度为100% // 当包含图表、表格或iframe时,宽度为100%
&:has(.message-table), &:has(.message-table),
&:has(.message-chart), &:has(.message-chart),
...@@ -247,10 +257,10 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li { ...@@ -247,10 +257,10 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li {
position: relative; position: relative;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05); box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
white-space: pre-wrap; white-space: pre-wrap;
.message-inner-box { .message-inner-box {
font-size: 0; font-size: 0;
:deep(.message-text) { :deep(.message-text) {
font-size: 14px; font-size: 14px;
} }
...@@ -313,10 +323,10 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li { ...@@ -313,10 +323,10 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease; transition: all 0.3s ease;
animation: fadeIn 0.3s ease-in-out; animation: fadeIn 0.3s ease-in-out;
.think-content { .think-content {
font-size: 0; font-size: 0;
.think-line { .think-line {
font-size: 13px; font-size: 13px;
color: @gray-7; color: @gray-7;
...@@ -333,7 +343,7 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li { ...@@ -333,7 +343,7 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li {
display: flex; display: flex;
align-items: flex-end; align-items: flex-end;
position: relative; position: relative;
// 语音识别按钮容器 - 移动到右边 // 语音识别按钮容器 - 移动到右边
.voice-recognition-wrapper { .voice-recognition-wrapper {
position: absolute; position: absolute;
...@@ -342,7 +352,7 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li { ...@@ -342,7 +352,7 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li {
transform: translateY(-50%); transform: translateY(-50%);
z-index: 10; z-index: 10;
} }
textarea { textarea {
flex: 1; flex: 1;
padding: 14px 110px 14px 16px; // 调整内边距:右侧为两个按钮留空间,左侧恢复正常 padding: 14px 110px 14px 16px; // 调整内边距:右侧为两个按钮留空间,左侧恢复正常
...@@ -357,12 +367,12 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li { ...@@ -357,12 +367,12 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li {
overflow: hidden; overflow: hidden;
position: relative; position: relative;
box-shadow: 0px 2px 8px 0px rgba(0, 0, 0, 0.12); box-shadow: 0px 2px 8px 0px rgba(0, 0, 0, 0.12);
&:focus { &:focus {
border-color: @primary-color; border-color: @primary-color;
box-shadow: 0 0 0 2px rgba(91, 138, 254, 0.1); box-shadow: 0 0 0 2px rgba(91, 138, 254, 0.1);
} }
&:disabled { &:disabled {
background-color: @gray-2; background-color: @gray-2;
border-color: @gray-3; border-color: @gray-3;
...@@ -370,7 +380,7 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li { ...@@ -370,7 +380,7 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li {
cursor: not-allowed; cursor: not-allowed;
} }
} }
button { button {
position: absolute; position: absolute;
right: 12px; right: 12px;
...@@ -388,12 +398,12 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li { ...@@ -388,12 +398,12 @@ 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%);
&: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);
} }
&:disabled { &:disabled {
color: @gray-4; color: @gray-4;
border-color: @gray-4; border-color: @gray-4;
...@@ -406,11 +416,11 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li { ...@@ -406,11 +416,11 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li {
.operation-box { .operation-box {
margin-top: 6px; margin-top: 6px;
p { p {
color: @gray-5; color: @gray-5;
font-size: 12px; font-size: 12px;
span { span {
margin-right: 15px; margin-right: 15px;
} }
...@@ -425,48 +435,49 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li { ...@@ -425,48 +435,49 @@ p, h1, h2, h3, h4, h5, h6, ul, ol, li {
width: 100%; width: 100%;
max-width: 100%; max-width: 100%;
margin: 8px 0; margin: 8px 0;
// 表格容器 // 表格容器
.table-container { .table-container {
width: 100%; width: 100%;
overflow-x: auto; overflow-x: auto;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
border-radius: 8px; border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
// 滚动条样式 // 滚动条样式
&::-webkit-scrollbar { &::-webkit-scrollbar {
height: 8px; height: 8px;
} }
&::-webkit-scrollbar-track { &::-webkit-scrollbar-track {
background: #f1f1f1; background: #f1f1f1;
border-radius: 4px; border-radius: 4px;
} }
&::-webkit-scrollbar-thumb { &::-webkit-scrollbar-thumb {
background: #c1c1c1; background: #c1c1c1;
border-radius: 4px; border-radius: 4px;
&:hover {
&:hover {
background: #a8a8a8; background: #a8a8a8;
} }
} }
} }
.data-table { .data-table {
width: auto; width: auto;
min-width: 100%; min-width: 100%;
border-collapse: collapse; border-collapse: collapse;
background-color: @white; background-color: @white;
table-layout: auto; table-layout: auto;
// 列类型样式 // 列类型样式
.text-cell { .text-cell {
text-align: left; text-align: left;
padding-left: 12px; padding-left: 12px;
padding-right: 8px; padding-right: 8px;
} }
.numeric-cell { .numeric-cell {
text-align: right; text-align: right;
padding-left: 8px; padding-left: 8px;
...@@ -474,13 +485,13 @@ background-color: @white; ...@@ -474,13 +485,13 @@ background-color: @white;
font-family: 'Courier New', monospace; font-family: 'Courier New', monospace;
font-weight: 500; font-weight: 500;
} }
.trend-cell { .trend-cell {
text-align: center; text-align: center;
padding-left: 8px; padding-left: 8px;
padding-right: 8px; padding-right: 8px;
} }
th { th {
background: linear-gradient(135deg, @primary-color 0%, @primary-hover 100%); background: linear-gradient(135deg, @primary-color 0%, @primary-hover 100%);
color: @white; color: @white;
...@@ -493,12 +504,12 @@ background-color: @white; ...@@ -493,12 +504,12 @@ background-color: @white;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
min-width: 80px; min-width: 80px;
&:last-child { &:last-child {
border-right: none; border-right: none;
} }
} }
td { td {
padding: 10px 8px; padding: 10px 8px;
font-size: 14px; font-size: 14px;
...@@ -507,47 +518,47 @@ background-color: @white; ...@@ -507,47 +518,47 @@ background-color: @white;
height: 35px; height: 35px;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
vertical-align: middle; vertical-align: middle;
min-width: 80px; min-width: 80px;
} }
// 奇偶行样式 // 奇偶行样式
tr:nth-child(odd) td { tr:nth-child(odd) td {
background-color: @blue-light-2; background-color: @blue-light-2;
} }
tr:nth-child(even) td { tr:nth-child(even) td {
background-color: @white; background-color: @white;
} }
tr:hover td { tr:hover td {
background-color: @blue-light-1; background-color: @blue-light-1;
} }
tr:last-child td { tr:last-child td {
border-bottom: none; border-bottom: none;
} }
} }
// 趋势箭头样式 // 趋势箭头样式
.trend-up { .trend-up {
color: @success-color; color: @success-color;
font-weight: bold; font-weight: bold;
font-size: 16px; font-size: 16px;
} }
.trend-down { .trend-down {
color: @error-color; color: @error-color;
font-weight: bold; font-weight: bold;
font-size: 16px; font-size: 16px;
} }
.table-footer { .table-footer {
margin-top: 12px; margin-top: 12px;
font-size: 14px; font-size: 14px;
span { span {
color: #2eb0a1; color: #2eb0a1;
font-weight: bold; font-weight: bold;
} }
...@@ -562,14 +573,14 @@ text-overflow: ellipsis; ...@@ -562,14 +573,14 @@ text-overflow: ellipsis;
width: 100%; width: 100%;
max-width: 100%; max-width: 100%;
margin: 8px 0; margin: 8px 0;
border-radius: 8px; border-radius: 8px;
background-color: @white; background-color: @white;
border: 1px solid @blue-light-3; border: 1px solid @blue-light-3;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
overflow: hidden; overflow: hidden;
position: relative; position: relative;
min-height: 300px; min-height: 300px;
// iframe提示信息样式 // iframe提示信息样式
.iframe-tips { .iframe-tips {
padding: 12px 16px; padding: 12px 16px;
...@@ -580,7 +591,7 @@ border-radius: 8px; ...@@ -580,7 +591,7 @@ border-radius: 8px;
font-weight: 500; font-weight: 500;
line-height: 1.4; line-height: 1.4;
} }
// iframe标题样式 // iframe标题样式
.iframe-title { .iframe-title {
padding: 8px 16px; padding: 8px 16px;
...@@ -591,7 +602,7 @@ border-radius: 8px; ...@@ -591,7 +602,7 @@ border-radius: 8px;
font-weight: 600; font-weight: 600;
line-height: 1.4; line-height: 1.4;
} }
iframe { iframe {
width: 100%; width: 100%;
height: 100%; height: 100%;
...@@ -600,7 +611,7 @@ border-radius: 8px; ...@@ -600,7 +611,7 @@ border-radius: 8px;
border-radius: 8px; border-radius: 8px;
background-color: @gray-1; background-color: @gray-1;
transition: height 0.5s ease-in-out, opacity 0.3s ease; transition: height 0.5s ease-in-out, opacity 0.3s ease;
// 加载状态样式 // 加载状态样式
&[src=""] { &[src=""] {
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
...@@ -608,15 +619,15 @@ border-radius: 8px; ...@@ -608,15 +619,15 @@ border-radius: 8px;
animation: loading 1.5s infinite; animation: loading 1.5s infinite;
} }
} }
// 加载状态 // 加载状态
&.iframe-loading { &.iframe-loading {
iframe { iframe {
opacity: 0; opacity: 0;
pointer-events: none; pointer-events: none;
min-height: 400px; min-height: 400px;
} }
.iframe-loading { .iframe-loading {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
...@@ -625,24 +636,24 @@ pointer-events: none; ...@@ -625,24 +636,24 @@ pointer-events: none;
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
z-index: 10; z-index: 10;
} }
} }
// 加载完成状态 // 加载完成状态
&.iframe-loaded { &.iframe-loaded {
iframe { iframe {
opacity: 1; opacity: 1;
} }
.iframe-loading { .iframe-loading {
display: none; display: none;
} }
} }
// 加载动画 // 加载动画
.loading-spinner { .loading-spinner {
width: 40px; width: 40px;
...@@ -653,21 +664,21 @@ width: 100%; ...@@ -653,21 +664,21 @@ width: 100%;
animation: spin 1s linear infinite; animation: spin 1s linear infinite;
margin-bottom: 16px; margin-bottom: 16px;
} }
.loading-text { .loading-text {
font-size: 16px; font-size: 16px;
color: @gray-6; color: @gray-6;
margin-bottom: 12px; margin-bottom: 12px;
font-weight: 500; font-weight: 500;
} }
.loading-progress { .loading-progress {
width: 200px; width: 200px;
height: 4px; height: 4px;
background: @gray-3; background: @gray-3;
border-radius: 2px; border-radius: 2px;
overflow: hidden; overflow: hidden;
.progress-bar { .progress-bar {
height: 100%; height: 100%;
background: linear-gradient(90deg, @primary-color, @primary-light); background: linear-gradient(90deg, @primary-color, @primary-light);
...@@ -685,6 +696,7 @@ width: 100%; ...@@ -685,6 +696,7 @@ width: 100%;
0% { 0% {
background-position: 200% 0; background-position: 200% 0;
} }
100% { 100% {
background-position: -200% 0; background-position: -200% 0;
} }
...@@ -694,6 +706,7 @@ width: 100%; ...@@ -694,6 +706,7 @@ width: 100%;
0% { 0% {
transform: rotate(0deg); transform: rotate(0deg);
} }
100% { 100% {
transform: rotate(360deg); transform: rotate(360deg);
} }
...@@ -703,12 +716,14 @@ width: 100%; ...@@ -703,12 +716,14 @@ width: 100%;
0% { 0% {
transform: translateX(-100%); transform: translateX(-100%);
} }
50% { 50% {
transform: translateX(200%); transform: translateX(200%);
} }
100% { 100% {
transform: translateX(200%); transform: translateX(200%);
} }
} }
// ============================================= // =============================================
...@@ -719,20 +734,21 @@ width: 100%; ...@@ -719,20 +734,21 @@ width: 100%;
:deep(.message-table) { :deep(.message-table) {
.data-table { .data-table {
font-size: 12px; font-size: 12px;
th, td { th,
td {
padding: 8px 4px; padding: 8px 4px;
height: 30px; height: 30px;
min-width: 60px; min-width: 60px;
} }
} }
.table-title { .table-title {
font-size: 14px; font-size: 14px;
} }
.table-summary { .table-summary {
font-size: 12px; font-size: 12px;
padding: 8px 10px; padding: 8px 10px;
} }
} }
...@@ -741,7 +757,9 @@ font-size: 12px; ...@@ -741,7 +757,9 @@ font-size: 12px;
@media (max-width: 480px) { @media (max-width: 480px) {
:deep(.message-table) { :deep(.message-table) {
.data-table { .data-table {
th, td {
th,
td {
min-width: 50px; min-width: 50px;
height: 28px; height: 28px;
} }
...@@ -769,7 +787,7 @@ font-size: 12px; ...@@ -769,7 +787,7 @@ font-size: 12px;
border: 1px solid @blue-light-3; border: 1px solid @blue-light-3;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
overflow: hidden; overflow: hidden;
.options-tips { .options-tips {
padding: 12px 16px; padding: 12px 16px;
background-color: @blue-light-2; background-color: @blue-light-2;
...@@ -779,44 +797,44 @@ font-size: 12px; ...@@ -779,44 +797,44 @@ font-size: 12px;
font-weight: 500; font-weight: 500;
line-height: 1.4; line-height: 1.4;
} }
.options-list { .options-list {
padding: 8px 0; padding: 8px 0;
} }
.option-item { .option-item {
padding: 8px 16px; padding: 8px 16px;
border-bottom: 1px solid @gray-2; border-bottom: 1px solid @gray-2;
transition: background-color 0.2s ease; transition: background-color 0.2s ease;
&:last-child { &:last-child {
border-bottom: none; border-bottom: none;
} }
&:hover { &:hover {
background-color: @blue-light-1; background-color: @blue-light-1;
} }
.option-content { .option-content {
line-height: 1.4; line-height: 1.4;
.option-number-title { .option-number-title {
font-size: 14px; font-size: 14px;
color: @gray-7; color: @gray-7;
font-weight: 500; font-weight: 500;
// 序号部分特殊样式 // 序号部分特殊样式
&::before { &::before {
content: ''; content: '';
display: inline; display: inline;
} }
} }
.option-number-title:first-letter { .option-number-title:first-letter {
color: @primary-color; color: @primary-color;
font-weight: 600; font-weight: 600;
} }
.option-url { .option-url {
font-size: 12px; font-size: 12px;
color: @gray-5; color: @gray-5;
...@@ -837,7 +855,7 @@ font-size: 12px; ...@@ -837,7 +855,7 @@ font-size: 12px;
.option-number-title { .option-number-title {
font-size: 14px; font-size: 14px;
} }
.option-url { .option-url {
font-size: 11px; font-size: 11px;
max-width: 100%; max-width: 100%;
...@@ -847,14 +865,15 @@ font-size: 12px; ...@@ -847,14 +865,15 @@ font-size: 12px;
} }
} }
// 音频消息样式 - 白色主题,无背景色 // 音频消息样式 - 简化版本,移除audio-icon
:deep(.audio-message) { :deep(.audio-message) {
display: inline-block; display: inline-block;
width: -webkit-fill-available; width: -webkit-fill-available;
audio { audio {
display: none; // 隐藏原生音频控件 display: none; // 隐藏原生音频控件
} }
.audio-player { .audio-player {
display: flex; display: flex;
align-items: center; align-items: center;
...@@ -862,56 +881,70 @@ font-size: 12px; ...@@ -862,56 +881,70 @@ font-size: 12px;
transition: all 0.3s ease; transition: all 0.3s ease;
user-select: none; user-select: none;
box-sizing: border-box; box-sizing: border-box;
padding: 4px 12px;
border-radius: 20px;
&.playing { &.playing {
.audio-wave .wave-bar { .audio-wave .wave-bar {
animation: waveAnimation 1.2s ease-in-out infinite; animation: waveAnimation 1.2s ease-in-out infinite;
&:nth-child(1) { animation-delay: 0s; } &:nth-child(1) {
&:nth-child(2) { animation-delay: 0.2s; } animation-delay: 0s;
&:nth-child(3) { animation-delay: 0.4s; } }
&:nth-child(4) { animation-delay: 0.6s; }
&:nth-child(5) { animation-delay: 0.8s; } &:nth-child(2) {
} animation-delay: 0.2s;
} }
.audio-icon { &:nth-child(3) {
width: 24px; animation-delay: 0.4s;
height: 24px; }
display: flex;
align-items: center; &:nth-child(4) {
justify-content: center; animation-delay: 0.6s;
margin-right: 8px; }
color: #ffffff; // 白色图标
&:nth-child(5) {
.play-icon, .pause-icon { animation-delay: 0.8s;
font-size: 12px; }
font-weight: bold;
} }
} }
.audio-wave { .audio-wave {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 2px; gap: 2px;
margin-right: 8px; margin-right: 8px;
.wave-bar { .wave-bar {
width: 2px; width: 2px;
height: 12px; height: 12px;
background: #ffffff; // 白色波形条 background: #ffffff; // 白色波形条
border-radius: 1px; border-radius: 1px;
transition: all 0.3s ease; transition: all 0.3s ease;
&:nth-child(1) { height: 4px; } &:nth-child(1) {
&:nth-child(2) { height: 8px; } height: 4px;
&:nth-child(3) { height: 12px; } }
&:nth-child(4) { height: 8px; }
&:nth-child(5) { height: 4px; } &:nth-child(2) {
height: 8px;
}
&:nth-child(3) {
height: 12px;
}
&:nth-child(4) {
height: 8px;
}
&:nth-child(5) {
height: 4px;
}
} }
} }
.audio-duration { .audio-duration {
font-size: 12px; font-size: 12px;
color: #ffffff; // 白色时长文字 color: #ffffff; // 白色时长文字
...@@ -922,10 +955,13 @@ font-size: 12px; ...@@ -922,10 +955,13 @@ font-size: 12px;
} }
@keyframes waveAnimation { @keyframes waveAnimation {
0%, 100% {
0%,
100% {
transform: scaleY(0.3); transform: scaleY(0.3);
opacity: 0.5; opacity: 0.5;
} }
50% { 50% {
transform: scaleY(1); transform: scaleY(1);
opacity: 1; opacity: 1;
...@@ -945,42 +981,47 @@ font-size: 12px; ...@@ -945,42 +981,47 @@ font-size: 12px;
border: 1px solid @blue-light-3; border: 1px solid @blue-light-3;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
overflow: hidden; overflow: hidden;
.markdown-content { .markdown-content {
padding: 12px 16px; padding: 12px 16px;
font-size: 14px; font-size: 14px;
line-height: 1.6; line-height: 1.6;
color: @gray-7; color: @gray-7;
background: none; background: none;
// 标题样式 // 标题样式
h1, h2, h3, h4, h5, h6 { h1,
h2,
h3,
h4,
h5,
h6 {
margin-bottom: 8px; margin-bottom: 8px;
font-weight: 600; font-weight: 600;
color: @gray-7; color: @gray-7;
line-height: 1.3; line-height: 1.3;
background: none; background: none;
} }
h1 { h1 {
font-size: 20px; font-size: 20px;
} }
h2 { h2 {
font-size: 18px; font-size: 18px;
} }
h3 { h3 {
font-size: 16px; font-size: 16px;
} }
// 段落样式 // 段落样式
p { p {
text-align: justify; text-align: justify;
background: none; background: none;
margin-bottom: 8px; margin-bottom: 8px;
} }
// 链接样式 // 链接样式
a { a {
color: @primary-color; color: @primary-color;
...@@ -988,13 +1029,13 @@ font-size: 12px; ...@@ -988,13 +1029,13 @@ font-size: 12px;
border-bottom: 1px solid transparent; border-bottom: 1px solid transparent;
transition: all 0.2s ease; transition: all 0.2s ease;
background: none; background: none;
&:hover { &:hover {
color: @primary-hover; color: @primary-hover;
border-bottom-color: @primary-hover; border-bottom-color: @primary-hover;
} }
} }
// 代码样式 // 代码样式
code { code {
background-color: @gray-2; background-color: @gray-2;
...@@ -1004,7 +1045,7 @@ font-size: 12px; ...@@ -1004,7 +1045,7 @@ font-size: 12px;
font-size: 13px; font-size: 13px;
color: @error-color; color: @error-color;
} }
pre { pre {
background-color: @gray-1; background-color: @gray-1;
border: 1px solid @gray-3; border: 1px solid @gray-3;
...@@ -1012,7 +1053,7 @@ font-size: 12px; ...@@ -1012,7 +1053,7 @@ font-size: 12px;
padding: 12px; padding: 12px;
overflow-x: auto; overflow-x: auto;
margin-bottom: 8px; margin-bottom: 8px;
code { code {
background: none; background: none;
padding: 0; padding: 0;
...@@ -1021,32 +1062,33 @@ font-size: 12px; ...@@ -1021,32 +1062,33 @@ font-size: 12px;
line-height: 1.4; line-height: 1.4;
} }
} }
// 列表样式 // 列表样式
ul, ol { ul,
ol {
padding-left: 24px; padding-left: 24px;
margin-bottom: 8px; margin-bottom: 8px;
} }
ul { ul {
list-style-type: disc; list-style-type: disc;
} }
ol { ol {
list-style-type: decimal; list-style-type: decimal;
} }
// 粗体和斜体 // 粗体和斜体
strong { strong {
font-weight: 600; font-weight: 600;
color: @gray-7; color: @gray-7;
} }
em { em {
font-style: italic; font-style: italic;
color: @gray-6; color: @gray-6;
} }
// 图片样式 // 图片样式
img { img {
max-width: 100%; max-width: 100%;
...@@ -1054,7 +1096,7 @@ font-size: 12px; ...@@ -1054,7 +1096,7 @@ font-size: 12px;
border-radius: 4px; border-radius: 4px;
margin-bottom: 8px; margin-bottom: 8px;
} }
// 引用块样式 // 引用块样式
blockquote { blockquote {
border-left: 4px solid @primary-color; border-left: 4px solid @primary-color;
...@@ -1064,24 +1106,25 @@ font-size: 12px; ...@@ -1064,24 +1106,25 @@ font-size: 12px;
color: @gray-6; color: @gray-6;
background: none; background: none;
} }
// 表格样式(如果Markdown中包含表格) // 表格样式(如果Markdown中包含表格)
table { table {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
margin-bottom: 8px; margin-bottom: 8px;
th, td { th,
td {
padding: 8px 12px; padding: 8px 12px;
border: 1px solid @gray-3; border: 1px solid @gray-3;
text-align: left; text-align: left;
} }
th { th {
background-color: @blue-light-2; background-color: @blue-light-2;
font-weight: 600; font-weight: 600;
} }
tr:nth-child(even) { tr:nth-child(even) {
background-color: @gray-1; background-color: @gray-1;
} }
...@@ -1095,22 +1138,22 @@ font-size: 12px; ...@@ -1095,22 +1138,22 @@ font-size: 12px;
.markdown-content { .markdown-content {
font-size: 13px; font-size: 13px;
padding: 10px 12px; padding: 10px 12px;
h1 { h1 {
font-size: 18px; font-size: 18px;
} }
h2 { h2 {
font-size: 16px; font-size: 16px;
} }
h3 { h3 {
font-size: 15px; font-size: 15px;
} }
pre { pre {
padding: 8px; padding: 8px;
code { code {
font-size: 12px; font-size: 12px;
} }
...@@ -1124,7 +1167,7 @@ font-size: 12px; ...@@ -1124,7 +1167,7 @@ font-size: 12px;
display: flex; display: flex;
align-items: flex-end; align-items: flex-end;
gap: 8px; gap: 8px;
textarea { textarea {
flex: 1; flex: 1;
padding: 14px 110px 14px 16px; // 调整内边距:右侧为两个按钮留空间,左侧恢复正常 padding: 14px 110px 14px 16px; // 调整内边距:右侧为两个按钮留空间,左侧恢复正常
...@@ -1139,12 +1182,12 @@ font-size: 12px; ...@@ -1139,12 +1182,12 @@ font-size: 12px;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
box-shadow: 0px 2px 8px 0px rgba(0, 0, 0, 0.12); box-shadow: 0px 2px 8px 0px rgba(0, 0, 0, 0.12);
&:focus { &:focus {
border-color: @primary-color; border-color: @primary-color;
box-shadow: 0 0 0 2px rgba(91, 138, 254, 0.1); box-shadow: 0 0 0 2px rgba(91, 138, 254, 0.1);
} }
&:disabled { &:disabled {
background-color: @gray-2; background-color: @gray-2;
border-color: @gray-3; border-color: @gray-3;
...@@ -1152,7 +1195,7 @@ font-size: 12px; ...@@ -1152,7 +1195,7 @@ font-size: 12px;
cursor: not-allowed; cursor: not-allowed;
} }
} }
// 语音识别按钮样式 // 语音识别按钮样式
.voice-recognition { .voice-recognition {
margin: 0; margin: 0;
...@@ -1161,21 +1204,25 @@ font-size: 12px; ...@@ -1161,21 +1204,25 @@ font-size: 12px;
.operation-box { .operation-box {
margin-top: 6px; margin-top: 6px;
p { p {
color: @gray-5; color: @gray-5;
font-size: 12px; font-size: 12px;
span { span {
margin-right: 15px; margin-right: 15px;
} }
} }
} }
@keyframes waveAnimation { @keyframes waveAnimation {
0%, 100% {
0%,
100% {
transform: scaleY(0.3); transform: scaleY(0.3);
opacity: 0.5; opacity: 0.5;
} }
50% { 50% {
transform: scaleY(1); transform: scaleY(1);
opacity: 1; opacity: 1;
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment