Commit 5e75750d authored by 水玉婷's avatar 水玉婷
Browse files

feat:优化md中有序、无序列表输出格式问题

parent f8edf3b5
......@@ -40,8 +40,8 @@
stage: 'wechat-demo',
};
// const dialogSessionId = '20251127180914709-00043912';
const dialogSessionId = '';
const dialogSessionId = '20251127180914709-00043912';
// const dialogSessionId = '';
const detailData = ref({
title: '国械小智',
});
......
......@@ -478,6 +478,7 @@ 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 || ''
......
......@@ -1060,8 +1060,7 @@ li {
// 表格样式(如果Markdown中包含表格)
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 8px;
border-collapse: collapse;
font-size: 13px;
background-color: @white;
......@@ -1093,7 +1092,6 @@ li {
width: 100%;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
border-radius: 8px;
font-size:0;
// 滚动条样式
......
......@@ -21,13 +21,13 @@ const processMarkdownFormat = (text: string): string => {
processedText = processedText.replace(/\*(.*?)\*/gim, '<em>$1</em>');
processedText = processedText.replace(/__(.*?)__/gim, '<strong>$1</strong>');
processedText = processedText.replace(/_(.*?)_/gim, '<em>$1</em>');
// 处理删除线
processedText = processedText.replace(/~~(.*?)~~/gim, '<del>$1</del>');
// 处理行内代码
processedText = processedText.replace(/`([^`]+)`/gim, '<code>$1</code>');
return processedText;
};
......@@ -41,7 +41,7 @@ const processMarkdownFormat = (text: string): string => {
const generateCompleteTable = (header: string[], alignments: string[], dataRows: string[][]): string => {
let tableHtml = '<div class="table-container">\n';
tableHtml += ' <table class="markdown-table" style="border-collapse: collapse; width: auto; min-width: 100%; border: 1px solid #ddd;">\n';
// 表头
tableHtml += ' <thead>\n <tr>\n';
header.forEach((cell, index) => {
......@@ -50,7 +50,7 @@ const generateCompleteTable = (header: string[], alignments: string[], dataRows:
tableHtml += ` <th style="text-align: ${align}; border: 1px solid #ddd; padding: 8px; background-color: #f5f5f5; font-weight: bold;">${processedCell}</th>\n`;
});
tableHtml += ' </tr>\n </thead>\n';
// 表体
if (dataRows.length > 0) {
tableHtml += ' <tbody>\n';
......@@ -65,7 +65,7 @@ const generateCompleteTable = (header: string[], alignments: string[], dataRows:
});
tableHtml += ' </tbody>\n';
}
tableHtml += ' </table>\n';
tableHtml += '</div>';
return tableHtml;
......@@ -98,16 +98,16 @@ export const parseMarkdown = (text: string): string => {
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 < 2) return match;
try {
// 解析表头
const headerRow = lines[0];
const headerCells = headerRow.split('|').slice(1, -1).map(cell => cell.trim());
// 解析对齐方式
let alignments: string[] = [];
let dataStartIndex = 1;
// 查找分隔线
for (let i = 1; i < lines.length; i++) {
const line = lines[i];
......@@ -122,12 +122,12 @@ export const parseMarkdown = (text: string): string => {
break;
}
}
// 如果没有找到分隔线,使用默认对齐方式
if (alignments.length === 0) {
alignments = headerCells.map(() => 'left');
}
// 解析数据行
const dataRows: string[][] = [];
for (let i = dataStartIndex; i < lines.length; i++) {
......@@ -135,7 +135,7 @@ export const parseMarkdown = (text: string): string => {
const cells = row.split('|').slice(1, -1).map(cell => cell.trim());
dataRows.push(cells);
}
// 生成完整表格HTML
return generateCompleteTable(headerCells, alignments, dataRows);
} catch (error) {
......@@ -159,11 +159,6 @@ export const parseMarkdown = (text: string): string => {
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');
......@@ -286,7 +281,7 @@ export const isTableSeparator = (text: string): boolean => {
if (!trimmedText.startsWith('|') || !trimmedText.endsWith('|')) {
return false;
}
const cells = trimmedText.split('|').slice(1, -1).map(cell => cell.trim());
return cells.every(cell => /^:?-+:?$/.test(cell));
};
......@@ -311,7 +306,7 @@ export const isTableStart = (text: string): boolean => {
class StreamingTableProcessor {
private tableData: string[] = [];
private isInTable: boolean = false;
/**
* 处理新的文本内容
* @param text 新的文本内容
......@@ -321,9 +316,9 @@ class StreamingTableProcessor {
if (!text || typeof text !== 'string') {
return null;
}
const trimmedText = text.trim();
// 检查是否是表格行
if (isTableRow(trimmedText)) {
if (!this.isInTable) {
......@@ -334,7 +329,7 @@ class StreamingTableProcessor {
} else {
// 继续当前表格,收集所有表格行
this.tableData.push(trimmedText);
// 不立即生成表格,继续收集直到遇到非表格行
return null; // 继续收集表格行
}
......@@ -343,7 +338,7 @@ class StreamingTableProcessor {
if (this.isInTable && this.tableData.length > 0) {
// 表格结束,处理缓存的表格数据
let result: string | null = null;
// 检查表格是否至少包含表头和数据行(至少2行)
if (this.tableData.length >= 2) {
// 表格完整,生成完整表格
......@@ -352,16 +347,16 @@ class StreamingTableProcessor {
// 表格不完整,显示加载提示
result = '<div class="table-loading" style="border: 1px solid #ddd; padding: 10px; background-color: #f9f9f9; margin: 10px 0;">表格数据加载中...</div>';
}
this.reset();
return result;
}
// 普通文本,直接返回
return text;
}
}
/**
* 生成完整表格HTML
* @returns 表格HTML
......@@ -370,7 +365,7 @@ class StreamingTableProcessor {
if (this.tableData.length < 2) {
return '';
}
try {
// 解析表头
const headerRow = this.tableData[0];
......@@ -378,11 +373,11 @@ class StreamingTableProcessor {
// 对表头单元格进行Markdown格式处理
return processMarkdownFormat(cell.trim());
});
// 解析对齐方式(从第二行)
let alignments: string[] = [];
let dataStartIndex = 1;
// 查找分隔线
for (let i = 1; i < this.tableData.length; i++) {
if (isTableSeparator(this.tableData[i])) {
......@@ -397,13 +392,13 @@ class StreamingTableProcessor {
break;
}
}
// 如果没有找到分隔线,使用默认对齐方式,数据从第二行开始
if (alignments.length === 0) {
alignments = headerCells.map(() => 'left');
dataStartIndex = 1; // 对于无分隔线表格,数据从第二行开始
}
// 解析数据行
const dataRows: string[][] = [];
for (let i = dataStartIndex; i < this.tableData.length; i++) {
......@@ -414,7 +409,7 @@ class StreamingTableProcessor {
});
dataRows.push(cells);
}
// 生成完整表格HTML
return generateCompleteTable(headerCells, alignments, dataRows);
} catch (error) {
......@@ -422,7 +417,7 @@ class StreamingTableProcessor {
return '<div class="table-error" style="border: 1px solid #ff6b6b; padding: 10px; background-color: #ffeaea; margin: 10px 0;">表格渲染失败</div>';
}
}
/**
* 重置表格处理器状态
*/
......@@ -430,7 +425,7 @@ class StreamingTableProcessor {
this.tableData = [];
this.isInTable = false;
}
/**
* 获取当前表格状态
*/
......@@ -445,6 +440,175 @@ class StreamingTableProcessor {
// 创建全局表格处理器实例
const streamingTableProcessor = new StreamingTableProcessor();
/**
* 检查文本是否是列表项(有序或无序)
* @param text 要检查的文本
* @returns 是否是列表项
*/
export const isListItem = (text: string): boolean => {
if (!text || typeof text !== 'string') {
return false;
}
const trimmedText = text.trim();
// 匹配有序列表(1. 2. 等)或无序列表(- * +)
return /^\d+\.\s+/.test(trimmedText) || /^[-*+]\s+/.test(trimmedText);
};
/**
* 检查文本是否是有序列表项
* @param text 要检查的文本
* @returns 是否是有序列表项
*/
export const isOrderedListItem = (text: string): boolean => {
if (!text || typeof text !== 'string') {
return false;
}
const trimmedText = text.trim();
return /^\d+\.\s+/.test(trimmedText);
};
/**
* 检查文本是否是无序列表项
* @param text 要检查的文本
* @returns 是否是无序列表项
*/
export const isUnorderedListItem = (text: string): boolean => {
if (!text || typeof text !== 'string') {
return false;
}
const trimmedText = text.trim();
return /^[-*+]\s+/.test(trimmedText);
};
/**
* 流式列表处理器
* 用于处理SSE逐条返回的列表数据
*/
class StreamingListProcessor {
private listItems: string[] = [];
private isInList: boolean = false;
private listType: 'ordered' | 'unordered' | null = null;
private orderedListStartNumber: number = 1; // 记录有序列表的起始序号
/**
* 处理新的文本内容
* @param text 新的文本内容
* @returns 处理结果:如果是列表项返回null,列表完成时返回完整列表HTML
*/
public processText(text: string): string | null {
if (!text || typeof text !== 'string') {
return null;
}
const trimmedText = text.trim();
// 检查是否是列表项
if (isListItem(trimmedText)) {
const isOrdered = isOrderedListItem(trimmedText);
const currentListType = isOrdered ? 'ordered' : 'unordered';
if (!this.isInList) {
// 开始新的列表
this.isInList = true;
this.listType = currentListType;
this.listItems = [trimmedText];
// 如果是有序列表,记录起始序号
if (isOrdered) {
const match = trimmedText.match(/^(\d+)\.\s+/);
this.orderedListStartNumber = match ? parseInt(match[1]) : 1;
}
return null; // 列表开始,不立即渲染
} else if (this.listType === currentListType) {
// 继续当前列表,收集所有列表项
this.listItems.push(trimmedText);
return null; // 继续收集列表项
} else {
// 列表类型改变,结束当前列表并开始新的列表
const result = this.generateCompleteList();
this.isInList = true;
this.listType = currentListType;
this.listItems = [trimmedText];
// 如果是有序列表,记录起始序号
if (isOrdered) {
const match = trimmedText.match(/^(\d+)\.\s+/);
this.orderedListStartNumber = match ? parseInt(match[1]) : 1;
}
return result;
}
} else {
// 非列表项
if (this.isInList && this.listItems.length > 0) {
// 列表结束,处理缓存的列表数据
const result = this.generateCompleteList();
this.reset();
return result;
}
// 普通文本,直接返回
return text;
}
}
/**
* 生成完整列表HTML
* @returns 列表HTML
*/
private generateCompleteList(): string {
if (this.listItems.length === 0) {
return '';
}
try {
// 处理每个列表项的内容
const processedItems = this.listItems.map((item, index) => {
if (this.listType === 'ordered') {
// 有序列表:移除数字标记,保留内容,但保持正确的序号
const content = item.replace(/^\d+\.\s+/, '').trim();
const processedContent = processMarkdownFormat(content);
return `<li value="${this.orderedListStartNumber + index}">${processedContent}</li>`;
} else {
// 无序列表:移除标记,保留内容
const content = item.replace(/^[-*+]\s+/, '').trim();
const processedContent = processMarkdownFormat(content);
return `<li>${processedContent}</li>`;
}
});
// 根据列表类型生成完整列表
const listTag = this.listType === 'ordered' ? 'ol' : 'ul';
return `<${listTag}>${processedItems.join('')}</${listTag}>`;
} catch (error) {
console.warn('列表生成失败:', error);
return '<div class="list-error" style="border: 1px solid #ff6b6b; padding: 10px; background-color: #ffeaea; margin: 10px 0;">列表渲染失败</div>';
}
}
/**
* 重置列表处理器状态
*/
private reset(): void {
this.listItems = [];
this.isInList = false;
this.listType = null;
this.orderedListStartNumber = 1;
}
/**
* 获取当前列表状态
*/
public getListState(): { isInList: boolean; listType: 'ordered' | 'unordered' | null; listItems: string[]; orderedListStartNumber: number } {
return {
isInList: this.isInList,
listType: this.listType,
listItems: [...this.listItems],
orderedListStartNumber: this.orderedListStartNumber
};
}
}
// 创建全局列表处理器实例
const streamingListProcessor = new StreamingListProcessor();
/**
* 处理流式Markdown文本(专门用于SSE逐条返回的数据)
* @param text 新的文本内容
......@@ -454,23 +618,36 @@ export const processStreamingMarkdown = (text: string): string => {
// 清理HTML标签,防止XSS攻击
text = text.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
text = text.replace(/<[^>]*>/g, '');
// 使用流式列表处理器处理文本
const listResult = streamingListProcessor.processText(text);
if (listResult === null) {
// 列表项被收集,不立即渲染
return '';
}
// 如果列表处理器返回了完整列表,直接返回
if (listResult && (listResult.includes('<ol>') || listResult.includes('<ul>'))) {
return listResult;
}
// 使用流式表格处理器处理文本
const result = streamingTableProcessor.processText(text);
if (result === null) {
const tableResult = streamingTableProcessor.processText(text);
if (tableResult === null) {
// 表格行被收集,不立即渲染
return '';
}
// 如果表格处理器返回了完整表格,需要确保表格内容中的Markdown格式也被处理
if (result.includes('<table')) {
if (tableResult.includes('<table')) {
// 对表格内容进行Markdown格式处理
return processMarkdownFormat(result);
return processMarkdownFormat(tableResult);
}
// 否则,处理其他Markdown语法
return parseMarkdown(result);
return parseMarkdown(tableResult || text);
};
/**
......@@ -525,11 +702,14 @@ export const mergeMarkdownContent = (existingContent: string, newContent: string
};
export default {
parseMarkdown,
markdownTemplate,
markdown,
isMarkdownBlock,
isLastBlockMarkdown,
getLastMarkdownBlockIndex,
mergeMarkdownContent
parseMarkdown, // 解析Markdown文本为HTML
markdownTemplate, // 应用Markdown模板
markdown, // 完整的Markdown处理函数
isMarkdownBlock, // 检查是否为Markdown块
isLastBlockMarkdown, // 检查最后一个块是否为Markdown
getLastMarkdownBlockIndex, // 获取最后一个Markdown块的索引
mergeMarkdownContent, // 合并Markdown内容
isListItem, // 检查是否为列表项
isOrderedListItem, // 检查是否为有序列表项
isUnorderedListItem // 检查是否为无序列表项
};
\ No newline at end of file
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