feat(markdown): 支持 Markdown 表格渲染

- 在 markdown.js 中添加表格检测和渲染逻辑
- 支持标准格式 (| cell |) 和简化格式 (cell | cell)
- 添加 renderTable() 辅助函数处理表格解析
- 在 chat.css 中添加美观的表格样式
- 表头高亮、斑马纹、悬停效果
This commit is contained in:
22605
2026-03-19 11:24:24 +08:00
parent 8485df7fab
commit 12040be868
2 changed files with 122 additions and 0 deletions

View File

@@ -89,6 +89,8 @@ export function renderMarkdown(text) {
const result = []
let inList = false
let listType = ''
let inTable = false
let tableRows = []
for (let i = 0; i < lines.length; i++) {
let line = lines[i]
@@ -100,6 +102,38 @@ export function renderMarkdown(text) {
continue
}
// 表格检测:表头分隔行 (|---|...|)
const isTableSeparator = /^\s*\|[\s\-:|]+\|\s*$/.test(line) ||
/^\s*[\-:]+(\s*\|\s*[\-:]+)+\s*$/.test(line)
// 检测是否可能是表格行
const isTableRow = /^\s*\|.*\|\s*$/.test(line) ||
/^\s*[^\|]+\s*\|\s*[^\|]+/.test(line)
// 如果在表格中,继续收集行
if (inTable) {
if (isTableRow && line.trim() !== '') {
tableRows.push(line)
continue
} else {
// 表格结束,渲染表格
result.push(renderTable(tableRows))
inTable = false
tableRows = []
}
}
// 检测表格开始:当前行是表格行,且下一行是分隔行
if (!inTable && isTableRow && i + 1 < lines.length) {
const nextLine = lines[i + 1]
if (/^\s*\|[\s\-:|]+\|\s*$/.test(nextLine) ||
/^\s*[\-:]+(\s*\|\s*[\-:]+)+\s*$/.test(nextLine)) {
inTable = true
tableRows.push(line)
continue
}
}
// 标题
const headingMatch = line.match(/^(#{1,3})\s+(.+)$/)
if (headingMatch) {
@@ -138,9 +172,69 @@ export function renderMarkdown(text) {
}
if (inList) result.push(`</${listType}>`)
// 处理剩余的表格
if (inTable && tableRows.length > 0) {
result.push(renderTable(tableRows))
}
return result.join('\n')
}
/**
* 渲染 Markdown 表格
* @param {string[]} rows - 表格行数组
* @returns {string} HTML 表格
*/
function renderTable(rows) {
if (!rows || rows.length < 2) return ''
const table = ['<table>']
let isHeaderRow = true
let hasSeparator = false
for (let i = 0; i < rows.length; i++) {
let row = rows[i].trim()
// 跳过空行
if (!row) continue
// 检测分隔行 (|---|...|)
const isSeparator = /^\s*\|[\s\-:|]+\|\s*$/.test(row) ||
/^\s*[\-:]+(\s*\|\s*[\-:]+)+\s*$/.test(row)
if (isSeparator) {
hasSeparator = true
continue
}
// 解析单元格
let cells = []
if (row.startsWith('|') && row.endsWith('|')) {
// 标准格式: | cell1 | cell2 |
cells = row.slice(1, -1).split('|')
} else {
// 简化格式: cell1 | cell2
cells = row.split('|')
}
// 清理单元格内容
cells = cells.map(cell => inlineFormat(cell.trim()))
if (cells.length === 0) continue
// 渲染行
const tag = isHeaderRow && !hasSeparator && i === 0 ? 'th' : 'td'
table.push(' <tr>')
cells.forEach(cell => {
table.push(` <${tag}>${cell}</${tag}>`)
})
table.push(' </tr>')
// 第一行后切换到数据行(如果有分隔行)
if (hasSeparator && i === 0) {
isHeaderRow = false
}
}
table.push('</table>')
return table.join('\n')
}
function inlineFormat(text) {
return text
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')

View File

@@ -304,6 +304,34 @@
.hl-func { color: #61afef; }
.hl-type { color: #e5c07b; }
/* Markdown 表格 */
.msg-ai .msg-bubble table {
width: 100%;
border-collapse: collapse;
margin: 8px 0;
font-size: 13px;
}
.msg-ai .msg-bubble th,
.msg-ai .msg-bubble td {
border: 1px solid var(--border);
padding: 8px 12px;
text-align: left;
}
.msg-ai .msg-bubble th {
background: var(--bg-tertiary);
font-weight: 600;
}
.msg-ai .msg-bubble tr:nth-child(even) {
background: var(--bg-hover);
}
.msg-ai .msg-bubble tr:hover {
background: rgba(99, 102, 241, 0.08);
}
/* 行内代码 */
.msg-ai .msg-bubble > code {
background: var(--bg-hover, rgba(255,255,255,0.08));