ui(chat): MarkdownContent 支持表格渲染
AI 聊天回复中常出现 Markdown 表格语法 (| h1 | h2 | + |---|---| ), 但 MarkdownContent 仅支持 headers/lists/code,未渲染表格, 显示为原始 | 分隔文本。 修复: 1. ChatPage.tsx MarkdownContent 新增表格解析步骤(步骤 2.7) - 匹配 | header | |---|---| 的多行表格块 - 解析对齐方式 (:---, :---:, ---:) - 生成 <table><thead><tbody> HTML 2. 步骤 10 恢复占位符,步骤 11 解包 <table> 不被 <p> 包裹 3. ChatPage.css 新增 .md-table 样式 - 蓝色渐变表头 + 斑马纹 + hover 高亮 - 用户消息气泡内深色适配
This commit is contained in:
1 parent
845355e4e7
commit
8486a2a0e5
2 files changed
+99
No files matched your search
@@ -446,6 +446,67 @@
|
|||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.md-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 12px 0;
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
background: #fff;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-table thead {
|
||||||
|
background: linear-gradient(135deg, #5B5FED 0%, #7B7FEE 100%);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-table th {
|
||||||
|
padding: 10px 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: left;
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-table td {
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-bottom: 1px solid #EEF0F4;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-table tbody tr:last-child td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-table tbody tr:hover {
|
||||||
|
background: #F8F9FC;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-table tbody tr:nth-child(even) {
|
||||||
|
background: #FAFBFD;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-table tbody tr:nth-child(even):hover {
|
||||||
|
background: #F0F2F7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-bubble.user .md-table {
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
.message-bubble.user .md-table th,
|
||||||
|
.message-bubble.user .md-table td {
|
||||||
|
color: inherit;
|
||||||
|
border-color: rgba(255, 255, 255, 0.15);
|
||||||
|
}
|
||||||
|
.message-bubble.user .md-table tbody tr:hover,
|
||||||
|
.message-bubble.user .md-table tbody tr:nth-child(even) {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
.md-hr {
|
.md-hr {
|
||||||
border: none;
|
border: none;
|
||||||
border-top: 1px solid var(--color-border);
|
border-top: 1px solid var(--color-border);
|
||||||
|
|||||||
@@ -173,6 +173,42 @@ const MarkdownContent: React.FC<{ content: string }> = ({ content }) => {
|
|||||||
return '\x00MEDIA' + (mediaBlocks.length - 1) + '\x00';
|
return '\x00MEDIA' + (mediaBlocks.length - 1) + '\x00';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 2.7 Extract Markdown tables: | h1 | h2 |\n|---|---| ... |
|
||||||
|
// 把整段表格替换为占位符,表格内容(已转义)放入 tableBlocks 数组
|
||||||
|
const tableBlocks: string[] = [];
|
||||||
|
text = text.replace(
|
||||||
|
/((?:^\|.+\|\r?\n)+^\|[\s\-:|]+\|\r?\n(?:^\|.+\|\r?\n?)+)/gm,
|
||||||
|
(_m: string, block: string) => {
|
||||||
|
const lines = block.trim().split(/\r?\n/).filter((l: string) => l.trim());
|
||||||
|
if (lines.length < 2) return block;
|
||||||
|
const headerCells = lines[0].split('|').slice(1, -1).map((c: string) => c.trim());
|
||||||
|
const alignCells = lines[1].split('|').slice(1, -1).map((c: string) => {
|
||||||
|
const t = c.trim();
|
||||||
|
const left = t.startsWith(':');
|
||||||
|
const right = t.endsWith(':');
|
||||||
|
if (left && right) return 'center';
|
||||||
|
if (right) return 'right';
|
||||||
|
if (left) return 'left';
|
||||||
|
return 'left';
|
||||||
|
});
|
||||||
|
const bodyRows = lines.slice(2).map((row: string) =>
|
||||||
|
row.split('|').slice(1, -1).map((c: string) => c.trim())
|
||||||
|
);
|
||||||
|
const thead = headerCells.map((c: string, i: number) =>
|
||||||
|
`<th style="text-align:${alignCells[i] || 'left'}">${c}</th>`
|
||||||
|
).join('');
|
||||||
|
const tbody = bodyRows.map((r: string[]) =>
|
||||||
|
'<tr>' + r.map((c: string, i: number) =>
|
||||||
|
`<td style="text-align:${alignCells[i] || 'left'}">${c}</td>`
|
||||||
|
).join('') + '</tr>'
|
||||||
|
).join('');
|
||||||
|
const html = `<table class="md-table"><thead><tr>${thead}</tr></thead><tbody>${tbody}</tbody></table>`;
|
||||||
|
const idx = tableBlocks.length;
|
||||||
|
tableBlocks.push(html);
|
||||||
|
return '\x00TABLE' + idx + '\x00';
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// 3. Headers
|
// 3. Headers
|
||||||
text = text.replace(/^#### (.+)$/gm, '<h4 class="md-h4">$1</h4>');
|
text = text.replace(/^#### (.+)$/gm, '<h4 class="md-h4">$1</h4>');
|
||||||
text = text.replace(/^### (.+)$/gm, '<h4 class="md-h4">$1</h4>');
|
text = text.replace(/^### (.+)$/gm, '<h4 class="md-h4">$1</h4>');
|
||||||
@@ -206,6 +242,7 @@ const MarkdownContent: React.FC<{ content: string }> = ({ content }) => {
|
|||||||
// 10. Restore code blocks and inline codes
|
// 10. Restore code blocks and inline codes
|
||||||
text = text.replace(/\x00CODEBLOCK(\d+)\x00/g, (_m: string, idx: string) => codeBlocks[parseInt(idx)] || '');
|
text = text.replace(/\x00CODEBLOCK(\d+)\x00/g, (_m: string, idx: string) => codeBlocks[parseInt(idx)] || '');
|
||||||
text = text.replace(/\x00INLINE(\d+)\x00/g, (_m: string, idx: string) => inlineCodes[parseInt(idx)] || '');
|
text = text.replace(/\x00INLINE(\d+)\x00/g, (_m: string, idx: string) => inlineCodes[parseInt(idx)] || '');
|
||||||
|
text = text.replace(/\x00TABLE(\d+)\x00/g, (_m: string, idx: string) => tableBlocks[parseInt(idx)] || '');
|
||||||
|
|
||||||
// 11. Wrap in paragraph and clean up
|
// 11. Wrap in paragraph and clean up
|
||||||
text = '<p class="md-p">' + text + '</p>';
|
text = '<p class="md-p">' + text + '</p>';
|
||||||
@@ -215,6 +252,7 @@ const MarkdownContent: React.FC<{ content: string }> = ({ content }) => {
|
|||||||
text = text.replace(/<p class="md-p">(<ul[\s\S]*?<\/ul>)<\/p>/g, '$1');
|
text = text.replace(/<p class="md-p">(<ul[\s\S]*?<\/ul>)<\/p>/g, '$1');
|
||||||
text = text.replace(/<p class="md-p">(<hr[\s\S]*?)<\/p>/g, '$1');
|
text = text.replace(/<p class="md-p">(<hr[\s\S]*?)<\/p>/g, '$1');
|
||||||
text = text.replace(/<p class="md-p">(<blockquote[\s\S]*?<\/blockquote>)<\/p>/g, '$1');
|
text = text.replace(/<p class="md-p">(<blockquote[\s\S]*?<\/blockquote>)<\/p>/g, '$1');
|
||||||
|
text = text.replace(/<p class="md-p">(<table[\s\S]*?<\/table>)<\/p>/g, '$1');
|
||||||
|
|
||||||
// 12. 拆分为文本片段与媒体组件
|
// 12. 拆分为文本片段与媒体组件
|
||||||
const parts = text.split(/(\x00MEDIA\d+\x00)/g);
|
const parts = text.split(/(\x00MEDIA\d+\x00)/g);
|
||||||
|
|||||||
Reference in new issue
Block a user