fix: 修复帖子系统5项缺陷 + 代码块插入位置/language-class/分割线占位
- 作者显示 用户名(UID): 修正Post.AuthorName GORM标签从 -:all 改为 -:migration;<-:false;column:author_name
- 审核流程: 帖子详情页新增"提交审核"按钮(draft/rejected时作者可见)
- 图片上传: 新增 POST /api/posts/upload-image 端点,限制5MB/jpg/png/gif/webp
- 代码块语言标注: 工具栏插入代码块时弹出语言输入框,生成 language-xxx class
- 状态中文显示: 管理后台/帖子详情页状态改为 PostStatusDisplayNames 映射
- 修复代码块插入位置错误: init时调用switchMode同步DOM; 无选区时appendChild; 防<pre>嵌套
- 修复 bluemonday.UGCPolicy() 剥离 code/pre 的 class 属性,显式 AllowAttrs("class")
- 修复分割线工具栏插入多余text占位符: hr/link/image以外不强制填占位文本
This commit is contained in:
@ -49,16 +49,17 @@ func (ctrl *AdminPostController) PostsPage(c *gin.Context) {
|
||||
if nextPage > totalPages { nextPage = totalPages }
|
||||
|
||||
c.HTML(http.StatusOK, "admin/posts/index.html", common.BuildAdminPageData(c, gin.H{
|
||||
"Title": "内容管理",
|
||||
"Posts": posts,
|
||||
"Total": total,
|
||||
"Page": p.Page,
|
||||
"TotalPages": totalPages,
|
||||
"PrevPage": prevPage,
|
||||
"NextPage": nextPage,
|
||||
"Keyword": keyword,
|
||||
"Status": status,
|
||||
"ExtraCSS": "/admin/static/css/posts.css",
|
||||
"Title": "内容管理",
|
||||
"Posts": posts,
|
||||
"Total": total,
|
||||
"Page": p.Page,
|
||||
"TotalPages": totalPages,
|
||||
"PrevPage": prevPage,
|
||||
"NextPage": nextPage,
|
||||
"Keyword": keyword,
|
||||
"Status": status,
|
||||
"StatusNames": model.PostStatusDisplayNames,
|
||||
"ExtraCSS": "/admin/static/css/posts.css",
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@ -1,9 +1,15 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"metazone.cc/metalab/internal/common"
|
||||
"metazone.cc/metalab/internal/model"
|
||||
@ -107,6 +113,7 @@ func (ctrl *PostController) ShowPage(c *gin.Context) {
|
||||
"Post": post,
|
||||
"PostBodyHTML": template.HTML(post.BodyHTML),
|
||||
"UID": uid,
|
||||
"StatusNames": model.PostStatusDisplayNames,
|
||||
"ExtraCSS": "/static/css/posts.css",
|
||||
}))
|
||||
}
|
||||
@ -319,3 +326,83 @@ func (ctrl *PostController) PreviewAPI(c *gin.Context) {
|
||||
|
||||
common.Ok(c, gin.H{"html": html})
|
||||
}
|
||||
|
||||
// allowedImageExt 允许的图片扩展名
|
||||
var allowedImageExt = map[string]bool{
|
||||
".jpg": true, ".jpeg": true, ".png": true, ".gif": true, ".webp": true,
|
||||
}
|
||||
|
||||
// UploadImage 上传帖子图片(需登录,multipart/form-data)
|
||||
func (ctrl *PostController) UploadImage(c *gin.Context) {
|
||||
uidObj, _ := c.Get("uid")
|
||||
uid, ok := uidObj.(uint)
|
||||
if !ok {
|
||||
common.Error(c, http.StatusUnauthorized, "请先登录")
|
||||
return
|
||||
}
|
||||
|
||||
file, header, err := c.Request.FormFile("file")
|
||||
if err != nil {
|
||||
common.Error(c, http.StatusBadRequest, "请选择文件")
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// 检查扩展名
|
||||
ext := strings.ToLower(filepath.Ext(header.Filename))
|
||||
if !allowedImageExt[ext] {
|
||||
common.Error(c, http.StatusBadRequest, "仅支持 jpg / jpeg / png / gif / webp 格式")
|
||||
return
|
||||
}
|
||||
|
||||
// 限制 5MB
|
||||
maxSize := int64(5 << 20)
|
||||
if header.Size > maxSize {
|
||||
common.Error(c, http.StatusBadRequest, "图片大小不能超过 5MB")
|
||||
return
|
||||
}
|
||||
|
||||
// 存储路径
|
||||
storageDir := "storage/uploads/posts"
|
||||
if err := os.MkdirAll(storageDir, 0755); err != nil {
|
||||
common.Error(c, http.StatusInternalServerError, "存储初始化失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 唯一文件名:uid_timestamp.ext
|
||||
ts := time.Now().UnixMilli()
|
||||
filename := fmt.Sprintf("%d_%d%s", uid, ts, ext)
|
||||
savePath := filepath.Join(storageDir, filename)
|
||||
|
||||
if err := saveUploadedFile(file, savePath); err != nil {
|
||||
common.Error(c, http.StatusInternalServerError, "保存图片失败")
|
||||
return
|
||||
}
|
||||
|
||||
url := "/uploads/posts/" + filename
|
||||
common.Ok(c, gin.H{"url": url})
|
||||
}
|
||||
|
||||
// saveUploadedFile 将 multipart.File 写入目标路径
|
||||
func saveUploadedFile(file multipart.File, dst string) error {
|
||||
out, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
// 限制读取 5MB 防止内存放大
|
||||
buf := make([]byte, 32*1024)
|
||||
for {
|
||||
n, err := file.Read(buf)
|
||||
if n > 0 {
|
||||
if _, writeErr := out.Write(buf[:n]); writeErr != nil {
|
||||
return writeErr
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user