MarkdownCompiler 组件
MarkdownCompiler 是一个功能强大的 Markdown 渲染组件,支持 Vue 组件渲染、数学公式、自定义插件等特性。
基础用法
vue
<template>
<MarkdownCompiler :markdown="markdownContent" />
</template>
<script setup>
import { ref } from 'vue'
import { MarkdownCompiler } from '@pt/common-ui'
const markdownContent = ref('# Hello World')
</script>组件 Props
核心属性
| 属性名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| markdown | string | - | 要渲染的 Markdown 内容 |
| is-compile-vue | boolean | false | 是否编译 Vue 组件 |
| options | object | {} | markdown-it 配置项 |
| plugins | array | [] | markdown-it 插件列表 |
| provideData | object | {} | 向 Markdown 内容中注入的数据 |
| ui-framework | object | - | UI 框架配置 |
| customComponents | object | {} | 自定义组件映射表 |
options 配置项
js
{
html: true, // 启用 HTML 标签
breaks: true, // 转换段落里的 '\n' 到 <br>
linkify: true, // 自动转换 URL 到链接
}UI 框架集成
js
const uiFrameworkConfig = {
framework: {
...Antd,
install: (app) => {
app.use(Antd);
// 注册自定义指令
Object.entries(directives).forEach(([name, directive]) => {
app.directive(name, directive);
});
}
},
config: {
locale: zhCn,
// 其他 UI 框架配置
}
}插槽
组件提供了两个具名插槽:
before 插槽
在 Markdown 内容之前渲染的内容:
vue
<template #before>
<div class="markdown-header">
<h1>文档标题</h1>
<div class="metadata">
<span>作者:{{ author }}</span>
<span>更新时间:{{ updateTime }}</span>
</div>
</div>
</template>after 插槽
在 Markdown 内容之后渲染的内容:
vue
<template #after>
<div class="markdown-footer">
<div class="tags">
<a-tag v-for="tag in tags" :key="tag">{{ tag }}</a-tag>
</div>
</div>
</template>事件
| 事件名 | 说明 | 回调参数 |
|---|---|---|
| rendered | Markdown 渲染完成时触发 | - |
| error | 渲染发生错误时触发 | (error: Error) |
插件使用
插件导入方式说明: 插件可以通过以下两种方式导入:
- 直接从 npm 包导入(如下面示例所示)
- 从统一插件包导入:
jsimport { markdownIt } from "@pt/common-ui/plugins"; const { MarkdownItPluginTable } = markdownIt;
数学公式支持 (KaTeX)
js
import katex from 'markdown-it-katex'
import 'katex/dist/katex.min.css'
const oPlugins = [
[katex, { throwOnError: false }]
]使用示例:
markdown
数学公式: $E = mc^2$图表支持 (ECharts)
js
import { plugins } from '@pt/common-ui'
const { markdown: { markdownItPluginEcharts } } = plugins
// 在 plugins 中添加
const oPlugins = [
markdownItPluginEcharts
]amis 支持
- 使用此插件前需在项目中引入 amis 的 依赖 文件
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Vue + TS</title>
<link rel="stylesheet" href="/amis/antd.css" />
<!-- 已修改此文件.label样式,默认设置导致mermaid.render方法拿到的svg图中label有遮挡问题 -->
<link rel="stylesheet" href="/amis/helper.css" />
<link rel="stylesheet" href="/amis/iconfont.css" />
</head>
<body>
<div id="app"></div>
<script src="/amis/sdk.js"></script>
<script type="module" src="/src/main.ts"></script>
</body>
</html>- 在项目main.ts中初始化配置
ts
import { createPlugin } from '@pt/common-ui';
app.use(await createPlugin());- 导入并使用插件
js
import { plugins } from '@pt/common-ui'
const { markdown: { markdownItPluginAmis } } = plugins
// 在 plugins 中添加
const oPlugins = [
markdownItPluginAmis
]- 使用 useAmis hook
js
import { useAmis } from '@pt/common-ui';
useAmis({
onSubmit: ({ formData, amisJSON }) => {
// 在这里处理表单提交
console.log('Form submitted:', formData);
console.log('Amis config:', amisJSON);
}
})下面是一段amis的示例代码
js
const markdownContent = ref("\n\n```form-json\n{\"data\": {\"agentId\": \"2418601414232530985\", \"satoken\": \"57c98b57-71fe-44e5-9ecd-a4b7ebd2ad6f\", \"officeId\": \"1765677208300953601\", \"sessionId\": \"a9746f3a-440b-40a3-bf7a-cf878daabbde\", \"routeId\": \"2418601414232530984\"}, \"config\": {\"info\": {\"task_id\": \"\", \"agent_id\": \"2418601414232530985\", \"message_id\": \"3edcd395-02c2-4f74-a5c0-97aed33d51cb\", \"related_message_id\": \"3edcd395-02c2-4f74-a5c0-97aed33d51cb\", \"session_id\": \"a9746f3a-440b-40a3-bf7a-cf878daabbde\", \"skill_id\": \"1772501205906386944\", \"skill_name\": \"query_skill_detail\", \"skill_exec_way\": \"SYNC\", \"skill_sort\": 0, \"route_id\": \"2418601414232530984\", \"auto_execute\": 0}}, \"xui\": {\"body\": [{\"submitText\": \"\\u786e\\u8ba4\", \"data\": {}, \"name\": \"form\", \"disabled\": false, \"style\": {}, \"body\": [{\"clearable\": true, \"hidden\": false, \"showCounter\": false, \"name\": \"id\", \"disabled\": false, \"id\": \"id\", \"label\": \"id\", \"type\": \"input-text\", \"value\": \"${id}\", \"required\": true, \"staticOn\": \"false\", \"trimContents\": true}], \"title\": \"\", \"type\": \"form\", \"wrapWithPanel\": true}], \"name\": \"page\", \"type\": \"page\"}}\n```\n\n");元素样式类插件
js
import { plugins } from '@pt/common-ui'
const { markdown: { markdownItElementClasses } } = plugins
// 在 plugins 中添加
const oPlugins = [
markdownItElementClasses
]插件实现详情
markdownItElementClasses 插件基于 UnoCSS 实现,为 Markdown 元素自动添加语义化的 CSS 类名。该插件通过重写 markdown-it 的渲染规则,为不同类型的元素注入预定义的样式类。
查看插件源码实现
ts
// markdown-it 元素样式类插件
import type MarkdownIt from 'markdown-it';
export default (md: MarkdownIt) => {
const defaultRender =
md.renderer.rules.paragraph_open ||
function (tokens: any[], idx: number, options: any, env: any, self: any) {
return self.renderToken(tokens, idx, options);
};
// 段落元素样式
md.renderer.rules.paragraph_open = function (
tokens: any[],
idx: number,
options: any,
env: any,
self: any
) {
const token = tokens[idx];
if (token.type === 'paragraph_open') {
token.attrJoin('class', 'md-paragraph');
}
return defaultRender(tokens, idx, options, env, self);
};
// 无序列表样式
md.renderer.rules.bullet_list_open = function (
tokens: any[],
idx: number,
options: any,
env: any,
self: any
) {
tokens[idx].attrPush([
'class',
'md-ul [&:not(:last-child)_ul]:pb-1 [&:not(:last-child)_ol]:pb-1 list-disc space-y-2.5 pl-7',
]);
return self.renderToken(tokens, idx, options);
};
// 有序列表样式
md.renderer.rules.ordered_list_open = function (
tokens: any[],
idx: number,
options: any,
env: any,
self: any
) {
tokens[idx].attrPush([
'class',
'md-ol [&:not(:last-child)_ul]:pb-1 [&:not(:last-child)_ol]:pb-1 list-decimal space-y-2.5 pl-7',
]);
return self.renderToken(tokens, idx, options);
};
tokens, idx, options, env, self);
};
// 分隔线样式
md.renderer.rules.hr = (
tokens: any[],
idx: number,
options: any,
env: any,
self: any
) => {
tokens[idx].attrPush(['class', 'md-hr']);
if (defaultRender) {
return defaultRender(tokens, idx, options, env, self);
} else {
return self.renderToken(tokens, idx, options);
}
};
};UnoCSS 样式配置
由于插件使用了 UnoCSS 的动态类名和复杂选择器,某些样式可能无法被 UnoCSS 自动检测到。为确保所有样式正常生效,需要在项目的 uno.config.ts 中配置样式安全列表:
ts
// uno.config.ts
export default defineConfig({
// ... 其他配置
safelist: [
// 列表相关样式
'[&:not(:last-child)_ul]:pb-1',
'[&:not(:last-child)_ol]:pb-1',
'list-disc',
'list-decimal',
'space-y-2.5',
'pl-7',
// 文本处理样式
'whitespace-normal',
'break-words',
]
});💡 提示: 了解更多关于 UnoCSS 样式提取机制,请参考 UnoCSS 官方文档。
表格插件(Table Plugin)
该插件用于解决 Markdown 表格在列数较多或单元格内容较长时,容易导致容器被撑开、布局错乱的问题。通过对表格渲染逻辑的优化,确保表格能够自适应容器宽度,或在必要时支持横向滚动,从而保持页面整体布局的稳定性和美观性。
js
import { plugins } from '@pt/common-ui'
const { markdown: { markdownItPluginTable } } = plugins
// 在 plugins 中添加
const oPlugins = [
markdownItPluginTable
]图片预览插件
该插件扩展了 Markdown 原生图片语法,支持图片尺寸指定(=宽度x高度)、图片懒加载、加载/错误过渡效果,以及可选的点击放大预览功能(基于内置的图片查看器)。图片默认居中显示,无自定义尺寸时自动宽度占满容器。
js
import { plugins } from '@pt/common-ui/plugins'
const {
markdown: {
markdownItPluginImage,
},
} = plugins
// 在 plugins 中添加(可传入选项)
const oPlugins = [
[markdownItPluginImage, {
hAlign: 'center', // 图片容器水平对齐方式:'left' | 'center' | 'right',默认 'center'
viewer: true // 是否启用点击图片放大预览,默认 true
}]
]功能特性
- 尺寸指定:在图片链接标题后添加
=宽度x高度(或只指定宽度/高度),例如:markdown  <!-- 只指定高度 -->  <!-- 只指定宽度 -->  <!-- 支持百分比 --> - 懒加载与过渡:自动为图片添加
loading="lazy",并通过 opacity 过渡实现加载完成后的淡入效果。 - 错误与加载处理:通过全局
window.md.mdImg.handleError/load处理图片加载失败或成功(组件内部已实现)。 - 点击预览:当
viewer: true时,图片添加cursor-zoom-in并绑定点击事件调用window.md.mdImg.preview(this)实现全屏预览。 - 样式类:
- 图片容器:
<div style="text-align: center"> - 图片本身:默认类
md-img,无自定义尺寸时追加w-full,启用预览时追加cursor-zoom-in。
- 图片容器:
使用注意
- 该插件依赖
@pt/common-ui提供的全局图片管理器window.md.mdImg(包含预览、加载成功/失败、错误恢复等逻辑)。请确保在项目入口文件(如main.ts)中正确初始化:tsimport { createPlugin } from '@pt/common-ui' // 异步初始化插件(会自动挂载 window.md.mdImg 等全局方法) app.use(await createPlugin()) - 若不需要预览功能,可将
viewer设为false。 - 尺寸支持百分比(如
50%)与像素值,解析逻辑兼容大多数常见写法。
社区生态插件
- markdown-it
- markdown-it-emoji
- markdown-it-sub
- markdown-it-sup
- markdown-it-footnote
- markdown-it-deflist
- markdown-it-abbr
- markdown-it-ins
- markdown-it-mark
- markdown-it-katex
- markdown-it-task-lists
- markdown-it-highlight
- markdown-it-latex
- markdown-it-container
- markdown-it-github-toc
- markdown-it-source-map
- markdown-it-link-attributes
- ...
总结
通过上述插件,你可以轻松地在 Markdown 内容中实现:
- 🧮 数学公式(KaTeX)
- 📊 交互式图表(ECharts)
- 📋 动态表单(AMIS)
- 📐 美观稳定的表格布局
- 🎨 一致的元素样式控制
- 🖼 增强的图片渲染(尺寸控制、懒加载、点击预览)
- 🔧 丰富的 markdown-it 生态扩展
所有插件均通过统一的导入方式接入,灵活、可配置、易于扩展。
自定义组件
可以注册自定义组件供 Markdown 内容使用:
vue
<MarkdownCompiler
:customComponents="{
'Demo1': Demo1
}"
/>然后在 Markdown 中使用:
markdown
<Demo1 />组件内部使用 provide 数据
vue
<template>
<div style="border: 1px solid red;">
<div>
{{ markdownProvideData?.author }}
</div>
<div>
<a-button type="primary" @click="markdownProvideData?.sendMessage?.('123')">点击按钮模拟发送消息</a-button>
</div>
</div>
</template>
<script setup lang="ts" name="Demo1">
import { inject } from 'vue';
interface MarkdownProvideData {
author?: string;
sendMessage?: (str: string) => void;
}
const markdownProvideData = inject<MarkdownProvideData>('markdownProvideData', {});
</script>完整示例
MarkdownCompiler组件
请打开代码查看
<template>
<div un-cloak class="overflow-y-auto h-100%">
<ClientOnly>
<MarkdownCompiler
:markdown="markdownContent"
:is-compile-vue="true"
:options="{
html: true,
breaks: true,
linkify: true,
}"
:plugins="oPlugins"
:provideData="{
author: '张三',
updateTime: '2024-03-21',
tags: ['Vue', 'Markdown', '文档'],
}"
:ui-framework="uiFrameworkConfig"
:customComponents="{
'Demo1': Demo1
}"
@rendered="onRendered"
@error="onError"
>
<template #before>
<!-- 在 Markdown 内容之前显示 -->
<div class="markdown-header">
🔍 我是 Markdown 内容前插槽内容
</div>
</template>
<template #after>
<!-- 在 Markdown 内容之后显示 -->
<div class="markdown-footer">
🔍 我是 Markdown 内容后插槽内容
</div>
</template>
</MarkdownCompiler>
</ClientOnly>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { App } from 'vue';
import Antd from 'ant-design-vue';
import zhCn from "ant-design-vue/es/locale/zh_CN";
import { MarkdownCompiler } from '@pt/common-ui';
import { plugins } from '@pt/common-ui/plugins';
const { markdown: { markdownItPluginEcharts } } = plugins;
import Demo1 from './demo1.vue';
// import "ant-design-vue/dist/antd.css";
// import "./modified-antd.css";
// @ts-ignore
import katex from 'markdown-it-katex';
import 'katex/dist/katex.min.css';
console.log(markdownItPluginEcharts);
// 自定义指令
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const directives = {
focus: {
mounted(el: HTMLElement) {
el.focus();
}
},
// 可以添加更多自定义指令
};
// 创建一个框架配置对象
const uiFrameworkConfig = {
framework: {
...Antd,
install: (app: App) => {
app.use(Antd);
// 注册自定义指令
Object.entries(directives).forEach(([name, directive]) => {
app.directive(name, directive);
});
}
},
config: {
locale: zhCn,
autoInsertSpace: false, // 配置 ant-design-vue 按钮文字中间是否有空格
componentSize: 'middle' as const,
getPopupContainer: (el: HTMLElement, dialogContext?: any) => {
return dialogContext?.getDialogWrap?.() || document.body;
}, // 配置弹窗
transformCellText: ({ text }: { text: any }) => {
return Array.isArray(text) && !text.length ? '--' : text;
}
}
}
const oPlugins = [
// 带配置的插件,使用数组形式
[katex, { throwOnError: false }]
]
const markdownContent = ref(`
# Hello World
数学公式: $E = mc^2$
<a-button type="primary">Primary Button</a-button>
<Demo1 />
<a-date-picker />
<a-input v-focus placeholder="Basic usage" />
+ 列表
+ 列表
\`\`\`js
console.log('hello world');
\`\`\`
`.trim());
// const markdownContent = ref("\n\n```js\nconsole.log('hello world');\n```\n\n");
const onRendered = ()=>{
console.log('onRendered');
}
const onError = (error: Error) => {
console.log('渲染错误:', error);
console.log('错误堆栈:', error.stack);
};
</script>
<style scoped lang="less">
.markdown-header {
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid #e8e8e8;
font-size: 24px;
font-weight: bold;
}
.markdown-footer {
margin-top: 24px;
padding-top: 16px;
border-top: 1px solid #e8e8e8;
font-size: 24px;
font-weight: bold;
}
</style>注意事项
- 使用 UI 框架组件时,需要确保正确配置了
ui-framework - 使用数学公式时,需要引入 KaTeX 的样式文件
- 自定义组件需要提前注册到
customComponents中 - 如果需要在 Markdown 中使用 Vue 指令,确保设置
is-compile-vue为true - 重要: 在项目的
vite.config.ts中必须配置以下内容,以支持运行时模板编译:js如果是在 VitePress 项目中使用,则需要在export default { // ... 其他配置 resolve: { alias: { vue: 'vue/dist/vue.esm-bundler.js', // 使用支持模板编译的 Vue 版本 } } // ... 其他配置 }.vitepress/config.ts中的vite配置项中添加上述配置。