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:
QihuAI Design committed 2026-07-16 16:15:29 +08:00
1 parent 845355e4e7
commit 8486a2a0e5
2 files changed
+99

No files matched your search

+61
View File
@@ -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);
+38
View File
@@ -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);