Vditor 一款浏览器端的 Markdown 编辑器,支持所见即所得(富文本)、即时渲染(类似 Typora)和分屏预览模式

本贴最后更新于 1115 天前,其中的信息可能已经沧海桑田

Vditor
易于使用的 Markdown 编辑器,为适配不同的应用场景而生

npm bundle size


English  |  Demo

💡 简介

Vditor 是一款浏览器端的 Markdown 编辑器,支持所见即所得、即时渲染(类似 Typora)和分屏预览模式。它使用 TypeScript 实现,支持原生 JavaScript 以及 Vue、React、Angular 和 Svelte 等框架。

欢迎到 Vditor 官方讨论区了解更多。同时也欢迎关注 B3log 开源社区微信公众号 B3log开源

b3logos.jpg

🗺️ 背景

随着 Markdown 排版方式的普及,越来越多的应用开始集成 Markdown 编辑器。目前主流可集成的 Markdown 编辑器现状如下:

  • 有的仅支持分屏预览,即编辑区和预览区分离
  • 有的同时支持所见即所得和分屏预览,但所见即所得模式下不能完整支持 Markdown 语法排版
  • 几乎没有类似 Typora 的即时渲染

而这三点恰好对应了三种应用场景:

  • 分屏预览:适配传统的 Markdown 使用场景,适合大屏下编辑排版
  • 所见即所得:对不熟悉 Markdown 的用户友好,熟悉 Markdown 的用户也可以无缝使用
  • 即时渲染:理论上这是最为优雅的 Markdown 编辑方式,让熟悉 Markdown 的用户能够更专注于内容创作

所以,一个能够适配应用场景的 Markdown 编辑器至关重要,它需要考虑到:

  • 传统 Markdown 用户的使用场景,提供分屏预览
  • 富文本编辑用户的使用场景,提供所见即所得
  • 高阶 Markdown 用户的使用场景,提供即时渲染

Vditor 在这些方面做了努力,希望能为现代化的通用 Markdown 编辑领域做出一些贡献。

✨ 特性

  • 支持三种编辑模式:所见即所得(wysiwyg)、即时渲染(ir)、分屏预览(sv)
  • 支持大纲、数学公式、脑图、图表、流程图、甘特图、时序图、五线谱、多媒体、语音阅读、标题锚点、代码高亮及复制、graphviz 渲染、plantumlUML 图
  • 导出、图片懒加载、任务列表、多平台预览、多主题切换、复制到微信公众号/知乎功能
  • 实现 CommonMark 和 GFM 规范,可对 Markdown 进行格式化和语法树查看,并支持 10+ 项配置
  • 工具栏包含 36+ 项操作,除支持扩展外还可对每一项中的快捷键、提示、提示位置、图标、点击事件、类名、子工具栏进行自定义
  • 表情/at/话题等自动补全扩展
  • 可使用拖拽、剪切板粘贴上传,显示实时上传进度,支持 CORS 跨域上传
  • 实时保存内容,防止意外丢失
  • 录音支持,用户可直接发布语音
  • 粘贴 HTML 自动转换为 Markdown,如粘贴中包含外链图片可通过指定接口上传到服务器
  • 支持主窗口大小拖拽、字符计数
  • 多主题支持,内置黑白绿三套主题
  • 多语言支持,内置中、英、韩文本地化
  • 支持主流浏览器,对移动端友好

editor.png

preview.png

🔮 编辑模式

所见即所得(WYSIWYG)

所见即所得模式对不熟悉 Markdown 的用户较为友好,熟悉 Markdown 的话也可以无缝使用。

vditor-wysiwyg

即时渲染(IR)

即时渲染模式对熟悉 Typora 的用户应该不会感到陌生,理论上这是最优雅的 Markdown 编辑方式。

vditor-ir

分屏预览(SV)

传统的分屏预览模式适合大屏下的 Markdown 编辑。

vditor-sv

🍱 语法支持

  • 所有 CommonMark 语法:分隔线、ATX 标题、Setext 标题、缩进代码块、围栏代码块、HTML 块、链接引用定义、段落、块引用、列表、反斜杠转义、HTML 实体、行级代码、强调、加粗、链接、图片、行级 HTML、硬换行、软换行和纯文本。
  • 所有 GFM 语法:表格、任务列表项、删除线、自动链接、XSS 过滤
  • 常用 Markdown 扩展语法:脚注、ToC、自定义标题 ID
  • 图表语法
    • 流程图、时序图、甘特图,通过 Mermaid 支持
    • Graphviz
    • 折线图、饼图、脑图等,通过 ECharts 支持
  • 五线谱:通过 abc.js 支持
  • 数学公式:数学公式块、行级数学公式,通过 MathJax 和 KaTeX 支持
  • YAML Front Matter
  • 中文语境优化
    • 中西文之间插入空格
    • 术语拼写修正
    • 中文后跟英文逗号句号等标点替换为中文对应标点

以上大部分特性可以通过开关配置是否启用,开发者可根据自己的应用场景选择搭配。

🗃 案例

  • Sym 一款用 Java 实现的现代化社区(论坛/BBS/社交网络/博客)平台
  • Solo & Pipe B3log 分布式社区的博客端节点,欢迎加入下一代社区网络
  • Tditor 基于 React、Vditor、Springboot, 一款打造极致文字创作体验的在线 Markdown 编辑平台
  • Arya 基于 Vue、Vditor,所构建的在线 Markdown 编辑器
  • 更多案例

🛠️ 使用文档

CommonJS

  • 安装依赖
npm install vditor --save
  • 在代码中引入并初始化对象,可参考 index.js
import Vditor from 'vditor'
import "~vditor/src/assets/less/index"

const vditor = new Vditor(id, {options...})

HTML script

  • 在 HTML 中插入 CSS 和 JavaScript,可参考 demo
<!-- ⚠️生产环境请指定版本号,如 https://unpkg.com/vditor@x.x.x/dist... -->
<link rel="stylesheet" href="https://unpkg.com/vditor/dist/index.css" />
<script src="https://unpkg.com/vditor/dist/index.min.js"></script>

示例代码

主题

编辑器主题

编辑器所展现的外观。内置 classic,dark 2 套主题。

  • 编辑器初始化时可通过 options.theme 设置内置主题
  • 初始化完成后可通过 setTheme 更新编辑器主题
  • 可通过修改 index.less 中的变量对主题颜色进行定制
  • 可参考现有结构和类名在原有基础上进行修改

