feat: 品牌CMS配置化 + cookie_secure配置

- config.yaml 新增 server.cookie_secure 配置项
- site_settings 新增 site_name/site_tagline/contact_email 站点信息
- SiteInfo 结构体扩展,支持运行时从 DB 读取站点品牌信息
- cookie.go 使用 cfg.Server.CookieSecure 替代 Mode!=debug 判断
- SetSessionCookie/ClearSessionCookie 支持 siteSettings 运行时覆盖
- 模板替换硬编码 MetaLab → {{.SiteName}}(nav/header/footer/home/login/register/guidelines/admin,12+处)
- 管理后台站点设置页新增站点名称/标语/联系邮箱输入项
- 页脚版权年份改为动态 CurrentYear
- 注册成功消息 CMS 化(后端+前端)
This commit is contained in:
2026-06-21 23:12:18 +08:00
parent 7b5856e0ba
commit 0014bdb9ff
21 changed files with 106 additions and 35 deletions

View File

@ -4,6 +4,7 @@
server: server:
port: 8080 port: 8080
mode: debug # debug | release | test mode: debug # debug | release | test
cookie_secure: false # Cookie Secure 标志(生产环境应设为 true部分反向代理环境下可能需要灵活控制
database: database:
host: 127.0.0.1 host: 127.0.0.1

View File

