This commit is contained in:
张超
2025-05-27 19:01:05 +08:00
parent f02495cc7a
commit 5ffc64e1dd
16 changed files with 512 additions and 123 deletions

View File

@@ -132,3 +132,9 @@ func GetCurrentTheme() string {
}
func SetCurrentTheme(theme string) {
}
// GetJWTSecret 获取 JWT 密钥
func (c *Config) GetJWTSecret() string {
//return c.JWT.Secret
return c.Security.JWTSecret
}

View File

@@ -2,16 +2,54 @@ package controllers
import (
"go_blog/models"
"go_blog/themes" // <-- 确保导入 themes 包
"net/http"
"github.com/gin-gonic/gin"
"gorm.io/gorm" // <-- 确保导入 gorm
)
func Home(c *gin.Context) {
tm, exists := c.Get("ThemeManager")
if !exists {
c.String(http.StatusInternalServerError, "Theme manager not found in context")
return
}
themeManager, ok := tm.(*themes.ThemeManager)
if !ok {
c.String(http.StatusInternalServerError, "Invalid theme manager type in context")
return
}
var items []models.Content
models.DB.Select("*").Limit(5).Find(&items, "type = ?", "post")
c.HTML(http.StatusOK, "index.tmpl", gin.H{
"Items": items,
})
// 从 Gin 上下文中获取 DB 实例
dbInterface, exists := c.Get("DB")
if !exists {
c.String(http.StatusInternalServerError, "DB not found in context")
return
}
db, ok := dbInterface.(*gorm.DB)
if !ok {
c.String(http.StatusInternalServerError, "Invalid DB type in context")
return
}
db.Select("*").Limit(5).Find(&items, "type = ?", "post")
tpl := themeManager.GetTemplate("index") // "index" 是模板的基本名 (例如 index.tmpl -> index)
if tpl == nil {
c.String(http.StatusInternalServerError, "Template 'index' not found in current theme: "+themeManager.CurrentTheme())
return
}
c.Status(http.StatusOK)
c.Header("Content-Type", "text/html; charset=utf-8")
err := tpl.Execute(c.Writer, gin.H{
"Items": items,
"Title": "首页", // 你可以根据需要传递更多数据
})
if err != nil {
// 实际项目中应记录错误
c.String(http.StatusInternalServerError, "Error rendering template: "+err.Error())
}
}

View File

@@ -2,19 +2,69 @@ package controllers
import (
"go_blog/models"
"go_blog/themes" // <-- 确保导入 themes 包
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"gorm.io/gorm" // <-- 确保导入 gorm
)
func ShowPost(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 32)
if err != nil {
tm, exists := c.Get("ThemeManager")
if !exists {
c.String(http.StatusInternalServerError, "Theme manager not found")
return
}
var content = models.Content{Cid: int32(id)}
models.DB.First(&content)
c.HTML(http.StatusOK, "post.tmpl", content)
themeManager, ok := tm.(*themes.ThemeManager)
if !ok {
c.String(http.StatusInternalServerError, "Invalid theme manager type")
return
}
idParam := c.Param("id")
id, err := strconv.ParseInt(idParam, 10, 32)
if err != nil {
c.String(http.StatusBadRequest, "Invalid post ID format")
return
}
var content = models.Content{Cid: int32(id)}
// 从 Gin 上下文中获取 DB 实例
dbInterface, exists := c.Get("DB")
if !exists {
c.String(http.StatusInternalServerError, "DB not found in context")
return
}
db, ok := dbInterface.(*gorm.DB)
if !ok {
c.String(http.StatusInternalServerError, "Invalid DB type in context")
return
}
result := db.First(&content)
if result.Error != nil {
if result.Error == gorm.ErrRecordNotFound {
c.String(http.StatusNotFound, "Post not found")
} else {
c.String(http.StatusInternalServerError, "Error fetching post: "+result.Error.Error())
}
return
}
// 假设你的主题中有一个名为 "post" 的模板 (post.html 或 post.tmpl)
// 注意:当前的 default 主题 (web/themes/default/templates/) 并没有 post.tmpl你需要添加它。
tpl := themeManager.GetTemplate("post")
if tpl == nil {
c.String(http.StatusInternalServerError, "Template 'post' not found in current theme: "+themeManager.CurrentTheme()+". Make sure 'post.html' or 'post.tmpl' exists in the theme's templates directory.")
return
}
c.Status(http.StatusOK)
c.Header("Content-Type", "text/html; charset=utf-8")
// 将整个 content 对象传递给模板。模板内部通过 {{ .Title }} {{ .Text }} 等访问。
err = tpl.Execute(c.Writer, content)
if err != nil {
c.String(http.StatusInternalServerError, "Error rendering template: "+err.Error())
}
}

39
controllers/themes.go Normal file
View File

@@ -0,0 +1,39 @@
package controllers
import (
"go_blog/themes"
"os"
"github.com/gin-gonic/gin"
)
// ListThemes 获取可用主题列表
func ListThemes(c *gin.Context) {
// 从主题管理器获取当前主题
tm, exists := c.Get("ThemeManager")
if !exists {
c.JSON(500, gin.H{"error": "主题管理器未找到"})
return
}
themeManager := tm.(*themes.ThemeManager)
// 读取 web/themes 目录下的所有主题文件夹
themesDir := "web/themes"
entries, err := os.ReadDir(themesDir)
if err != nil {
c.JSON(500, gin.H{"error": "读取主题目录失败: " + err.Error()})
return
}
var themeList []string
for _, entry := range entries {
if entry.IsDir() {
themeList = append(themeList, entry.Name())
}
}
c.JSON(200, gin.H{
"current": themeManager.CurrentTheme(),
"themes": themeList,
})
}

View File

@@ -21,7 +21,8 @@ func UsersLoginHandler(ctx *gin.Context) {
response := Response{Ctx: ctx}
var loginUser serializers.Login
if err := ctx.ShouldBind(&loginUser); err != nil {
panic(err)
response.BadRequest("请求参数错误: " + err.Error()) // 替换 panic 为错误响应
return
}
user := loginUser.GetUser()
isLoginUser := user.CheckPassword()
@@ -29,7 +30,7 @@ func UsersLoginHandler(ctx *gin.Context) {
response.BadRequest("密码错误")
return
}
token, err := jwt.GenToken(user.ID, user.Username)
token, err := jwt.GenerateToken(user)
if err != nil {
panic(err)
}
@@ -44,7 +45,8 @@ func UsersRegisterHandler(ctx *gin.Context) {
response := Response{Ctx: ctx}
var registerUser serializers.Login
if err := ctx.ShouldBind(&registerUser); err != nil {
panic(err)
response.BadRequest("请求参数错误: " + err.Error()) // 替换 panic 为错误响应
return
}
user := registerUser.GetUser()
status := user.CheckDuplicateUsername()
@@ -73,12 +75,20 @@ func UsersSetInfoHandler(ctx *gin.Context) {
response.BadRequest("获取不到参数")
return
}
currentUser := jwt.AssertUser(ctx)
if currentUser != nil {
models.DB.Model(&currentUser).Updates(jsonData)
response.Response(currentUser, nil)
// 从上下文中获取用户(假设 JWT 中间件已将用户存入 "user" 键)
user, exists := ctx.Get("user")
if !exists {
response.Unauthenticated("未登录")
return
}
currentUser, ok := user.(*models.Account) // 明确类型为 models.Account
if !ok {
response.ServerError("用户类型错误")
return
}
models.DB.Model(currentUser).Updates(jsonData)
response.Response(currentUser, nil)
}
// 修改密码
@@ -119,10 +129,14 @@ func UsersListHandler(ctx *gin.Context) {
var pager serializers.Pager
pager.InitPager(ctx)
var users []models.Account
db := models.DB.Model(&users)
total := int64(pager.Total)
db.Count(&total)
db.Offset(pager.OffSet).Limit(pager.PageSize).Find(&users)
// 先查询总记录数
var totalCount int64
models.DB.Model(&models.Account{}).Count(&totalCount)
pager.Total = int(totalCount) // 正确设置总数
// 分页查询
models.DB.Offset(pager.OffSet()).Limit(pager.PageSize).Find(&users)
pager.GetPager()
response.Response(users, pager)
}

2
go.mod
View File

@@ -7,7 +7,7 @@ toolchain go1.23.8
require (
github.com/fsnotify/fsnotify v1.7.0
github.com/gin-gonic/gin v1.9.1
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/golang-jwt/jwt/v5 v5.2.2
github.com/gorilla/websocket v1.5.1
golang.org/x/crypto v0.33.0
gorm.io/driver/mysql v1.5.6

4
go.sum
View File

@@ -30,8 +30,8 @@ github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=

View File

@@ -18,7 +18,6 @@ import (
// @title 个人博客系统API
// @version 1.0
// @description 基于Go语言的可定制主题博客系统
const templatePath = "./templates/*"
func main() {
@@ -60,7 +59,6 @@ func main() {
gin.SetMode(gin.ReleaseMode)
}
router := gin.Default()
router.LoadHTMLGlob(templatePath)
themeManager.RegisterStaticRoutes(router)
// 6. 注册中间件

View File

@@ -69,3 +69,12 @@ func (a *Account) CheckDuplicateUsername() bool {
return true
}
}
// GetAccountByID 根据 ID 查询用户
func GetAccountByID(id uint) (*Account, error) {
var account Account
if err := DB.First(&account, id).Error; err != nil {
return nil, err
}
return &account, nil
}

View File

@@ -7,3 +7,12 @@ type User struct {
Password string
PasswordHash []byte
}
// GetUserByID 根据 ID 查询用户
func GetUserByID(id uint) (*User, error) {
var user User
if err := DB.First(&user, id).Error; err != nil {
return nil, err
}
return &user, nil
}

View File

@@ -1,77 +1,90 @@
/*
@Time : 2020/6/29 9:05
@Author : xuyiqing
@File : auth.py
*/
package jwt
import (
"go_blog/models"
"errors"
"fmt"
"net/http"
"strings"
"time"
"go_blog/config"
"go_blog/models"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt"
"github.com/spf13/viper"
"github.com/golang-jwt/jwt/v5"
)
// 定义jwt载荷
type UserClaims struct {
jwt.StandardClaims
ID uint64 `json:"id"`
Username string `json:"username"`
// 定义 JWT 密钥(从配置文件读取)
var jwtSecret = []byte(config.GetJWTSecret())
// CustomClaims 自定义 JWT 载荷(包含用户 ID
type CustomClaims struct {
UserID uint `json:"user_id"`
jwt.RegisteredClaims
}
// 根据payload查询user返回
func (c *UserClaims) GetUserByID() *models.Account {
var user models.Account
models.DB.Model(&models.Account{}).First(&user, c.ID)
if user.ID > 0 {
return &user
} else {
return nil
}
}
// 生成jwt token字符串
func GenToken(id uint64, username string) (string, error) {
expiredTime := time.Now().Add(time.Hour * time.Duration(24)).Unix()
claims := UserClaims{
jwt.StandardClaims{
ExpiresAt: expiredTime,
// GenerateToken 生成 JWT 令牌
func GenerateToken(user *models.Account) (string, error) {
expiresAt := jwt.NewNumericDate(time.Now().Add(24 * time.Hour)) // 令牌有效期 24 小时
claims := CustomClaims{
UserID: uint(user.ID),
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: expiresAt,
Issuer: "go_blog",
},
id,
username,
}
tokenClaims := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
token, err := tokenClaims.SignedString([]byte(viper.GetString("config.JwtSecretKey.secretKey")))
return token, err
}
// 验证token合法性
func ValidateJwtToken(token string) (*UserClaims, error) {
tokenClaims, err := jwt.ParseWithClaims(token, &UserClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(viper.GetString("config.JwtSecretKey.secretKey")), nil
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(jwtSecret)
}
// VerifyToken 验证 JWT 令牌并返回用户信息
func VerifyToken(tokenStr string) (*models.Account, error) { // 修改返回类型为 *models.Account
token, err := jwt.ParseWithClaims(tokenStr, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return jwtSecret, nil
})
if tokenClaims != nil {
if claims, ok := tokenClaims.Claims.(*UserClaims); ok && tokenClaims.Valid {
return claims, nil
}
}
if err != nil {
return nil, err
}
// 断言设定ctx的当前用户
func AssertUser(ctx *gin.Context) *models.Account {
currentUser, isExists := ctx.Get("CurrentUser")
if !isExists {
return nil
if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {
return models.GetAccountByID(claims.UserID) // 假设 models 包新增 GetAccountByID 方法
}
user, ok := currentUser.(*models.Account)
if ok {
return user
} else {
return nil
return nil, errors.New("invalid token")
}
// AuthRequired Gin 中间件:验证 JWT 令牌
func AuthRequired() gin.HandlerFunc {
return func(c *gin.Context) {
// 从请求头获取 Authorization 字段格式Bearer <token>
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "请提供认证令牌"})
c.Abort()
return
}
// 解析令牌
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "令牌格式错误(应为 Bearer <token>"})
c.Abort()
return
}
// 验证令牌并获取用户
user, err := VerifyToken(parts[1])
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "无效或过期的令牌: " + err.Error()})
c.Abort()
return
}
// 将用户信息存入上下文(后续路由可通过 c.Get("user") 获取)
c.Set("user", user)
c.Next()
}
}

View File

@@ -5,8 +5,10 @@ import (
"go_blog/controllers"
"go_blog/models"
"go_blog/serializers"
"go_blog/themes" // <-- 确保导入 themes 包
"log"
"net/http"
"strconv"
"time"
"github.com/gin-gonic/gin"
@@ -19,32 +21,87 @@ func RegisterRoutes(r *gin.Engine) {
r.GET("/events", esSSE)
r.GET("/page", func(c *gin.Context) {
var items []models.Content
var pager serializers.Pager
pager.InitPager(c)
offset := (pager.Page - 1) * pager.PageSize
if dbInterface, ok := c.Get("DB"); ok {
if db, ok := dbInterface.(*gorm.DB); ok {
db.Select("*").Offset(offset).Limit(pager.PageSize).Find(&items, "type = ?", "post")
} else {
log.Println("无法将 DB 转换为 *gorm.DB 类型")
c.JSON(http.StatusInternalServerError, gin.H{"error": "内部服务器错误"})
tm, exists := c.Get("ThemeManager")
if !exists {
c.String(http.StatusInternalServerError, "Theme manager not found")
return
}
} else {
themeManager, ok := tm.(*themes.ThemeManager)
if !ok {
c.String(http.StatusInternalServerError, "Invalid theme manager type")
return
}
var items []models.Content
var pager serializers.Pager
pager.InitPager(c) // 这会从查询参数中读取 page 和 pageSize
offset := (pager.Page - 1) * pager.PageSize
dbInterface, dbOk := c.Get("DB")
if !dbOk {
log.Println("未找到键 'DB' 的上下文值")
c.JSON(http.StatusInternalServerError, gin.H{"error": "内部服务器错误"})
return
}
db, typeOk := dbInterface.(*gorm.DB)
if !typeOk {
log.Println("无法将 DB 转换为 *gorm.DB 类型")
c.JSON(http.StatusInternalServerError, gin.H{"error": "内部服务器错误"})
return
}
c.HTML(http.StatusOK, "index.tmpl", gin.H{
db.Select("*").Offset(offset).Limit(pager.PageSize).Find(&items, "type = ?", "post")
var totalCount int64
db.Model(&models.Content{}).Where("type = ?", "post").Count(&totalCount)
pager.Total = int(totalCount) // 确保 Pager 结构体中的 Total 类型匹配
tpl := themeManager.GetTemplate("index") // 假设分页列表也使用 index 模板
if tpl == nil {
c.String(http.StatusInternalServerError, "Template 'index' not found in current theme: "+themeManager.CurrentTheme())
return
}
c.Status(http.StatusOK)
c.Header("Content-Type", "text/html; charset=utf-8")
err := tpl.Execute(c.Writer, gin.H{
"Items": items,
"Pager": pager,
"Title": "文章列表",
"Pager": pager, // 确保你的 index.tmpl 支持 Pager 结构
"Title": "文章列表 - 第 " + strconv.Itoa(pager.Page) + " 页",
})
if err != nil {
c.String(http.StatusInternalServerError, "Error rendering template: "+err.Error())
}
})
r.GET("/createcontent", func(c *gin.Context) {
c.HTML(http.StatusOK, "content.tmpl", nil)
tm, exists := c.Get("ThemeManager")
if !exists {
c.String(http.StatusInternalServerError, "Theme manager not found")
return
}
themeManager, ok := tm.(*themes.ThemeManager)
if !ok {
c.String(http.StatusInternalServerError, "Invalid theme manager type")
return
}
// 假设你的主题中有一个名为 "content" 的模板 (content.html 或 content.tmpl)
// 注意:当前的 default 主题并没有 content.tmpl你需要添加它。
// 如果这个页面是后台页面,它可能不需要主题化,或者使用一个固定的后台模板。
tpl := themeManager.GetTemplate("content")
if tpl == nil {
c.String(http.StatusInternalServerError, "Template 'content' not found in current theme: "+themeManager.CurrentTheme()+". Make sure 'content.html' or 'content.tmpl' exists.")
return
}
c.Status(http.StatusOK)
c.Header("Content-Type", "text/html; charset=utf-8")
err := tpl.Execute(c.Writer, gin.H{
"Title": "创建新内容",
})
if err != nil {
c.String(http.StatusInternalServerError, "Error rendering template: "+err.Error())
}
})
r.GET("/ws", controllers.WebSocketHandler)
@@ -57,12 +114,12 @@ func RegisterRoutes(r *gin.Engine) {
r.GET("/", controllers.Home)
r.GET("/post/:id", controllers.ShowPost)
// // Admin panel
// admin := r.Group("/admin", middleware.AuthRequired())
// {
// admin.GET("/themes", controllers.ListThemes)
// admin.POST("/themes/switch", controllers.SwitchTheme)
// }
// Admin panel(取消注释并完善)
admin := r.Group("/admin", jwt.AuthRequired()) // 新增中间件
{
admin.GET("/themes", controllers.ListThemes)
admin.POST("/themes/switch", controllers.SwitchTheme)
}
// Static files for themes
r.StaticFS("/themes", http.Dir("web/themes"))

View File

@@ -0,0 +1,29 @@
<!DOCTYPE html>
<html>
<head>
<title>主题管理</title>
<style>
.container { max-width: 800px; margin: 20px auto; padding: 20px; }
.theme-list { margin-top: 20px; }
.theme-item { padding: 10px; border: 1px solid #eee; margin: 5px 0; }
</style>
</head>
<body>
<div class="container">
<h1>主题管理</h1>
<p>当前主题:<strong>{{.CurrentTheme}}</strong></p>
<div class="theme-list">
<h3>可选主题</h3>
<form action="/admin/themes/switch" method="post">
<select name="theme" style="padding: 8px; width: 200px;">
{{range .Themes}}
<option value="{{.}}" {{if eq . $.CurrentTheme}}selected{{end}}>{{.}}</option>
{{end}}
</select>
<button type="submit" style="padding: 8px 15px; margin-left: 10px;">切换主题</button>
</form>
</div>
</div>
</body>
</html>

View File

@@ -39,6 +39,12 @@ func NewManager(themesDir string) *ThemeManager {
}
}
// 自定义错误类型
var (
ErrInvalidThemeStructure = errors.New("invalid theme structure")
ErrNoValidTemplates = errors.New("no valid templates found")
)
// 核心方法:加载主题
func (m *ThemeManager) LoadTheme(themeName string) error {
m.mu.Lock()
@@ -47,7 +53,7 @@ func (m *ThemeManager) LoadTheme(themeName string) error {
// 1. 验证主题目录结构
themePath := filepath.Join(m.themesDir, themeName)
if !isValidTheme(themePath) {
return fmt.Errorf("invalid theme structure: %s", themeName)
return fmt.Errorf("%w: %s", ErrInvalidThemeStructure, themeName)
}
// 2. 加载模板文件
@@ -85,21 +91,50 @@ func (m *ThemeManager) RegisterStaticRoutes(router *gin.Engine) {
// 校验主题完整性
func isValidTheme(themePath string) bool {
requiredFiles := []string{
requiredBaseFiles := []string{ // 不带扩展名的基本文件名
"theme.yaml",
// "templates/home.html",
// "templates/post.html",
"templates/index.html",
"templates/index", // 例如: templates/index.html 或 templates/index.tmpl
// "templates/post", // 如果 post 模板是必须的,也加入这里
}
supportedTemplateExtensions := []string{".html", ".tmpl"}
for _, baseFile := range requiredBaseFiles {
found := false
if strings.HasPrefix(baseFile, "templates/") {
// 对于模板文件,尝试所有支持的扩展名
for _, ext := range supportedTemplateExtensions {
if exists, _ := fileExists(filepath.Join(themePath, baseFile+ext)); exists {
found = true
break
}
}
} else {
// 对于非模板文件 (如 theme.yaml),直接检查
if exists, _ := fileExists(filepath.Join(themePath, baseFile)); exists {
found = true
}
}
for _, f := range requiredFiles {
if _, err := os.Stat(filepath.Join(themePath, f)); os.IsNotExist(err) {
if !found {
// log.Printf("Required file/template %s not found in theme %s", baseFile, themePath) // 可选的调试日志
return false
}
}
return true
}
// 检查文件是否存在
func fileExists(path string) (bool, error) {
_, err := os.Stat(path)
if err == nil {
return true, nil
}
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
// 解析模板文件(支持布局继承)
func (m *ThemeManager) parseTemplates(themePath string) (map[string]*template.Template, error) {
templates := make(map[string]*template.Template)
@@ -112,25 +147,37 @@ func (m *ThemeManager) parseTemplates(themePath string) (map[string]*template.Te
// 从主题目录加载模板
tplDir := filepath.Join(themePath, "templates")
err := filepath.WalkDir(tplDir, func(path string, d fs.DirEntry, err error) error {
if err != nil || d.IsDir() || !strings.HasSuffix(path, ".html") {
return nil
if err != nil {
return err // 返回错误以停止 WalkDir
}
if d.IsDir() {
return nil // 跳过目录
}
// 支持 .html 和 .tmpl 扩展名
lowerPath := strings.ToLower(path)
if !strings.HasSuffix(lowerPath, ".html") && !strings.HasSuffix(lowerPath, ".tmpl") {
return nil // 非模板文件,跳过
}
// 读取模板内容
content, err := os.ReadFile(path)
content, err := readFile(path)
if err != nil {
return err
return fmt.Errorf("failed to read template file %s: %w", path, err)
}
// 生成模板名称(相对路径)
// 生成模板名称(相对路径,不含扩展名
name := strings.TrimPrefix(path, tplDir+string(filepath.Separator))
name = strings.TrimSuffix(name, filepath.Ext(name))
// 克隆基础模板并解析
tpl := template.Must(baseTpl.Clone())
tpl, err := baseTpl.Clone()
if err != nil {
return fmt.Errorf("failed to clone base template for %s: %w", name, err)
}
tpl, err = tpl.Parse(string(content))
if err != nil {
return fmt.Errorf("parse error in %s: %w", name, err)
return fmt.Errorf("parse error in %s (file: %s): %w", name, path, err)
}
templates[name] = tpl
@@ -142,12 +189,17 @@ func (m *ThemeManager) parseTemplates(themePath string) (map[string]*template.Te
}
if len(templates) == 0 {
return nil, errors.New("no valid templates found")
return nil, ErrNoValidTemplates
}
return templates, nil
}
// 读取文件内容
func readFile(path string) ([]byte, error) {
return os.ReadFile(path)
}
// 获取可用主题列表
func (m *ThemeManager) ListThemes() ([]string, error) {
dirs, err := os.ReadDir(m.themesDir)

View File

@@ -21,14 +21,25 @@ func RenderTemplate(c *gin.Context, tmpl string, data gin.H) {
}
// SwitchTheme handles POST requests to switch the current theme
// SwitchTheme 处理主题切换请求(修正后)
func SwitchTheme(c *gin.Context) {
newTheme := c.PostForm("theme")
manager := &ThemeManager{} //themes.GetManager()
// 从上下文中获取主题管理器(通过 main.go 的 themeMiddleware 注入)
tm, exists := c.Get("ThemeManager")
if !exists {
c.JSON(http.StatusInternalServerError, gin.H{"error": "主题管理器未找到"})
return
}
themeManager, ok := tm.(*themes.ThemeManager)
if !ok {
c.JSON(http.StatusInternalServerError, gin.H{"error": "主题管理器类型错误"})
return
}
if err := manager.LoadTheme(newTheme); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
} else {
config.SetCurrentTheme(newTheme) // Update the configuration
c.JSON(http.StatusOK, gin.H{"status": "success"})
newTheme := c.PostForm("theme")
if err := themeManager.LoadTheme(newTheme); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "加载主题失败: " + err.Error()})
return
}
config.SetCurrentTheme(newTheme) // 持久化主题配置(需确保 config 包支持)
c.JSON(http.StatusOK, gin.H{"status": "主题切换成功"})
}

View File

@@ -0,0 +1,64 @@
<!DOCTYPE html>
<html>
<head>
<title>Page 1</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style type="text/css">
.container {
width: 1170px;
margin: 0 auto;
}
.post-list {
width: 75%;
float: left;
display: block;
}
.sidebar {
float: left;
width: 25%;
}
.header {}
</style>
</head>
<body>
<div class="container">
<div class="layui-col-md9 layui-col-lg9">
<div class="title-article">
<h1>{{.Title}}</h1>
<div class="title-msg">
<span><i class="layui-icon">&#xe612;</i> {{.AuthorID}} </span>
<span><i class="layui-icon">&#xe60e;</i> {{.Created}}</span>
<span><i class="layui-icon">&#xe63a;</i> {{.CommentsNum}}</span>
</div>
</div>
<div class="text" itemprop="articleBody">
{{.Text}}
</div>
<div class="tags-text">
<i class="layui-icon">&#xe66e;</i>标签:{{.Type}}
</div>
<div class="copy-text">
<div>
<p>非特殊说明,本博所有文章均为博主原创。</p>
<p class="hidden-xs">如若转载,请注明出处: </p>
</div>
</div>
<div class="page-text">
<div>
<span class="layui-badge layui-bg-gray">上一篇</span>
</div>
<div>
<span class="layui-badge layui-bg-gray">下一篇</span>
</div>
</div>
<div class="comment-text layui-form">
</div>
</div>
</div>
</body>
</html>