feat: Markdown 工具栏编辑器与服务端预览

共用 md_editor 于发帖/改帖/评论,扩展 ComposeBodyToHTML,并提供 /compose/preview。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-30 04:50:07 +08:00
parent 7f048bd9b5
commit 1f777eebb1
17 changed files with 1056 additions and 232 deletions

View File

@@ -63,20 +63,21 @@
<label>标签
<input name="tags" maxlength="{{.TagsMax}}" value="{{.Tags}}" placeholder="逗号分隔,可选"/>
</label>
<label>正文
<textarea name="content" id="compose-content" rows="16" required maxlength="{{.ContentMax}}" placeholder="支持纯文本;图片上传后插入 Markdown 图片语法">{{.Content}}</textarea>
</label>
<div class="j13-compose__tools">
<label class="j13-btn-secondary j13-filebtn">
插入图片
<input type="file" id="compose-image" accept="image/*" hidden/>
</label>
<button type="button" class="j13-btn-secondary" id="compose-gate-login" data-gate="login">登录可见</button>
<button type="button" class="j13-btn-secondary" id="compose-gate-reply" data-gate="reply">回复可见</button>
<button type="button" class="j13-btn-secondary" id="compose-gate-points" data-gate="points">积分可见</button>
<span class="j13-muted" id="compose-upload-status"></span>
</div>
<p class="j13-muted">门控选中正文后点按钮包裹积分可见会询问价格19999。含门控时正文以 HTML 提交。</p>
<label>正文</label>
{{template "shared/md_editor" (dict
"Name" "content"
"ID" "compose-content"
"Content" .Content
"Rows" 16
"MaxLength" .ContentMax
"Placeholder" "支持 Markdown标题/粗斜体/列表/代码块/链接;可用工具栏插入图片与门控块"
"CSRF" .CSRF
"ShowGates" 1
"ShowUpload" 1
"Unsaved" 1
"Required" 1
)}}
<p class="j13-muted">门控选中正文后点按钮包裹积分可见会询问价格19999。含门控时正文以 HTML 提交。Tab 缩进,可切换预览。</p>
<button type="submit">{{if .IsEdit}}保存{{else}}发布{{end}}</button>
</form>
</main>

View File

@@ -218,7 +218,19 @@
<input type="hidden" name="reply_to" id="comment-reply-to" value=""/>
<p class="j13-muted j13-comment-reply-hint" id="comment-reply-hint" hidden></p>
<label for="comment-content">发表评论</label>
<textarea id="comment-content" name="content" rows="4" required maxlength="8000"></textarea>
{{template "shared/md_editor" (dict
"Name" "content"
"ID" "comment-content"
"Content" ""
"Rows" 5
"MaxLength" 8000
"Placeholder" "支持 Markdown可回复楼上"
"CSRF" .CSRF
"ShowGates" 0
"ShowUpload" 1
"Unsaved" 0
"Required" 1
)}}
<label class="j13-check">
<input type="checkbox" name="is_private" value="1"/>
私密评论(仅相关可见)

View File

@@ -8,7 +8,19 @@
<form method="post" action="/post/{{.PostID}}/comments/{{.CommentID}}/edit" class="j13-form j13-comment-form">
<input type="hidden" name="_csrf" value="{{.CSRF}}"/>
<label for="comment-edit-content">内容</label>
<textarea id="comment-edit-content" name="content" rows="6" required maxlength="8000">{{.Content}}</textarea>
{{template "shared/md_editor" (dict
"Name" "content"
"ID" "comment-edit-content"
"Content" .Content
"Rows" 8
"MaxLength" 8000
"Placeholder" "支持 Markdown"
"CSRF" .CSRF
"ShowGates" 0
"ShowUpload" 1
"Unsaved" 1
"Required" 1
)}}
<div class="j13-form__row">
<button type="submit">保存</button>
<a class="j13-btn-secondary" href="/post/{{.PostID}}#floor-{{.Floor}}">取消</a>

View File

@@ -0,0 +1,52 @@
{{define "shared/md_editor"}}
{{/* 期望:.Name .ID .Content .Rows .MaxLength .Placeholder .CSRF .ShowGates .ShowUpload .Unsaved .Required */}}
<div class="j13-md-editor"
data-j13-md-editor
data-csrf="{{.CSRF}}"
data-upload="/compose/upload"
data-preview="/compose/preview"
data-gates="{{if .ShowGates}}1{{else}}0{{end}}"
data-upload-enabled="{{if .ShowUpload}}1{{else}}0{{end}}"
data-unsaved="{{if .Unsaved}}1{{else}}0{{end}}">
<div class="j13-md-editor__toolbar" role="toolbar" aria-label="Markdown 工具栏">
<button type="button" class="j13-md-editor__btn" data-md-cmd="bold" title="粗体">B</button>
<button type="button" class="j13-md-editor__btn" data-md-cmd="italic" title="斜体"><em>I</em></button>
<button type="button" class="j13-md-editor__btn" data-md-cmd="strike" title="删除线"><s>S</s></button>
<button type="button" class="j13-md-editor__btn" data-md-cmd="code" title="行内代码">&lt;/&gt;</button>
<button type="button" class="j13-md-editor__btn" data-md-cmd="link" title="链接">链接</button>
<span class="j13-md-editor__sep" aria-hidden="true"></span>
<button type="button" class="j13-md-editor__btn" data-md-cmd="h2" title="标题 2">H2</button>
<button type="button" class="j13-md-editor__btn" data-md-cmd="h3" title="标题 3">H3</button>
<button type="button" class="j13-md-editor__btn" data-md-cmd="ul" title="无序列表">• 列表</button>
<button type="button" class="j13-md-editor__btn" data-md-cmd="ol" title="有序列表">1. 列表</button>
<button type="button" class="j13-md-editor__btn" data-md-cmd="quote" title="引用">引用</button>
<button type="button" class="j13-md-editor__btn" data-md-cmd="fence" title="代码块">代码块</button>
{{if .ShowUpload}}
<span class="j13-md-editor__sep" aria-hidden="true"></span>
<label class="j13-md-editor__btn j13-md-editor__filebtn">
图片
<input type="file" accept="image/*" data-md-upload hidden/>
</label>
{{end}}
{{if .ShowGates}}
<span class="j13-md-editor__sep" aria-hidden="true"></span>
<button type="button" class="j13-md-editor__btn" data-md-cmd="gate-login">登录可见</button>
<button type="button" class="j13-md-editor__btn" data-md-cmd="gate-reply">回复可见</button>
<button type="button" class="j13-md-editor__btn" data-md-cmd="gate-points">积分可见</button>
{{end}}
<span class="j13-md-editor__sep" aria-hidden="true"></span>
<button type="button" class="j13-md-editor__btn" data-md-cmd="preview" data-md-preview-toggle>预览</button>
</div>
<textarea
name="{{.Name}}"
id="{{.ID}}"
class="j13-md-editor__input"
rows="{{.Rows}}"
{{if .Required}}required{{end}}
{{if .MaxLength}}maxlength="{{.MaxLength}}"{{end}}
placeholder="{{.Placeholder}}"
data-md-input>{{.Content}}</textarea>
<div class="j13-md-editor__preview j13-post__body" data-md-preview hidden></div>
<p class="j13-muted j13-md-editor__status" data-md-status></p>
</div>
{{end}}