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:
2026-05-30 15:56:22 +08:00
parent c9808ab9b2
commit 6ec12010f3
121 changed files with 8267 additions and 613 deletions

View File

@ -8,17 +8,17 @@ import (
// Post 帖子/文章模型
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"`
BodyHTML string `gorm:"type:text" json:"body_html"`
UserID uint `gorm:"index;not null" json:"user_id"`
Status string `gorm:"type:varchar(20);index;default:draft;not null" json:"status"`
RejectReason string `gorm:"type:varchar(500);default:''" json:"reject_reason,omitempty"`
AllowComment bool `gorm:"default:true" json:"allow_comment"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
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 for list
UserID uint `gorm:"index;not null" json:"user_id"`
Status string `gorm:"type:varchar(20);index;default:draft;not null" json:"status"`
RejectReason string `gorm:"type:varchar(500);default:''" json:"reject_reason,omitempty"`
AllowComment bool `gorm:"default:true" json:"allow_comment"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
// 非数据库字段(联表查询填充)
// gorm:"-:migration" 指定不创建/迁移该列,"<-:false" 禁止写入,"column:author_name" 允许

View 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 替换后的 bodyshortcode → 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
}