Files
jiang13-forum/handler/admin.go
freefire 55c256654b 支持板块图标与主题色自定义,并优化列表加载与未保存离开提示。
新增后端图标校验与色槽规范化,前端展示 BoardBadge/骨架屏,发帖与板块管理页增加未保存确认。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-28 17:13:32 +08:00

247 lines
7.4 KiB
Go

package handler
import (
"net/http"
"path/filepath"
"strconv"
"strings"
"github.com/gin-gonic/gin"
"git.iioio.com/freefire/jiang13-forum/model"
"git.iioio.com/freefire/jiang13-forum/service"
)
// --- 后台管理页面 ---
func (h *Handlers) adminPageData(c *gin.Context, title, activeNav string, data gin.H) gin.H {
if data == nil {
data = gin.H{}
}
data["ActiveNav"] = activeNav
return h.pageData(c, title, data)
}
func (h *Handlers) AdminLoginPage(c *gin.Context) {
c.HTML(http.StatusOK, "admin/login.html", h.pageData(c, "后台登录", gin.H{
"ActiveNav": "login",
"QueryBanned": c.Query("banned"),
}))
}
func (h *Handlers) AdminDashboard(c *gin.Context) {
var userCount, postCount, boardCount, commentCount int64
model.DB.Model(&model.User{}).Count(&userCount)
model.DB.Model(&model.Post{}).Count(&postCount)
model.DB.Model(&model.Board{}).Count(&boardCount)
model.DB.Model(&model.Comment{}).Count(&commentCount)
recentPosts, _, _ := h.Post.List(service.PostListQuery{Page: 1, Size: 8})
c.HTML(http.StatusOK, "admin/dashboard.html", h.adminPageData(c, "仪表盘", "dashboard", gin.H{
"UserCount": userCount,
"PostCount": postCount,
"BoardCount": boardCount,
"CommentCount": commentCount,
"OnlineCount": h.Online.Count(),
"RecentPosts": recentPosts,
}))
}
func (h *Handlers) AdminBoardsPage(c *gin.Context) {
boards, _ := h.Board.ListWithStats()
c.HTML(http.StatusOK, "admin/boards.html", h.adminPageData(c, "板块管理", "boards", gin.H{
"Boards": boards,
}))
}
func (h *Handlers) AdminPostsPage(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
keyword := strings.TrimSpace(c.Query("keyword"))
posts, total, _ := h.Post.List(service.PostListQuery{Page: page, Size: 20, Keyword: keyword})
c.HTML(http.StatusOK, "admin/posts.html", h.adminPageData(c, "帖子管理", "posts", gin.H{
"Posts": posts,
"Total": total,
"Page": page,
"Keyword": keyword,
"TotalPages": calcTotalPages(total, 20),
}))
}
func (h *Handlers) AdminCommentsPage(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
comments, total, _ := h.Comment.ListRecent(page, 20)
c.HTML(http.StatusOK, "admin/comments.html", h.adminPageData(c, "评论管理", "comments", gin.H{
"Comments": comments,
"Total": total,
"Page": page,
"TotalPages": calcTotalPages(total, 20),
}))
}
func (h *Handlers) AdminUsersPage(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
users, total, _ := h.User.ListUsers(page, 20)
c.HTML(http.StatusOK, "admin/users.html", h.adminPageData(c, "用户管理", "users", gin.H{
"Users": users,
"Total": total,
"Page": page,
"TotalPages": calcTotalPages(total, 20),
}))
}
func (h *Handlers) AdminSettingsPage(c *gin.Context) {
c.HTML(http.StatusOK, "admin/settings.html", h.adminPageData(c, "系统设置", "settings", gin.H{
"FilterPath": h.Cfg.FilterWordsPath(),
"DataDir": h.Cfg.DataDir,
"DBPath": h.Cfg.DBPath(),
"Port": h.Cfg.Port,
}))
}
func calcTotalPages(total int64, size int) int {
if total == 0 {
return 1
}
pages := int(total) / size
if int(total)%size > 0 {
pages++
}
if pages < 1 {
return 1
}
return pages
}
// --- 后台 API ---
func (h *Handlers) AdminAPICreateBoard(c *gin.Context) {
sortOrder, _ := strconv.Atoi(c.PostForm("sort_order"))
colorIndex, _ := strconv.Atoi(c.PostForm("color_index"))
board, err := h.Board.Create(c.PostForm("name"), c.PostForm("description"), c.PostForm("icon"), colorIndex, sortOrder)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "板块已创建", "id": board.ID})
}
func (h *Handlers) AdminAPIUpdateBoard(c *gin.Context) {
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
sortOrder, _ := strconv.Atoi(c.PostForm("sort_order"))
colorIndex, _ := strconv.Atoi(c.PostForm("color_index"))
if err := h.Board.Update(uint(id), c.PostForm("name"), c.PostForm("description"), c.PostForm("icon"), colorIndex, sortOrder); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "板块已更新"})
}
func (h *Handlers) AdminAPIDeleteBoard(c *gin.Context) {
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
if err := h.Board.Delete(uint(id)); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "板块已删除"})
}
func (h *Handlers) AdminAPIPinPost(c *gin.Context) {
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
pinned := c.PostForm("pinned") == "true" || c.PostForm("pinned") == "1"
if err := h.Post.SetPinned(uint(id), pinned); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
msg := "已取消置顶"
if pinned {
msg = "已置顶"
}
c.JSON(http.StatusOK, gin.H{"message": msg, "pinned": pinned})
}
func (h *Handlers) AdminAPIDeletePost(c *gin.Context) {
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
if err := h.Post.Delete(0, uint(id), true); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "帖子已删除"})
}
func (h *Handlers) AdminAPIDeleteComment(c *gin.Context) {
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
if err := h.Comment.AdminDelete(uint(id)); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "评论已删除"})
}
func (h *Handlers) AdminAPIBanUser(c *gin.Context) {
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
banned := c.PostForm("banned") == "true" || c.PostForm("banned") == "1"
if err := h.User.BanUser(uint(id), banned); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
msg := "已解除禁言"
if banned {
msg = "已禁言"
}
c.JSON(http.StatusOK, gin.H{"message": msg})
}
func (h *Handlers) AdminAPIBackup(c *gin.Context) {
path, err := h.Backup.ExportSQLite()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
filename := filepath.Base(path)
c.JSON(http.StatusOK, gin.H{
"message": "备份成功",
"path": path,
"filename": filename,
"download": "/admin/api/backup/download/" + filename,
})
}
func (h *Handlers) AdminDownloadBackup(c *gin.Context) {
name := c.Param("name")
if !strings.HasPrefix(name, "jiang13_backup_") || !strings.HasSuffix(name, ".db") {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的备份文件名"})
return
}
path := filepath.Join(h.Cfg.DataDir, name)
c.FileAttachment(path, name)
}
func (h *Handlers) AdminAPILogin(c *gin.Context) {
var req struct {
Username string `json:"username" form:"username" binding:"required"`
Password string `json:"password" form:"password" binding:"required"`
}
if err := c.ShouldBind(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
return
}
token, user, err := h.Auth.Login(req.Username, req.Password)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
return
}
if user.Role != model.RoleAdmin {
c.JSON(http.StatusForbidden, gin.H{"error": "需要管理员账号才能登录后台"})
return
}
h.setAuthCookie(c, token)
c.JSON(http.StatusOK, gin.H{
"message": "登录成功",
"user": gin.H{
"id": user.ID, "nickname": user.Nickname, "role": user.Role,
},
})
}
func (h *Handlers) AdminAPILogout(c *gin.Context) {
h.APILogout(c)
}