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

feat:语音时长调试

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