refactor: 彻底移除 /posts/new 旧编辑器,统一迁移至 /studio/write
- 删除 /posts/new 路由,不再保留任何兼容层 - /posts/:id/edit 改为 301 跳转 /studio/write?id=:id - 删除 posts/new.html 模板和 editor.js - 移除 PostController.EditPage/Create/Update/Delete 孤儿方法 - 移除 /api/posts POST/PUT/DELETE 孤儿 API(保留 submit 和 upload-image) - posts/show.html 编辑链接指向 /studio/write
This commit is contained in:
@ -135,147 +135,6 @@ func (ctrl *PostController) ShowPage(c *gin.Context) {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
// EditPage 编辑页面
|
|
||||||
func (ctrl *PostController) EditPage(c *gin.Context) {
|
|
||||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
c.HTML(http.StatusNotFound, "posts/404.html", common.BuildPageData(c, gin.H{
|
|
||||||
"Title": "未找到",
|
|
||||||
"ExtraCSS": "/static/css/posts.css",
|
|
||||||
}))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
post, err := ctrl.postService.GetByID(uint(id))
|
|
||||||
if err != nil || post.Status == model.PostStatusPending || post.IsLocked {
|
|
||||||
c.HTML(http.StatusNotFound, "posts/404.html", common.BuildPageData(c, gin.H{
|
|
||||||
"Title": "未找到或无法编辑",
|
|
||||||
"ExtraCSS": "/static/css/posts.css",
|
|
||||||
}))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
uid, role, _ := common.GetGinUser(c)
|
|
||||||
if !ctrl.postService.IsPostAccessible(uid, role, post.UserID) {
|
|
||||||
c.HTML(http.StatusNotFound, "posts/404.html", common.BuildPageData(c, gin.H{
|
|
||||||
"Title": "未找到",
|
|
||||||
"ExtraCSS": "/static/css/posts.css",
|
|
||||||
}))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 已通过帖子有待审修订时,编辑页展示待审内容
|
|
||||||
if post.PendingBody != "" {
|
|
||||||
post.Title = post.PendingTitle
|
|
||||||
post.Body = post.PendingBody
|
|
||||||
}
|
|
||||||
|
|
||||||
c.HTML(http.StatusOK, "posts/new.html", common.BuildPageData(c, gin.H{
|
|
||||||
"Title": "编辑帖子",
|
|
||||||
"Post": post,
|
|
||||||
"ExtraCSS": "/static/css/posts.css",
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create API 发帖
|
|
||||||
func (ctrl *PostController) Create(c *gin.Context) {
|
|
||||||
uid, _, ok := common.GetGinUser(c)
|
|
||||||
if !ok {
|
|
||||||
common.Error(c, http.StatusUnauthorized, "请先登录")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var req model.PostCreateRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
common.Error(c, http.StatusBadRequest, "标题和正文不能为空")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
post, err := ctrl.postService.Create(uid, req.Title, req.Body)
|
|
||||||
if err != nil {
|
|
||||||
common.Error(c, http.StatusInternalServerError, "发帖失败")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
common.OkWithMessage(c, post, "发布成功")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update API 编辑帖子
|
|
||||||
func (ctrl *PostController) Update(c *gin.Context) {
|
|
||||||
uid, role, ok := common.GetGinUser(c)
|
|
||||||
if !ok {
|
|
||||||
common.Error(c, http.StatusUnauthorized, "请先登录")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
common.Error(c, http.StatusBadRequest, "无效的帖子 ID")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var req model.PostUpdateRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
common.Error(c, http.StatusBadRequest, "标题和正文不能为空")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := ctrl.getPostAndCheckAccess(uint(id), c, uid, role); !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := ctrl.postService.Update(uint(id), req.Title, req.Body); err != nil {
|
|
||||||
if errors.Is(err, common.ErrPostCannotEdit) {
|
|
||||||
common.Error(c, http.StatusBadRequest, err.Error())
|
|
||||||
} else {
|
|
||||||
common.Error(c, http.StatusInternalServerError, "编辑失败")
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
common.OkMessage(c, "编辑成功")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete API 删除帖子
|
|
||||||
func (ctrl *PostController) Delete(c *gin.Context) {
|
|
||||||
uid, role, ok := common.GetGinUser(c)
|
|
||||||
if !ok {
|
|
||||||
common.Error(c, http.StatusUnauthorized, "请先登录")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
common.Error(c, http.StatusBadRequest, "无效的帖子 ID")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := ctrl.getPostAndCheckAccess(uint(id), c, uid, role); !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := ctrl.postService.Delete(uint(id)); err != nil {
|
|
||||||
common.Error(c, http.StatusInternalServerError, "删除失败")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
common.OkMessage(c, "删除成功")
|
|
||||||
}
|
|
||||||
|
|
||||||
// getPostAndCheckAccess 获取帖子并检查当前用户权限
|
|
||||||
// 返回 (*model.Post, true) 表示通过;返回 (nil, false) 表示已写入错误响应
|
|
||||||
func (ctrl *PostController) getPostAndCheckAccess(id uint, c *gin.Context, uid uint, role string) (*model.Post, bool) {
|
|
||||||
post, err := ctrl.postService.GetByID(id)
|
|
||||||
if err != nil {
|
|
||||||
common.Error(c, http.StatusNotFound, "帖子不存在")
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
if !ctrl.postService.IsPostAccessible(uid, role, post.UserID) {
|
|
||||||
common.Error(c, http.StatusForbidden, "无权操作此帖子")
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
return post, true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Submit API 提交审核
|
// Submit API 提交审核
|
||||||
func (ctrl *PostController) Submit(c *gin.Context) {
|
func (ctrl *PostController) Submit(c *gin.Context) {
|
||||||
|
|||||||
@ -57,9 +57,6 @@ func setupAPIRoutes(r *gin.Engine, cfg *config.Config, d *dependencies) {
|
|||||||
postsAPI.Use(d.authMdw.Required())
|
postsAPI.Use(d.authMdw.Required())
|
||||||
postsAPI.Use(middleware.CSRF(cfg))
|
postsAPI.Use(middleware.CSRF(cfg))
|
||||||
{
|
{
|
||||||
postsAPI.POST("", d.postCtrl.Create)
|
|
||||||
postsAPI.PUT("/:id", d.postCtrl.Update)
|
|
||||||
postsAPI.DELETE("/:id", d.postCtrl.Delete)
|
|
||||||
postsAPI.POST("/:id/submit", d.postCtrl.Submit)
|
postsAPI.POST("/:id/submit", d.postCtrl.Submit)
|
||||||
postsAPI.POST("/upload-image", d.postCtrl.UploadImage)
|
postsAPI.POST("/upload-image", d.postCtrl.UploadImage)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -40,11 +40,10 @@ func setupFrontendRoutes(r *gin.Engine, cfg *config.Config, d *dependencies) {
|
|||||||
|
|
||||||
// 帖子页面
|
// 帖子页面
|
||||||
pages.GET("/posts", d.postCtrl.ListPage)
|
pages.GET("/posts", d.postCtrl.ListPage)
|
||||||
pages.GET("/posts/new", d.authMdw.Required(), func(c *gin.Context) {
|
|
||||||
c.Redirect(http.StatusMovedPermanently, "/studio/write")
|
|
||||||
})
|
|
||||||
pages.GET("/posts/:id", d.postCtrl.ShowPage)
|
pages.GET("/posts/:id", d.postCtrl.ShowPage)
|
||||||
pages.GET("/posts/:id/edit", d.authMdw.Required(), d.postCtrl.EditPage)
|
pages.GET("/posts/:id/edit", d.authMdw.Required(), func(c *gin.Context) {
|
||||||
|
c.Redirect(http.StatusMovedPermanently, "/studio/write?id="+c.Param("id"))
|
||||||
|
})
|
||||||
|
|
||||||
// 创作中心(需登录)
|
// 创作中心(需登录)
|
||||||
studioPages := pages.Group("/studio")
|
studioPages := pages.Group("/studio")
|
||||||
|
|||||||
@ -1,70 +0,0 @@
|
|||||||
{{template "layout/header.html" .}}
|
|
||||||
<body>
|
|
||||||
|
|
||||||
{{template "layout/nav.html" .}}
|
|
||||||
|
|
||||||
<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">
|
|
||||||
<input type="text" id="postTitle" class="editor-title-input"
|
|
||||||
value="{{if .Post}}{{.Post.Title}}{{end}}"
|
|
||||||
placeholder="输入文章标题..."
|
|
||||||
maxlength="200" required autofocus>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{/* Vditor 编辑器容器(工具栏+编辑区由 Vditor 自动生成) */}}
|
|
||||||
<div class="editor-main">
|
|
||||||
<div class="editor-pane">
|
|
||||||
<div id="vditor"></div>
|
|
||||||
</div>
|
|
||||||
<aside class="editor-sidebar">
|
|
||||||
<div class="editor-sidebar-section">
|
|
||||||
<h4>快捷语法</h4>
|
|
||||||
<ul class="tips-list">
|
|
||||||
<li><code>#</code> <code>##</code> <code>###</code> 标题</li>
|
|
||||||
<li><code>**粗体**</code> <code>*斜体*</code></li>
|
|
||||||
<li><code>`行内代码`</code></li>
|
|
||||||
<li><code>```</code> 代码块</li>
|
|
||||||
<li><code>></code> 引用</li>
|
|
||||||
<li><code>-</code> 无序列表</li>
|
|
||||||
<li><code>[文字](链接)</code></li>
|
|
||||||
<li><code></code></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="editor-sidebar-section">
|
|
||||||
<h4>发布提示</h4>
|
|
||||||
<ul class="tips-list">
|
|
||||||
<li>标题控制在 50 字以内</li>
|
|
||||||
<li>正文清晰分段,便于阅读</li>
|
|
||||||
<li>代码使用代码块包裹</li>
|
|
||||||
<li>Ctrl+S 快速保存草稿</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{/* 状态栏 */}}
|
|
||||||
<div class="editor-statusbar">
|
|
||||||
<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>
|
|
||||||
<button type="submit" class="btn btn-primary btn-submit">{{if .Post}}保存修改{{else}}发布帖子{{end}}</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{template "layout/footer.html" .}}
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="/static/vditor/dist/index.css?v={{assetV "/static/vditor/dist/index.css"}}">
|
|
||||||
<script src="/static/vditor/dist/index.min.js?v={{assetV "/static/vditor/dist/index.min.js"}}"></script>
|
|
||||||
<script src="/static/js/common.js?v={{assetV "/static/js/common.js"}}"></script>
|
|
||||||
<script type="module" src="/static/js/editor.js?v={{assetV "/static/js/editor.js"}}"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -34,7 +34,7 @@
|
|||||||
{{if .IsLoggedIn}}
|
{{if .IsLoggedIn}}
|
||||||
<div class="post-actions">
|
<div class="post-actions">
|
||||||
{{if and $.Post (or (eq $.Post.UserID $.UID) $.CanAccessAdmin) (ne $.Post.Status "pending") (not $.Post.IsLocked)}}
|
{{if and $.Post (or (eq $.Post.UserID $.UID) $.CanAccessAdmin) (ne $.Post.Status "pending") (not $.Post.IsLocked)}}
|
||||||
<a href="/posts/{{$.Post.ID}}/edit" class="btn btn-secondary">编辑</a>
|
<a href="/studio/write?id={{$.Post.ID}}" class="btn btn-secondary">编辑</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if and $.Post (eq $.Post.UserID $.UID) (or (eq $.Post.Status "draft") (eq $.Post.Status "rejected"))}}
|
{{if and $.Post (eq $.Post.UserID $.UID) (or (eq $.Post.Status "draft") (eq $.Post.Status "rejected"))}}
|
||||||
<button class="btn btn-primary" id="submitAuditBtn" data-id="{{$.Post.ID}}">提交审核</button>
|
<button class="btn btn-primary" id="submitAuditBtn" data-id="{{$.Post.ID}}">提交审核</button>
|
||||||
|
|||||||
@ -1,295 +0,0 @@
|
|||||||
/**
|
|
||||||
* MetaLab Editor — Vditor Markdown 所见即所得编辑器
|
|
||||||
*
|
|
||||||
* Vditor 原生支持 MD + WYSIWYG,后端存储 Markdown。
|
|
||||||
* 图片上传复用现有 /api/posts/upload-image,后端已返回 Vditor 原生响应格式。
|
|
||||||
* 安全模型:前端输出 MD(纯文本)→ 后端直接存储 → 详情页客户端 Vditor 渲染为 HTML。
|
|
||||||
*
|
|
||||||
* Shortcode 扩展语法([zone:type:params]):
|
|
||||||
* 输入 [zone: 触发自动补全,选择类型后自动生成对应语法。
|
|
||||||
* 支持的 shortcode 详情见 /docs/vditor-migration-plan.md
|
|
||||||
*
|
|
||||||
* Vditor 全局对象由 /static/vditor/dist/index.min.js 提供。
|
|
||||||
*/
|
|
||||||
|
|
||||||
let vditor = null
|
|
||||||
let draftTimer = null
|
|
||||||
let submitting = false
|
|
||||||
|
|
||||||
// ---- CSRF Token ----
|
|
||||||
// 直接从 cookie 读取 mlb_csrf(httpOnly=false),确保与服务端验证时读取的值一致
|
|
||||||
function getCsrfToken() {
|
|
||||||
var match = document.cookie.match(/(?:^|;\s*)mlb_csrf=([^;]*)/)
|
|
||||||
return match ? match[1] : ''
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- 初始化 ----
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
const container = document.getElementById('vditor')
|
|
||||||
if (!container) return
|
|
||||||
|
|
||||||
const postId = document.getElementById('postId')?.value
|
|
||||||
const isEdit = !!postId
|
|
||||||
|
|
||||||
// 获取初始内容:编辑模式下从后端传入,新帖模式尝试恢复草稿
|
|
||||||
let initialMD = ''
|
|
||||||
if (isEdit) {
|
|
||||||
// 编辑模式:从后端 JSON 嵌入的 Post.Body(MD)中获取
|
|
||||||
// 由 Go 模板 {{.Post.Body}} 嵌入到 hidden 字段或 script 标签
|
|
||||||
initialMD = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从隐藏字段获取编辑内容(Go 模板传入)
|
|
||||||
const editBodyEl = document.getElementById('editBody')
|
|
||||||
if (editBodyEl) {
|
|
||||||
initialMD = editBodyEl.value
|
|
||||||
}
|
|
||||||
|
|
||||||
vditor = new Vditor('vditor', {
|
|
||||||
// 核心配置
|
|
||||||
mode: 'wysiwyg',
|
|
||||||
cdn: '/static/vditor',
|
|
||||||
height: '100%',
|
|
||||||
lang: 'zh_CN',
|
|
||||||
placeholder: '在此输入正文...',
|
|
||||||
|
|
||||||
// 工具栏(精简,适合技术社区)
|
|
||||||
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',
|
|
||||||
],
|
|
||||||
|
|
||||||
// Vditor 3.11.2 中 highlightToolbarWYSIWYG 直接调用此回调
|
|
||||||
// 不提供会导致 TypeError: not a function,所有弹窗(代码块语言等)都不可用
|
|
||||||
customWysiwygToolbar(type, popover) {},
|
|
||||||
|
|
||||||
// 计数器
|
|
||||||
counter: {
|
|
||||||
enable: true,
|
|
||||||
type: 'text',
|
|
||||||
},
|
|
||||||
|
|
||||||
// Shortcode 自动补全提示
|
|
||||||
// 输入 [zone: 后触发,列出所有已注册的 shortcode 类型
|
|
||||||
hint: {
|
|
||||||
extend: [
|
|
||||||
{
|
|
||||||
key: '[zone:event:',
|
|
||||||
value: '[zone:event:活动ID]',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: '[zone:game:',
|
|
||||||
value: '[zone:game:游戏Slug]',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: '[zone:poll:',
|
|
||||||
value: '[zone:poll:投票ID]',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: '[zone:resource:',
|
|
||||||
value: '[zone:resource:资源ID]',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
|
|
||||||
// 大纲
|
|
||||||
outline: {
|
|
||||||
enable: false,
|
|
||||||
position: 'right',
|
|
||||||
},
|
|
||||||
|
|
||||||
// 预览配置
|
|
||||||
// 注意:WYSIWYG 编辑模式下代码块不做语法高亮(Vditor 3.11.2 源码中
|
|
||||||
// highlightRender 会跳过 .vditor-wysiwyg__pre,仅在预览模式/详情页渲染)
|
|
||||||
// langs 确保语言选择下拉始终可用,不受 hljs 异步加载时机影响
|
|
||||||
preview: {
|
|
||||||
theme: { current: 'light', path: '/static/vditor/dist/css/content-theme' },
|
|
||||||
hljs: {
|
|
||||||
style: 'github-dark',
|
|
||||||
enable: true,
|
|
||||||
langs: [
|
|
||||||
'javascript', 'typescript', 'python', 'java', 'go', 'rust',
|
|
||||||
'c', 'cpp', 'csharp', 'php', 'ruby', 'swift', 'kotlin',
|
|
||||||
'html', 'css', 'scss', 'xml', 'json', 'yaml', 'markdown',
|
|
||||||
'sql', 'bash', 'shell', 'powershell', 'dockerfile', 'nginx',
|
|
||||||
'diff', 'http', 'graphql', 'makefile',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
markdown: { codeBlockPreview: true },
|
|
||||||
},
|
|
||||||
|
|
||||||
// 图片上传
|
|
||||||
// CSRF token 由 common.js 的全局 XHR monkey-patch 自动注入,此处无需重复设置
|
|
||||||
// 重复设置会导致浏览器将 header 合并为 "TOKEN, TOKEN" 从而验证失败
|
|
||||||
upload: {
|
|
||||||
url: '/api/posts/upload-image',
|
|
||||||
fieldName: 'file',
|
|
||||||
max: 5 * 1024 * 1024,
|
|
||||||
accept: 'image/jpg,image/jpeg,image/png,image/gif,image/webp',
|
|
||||||
},
|
|
||||||
|
|
||||||
// 初始内容
|
|
||||||
value: initialMD,
|
|
||||||
|
|
||||||
// 初始化完成
|
|
||||||
after() {
|
|
||||||
// 新帖模式下恢复草稿
|
|
||||||
if (!isEdit) {
|
|
||||||
setTimeout(() => restoreDraft(), 100)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新字数
|
|
||||||
updateWordCount()
|
|
||||||
},
|
|
||||||
|
|
||||||
// 内容变化
|
|
||||||
input(value) {
|
|
||||||
scheduleDraft(value)
|
|
||||||
updateWordCount()
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// 表单提交
|
|
||||||
document.getElementById('postForm')?.addEventListener('submit', handleSubmit)
|
|
||||||
|
|
||||||
// 快捷键 Ctrl+S
|
|
||||||
document.addEventListener('keydown', (e) => {
|
|
||||||
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
|
|
||||||
e.preventDefault()
|
|
||||||
document.getElementById('postForm')?.dispatchEvent(new Event('submit'))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 全屏快捷键 F11
|
|
||||||
document.addEventListener('keydown', (e) => {
|
|
||||||
if (e.key === 'F11') {
|
|
||||||
e.preventDefault()
|
|
||||||
const layout = document.querySelector('.editor-layout')
|
|
||||||
if (layout) layout.classList.toggle('fullscreen')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// ---- 字数统计 ----
|
|
||||||
function updateWordCount() {
|
|
||||||
const el = document.getElementById('wordCount')
|
|
||||||
if (!el || !vditor) return
|
|
||||||
// Vditor 自带计数器已渲染到 .vditor-counter,我们同步到自定义的状态栏
|
|
||||||
const counterEl = document.querySelector('.vditor-counter')
|
|
||||||
if (counterEl) {
|
|
||||||
el.textContent = counterEl.textContent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- 草稿保存/恢复 ----
|
|
||||||
function scheduleDraft(md) {
|
|
||||||
clearTimeout(draftTimer)
|
|
||||||
draftTimer = setTimeout(() => saveDraft(md), 2000)
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveDraft(md) {
|
|
||||||
const title = document.getElementById('postTitle')?.value || ''
|
|
||||||
if (!md.trim() && !title.trim()) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
localStorage.setItem('draft_post_title', title)
|
|
||||||
localStorage.setItem('draft_post_body_md', md)
|
|
||||||
const statusEl = document.getElementById('draftStatus')
|
|
||||||
if (statusEl) {
|
|
||||||
statusEl.style.display = ''
|
|
||||||
setTimeout(() => { statusEl.style.display = 'none' }, 2000)
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// localStorage 不可用
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function restoreDraft() {
|
|
||||||
try {
|
|
||||||
const title = localStorage.getItem('draft_post_title')
|
|
||||||
const md = localStorage.getItem('draft_post_body_md')
|
|
||||||
|
|
||||||
if (title) {
|
|
||||||
document.getElementById('postTitle').value = title
|
|
||||||
}
|
|
||||||
if (md && vditor) {
|
|
||||||
vditor.setValue(md)
|
|
||||||
updateWordCount()
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// 静默忽略
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearDraft() {
|
|
||||||
try {
|
|
||||||
localStorage.removeItem('draft_post_title')
|
|
||||||
localStorage.removeItem('draft_post_body_md')
|
|
||||||
} catch (e) { /* ignore */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- 表单提交 ----
|
|
||||||
async function handleSubmit(e) {
|
|
||||||
e.preventDefault()
|
|
||||||
|
|
||||||
if (submitting) return
|
|
||||||
submitting = true
|
|
||||||
|
|
||||||
const submitBtn = document.querySelector('.btn-submit')
|
|
||||||
if (submitBtn) {
|
|
||||||
submitBtn.disabled = true
|
|
||||||
submitBtn.textContent = '提交中...'
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const postId = document.getElementById('postId')?.value
|
|
||||||
const title = document.getElementById('postTitle')?.value.trim()
|
|
||||||
|
|
||||||
if (!title) { alert('请输入标题'); return }
|
|
||||||
|
|
||||||
// 获取 Vditor Markdown 内容
|
|
||||||
const body = vditor ? vditor.getValue() : ''
|
|
||||||
if (!body.trim()) { alert('请输入正文'); return }
|
|
||||||
|
|
||||||
const isEdit = !!postId
|
|
||||||
const url = isEdit ? '/api/posts/' + postId : '/api/posts'
|
|
||||||
const method = isEdit ? 'PUT' : 'POST'
|
|
||||||
|
|
||||||
const resp = await fetch(url, {
|
|
||||||
method,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-CSRF-Token': getCsrfToken(),
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ title, body }),
|
|
||||||
})
|
|
||||||
const data = await resp.json()
|
|
||||||
|
|
||||||
if (data.success || data.code === 200) {
|
|
||||||
clearDraft()
|
|
||||||
const newPostId = data.data?.id || data.data?.ID || postId
|
|
||||||
window.location.href = newPostId ? '/posts/' + newPostId : '/posts'
|
|
||||||
} else {
|
|
||||||
alert(data.message || '操作失败')
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
if (err && err.message === 'refresh_failed') {
|
|
||||||
alert('登录已过期,请重新登录')
|
|
||||||
window.location.href = '/auth/login?redirect=' + encodeURIComponent(window.location.pathname)
|
|
||||||
} else {
|
|
||||||
alert('请求失败,请重试')
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
submitting = false
|
|
||||||
if (submitBtn) {
|
|
||||||
submitBtn.disabled = false
|
|
||||||
const isEdit = !!document.getElementById('postId')?.value
|
|
||||||
submitBtn.textContent = isEdit ? '保存修改' : '发布帖子'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user