内容主题

Markdown 输出的 HTML 所展现的外观。内置 ant-design, light,dark,wechat 4 套主题。支持内容主题扩展接口。

  • 需在显示元素上添加 class="vditor-reset"
  • 编辑器初始化时可通过 options.preview.theme 设置内置或自己开发的主题列表
  • 内容渲染初始化时可通过 IPreviewOptions.theme 设置内置或自己开发的主题
  • 初始化完成后可通过 setThemesetContentTheme 更新内容主题

代码主题

代码块所展现的外观。内置 github 等 36 套主题。

  • 编辑器初始化时可通过 options.preview.hljs 对代码块样式、行号、是否启用进行设置
  • 内容渲染初始化时可通过 IPreviewOptions.hljs 对代码块样式、行号、是否启用进行设置
  • 初始化完成后可通过 setThemesetCodeTheme 更新代码主题

API

id

可填入元素 id 或元素自身 HTMLElement

⚠️:当填入元素自身的 HTMLElement 时需设置 options.cache.id 或将 options.cache.enable 设置为 false

options

说明 默认值
i18n 多语言,参见 ITips -
undoDelay 历史记录间隔 -
after 编辑器异步渲染完成后的回调方法 -
height 编辑器总高度 'auto'
minHeight 编辑区域最小高度 -
width 编辑器总宽度,支持 % 'auto'
placeholder 输入区域为空时的提示 ''
lang 语言种类:en_US, fr_FR, pt_BR, ja_JP, ko_KR, ru_RU, sv_SE, zh_CN, zh_TW 'zh_CN'
input(value: string) 输入后触发 -
focus(value: string) 聚焦后触发 -
blur(value: string) 失焦后触发 -
keydown(event: KeyboardEvent) 按下后触发 -
esc(value: string) esc 按下后触发 -
ctrlEnter(value: string) ⌘/ctrl+enter 按下后触发 -
select(value: string) 编辑器中选中文字后触发 -
tab tab 键操作字符串,支持 \t 及任意字符串 -
typewriterMode 是否启用打字机模式 false
cdn 配置自建 CDN 地址 https://unpkg.com/vditor@${VDITOR_VERSION}
mode 可选模式:sv, ir, wysiwyg 'ir'
debugger 是否显示日志 false
value 编辑器初始化值 ''
theme 主题:classic, dark 'classic'
icon 图标风格:ant, material 'ant'

options.toolbar

  • 工具栏,可使用 name 进行简写: toolbar: ['emoji', 'br', 'bold', '|', 'line'] 。默认值参见 src/ts/util/Options.ts
  • name 可枚举为: emoji , headings , bold , italic , strike , | , line , quote , list , ordered-list , check ,outdent ,indent , code , inline-code , insert-after , insert-before ,undo , redo , upload , link , table , record , edit-mode , both , preview , fullscreen , outline , code-theme , content-theme , export, devtools , info , help , br
  • name 不在枚举中时,可以添加自定义按钮,格式如下:
new Vditor('vditor', {
  toolbar: [
    {
      hotkey: '⇧⌘S',
      name: 'sponsor',
      tipPosition: 's',
      tip: '成为赞助者',
      className: 'right',
      icon: '<svg t="1589994565028" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2808" width="32" height="32"><path d="M506.6 423.6m-29.8 0a29.8 29.8 0 1 0 59.6 0 29.8 29.8 0 1 0-59.6 0Z" fill="#0F0F0F" p-id="2809"></path><path d="M717.8 114.5c-83.5 0-158.4 65.4-211.2 122-52.7-56.6-127.7-122-211.2-122-159.5 0-273.9 129.3-273.9 288.9C21.5 562.9 429.3 913 506.6 913s485.1-350.1 485.1-509.7c0.1-159.5-114.4-288.8-273.9-288.8z" fill="#FAFCFB" p-id="2810"></path><path d="M506.6 926c-22 0-61-20.1-116-59.6-51.5-37-109.9-86.4-164.6-139-65.4-63-217.5-220.6-217.5-324 0-81.4 28.6-157.1 80.6-213.1 53.2-57.2 126.4-88.8 206.3-88.8 40 0 81.8 14.1 124.2 41.9 28.1 18.4 56.6 42.8 86.9 74.2 30.3-31.5 58.9-55.8 86.9-74.2 42.5-27.8 84.3-41.9 124.2-41.9 79.9 0 153.2 31.5 206.3 88.8 52 56 80.6 131.7 80.6 213.1 0 103.4-152.1 261-217.5 324-54.6 52.6-113.1 102-164.6 139-54.8 39.5-93.8 59.6-115.8 59.6zM295.4 127.5c-72.6 0-139.1 28.6-187.3 80.4-47.5 51.2-73.7 120.6-73.7 195.4 0 64.8 78.3 178.9 209.6 305.3 53.8 51.8 111.2 100.3 161.7 136.6 56.1 40.4 88.9 54.8 100.9 54.8s44.7-14.4 100.9-54.8c50.5-36.3 108-84.9 161.7-136.6 131.2-126.4 209.6-240.5 209.6-305.3 0-74.9-26.2-144.2-73.7-195.4-48.2-51.9-114.7-80.4-187.3-80.4-61.8 0-127.8 38.5-201.7 117.9-2.5 2.6-5.9 4.1-9.5 4.1s-7.1-1.5-9.5-4.1C423.2 166 357.2 127.5 295.4 127.5z" fill="#141414" p-id="2811"></path><path d="M353.9 415.6m-33.8 0a33.8 33.8 0 1 0 67.6 0 33.8 33.8 0 1 0-67.6 0Z" fill="#0F0F0F" p-id="2812"></path><path d="M659.3 415.6m-33.8 0a33.8 33.8 0 1 0 67.6 0 33.8 33.8 0 1 0-67.6 0Z" fill="#0F0F0F" p-id="2813"></path><path d="M411.6 538.5c0 52.3 42.8 95 95 95 52.3 0 95-42.8 95-95v-31.7h-190v31.7z" fill="#5B5143" p-id="2814"></path><path d="M506.6 646.5c-59.6 0-108-48.5-108-108v-31.7c0-7.2 5.8-13 13-13h190.1c7.2 0 13 5.8 13 13v31.7c0 59.5-48.5 108-108.1 108z m-82-126.7v18.7c0 45.2 36.8 82 82 82s82-36.8 82-82v-18.7h-164z" fill="#141414" p-id="2815"></path><path d="M450.4 578.9a54.7 27.5 0 1 0 109.4 0 54.7 27.5 0 1 0-109.4 0Z" fill="#EA64F9" p-id="2816"></path><path d="M256 502.7a32.1 27.5 0 1 0 64.2 0 32.1 27.5 0 1 0-64.2 0Z" fill="#EFAFF9" p-id="2817"></path><path d="M703.3 502.7a32.1 27.5 0 1 0 64.2 0 32.1 27.5 0 1 0-64.2 0Z" fill="#EFAFF9" p-id="2818"></path></svg>',
      click () {alert('捐赠地址:https://ld246.com/sponsor')},
    }],
})
说明 默认值
name 唯一标示 -
icon svg 图标 -
tip 提示 -
tipPosition 提示位置:'n', 'ne', 'nw', 's', 'se', 'sw', 'w', 'e' -
hotkey 快捷键,格式为⇧⌘//⌥⌘ -
suffix 插入编辑器中的后缀 -
prefix 插入编辑器中的前缀 -
click(event: Event, vditor: IVditor) 自定义按钮点击时触发的事件 -
className 样式名 ''
toolbar?: Array<options.toolbar> 子菜单 -

