feat(ai-chat): 增强Markdown解析及调整聊天气泡布局
This commit is contained in:
@@ -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;
|
||||||
@@ -1128,17 +1128,187 @@ async loadChatHistoryIfExist() {
|
|||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
// 解析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, '&')
|
||||||
.replace(/\*(.*?)\*/g, '<em>$1</em>') // 斜体
|
.replace(/</g, '<')
|
||||||
.replace(/`(.*?)`/g, '<code>$1</code>') // 代码
|
.replace(/>/g, '>')
|
||||||
.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, '<').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
|
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;
|
||||||
|
|||||||
Reference in New Issue
Block a user