+
@@ -152,6 +157,8 @@
background: '{{background}}'
};
+
+
diff --git a/themes/2025/js/auth.js b/themes/2025/js/auth.js
index 3700595..eac4c51 100644
--- a/themes/2025/js/auth.js
+++ b/themes/2025/js/auth.js
@@ -166,7 +166,8 @@ const UserSystem = {
console.log('用户系统已启用');
// 检查是否允许注册,动态显示注册链接
- const allowRegistration = result.data.allow_user_registration;
+ // 后端使用 0/1 表示开关,严格比较为 1
+ const allowRegistration = result.data.allow_user_registration === 1;
const guestLinks = document.getElementById('guest-links');
if (guestLinks) {
diff --git a/themes/2025/js/dashboard.js b/themes/2025/js/dashboard.js
index 389ebd6..a7c6f12 100644
--- a/themes/2025/js/dashboard.js
+++ b/themes/2025/js/dashboard.js
@@ -7,16 +7,63 @@ const Dashboard = {
// 分页配置
currentPage: 1,
pageSize: 20,
+
+ // Helper: 安全解析 JSON
+ async parseJsonSafe(response) {
+ try {
+ return await response.json();
+ } catch (err) {
+ console.error('[dashboard] 解析 JSON 失败:', err);
+ return null;
+ }
+ },
+
+ // Helper: 处理认证相关返回(401/403)
+ handleAuthError(result) {
+ if (!result) return false;
+ if (result.code === 401 || result.code === 403) {
+ // 清理本地登录信息并提示重新登录
+ UserAuth.removeToken();
+ UserAuth.removeUserInfo();
+ UserAuth.updateUI();
+ this.showLoginPrompt();
+ return true;
+ }
+ return false;
+ },
/**
* 初始化仪表板
*/
async init() {
+ // 如果有 token 但缺少 user_info,先尝试在初始化阶段拉取用户信息(自愈),最多重试3次
+ const token = UserAuth.getToken();
+ if (token && !UserAuth.getUserInfo()) {
+ console.log('[dashboard] 检测到 token 存在但 user_info 缺失,开始最多 3 次尝试拉取用户信息');
+ let success = false;
+ for (let attempt = 1; attempt <= 3; attempt++) {
+ try {
+ console.log(`[dashboard] 拉取 user_info 尝试 #${attempt}`);
+ const userInfo = await this.loadUserInfo();
+ if (userInfo) {
+ success = true;
+ break;
+ }
+ } catch (err) {
+ console.error('[dashboard] 尝试拉取 user_info 时出错:', err);
+ }
+ // 指数退避等待
+ await new Promise(res => setTimeout(res, 300 * attempt));
+ }
+ if (!success) {
+ console.warn('[dashboard] 多次尝试后仍无法获取 user_info');
+ this.showProfileRetryPrompt();
+ }
+ }
+
+ // 认证检查(如果没有 token,会在页面内显示登录提示)
if (!this.checkAuth()) return;
- // 加载用户信息
- await this.loadUserInfo();
-
const userInfo = UserAuth.getUserInfo();
if (userInfo) {
this.updateUserDisplay(userInfo);
@@ -44,11 +91,56 @@ const Dashboard = {
checkAuth() {
const token = UserAuth.getToken();
if (!token) {
- window.location.href = '/user/login';
+ // 不再直接重定向到登录页,避免在某些环境下导致页面闪现为空白。
+ // 改为在页面内显示友好的登录提示,用户可以点击跳转登录。
+ this.showLoginPrompt();
return false;
}
return true;
},
+
+ /**
+ * 在页面中间显示登录提示(当用户未登录或 token 缺失时)
+ */
+ showLoginPrompt() {
+ try {
+ const container = document.querySelector('.container') || document.body;
+ // 避免重复创建
+ if (document.getElementById('dashboard-login-prompt')) return;
+
+ const prompt = document.createElement('div');
+ prompt.id = 'dashboard-login-prompt';
+ prompt.style.position = 'fixed';
+ prompt.style.left = '50%';
+ prompt.style.top = '50%';
+ prompt.style.transform = 'translate(-50%, -50%)';
+ prompt.style.zIndex = '9999';
+ prompt.style.background = 'rgba(255,255,255,0.96)';
+ prompt.style.padding = '24px 32px';
+ prompt.style.borderRadius = '8px';
+ prompt.style.boxShadow = '0 6px 20px rgba(0,0,0,0.12)';
+ prompt.style.textAlign = 'center';
+ prompt.innerHTML = `
+
您尚未登录
+
要访问用户中心,请先登录账户。
+
+
+
+
+ `;
+
+ container.appendChild(prompt);
+
+ document.getElementById('dashboard-login-btn').addEventListener('click', () => {
+ window.location.href = '/user/login';
+ });
+ document.getElementById('dashboard-refresh-btn').addEventListener('click', () => {
+ window.location.reload();
+ });
+ } catch (err) {
+ console.error('显示登录提示失败:', err);
+ }
+ },
/**
* 更新用户显示信息
@@ -73,19 +165,82 @@ const Dashboard = {
const response = await fetch('/user/profile', {
headers: UserAuth.getAuthHeaders()
});
-
- if (response.ok) {
- const result = await response.json();
+ const result = await this.parseJsonSafe(response);
+ if (this.handleAuthError(result)) return null;
+ if (result && result.code === 200 && result.data) {
const userInfo = result.data;
UserAuth.setUserInfo(userInfo);
+ // 更新 UI 状态以反映登录状态
+ UserAuth.updateUI();
+ console.log('[dashboard] 已获取并保存 user_info');
return userInfo;
+ } else {
+ console.warn('[dashboard] /user/profile 返回结构非预期:', result);
+ return null;
}
} catch (error) {
console.error('获取用户信息失败:', error);
}
return null;
},
-
+
+ /**
+ * 当拉取 user_info 多次失败时,提供一个可操作提示(重试或重新登录)
+ */
+ showProfileRetryPrompt() {
+ try {
+ const container = document.querySelector('.container') || document.body;
+ // 避免重复创建
+ if (document.getElementById('dashboard-profile-retry')) return;
+
+ const prompt = document.createElement('div');
+ prompt.id = 'dashboard-profile-retry';
+ prompt.style.position = 'fixed';
+ prompt.style.left = '50%';
+ prompt.style.top = '60%';
+ prompt.style.transform = 'translate(-50%, -50%)';
+ prompt.style.zIndex = '9999';
+ prompt.style.background = 'rgba(255,255,255,0.96)';
+ prompt.style.padding = '16px 20px';
+ prompt.style.borderRadius = '6px';
+ prompt.style.boxShadow = '0 6px 20px rgba(0,0,0,0.12)';
+ prompt.style.textAlign = 'center';
+ prompt.innerHTML = `
+
获取用户信息失败
+
系统检测到你已登录(token 存在),但无法获取到账户信息,可能是网络或会话问题。
+
+
+
+
+ `;
+
+ container.appendChild(prompt);
+
+ document.getElementById('dashboard-retry-profile').addEventListener('click', async () => {
+ document.getElementById('dashboard-profile-retry').remove();
+ console.log('[dashboard] 用户触发重试获取 user_info');
+ await this.loadUserInfo();
+ const ui = UserAuth.getUserInfo();
+ if (ui) {
+ this.updateUserDisplay(ui);
+ this.loadDashboard();
+ } else {
+ // 如果仍失败,重新展示提示
+ this.showProfileRetryPrompt();
+ }
+ });
+
+ document.getElementById('dashboard-rel-login').addEventListener('click', () => {
+ // 清理本地登录信息并跳转登录页
+ UserAuth.removeToken();
+ UserAuth.removeUserInfo();
+ window.location.href = '/user/login';
+ });
+ } catch (err) {
+ console.error('显示 profile 重试提示失败:', err);
+ }
+ },
+
/**
* 切换标签页
*/
@@ -138,16 +293,14 @@ const Dashboard = {
const response = await fetch('/user/stats', {
headers: UserAuth.getAuthHeaders()
});
-
- if (response.ok) {
- const result = await response.json();
+ const result = await this.parseJsonSafe(response);
+ if (this.handleAuthError(result)) return;
+ if (result && result.code === 200 && result.data) {
const stats = result.data;
-
- // 更新统计卡片
this.updateStatsCards(stats);
-
- // 更新存储进度条
this.updateStorageProgress(stats);
+ } else {
+ console.warn('[dashboard] /user/stats 返回非预期结果:', result);
}
} catch (error) {
console.error('加载仪表板数据失败:', error);
@@ -208,13 +361,14 @@ const Dashboard = {
const response = await fetch(`/user/files?page=${page}&page_size=${this.pageSize}`, {
headers: UserAuth.getAuthHeaders()
});
-
- if (response.ok) {
- const result = await response.json();
- const files = result.data.files;
- const pagination = result.data.pagination;
-
+ const result = await this.parseJsonSafe(response);
+ if (this.handleAuthError(result)) return;
+ if (result && result.code === 200 && result.data) {
+ const files = result.data.files || [];
+ const pagination = result.data.pagination || { page: 1, total_pages: 1, total: 0 };
this.renderFilesList(files, pagination);
+ } else {
+ console.warn('[dashboard] /user/files 返回非预期结果:', result);
}
} catch (error) {
console.error('加载文件列表失败:', error);
@@ -271,10 +425,10 @@ const Dashboard = {
`;
files.forEach(file => {
- const fileName = file.file_name || (file.prefix + file.suffix);
+ const fileName = file.file_name || `文件-${file.code}`;
const uploadType = file.upload_type === 'authenticated' ? '认证上传' : '匿名上传';
const authRequired = file.require_auth ? '🔒' : '🔓';
- const fileExtension = fileName.split('.').pop().toUpperCase();
+ const fileExtension = fileName ? fileName.split('.').pop().toUpperCase() : 'FILE';
// 根据文件扩展名选择图标
const fileIcon = this.getFileIcon(fileExtension);
@@ -303,7 +457,7 @@ const Dashboard = {
📥 下载
-
@@ -413,21 +567,20 @@ const Dashboard = {
const response = await fetch('/user/profile', {
headers: UserAuth.getAuthHeaders()
});
-
- if (response.ok) {
- const result = await response.json();
+ const result = await this.parseJsonSafe(response);
+ if (this.handleAuthError(result)) return;
+ if (result && result.code === 200 && result.data) {
const profile = result.data;
-
const form = document.getElementById('profile-form');
if (form) {
- form.username.value = profile.username;
- form.email.value = profile.email;
- form.nickname.value = profile.nickname;
-
- // 处理日期字段,如果不存在则显示暂无数据
+ form.username.value = profile.username || '';
+ form.email.value = profile.email || '';
+ form.nickname.value = profile.nickname || '';
form.created_at.value = profile.created_at ? formatDateTime(profile.created_at) : '暂无数据';
form.last_login_at.value = profile.last_login_at ? formatDateTime(profile.last_login_at) : '暂无数据';
}
+ } else {
+ console.warn('[dashboard] /user/profile 返回非预期结果:', result);
}
} catch (error) {
console.error('加载个人资料失败:', error);
@@ -478,13 +631,13 @@ const Dashboard = {
method: 'DELETE',
headers: UserAuth.getAuthHeaders()
});
-
- if (response.ok) {
+ const result = await this.parseJsonSafe(response);
+ if (this.handleAuthError(result)) return;
+ if (result && result.code === 200) {
showNotification('文件删除成功', 'success');
this.loadMyFiles(this.currentPage);
} else {
- const result = await response.json();
- showNotification('删除失败: ' + (result.message || '未知错误'), 'error');
+ showNotification('删除失败: ' + (result && result.message ? result.message : '未知错误'), 'error');
}
} catch (error) {
console.error('删除文件失败:', error);
@@ -496,14 +649,52 @@ const Dashboard = {
* 设置文件上传
*/
setupFileUpload() {
- const uploadArea = document.querySelector('.upload-area');
+ this.setupFileInput();
+ this.setupDragAndDrop();
+ },
+
+ /**
+ * 设置文件输入
+ */
+ setupFileInput() {
const fileInput = document.getElementById('file-input');
+ const folderInput = document.getElementById('folder-input');
const uploadText = document.getElementById('upload-text');
- if (!uploadArea || !fileInput || !uploadText) return;
+ if (!fileInput || !folderInput || !uploadText) return;
- // 点击选择文件
- uploadArea.addEventListener('click', () => fileInput.click());
+ // 文件选择
+ fileInput.addEventListener('change', (e) => {
+ const file = e.target.files[0];
+ if (file) {
+ const fileSizeMB = (file.size / 1024 / 1024).toFixed(2);
+ uploadText.textContent = `已选择: ${file.name} (${fileSizeMB}MB)`;
+ // 清空文件夹输入
+ folderInput.value = '';
+ }
+ });
+
+ // 文件夹选择
+ folderInput.addEventListener('change', (e) => {
+ const files = e.target.files;
+ if (files.length > 0) {
+ this.updateFolderDisplay(files, uploadText);
+ // 清空文件输入
+ fileInput.value = '';
+ }
+ });
+ },
+
+ /**
+ * 设置拖拽上传
+ */
+ setupDragAndDrop() {
+ const uploadArea = document.querySelector('.upload-area');
+ const fileInput = document.getElementById('file-input');
+ const folderInput = document.getElementById('folder-input');
+ const uploadText = document.getElementById('upload-text');
+
+ if (!uploadArea || !fileInput || !folderInput || !uploadText) return;
// 拖拽上传
uploadArea.addEventListener('dragover', (e) => {
@@ -519,22 +710,64 @@ const Dashboard = {
e.preventDefault();
uploadArea.classList.remove('dragover');
- const files = e.dataTransfer.files;
- if (files.length > 0) {
- fileInput.files = files;
+ const files = Array.from(e.dataTransfer.files);
+ if (files.length === 0) return;
+
+ // 检查是否拖拽了文件夹(通过检查DataTransfer items)
+ const items = e.dataTransfer.items;
+ let hasFolders = false;
+
+ if (items) {
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+ if (item.webkitGetAsEntry && item.webkitGetAsEntry().isDirectory) {
+ hasFolders = true;
+ break;
+ }
+ }
+ }
+
+ if (hasFolders) {
+ // 文件夹拖拽,需要处理文件夹结构
+ this.handleFolderDrop(e.dataTransfer, uploadText);
+ } else if (files.length === 1) {
+ // 单文件
+ fileInput.files = e.dataTransfer.files;
const fileSizeMB = (files[0].size / 1024 / 1024).toFixed(2);
uploadText.textContent = `已选择: ${files[0].name} (${fileSizeMB}MB)`;
+ } else {
+ // 多文件,模拟文件夹上传
+ this.updateFolderDisplay(files, uploadText);
+ // 创建新的FileList并赋值给folderInput
+ const dt = new DataTransfer();
+ files.forEach(file => dt.items.add(file));
+ folderInput.files = dt.files;
}
});
+ },
+
+ /**
+ * 处理文件夹拖拽
+ */
+ async handleFolderDrop(dataTransfer, uploadText) {
+ // 这里可以实现更复杂的文件夹拖拽处理
+ // 目前先显示提示信息
+ uploadText.textContent = '检测到文件夹,请使用"选择文件夹"按钮';
+ },
+
+ /**
+ * 更新文件夹显示
+ */
+ updateFolderDisplay(files, uploadText) {
+ const fileCount = files.length;
+ let totalSize = 0;
- // 文件选择
- fileInput.addEventListener('change', (e) => {
- const file = e.target.files[0];
- if (file) {
- const fileSizeMB = (file.size / 1024 / 1024).toFixed(2);
- uploadText.textContent = `已选择: ${file.name} (${fileSizeMB}MB)`;
- }
- });
+ for (let i = 0; i < files.length; i++) {
+ totalSize += files[i].size;
+ }
+
+ const totalSizeMB = (totalSize / 1024 / 1024).toFixed(2);
+ uploadText.textContent = `已选择 ${fileCount} 个文件 (总计 ${totalSizeMB}MB)`;
},
/**
@@ -557,14 +790,20 @@ const Dashboard = {
e.preventDefault();
const fileInput = document.getElementById('file-input');
- const file = fileInput.files[0];
+ const folderInput = document.getElementById('folder-input');
- if (!file) {
- showNotification('请选择文件', 'error');
+ // 检查是单文件还是文件夹
+ if (fileInput.files.length > 0) {
+ // 单文件上传
+ const file = fileInput.files[0];
+ await this.handleFileUpload(e.target, file);
+ } else if (folderInput.files.length > 0) {
+ // 文件夹上传
+ await this.handleFolderUpload(e.target, folderInput.files);
+ } else {
+ showNotification('请选择文件或文件夹', 'error');
return;
}
-
- await this.handleFileUpload(e.target, file);
});
},
@@ -648,9 +887,18 @@ const Dashboard = {
// 重置表单
form.reset();
const uploadText = document.getElementById('upload-text');
+ const fileInput = document.getElementById('file-input');
+ const folderInput = document.getElementById('folder-input');
+
if (uploadText) {
uploadText.textContent = '点击选择文件或拖拽到此处';
}
+ if (fileInput) {
+ fileInput.value = '';
+ }
+ if (folderInput) {
+ folderInput.value = '';
+ }
// 刷新统计
this.loadDashboard();
@@ -670,6 +918,129 @@ const Dashboard = {
`;
showNotification('上传失败: ' + message, 'error');
},
+
+ /**
+ * 处理文件夹上传
+ */
+ async handleFolderUpload(form, files) {
+ const uploadBtn = document.getElementById('upload-btn');
+ const uploadProgress = document.getElementById('upload-progress');
+ const uploadProgressFill = document.getElementById('upload-progress-fill');
+ const uploadResult = document.getElementById('upload-result');
+
+ if (!uploadBtn || !uploadProgress || !uploadProgressFill || !uploadResult) return;
+
+ // 检查JSZip是否可用
+ if (typeof JSZip === 'undefined') {
+ this.showUploadError('JSZip库未加载,无法上传文件夹', uploadResult);
+ return;
+ }
+
+ uploadBtn.disabled = true;
+ uploadBtn.textContent = '压缩中...';
+ uploadProgress.style.display = 'block';
+
+ try {
+ // 创建ZIP文件
+ const zip = new JSZip();
+ const fileArray = Array.from(files);
+
+ // 获取文件夹名称(从第一个文件的路径中提取)
+ let folderName = 'folder';
+ if (fileArray.length > 0 && fileArray[0].webkitRelativePath) {
+ const pathParts = fileArray[0].webkitRelativePath.split('/');
+ folderName = pathParts[0] || 'folder';
+ }
+
+ // 添加所有文件到ZIP
+ for (let i = 0; i < fileArray.length; i++) {
+ const file = fileArray[i];
+ const relativePath = file.webkitRelativePath || file.name;
+ zip.file(relativePath, file);
+
+ // 更新进度(压缩阶段占50%)
+ const progress = Math.floor((i / fileArray.length) * 50);
+ uploadProgressFill.style.width = progress + '%';
+ }
+
+ uploadBtn.textContent = '生成压缩包...';
+
+ // 生成ZIP blob
+ const zipBlob = await zip.generateAsync({
+ type: 'blob',
+ compression: 'DEFLATE',
+ compressionOptions: { level: 6 }
+ }, (metadata) => {
+ // 更新压缩进度(50%-80%)
+ const progress = 50 + Math.floor(metadata.percent * 0.3);
+ uploadProgressFill.style.width = progress + '%';
+ });
+
+ uploadBtn.textContent = '上传中...';
+
+ // 创建新的File对象
+ const zipFile = new File([zipBlob], `${folderName}.zip`, { type: 'application/zip' });
+
+ // 上传ZIP文件
+ await this.uploadSingleFile(form, zipFile, uploadProgressFill, uploadResult);
+
+ } catch (error) {
+ console.error('文件夹上传失败:', error);
+ this.showUploadError(error.message, uploadResult);
+ } finally {
+ uploadBtn.disabled = false;
+ uploadBtn.textContent = '上传文件';
+ setTimeout(() => {
+ uploadProgress.style.display = 'none';
+ uploadProgressFill.style.width = '0%';
+ }, 1000);
+ }
+ },
+
+ /**
+ * 上传单个文件(用于文件夹上传中的ZIP文件)
+ */
+ async uploadSingleFile(form, file, uploadProgressFill, uploadResult) {
+ const formData = new FormData();
+ formData.append('file', file);
+ formData.append('expire_style', form.expire_style.value);
+ formData.append('expire_value', form.expire_value.value);
+ formData.append('require_auth', form.require_auth.checked ? 'true' : 'false');
+
+ return new Promise((resolve, reject) => {
+ const xhr = new XMLHttpRequest();
+
+ // 上传进度(80%-100%)
+ xhr.upload.addEventListener('progress', (e) => {
+ if (e.lengthComputable) {
+ const progress = 80 + Math.floor((e.loaded / e.total) * 20);
+ uploadProgressFill.style.width = progress + '%';
+ }
+ });
+
+ xhr.onload = () => {
+ if (xhr.status === 200) {
+ const result = JSON.parse(xhr.responseText);
+ if (result.code === 200) {
+ this.showUploadSuccess(result.data, uploadResult, form);
+ resolve(result);
+ } else {
+ reject(new Error(result.message));
+ }
+ } else {
+ reject(new Error('上传失败'));
+ }
+ };
+
+ xhr.onerror = () => {
+ reject(new Error('网络错误'));
+ };
+
+ xhr.open('POST', '/share/file/');
+ xhr.setRequestHeader('Authorization', 'Bearer ' + UserAuth.getToken());
+ xhr.send(formData);
+ });
+ },
/**
* 设置个人资料表单
@@ -692,10 +1063,10 @@ const Dashboard = {
headers: UserAuth.getAuthHeaders(),
body: JSON.stringify(data)
});
-
- if (response.ok) {
+ const result = await this.parseJsonSafe(response);
+ if (this.handleAuthError(result)) return;
+ if (result && result.code === 200) {
showNotification('资料更新成功', 'success');
-
// 更新本地存储的用户信息
const userInfo = UserAuth.getUserInfo();
if (userInfo) {
@@ -704,8 +1075,7 @@ const Dashboard = {
this.updateUserDisplay(userInfo);
}
} else {
- const result = await response.json();
- showNotification('更新失败: ' + result.message, 'error');
+ showNotification('更新失败: ' + (result && result.message ? result.message : '未知错误'), 'error');
}
} catch (error) {
showNotification('更新失败: ' + error.message, 'error');
diff --git a/themes/2025/js/main.js b/themes/2025/js/main.js
index f8967b5..6bdd670 100644
--- a/themes/2025/js/main.js
+++ b/themes/2025/js/main.js
@@ -17,6 +17,29 @@ class FileCodeBoxApp {
constructor() {
this.modules = [];
this.eventListeners = [];
+ // AbortController 用于统一管理并移除通过 signal 注册的事件监听器
+ this.abortController = new AbortController();
+ }
+
+ /**
+ * 统一注册全局事件监听器,优先使用 AbortController.signal(可统一取消),
+ * 不支持时退回到手动记录并在 destroy 时移除。
+ */
+ addGlobalListener(element, event, handler, options) {
+ try {
+ if (this.abortController && this.abortController.signal) {
+ // 合并 options,确保 signal 被传入
+ const opts = Object.assign({}, options || {}, { signal: this.abortController.signal });
+ element.addEventListener(event, handler, opts);
+ return;
+ }
+ } catch (err) {
+ // 有些浏览器可能不支持 signal 参数,我们将回退到手动管理
+ }
+
+ // 回退:手动注册并记录以便在 destroy 中移除
+ element.addEventListener(event, handler, options);
+ this.eventListeners.push({ element, event, handler });
}
/**
@@ -106,8 +129,8 @@ class FileCodeBoxApp {
* 设置全局事件监听器
*/
setupGlobalEvents() {
- // 页面可见性变化
- document.addEventListener('visibilitychange', () => {
+ // 页面可见性变化(使用统一注册函数以便回退处理)
+ this.addGlobalListener(document, 'visibilitychange', () => {
if (document.visibilityState === 'visible') {
// 页面变为可见时,检查用户状态
if (UserAuth.isLoggedIn()) {
@@ -117,24 +140,55 @@ class FileCodeBoxApp {
});
// 窗口大小变化
- window.addEventListener('resize', debounce(() => {
+ this.addGlobalListener(window, 'resize', debounce(() => {
this.handleResize();
}, 250));
// 在线/离线状态
- window.addEventListener('online', () => {
+ this.addGlobalListener(window, 'online', () => {
showNotification('网络连接已恢复', 'success');
});
-
- window.addEventListener('offline', () => {
+
+ this.addGlobalListener(window, 'offline', () => {
showNotification('网络连接已断开', 'warning');
});
// 键盘快捷键
- document.addEventListener('keydown', (e) => {
+ this.addGlobalListener(document, 'keydown', (e) => {
this.handleKeyboard(e);
});
-
+ // 诊断:捕获所有锚点点击,记录可能阻止导航的事件
+ const logAnchorClicks = function(e) {
+ try {
+ const target = e.target;
+ if (!target) return;
+ // 找到最近的锚点元素
+ const anchor = target.closest && target.closest('a');
+ if (anchor && anchor.href) {
+ // 只关注以 /user/ 开头或指向站内的链接
+ try {
+ const url = new URL(anchor.href, window.location.origin);
+ if (url.pathname.startsWith('/user')) {
+ console.log('[diag] anchor click', {
+ href: anchor.href,
+ pathname: url.pathname,
+ defaultPrevented: e.defaultPrevented,
+ button: e.button,
+ ctrlKey: e.ctrlKey,
+ metaKey: e.metaKey
+ });
+ }
+ } catch (err) {
+ console.log('[diag] anchor click (raw)', anchor.href, 'error parsing URL', err);
+ }
+ }
+ } catch (err) {
+ console.error('[diag] logAnchorClicks error', err);
+ }
+ };
+
+ this.addGlobalListener(document, 'click', logAnchorClicks);
+
console.log('全局事件监听器已设置');
}
@@ -244,9 +298,14 @@ class FileCodeBoxApp {
applyTemplateConfig() {
if (window.AppConfig) {
// 应用不透明度
- if (window.AppConfig.opacity && window.AppConfig.opacity !== '{{opacity}}') {
- document.body.style.opacity = window.AppConfig.opacity;
- }
+ if (window.AppConfig.opacity && window.AppConfig.opacity !== '{{opacity}}') {
+ // 如果 opacity 为字符串 '0' 或 '0.0',不应把整个页面设为完全透明。
+ // 只在解析为数字且大于0时应用不透明度设置。
+ const op = parseFloat(window.AppConfig.opacity);
+ if (!isNaN(op) && op > 0) {
+ document.body.style.opacity = op;
+ }
+ }
// 应用背景图片
if (window.AppConfig.background && window.AppConfig.background !== '{{background}}') {
@@ -288,14 +347,14 @@ class FileCodeBoxApp {
*/
setupGlobalErrorHandling() {
// 捕获未处理的Promise错误
- window.addEventListener('unhandledrejection', (event) => {
+ this.addGlobalListener(window, 'unhandledrejection', (event) => {
console.error('未处理的Promise错误:', event.reason);
showNotification('发生了未知错误', 'error');
event.preventDefault();
});
// 捕获JavaScript错误
- window.addEventListener('error', (event) => {
+ this.addGlobalListener(window, 'error', (event) => {
console.error('JavaScript错误:', event.error);
// 只在开发模式下显示详细错误
if (window.location.hostname === 'localhost') {
@@ -308,10 +367,17 @@ class FileCodeBoxApp {
* 销毁应用程序
*/
destroy() {
- // 清理事件监听器
- this.eventListeners.forEach(({ element, event, handler }) => {
- element.removeEventListener(event, handler);
- });
+ // 使用 AbortController 统一取消所有通过 signal 注册的监听器
+ try {
+ if (this.abortController) {
+ this.abortController.abort();
+ }
+ } catch (err) {
+ console.warn('AbortController abort 失败:', err);
+ }
+
+ // 如果还存在以手动方式记录的监听器,可选地清理数组(兼容历史代码)
+ this.eventListeners = [];
// 重置状态
AppState.initialized = false;
diff --git a/themes/2025/js/upload.js b/themes/2025/js/upload.js
index d6dc755..70c50ae 100644
--- a/themes/2025/js/upload.js
+++ b/themes/2025/js/upload.js
@@ -50,14 +50,29 @@ const FileUpload = {
*/
setupFileInput() {
const fileInput = document.getElementById('file-input');
- if (!fileInput) return;
+ const folderInput = document.getElementById('folder-input');
- fileInput.addEventListener('change', (e) => {
- const file = e.target.files[0];
- if (file) {
- this.updateFileDisplay(file);
- }
- });
+ if (fileInput) {
+ fileInput.addEventListener('change', (e) => {
+ const file = e.target.files[0];
+ if (file) {
+ this.updateFileDisplay(file);
+ // 清空文件夹选择器
+ if (folderInput) folderInput.value = '';
+ }
+ });
+ }
+
+ if (folderInput) {
+ folderInput.addEventListener('change', (e) => {
+ const files = e.target.files;
+ if (files.length > 0) {
+ this.updateFolderDisplay(files);
+ // 清空文件选择器
+ if (fileInput) fileInput.value = '';
+ }
+ });
+ }
},
/**
@@ -67,10 +82,10 @@ const FileUpload = {
const uploadArea = document.querySelector('.upload-area');
if (!uploadArea) return;
- // 点击选择文件
- uploadArea.addEventListener('click', () => {
- document.getElementById('file-input')?.click();
- });
+ // 移除原有的点击事件,改用标签按钮处理
+ // uploadArea.addEventListener('click', () => {
+ // document.getElementById('file-input')?.click();
+ // });
// 拖拽事件
uploadArea.addEventListener('dragover', (e) => {
@@ -89,10 +104,22 @@ const FileUpload = {
const files = e.dataTransfer.files;
if (files.length > 0) {
- const fileInput = document.getElementById('file-input');
- if (fileInput) {
- fileInput.files = files;
- this.updateFileDisplay(files[0]);
+ // 检查是否是文件夹拖拽 (通过检查第一个文件的webkitRelativePath)
+ const firstFile = files[0];
+ if (firstFile.webkitRelativePath) {
+ // 文件夹拖拽
+ const folderInput = document.getElementById('folder-input');
+ if (folderInput) {
+ // 注意:不能直接设置folderInput.files,需要通过其他方式处理
+ this.updateFolderDisplay(files);
+ }
+ } else {
+ // 单文件拖拽
+ const fileInput = document.getElementById('file-input');
+ if (fileInput) {
+ fileInput.files = files;
+ this.updateFileDisplay(files[0]);
+ }
}
}
});
@@ -122,6 +149,28 @@ const FileUpload = {
}
},
+ /**
+ * 更新文件夹显示
+ */
+ updateFolderDisplay(files) {
+ const uploadText = document.querySelector('.upload-text');
+ if (uploadText && files.length > 0) {
+ const totalSize = Array.from(files).reduce((sum, file) => sum + file.size, 0);
+ const totalSizeMB = (totalSize / 1024 / 1024).toFixed(2);
+
+ // 获取文件夹名称(从第一个文件的路径中提取)
+ const firstFile = files[0];
+ const folderName = firstFile.webkitRelativePath ?
+ firstFile.webkitRelativePath.split('/')[0] :
+ '未知文件夹';
+
+ uploadText.textContent = `已选择文件夹: ${folderName} (${files.length}个文件, ${totalSizeMB}MB)`;
+
+ // 存储文件夹信息供上传使用
+ this.currentFolderFiles = files;
+ }
+ },
+
/**
* 验证文件
*/
@@ -146,28 +195,101 @@ const FileUpload = {
*/
async handleFileUpload(event) {
const fileInput = document.getElementById('file-input');
- const file = fileInput?.files[0];
+ const folderInput = document.getElementById('folder-input');
+
+ // 检查是单文件还是文件夹
+ let files = [];
+ if (fileInput?.files?.length > 0) {
+ files = [fileInput.files[0]];
+ } else if (folderInput?.files?.length > 0) {
+ files = Array.from(folderInput.files);
+ } else if (this.currentFolderFiles?.length > 0) {
+ files = Array.from(this.currentFolderFiles);
+ }
- if (!file) {
- showNotification('请选择文件', 'error');
+ if (files.length === 0) {
+ showNotification('请选择文件或文件夹', 'error');
return;
}
try {
- // 验证文件
- this.validateFile(file);
+ if (files.length === 1) {
+ // 单文件上传
+ await this.uploadSingleFile(files[0], event);
+ } else {
+ // 文件夹上传(多文件)
+ await this.uploadMultipleFiles(files, event);
+ }
- // 获取表单数据
+ } catch (error) {
+ showNotification(error.message, 'error');
+ }
+ },
+
+ /**
+ * 上传单个文件
+ */
+ async uploadSingleFile(file, event) {
+ // 验证文件
+ this.validateFile(file);
+
+ // 获取表单数据
+ const formData = new FormData();
+ formData.append('file', file);
+ formData.append('expire_style', event.target.expire_style.value);
+ formData.append('expire_value', event.target.expire_value.value);
+
+ // 开始上传
+ await this.uploadFile(formData, file);
+ },
+
+ /**
+ * 上传多个文件(文件夹)
+ */
+ async uploadMultipleFiles(files, event) {
+ // 创建压缩包
+ showNotification('正在打包文件夹,请稍候...', 'info');
+
+ try {
+ // 使用JSZip创建压缩包
+ if (typeof JSZip === 'undefined') {
+ throw new Error('文件夹上传功能需要加载JSZip库,请刷新页面重试');
+ }
+
+ const zip = new JSZip();
+
+ // 添加文件到压缩包
+ for (const file of files) {
+ const relativePath = file.webkitRelativePath || file.name;
+ zip.file(relativePath, file);
+ }
+
+ // 生成压缩包
+ const zipBlob = await zip.generateAsync({
+ type: 'blob',
+ compression: 'DEFLATE',
+ compressionOptions: { level: 6 }
+ });
+
+ // 创建文件对象
+ const folderName = files[0].webkitRelativePath ?
+ files[0].webkitRelativePath.split('/')[0] :
+ 'folder';
+ const zipFile = new File([zipBlob], `${folderName}.zip`, { type: 'application/zip' });
+
+ // 验证压缩包大小
+ this.validateFile(zipFile);
+
+ // 上传压缩包
const formData = new FormData();
- formData.append('file', file);
+ formData.append('file', zipFile);
formData.append('expire_style', event.target.expire_style.value);
formData.append('expire_value', event.target.expire_value.value);
- // 开始上传
- await this.uploadFile(formData, file);
+ await this.uploadFile(formData, zipFile);
} catch (error) {
- showNotification(error.message, 'error');
+ throw new Error('文件夹打包失败: ' + error.message);
}
},
@@ -337,6 +459,7 @@ const FileUpload = {
const progressText = document.getElementById('progress-text');
const uploadBtn = document.getElementById('upload-btn');
const fileInput = document.getElementById('file-input');
+ const folderInput = document.getElementById('folder-input');
const uploadText = document.querySelector('.upload-text');
if (progressContainer) progressContainer.classList.remove('show');
@@ -344,6 +467,10 @@ const FileUpload = {
if (progressFill) progressFill.style.width = '0%';
if (progressText) progressText.textContent = '0%';
if (fileInput) fileInput.value = '';
+ if (folderInput) folderInput.value = '';
if (uploadText) uploadText.textContent = '点击选择文件或拖拽到此处';
+
+ // 清空文件夹文件缓存
+ this.currentFolderFiles = null;
}
};
\ No newline at end of file
diff --git a/themes/2025/login.html b/themes/2025/login.html
index 98f8e8f..a7efca0 100644
--- a/themes/2025/login.html
+++ b/themes/2025/login.html
@@ -87,7 +87,7 @@
// 用户系统始终启用,直接显示登录界面
loginWrapper.classList.remove('hidden-content');
- if (result.code === 200 && result.data && result.data.allow_user_registration) {
+ if (result.code === 200 && result.data && result.data.allow_user_registration === 1) {
// 允许注册,显示注册区域
registerSection.classList.remove('hidden-content');
console.log('用户注册已启用');
@@ -130,20 +130,48 @@
const result = await response.json();
if (result.code === 200) {
+ // 存储 token
localStorage.setItem('user_token', result.data.token);
- showMessage('登录成功!正在跳转...', false);
-
+ console.log('[login] 登录成功,已保存 token');
+ showMessage('登录成功!正在获取用户信息...', false);
+
+ // 立即请求用户信息并保存到 localStorage,以便前端其它模块不会因为缺少 user_info 而阻塞渲染
+ try {
+ const profileResp = await fetch('/user/profile', {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': 'Bearer ' + result.data.token
+ }
+ });
+
+ const profileResult = await profileResp.json();
+ if (profileResult && profileResult.code === 200 && profileResult.data) {
+ localStorage.setItem('user_info', JSON.stringify(profileResult.data));
+ console.log('[login] 已获取并保存 user_info');
+ } else {
+ console.warn('[login] 获取 user_info 未返回预期结果', profileResult);
+ }
+ } catch (err) {
+ console.error('[login] 获取用户信息失败:', err);
+ // 不阻塞跳转,仅告知用户
+ showMessage('登录成功,但获取用户信息失败,稍后可能需要刷新页面', true);
+ }
+
+ // 跳转到期望页面(使用 replace 更可靠,不会留下历史记录)
setTimeout(() => {
- // 检查是否需要跳转回管理页面
const redirectUrl = sessionStorage.getItem('redirect_after_login');
if (redirectUrl) {
sessionStorage.removeItem('redirect_after_login');
- window.location.href = redirectUrl;
+ console.log('[login] 跳转到 redirect_after_login:', redirectUrl);
+ window.location.replace(redirectUrl);
} else {
- window.location.href = '/user/dashboard';
+ console.log('[login] 跳转到 /user/dashboard');
+ window.location.replace('/user/dashboard');
}
- }, 1000);
+ }, 800);
} else {
+ console.log('[login] 登录失败响应:', result);
showMessage(result.message || '登录失败,请检查用户名和密码');
}
} catch (error) {
diff --git a/themes/2025/register.html b/themes/2025/register.html
index 93dac3d..caa2578 100644
--- a/themes/2025/register.html
+++ b/themes/2025/register.html
@@ -97,7 +97,8 @@
systemInfo = data.data;
// 用户系统始终可用(系统已初始化),只检查是否允许注册
- if (!systemInfo.allow_user_registration) {
+ // 后端返回 0/1 表示开关,使用严格比较
+ if (systemInfo.allow_user_registration !== 1) {
showAlert('当前不允许用户注册', 'error');
document.getElementById('registerForm').style.display = 'none';
return;
@@ -320,7 +321,8 @@
const btnText = registerBtn.querySelector('.btn-text');
// 检查系统是否允许注册
- if (systemInfo && !systemInfo.allow_user_registration) {
+ // 使用严格比较,后端返回 0/1
+ if (systemInfo && systemInfo.allow_user_registration !== 1) {
showAlert('当前不允许用户注册', 'error');
return;
}
diff --git a/themes/2025/setup.html b/themes/2025/setup.html
index 19f002e..f0f2a07 100644
--- a/themes/2025/setup.html
+++ b/themes/2025/setup.html
@@ -236,19 +236,41 @@
创建系统管理员账户,用于管理系统配置和用户。
-