options.toolbarConfig

说明 默认值
hide 是否隐藏工具栏 false
pin 是否固定工具栏 false

options.counter

说明 默认值
enable 是否启用计数器 false
after(length: number, counter: options.counter): void 字数统计回调 -
max 允许输入的最大值 -
type 统计类型:'markdown', 'text' 'markdown'

options.cache

说明 默认值
enable 是否使用 localStorage 进行缓存 true
id 缓存 key,第一个参数为元素且启用缓存时必填 -
after(html: string): string 缓存后的回调 -

options.comment

⚠️:仅支持 wysiwyg 模式

说明 默认值
enable 是否启用评论模式 false
add(id: string, text: string, commentsData: ICommentsData[]) 添加评论回调 -
remove(ids: string[]) 删除评论回调 -
scroll(top: number) 滚动回调 -
adjustTop(commentsData: ICommentsData[]) 文档修改时,适配评论高度 -

options.preview

说明 默认值
delay 预览 debounce 毫秒间隔 1000
maxWidth 预览区域最大宽度 800
mode 显示模式:both, editor 'both'
url md 解析请求 -
parse(element: HTMLElement) 预览回调 -
transform(html: string): string 渲染之前回调 -

options.preview.hljs

说明 默认值
defaultLang 未指定语言时默认使用该语言 ''
enable 是否启用代码高亮 true
style 可选值参见 Chroma github
lineNumber 是否启用行号 false

options.preview.markdown

说明 默认值
autoSpace 自动空格 false
fixTermTypo 自动矫正术语 false
toc 插入目录 false
footnotes 脚注 true
codeBlockPreview wysiwyg 和 ir 模式下是否对代码块进行渲染 true
mathBlockPreview wysiwyg 和 ir 模式下是否对数学公式进行渲染 true
paragraphBeginningSpace 段落开头空两个 false
sanitize 是否启用过滤 XSS true
listStyle 为列表添加 data-style 属性 false
linkBase 链接相对路径前缀 ''
linkPrefix 链接强制前缀 ''
mark 启用 mark 标记 false

options.preview.theme

说明 默认值
current 当前主题 "light"
list 可选主题列表 { "ant-design": "Ant Design", dark: "Dark", light: "Light", wechat: "WeChat" }
path 主题样式地址 https://unpkg.com/vditor@${VDITOR_VERSION}/dist/css/content-theme

options.preview.math

说明 默认值
inlineDigit 内联数学公式起始 $ 后是否允许数字 false
macros 使用 MathJax 渲染时传入的宏定义 {}
engine 数学公式渲染引擎:KaTeX, MathJax 'KaTeX'

options.preview.actions?: Array<IPreviewAction | IPreviewActionCustom>

默认值为 ["desktop", "tablet", "mobile", "mp-wechat", "zhihu"]。
可从默认值中挑选进行配置,也可使用以下字段进行自定制开发。

说明 默认值
key 按钮唯一标识,不能为空 -
text 按钮文字 -
tooltip 提示 -
className 按钮类名 -
click(key: string) 按钮点击回调事件 -

options.image

说明 默认值
isPreview 是否预览图片 true
preview(bom: Element) => void 图片预览处理 -
说明 默认值
isOpen 是否打开链接地址 true
click(bom: Element) => void 点击链接事件 -

options.hint

说明 默认值
parse 是否进行 md 解析 true
delay 提示 debounce 毫秒间隔 200
emoji 默认表情,可从 lute/emoji_map 中选取,也可自定义 { '+1': '👍', '-1': '👎', 'heart': '❤️', 'cold_sweat': '😰' }
emojiTail 常用表情提示 -
emojiPath 表情图片地址 https://unpkg.com/vditor@${VDITOR_VERSION}/dist/images/emoji
extend: IHintExtend[] 对 @/话题等关键字自动补全的扩展 []
interface IHintData {
  html: string;
  value: string;
}

interface IHintExtend {
    key: string;

    hint?(value: string): IHintData[] | Promise<IHintData[]>;
}

options.upload

  • 文件上传的数据结构如下。后端返回的数据结构不一致时,可使用 format 进行转换。
// POST data
xhr.send(formData);  // formData = FormData.append("file[]", File)
// return data
{
 "msg": "",
 "code": 0,
 "data": {
 "errFiles": ['filename', 'filename2'],
 "succMap": {
   "filename3": "filepath3",
   "filename3": "filepath3"
   }
 }
}
  • 为了防止站外图片失效, linkToImgUrl 可将剪贴板中的站外图片地址传到服务器端进行保存处理,其数据结构如下:
// POST data
xhr.send(JSON.stringify({url: src})); // src 为站外图片地址
// return data
{
 msg: '',
 code: 0,
 data : {
   originalURL: '',
   url: ''
 }
}
  • successformaterror 不会同时触发,具体调用情况如下:
