This repository has been archived on 2026-06-21. You can view files and clone it, but cannot push or open issues or pull requests.
Files
MetaLab/docs/vditor-migration-plan.md
Victor_Jay 6ec12010f3 feat: Tiptap替换为Vditor + MD存储重构 + Shortcode卡片扩展
- 编辑器:Tiptap 替换为 Vditor 3.11.2(wysiwyg 模式,本地托管 5.8MB,去 CDN)
- 存储:Body 字段从 HTML 改为 Markdown,去除 BodyHTML,新增 Excerpt 列表摘要
- 安全:去除 bluemonday 依赖(MD 纯文本无 XSS 风险),go.mod 已清理
- Shortcode:新增 [zone:type:params] 扩展语法,支持活动/游戏/投票/资源卡片
- 图片上传:POST /api/posts/upload-image 直接返回 Vditor 原生响应格式
- 草稿:localStorage 键名改为 draft_post_body_md,与旧 HTML 草稿隔离
- 详情页:Vditor.method.min.js 客户端 MD → HTML 渲染,去 highlight.js CDN
- 样式:去 Tiptap 样式 ~190 行,精简工具栏 CSS,新增卡片样式
- 文档:vditor-migration-plan.md 完整记录迁移决策、架构、用法
2026-05-30 15:56:22 +08:00

12 KiB
Raw Blame History

Vditor 整合 + MD 存储迁移方案

状态:已实施 ✓ 2026-05-30

决策与结果

决策 结论 结果
新增 API 不新增 仅改 POST /api/posts/upload-image 响应格式
桥接/转换层 不做 API 直接返回 Vditor 原生格式,前端零适配
存储格式 HTML → Markdown MD 更小、更灵活、更可移植
向后兼容 不考虑 开发阶段,已有帖子数据量小
依赖方式 本地托管 从 npm registry 下载 dist5.8MB(仅必需插件)

最终数据流

编辑器: Vditor(wysiwyg) → vditor.getValue() → MD 纯文本
                                    ↓
存储:   后端直接存 MD纯文本无 XSS 风险,无需 sanitize
           └─ generateExcerpt() 生成 plain text 摘要
                                    ↓
显示:   Vditor.preview() 客户端 MD → HTML + github-dark 代码主题

变更文件清单11 个)

