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

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

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