vue3针对markdown和代码块进行解析组件
vue3对AI大模型给出的回答,富文本和代码块进行解析,以及实现代码块复制功能的组件,
·
可用于大模型回复的富文本解析,解析样式效果如下:
放大来看一下代码块部分,可以识别代码语言以及可以复制
以上就是效果展示,至于字体的大小粗细间距等等可以通过css自己调整合适的即可
引入依赖
pnpm add markdown-it @traptitech/markdown-it-katex highlight.js clipboard markdown-it-task-lists
代码如下
<template>
<div>
<div v-html="parsedContent" class="message markdown-body"></div>
</div>
</template>
<script setup>
import { ref, watchEffect, onMounted, nextTick } from "vue";
import MarkdownIt from "markdown-it";
import mdKatex from "@traptitech/markdown-it-katex";
import hljs from "highlight.js";
import ClipboardJS from "clipboard";
import markdownItTaskLists from "markdown-it-task-lists";
const props = defineProps({
contentData: {
required: true
}
});
// 初始化剪贴板
let clipboard = null;
// 创建并配置 markdown-it 实例
const mdi = new MarkdownIt({
html: true, // 启用HTML标签(关键:允许HTML注入)
xhtmlOut: true, // 使用'/'关闭单标签
breaks: true, // 转换'\n'为<br>
linkify: true, // 自动转换URL文本为链接
typographer: true, // 启用一些语言中性的替换和引号美化
highlight(code, language) {
const validLang = !!(language && hljs.getLanguage(language));
if (validLang) {
const lang = language ?? "";
return highlightBlock(
hljs.highlight(code, { language: lang }).value,
lang
);
}
return highlightBlock(hljs.highlightAuto(code).value, "");
}
});
// 使用任务列表插件
mdi.use(markdownItTaskLists, {
enabled: true,
label: true,
labelAfter: false
});
// 启用数学公式插件
mdi.use(mdKatex, {
blockClass: "katexmath-block rounded-md p-[10px]",
errorColor: "#cc0000"
});
// 预处理内容,替换固定的"0."为递增的编号
function preprocessNumbering(content) {
let sectionCounter = 0;
const lines = content.split("\n");
const result = [];
// 更新的正则表达式,处理包含空格、标题(有或没有冒号)等的情况
const pattern = /^(.*?)(\d+\.)(\s+)(.*?)(:|\s*:)?(.*)$/;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const match = line.match(pattern);
if (match) {
sectionCounter++;
const [_, indent, number, space, title, colon, suffix] = match;
// 使用新的递增编号替换
const newLine = `${indent}<span style="display:inline-block;width:0;overflow:hidden;">0</span>${sectionCounter}.${space}${title}${colon ||
""}${suffix}`;
result.push(newLine);
} else {
result.push(line);
}
}
return result.join("\n");
}
// 解析代码块的渲染方法
function highlightBlock(str, lang) {
const uniqueId = `code-${Math.random()
.toString(36)
.substr(2, 9)}`;
lang = lang || "text";
// 去除代码末尾的换行符
const trimmedStr = str.replace(/\n$/, "");
return `<pre><div class="code-header"><span>${lang}</span><button class="copy-btn" data-clipboard-target="#${uniqueId}">复制</button></div><div><code id="${uniqueId}" class="hljs ${lang}" style="border-bottom-left-radius: 10px;border-bottom-right-radius: 10px;font-size: 10px; font-family: '微软雅黑';">${trimmedStr}</code></div></pre>`;
}
const parsedContent = ref("");
// 确保正确处理Markdown内容
const parseMarkdown = content => {
// 预处理内容,替换固定编号为递增编号
const processedContent = preprocessNumbering(content);
// 渲染处理后的内容
return mdi.render(processedContent);
};
// 初始化剪贴板并处理复制反馈
const initClipboard = () => {
clipboard && clipboard.destroy();
clipboard = new ClipboardJS(".copy-btn", {
text: trigger => {
const target = document.querySelector(
trigger.getAttribute("data-clipboard-target")
);
return target.innerText;
}
});
// 复制成功提示
clipboard.on("success", e => {
const btn = e.trigger;
btn.textContent = "已复制!";
setTimeout(() => (btn.textContent = "复制"), 2000);
e.clearSelection();
});
};
watchEffect(async () => {
const currentItem = props.contentData;
if (currentItem) {
// 使用修复后的解析方法
parsedContent.value = parseMarkdown(currentItem);
await nextTick();
initClipboard();
}
});
const styles = `
.code-header {
height: 30px;
background-color: #50505a;
padding: 6px;
color: white;
font-size: 10px;
font-family: "微软雅黑";
display: flex;
justify-content: space-between;
align-items: center;
}
.copy-btn {
height: 20px;
background: #6b6b76;
border: none;
color: white;
padding: 4px 10px;
border-radius: 4px;
cursor: pointer;
transition: background 0.3s;
font-size: 10px;
}
.copy-btn:hover {
background: #888;
}
`;
// 动态注入样式
onMounted(() => {
const styleTag = document.createElement("style");
styleTag.innerHTML = styles + markdownStyles;
document.head.appendChild(styleTag);
});
// Markdown样式
const markdownStyles = `
.markdown-body {
font-size: 12px;
font-family: "微软雅黑";
line-height: 1.6;
word-wrap: break-word;
}
.markdown-body h1 {
font-size: 2em;
margin: 0.67em 0;
font-weight: bold;
}
.markdown-body h2 {
font-size: 1.5em;
margin: 0.83em 0;
font-weight: bold;
}
.markdown-body h3 {
font-size: 1.17em;
margin: 1em 0;
font-weight: bold;
}
.markdown-body h4 {
font-size: 1em;
margin: 1.33em 0;
font-weight: bold;
}
.markdown-body h5 {
font-size: 0.83em;
margin: 1.67em 0;
font-weight: bold;
}
.markdown-body h6 {
font-size: 0.67em;
margin: 2.33em 0;
font-weight: bold;
}
.markdown-body strong {
font-weight: bold;
}
.markdown-body em {
font-style: italic;
}
.markdown-body blockquote {
padding: 0 1em;
color: #6a737d;
border-left: 0.25em solid #dfe2e5;
margin: 0 0 16px 0;
}
.markdown-body ul,
.markdown-body ol {
list-style: none !important; /* 强制覆盖默认样式 */
padding-left: 1.5em !important;
margin: 0 0 16px 0 !important;
}
.markdown-body li {
position: relative !important;
margin-bottom: 0.25em !important;
display: block !important;
}
.markdown-body ul > li::before {
content: '•' !important;
position: absolute !important;
left: -1.5em !important;
width: 1em !important;
display: inline-block !important;
text-align: center !important;
}
.markdown-body ol > li::before {
content: counter(list-item) "." !important;
position: absolute !important;
left: -1.5em !important;
width: 1em !important;
display: inline-block !important;
text-align: right !important;
margin-right: 0.5em !important;
}
.markdown-body table {
border-collapse: collapse;
margin: 0 0 16px 0;
width: 100%;
}
.markdown-body table th,
.markdown-body table td {
border: 1px solid #dfe2e5;
padding: 6px 13px;
}
.markdown-body table tr {
background-color: #fff;
border-top: 1px solid #c6cbd1;
}
.markdown-body table tr:nth-child(2n) {
background-color: #f6f8fa;
}
.markdown-body hr {
height: 0.25em;
padding: 0;
margin: 24px 0;
background-color: #e1e4e8;
border: 0;
}
.markdown-body code:not(.hljs) {
padding: 0.2em 0.4em;
margin: 0;
font-size: 85%;
background-color: rgba(27,31,35,0.05);
border-radius: 3px;
font-family: monospace;
}
.markdown-body img {
max-width: 100%;
box-sizing: content-box;
}
`;
</script>
<style scoped>
.message {
font-size: 12px;
font-family: "微软雅黑";
}
</style>
使用方法
<script setup>
import CodeParser from "@/components/CodeParser.vue";
</script>
<template>
<code-parser contentData="需要输入的富文本" />
</template>
大家有更好的方案或者样式或者修改可以推荐一下
更多推荐
所有评论(0)