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}}
-
+