feat(admin): 站点信息 + 注册开关可后台设置,模板动态渲染
新增 4 项可配置站点设置(DB持久化+内存缓存,即时生效): - site.brand 品牌名(页面标题/页脚/管理面板,默认 MetaLab) - site.framework 框架名(页脚标识,默认 MetaZone) - site.copyright_start 版权起始年(页脚,默认 2024) - registration.enabled 注册开关(关闭后禁止新用户注册,默认 true) 实现方式: - InjectSiteInfo 中间件:全局注入 Brand/Framework/CopyrightStart 到 gin.Context - BuildPageData 读取 context,所有 SSR 页面自动获取站点信息 - AuthService.Register 最先检查 IsRegistrationEnabled() - 管理后台新增「基本设置」+「注册设置」区块,文本输入手动保存 + Toggle 即时生效
This commit is contained in:
@ -34,4 +34,5 @@ var (
|
|||||||
ErrLoginBeforeDelete = errors.New("请先登录再注销")
|
ErrLoginBeforeDelete = errors.New("请先登录再注销")
|
||||||
ErrNeedsConfirmRestore = errors.New("需要二次确认恢复账号")
|
ErrNeedsConfirmRestore = errors.New("需要二次确认恢复账号")
|
||||||
ErrOwnerCannotDelete = errors.New("站长不可自主注销,请先转让站长权限")
|
ErrOwnerCannotDelete = errors.New("站长不可自主注销,请先转让站长权限")
|
||||||
|
ErrRegistrationDisabled = errors.New("注册功能已关闭")
|
||||||
)
|
)
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
// BuildPageData 构建页面模板数据,自动注入登录状态与 CSRF token
|
// BuildPageData 构建页面模板数据,自动注入登录状态、站点信息与 CSRF token
|
||||||
func BuildPageData(c *gin.Context, extra gin.H) gin.H {
|
func BuildPageData(c *gin.Context, extra gin.H) gin.H {
|
||||||
data := gin.H{}
|
data := gin.H{}
|
||||||
for k, v := range extra {
|
for k, v := range extra {
|
||||||
@ -20,6 +20,8 @@ func BuildPageData(c *gin.Context, extra gin.H) gin.H {
|
|||||||
if token, exists := c.Get("csrf_token"); exists {
|
if token, exists := c.Get("csrf_token"); exists {
|
||||||
data["CSRFToken"] = token
|
data["CSRFToken"] = token
|
||||||
}
|
}
|
||||||
|
// 注入站点信息(由 InjectSiteInfo 中间件设置)
|
||||||
|
injectSiteInfo(c, data)
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,3 +50,16 @@ func BuildAdminPageData(c *gin.Context, extra gin.H) gin.H {
|
|||||||
}
|
}
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// injectSiteInfo 从 context 注入站点展示信息
|
||||||
|
func injectSiteInfo(c *gin.Context, data gin.H) {
|
||||||
|
if brand, exists := c.Get("site_brand"); exists {
|
||||||
|
data["Brand"] = brand
|
||||||
|
}
|
||||||
|
if framework, exists := c.Get("site_framework"); exists {
|
||||||
|
data["Framework"] = framework
|
||||||
|
}
|
||||||
|
if copyrightStart, exists := c.Get("site_copyright_start"); exists {
|
||||||
|
data["CopyrightStart"] = copyrightStart
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -107,3 +107,26 @@ func (s *SiteSettings) IsAuditEnabled() bool {
|
|||||||
func (s *SiteSettings) IsAuditTypeEnabled(auditType string, defaultVal bool) bool {
|
func (s *SiteSettings) IsAuditTypeEnabled(auditType string, defaultVal bool) bool {
|
||||||
return s.GetBool("audit."+auditType, defaultVal)
|
return s.GetBool("audit."+auditType, defaultVal)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- 站点信息设置 ---
|
||||||
|
|
||||||
|
// SiteInfoDefaults 站点展示信息的默认值
|
||||||
|
type SiteInfoDefaults struct {
|
||||||
|
Brand string // 品牌名,默认 "MetaLab"
|
||||||
|
Framework string // 框架名,默认 "MetaZone"
|
||||||
|
CopyrightStart string // 版权起始年份,默认 "2024"
|
||||||
|
}
|
||||||
|
|
||||||
|
// SiteInfo 获取站点展示信息(优先 DB 设置,fallback 默认值)
|
||||||
|
func (s *SiteSettings) SiteInfo() SiteInfoDefaults {
|
||||||
|
return SiteInfoDefaults{
|
||||||
|
Brand: s.Get("site.brand", "MetaLab"),
|
||||||
|
Framework: s.Get("site.framework", "MetaZone"),
|
||||||
|
CopyrightStart: s.Get("site.copyright_start", "2024"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsRegistrationEnabled 是否允许新用户注册
|
||||||
|
func (s *SiteSettings) IsRegistrationEnabled() bool {
|
||||||
|
return s.GetBool("registration.enabled", true)
|
||||||
|
}
|
||||||
|
|||||||
@ -51,6 +51,8 @@ func (ac *AuthController) Register(c *gin.Context) {
|
|||||||
common.Error(c, http.StatusConflict, "该邮箱已注册")
|
common.Error(c, http.StatusConflict, "该邮箱已注册")
|
||||||
case common.ErrWeakPassword:
|
case common.ErrWeakPassword:
|
||||||
common.Error(c, http.StatusBadRequest, err.Error())
|
common.Error(c, http.StatusBadRequest, err.Error())
|
||||||
|
case common.ErrRegistrationDisabled:
|
||||||
|
common.Error(c, http.StatusForbidden, "注册功能已关闭")
|
||||||
default:
|
default:
|
||||||
common.Error(c, http.StatusInternalServerError, "注册失败,请稍后重试")
|
common.Error(c, http.StatusInternalServerError, "注册失败,请稍后重试")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,7 +23,7 @@ func NewAuthController(authService authUseCase, tokenSvc tokenRefresher, limiter
|
|||||||
return &AuthController{authService: authService, tokenService: tokenSvc, rateLimiter: limiter, cfg: cfg}
|
return &AuthController{authService: authService, tokenService: tokenSvc, rateLimiter: limiter, cfg: cfg}
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegisterPage 注册页面(已登录用户重定向到首页)
|
// RegisterPage 注册页面(已登录用户重定向到首页,注册关闭时重定向到登录页)
|
||||||
func (ac *AuthController) RegisterPage(c *gin.Context) {
|
func (ac *AuthController) RegisterPage(c *gin.Context) {
|
||||||
if _, exists := c.Get("uid"); exists {
|
if _, exists := c.Get("uid"); exists {
|
||||||
c.Redirect(http.StatusFound, "/")
|
c.Redirect(http.StatusFound, "/")
|
||||||
|
|||||||
19
internal/middleware/site_info.go
Normal file
19
internal/middleware/site_info.go
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"metazone.cc/metalab/internal/config"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// InjectSiteInfo 注入站点展示信息到请求上下文(供 BuildPageData 使用)
|
||||||
|
// 应用于引擎级全局中间件,确保所有 SSR 页面都能读取 Brand/Framework/CopyrightStart
|
||||||
|
func InjectSiteInfo(siteSettings *config.SiteSettings) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
info := siteSettings.SiteInfo()
|
||||||
|
c.Set("site_brand", info.Brand)
|
||||||
|
c.Set("site_framework", info.Framework)
|
||||||
|
c.Set("site_copyright_start", info.CopyrightStart)
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -22,14 +22,14 @@ type dependencies struct {
|
|||||||
tokenCtrl *controller.AuthController
|
tokenCtrl *controller.AuthController
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildDepsCore(db *gorm.DB, cfg *config.Config) (
|
func buildDepsCore(db *gorm.DB, cfg *config.Config, siteSettings *config.SiteSettings) (
|
||||||
*repository.UserRepo, *service.TokenService, *service.AuthService,
|
*repository.UserRepo, *service.TokenService, *service.AuthService,
|
||||||
*middleware.RateLimiter, *controller.AuthController,
|
*middleware.RateLimiter, *controller.AuthController,
|
||||||
*service.AvatarService, *middleware.AuthMiddleware, *adminCtrl.AdminController,
|
*service.AvatarService, *middleware.AuthMiddleware, *adminCtrl.AdminController,
|
||||||
) {
|
) {
|
||||||
userRepo := repository.NewUserRepo(db)
|
userRepo := repository.NewUserRepo(db)
|
||||||
tokenSvc := service.NewTokenService(cfg, userRepo)
|
tokenSvc := service.NewTokenService(cfg, userRepo)
|
||||||
authService := service.NewAuthService(userRepo, tokenSvc, cfg)
|
authService := service.NewAuthService(userRepo, tokenSvc, cfg, siteSettings)
|
||||||
rateLimiter := middleware.NewRateLimiter()
|
rateLimiter := middleware.NewRateLimiter()
|
||||||
authCtrl := controller.NewAuthController(authService, tokenSvc, rateLimiter, cfg)
|
authCtrl := controller.NewAuthController(authService, tokenSvc, rateLimiter, cfg)
|
||||||
avatarSvc := service.NewAvatarService(userRepo)
|
avatarSvc := service.NewAvatarService(userRepo)
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func buildDeps(db *gorm.DB, cfg *config.Config, siteSettings *config.SiteSettings) *dependencies {
|
func buildDeps(db *gorm.DB, cfg *config.Config, siteSettings *config.SiteSettings) *dependencies {
|
||||||
userRepo, _, authService, _, authCtrl, avatarSvc, authMdw, adminController := buildDepsCore(db, cfg)
|
userRepo, _, authService, _, authCtrl, avatarSvc, authMdw, adminController := buildDepsCore(db, cfg, siteSettings)
|
||||||
|
|
||||||
auditRepo := repository.NewAuditRepo(db)
|
auditRepo := repository.NewAuditRepo(db)
|
||||||
notificationRepo := repository.NewNotificationRepo(db)
|
notificationRepo := repository.NewNotificationRepo(db)
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import (
|
|||||||
// Setup 注册所有路由
|
// Setup 注册所有路由
|
||||||
func Setup(r *gin.Engine, db *gorm.DB, cfg *config.Config, siteSettings *config.SiteSettings) {
|
func Setup(r *gin.Engine, db *gorm.DB, cfg *config.Config, siteSettings *config.SiteSettings) {
|
||||||
r.Use(middleware.SecurityHeaders())
|
r.Use(middleware.SecurityHeaders())
|
||||||
|
r.Use(middleware.InjectSiteInfo(siteSettings))
|
||||||
|
|
||||||
deps := buildDeps(db, cfg, siteSettings)
|
deps := buildDeps(db, cfg, siteSettings)
|
||||||
|
|
||||||
|
|||||||
@ -18,11 +18,12 @@ type AuthService struct {
|
|||||||
userRepo userAuthStore
|
userRepo userAuthStore
|
||||||
tokenService tokenProvider
|
tokenService tokenProvider
|
||||||
cfg *config.Config
|
cfg *config.Config
|
||||||
|
siteSettings *config.SiteSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAuthService 构造函数
|
// NewAuthService 构造函数
|
||||||
func NewAuthService(userRepo userAuthStore, tokenSvc tokenProvider, cfg *config.Config) *AuthService {
|
func NewAuthService(userRepo userAuthStore, tokenSvc tokenProvider, cfg *config.Config, siteSettings *config.SiteSettings) *AuthService {
|
||||||
return &AuthService{userRepo: userRepo, tokenService: tokenSvc, cfg: cfg}
|
return &AuthService{userRepo: userRepo, tokenService: tokenSvc, cfg: cfg, siteSettings: siteSettings}
|
||||||
}
|
}
|
||||||
|
|
||||||
var pwLetter = regexp.MustCompile(`[a-zA-Z]`)
|
var pwLetter = regexp.MustCompile(`[a-zA-Z]`)
|
||||||
@ -56,6 +57,11 @@ func validatePassword(pw string) error {
|
|||||||
// rememberMe=true: refresh Cookie 持久化(30天);false: session cookie(关浏览器即清除)
|
// rememberMe=true: refresh Cookie 持久化(30天);false: session cookie(关浏览器即清除)
|
||||||
// regIP: 注册 IP 地址
|
// regIP: 注册 IP 地址
|
||||||
func (s *AuthService) Register(req model.RegisterRequest, regIP string) (string, string, *model.User, error) {
|
func (s *AuthService) Register(req model.RegisterRequest, regIP string) (string, string, *model.User, error) {
|
||||||
|
// 0. 检查注册开关
|
||||||
|
if !s.siteSettings.IsRegistrationEnabled() {
|
||||||
|
return "", "", nil, common.ErrRegistrationDisabled
|
||||||
|
}
|
||||||
|
|
||||||
// 1. 密码强度
|
// 1. 密码强度
|
||||||
if err := validatePassword(req.Password); err != nil {
|
if err := validatePassword(req.Password); err != nil {
|
||||||
return "", "", nil, err
|
return "", "", nil, err
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<footer>
|
<footer>
|
||||||
<div class="container footer-content">
|
<div class="container footer-content">
|
||||||
<p class="copyright">© 2024-2026 MetaLab | Framework: MetaZone</p>
|
<p class="copyright">© {{.CopyrightStart}}-2026 {{.Brand}} | Framework: {{.Framework}}</p>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@ -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}} - {{.Brand}}</title>
|
||||||
<link rel="stylesheet" href="/static/css/common.css">
|
<link rel="stylesheet" href="/static/css/common.css">
|
||||||
{{if .ExtraCSS}}
|
{{if .ExtraCSS}}
|
||||||
<link rel="stylesheet" href="{{.ExtraCSS}}">
|
<link rel="stylesheet" href="{{.ExtraCSS}}">
|
||||||
|
|||||||
@ -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}} - {{.Brand}} 管理面板</title>
|
||||||
<link rel="stylesheet" href="/admin/static/css/common.css">
|
<link rel="stylesheet" href="/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">{{.Brand}} <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" style="vertical-align:-2px;margin-right:3px;"><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" style="vertical-align:-2px;margin-right:3px;"><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>
|
||||||
|
|||||||
@ -1,7 +1,61 @@
|
|||||||
{{template "admin/layout/base.html" .}}
|
{{template "admin/layout/base.html" .}}
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h1>站点设置</h1>
|
<h1>站点设置</h1>
|
||||||
<p class="page-desc">管理审核开关与站点全局配置</p>
|
<p class="page-desc">管理站点基本配置、审核开关与注册策略</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 基本设置 -->
|
||||||
|
<div class="settings-section">
|
||||||
|
<div class="section-title">基本设置</div>
|
||||||
|
<div class="section-body">
|
||||||
|
<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-brand" class="setting-input" placeholder="MetaLab" value="{{.Brand}}">
|
||||||
|
<button class="btn btn-sm btn-primary btn-save" data-key="site.brand" data-input="setting-site-brand">保存</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="setting-item">
|
||||||
|
<div class="setting-info">
|
||||||
|
<span class="setting-label">框架名称</span>
|
||||||
|
<span class="setting-desc">显示在页脚 Framework 标识中</span>
|
||||||
|
</div>
|
||||||
|
<div class="setting-input-wrap">
|
||||||
|
<input type="text" id="setting-site-framework" class="setting-input" placeholder="MetaZone" value="{{.Framework}}">
|
||||||
|
<button class="btn btn-sm btn-primary btn-save" data-key="site.framework" data-input="setting-site-framework">保存</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="setting-item">
|
||||||
|
<div class="setting-info">
|
||||||
|
<span class="setting-label">版权起始年份</span>
|
||||||
|
<span class="setting-desc">页脚版权标起始年(如 2024 → © 2024-当前年)</span>
|
||||||
|
</div>
|
||||||
|
<div class="setting-input-wrap">
|
||||||
|
<input type="text" id="setting-site-copyright" class="setting-input" placeholder="2024" value="{{.CopyrightStart}}">
|
||||||
|
<button class="btn btn-sm btn-primary btn-save" data-key="site.copyright_start" data-input="setting-site-copyright">保存</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 注册设置 -->
|
||||||
|
<div class="settings-section">
|
||||||
|
<div class="section-title">注册设置</div>
|
||||||
|
<div class="section-body">
|
||||||
|
<div class="setting-item">
|
||||||
|
<div class="setting-info">
|
||||||
|
<span class="setting-label">允许注册</span>
|
||||||
|
<span class="setting-desc">关闭后,新用户将无法注册</span>
|
||||||
|
</div>
|
||||||
|
<label class="toggle-switch">
|
||||||
|
<input type="checkbox" id="registration-enabled" data-key="registration.enabled">
|
||||||
|
<span class="toggle-slider"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 审核设置 -->
|
<!-- 审核设置 -->
|
||||||
@ -56,7 +110,7 @@
|
|||||||
|
|
||||||
<!-- 通用设置 -->
|
<!-- 通用设置 -->
|
||||||
<div class="settings-section">
|
<div class="settings-section">
|
||||||
<div class="section-title">通用设置</div>
|
<div class="section-title">其他设置</div>
|
||||||
<div class="section-body" id="general-settings">
|
<div class="section-body" id="general-settings">
|
||||||
<div class="settings-empty" id="settings-empty">加载中...</div>
|
<div class="settings-empty" id="settings-empty">加载中...</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -45,6 +45,19 @@
|
|||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- 文本输入框 + 保存按钮 --- */
|
||||||
|
.setting-input-wrap {
|
||||||
|
display: flex; gap: 8px; align-items: center; flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.setting-input {
|
||||||
|
padding: 6px 10px; border: 1px solid var(--border);
|
||||||
|
border-radius: 4px; font-size: 13px; width: 200px;
|
||||||
|
outline: none; transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
.setting-input:focus { border-color: var(--primary); }
|
||||||
|
.btn-save { width: 56px; text-align: center; }
|
||||||
|
.btn-save:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||||
|
|
||||||
/* --- 空状态 --- */
|
/* --- 空状态 --- */
|
||||||
.settings-empty {
|
.settings-empty {
|
||||||
text-align: center; padding: 28px 20px;
|
text-align: center; padding: 28px 20px;
|
||||||
|
|||||||
@ -1,43 +1,94 @@
|
|||||||
/* =====================================================
|
/* =====================================================
|
||||||
MetaLab 管理面板 - 站点设置
|
MetaLab 管理面板 - 站点设置
|
||||||
单一职责:加载/保存站点设置,开关即时生效
|
单一职责:加载/保存站点设置,开关即时生效,文本输入手动保存
|
||||||
===================================================== */
|
===================================================== */
|
||||||
|
|
||||||
(function() {
|
(function() {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const TOGGLE_KEYS = [
|
const TOGGLE_KEYS = [
|
||||||
|
{ id: 'registration-enabled', key: 'registration.enabled' },
|
||||||
{ id: 'audit-enabled', key: 'audit.enabled' },
|
{ id: 'audit-enabled', key: 'audit.enabled' },
|
||||||
{ id: 'audit-username', key: 'audit.username_audit' },
|
{ id: 'audit-username', key: 'audit.username_audit' },
|
||||||
{ id: 'audit-avatar', key: 'audit.avatar_audit' },
|
{ id: 'audit-avatar', key: 'audit.avatar_audit' },
|
||||||
{ id: 'audit-bio', key: 'audit.bio_audit' }
|
{ id: 'audit-bio', key: 'audit.bio_audit' }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// 已有专属 UI 的设置 key,不在"其他设置"中重复显示
|
||||||
|
var DEDICATED_KEYS = {
|
||||||
|
'site.brand': true, 'site.framework': true, 'site.copyright_start': true,
|
||||||
|
'registration.enabled': true,
|
||||||
|
'audit.enabled': true, 'audit.username_audit': true, 'audit.avatar_audit': true, 'audit.bio_audit': true
|
||||||
|
};
|
||||||
|
|
||||||
// --- 初始加载 ---
|
// --- 初始加载 ---
|
||||||
async function loadSettings() {
|
async function loadSettings() {
|
||||||
try {
|
try {
|
||||||
const res = await api.get('/api/admin/site-settings');
|
var res = await api.get('/api/admin/site-settings');
|
||||||
if (!res.success) return;
|
if (!res.success) return;
|
||||||
|
|
||||||
// 审核开关
|
// 所有开关
|
||||||
const audit = res.data.audit || {};
|
var audit = res.data.audit || {};
|
||||||
|
var allSettings = res.data.all || {};
|
||||||
TOGGLE_KEYS.forEach(function(t) {
|
TOGGLE_KEYS.forEach(function(t) {
|
||||||
var el = document.getElementById(t.id);
|
var el = document.getElementById(t.id);
|
||||||
if (el) {
|
if (!el) return;
|
||||||
|
|
||||||
|
if (t.id === 'registration-enabled') {
|
||||||
|
el.checked = allSettings['registration.enabled'] !== 'false';
|
||||||
|
} else {
|
||||||
el.checked = audit[t.key.split('.')[1]] === true;
|
el.checked = audit[t.key.split('.')[1]] === true;
|
||||||
el.addEventListener('change', function() {
|
|
||||||
updateBoolSetting(t.key, el.checked, el);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
el.addEventListener('change', function() {
|
||||||
|
updateBoolSetting(t.key, el.checked, el);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// 通用设置
|
// 文本输入保存按钮
|
||||||
renderGeneralSettings(res.data.all || {});
|
bindSaveButtons();
|
||||||
|
|
||||||
|
// 其他设置(过滤已有专属UI的key)
|
||||||
|
renderOtherSettings(allSettings);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showToast('加载设置失败', 'error');
|
showToast('加载设置失败', 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- 文本输入保存 ---
|
||||||
|
function bindSaveButtons() {
|
||||||
|
var buttons = document.querySelectorAll('.btn-save');
|
||||||
|
buttons.forEach(function(btn) {
|
||||||
|
btn.addEventListener('click', function() {
|
||||||
|
var key = btn.getAttribute('data-key');
|
||||||
|
var inputId = btn.getAttribute('data-input');
|
||||||
|
var input = document.getElementById(inputId);
|
||||||
|
if (!key || !input) return;
|
||||||
|
updateTextSetting(key, input.value, btn);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateTextSetting(key, value, btn) {
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = '...';
|
||||||
|
try {
|
||||||
|
var res = await api.put('/api/admin/site-settings', {
|
||||||
|
key: key,
|
||||||
|
value: value
|
||||||
|
});
|
||||||
|
if (res.success) {
|
||||||
|
showToast('设置已更新', 'success');
|
||||||
|
} else {
|
||||||
|
showToast(res.message || '保存失败', 'error');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
showToast('保存失败', 'error');
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = '保存';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- 更新布尔设置 ---
|
// --- 更新布尔设置 ---
|
||||||
async function updateBoolSetting(key, value, toggle) {
|
async function updateBoolSetting(key, value, toggle) {
|
||||||
toggle.disabled = true;
|
toggle.disabled = true;
|
||||||
@ -60,16 +111,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 渲染通用设置 ---
|
// --- 渲染其他设置(过滤已有专属UI的key) ---
|
||||||
function renderGeneralSettings(all) {
|
function renderOtherSettings(all) {
|
||||||
var container = document.getElementById('general-settings');
|
var container = document.getElementById('general-settings');
|
||||||
var keys = Object.keys(all);
|
var filtered = [];
|
||||||
if (keys.length === 0) {
|
Object.keys(all).forEach(function(k) {
|
||||||
|
if (!DEDICATED_KEYS[k]) filtered.push(k);
|
||||||
|
});
|
||||||
|
if (filtered.length === 0) {
|
||||||
container.innerHTML = '<div class="settings-empty">暂无其他设置</div>';
|
container.innerHTML = '<div class="settings-empty">暂无其他设置</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
keys.forEach(function(key) {
|
filtered.forEach(function(key) {
|
||||||
var val = all[key] || '';
|
var val = all[key] || '';
|
||||||
var item = document.createElement('div');
|
var item = document.createElement('div');
|
||||||
item.className = 'setting-item';
|
item.className = 'setting-item';
|
||||||
|
|||||||
Reference in New Issue
Block a user