handlers区分backend和frontend,需要确认。
This commit is contained in:
@@ -117,3 +117,11 @@ func watchConfig() {
|
||||
func GetGlobalConfig() *Config {
|
||||
return globalConfig
|
||||
}
|
||||
|
||||
// getConfig 获取全局配置(启动时已初始化,直接返回)
|
||||
func GetConfig() *Config {
|
||||
if cfg := GetGlobalConfig(); cfg != nil {
|
||||
return cfg
|
||||
}
|
||||
return Load()
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package handlers
|
||||
package backend
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -21,7 +21,7 @@ type AdminPageData struct {
|
||||
Menus []AdminMenuGroup // 当前角色可见菜单(登录接口同源)
|
||||
PageSet map[string]bool // 菜单中的页面标识集合,模板据此渲染对应页面框架
|
||||
DisplayName string // 侧边栏显示的用户名
|
||||
CurrentUser template.JS // JSON 序列化的当前用户信息(id/username/nickname/role),注入前端
|
||||
CurrentUser template.JS // JSON 序列化的当前用户信息(id/username/nickname/role),注入前端
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -1,4 +1,4 @@
|
||||
package handlers
|
||||
package backend
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
@@ -1,4 +1,4 @@
|
||||
package handlers
|
||||
package backend
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
@@ -1,4 +1,4 @@
|
||||
package handlers
|
||||
package backend
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
@@ -0,0 +1,43 @@
|
||||
package backend
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"goblog/database"
|
||||
"goblog/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// GetDashboardStats 返回仪表盘统计数字(按角色过滤:管理员统计全部,普通用户统计自己的)
|
||||
func GetDashboardStats(c *gin.Context) {
|
||||
// 文章数:管理员统计全部,普通用户统计自己的(与文章管理页可见范围一致)
|
||||
postQuery := database.DB.Model(&models.Post{})
|
||||
if !isAdmin(c) {
|
||||
userID, _ := c.Get("userID")
|
||||
postQuery = postQuery.Where("author_id = ?", userID)
|
||||
}
|
||||
var postCount int64
|
||||
postQuery.Count(&postCount)
|
||||
|
||||
var categoryCount, tagCount int64
|
||||
database.DB.Model(&models.Category{}).Count(&categoryCount)
|
||||
database.DB.Model(&models.Tag{}).Count(&tagCount)
|
||||
|
||||
// 待审核评论数:管理员统计全部,普通用户统计自己文章上的
|
||||
commentQuery := database.DB.Model(&models.Comment{}).Where("status = ?", "pending")
|
||||
if !isAdmin(c) {
|
||||
userID, _ := c.Get("userID")
|
||||
commentQuery = commentQuery.Where("post_id IN (?)",
|
||||
database.DB.Model(&models.Post{}).Select("id").Where("author_id = ?", userID))
|
||||
}
|
||||
var pendingCommentCount int64
|
||||
commentQuery.Count(&pendingCommentCount)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"posts": postCount,
|
||||
"categories": categoryCount,
|
||||
"tags": tagCount,
|
||||
"pending_comments": pendingCommentCount,
|
||||
})
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package handlers
|
||||
package backend
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
@@ -1,4 +1,4 @@
|
||||
package handlers
|
||||
package backend
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
@@ -1,26 +1,18 @@
|
||||
package handlers
|
||||
package backend
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"goblog/config"
|
||||
"goblog/database"
|
||||
"goblog/models"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// 站点基础设置键名
|
||||
const (
|
||||
OptionSiteName = "site_name"
|
||||
OptionSiteDesc = "site_desc"
|
||||
OptionShowComments = "show_comments" // "1" 显示评论 / "0" 隐藏评论
|
||||
OptionCommentPageSize = "comments_per_page" // 前台评论每页条数
|
||||
)
|
||||
|
||||
// GetSettings 获取站点基础设置(管理员)
|
||||
func GetSettings(c *gin.Context) {
|
||||
cfg := getConfig()
|
||||
cfg := config.GetConfig()
|
||||
|
||||
opts, err := models.GetOptionsByUser(database.DB, 0)
|
||||
if err != nil {
|
||||
@@ -30,10 +22,10 @@ func GetSettings(c *gin.Context) {
|
||||
|
||||
// 默认值:站点信息取配置文件,评论默认显示、每页 10 条
|
||||
settings := map[string]string{
|
||||
OptionSiteName: cfg.App.Name,
|
||||
OptionSiteDesc: cfg.App.Description,
|
||||
OptionShowComments: "1",
|
||||
OptionCommentPageSize: "10",
|
||||
models.OptionSiteName: cfg.App.Name,
|
||||
models.OptionSiteDesc: cfg.App.Description,
|
||||
models.OptionShowComments: "1",
|
||||
models.OptionCommentPageSize: "10",
|
||||
}
|
||||
for name := range settings {
|
||||
if v, ok := opts[name]; ok && v != "" {
|
||||
@@ -65,10 +57,10 @@ func UpdateSettings(c *gin.Context) {
|
||||
showComments = "1"
|
||||
}
|
||||
options := map[string]string{
|
||||
OptionSiteName: req.SiteName,
|
||||
OptionSiteDesc: req.SiteDesc,
|
||||
OptionShowComments: showComments,
|
||||
OptionCommentPageSize: strconv.Itoa(req.CommentsPerPage),
|
||||
models.OptionSiteName: req.SiteName,
|
||||
models.OptionSiteDesc: req.SiteDesc,
|
||||
models.OptionShowComments: showComments,
|
||||
models.OptionCommentPageSize: strconv.Itoa(req.CommentsPerPage),
|
||||
}
|
||||
|
||||
if err := models.BatchSetOptions(database.DB, 0, options); err != nil {
|
||||
@@ -1,4 +1,4 @@
|
||||
package handlers
|
||||
package backend
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
@@ -1,4 +1,4 @@
|
||||
package handlers
|
||||
package backend
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
@@ -1,4 +1,4 @@
|
||||
package handlers
|
||||
package backend
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -1,4 +1,4 @@
|
||||
package handlers
|
||||
package backend
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
@@ -1,4 +1,4 @@
|
||||
package handlers
|
||||
package frontend
|
||||
|
||||
import (
|
||||
"math"
|
||||
@@ -26,10 +26,10 @@ func getConfig() *config.Config {
|
||||
func loadSiteInfo(cfg *config.Config) (siteName, siteDesc string) {
|
||||
siteName = cfg.App.Name
|
||||
siteDesc = cfg.App.Description
|
||||
if v, err := models.GetOptionValue(database.DB, OptionSiteName, 0); err == nil && v != "" {
|
||||
if v, err := models.GetOptionValue(database.DB, models.OptionSiteName, 0); err == nil && v != "" {
|
||||
siteName = v
|
||||
}
|
||||
if v, err := models.GetOptionValue(database.DB, OptionSiteDesc, 0); err == nil && v != "" {
|
||||
if v, err := models.GetOptionValue(database.DB, models.OptionSiteDesc, 0); err == nil && v != "" {
|
||||
siteDesc = v
|
||||
}
|
||||
return
|
||||
@@ -98,12 +98,6 @@ func IndexView(c *gin.Context) {
|
||||
db := database.DB.Model(&models.Post{}).Preload("Category").Preload("Tags").Preload("Author").
|
||||
Where("status = ?", "published")
|
||||
|
||||
userId, exists := c.Get("userID")
|
||||
|
||||
if exists {
|
||||
db = db.Or("author_id = ?", userId)
|
||||
}
|
||||
|
||||
if categoryID > 0 {
|
||||
db = db.Where("category_id = ?", categoryID)
|
||||
}
|
||||
@@ -212,11 +206,11 @@ func PostView(c *gin.Context) {
|
||||
|
||||
// 读取评论展示设置
|
||||
showComments := true
|
||||
if v, optErr := models.GetOptionValue(database.DB, OptionShowComments, 0); optErr == nil {
|
||||
if v, optErr := models.GetOptionValue(database.DB, models.OptionShowComments, 0); optErr == nil {
|
||||
showComments = v != "0"
|
||||
}
|
||||
commentPageSize := 10
|
||||
if v, optErr := models.GetOptionValue(database.DB, OptionCommentPageSize, 0); optErr == nil {
|
||||
if v, optErr := models.GetOptionValue(database.DB, models.OptionCommentPageSize, 0); optErr == nil {
|
||||
if n, convErr := strconv.Atoi(v); convErr == nil && n > 0 {
|
||||
commentPageSize = n
|
||||
}
|
||||
+5
-6
@@ -1,11 +1,10 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"goblog/handlers/backend"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"goblog/handlers"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
@@ -36,10 +35,10 @@ func JWTAuth() gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
claims := &handlers.Claims{}
|
||||
claims := &backend.Claims{}
|
||||
|
||||
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
|
||||
return handlers.JWTSecret(), nil
|
||||
return backend.JWTSecret(), nil
|
||||
})
|
||||
|
||||
if err != nil || !token.Valid {
|
||||
@@ -77,10 +76,10 @@ func OptionalAuth() gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
claims := &handlers.Claims{}
|
||||
claims := &backend.Claims{}
|
||||
|
||||
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
|
||||
return handlers.JWTSecret(), nil
|
||||
return backend.JWTSecret(), nil
|
||||
})
|
||||
|
||||
if err == nil && token.Valid {
|
||||
|
||||
@@ -10,6 +10,14 @@ import (
|
||||
|
||||
const PasswordCryptLevel = 12
|
||||
|
||||
// 站点基础设置键名
|
||||
const (
|
||||
OptionSiteName = "site_name"
|
||||
OptionSiteDesc = "site_desc"
|
||||
OptionShowComments = "show_comments" // "1" 显示评论 / "0" 隐藏评论
|
||||
OptionCommentPageSize = "comments_per_page" // 前台评论每页条数
|
||||
)
|
||||
|
||||
// ==================== 用户模型 ====================
|
||||
|
||||
type User struct {
|
||||
|
||||
+67
-63
@@ -3,7 +3,8 @@ package routers
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"goblog/handlers"
|
||||
"goblog/handlers/backend"
|
||||
"goblog/handlers/frontend"
|
||||
"goblog/middleware"
|
||||
"goblog/utils"
|
||||
|
||||
@@ -18,74 +19,77 @@ func SetupRoutes(r *gin.Engine) {
|
||||
// 后台管理(放在前面避免冲突)
|
||||
r.Static("/admin/static", "./static/admin")
|
||||
// 独立登录页
|
||||
r.GET("/admin/login", handlers.AdminLoginView)
|
||||
r.GET("/admin/login", backend.AdminLoginView)
|
||||
// 后台首页:可选认证中间件注入用户信息,服务端按角色渲染菜单与页面框架
|
||||
r.GET("/admin", middleware.OptionalAuth(), handlers.AdminView)
|
||||
r.GET("/admin", middleware.OptionalAuth(), backend.AdminView)
|
||||
|
||||
// 前端页面路由(可选认证:侧边栏根据登录态显示系统菜单)
|
||||
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)
|
||||
r.GET("/", middleware.OptionalAuth(), frontend.IndexView)
|
||||
r.GET("/post/:slug", middleware.OptionalAuth(), frontend.PostView)
|
||||
r.GET("/page/:slug", middleware.OptionalAuth(), frontend.PageView)
|
||||
r.GET("/search", middleware.OptionalAuth(), frontend.SearchView)
|
||||
r.POST("/comment", frontend.SubmitComment)
|
||||
// 前台退出登录:清除 Cookie 后回首页
|
||||
r.GET("/logout", handlers.WebLogout)
|
||||
r.GET("/logout", backend.WebLogout)
|
||||
|
||||
// API 路由组
|
||||
api := r.Group("/api")
|
||||
{
|
||||
// 公开 API(支持可选认证,以便管理员查看所有文章)
|
||||
api.GET("/posts", middleware.OptionalAuth(), handlers.GetPosts)
|
||||
api.GET("/posts/:id", middleware.OptionalAuth(), handlers.GetPost)
|
||||
api.GET("/categories", handlers.GetCategories)
|
||||
api.GET("/categories/:id", handlers.GetCategory)
|
||||
api.GET("/tags", handlers.GetTags)
|
||||
api.GET("/tags/:id", handlers.GetTag)
|
||||
api.GET("/pages", handlers.GetPages)
|
||||
api.GET("/pages/:id", handlers.GetPage)
|
||||
api.GET("/comments", handlers.GetComments)
|
||||
api.GET("/posts", middleware.OptionalAuth(), backend.GetPosts)
|
||||
api.GET("/posts/:id", middleware.OptionalAuth(), backend.GetPost)
|
||||
api.GET("/categories", backend.GetCategories)
|
||||
api.GET("/categories/:id", backend.GetCategory)
|
||||
api.GET("/tags", backend.GetTags)
|
||||
api.GET("/tags/:id", backend.GetTag)
|
||||
api.GET("/pages", backend.GetPages)
|
||||
api.GET("/pages/:id", backend.GetPage)
|
||||
api.GET("/comments", backend.GetComments)
|
||||
|
||||
// 需要认证的 API
|
||||
api.POST("/comments", middleware.OptionalAuth(), handlers.CreateComment)
|
||||
api.POST("/comments", middleware.OptionalAuth(), backend.CreateComment)
|
||||
|
||||
// 用户认证
|
||||
api.POST("/auth/login", handlers.Login)
|
||||
api.POST("/auth/logout", handlers.Logout)
|
||||
api.POST("/auth/login", backend.Login)
|
||||
api.POST("/auth/logout", backend.Logout)
|
||||
|
||||
// 需要登录的 API
|
||||
auth := api.Group("/")
|
||||
auth.Use(middleware.JWTAuth())
|
||||
{
|
||||
auth.GET("/auth/me", handlers.GetCurrentUser)
|
||||
auth.PUT("/auth/me", handlers.UpdateUser)
|
||||
auth.PUT("/auth/password", handlers.ChangePassword)
|
||||
auth.GET("/auth/me", backend.GetCurrentUser)
|
||||
|
||||
// 仪表盘统计(所有登录用户可访问,handler 内部按角色过滤数据)
|
||||
auth.GET("/dashboard", middleware.OptionalAuth(), backend.GetDashboardStats)
|
||||
auth.PUT("/auth/me", backend.UpdateUser)
|
||||
auth.PUT("/auth/password", backend.ChangePassword)
|
||||
|
||||
// 文章管理(所有登录用户可新建文章,修改/删除自己的文章)
|
||||
auth.POST("/posts", handlers.CreatePost)
|
||||
auth.PUT("/posts/:id", handlers.UpdatePost)
|
||||
auth.DELETE("/posts/:id", handlers.DeletePost)
|
||||
auth.POST("/posts", backend.CreatePost)
|
||||
auth.PUT("/posts/:id", backend.UpdatePost)
|
||||
auth.DELETE("/posts/:id", backend.DeletePost)
|
||||
|
||||
// Markdown 预览(文章编辑器用)
|
||||
auth.POST("/markdown/preview", handlers.PreviewMarkdown)
|
||||
auth.POST("/markdown/preview", backend.PreviewMarkdown)
|
||||
|
||||
// 文件上传(文章编辑器封面图等)
|
||||
auth.POST("/upload", handlers.UploadImage)
|
||||
auth.POST("/upload", backend.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)
|
||||
auth.GET("/my/comments", backend.GetComments)
|
||||
auth.PUT("/my/comments/:id/approve", backend.ApproveComment)
|
||||
auth.PUT("/my/comments/:id/spam", backend.MarkSpamComment)
|
||||
auth.DELETE("/my/comments/:id", backend.DeleteComment)
|
||||
}
|
||||
|
||||
// 主题管理(可选认证)
|
||||
api.GET("/themes", handlers.GetThemes)
|
||||
api.GET("/theme", handlers.GetCurrentTheme)
|
||||
api.POST("/theme", middleware.JWTAuth(), handlers.SwitchTheme)
|
||||
api.POST("/themes/create", middleware.JWTAuth(), handlers.CreateTheme)
|
||||
api.GET("/themes/:theme/files", middleware.JWTAuth(), handlers.GetThemeFiles)
|
||||
api.GET("/themes/:theme/files/:file", middleware.JWTAuth(), handlers.GetThemeFile)
|
||||
api.PUT("/themes/:theme/files/:file", middleware.JWTAuth(), handlers.SaveThemeFile)
|
||||
api.GET("/themes", backend.GetThemes)
|
||||
api.GET("/theme", backend.GetCurrentTheme)
|
||||
api.POST("/theme", middleware.JWTAuth(), backend.SwitchTheme)
|
||||
api.POST("/themes/create", middleware.JWTAuth(), backend.CreateTheme)
|
||||
api.GET("/themes/:theme/files", middleware.JWTAuth(), backend.GetThemeFiles)
|
||||
api.GET("/themes/:theme/files/:file", middleware.JWTAuth(), backend.GetThemeFile)
|
||||
api.PUT("/themes/:theme/files/:file", middleware.JWTAuth(), backend.SaveThemeFile)
|
||||
|
||||
// 管理员 API
|
||||
admin := api.Group("/admin")
|
||||
@@ -94,41 +98,41 @@ func SetupRoutes(r *gin.Engine) {
|
||||
// Markdown 预览(文章编辑器用)已移至登录组
|
||||
|
||||
// 分类管理
|
||||
admin.POST("/categories", handlers.CreateCategory)
|
||||
admin.PUT("/categories/:id", handlers.UpdateCategory)
|
||||
admin.DELETE("/categories/:id", handlers.DeleteCategory)
|
||||
admin.POST("/categories", backend.CreateCategory)
|
||||
admin.PUT("/categories/:id", backend.UpdateCategory)
|
||||
admin.DELETE("/categories/:id", backend.DeleteCategory)
|
||||
|
||||
// 标签管理
|
||||
admin.POST("/tags", handlers.CreateTag)
|
||||
admin.PUT("/tags/:id", handlers.UpdateTag)
|
||||
admin.DELETE("/tags/:id", handlers.DeleteTag)
|
||||
admin.POST("/tags", backend.CreateTag)
|
||||
admin.PUT("/tags/:id", backend.UpdateTag)
|
||||
admin.DELETE("/tags/:id", backend.DeleteTag)
|
||||
|
||||
// 页面管理
|
||||
admin.POST("/pages", handlers.CreatePage)
|
||||
admin.PUT("/pages/:id", handlers.UpdatePage)
|
||||
admin.DELETE("/pages/:id", handlers.DeletePage)
|
||||
admin.POST("/pages", backend.CreatePage)
|
||||
admin.PUT("/pages/:id", backend.UpdatePage)
|
||||
admin.DELETE("/pages/:id", backend.DeletePage)
|
||||
|
||||
// 评论管理
|
||||
admin.GET("/comments", handlers.GetComments)
|
||||
admin.PUT("/comments/:id/approve", handlers.ApproveComment)
|
||||
admin.PUT("/comments/:id/spam", handlers.MarkSpamComment)
|
||||
admin.DELETE("/comments/:id", handlers.DeleteComment)
|
||||
admin.GET("/comments", backend.GetComments)
|
||||
admin.PUT("/comments/:id/approve", backend.ApproveComment)
|
||||
admin.PUT("/comments/:id/spam", backend.MarkSpamComment)
|
||||
admin.DELETE("/comments/:id", backend.DeleteComment)
|
||||
|
||||
// 用户管理
|
||||
admin.GET("/users", handlers.GetUsers)
|
||||
admin.POST("/users", handlers.CreateUser)
|
||||
admin.PUT("/users/:id", handlers.AdminUpdateUser)
|
||||
admin.PUT("/users/:id/password", handlers.AdminResetPassword)
|
||||
admin.DELETE("/users/:id", handlers.DeleteUser)
|
||||
admin.GET("/users", backend.GetUsers)
|
||||
admin.POST("/users", backend.CreateUser)
|
||||
admin.PUT("/users/:id", backend.AdminUpdateUser)
|
||||
admin.PUT("/users/:id/password", backend.AdminResetPassword)
|
||||
admin.DELETE("/users/:id", backend.DeleteUser)
|
||||
|
||||
// 基础设置
|
||||
admin.GET("/settings", handlers.GetSettings)
|
||||
admin.PUT("/settings", handlers.UpdateSettings)
|
||||
admin.GET("/settings", backend.GetSettings)
|
||||
admin.PUT("/settings", backend.UpdateSettings)
|
||||
|
||||
// 文件管理(图片上传)
|
||||
admin.POST("/upload", handlers.UploadImage)
|
||||
admin.GET("/uploads", handlers.ListUploads)
|
||||
admin.DELETE("/uploads/:name", handlers.DeleteUpload)
|
||||
admin.POST("/upload", backend.UploadImage)
|
||||
admin.GET("/uploads", backend.ListUploads)
|
||||
admin.DELETE("/uploads/:name", backend.DeleteUpload)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+63
-16
@@ -4,6 +4,32 @@ const API_BASE = '/api';
|
||||
// 当前登录用户
|
||||
let currentUser = null;
|
||||
|
||||
// ============ Hash 路由 ============
|
||||
|
||||
// 从 URL hash 解析当前页面名(支持 #/posts、#/posts/5 等形式)
|
||||
function currentHashPage() {
|
||||
const hash = location.hash.replace(/^#\/?/, '').split('/')[0];
|
||||
return hash || 'dashboard';
|
||||
}
|
||||
|
||||
// 由 showPage 主动触发的 hash 变更(hashchange 事件中据此跳过,避免重复加载)
|
||||
let internalHashChange = false;
|
||||
|
||||
// 切换页面时同步 URL hash
|
||||
function setHashPage(pageName) {
|
||||
if (location.hash !== '#/' + pageName) {
|
||||
internalHashChange = true;
|
||||
location.hash = '#/' + pageName;
|
||||
}
|
||||
}
|
||||
|
||||
// 同步侧边栏菜单高亮
|
||||
function syncMenuActive(pageName) {
|
||||
document.querySelectorAll('.menu-item').forEach(i => {
|
||||
i.classList.toggle('active', i.dataset.page === pageName);
|
||||
});
|
||||
}
|
||||
|
||||
// 跳转到登录页
|
||||
function goLogin() {
|
||||
currentUser = null;
|
||||
@@ -27,17 +53,38 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
// 向服务端校验登录态与角色(不信任本地缓存,防止残留会话导致权限误判)
|
||||
refreshCurrentUser();
|
||||
|
||||
showPage('dashboard');
|
||||
// 按 URL hash 显示初始页面(默认仪表盘;无权限/不存在时静默回退)
|
||||
const initialPage = currentHashPage();
|
||||
if (document.getElementById(initialPage + '-page')) {
|
||||
showPage(initialPage);
|
||||
} else {
|
||||
showPage('dashboard');
|
||||
}
|
||||
|
||||
// Hash 路由:浏览器前进/后退或手动改 hash 时切换页面
|
||||
window.addEventListener('hashchange', () => {
|
||||
if (internalHashChange) {
|
||||
// 由 showPage 主动设置,页面已切换,跳过
|
||||
internalHashChange = false;
|
||||
return;
|
||||
}
|
||||
const page = currentHashPage();
|
||||
if (!document.getElementById(page + '-page')) {
|
||||
// 无权限或不存在时回退仪表盘,避免空白/报错
|
||||
setHashPage('dashboard');
|
||||
showPage('dashboard');
|
||||
return;
|
||||
}
|
||||
showPage(page);
|
||||
});
|
||||
|
||||
// 绑定菜单点击事件(菜单由服务端模板渲染,只包含有权限的页面)
|
||||
document.querySelectorAll('.menu-item').forEach(item => {
|
||||
item.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const page = item.dataset.page;
|
||||
setHashPage(page);
|
||||
showPage(page);
|
||||
|
||||
document.querySelectorAll('.menu-item').forEach(i => i.classList.remove('active'));
|
||||
item.classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -111,6 +158,9 @@ async function refreshCurrentUser() {
|
||||
|
||||
// 切换页面(页面框架由服务端模板渲染,未渲染的页面不存在也不加载数据)
|
||||
function showPage(pageName) {
|
||||
setHashPage(pageName);
|
||||
syncMenuActive(pageName);
|
||||
|
||||
document.querySelectorAll('.page').forEach(p => p.classList.add('hidden'));
|
||||
const targetPage = document.getElementById(pageName + '-page');
|
||||
if (!targetPage) {
|
||||
@@ -174,20 +224,17 @@ function getHeaders() {
|
||||
};
|
||||
}
|
||||
|
||||
// 加载仪表盘
|
||||
// 加载仪表盘(专用统计接口,只返回数字)
|
||||
async function loadDashboard() {
|
||||
try {
|
||||
const [posts, categories, tags, comments] = await Promise.all([
|
||||
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(`${commentsAPIBase()}/comments?status=pending`).then(r => r.json())
|
||||
]);
|
||||
|
||||
document.getElementById('stat-posts').textContent = posts.total || 0;
|
||||
document.getElementById('stat-categories').textContent = categories.data?.length || 0;
|
||||
document.getElementById('stat-tags').textContent = tags.data?.length || 0;
|
||||
document.getElementById('stat-comments').textContent = comments.data?.length || 0;
|
||||
const res = await fetch(`${API_BASE}/dashboard/stats`, { headers: getHeaders() });
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
|
||||
document.getElementById('stat-posts').textContent = data.posts || 0;
|
||||
document.getElementById('stat-categories').textContent = data.categories || 0;
|
||||
document.getElementById('stat-tags').textContent = data.tags || 0;
|
||||
document.getElementById('stat-comments').textContent = data.pending_comments || 0;
|
||||
} catch (err) {
|
||||
console.error('加载仪表盘失败:', err);
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
{{if index .PageSet "themes"}}{{template "admin_modal_theme" .}}{{end}}
|
||||
|
||||
<script>window.__currentUser = {{.CurrentUser}};</script>
|
||||
<script src="/static/admin/app.js?v=20"></script>
|
||||
<script src="/static/admin/app.js?v=21"></script>
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
@@ -61,22 +61,22 @@
|
||||
<div id="dashboard-page" class="page">
|
||||
<h1>仪表盘</h1>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<a href="#posts" class="stat-card" data-page="posts" onclick="showPage('posts')">
|
||||
<h3>文章总数</h3>
|
||||
<p class="stat-number" id="stat-posts">0</p>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
</a>
|
||||
<a href="#categories" class="stat-card" data-page="categories" onclick="showPage('categories')">
|
||||
<h3>分类总数</h3>
|
||||
<p class="stat-number" id="stat-categories">0</p>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
</a>
|
||||
<a href="#tags" class="stat-card" data-page="tags" onclick="showPage('tags')">
|
||||
<h3>标签总数</h3>
|
||||
<p class="stat-number" id="stat-tags">0</p>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
</a>
|
||||
<a href="#comments" class="stat-card" data-page="comments" onclick="showPage('comments')">
|
||||
<h3>待审核评论</h3>
|
||||
<p class="stat-number" id="stat-comments">0</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
@@ -199,6 +199,16 @@ body {
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
a.stat-card {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
transition: box-shadow 0.2s, transform 0.15s;
|
||||
}
|
||||
a.stat-card:hover {
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,0.13);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.stat-card {
|
||||
background: #fff;
|
||||
padding: 24px;
|
||||
|
||||
Reference in New Issue
Block a user