feat(ai-chat): 增强Markdown解析及调整聊天气泡布局
This commit is contained in:
@@ -1126,20 +1126,190 @@ async loadChatHistoryIfExist() {
|
||||
}
|
||||
return `感谢您的咨询!关于"${userMessage}"的问题,我们的专业客服会尽快为您解答。您也可以查看我们的帮助文档获取更多信息。`;
|
||||
},
|
||||
|
||||
|
||||
// 解析Markdown
|
||||
|
||||
|
||||
// 增强版Markdown解析(无第三方依赖)
|
||||
parseMarkdown(content) {
|
||||
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
|
||||
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>') // 粗体
|
||||
.replace(/\*(.*?)\*/g, '<em>$1</em>') // 斜体
|
||||
.replace(/`(.*?)`/g, '<code>$1</code>') // 代码
|
||||
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>') // 链接
|
||||
.replace(/\n/g, '<br>') // 换行
|
||||
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
|
||||
// 2. 提取代码块(保护其内容不被后续处理污染)
|
||||
const codeBlocks = []
|
||||
html = html.replace(/```(\w*)\n?([\s\S]*?)```/g, (m, lang, code) => {
|
||||
const idx = codeBlocks.length
|
||||
const safe = code.replace(/</g, '<').replace(/>/g, '>')
|
||||
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
|
||||
},
|
||||
|
||||
@@ -2045,7 +2215,7 @@ $radius-lg: 36rpx;
|
||||
.message-content {
|
||||
flex: 1;
|
||||
margin-left: 24rpx;
|
||||
max-width: calc(100% - 112rpx);
|
||||
max-width: calc(100% - 96rpx);
|
||||
min-width: 0;
|
||||
width: 0; /* 添加这个属性防止flex元素溢出 */
|
||||
|
||||
@@ -2543,7 +2713,6 @@ $radius-lg: 36rpx;
|
||||
height: auto;
|
||||
padding: 16rpx 20rpx; /* 微信风格内边距 */
|
||||
display: inline-block;
|
||||
max-width: 70%; /* 微信风格宽度 */
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.5;
|
||||
@@ -2580,7 +2749,6 @@ $radius-lg: 36rpx;
|
||||
height: auto;
|
||||
padding: 16rpx 20rpx; /* 微信风格内边距 */
|
||||
display: inline-block;
|
||||
max-width: 70%; /* 微信风格宽度 */
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.5;
|
||||
@@ -3513,7 +3681,7 @@ $radius-lg: 36rpx;
|
||||
|
||||
.message-content {
|
||||
margin-left: 16rpx;
|
||||
max-width: calc(100% - 88rpx);
|
||||
max-width: calc(100% - 72rpx);
|
||||
|
||||
.message-bubble {
|
||||
padding: 16rpx 24rpx;
|
||||
|
||||
Reference in New Issue
Block a user