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

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

技术实现:
- 后端:数据库添加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

@@ -446,7 +446,7 @@ const ShareDB = {
}); });
const result = db.prepare(` const result = db.prepare(`
SELECT s.*, u.username, u.ftp_host, u.ftp_port, u.ftp_user, u.ftp_password, u.http_download_base_url SELECT s.*, u.username, u.ftp_host, u.ftp_port, u.ftp_user, u.ftp_password, u.http_download_base_url, u.theme_preference
FROM shares s FROM shares s
JOIN users u ON s.user_id = u.id JOIN users u ON s.user_id = u.id
WHERE s.share_code = ? WHERE s.share_code = ?
@@ -616,6 +616,26 @@ function initDefaultSettings() {
if (!SettingsDB.get('max_upload_size')) { if (!SettingsDB.get('max_upload_size')) {
SettingsDB.set('max_upload_size', '10737418240'); // 10GB in bytes SettingsDB.set('max_upload_size', '10737418240'); // 10GB in bytes
} }
// 默认全局主题为暗色
if (!SettingsDB.get('global_theme')) {
SettingsDB.set('global_theme', 'dark');
}
}
// 数据库迁移 - 主题偏好字段
function migrateThemePreference() {
try {
const columns = db.prepare("PRAGMA table_info(users)").all();
const hasThemePreference = columns.some(col => col.name === 'theme_preference');
if (!hasThemePreference) {
console.log('[数据库迁移] 添加主题偏好字段...');
db.exec(`ALTER TABLE users ADD COLUMN theme_preference TEXT DEFAULT NULL`);
console.log('[数据库迁移] ✓ 主题偏好字段已添加');
}
} catch (error) {
console.error('[数据库迁移] 主题偏好迁移失败:', error);
}
} }
// 数据库版本迁移 - v2.0 本地存储功能 // 数据库版本迁移 - v2.0 本地存储功能
@@ -798,6 +818,7 @@ initDatabase();
createDefaultAdmin(); createDefaultAdmin();
initDefaultSettings(); initDefaultSettings();
migrateToV2(); // 执行数据库迁移 migrateToV2(); // 执行数据库迁移
migrateThemePreference(); // 主题偏好迁移
module.exports = { module.exports = {
db, db,

View File

@@ -1628,6 +1628,56 @@ app.get('/api/user/profile', authMiddleware, (req, res) => {
}); });
}); });
// 获取用户主题偏好(包含全局默认主题)
app.get('/api/user/theme', authMiddleware, (req, res) => {
try {
const globalTheme = SettingsDB.get('global_theme') || 'dark';
const userTheme = req.user.theme_preference; // null表示跟随全局
res.json({
success: true,
theme: {
global: globalTheme,
user: userTheme,
effective: userTheme || globalTheme // 用户设置优先,否则使用全局
}
});
} catch (error) {
res.status(500).json({ success: false, message: '获取主题失败' });
}
});
// 设置用户主题偏好
app.post('/api/user/theme', authMiddleware, (req, res) => {
try {
const { theme } = req.body;
const validThemes = ['dark', 'light', null]; // null表示跟随全局
if (!validThemes.includes(theme)) {
return res.status(400).json({
success: false,
message: '无效的主题设置,可选: dark, light, null(跟随全局)'
});
}
UserDB.update(req.user.id, { theme_preference: theme });
const globalTheme = SettingsDB.get('global_theme') || 'dark';
res.json({
success: true,
message: '主题偏好已更新',
theme: {
global: globalTheme,
user: theme,
effective: theme || globalTheme
}
});
} catch (error) {
console.error('更新主题失败:', error);
res.status(500).json({ success: false, message: '更新主题失败' });
}
});
// 更新FTP配置 // 更新FTP配置
app.post('/api/user/update-ftp', app.post('/api/user/update-ftp',
authMiddleware, authMiddleware,
@@ -2888,6 +2938,44 @@ app.delete('/api/share/:id', authMiddleware, (req, res) => {
// ===== 分享链接访问(公开) ===== // ===== 分享链接访问(公开) =====
// 获取公共主题设置(用于分享页面,无需认证)
app.get('/api/public/theme', (req, res) => {
try {
const globalTheme = SettingsDB.get('global_theme') || 'dark';
res.json({
success: true,
theme: globalTheme
});
} catch (error) {
res.json({ success: true, theme: 'dark' }); // 出错默认暗色
}
});
// 获取分享页面主题(基于分享者偏好或全局设置)
app.get('/api/share/:code/theme', (req, res) => {
try {
const { code } = req.params;
const share = ShareDB.findByCode(code);
const globalTheme = SettingsDB.get('global_theme') || 'dark';
if (!share) {
return res.json({
success: true,
theme: globalTheme
});
}
// 优先使用分享者的主题偏好,否则使用全局主题
const effectiveTheme = share.theme_preference || globalTheme;
res.json({
success: true,
theme: effectiveTheme
});
} catch (error) {
res.json({ success: true, theme: 'dark' });
}
});
// 访问分享链接 - 验证密码支持本地存储和SFTP // 访问分享链接 - 验证密码支持本地存储和SFTP
app.post('/api/share/:code/verify', shareRateLimitMiddleware, async (req, res) => { app.post('/api/share/:code/verify', shareRateLimitMiddleware, async (req, res) => {
const { code } = req.params; const { code } = req.params;
@@ -3418,11 +3506,13 @@ app.get('/api/admin/settings', authMiddleware, adminMiddleware, (req, res) => {
const smtpUser = SettingsDB.get('smtp_user'); const smtpUser = SettingsDB.get('smtp_user');
const smtpFrom = SettingsDB.get('smtp_from') || smtpUser; const smtpFrom = SettingsDB.get('smtp_from') || smtpUser;
const smtpHasPassword = !!SettingsDB.get('smtp_password'); const smtpHasPassword = !!SettingsDB.get('smtp_password');
const globalTheme = SettingsDB.get('global_theme') || 'dark';
res.json({ res.json({
success: true, success: true,
settings: { settings: {
max_upload_size: maxUploadSize, max_upload_size: maxUploadSize,
global_theme: globalTheme,
smtp: { smtp: {
host: smtpHost || '', host: smtpHost || '',
port: smtpPort ? parseInt(smtpPort, 10) : 465, port: smtpPort ? parseInt(smtpPort, 10) : 465,
@@ -3445,7 +3535,7 @@ app.get('/api/admin/settings', authMiddleware, adminMiddleware, (req, res) => {
// 更新系统设置 // 更新系统设置
app.post('/api/admin/settings', authMiddleware, adminMiddleware, (req, res) => { app.post('/api/admin/settings', authMiddleware, adminMiddleware, (req, res) => {
try { try {
const { max_upload_size, smtp } = req.body; const { max_upload_size, smtp, global_theme } = req.body;
if (max_upload_size !== undefined) { if (max_upload_size !== undefined) {
const size = parseInt(max_upload_size); const size = parseInt(max_upload_size);
@@ -3458,6 +3548,18 @@ app.post('/api/admin/settings', authMiddleware, adminMiddleware, (req, res) => {
SettingsDB.set('max_upload_size', size.toString()); SettingsDB.set('max_upload_size', size.toString());
} }
// 更新全局主题
if (global_theme !== undefined) {
const validThemes = ['dark', 'light'];
if (!validThemes.includes(global_theme)) {
return res.status(400).json({
success: false,
message: '无效的主题设置'
});
}
SettingsDB.set('global_theme', global_theme);
}
if (smtp) { if (smtp) {
if (!smtp.host || !smtp.port || !smtp.user) { if (!smtp.host || !smtp.port || !smtp.user) {
return res.status(400).json({ success: false, message: 'SMTP配置不完整' }); return res.status(400).json({ success: false, message: 'SMTP配置不完整' });

View File

@@ -107,7 +107,7 @@
<script src="libs/axios.min.js"></script> <script src="libs/axios.min.js"></script>
<link rel="stylesheet" href="libs/fontawesome/css/all.min.css"> <link rel="stylesheet" href="libs/fontawesome/css/all.min.css">
<style> <style>
/* ========== 暗色主题 CSS 变量 ========== */ /* ========== 暗色主题 CSS 变量(默认) ========== */
:root { :root {
--bg-primary: #0a0a0f; --bg-primary: #0a0a0f;
--bg-secondary: #12121a; --bg-secondary: #12121a;
@@ -128,6 +128,32 @@
--info: #3b82f6; --info: #3b82f6;
} }
/* ========== 亮色玻璃主题 ========== */
.light-theme {
--bg-primary: #f0f4f8;
--bg-secondary: #ffffff;
--bg-card: rgba(255, 255, 255, 0.7);
--bg-card-hover: rgba(255, 255, 255, 0.9);
--glass-border: rgba(102, 126, 234, 0.2);
--glass-border-hover: rgba(102, 126, 234, 0.4);
--text-primary: #1a1a2e;
--text-secondary: rgba(26, 26, 46, 0.7);
--text-muted: rgba(26, 26, 46, 0.5);
--accent-1: #5a67d8;
--accent-2: #6b46c1;
--accent-3: #d53f8c;
--glow: rgba(90, 103, 216, 0.3);
}
/* 亮色主题背景渐变 */
.light-theme body::before,
body.light-theme::before {
background:
radial-gradient(ellipse at top right, rgba(102, 126, 234, 0.12) 0%, transparent 50%),
radial-gradient(ellipse at bottom left, rgba(118, 75, 162, 0.12) 0%, transparent 50%),
linear-gradient(135deg, #e0e7ff 0%, #f0f4f8 50%, #fdf2f8 100%);
}
/* 防止 Vue 初始化前显示原始模板 */ /* 防止 Vue 初始化前显示原始模板 */
[v-cloak] { display: none !important; } [v-cloak] { display: none !important; }
@@ -1691,8 +1717,44 @@
</div> </div>
</div> </div>
<!-- 界面设置 -->
<h3 style="margin: 40px 0 20px 0;"><i class="fas fa-palette"></i> 界面设置</h3>
<div style="background: var(--bg-card); padding: 20px; border-radius: 12px; border: 1px solid var(--glass-border); margin-bottom: 30px;">
<div style="margin-bottom: 15px;">
<span style="font-weight: 600; color: var(--text-primary);">主题模式</span>
<span style="color: var(--text-secondary); font-size: 13px; margin-left: 10px;">选择你喜欢的界面风格</span>
</div>
<div style="display: flex; gap: 12px; flex-wrap: wrap;">
<button
class="btn"
:class="userThemePreference === null ? 'btn-primary' : 'btn-secondary'"
@click="setUserTheme(null)"
style="flex: 1; min-width: 120px; padding: 12px 16px;">
<i class="fas fa-globe"></i> 跟随全局
<span v-if="userThemePreference === null" style="margin-left: 8px; font-size: 12px; opacity: 0.8;">({{ globalTheme === 'dark' ? '暗色' : '亮色' }})</span>
</button>
<button
class="btn"
:class="userThemePreference === 'dark' ? 'btn-primary' : 'btn-secondary'"
@click="setUserTheme('dark')"
style="flex: 1; min-width: 120px; padding: 12px 16px;">
<i class="fas fa-moon"></i> 暗色主题
</button>
<button
class="btn"
:class="userThemePreference === 'light' ? 'btn-primary' : 'btn-secondary'"
@click="setUserTheme('light')"
style="flex: 1; min-width: 120px; padding: 12px 16px;">
<i class="fas fa-sun"></i> 亮色主题
</button>
</div>
<div style="margin-top: 12px; font-size: 13px; color: var(--text-muted);">
<i class="fas fa-info-circle"></i> 主题设置会影响你的文件页面和分享页面的外观
</div>
</div>
<!-- 账号设置 --> <!-- 账号设置 -->
<h3 style="margin: 40px 0 20px 0;">账号设置</h3> <h3 style="margin: 40px 0 20px 0;"><i class="fas fa-user-cog"></i> 账号设置</h3>
<!-- 管理员可以改用户名 --> <!-- 管理员可以改用户名 -->
<form v-if="user && user.is_admin" @submit.prevent="updateUsername" style="margin-bottom: 30px;"> <form v-if="user && user.is_admin" @submit.prevent="updateUsername" style="margin-bottom: 30px;">
@@ -1970,8 +2032,35 @@
</button> </button>
<span style="color: var(--text-secondary); font-size: 13px;">修改后需要重启服务才能生效</span> <span style="color: var(--text-secondary); font-size: 13px;">修改后需要重启服务才能生效</span>
</div> </div>
<hr style="margin: 20px 0;"> <hr style="margin: 20px 0;">
<h4 style="margin-bottom: 12px;">SMTP 邮件配置(用于注册激活和找回密码)</h4> <h4 style="margin-bottom: 12px;"><i class="fas fa-palette"></i> 全局主题设置</h4>
<div style="margin-bottom: 15px;">
<span style="color: var(--text-secondary); font-size: 13px;">设置系统默认主题,用户可以在个人设置中覆盖此设置</span>
</div>
<div style="display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 20px;">
<button
class="btn"
:class="globalTheme === 'dark' ? 'btn-primary' : 'btn-secondary'"
@click="setGlobalTheme('dark')"
style="padding: 12px 24px;">
<i class="fas fa-moon"></i> 暗色主题(默认)
</button>
<button
class="btn"
:class="globalTheme === 'light' ? 'btn-primary' : 'btn-secondary'"
@click="setGlobalTheme('light')"
style="padding: 12px 24px;">
<i class="fas fa-sun"></i> 亮色主题
</button>
</div>
<div style="font-size: 13px; color: var(--text-muted); background: var(--bg-card); padding: 12px; border-radius: 8px; border: 1px solid var(--glass-border);">
<i class="fas fa-info-circle"></i> 当前全局主题: <strong>{{ globalTheme === 'dark' ? '暗色' : '亮色' }}</strong>
未设置个人偏好的用户将使用此主题。分享页面也会默认使用分享者的主题设置。
</div>
<hr style="margin: 20px 0;">
<h4 style="margin-bottom: 12px;"><i class="fas fa-envelope"></i> SMTP 邮件配置(用于注册激活和找回密码)</h4>
<div class="alert alert-info" style="margin-bottom: 15px;"> <div class="alert alert-info" style="margin-bottom: 15px;">
支持 QQ/163/企业邮箱等。QQ 邮箱示例:主机 <code>smtp.qq.com</code>,端口 <code>465</code>,勾选 SSL用户名=邮箱地址,密码=授权码。 支持 QQ/163/企业邮箱等。QQ 邮箱示例:主机 <code>smtp.qq.com</code>,端口 <code>465</code>,勾选 SSL用户名=邮箱地址,密码=授权码。
</div> </div>

View File

@@ -250,7 +250,12 @@ createApp({
// SFTP空间使用统计 // SFTP空间使用统计
sftpUsage: null, // { totalSize, totalSizeFormatted, fileCount, dirCount } sftpUsage: null, // { totalSize, totalSizeFormatted, fileCount, dirCount }
sftpUsageLoading: false, sftpUsageLoading: false,
sftpUsageError: null sftpUsageError: null,
// 主题设置
currentTheme: 'dark', // 当前生效的主题: 'dark' 或 'light'
globalTheme: 'dark', // 全局默认主题(管理员设置)
userThemePreference: null // 用户主题偏好: 'dark', 'light', 或 null(跟随全局)
}; };
}, },
@@ -281,6 +286,91 @@ createApp({
}, },
methods: { 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兼容缺少 ? 的场景) // 提取URL中的token兼容缺少 ? 的场景)
getTokenFromUrl(key) { getTokenFromUrl(key) {
const currentHref = window.location.href; const currentHref = window.location.href;
@@ -438,6 +528,8 @@ handleDragLeave(e) {
// 启动定期检查用户配置 // 启动定期检查用户配置
this.startProfileSync(); this.startProfileSync();
// 加载用户主题设置
this.loadUserTheme();
// 管理员直接跳转到管理后台 // 管理员直接跳转到管理后台
if (this.user.is_admin) { if (this.user.is_admin) {
this.currentView = 'admin'; this.currentView = 'admin';
@@ -878,6 +970,8 @@ handleDragLeave(e) {
// 启动定期检查用户配置 // 启动定期检查用户配置
this.startProfileSync(); this.startProfileSync();
// 加载用户主题设置
this.loadUserTheme();
// 读取上次停留的视图(需合法才生效) // 读取上次停留的视图(需合法才生效)
const savedView = localStorage.getItem('lastView'); const savedView = localStorage.getItem('lastView');
@@ -2207,6 +2301,10 @@ handleDragLeave(e) {
if (response.data.success) { if (response.data.success) {
const settings = response.data.settings; const settings = response.data.settings;
this.systemSettings.maxUploadSizeMB = Math.round(settings.max_upload_size / (1024 * 1024)); this.systemSettings.maxUploadSizeMB = Math.round(settings.max_upload_size / (1024 * 1024));
// 加载全局主题设置
if (settings.global_theme) {
this.globalTheme = settings.global_theme;
}
if (settings.smtp) { if (settings.smtp) {
this.systemSettings.smtp.host = settings.smtp.host || ''; this.systemSettings.smtp.host = settings.smtp.host || '';
this.systemSettings.smtp.port = settings.smtp.port || 465; this.systemSettings.smtp.port = settings.smtp.port || 465;
@@ -2584,6 +2682,9 @@ handleDragLeave(e) {
// 初始化调试模式状态 // 初始化调试模式状态
this.debugMode = localStorage.getItem('debugMode') === 'true'; this.debugMode = localStorage.getItem('debugMode') === 'true';
// 初始化主题从localStorage加载避免闪烁
this.initTheme();
// 处理URL中的验证/重置token兼容缺少?的旧链接) // 处理URL中的验证/重置token兼容缺少?的旧链接)
const verifyToken = this.getTokenFromUrl('verifyToken'); const verifyToken = this.getTokenFromUrl('verifyToken');
const resetToken = this.getTokenFromUrl('resetToken'); const resetToken = this.getTokenFromUrl('resetToken');

View File

@@ -11,7 +11,7 @@
/* 防止 Vue 初始化前显示原始模板 */ /* 防止 Vue 初始化前显示原始模板 */
[v-cloak] { display: none !important; } [v-cloak] { display: none !important; }
/* ========== 暗色主题 CSS 变量 ========== */ /* ========== 暗色主题 CSS 变量(默认) ========== */
:root { :root {
--bg-primary: #0a0a0f; --bg-primary: #0a0a0f;
--bg-secondary: #12121a; --bg-secondary: #12121a;
@@ -31,6 +31,31 @@
--warning: #f59e0b; --warning: #f59e0b;
} }
/* ========== 亮色玻璃主题 ========== */
.light-theme {
--bg-primary: #f0f4f8;
--bg-secondary: #ffffff;
--bg-card: rgba(255, 255, 255, 0.7);
--bg-card-hover: rgba(255, 255, 255, 0.9);
--glass-border: rgba(102, 126, 234, 0.2);
--glass-border-hover: rgba(102, 126, 234, 0.4);
--text-primary: #1a1a2e;
--text-secondary: rgba(26, 26, 46, 0.7);
--text-muted: rgba(26, 26, 46, 0.5);
--accent-1: #5a67d8;
--accent-2: #6b46c1;
--accent-3: #d53f8c;
--glow: rgba(90, 103, 216, 0.3);
}
/* 亮色主题背景渐变 */
body.light-theme::before {
background:
radial-gradient(ellipse at top right, rgba(102, 126, 234, 0.12) 0%, transparent 50%),
radial-gradient(ellipse at bottom left, rgba(118, 75, 162, 0.12) 0%, transparent 50%),
linear-gradient(135deg, #e0e7ff 0%, #f0f4f8 50%, #fdf2f8 100%);
}
* { margin: 0; padding: 0; box-sizing: border-box; } * { margin: 0; padding: 0; box-sizing: border-box; }
body { body {
@@ -746,6 +771,8 @@
loading: true, loading: true,
errorMessage: '', errorMessage: '',
viewMode: "grid", // 视图模式: grid 大图标, list 列表(默认大图标) viewMode: "grid", // 视图模式: grid 大图标, list 列表(默认大图标)
// 主题
currentTheme: 'dark',
// 媒体预览 // 媒体预览
showImageViewer: false, showImageViewer: false,
showVideoPlayer: false, showVideoPlayer: false,
@@ -777,10 +804,36 @@
return; return;
} }
// 加载分享页面主题(基于分享者的主题偏好)
await this.loadTheme();
// 尝试验证分享 // 尝试验证分享
await this.verifyShare(); await this.verifyShare();
}, },
// 加载主题
async loadTheme() {
try {
const response = await axios.get(`${this.apiBase}/api/share/${this.shareCode}/theme`);
if (response.data.success) {
this.currentTheme = response.data.theme;
this.applyTheme(this.currentTheme);
}
} catch (error) {
// 出错时使用默认暗色主题
console.error('加载主题失败:', error);
}
},
// 应用主题
applyTheme(theme) {
if (theme === 'light') {
document.body.classList.add('light-theme');
} else {
document.body.classList.remove('light-theme');
}
},
async verifyShare() { async verifyShare() {
this.errorMessage = ''; this.errorMessage = '';
this.loading = true; this.loading = true;