feat: SSR 帖运营标记与回收站
EOF Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
285
routers/web/admin_post_ops.go
Normal file
285
routers/web/admin_post_ops.go
Normal file
@@ -0,0 +1,285 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.iioio.com/freefire/jiang13-forum/modules/webctx"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func (d Deps) adminPostRedirect(postID uint) string {
|
||||
return fmt.Sprintf("/post/%d", postID)
|
||||
}
|
||||
|
||||
func (d Deps) parseAdminPostID(c *gin.Context) (uint, string, bool) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
return 0, "/", false
|
||||
}
|
||||
return uint(id), d.adminPostRedirect(uint(id)), true
|
||||
}
|
||||
|
||||
func (d Deps) requireAdminPostCSRF(ctx *webctx.Context, redir string) bool {
|
||||
if !ctx.CheckCSRF() {
|
||||
ctx.SetFlash("无效请求,请重试")
|
||||
ctx.Redirect(redir)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func formBoolEnable(c *gin.Context) bool {
|
||||
v := strings.TrimSpace(c.PostForm("enable"))
|
||||
return v == "1" || strings.EqualFold(v, "true") || v == "on"
|
||||
}
|
||||
|
||||
// AdminPostPinPost 全局置顶切换
|
||||
func (d Deps) AdminPostPinPost(c *gin.Context) {
|
||||
ctx := d.ctx(c)
|
||||
id, redir, ok := d.parseAdminPostID(c)
|
||||
if !ok {
|
||||
ctx.Redirect("/")
|
||||
return
|
||||
}
|
||||
if !d.requireAdminPostCSRF(ctx, redir) {
|
||||
return
|
||||
}
|
||||
enable := formBoolEnable(c)
|
||||
if err := d.Post.SetPinned(id, enable); err != nil {
|
||||
ctx.SetFlash(err.Error())
|
||||
ctx.Redirect(redir)
|
||||
return
|
||||
}
|
||||
if enable {
|
||||
ctx.SetFlash("已全局置顶")
|
||||
} else {
|
||||
ctx.SetFlash("已取消全局置顶")
|
||||
}
|
||||
ctx.Redirect(redir)
|
||||
}
|
||||
|
||||
// AdminPostBoardPinPost 版内置顶切换
|
||||
func (d Deps) AdminPostBoardPinPost(c *gin.Context) {
|
||||
ctx := d.ctx(c)
|
||||
id, redir, ok := d.parseAdminPostID(c)
|
||||
if !ok {
|
||||
ctx.Redirect("/")
|
||||
return
|
||||
}
|
||||
if !d.requireAdminPostCSRF(ctx, redir) {
|
||||
return
|
||||
}
|
||||
enable := formBoolEnable(c)
|
||||
if err := d.Post.SetBoardPinned(id, enable); err != nil {
|
||||
ctx.SetFlash(err.Error())
|
||||
ctx.Redirect(redir)
|
||||
return
|
||||
}
|
||||
if enable {
|
||||
ctx.SetFlash("已版内置顶")
|
||||
} else {
|
||||
ctx.SetFlash("已取消版内置顶")
|
||||
}
|
||||
ctx.Redirect(redir)
|
||||
}
|
||||
|
||||
// AdminPostFeaturePost 精华切换
|
||||
func (d Deps) AdminPostFeaturePost(c *gin.Context) {
|
||||
ctx := d.ctx(c)
|
||||
id, redir, ok := d.parseAdminPostID(c)
|
||||
if !ok {
|
||||
ctx.Redirect("/")
|
||||
return
|
||||
}
|
||||
if !d.requireAdminPostCSRF(ctx, redir) {
|
||||
return
|
||||
}
|
||||
enable := formBoolEnable(c)
|
||||
if err := d.Post.SetFeatured(id, enable); err != nil {
|
||||
ctx.SetFlash(err.Error())
|
||||
ctx.Redirect(redir)
|
||||
return
|
||||
}
|
||||
if enable {
|
||||
ctx.SetFlash("已设为精华")
|
||||
} else {
|
||||
ctx.SetFlash("已取消精华")
|
||||
}
|
||||
ctx.Redirect(redir)
|
||||
}
|
||||
|
||||
// AdminPostEditLockPost 禁止编辑切换
|
||||
func (d Deps) AdminPostEditLockPost(c *gin.Context) {
|
||||
ctx := d.ctx(c)
|
||||
id, redir, ok := d.parseAdminPostID(c)
|
||||
if !ok {
|
||||
ctx.Redirect("/")
|
||||
return
|
||||
}
|
||||
if !d.requireAdminPostCSRF(ctx, redir) {
|
||||
return
|
||||
}
|
||||
enable := formBoolEnable(c)
|
||||
if err := d.Post.SetEditLocked(id, enable); err != nil {
|
||||
ctx.SetFlash(err.Error())
|
||||
ctx.Redirect(redir)
|
||||
return
|
||||
}
|
||||
if enable {
|
||||
ctx.SetFlash("已禁止编辑")
|
||||
} else {
|
||||
ctx.SetFlash("已允许编辑")
|
||||
}
|
||||
ctx.Redirect(redir)
|
||||
}
|
||||
|
||||
// AdminPostCommentsLockPost 禁止评论切换
|
||||
func (d Deps) AdminPostCommentsLockPost(c *gin.Context) {
|
||||
ctx := d.ctx(c)
|
||||
id, redir, ok := d.parseAdminPostID(c)
|
||||
if !ok {
|
||||
ctx.Redirect("/")
|
||||
return
|
||||
}
|
||||
if !d.requireAdminPostCSRF(ctx, redir) {
|
||||
return
|
||||
}
|
||||
enable := formBoolEnable(c)
|
||||
if err := d.Post.SetCommentsLocked(id, enable); err != nil {
|
||||
ctx.SetFlash(err.Error())
|
||||
ctx.Redirect(redir)
|
||||
return
|
||||
}
|
||||
if enable {
|
||||
ctx.SetFlash("已锁定评论")
|
||||
} else {
|
||||
ctx.SetFlash("已解锁评论")
|
||||
}
|
||||
ctx.Redirect(redir)
|
||||
}
|
||||
|
||||
// AdminPostDeletePost 软删进回收站
|
||||
func (d Deps) AdminPostDeletePost(c *gin.Context) {
|
||||
ctx := d.ctx(c)
|
||||
id, redir, ok := d.parseAdminPostID(c)
|
||||
if !ok {
|
||||
ctx.Redirect("/")
|
||||
return
|
||||
}
|
||||
if !d.requireAdminPostCSRF(ctx, redir) {
|
||||
return
|
||||
}
|
||||
if err := d.Post.Delete(ctx.UserID(), id, true); err != nil {
|
||||
ctx.SetFlash(err.Error())
|
||||
ctx.Redirect(redir)
|
||||
return
|
||||
}
|
||||
ctx.SetFlash("帖子已移入回收站")
|
||||
ctx.Redirect("/admin/trash")
|
||||
}
|
||||
|
||||
type adminTrashRow struct {
|
||||
ID uint
|
||||
Title string
|
||||
AuthorName string
|
||||
BoardName string
|
||||
DeletedLabel string
|
||||
}
|
||||
|
||||
type adminTrashData struct {
|
||||
AdminChrome
|
||||
Items []adminTrashRow
|
||||
Page int
|
||||
HasPrev bool
|
||||
HasMore bool
|
||||
PrevPage int
|
||||
NextPage int
|
||||
Keyword string
|
||||
}
|
||||
|
||||
// AdminTrashGet 回收站
|
||||
func (d Deps) AdminTrashGet(c *gin.Context) {
|
||||
ctx := d.ctx(c)
|
||||
d.renderAdminTrash(ctx, "")
|
||||
}
|
||||
|
||||
func (d Deps) renderAdminTrash(ctx *webctx.Context, errMsg string) {
|
||||
chrome := d.adminChrome(ctx, "回收站", "trash")
|
||||
chrome.Error = errMsg
|
||||
page, _ := strconv.Atoi(ctx.C.DefaultQuery("page", "1"))
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
keyword := strings.TrimSpace(ctx.C.Query("keyword"))
|
||||
size := d.Settings.PageSizeDefault()
|
||||
data := adminTrashData{
|
||||
AdminChrome: chrome,
|
||||
Page: page,
|
||||
PrevPage: page - 1,
|
||||
NextPage: page + 1,
|
||||
HasPrev: page > 1,
|
||||
Keyword: keyword,
|
||||
}
|
||||
list, total, err := d.Post.ListTrash(page, size, keyword)
|
||||
if err != nil {
|
||||
data.Error = err.Error()
|
||||
ctx.HTML(http.StatusOK, "admin/trash", data)
|
||||
return
|
||||
}
|
||||
data.HasMore = int64(page*size) < total
|
||||
data.Items = make([]adminTrashRow, 0, len(list))
|
||||
for _, it := range list {
|
||||
author := strings.TrimSpace(it.User.Nickname)
|
||||
if author == "" {
|
||||
author = it.User.Username
|
||||
}
|
||||
bname := ""
|
||||
if it.Board.ID > 0 {
|
||||
bname = it.Board.Name
|
||||
}
|
||||
data.Items = append(data.Items, adminTrashRow{
|
||||
ID: it.ID, Title: it.Title, AuthorName: author, BoardName: bname,
|
||||
DeletedLabel: it.DeletedAt.Local().Format("2006-01-02 15:04"),
|
||||
})
|
||||
}
|
||||
ctx.HTML(http.StatusOK, "admin/trash", data)
|
||||
}
|
||||
|
||||
// AdminTrashRestorePost 恢复
|
||||
func (d Deps) AdminTrashRestorePost(c *gin.Context) {
|
||||
ctx := d.ctx(c)
|
||||
if !ctx.CheckCSRF() {
|
||||
ctx.SetFlash("无效请求,请重试")
|
||||
ctx.Redirect("/admin/trash")
|
||||
return
|
||||
}
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err := d.Post.Restore(uint(id)); err != nil {
|
||||
ctx.SetFlash(err.Error())
|
||||
ctx.Redirect("/admin/trash")
|
||||
return
|
||||
}
|
||||
ctx.SetFlash("已恢复")
|
||||
ctx.Redirect(fmt.Sprintf("/post/%d", id))
|
||||
}
|
||||
|
||||
// AdminTrashPurgePost 彻底删除
|
||||
func (d Deps) AdminTrashPurgePost(c *gin.Context) {
|
||||
ctx := d.ctx(c)
|
||||
if !ctx.CheckCSRF() {
|
||||
ctx.SetFlash("无效请求,请重试")
|
||||
ctx.Redirect("/admin/trash")
|
||||
return
|
||||
}
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err := d.Post.Purge(uint(id)); err != nil {
|
||||
ctx.SetFlash(err.Error())
|
||||
ctx.Redirect("/admin/trash")
|
||||
return
|
||||
}
|
||||
ctx.SetFlash("已彻底删除")
|
||||
ctx.Redirect("/admin/trash")
|
||||
}
|
||||
@@ -29,6 +29,12 @@ func Register(r *gin.Engine, deps Deps, authMW *auth.AuthMiddleware) {
|
||||
g.POST("/post/:id/unlock", authMW.RequireAuth(), deps.PostUnlock)
|
||||
g.POST("/post/:id/report", authMW.RequireAuth(), deps.PostReportPost)
|
||||
g.POST("/post/:id/comments/:cid/report", authMW.RequireAuth(), deps.CommentReportPost)
|
||||
g.POST("/post/:id/admin/pin", authMW.RequireAuth(), authMW.RequireAdmin(), deps.AdminPostPinPost)
|
||||
g.POST("/post/:id/admin/board-pin", authMW.RequireAuth(), authMW.RequireAdmin(), deps.AdminPostBoardPinPost)
|
||||
g.POST("/post/:id/admin/feature", authMW.RequireAuth(), authMW.RequireAdmin(), deps.AdminPostFeaturePost)
|
||||
g.POST("/post/:id/admin/edit-lock", authMW.RequireAuth(), authMW.RequireAdmin(), deps.AdminPostEditLockPost)
|
||||
g.POST("/post/:id/admin/comments-lock", authMW.RequireAuth(), authMW.RequireAdmin(), deps.AdminPostCommentsLockPost)
|
||||
g.POST("/post/:id/admin/delete", authMW.RequireAuth(), authMW.RequireAdmin(), deps.AdminPostDeletePost)
|
||||
g.GET("/login", deps.LoginGet)
|
||||
g.POST("/login", deps.LoginPost)
|
||||
g.POST("/logout", deps.LogoutPost)
|
||||
@@ -76,6 +82,9 @@ func Register(r *gin.Engine, deps Deps, authMW *auth.AuthMiddleware) {
|
||||
admin.POST("/pages/:id/publish", deps.AdminPagePublishPost)
|
||||
admin.GET("/reports", deps.AdminReportsGet)
|
||||
admin.POST("/reports/:id/handle", deps.AdminReportHandlePost)
|
||||
admin.GET("/trash", deps.AdminTrashGet)
|
||||
admin.POST("/trash/:id/restore", deps.AdminTrashRestorePost)
|
||||
admin.POST("/trash/:id/purge", deps.AdminTrashPurgePost)
|
||||
}
|
||||
|
||||
g.GET("/user/:id", deps.UserPublic)
|
||||
|
||||
@@ -23,8 +23,11 @@ type PostPageData struct {
|
||||
AuthorID uint
|
||||
BoardID uint
|
||||
BoardName string
|
||||
Pinned bool
|
||||
Pinned bool // 展示用:全局或版内置顶
|
||||
GlobalPinned bool
|
||||
BoardPinned bool
|
||||
Featured bool
|
||||
EditLocked bool
|
||||
PostTypeLabel string
|
||||
CreatedLabel string
|
||||
ViewCount int
|
||||
@@ -37,6 +40,7 @@ type PostPageData struct {
|
||||
CommentsLocked bool
|
||||
CanEdit bool
|
||||
CanReportPost bool
|
||||
IsAdmin bool
|
||||
}
|
||||
|
||||
// CommentView 评论
|
||||
@@ -126,7 +130,8 @@ func (d Deps) PostView(c *gin.Context) {
|
||||
PostPath: url.QueryEscape(fmt.Sprintf("/post/%d", post.ID)),
|
||||
PostTitle: post.Title, AuthorName: author, AuthorID: post.UserID,
|
||||
BoardID: post.BoardID, BoardName: boardName,
|
||||
Pinned: post.Pinned || post.BoardPinned, Featured: post.Featured,
|
||||
Pinned: post.Pinned || post.BoardPinned, GlobalPinned: post.Pinned, BoardPinned: post.BoardPinned,
|
||||
Featured: post.Featured, EditLocked: post.EditLocked,
|
||||
PostTypeLabel: postTypeLabel(post.PostType), CreatedLabel: formatTime(post.CreatedAt),
|
||||
ViewCount: post.ViewCount, LikeCount: post.LikeCount,
|
||||
Liked: d.Post.IsLiked(ctx.UserID(), post.ID), Favorited: d.Post.IsFavorited(ctx.UserID(), post.ID),
|
||||
@@ -134,6 +139,7 @@ func (d Deps) PostView(c *gin.Context) {
|
||||
CommentsLocked: post.CommentsLocked,
|
||||
CanEdit: d.Post.CanUserEdit(post, ctx.UserID(), ctx.IsAdmin()),
|
||||
CanReportPost: ctx.IsSigned() && post.UserID != ctx.UserID(),
|
||||
IsAdmin: ctx.IsAdmin(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user