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 完整记录迁移决策、架构、用法
This commit is contained in:
92
internal/model/shortcode.go
Normal file
92
internal/model/shortcode.go
Normal file
@ -0,0 +1,92 @@
|
||||
package model
|
||||
|
||||
// =============================================================================
|
||||
// Shortcode — Markdown 扩展语法
|
||||
//
|
||||
// 用法:在 MD 正文中插入 [zone:类型:参数],渲染时替换为对应 UI 卡片。
|
||||
//
|
||||
// 支持的 shortcode:
|
||||
// [zone:event:活动ID] → 官方活动卡片(标题、时间、封面、链接)
|
||||
// [zone:game:游戏slug] → 游戏信息卡片(名称、封面、类型)
|
||||
// [zone:poll:投票ID] → 投票组件(预留)
|
||||
// [zone:resource:资源ID] → 资源卡片(工具/素材推荐,预留)
|
||||
//
|
||||
// 扩展现有类型:在 ShortcodeRegistry 中注册新类型 + 实现 Renderer 即可。
|
||||
// =============================================================================
|
||||
|
||||
import "regexp"
|
||||
|
||||
// =============================================================================
|
||||
// 语法常量
|
||||
// =============================================================================
|
||||
|
||||
// ShortcodePattern 匹配所有 [zone:type:params] 模式的 shortcode
|
||||
// 捕获组:$1=类型, $2=参数
|
||||
var ShortcodePattern = regexp.MustCompile(`\[zone:(\w+):([^\]]+)\]`)
|
||||
|
||||
// =============================================================================
|
||||
// 已注册的 Shortcode 类型
|
||||
// =============================================================================
|
||||
|
||||
// ShortcodeType 定义一种 shortcode 的类型标识
|
||||
type ShortcodeType string
|
||||
|
||||
const (
|
||||
ShortcodeEvent ShortcodeType = "event" // [zone:event:活动ID]
|
||||
ShortcodeGame ShortcodeType = "game" // [zone:game:游戏slug]
|
||||
ShortcodePoll ShortcodeType = "poll" // [zone:poll:投票ID] ※预留(后端API未实现)
|
||||
ShortcodeResource ShortcodeType = "resource" // [zone:resource:资源ID] ※预留(后端API未实现)
|
||||
)
|
||||
|
||||
// allTypes 所有已定义的 shortcode 类型,添加新类型时在这里追加
|
||||
var allTypes = []ShortcodeType{
|
||||
ShortcodeEvent,
|
||||
ShortcodeGame,
|
||||
ShortcodePoll,
|
||||
ShortcodeResource,
|
||||
}
|
||||
|
||||
// IsValidType 检查给定类型是否已注册
|
||||
func IsValidType(t string) bool {
|
||||
for _, st := range allTypes {
|
||||
if string(st) == t {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 解析结果
|
||||
// =============================================================================
|
||||
|
||||
// Shortcode 一次解析的结果
|
||||
type Shortcode struct {
|
||||
Raw string // 原始文本,如 "[zone:event:abc123]"
|
||||
Type ShortcodeType // 类型
|
||||
Params string // 参数(ID/slug 等)
|
||||
}
|
||||
|
||||
// ShortcodeResult 整个 body 的解析结果
|
||||
type ShortcodeResult struct {
|
||||
// ProcessedBody 替换后的 body(shortcode → HTML 占位符),可直接传给模板渲染
|
||||
ProcessedBody string
|
||||
// Shortcodes 本次解析到的所有 shortcode 列表
|
||||
Shortcodes []Shortcode
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 卡片渲染数据 DTO
|
||||
// =============================================================================
|
||||
|
||||
// ShortcodeCard 渲染一张卡片的通用数据
|
||||
// 前端根据 Type 决定具体 UI 组件,字段按需填充
|
||||
type ShortcodeCard struct {
|
||||
Type ShortcodeType `json:"type"`
|
||||
ID string `json:"id"` // 业务 ID(活动ID / 游戏slug)
|
||||
Title string `json:"title,omitempty"`
|
||||
Cover string `json:"cover,omitempty"`
|
||||
Desc string `json:"desc,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
Extra string `json:"extra,omitempty"` // 扩展字段(时间、标签等,JSON string)
|
||||
}
|
||||
Reference in New Issue
Block a user