@ -17,8 +17,8 @@ const (
// SetSessionCookie 写入会话 ID Cookie // SetSessionCookie 写入会话 ID Cookie
// rememberMe=true → Cookie maxAge=30天关浏览器后仍保持登录 // rememberMe=true → Cookie maxAge=30天关浏览器后仍保持登录
// rememberMe=false → Cookie maxAge=空闲超时默认2h与Redis TTL一致关浏览器后自动清除 // rememberMe=false → Cookie maxAge=空闲超时默认2h与Redis TTL一致关浏览器后自动清除
func SetSessionCookie(c *gin.Context, sid string, rememberMe bool, cfg *config.Config) { func SetSessionCookie(c *gin.Context, sid string, rememberMe bool, cfg *config.Config, siteSettings *config.SiteSettings) {
secure := cfg.Server.Mode != "debug" secure := siteSettings.CookieSecure()
c.SetSameSite(http.SameSiteLaxMode) c.SetSameSite(http.SameSiteLaxMode)
var maxAge int var maxAge int
@ -33,8 +33,8 @@ func SetSessionCookie(c *gin.Context, sid string, rememberMe bool, cfg *config.C
} }
// ClearSessionCookie 清除会话 ID Cookielogout 调用) // ClearSessionCookie 清除会话 ID Cookielogout 调用)
func ClearSessionCookie(c *gin.Context, cfg *config.Config) { func ClearSessionCookie(c *gin.Context, cfg *config.Config, siteSettings *config.SiteSettings) {
secure := cfg.Server.Mode != "debug" secure := siteSettings.CookieSecure()
c.SetCookie(SessionCookieName, "", -1, "/", "", secure, true) c.SetCookie(SessionCookieName, "", -1, "/", "", secure, true)
} }

View File

@ -11,6 +11,7 @@ import (
"path/filepath" "path/filepath"
"regexp" "regexp"
"strconv" "strconv"
"time"
"metazone.cc/mce/internal/model" "metazone.cc/mce/internal/model"
@ -159,10 +160,20 @@ func RedirectToLogin(c *gin.Context) {
// injectSiteInfo 从 context 注入站点展示信息 // injectSiteInfo 从 context 注入站点展示信息
func injectSiteInfo(c *gin.Context, data gin.H) { func injectSiteInfo(c *gin.Context, data gin.H) {
if siteName, exists := c.Get("site_name"); exists {
data["SiteName"] = siteName
}
if siteTagline, exists := c.Get("site_tagline"); exists {
data["SiteTagline"] = siteTagline
}
if contactEmail, exists := c.Get("site_contact_email"); exists {
data["ContactEmail"] = contactEmail
}
if framework, exists := c.Get("site_framework"); exists { if framework, exists := c.Get("site_framework"); exists {
data["Framework"] = framework data["Framework"] = framework
} }
if copyrightStart, exists := c.Get("site_copyright_start"); exists { if copyrightStart, exists := c.Get("site_copyright_start"); exists {
data["CopyrightStart"] = copyrightStart data["CopyrightStart"] = copyrightStart
} }
data["CurrentYear"] = time.Now().Year()
} }

View File

@ -29,8 +29,9 @@ type AuditConfig struct {
} }
type ServerConfig struct { type ServerConfig struct {
Port string `mapstructure:"port"` Port string `mapstructure:"port"`
Mode string `mapstructure:"mode"` Mode string `mapstructure:"mode"`
CookieSecure bool `mapstructure:"cookie_secure"`
} }
type DatabaseConfig struct { type DatabaseConfig struct {

View File

@ -112,18 +112,29 @@ func (s *SiteSettings) IsAuditTypeEnabled(auditType string, defaultVal bool) boo
// SiteInfoDefaults 站点展示信息的默认值 // SiteInfoDefaults 站点展示信息的默认值
type SiteInfoDefaults struct { type SiteInfoDefaults struct {
Framework string // 附加标识,默认 "Framework: MetaZone" SiteName string // 站点名称,默认 "MetaLab"
SiteTagline string // 站点标语,默认 "下一代开发者社区"
ContactEmail string // 联系/申诉邮箱,默认 "metazone@foxmail.com"
Framework string // 附加标识,默认 "Framework: MetaGenesis"
CopyrightStart string // 版权起始年份,默认 "2024" CopyrightStart string // 版权起始年份,默认 "2024"
} }
// SiteInfo 获取站点展示信息(优先 DB 设置fallback 默认值) // SiteInfo 获取站点展示信息(优先 DB 设置fallback 默认值)
func (s *SiteSettings) SiteInfo() SiteInfoDefaults { func (s *SiteSettings) SiteInfo() SiteInfoDefaults {
return SiteInfoDefaults{ return SiteInfoDefaults{
Framework: s.Get("site.framework", "Framework: MetaZone"), SiteName: s.Get("site.name", "MetaLab"),
SiteTagline: s.Get("site.tagline", "下一代开发者社区"),
ContactEmail: s.Get("site.contact_email", "metazone@foxmail.com"),
Framework: s.Get("site.framework", "Framework: MetaGenesis"),
CopyrightStart: s.Get("site.copyright_start", "2024"), CopyrightStart: s.Get("site.copyright_start", "2024"),
} }
} }
// CookieSecure 返回 cookie_secure 配置值优先站点设置fallback config.yaml
func (s *SiteSettings) CookieSecure() bool {
return s.GetBool("site.cookie_secure", App.Server.CookieSecure)
}
// IsRegistrationEnabled 是否允许新用户注册 // IsRegistrationEnabled 是否允许新用户注册
func (s *SiteSettings) IsRegistrationEnabled() bool { func (s *SiteSettings) IsRegistrationEnabled() bool {
return s.GetBool("registration.enabled", true) return s.GetBool("registration.enabled", true)

View File

@ -78,7 +78,7 @@ func (ac *AuthController) Login(c *gin.Context) {
return return
} }
common.SetSessionCookie(c, sid, req.RememberMe, ac.cfg) common.SetSessionCookie(c, sid, req.RememberMe, ac.cfg, ac.siteSettings)
common.OkWithMessage(c, user, "登录成功") common.OkWithMessage(c, user, "登录成功")
} }
@ -108,6 +108,6 @@ func (ac *AuthController) ConfirmRestore(c *gin.Context) {
return return
} }
common.SetSessionCookie(c, sid, req.RememberMe, ac.cfg) common.SetSessionCookie(c, sid, req.RememberMe, ac.cfg, ac.siteSettings)
common.OkWithMessage(c, user, "注销已撤销,欢迎回来") common.OkWithMessage(c, user, "注销已撤销,欢迎回来")
} }

View File

@ -69,9 +69,9 @@ func (ac *AuthController) Register(c *gin.Context) {
return return
} }
common.SetSessionCookie(c, sid, req.RememberMe, ac.cfg) common.SetSessionCookie(c, sid, req.RememberMe, ac.cfg, ac.siteSettings)
common.OkWithMessage(c, user, "注册成功!欢迎加入 MetaLab") common.OkWithMessage(c, user, "注册成功!欢迎加入 "+ac.siteSettings.SiteInfo().SiteName)
} }
// Logout 退出登录:删除服务端 session + 清除 Cookie // Logout 退出登录:删除服务端 session + 清除 Cookie
@ -79,7 +79,7 @@ func (ac *AuthController) Logout(c *gin.Context) {
if sid, err := c.Cookie(common.SessionCookieName); err == nil && sid != "" { if sid, err := c.Cookie(common.SessionCookieName); err == nil && sid != "" {
_ = ac.sessionManager.Destroy(sid) _ = ac.sessionManager.Destroy(sid)
} }
common.ClearSessionCookie(c, ac.cfg) common.ClearSessionCookie(c, ac.cfg, ac.siteSettings)
common.OkMessage(c, "已退出登录") common.OkMessage(c, "已退出登录")
} }
@ -93,7 +93,7 @@ func (ac *AuthController) RefreshToken(c *gin.Context) {
s, err := ac.sessionManager.Validate(sid) s, err := ac.sessionManager.Validate(sid)
if err != nil || s == nil { if err != nil || s == nil {
common.ClearSessionCookie(c, ac.cfg) common.ClearSessionCookie(c, ac.cfg, ac.siteSettings)
common.Error(c, http.StatusUnauthorized, "登录凭证已失效,请重新登录") common.Error(c, http.StatusUnauthorized, "登录凭证已失效,请重新登录")
return return
} }

View File

@ -3,6 +3,7 @@ package controller
import ( import (
"html/template" "html/template"
"net/http" "net/http"
"strings"
"metazone.cc/mce/internal/common" "metazone.cc/mce/internal/common"
"metazone.cc/mce/internal/config" "metazone.cc/mce/internal/config"
@ -45,6 +46,12 @@ func (ac *AuthController) RegisterPage(c *gin.Context) {
} }
guidelines = content guidelines = content
} }
// 替换准则中的硬编码站点名和邮箱
siteInfo := ac.siteSettings.SiteInfo()
guidelines = template.HTML(strings.ReplaceAll(
strings.ReplaceAll(string(guidelines), "MetaLab", siteInfo.SiteName),
"metazone@foxmail.com", siteInfo.ContactEmail,
))
c.HTML(http.StatusOK, "auth/register.html", common.BuildPageData(c, gin.H{ c.HTML(http.StatusOK, "auth/register.html", common.BuildPageData(c, gin.H{
"Title": "注册", "Title": "注册",
"ExtraCSS": "/static/css/auth.css", "ExtraCSS": "/static/css/auth.css",

View File

@ -22,6 +22,7 @@ type AuthMiddleware struct {
cfg *config.Config cfg *config.Config
sessionManager *session.Manager sessionManager *session.Manager
levelSvc autoCheckIner levelSvc autoCheckIner
siteSettings *config.SiteSettings
} }
// NewAuthMiddleware 构造函数 // NewAuthMiddleware 构造函数
@ -29,6 +30,12 @@ func NewAuthMiddleware(cfg *config.Config, sm *session.Manager) *AuthMiddleware
return &AuthMiddleware{cfg: cfg, sessionManager: sm} return &AuthMiddleware{cfg: cfg, sessionManager: sm}
} }
// WithSiteSettings 链式注入站点设置(用于 cookie_secure 运行时覆盖)
func (am *AuthMiddleware) WithSiteSettings(ss *config.SiteSettings) *AuthMiddleware {
am.siteSettings = ss
return am
}
// WithLevelService 链式注入等级服务(用于自动签到) // WithLevelService 链式注入等级服务(用于自动签到)
func (am *AuthMiddleware) WithLevelService(svc autoCheckIner) *AuthMiddleware { func (am *AuthMiddleware) WithLevelService(svc autoCheckIner) *AuthMiddleware {
am.levelSvc = svc am.levelSvc = svc
@ -60,7 +67,7 @@ func (am *AuthMiddleware) Required() gin.HandlerFunc {
// abortUnauthorized 统一处理未认证API 返回 JSON页面请求跳转登录 // abortUnauthorized 统一处理未认证API 返回 JSON页面请求跳转登录
func (am *AuthMiddleware) abortUnauthorized(c *gin.Context) { func (am *AuthMiddleware) abortUnauthorized(c *gin.Context) {
common.ClearSessionCookie(c, am.cfg) common.ClearSessionCookie(c, am.cfg, am.siteSettings)
if strings.HasPrefix(c.Request.URL.Path, "/api/") { if strings.HasPrefix(c.Request.URL.Path, "/api/") {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{ c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"success": false, "message": "登录已过期,请重新登录", "success": false, "message": "登录已过期,请重新登录",
@ -81,7 +88,7 @@ func (am *AuthMiddleware) Optional() gin.HandlerFunc {
s, err := am.sessionManager.Validate(sid) s, err := am.sessionManager.Validate(sid)
if err != nil || s == nil { if err != nil || s == nil {
common.ClearSessionCookie(c, am.cfg) common.ClearSessionCookie(c, am.cfg, am.siteSettings)
c.Next() c.Next()
return return
} }

View File

@ -14,7 +14,7 @@ func (am *AuthMiddleware) AdminAuth() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
sid, err := c.Cookie(common.SessionCookieName) sid, err := c.Cookie(common.SessionCookieName)
if err != nil || sid == "" { if err != nil || sid == "" {
common.ClearSessionCookie(c, am.cfg) common.ClearSessionCookie(c, am.cfg, am.siteSettings)
c.Redirect(http.StatusFound, "/") c.Redirect(http.StatusFound, "/")
c.Abort() c.Abort()
return return
@ -23,7 +23,7 @@ func (am *AuthMiddleware) AdminAuth() gin.HandlerFunc {
s, err := am.sessionManager.Validate(sid) s, err := am.sessionManager.Validate(sid)
if err != nil || s == nil { if err != nil || s == nil {
log.Printf("[AdminAuth] session invalid path=%s → 302", c.Request.URL.Path) log.Printf("[AdminAuth] session invalid path=%s → 302", c.Request.URL.Path)
common.ClearSessionCookie(c, am.cfg) common.ClearSessionCookie(c, am.cfg, am.siteSettings)
c.Redirect(http.StatusFound, "/") c.Redirect(http.StatusFound, "/")
c.Abort() c.Abort()
return return

View File

@ -11,6 +11,9 @@ import (
func InjectSiteInfo(siteSettings *config.SiteSettings) gin.HandlerFunc { func InjectSiteInfo(siteSettings *config.SiteSettings) gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
info := siteSettings.SiteInfo() info := siteSettings.SiteInfo()
c.Set("site_name", info.SiteName)
c.Set("site_tagline", info.SiteTagline)
c.Set("site_contact_email", info.ContactEmail)
c.Set("site_framework", info.Framework) c.Set("site_framework", info.Framework)
c.Set("site_copyright_start", info.CopyrightStart) c.Set("site_copyright_start", info.CopyrightStart)
c.Set("is_maintenance", siteSettings.IsMaintenanceEnabled()) c.Set("is_maintenance", siteSettings.IsMaintenanceEnabled())

View File

@ -80,7 +80,7 @@ func buildDepsCore(db *gorm.DB, cfg *config.Config, siteSettings *config.SiteSet
rateLimiter := middleware.NewRateLimiter() rateLimiter := middleware.NewRateLimiter()
authCtrl := controller.NewAuthController(authService, sessionMgr, rateLimiter, cfg, siteSettings) authCtrl := controller.NewAuthController(authService, sessionMgr, rateLimiter, cfg, siteSettings)
avatarSvc := service.NewAvatarService(userRepo) avatarSvc := service.NewAvatarService(userRepo)
authMdw := middleware.NewAuthMiddleware(cfg, sessionMgr) authMdw := middleware.NewAuthMiddleware(cfg, sessionMgr).WithSiteSettings(siteSettings)
return userRepo, sessionMgr, authService, rateLimiter, authCtrl, avatarSvc, authMdw return userRepo, sessionMgr, authService, rateLimiter, authCtrl, avatarSvc, authMdw
} }

View File

@ -5,7 +5,7 @@
<main class="auth-page"> <main class="auth-page">
<div class="auth-card"> <div class="auth-card">
<h2>登录 MetaLab</h2> <h2>登录 {{.SiteName}}</h2>
<p class="auth-subtitle">回到开发者社区</p> <p class="auth-subtitle">回到开发者社区</p>
<form id="loginForm" novalidate> <form id="loginForm" novalidate>
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}"> <input type="hidden" name="csrf_token" value="{{.CSRFToken}}">

View File

@ -13,7 +13,7 @@
</div> </div>
{{else if .RegistrationEnabled}} {{else if .RegistrationEnabled}}
<h2>创建账号</h2> <h2>创建账号</h2>
<p class="auth-subtitle">加入 MetaLab 开发者社区</p> <p class="auth-subtitle">加入 {{.SiteName}} 开发者社区</p>
<form id="registerForm" novalidate> <form id="registerForm" novalidate>
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}"> <input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
<div class="form-group"> <div class="form-group">
@ -38,7 +38,7 @@
</label> </label>
<span class="remember-hint">非共享设备推荐</span> <span class="remember-hint">非共享设备推荐</span>
</div> </div>
<p class="guidelines-link">点击注册即表示同意 <a href="#" id="openGuidelines">MetaLab 社区用户准则》</a></p> <p class="guidelines-link">点击注册即表示同意 <a href="#" id="openGuidelines">{{.SiteName}} 社区用户准则》</a></p>
<button type="submit" class="submit-btn" id="submitBtn">注 册</button> <button type="submit" class="submit-btn" id="submitBtn">注 册</button>
</form> </form>
{{else}} {{else}}
@ -59,7 +59,7 @@
<div class="modal-overlay" id="modalOverlay"> <div class="modal-overlay" id="modalOverlay">
<div class="modal"> <div class="modal">
<div class="modal-header"> <div class="modal-header">
<h3>MetaLab 社区用户准则</h3> <h3>{{.SiteName}} 社区用户准则</h3>
<button class="modal-close" id="modalClose">&times;</button> <button class="modal-close" id="modalClose">&times;</button>
</div> </div>
<div class="modal-body" id="modalBody"> <div class="modal-body" id="modalBody">

View File

@ -7,8 +7,8 @@
<section class="hero-banner"> <section class="hero-banner">
<div class="hero-bg"></div> <div class="hero-bg"></div>
<div class="hero-content"> <div class="hero-content">
<h1 class="hero-title">Meta<span class="hero-accent">Lab</span></h1> <h1 class="hero-title">{{.SiteName}}</h1>
<p class="hero-tagline">下一代开发者社区</p> <p class="hero-tagline">{{.SiteTagline}}</p>
{{if .IsLoggedIn}} {{if .IsLoggedIn}}
<p class="hero-welcome">欢迎回来,{{.Username}} 👋</p> <p class="hero-welcome">欢迎回来,{{.Username}} 👋</p>
{{else}} {{else}}
@ -69,7 +69,7 @@
<div class="user-avatar-fallback"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg></div> <div class="user-avatar-fallback"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg></div>
{{end}} {{end}}
<h3 class="user-card-name">{{.Username}}</h3> <h3 class="user-card-name">{{.Username}}</h3>
<p class="user-card-desc">欢迎加入 MetaLab</p> <p class="user-card-desc">欢迎加入 {{.SiteName}}</p>
<a href="/space" class="user-card-link">我的空间</a> <a href="/space" class="user-card-link">我的空间</a>
{{else}} {{else}}
<div class="user-avatar-fallback guest-avatar"> <div class="user-avatar-fallback guest-avatar">

View File

@ -1,6 +1,6 @@
<footer> <footer>
<div class="container footer-content"> <div class="container footer-content">
<p class="copyright">&copy; {{.CopyrightStart}}-2026 MetaLab{{if .Framework}} | {{.Framework}}{{end}}{{if .CanAccessAdmin}} | <a href="/admin">管理面板</a>{{end}}</p> <p class="copyright">&copy; {{.CopyrightStart}}-{{.CurrentYear}} {{.SiteName}}{{if .Framework}} | {{.Framework}}{{end}}{{if .CanAccessAdmin}} | <a href="/admin">管理面板</a>{{end}}</p>
</div> </div>
</footer> </footer>
<script src="/static/js/common.js?v={{assetV "/static/js/common.js"}}" nonce="{{.CSPNonce}}"></script> <script src="/static/js/common.js?v={{assetV "/static/js/common.js"}}" nonce="{{.CSPNonce}}"></script>

View File

@ -7,12 +7,12 @@
{{if .OgTitle}} {{if .OgTitle}}
<meta property="og:title" content="{{.OgTitle}}"> <meta property="og:title" content="{{.OgTitle}}">
<meta property="og:type" content="article"> <meta property="og:type" content="article">
<meta property="og:site_name" content="MetaLab"> <meta property="og:site_name" content="{{.SiteName}}">
{{if .OgDescription}}<meta property="og:description" content="{{.OgDescription}}">{{end}} {{if .OgDescription}}<meta property="og:description" content="{{.OgDescription}}">{{end}}
{{if .OgImage}}<meta property="og:image" content="{{.OgImage}}">{{end}} {{if .OgImage}}<meta property="og:image" content="{{.OgImage}}">{{end}}
{{if .OgURL}}<meta property="og:url" content="{{.OgURL}}">{{end}} {{if .OgURL}}<meta property="og:url" content="{{.OgURL}}">{{end}}
{{end}} {{end}}
<title>{{.Title}} - MetaLab</title> <title>{{.Title}} - {{.SiteName}}</title>
<link rel="stylesheet" href="/static/css/common.css?v={{assetV "/static/css/common.css"}}"> <link rel="stylesheet" href="/static/css/common.css?v={{assetV "/static/css/common.css"}}">
{{if .ExtraCSS}} {{if .ExtraCSS}}
<link rel="stylesheet" href="{{.ExtraCSS}}?v={{assetV .ExtraCSS}}"> <link rel="stylesheet" href="{{.ExtraCSS}}?v={{assetV .ExtraCSS}}">

View File

@ -1,6 +1,6 @@
<header> <header>
<div class="container nav-container"> <div class="container nav-container">
<a href="/" class="logo">Meta<span>Lab</span></a> <a href="/" class="logo">{{.SiteName}}</a>
<button class="mobile-menu-btn" id="mobileMenuBtn"> <button class="mobile-menu-btn" id="mobileMenuBtn">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg> <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
</button> </button>

View File

@ -290,7 +290,7 @@
try { resp = JSON.parse(xhr.responseText); } catch (err) { resp = null; } try { resp = JSON.parse(xhr.responseText); } catch (err) { resp = null; }
if (xhr.status === 200 && resp && resp.success) { if (xhr.status === 200 && resp && resp.success) {
window.MetaLab.toast(toast, '🎉 注册成功!欢迎加入 MetaLab', false); window.MetaLab.toast(toast, resp.message || '🎉 注册成功!', false);
// Token 已通过 HttpOnly Cookie 自动设置JS 无需处理 // Token 已通过 HttpOnly Cookie 自动设置JS 无需处理
// 注册即登录,跳转首页 // 注册即登录,跳转首页
setTimeout(function () { setTimeout(function () {

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
{{if .CSRFToken}}<meta name="csrf-token" content="{{.CSRFToken}}">{{end}} {{if .CSRFToken}}<meta name="csrf-token" content="{{.CSRFToken}}">{{end}}
<title>{{.Title}} - MetaLab 管理面板</title> <title>{{.Title}} - {{.SiteName}} 管理面板</title>
<link rel="stylesheet" href="/admin/static/css/common.css?v={{assetV "/admin/static/css/common.css"}}"> <link rel="stylesheet" href="/admin/static/css/common.css?v={{assetV "/admin/static/css/common.css"}}">
{{if .ExtraCSS}} {{if .ExtraCSS}}
<link rel="stylesheet" href="{{.ExtraCSS}}"> <link rel="stylesheet" href="{{.ExtraCSS}}">
@ -13,7 +13,7 @@
<body> <body>
<header class="admin-header"> <header class="admin-header">
<div class="admin-header-inner"> <div class="admin-header-inner">
<a href="/admin" class="admin-logo">MetaLab <span>管理面板</span></a> <a href="/admin" class="admin-logo">{{.SiteName}} <span>管理面板</span></a>
<div class="admin-header-right"> <div class="admin-header-right">
<span class="admin-user">{{.Username}}</span> <span class="admin-user">{{.Username}}</span>
<a href="/" class="admin-back-btn"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>返回前台</a> <a href="/" class="admin-back-btn"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>返回前台</a>

View File

@ -10,8 +10,38 @@
<div class="section-body"> <div class="section-body">
<div class="setting-item"> <div class="setting-item">
<div class="setting-info"> <div class="setting-info">
<span class="setting-label">版权信息</span> <span class="setting-label">站点名称</span>
<span class="setting-desc">页脚附加信息,留空则隐藏(如 "Framework: MetaZone"</span> <span class="setting-desc">全站品牌名(导航栏、标题、页脚等位置</span>
</div>
<div class="setting-input-wrap">
<input type="text" id="setting-site-name" class="setting-input" placeholder="MetaLab" value="{{.SiteName}}">
<button class="btn btn-sm btn-primary btn-save" data-key="site.name" data-input="setting-site-name">保存</button>
</div>
</div>
<div class="setting-item">
<div class="setting-info">
<span class="setting-label">站点标语</span>
<span class="setting-desc">首页 Hero 区域的副标题</span>
</div>
<div class="setting-input-wrap">
<input type="text" id="setting-site-tagline" class="setting-input" placeholder="下一代开发者社区" value="{{.SiteTagline}}">
<button class="btn btn-sm btn-primary btn-save" data-key="site.tagline" data-input="setting-site-tagline">保存</button>
</div>
</div>
<div class="setting-item">
<div class="setting-info">
<span class="setting-label">联系邮箱</span>
<span class="setting-desc">申诉/联系邮箱,显示在社区准则等页面</span>
</div>
<div class="setting-input-wrap">
<input type="text" id="setting-site-email" class="setting-input" placeholder="metazone@foxmail.com" value="{{.ContactEmail}}">
<button class="btn btn-sm btn-primary btn-save" data-key="site.contact_email" data-input="setting-site-email">保存</button>
</div>
</div>
<div class="setting-item">
<div class="setting-info">
<span class="setting-label">附加信息</span>
<span class="setting-desc">页脚附加信息,留空则隐藏(如 "Framework: MetaGenesis"</span>
</div> </div>
<div class="setting-input-wrap"> <div class="setting-input-wrap">
<input type="text" id="setting-site-framework" class="setting-input" placeholder="留空则不显示" value="{{.Framework}}"> <input type="text" id="setting-site-framework" class="setting-input" placeholder="留空则不显示" value="{{.Framework}}">