const { createApp } = Vue; createApp({ data() { return { // API配置 // API配置 - 通过nginx代理访问 apiBase: window.location.protocol + '//' + window.location.host, // 用户状态 isLoggedIn: false, user: null, token: null, refreshToken: null, tokenRefreshTimer: null, // 视图状态 currentView: 'files', isLogin: true, fileViewMode: 'grid', // 文件显示模式: grid 大图标, list 列表 shareViewMode: 'list', // 分享显示模式: grid 大图标, list 列表 debugMode: false, // 调试模式(管理员可切换) adminTab: 'overview', // 管理员页面当前标签:overview, settings, monitor, users, tools // 表单数据 loginForm: { username: '', password: '', captcha: '' }, registerForm: { username: '', email: '', password: '', captcha: '' }, registerCaptchaUrl: '', // 验证码相关 showCaptcha: false, captchaUrl: '', // SFTP配置表单 ftpConfigForm: { ftp_host: '', ftp_port: 22, ftp_user: '', ftp_password: '', http_download_base_url: '' }, showFtpConfigModal: false, // 修改密码表单 changePasswordForm: { current_password: '', new_password: '' }, // 用户名修改表单 usernameForm: { newUsername: '' }, currentPath: '/', files: [], loading: false, // 分享管理 shares: [], showShareAllModal: false, showShareFileModal: false, shareAllForm: { password: "", expiryType: "never", customDays: 7 }, shareFileForm: { fileName: "", filePath: "", isDirectory: false, // 新增:标记是否为文件夹 password: "", expiryType: "never", customDays: 7 }, shareResult: null, // 文件重命名 showRenameModal: false, renameForm: { oldName: "", newName: "", path: "" }, // 创建文件夹 showCreateFolderModal: false, createFolderForm: { folderName: "" }, // 文件夹详情 showFolderInfoModal: false, folderInfo: null, // 上传 showUploadModal: false, uploadProgress: 0, uploadedBytes: 0, totalBytes: 0, uploadingFileName: '', isDragging: false, modalMouseDownTarget: null, // 模态框鼠标按下的目标 // 上传工具下载 downloadingTool: false, // 管理员 adminUsers: [], showResetPwdModal: false, resetPwdUser: {}, newPassword: '', // 文件审查 showFileInspectionModal: false, inspectionUser: null, inspectionFiles: [], inspectionPath: '/', inspectionLoading: false, inspectionViewMode: 'grid', // 文件审查显示模式: grid 大图标, list 列表 // 忘记密码 showForgotPasswordModal: false, forgotPasswordForm: { email: '', captcha: '' }, forgotPasswordCaptchaUrl: '', showResetPasswordModal: false, resetPasswordForm: { token: '', new_password: '' }, showResendVerify: false, resendVerifyEmail: '', resendVerifyCaptcha: '', resendVerifyCaptchaUrl: '', // 系统设置 systemSettings: { maxUploadSizeMB: 100, smtp: { host: '', port: 465, secure: true, user: '', from: '', password: '', has_password: false } }, // 健康检测 healthCheck: { loading: false, lastCheck: null, overallStatus: null, // healthy, warning, critical summary: { total: 0, pass: 0, warning: 0, fail: 0, info: 0 }, checks: [] }, // 系统日志 systemLogs: { loading: false, logs: [], total: 0, page: 1, pageSize: 30, totalPages: 0, filters: { level: '', category: '', keyword: '' } }, // Toast通知 toasts: [], toastIdCounter: 0, // 上传限制(字节),默认10GB maxUploadSize: 10737418240, // 提示信息 errorMessage: '', successMessage: '', verifyMessage: '', // 存储相关 storageType: 'sftp', // 当前使用的存储类型 storagePermission: 'sftp_only', // 存储权限 localQuota: 0, // 本地存储配额(字节) localUsed: 0, // 本地存储已使用(字节) // 右键菜单 showContextMenu: false, contextMenuX: 0, contextMenuY: 0, contextMenuFile: null, // 长按检测 longPressTimer: null, longPressStartX: 0, longPressStartY: 0, longPressFile: null, // 媒体预览 showImageViewer: false, showVideoPlayer: false, showAudioPlayer: false, currentMediaUrl: '', currentMediaName: '', currentMediaType: '', // 'image', 'video', 'audio' longPressDuration: 500, // 长按时间(毫秒) // 管理员编辑用户存储权限 showEditStorageModal: false, editStorageForm: { userId: null, username: '', storage_permission: 'sftp_only', local_storage_quota_value: 1, // 配额数值 quota_unit: 'GB' // 配额单位:MB 或 GB }, // 服务器存储统计 serverStorageStats: { totalDisk: 0, usedDisk: 0, availableDisk: 0, totalUserQuotas: 0, totalUserUsed: 0, totalUsers: 0 }, // 定期检查用户配置更新的定时器 profileCheckInterval: null, // 上传工具管理 uploadToolStatus: null, // 上传工具状态 { exists, fileInfo: { size, sizeMB, modifiedAt } } checkingUploadTool: false, // 是否正在检测上传工具 uploadingTool: false, // 是否正在上传工具 // 存储切换状态 storageSwitching: false, storageSwitchTarget: null, suppressStorageToast: false, profileInitialized: false, // SFTP配置引导弹窗 showSftpGuideModal: false, showSftpConfigModal: false, // SFTP空间使用统计 sftpUsage: null, // { totalSize, totalSizeFormatted, fileCount, dirCount } sftpUsageLoading: false, sftpUsageError: null, // 主题设置 currentTheme: 'dark', // 当前生效的主题: 'dark' 或 'light' globalTheme: 'dark', // 全局默认主题(管理员设置) userThemePreference: null // 用户主题偏好: 'dark', 'light', 或 null(跟随全局) }; }, computed: { pathParts() { return this.currentPath.split('/').filter(p => p !== ''); }, // 格式化配额显示 localQuotaFormatted() { return this.formatBytes(this.localQuota); }, localUsedFormatted() { return this.formatBytes(this.localUsed); }, // 配额使用百分比 quotaPercentage() { if (this.localQuota === 0) return 0; return Math.round((this.localUsed / this.localQuota) * 100); }, // 存储类型显示文本 storageTypeText() { return this.storageType === 'local' ? '本地存储' : 'SFTP存储'; } }, methods: { // ========== 主题管理 ========== // 初始化主题 async initTheme() { // 先从localStorage读取,避免页面闪烁 const savedTheme = localStorage.getItem('theme'); if (savedTheme) { this.applyTheme(savedTheme); } // 如果没有登录,从公开API获取全局主题 if (!this.token) { try { const res = await axios.get(`${this.apiBase}/api/public/theme`); if (res.data.success) { this.globalTheme = res.data.theme; this.applyTheme(res.data.theme); } } catch (e) { console.log('无法加载全局主题'); } } }, // 加载用户主题设置(登录后调用) 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 { console.log('[主题] 设置全局主题:', theme); const res = await axios.post(`${this.apiBase}/api/admin/settings`, { global_theme: theme }, { headers: { Authorization: `Bearer ${this.token}` }} ); console.log('[主题] API响应:', res.data); if (res.data.success) { this.globalTheme = theme; console.log('[主题] globalTheme已更新为:', this.globalTheme); // 如果用户没有设置个人偏好,则跟随全局 if (this.userThemePreference === null) { this.currentTheme = theme; this.applyTheme(theme); localStorage.setItem('theme', theme); } else { console.log('[主题] 用户有个人偏好,不更改当前显示主题:', this.userThemePreference); } // 提示信息 let toastMsg = theme === 'dark' ? '默认暗色主题' : '默认亮色主题'; if (this.userThemePreference !== null) { toastMsg += '(你设置了个人偏好,不受全局影响)'; } this.showToast('success', '全局主题已更新', toastMsg); } else { console.error('[主题] API返回失败:', res.data); this.showToast('error', '设置失败', res.data.message || '未知错误'); } } catch (error) { console.error('[主题] 设置全局主题失败:', error); this.showToast('error', '设置失败', error.response?.data?.message || '请稍后重试'); } }, // 提取URL中的token(兼容缺少 ? 的场景) getTokenFromUrl(key) { const currentHref = window.location.href; const url = new URL(currentHref); let token = url.searchParams.get(key); if (!token) { const match = currentHref.match(new RegExp(`${key}=([\\w-]+)`)); if (match && match[1]) { token = match[1]; } } return token; }, // 清理URL中的token(同时处理路径和查询参数) sanitizeUrlToken(key) { const url = new URL(window.location.href); url.searchParams.delete(key); if (url.pathname.includes(`${key}=`)) { url.pathname = url.pathname.split(`${key}=`)[0]; } window.history.replaceState({}, document.title, url.toString()); }, // 模态框点击外部关闭优化 - 防止拖动选择文本时误关闭 modalMouseDownTarget: null, handleModalMouseDown(e) { // 记录鼠标按下时的目标 this.modalMouseDownTarget = e.target; }, handleModalMouseUp(modalName) { // 只有在同一个overlay元素上按下和释放鼠标时才关闭 return (e) => { if (e.target === this.modalMouseDownTarget) { this[modalName] = false; this.shareResult = null; // 重置分享结果 } this.modalMouseDownTarget = null; }; }, // 格式化文件大小 formatFileSize(bytes) { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i]; }, // 拖拽上传处理 handleDragEnter(e) { e.preventDefault(); e.stopPropagation(); this.isDragging = true; }, handleDragOver(e) { e.preventDefault(); e.stopPropagation(); this.isDragging = true; }, handleDragLeave(e) { e.preventDefault(); e.stopPropagation(); // 使用更可靠的检测:检查鼠标实际位置 const container = e.currentTarget; const rect = container.getBoundingClientRect(); const x = e.clientX; const y = e.clientY; // 如果鼠标位置在容器边界外,隐藏覆盖层 // 添加5px的容差,避免边界问题 const margin = 5; const isOutside = x < rect.left - margin || x > rect.right + margin || y < rect.top - margin || y > rect.bottom + margin; if (isOutside) { this.isDragging = false; return; } // 备用检测:检查 relatedTarget const related = e.relatedTarget; if (!related || !container.contains(related)) { this.isDragging = false; } }, async handleDrop(e) { e.preventDefault(); e.stopPropagation(); this.isDragging = false; const files = e.dataTransfer.files; if (files.length > 0) { const file = files[0]; await this.uploadFile(file); } }, // ===== 认证相关 ===== toggleAuthMode() { this.isLogin = !this.isLogin; this.errorMessage = ''; this.successMessage = ''; // 切换到注册模式时加载验证码 if (!this.isLogin) { this.refreshRegisterCaptcha(); } }, async handleLogin() { this.errorMessage = ''; try { const response = await axios.post(`${this.apiBase}/api/login`, this.loginForm); if (response.data.success) { this.token = response.data.token; this.refreshToken = response.data.refreshToken; this.user = response.data.user; this.isLoggedIn = true; this.showResendVerify = false; this.resendVerifyEmail = ''; // 登录成功后隐藏验证码并清空验证码输入 this.showCaptcha = false; this.loginForm.captcha = ''; // 保存token到localStorage localStorage.setItem('token', this.token); localStorage.setItem('refreshToken', this.refreshToken); localStorage.setItem('user', JSON.stringify(this.user)); // 启动token自动刷新(在过期前5分钟刷新) const expiresIn = response.data.expiresIn || (2 * 60 * 60 * 1000); this.startTokenRefresh(expiresIn); // 直接从登录响应中获取存储信息 this.storagePermission = this.user.storage_permission || 'sftp_only'; this.storageType = this.user.current_storage_type || 'sftp'; this.localQuota = this.user.local_storage_quota || 0; this.localUsed = this.user.local_storage_used || 0; console.log('[登录] 存储权限:', this.storagePermission, '存储类型:', this.storageType, 'SFTP配置:', this.user.has_ftp_config); // 智能存储类型修正:如果当前是SFTP但未配置,且用户有本地存储权限,自动切换到本地 if (this.storageType === 'sftp' && !this.user.has_ftp_config) { if (this.storagePermission === 'local_only' || this.storagePermission === 'user_choice') { console.log('[登录] SFTP未配置但用户有本地存储权限,自动切换到本地存储'); this.storageType = 'local'; // 异步更新到后端(不等待,避免阻塞登录流程) axios.post( `${this.apiBase}/api/user/switch-storage`, { storage_type: 'local' }, { headers: { Authorization: `Bearer ${this.token}` } } ).catch(err => console.error('[登录] 自动切换存储类型失败:', err)); } } // 启动定期检查用户配置 this.startProfileSync(); // 加载用户主题设置 this.loadUserTheme(); // 管理员直接跳转到管理后台 if (this.user.is_admin) { this.currentView = 'admin'; } // 普通用户:检查存储权限 else { // 如果用户可以使用本地存储,直接进入文件页面 if (this.storagePermission === 'local_only' || this.storagePermission === 'user_choice') { this.currentView = 'files'; this.loadFiles('/'); } // 如果仅SFTP模式,需要检查是否配置了SFTP else if (this.storagePermission === 'sftp_only') { if (this.user.has_ftp_config) { this.currentView = 'files'; this.loadFiles('/'); } else { this.currentView = 'settings'; alert('欢迎!请先配置您的SFTP服务器'); this.openSftpConfigModal(); } } else { // 默认行为:跳转到文件页面 this.currentView = 'files'; this.loadFiles('/'); } } } } catch (error) { this.errorMessage = error.response?.data?.message || '登录失败'; // 检查是否需要显示验证码 if (error.response?.data?.needCaptcha) { this.showCaptcha = true; this.refreshCaptcha(); } // 邮箱未验证提示 if (error.response?.data?.needVerify) { this.showResendVerify = true; this.resendVerifyEmail = error.response?.data?.email || this.loginForm.username || ''; } else { this.showResendVerify = false; this.resendVerifyEmail = ''; } } }, // 通用验证码加载函数(带防抖) async loadCaptcha(targetField) { // 防抖:2秒内不重复请求 const now = Date.now(); if (this._lastCaptchaTime && (now - this._lastCaptchaTime) < 2000) { console.log('[验证码] 请求过于频繁,跳过'); return; } this._lastCaptchaTime = now; try { const response = await axios.get(`${this.apiBase}/api/captcha?t=${now}`, { responseType: 'blob' }); this[targetField] = URL.createObjectURL(response.data); } catch (error) { console.error('获取验证码失败:', error); // 如果是429错误,不清除已有验证码 if (error.response?.status !== 429) { this[targetField] = ''; } } }, // 刷新验证码(登录) refreshCaptcha() { this.loadCaptcha('captchaUrl'); }, // 刷新注册验证码 refreshRegisterCaptcha() { this.loadCaptcha('registerCaptchaUrl'); }, // 刷新忘记密码验证码 refreshForgotPasswordCaptcha() { this.loadCaptcha('forgotPasswordCaptchaUrl'); }, // 刷新重发验证邮件验证码 refreshResendVerifyCaptcha() { this.loadCaptcha('resendVerifyCaptchaUrl'); }, async resendVerification() { if (!this.resendVerifyEmail) { this.showToast('error', '错误', '请输入邮箱或用户名后再重试'); return; } if (!this.resendVerifyCaptcha) { this.showToast('error', '错误', '请输入验证码'); return; } try { const payload = { captcha: this.resendVerifyCaptcha }; if (this.resendVerifyEmail.includes('@')) { payload.email = this.resendVerifyEmail; } else { payload.username = this.resendVerifyEmail; } const response = await axios.post(`${this.apiBase}/api/resend-verification`, payload); if (response.data.success) { this.showToast('success', '成功', '验证邮件已发送,请查收'); this.showResendVerify = false; this.resendVerifyEmail = ''; this.resendVerifyCaptcha = ''; this.resendVerifyCaptchaUrl = ''; } } catch (error) { console.error('重发验证邮件失败:', error); this.showToast('error', '错误', error.response?.data?.message || '发送失败'); // 刷新验证码 this.resendVerifyCaptcha = ''; this.refreshResendVerifyCaptcha(); } }, async handleVerifyToken(token) { try { const response = await axios.get(`${this.apiBase}/api/verify-email`, { params: { token } }); if (response.data.success) { this.verifyMessage = '邮箱验证成功,请登录'; this.isLogin = true; // 清理URL this.sanitizeUrlToken('verifyToken'); } } catch (error) { console.error('邮箱验证失败:', error); this.verifyMessage = error.response?.data?.message || '验证失败'; } }, async handleRegister() { this.errorMessage = ''; this.successMessage = ''; try { const response = await axios.post(`${this.apiBase}/api/register`, this.registerForm); if (response.data.success) { this.successMessage = '注册成功!请查收邮箱完成验证后再登录'; this.isLogin = true; // 清空表单 this.registerForm = { username: '', email: '', password: '', captcha: '' }; this.registerCaptchaUrl = ''; } } catch (error) { const errorData = error.response?.data; if (errorData?.errors) { this.errorMessage = errorData.errors.map(e => e.msg).join(', '); } else { this.errorMessage = errorData?.message || '注册失败'; } // 刷新验证码 this.registerForm.captcha = ''; this.refreshRegisterCaptcha(); } }, async updateFtpConfig() { try { const response = await axios.post( `${this.apiBase}/api/user/update-ftp`, this.ftpConfigForm, { headers: { Authorization: `Bearer ${this.token}` } } ); if (response.data.success) { alert('SFTP配置已保存!'); // 更新用户信息 this.user.has_ftp_config = 1; // 如果用户有 user_choice 权限,自动切换到 SFTP 存储 if (this.storagePermission === 'user_choice' || this.storagePermission === 'sftp_only') { try { const switchResponse = await axios.post( `${this.apiBase}/api/user/switch-storage`, { storage_type: 'sftp' }, { headers: { Authorization: `Bearer ${this.token}` } } ); if (switchResponse.data.success) { this.storageType = 'sftp'; console.log('[SFTP配置] 已自动切换到SFTP存储模式'); } } catch (err) { console.error('[SFTP配置] 自动切换存储模式失败:', err); } } // 关闭配置弹窗 this.showSftpConfigModal = false; // 刷新到文件页面 this.currentView = 'files'; this.loadFiles('/'); } } catch (error) { alert('配置失败: ' + (error.response?.data?.message || error.message)); } }, async updateAdminProfile() { try { const response = await axios.post( `${this.apiBase}/api/admin/update-profile`, { username: this.adminProfileForm.username }, { headers: { Authorization: `Bearer ${this.token}` } } ); if (response.data.success) { alert('用户名已更新!请重新登录。'); // 更新token和用户信息 if (response.data.token) { this.token = response.data.token; localStorage.setItem('token', response.data.token); } if (response.data.user) { this.user = response.data.user; localStorage.setItem('user', JSON.stringify(response.data.user)); } // 重新登录 this.logout(); } } catch (error) { alert('修改失败: ' + (error.response?.data?.message || error.message)); } }, async changePassword() { if (!this.changePasswordForm.current_password) { alert('请输入当前密码'); return; } if (this.changePasswordForm.new_password.length < 6) { alert('新密码至少6个字符'); return; } try { const response = await axios.post( `${this.apiBase}/api/user/change-password`, { current_password: this.changePasswordForm.current_password, new_password: this.changePasswordForm.new_password }, { headers: { Authorization: `Bearer ${this.token}` } } ); if (response.data.success) { alert('密码修改成功!'); this.changePasswordForm.new_password = ''; this.changePasswordForm.current_password = ''; } } catch (error) { alert('密码修改失败: ' + (error.response?.data?.message || error.message)); } }, async loadFtpConfig() { try { const response = await axios.get( `${this.apiBase}/api/user/profile`, { headers: { Authorization: `Bearer ${this.token}` } } ); if (response.data.success && response.data.user) { const user = response.data.user; // 填充SFTP配置表单(密码不回显) this.ftpConfigForm.ftp_host = user.ftp_host || ''; this.ftpConfigForm.ftp_port = user.ftp_port || 22; this.ftpConfigForm.ftp_user = user.ftp_user || ''; this.ftpConfigForm.ftp_password = ''; // 密码不回显 this.ftpConfigForm.http_download_base_url = user.http_download_base_url || ''; } } catch (error) { console.error('加载SFTP配置失败:', error); } }, // 处理配置文件上传 handleConfigFileUpload(event) { const file = event.target.files[0]; if (!file) return; this.processConfigFile(file); // 清空文件选择,允许重复选择同一文件 event.target.value = ''; }, // 处理配置文件拖拽 handleConfigFileDrop(event) { const file = event.dataTransfer.files[0]; if (!file) return; // 检查文件扩展名 if (!file.name.toLowerCase().endsWith('.inf')) { this.showToast('error', '错误', '只支持 .inf 格式的配置文件'); return; } this.processConfigFile(file); // 恢复背景色 event.currentTarget.style.background = '#f8f9ff'; }, // 处理配置文件 async processConfigFile(file) { const reader = new FileReader(); reader.onload = async (e) => { try { const content = e.target.result; const config = this.parseConfigFile(content); if (config) { // 填充表单 this.ftpConfigForm.ftp_host = config.ip || ''; this.ftpConfigForm.ftp_port = config.port || 22; this.ftpConfigForm.ftp_user = config.id || ''; this.ftpConfigForm.ftp_password = config.pw || ''; this.ftpConfigForm.http_download_base_url = config.arr || ''; // 提示用户配置已导入,需要确认后保存 this.showToast('success', '成功', '配置文件已导入!请检查并确认信息后点击"保存配置"按钮'); } else { this.showToast('error', '错误', '配置文件格式不正确,请检查文件内容'); } } catch (error) { console.error('解析配置文件失败:', error); this.showToast('error', '错误', '解析配置文件失败: ' + error.message); } }; reader.readAsText(file); }, // 解析INI格式的配置文件 parseConfigFile(content) { const lines = content.split('\n'); const config = {}; for (let line of lines) { line = line.trim(); // 跳过空行和注释 if (!line || line.startsWith('#') || line.startsWith(';') || line.startsWith('[')) { continue; } // 解析 key=value 格式 const equalsIndex = line.indexOf('='); if (equalsIndex > 0) { const key = line.substring(0, equalsIndex).trim(); const value = line.substring(equalsIndex + 1).trim(); config[key] = value; } } // 验证必需字段 if (config.ip && config.id && config.pw && config.port) { return config; } return null; }, async updateUsername() { if (!this.usernameForm.newUsername || this.usernameForm.newUsername.length < 3) { alert('用户名至少3个字符'); return; } try { const response = await axios.post( `${this.apiBase}/api/user/update-username`, { username: this.usernameForm.newUsername }, { headers: { Authorization: `Bearer ${this.token}` } } ); if (response.data.success) { alert('用户名修改成功!请重新登录'); // 更新本地用户信息 this.user.username = this.usernameForm.newUsername; localStorage.setItem('user', JSON.stringify(this.user)); this.usernameForm.newUsername = ''; } } catch (error) { alert('用户名修改失败: ' + (error.response?.data?.message || error.message)); } }, async updateProfile() { try { const response = await axios.post( `${this.apiBase}/api/user/update-profile`, { email: this.profileForm.email }, { headers: { Authorization: `Bearer ${this.token}` } } ); if (response.data.success) { alert('邮箱已更新!'); // 更新本地用户信息 if (response.data.user) { this.user = response.data.user; localStorage.setItem('user', JSON.stringify(this.user)); } } } catch (error) { alert('更新失败: ' + (error.response?.data?.message || error.message)); } }, logout() { this.isLoggedIn = false; this.user = null; this.token = null; this.refreshToken = null; this.stopTokenRefresh(); localStorage.removeItem('token'); localStorage.removeItem('refreshToken'); localStorage.removeItem('user'); localStorage.removeItem('lastView'); this.showResendVerify = false; this.resendVerifyEmail = ''; // 停止定期检查 this.stopProfileSync(); }, // 获取公开的系统配置(上传限制等) async loadPublicConfig() { try { const response = await axios.get(`${this.apiBase}/api/config`); if (response.data.success) { this.maxUploadSize = response.data.config.max_upload_size || 10737418240; } } catch (error) { console.error('获取系统配置失败:', error); // 使用默认值 } }, // 检查本地存储的登录状态 async checkLoginStatus() { const token = localStorage.getItem('token'); const refreshToken = localStorage.getItem('refreshToken'); const user = localStorage.getItem('user'); if (token && user) { this.token = token; this.refreshToken = refreshToken; this.user = JSON.parse(user); // 先验证token是否有效 try { const response = await axios.get( `${this.apiBase}/api/user/profile`, { headers: { Authorization: `Bearer ${token}` } } ); if (response.data.success && response.data.user) { // token有效,更新用户信息 this.user = response.data.user; this.isLoggedIn = true; // 更新localStorage中的用户信息 localStorage.setItem('user', JSON.stringify(this.user)); // 从最新的用户信息初始化存储相关字段 this.storagePermission = this.user.storage_permission || 'sftp_only'; this.storageType = this.user.current_storage_type || 'sftp'; this.localQuota = this.user.local_storage_quota || 0; this.localUsed = this.user.local_storage_used || 0; console.log('[页面加载] Token验证成功,存储权限:', this.storagePermission, '存储类型:', this.storageType); // 启动token自动刷新(假设剩余1.5小时,实际由服务端控制) this.startTokenRefresh(1.5 * 60 * 60 * 1000); // 启动定期检查用户配置 this.startProfileSync(); // 加载用户主题设置 this.loadUserTheme(); // 读取上次停留的视图(需合法才生效) const savedView = localStorage.getItem('lastView'); let targetView = null; if (savedView && this.isViewAllowed(savedView)) { targetView = savedView; } else if (this.user.is_admin) { targetView = 'admin'; } else if (this.storagePermission === 'sftp_only' && !this.user.has_ftp_config) { targetView = 'settings'; } else { targetView = 'files'; } // 强制切换到目标视图并加载数据 this.switchView(targetView, true); } else { // 响应异常,尝试刷新token await this.tryRefreshOrLogout(); } } catch (error) { console.warn('[页面加载] Token验证失败:', error.response?.status || error.message); // token无效或过期,尝试使用refresh token刷新 if (error.response?.status === 401 && this.refreshToken) { console.log('[页面加载] 尝试使用refresh token刷新...'); const refreshed = await this.doRefreshToken(); if (refreshed) { // 刷新成功,重新检查登录状态 await this.checkLoginStatus(); return; } } // 刷新失败或无refresh token,清除登录状态 this.handleTokenExpired(); } } }, // 尝试刷新token,失败则登出 async tryRefreshOrLogout() { if (this.refreshToken) { const refreshed = await this.doRefreshToken(); if (refreshed) { await this.checkLoginStatus(); return; } } this.handleTokenExpired(); }, // 处理token过期/失效 handleTokenExpired() { console.log('[认证] Token已失效,清除登录状态'); this.isLoggedIn = false; this.user = null; this.token = null; this.refreshToken = null; this.stopTokenRefresh(); localStorage.removeItem('token'); localStorage.removeItem('refreshToken'); localStorage.removeItem('user'); localStorage.removeItem('lastView'); this.stopProfileSync(); }, // 启动token自动刷新定时器 startTokenRefresh(expiresIn) { this.stopTokenRefresh(); // 先清除旧的定时器 // 在token过期前5分钟刷新 const refreshTime = Math.max(expiresIn - 5 * 60 * 1000, 60 * 1000); console.log(`[认证] Token将在 ${Math.round(refreshTime / 60000)} 分钟后刷新`); this.tokenRefreshTimer = setTimeout(async () => { await this.doRefreshToken(); }, refreshTime); }, // 停止token刷新定时器 stopTokenRefresh() { if (this.tokenRefreshTimer) { clearTimeout(this.tokenRefreshTimer); this.tokenRefreshTimer = null; } }, // 执行token刷新 async doRefreshToken() { if (!this.refreshToken) { console.log('[认证] 无refresh token,无法刷新'); return false; } try { console.log('[认证] 正在刷新access token...'); const response = await axios.post(`${this.apiBase}/api/refresh-token`, { refreshToken: this.refreshToken }); if (response.data.success) { this.token = response.data.token; localStorage.setItem('token', this.token); console.log('[认证] Token刷新成功'); // 继续下一次刷新 const expiresIn = response.data.expiresIn || (2 * 60 * 60 * 1000); this.startTokenRefresh(expiresIn); return true; } } catch (error) { console.error('[认证] Token刷新失败:', error.response?.data?.message || error.message); // 刷新失败,需要重新登录 this.handleTokenExpired(); } return false; }, // 检查URL参数 checkUrlParams() { const urlParams = new URLSearchParams(window.location.search); const action = urlParams.get('action'); if (action === 'login') { this.isLogin = true; } else if (action === 'register') { this.isLogin = false; } }, // ===== 文件管理 ===== async loadFiles(path) { this.loading = true; // 确保路径不为undefined this.currentPath = path || '/'; try { const response = await axios.get(`${this.apiBase}/api/files`, { params: { path }, headers: { Authorization: `Bearer ${this.token}` } }); if (response.data.success) { this.files = response.data.items; // 更新存储类型信息 if (response.data.storageType) { this.storageType = response.data.storageType; } if (response.data.storagePermission) { this.storagePermission = response.data.storagePermission; } // 更新用户本地存储信息 await this.loadUserProfile(); } } catch (error) { console.error('加载文件失败:', error); alert('加载文件失败: ' + (error.response?.data?.message || error.message)); if (error.response?.status === 401) { this.logout(); } } finally { this.loading = false; } }, handleFileClick(file) { if (file.isDirectory) { const newPath = this.currentPath === '/' ? `/${file.name}` : `${this.currentPath}/${file.name}`; this.loadFiles(newPath); } else { // 检查文件类型,打开相应的预览 if (file.name.match(/\.(jpg|jpeg|png|gif|bmp|svg|webp)$/i)) { this.openImageViewer(file); } else if (file.name.match(/\.(mp4|avi|mov|wmv|flv|mkv|webm)$/i)) { this.openVideoPlayer(file); } else if (file.name.match(/\.(mp3|wav|flac|aac|ogg|m4a)$/i)) { this.openAudioPlayer(file); } // 其他文件类型不做任何操作,用户可以通过右键菜单下载 } }, navigateToPath(path) { this.loadFiles(path); }, navigateToIndex(index) { const parts = this.pathParts.slice(0, index + 1); const path = '/' + parts.join('/'); this.loadFiles(path); }, // 返回上一级目录 navigateUp() { if (this.currentPath === '/') return; const parts = this.currentPath.split('/').filter(p => p !== ''); parts.pop(); const newPath = parts.length === 0 ? '/' : '/' + parts.join('/'); this.loadFiles(newPath); }, downloadFile(file) { console.log("[DEBUG] 下载文件:", file); // SFTP存储且有HTTP直链,新窗口打开直接下载(避免Mixed Content问题) if (file.httpDownloadUrl) { window.open(file.httpDownloadUrl, '_blank'); return; } // 本地存储,使用隐藏链接触发下载 const url = `${this.apiBase}/api/files/download?path=${encodeURIComponent(this.currentPath === '/' ? `/${file.name}` : `${this.currentPath}/${file.name}`)}`; const link = document.createElement('a'); link.href = url; link.setAttribute('download', file.name); document.body.appendChild(link); link.click(); document.body.removeChild(link); }, // ===== 文件操作 ===== openRenameModal(file) { this.renameForm.oldName = file.name; this.renameForm.newName = file.name; this.renameForm.path = this.currentPath; this.showRenameModal = true; }, async renameFile() { if (!this.renameForm.newName || this.renameForm.newName === this.renameForm.oldName) { alert('请输入新的文件名'); return; } try { const response = await axios.post( `${this.apiBase}/api/files/rename`, this.renameForm, { headers: { Authorization: `Bearer ${this.token}` } } ); if (response.data.success) { this.showToast('success', '成功', '文件已重命名'); this.showRenameModal = false; this.loadFiles(this.currentPath); } } catch (error) { console.error('重命名失败:', error); this.showToast('error', '错误', error.response?.data?.message || '重命名失败'); } }, // 创建文件夹 async createFolder() { const folderName = this.createFolderForm.folderName.trim(); if (!folderName) { this.showToast('error', '错误', '请输入文件夹名称'); return; } // 前端验证文件夹名称 if (folderName.includes('/') || folderName.includes('\\') || folderName.includes('..') || folderName.includes(':')) { this.showToast('error', '错误', '文件夹名称不能包含特殊字符 (/ \\ .. :)'); return; } try { const response = await axios.post(`${this.apiBase}/api/files/mkdir`, { path: this.currentPath, folderName: folderName }, { headers: { 'Authorization': `Bearer ${this.token}` } }); if (response.data.success) { this.showToast('success', '成功', '文件夹创建成功'); this.showCreateFolderModal = false; this.createFolderForm.folderName = ''; await this.loadFiles(this.currentPath); // 刷新文件列表 } } catch (error) { console.error('[创建文件夹失败]', error); this.showToast('error', '错误', error.response?.data?.message || '创建文件夹失败'); } }, // 显示文件夹详情 async showFolderInfo(file) { if (!file.isDirectory) { this.showToast('error', '错误', '只能查看文件夹详情'); return; } this.showFolderInfoModal = true; this.folderInfo = null; // 先清空,显示加载中 try { const response = await axios.post(`${this.apiBase}/api/files/folder-info`, { path: this.currentPath, folderName: file.name }, { headers: { 'Authorization': `Bearer ${this.token}` } }); if (response.data.success) { this.folderInfo = response.data.data; } } catch (error) { console.error('[获取文件夹详情失败]', error); this.showToast('error', '错误', error.response?.data?.message || '获取文件夹详情失败'); this.showFolderInfoModal = false; } }, confirmDeleteFile(file) { const fileType = file.isDirectory ? '文件夹' : '文件'; const warning = file.isDirectory ? "\n⚠️ 警告:文件夹内所有文件将被永久删除!" : ""; if (confirm(`确定要删除${fileType} "${file.name}" 吗?此操作无法撤销!${warning}`)) { this.deleteFile(file); } }, // ===== 右键菜单和长按功能 ===== // 显示右键菜单(PC端) showFileContextMenu(file, event) { // 文件和文件夹都可以显示右键菜单 event.preventDefault(); this.contextMenuFile = file; this.contextMenuX = event.clientX; this.contextMenuY = event.clientY; this.showContextMenu = true; // 点击其他地方关闭菜单 this.$nextTick(() => { document.addEventListener('click', this.hideContextMenu, { once: true }); }); }, // 隐藏右键菜单 hideContextMenu() { this.showContextMenu = false; this.contextMenuFile = null; }, // 长按开始(移动端) handleLongPressStart(file, event) { if (file.isDirectory) return; // 文件夹不响应长按 // 记录初始触摸位置,用于检测是否在滑动 const touch = event.touches[0]; this.longPressStartX = touch.clientX; this.longPressStartY = touch.clientY; this.longPressFile = file; this.longPressTimer = setTimeout(() => { // 触发长按菜单 this.contextMenuFile = file; // 使用记录的触摸位置 this.contextMenuX = this.longPressStartX; this.contextMenuY = this.longPressStartY; this.showContextMenu = true; // 触摸震动反馈(如果支持) if (navigator.vibrate) { navigator.vibrate(50); } // 点击其他地方关闭菜单 this.$nextTick(() => { document.addEventListener('click', this.hideContextMenu, { once: true }); }); }, this.longPressDuration); }, // 长按移动检测(移动端)- 滑动时取消长按 handleLongPressMove(event) { if (!this.longPressTimer) return; const touch = event.touches[0]; const moveX = Math.abs(touch.clientX - this.longPressStartX); const moveY = Math.abs(touch.clientY - this.longPressStartY); // 如果移动超过10px,认为是滑动,取消长按 if (moveX > 10 || moveY > 10) { clearTimeout(this.longPressTimer); this.longPressTimer = null; } }, // 长按取消(移动端) handleLongPressEnd() { if (this.longPressTimer) { clearTimeout(this.longPressTimer); this.longPressTimer = null; } }, // 从菜单执行操作 contextMenuAction(action) { if (!this.contextMenuFile) return; switch (action) { case 'preview': // 根据文件类型打开对应的预览 if (this.contextMenuFile.name.match(/\.(jpg|jpeg|png|gif|bmp|svg|webp)$/i)) { this.openImageViewer(this.contextMenuFile); } else if (this.contextMenuFile.name.match(/\.(mp4|avi|mov|wmv|flv|mkv|webm)$/i)) { this.openVideoPlayer(this.contextMenuFile); } else if (this.contextMenuFile.name.match(/\.(mp3|wav|flac|aac|ogg|m4a)$/i)) { this.openAudioPlayer(this.contextMenuFile); } break; case 'download': this.downloadFile(this.contextMenuFile); break; case 'rename': this.openRenameModal(this.contextMenuFile); break; case 'info': this.showFolderInfo(this.contextMenuFile); break; case 'share': this.openShareFileModal(this.contextMenuFile); break; case 'delete': this.confirmDeleteFile(this.contextMenuFile); break; } this.hideContextMenu(); }, // ===== 媒体预览功能 ===== // 获取媒体文件URL getMediaUrl(file) { const filePath = this.currentPath === '/' ? `/${file.name}` : `${this.currentPath}/${file.name}`; // SFTP存储且配置了HTTP下载URL,使用HTTP直接访问;否则使用API下载 if (file.httpDownloadUrl) { return file.httpDownloadUrl; } // 本地存储或未配置HTTP URL,使用API下载(同域 Cookie 验证) return `${this.apiBase}/api/files/download?path=${encodeURIComponent(filePath)}`; }, // 获取文件缩略图URL getThumbnailUrl(file) { if (!file || file.isDirectory) return null; // 检查是否是图片或视频 const isImage = file.name.match(/\.(jpg|jpeg|png|gif|bmp|svg|webp)$/i); const isVideo = file.name.match(/\.(mp4|avi|mov|wmv|flv|mkv|webm)$/i); if (!isImage && !isVideo) return null; return this.getMediaUrl(file); }, // 打开图片预览 openImageViewer(file) { this.currentMediaUrl = this.getMediaUrl(file); this.currentMediaName = file.name; this.currentMediaType = 'image'; this.showImageViewer = true; }, // 打开视频播放器 openVideoPlayer(file) { this.currentMediaUrl = this.getMediaUrl(file); this.currentMediaName = file.name; this.currentMediaType = 'video'; this.showVideoPlayer = true; }, // 打开音频播放器 openAudioPlayer(file) { this.currentMediaUrl = this.getMediaUrl(file); this.currentMediaName = file.name; this.currentMediaType = 'audio'; this.showAudioPlayer = true; }, // 关闭媒体预览 closeMediaViewer() { this.showImageViewer = false; this.showVideoPlayer = false; this.showAudioPlayer = false; this.currentMediaUrl = ''; this.currentMediaName = ''; this.currentMediaType = ''; }, // 下载当前预览的媒体文件 downloadCurrentMedia() { if (!this.currentMediaUrl) return; // 创建临时a标签触发下载 const link = document.createElement('a'); link.href = this.currentMediaUrl; link.setAttribute('download', this.currentMediaName); document.body.appendChild(link); link.click(); document.body.removeChild(link); }, // 判断文件是否支持预览 isPreviewable(file) { if (!file || file.isDirectory) return false; return file.name.match(/\.(jpg|jpeg|png|gif|bmp|svg|webp|mp4|avi|mov|wmv|flv|mkv|webm|mp3|wav|flac|aac|ogg|m4a)$/i); }, async deleteFile(file) { try { const response = await axios.post( `${this.apiBase}/api/files/delete`, { fileName: file.name, path: this.currentPath, isDirectory: file.isDirectory }, { headers: { Authorization: `Bearer ${this.token}` } } ); if (response.data.success) { this.showToast('success', '成功', '文件已删除'); this.loadFiles(this.currentPath); } } catch (error) { console.error('删除失败:', error); this.showToast('error', '错误', error.response?.data?.message || '删除失败'); } }, downloadUploadTool() { try { this.downloadingTool = true; this.showToast('info', '提示', '正在生成上传工具,下载即将开始...'); // 使用标签下载,通过URL参数传递token,浏览器会显示下载进度 const link = document.createElement('a'); link.href = `${this.apiBase}/api/upload/download-tool`; link.setAttribute('download', `玩玩云上传工具_${this.user.username}.zip`); document.body.appendChild(link); link.click(); document.body.removeChild(link); // 延迟重置按钮状态,给下载一些启动时间 setTimeout(() => { this.downloadingTool = false; this.showToast('success', '提示', '下载已开始,请查看浏览器下载进度'); }, 2000); } catch (error) { console.error('下载上传工具失败:', error); this.showToast('error', '错误', '下载失败'); this.downloadingTool = false; } }, // ===== 分享功能 ===== openShareFileModal(file) { this.shareFileForm.fileName = file.name; this.shareFileForm.filePath = this.currentPath === '/' ? file.name : `${this.currentPath}/${file.name}`; this.shareFileForm.isDirectory = file.isDirectory; // 设置是否为文件夹 this.shareFileForm.password = ''; this.shareFileForm.expiryType = 'never'; this.shareFileForm.customDays = 7; this.shareResult = null; // 清空上次的分享结果 this.showShareFileModal = true; }, async createShareAll() { try { const expiryDays = this.shareAllForm.expiryType === 'never' ? null : this.shareAllForm.expiryType === 'custom' ? this.shareAllForm.customDays : parseInt(this.shareAllForm.expiryType); const response = await axios.post( `${this.apiBase}/api/share/create`, { share_type: 'all', password: this.shareAllForm.password || null, expiry_days: expiryDays }, { headers: { Authorization: `Bearer ${this.token}` } } ); if (response.data.success) { this.shareResult = response.data; this.showToast('success', '成功', '分享链接已创建'); this.loadShares(); } } catch (error) { console.error('创建分享失败:', error); this.showToast('error', '错误', error.response?.data?.message || '创建分享失败'); } }, async createShareFile() { try { const expiryDays = this.shareFileForm.expiryType === 'never' ? null : this.shareFileForm.expiryType === 'custom' ? this.shareFileForm.customDays : parseInt(this.shareFileForm.expiryType); // 根据是否为文件夹决定share_type const shareType = this.shareFileForm.isDirectory ? 'directory' : 'file'; const response = await axios.post( `${this.apiBase}/api/share/create`, { share_type: shareType, // 修复:文件夹使用directory类型 file_path: this.shareFileForm.filePath, file_name: this.shareFileForm.fileName, password: this.shareFileForm.password || null, expiry_days: expiryDays }, { headers: { Authorization: `Bearer ${this.token}` } } ); if (response.data.success) { this.shareResult = response.data; const itemType = this.shareFileForm.isDirectory ? '文件夹' : '文件'; this.showToast('success', '成功', `${itemType}分享链接已创建`); this.loadShares(); } } catch (error) { console.error('创建分享失败:', error); this.showToast('error', '错误', error.response?.data?.message || '创建分享失败'); } }, // ===== 文件上传 ===== handleFileSelect(event) { const files = event.target.files; if (files && files.length > 0) { // 支持多文件上传 Array.from(files).forEach(file => { this.uploadFile(file); }); // 清空input,允许重复上传相同文件 event.target.value = ''; } }, handleFileDrop(event) { this.isDragging = false; const file = event.dataTransfer.files[0]; if (file) { this.uploadFile(file); } }, async uploadFile(file) { // 文件大小限制预检查(在上传前检查,避免用户等待上传完才发现超限) if (file.size > this.maxUploadSize) { const fileSizeMB = Math.round(file.size / (1024 * 1024)); const maxSizeMB = Math.round(this.maxUploadSize / (1024 * 1024)); this.showToast( 'error', '文件超过上传限制', `文件大小 ${fileSizeMB}MB 超过系统限制 ${maxSizeMB}MB,请选择更小的文件` ); return; } // 本地存储配额预检查 if (this.storageType === 'local') { const estimatedUsage = this.localUsed + file.size; if (estimatedUsage > this.localQuota) { this.showToast( 'error', '配额不足', `文件大小 ${this.formatBytes(file.size)},剩余配额 ${this.formatBytes(this.localQuota - this.localUsed)},无法上传` ); return; } // 如果使用率将超过90%,给出警告 const willExceed90 = (estimatedUsage / this.localQuota) > 0.9; if (willExceed90) { const confirmed = confirm( `警告:上传此文件后将使用 ${Math.round((estimatedUsage / this.localQuota) * 100)}% 的配额。是否继续?` ); if (!confirmed) return; } } const formData = new FormData(); formData.append('file', file); formData.append('path', this.currentPath); try { // 设置上传文件名和进度 this.uploadingFileName = file.name; this.uploadProgress = 0; this.uploadedBytes = 0; this.totalBytes = 0; const response = await axios.post(`${this.apiBase}/api/upload`, formData, { headers: { 'Authorization': `Bearer ${this.token}`, 'Content-Type': 'multipart/form-data' }, timeout: 30 * 60 * 1000, // 30分钟超时,支持大文件上传 onUploadProgress: (progressEvent) => { this.uploadProgress = Math.round((progressEvent.loaded * 100) / progressEvent.total); this.uploadedBytes = progressEvent.loaded; this.totalBytes = progressEvent.total; } }); if (response.data.success) { // 显示成功提示 this.showToast('success', '上传成功', `文件 ${file.name} 已上传`); // 重置上传进度 this.uploadProgress = 0; this.uploadedBytes = 0; this.totalBytes = 0; this.uploadingFileName = ''; // 自动刷新文件列表 await this.loadFiles(this.currentPath); } } catch (error) { console.error('上传失败:', error); // 重置上传进度 this.uploadProgress = 0; this.uploadedBytes = 0; this.totalBytes = 0; this.uploadingFileName = ''; // 处理文件大小超限错误 if (error.response?.status === 413) { const errorData = error.response.data; // 判断响应是JSON还是HTML(Nginx返回HTML,Backend返回JSON) if (typeof errorData === 'object' && errorData.maxSize && errorData.fileSize) { // Backend返回的JSON响应 const maxSizeMB = Math.round(errorData.maxSize / (1024 * 1024)); const fileSizeMB = Math.round(errorData.fileSize / (1024 * 1024)); this.showToast( 'error', '文件超过上传限制', `文件大小 ${fileSizeMB}MB 超过限制 ${maxSizeMB}MB` ); } else { // Nginx返回的HTML响应,显示通用消息 const fileSizeMB = Math.round(file.size / (1024 * 1024)); this.showToast( 'error', '文件超过上传限制', `文件大小 ${fileSizeMB}MB 超过系统限制,请联系管理员` ); } } else { this.showToast('error', '上传失败', error.response?.data?.message || error.message); } } }, // ===== 分享管理 ===== async loadShares() { try { const response = await axios.get(`${this.apiBase}/api/share/my`, { headers: { Authorization: `Bearer ${this.token}` } }); if (response.data.success) { this.shares = response.data.shares; } } catch (error) { console.error('加载分享列表失败:', error); alert('加载分享列表失败: ' + (error.response?.data?.message || error.message)); } }, async createShare() { this.shareForm.path = this.currentPath; try { const response = await axios.post(`${this.apiBase}/api/share/create`, this.shareForm, { headers: { Authorization: `Bearer ${this.token}` } }); if (response.data.success) { this.shareResult = response.data; this.loadShares(); } } catch (error) { console.error('创建分享失败:', error); alert('创建分享失败: ' + (error.response?.data?.message || error.message)); } }, async deleteShare(id) { if (!confirm('确定要删除这个分享吗?')) return; try { const response = await axios.delete(`${this.apiBase}/api/share/${id}`, { headers: { Authorization: `Bearer ${this.token}` } }); if (response.data.success) { alert('分享已删除'); this.loadShares(); } } catch (error) { console.error('删除分享失败:', error); alert('删除分享失败: ' + (error.response?.data?.message || error.message)); } }, // 格式化到期时间显示 formatExpireTime(expiresAt) { if (!expiresAt) return '永久有效'; const expireDate = new Date(expiresAt); const now = new Date(); const diffMs = expireDate - now; const diffMinutes = Math.floor(diffMs / (1000 * 60)); const diffHours = Math.floor(diffMs / (1000 * 60 * 60)); const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); // 格式化日期 const dateStr = expireDate.toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); if (diffMs < 0) { return `已过期 (${dateStr})`; } else if (diffMinutes < 60) { return `${diffMinutes}分钟后过期 (${dateStr})`; } else if (diffHours < 24) { return `${diffHours}小时后过期 (${dateStr})`; } else if (diffDays === 1) { return `明天过期 (${dateStr})`; } else if (diffDays <= 7) { return `${diffDays}天后过期 (${dateStr})`; } else { return dateStr; } }, // 判断是否即将过期(3天内) isExpiringSoon(expiresAt) { if (!expiresAt) return false; const expireDate = new Date(expiresAt); const now = new Date(); const diffMs = expireDate - now; const diffDays = diffMs / (1000 * 60 * 60 * 24); return diffDays > 0 && diffDays <= 3; }, // 判断是否已过期 isExpired(expiresAt) { if (!expiresAt) return false; const expireDate = new Date(expiresAt); const now = new Date(); return expireDate <= now; }, copyShareLink(url) { // 复制分享链接到剪贴板 if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(url).then(() => { this.showToast('success', '成功', '分享链接已复制到剪贴板'); }).catch(() => { this.fallbackCopyToClipboard(url); }); } else { this.fallbackCopyToClipboard(url); } }, fallbackCopyToClipboard(text) { // 备用复制方法 const textArea = document.createElement('textarea'); textArea.value = text; textArea.style.position = 'fixed'; textArea.style.left = '-999999px'; document.body.appendChild(textArea); textArea.select(); try { document.execCommand('copy'); this.showToast('success', '成功', '分享链接已复制到剪贴板'); } catch (err) { this.showToast('error', '错误', '复制失败,请手动复制'); } document.body.removeChild(textArea); }, // ===== 管理员功能 ===== async loadUsers() { try { const response = await axios.get(`${this.apiBase}/api/admin/users`, { headers: { Authorization: `Bearer ${this.token}` } }); if (response.data.success) { this.adminUsers = response.data.users; } } catch (error) { console.error('加载用户列表失败:', error); alert('加载用户列表失败: ' + (error.response?.data?.message || error.message)); } }, async banUser(userId, banned) { const action = banned ? '封禁' : '解封'; if (!confirm(`确定要${action}这个用户吗?`)) return; try { const response = await axios.post( `${this.apiBase}/api/admin/users/${userId}/ban`, { banned }, { headers: { Authorization: `Bearer ${this.token}` } } ); if (response.data.success) { alert(response.data.message); this.loadUsers(); } } catch (error) { console.error('操作失败:', error); alert('操作失败: ' + (error.response?.data?.message || error.message)); } }, async deleteUser(userId) { if (!confirm('确定要删除这个用户吗?此操作不可恢复!')) return; try { const response = await axios.delete(`${this.apiBase}/api/admin/users/${userId}`, { headers: { Authorization: `Bearer ${this.token}` } }); if (response.data.success) { alert('用户已删除'); this.loadUsers(); } } catch (error) { console.error('删除用户失败:', error); alert('删除用户失败: ' + (error.response?.data?.message || error.message)); } }, // ===== 忘记密码功能 ===== async requestPasswordReset() { if (!this.forgotPasswordForm.email) { this.showToast('error', '错误', '请输入注册邮箱'); return; } if (!this.forgotPasswordForm.captcha) { this.showToast('error', '错误', '请输入验证码'); return; } try { const response = await axios.post( `${this.apiBase}/api/password/forgot`, this.forgotPasswordForm ); if (response.data.success) { this.showToast('success', '成功', '如果邮箱存在,将收到重置邮件'); this.showForgotPasswordModal = false; this.forgotPasswordForm = { email: '', captcha: '' }; this.forgotPasswordCaptchaUrl = ''; } } catch (error) { console.error('提交密码重置请求失败:', error); this.showToast('error', '错误', error.response?.data?.message || '提交失败'); // 刷新验证码 this.forgotPasswordForm.captcha = ''; this.refreshForgotPasswordCaptcha(); } }, async submitResetPassword() { if (!this.resetPasswordForm.token || !this.resetPasswordForm.new_password || this.resetPasswordForm.new_password.length < 6) { this.showToast('error', '错误', '请输入有效的重置链接和新密码(至少6位)'); return; } try { const response = await axios.post(`${this.apiBase}/api/password/reset`, this.resetPasswordForm); if (response.data.success) { this.verifyMessage = '密码已重置,请登录'; this.isLogin = true; this.showResetPasswordModal = false; this.resetPasswordForm = { token: '', new_password: '' }; // 清理URL中的token this.sanitizeUrlToken('resetToken'); } } catch (error) { console.error('密码重置失败:', error); this.showToast('error', '错误', error.response?.data?.message || '重置失败'); } }, // ===== 管理员:文件审查功能 ===== async openFileInspection(user) { this.inspectionUser = user; this.inspectionPath = '/'; this.showFileInspectionModal = true; await this.loadUserFiles('/'); }, async loadUserFiles(path) { this.inspectionLoading = true; this.inspectionPath = path; try { const response = await axios.get( `${this.apiBase}/api/admin/users/${this.inspectionUser.id}/files`, { params: { path }, headers: { Authorization: `Bearer ${this.token}` } } ); if (response.data.success) { this.inspectionFiles = response.data.items; } } catch (error) { console.error('加载用户文件失败:', error); this.showToast('error', '错误', error.response?.data?.message || '加载文件失败'); } finally { this.inspectionLoading = false; } }, handleInspectionFileClick(file) { if (file.isDirectory) { const newPath = this.inspectionPath === '/' ? `/${file.name}` : `${this.inspectionPath}/${file.name}`; this.loadUserFiles(newPath); } }, navigateInspectionToRoot() { this.loadUserFiles('/'); }, navigateInspectionUp() { if (this.inspectionPath === '/') return; const lastSlash = this.inspectionPath.lastIndexOf('/'); const parentPath = lastSlash > 0 ? this.inspectionPath.substring(0, lastSlash) : '/'; this.loadUserFiles(parentPath); }, // ===== 存储管理 ===== // 加载用户个人资料(包含存储信息) async loadUserProfile() { try { const response = await axios.get( `${this.apiBase}/api/user/profile`, { headers: { Authorization: `Bearer ${this.token}` } } ); if (response.data.success && response.data.user) { const user = response.data.user; // 同步用户信息(含 has_ftp_config) this.user = { ...(this.user || {}), ...user }; // 检测存储配置是否被管理员更改 const oldStorageType = this.storageType; const oldStoragePermission = this.storagePermission; const newStorageType = user.current_storage_type || 'sftp'; const newStoragePermission = user.storage_permission || 'sftp_only'; // 更新本地数据 this.localQuota = user.local_storage_quota || 0; this.localUsed = user.local_storage_used || 0; this.storagePermission = newStoragePermission; this.storageType = newStorageType; // 首次加载仅同步,不提示 if (!this.profileInitialized) { this.profileInitialized = true; return; } // 如果存储类型被管理员更改,通知用户并重新加载文件 if (oldStorageType !== newStorageType || oldStoragePermission !== newStoragePermission) { console.log('[存储配置更新] 旧类型:', oldStorageType, '新类型:', newStorageType); console.log('[存储配置更新] 旧权限:', oldStoragePermission, '新权限:', newStoragePermission); if (!this.suppressStorageToast) { this.showToast('info', '存储配置已更新', `管理员已将您的存储方式更改为${newStorageType === 'local' ? '本地存储' : 'SFTP存储'}`); } else { this.suppressStorageToast = false; } // 如果当前在文件页面,重新加载文件列表 if (this.currentView === 'files') { await this.loadFiles(this.currentPath); } } } } catch (error) { console.error('加载用户资料失败:', error); } }, // 加载SFTP空间使用统计 async loadSftpUsage() { // 仅在用户已配置SFTP时才加载 if (!this.user?.has_ftp_config) { this.sftpUsage = null; return; } this.sftpUsageLoading = true; this.sftpUsageError = null; try { const response = await axios.get( `${this.apiBase}/api/user/sftp-usage`, { headers: { Authorization: `Bearer ${this.token}` } } ); if (response.data.success) { this.sftpUsage = response.data.usage; } } catch (error) { console.error('获取SFTP空间使用情况失败:', error); this.sftpUsageError = error.response?.data?.message || '获取失败'; } finally { this.sftpUsageLoading = false; } }, // 启动定期检查用户配置 startProfileSync() { // 清除已有的定时器 if (this.profileCheckInterval) { clearInterval(this.profileCheckInterval); } // 每30秒检查一次用户配置是否有更新 this.profileCheckInterval = setInterval(() => { if (this.isLoggedIn && this.token) { this.loadUserProfile(); } }, 30000); // 30秒 console.log('[配置同步] 已启动定期检查(30秒间隔)'); }, // 停止定期检查 stopProfileSync() { if (this.profileCheckInterval) { clearInterval(this.profileCheckInterval); this.profileCheckInterval = null; console.log('[配置同步] 已停止定期检查'); } }, // 用户切换存储方式 async switchStorage(type) { if (this.storageSwitching || type === this.storageType) { return; } // 切到SFTP但还未配置,引导弹窗 if (type === 'sftp' && (!this.user?.has_ftp_config)) { this.showSftpGuideModal = true; return; } this.storageSwitching = true; this.storageSwitchTarget = type; try { const response = await axios.post( `${this.apiBase}/api/user/switch-storage`, { storage_type: type }, { headers: { Authorization: `Bearer ${this.token}` } } ); if (response.data.success) { this.storageType = type; // 用户主动切换后,下一次配置同步不提示管理员修改 this.suppressStorageToast = true; this.showToast('success', '成功', `已切换到${type === 'local' ? '本地存储' : 'SFTP存储'}`); // 重新加载文件列表 if (this.currentView === 'files') { this.loadFiles(this.currentPath); } } } catch (error) { console.error('切换存储失败:', error); this.showToast('error', '错误', error.response?.data?.message || '切换存储失败'); } finally { this.storageSwitching = false; this.storageSwitchTarget = null; } }, ensureSftpConfigSection() { this.openSftpConfigModal(); }, openSftpGuideModal() { this.showSftpGuideModal = true; }, closeSftpGuideModal() { this.showSftpGuideModal = false; }, proceedSftpGuide() { this.showSftpGuideModal = false; this.ensureSftpConfigSection(); }, openSftpConfigModal() { this.showSftpGuideModal = false; this.showSftpConfigModal = true; if (this.user && !this.user.is_admin) { this.loadFtpConfig(); } }, closeSftpConfigModal() { this.showSftpConfigModal = false; }, // 检查视图权限 isViewAllowed(view) { if (!this.isLoggedIn) return false; const commonViews = ['files', 'shares', 'settings']; if (view === 'admin') { return !!(this.user && this.user.is_admin); } return commonViews.includes(view); }, // 切换视图并自动刷新数据 switchView(view, force = false) { if (this.isLoggedIn && !this.isViewAllowed(view)) { return; } // 如果已经在当前视图,不重复刷新 if (!force && this.currentView === view) { return; } this.currentView = view; // 根据视图类型自动加载对应数据 switch (view) { case 'files': // 切换到文件视图时,重新加载文件列表 this.loadFiles(this.currentPath); break; case 'shares': // 切换到分享视图时,重新加载分享列表 this.loadShares(); break; case 'admin': // 切换到管理后台时,重新加载用户列表、健康检测和系统日志 if (this.user && this.user.is_admin) { this.loadUsers(); this.loadServerStorageStats(); this.loadHealthCheck(); this.loadSystemLogs(1); } break; case 'settings': // 设置页面不需要额外加载数据 break; } }, // 管理员:打开编辑用户存储权限模态框 openEditStorageModal(user) { this.editStorageForm.userId = user.id; this.editStorageForm.username = user.username; this.editStorageForm.storage_permission = user.storage_permission || 'sftp_only'; // 智能识别配额单位 const quotaBytes = user.local_storage_quota || 1073741824; const quotaMB = quotaBytes / 1024 / 1024; const quotaGB = quotaMB / 1024; // 如果配额能被1024整除且大于等于1GB,使用GB单位,否则使用MB if (quotaMB >= 1024 && quotaMB % 1024 === 0) { this.editStorageForm.local_storage_quota_value = quotaGB; this.editStorageForm.quota_unit = 'GB'; } else { this.editStorageForm.local_storage_quota_value = Math.round(quotaMB); this.editStorageForm.quota_unit = 'MB'; } this.showEditStorageModal = true; }, // 管理员:更新用户存储权限 async updateUserStorage() { try { // 根据单位计算字节数 let quotaBytes; if (this.editStorageForm.quota_unit === 'GB') { quotaBytes = this.editStorageForm.local_storage_quota_value * 1024 * 1024 * 1024; } else { quotaBytes = this.editStorageForm.local_storage_quota_value * 1024 * 1024; } const response = await axios.post( `${this.apiBase}/api/admin/users/${this.editStorageForm.userId}/storage-permission`, { storage_permission: this.editStorageForm.storage_permission, local_storage_quota: quotaBytes }, { headers: { Authorization: `Bearer ${this.token}` } } ); if (response.data.success) { this.showToast('success', '成功', '存储权限已更新'); this.showEditStorageModal = false; this.loadUsers(); } } catch (error) { console.error('更新存储权限失败:', error); this.showToast('error', '错误', error.response?.data?.message || '更新失败'); } }, // ===== 工具函数 ===== formatBytes(bytes) { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]; }, formatDate(dateString) { if (!dateString) return '-'; // SQLite 返回的是 UTC 时间字符串,需要显式处理 // 如果字符串不包含时区信息,手动添加 'Z' 标记为 UTC let dateStr = dateString; if (!dateStr.includes('Z') && !dateStr.includes('+') && !dateStr.includes('T')) { // SQLite 格式: "2025-11-13 16:37:19" -> ISO格式: "2025-11-13T16:37:19Z" dateStr = dateStr.replace(' ', 'T') + 'Z'; } const date = new Date(dateStr); const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); const hours = String(date.getHours()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0'); return `${year}-${month}-${day} ${hours}:${minutes}`; }, // ===== Toast通知 ===== showToast(type, title, message) { const toast = { id: ++this.toastIdCounter, type, title, message, icon: type === 'error' ? 'fas fa-circle-exclamation' : type === 'success' ? 'fas fa-circle-check' : 'fas fa-circle-info', hiding: false }; // 清除之前的所有通知,只保留最新的一个 this.toasts = [toast]; // 4.5秒后开始淡出动画 setTimeout(() => { const index = this.toasts.findIndex(t => t.id === toast.id); if (index !== -1) { this.toasts[index].hiding = true; // 0.5秒后移除(动画时长) setTimeout(() => { const removeIndex = this.toasts.findIndex(t => t.id === toast.id); if (removeIndex !== -1) { this.toasts.splice(removeIndex, 1); } }, 500); } }, 4500); }, // ===== 系统设置管理 ===== async loadSystemSettings() { try { const response = await axios.get(`${this.apiBase}/api/admin/settings`, { headers: { Authorization: `Bearer ${this.token}` } }); if (response.data.success) { const settings = response.data.settings; this.systemSettings.maxUploadSizeMB = Math.round(settings.max_upload_size / (1024 * 1024)); // 加载全局主题设置 console.log('[主题] 从服务器加载全局主题:', settings.global_theme); if (settings.global_theme) { this.globalTheme = settings.global_theme; console.log('[主题] globalTheme已设置为:', this.globalTheme); } if (settings.smtp) { this.systemSettings.smtp.host = settings.smtp.host || ''; this.systemSettings.smtp.port = settings.smtp.port || 465; this.systemSettings.smtp.secure = !!settings.smtp.secure; this.systemSettings.smtp.user = settings.smtp.user || ''; this.systemSettings.smtp.from = settings.smtp.from || settings.smtp.user || ''; this.systemSettings.smtp.has_password = !!settings.smtp.has_password; this.systemSettings.smtp.password = ''; } } } catch (error) { console.error('加载系统设置失败:', error); this.showToast('error', '错误', '加载系统设置失败'); } }, async loadServerStorageStats() { try { const response = await axios.get(`${this.apiBase}/api/admin/storage-stats`, { headers: { Authorization: `Bearer ${this.token}` } }); if (response.data.success) { this.serverStorageStats = response.data.stats; } } catch (error) { console.error('加载服务器存储统计失败:', error); this.showToast('error', '错误', '加载服务器存储统计失败'); } }, async updateSystemSettings() { try { const maxUploadSize = parseInt(this.systemSettings.maxUploadSizeMB) * 1024 * 1024; const payload = { max_upload_size: maxUploadSize, smtp: { host: this.systemSettings.smtp.host, port: this.systemSettings.smtp.port, secure: this.systemSettings.smtp.secure, user: this.systemSettings.smtp.user, from: this.systemSettings.smtp.from || this.systemSettings.smtp.user } }; if (this.systemSettings.smtp.password) { payload.smtp.password = this.systemSettings.smtp.password; } const response = await axios.post( `${this.apiBase}/api/admin/settings`, payload, { headers: { Authorization: `Bearer ${this.token}` } } ); if (response.data.success) { this.showToast('success', '成功', '系统设置已更新'); this.systemSettings.smtp.password = ''; } } catch (error) { console.error('更新系统设置失败:', error); this.showToast('error', '错误', '更新系统设置失败'); } } , async testSmtp() { try { const response = await axios.post( `${this.apiBase}/api/admin/settings/test-smtp`, { to: this.systemSettings.smtp.user }, { headers: { Authorization: `Bearer ${this.token}` } } ); this.showToast('success', '成功', response.data.message || '测试邮件已发送'); } catch (error) { console.error('测试SMTP失败:', error); this.showToast('error', '错误', error.response?.data?.message || '测试失败'); } }, // ===== 健康检测 ===== async loadHealthCheck() { this.healthCheck.loading = true; try { const response = await axios.get(`${this.apiBase}/api/admin/health-check`, { headers: { Authorization: `Bearer ${this.token}` } }); if (response.data.success) { this.healthCheck.overallStatus = response.data.overallStatus; this.healthCheck.summary = response.data.summary; this.healthCheck.checks = response.data.checks; this.healthCheck.lastCheck = response.data.timestamp; } } catch (error) { console.error('健康检测失败:', error); this.showToast('error', '错误', '健康检测失败'); } finally { this.healthCheck.loading = false; } }, getHealthStatusColor(status) { const colors = { pass: 'bg-green-100 text-green-800', warning: 'bg-yellow-100 text-yellow-800', fail: 'bg-red-100 text-red-800', info: 'bg-blue-100 text-blue-800' }; return colors[status] || 'bg-gray-100 text-gray-800'; }, getHealthStatusIcon(status) { const icons = { pass: '✓', warning: '⚠', fail: '✗', info: 'ℹ' }; return icons[status] || '?'; }, getOverallStatusColor(status) { const colors = { healthy: 'text-green-600', warning: 'text-yellow-600', critical: 'text-red-600' }; return colors[status] || 'text-gray-600'; }, getOverallStatusText(status) { const texts = { healthy: '系统健康', warning: '存在警告', critical: '存在问题' }; return texts[status] || '未知'; }, // ===== 系统日志 ===== async loadSystemLogs(page = 1) { this.systemLogs.loading = true; try { const params = new URLSearchParams({ page: page, pageSize: this.systemLogs.pageSize }); if (this.systemLogs.filters.level) { params.append('level', this.systemLogs.filters.level); } if (this.systemLogs.filters.category) { params.append('category', this.systemLogs.filters.category); } if (this.systemLogs.filters.keyword) { params.append('keyword', this.systemLogs.filters.keyword); } const response = await axios.get(`${this.apiBase}/api/admin/logs?${params}`, { headers: { Authorization: `Bearer ${this.token}` } }); if (response.data.success) { this.systemLogs.logs = response.data.logs; this.systemLogs.total = response.data.total; this.systemLogs.page = response.data.page; this.systemLogs.totalPages = response.data.totalPages; } } catch (error) { console.error('加载系统日志失败:', error); this.showToast('error', '错误', '加载系统日志失败'); } finally { this.systemLogs.loading = false; } }, filterLogs() { this.loadSystemLogs(1); }, clearLogFilters() { this.systemLogs.filters = { level: '', category: '', keyword: '' }; this.loadSystemLogs(1); }, getLogLevelColor(level) { const colors = { debug: 'background: #6c757d; color: white;', info: 'background: #17a2b8; color: white;', warn: 'background: #ffc107; color: black;', error: 'background: #dc3545; color: white;' }; return colors[level] || 'background: #6c757d; color: white;'; }, getLogLevelText(level) { const texts = { debug: '调试', info: '信息', warn: '警告', error: '错误' }; return texts[level] || level; }, getLogCategoryText(category) { const texts = { auth: '认证', user: '用户', file: '文件', share: '分享', system: '系统', security: '安全' }; return texts[category] || category; }, getLogCategoryIcon(category) { const icons = { auth: 'fa-key', user: 'fa-user', file: 'fa-file', share: 'fa-share-alt', system: 'fa-cog', security: 'fa-shield-alt' }; return icons[category] || 'fa-info'; }, formatLogTime(timestamp) { if (!timestamp) return ''; const date = new Date(timestamp); return date.toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' }); }, async cleanupLogs() { if (!confirm('确定要清理90天前的日志吗?此操作不可恢复。')) return; try { const response = await axios.post( `${this.apiBase}/api/admin/logs/cleanup`, { keepDays: 90 }, { headers: { Authorization: `Bearer ${this.token}` } } ); if (response.data.success) { this.showToast('success', '成功', response.data.message); this.loadSystemLogs(1); } } catch (error) { console.error('清理日志失败:', error); this.showToast('error', '错误', '清理日志失败'); } }, // ===== 上传工具管理 ===== // 检测上传工具是否存在 async checkUploadTool() { this.checkingUploadTool = true; try { const response = await axios.get( `${this.apiBase}/api/admin/check-upload-tool`, { headers: { Authorization: `Bearer ${this.token}` } } ); if (response.data.success) { this.uploadToolStatus = response.data; if (response.data.exists) { this.showToast('success', '检测完成', '上传工具文件存在'); } else { this.showToast('warning', '提示', '上传工具文件不存在,请上传'); } } } catch (error) { console.error('检测上传工具失败:', error); this.showToast('error', '错误', '检测失败: ' + (error.response?.data?.message || error.message)); } finally { this.checkingUploadTool = false; } }, // 处理上传工具文件 async handleUploadToolFile(event) { const file = event.target.files[0]; if (!file) return; // 验证文件类型 if (!file.name.toLowerCase().endsWith('.exe')) { this.showToast('error', '错误', '只能上传 .exe 文件'); event.target.value = ''; return; } // 验证文件大小(至少20MB) const minSizeMB = 20; const fileSizeMB = file.size / (1024 * 1024); if (fileSizeMB < minSizeMB) { this.showToast('error', '错误', `文件大小过小(${fileSizeMB.toFixed(2)}MB),上传工具通常大于${minSizeMB}MB`); event.target.value = ''; return; } // 确认上传 if (!confirm(`确定要上传 ${file.name} (${fileSizeMB.toFixed(2)}MB) 吗?`)) { event.target.value = ''; return; } this.uploadingTool = true; try { const formData = new FormData(); formData.append('file', file); const response = await axios.post( `${this.apiBase}/api/admin/upload-tool`, formData, { headers: { Authorization: `Bearer ${this.token}`, 'Content-Type': 'multipart/form-data' } } ); if (response.data.success) { this.showToast('success', '成功', '上传工具已上传'); // 重新检测 await this.checkUploadTool(); } } catch (error) { console.error('上传工具失败:', error); this.showToast('error', '错误', error.response?.data?.message || '上传失败'); } finally { this.uploadingTool = false; event.target.value = ''; // 清空input,允许重复上传 } }, // ===== 调试模式管理 ===== // 切换调试模式 toggleDebugMode() { this.debugMode = !this.debugMode; // 保存到 localStorage if (this.debugMode) { localStorage.setItem('debugMode', 'true'); this.showToast('success', '调试模式已启用', 'F12和开发者工具快捷键已启用'); // 刷新页面以应用更改 setTimeout(() => { window.location.reload(); }, 1000); } else { localStorage.removeItem('debugMode'); this.showToast('info', '调试模式已禁用', '页面将重新加载以应用更改'); // 刷新页面以应用更改 setTimeout(() => { window.location.reload(); }, 1000); } } }, mounted() { // 配置axios全局设置 - 确保验证码session cookie正确传递 axios.defaults.withCredentials = true; // 初始化调试模式状态 this.debugMode = localStorage.getItem('debugMode') === 'true'; // 初始化主题(从localStorage加载,避免闪烁) this.initTheme(); // 处理URL中的验证/重置token(兼容缺少?的旧链接) const verifyToken = this.getTokenFromUrl('verifyToken'); const resetToken = this.getTokenFromUrl('resetToken'); if (verifyToken) { this.handleVerifyToken(verifyToken); this.sanitizeUrlToken('verifyToken'); } if (resetToken) { this.resetPasswordForm.token = resetToken; this.showResetPasswordModal = true; this.sanitizeUrlToken('resetToken'); } // 阻止全局拖拽默认行为(防止拖到区域外打开新页面) window.addEventListener("dragover", (e) => { e.preventDefault(); }); window.addEventListener("drop", (e) => { e.preventDefault(); }); // 添加全局 dragend 监听(拖拽结束时总是隐藏覆盖层) window.addEventListener("dragend", () => { this.isDragging = false; }); // 添加 ESC 键监听(按 ESC 关闭拖拽覆盖层) window.addEventListener("keydown", (e) => { if (e.key === "Escape" && this.isDragging) { this.isDragging = false; } }); // 设置axios响应拦截器,处理401错误(token过期/失效) axios.interceptors.response.use( response => response, error => { if (error.response && error.response.status === 401) { // 排除登录接口本身的401(密码错误等) const isLoginApi = error.config?.url?.includes('/api/login'); if (!isLoginApi && this.isLoggedIn) { console.warn('[认证] 收到401响应,Token已失效'); this.handleTokenExpired(); this.showToast('warning', '登录已过期', '请重新登录'); } } return Promise.reject(error); } ); // 检查URL参数 this.checkUrlParams(); // 获取系统配置(上传限制等) this.loadPublicConfig(); // 检查登录状态 this.checkLoginStatus(); }, watch: { currentView(newView) { if (newView === 'shares') { this.loadShares(); } else if (newView === 'admin' && this.user?.is_admin) { this.loadUsers(); this.loadSystemSettings(); this.loadServerStorageStats(); } else if (newView === 'settings' && this.user && !this.user.is_admin) { // 普通用户进入设置页面时加载SFTP配置 this.loadFtpConfig(); } // 记住最后停留的视图(需合法且已登录) if (this.isLoggedIn && this.isViewAllowed(newView)) { localStorage.setItem('lastView', newView); } } } }).mount('#app');