Commit 54a022a9 authored by 水玉婷's avatar 水玉婷
Browse files

feat:md添加图片,优化iframe高度自动获取

parent 5e75750d
......@@ -40,8 +40,8 @@
stage: 'wechat-demo',
};
const dialogSessionId = '20251127180914709-00043912';
// const dialogSessionId = '';
// const dialogSessionId = '20251127180914709-00043912';
const dialogSessionId = '';
const detailData = ref({
title: '国械小智',
});
......
......@@ -106,7 +106,6 @@ import VoiceRecognition from './VoiceRecognition.vue'; // 导入语音识别组
import AudioPlayer from './AudioPlayer.vue'; // 导入音频播放器组件
import { createSSEService, type SSEData } from './utils/sseService'; // 导入SSE服务
import { createContentTemplateService, type Message } from './utils/contentTemplateService'; // 导入模板服务
import { init } from 'echarts/types/src/echarts.all.js';
// 定义组件属性接口
interface Props {
......@@ -390,39 +389,39 @@ const sendMessage = async (type: MessageType = 'text', params: MessageParams = {
scrollToBottom();
try {
// 调用外部传入的消息发送函数
if (props.onMessageSend) {
const sendContent = type === 'audio' ? audioUrl! : messageContent;
console.log(`调用外部发送函数`, sendContent);
await props.onMessageSend(sendContent);
} else {
// 默认的API调用逻辑
console.log(`默认API调用逻辑`, dialogSessionId.value);
const requestData = type === 'audio' ? {
questionLocalAudioFilePath: audioUrl,
audioDuration: durationTime,
...props.params,
} : {
question: messageContent,
...props.params,
};
// 调用外部传入的消息发送函数
if (props.onMessageSend) {
const sendContent = type === 'audio' ? audioUrl! : messageContent;
console.log(`调用外部发送函数`, sendContent);
await props.onMessageSend(sendContent);
} else {
// 默认的API调用逻辑
console.log(`默认API调用逻辑`, dialogSessionId.value);
const requestData = type === 'audio' ? {
questionLocalAudioFilePath: audioUrl,
audioDuration: durationTime,
...props.params,
} : {
question: messageContent,
...props.params,
};
const response = await fetch(`${props.apiBaseUrl}/aiService/ask/app/${props.params?.appId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'token': props.token,
'x-session-id': props.token,
'x-app-code': props.appCode || ''
},
body: JSON.stringify(requestData)
});
const data = await response.json();
if (data.code === 0) {
console.log(`发送成功`);
method: 'POST',
headers: {
'Content-Type': 'application/json',
'token': props.token,
'x-session-id': props.token,
'x-app-code': props.appCode || ''
},
body: JSON.stringify(requestData)
});
const data = await response.json();
if (data.code === 0) {
console.log(`发送成功`);
}
}
}
} catch (e) {
console.error(`发送失败:`, e);
} finally {
......@@ -478,7 +477,6 @@ const getChatRecord = async (dialogSessionId: string) => {
const response = await fetch(`${props.apiBaseUrl}/aiService/ask/list/chat/${dialogSessionId}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'token': props.token,
'x-session-id': props.token,
'x-app-code': props.appCode || ''
......
......@@ -627,7 +627,7 @@ li {
iframe {
width: 100%;
height: 100%;
min-height: 800px;
min-height: 600px;
border: none;
border-radius: 8px;
background-color: @gray-1;
......@@ -930,6 +930,7 @@ li {
line-height: 1.6;
color: @gray-7;
background: none;
white-space: initial;
// 标题样式
h1,
......@@ -1039,9 +1040,45 @@ li {
margin-bottom: 8px;
}
// Markdown图片容器样式
.markdown-image-container {
img {
max-width: 100%;
height: auto;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
transition: transform 0.3s ease, box-shadow 0.3s ease;
&:hover {
transform: scale(1.02);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.2);
}
}
.image-error {
border: 1px solid #ff4d4f;
padding: 12px;
background-color: #fff2f0;
border-radius: 6px;
margin: 8px 0;
span {
color: #ff4d4f;
font-size: 14px;
}
}
.image-caption {
font-size: 13px;
color: #666;
font-style: italic;
line-height: 1.4;
}
}
// 引用块样式
blockquote {
border-left: 4px solid @primary-color;
border-left: 4px solid @gray-2-3;
margin-bottom: 8px;
padding: 0 16px;
font-style: italic;
......@@ -1093,6 +1130,7 @@ li {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
font-size:0;
margin-bottom:8px;
// 滚动条样式
&::-webkit-scrollbar {
......
......@@ -126,9 +126,9 @@ export class ContentTemplateService {
`;
},
// 简化的iframe模板 - 移除全屏功能,设置宽高100%固定
// 简化的iframe模板
iframe: (iframeData: any) => {
const { tips, title, url, height } = iframeData || {};
const { tips, title, url } = iframeData || {};
return `<div class="message-iframe iframe-loading">
<!-- 加载状态 -->
<div class="iframe-loading">
......@@ -145,12 +145,12 @@ export class ContentTemplateService {
<iframe
src="${url}"
width="100%"
height="${height}"
height="100%"
frameborder="0"
sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox"
scrolling="no"
style="overflow: hidden;"
onload="this.parentElement.classList.add('iframe-loaded'); this.parentElement.classList.remove('iframe-loading');"
onload="(function(){this.parentElement.classList.add('iframe-loaded');this.parentElement.classList.remove('iframe-loading');try{let iframe=this.contentDocument.getElementById('iframe_view');if(iframe&&iframe.contentDocument&&iframe.contentDocument.body){let nestedHeight=iframe.contentDocument.body.clientHeight + 30;if(nestedHeight>0){this.style.height=nestedHeight+'px';}}}catch(error){console.warn('无法获取嵌套iframe高度:',error);}}).call(this)"
onerror="console.error('iframe加载失败:', this.src)"
></iframe>
</div>`;
......
......@@ -82,13 +82,17 @@ export const parseMarkdown = (text: string): string => {
text = String(text || '');
}
// 先进行HTML转义,防止XSS攻击
text = text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#x27;');
// 处理引用块(必须在HTML转义之前,避免>被转义为&gt;)
text = text.replace(/^>\s*(.*)$/gim, '<blockquote>$1</blockquote>');
// 处理图片(必须在任何格式处理之前,避免图片格式被破坏)
text = text.replace(/!\[([^\]]*)\]\(([^\)]+)\)/g, (match, alt, src) => {
const altText = alt || '图片';
return `<div class="markdown-image-container">
<img src="${src}" alt="${altText}">
${alt ? `<div class="image-caption">${alt}</div>` : ''}
</div>`;
});
// 处理基本的Markdown格式(在表格解析之前处理,确保表格单元格中的格式也能被处理)
text = processMarkdownFormat(text);
......@@ -147,14 +151,14 @@ export const parseMarkdown = (text: string): string => {
// 使用统一的Markdown格式处理函数处理基础格式(包括标题)
text = processMarkdownFormat(text);
// 处理代码块(支持语言标识)
// 处理代码块(支持语言标识)- 必须在换行处理之前
text = text.replace(/```(\w+)?\n([\s\S]*?)```/gim, '<pre><code class="language-$1">$2</code></pre>');
text = text.replace(/```([\s\S]*?)```/gim, '<pre><code>$1</code></pre>');
// 处理链接(支持相对路径和绝对路径)
text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/gim, (match, text, url) => {
// 验证URL格式
const isValidUrl = /^(https?|ftp):\/\/[^\s/$.?#].[^\s]*$/i.test(url);
const isValidUrl = /^(https?|ftp):\/\/[^\s/$.#?].[^\s]*$/i.test(url);
const target = isValidUrl ? 'target="_blank" rel="noopener noreferrer"' : '';
return `<a href="${url}" ${target}>${text}</a>`;
});
......@@ -168,9 +172,6 @@ export const parseMarkdown = (text: string): string => {
text = text.replace(/^[-*+]\s+(.*)$/gim, '<li>$1</li>');
text = text.replace(/(<li>.*<\/li>)/gim, '<ul>$1</ul>');
// 处理引用块
text = text.replace(/^>\s+(.*)$/gim, '<blockquote>$1</blockquote>');
// 处理水平分割线
text = text.replace(/^\s*---\s*$/gim, '<hr />');
text = text.replace(/^\s*\*\*\*\s*$/gim, '<hr />');
......@@ -210,11 +211,28 @@ export const markdownTemplate = (content: any, isStreaming: boolean = false): st
// 根据是否流式处理选择不同的解析方式
const htmlContent = isStreaming ? processStreamingMarkdown(content) : parseMarkdown(content);
// 清理HTML内容:移除br标签和空p段落
// 清理HTML内容:移除不必要的br标签和空p段落
const cleanHtml = htmlContent
.trim()
// 移除所有<br>标签
.replace(/<br\s*\/?>/gi, '')
// 保留blockquote和code块内的换行符,移除其他不必要的br标签
.replace(/<br\s*\/?>/gi, (match, offset, originalString) => {
// 检查当前br标签是否在blockquote或code/pre标签内
const beforeContent = originalString.substring(0, offset);
const afterContent = originalString.substring(offset + match.length);
// 检查前后是否有未闭合的blockquote标签
const openBlockquotesBefore = (beforeContent.match(/<blockquote/g) || []).length;
const closeBlockquotesBefore = (beforeContent.match(/<\/blockquote>/g) || []).length;
const openBlockquotesAfter = (afterContent.match(/<blockquote/g) || []).length;
const closeBlockquotesAfter = (afterContent.match(/<\/blockquote>/g) || []).length;
// 检查是否在code/pre标签内
const isInCodeBlock = beforeContent.includes('<pre>') || beforeContent.includes('<code>');
const isInBlockquote = (openBlockquotesBefore - closeBlockquotesBefore) > 0;
return (isInBlockquote || isInCodeBlock) ? match : '';
})
// 移除空的<p>段落(只包含空格或换行符)
.replace(/<p[^>]*>\s*<\/p>/gi, '')
// 移除只包含空格的<p>段落
......
......@@ -51,7 +51,7 @@ export class SSEService {
'x-app-code': this.config.appCode || '',
},
withCredentials: true,
connectionTimeout: 30000,
connectionTimeout: 60000,
});
this.eventSource.onopen = (event) => {
......
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