if (xhr.status === 200) {
    if (vditor.options.upload.success) {
        vditor.options.upload.success(editorElement, xhr.responseText);
    } else {
        let responseText = xhr.responseText;
        if (vditor.options.upload.format) {
            responseText = vditor.options.upload.format(files as File [], xhr.responseText);
        }
        genUploadedLabel(responseText, vditor);
    }
} else {
    if (vditor.options.upload.error) {
        vditor.options.upload.error(xhr.responseText);
    } else {
        vditor.tip.show(xhr.responseText);
    }
}
说明 默认值
url 上传 url,为空则不会触发上传相关事件 ''
max 上传文件最大 Byte 10 * 1024 * 1024
linkToImgUrl 剪切板中包含图片地址时,使用此 url 重新上传 ''
linkToImgCallback(responseText: string) 图片地址上传回调 -
linkToImgFormat(responseText: string): string 对图片地址上传的返回值进行格式化 -
success(editor: HTMLPreElement, msg: string) 上传成功回调 -
error(msg: string) 上传失败回调 -
token CORS 上传验证,头为 X-Upload-Token -
withCredentials 跨站点访问控制 false
headers 请求头设置 -
filename(name: string): string 文件名安全处理 name => name.replace(/\W/g, '')
accept 文件上传类型,同 input accept -
validate(files: File[]) => string | boolean 校验,成功时返回 true 否则返回错误信息 -
handler(files: File[]) => string | null | Promise | Promise 自定义上传,当发生错误时返回错误信息 -
format(files: File[], responseText: string): string 对服务端返回的数据进行转换,以满足内置的数据结构 -
file(files: File[]): File[] | Promise<File[]> 将上传的文件处理后再返回 -
setHeaders(): { [key: string]: string } 上传前使用返回值设置头 -
extraData: { [key: string]: string | Blob } 为 FormData 添加额外的参数 -
multiple 上传文件是否为多个 true
fieldName 上传字段名称 'file[]'

options.resize

说明 默认值
enable 是否支持大小拖拽 false
position 拖拽栏位置:'top', 'bottom' 'bottom'
after(height: number) 拖拽结束的回调 -

options.classes

说明 默认值
preview 预览元素上的 className ''

options.fullscreen

说明 默认值
index 全屏层级 90

options.outline

说明 默认值
enable 初始化是否展现大纲 false
position 大纲位置:'left', 'right' 'left'

methods

说明
exportJSON(markdown: string) 根据 Markdown 获取对应 JSON
getValue() 获取 Markdown 内容
getHTML() 获取 HTML 内容
insertValue(value: string, render = true) 在焦点处插入内容,并默认进行 Markdown 渲染
focus() 聚焦到编辑器
blur() 让编辑器失焦
disabled() 禁用编辑器
enable() 解除编辑器禁用
getSelection(): string 返回选中的字符串
setValue(markdown: string, clearStack = false) 设置编辑器内容且选中清空历史栈
clearStack() 清空撤销和重做记录栈
renderPreview(value?: string) 设置预览区域内容
getCursorPosition():{top: number, left: number} 获取焦点位置
deleteValue() 删除选中内容
updateValue(value: string) 更新选中内容
isUploading() 上传是否还在进行中
clearCache() 清除缓存
disabledCache() 禁用缓存
enableCache() 启用缓存
html2md(value: string) HTML 转 md
tip(text: string, time: number) 消息提示。time 为 0 将一直显示
setPreviewMode(mode: "both" | "editor") 设置预览模式
setTheme(theme: "dark" | "classic", contentTheme?: string, codeTheme?: string, contentThemePath?: string) 设置主题、内容主题及代码块风格
getCurrentMode(): string 获取编辑器当前编辑模式
destroy() 销毁编辑器
getCommentIds(): {id: string, top: number}[] 获取所有评论
hlCommentIds(ids: string[]) 高亮评论
unHlCommentIds(ids: string[]) 取消评论高亮
removeCommentIds(removeIds: string[]) 删除评论

static methods

  • 不需要进行编辑操作时,仅需引入 method.min.js 后如下直接调用
Vditor.mermaidRender(document)
import VditorPreview from 'vditor/dist/method.min'
VditorPreview.mermaidRender(document)
  • 需要对页面中的 Markdown 进行渲染时可直接调用 preview 方法,参数如下:
previewElement: HTMLDivElement,   // 使用该元素进行渲染
markdown: string,  // 需要渲染的 markdown 原文
options?: IPreviewOptions {
  mode: "dark" | "light";
  anchor?: number;  // 为标题添加锚点 0:不渲染;1:渲染于标题前;2:渲染于标题后,默认 0
  customEmoji?: { [key: string]: string };    // 自定义 emoji,默认为 {}
  lang?: (keyof II18nLang);    // 语言,默认为 'zh_CN'
  emojiPath?: string;    // 表情图片路径
  hljs?: IHljs; // 参见 options.preview.hljs
  speech?: {  // 对选中后的内容进行阅读
    enable?: boolean,
  };
  math?: IMath; // 数学公式渲染配置
  cdn?: string; // 自建 CDN 地址
  transform?(html: string): string; // 在渲染前进行的回调方法
  after?(); // 渲染完成后的回调
  lazyLoadImage?: string; // 设置为 Loading 图片地址后将启用图片的懒加载
  markdown?: options.preview.markdown;
  theme?: options.preview.theme;
  renderers?: ILuteRender; // 自定义渲染 https://ld246.com/article/1588412297062
}
  • ⚠️ method.min.jsindex.min.js 不可同时引入
说明
previewImage(oldImgElement: HTMLImageElement, lang: keyof II18n = "zh_CN", theme = "classic") 点击图片预览
mermaidRender(element: HTMLElement, cdn = options.cdn, theme = options.theme) 流程图/时序图/甘特图
flowchartRender(element: HTMLElement, cdn = options.cdn) flowchart 渲染
codeRender(element: HTMLElement) 为 element 中的代码块添加复制按钮
chartRender(element: (HTMLElement | Document) = document, cdn = options.cdn, theme = options.theme) 图表渲染
mindmapRender(element: (HTMLElement | Document) = document, cdn = options.cdn, theme = options.theme) 脑图渲染
plantumlRender(element: (HTMLElement | Document) = document, cdn = options.cdn) plantuml 渲染
abcRender(element: (HTMLElement | Document) = document, cdn = options.cdn) 五线谱渲染
md2html(mdText: string, options?: IPreviewOptions): Promise<string> Markdown 文本转换为 HTML,该方法需使用异步编程
preview(previewElement: HTMLDivElement, markdown: string, options?: IPreviewOptions) 页面 Markdown 文章渲染
highlightRender(hljsOption?: IHljs, element?: HTMLElement | Document, cdn = options.cdn) 为 element 中的代码块进行高亮渲染
mediaRender(element: HTMLElement) 特定链接分别渲染为视频、音频、嵌入的 iframe
mathRender(element: HTMLElement, options?: {cdn?: string, math?: IMath}) 对数学公式进行渲染
speechRender(element: HTMLElement, lang?: (keyof II18nLang)) 对选中的文字进行阅读
graphvizRender(element: HTMLElement, cdn?: string) 对 graphviz 进行渲染
outlineRender(contentElement: HTMLElement, targetElement: Element) 对大纲进行渲染
lazyLoadImageRender(element: (HTMLElement | Document) = document) 对启用懒加载的图片进行渲染
setCodeTheme(codeTheme: string, cdn = options.cdn) 设置代码主题,codeTheme 参见 options.preview.hljs.style
setContentTheme(contentTheme: string, path: string) 设置内容主题,contentTheme 参见 options.preview.theme.list

