https://www.flinthub.top/plughub/vditor
已经发布,用户后台安装体验即可
用 Vditor 4.0.1(MIT)替换核心富文本编辑器。
纯插件实现,不改核心一行代码;关掉插件即完全回退,已发布内容不受影响。
一、它做了什么
| 项 | 说明 |
|---|---|
| 替换范围 | 后台可逐个勾选:发帖 / 回复 / 博客 / 私信 / 后台编辑 / 插件页面 |
| 编辑器能力 | 即时渲染(IR)、所见即所得(WYSIWYG)、分屏(SV)三种模式;大纲、表格、代码高亮、全屏、字数统计、全套快捷键 |
| 数据格式 | 提交的仍是 base64(HTML),与核心编辑器完全一致 —— 服务端零改动 |
| 兼容层 | 提供 window.ACTIVE_EDITORS,draft_saver(草稿)、notice_center、qa 等依赖它的插件照常工作 |
| 上传 | 走核心 POST /api/upload?ajax=1,沿用核心的权限、CSRF、限流、缩略图规则 |
二、⚠️ 已知取舍(先读这段)
Vditor 是 Markdown 编辑器,而本站存的是 HTML。两者不是无损互转,以下格式会丢:
| 格式 | 结果 | 原因 |
|---|---|---|
| 字体颜色 / 背景色 | ❌ 不提供 | Markdown 无对应语法 |
| 段落对齐(左/中/右) | ❌ 不提供 | 同上 |
| 合并单元格(colspan/rowspan) | ❌ 不提供 | 核心净化白名单无该属性 |
| 任务列表(勾选框) | ❌ 不提供 | 白名单无 <input> |
| 录音 / 数学公式 / 流程图 | ❌ 不提供 | 白名单无 <audio> / <math> / <svg> |
| 一级标题 h1 | ⚠️ 默认关闭 | 白名单只放行 h2~h4,开了会「发布后降级成正文」 |
设计原则:不提供「点了会坏」的按钮。所有 Markdown 表达不了的格式,在工具栏层面就不存在。
编辑历史帖子时:插件会做一次真实的「HTML → Markdown → HTML」往返比对
(用 Vditor 自带的 lute 引擎跑,不是猜的)。差异率超过阈值(后台可配,默认 15%)
→ 编辑器上方出现黄色提示条 + 「改用源码模式」按钮。
源码模式不是 Vditor 的 SV 分屏(SV 编辑的仍是 Markdown,格式该丢还是丢)。
提示条给的是插件自己实现的 HTML 源码模式:一个直接装原始 HTML 的 textarea,
提交时逐字节原样交给服务端 → 真正无损。改完点「返回可视化编辑」会重新转成 Markdown。
工具栏里本来就有 Vditor 的 edit-mode 按钮可切 IR / WYSIWYG / SV,所以提示条不重复提供。
三、构建产物
运行文件已随包提供,位于:
plugins/vditor/assets/lib/vditor/dist/
├── index.min.js 296 KB webpack 主包
├── index.css 48 KB
├── js/lute/lute.min.js 3.57 MB Markdown↔HTML 引擎(硬依赖)
├── js/highlight.js/highlight.min.js 1.03 MB 代码高亮
├── js/highlight.js/third-languages.js 21 KB
├── js/highlight.js/styles/{github,github-dark}.min.css
├── js/icons/ant.js 42 KB
├── js/i18n/{zh_CN,zh_TW,en_US}.js
├── css/content-theme/{light,dark}.css
└── images/emoji/*.png
合计 30 个文件 / 约 5.04 MB。
自己重新构建(升级 Vditor 时)
cd D:\phpstudy_pro\WWW\vditor-4.0.1
# 1) 装依赖(跳过 puppeteer 的 Chromium 下载,构建不需要它)
$env:PUPPETEER_SKIP_DOWNLOAD = '1'
npm install --legacy-peer-deps
# 2) 构建
npm run build
# 3) 裁剪 + 转 LF / 去 BOM + 落到插件目录
php ..\.workbuddy-ai\tmp\vditor_trim.php
两个必须的依赖修正(已写进 vditor-4.0.1/package.json,重装不会丢):
| 包 | 值 | 原因 |
|---|---|---|
ajv |
^8.17.1 |
上游用 pnpm(不强制 peer),npm 下 ajv-keywords@5 会解析到 ajv 6 → 构建报 Cannot find module 'ajv/dist/compile/codegen' |
@types/diff-match-patch |
1.0.32(锁死) |
1.0.36 把声明改成 export = 形式 → src/ts/undo/index.ts 报 TS2749 |
只裁剪
dist里的白名单文件,不要整个 dist 拷过去(全量 21.9 MB,含 mathjax 6.3 MB / mermaid 3.4 MB 等用不到的重货)。
四、后台配置
入口:后台 → 插件 → Vditor 编辑器(/admin/vditor)
| 项 | 默认 | 说明 |
|---|---|---|
| 启用 Vditor 编辑器 | 关 | 总开关。关掉立即全站回退核心编辑器 |
| 发帖 / 编辑帖子 | 开 | post/create、post/edit、thread/edit |
| 帖子回复 | 开 | thread/show |
| 博客 / 私信 / 后台编辑 | 关 | 按需开 |
| 插件页面 | 关 | 问答 / 网盘 / 单页 / 投稿 / 公告。建议最后再开 |
| 默认编辑模式 | IR | IR 最接近原可视化体验;SV 适合写源码 |
| 发帖 / 回复高度 | 560 / 320 | 160~2000 |
| 代码高亮主题 | 跟随主题 | 浅色页用 github、深色页用 github-dark。站点同时有浅色/暗色用户时保持这一项 |
| 格式损失提示阈值 | 15% | 0 = 关闭探测 |
| 允许一级标题 | 关 | 关时 h1 自动降成 h2(白名单没有 h1) |
五、生效条件(三者必须同时满足)
- 插件已激活
- 总开关已开启
- 构建产物完整(
Plugin::libReady()检查 4 个关键文件)
任一不满足 → 所有钩子早退,核心编辑器照常工作(Fail-Safe)。
编辑器是「增强」不是「门禁」,绝不允许因为插件没配好就让人发不了帖。
后台设置页顶部会用红/黄条明确告诉你差哪一项。
六、常见问题
Q:装上之后发帖页没变化?
A:检查后台状态徽章。三个条件任一未满足都会这样,页面顶部有具体原因。
Q:为什么颜色/对齐按钮没了?
A:见第二节。这是刻意的设计取舍,不是 bug。
Q:历史帖子里的彩色文字打开编辑后变黑白了?
A:Markdown 无法表达颜色。如果差异超阈值会有黄色提示条,点「切换到源码模式」可保留原始 HTML。
Q:草稿插件还能用吗?
A:能。插件提供了 window.ACTIVE_EDITORS 兼容层,读写都走 Vditor 的 API。
Q:控制台报 404 / 被 CSP 拦截?
A:检查 plugins/vditor/assets/lib/vditor/dist/ 是否完整;options.cdn 已强制指向站点同源路径,不会请求外网。
Q:为什么公式显示成原始的 $x^2$?
A:KaTeX 有 326 KB JS + 1 MB 字体,没随包带(会让体积从 5 MB 涨到 6.4 MB)。所以公式渲染是关掉的,
不会去请求不存在的 /dist/js/katex/(那才是真 404)。要公式就把 Vditor 完整 dist 里的
js/katex/ 拷进 dist/js/,再把 assets/boot.js 里的 math: { engine: '' } 改成 math: { engine: 'KaTeX' }。
Q:为什么没有「代码主题」切换按钮?
A:Vditor 那个面板列的是它自带的全部 ~90 个主题 CSS,我们只带了 github / github-dark 两份 ——
放了就是「点别的必 404」。主题在后台选,不在前台切。
Q:mermaid / 流程图 / 脑图能渲染吗?
A:不能,同样没带产物(mermaid 3.4 MB、graphviz 1.9 MB…)。这类代码块会原样显示成代码,不会报错。
Q:能只对发帖开、回复不开吗?
A:可以,页面开关互相独立。
七、实现要点(改代码前必读)
| 环节 | 做法 | 为什么 |
|---|---|---|
| 关掉核心编辑器 | controller_view_before 里置 $data['needsEditor'] = false |
该钩子的 $data 是引用(Controller.php:49 的 &$data + EXTR_REFS)→ 能真的改到控制器里的数组 |
| 跨钩子传状态 | Plugin::markActive() 写类静态属性 |
钩子在闭包作用域里 include,变量传不出去,只有类静态可靠 |
| 回填已有内容 | 挂载前先写 mount.innerHTML |
initUI.ts:9 第一行就清空宿主;originalInnerHTML 在 init() 里已捕获,afterRender 会 HTML2Md() —— 无时序竞态 |
| 关掉草稿缓存 | cache: { enable: false } |
默认 true,initUI.ts:173 先读 localStorage → 会盖掉回填的旧内容 |
| 只引 1 个 JS | 只写 index.min.js |
lute / i18n / icons / highlight 都由 Vditor 自己按 options.cdn 加载;手写会重复加载 |
| 同步脚本 | index.min.js 在 head 同步、boot.js 在 body 末尾同步 |
defer 要等解析完才跑,boot.js 会先执行 → 那时 window.Vditor 还没定义 |
ACTIVE_EDITORS |
detached 假节点 + defineProperty 接管 innerHTML |
draft_saver 会 ed.editor.innerHTML = ... 直接赋值;给真实 DOM 会绕过 Vditor 内部模型(表面有字、getHTML() 还是旧的) |
| 提交格式 | base64(UTF-8 bytes) 写进同名隐藏域 |
与 editor.js:1113 完全一致,Content::decode() 零改动 |
| 引用功能 | 重写 window.quotePost |
核心 editor.js 不加载 → 原实现不存在 |
| 写设置 | Settings::get/update 绝不能传null |
两个签名都是非可空 string(Settings.php:31/255)。userland 函数传 null 直接 TypeError,而核心 Plugin::activate() 是 catch (\Throwable) 静默回滚 → 后台只显示「启用失败」、零日志。判「键是否存在」用哨兵串;卸载清空写空串(Settings 没有 delete) |
| 校验 CSRF | Csrf::verifyOrDie($_POST['csrf'] ?? '') |
Csrf::verifyOrDie($token) 是必填 1 参(Csrf.php:60)。写成无参 → ArgumentCountError → 保存接口 500 |
已带的产物之外都是关的:KaTeX(math.engine='')、mermaid / graphviz / echarts / plantuml /
markmap / SMILES / wavedrom(无产物时 Vditor 只是不渲染,不会抛错)。
八、许可
- Vditor 4.0.1:MIT,Copyright (c) 2018-present B3log 开源
- 插件本体:与 FlintHub 一致