后台独立登录页面,并加入权限控制。
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"goblog/database"
|
||||
"goblog/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
// adminTemplatePath 后台单页模板(含 {{define}} 分块,按权限渲染)
|
||||
const adminTemplatePath = "./static/admin/index.html"
|
||||
|
||||
// AdminPageData 后台页面模板数据
|
||||
type AdminPageData struct {
|
||||
Menus []AdminMenuGroup // 当前角色可见菜单(登录接口同源)
|
||||
PageSet map[string]bool // 菜单中的页面标识集合,模板据此渲染对应页面框架
|
||||
DisplayName string // 侧边栏显示的用户名
|
||||
}
|
||||
|
||||
var (
|
||||
adminTmplOnce sync.Once
|
||||
adminTmpl *template.Template
|
||||
adminTmplErr error
|
||||
)
|
||||
|
||||
// getAdminTemplate 懒加载后台模板(仅解析一次)
|
||||
func getAdminTemplate() (*template.Template, error) {
|
||||
adminTmplOnce.Do(func() {
|
||||
adminTmpl, adminTmplErr = template.ParseFiles(adminTemplatePath)
|
||||
if adminTmplErr != nil {
|
||||
slog.Error("解析后台模板失败", "path", adminTemplatePath, "error", adminTmplErr)
|
||||
}
|
||||
})
|
||||
return adminTmpl, adminTmplErr
|
||||
}
|
||||
|
||||
// menuPageSet 从菜单提取页面标识集合
|
||||
func menuPageSet(menus []AdminMenuGroup) map[string]bool {
|
||||
set := make(map[string]bool)
|
||||
for _, g := range menus {
|
||||
for _, item := range g.Items {
|
||||
set[item.Key] = true
|
||||
}
|
||||
}
|
||||
return set
|
||||
}
|
||||
|
||||
// AdminLoginView 独立登录页
|
||||
func AdminLoginView(c *gin.Context) {
|
||||
c.File("./static/admin/login.html")
|
||||
}
|
||||
|
||||
// AdminView 后台管理页面:校验登录态后按角色用模板渲染菜单与页面框架
|
||||
func AdminView(c *gin.Context) {
|
||||
tokenString, err := c.Cookie("token")
|
||||
if err != nil || tokenString == "" {
|
||||
c.Redirect(http.StatusFound, "/admin/login")
|
||||
return
|
||||
}
|
||||
|
||||
claims := &Claims{}
|
||||
token, err := jwt.ParseWithClaims(tokenString, claims, func(t *jwt.Token) (interface{}, error) {
|
||||
return jwtSecret, nil
|
||||
})
|
||||
if err != nil || !token.Valid {
|
||||
c.Redirect(http.StatusFound, "/admin/login")
|
||||
return
|
||||
}
|
||||
|
||||
// 校验用户当前状态(禁用账号不允许进入后台)
|
||||
var user models.User
|
||||
if err := database.DB.First(&user, claims.UserID).Error; err != nil || user.Status == 0 || !user.IsActive {
|
||||
c.Redirect(http.StatusFound, "/admin/login")
|
||||
return
|
||||
}
|
||||
|
||||
tmpl, tmplErr := getAdminTemplate()
|
||||
if tmplErr != nil {
|
||||
c.String(http.StatusInternalServerError, "后台模板加载失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 菜单与登录接口保持同一来源,页面框架只渲染菜单内的页面
|
||||
menus := GetMenusByRole(user.Role)
|
||||
displayName := user.Nickname
|
||||
if displayName == "" {
|
||||
displayName = user.Username
|
||||
}
|
||||
data := AdminPageData{
|
||||
Menus: menus,
|
||||
PageSet: menuPageSet(menus),
|
||||
DisplayName: displayName,
|
||||
}
|
||||
|
||||
c.Header("Content-Type", "text/html; charset=utf-8")
|
||||
if err := tmpl.ExecuteTemplate(c.Writer, "admin_index", data); err != nil {
|
||||
slog.Error("渲染后台页面失败", "error", err)
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,50 @@ func JWTSecret() []byte {
|
||||
return jwtSecret
|
||||
}
|
||||
|
||||
// AdminMenuItem 后台菜单项
|
||||
type AdminMenuItem struct {
|
||||
Key string `json:"key"` // 页面标识,与页面 div 的 data-page 对应
|
||||
Name string `json:"name"` // 菜单显示名称
|
||||
}
|
||||
|
||||
// AdminMenuGroup 后台菜单分组(Group 为空表示顶级菜单)
|
||||
type AdminMenuGroup struct {
|
||||
Group string `json:"group"`
|
||||
Items []AdminMenuItem `json:"items"`
|
||||
}
|
||||
|
||||
// GetMenusByRole 根据角色返回后台菜单
|
||||
func GetMenusByRole(role string) []AdminMenuGroup {
|
||||
if role == "admin" {
|
||||
return []AdminMenuGroup{
|
||||
{Group: "", Items: []AdminMenuItem{
|
||||
{Key: "dashboard", Name: "仪表盘"},
|
||||
}},
|
||||
{Group: "内容管理", Items: []AdminMenuItem{
|
||||
{Key: "posts", Name: "文章管理"},
|
||||
{Key: "categories", Name: "分类管理"},
|
||||
{Key: "tags", Name: "标签管理"},
|
||||
{Key: "pages", Name: "页面管理"},
|
||||
{Key: "comments", Name: "评论管理"},
|
||||
}},
|
||||
{Group: "系统管理", Items: []AdminMenuItem{
|
||||
{Key: "users", Name: "用户管理"},
|
||||
{Key: "files", Name: "文件管理"},
|
||||
{Key: "themes", Name: "主题管理"},
|
||||
{Key: "settings", Name: "基础设置"},
|
||||
}},
|
||||
}
|
||||
}
|
||||
// 普通用户:仪表盘 + 文章管理 + 评论管理
|
||||
return []AdminMenuGroup{
|
||||
{Group: "", Items: []AdminMenuItem{
|
||||
{Key: "dashboard", Name: "仪表盘"},
|
||||
{Key: "posts", Name: "文章管理"},
|
||||
{Key: "comments", Name: "评论管理"},
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
// 登录请求
|
||||
type LoginRequest struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
@@ -88,6 +132,9 @@ func Login(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 同时写入 Cookie,供服务端渲染后台页面时识别登录态
|
||||
c.SetCookie("token", tokenString, 7*24*3600, "/", "", false, true)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"token": tokenString,
|
||||
"user": gin.H{
|
||||
@@ -100,9 +147,23 @@ func Login(c *gin.Context) {
|
||||
"last_login_at": user.LastLoginAt,
|
||||
"login_count": user.LoginCount,
|
||||
},
|
||||
// 菜单由登录接口根据角色返回
|
||||
"menus": GetMenusByRole(user.Role),
|
||||
})
|
||||
}
|
||||
|
||||
// 退出登录(清除服务端 Cookie)
|
||||
func Logout(c *gin.Context) {
|
||||
c.SetCookie("token", "", -1, "/", "", false, true)
|
||||
c.JSON(http.StatusOK, gin.H{"message": "已退出登录"})
|
||||
}
|
||||
|
||||
// WebLogout 前台页面退出登录(清除 Cookie 后回首页)
|
||||
func WebLogout(c *gin.Context) {
|
||||
c.SetCookie("token", "", -1, "/", "", false, true)
|
||||
c.Redirect(http.StatusFound, "/")
|
||||
}
|
||||
|
||||
// 获取当前用户信息
|
||||
func GetCurrentUser(c *gin.Context) {
|
||||
userID, _ := c.Get("userID")
|
||||
|
||||
@@ -30,6 +30,14 @@ func GetComments(c *gin.Context) {
|
||||
return db.Select("id", "title", "slug")
|
||||
})
|
||||
|
||||
// 非管理员只能查看自己文章上的评论
|
||||
if !isAdmin(c) {
|
||||
if userID, exists := c.Get("userID"); exists {
|
||||
db = db.Where("post_id IN (?)",
|
||||
database.DB.Model(&models.Post{}).Select("id").Where("author_id = ?", userID))
|
||||
}
|
||||
}
|
||||
|
||||
if postID != "" {
|
||||
db = db.Where("post_id = ?", postID)
|
||||
}
|
||||
@@ -95,6 +103,21 @@ func CreateComment(c *gin.Context) {
|
||||
c.JSON(http.StatusCreated, gin.H{"data": comment})
|
||||
}
|
||||
|
||||
// canManageComment 检查当前用户是否有权管理该评论(管理员始终可以,普通用户只能管理自己文章上的评论)
|
||||
func canManageComment(c *gin.Context, comment *models.Comment) bool {
|
||||
if isAdmin(c) {
|
||||
return true
|
||||
}
|
||||
if userID, exists := c.Get("userID"); exists {
|
||||
var post models.Post
|
||||
if err := database.DB.Select("author_id").First(&post, comment.PostID).Error; err != nil {
|
||||
return false
|
||||
}
|
||||
return post.AuthorID == userID.(uint)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 审核评论
|
||||
func ApproveComment(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
@@ -105,6 +128,11 @@ func ApproveComment(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if !canManageComment(c, &comment) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权管理该评论"})
|
||||
return
|
||||
}
|
||||
|
||||
comment.Status = "approved"
|
||||
if err := database.DB.Save(&comment).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "审核失败"})
|
||||
@@ -124,6 +152,11 @@ func MarkSpamComment(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if !canManageComment(c, &comment) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权管理该评论"})
|
||||
return
|
||||
}
|
||||
|
||||
comment.Status = "spam"
|
||||
if err := database.DB.Save(&comment).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "操作失败"})
|
||||
@@ -143,6 +176,11 @@ func DeleteComment(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if !canManageComment(c, &comment) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权管理该评论"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := database.DB.Delete(&comment).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "删除评论失败"})
|
||||
return
|
||||
|
||||
+39
-13
@@ -82,22 +82,23 @@ func GetPosts(c *gin.Context) {
|
||||
|
||||
db := database.DB.Model(&models.Post{}).Preload("Category").Preload("Tags").Preload("Author")
|
||||
|
||||
// 前端只显示已发布的文章
|
||||
if !isAdmin(c) {
|
||||
// 管理员可查看全部;登录用户可看到自己的全部文章(含草稿);未登录只能看已发布
|
||||
if isAdmin(c) {
|
||||
if query.Status != "" {
|
||||
db = db.Where("status = ?", query.Status)
|
||||
}
|
||||
} else if userId, exists := c.Get("userID"); exists {
|
||||
// 登录用户:只看自己的,不限状态(后台管理需要看到草稿)
|
||||
db = db.Where("author_id = ?", userId)
|
||||
} else {
|
||||
// 未登录:只看已发布
|
||||
db = db.Where("status = ?", "published")
|
||||
} else if query.Status != "" {
|
||||
db = db.Where("status = ?", query.Status)
|
||||
}
|
||||
|
||||
if query.CategoryID > 0 {
|
||||
db = db.Where("category_id = ?", query.CategoryID)
|
||||
}
|
||||
|
||||
userId, exists := c.Get("userID")
|
||||
if exists {
|
||||
db = db.Where("author_id = ?", userId)
|
||||
}
|
||||
|
||||
if query.TagID > 0 {
|
||||
db = db.Joins("JOIN post_tags ON post_tags.post_id = posts.id").
|
||||
Where("post_tags.tag_id = ?", query.TagID)
|
||||
@@ -141,9 +142,13 @@ func GetPost(c *gin.Context) {
|
||||
query = query.Where("slug = ?", id)
|
||||
}
|
||||
|
||||
// 非管理员只能查看已发布文章
|
||||
// 非管理员只能查看已发布文章,但可以查看自己的草稿(编辑用)
|
||||
if !isAdmin(c) {
|
||||
query = query.Where("status = ?", "published")
|
||||
if userId, exists := c.Get("userID"); exists {
|
||||
query = query.Where("status = ? OR author_id = ?", "published", userId)
|
||||
} else {
|
||||
query = query.Where("status = ?", "published")
|
||||
}
|
||||
}
|
||||
|
||||
if err := query.First(&post).Error; err != nil {
|
||||
@@ -176,7 +181,7 @@ func CreatePost(c *gin.Context) {
|
||||
AuthorID: userID.(uint),
|
||||
CategoryID: req.CategoryID,
|
||||
Status: req.Status,
|
||||
IsTop: req.IsTop,
|
||||
IsTop: req.IsTop && isAdmin(c), // 非管理员不允许置顶
|
||||
}
|
||||
|
||||
if req.Status == "published" {
|
||||
@@ -216,6 +221,15 @@ func UpdatePost(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 非管理员只能编辑自己的文章
|
||||
if !isAdmin(c) {
|
||||
userID, _ := c.Get("userID")
|
||||
if post.AuthorID != userID.(uint) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权编辑他人的文章"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var req UpdatePostRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
@@ -251,7 +265,10 @@ func UpdatePost(c *gin.Context) {
|
||||
now := time.Now()
|
||||
updates["published_at"] = &now
|
||||
}
|
||||
updates["is_top"] = req.IsTop
|
||||
// 非管理员不允许置顶
|
||||
if isAdmin(c) {
|
||||
updates["is_top"] = req.IsTop
|
||||
}
|
||||
|
||||
// 处理标签
|
||||
if len(req.Tags) > 0 {
|
||||
@@ -288,6 +305,15 @@ func DeletePost(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 非管理员只能删除自己的文章
|
||||
if !isAdmin(c) {
|
||||
userID, _ := c.Get("userID")
|
||||
if post.AuthorID != userID.(uint) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权删除他人的文章"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := database.DB.Delete(&post).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "删除文章失败"})
|
||||
return
|
||||
|
||||
@@ -25,6 +25,16 @@ func loadSidebarData() (categories []models.Category, tags []models.Tag, postCou
|
||||
return
|
||||
}
|
||||
|
||||
// loginInfo 从上下文提取登录态信息(供侧边栏系统菜单区分已登录/未登录)
|
||||
func loginInfo(c *gin.Context) (isLoggedIn bool, loginName string) {
|
||||
if _, ok := c.Get("userID"); !ok {
|
||||
return false, ""
|
||||
}
|
||||
name, _ := c.Get("username")
|
||||
loginName, _ = name.(string)
|
||||
return true, loginName
|
||||
}
|
||||
|
||||
// 首页
|
||||
type IndexData struct {
|
||||
Title string
|
||||
@@ -42,6 +52,9 @@ type IndexData struct {
|
||||
PostCount int64
|
||||
CategoryCount int64
|
||||
TagCount int64
|
||||
// 登录态(侧边栏系统菜单用)
|
||||
IsLoggedIn bool
|
||||
LoginName string
|
||||
}
|
||||
|
||||
func IndexView(c *gin.Context) {
|
||||
@@ -129,6 +142,7 @@ func IndexView(c *gin.Context) {
|
||||
CategoryCount: categoryCount,
|
||||
TagCount: tagCount,
|
||||
}
|
||||
data.IsLoggedIn, data.LoginName = loginInfo(c)
|
||||
|
||||
c.HTML(http.StatusOK, "index", data)
|
||||
}
|
||||
@@ -156,6 +170,9 @@ type PostDetailData struct {
|
||||
PostCount int64
|
||||
CategoryCount int64
|
||||
TagCount int64
|
||||
// 登录态(侧边栏系统菜单用)
|
||||
IsLoggedIn bool
|
||||
LoginName string
|
||||
}
|
||||
|
||||
func PostView(c *gin.Context) {
|
||||
@@ -280,6 +297,7 @@ func PostView(c *gin.Context) {
|
||||
CategoryCount: categoryCount,
|
||||
TagCount: tagCount,
|
||||
}
|
||||
data.IsLoggedIn, data.LoginName = loginInfo(c)
|
||||
|
||||
// 只有当查询成功时才赋值
|
||||
if prevErr == nil {
|
||||
@@ -307,6 +325,9 @@ type PageDetailData struct {
|
||||
PostCount int64
|
||||
CategoryCount int64
|
||||
TagCount int64
|
||||
// 登录态(侧边栏系统菜单用)
|
||||
IsLoggedIn bool
|
||||
LoginName string
|
||||
}
|
||||
|
||||
func PageView(c *gin.Context) {
|
||||
@@ -359,6 +380,7 @@ func PageView(c *gin.Context) {
|
||||
CategoryCount: categoryCount,
|
||||
TagCount: tagCount,
|
||||
}
|
||||
data.IsLoggedIn, data.LoginName = loginInfo(c)
|
||||
|
||||
c.HTML(http.StatusOK, "page", data)
|
||||
}
|
||||
@@ -464,6 +486,7 @@ func SearchView(c *gin.Context) {
|
||||
CategoryCount: categoryCount,
|
||||
TagCount: tagCount,
|
||||
}
|
||||
data.IsLoggedIn, data.LoginName = loginInfo(c)
|
||||
|
||||
c.HTML(http.StatusOK, "index", data)
|
||||
}
|
||||
|
||||
+13
-9
@@ -60,22 +60,26 @@ func AdminRequired() gin.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// 可选认证中间件(用于某些既支持游客又支持登录用户的接口)
|
||||
// OptionalAuth 可选认证中间件(用于某些既支持游客又支持登录用户的接口/页面)
|
||||
// 优先读 Authorization 头,其次回退到 Cookie(服务端渲染的前台页面依赖 Cookie)
|
||||
func OptionalAuth() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
tokenString := ""
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" {
|
||||
if authHeader != "" {
|
||||
parts := strings.SplitN(authHeader, " ", 2)
|
||||
if len(parts) == 2 && parts[0] == "Bearer" {
|
||||
tokenString = parts[1]
|
||||
}
|
||||
}
|
||||
if tokenString == "" {
|
||||
tokenString, _ = c.Cookie("token")
|
||||
}
|
||||
if tokenString == "" {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
parts := strings.SplitN(authHeader, " ", 2)
|
||||
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
tokenString := parts[1]
|
||||
claims := &handlers.Claims{}
|
||||
|
||||
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
|
||||
|
||||
+30
-15
@@ -17,16 +17,19 @@ func SetupRoutes(r *gin.Engine, cfg interface{}) {
|
||||
|
||||
// 后台管理(放在前面避免冲突)
|
||||
r.Static("/admin/static", "./static/admin")
|
||||
r.GET("/admin", func(c *gin.Context) {
|
||||
c.File("./static/admin/index.html")
|
||||
})
|
||||
// 独立登录页
|
||||
r.GET("/admin/login", handlers.AdminLoginView)
|
||||
// 后台首页:服务端按登录角色用模板渲染菜单与页面框架
|
||||
r.GET("/admin", handlers.AdminView)
|
||||
|
||||
// 前端页面路由
|
||||
r.GET("/", handlers.IndexView)
|
||||
r.GET("/post/:slug", handlers.PostView)
|
||||
r.GET("/page/:slug", handlers.PageView)
|
||||
r.GET("/search", handlers.SearchView)
|
||||
// 前端页面路由(可选认证:侧边栏根据登录态显示系统菜单)
|
||||
r.GET("/", middleware.OptionalAuth(), handlers.IndexView)
|
||||
r.GET("/post/:slug", middleware.OptionalAuth(), handlers.PostView)
|
||||
r.GET("/page/:slug", middleware.OptionalAuth(), handlers.PageView)
|
||||
r.GET("/search", middleware.OptionalAuth(), handlers.SearchView)
|
||||
r.POST("/comment", handlers.SubmitComment)
|
||||
// 前台退出登录:清除 Cookie 后回首页
|
||||
r.GET("/logout", handlers.WebLogout)
|
||||
|
||||
// API 路由组
|
||||
api := r.Group("/api")
|
||||
@@ -47,6 +50,7 @@ func SetupRoutes(r *gin.Engine, cfg interface{}) {
|
||||
|
||||
// 用户认证
|
||||
api.POST("/auth/login", handlers.Login)
|
||||
api.POST("/auth/logout", handlers.Logout)
|
||||
|
||||
// 需要登录的 API
|
||||
auth := api.Group("/")
|
||||
@@ -55,6 +59,23 @@ func SetupRoutes(r *gin.Engine, cfg interface{}) {
|
||||
auth.GET("/auth/me", handlers.GetCurrentUser)
|
||||
auth.PUT("/auth/me", handlers.UpdateUser)
|
||||
auth.PUT("/auth/password", handlers.ChangePassword)
|
||||
|
||||
// 文章管理(所有登录用户可新建文章,修改/删除自己的文章)
|
||||
auth.POST("/posts", handlers.CreatePost)
|
||||
auth.PUT("/posts/:id", handlers.UpdatePost)
|
||||
auth.DELETE("/posts/:id", handlers.DeletePost)
|
||||
|
||||
// Markdown 预览(文章编辑器用)
|
||||
auth.POST("/markdown/preview", handlers.PreviewMarkdown)
|
||||
|
||||
// 文件上传(文章编辑器封面图等)
|
||||
auth.POST("/upload", handlers.UploadImage)
|
||||
|
||||
// 评论管理(登录用户管理自己文章的评论)
|
||||
auth.GET("/my/comments", handlers.GetComments)
|
||||
auth.PUT("/my/comments/:id/approve", handlers.ApproveComment)
|
||||
auth.PUT("/my/comments/:id/spam", handlers.MarkSpamComment)
|
||||
auth.DELETE("/my/comments/:id", handlers.DeleteComment)
|
||||
}
|
||||
|
||||
// 主题管理(可选认证)
|
||||
@@ -70,13 +91,7 @@ func SetupRoutes(r *gin.Engine, cfg interface{}) {
|
||||
admin := api.Group("/admin")
|
||||
admin.Use(middleware.JWTAuth(), middleware.AdminRequired())
|
||||
{
|
||||
// 文章管理
|
||||
admin.POST("/posts", handlers.CreatePost)
|
||||
admin.PUT("/posts/:id", handlers.UpdatePost)
|
||||
admin.DELETE("/posts/:id", handlers.DeletePost)
|
||||
|
||||
// Markdown 预览(文章编辑器用)
|
||||
admin.POST("/markdown/preview", handlers.PreviewMarkdown)
|
||||
// Markdown 预览(文章编辑器用)已移至登录组
|
||||
|
||||
// 分类管理
|
||||
admin.POST("/categories", handlers.CreateCategory)
|
||||
|
||||
+127
-93
@@ -4,25 +4,34 @@ const API_BASE = '/api';
|
||||
// 当前登录用户
|
||||
let currentUser = null;
|
||||
|
||||
// 页面初始化
|
||||
// 跳转到独立登录页
|
||||
function goLogin() {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
localStorage.removeItem('menus');
|
||||
currentUser = null;
|
||||
location.replace('/admin/login');
|
||||
}
|
||||
|
||||
// 页面初始化(登录已移至独立页面 /admin/login,菜单与页面框架由服务端模板按权限渲染)
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
currentUser = JSON.parse(localStorage.getItem('user') || '{}');
|
||||
document.getElementById('username').textContent = currentUser.nickname || currentUser.username;
|
||||
showSidebar();
|
||||
showPage('dashboard');
|
||||
} else {
|
||||
// 未登录状态:隐藏所有页面,只显示登录页
|
||||
document.querySelectorAll('.page').forEach(p => p.classList.add('hidden'));
|
||||
const loginPage = document.getElementById('login-page');
|
||||
if (loginPage) {
|
||||
loginPage.classList.remove('hidden');
|
||||
}
|
||||
hideSidebar();
|
||||
if (!token) {
|
||||
goLogin();
|
||||
return;
|
||||
}
|
||||
currentUser = JSON.parse(localStorage.getItem('user') || '{}');
|
||||
const usernameEl = document.getElementById('username');
|
||||
if (usernameEl) {
|
||||
usernameEl.textContent = currentUser.nickname || currentUser.username;
|
||||
}
|
||||
|
||||
// 绑定菜单点击事件
|
||||
// 向服务端校验登录态与角色(不信任本地缓存,防止残留会话导致权限误判)
|
||||
refreshCurrentUser();
|
||||
|
||||
showPage('dashboard');
|
||||
|
||||
// 绑定菜单点击事件(菜单由服务端模板渲染,只包含有权限的页面)
|
||||
document.querySelectorAll('.menu-item').forEach(item => {
|
||||
item.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
@@ -43,20 +52,17 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// 登录表单
|
||||
document.getElementById('login-form').addEventListener('submit', handleLogin);
|
||||
|
||||
// 文章表单
|
||||
document.getElementById('post-form').addEventListener('submit', handlePostSubmit);
|
||||
|
||||
// 模态框表单
|
||||
document.getElementById('modal-form').addEventListener('submit', handleModalSubmit);
|
||||
// 退出登录
|
||||
const logoutBtn = document.getElementById('logout-btn');
|
||||
if (logoutBtn) {
|
||||
logoutBtn.addEventListener('click', logout);
|
||||
}
|
||||
|
||||
// 用户表单
|
||||
document.getElementById('user-form').addEventListener('submit', handleUserSubmit);
|
||||
|
||||
// 基础设置表单
|
||||
document.getElementById('settings-form').addEventListener('submit', handleSettingsSubmit);
|
||||
// 各表单绑定(页面未渲染时对应元素不存在,跳过绑定)
|
||||
bindForm('post-form', handlePostSubmit);
|
||||
bindForm('modal-form', handleModalSubmit);
|
||||
bindForm('user-form', handleUserSubmit);
|
||||
bindForm('settings-form', handleSettingsSubmit);
|
||||
|
||||
// 封面图上传
|
||||
const coverFileInput = document.getElementById('post-cover-file');
|
||||
@@ -71,33 +77,50 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
});
|
||||
|
||||
// 显示登录页
|
||||
function showLogin() {
|
||||
hideSidebar();
|
||||
document.querySelectorAll('.page').forEach(p => p.classList.add('hidden'));
|
||||
const loginPage = document.getElementById('login-page');
|
||||
if (loginPage) {
|
||||
loginPage.classList.remove('hidden');
|
||||
// 安全绑定表单提交(元素不存在时忽略)
|
||||
function bindForm(formId, handler) {
|
||||
const form = document.getElementById(formId);
|
||||
if (form) {
|
||||
form.addEventListener('submit', handler);
|
||||
}
|
||||
}
|
||||
|
||||
// 显示侧边栏
|
||||
function showSidebar() {
|
||||
document.querySelector('.sidebar').style.display = 'flex';
|
||||
// 向服务端获取权威用户信息,校验 token 与角色
|
||||
async function refreshCurrentUser() {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/auth/me`, { headers: getHeaders() });
|
||||
if (!res.ok) {
|
||||
goLogin();
|
||||
return;
|
||||
}
|
||||
const data = await res.json();
|
||||
const fresh = data.data || {};
|
||||
// 角色发生变化时重新加载页面,让服务端按新角色重新渲染菜单
|
||||
if (currentUser && fresh.role && currentUser.role !== fresh.role) {
|
||||
localStorage.setItem('user', JSON.stringify(fresh));
|
||||
location.reload();
|
||||
return;
|
||||
}
|
||||
currentUser = Object.assign({}, currentUser, fresh);
|
||||
localStorage.setItem('user', JSON.stringify(currentUser));
|
||||
const usernameEl = document.getElementById('username');
|
||||
if (usernameEl) {
|
||||
usernameEl.textContent = currentUser.nickname || currentUser.username;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('校验登录态失败:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// 隐藏侧边栏
|
||||
function hideSidebar() {
|
||||
document.querySelector('.sidebar').style.display = 'none';
|
||||
}
|
||||
|
||||
// 切换页面
|
||||
// 切换页面(页面框架由服务端模板渲染,未渲染的页面不存在也不加载数据)
|
||||
function showPage(pageName) {
|
||||
document.querySelectorAll('.page').forEach(p => p.classList.add('hidden'));
|
||||
const targetPage = document.getElementById(pageName + '-page');
|
||||
if (targetPage) {
|
||||
targetPage.classList.remove('hidden');
|
||||
if (!targetPage) {
|
||||
alert('无权访问该页面');
|
||||
return;
|
||||
}
|
||||
targetPage.classList.remove('hidden');
|
||||
|
||||
// 加载对应数据
|
||||
switch(pageName) {
|
||||
@@ -137,41 +160,14 @@ function showPage(pageName) {
|
||||
}
|
||||
}
|
||||
|
||||
// 登录
|
||||
async function handleLogin(e) {
|
||||
e.preventDefault();
|
||||
const username = document.getElementById('login-username').value;
|
||||
const password = document.getElementById('login-password').value;
|
||||
|
||||
// 退出登录:通知服务端清除 Cookie 后跳转登录页
|
||||
async function logout() {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password })
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
localStorage.setItem('token', data.token);
|
||||
localStorage.setItem('user', JSON.stringify(data.user));
|
||||
currentUser = data.user;
|
||||
document.getElementById('username').textContent = data.user.nickname || data.user.username;
|
||||
showSidebar();
|
||||
showPage('dashboard');
|
||||
} else {
|
||||
alert(data.error || '登录失败');
|
||||
}
|
||||
await fetch(`${API_BASE}/auth/logout`, { method: 'POST', headers: getHeaders() });
|
||||
} catch (err) {
|
||||
alert('网络错误');
|
||||
// 忽略网络异常,本地登出继续执行
|
||||
}
|
||||
}
|
||||
|
||||
// 退出登录
|
||||
function logout() {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
currentUser = null;
|
||||
showLogin();
|
||||
goLogin();
|
||||
}
|
||||
|
||||
// 获取请求头
|
||||
@@ -189,7 +185,7 @@ async function loadDashboard() {
|
||||
fetch(`${API_BASE}/posts?page_size=1`).then(r => r.json()),
|
||||
fetch(`${API_BASE}/categories`).then(r => r.json()),
|
||||
fetch(`${API_BASE}/tags`).then(r => r.json()),
|
||||
fetch(`${API_BASE}/comments?status=pending`).then(r => r.json())
|
||||
fetch(`${commentsAPIBase()}/comments?status=pending`).then(r => r.json())
|
||||
]);
|
||||
|
||||
document.getElementById('stat-posts').textContent = posts.total || 0;
|
||||
@@ -201,16 +197,26 @@ async function loadDashboard() {
|
||||
}
|
||||
}
|
||||
|
||||
// 加载文章列表
|
||||
async function loadPosts() {
|
||||
// 文章列表分页状态(每页 15 条)
|
||||
const POSTS_PAGE_SIZE = 15;
|
||||
let postsCurrentPage = 1;
|
||||
|
||||
// 加载文章列表(分页)
|
||||
async function loadPosts(page = postsCurrentPage) {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/posts?page_size=100`, { headers: getHeaders() });
|
||||
const res = await fetch(`${API_BASE}/posts?page=${page}&page_size=${POSTS_PAGE_SIZE}`, { headers: getHeaders() });
|
||||
const data = await res.json();
|
||||
|
||||
|
||||
const list = data.data || [];
|
||||
const total = data.total || 0;
|
||||
const totalPages = Math.max(1, Math.ceil(total / POSTS_PAGE_SIZE));
|
||||
postsCurrentPage = Math.min(Math.max(1, data.page || page), totalPages);
|
||||
|
||||
const tbody = document.getElementById('posts-list');
|
||||
tbody.innerHTML = data.data?.map(post => `
|
||||
tbody.innerHTML = list.map(post => `
|
||||
<tr>
|
||||
<td>${post.title}</td>
|
||||
<td>${post.author?.nickname || post.author?.username || '-'}</td>
|
||||
<td>${post.category?.name || '-'}</td>
|
||||
<td><span class="status-badge status-${post.status}">${getStatusText(post.status)}</span></td>
|
||||
<td>${post.views}</td>
|
||||
@@ -221,12 +227,29 @@ async function loadPosts() {
|
||||
<button class="btn-danger" onclick="deletePost(${post.id})">删除</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('') || '<tr><td colspan="6" style="text-align:center">暂无文章</td></tr>';
|
||||
`).join('') || '<tr><td colspan="7" style="text-align:center">暂无文章</td></tr>';
|
||||
|
||||
renderPostsPagination(total, totalPages);
|
||||
} catch (err) {
|
||||
console.error('加载文章失败:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染文章列表分页条
|
||||
function renderPostsPagination(total, totalPages) {
|
||||
const box = document.getElementById('posts-pagination');
|
||||
if (!box) return;
|
||||
|
||||
const buttons = [];
|
||||
buttons.push(`<button ${postsCurrentPage <= 1 ? 'disabled' : ''} onclick="loadPosts(${postsCurrentPage - 1})">上一页</button>`);
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
buttons.push(`<button class="${i === postsCurrentPage ? 'active' : ''}" onclick="loadPosts(${i})">${i}</button>`);
|
||||
}
|
||||
buttons.push(`<button ${postsCurrentPage >= totalPages ? 'disabled' : ''} onclick="loadPosts(${postsCurrentPage + 1})">下一页</button>`);
|
||||
|
||||
box.innerHTML = buttons.join('') + `<span class="pagination-info">共 ${total} 条,第 ${postsCurrentPage}/${totalPages} 页</span>`;
|
||||
}
|
||||
|
||||
// 显示文章表单
|
||||
async function showPostForm(isEdit = false) {
|
||||
document.getElementById('post-form-title').textContent = isEdit ? '编辑文章' : '新建文章';
|
||||
@@ -235,6 +258,12 @@ async function showPostForm(isEdit = false) {
|
||||
setCover('');
|
||||
mdShowEdit();
|
||||
|
||||
// 非管理员隐藏置顶选项
|
||||
const istopGroup = document.getElementById('post-istop')?.closest('.form-group');
|
||||
if (istopGroup) {
|
||||
istopGroup.style.display = (currentUser && currentUser.role === 'admin') ? '' : 'none';
|
||||
}
|
||||
|
||||
// 加载分类选项
|
||||
const res = await fetch(`${API_BASE}/categories`);
|
||||
const data = await res.json();
|
||||
@@ -296,7 +325,7 @@ async function handlePostSubmit(e) {
|
||||
};
|
||||
|
||||
try {
|
||||
const url = id ? `${API_BASE}/admin/posts/${id}` : `${API_BASE}/admin/posts`;
|
||||
const url = id ? `${API_BASE}/posts/${id}` : `${API_BASE}/posts`;
|
||||
const method = id ? 'PUT' : 'POST';
|
||||
|
||||
const res = await fetch(url, {
|
||||
@@ -376,7 +405,7 @@ async function mdShowPreview() {
|
||||
preview.classList.remove('hidden');
|
||||
preview.innerHTML = '<p class="md-preview-tip">渲染中...</p>';
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/admin/markdown/preview`, {
|
||||
const res = await fetch(`${API_BASE}/markdown/preview`, {
|
||||
method: 'POST',
|
||||
headers: getHeaders(),
|
||||
body: JSON.stringify({ content: ta.value })
|
||||
@@ -394,7 +423,7 @@ async function changePostStatus(id, status) {
|
||||
if (!confirm(`确定要${actionText}这篇文章吗?`)) return;
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/admin/posts/${id}`, {
|
||||
const res = await fetch(`${API_BASE}/posts/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: getHeaders(),
|
||||
body: JSON.stringify({ status })
|
||||
@@ -416,7 +445,7 @@ async function deletePost(id) {
|
||||
if (!confirm('确定要删除这篇文章吗?')) return;
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/admin/posts/${id}`, {
|
||||
const res = await fetch(`${API_BASE}/posts/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: getHeaders()
|
||||
});
|
||||
@@ -501,10 +530,15 @@ async function loadPages() {
|
||||
}
|
||||
}
|
||||
|
||||
// 评论管理 API 基础路径(管理员走 admin 组,普通用户走 my 组)
|
||||
function commentsAPIBase() {
|
||||
return (currentUser && currentUser.role === 'admin') ? `${API_BASE}/admin` : `${API_BASE}/my`;
|
||||
}
|
||||
|
||||
// 加载评论(postId 为空时加载全部)
|
||||
async function loadComments(postId = '') {
|
||||
try {
|
||||
const url = postId ? `${API_BASE}/admin/comments?post_id=${postId}` : `${API_BASE}/admin/comments`;
|
||||
const url = postId ? `${commentsAPIBase()}/comments?post_id=${postId}` : `${commentsAPIBase()}/comments`;
|
||||
const res = await fetch(url, { headers: getHeaders() });
|
||||
const data = await res.json();
|
||||
|
||||
@@ -708,7 +742,7 @@ async function deletePage(id) {
|
||||
|
||||
// 通过评论
|
||||
async function approveComment(id) {
|
||||
await fetch(`${API_BASE}/admin/comments/${id}/approve`, {
|
||||
await fetch(`${commentsAPIBase()}/comments/${id}/approve`, {
|
||||
method: 'PUT',
|
||||
headers: getHeaders()
|
||||
});
|
||||
@@ -718,7 +752,7 @@ async function approveComment(id) {
|
||||
// 删除评论
|
||||
async function deleteComment(id) {
|
||||
if (!confirm('确定要删除这条评论吗?')) return;
|
||||
await fetch(`${API_BASE}/admin/comments/${id}`, {
|
||||
await fetch(`${commentsAPIBase()}/comments/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: getHeaders()
|
||||
});
|
||||
@@ -1421,7 +1455,7 @@ async function uploadImageFile(file) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const res = await fetch(`${API_BASE}/admin/upload`, {
|
||||
const res = await fetch(`${API_BASE}/upload`, {
|
||||
method: 'POST',
|
||||
// 注意:上传 FormData 时不能手动设置 Content-Type,浏览器会自动处理 boundary
|
||||
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') },
|
||||
|
||||
+65
-47
@@ -1,11 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
{{define "admin_index"}}<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>后台管理 - GoBlog</title>
|
||||
<link rel="icon" href="/static/images/favicon.ico" type="image/x-icon">
|
||||
<link rel="stylesheet" href="/static/admin/style.css?v=2">
|
||||
<link rel="stylesheet" href="/static/admin/style.css?v=3">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
@@ -15,56 +15,49 @@
|
||||
<p>后台管理</p>
|
||||
</div>
|
||||
<nav class="menu">
|
||||
<a href="#dashboard" class="menu-item active" data-page="dashboard">仪表盘</a>
|
||||
|
||||
{{range .Menus}}{{if eq .Group ""}}{{range $i, $item := .Items}}<a href="#{{$item.Key}}" class="menu-item{{if eq $item.Key "dashboard"}} active{{end}}" data-page="{{$item.Key}}">{{$item.Name}}</a>
|
||||
{{end}}{{else}}
|
||||
<div class="menu-group open">
|
||||
<button class="menu-group-header" type="button">内容管理</button>
|
||||
<button class="menu-group-header" type="button">{{.Group}}</button>
|
||||
<div class="menu-sub">
|
||||
<a href="#posts" class="menu-item" data-page="posts">文章管理</a>
|
||||
<a href="#categories" class="menu-item" data-page="categories">分类管理</a>
|
||||
<a href="#tags" class="menu-item" data-page="tags">标签管理</a>
|
||||
<a href="#pages" class="menu-item" data-page="pages">页面管理</a>
|
||||
<a href="#comments" class="menu-item" data-page="comments">评论管理</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="menu-group open">
|
||||
<button class="menu-group-header" type="button">系统管理</button>
|
||||
<div class="menu-sub">
|
||||
<a href="#users" class="menu-item" data-page="users">用户管理</a>
|
||||
<a href="#files" class="menu-item" data-page="files">文件管理</a>
|
||||
<a href="#themes" class="menu-item" data-page="themes">主题管理</a>
|
||||
<a href="#settings" class="menu-item" data-page="settings">基础设置</a>
|
||||
{{range .Items}}<a href="#{{.Key}}" class="menu-item" data-page="{{.Key}}">{{.Name}}</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}{{end}}
|
||||
</nav>
|
||||
<div class="user-info">
|
||||
<span id="username">Admin</span>
|
||||
<button onclick="logout()">退出</button>
|
||||
<span id="username">{{.DisplayName}}</span>
|
||||
<button id="logout-btn">退出</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main class="main-content">
|
||||
<!-- 登录页面 -->
|
||||
<div id="login-page" class="page">
|
||||
<div class="login-box">
|
||||
<h2>管理员登录</h2>
|
||||
<form id="login-form">
|
||||
<div class="form-group">
|
||||
<label>用户名</label>
|
||||
<input type="text" id="login-username" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>密码</label>
|
||||
<input type="password" id="login-password" required>
|
||||
</div>
|
||||
<button type="submit">登录</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{{if index .PageSet "dashboard"}}{{template "admin_page_dashboard" .}}{{end}}
|
||||
{{if index .PageSet "posts"}}{{template "admin_page_posts" .}}{{template "admin_page_post_form" .}}{{end}}
|
||||
{{if index .PageSet "categories"}}{{template "admin_page_categories" .}}{{end}}
|
||||
{{if index .PageSet "tags"}}{{template "admin_page_tags" .}}{{end}}
|
||||
{{if index .PageSet "pages"}}{{template "admin_page_pages" .}}{{end}}
|
||||
{{if index .PageSet "comments"}}{{template "admin_page_comments" .}}{{end}}
|
||||
{{if index .PageSet "users"}}{{template "admin_page_users" .}}{{end}}
|
||||
{{if index .PageSet "settings"}}{{template "admin_page_settings" .}}{{end}}
|
||||
{{if index .PageSet "files"}}{{template "admin_page_files" .}}{{end}}
|
||||
{{if index .PageSet "themes"}}{{template "admin_page_themes" .}}{{template "admin_page_theme_editor" .}}{{end}}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{{if or (index .PageSet "categories") (index .PageSet "tags") (index .PageSet "pages")}}{{template "admin_modal_common" .}}{{end}}
|
||||
{{if index .PageSet "users"}}{{template "admin_modal_user" .}}{{end}}
|
||||
{{if index .PageSet "themes"}}{{template "admin_modal_theme" .}}{{end}}
|
||||
|
||||
<script src="/static/admin/app.js?v=19"></script>
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
|
||||
{{define "admin_page_dashboard"}}
|
||||
<!-- 仪表盘 -->
|
||||
<div id="dashboard-page" class="page hidden">
|
||||
<div id="dashboard-page" class="page">
|
||||
<h1>仪表盘</h1>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
@@ -85,7 +78,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{define "admin_page_posts"}}
|
||||
<!-- 文章管理 -->
|
||||
<div id="posts-page" class="page hidden">
|
||||
<div class="page-header">
|
||||
@@ -96,6 +91,7 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>标题</th>
|
||||
<th>作者</th>
|
||||
<th>分类</th>
|
||||
<th>状态</th>
|
||||
<th>浏览量</th>
|
||||
@@ -105,8 +101,11 @@
|
||||
</thead>
|
||||
<tbody id="posts-list"></tbody>
|
||||
</table>
|
||||
<div id="posts-pagination" class="pagination"></div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{define "admin_page_post_form"}}
|
||||
<!-- 文章表单 -->
|
||||
<div id="post-form-page" class="page hidden">
|
||||
<div class="page-header">
|
||||
@@ -191,7 +190,9 @@
|
||||
<button type="submit" class="btn-primary" onclick="mdShowEdit()">保存</button>
|
||||
</form>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{define "admin_page_categories"}}
|
||||
<!-- 分类管理 -->
|
||||
<div id="categories-page" class="page hidden">
|
||||
<div class="page-header">
|
||||
@@ -210,7 +211,9 @@
|
||||
<tbody id="categories-list"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{define "admin_page_tags"}}
|
||||
<!-- 标签管理 -->
|
||||
<div id="tags-page" class="page hidden">
|
||||
<div class="page-header">
|
||||
@@ -229,7 +232,9 @@
|
||||
<tbody id="tags-list"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{define "admin_page_pages"}}
|
||||
<!-- 页面管理 -->
|
||||
<div id="pages-page" class="page hidden">
|
||||
<div class="page-header">
|
||||
@@ -249,7 +254,9 @@
|
||||
<tbody id="pages-list"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{define "admin_page_comments"}}
|
||||
<!-- 评论管理 -->
|
||||
<div id="comments-page" class="page hidden">
|
||||
<div class="page-header">
|
||||
@@ -272,7 +279,9 @@
|
||||
<tbody id="comments-list"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{define "admin_page_users"}}
|
||||
<!-- 用户管理 -->
|
||||
<div id="users-page" class="page hidden">
|
||||
<div class="page-header">
|
||||
@@ -294,9 +303,11 @@
|
||||
<tbody id="users-list"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{define "admin_page_settings"}}
|
||||
<!-- 基础设置 -->
|
||||
<div id="settings-page" class="page hidden">
|
||||
<div id="settings-page" class="page hidden">
|
||||
<div class="page-header">
|
||||
<h1>基础设置</h1>
|
||||
</div>
|
||||
@@ -322,7 +333,9 @@
|
||||
<button type="submit" class="btn-primary">保存设置</button>
|
||||
</form>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{define "admin_page_files"}}
|
||||
<!-- 文件管理 -->
|
||||
<div id="files-page" class="page hidden">
|
||||
<div class="page-header">
|
||||
@@ -335,7 +348,9 @@
|
||||
<p class="upload-hint">支持 jpg/png/gif/webp 格式,单个文件最大 5MB</p>
|
||||
<div class="file-grid" id="file-grid"></div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{define "admin_page_themes"}}
|
||||
<!-- 主题管理 -->
|
||||
<div id="themes-page" class="page hidden">
|
||||
<div class="page-header">
|
||||
@@ -356,7 +371,9 @@
|
||||
<!-- 主题列表将动态加载 -->
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{define "admin_page_theme_editor"}}
|
||||
<!-- 主题编辑器 -->
|
||||
<div id="theme-editor-page" class="page hidden">
|
||||
<div class="page-header">
|
||||
@@ -379,9 +396,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{define "admin_modal_common"}}
|
||||
<!-- 分类/标签/页面 表单弹窗 -->
|
||||
<div id="modal" class="modal hidden">
|
||||
<div class="modal-content">
|
||||
@@ -407,7 +424,9 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{define "admin_modal_user"}}
|
||||
<!-- 用户表单弹窗 -->
|
||||
<div id="user-modal" class="modal hidden">
|
||||
<div class="modal-content">
|
||||
@@ -453,7 +472,9 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{define "admin_modal_theme"}}
|
||||
<!-- 创建主题弹窗 -->
|
||||
<div id="theme-modal" class="modal hidden">
|
||||
<div class="modal-content modal-large">
|
||||
@@ -477,7 +498,4 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/admin/app.js?v=16"></script>
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>登录 - GoBlog 后台管理</title>
|
||||
<link rel="icon" href="/static/images/favicon.ico" type="image/x-icon">
|
||||
<link rel="stylesheet" href="/static/admin/style.css?v=3">
|
||||
</head>
|
||||
<body>
|
||||
<div id="login-page">
|
||||
<div class="login-box">
|
||||
<h2>管理员登录</h2>
|
||||
<form id="login-form">
|
||||
<div class="form-group">
|
||||
<label>用户名</label>
|
||||
<input type="text" id="login-username" required autocomplete="username">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>密码</label>
|
||||
<input type="password" id="login-password" required autocomplete="current-password">
|
||||
</div>
|
||||
<div id="login-error" class="login-error hidden"></div>
|
||||
<button type="submit" id="login-submit">登录</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 已登录则直接进入后台
|
||||
if (localStorage.getItem('token')) {
|
||||
location.replace('/admin');
|
||||
}
|
||||
|
||||
function showError(msg) {
|
||||
const el = document.getElementById('login-error');
|
||||
el.textContent = msg;
|
||||
el.classList.remove('hidden');
|
||||
}
|
||||
|
||||
document.getElementById('login-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const btn = document.getElementById('login-submit');
|
||||
btn.disabled = true;
|
||||
btn.textContent = '登录中...';
|
||||
|
||||
const username = document.getElementById('login-username').value;
|
||||
const password = document.getElementById('login-password').value;
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
localStorage.setItem('token', data.token);
|
||||
localStorage.setItem('user', JSON.stringify(data.user));
|
||||
// 菜单由登录接口返回,缓存供前端参考(页面菜单以服务端模板渲染为准)
|
||||
localStorage.setItem('menus', JSON.stringify(data.menus || []));
|
||||
location.href = '/admin';
|
||||
} else {
|
||||
showError(data.error || '登录失败');
|
||||
btn.disabled = false;
|
||||
btn.textContent = '登录';
|
||||
}
|
||||
} catch (err) {
|
||||
showError('网络错误,请稍后重试');
|
||||
btn.disabled = false;
|
||||
btn.textContent = '登录';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -313,6 +313,60 @@ body {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.login-box button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.login-error {
|
||||
color: #e74c3c;
|
||||
background: #fdecea;
|
||||
border-radius: 4px;
|
||||
padding: 8px 12px;
|
||||
margin-bottom: 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 列表分页条 */
|
||||
.pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.pagination button {
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ddd;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.pagination button:hover:not(:disabled):not(.active) {
|
||||
border-color: #3498db;
|
||||
color: #3498db;
|
||||
}
|
||||
|
||||
.pagination button.active {
|
||||
background: #3498db;
|
||||
border-color: #3498db;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.pagination button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.pagination .pagination-info {
|
||||
margin-left: auto;
|
||||
color: #888;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* 弹窗 */
|
||||
.modal {
|
||||
position: fixed;
|
||||
|
||||
@@ -61,16 +61,22 @@
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<!-- 系统菜单 -->
|
||||
<!-- 系统菜单(根据登录态区分:已登录显示进入后台/退出登录,未登录显示用户登录) -->
|
||||
<div class="sidebar-widget">
|
||||
<h3 class="widget-title">⚙️ 系统</h3>
|
||||
<ul class="system-menu">
|
||||
{{if .IsLoggedIn}}
|
||||
<li class="system-menu-item">
|
||||
<a href="/admin" class="system-menu-link">🔐 进入后台</a>
|
||||
</li>
|
||||
<li class="system-menu-item">
|
||||
<a href="/logout" class="system-menu-link">🚪 退出登录{{if .LoginName}}({{.LoginName}}){{end}}</a>
|
||||
</li>
|
||||
{{else}}
|
||||
<li class="system-menu-item">
|
||||
<a href="/admin/login" class="system-menu-link">👤 用户登录</a>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
Reference in New Issue
Block a user