feat(theme): 管理后台新增模板选择器,支持运行时动态切换主题
Some checks failed
CI / Lint + Build (push) Has been cancelled

新增功能:
- 主题发现模块:扫描 templates/ 目录下含 theme.json 的目录
- 站点设置新增 site.theme 键,DB 持久化 + 内存缓存
- 运行时切换主题:保存后即时重载 Gin 模板 + 静态资源哈希
- 管理后台 '外观主题' 页面:列表展示、选择、一键切换
- 动态静态资源服务:根据当前主题目录提供 CSS/JS

修复:
- account.html 残留孤儿 {{end}} 模板语法错误
- 动态静态资源 handler 误判 Gin *filepath 路径前缀为绝对路径

变更文件(12 modified + 3 new):
- cmd/server/main.go: 动态主题加载 + 运行时重载 + 动态静态服务
- internal/theme/discovery.go: 新增主题扫描
- internal/config/site_settings.go: ThemeName()
- internal/service/site_setting_service.go: ThemeReloader + UpdateTheme
- internal/controller/admin/: ISP 扩展 + GetThemes/UpdateTheme/ThemePage
- internal/controller/auth_controller.go: 准则路径动态化
- internal/router/: InjectThemeReloader + 新路由
- templates/: 外观主题页面 + JS + CSS + 子导航
This commit is contained in:
2026-06-30 17:43:08 +08:00
parent 3d69dae799
commit e7092254ab
14 changed files with 426 additions and 18 deletions

View File

@ -6,6 +6,7 @@ import (
"metazone.cc/mce/internal/common"
"metazone.cc/mce/internal/service"
"metazone.cc/mce/internal/theme"
"github.com/gin-gonic/gin"
)
@ -14,6 +15,7 @@ import (
type SiteSettingController struct {
settingService siteSettingUseCase
defaults *service.AuditDefaults
themeReload service.ThemeReloader // 主题重载回调(切换后实时生效)
}
// NewSiteSettingController 构造函数
@ -21,6 +23,11 @@ func NewSiteSettingController(settingService siteSettingUseCase, defaults *servi
return &SiteSettingController{settingService: settingService, defaults: defaults}
}
// SetThemeReloader 注入主题重载回调main.go 中设置)
func (sc *SiteSettingController) SetThemeReloader(reload service.ThemeReloader) {
sc.themeReload = reload
}
// SiteSettingsPage 旧站点设置页 — 301 重定向到品牌页
func (sc *SiteSettingController) SiteSettingsPage(c *gin.Context) {
c.Redirect(http.StatusMovedPermanently, "/admin/settings/brand")
@ -66,6 +73,16 @@ func (sc *SiteSettingController) ContentPage(c *gin.Context) {
}))
}
// ThemePage 外观主题设置页
func (sc *SiteSettingController) ThemePage(c *gin.Context) {
c.HTML(http.StatusOK, "admin/site-settings/theme.html", common.BuildAdminPageData(c, gin.H{
"Title": "站点设置 - 外观主题",
"ExtraCSS": "/admin/static/css/site-settings.css",
"ExtraJS": "/admin/static/js/site-settings-theme.js",
"ActiveSetting": "theme",
}))
}
// GetSettings 获取所有站点设置
func (sc *SiteSettingController) GetSettings(c *gin.Context) {
all := sc.settingService.GetSettings()
@ -146,3 +163,60 @@ func (sc *SiteSettingController) GetBoolSetting(c *gin.Context) {
common.Ok(c, gin.H{"value": d})
}
}
// GetThemes 获取所有可用主题列表
func (sc *SiteSettingController) GetThemes(c *gin.Context) {
themes, err := theme.DiscoverThemes("templates")
if err != nil {
common.Error(c, http.StatusInternalServerError, "读取主题列表失败")
return
}
if themes == nil {
themes = []theme.ThemeInfo{}
}
// 读取当前主题
all := sc.settingService.GetSettings()
currentTheme := "MetaLab-2026"
if t, ok := all["site.theme"]; ok && t != "" {
currentTheme = t
}
common.Ok(c, gin.H{
"themes": themes,
"current": currentTheme,
})
}
// UpdateTheme 切换主题
func (sc *SiteSettingController) UpdateTheme(c *gin.Context) {
var req struct {
Theme string `json:"theme" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
common.Error(c, http.StatusBadRequest, "参数错误")
return
}
// 验证主题是否存在
themes, err := theme.DiscoverThemes("templates")
if err != nil {
common.Error(c, http.StatusInternalServerError, "读取主题列表失败")
return
}
found := false
for _, t := range themes {
if t.Dir == req.Theme {
found = true
break
}
}
if !found {
common.Error(c, http.StatusBadRequest, "主题不存在")
return
}
if err := sc.settingService.UpdateTheme(req.Theme, sc.themeReload); err != nil {
common.Error(c, http.StatusInternalServerError, "主题切换失败")
return
}
common.OkMessage(c, "主题已切换")
}