- 编辑器: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 完整记录迁移决策、架构、用法
93 lines
3.5 KiB
Go
93 lines
3.5 KiB
Go
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)
|
||
}
|