添加主题切换功能:支持暗色/亮色玻璃主题

功能说明:
- 管理员可在系统设置中配置全局默认主题
- 普通用户可在设置页面选择:跟随全局/暗色/亮色
- 分享页面自动继承分享者的主题偏好
- 主题设置实时保存,刷新后保持

技术实现:
- 后端:数据库添加theme_preference字段,新增主题API
- 前端:CSS变量实现主题切换,localStorage缓存避免闪烁
- 分享页:加载时获取分享者主题设置

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-27 23:02:48 +08:00
parent 138bda9ae5
commit f12b9b7291
5 changed files with 374 additions and 8 deletions

View File

@@ -250,7 +250,12 @@ createApp({
// SFTP空间使用统计
sftpUsage: null, // { totalSize, totalSizeFormatted, fileCount, dirCount }
sftpUsageLoading: false,
sftpUsageError: null
sftpUsageError: null,
// 主题设置
currentTheme: 'dark', // 当前生效的主题: 'dark' 或 'light'
globalTheme: 'dark', // 全局默认主题(管理员设置)
userThemePreference: null // 用户主题偏好: 'dark', 'light', 或 null(跟随全局)
};
},
@@ -281,6 +286,91 @@ createApp({
},
methods: {
// ========== 主题管理 ==========
// 初始化主题
initTheme() {
// 先从localStorage读取避免页面闪烁
const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
this.applyTheme(savedTheme);
}
},
// 加载用户主题设置(登录后调用)
async loadUserTheme() {
try {
const res = await axios.get(`${this.apiBase}/api/user/theme`, {
headers: { Authorization: `Bearer ${this.token}` }
});
if (res.data.success) {
this.globalTheme = res.data.theme.global;
this.userThemePreference = res.data.theme.user;
this.currentTheme = res.data.theme.effective;
this.applyTheme(this.currentTheme);
localStorage.setItem('theme', this.currentTheme);
}
} catch (error) {
console.error('加载主题设置失败:', error);
}
},
// 应用主题到DOM
applyTheme(theme) {
this.currentTheme = theme;
if (theme === 'light') {
document.body.classList.add('light-theme');
} else {
document.body.classList.remove('light-theme');
}
},
// 切换用户主题偏好
async setUserTheme(theme) {
try {
const res = await axios.post(`${this.apiBase}/api/user/theme`,
{ theme },
{ headers: { Authorization: `Bearer ${this.token}` }}
);
if (res.data.success) {
this.userThemePreference = res.data.theme.user;
this.currentTheme = res.data.theme.effective;
this.applyTheme(this.currentTheme);
localStorage.setItem('theme', this.currentTheme);
this.showToast('success', '主题已更新', theme === null ? '已设为跟随全局' : (theme === 'dark' ? '已切换到暗色主题' : '已切换到亮色主题'));
}
} catch (error) {
this.showToast('error', '主题更新失败', error.response?.data?.message || '请稍后重试');
}
},
// 获取主题显示文本
getThemeText(theme) {
if (theme === null) return '跟随全局';
return theme === 'dark' ? '暗色主题' : '亮色主题';
},
// 设置全局主题(管理员)
async setGlobalTheme(theme) {
try {
const res = await axios.post(`${this.apiBase}/api/admin/settings`,
{ global_theme: theme },
{ headers: { Authorization: `Bearer ${this.token}` }}
);
if (res.data.success) {
this.globalTheme = theme;
// 如果用户没有设置个人偏好,则跟随全局
if (this.userThemePreference === null) {
this.currentTheme = theme;
this.applyTheme(theme);
localStorage.setItem('theme', theme);
}
this.showToast('success', '全局主题已更新', theme === 'dark' ? '默认暗色主题' : '默认亮色主题');
}
} catch (error) {
this.showToast('error', '设置失败', error.response?.data?.message || '请稍后重试');
}
},
// 提取URL中的token兼容缺少 ? 的场景)
getTokenFromUrl(key) {
const currentHref = window.location.href;
@@ -438,6 +528,8 @@ handleDragLeave(e) {
// 启动定期检查用户配置
this.startProfileSync();
// 加载用户主题设置
this.loadUserTheme();
// 管理员直接跳转到管理后台
if (this.user.is_admin) {
this.currentView = 'admin';
@@ -878,6 +970,8 @@ handleDragLeave(e) {
// 启动定期检查用户配置
this.startProfileSync();
// 加载用户主题设置
this.loadUserTheme();
// 读取上次停留的视图(需合法才生效)
const savedView = localStorage.getItem('lastView');
@@ -2207,6 +2301,10 @@ handleDragLeave(e) {
if (response.data.success) {
const settings = response.data.settings;
this.systemSettings.maxUploadSizeMB = Math.round(settings.max_upload_size / (1024 * 1024));
// 加载全局主题设置
if (settings.global_theme) {
this.globalTheme = settings.global_theme;
}
if (settings.smtp) {
this.systemSettings.smtp.host = settings.smtp.host || '';
this.systemSettings.smtp.port = settings.smtp.port || 465;
@@ -2584,6 +2682,9 @@ handleDragLeave(e) {
// 初始化调试模式状态
this.debugMode = localStorage.getItem('debugMode') === 'true';
// 初始化主题从localStorage加载避免闪烁
this.initTheme();
// 处理URL中的验证/重置token兼容缺少?的旧链接)
const verifyToken = this.getTokenFromUrl('verifyToken');
const resetToken = this.getTokenFromUrl('resetToken');