Commit 203ea4e2 authored by 水玉婷's avatar 水玉婷
Browse files

feat:把md跟audio 代码抽离

parent 7d03462d
......@@ -40,8 +40,8 @@
stage: 'wechat-demo',
};
// const dialogSessionId = '20251028143404893-00045166';
const dialogSessionId = '';
const dialogSessionId = '20251127180914709-00043912';
// const dialogSessionId = '';
const detailData = ref({
title: '国械小智',
});
......
......@@ -89,6 +89,8 @@ import { ref, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
import dayjs from 'dayjs';
import { post, get } from '@/utils/axios.js'; // 导入axios的post方法
import { tableTemplate } from './tableTemplate';
import { markdownTemplate, isLastBlockMarkdown, getLastMarkdownBlockIndex, mergeMarkdownContent } from './markdownTemplate';
import { audioTemplate, initAudioPlayers, pauseAllOtherAudios } from './audioTemplate';
import { SendOutlined, UserOutlined } from '@ant-design/icons-vue';
import defaultAvatar from '@/assets/logo.png';
import ChartComponent from './ChartComponent.vue'; // 导入独立的图表组件
......@@ -167,6 +169,10 @@ const contentTemplates = {
table: (tableData: any) => {
return tableTemplate(tableData);
},
// Markdown模板 - 使用独立的markdown模板工具
markdown: (content: any) => {
return markdownTemplate(content);
},
// 选项数据模板 - 纯渲染,不允许点击
option: (optionData: any) => {
const { tips, options } = optionData;
......@@ -223,153 +229,10 @@ const contentTemplates = {
></iframe>
</div>`;
},
// 音频消息模板 - 简化版本,移除audio-icon
// 音频消息模板
audio: (audioData: any) => {
const { audioUrl, audioBlob, durationTime } = audioData;
let src = audioUrl;
// 如果提供了Blob对象,创建对象URL
if (audioBlob && !audioUrl) {
src = URL.createObjectURL(audioBlob);
}
// 生成唯一ID用于音频播放器
const audioId = `audio_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
return `<div class="audio-message" data-audio-id="${audioId}">
<div class="audio-player" data-audio-src="${src}">
<div class="audio-wave">
<div class="wave-bar"></div>
<div class="wave-bar"></div>
<div class="wave-bar"></div>
<div class="wave-bar"></div>
<div class="wave-bar"></div>
</div>
<div class="audio-duration">${durationTime+'"' || '0"'}</div>
</div>
<audio id="${audioId}" src="${src}" preload="metadata" style="display: none;"></audio>
</div>`;
return audioTemplate(audioData);
},
// Markdown模板
markdown: (content: any) => {
// 类型检查和默认值处理
if (typeof content !== 'string') {
// 如果是对象,尝试转换为字符串
if (content && typeof content === 'object') {
content = JSON.stringify(content);
} else {
// 其他类型转换为字符串
content = String(content || '');
}
}
// 增强的Markdown解析器
const parseMarkdown = (text: string) => {
// 确保text是字符串
if (typeof text !== 'string') {
text = String(text || '');
}
// 转义HTML特殊字符,防止XSS攻击
text = text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#x27;');
// 处理标题(支持1-6级)
text = text.replace(/^######\s+(.*)$/gim, '<h6>$1</h6>');
text = text.replace(/^#####\s+(.*)$/gim, '<h5>$1</h5>');
text = text.replace(/^####\s+(.*)$/gim, '<h4>$1</h4>');
text = text.replace(/^###\s+(.*)$/gim, '<h3>$1</h3>');
text = text.replace(/^##\s+(.*)$/gim, '<h2>$1</h2>');
text = text.replace(/^#\s+(.*)$/gim, '<h1>$1</h1>');
// 处理粗体和斜体
text = text.replace(/\*\*(.*?)\*\*/gim, '<strong>$1</strong>');
text = text.replace(/\*(.*?)\*/gim, '<em>$1</em>');
text = text.replace(/__(.*?)__/gim, '<strong>$1</strong>');
text = text.replace(/_(.*?)_/gim, '<em>$1</em>');
// 处理删除线
text = text.replace(/~~(.*?)~~/gim, '<del>$1</del>');
// 处理代码块(支持语言标识)
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, '<code>$1</code>');
// 处理链接(支持相对路径和绝对路径)
text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/gim, (match, text, url) => {
// 验证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>`;
});
// 处理图片(添加加载失败处理)
text = text.replace(/!\[([^\]]*)\]\(([^)]+)\)/gim, (match, alt, src) => {
return `<img src="${src}" alt="${alt || '图片'}" style="max-width: 100%; height: auto;" onerror="this.style.display='none'" />`;
});
// 处理有序列表
text = text.replace(/^\d+\.\s+(.*)$/gim, '<li>$1</li>');
text = text.replace(/(<li>.*<\/li>)(?=\s*<li>)/gim, '$1');
text = text.replace(/(<li>.*<\/li>)/gim, '<ol>$1</ol>');
// 处理无序列表
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 />');
// 处理换行(保留段落结构)
text = text.replace(/\n{3,}/g, '\n\n'); // 多个换行合并为两个
text = text.replace(/\n\n/g, '</p><p>');
text = text.replace(/\n/g, '<br>');
text = '<p>' + text + '</p>';
// 清理HTML结构
text = text.replace(/<p><(h[1-6]|ul|ol|blockquote|pre|img|hr)/gim, '</p><$1');
text = text.replace(/(<\/(h[1-6]|ul|ol|blockquote|pre|img|hr)>)<p>/gim, '$1</p><p>');
text = text.replace(/<p>\s*<\/p>/g, ''); // 移除空段落
return text;
};
const htmlContent = parseMarkdown(content);
// 清理HTML内容:移除br标签和空p段落
const cleanHtml = htmlContent
.trim()
// 移除所有<br>标签
.replace(/<br\s*\/?>/gi, '')
// 移除空的<p>段落只包含空格或换行符
.replace(/<p[^>]*>\s*<\/p>/gi, '')
// 移除只包含空格的<p>段落
.replace(/<p[^>]*>(\s|&nbsp;)*<\/p>/gi, '')
// 移除连续的空白段落
.replace(/(<\/p>\s*<p[^>]*>)+/gi, '')
// 移除开头和结尾的空白段落
.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>`;
}
};
// 定义消息类型 - 更新接口添加图表相关字段
......@@ -448,7 +311,6 @@ const handleVoiceAudio = (audioUrl: string, audioBlob?: Blob, durationTime?: num
if (audioUrl) {
sendAudioMessage(audioUrl, durationTime);
}
// 滚动到底部
nextTick(() => {
scrollToBottom();
......@@ -599,44 +461,6 @@ const processSSEMessage = (
// 根据是否为历史数据设置默认展开状态
const defaultThinkBoxExpanded = !isHistoryData;
// 辅助函数:检查最后一个contentBlock是否是type为4的markdown块
const isLastBlockMarkdown = () => {
if (!updatedResponse || updatedResponse.contentBlocks.length === 0) {
return false;
}
const lastBlock = updatedResponse.contentBlocks[updatedResponse.contentBlocks.length - 1];
return lastBlock.content.includes('message-markdown');
};
// 辅助函数:获取最后一个markdown块的索引
const getLastMarkdownBlockIndex = () => {
if (!updatedResponse || updatedResponse.contentBlocks.length === 0) {
return -1;
}
for (let i = updatedResponse.contentBlocks.length - 1; i >= 0; i--) {
if (updatedResponse.contentBlocks[i].content.includes('message-markdown')) {
return i;
}
}
return -1;
};
// 辅助函数:合并markdown内容
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>`;
};
switch (contentStatus) {
case -1: // 错误信息
if (updatedResponse) {
......@@ -699,9 +523,9 @@ const processSSEMessage = (
const markdownContent = contentTemplates.markdown(messageContent || '');
// 检查最后一个块是否是markdown块
if (isLastBlockMarkdown()) {
if (isLastBlockMarkdown(updatedResponse.contentBlocks)) {
// 合并到现有的markdown块
const lastMarkdownIndex = getLastMarkdownBlockIndex();
const lastMarkdownIndex = getLastMarkdownBlockIndex(updatedResponse.contentBlocks);
if (lastMarkdownIndex !== -1) {
updatedResponse.contentBlocks[lastMarkdownIndex].content =
mergeMarkdownContent(
......@@ -1012,7 +836,6 @@ const processHistoryData = (dataArray: any[]) => {
// 处理问题消息
if (data.question || data.audioPath) {
let questionContent = '';
// 检查是否为音频消息
if (data.audioPath) {
// 处理音频消息
......@@ -1171,98 +994,30 @@ defineExpose({
isInThinkingMode // 是否在思考模式
});
// 生命周期
onMounted(() => {
console.log('组件挂载,初始 dialogSessionId:', props.dialogSessionId);
initSSE();
scrollToBottom();
if (props.dialogSessionId) {
getChatRecord(props.dialogSessionId);
}
// 初始化音频播放器事件监听
initAudioPlayers();
});
// 初始化音频播放器
const initAudioPlayers = () => {
const initAudioPlayersWrapper = () => {
// 监听消息变化,为新的音频消息添加事件监听
watch(messages, () => {
nextTick(() => {
setupAudioPlayers();
initAudioPlayers();
});
}, { deep: true });
};
// 设置音频播放器事件
const setupAudioPlayers = () => {
const audioPlayers = document.querySelectorAll('.audio-player');
audioPlayers.forEach((player) => {
// 检查是否已经绑定过事件监听器
if (player.hasAttribute('data-audio-bound')) {
return; // 如果已经绑定过,跳过
}
// 标记为已绑定
player.setAttribute('data-audio-bound', 'true');
const audioMessage = player.closest('.audio-message');
const audioId = audioMessage?.getAttribute('data-audio-id');
const audioElement = audioId ? document.getElementById(audioId) : null;
if (!audioElement) {
console.warn('未找到音频元素,audioId:', audioId);
return;
}
// 音频播放结束,重置状态
audioElement.addEventListener('ended', () => {
player.classList.remove('playing');
});
// 设置播放/暂停事件
player.addEventListener('click', (e) => {
e.stopPropagation();
if (audioElement.paused) {
// 暂停其他正在播放的音频
pauseAllOtherAudios(audioElement);
audioElement.play().catch(error => {
console.error('播放音频失败:', error);
});
player.classList.add('playing');
} else {
audioElement.pause();
player.classList.remove('playing');
}
});
// 音频播放事件
audioElement.addEventListener('play', () => {
player.classList.add('playing');
});
// 音频暂停事件
audioElement.addEventListener('pause', () => {
player.classList.remove('playing');
});
});
};
// 生命周期
onMounted(() => {
console.log('组件挂载,初始 dialogSessionId:', props.dialogSessionId);
initSSE();
scrollToBottom();
if (props.dialogSessionId) {
getChatRecord(props.dialogSessionId);
}
// 暂停所有其他正在播放的音频
const pauseAllOtherAudios = (currentAudio: HTMLAudioElement) => {
const allAudios = document.querySelectorAll('audio');
allAudios.forEach((audio) => {
if (audio !== currentAudio && !audio.paused) {
audio.pause();
const player = audio.closest('.audio-message')?.querySelector('.audio-player');
if (player) {
player.classList.remove('playing');
}
}
});
};
// 初始化音频播放器事件监听
initAudioPlayersWrapper();
});
onBeforeUnmount(() => {
closeSSE();
......
/**
* 音频模板工具类
* 用于生成音频消息的HTML模板和音频播放器管理
*/
/**
* 音频消息模板 - 简化版本,移除audio-icon
* @param audioData 音频数据对象
* @returns 音频消息的HTML字符串
*/
export const audioTemplate = (audioData: any): string => {
const { audioUrl, audioBlob, durationTime } = audioData;
let src = audioUrl;
// 如果提供了Blob对象,创建对象URL
if (audioBlob && !audioUrl) {
src = URL.createObjectURL(audioBlob);
}
// 生成唯一ID用于音频播放器
const audioId = `audio_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
return `<div class="audio-message" data-audio-id="${audioId}">
<div class="audio-player" data-audio-src="${src}">
<div class="audio-wave">
<div class="wave-bar"></div>
<div class="wave-bar"></div>
<div class="wave-bar"></div>
<div class="wave-bar"></div>
<div class="wave-bar"></div>
</div>
<div class="audio-duration">${durationTime+'"' || '0"'}</div>
</div>
<audio id="${audioId}" src="${src}" preload="metadata" style="display: none;"></audio>
</div>`;
};
/**
* 初始化音频播放器
* 设置音频播放器的事件监听
*/
export const initAudioPlayers = (): void => {
const audioPlayers = document.querySelectorAll('.audio-player');
audioPlayers.forEach((player) => {
// 检查是否已经绑定过事件监听器
if (player.hasAttribute('data-audio-bound')) {
return; // 如果已经绑定过,跳过
}
// 标记为已绑定
player.setAttribute('data-audio-bound', 'true');
const audioMessage = player.closest('.audio-message');
const audioId = audioMessage?.getAttribute('data-audio-id');
const audioElement = audioId ? document.getElementById(audioId) : null;
if (!audioElement) {
console.warn('未找到音频元素,audioId:', audioId);
return;
}
// 音频播放结束,重置状态
audioElement.addEventListener('ended', () => {
player.classList.remove('playing');
});
// 设置播放/暂停事件
player.addEventListener('click', (e) => {
e.stopPropagation();
if (audioElement.paused) {
// 暂停其他正在播放的音频
pauseAllOtherAudios(audioElement);
audioElement.play().catch(error => {
console.error('播放音频失败:', error);
});
player.classList.add('playing');
} else {
audioElement.pause();
player.classList.remove('playing');
}
});
// 音频播放事件
audioElement.addEventListener('play', () => {
player.classList.add('playing');
});
// 音频暂停事件
audioElement.addEventListener('pause', () => {
player.classList.remove('playing');
});
});
};
/**
* 暂停所有其他正在播放的音频
* @param currentAudio 当前正在播放的音频元素
*/
export const pauseAllOtherAudios = (currentAudio: HTMLAudioElement): void => {
const allAudios = document.querySelectorAll('audio');
allAudios.forEach((audio) => {
if (audio !== currentAudio && !audio.paused) {
audio.pause();
const player = audio.closest('.audio-message')?.querySelector('.audio-player');
if (player) {
player.classList.remove('playing');
}
}
});
};
/**
* 简化的音频模板函数(兼容旧版本)
* @param audioData 音频数据对象
* @returns 音频消息的HTML字符串
*/
export const audio = (audioData: any): string => {
return audioTemplate(audioData);
};
// 默认导出对象
export default {
audioTemplate,
audio,
initAudioPlayers,
pauseAllOtherAudios
};
\ No newline at end of file
/**
* Markdown模板工具类
* 用于解析和渲染Markdown内容
*/
/**
* 增强的Markdown解析器
* @param text Markdown文本内容
* @returns 解析后的HTML字符串
*/
export const parseMarkdown = (text: string): string => {
// 确保text是字符串
if (typeof text !== 'string') {
text = String(text || '');
}
// 先进行HTML转义,防止XSS攻击
text = text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#x27;');
// 然后处理表格(在HTML转义之后)
text = text.replace(/(\|[^\n]+\|(?:\r?\n|\r))+/g, (match) => {
const lines = match.trim().split(/\r?\n/).filter(line => line.trim() && line.includes('|'));
if (lines.length < 1) return match;
try {
// 检查是否有分隔线
let hasSeparator = false;
let headerIndex = 0;
let separatorIndex = -1;
// 查找分隔线(第二行通常是分隔线)
if (lines.length >= 2) {
const secondLine = lines[1].trim();
// 分隔线通常只包含 |、-、: 和空格
if (/^[\s|:\-]+$/.test(secondLine.replace(/[^:\-|]/g, ''))) {
hasSeparator = true;
headerIndex = 0;
separatorIndex = 1;
}
}
// 解析表头
const header = parseTableRow(lines[headerIndex]);
// 确定列对齐方式
const alignments = header.map(() => 'left'); // 默认左对齐
if (hasSeparator && separatorIndex !== -1) {
const separatorCells = parseTableRow(lines[separatorIndex]);
separatorCells.forEach((cell, index) => {
const content = cell.trim();
if (content.startsWith(':') && content.endsWith(':')) {
alignments[index] = 'center';
} else if (content.startsWith(':')) {
alignments[index] = 'left';
} else if (content.endsWith(':')) {
alignments[index] = 'right';
}
});
}
// 解析数据行
const dataRows = [];
const startIndex = hasSeparator ? 2 : 1;
for (let i = startIndex; i < lines.length; i++) {
dataRows.push(parseTableRow(lines[i]));
}
// 生成表格HTML
let tableHtml = '<table class="markdown-table" style="border-collapse: collapse; width: 100%; border: 1px solid #ddd; margin: 10px 0;">\n';
// 表头
tableHtml += ' <thead>\n <tr>\n';
header.forEach((cell, index) => {
const align = alignments[index] || 'left';
tableHtml += ` <th style="text-align: ${align}; border: 1px solid #ddd; padding: 8px; background-color: #f5f5f5; font-weight: bold;">${cell}</th>\n`;
});
tableHtml += ' </tr>\n </thead>\n';
// 表体
if (dataRows.length > 0) {
tableHtml += ' <tbody>\n';
dataRows.forEach(row => {
tableHtml += ' <tr>\n';
row.forEach((cell, index) => {
const align = alignments[index] || 'left';
tableHtml += ` <td style="text-align: ${align}; border: 1px solid #ddd; padding: 8px;">${cell}</td>\n`;
});
tableHtml += ' </tr>\n';
});
tableHtml += ' </tbody>\n';
}
tableHtml += '</table>';
return tableHtml;
} catch (error) {
console.warn('表格解析失败:', error);
return match;
}
});
// 处理标题(支持1-6级)
text = text.replace(/^######\s+(.*)$/gim, '<h6>$1</h6>');
text = text.replace(/^#####\s+(.*)$/gim, '<h5>$1</h5>');
text = text.replace(/^####\s+(.*)$/gim, '<h4>$1</h4>');
text = text.replace(/^###\s+(.*)$/gim, '<h3>$1</h3>');
text = text.replace(/^##\s+(.*)$/gim, '<h2>$1</h2>');
text = text.replace(/^#\s+(.*)$/gim, '<h1>$1</h1>');
// 处理粗体和斜体
text = text.replace(/\*\*(.*?)\*\*/gim, '<strong>$1</strong>');
text = text.replace(/\*(.*?)\*/gim, '<em>$1</em>');
text = text.replace(/__(.*?)__/gim, '<strong>$1</strong>');
text = text.replace(/_(.*?)_/gim, '<em>$1</em>');
// 处理删除线
text = text.replace(/~~(.*?)~~/gim, '<del>$1</del>');
// 处理代码块(支持语言标识)
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, '<code>$1</code>');
// 处理链接(支持相对路径和绝对路径)
text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/gim, (match, text, url) => {
// 验证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>`;
});
// 处理图片(添加加载失败处理)
text = text.replace(/!\[([^\]]*)\]\(([^)]+)\)/gim, (match, alt, src) => {
return `<img src="${src}" alt="${alt || '图片'}" style="max-width: 100%; height: auto;" onerror="this.style.display='none'" />`;
});
// 处理有序列表
text = text.replace(/^\d+\.\s+(.*)$/gim, '<li>$1</li>');
text = text.replace(/(<li>.*<\/li>)(?=\s*<li>)/gim, '$1');
text = text.replace(/(<li>.*<\/li>)/gim, '<ol>$1</ol>');
// 处理无序列表
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 />');
// 完全删除段落处理逻辑,避免表格被p标签包裹
// 只处理换行,不添加p标签
text = text.replace(/\n{3,}/g, '\n\n'); // 多个换行合并为两个
text = text.replace(/\n\n/g, '<br><br>');
text = text.replace(/\n/g, '<br>');
// 清理HTML结构(移除空行)
text = text.replace(/(<br>\s*)+/g, '<br>');
return text;
};
/**
* 改进的表格行解析
* @param line 表格行文本
* @returns 单元格数组
*/
const parseTableRow = (line: string): string[] => {
// 移除行首尾的 | 和空格
const trimmedLine = line.trim().replace(/^\||\|$/g, '');
// 按 | 分割,保留空单元格
const cells = trimmedLine.split('|').map(cell => cell.trim());
return cells;
};
/**
* Markdown模板函数
* @param content 要处理的Markdown内容
* @returns 渲染后的HTML字符串
*/
export const markdownTemplate = (content: any): string => {
// 类型检查和默认值处理
if (typeof content !== 'string') {
// 如果是对象,尝试转换为字符串
if (content && typeof content === 'object') {
content = JSON.stringify(content);
} else {
// 其他类型转换为字符串
content = String(content || '');
}
}
const htmlContent = parseMarkdown(content);
// 清理HTML内容:移除br标签和空p段落
const cleanHtml = htmlContent
.trim()
// 移除所有<br>标签
.replace(/<br\s*\/?>/gi, '')
// 移除空的<p>段落(只包含空格或换行符)
.replace(/<p[^>]*>\s*<\/p>/gi, '')
// 移除只包含空格的<p>段落
.replace(/<p[^>]*>(\s|&nbsp;)*<\/p>/gi, '')
// 移除连续的空白段落
.replace(/(<\/p>\s*<p[^>]*>)+/gi, '')
// 移除开头和结尾的空白段落
.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>`;
};
/**
* 简化的Markdown生成函数(兼容旧版本)
* @param content Markdown内容
* @returns 渲染后的HTML字符串
*/
export const markdown = (content: any): string => {
return markdownTemplate(content);
};
/**
* 检查内容块是否是markdown块
* @param contentBlock 内容块对象
* @returns 是否是markdown块
*/
export const isMarkdownBlock = (contentBlock: any): boolean => {
return contentBlock && contentBlock.content && contentBlock.content.includes('message-markdown');
};
/**
* 检查最后一个contentBlock是否是markdown块
* @param contentBlocks 内容块数组
* @returns 最后一个块是否是markdown块
*/
export const isLastBlockMarkdown = (contentBlocks: any[]): boolean => {
if (!contentBlocks || contentBlocks.length === 0) {
return false;
}
const lastBlock = contentBlocks[contentBlocks.length - 1];
return isMarkdownBlock(lastBlock);
};
/**
* 获取最后一个markdown块的索引
* @param contentBlocks 内容块数组
* @returns 最后一个markdown块的索引,如果没有找到返回-1
*/
export const getLastMarkdownBlockIndex = (contentBlocks: any[]): number => {
if (!contentBlocks || contentBlocks.length === 0) {
return -1;
}
for (let i = contentBlocks.length - 1; i >= 0; i--) {
if (isMarkdownBlock(contentBlocks[i])) {
return i;
}
}
return -1;
};
/**
* 合并markdown内容
* @param existingContent 现有的markdown内容
* @param newContent 新的markdown内容
* @returns 合并后的markdown内容
*/
export const mergeMarkdownContent = (existingContent: string, newContent: string): 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>`;
};
export default {
parseMarkdown,
markdownTemplate,
markdown,
isMarkdownBlock,
isLastBlockMarkdown,
getLastMarkdownBlockIndex,
mergeMarkdownContent
};
\ No newline at end of file
......@@ -1137,7 +1137,9 @@ li {
// 下划线
hr {
border: 0;
border-top: 1px solid @gray-2-3;
border-bottom: 1px solid @gray-2-3;
padding-top: 5px;
margin-bottom: 5px;
}
// 表格样式(如果Markdown中包含表格)
......@@ -1145,22 +1147,79 @@ li {
width: 100%;
border-collapse: collapse;
margin-bottom: 8px;
font-size: 13px;
background-color: @white;
th,
td {
padding: 8px 12px;
border: 1px solid @gray-3;
text-align: left;
line-height: 1.4;
}
th {
background-color: @blue-light-2;
font-weight: 600;
color: @gray-7;
}
tr:nth-child(even) {
background-color: @gray-1;
}
tr:hover {
background-color: @blue-light-1;
}
}
// 专门为markdown表格添加的样式
.markdown-table {
width: 100%;
border-collapse: collapse;
margin: 12px 0;
font-size: 13px;
background-color: @white;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
th {
background-color: @blue-light-2;
font-weight: 600;
color: @gray-7;
border-bottom: 2px solid @primary-color;
padding: 10px 12px;
}
td {
padding: 8px 12px;
border: 1px solid @gray-3;
line-height: 1.4;
}
tr:nth-child(even) {
background-color: @gray-1;
}
tr:hover {
background-color: @blue-light-1;
transition: background-color 0.2s ease;
}
// 对齐方式样式
th[style*="text-align: center"],
td[style*="text-align: center"] {
text-align: center;
}
th[style*="text-align: right"],
td[style*="text-align: right"] {
text-align: right;
}
th[style*="text-align: left"],
td[style*="text-align: left"] {
text-align: left;
}
}
}
}
......
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