🏗 开发文档

原理相关

环境

  1. 安装 node LTS 版本
  2. 下载最新代码并解压
  3. 根目录运行 npm install
  4. npm run start 启动本地服务器,打开 http://localhost:9000
  5. 修改代码
  6. npm run build 打包代码到 dist 目录

CDN 切换

由于使用了按需加载的机制,默认 CDN 为 https://unpkg.com/vditor@ 版本号

如果代码有修改或需要使用自建 CDN 的话,可按以下步骤进行操作:

  • 初始化时,需对 optionsIPreviewOptions 中的 cdnemojiPath, themes 进行配置
  • highlightRender , mathRender , abcRender , chartRender , mermaidRenderflowchartRendermindmapRendergraphvizRendersetCodeThemesetContentTheme 方法中需添加 cdn 参数
  • 将 build 成功的 dist 目录或 jsDelivr 中的 dist 目录拷贝至正确的位置

升级

版本升级时请仔细阅读 CHANGELOG 中的升级部分

Ⓜ️ Markdown 使用指南

🏘️ 社区

📄 授权

Vditor 使用 MIT 开源协议。

🙏 鸣谢

  • Lute:🎼 一款结构化的 Markdown 引擎,支持 Go 和 JavaScript
  • highlight.js:JavaScript syntax highlighter
  • mermaid:Generation of diagram and flowchart from text in a similar manner as Markdown
  • incubator-echarts:A powerful, interactive charting and visualization library for browser
  • abcjs:JavaScript library for rendering standard music notation in a browser

📽️ 历史

我们在开发 Sym 的初期是直接使用 WYSIWYG 富文本编辑器的。那时候基于 HTML 的编辑器非常流行,项目中引用起来也很方便,也符合用户当时的使用习惯。

后来,Markdown 的崛起逐步改变了大家的排版方式。再加上我们其他几个项目都是面向程序员用户的,所以迁移到 md 上也是大势所趋。我们选择了 CodeMirror,这是一款优秀的编辑器,它对开发者提供了丰富的编程接口,对各种浏览器的兼容性也比较好。

再后来,随着我们项目业务需求方面的沉淀,使用 CodeMirror 有时候会感到比较“笨重”。比如要实现 @ 自动完成用户名列表、插入 Emoji、上传文件等就需要比较深入的二次开发,而这些业务需求恰恰是很多项目场景共有且必备的。

终于,我们决定开始在 Sym 中自己实现编辑器。随着几个版本的迭代,Sym 的编辑器也日趋成熟。在我们运营的社区链滴上陆续有人问我们是否能将编辑器单独抽离出来提供给大家使用。与此同时,我们的前端主程 V 同学对于维护分散在各个项目中的编辑器也感到有点力不从心,外加对 TypeScript 的好感,所以就决定使用 ts 来实现一个全新的浏览器端 md 编辑器。

于是,Vditor 就这样诞生了。

相关帖子

