feat(ai-chat): 增强Markdown解析及调整聊天气泡布局

This commit is contained in:
2026-05-12 08:30:26 +08:00
parent e16402c9c1
commit 875cf0b428
2 changed files with 184 additions and 16 deletions

View File

@@ -62,5 +62,5 @@ const localDevConfig = ({
uniacid: 2075, uniacid: 2075,
domain: 'https://xcx.aigc-quickapp.com/', domain: 'https://xcx.aigc-quickapp.com/',
}, },
})['2811']; // 选择要使用的环境配置 })['1']; // 选择要使用的环境配置
export default localDevConfig; export default localDevConfig;

View File

@@ -1126,20 +1126,190 @@ async loadChatHistoryIfExist() {
} }
return `感谢您的咨询!关于"${userMessage}"的问题,我们的专业客服会尽快为您解答。您也可以查看我们的帮助文档获取更多信息。`; return `感谢您的咨询!关于"${userMessage}"的问题,我们的专业客服会尽快为您解答。您也可以查看我们的帮助文档获取更多信息。`;
}, },
// 解析Markdown // 增强版Markdown解析(无第三方依赖)
parseMarkdown(content) { parseMarkdown(content) {
if (!content) return '' if (!content) return ''
// 简单的Markdown解析 // 辅助:内联格式处理
const parseInline = (text) => {
return text
// 行内图片
.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1" style="max-width:100%;border-radius:8rpx;margin:8rpx 0;display:block;" />')
// 链接
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" style="color:#4a90d9;text-decoration:none;">$1</a>')
// 行内代码
.replace(/`([^`]+)`/g, '<code style="background:#f0f4f8;padding:2rpx 10rpx;border-radius:6rpx;font-size:24rpx;font-family:monospace;color:#d63384;">$1</code>')
// 粗体(优先匹配**)
.replace(/\*\*(.+?)\*\*/g, '<strong style="font-weight:700;">$1</strong>')
// 斜体
.replace(/\*(.+?)\*/g, '<em style="font-style:italic;">$1</em>')
// 删除线
.replace(/~~(.+?)~~/g, '<del style="text-decoration:line-through;color:#999;">$1</del>')
// 手机号
.replace(/\b(1[3-9]\d{9})\b/g, '<a href="tel:$1" style="color:#4a90d9;text-decoration:none;">$1</a>')
// 400电话
.replace(/\b(400[\d-]{7,10})\b/g, '<a href="tel:$1" style="color:#4a90d9;text-decoration:none;">$1</a>')
}
// 1. 转义HTML特殊字符
let html = content let html = content
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>') // 粗体 .replace(/&/g, '&amp;')
.replace(/\*(.*?)\*/g, '<em>$1</em>') // 斜体 .replace(/</g, '&lt;')
.replace(/`(.*?)`/g, '<code>$1</code>') // 代码 .replace(/>/g, '&gt;')
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>') // 链接
.replace(/\n/g, '<br>') // 换行 // 2. 提取代码块(保护其内容不被后续处理污染)
const codeBlocks = []
html = html.replace(/```(\w*)\n?([\s\S]*?)```/g, (m, lang, code) => {
const idx = codeBlocks.length
const safe = code.replace(/</g, '&lt;').replace(/>/g, '&gt;')
codeBlocks.push(
`<pre style="background:#f5f5f5;padding:24rpx;border-radius:8rpx;overflow-x:auto;font-size:24rpx;line-height:1.6;margin:16rpx 0;white-space:pre-wrap;">` +
`<code style="background:transparent;padding:0;font-family:monospace;color:#333;font-size:24rpx;">${safe}</code></pre>`
)
return `%%CODEBLOCK_${idx}%%`
})
// 3. 按行处理,用 <br> 连接单换行,双换行分割段落块
const lines = html.split('\n')
const out = []
let inList = false
let buf = [] // 普通文本行缓冲区
let inTable = false
let tableRows = [] // 表格行缓冲区
const closeList = () => {
if (inList === 'ul') { inList = false; return '</ul>' }
if (inList === 'ol') { inList = false; return '</ol>' }
return ''
}
const flushBuf = () => {
if (buf.length === 0) return
// 去掉首尾的空串,避免产生尾随 <br>
while (buf.length > 0 && buf[0] === '') buf.shift()
while (buf.length > 0 && buf[buf.length - 1] === '') buf.pop()
if (buf.length === 0) return
const content = buf.map(l => parseInline(l)).join('<br>')
out.push(`<div style="line-height:1.8;font-size:26rpx;">${content}</div>`)
buf = []
}
const flushTable = () => {
if (!inTable || tableRows.length === 0) return
const thead = tableRows[0].map(c =>
`<th style="padding:8rpx 12rpx;border:1rpx solid #ddd;font-weight:600;background:#f5f5f5;text-align:left;font-size:24rpx;">${parseInline(c)}</th>`
).join('')
const tbody = tableRows.slice(1).map(row =>
'<tr>' + row.map(c =>
`<td style="padding:8rpx 12rpx;border:1rpx solid #ddd;line-height:1.6;font-size:24rpx;">${parseInline(c)}</td>`
).join('') + '</tr>'
).join('')
out.push(
`<table style="border-collapse:collapse;margin:8rpx 0;width:100%;font-size:24rpx;">` +
`<thead><tr>${thead}</tr></thead><tbody>${tbody}</tbody></table>`
)
tableRows = []
inTable = false
}
for (let i = 0; i < lines.length; i++) {
const line = lines[i]
const trimmed = line.trim()
// 代码块占位符
if (/^%%CODEBLOCK_\d+%%$/.test(trimmed)) {
flushBuf(); flushTable(); out.push(closeList())
out.push(line)
continue
}
// 表格行(以 | 开头)
if (trimmed.charAt(0) === '|') {
flushBuf(); out.push(closeList())
// 去除首尾空 cell
const rawCells = trimmed.split('|').filter((c, j, a) => j > 0 && j < a.length - 1)
if (rawCells.length === 0) continue
// 跳过分隔行 | --- | :--- | :---: | ---: |
const isSep = rawCells.every(c => /^[\s:\-|]+$/.test(c.trim()))
if (isSep) continue
if (!inTable) { inTable = true; tableRows = [] }
tableRows.push(rawCells.map(c => c.trim()))
continue
}
// 非表格行 → 刷新已积累的表格
if (inTable) flushTable()
// 标题
const hMatch = line.match(/^(#{1,6})\s+(.+)$/)
if (hMatch) {
flushBuf(); out.push(closeList())
const lv = hMatch[1].length
const size = ['', '40rpx', '36rpx', '32rpx', '28rpx', '26rpx', '24rpx'][lv]
const wgt = ['', '700', '700', '600', '600', '500', '500'][lv]
const mg = ['', '20rpx 0 10rpx', '16rpx 0 8rpx', '14rpx 0 6rpx', '12rpx 0 6rpx', '10rpx 0 4rpx', '8rpx 0 4rpx'][lv]
out.push(`<h${lv} style="font-size:${size};font-weight:${wgt};margin:${mg};line-height:1.5;">${parseInline(hMatch[2])}</h${lv}>`)
continue
}
// 分隔线
if (/^(-{3,}|\*{3,}|_{3,})\s*$/.test(trimmed)) {
flushBuf(); out.push(closeList())
out.push('<hr style="border:none;border-top:2rpx solid #e8e8e8;margin:20rpx 0;">')
continue
}
// 引用
const bqMatch = line.match(/^>\s?(.+)$/)
if (bqMatch) {
flushBuf(); out.push(closeList())
out.push(`<blockquote style="border-left:4rpx solid #4a90d9;padding:10rpx 20rpx;margin:8rpx 0;background:#f8f9fa;color:#666;line-height:1.8;border-radius:0 6rpx 6rpx 0;">${parseInline(bqMatch[1])}</blockquote>`)
continue
}
// 无序列表
const ulMatch = line.match(/^[-*+]\s+(.+)$/)
if (ulMatch) {
flushBuf()
if (!inList) { out.push('<ul style="padding-left:36rpx;margin:6rpx 0;">'); inList = 'ul' }
out.push(`<li style="margin:3rpx 0;line-height:1.8;">${parseInline(ulMatch[1])}</li>`)
continue
}
// 有序列表
const olMatch = line.match(/^\d+\.\s+(.+)$/)
if (olMatch) {
flushBuf()
if (!inList) { out.push('<ol style="padding-left:36rpx;margin:6rpx 0;">'); inList = 'ol' }
out.push(`<li style="margin:3rpx 0;line-height:1.8;">${parseInline(olMatch[1])}</li>`)
continue
}
// 空行 → 加入缓冲区作段落分隔(<br><br>),同时关闭列表
if (trimmed === '') {
// 关闭列表(标准Markdown行为)
if (inList) {
flushBuf()
out.push(closeList())
}
// 避免连续空行推入多个空串
if (buf.length > 0 && buf[buf.length - 1] !== '') {
buf.push('')
}
continue
}
// 普通文本行 => 加入缓冲区
buf.push(line)
}
// 扫尾
flushBuf(); flushTable(); out.push(closeList())
html = out.join('')
// 4. 还原代码块
html = html.replace(/%%CODEBLOCK_(\d+)%%/g, (m, idx) => codeBlocks[parseInt(idx)])
return html return html
}, },
@@ -2045,7 +2215,7 @@ $radius-lg: 36rpx;
.message-content { .message-content {
flex: 1; flex: 1;
margin-left: 24rpx; margin-left: 24rpx;
max-width: calc(100% - 112rpx); max-width: calc(100% - 96rpx);
min-width: 0; min-width: 0;
width: 0; /* 添加这个属性防止flex元素溢出 */ width: 0; /* 添加这个属性防止flex元素溢出 */
@@ -2543,7 +2713,6 @@ $radius-lg: 36rpx;
height: auto; height: auto;
padding: 16rpx 20rpx; /* 微信风格内边距 */ padding: 16rpx 20rpx; /* 微信风格内边距 */
display: inline-block; display: inline-block;
max-width: 70%; /* 微信风格宽度 */
word-break: break-word; word-break: break-word;
white-space: pre-wrap; white-space: pre-wrap;
line-height: 1.5; line-height: 1.5;
@@ -2580,7 +2749,6 @@ $radius-lg: 36rpx;
height: auto; height: auto;
padding: 16rpx 20rpx; /* 微信风格内边距 */ padding: 16rpx 20rpx; /* 微信风格内边距 */
display: inline-block; display: inline-block;
max-width: 70%; /* 微信风格宽度 */
word-break: break-word; word-break: break-word;
white-space: pre-wrap; white-space: pre-wrap;
line-height: 1.5; line-height: 1.5;
@@ -3513,7 +3681,7 @@ $radius-lg: 36rpx;
.message-content { .message-content {
margin-left: 16rpx; margin-left: 16rpx;
max-width: calc(100% - 88rpx); max-width: calc(100% - 72rpx);
.message-bubble { .message-bubble {
padding: 16rpx 24rpx; padding: 16rpx 24rpx;