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:
2026-05-27 15:12:28 +08:00
parent 7bc2dbd970
commit 0b948cf70f
16 changed files with 216 additions and 28 deletions

View File

@ -1,5 +1,5 @@
<footer>
<div class="container footer-content">
<p class="copyright">&copy; 2024-2026 MetaLab | Framework: MetaZone</p>
<p class="copyright">&copy; {{.CopyrightStart}}-2026 {{.Brand}} | Framework: {{.Framework}}</p>
</div>
</footer>

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{{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">
{{if .ExtraCSS}}
<link rel="stylesheet" href="{{.ExtraCSS}}">

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{{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">
{{if .ExtraCSS}}
<link rel="stylesheet" href="{{.ExtraCSS}}">
@ -13,7 +13,7 @@
<body>
<header class="admin-header">
<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">
<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>

View File

@ -1,7 +1,61 @@
{{template "admin/layout/base.html" .}}
<div class="page-header">
<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>
<!-- 审核设置 -->
@ -56,7 +110,7 @@
<!-- 通用设置 -->
<div class="settings-section">
<div class="section-title">通用设置</div>
<div class="section-title">其他设置</div>
<div class="section-body" id="general-settings">
<div class="settings-empty" id="settings-empty">加载中...</div>
</div>

View File

@ -45,6 +45,19 @@
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 {
text-align: center; padding: 28px 20px;

View File

@ -1,43 +1,94 @@
/* =====================================================
MetaLab 管理面板 - 站点设置
单一职责:加载/保存站点设置,开关即时生效
单一职责:加载/保存站点设置,开关即时生效,文本输入手动保存
===================================================== */
(function() {
'use strict';
const TOGGLE_KEYS = [
{ id: 'registration-enabled', key: 'registration.enabled' },
{ id: 'audit-enabled', key: 'audit.enabled' },
{ id: 'audit-username', key: 'audit.username_audit' },
{ id: 'audit-avatar', key: 'audit.avatar_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() {
try {
const res = await api.get('/api/admin/site-settings');
var res = await api.get('/api/admin/site-settings');
if (!res.success) return;
// 审核开关
const audit = res.data.audit || {};
// 所有开关
var audit = res.data.audit || {};
var allSettings = res.data.all || {};
TOGGLE_KEYS.forEach(function(t) {
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.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) {
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) {
toggle.disabled = true;
@ -60,16 +111,19 @@
}
}
// --- 渲染通用设置 ---
function renderGeneralSettings(all) {
// --- 渲染其他设置过滤已有专属UI的key ---
function renderOtherSettings(all) {
var container = document.getElementById('general-settings');
var keys = Object.keys(all);
if (keys.length === 0) {
var filtered = [];
Object.keys(all).forEach(function(k) {
if (!DEDICATED_KEYS[k]) filtered.push(k);
});
if (filtered.length === 0) {
container.innerHTML = '<div class="settings-empty">暂无其他设置</div>';
return;
}
container.innerHTML = '';
keys.forEach(function(key) {
filtered.forEach(function(key) {
var val = all[key] || '';
var item = document.createElement('div');
item.className = 'setting-item';