From 8486a2a0e5bfadc15b2b06916c6a1944afdd4b93 Mon Sep 17 00:00:00 2001 From: QihuAI Design Date: Thu, 16 Jul 2026 16:15:29 +0800 Subject: [PATCH] =?UTF-8?q?ui(chat):=20MarkdownContent=20=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E8=A1=A8=E6=A0=BC=E6=B8=B2=E6=9F=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AI 聊天回复中常出现 Markdown 表格语法 (| h1 | h2 | + |---|---| ), 但 MarkdownContent 仅支持 headers/lists/code,未渲染表格, 显示为原始 | 分隔文本。 修复: 1. ChatPage.tsx MarkdownContent 新增表格解析步骤(步骤 2.7) - 匹配 | header | |---|---| 的多行表格块 - 解析对齐方式 (:---, :---:, ---:) - 生成 HTML 2. 步骤 10 恢复占位符,步骤 11 解包
不被

包裹 3. ChatPage.css 新增 .md-table 样式 - 蓝色渐变表头 + 斑马纹 + hover 高亮 - 用户消息气泡内深色适配 --- frontend/src/pages/front/ChatPage.css | 61 +++++++++++++++++++++++++++ frontend/src/pages/front/ChatPage.tsx | 38 +++++++++++++++++ 2 files changed, 99 insertions(+) diff --git a/frontend/src/pages/front/ChatPage.css b/frontend/src/pages/front/ChatPage.css index 0ac13ea..0ce673f 100644 --- a/frontend/src/pages/front/ChatPage.css +++ b/frontend/src/pages/front/ChatPage.css @@ -446,6 +446,67 @@ 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 { border: none; border-top: 1px solid var(--color-border); diff --git a/frontend/src/pages/front/ChatPage.tsx b/frontend/src/pages/front/ChatPage.tsx index 7bd0c20..337998f 100644 --- a/frontend/src/pages/front/ChatPage.tsx +++ b/frontend/src/pages/front/ChatPage.tsx @@ -173,6 +173,42 @@ const MarkdownContent: React.FC<{ content: string }> = ({ content }) => { 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) => + `

` + ).join(''); + const tbody = bodyRows.map((r: string[]) => + '' + r.map((c: string, i: number) => + `` + ).join('') + '' + ).join(''); + const html = `
${c}
${c}
${thead}${tbody}
`; + const idx = tableBlocks.length; + tableBlocks.push(html); + return '\x00TABLE' + idx + '\x00'; + } + ); + // 3. Headers text = text.replace(/^#### (.+)$/gm, '

$1

'); text = text.replace(/^### (.+)$/gm, '

$1

'); @@ -206,6 +242,7 @@ const MarkdownContent: React.FC<{ content: string }> = ({ content }) => { // 10. Restore code blocks and inline codes 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(/\x00TABLE(\d+)\x00/g, (_m: string, idx: string) => tableBlocks[parseInt(idx)] || ''); // 11. Wrap in paragraph and clean up text = '

' + text + '

'; @@ -215,6 +252,7 @@ const MarkdownContent: React.FC<{ content: string }> = ({ content }) => { text = text.replace(/

()<\/p>/g, '$1'); text = text.replace(/

(/g, '$1'); text = text.replace(/

()<\/p>/g, '$1'); + text = text.replace(/

()<\/p>/g, '$1'); // 12. 拆分为文本片段与媒体组件 const parts = text.split(/(\x00MEDIA\d+\x00)/g);