文件 改动 详情
internal/model/post.go 修改 Body → MD 存储,去 BodyHTML,加 Excerptvarchar 500
internal/service/post_service.go 重写 去 bluemondaygenerateExcerpt()(纯 Go regex无外部依赖
internal/controller/post_controller.go 修改 UploadImage → Vditor 原生格式,ShowPage 去 BodyHTMLhtml/template import
internal/common/response.go 新增函数 VditorUploadOk() — Vditor 图片上传成功响应
templates/.../posts/new.html 重写 Tiptap → Vditor去除 importmap/工具栏/语言选择器
templates/.../posts/show.html 重写 Vditor 客户端 MD 渲染,去 highlight.js CDN去代码标签注入脚本
templates/.../posts/index.html 微调 Body 截断 → Excerpt(带降级回退)
templates/.../editor.js 重写 Vditor 初始化 + CSRF + upload + draft + word count + submit
templates/.../posts.css 删减 去 Tiptap 样式(~150 行)+ 工具栏样式(~40 行),加 Vditor 微调
static/vditor/dist/ 新增 本地 Vditor 文件5.8MB
go.mod / go.sum 清理 移除 bluemonday 依赖

不涉及的路由/中间件/仓储:零改动。


各层详细实现

1. Model internal/model/post.go

type Post struct {
    ID           uint           `gorm:"primarykey" json:"id"`
    Title        string         `gorm:"type:varchar(200);not null" json:"title"`
    Body         string         `gorm:"type:text" json:"body"`            // Markdown content
    Excerpt      string         `gorm:"type:varchar(500)" json:"excerpt"` // plain text summary
    // ... 其余字段不变
}

2. Service internal/service/post_service.go

  • 删除 sanitizePolicybluemonday UGCsanitizeHTML()
  • 新增 generateExcerpt(md string) string
    • 按行逐条去掉 MD 语法(代码块、图片、标题 #、粗体/斜体、引用 >、列表标记等)
    • 链接保留文字 [text](url)text
    • 按 rune 截断 300 字符,末尾加 ...
    • 纯 Go regexp 实现,零外部依赖
  • Create() / Update() 生成 ExcerptMD 直存无消毒

3. Controller internal/controller/post_controller.go

UploadImage — Vditor 原生响应格式:

{
    "code": 0,
    "msg": "",
    "data": {
        "errFiles": [],
        "succMap": {
            "微信截图_2026.png": "/uploads/posts/1_1717071234567.png"
        }
    }
}

ShowPage — 去掉 PostBodyHTMLMD 由前端渲染。

4. Common internal/common/response.go

func VditorUploadOk(c *gin.Context, succMap map[string]string) {
    c.JSON(http.StatusOK, gin.H{
        "code": 0,
        "msg":  "",
        "data": gin.H{
            "errFiles": []string{},
            "succMap":  succMap,
        },
    })
}

5. 编辑器 posts/new.html + editor.js

Vditor 初始化关键配置:

new Vditor('vditor', {
    mode: 'wysiwyg',
    cdn: '/static/vditor',           // 所有动态加载走本地
    height: '100%',
    lang: 'zh_CN',
    toolbar: [
        'headings', 'bold', 'italic', 'strike', '|',
        'line', 'code', 'inline-code', 'link', 'quote', '|',
        'list', 'ordered-list', 'check', 'outdent', 'indent', '|',
        'upload', 'table', '|',
        'undo', 'redo', '|',
        'fullscreen', 'code-theme', '|',
        'outline', 'preview', 'devtools',
    ],
    upload: {
        url: '/api/posts/upload-image',
        fieldName: 'file',
        max: 5 * 1024 * 1024,
        accept: 'image/jpg,image/jpeg,image/png,image/gif,image/webp',
        setHeaders() {                         // 每次请求重新读取 CSRF
            const meta = document.querySelector('meta[name="csrf-token"]');
            return { 'X-CSRF-Token': meta ? meta.getAttribute('content') : '' };
        },
    },
    preview: {
        theme: { current: 'light', path: '/static/vditor/dist/css/content-theme' },
        hljs: { style: 'github-dark', enable: true },
    },
})

CSRF 处理upload.setHeaders 为函数,每次上传前动态读取 meta[name="csrf-token"],不会因 token 过期而失败。

草稿机制

  • 每 2 秒自动保存 title + mdlocalStorage
  • 键名:draft_post_title / draft_post_body_md(与旧 Tiptap HTML 草稿隔离)
  • 新帖模式下自动恢复草稿

表单提交vditor.getValue() 获取 MDPOST /api/postsPUT /api/posts/:id

6. 详情页 posts/show.html

<!-- MD 内容通过隐藏 textarea 安全传递给 JS -->
<textarea id="postMdContent" style="display:none">{{.Post.Body}}</textarea>

<!-- Vditor 仅需 method.min.js47KB不需要完整编辑器 -->
<script src="/static/vditor/dist/method.min.js"></script>
<script>
Vditor.preview(document.getElementById('postContent'), md, {
    cdn: '/static/vditor',
    theme: { current: 'light', path: '/static/vditor/dist/css/content-theme' },
    hljs: { style: 'github-dark', enable: true },
});
</script>

7. 列表页 posts/index.html

<!-- Excerpt 优先,降级回退 Body 截断(兼容旧数据) -->
<p class="post-card-summary">
    {{if .Excerpt}}{{.Excerpt}}{{else}}{{printf "%.200s" .Body}}{{end}}
</p>

8. 样式 posts.css

  • 移除:.tiptap 及所有子选择器(~100 行、highlight.js 硬编码主题色(~35 行)、手动工具栏样式(~40 行、JS 注入代码标签 CSS
  • 保留:.editor-layout / .editor-header / .editor-main / .editor-pane / .editor-statusbar(布局)
  • 新增:#vditor flex 适配、.vditor-toolbar / .vditor-content 主题微调

Vditor 本地文件结构

templates/MetaLab-2026/static/vditor/dist/
├── index.css                          # 43KB  编辑器 + 预览全部样式
├── index.min.js                       # 292KB 编辑器核心wysiwyg/sv/ir + 工具栏 + 上传)
├── method.min.js                      # 47KB  独立方法Vditor.preview 等,详情页用)
├── css/content-theme/
│   ├── light.css                      # 内容区明亮主题
│   ├── dark.css                       # 内容区暗色主题
│   ├── ant-design.css                 # Ant Design 主题
│   └── wechat.css                     # 微信主题
├── images/
│   ├── img-loading.svg                # 上传进度指示
│   ├── logo.png                       # Vditor logo
│   └── emoji/                         # 内置表情包b3log/octocat/doge 等)
├── js/
│   ├── highlight.js/
│   │   ├── highlight.min.js           # 1.4MB 代码高亮核心
│   │   ├── third-languages.js         # 额外语言支持
│   │   └── styles/*.min.css           # 60+ 代码主题
│   ├── lute/
│   │   └── lute.min.js                # 3.9MB WASM MD 解析器Vditor 核心依赖)
│   ├── i18n/
│   │   └── zh_CN.js                   # 8KB  中文语言包
│   └── icons/
│       ├── material.js                # Material Design 图标
│       └── ant.js                     # Ant Design 图标

已剔除的插件(节省 16MB+

插件 大小 功能 对技术论坛无用
mathjax 6.5MB LaTeX 数学公式
mermaid 2.6MB 流程图/时序图
graphviz 2.0MB 图谱渲染
katex 1.5MB 数学公式引擎
echarts 1.0MB 图表渲染
markmap 836KB 思维导图
abcjs 356KB 五线谱
plantuml 32KB PlantUML
flowchart.js / smiles-drawer ~400KB 流程图 / 化学分子式

Shortcode 扩展([zone:type:params]

语法

在 Markdown 正文中嵌入特殊卡片,语法为 [zone:类型:参数]

# 周末活动汇总

下面是我们本周的推荐活动:

[zone:event:summer2026]

更多内容请关注官方动态...

已注册类型

语法 渲染结果 状态
[zone:event:活动ID] 活动卡片(🎪 图标 + ID + 跳转链接) 已实现(占位)
[zone:game:游戏slug] 游戏卡片(🎮 图标 + slug + 跳转链接) 已实现(占位)
[zone:poll:投票ID] 投票组件 预留
[zone:resource:资源ID] 资源推荐卡片 预留

架构

MD body "[zone:event:summer2026]"
     │
     ▼
ShortcodeService.Process()        ← 后端:正则替换 [zone:...] → 占位 <div>
     │
     ▼
VDitor.preview()                  ← 前端MD → HTML占位 div 原样保留
     │
     ▼
renderShortcodes()                ← 前端 shortcode.js扫描 .zone-card渲染 UI 卡片

文件清单

文件 职责
internal/model/shortcode.go 类型常量 + 正则 + DTO
internal/service/shortcode_service.go 解析器 + 占位 HTML 生成器
internal/controller/post_controller.go ShowPage/ShowAPI 调用 Process()
internal/router/deps_extra.go DI 注入 ShortcodeService
templates/.../js/shortcode.js 前端卡片渲染器
templates/.../html/posts/show.html 引入 shortcode.js + 调用 renderShortcodes()
templates/.../static/css/posts.css 卡片样式
templates/.../static/js/editor.js hint 自动补全提示

如何添加新类型

  1. internal/model/shortcode.go:注册 ShortcodeType 常量
  2. internal/service/shortcode_service.go:在 renderPlaceholder() 添加 case
  3. templates/.../js/shortcode.js:在 cardRenderers 注册渲染函数
  4. templates/.../static/js/editor.js:在 hint.extend 添加补全提示

注意事项

  • 未注册或格式错误的 shortcode 不报错,原文保留,避免破坏用户内容
  • 占位 div 结构为 <div class="zone-card" data-zone-type="..." data-zone-id="...">
  • 编辑器输入 [zone: 时自动弹出补全列表Vditor hint.extend
  • 后端 API 就绪后,将 shortcode.js 中的静态卡片替换为 fetch() 动态数据即可

不涉及的部分

  • internal/router/api.go — 路由不变(POST /api/posts/upload-image 端点不变)
  • internal/router/frontend.go — SSR 页面路由不变
  • internal/repository/post_repo.go — 仓储不变Model 字段变更由 GORM 自动映射)
  • internal/middleware/ — CSRF / Auth 中间件不变
  • internal/config/ — 配置不变
  • cmd/server/main.goStatic() 映射不变(/statictemplates/.../static

注意事项

  1. DB SchemaGORM AutoMigrate 自动添加 excerpt 列;旧的 body_html 列残留但不影响运行GORM 不会 DROP可手动清理
  2. 已有帖子:旧 HTML body 在 MD 预览中会显示为 HTML 源码,可手动清理或通过 SQL 迁移
  3. CSRFupload.setHeaders 为函数,每次上传前动态读取最新 token不受 token 过期影响
  4. 时序Vditor 主文件先加载lute/highlight.js/i18n 等由 Vditor 内部按需动态加载,无需手动控制
  5. bluemonday:已从 go.mod 中移除(go mod tidy 自动清理)