Skip to content

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

核心属性

属性名类型默认值说明
markdownstring-要渲染的 Markdown 内容
is-compile-vuebooleanfalse是否编译 Vue 组件
optionsobject{}markdown-it 配置项
pluginsarray[]markdown-it 插件列表
provideDataobject{}向 Markdown 内容中注入的数据
ui-frameworkobject-UI 框架配置
customComponentsobject{}自定义组件映射表

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>

事件

事件名说明回调参数
renderedMarkdown 渲染完成时触发-
error渲染发生错误时触发(error: Error)

插件使用

插件导入方式说明: 插件可以通过以下两种方式导入:

  1. 直接从 npm 包导入(如下面示例所示)
  2. 从统一插件包导入:
js
import { 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 支持

  1. 使用此插件前需在项目中引入 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>

👉点击这里下载amis

  1. 在项目main.ts中初始化配置
ts
import { createPlugin } from '@pt/common-ui';
app.use(await createPlugin());
  1. 导入并使用插件
js
import { plugins } from '@pt/common-ui'
const { markdown: { markdownItPluginAmis } } = plugins

// 在 plugins 中添加
const oPlugins = [
  markdownItPluginAmis
]
  1. 使用 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
    ![alt text](https://example.com/image.jpg "标题" =300x200)
    ![alt text](https://example.com/image.jpg =x500)   <!-- 只指定高度 -->
    ![alt text](https://example.com/image.jpg =800x)   <!-- 只指定宽度 -->
    ![alt text](https://example.com/image.jpg =50%)    <!-- 支持百分比 -->
  • 懒加载与过渡:自动为图片添加 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)中正确初始化:
    ts
    import { 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>

注意事项

  1. 使用 UI 框架组件时,需要确保正确配置了 ui-framework
  2. 使用数学公式时,需要引入 KaTeX 的样式文件
  3. 自定义组件需要提前注册到 customComponents
  4. 如果需要在 Markdown 中使用 Vue 指令,确保设置 is-compile-vuetrue
  5. 重要: 在项目的 vite.config.ts 中必须配置以下内容,以支持运行时模板编译:
    js
    export default {
      // ... 其他配置
      resolve: {
        alias: {
          vue: 'vue/dist/vue.esm-bundler.js', // 使用支持模板编译的 Vue 版本
        }
      }
      // ... 其他配置
    }
    如果是在 VitePress 项目中使用,则需要在 .vitepress/config.ts 中的 vite 配置项中添加上述配置。

Released under the MIT License.