优质回帖
  • Vanessa 1
    支持者 赞助者 订阅者 作者

    前端是这样传送的

        const formData = new FormData();
        for (let i = 0, iMax = uploadFileList.length; i < iMax; i++) {
            formData.append("file[]", uploadFileList[i]);
        }
         xhr.send(formData);
    
  • haaid 1

    文字错误:接触编辑器禁用,「接触」->「解除」

  • someone9891 1
    捐赠者

    此链接 404

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
  • 547176052 1 评论

    image.png

    需要鼠标移开,这个是 css 样式
    Vanessa
  • 547176052

    image.png

    1 回复
  • Vanessa
    支持者 赞助者 订阅者 作者

    按了以后是这样的image.png

  • chuzhuang 2 评论

    你好,当文字是被拖进来的时候,撤销按钮无效。
    影响:分屏模式下,不小心将预览内容拖进来,无法撤销

    而且预览内容不会发生变化
    chuzhuang
  • chuzhuang 6 评论

    image.png
    分屏预览模式下,鼠标从圆圈位置向左上方选中文字,注意,一定要在箭头结束处抬起(多选一点),然后点击右键,bang!我试了,百分之百复现

    image.png

    右键以后选中的内容就消失了,你用的浏览器是?
    Vanessa
    谷歌:版本 87.0.4280.66(正式版本) (64 位)
    chuzhuang
    好像 webkit 内核的都会出现崩溃
    chuzhuang
    @chuzhuang 问题是我重现不了
    Vanessa
    用 webkit 内核的浏览器,比如谷歌,按图的路线多选一点,然后右键点击选中文字的部分,我觉得你是点击其他空白处了,所以没复现
    chuzhuang
    @chuzhuang 版本 88.0.4324.96(正式版本) (x86_64) 还是不行
    Vanessa
  • chuzhuang 2 评论

    image.png

    想问下,这里为啥高度只能是 number,这样就不能设置 100% 或 100vh 之类的,建议改成

    "number"==typeof e.options.height?e.element.style.height=e.options.height+"px":e.element.style.height=e.options.height

    可以使用 auto 或者 window.innerHeight
    Vanessa
    auto 是自动高度,window.innerHeight 是固定高度。我想要的效果是,(100vh - 50px),始终是总高度减去一个固定高度,高度和父容器一致,类似 100% 的高度,谢谢
    chuzhuang
  • chuzhuang 1 评论

    image.png右边的复制公众号等功能虽然设置了 options.preview.actions=[],但还是会留白,所以要加个判断 length=0 的情况

    收到,下个版本修复 Issue #923 · Vanessa219/vditor
    Vanessa
  • chuzhuang 2 评论

    图片的 url 地址可以写相对地址吗,或者是转换的,因为如果服务器换了,那之前的文件图片都要换了 😳

    options.linkBase
    Vanessa
    谢谢,准确的说是 options.preview.markdown.linkBase
    chuzhuang
  • chuzhuang

    image.png
    我想问下,如何加载的时候就是预览模式?

    2 回复
  • Vanessa
    支持者 赞助者 订阅者 作者
      after() {
        window.vditor.vditor.toolbar.elements.preview.firstElementChild.dispatchEvent(new Event("click"))
      },
    
  • 547176052 1 评论

    image.png

    这个是什么错误

    链滴上没有这个 js ,是你用的第三方的插件吧?
    Vanessa 1
  • emmm 1 评论

    能不能暴露一个方法,类似 marked(markown)转换成 html 的方法?

    文档中有写的哦,亲。 md2html
    Vanessa
  • sidian 1 评论

    怎么配置全屏? 🙏

    点击全屏按钮或者 window.vditor.vditor.toolbar.elements.fullscreen.firstElementChild.dispatchEvent(new Event("click"))
    Vanessa
  • 547176052 1 评论

    获得焦点位置

    getCursorPosition():{top: number, left: number}
    

    如何设置焦点位置??

    Issue #720 · Vanessa219/vditor 看一下这个能不能满足你的需求
    Vanessa
  • 547176052 5 评论

    image.png

    好像没有给出解决方法

    你的使用场景是?
    Vanessa
    @Vanessa 控制光标到编辑器尾部,或者上一次的位置
    547176052
    @547176052 末尾的话需要自己写一下了
    Vanessa
    @Vanessa 能不能给个例子 非常感谢
    547176052
    @547176052 尾部的话使用 getSelection,上一次的话会自动记录,直接调用 API 就可以了
    Vanessa
  • 547176052 1 评论

    可以给个让编辑器尾部获取焦点吗?

    也就是说然光标移动到编辑器尾部

    不是尾部,是上次光标的位置
    Vanessa
  • 547176052 1 评论

    怎么让光标不要跑到开始的位置

    需要等待 3.8.2 发布
    Vanessa
  • chuzhuang 1 评论

    初始化时设置

     options.theme: 'dark'
    

    结果文字也黑了

    image.png

    还需要设置 contenttheme
    Vanessa
  • chuzhuang 2 评论

    请问,能打开搜索功能吗,比如 vditor.search('value')

    ctrl+F 使用浏览器的搜索
    Vanessa
    主要是不支持 ctrl+F 啊
    chuzhuang
  • 547176052 2 评论

    能不能自定义解析语法

    比如 输入 but 就解析成为一个 button 按钮

    参考 hint
    Vanessa
    @Vanessa hint 为语法补全 并不是 自定义语法解析渲染
    547176052
  • 547176052 1 评论

    20210223185442.gif

    handler(l)
    

    火狐浏览器不能触发这个方法

    image.png

    firefox 84 无法重现
    Vanessa
  • gztrljh 1 评论

    image.png 这个提示,可以关闭么?

    可以重置样式
    Vanessa 1 赞同
  • chuzhuang 2 评论

    问下可以跳转到第几行,并高亮这一行吗

    类似 gotoLine(n)🙏

    我想同时搜索多个文件,在这个文件中找到后,点击打开这个文件,跳转到搜索内容所在的行
    chuzhuang
    不支持这个功能
    Vanessa
  • gztrljh 1 评论

    我要发表文章的时候要判断用户是否输入了内容是通过 getValue 获取输入判断用户是否输入内容么?

    getValue
    
    input 也可以
    Vanessa
  • gztrljh 1 评论

    image.png

    请问这个 errFiles 一般是什么情况下才会出现这个呢??后台一般是判断出什么情况才需要添加进这个数组呢?我现在就只是给 succMap 给用上了,不知道这个 errFiles 是什么时候需要的。V 姐麻烦解答下

    上传失败
    Vanessa
  • gztrljh 1 评论

    image.png 这个地方如何添加更多的语言?比如 java c++ 等

    输入 j 的话就会提示 java 了
    Vanessa
  • gztrljh 1 评论

    V 姐如何做到页面一刷新自动聚焦到编辑器呢?

    我直接初始化后,这样

    image.png

    好像页面刷新,还是不能聚焦

    写在 after 里面
    Vanessa 1 赞同
  • gztrljh

    3.8.1 之后那个渲染大纲为什么特别大?在 3.7.5 之后没问题的。特别巨大。。

    微信图片 20210121220601.png

    差不多和这个兄弟的类似,之前都不用设置样式的啊。我把 vditor 全部换回 3.7.5 不用设置任何样式,这个大纲没问题的。

    该回帖因已过时而被折叠
    1 操作
    Vanessa 在 2021-02-27 20:46:23 折叠了该回帖
  • gztrljh

    image.png

    版本 3.8.1 ,大纲功能貌似就没用了。

    亲测退回 3.7.5 没问题,可以点击,也不会出现那么大的图标,就只有我上次反馈的点击大纲报错问题。

    该回帖因已过时而被折叠
    1 操作
    Vanessa 在 2021-02-27 20:46:34 折叠了该回帖
  • gztrljh 3 评论

    3.7.6 版本大纲 可以点击 内容也会跟着跳转 但是还是特别大

    image.png

    该回帖因已过时而被折叠
    1 操作
    Vanessa 在 2021-03-01 10:43:04 折叠了该回帖
    使用最新版并添加 vditor 的 css
    Vanessa
    @Vanessa 就是使用最新的 3.8.1 这个是展示文章的时候渲染大纲会这样,编辑的时候不会这样
    gztrljh
    @gztrljh 编辑的时候已经有 css 了,展现的时候你再加一下
    Vanessa
  • 547176052 4 评论

    image.png

    自定义语法解析

    字符串是固定的么?
    Vanessa
    @Vanessa 售价:xx 文件大小:xx 下载链接:xx xx 代表不固定
    547176052
    @547176052 用正则替换试试
    Vanessa
    @Vanessa 虽然没证据,但是我严重怀疑你是个机器人
    547176052
  • gztrljh 6 评论

    3.8.1 大纲渲染有问题,我担心自己样式干扰,我什么样式都不加,引入了 vditor 的样式后,发现确实有问题,

    image.png

    有 UI 的库都需要配合 css 使用
    Vanessa
    @Vanessa 我无论是编辑还是展示的时候都有引入/vditor/dist/index.css 这个 css 的呀
    gztrljh
    @gztrljh 不行的话你参考 https://b3log.org/vditor/demo 这个看一下。现在有点忙
    Vanessa
    @Vanessa 好的,你先忙着,有空的话,V 姐你可以亲自试试,我自认为还算很熟悉 vditor 的用法了,我已经回退到 3.7.5 了
    gztrljh
    @gztrljh 无法重现,请参考源码并运行后查看 http://localhost:9000/render.html
    Vanessa
    @Vanessa 我上传了重现 demo,在另外一个回复里。
    gztrljh
  • gztrljh 3 评论

    大纲渲染重现 demo

    vditor.rar

    这是重现 demo,也就两个简单的 html 页面

    image.png

    另外关于渲染时出现的各种报错和打印信息,我已经研究好了,可以做到控制台干干净净

    image.png

    只需要把 mermaid.min.js 替换成这个地址的就行了

    image.png

    同时把 flowchart.min.js 中的 引入 map 文件那行删掉就行了

    希望 V 姐在下个版本中处理一下,再次感谢。

    你在大纲上先手动加一下 vditor-outline 这个 class
    Vanessa
    @Vanessa 3.8.2 大纲样式修复后了,但是点击大纲无法跳转了。
    gztrljh
    @gztrljh 收到,稍后我看看
    Vanessa
  • chenxiaobin 4 评论

    更新了 3.8.2 版本,自定义渲染的大纲, dom 上的 `data-target-id 属性都没有掉了,这是怎么回事儿?

    Vditor.preview(document.getElementById(this.id), this.docContent, {
            hljs: {
              style: 'monokai' // vim
            },
            after: async () => {
              await this.refreshOutline()
              const element = document.getElementById('preview-outline')
              element.firstElementChild && element.firstElementChild.addEventListener('click', (event) => {
                let target = event.target
                while (target && !target.isEqualNode(element)) {
                  const dom = document.getElementsByClassName('details-center')[0]
                  const targetId = target.getAttribute('data-target-id')
                  if (targetId) {
                    event.preventDefault()
                    event.stopPropagation()
                    dom.scrollTo(0, document.querySelectorAll(`[id="${targetId}"]`)[0].offsetTop - 30)
                    break
                  }
                  target = target.parentElement
                }
              })
            }
          })
    
    2 回复
    传入的 html 需要保持 id
    Vanessa
    @Vanessa 什么意思?没太明白,是否哪里有解释说明,更新前 3.7.6 版本这么写还是正常的?
    chenxiaobin 1 赞同
    @chenxiaobin 我再看看
    Vanessa
    看了下,可能是解析器的问题,3.8.3 修复。感谢耐心的反馈
    Vanessa
  • gztrljh

    github 才 3.8.1 你从哪里升级的 3.8.2 啊 😄

  • ccitsxy 1 评论

    一样的问题 😭

    标题上需要带 id
    Vanessa
  • puzhiwei 1 评论

    在设置好自动补全的配置后,拿到用户列表,之后要如何获取用户 @ 的那个用户呢。

    设置 hint 只能实现返回用户列表,之后有没有什么事件可以确定用户选择的是那个人呢。

    还有就是如何在渲染时给 @ 用户渲染出链接

  • chenxiaobin 1 评论

    删除 H 标题的时候,后面的文档为什么会跟进到光标上,是否可以删除 H 标题是和删除普通文本一样,删除完后保留空行

    收到,下个版本修复
    Vanessa
  • chenxiaobin 8 评论

    1、第一行是一个代码块

    2、第二行是一个 H 标题

    3、现在我希望在代码块和标题中间插入一行普通文本很困难。代码块末尾没办法回车出新行,标题前回车出的新行又带有标题大小属性,操作就很繁琐,是否有优化空间?

    上下键或者 ctrl+shift+e
    Vanessa
    @Vanessa 好像没有解决实际问题,上下键只能移动光标吧,移到标题前回车出来的空白行也是个标题行。快捷键在官方示例上没有效果
    chenxiaobin
    @chenxiaobin 用 Chrome 么?我这里是 ok 的
    Vanessa
    @Vanessa V 姐确实还会出现他提出的问题,我亲测了,3.8.3 谷歌浏览器,刚好是这个代码块和标题连在一起的时候有点不太好操作。当然了,如果标题上方是普通文本内容,那是无伤大雅的
    gztrljh
    @gztrljh 在标题后 ctrl+shift+e @chenxiaobin
    Vanessa
    @Vanessa ctrl+shift+e 这个快捷键是内置的还是要配置啥?为什么我按不出效果呢?
    chenxiaobin
    @chenxiaobin 工具栏上自带的。你工具栏上配置一下
    Vanessa
    @chenxiaobin 是的,ctrl+shift+e 有用。
    gztrljh
  • 547176052 2 评论
    其他 B 站的也解析不出来么?
    Vanessa
    @Vanessa 我亲测,可以解析出来。
    gztrljh
  • gztrljh 2 评论

    image.png

    这个提示点击激活后不会消失了,会把菜单挡住

    这个我也没有好的解决方案,如果可以的话欢迎 PR
    Vanessa
    @Vanessa pr 就算了,我只会用,不过我有个人看法,目前这种情况出现是点击激活的时候这个 tips 会一直存在,正常不激活,鼠标 hover 的时候它才会提示,这个可以不用管,可以加一个判断,就是当前被点击的这个按钮聚焦的时候,就是被点击的时候把 tips 隐藏就行了。你看这个方案怎么样呢?也许是我太吹毛求疵了
    gztrljh
  • gztrljh 3 评论

    image.png

    录音点击

    image.png

    没有权限
    Vanessa
    @Vanessa 哈?V 姐那你下个版本更新是否可以判断一下如果没有权限就用 那个消息提示提示一下 没有权限呢?这也许是个好一点的用户体验呢?
    gztrljh
    @gztrljh 有提示的
    Vanessa
  • felicityyin

    在 after 里调用:

    var evt = document.createEvent('Event');

    evt.initEvent('click', true, true);

    this.vditor.vditor.toolbar.elements.preview.firstElementChild.dispatchEvent(evt);

  • gztrljh

    image.png

  • qibin 1 评论

    若在电脑没有触摸滚动页面的时候,当输入的表格超过宽度,出现的横向滚动条是整个内容 div 的横向滚动条,导致预览的时候,横向滚动条在内容的底部,如果内容过多,一页看不到底部滚动条,那么给用户的错觉是表格就看不了后面的内容,建议给 table 外面加一个 div 层,滚动条在 table 下面,而不是整个内容的底部

    image.png

    image.png

    Vanessa 1
  • gztrljh 2 评论

    V 姐文章展示的时候,图片太小了,想要放大查看是不是需要另外用插件? vditor 在展示的时候没有这个功能哦?编辑的时候我知道点击图片可以放大查看

    previewElement.addEventListener('click', function (event) { if (event.target.tagName === 'IMG') { Vditor.previewImage(event.target, 'zh_CN', outlineElement.classList.contains('dark') ? 'dark' : 'classic') } })
    Vanessa
    @Vanessa 谢谢。
    gztrljh
  • gztrljh 1 评论

    V 姐,linkToImgUrl 这个方法,不是可以把外链的图片都上传到本地服务器么?我图片比较多,比如有几百张,一下粘贴上去,还没完全上传完,拖到文章最下方点击其中一张图片放大没有内容,且此时我提交文章也会失败,是否有判断所有图片上传完毕的方法呢?我想等它图片全部上传完毕后再提交文章,否则就给用户提示,图片上传中?

    isUploading() 方法貌似无法判断 linkToImgUrl 上传图片是否完毕

    可以检测图片地址是否都变为上传后的地址
    Vanessa
  • gztrljh 1 评论

    V 姐文章编辑的时候图片是无法进行懒加载的吧?

    是的
    Vanessa
  • gztrljh 1 评论
     toolbar: [
        {
          hotkey: '⇧⌘S',
          name: 'sponsor',
          tipPosition: 's',
          tip: '成为赞助者',
          className: 'right',
          icon: '<svg t="1589994565028" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2808" width="32" height="32"><path d="M506.6 423.6m-29.8 0a29.8 29.8 0 1 0 59.6 0 29.8 29.8 0 1 0-59.6 0Z" fill="#0F0F0F" p-id="2809"></path><path d="M717.8 114.5c-83.5 0-158.4 65.4-211.2 122-52.7-56.6-127.7-122-211.2-122-159.5 0-273.9 129.3-273.9 288.9C21.5 562.9 429.3 913 506.6 913s485.1-350.1 485.1-509.7c0.1-159.5-114.4-288.8-273.9-288.8z" fill="#FAFCFB" p-id="2810"></path><path d="M506.6 926c-22 0-61-20.1-116-59.6-51.5-37-109.9-86.4-164.6-139-65.4-63-217.5-220.6-217.5-324 0-81.4 28.6-157.1 80.6-213.1 53.2-57.2 126.4-88.8 206.3-88.8 40 0 81.8 14.1 124.2 41.9 28.1 18.4 56.6 42.8 86.9 74.2 30.3-31.5 58.9-55.8 86.9-74.2 42.5-27.8 84.3-41.9 124.2-41.9 79.9 0 153.2 31.5 206.3 88.8 52 56 80.6 131.7 80.6 213.1 0 103.4-152.1 261-217.5 324-54.6 52.6-113.1 102-164.6 139-54.8 39.5-93.8 59.6-115.8 59.6zM295.4 127.5c-72.6 0-139.1 28.6-187.3 80.4-47.5 51.2-73.7 120.6-73.7 195.4 0 64.8 78.3 178.9 209.6 305.3 53.8 51.8 111.2 100.3 161.7 136.6 56.1 40.4 88.9 54.8 100.9 54.8s44.7-14.4 100.9-54.8c50.5-36.3 108-84.9 161.7-136.6 131.2-126.4 209.6-240.5 209.6-305.3 0-74.9-26.2-144.2-73.7-195.4-48.2-51.9-114.7-80.4-187.3-80.4-61.8 0-127.8 38.5-201.7 117.9-2.5 2.6-5.9 4.1-9.5 4.1s-7.1-1.5-9.5-4.1C423.2 166 357.2 127.5 295.4 127.5z" fill="#141414" p-id="2811"></path><path d="M353.9 415.6m-33.8 0a33.8 33.8 0 1 0 67.6 0 33.8 33.8 0 1 0-67.6 0Z" fill="#0F0F0F" p-id="2812"></path><path d="M659.3 415.6m-33.8 0a33.8 33.8 0 1 0 67.6 0 33.8 33.8 0 1 0-67.6 0Z" fill="#0F0F0F" p-id="2813"></path><path d="M411.6 538.5c0 52.3 42.8 95 95 95 52.3 0 95-42.8 95-95v-31.7h-190v31.7z" fill="#5B5143" p-id="2814"></path><path d="M506.6 646.5c-59.6 0-108-48.5-108-108v-31.7c0-7.2 5.8-13 13-13h190.1c7.2 0 13 5.8 13 13v31.7c0 59.5-48.5 108-108.1 108z m-82-126.7v18.7c0 45.2 36.8 82 82 82s82-36.8 82-82v-18.7h-164z" fill="#141414" p-id="2815"></path><path d="M450.4 578.9a54.7 27.5 0 1 0 109.4 0 54.7 27.5 0 1 0-109.4 0Z" fill="#EA64F9" p-id="2816"></path><path d="M256 502.7a32.1 27.5 0 1 0 64.2 0 32.1 27.5 0 1 0-64.2 0Z" fill="#EFAFF9" p-id="2817"></path><path d="M703.3 502.7a32.1 27.5 0 1 0 64.2 0 32.1 27.5 0 1 0-64.2 0Z" fill="#EFAFF9" p-id="2818"></path></svg>',
          click () {alert('捐赠地址:https://ld246.com/sponsor')},
        }],
    
    

    V 姐这个 icon 属性好像只能替换正常的 图标?

    比如这个全屏图标,全屏后它默认的是这个,image.png

    好像没有配置 替换这个 图标的?😂

    目前没有
    Vanessa
  • gztrljh 1 评论

    V 姐 渲染和编辑文章的时候,必须要异步请求渲染吧?

    首次初始化的时候需要在 after 中回掉
    Vanessa
请输入回帖内容 ...