diff --git a/config/config.go b/config/config.go index e7e4bdf..72f54e3 100644 --- a/config/config.go +++ b/config/config.go @@ -117,3 +117,11 @@ func watchConfig() { func GetGlobalConfig() *Config { return globalConfig } + +// getConfig 获取全局配置(启动时已初始化,直接返回) +func GetConfig() *Config { + if cfg := GetGlobalConfig(); cfg != nil { + return cfg + } + return Load() +} diff --git a/handlers/admin_view.go b/handlers/backend/admin_view.go similarity index 95% rename from handlers/admin_view.go rename to handlers/backend/admin_view.go index fabebe1..a76b00a 100644 --- a/handlers/admin_view.go +++ b/handlers/backend/admin_view.go @@ -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 ( diff --git a/handlers/auth.go b/handlers/backend/auth.go similarity index 99% rename from handlers/auth.go rename to handlers/backend/auth.go index 05188a4..b11f2b3 100644 --- a/handlers/auth.go +++ b/handlers/backend/auth.go @@ -1,4 +1,4 @@ -package handlers +package backend import ( "log/slog" diff --git a/handlers/category.go b/handlers/backend/category.go similarity index 99% rename from handlers/category.go rename to handlers/backend/category.go index f732817..e6b503d 100644 --- a/handlers/category.go +++ b/handlers/backend/category.go @@ -1,4 +1,4 @@ -package handlers +package backend import ( "net/http" diff --git a/handlers/comment.go b/handlers/backend/comment.go similarity index 99% rename from handlers/comment.go rename to handlers/backend/comment.go index 79499df..d7bcb12 100644 --- a/handlers/comment.go +++ b/handlers/backend/comment.go @@ -1,4 +1,4 @@ -package handlers +package backend import ( "net/http" diff --git a/handlers/backend/dashboard.go b/handlers/backend/dashboard.go new file mode 100644 index 0000000..68d29b4 --- /dev/null +++ b/handlers/backend/dashboard.go @@ -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, + }) +} diff --git a/handlers/page.go b/handlers/backend/page.go similarity index 99% rename from handlers/page.go rename to handlers/backend/page.go index 96bba03..b89a9c0 100644 --- a/handlers/page.go +++ b/handlers/backend/page.go @@ -1,4 +1,4 @@ -package handlers +package backend import ( "net/http" diff --git a/handlers/post.go b/handlers/backend/post.go similarity index 99% rename from handlers/post.go rename to handlers/backend/post.go index 97c13cb..844ae18 100644 --- a/handlers/post.go +++ b/handlers/backend/post.go @@ -1,4 +1,4 @@ -package handlers +package backend import ( "net/http" diff --git a/handlers/setting.go b/handlers/backend/setting.go similarity index 71% rename from handlers/setting.go rename to handlers/backend/setting.go index 68d06e6..5257ea5 100644 --- a/handlers/setting.go +++ b/handlers/backend/setting.go @@ -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 { diff --git a/handlers/tag.go b/handlers/backend/tag.go similarity index 99% rename from handlers/tag.go rename to handlers/backend/tag.go index e427c4c..27ae7c2 100644 --- a/handlers/tag.go +++ b/handlers/backend/tag.go @@ -1,4 +1,4 @@ -package handlers +package backend import ( "net/http" diff --git a/handlers/theme.go b/handlers/backend/theme.go similarity index 99% rename from handlers/theme.go rename to handlers/backend/theme.go index 94ae3bf..26d1878 100644 --- a/handlers/theme.go +++ b/handlers/backend/theme.go @@ -1,4 +1,4 @@ -package handlers +package backend import ( "net/http" diff --git a/handlers/upload.go b/handlers/backend/upload.go similarity index 99% rename from handlers/upload.go rename to handlers/backend/upload.go index 37eadbe..c2b6a5b 100644 --- a/handlers/upload.go +++ b/handlers/backend/upload.go @@ -1,4 +1,4 @@ -package handlers +package backend import ( "fmt" diff --git a/handlers/user.go b/handlers/backend/user.go similarity index 99% rename from handlers/user.go rename to handlers/backend/user.go index 2ed6b56..e6c4ca5 100644 --- a/handlers/user.go +++ b/handlers/backend/user.go @@ -1,4 +1,4 @@ -package handlers +package backend import ( "net/http" diff --git a/handlers/view.go b/handlers/frontend/view.go similarity index 95% rename from handlers/view.go rename to handlers/frontend/view.go index 81b1a80..8915bd0 100644 --- a/handlers/view.go +++ b/handlers/frontend/view.go @@ -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 } diff --git a/middleware/auth.go b/middleware/auth.go index 5d09756..58f8e85 100644 --- a/middleware/auth.go +++ b/middleware/auth.go @@ -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 { diff --git a/models/models.go b/models/models.go index 26257df..44eca2d 100644 --- a/models/models.go +++ b/models/models.go @@ -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 { diff --git a/routers/router.go b/routers/router.go index 26f8526..9e4a83d 100644 --- a/routers/router.go +++ b/routers/router.go @@ -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) } } } diff --git a/static/admin/app.js b/static/admin/app.js index 815492c..7f2195b 100644 --- a/static/admin/app.js +++ b/static/admin/app.js @@ -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); } diff --git a/static/admin/index.html b/static/admin/index.html index 9624179..f036312 100644 --- a/static/admin/index.html +++ b/static/admin/index.html @@ -51,7 +51,7 @@ {{if index .PageSet "themes"}}{{template "admin_modal_theme" .}}{{end}} - + {{end}} @@ -61,22 +61,22 @@

仪表盘

- - - - +
{{end}} diff --git a/static/admin/style.css b/static/admin/style.css index 49a12af..9b18b19 100644 --- a/static/admin/style.css +++ b/static/admin/style.css @@ -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;