Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
水玉婷
ai-wechat
Commits
53a84388
Commit
53a84388
authored
Nov 28, 2025
by
水玉婷
Browse files
feat:md中支持表格流式输出展示
parent
c8ee53e3
Changes
4
Hide whitespace changes
Inline
Side-by-side
src/views/components/AiChat.vue
View file @
53a84388
...
@@ -82,7 +82,7 @@
...
@@ -82,7 +82,7 @@
<textarea
ref=
"textarea"
v-model=
"messageText"
placeholder=
"输入消息..."
@
keypress=
"handleKeyPress"
<textarea
ref=
"textarea"
v-model=
"messageText"
placeholder=
"输入消息..."
@
keypress=
"handleKeyPress"
@
input=
"adjustTextareaHeight"
:disabled=
"loading"
></textarea>
@
input=
"adjustTextareaHeight"
:disabled=
"loading"
></textarea>
<button
@
click=
"sendMessage"
:disabled=
"loading"
class=
"send-button"
>
<button
@
click=
"sendMessage
()
"
:disabled=
"loading"
class=
"send-button"
>
<send-outlined
/>
<send-outlined
/>
</button>
</button>
</div>
</div>
...
...
src/views/components/style.less
View file @
53a84388
...
@@ -404,11 +404,6 @@ li {
...
@@ -404,11 +404,6 @@ li {
z-index: 10;
z-index: 10;
transform: translateY(-50%);
transform: translateY(-50%);
&:active {
background-color: rgba(91, 138, 254, 0.1);
transform: translateY(-50%) scale(0.95);
}
&:disabled {
&:disabled {
color: @gray-4;
color: @gray-4;
border-color: @gray-4;
border-color: @gray-4;
...
...
src/views/components/utils/contentTemplateService.ts
View file @
53a84388
...
@@ -8,7 +8,7 @@ import { markdownTemplate, isLastBlockMarkdown, getLastMarkdownBlockIndex, merge
...
@@ -8,7 +8,7 @@ import { markdownTemplate, isLastBlockMarkdown, getLastMarkdownBlockIndex, merge
thinking
:
(
content
:
string
)
=>
string
;
thinking
:
(
content
:
string
)
=>
string
;
error
:
(
content
:
string
)
=>
string
;
error
:
(
content
:
string
)
=>
string
;
table
:
(
tableData
:
any
)
=>
string
;
table
:
(
tableData
:
any
)
=>
string
;
markdown
:
(
content
:
any
)
=>
string
;
markdown
:
(
content
:
any
,
isStreaming
?:
boolean
)
=>
string
;
option
:
(
optionData
:
any
)
=>
string
;
option
:
(
optionData
:
any
)
=>
string
;
iframe
:
(
iframeData
:
any
)
=>
string
;
iframe
:
(
iframeData
:
any
)
=>
string
;
}
}
...
@@ -95,8 +95,8 @@ export class ContentTemplateService {
...
@@ -95,8 +95,8 @@ export class ContentTemplateService {
},
},
// Markdown模板 - 使用独立的markdown模板工具
// Markdown模板 - 使用独立的markdown模板工具
markdown
:
(
content
:
any
)
=>
{
markdown
:
(
content
:
any
,
isStreaming
:
boolean
=
false
)
=>
{
return
markdownTemplate
(
content
);
return
markdownTemplate
(
content
,
isStreaming
);
},
},
// 选项数据模板 - 纯渲染,不允许点击
// 选项数据模板 - 纯渲染,不允许点击
...
@@ -188,7 +188,7 @@ export class ContentTemplateService {
...
@@ -188,7 +188,7 @@ export class ContentTemplateService {
isHistoryData
=
false
,
isHistoryData
=
false
,
templateTools
?:
{
templateTools
?:
{
tableTemplate
:
(
tableData
:
any
)
=>
string
;
tableTemplate
:
(
tableData
:
any
)
=>
string
;
markdownTemplate
:
(
content
:
any
)
=>
string
;
markdownTemplate
:
(
content
:
any
,
isStreaming
?:
boolean
)
=>
string
;
isLastBlockMarkdown
:
(
blocks
:
MessageBlock
[])
=>
boolean
;
isLastBlockMarkdown
:
(
blocks
:
MessageBlock
[])
=>
boolean
;
getLastMarkdownBlockIndex
:
(
blocks
:
MessageBlock
[])
=>
number
;
getLastMarkdownBlockIndex
:
(
blocks
:
MessageBlock
[])
=>
number
;
mergeMarkdownContent
:
(
existing
:
string
,
newContent
:
string
)
=>
string
;
mergeMarkdownContent
:
(
existing
:
string
,
newContent
:
string
)
=>
string
;
...
@@ -272,9 +272,10 @@ export class ContentTemplateService {
...
@@ -272,9 +272,10 @@ export class ContentTemplateService {
case
4
:
// MD格式
case
4
:
// MD格式
if
(
updatedResponse
)
{
if
(
updatedResponse
)
{
// 对于SSE流式数据,使用流式处理
const
markdownContent
=
templateTools
?.
markdownTemplate
?
const
markdownContent
=
templateTools
?.
markdownTemplate
?
templateTools
.
markdownTemplate
(
messageContent
||
''
)
:
templateTools
.
markdownTemplate
(
messageContent
||
''
,
true
)
:
this
.
templates
.
markdown
(
messageContent
||
''
);
this
.
templates
.
markdown
(
messageContent
||
''
,
true
);
// 检查最后一个块是否是markdown块
// 检查最后一个块是否是markdown块
const
isLastMarkdown
=
templateTools
?.
isLastBlockMarkdown
?
const
isLastMarkdown
=
templateTools
?.
isLastBlockMarkdown
?
...
@@ -414,11 +415,17 @@ export class ContentTemplateService {
...
@@ -414,11 +415,17 @@ export class ContentTemplateService {
}
}
// 处理历史记录数据
// 处理历史记录数据
/**
* 处理历史记录数据,将其转换为可渲染的消息格式
* @param dataArray 包含历史记录数据的数组
* @param templateTools 包含模板工具函数的对象(可选)
* @returns 转换后的消息数组
*/
public
processHistoryData
(
public
processHistoryData
(
dataArray
:
any
[],
dataArray
:
any
[],
templateTools
?:
{
templateTools
?:
{
tableTemplate
:
(
tableData
:
any
)
=>
string
;
tableTemplate
:
(
tableData
:
any
)
=>
string
;
markdownTemplate
:
(
content
:
any
)
=>
string
;
markdownTemplate
:
(
content
:
any
,
isStreaming
?:
boolean
)
=>
string
;
isLastBlockMarkdown
:
(
blocks
:
MessageBlock
[])
=>
boolean
;
isLastBlockMarkdown
:
(
blocks
:
MessageBlock
[])
=>
boolean
;
getLastMarkdownBlockIndex
:
(
blocks
:
MessageBlock
[])
=>
number
;
getLastMarkdownBlockIndex
:
(
blocks
:
MessageBlock
[])
=>
number
;
mergeMarkdownContent
:
(
existing
:
string
,
newContent
:
string
)
=>
string
;
mergeMarkdownContent
:
(
existing
:
string
,
newContent
:
string
)
=>
string
;
...
...
src/views/components/utils/markdownTemplate.ts
View file @
53a84388
...
@@ -2,6 +2,41 @@
...
@@ -2,6 +2,41 @@
* Markdown模板工具类
* Markdown模板工具类
* 用于解析和渲染Markdown内容
* 用于解析和渲染Markdown内容
*/
*/
/**
* 生成完整的表格HTML
* @param header 表头
* @param alignments 对齐方式
* @param dataRows 数据行
* @returns 表格HTML
*/
const
generateCompleteTable
=
(
header
:
string
[],
alignments
:
string
[],
dataRows
:
string
[][]):
string
=>
{
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
;
};
/**
/**
* 增强的Markdown解析器
* 增强的Markdown解析器
...
@@ -22,81 +57,51 @@ export const parseMarkdown = (text: string): string => {
...
@@ -22,81 +57,51 @@ export const parseMarkdown = (text: string): string => {
.
replace
(
/"/g
,
'
"
'
)
.
replace
(
/"/g
,
'
"
'
)
.
replace
(
/'/g
,
'
'
'
);
.
replace
(
/'/g
,
'
'
'
);
// 然后处理表格(在HTML转义之后)
// 对于完整文本,使用简单的表格处理逻辑
// 使用正则表达式处理表格,避免复杂的流式处理
text
=
text
.
replace
(
/
(\|[^\n]
+
\|(?:\r?\n
|
\r))
+/g
,
(
match
)
=>
{
text
=
text
.
replace
(
/
(\|[^\n]
+
\|(?:\r?\n
|
\r))
+/g
,
(
match
)
=>
{
const
lines
=
match
.
trim
().
split
(
/
\r?\n
/
).
filter
(
line
=>
line
.
trim
()
&&
line
.
includes
(
'
|
'
));
const
lines
=
match
.
trim
().
split
(
/
\r?\n
/
).
filter
(
line
=>
line
.
trim
()
&&
line
.
includes
(
'
|
'
));
if
(
lines
.
length
<
1
)
return
match
;
if
(
lines
.
length
<
2
)
return
match
;
try
{
try
{
// 检查是否有分隔线
// 解析表头
let
hasSeparator
=
false
;
const
headerRow
=
lines
[
0
];
let
headerIndex
=
0
;
const
headerCells
=
headerRow
.
split
(
'
|
'
).
slice
(
1
,
-
1
).
map
(
cell
=>
cell
.
trim
());
let
separatorIndex
=
-
1
;
// 解析对齐方式
let
alignments
:
string
[]
=
[];
let
dataStartIndex
=
1
;
// 查找分隔线(第二行通常是分隔线)
// 查找分隔线
if
(
lines
.
length
>=
2
)
{
for
(
let
i
=
1
;
i
<
lines
.
length
;
i
++
)
{
const
secondLine
=
lines
[
1
].
trim
();
const
line
=
lines
[
i
];
// 分隔线通常只包含 |、-、: 和空格
const
cells
=
line
.
split
(
'
|
'
).
slice
(
1
,
-
1
).
map
(
cell
=>
cell
.
trim
());
if
(
/^
[\s
|:
\-]
+$/
.
test
(
secondLine
.
replace
(
/
[^
:
\-
|
]
/g
,
''
)))
{
if
(
cells
.
every
(
cell
=>
/^:
?
-+:
?
$/
.
test
(
cell
)))
{
hasSeparator
=
true
;
alignments
=
cells
.
map
(
cell
=>
{
headerIndex
=
0
;
if
(
cell
.
startsWith
(
'
:
'
)
&&
cell
.
endsWith
(
'
:
'
))
return
'
center
'
;
separatorIndex
=
1
;
if
(
cell
.
endsWith
(
'
:
'
))
return
'
right
'
;
return
'
left
'
;
});
dataStartIndex
=
i
+
1
;
break
;
}
}
}
}
// 解析表头
// 如果没有找到分隔线,使用默认对齐方式
const
header
=
parseTableRow
(
lines
[
headerIndex
]);
if
(
alignments
.
length
===
0
)
{
alignments
=
headerCells
.
map
(()
=>
'
left
'
);
// 确定列对齐方式
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
dataRows
:
string
[][]
=
[];
const
startIndex
=
hasSeparator
?
2
:
1
;
for
(
let
i
=
dataStartIndex
;
i
<
lines
.
length
;
i
++
)
{
for
(
let
i
=
startIndex
;
i
<
lines
.
length
;
i
++
)
{
const
row
=
lines
[
i
];
dataRows
.
push
(
parseTableRow
(
lines
[
i
]));
const
cells
=
row
.
split
(
'
|
'
).
slice
(
1
,
-
1
).
map
(
cell
=>
cell
.
trim
());
}
dataRows
.
push
(
cells
);
// 生成表格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>
'
;
// 生成完整表格HTML
return
tableHtml
;
return
generateCompleteTable
(
headerCells
,
alignments
,
dataRows
)
;
}
catch
(
error
)
{
}
catch
(
error
)
{
console
.
warn
(
'
表格解析失败:
'
,
error
);
console
.
warn
(
'
表格解析失败:
'
,
error
);
return
match
;
return
match
;
...
@@ -166,25 +171,15 @@ export const parseMarkdown = (text: string): string => {
...
@@ -166,25 +171,15 @@ export const parseMarkdown = (text: string): string => {
return
text
;
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模板函数
* Markdown模板函数
* @param content 要处理的Markdown内容
* @param content 要处理的Markdown内容
* @param isStreaming 是否使用流式处理(针对SSE逐条返回的数据)
* @returns 渲染后的HTML字符串
* @returns 渲染后的HTML字符串
*/
*/
export
const
markdownTemplate
=
(
content
:
any
):
string
=>
{
export
const
markdownTemplate
=
(
content
:
any
,
isStreaming
:
boolean
=
false
):
string
=>
{
// 类型检查和默认值处理
// 类型检查和默认值处理
if
(
typeof
content
!==
'
string
'
)
{
if
(
typeof
content
!==
'
string
'
)
{
// 如果是对象,尝试转换为字符串
// 如果是对象,尝试转换为字符串
...
@@ -196,7 +191,8 @@ export const markdownTemplate = (content: any): string => {
...
@@ -196,7 +191,8 @@ export const markdownTemplate = (content: any): string => {
}
}
}
}
const
htmlContent
=
parseMarkdown
(
content
);
// 根据是否流式处理选择不同的解析方式
const
htmlContent
=
isStreaming
?
processStreamingMarkdown
(
content
)
:
parseMarkdown
(
content
);
// 清理HTML内容:移除br标签和空p段落
// 清理HTML内容:移除br标签和空p段落
const
cleanHtml
=
htmlContent
const
cleanHtml
=
htmlContent
...
@@ -227,10 +223,11 @@ export const markdownTemplate = (content: any): string => {
...
@@ -227,10 +223,11 @@ export const markdownTemplate = (content: any): string => {
/**
/**
* 简化的Markdown生成函数(兼容旧版本)
* 简化的Markdown生成函数(兼容旧版本)
* @param content Markdown内容
* @param content Markdown内容
* @param isStreaming 是否使用流式处理(可选参数)
* @returns 渲染后的HTML字符串
* @returns 渲染后的HTML字符串
*/
*/
export
const
markdown
=
(
content
:
any
):
string
=>
{
export
const
markdown
=
(
content
:
any
,
isStreaming
:
boolean
=
false
):
string
=>
{
return
markdownTemplate
(
content
);
return
markdownTemplate
(
content
,
isStreaming
);
};
};
/**
/**
...
@@ -242,6 +239,212 @@ export const isMarkdownBlock = (contentBlock: any): boolean => {
...
@@ -242,6 +239,212 @@ export const isMarkdownBlock = (contentBlock: any): boolean => {
return
contentBlock
&&
contentBlock
.
content
&&
contentBlock
.
content
.
includes
(
'
message-markdown
'
);
return
contentBlock
&&
contentBlock
.
content
&&
contentBlock
.
content
.
includes
(
'
message-markdown
'
);
};
};
/**
* 检查文本是否是表格行
* @param text 要检查的文本
* @returns 是否是表格行
*/
export
const
isTableRow
=
(
text
:
string
):
boolean
=>
{
if
(
!
text
||
typeof
text
!==
'
string
'
)
{
return
false
;
}
const
trimmedText
=
text
.
trim
();
return
trimmedText
.
startsWith
(
'
|
'
)
&&
trimmedText
.
endsWith
(
'
|
'
);
};
/**
* 检查文本是否是表格分隔线
* @param text 要检查的文本
* @returns 是否是表格分隔线
*/
export
const
isTableSeparator
=
(
text
:
string
):
boolean
=>
{
if
(
!
text
||
typeof
text
!==
'
string
'
)
{
return
false
;
}
const
trimmedText
=
text
.
trim
();
if
(
!
trimmedText
.
startsWith
(
'
|
'
)
||
!
trimmedText
.
endsWith
(
'
|
'
))
{
return
false
;
}
const
cells
=
trimmedText
.
split
(
'
|
'
).
slice
(
1
,
-
1
).
map
(
cell
=>
cell
.
trim
());
return
cells
.
every
(
cell
=>
/^:
?
-+:
?
$/
.
test
(
cell
));
};
/**
* 检查文本是否是表格的开始(表头)
* @param text 要检查的文本
* @returns 是否是表格开始
*/
export
const
isTableStart
=
(
text
:
string
):
boolean
=>
{
if
(
!
text
||
typeof
text
!==
'
string
'
)
{
return
false
;
}
const
trimmedText
=
text
.
trim
();
return
trimmedText
.
startsWith
(
'
|
'
)
&&
trimmedText
.
endsWith
(
'
|
'
)
&&
!
isTableSeparator
(
trimmedText
);
};
/**
* 流式表格处理器
* 用于处理SSE逐条返回的表格数据
*/
class
StreamingTableProcessor
{
private
tableData
:
string
[]
=
[];
private
isInTable
:
boolean
=
false
;
/**
* 处理新的文本内容
* @param text 新的文本内容
* @returns 处理结果:如果是表格行返回null,表格完成时返回完整表格HTML
*/
public
processText
(
text
:
string
):
string
|
null
{
if
(
!
text
||
typeof
text
!==
'
string
'
)
{
return
null
;
}
const
trimmedText
=
text
.
trim
();
// 检查是否是表格行
if
(
isTableRow
(
trimmedText
))
{
if
(
!
this
.
isInTable
)
{
// 开始新的表格
this
.
isInTable
=
true
;
this
.
tableData
=
[
trimmedText
];
return
null
;
// 表格开始,不立即渲染
}
else
{
// 继续当前表格,收集所有表格行
this
.
tableData
.
push
(
trimmedText
);
// 不立即生成表格,继续收集直到遇到非表格行
return
null
;
// 继续收集表格行
}
}
else
{
// 非表格行
if
(
this
.
isInTable
&&
this
.
tableData
.
length
>
0
)
{
// 表格结束,处理缓存的表格数据
let
result
:
string
|
null
=
null
;
// 检查表格是否至少包含表头和数据行(至少2行)
if
(
this
.
tableData
.
length
>=
2
)
{
// 表格完整,生成完整表格
result
=
this
.
generateCompleteTable
();
}
else
{
// 表格不完整,显示加载提示
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
*/
private
generateCompleteTable
():
string
{
if
(
this
.
tableData
.
length
<
2
)
{
return
''
;
}
try
{
// 解析表头
const
headerRow
=
this
.
tableData
[
0
];
const
headerCells
=
headerRow
.
split
(
'
|
'
).
slice
(
1
,
-
1
).
map
(
cell
=>
cell
.
trim
());
// 解析对齐方式(从第二行)
let
alignments
:
string
[]
=
[];
let
dataStartIndex
=
1
;
// 查找分隔线
for
(
let
i
=
1
;
i
<
this
.
tableData
.
length
;
i
++
)
{
if
(
isTableSeparator
(
this
.
tableData
[
i
]))
{
const
alignRow
=
this
.
tableData
[
i
];
const
alignCells
=
alignRow
.
split
(
'
|
'
).
slice
(
1
,
-
1
).
map
(
cell
=>
cell
.
trim
());
alignments
=
alignCells
.
map
(
cell
=>
{
if
(
cell
.
startsWith
(
'
:
'
)
&&
cell
.
endsWith
(
'
:
'
))
return
'
center
'
;
if
(
cell
.
endsWith
(
'
:
'
))
return
'
right
'
;
return
'
left
'
;
});
dataStartIndex
=
i
+
1
;
break
;
}
}
// 如果没有找到分隔线,使用默认对齐方式,数据从第二行开始
if
(
alignments
.
length
===
0
)
{
alignments
=
headerCells
.
map
(()
=>
'
left
'
);
dataStartIndex
=
1
;
// 对于无分隔线表格,数据从第二行开始
}
// 解析数据行
const
dataRows
:
string
[][]
=
[];
for
(
let
i
=
dataStartIndex
;
i
<
this
.
tableData
.
length
;
i
++
)
{
const
row
=
this
.
tableData
[
i
];
const
cells
=
row
.
split
(
'
|
'
).
slice
(
1
,
-
1
).
map
(
cell
=>
cell
.
trim
());
dataRows
.
push
(
cells
);
}
// 生成完整表格HTML
return
generateCompleteTable
(
headerCells
,
alignments
,
dataRows
);
}
catch
(
error
)
{
console
.
warn
(
'
表格生成失败:
'
,
error
);
return
'
<div class="table-error" style="border: 1px solid #ff6b6b; padding: 10px; background-color: #ffeaea; margin: 10px 0;">表格渲染失败</div>
'
;
}
}
/**
* 重置表格处理器状态
*/
private
reset
():
void
{
this
.
tableData
=
[];
this
.
isInTable
=
false
;
}
/**
* 获取当前表格状态
*/
public
getTableState
():
{
isInTable
:
boolean
;
tableData
:
string
[]
}
{
return
{
isInTable
:
this
.
isInTable
,
tableData
:
[...
this
.
tableData
]
};
}
}
// 创建全局表格处理器实例
const
streamingTableProcessor
=
new
StreamingTableProcessor
();
/**
* 处理流式Markdown文本(专门用于SSE逐条返回的数据)
* @param text 新的文本内容
* @returns 处理后的HTML内容
*/
export
const
processStreamingMarkdown
=
(
text
:
string
):
string
=>
{
// 清理HTML标签,防止XSS攻击
text
=
text
.
replace
(
/<script
\b[^
<
]
*
(?:(?!
<
\/
script>
)
<
[^
<
]
*
)
*<
\/
script>/gi
,
''
);
text
=
text
.
replace
(
/<
[^
>
]
*>/g
,
''
);
// 使用流式表格处理器处理文本
const
result
=
streamingTableProcessor
.
processText
(
text
);
if
(
result
===
null
)
{
// 表格行被收集,不立即渲染
return
''
;
}
// 如果表格处理器返回了完整表格,直接返回
if
(
result
.
includes
(
'
<table
'
))
{
return
result
;
}
// 否则,处理其他Markdown语法
return
parseMarkdown
(
result
);
};
/**
/**
* 检查最后一个contentBlock是否是markdown块
* 检查最后一个contentBlock是否是markdown块
* @param contentBlocks 内容块数组
* @param contentBlocks 内容块数组
...
...
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment