package handlers import ( "net/http" "os" "path/filepath" "goblog/database" "goblog/models" "goblog/utils" "github.com/gin-gonic/gin" ) // 获取可用主题列表 func GetThemes(c *gin.Context) { themes := []string{} // 读取 templates 目录 entries, err := os.ReadDir("templates") if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "error": "无法读取主题目录", }) return } for _, entry := range entries { if entry.IsDir() { // 检查是否是有效主题(包含 base.html) basePath := filepath.Join("templates", entry.Name(), "base.html") if _, err := os.Stat(basePath); err == nil { themes = append(themes, entry.Name()) } } } c.JSON(http.StatusOK, gin.H{ "themes": themes, }) } // 获取当前主题 func GetCurrentTheme(c *gin.Context) { currentTheme := "default" // 使用新的 Option 查询方法 theme, err := models.GetOptionValue(database.DB, "theme", 0) if err == nil && theme != "" { currentTheme = theme } c.JSON(http.StatusOK, gin.H{ "theme": currentTheme, }) } // 切换主题 func SwitchTheme(c *gin.Context) { type Request struct { Theme string `json:"theme" binding:"required"` } var req Request if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "error": "请提供主题名称", }) return } // 验证主题是否存在 themePath := filepath.Join("templates", req.Theme) if _, err := os.Stat(themePath); os.IsNotExist(err) { c.JSON(http.StatusBadRequest, gin.H{ "error": "主题不存在", }) return } // 验证主题是否有效 basePath := filepath.Join(themePath, "base.html") if _, err := os.Stat(basePath); os.IsNotExist(err) { c.JSON(http.StatusBadRequest, gin.H{ "error": "主题文件不完整", }) return } // 更新数据库中的主题设置 err := models.SetOptionValue(database.DB, "theme", 0, req.Theme) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "保存主题设置失败"}) return } // 重新加载模板(立即生效) renderer := utils.GetGlobalRenderer() if renderer != nil { renderer.Reload(req.Theme) } c.JSON(http.StatusOK, gin.H{ "message": "主题切换成功", "theme": req.Theme, }) } // 创建新主题 func CreateTheme(c *gin.Context) { type Request struct { Name string `json:"name" binding:"required"` Template string `json:"template"` CreateCSS bool `json:"create_css"` } var req Request if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "error": "请提供主题名称", }) return } // 验证主题名称(只能包含字母、数字和下划线) validName := true for _, ch := range req.Name { if !((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '_') { validName = false break } } if !validName || req.Name == "" { c.JSON(http.StatusBadRequest, gin.H{ "error": "主题名称只能包含字母、数字和下划线", }) return } // 检查主题是否已存在 themePath := filepath.Join("templates", req.Name) if _, err := os.Stat(themePath); err == nil { c.JSON(http.StatusBadRequest, gin.H{ "error": "主题已存在", }) return } // 创建主题目录 if err := os.MkdirAll(themePath, 0700); err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "error": "创建主题目录失败", }) return } // 如果指定了模板,复制模板文件 if req.Template != "" && req.Template != "blank" { templateSrc := filepath.Join("templates", req.Template) if _, err := os.Stat(templateSrc); err == nil { // 复制模板文件 files := []string{"base.html", "index.html", "post.html", "page.html"} for _, file := range files { srcFile := filepath.Join(templateSrc, file) dstFile := filepath.Join(themePath, file) if data, err := os.ReadFile(srcFile); err == nil { os.WriteFile(dstFile, data, 0644) } } } } else { // 创建空白模板 baseHTML := ` {{.Title}} - {{.SiteName}}

{{.SiteName}}

{{.SiteDesc}}

{{template "content" .}}
` os.WriteFile(filepath.Join(themePath, "base.html"), []byte(baseHTML), 0644) os.WriteFile(filepath.Join(themePath, "index.html"), []byte(`{{define "content"}}

欢迎来到我的博客

这是一个新主题

