feat: SSR 帖运营标记与回收站

EOF

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-29 18:20:47 +08:00
parent 83994e2707
commit 2d09729976
11 changed files with 415 additions and 16 deletions

View 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")
}