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:
@ -32,7 +32,7 @@
|
||||
<span class="post-author">{{if .AuthorName}}{{.AuthorName}}({{.UserID}}){{else}}UID{{.UserID}}{{end}}</span>
|
||||
<span class="post-time">{{.CreatedAt.Format "2006-01-02 15:04"}}</span>
|
||||
</div>
|
||||
<p class="post-card-summary">{{printf "%.200s" .Body}}</p>
|
||||
<p class="post-card-summary">{{if .Excerpt}}{{.Excerpt}}{{else}}{{printf "%.200s" .Body}}{{end}}</p>
|
||||
</article>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
@ -6,6 +6,10 @@
|
||||
<div class="editor-layout">
|
||||
<form id="postForm" class="editor-form">
|
||||
<input type="hidden" id="postId" value="{{if .Post}}{{.Post.ID}}{{end}}">
|
||||
{{/* 编辑模式下传入已有的 MD 内容 */}}
|
||||
{{if .Post}}
|
||||
<textarea id="editBody" style="display:none">{{.Post.Body}}</textarea>
|
||||
{{end}}
|
||||
|
||||
{{/* 标题区域 */}}
|
||||
<div class="editor-header">
|
||||
@ -15,62 +19,10 @@
|
||||
maxlength="200" required autofocus>
|
||||
</div>
|
||||
|
||||
{{/* 工具栏 */}}
|
||||
<div class="editor-toolbar" id="toolbar">
|
||||
<button type="button" data-action="bold" title="加粗 (Ctrl+B)"><strong>B</strong></button>
|
||||
<button type="button" data-action="italic" title="斜体 (Ctrl+I)"><em>I</em></button>
|
||||
<button type="button" data-action="strike" title="删除线"><s>S</s></button>
|
||||
<button type="button" data-action="code" title="行内代码"></></button>
|
||||
<span class="toolbar-divider"></span>
|
||||
<button type="button" data-action="h1" title="标题 1">H1</button>
|
||||
<button type="button" data-action="h2" title="标题 2">H2</button>
|
||||
<button type="button" data-action="h3" title="标题 3">H3</button>
|
||||
<span class="toolbar-divider"></span>
|
||||
<button type="button" data-action="bulletList" title="无序列表">•</button>
|
||||
<button type="button" data-action="orderedList" title="有序列表">1.</button>
|
||||
<button type="button" data-action="blockquote" title="引用">"</button>
|
||||
<span class="toolbar-divider"></span>
|
||||
<button type="button" data-action="codeBlock" title="代码块">{ }</button>
|
||||
<select id="codeLangSelect" class="code-lang-select" style="display:none" title="选择语言">
|
||||
<option value="text">text</option>
|
||||
<option value="javascript">JavaScript</option>
|
||||
<option value="typescript">TypeScript</option>
|
||||
<option value="python">Python</option>
|
||||
<option value="go">Go</option>
|
||||
<option value="rust">Rust</option>
|
||||
<option value="java">Java</option>
|
||||
<option value="cpp">C++</option>
|
||||
<option value="c">C</option>
|
||||
<option value="csharp">C#</option>
|
||||
<option value="ruby">Ruby</option>
|
||||
<option value="php">PHP</option>
|
||||
<option value="swift">Swift</option>
|
||||
<option value="kotlin">Kotlin</option>
|
||||
<option value="sql">SQL</option>
|
||||
<option value="bash">Bash</option>
|
||||
<option value="json">JSON</option>
|
||||
<option value="yaml">YAML</option>
|
||||
<option value="xml">XML</option>
|
||||
<option value="css">CSS</option>
|
||||
<option value="scss">SCSS</option>
|
||||
<option value="html">HTML</option>
|
||||
<option value="markdown">Markdown</option>
|
||||
<option value="shell">Shell</option>
|
||||
<option value="dockerfile">Dockerfile</option>
|
||||
<option value="nginx">Nginx</option>
|
||||
<option value="diff">Diff</option>
|
||||
</select>
|
||||
<button type="button" data-action="horizontalRule" title="分隔线">—</button>
|
||||
<span class="toolbar-divider"></span>
|
||||
<button type="button" data-action="undo" title="撤销">↩</button>
|
||||
<button type="button" data-action="redo" title="重做">↪</button>
|
||||
</div>
|
||||
|
||||
{{/* 编辑器主体 */}}
|
||||
{{/* Vditor 编辑器容器(工具栏+编辑区由 Vditor 自动生成) */}}
|
||||
<div class="editor-main">
|
||||
<div class="editor-pane" id="editorPane">
|
||||
<div id="editor-container"
|
||||
data-content="{{if .Post}}{{.Post.BodyHTML}}{{end}}"></div>
|
||||
<div class="editor-pane">
|
||||
<div id="vditor"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -79,7 +31,6 @@
|
||||
<span class="status-item" id="wordCount">0 字</span>
|
||||
<span class="status-item status-draft" id="draftStatus" style="display:none">草稿已保存</span>
|
||||
<span class="status-spacer"></span>
|
||||
<span class="status-item" id="btnFullscreen" style="cursor:pointer" title="全屏编辑 (F11)">全屏</span>
|
||||
<button type="submit" class="btn btn-primary btn-submit">{{if .Post}}保存修改{{else}}发布帖子{{end}}</button>
|
||||
</div>
|
||||
</form>
|
||||
@ -87,18 +38,9 @@
|
||||
|
||||
{{template "layout/footer.html" .}}
|
||||
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"@tiptap/core": "https://esm.sh/@tiptap/core@3.13.0",
|
||||
"@tiptap/starter-kit": "https://esm.sh/@tiptap/starter-kit@3.13.0",
|
||||
"@tiptap/extension-placeholder": "https://esm.sh/@tiptap/extension-placeholder@3.13.0",
|
||||
"@tiptap/extension-image": "https://esm.sh/@tiptap/extension-image@3.13.0",
|
||||
"@tiptap/extension-code-block-lowlight": "https://esm.sh/@tiptap/extension-code-block-lowlight@3.13.0",
|
||||
"lowlight": "https://esm.sh/lowlight@3.3.0",
|
||||
"highlight.js/lib/languages/": "https://esm.sh/highlight.js@11.11.0/es/languages/"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<link rel="stylesheet" href="/static/vditor/dist/index.css">
|
||||
<script src="/static/vditor/dist/index.min.js"></script>
|
||||
<script src="/static/js/common.js"></script>
|
||||
<script type="module" src="/static/js/editor.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
{{template "layout/header.html" .}}
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.0/styles/github-dark.min.css">
|
||||
<body>
|
||||
|
||||
{{template "layout/nav.html" .}}
|
||||
@ -9,7 +8,7 @@
|
||||
<header class="post-detail-header">
|
||||
<h1 class="post-detail-title">{{.Post.Title}}</h1>
|
||||
<div class="post-detail-meta">
|
||||
<span class="post-author">{{if .Post.AuthorName}}{{.Post.AuthorName}}{{else}}UID{{.Post.UserID}}{{end}}</span>
|
||||
<span class="post-author">{{if .Post.AuthorName}}{{.Post.AuthorName}}({{.Post.UserID}}){{else}}UID{{.Post.UserID}}{{end}}</span>
|
||||
<span class="post-time">{{.Post.CreatedAt.Format "2006-01-02 15:04"}}</span>
|
||||
{{if ne .Post.Status "approved"}}
|
||||
<span class="post-status post-status-{{.Post.Status}}">{{index $.StatusNames .Post.Status}}</span>
|
||||
@ -17,13 +16,7 @@
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="post-detail-body">
|
||||
{{.PostBodyHTML}}
|
||||
{{/* 如果 BodyHTML 为空,直接显示原文 */}}
|
||||
</div>
|
||||
{{if not .PostBodyHTML}}
|
||||
<div class="post-detail-body-raw">{{.Post.Body}}</div>
|
||||
{{end}}
|
||||
<div class="post-detail-body" id="postContent"></div>
|
||||
|
||||
{{if .Post.RejectReason}}
|
||||
<div class="post-reject-reason">
|
||||
@ -46,38 +39,34 @@
|
||||
|
||||
{{template "layout/footer.html" .}}
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.0/highlight.min.js"></script>
|
||||
{{/* MD 内容安全传递给 JS */}}
|
||||
<textarea id="postMdContent" style="display:none">{{.Post.Body}}</textarea>
|
||||
|
||||
<link rel="stylesheet" href="/static/vditor/dist/index.css">
|
||||
<script src="/static/vditor/dist/method.min.js"></script>
|
||||
<script src="/static/js/common.js"></script>
|
||||
<script src="/static/js/shortcode.js"></script>
|
||||
<script>
|
||||
// ---- 代码块语言标签注入 + 语法高亮 ----
|
||||
(function() {
|
||||
document.querySelectorAll('.post-detail-body pre').forEach(function(pre) {
|
||||
// 从 pre 的 data-language 或 code 的 class 中获取语言
|
||||
var lang = pre.getAttribute('data-language');
|
||||
if (!lang) {
|
||||
var code = pre.querySelector('code');
|
||||
if (code) {
|
||||
var match = code.className.match(/language-(\w+)/);
|
||||
if (match) lang = match[1];
|
||||
}
|
||||
}
|
||||
lang = lang || 'text';
|
||||
var el = document.getElementById('postContent');
|
||||
var mdEl = document.getElementById('postMdContent');
|
||||
if (!el || !mdEl) return;
|
||||
|
||||
var label = document.createElement('div');
|
||||
label.className = 'code-lang-label';
|
||||
label.textContent = lang;
|
||||
pre.insertBefore(label, pre.firstChild);
|
||||
pre.style.paddingTop = '32px';
|
||||
|
||||
// 语法高亮
|
||||
var code = pre.querySelector('code');
|
||||
if (code && lang !== 'text') {
|
||||
code.classList.add('language-' + lang);
|
||||
hljs.highlightElement(code);
|
||||
}
|
||||
var md = mdEl.value;
|
||||
Vditor.preview(el, md, {
|
||||
cdn: '/static/vditor',
|
||||
theme: { current: 'light', path: '/static/vditor/dist/css/content-theme' },
|
||||
hljs: { style: 'github-dark', enable: true },
|
||||
});
|
||||
})();
|
||||
|
||||
// Vditor.preview() 渲染 MD,shortcode 占位 div 原样保留
|
||||
// 渲染完成后,将占位 div 替换为对应卡片 UI
|
||||
renderShortcodes(el);
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script>
|
||||
// 提交审核按钮
|
||||
(function() {
|
||||
var btn = document.getElementById('submitAuditBtn');
|
||||
if (!btn) return;
|
||||
|
||||
Reference in New Issue
Block a user