{{end}} `), 0644) os.WriteFile(filepath.Join(themePath, "post.html"), []byte(`{{define "content"}}

{{.Post.Title}}

{{.Post.Content | html}}
{{end}} `), 0644) os.WriteFile(filepath.Join(themePath, "page.html"), []byte(`{{define "content"}}

{{.Page.Title}}

{{.Page.Content | html}}
{{end}} `), 0644) } // 创建 CSS 文件 if req.CreateCSS { cssPath := filepath.Join("static", "css", req.Name+".css") os.MkdirAll(filepath.Dir(cssPath), 0755) cssContent := `/* ` + req.Name + ` Theme */ /* 在此添加你的样式 */ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; margin: 0; padding: 0; } .container { max-width: 1200px; margin: 0 auto; padding: 0 20px; } .header { background: #fff; padding: 20px 0; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .site-title { margin: 0; } .site-title a { color: #333; text-decoration: none; } .site-desc { color: #666; margin: 10px 0 0; } .main-nav { margin-top: 15px; } .main-nav a { margin-right: 15px; color: #666; text-decoration: none; } .main { padding: 30px 0; min-height: 500px; } .footer { background: #f5f5f5; padding: 20px 0; text-align: center; color: #999; } ` os.WriteFile(cssPath, []byte(cssContent), 0644) } c.JSON(http.StatusOK, gin.H{ "message": "主题创建成功", "theme": req.Name, }) } // 获取主题文件列表 func GetThemeFiles(c *gin.Context) { themeName := c.Param("theme") themePath := filepath.Join("templates", themeName) if _, err := os.Stat(themePath); os.IsNotExist(err) { c.JSON(http.StatusBadRequest, gin.H{ "error": "主题不存在", }) return } files := []string{} // 读取模板文件 entries, err := os.ReadDir(themePath) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "error": "读取文件列表失败", }) return } for _, entry := range entries { if !entry.IsDir() { ext := filepath.Ext(entry.Name()) if ext == ".html" || ext == ".css" || ext == ".js" { files = append(files, entry.Name()) } } } // 检查是否有 CSS 文件 cssPath := filepath.Join("static", "css", themeName+".css") if _, err := os.Stat(cssPath); err == nil { files = append(files, themeName+".css") } c.JSON(http.StatusOK, gin.H{ "files": files, }) } // 获取主题文件内容 func GetThemeFile(c *gin.Context) { themeName := c.Param("theme") filename := c.Param("file") var filePath string // 判断是模板文件还是 CSS 文件 if filename == themeName+".css" { filePath = filepath.Join("static", "css", filename) } else { filePath = filepath.Join("templates", themeName, filename) } if _, err := os.Stat(filePath); os.IsNotExist(err) { c.JSON(http.StatusBadRequest, gin.H{ "error": "文件不存在", }) return } content, err := os.ReadFile(filePath) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "error": "读取文件失败", }) return } c.JSON(http.StatusOK, gin.H{ "content": string(content), }) } // 保存主题文件 func SaveThemeFile(c *gin.Context) { themeName := c.Param("theme") filename := c.Param("file") type Request struct { Content string `json:"content"` } var req Request if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "error": "请提供文件内容", }) return } var filePath string // 判断是模板文件还是 CSS 文件 if filename == themeName+".css" { filePath = filepath.Join("static", "css", filename) } else { filePath = filepath.Join("templates", themeName, filename) } // 保存文件 if err := os.WriteFile(filePath, []byte(req.Content), 0600); err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "error": "保存文件失败", }) return } // 如果是模板文件且是当前使用的主题,重新加载 if filename != themeName+".css" { currentTheme, err := models.GetOptionValue(database.DB, "theme", 0) if err == nil && currentTheme == themeName { renderer := utils.GetGlobalRenderer() if renderer != nil { renderer.Reload(themeName) } } } c.JSON(http.StatusOK, gin.H{ "message": "文件保存成功", }) }