支持markdown格式显示,效果
This commit is contained in:
@@ -74,7 +74,21 @@
|
||||
<text class="qa-copy-btn-icon">⧉</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="qa-msg-text" selectable="true">{{ m.text }}</text>
|
||||
<!-- 用户消息仍按纯文本展示 -->
|
||||
<text
|
||||
v-if="m.role === 'user'"
|
||||
class="qa-msg-text"
|
||||
selectable="true"
|
||||
>{{ m.text }}</text>
|
||||
<!-- AI 消息支持 Markdown 渲染,保留纯文本兜底 -->
|
||||
<view
|
||||
v-else
|
||||
class="qa-msg-text qa-msg-text-markdown"
|
||||
>
|
||||
<rich-text
|
||||
:nodes="renderMarkdownNodes(m.text)"
|
||||
></rich-text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
@@ -601,7 +615,16 @@ export default {
|
||||
parsed.content ??
|
||||
parsed.answer ??
|
||||
parsed.text ??
|
||||
parsed.data ??
|
||||
parsed.result ??
|
||||
parsed.message ??
|
||||
parsed.output ??
|
||||
parsed.token ??
|
||||
'';
|
||||
// 若对象结构未知,至少回退到原始行,避免“有返回但前端无内容”
|
||||
if ((textToAppend === undefined || textToAppend === null || String(textToAppend) === '') && raw) {
|
||||
textToAppend = raw;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// 非 JSON 分片按纯文本处理
|
||||
@@ -850,6 +873,151 @@ export default {
|
||||
return String(value);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 非常轻量的 Markdown 转换,仅支持:
|
||||
* - 换行 => <br/>
|
||||
* - **加粗** => <strong>
|
||||
* - *斜体* => <em>
|
||||
* - 无序列表行 "- " => <ul><li>
|
||||
* - 代码块 ``` => <pre><code>
|
||||
* 复杂 Markdown 语法暂不完全覆盖,但足够保证结构清晰。
|
||||
*/
|
||||
parseInlineMarkdownToNodes(line) {
|
||||
const text = String(line || '');
|
||||
const nodes = [];
|
||||
const tokenRe = /(\*\*[^*]+\*\*|`[^`\n]+`|\*[^*\n]+\*)/g;
|
||||
let lastIndex = 0;
|
||||
let match = null;
|
||||
|
||||
while ((match = tokenRe.exec(text)) !== null) {
|
||||
const idx = match.index;
|
||||
if (idx > lastIndex) {
|
||||
nodes.push({ type: 'text', text: text.slice(lastIndex, idx) });
|
||||
}
|
||||
const token = match[0];
|
||||
if (token.startsWith('**') && token.endsWith('**')) {
|
||||
nodes.push({
|
||||
name: 'strong',
|
||||
attrs: { style: 'font-weight:600;' },
|
||||
children: [{ type: 'text', text: token.slice(2, -2) }]
|
||||
});
|
||||
} else if (token.startsWith('`') && token.endsWith('`')) {
|
||||
nodes.push({
|
||||
name: 'span',
|
||||
attrs: {
|
||||
style: 'font-family:monospace;background:#f2f2f4;padding:1px 4px;border-radius:3px;'
|
||||
},
|
||||
children: [{ type: 'text', text: token.slice(1, -1) }]
|
||||
});
|
||||
} else if (token.startsWith('*') && token.endsWith('*')) {
|
||||
nodes.push({
|
||||
name: 'em',
|
||||
attrs: { style: 'font-style:italic;' },
|
||||
children: [{ type: 'text', text: token.slice(1, -1) }]
|
||||
});
|
||||
} else {
|
||||
nodes.push({ type: 'text', text: token });
|
||||
}
|
||||
lastIndex = idx + token.length;
|
||||
}
|
||||
|
||||
if (lastIndex < text.length) {
|
||||
nodes.push({ type: 'text', text: text.slice(lastIndex) });
|
||||
}
|
||||
if (nodes.length === 0) {
|
||||
nodes.push({ type: 'text', text: '' });
|
||||
}
|
||||
return nodes;
|
||||
},
|
||||
renderMarkdownNodes(text) {
|
||||
try {
|
||||
const raw = String(text || '');
|
||||
const lines = raw.replace(/\r\n/g, '\n').split('\n');
|
||||
const nodes = [];
|
||||
let i = 0;
|
||||
let inCode = false;
|
||||
let codeLines = [];
|
||||
|
||||
while (i < lines.length) {
|
||||
const line = lines[i];
|
||||
|
||||
// 代码块围栏
|
||||
if (line.trim().startsWith('```')) {
|
||||
if (!inCode) {
|
||||
inCode = true;
|
||||
codeLines = [];
|
||||
} else {
|
||||
inCode = false;
|
||||
nodes.push({
|
||||
name: 'div',
|
||||
attrs: {
|
||||
style: 'background:#f7f7f9;border-radius:6px;padding:8px 10px;font-family:monospace;white-space:pre-wrap;word-break:break-word;'
|
||||
},
|
||||
children: [{ type: 'text', text: codeLines.join('\n') }]
|
||||
});
|
||||
}
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inCode) {
|
||||
codeLines.push(line);
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 无序列表
|
||||
if (/^\s*-\s+/.test(line)) {
|
||||
const ulChildren = [];
|
||||
while (i < lines.length && /^\s*-\s+/.test(lines[i])) {
|
||||
const content = lines[i].replace(/^\s*-\s+/, '');
|
||||
ulChildren.push({
|
||||
name: 'li',
|
||||
attrs: { style: 'margin:4px 0;' },
|
||||
children: this.parseInlineMarkdownToNodes(content)
|
||||
});
|
||||
i += 1;
|
||||
}
|
||||
nodes.push({
|
||||
name: 'ul',
|
||||
attrs: { style: 'padding-left:18px;margin:6px 0;' },
|
||||
children: ulChildren
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// 空行
|
||||
if (!line.trim()) {
|
||||
nodes.push({ name: 'div', children: [{ type: 'text', text: ' ' }] });
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 普通段落
|
||||
nodes.push({
|
||||
name: 'div',
|
||||
attrs: { style: 'line-height:1.6;margin:2px 0;word-break:break-word;' },
|
||||
children: this.parseInlineMarkdownToNodes(line)
|
||||
});
|
||||
i += 1;
|
||||
}
|
||||
|
||||
// 兜底:如果代码块未闭合,也照样展示
|
||||
if (inCode && codeLines.length > 0) {
|
||||
nodes.push({
|
||||
name: 'div',
|
||||
attrs: {
|
||||
style: 'background:#f7f7f9;border-radius:6px;padding:8px 10px;font-family:monospace;white-space:pre-wrap;word-break:break-word;'
|
||||
},
|
||||
children: [{ type: 'text', text: codeLines.join('\n') }]
|
||||
});
|
||||
}
|
||||
|
||||
return nodes;
|
||||
} catch (e) {
|
||||
return [{ type: 'text', text: String(text || '') }];
|
||||
}
|
||||
},
|
||||
onSend() {
|
||||
const text = (this.message || '').trim();
|
||||
if (!text) {
|
||||
@@ -1060,6 +1228,48 @@ export default {
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.qa-msg-text-markdown {
|
||||
padding-top: 6rpx;
|
||||
}
|
||||
|
||||
.md-root {
|
||||
font-size: 26rpx;
|
||||
line-height: 1.6;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.md-root strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.md-root em {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.md-root ul {
|
||||
padding-left: 32rpx;
|
||||
margin: 8rpx 0;
|
||||
}
|
||||
|
||||
.md-root li {
|
||||
margin: 4rpx 0;
|
||||
}
|
||||
|
||||
.md-code-block {
|
||||
background-color: #f7f7f9;
|
||||
border-radius: 8rpx;
|
||||
padding: 10rpx 14rpx;
|
||||
font-family: monospace;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.md-inline-code {
|
||||
font-family: monospace;
|
||||
background-color: #f2f2f4;
|
||||
padding: 2rpx 6rpx;
|
||||
border-radius: 4rpx;
|
||||
}
|
||||
|
||||
/* 复制按钮在角上绝对定位;气泡已有左右 padding,再给正文加一点边距避免与 44rpx 按钮重叠 */
|
||||
.qa-msg--ai .qa-msg-text {
|
||||
padding-left: 40rpx;
|
||||
|
||||
Reference in New Issue
Block a user