新增功能:
- 主题发现模块:扫描 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:
109
templates/admin/static/js/site-settings-theme.js
Normal file
109
templates/admin/static/js/site-settings-theme.js
Normal file
@ -0,0 +1,109 @@
|
||||
/* =====================================================
|
||||
MetaLab 管理面板 - 主题切换
|
||||
立即生效,无需重启
|
||||
===================================================== */
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
var selector = document.getElementById('theme-selector');
|
||||
var saveBtn = document.getElementById('theme-save-btn');
|
||||
var infoEl = document.getElementById('theme-info');
|
||||
var statusSection = document.getElementById('theme-status');
|
||||
var statusMsg = document.getElementById('theme-status-msg');
|
||||
var themes = [];
|
||||
var currentTheme = '';
|
||||
|
||||
async function loadThemes() {
|
||||
try {
|
||||
var res = await api.get('/api/admin/site-settings/themes');
|
||||
if (!res.success) {
|
||||
showToast('加载主题列表失败', 'error');
|
||||
return;
|
||||
}
|
||||
themes = res.data.themes || [];
|
||||
currentTheme = res.data.current || 'MetaLab-2026';
|
||||
|
||||
// 填充下拉
|
||||
selector.innerHTML = '';
|
||||
themes.forEach(function(t) {
|
||||
var opt = document.createElement('option');
|
||||
opt.value = t.dir;
|
||||
opt.textContent = t.display_name || t.name;
|
||||
if (t.dir === currentTheme) {
|
||||
opt.selected = true;
|
||||
updateInfo(t);
|
||||
}
|
||||
selector.appendChild(opt);
|
||||
});
|
||||
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.textContent = '保存';
|
||||
|
||||
// 选择变化时更新信息
|
||||
selector.addEventListener('change', function() {
|
||||
var selected = selector.value;
|
||||
var found = themes.find(function(t) { return t.dir === selected; });
|
||||
if (found) {
|
||||
updateInfo(found);
|
||||
}
|
||||
saveBtn.disabled = (selected === currentTheme);
|
||||
});
|
||||
|
||||
// 保存
|
||||
saveBtn.addEventListener('click', function() {
|
||||
applyTheme(selector.value);
|
||||
});
|
||||
} catch (e) {
|
||||
showToast('加载主题列表失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function updateInfo(theme) {
|
||||
var parts = [];
|
||||
if (theme.display_name) parts.push(theme.display_name);
|
||||
if (theme.version) parts.push('v' + theme.version);
|
||||
if (theme.author) parts.push('作者:' + theme.author);
|
||||
if (theme.description) parts.push(theme.description);
|
||||
infoEl.innerHTML = '<span class="setting-value">' + escapeHtml(parts.join(' · ')) + '</span>';
|
||||
}
|
||||
|
||||
async function applyTheme(themeDir) {
|
||||
saveBtn.disabled = true;
|
||||
saveBtn.textContent = '切换中...';
|
||||
showStatus('正在切换主题...', '');
|
||||
|
||||
try {
|
||||
var res = await api.put('/api/admin/site-settings/theme', { theme: themeDir });
|
||||
if (res.success) {
|
||||
currentTheme = themeDir;
|
||||
saveBtn.textContent = '已生效';
|
||||
showToast('主题已切换,即时生效', 'success');
|
||||
showStatus('✅ 主题已切换为:<strong>' + themeDir + '</strong>,所有访客可见新主题', 'success');
|
||||
} else {
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.textContent = '保存';
|
||||
showToast(res.message || '切换失败', 'error');
|
||||
showStatus('❌ 切换失败:' + (res.message || '未知错误'), 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.textContent = '保存';
|
||||
showToast('切换失败', 'error');
|
||||
showStatus('❌ 切换失败,请重试', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function showStatus(msg, type) {
|
||||
statusSection.style.display = 'block';
|
||||
statusSection.className = 'settings-section';
|
||||
if (type === 'success') {
|
||||
statusSection.classList.add('status-success');
|
||||
} else if (type === 'error') {
|
||||
statusSection.classList.add('status-error');
|
||||
}
|
||||
statusMsg.innerHTML = msg;
|
||||
}
|
||||
|
||||
loadThemes();
|
||||
})();
|
||||
Reference in New Issue
Block a user