const { createApp } = Vue; const DEFAULT_LOCAL_STORAGE_QUOTA_BYTES = 1024 * 1024 * 1024; // 1GB const DEFAULT_OSS_STORAGE_QUOTA_BYTES = 1024 * 1024 * 1024; // 1GB createApp({ data() { // 预先确定管理员标签页,避免刷新时状态丢失导致闪烁 const initialAdminTab = (() => { const saved = localStorage.getItem('adminTab'); return (saved && ['overview', 'settings', 'monitor', 'users'].includes(saved)) ? saved : 'overview'; })(); return { // API配置 // API配置 - 通过nginx代理访问 apiBase: window.location.protocol + '//' + window.location.host, // 应用状态 appReady: false, // 应用是否初始化完成(防止UI闪烁) // 用户状态 isLoggedIn: false, user: null, token: null, // 仅用于内部状态跟踪,实际认证通过 HttpOnly Cookie tokenRefreshTimer: null, // 视图状态 currentView: 'files', isLogin: true, fileViewMode: 'grid', // 文件显示模式: grid 大图标, list 列表 shareViewMode: 'list', // 分享显示模式: grid 大图标, list 列表 debugMode: false, // 调试模式(管理员可切换) adminTab: initialAdminTab, // 管理员页面当前标签:overview, settings, monitor, users // 表单数据 loginForm: { username: '', password: '', captcha: '' }, registerForm: { username: '', email: '', password: '', captcha: '' }, registerCaptchaUrl: '', // 验证码相关 showCaptcha: false, captchaUrl: '', // OSS配置表单 ossConfigForm: { oss_provider: 'aliyun', oss_region: '', oss_access_key_id: '', oss_access_key_secret: '', oss_bucket: '', oss_endpoint: '' }, showOssConfigModal: false, ossConfigSaving: false, // OSS 配置保存中状态 ossConfigTesting: false, // OSS 配置测试中状态 // 修改密码表单 changePasswordForm: { current_password: '', new_password: '' }, // 用户名修改表单 usernameForm: { newUsername: '' }, // 用户资料表单 profileForm: { email: '' }, // 管理员资料表单 adminProfileForm: { username: '' }, // 分享表单(通用) shareForm: { path: '', password: '', expiryDays: null }, currentPath: '/', files: [], loading: false, // 分享管理 shares: [], directLinks: [], directLinksLoading: false, showShareFileModal: false, creatingShare: false, // 创建分享中状态 shareFileForm: { fileName: "", filePath: "", isDirectory: false, // 新增:标记是否为文件夹 enablePassword: false, password: "", expiryType: "never", customDays: 7, enableAdvancedSecurity: false, maxDownloadsEnabled: false, maxDownloads: 10, ipWhitelist: '', deviceLimit: 'all', accessTimeEnabled: false, accessTimeStart: '09:00', accessTimeEnd: '23:00' }, shareResult: null, showDirectLinkModal: false, creatingDirectLink: false, directLinkForm: { fileName: '', filePath: '', expiryType: 'never', customDays: 7 }, directLinkResult: null, shareFilters: { keyword: '', type: 'all', // all/file/directory/all_files status: 'all', // all/active/expiring/expired/protected/public sort: 'created_desc' // created_desc/created_asc/views_desc/downloads_desc/expire_asc }, // 文件重命名 showRenameModal: false, renameForm: { oldName: "", newName: "", path: "" }, // 创建文件夹 showCreateFolderModal: false, creatingFolder: false, // 创建文件夹中状态 createFolderForm: { folderName: "" }, // 文件夹详情 showFolderInfoModal: false, folderInfo: null, // 上传 showUploadModal: false, uploadProgress: 0, uploadedBytes: 0, totalBytes: 0, uploadingFileName: '', isDragging: false, modalMouseDownTarget: null, // 模态框鼠标按下的目标 // 全局搜索(文件页) globalSearchKeyword: '', globalSearchType: 'all', // all/file/directory globalSearchLoading: false, globalSearchError: '', globalSearchResults: [], globalSearchMeta: null, globalSearchVisible: false, // 管理员 adminUsers: [], adminUsersLoading: false, adminUsersPage: 1, adminUsersPageSize: 20, adminUsersTotalCount: 0, adminUsersTotalPages: 1, adminUsersGlobalCount: 0, adminUserStats: { active: 0, banned: 0, unverified: 0, download_blocked: 0 }, adminUserFilters: { keyword: '', role: 'all', // all/admin/user status: 'all', // all/active/banned/unverified/download_blocked storage: 'all', // all/local/oss/local_only/oss_only/user_choice sort: 'created_desc' // created_desc/created_asc/username_asc/username_desc/storage_usage_desc/download_usage_desc }, 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: '', // 加载状态 loginLoading: false, // 登录中 registerLoading: false, // 注册中 passwordChanging: false, // 修改密码中 usernameChanging: false, // 修改用户名中 passwordResetting: false, // 重置密码中 resendingVerify: false, // 重发验证邮件中 // 系统设置 systemSettings: { maxUploadSizeMB: 100, smtp: { host: '', port: 465, secure: true, user: '', from: '', password: '', has_password: false } }, // 健康检测 healthCheck: { loading: initialAdminTab === 'monitor', lastCheck: null, overallStatus: null, // healthy, warning, critical summary: { total: 0, pass: 0, warning: 0, fail: 0, info: 0 }, checks: [] }, // 系统日志 systemLogs: { loading: initialAdminTab === 'monitor', logs: [], total: 0, page: 1, pageSize: 30, totalPages: 0, filters: { level: '', category: '', keyword: '' } }, // 下载预扣运维面板(管理员-监控) reservationMonitor: { loading: false, cleaning: false, rows: [], summary: null, page: 1, pageSize: 20, total: 0, totalPages: 1, filters: { status: 'pending', keyword: '', userId: '' } }, // 监控页整体加载遮罩(避免刷新时闪一下空态) monitorTabLoading: initialAdminTab === 'monitor', // Toast通知 toasts: [], toastIdCounter: 0, // 上传限制(字节),默认10GB maxUploadSize: 10737418240, // 提示信息 errorMessage: '', successMessage: '', verifyMessage: '', // 存储相关 storageType: 'oss', // 当前使用的存储类型 storagePermission: 'oss_only', // 存储权限 localQuota: 0, // 本地存储配额(字节) localUsed: 0, // 本地存储已使用(字节) // 右键菜单 showContextMenu: false, contextMenuX: 0, contextMenuY: 0, contextMenuFile: null, showMobileFileActionSheet: false, mobileActionFile: null, // 长按检测 longPressTimer: null, longPressStartX: 0, longPressStartY: 0, longPressFile: null, longPressTriggered: false, // 媒体预览 showImageViewer: false, showVideoPlayer: false, showAudioPlayer: false, currentMediaUrl: '', currentMediaName: '', currentMediaType: '', // 'image', 'video', 'audio' mediaPreviewLoading: false, mediaPreviewCache: {}, // { [filePath]: { url, expiresAt } } thumbnailLoadErrors: {}, // 缩略图加载失败标记(按文件路径) longPressDuration: 420, // 长按时间(毫秒) // 管理员编辑用户存储权限 showEditStorageModal: false, editStorageForm: { userId: null, username: '', storage_permission: 'oss_only', local_storage_quota_value: 1, // 本地配额数值 quota_unit: 'GB', // 本地配额单位:MB 或 GB oss_storage_quota_value: 1, // OSS配额数值(默认1GB) oss_quota_unit: 'GB', // OSS配额单位:MB / GB / TB oss_quota_unlimited: false, // 兼容旧数据字段(当前固定为有限配额) download_traffic_quota_value: 1, // 下载流量配额数值 download_quota_unit: 'GB', // 下载流量单位:MB / GB / TB download_quota_unlimited: false, // 下载流量:true=不限(后端值为 -1) download_traffic_used: 0, // 下载流量已使用(字节) download_quota_operation: 'set', // set/increase/decrease download_quota_adjust_value: 1, // 增减额度数值 download_quota_adjust_unit: 'GB', // 增减额度单位 download_quota_expires_at: '', // 到期时间(datetime-local) download_quota_reset_cycle: 'none', // none/daily/weekly/monthly reset_download_used_now: false // 保存时立即重置已用流量 }, // 服务器存储统计 serverStorageStats: { totalDisk: 0, usedDisk: 0, availableDisk: 0, totalUserQuotas: 0, totalUserUsed: 0, totalUsers: 0 }, // 定期检查用户配置更新的定时器 profileCheckInterval: null, // 存储切换状态 storageSwitching: false, storageSwitchTarget: null, suppressStorageToast: false, profileInitialized: false, // OSS配置引导弹窗 showOssGuideModal: false, // OSS空间使用统计 ossUsage: null, // { totalSize, totalSizeFormatted, fileCount, dirCount } ossUsageLoading: false, ossUsageError: null, // 下载流量报表 downloadTrafficReport: { days: 30, loading: false, error: null, quota: null, daily: [], summary: null, lastUpdatedAt: 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); }, // OSS 配额相关(文件页展示) ossQuotaBytes() { const quota = Number(this.user?.oss_storage_quota || DEFAULT_OSS_STORAGE_QUOTA_BYTES); if (!Number.isFinite(quota) || quota <= 0) { return DEFAULT_OSS_STORAGE_QUOTA_BYTES; } return quota; }, ossUsedBytes() { const usageFromStats = Number(this.ossUsage?.totalSize); if (Number.isFinite(usageFromStats) && usageFromStats >= 0) { return usageFromStats; } const usageFromUser = Number(this.user?.storage_used || 0); if (Number.isFinite(usageFromUser) && usageFromUser >= 0) { return usageFromUser; } return 0; }, ossQuotaFormatted() { return this.formatBytes(this.ossQuotaBytes); }, ossUsedFormatted() { return this.formatBytes(this.ossUsedBytes); }, ossQuotaPercentage() { if (this.ossQuotaBytes <= 0) return 0; return Math.min(100, Math.round((this.ossUsedBytes / this.ossQuotaBytes) * 100)); }, downloadTrafficQuotaBytes() { const reportQuota = Number(this.downloadTrafficReport?.quota?.quota); if (Number.isFinite(reportQuota) && reportQuota >= 0) { return reportQuota; } const userQuota = Number(this.user?.download_traffic_quota || 0); return Number.isFinite(userQuota) && userQuota > 0 ? Math.floor(userQuota) : 0; }, downloadTrafficUsedBytes() { const reportUsed = Number(this.downloadTrafficReport?.quota?.used); if (Number.isFinite(reportUsed) && reportUsed >= 0) { return Math.floor(reportUsed); } const userUsed = Number(this.user?.download_traffic_used || 0); return Number.isFinite(userUsed) && userUsed > 0 ? Math.floor(userUsed) : 0; }, downloadTrafficIsUnlimited() { if (this.downloadTrafficReport?.quota?.is_unlimited === true) { return true; } const userQuota = Number(this.user?.download_traffic_quota); return Number.isFinite(userQuota) && userQuota < 0; }, downloadTrafficRemainingBytes() { if (this.downloadTrafficIsUnlimited) { return null; } const reportRemaining = Number(this.downloadTrafficReport?.quota?.remaining); if (Number.isFinite(reportRemaining) && reportRemaining >= 0) { return Math.floor(reportRemaining); } return Math.max(0, this.downloadTrafficQuotaBytes - this.downloadTrafficUsedBytes); }, downloadTrafficUsagePercentage() { if (this.downloadTrafficIsUnlimited) { return 0; } const reportPercentage = Number(this.downloadTrafficReport?.quota?.usage_percentage); if (Number.isFinite(reportPercentage) && reportPercentage >= 0) { return Math.min(100, Math.round(reportPercentage)); } if (this.downloadTrafficQuotaBytes <= 0) { return 100; } return Math.min(100, Math.round((this.downloadTrafficUsedBytes / this.downloadTrafficQuotaBytes) * 100)); }, downloadTrafficResetCycle() { return this.downloadTrafficReport?.quota?.reset_cycle || this.user?.download_traffic_reset_cycle || 'none'; }, downloadTrafficExpiresAt() { return this.downloadTrafficReport?.quota?.expires_at || this.user?.download_traffic_quota_expires_at || null; }, downloadTrafficDailyRowsDesc() { const rows = Array.isArray(this.downloadTrafficReport?.daily) ? this.downloadTrafficReport.daily : []; return [...rows].reverse(); }, // 存储类型显示文本 storageTypeText() { return this.storageType === 'local' ? '本地存储' : 'OSS存储'; }, // 文件统计信息(用于文件页工具栏) fileStats() { const list = Array.isArray(this.files) ? this.files : []; let directoryCount = 0; let fileCount = 0; let totalFileBytes = 0; for (const item of list) { if (item?.isDirectory) { directoryCount += 1; continue; } fileCount += 1; const currentSize = Number(item?.size || 0); if (Number.isFinite(currentSize) && currentSize > 0) { totalFileBytes += currentSize; } } return { totalCount: list.length, directoryCount, fileCount, totalFileBytes }; }, currentPathLabel() { if (!this.currentPath || this.currentPath === '/') { return '根目录'; } return this.currentPath; }, // 分享筛选+排序后的列表 filteredShares() { let list = [...this.shares]; const keyword = this.shareFilters.keyword.trim().toLowerCase(); if (keyword) { list = list.filter(s => (s.share_path || '').toLowerCase().includes(keyword) || (s.share_code || '').toLowerCase().includes(keyword) || (s.share_url || '').toLowerCase().includes(keyword) ); } if (this.shareFilters.type !== 'all') { const selectedType = this.shareFilters.type; list = list.filter(s => { if (selectedType === 'all_files') { return this.isShareAllFiles(s); } if (selectedType === 'directory') { return (s.share_type || 'file') === 'directory' && !this.isShareAllFiles(s); } return (s.share_type || 'file') === selectedType; }); } if (this.shareFilters.status !== 'all') { list = list.filter(s => { if (this.shareFilters.status === 'expired') return this.isExpired(s.expires_at); if (this.shareFilters.status === 'expiring') return this.isExpiringSoon(s.expires_at) && !this.isExpired(s.expires_at); if (this.shareFilters.status === 'active') return !this.isExpired(s.expires_at); if (this.shareFilters.status === 'protected') return this.hasSharePassword(s); if (this.shareFilters.status === 'public') return !this.hasSharePassword(s); return true; }); } list.sort((a, b) => { const getTime = s => s.created_at ? new Date(s.created_at).getTime() : 0; const getExpire = s => s.expires_at ? new Date(s.expires_at).getTime() : Number.MAX_SAFE_INTEGER; switch (this.shareFilters.sort) { case 'created_asc': return getTime(a) - getTime(b); case 'views_desc': return (b.view_count || 0) - (a.view_count || 0); case 'downloads_desc': return (b.download_count || 0) - (a.download_count || 0); case 'expire_asc': return getExpire(a) - getExpire(b); default: return getTime(b) - getTime(a); // created_desc } }); return list; }, filteredDirectLinks() { let list = [...this.directLinks]; const keyword = this.shareFilters.keyword.trim().toLowerCase(); if (keyword) { list = list.filter((link) => (link.file_path || '').toLowerCase().includes(keyword) || (link.file_name || '').toLowerCase().includes(keyword) || (link.link_code || '').toLowerCase().includes(keyword) || (link.direct_url || '').toLowerCase().includes(keyword) ); } list.sort((a, b) => { const ta = a.created_at ? new Date(a.created_at).getTime() : 0; const tb = b.created_at ? new Date(b.created_at).getTime() : 0; return tb - ta; }); return list; }, adminUsersFilteredCount() { return Math.max(0, Number(this.adminUsersTotalCount) || 0); }, adminUsersCurrentPage() { const page = Math.max(1, Number(this.adminUsersPage) || 1); return Math.min(page, this.adminUsersTotalPages); }, adminUsersPageStart() { if (this.adminUsersFilteredCount <= 0) return 0; const size = Math.max(1, Number(this.adminUsersPageSize) || 20); return (this.adminUsersCurrentPage - 1) * size + 1; }, adminUsersPageEnd() { if (this.adminUsersFilteredCount <= 0) return 0; const size = Math.max(1, Number(this.adminUsersPageSize) || 20); return Math.min(this.adminUsersCurrentPage * size, this.adminUsersFilteredCount); } }, methods: { // ========== 工具函数 ========== // 防抖函数 - 避免频繁调用 debounce(fn, delay) { let timer = null; return function(...args) { if (timer) clearTimeout(timer); timer = setTimeout(() => fn.apply(this, args), delay); }; }, // 创建防抖版本的 loadUserProfile(延迟2秒,避免频繁请求) debouncedLoadUserProfile() { if (!this._debouncedLoadUserProfile) { this._debouncedLoadUserProfile = this.debounce(() => { this.loadUserProfile(); }, 2000); } this._debouncedLoadUserProfile(); }, // ========== 主题管理 ========== // 初始化主题 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`); 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 }, ); 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 }, ); 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()); }, // 模态框点击外部关闭优化 - 防止拖动选择文本时误关闭 handleModalMouseDown(e) { // 记录鼠标按下时的目标 this.modalMouseDownTarget = e.target; }, handleModalMouseUp(modalName, e) { // 只有在同一个overlay元素上按下和释放鼠标时才关闭 if (e && 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 = ''; this.loginLoading = true; try { const response = await axios.post(`${this.apiBase}/api/login`, this.loginForm); if (response.data.success) { // token 和 refreshToken 都通过 HttpOnly Cookie 自动管理 this.user = response.data.user; this.isLoggedIn = true; this.showResendVerify = false; this.resendVerifyEmail = ''; // 登录成功后隐藏验证码并清空验证码输入 this.showCaptcha = false; this.loginForm.captcha = ''; // 保存用户信息到localStorage(非敏感信息,用于页面刷新后恢复) // 注意:token 通过 HttpOnly Cookie 传递,不再存储在 localStorage 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 || 'oss_only'; this.storageType = this.user.current_storage_type || 'oss'; this.localQuota = this.user.local_storage_quota || 0; this.localUsed = this.user.local_storage_used || 0; console.log('[登录] 存储权限:', this.storagePermission, '存储类型:', this.storageType, 'OSS配置:', this.user.oss_config_source); // 智能存储类型修正:如果当前是OSS但未配置(包括个人配置和系统级配置),且用户有本地存储权限,自动切换到本地 if (this.storageType === 'oss' && (!this.user || this.user.oss_config_source === 'none')) { if (this.storagePermission === 'local_only' || this.storagePermission === 'user_choice') { console.log('[登录] OSS未配置但用户有本地存储权限,自动切换到本地存储'); this.storageType = 'local'; // 异步更新到后端(不等待,避免阻塞登录流程) axios.post(`${this.apiBase}/api/user/switch-storage`, { storage_type: 'local' }) .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('/'); } // 如果仅OSS模式,需要检查是否配置了OSS(包括系统级统一配置) else if (this.storagePermission === 'oss_only') { if (this.user?.oss_config_source !== 'none') { this.currentView = 'files'; this.loadFiles('/'); } else { this.currentView = 'settings'; this.showToast('info', '欢迎', '请先配置您的OSS服务'); this.openOssConfigModal(); } } 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 = ''; } } finally { this.loginLoading = false; } }, // 通用验证码加载函数(带防抖) 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; } this.resendingVerify = true; 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(); } finally { this.resendingVerify = false; } }, 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 = ''; this.registerLoading = true; 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(); } finally { this.registerLoading = false; } }, async updateOssConfig() { // 防止重复提交 if (this.ossConfigSaving) { return; } // 前端验证 if (!this.ossConfigForm.oss_provider || !['aliyun', 'tencent', 'aws'].includes(this.ossConfigForm.oss_provider)) { this.showToast('error', '配置错误', '请选择有效的 OSS 服务商'); return; } if (!this.ossConfigForm.oss_region || this.ossConfigForm.oss_region.trim() === '') { this.showToast('error', '配置错误', '地域/Region 不能为空'); return; } if (!this.ossConfigForm.oss_access_key_id || this.ossConfigForm.oss_access_key_id.trim() === '') { this.showToast('error', '配置错误', 'Access Key ID 不能为空'); return; } if (!this.ossConfigForm.oss_access_key_secret || this.ossConfigForm.oss_access_key_secret.trim() === '') { this.showToast('error', '配置错误', 'Access Key Secret 不能为空'); return; } if (!this.ossConfigForm.oss_bucket || this.ossConfigForm.oss_bucket.trim() === '') { this.showToast('error', '配置错误', 'Bucket 名称不能为空'); return; } this.ossConfigSaving = true; try { const response = await axios.post( `${this.apiBase}/api/user/update-oss`, this.ossConfigForm, ); if (response.data.success) { // 更新用户信息 this.user.has_oss_config = 1; // 如果用户有 user_choice 权限,自动切换到 OSS 存储 if (this.storagePermission === 'user_choice' || this.storagePermission === 'oss_only') { try { const switchResponse = await axios.post( `${this.apiBase}/api/user/switch-storage`, { storage_type: 'oss' }, ); if (switchResponse.data.success) { this.storageType = 'oss'; console.log('[OSS配置] 已自动切换到OSS存储模式'); } } catch (err) { console.error('[OSS配置] 自动切换存储模式失败:', err); } } // 关闭配置弹窗 this.showOssConfigModal = false; // 刷新到文件页面 this.currentView = 'files'; this.loadFiles('/'); // 显示成功提示 this.showToast('success', '配置成功', 'OSS存储配置已保存!'); } } catch (error) { console.error('OSS配置保存失败:', error); this.showToast('error', '配置失败', error.response?.data?.message || error.message || '请检查配置信息后重试'); } finally { this.ossConfigSaving = false; } }, // 测试 OSS 连接(不保存配置) async testOssConnection() { // 防止重复提交 if (this.ossConfigTesting) { return; } // 前端验证 if (!this.ossConfigForm.oss_provider || !['aliyun', 'tencent', 'aws'].includes(this.ossConfigForm.oss_provider)) { this.showToast('error', '配置错误', '请选择有效的 OSS 服务商'); return; } if (!this.ossConfigForm.oss_region || this.ossConfigForm.oss_region.trim() === '') { this.showToast('error', '配置错误', '地域/Region 不能为空'); return; } if (!this.ossConfigForm.oss_access_key_id || this.ossConfigForm.oss_access_key_id.trim() === '') { this.showToast('error', '配置错误', 'Access Key ID 不能为空'); return; } // 如果用户已有配置,Secret 可以为空(使用现有密钥) if (this.user?.oss_config_source === 'none' && (!this.ossConfigForm.oss_access_key_secret || this.ossConfigForm.oss_access_key_secret.trim() === '')) { this.showToast('error', '配置错误', 'Access Key Secret 不能为空'); return; } if (!this.ossConfigForm.oss_bucket || this.ossConfigForm.oss_bucket.trim() === '') { this.showToast('error', '配置错误', 'Bucket 名称不能为空'); return; } this.ossConfigTesting = true; try { const response = await axios.post( `${this.apiBase}/api/user/test-oss`, this.ossConfigForm, ); if (response.data.success) { this.showToast('success', '连接成功', 'OSS 配置验证通过,可以保存'); } } catch (error) { console.error('OSS连接测试失败:', error); this.showToast('error', '连接失败', error.response?.data?.message || error.message || '请检查配置信息'); } finally { this.ossConfigTesting = false; } }, async updateAdminProfile() { try { const response = await axios.post( `${this.apiBase}/api/admin/update-profile`, { username: this.adminProfileForm.username }, ); if (response.data.success) { this.showToast('success', '成功', '用户名已更新!即将重新登录'); // 更新用户信息(后端已通过 Cookie 更新 token) if (response.data.user) { this.user = response.data.user; localStorage.setItem('user', JSON.stringify(response.data.user)); } // 延迟后重新登录 setTimeout(() => this.logout(), 1500); } } catch (error) { this.showToast('error', '错误', '修改失败: ' + (error.response?.data?.message || error.message)); } }, async changePassword() { if (!this.changePasswordForm.current_password) { this.showToast('warning', '提示', '请输入当前密码'); return; } if (this.changePasswordForm.new_password.length < 6) { this.showToast('warning', '提示', '新密码至少6个字符'); return; } this.passwordChanging = true; try { const response = await axios.post( `${this.apiBase}/api/user/change-password`, { current_password: this.changePasswordForm.current_password, new_password: this.changePasswordForm.new_password }, ); if (response.data.success) { this.showToast('success', '成功', '密码修改成功!'); this.changePasswordForm.new_password = ''; this.changePasswordForm.current_password = ''; } } catch (error) { this.showToast('error', '错误', '密码修改失败: ' + (error.response?.data?.message || error.message)); } finally { this.passwordChanging = false; } }, async loadOssConfig() { try { const response = await axios.get( `${this.apiBase}/api/user/profile`, ); if (response.data.success && response.data.user) { const user = response.data.user; // 填充OSS配置表单(密钥不回显) this.ossConfigForm.oss_provider = user.oss_provider || 'aliyun'; this.ossConfigForm.oss_region = user.oss_region || ''; this.ossConfigForm.oss_access_key_id = user.oss_access_key_id || ''; this.ossConfigForm.oss_access_key_secret = ''; // 密钥不回显 this.ossConfigForm.oss_bucket = user.oss_bucket || ''; this.ossConfigForm.oss_endpoint = user.oss_endpoint || ''; } } catch (error) { console.error('加载OSS配置失败:', error); } }, // 上传工具配置引导已移除(OSS 不需要配置文件导入) async updateUsername() { if (!this.usernameForm.newUsername || this.usernameForm.newUsername.length < 3) { this.showToast('warning', '提示', '用户名至少3个字符'); return; } this.usernameChanging = true; try { const response = await axios.post( `${this.apiBase}/api/user/update-username`, { username: this.usernameForm.newUsername }, ); if (response.data.success) { this.showToast('success', '成功', '用户名修改成功!'); // 更新本地用户信息 this.user.username = this.usernameForm.newUsername; localStorage.setItem('user', JSON.stringify(this.user)); this.usernameForm.newUsername = ''; } } catch (error) { this.showToast('error', '错误', '用户名修改失败: ' + (error.response?.data?.message || error.message)); } finally { this.usernameChanging = false; } }, async updateProfile() { try { const response = await axios.post( `${this.apiBase}/api/user/update-profile`, { email: this.profileForm.email }, ); if (response.data.success) { this.showToast('success', '成功', '邮箱已更新!'); // 更新本地用户信息 if (response.data.user) { this.user = response.data.user; localStorage.setItem('user', JSON.stringify(this.user)); } } } catch (error) { this.showToast('error', '错误', '更新失败: ' + (error.response?.data?.message || error.message)); } }, async logout() { // 调用后端清除 HttpOnly Cookie try { await axios.post(`${this.apiBase}/api/logout`); } catch (err) { console.error('[登出] 清除Cookie失败:', err); } this.isLoggedIn = false; this.user = null; this.token = null; this.stopTokenRefresh(); localStorage.removeItem('user'); localStorage.removeItem('lastView'); localStorage.removeItem('adminTab'); this.showResendVerify = false; this.resendVerifyEmail = ''; // 停止定期检查 this.stopProfileSync(); this.closeMobileFileActionSheet(); }, // 获取公开的系统配置(上传限制等) 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); // 使用默认值 } }, // 检查登录状态(通过 HttpOnly Cookie 验证) async checkLoginStatus() { // 直接调用API验证,Cookie会自动携带 try { const response = await axios.get(`${this.apiBase}/api/user/profile`); if (response.data.success && response.data.user) { // Cookie有效,用户已登录 this.user = response.data.user; this.isLoggedIn = true; // 更新localStorage中的用户信息(非敏感信息) localStorage.setItem('user', JSON.stringify(this.user)); // 从最新的用户信息初始化存储相关字段 this.storagePermission = this.user.storage_permission || 'oss_only'; this.storageType = this.user.current_storage_type || 'oss'; this.localQuota = this.user.local_storage_quota || 0; this.localUsed = this.user.local_storage_used || 0; console.log('[页面加载] Cookie验证成功,存储权限:', 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 === 'oss_only' && this.user?.oss_config_source === 'none') { targetView = 'settings'; } else { targetView = 'files'; } // 强制切换到目标视图并加载数据 this.switchView(targetView, true); } } catch (error) { // 401表示未登录或Cookie过期,静默处理(用户需要重新登录) if (error.response?.status === 401) { console.log('[页面加载] 未登录或Cookie已过期'); } else { console.warn('[页面加载] 验证登录状态失败:', error.message); } // 清理可能残留的用户信息 localStorage.removeItem('user'); } finally { // 无论登录验证成功还是失败,都标记应用已准备就绪 this.appReady = true; } }, // 尝试刷新token,失败则登出 async tryRefreshOrLogout() { // refreshToken 通过 Cookie 自动管理,直接尝试刷新 const refreshed = await this.doRefreshToken(); if (refreshed) { await this.checkLoginStatus(); return; } this.handleTokenExpired(); }, // 处理token过期/失效 handleTokenExpired() { console.log('[认证] Cookie已失效,清除登录状态'); this.isLoggedIn = false; this.user = null; this.token = null; this.stopTokenRefresh(); 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刷新(refreshToken 通过 HttpOnly Cookie 自动发送) async doRefreshToken() { try { console.log('[认证] 正在刷新access token...'); // refreshToken 通过 Cookie 自动携带,无需手动传递 const response = await axios.post(`${this.apiBase}/api/refresh-token`); if (response.data.success) { // 后端已自动更新 HttpOnly Cookie 中的 token console.log('[认证] Token刷新成功(Cookie已更新)'); // 继续下一次刷新 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 || '/'; this.globalSearchVisible = false; try { const response = await axios.get(`${this.apiBase}/api/files`, { params: { path } }); if (response.data.success) { this.files = response.data.items; this.thumbnailLoadErrors = {}; // 更新存储类型信息 if (response.data.storageType) { this.storageType = response.data.storageType; } if (response.data.storagePermission) { this.storagePermission = response.data.storagePermission; } // 更新用户本地存储信息(使用防抖避免频繁请求) this.debouncedLoadUserProfile(); // OSS 模式下刷新一次空间统计,保证文件页配额显示实时 if (this.storageType === 'oss' && this.user?.oss_config_source !== 'none') { this.loadOssUsage(); } } } catch (error) { console.error('加载文件失败:', error); this.showToast('error', '加载失败', error.response?.data?.message || error.message); if (error.response?.status === 401) { this.logout(); } } finally { this.loading = false; } }, triggerGlobalSearch() { const keyword = String(this.globalSearchKeyword || '').trim(); if (!keyword) { this.clearGlobalSearch(false); return; } if (!this._debouncedGlobalSearch) { this._debouncedGlobalSearch = this.debounce(() => { this.runGlobalSearch(); }, 260); } this._debouncedGlobalSearch(); }, async runGlobalSearch() { const keyword = String(this.globalSearchKeyword || '').trim(); if (!keyword) { this.clearGlobalSearch(false); return; } this.globalSearchLoading = true; this.globalSearchError = ''; this.globalSearchVisible = true; try { const response = await axios.get(`${this.apiBase}/api/files/search`, { params: { keyword, path: '/', type: this.globalSearchType || 'all', limit: 80 } }); if (response.data?.success) { this.globalSearchResults = Array.isArray(response.data.items) ? response.data.items : []; this.globalSearchMeta = response.data.meta || null; } else { this.globalSearchResults = []; this.globalSearchMeta = null; this.globalSearchError = response.data?.message || '搜索失败'; } } catch (error) { this.globalSearchResults = []; this.globalSearchMeta = null; this.globalSearchError = error.response?.data?.message || '搜索失败'; } finally { this.globalSearchLoading = false; } }, clearGlobalSearch(clearKeyword = true) { if (clearKeyword) { this.globalSearchKeyword = ''; } this.globalSearchLoading = false; this.globalSearchError = ''; this.globalSearchResults = []; this.globalSearchMeta = null; this.globalSearchVisible = false; }, async jumpToSearchResult(item) { if (!item || !item.parent_path) return; this.clearGlobalSearch(false); await this.loadFiles(item.parent_path); this.showToast('info', '已定位', `已定位到 ${item.name}`); }, async handleFileClick(file) { // 修复:长按后会触发一次 click,需要忽略避免误打开文件/目录 if (this.longPressTriggered) { this.longPressTriggered = false; return; } 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)) { await this.openImageViewer(file); } else if (file.name.match(/\.(mp4|avi|mov|wmv|flv|mkv|webm)$/i)) { await this.openVideoPlayer(file); } else if (file.name.match(/\.(mp3|wav|flac|aac|ogg|m4a)$/i)) { await 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); }, async downloadFile(file) { // 构建文件路径 const filePath = this.currentPath === '/' ? `/${file.name}` : `${this.currentPath}/${file.name}`; const canDirectOssDownload = this.storageType === 'oss' && this.user?.oss_config_source !== 'none'; // OSS 下载优先使用直连(避免服务器中转导致带宽与费用双重损耗) if (canDirectOssDownload) { await this.downloadFromOSS(filePath); return; } // 其他场景走后端下载接口(支持下载流量计量/权限控制) await this.downloadFromLocal(filePath); }, async downloadFromOSS(filePath) { try { const response = await axios.get(`${this.apiBase}/api/files/download-url`, { params: { path: filePath } }); if (!response.data?.success || !response.data?.downloadUrl) { return false; } const link = document.createElement('a'); link.href = response.data.downloadUrl; link.setAttribute('download', ''); link.rel = 'noopener noreferrer'; link.target = '_blank'; document.body.appendChild(link); link.click(); document.body.removeChild(link); return true; } catch (error) { console.error('OSS直连下载失败:', error); const message = error.response?.data?.message || '下载失败,请稍后重试'; this.showToast('error', '下载失败', message); if (error.response?.status === 401) { this.logout(); } return false; } }, // 本地存储下载(先预检,避免浏览器下载 JSON 错误文件) async downloadFromLocal(filePath) { try { const checkResp = await axios.get(`${this.apiBase}/api/files/download-check`, { params: { path: filePath } }); if (!checkResp.data?.success) { this.showToast('error', '下载失败', checkResp.data?.message || '下载失败,请稍后重试'); return false; } const url = `${this.apiBase}/api/files/download?path=${encodeURIComponent(filePath)}`; const link = document.createElement('a'); link.href = url; link.setAttribute('download', ''); document.body.appendChild(link); link.click(); document.body.removeChild(link); return true; } catch (error) { console.error('本地下载预检失败:', error); const message = error.response?.data?.message || '下载失败,请稍后重试'; this.showToast('error', '下载失败', message); if (error.response?.status === 401) { this.logout(); } return false; } }, // ===== 文件操作 ===== 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) { this.showToast('warning', '提示', '请输入新的文件名'); return; } try { const response = await axios.post( `${this.apiBase}/api/files/rename`, this.renameForm, ); 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() { if (this.creatingFolder) return; // 防止重复提交 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; } this.creatingFolder = true; try { const response = await axios.post(`${this.apiBase}/api/files/mkdir`, { path: this.currentPath, folderName: folderName }); if (response.data.success) { this.showToast('success', '成功', '文件夹创建成功'); this.showCreateFolderModal = false; this.createFolderForm.folderName = ''; await this.loadFiles(this.currentPath); // 刷新文件列表 await this.refreshStorageUsage(); // 刷新空间统计(OSS会增加空对象) } } catch (error) { console.error('[创建文件夹失败]', error); this.showToast('error', '错误', error.response?.data?.message || '创建文件夹失败'); } finally { this.creatingFolder = false; } }, // 显示文件夹详情 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 }); 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?.(); event?.stopPropagation?.(); const isTouchContextMenu = this.isMobileViewport() || event?.pointerType === 'touch' || Boolean(event?.sourceCapabilities?.firesTouchEvents); if (isTouchContextMenu) { this.openMobileFileActionSheet(file, event); return; } this.contextMenuFile = file; this.contextMenuX = event?.clientX || 0; this.contextMenuY = event?.clientY || 0; this.showContextMenu = true; // 点击其他地方关闭菜单 this.$nextTick(() => { document.addEventListener('click', this.hideContextMenu, { once: true }); }); }, // 隐藏右键菜单 hideContextMenu() { this.showContextMenu = false; this.contextMenuFile = null; }, isMobileViewport() { return window.matchMedia('(max-width: 768px)').matches; }, setMobileSheetScrollLock(locked) { if (!document.body) return; document.body.style.overflow = locked ? 'hidden' : ''; }, openMobileFileActionSheet(file, event) { if (event) { event.preventDefault(); event.stopPropagation(); } this.hideContextMenu(); this.mobileActionFile = file; this.showMobileFileActionSheet = true; this.setMobileSheetScrollLock(true); }, closeMobileFileActionSheet() { this.showMobileFileActionSheet = false; this.mobileActionFile = null; this.setMobileSheetScrollLock(false); }, async mobileFileAction(action) { const targetFile = this.mobileActionFile; this.closeMobileFileActionSheet(); if (!targetFile) { return; } this.contextMenuFile = targetFile; await this.contextMenuAction(action); }, // 长按开始(移动端) handleLongPressStart(file, event) { if (!event?.touches?.length) return; if (this.showMobileFileActionSheet) return; this.longPressTriggered = false; // 记录初始触摸位置,用于检测是否在滑动 const touch = event.touches[0]; this.longPressStartX = touch.clientX; this.longPressStartY = touch.clientY; this.longPressFile = file; this.longPressTimer = setTimeout(() => { this.longPressTriggered = true; if (event?.cancelable) { event.preventDefault(); } if (this.isMobileViewport()) { this.openMobileFileActionSheet(file, event); } else { // 触发长按菜单 this.contextMenuFile = file; // 使用记录的触摸位置 this.contextMenuX = this.longPressStartX; this.contextMenuY = this.longPressStartY; this.showContextMenu = true; // 点击其他地方关闭菜单 this.$nextTick(() => { document.addEventListener('click', this.hideContextMenu, { once: true }); }); } // 触摸震动反馈(如果支持) if (navigator.vibrate) { navigator.vibrate(50); } }, this.longPressDuration); }, // 长按移动检测(移动端)- 滑动时取消长按 handleLongPressMove(event) { if (!this.longPressTimer || !event?.touches?.length) 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; } this.longPressFile = null; }, // 从菜单执行操作 async contextMenuAction(action) { if (!this.contextMenuFile) return; switch (action) { case 'preview': // 根据文件类型打开对应的预览(异步) if (this.contextMenuFile.name.match(/\.(jpg|jpeg|png|gif|bmp|svg|webp)$/i)) { await this.openImageViewer(this.contextMenuFile); } else if (this.contextMenuFile.name.match(/\.(mp4|avi|mov|wmv|flv|mkv|webm)$/i)) { await this.openVideoPlayer(this.contextMenuFile); } else if (this.contextMenuFile.name.match(/\.(mp3|wav|flac|aac|ogg|m4a)$/i)) { await 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 'direct_link': this.openDirectLinkModal(this.contextMenuFile); break; case 'delete': this.confirmDeleteFile(this.contextMenuFile); break; } this.hideContextMenu(); }, // ===== 媒体预览功能 ===== getCachedMediaUrl(filePath) { if (!filePath) return null; const cached = this.mediaPreviewCache[filePath]; if (!cached || !cached.url || !cached.expiresAt) return null; if (Date.now() >= cached.expiresAt) return null; return cached.url; }, setCachedMediaUrl(filePath, url, expiresInSeconds = 3600) { if (!filePath || !url) return; const ttlMs = Math.max(60 * 1000, (Number(expiresInSeconds) || 3600) * 1000); const expiresAt = Date.now() + ttlMs - 20 * 1000; this.mediaPreviewCache = { ...this.mediaPreviewCache, [filePath]: { url, expiresAt } }; }, getCurrentFilePath(file) { if (!file || !file.name) return ''; return this.currentPath === '/' ? `/${file.name}` : `${this.currentPath}/${file.name}`; }, isThumbnailLoadFailed(file) { const filePath = this.getCurrentFilePath(file); return !!(filePath && this.thumbnailLoadErrors[filePath]); }, markThumbnailLoadFailed(file) { const filePath = this.getCurrentFilePath(file); if (!filePath || this.thumbnailLoadErrors[filePath]) return; this.thumbnailLoadErrors = { ...this.thumbnailLoadErrors, [filePath]: true }; }, // 获取媒体文件URL(OSS直连或后端代理) async getMediaUrl(file) { const filePath = this.getCurrentFilePath(file); // OSS 模式优先直连,避免限流场景回退为后端中转 if (this.storageType === 'oss' && this.user?.oss_config_source !== 'none') { const cachedUrl = this.getCachedMediaUrl(filePath); if (cachedUrl) { return cachedUrl; } try { const { data } = await axios.get(`${this.apiBase}/api/files/download-url`, { params: { path: filePath, mode: 'preview' } }); if (data.success) { this.setCachedMediaUrl(filePath, data.downloadUrl, data.expiresIn || 3600); return data.downloadUrl; } } catch (error) { console.error('获取媒体URL失败:', error); } return null; } // 本地存储模式:通过后端 API return `${this.apiBase}/api/files/download?path=${encodeURIComponent(filePath)}`; }, // 获取文件缩略图URL(同步方法,用于本地存储模式) // 注意:OSS 模式下缩略图需要单独处理,此处返回本地存储的直接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; const downloadQuota = Number(this.user?.download_traffic_quota); const downloadUsed = Number(this.user?.download_traffic_used || 0); if (Number.isFinite(downloadQuota) && downloadQuota >= 0) { if (downloadQuota === 0 || downloadUsed >= downloadQuota) { return null; } } // 本地存储模式:返回同步的下载 URL // OSS 模式下缩略图功能暂不支持(需要预签名 URL,建议点击文件预览) if (this.storageType !== 'oss') { const filePath = this.getCurrentFilePath(file); return `${this.apiBase}/api/files/download?path=${encodeURIComponent(filePath)}`; } // OSS 模式暂不支持同步缩略图,返回 null return null; }, // 打开图片预览 async openImageViewer(file) { this.mediaPreviewLoading = true; const url = await this.getMediaUrl(file); if (url) { this.currentMediaUrl = url; this.currentMediaName = file.name; this.currentMediaType = 'image'; this.showImageViewer = true; } else { this.mediaPreviewLoading = false; this.showToast('error', '错误', '无法获取文件预览链接'); } }, handleMediaPreviewLoaded() { this.mediaPreviewLoading = false; }, handleMediaPreviewWaiting() { if (this.currentMediaType === 'video' || this.currentMediaType === 'audio') { this.mediaPreviewLoading = true; } }, handleMediaPreviewPlaying() { this.mediaPreviewLoading = false; }, handleMediaPreviewError(type = 'file') { const typeTextMap = { image: '图片', video: '视频', audio: '音频' }; const typeText = typeTextMap[type] || '文件'; this.mediaPreviewLoading = false; this.showToast('error', '预览失败', `${typeText}预览失败,请尝试下载后查看`); this.closeMediaViewer(); }, // 打开视频播放器 async openVideoPlayer(file) { this.mediaPreviewLoading = true; const url = await this.getMediaUrl(file); if (url) { this.currentMediaUrl = url; this.currentMediaName = file.name; this.currentMediaType = 'video'; this.showVideoPlayer = true; } else { this.mediaPreviewLoading = false; this.showToast('error', '错误', '无法获取文件预览链接'); } }, // 打开音频播放器 async openAudioPlayer(file) { this.mediaPreviewLoading = true; const url = await this.getMediaUrl(file); if (url) { this.currentMediaUrl = url; this.currentMediaName = file.name; this.currentMediaType = 'audio'; this.showAudioPlayer = true; } else { this.mediaPreviewLoading = false; this.showToast('error', '错误', '无法获取文件预览链接'); } }, // 关闭媒体预览 closeMediaViewer() { this.showImageViewer = false; this.showVideoPlayer = false; this.showAudioPlayer = false; this.currentMediaUrl = ''; this.currentMediaName = ''; this.currentMediaType = ''; this.mediaPreviewLoading = false; }, // 下载当前预览的媒体文件 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 }, ); if (response.data.success) { this.showToast('success', '成功', '文件已删除'); // 刷新文件列表和空间统计 await this.loadFiles(this.currentPath); await this.refreshStorageUsage(); } } catch (error) { console.error('删除失败:', error); this.showToast('error', '错误', error.response?.data?.message || '删除失败'); } }, // ===== 分享功能 ===== 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.enablePassword = false; this.shareFileForm.password = ''; this.shareFileForm.expiryType = 'never'; this.shareFileForm.customDays = 7; this.shareFileForm.enableAdvancedSecurity = false; this.shareFileForm.maxDownloadsEnabled = false; this.shareFileForm.maxDownloads = 10; this.shareFileForm.ipWhitelist = ''; this.shareFileForm.deviceLimit = 'all'; this.shareFileForm.accessTimeEnabled = false; this.shareFileForm.accessTimeStart = '09:00'; this.shareFileForm.accessTimeEnd = '23:00'; this.shareResult = null; // 清空上次的分享结果 this.showShareFileModal = true; }, toggleSharePassword(formType) { if (formType === 'file' && !this.shareFileForm.enablePassword) { this.shareFileForm.password = ''; } }, resolveShareExpiry(expiryType, customDays) { if (expiryType === 'never') { return { valid: true, value: null }; } const days = expiryType === 'custom' ? Number(customDays) : Number(expiryType); if (!Number.isInteger(days) || days < 1 || days > 365) { return { valid: false, message: '有效期必须是 1-365 天的整数' }; } return { valid: true, value: days }; }, normalizeSharePassword(enablePassword, rawPassword) { if (!enablePassword) { return { valid: true, value: null }; } const password = (rawPassword || '').trim(); if (!password) { return { valid: false, message: '已启用密码保护,请输入访问密码' }; } if (password.length > 32) { return { valid: false, message: '访问密码不能超过32个字符' }; } return { valid: true, value: password }; }, buildShareSecurityPayload() { if (!this.shareFileForm.enableAdvancedSecurity) { return { valid: true, payload: {} }; } const payload = {}; if (this.shareFileForm.maxDownloadsEnabled) { const maxDownloads = Number(this.shareFileForm.maxDownloads); if (!Number.isInteger(maxDownloads) || maxDownloads < 1 || maxDownloads > 1000000) { return { valid: false, message: '下载次数上限需为 1 到 1000000 的整数' }; } payload.max_downloads = maxDownloads; } const whitelistText = String(this.shareFileForm.ipWhitelist || '').trim(); if (whitelistText) { payload.ip_whitelist = whitelistText; } if (!['all', 'mobile', 'desktop'].includes(this.shareFileForm.deviceLimit)) { return { valid: false, message: '设备限制参数无效' }; } payload.device_limit = this.shareFileForm.deviceLimit; if (this.shareFileForm.accessTimeEnabled) { const start = String(this.shareFileForm.accessTimeStart || '').trim(); const end = String(this.shareFileForm.accessTimeEnd || '').trim(); const timeReg = /^([01]\d|2[0-3]):([0-5]\d)$/; if (!timeReg.test(start) || !timeReg.test(end)) { return { valid: false, message: '访问时段格式必须为 HH:mm' }; } payload.access_time_start = start; payload.access_time_end = end; } return { valid: true, payload }; }, buildShareResult(data, options = {}) { return { ...data, has_password: typeof options.hasPassword === 'boolean' ? options.hasPassword : !!data.has_password, target_name: options.targetName || '文件', target_type: options.targetType || 'file', share_password_plain: options.password || '' }; }, async createShareFile() { if (this.creatingShare) return; // 防止重复提交 this.creatingShare = true; try { const expiryCheck = this.resolveShareExpiry(this.shareFileForm.expiryType, this.shareFileForm.customDays); if (!expiryCheck.valid) { this.showToast('warning', '提示', expiryCheck.message); return; } const passwordCheck = this.normalizeSharePassword(this.shareFileForm.enablePassword, this.shareFileForm.password); if (!passwordCheck.valid) { this.showToast('warning', '提示', passwordCheck.message); return; } const expiryDays = expiryCheck.value; const password = passwordCheck.value; const securityCheck = this.buildShareSecurityPayload(); if (!securityCheck.valid) { this.showToast('warning', '提示', securityCheck.message); return; } // 根据是否为文件夹决定share_type const shareType = this.shareFileForm.isDirectory ? 'directory' : 'file'; const response = await axios.post( `${this.apiBase}/api/share/create`, Object.assign({ share_type: shareType, // 修复:文件夹使用directory类型 file_path: this.shareFileForm.filePath, file_name: this.shareFileForm.fileName, password, expiry_days: expiryDays }, securityCheck.payload), ); if (response.data.success) { const itemType = this.shareFileForm.isDirectory ? '文件夹' : '文件'; this.shareResult = this.buildShareResult(response.data, { hasPassword: this.shareFileForm.enablePassword, targetName: this.shareFileForm.fileName, targetType: shareType, password }); this.showToast('success', '成功', this.shareFileForm.enablePassword ? `${itemType}加密分享已创建` : `${itemType}分享链接已创建`); this.loadShares(); } } catch (error) { console.error('创建分享失败:', error); this.showToast('error', '错误', error.response?.data?.message || '创建分享失败'); } finally { this.creatingShare = false; } }, openDirectLinkModal(file) { if (!file || file.isDirectory) { this.showToast('warning', '提示', '目录不支持生成直链,请选择文件'); return; } this.directLinkForm.fileName = file.name; this.directLinkForm.filePath = this.currentPath === '/' ? `/${file.name}` : `${this.currentPath}/${file.name}`; this.directLinkForm.expiryType = 'never'; this.directLinkForm.customDays = 7; this.directLinkResult = null; this.showDirectLinkModal = true; }, async createDirectLink() { if (this.creatingDirectLink) return; this.creatingDirectLink = true; try { const expiryCheck = this.resolveShareExpiry(this.directLinkForm.expiryType, this.directLinkForm.customDays); if (!expiryCheck.valid) { this.showToast('warning', '提示', expiryCheck.message); return; } const response = await axios.post(`${this.apiBase}/api/direct-link/create`, { file_path: this.directLinkForm.filePath, file_name: this.directLinkForm.fileName, expiry_days: expiryCheck.value }); if (response.data?.success) { this.directLinkResult = { ...response.data, target_name: this.directLinkForm.fileName }; this.showToast('success', '成功', '直链已创建'); this.loadDirectLinks(); } } catch (error) { console.error('创建直链失败:', error); this.showToast('error', '错误', error.response?.data?.message || '创建直链失败'); } finally { this.creatingDirectLink = false; } }, // ===== 文件上传 ===== 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; } // 设置上传状态 this.uploadingFileName = file.name; this.uploadProgress = 0; this.uploadedBytes = 0; this.totalBytes = file.size; try { const fileHash = await this.computeQuickFileHash(file); const instantUploaded = await this.checkInstantUpload(file, fileHash); if (instantUploaded) { this.uploadProgress = 0; this.uploadedBytes = 0; this.totalBytes = 0; this.uploadingFileName = ''; await this.loadFiles(this.currentPath); await this.refreshStorageUsage(); return; } if (this.storageType === 'oss' && this.user?.oss_config_source !== 'none') { // ===== OSS 直连上传(不经过后端) ===== await this.uploadToOSSDirect(file, fileHash); } else { // ===== 本地存储优先分片上传(断点续传) ===== const resumableOk = await this.uploadToLocalResumable(file, fileHash); if (!resumableOk) { await this.uploadToLocal(file, fileHash); } } } catch (error) { console.error('上传失败:', error); // 重置上传进度 this.uploadProgress = 0; this.uploadedBytes = 0; this.totalBytes = 0; this.uploadingFileName = ''; this.showToast('error', '上传失败', error.message || '上传失败,请重试'); } }, async computeQuickFileHash(file) { try { if (!file || !window.crypto?.subtle) { return null; } const sampleSize = 2 * 1024 * 1024; // 2MB const fullHashLimit = 8 * 1024 * 1024; // 8MB const chunks = []; if (file.size <= fullHashLimit) { chunks.push(new Uint8Array(await file.arrayBuffer())); } else { const first = await file.slice(0, sampleSize).arrayBuffer(); const middleStart = Math.max(0, Math.floor(file.size / 2) - Math.floor(sampleSize / 2)); const middle = await file.slice(middleStart, middleStart + sampleSize).arrayBuffer(); const lastStart = Math.max(0, file.size - sampleSize); const last = await file.slice(lastStart).arrayBuffer(); const meta = new TextEncoder().encode(`${file.name}|${file.size}|${file.lastModified}`); chunks.push( new Uint8Array(first), new Uint8Array(middle), new Uint8Array(last), meta ); } const totalLength = chunks.reduce((sum, arr) => sum + arr.length, 0); const merged = new Uint8Array(totalLength); let offset = 0; for (const arr of chunks) { merged.set(arr, offset); offset += arr.length; } const digest = await window.crypto.subtle.digest('SHA-256', merged.buffer); const hashHex = Array.from(new Uint8Array(digest)) .map(byte => byte.toString(16).padStart(2, '0')) .join(''); return hashHex || null; } catch (error) { console.warn('计算文件哈希失败,跳过秒传:', error); return null; } }, async checkInstantUpload(file, fileHash) { if (!file || !fileHash) { return false; } try { const response = await axios.post(`${this.apiBase}/api/files/instant-upload/check`, { filename: file.name, path: this.currentPath, size: file.size, file_hash: fileHash }); if (response.data?.success && response.data.instant) { this.showToast('success', '秒传成功', response.data.message || `文件 ${file.name} 已秒传`); return true; } return false; } catch (error) { const status = Number(error.response?.status || 0); const message = error.response?.data?.message; // 4xx 视为明确业务失败,直接抛给外层;5xx/网络错误降级为普通上传 if (status >= 400 && status < 500 && message) { throw new Error(message); } console.warn('秒传检查失败,降级普通上传:', error); return false; } }, // OSS 直连上传 async uploadToOSSDirect(file, fileHash = null) { try { // 预检查 OSS 配额(后端也会做强校验,未配置默认 1GB) const ossQuota = Number(this.user?.oss_storage_quota || DEFAULT_OSS_STORAGE_QUOTA_BYTES); const currentUsage = Number(this.user?.storage_used || this.ossUsage?.totalSize || 0); if (currentUsage + file.size > ossQuota) { const remaining = Math.max(ossQuota - currentUsage, 0); throw new Error(`OSS 配额不足:文件 ${this.formatBytes(file.size)},剩余 ${this.formatBytes(remaining)}(总配额 ${this.formatBytes(ossQuota)})`); } // 1. 获取签名 URL(传递当前路径) const { data: signData } = await axios.get(`${this.apiBase}/api/files/upload-signature`, { params: { filename: file.name, path: this.currentPath, contentType: file.type || 'application/octet-stream', size: file.size, fileHash: fileHash || undefined } }); if (!signData.success) { throw new Error(signData.message || '获取上传签名失败'); } if (!signData.completionToken) { throw new Error('上传签名缺少完成凭证,请刷新页面后重试'); } // 2. 直连 OSS 上传(不经过后端!) await axios.put(signData.uploadUrl, file, { headers: { 'Content-Type': file.type || 'application/octet-stream' }, onUploadProgress: (progressEvent) => { this.uploadProgress = Math.round((progressEvent.loaded * 100) / progressEvent.total); this.uploadedBytes = progressEvent.loaded; this.totalBytes = progressEvent.total; }, timeout: 30 * 60 * 1000 // 30分钟超时 }); // 3. 通知后端上传完成 await axios.post(`${this.apiBase}/api/files/upload-complete`, { objectKey: signData.objectKey, size: file.size, completionToken: signData.completionToken, path: this.currentPath }); // 4. 显示成功提示 this.showToast('success', '上传成功', `文件 ${file.name} 已上传到 OSS`); // 5. 重置上传进度 this.uploadProgress = 0; this.uploadedBytes = 0; this.totalBytes = 0; this.uploadingFileName = ''; // 6. 刷新文件列表和空间统计 await this.loadFiles(this.currentPath); await this.refreshStorageUsage(); } catch (error) { // 处理 CORS 错误 if (error.message?.includes('CORS') || error.message?.includes('Cross-Origin')) { throw new Error('OSS 跨域配置错误,请联系管理员检查 Bucket CORS 设置'); } throw error; } }, // 本地分片上传(断点续传) async uploadToLocalResumable(file, fileHash = null) { // 本地存储配额预检查 const estimatedUsage = this.localUsed + file.size; if (estimatedUsage > this.localQuota) { this.showToast( 'error', '配额不足', `文件大小 ${this.formatBytes(file.size)},剩余配额 ${this.formatBytes(this.localQuota - this.localUsed)}` ); return true; } try { const initResponse = await axios.post(`${this.apiBase}/api/upload/resumable/init`, { filename: file.name, path: this.currentPath, size: file.size, chunk_size: 4 * 1024 * 1024, file_hash: fileHash || undefined }); if (!initResponse.data?.success) { throw new Error(initResponse.data?.message || '初始化分片上传失败'); } const sessionId = initResponse.data.session_id; const chunkSize = Number(initResponse.data.chunk_size || 4 * 1024 * 1024); const totalChunks = Number(initResponse.data.total_chunks || Math.ceil(file.size / chunkSize)); const uploadedSet = new Set(Array.isArray(initResponse.data.uploaded_chunks) ? initResponse.data.uploaded_chunks : []); let uploadedBytes = Number(initResponse.data.uploaded_bytes || 0); if (uploadedBytes > 0 && file.size > 0) { this.uploadedBytes = uploadedBytes; this.uploadProgress = Math.min(100, Math.round((uploadedBytes / file.size) * 100)); } for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex += 1) { if (uploadedSet.has(chunkIndex)) { continue; } const start = chunkIndex * chunkSize; const end = Math.min(file.size, start + chunkSize); const chunkBlob = file.slice(start, end); const formData = new FormData(); formData.append('session_id', sessionId); formData.append('chunk_index', String(chunkIndex)); formData.append('chunk', chunkBlob, `${file.name}.part${chunkIndex}`); const chunkResp = await axios.post(`${this.apiBase}/api/upload/resumable/chunk`, formData, { headers: { 'Content-Type': 'multipart/form-data' }, timeout: 30 * 60 * 1000 }); if (!chunkResp.data?.success) { throw new Error(chunkResp.data?.message || '上传分片失败'); } uploadedBytes = Number(chunkResp.data.uploaded_bytes || uploadedBytes + chunkBlob.size); this.uploadedBytes = uploadedBytes; this.totalBytes = file.size; this.uploadProgress = file.size > 0 ? Math.min(100, Math.round((uploadedBytes / file.size) * 100)) : 0; } const completeResp = await axios.post(`${this.apiBase}/api/upload/resumable/complete`, { session_id: sessionId }); if (!completeResp.data?.success) { throw new Error(completeResp.data?.message || '完成分片上传失败'); } this.showToast('success', '上传成功', `文件 ${file.name} 已上传`); this.uploadProgress = 0; this.uploadedBytes = 0; this.totalBytes = 0; this.uploadingFileName = ''; await this.loadFiles(this.currentPath); await this.refreshStorageUsage(); return true; } catch (error) { if (error.response?.status === 404) { // 后端未启用分片接口时自动降级 return false; } throw error; } }, // 本地存储上传(经过后端) async uploadToLocal(file, fileHash = null) { // 本地存储配额预检查 const estimatedUsage = this.localUsed + file.size; if (estimatedUsage > this.localQuota) { this.showToast( 'error', '配额不足', `文件大小 ${this.formatBytes(file.size)},剩余配额 ${this.formatBytes(this.localQuota - this.localUsed)}` ); this.uploadProgress = 0; this.uploadedBytes = 0; this.totalBytes = 0; this.uploadingFileName = ''; return true; } const formData = new FormData(); formData.append('file', file); formData.append('path', this.currentPath); if (fileHash) { formData.append('file_hash', fileHash); } const response = await axios.post(`${this.apiBase}/api/upload`, formData, { headers: { 'Content-Type': 'multipart/form-data' }, timeout: 30 * 60 * 1000, 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); await this.refreshStorageUsage(); } return true; }, // ===== 分享管理 ===== async loadShares() { try { const response = await axios.get(`${this.apiBase}/api/share/my`); if (response.data.success) { this.shares = response.data.shares; } } catch (error) { console.error('加载分享列表失败:', error); this.showToast('error', '加载失败', error.response?.data?.message || error.message); } }, async loadDirectLinks() { this.directLinksLoading = true; try { const response = await axios.get(`${this.apiBase}/api/direct-link/my`); if (response.data?.success) { this.directLinks = response.data.links || []; } } catch (error) { console.error('加载直链列表失败:', error); this.showToast('error', '加载失败', error.response?.data?.message || '加载直链列表失败'); } finally { this.directLinksLoading = false; } }, async deleteDirectLink(id) { if (!confirm('确定要删除这个直链吗?')) return; try { const response = await axios.delete(`${this.apiBase}/api/direct-link/${id}`); if (response.data?.success) { this.showToast('success', '成功', '直链已删除'); this.loadDirectLinks(); } } catch (error) { console.error('删除直链失败:', error); this.showToast('error', '删除失败', error.response?.data?.message || '删除直链失败'); } }, async refreshShareResources() { await Promise.all([this.loadShares(), this.loadDirectLinks()]); }, async createShare() { this.shareForm.path = this.currentPath; try { const response = await axios.post(`${this.apiBase}/api/share/create`, this.shareForm); if (response.data.success) { this.shareResult = response.data; this.loadShares(); } } catch (error) { console.error('创建分享失败:', error); this.showToast('error', '创建失败', error.response?.data?.message || error.message); } }, async deleteShare(id) { if (!confirm('确定要删除这个分享吗?')) return; try { const response = await axios.delete(`${this.apiBase}/api/share/${id}`); if (response.data.success) { this.showToast('success', '成功', '分享已删除'); this.loadShares(); } } catch (error) { console.error('删除分享失败:', error); this.showToast('error', '删除失败', 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; }, normalizeSharePath(sharePath) { if (!sharePath) return '/'; const normalized = String(sharePath) .replace(/\\/g, '/') .replace(/\/+/g, '/'); if (!normalized || normalized === '.') { return '/'; } return normalized.startsWith('/') ? normalized : `/${normalized}`; }, getPathBaseName(pathValue, preferredName = '') { if (typeof preferredName === 'string' && preferredName.trim()) { return preferredName.trim(); } const normalized = this.normalizeSharePath(pathValue || '/'); if (normalized === '/') { return '/'; } const segments = normalized.split('/').filter(Boolean); return segments.length > 0 ? segments[segments.length - 1] : '/'; }, isShareAllFiles(share) { if (!share) return false; const shareType = share.share_type || 'file'; const sharePath = this.normalizeSharePath(share.share_path || '/'); return shareType === 'all' || (shareType === 'directory' && sharePath === '/'); }, hasSharePassword(share) { if (!share) return false; if (typeof share.has_password === 'boolean') { return share.has_password; } return !!share.share_password; }, getShareTypeIcon(share) { if (this.isShareAllFiles(share)) return 'fa-layer-group'; if ((share?.share_type || 'file') === 'directory') return 'fa-folder'; return 'fa-file-alt'; }, // 分享类型标签 getShareTypeLabel(shareOrType, sharePath) { const share = typeof shareOrType === 'object' && shareOrType !== null ? shareOrType : { share_type: shareOrType, share_path: sharePath }; if (this.isShareAllFiles(share)) { return '全部文件'; } switch (share.share_type) { case 'directory': return '文件夹'; case 'file': default: return '文件'; } }, // 分享状态标签 getShareStatus(share) { if (this.isExpired(share.expires_at)) { return { text: '已过期', class: 'danger', icon: 'fa-clock' }; } if (this.isExpiringSoon(share.expires_at)) { return { text: '即将到期', class: 'warn', icon: 'fa-hourglass-half' }; } return { text: '有效', class: 'success', icon: 'fa-check-circle' }; }, // 分享保护标签 getShareProtection(share) { if (this.hasSharePassword(share)) { return { text: '已加密', class: 'info', icon: 'fa-lock' }; } return { text: '公开', class: 'info', icon: 'fa-unlock' }; }, // 存储来源 getStorageLabel(storageType) { if (!storageType) return '默认'; return storageType === 'local' ? '本地存储' : storageType.toUpperCase(); }, // 格式化时间 formatDateTime(value) { if (!value) return '--'; const d = new Date(value); if (Number.isNaN(d.getTime())) return value; return d.toLocaleString(); }, // HTML实体解码(前端兜底,防止已实体化的文件名显示乱码) decodeHtmlEntities(str) { if (typeof str !== 'string') return ''; const entityMap = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", '#x27': "'", '#x2F': '/', '#x60': '`' }; const decodeOnce = (input) => input.replace(/&(#x[0-9a-fA-F]+|#\d+|[a-zA-Z]+);/g, (match, code) => { if (code[0] === '#') { const isHex = code[1]?.toLowerCase() === 'x'; const num = isHex ? parseInt(code.slice(2), 16) : parseInt(code.slice(1), 10); if (!Number.isNaN(num)) { return String.fromCharCode(num); } return match; } const mapped = entityMap[code]; return mapped !== undefined ? mapped : match; }); let output = str; let decoded = decodeOnce(output); while (decoded !== output) { output = decoded; decoded = decodeOnce(output); } return output; }, getFileDisplayName(file) { if (!file) return ''; const base = (typeof file.displayName === 'string' && file.displayName !== '') ? file.displayName : (typeof file.name === 'string' ? file.name : ''); const decoded = this.decodeHtmlEntities(base); return decoded || base || ''; }, openShare(url) { if (!url) return; const newWindow = window.open(url, '_blank', 'noopener'); if (!newWindow || newWindow.closed || typeof newWindow.closed === 'undefined') { // 弹窗被拦截时提示用户手动打开,避免当前页跳转 this.showToast('info', '提示', '浏览器阻止了新标签页,请允许弹窗或手动打开链接'); } }, copyTextToClipboard(text, successMessage = '已复制到剪贴板') { if (!text) { this.showToast('warning', '提示', '没有可复制的内容'); return; } if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(text).then(() => { this.showToast('success', '成功', successMessage); }).catch(() => { this.fallbackCopyToClipboard(text, successMessage); }); } else { this.fallbackCopyToClipboard(text, successMessage); } }, copyShareLink(url) { this.copyTextToClipboard(url, '分享链接已复制到剪贴板'); }, copyDirectLink(url) { this.copyTextToClipboard(url, '直链已复制到剪贴板'); }, copySharePassword(password) { this.copyTextToClipboard(password, '访问密码已复制到剪贴板'); }, fallbackCopyToClipboard(text, successMessage = '已复制到剪贴板') { // 备用复制方法 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', '成功', successMessage); } catch (err) { this.showToast('error', '错误', '复制失败,请手动复制'); } document.body.removeChild(textArea); }, // ===== 管理员功能 ===== calculateAdminUserStats(users = []) { const stats = { active: 0, banned: 0, unverified: 0, download_blocked: 0 }; for (const user of users) { const statusTag = this.getAdminUserStatusTag(user); if (Object.prototype.hasOwnProperty.call(stats, statusTag)) { stats[statusTag] += 1; } } return stats; }, buildAdminUsersQueryParams() { const params = { paged: 1, page: Math.max(1, Number(this.adminUsersPage) || 1), pageSize: Math.max(1, Number(this.adminUsersPageSize) || 20), sort: this.adminUserFilters.sort || 'created_desc' }; const keyword = String(this.adminUserFilters.keyword || '').trim(); if (keyword) params.keyword = keyword; if (this.adminUserFilters.role && this.adminUserFilters.role !== 'all') { params.role = this.adminUserFilters.role; } if (this.adminUserFilters.status && this.adminUserFilters.status !== 'all') { params.status = this.adminUserFilters.status; } if (this.adminUserFilters.storage && this.adminUserFilters.storage !== 'all') { params.storage = this.adminUserFilters.storage; } return params; }, async loadUsers(options = {}) { const resetPage = options && options.resetPage === true; if (resetPage) { this.adminUsersPage = 1; } this.adminUsersLoading = true; try { const response = await axios.get(`${this.apiBase}/api/admin/users`, { params: this.buildAdminUsersQueryParams() }); if (response.data.success) { const rows = Array.isArray(response.data.users) ? response.data.users : []; this.adminUsers = rows; const rawTotal = Number(response.data?.pagination?.total); const totalCount = Number.isFinite(rawTotal) && rawTotal >= 0 ? Math.floor(rawTotal) : rows.length; this.adminUsersTotalCount = totalCount; const rawTotalPages = Number(response.data?.pagination?.totalPages); this.adminUsersTotalPages = Number.isFinite(rawTotalPages) && rawTotalPages > 0 ? Math.floor(rawTotalPages) : Math.max(1, Math.ceil(totalCount / Math.max(1, Number(this.adminUsersPageSize) || 20))); const rawPage = Number(response.data?.pagination?.page); const nextPage = Number.isFinite(rawPage) && rawPage > 0 ? Math.floor(rawPage) : this.adminUsersPage; this.adminUsersPage = Math.min(Math.max(1, nextPage), this.adminUsersTotalPages); const summary = response.data?.summary || {}; const rawGlobalTotal = Number(summary.global_total); this.adminUsersGlobalCount = Number.isFinite(rawGlobalTotal) && rawGlobalTotal >= 0 ? Math.floor(rawGlobalTotal) : this.adminUsersTotalCount; const fallbackStats = this.calculateAdminUserStats(rows); this.adminUserStats = { active: Number.isFinite(Number(summary.active)) ? Math.max(0, Math.floor(Number(summary.active))) : fallbackStats.active, banned: Number.isFinite(Number(summary.banned)) ? Math.max(0, Math.floor(Number(summary.banned))) : fallbackStats.banned, unverified: Number.isFinite(Number(summary.unverified)) ? Math.max(0, Math.floor(Number(summary.unverified))) : fallbackStats.unverified, download_blocked: Number.isFinite(Number(summary.download_blocked)) ? Math.max(0, Math.floor(Number(summary.download_blocked))) : fallbackStats.download_blocked }; } } catch (error) { console.error('加载用户列表失败:', error); this.showToast('error', '加载失败', error.response?.data?.message || error.message); } finally { this.adminUsersLoading = false; } }, 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 }, ); if (response.data.success) { this.showToast('success', '成功', response.data.message); this.loadUsers(); } } catch (error) { console.error('操作失败:', error); this.showToast('error', '操作失败', error.response?.data?.message || error.message); } }, async deleteUser(userId) { if (!confirm('确定要删除这个用户吗?此操作不可恢复!')) return; try { const response = await axios.delete(`${this.apiBase}/api/admin/users/${userId}`); if (response.data.success) { this.showToast('success', '成功', '用户已删除'); this.loadUsers(); } } catch (error) { console.error('删除用户失败:', error); this.showToast('error', '删除失败', error.response?.data?.message || error.message); } }, // ===== 忘记密码功能 ===== async requestPasswordReset() { if (!this.forgotPasswordForm.email) { this.showToast('error', '错误', '请输入注册邮箱'); return; } if (!this.forgotPasswordForm.captcha) { this.showToast('error', '错误', '请输入验证码'); return; } this.passwordResetting = true; 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(); } finally { this.passwordResetting = false; } }, async submitResetPassword() { if (!this.resetPasswordForm.token || !this.resetPasswordForm.new_password || this.resetPasswordForm.new_password.length < 6) { this.showToast('error', '错误', '请输入有效的重置链接和新密码(至少6位)'); return; } this.passwordResetting = true; 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 || '重置失败'); } finally { this.passwordResetting = false; } }, // ===== 管理员:文件审查功能 ===== 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 } } ); 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`, ); if (response.data.success && response.data.user) { const user = response.data.user; // 同步用户信息(含 has_oss_config) this.user = { ...(this.user || {}), ...user }; // 检测存储配置是否被管理员更改 const oldStorageType = this.storageType; const oldStoragePermission = this.storagePermission; const newStorageType = user.current_storage_type || 'oss'; const newStoragePermission = user.storage_permission || 'oss_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' ? '本地存储' : 'OSS存储'}`); } else { this.suppressStorageToast = false; } // 如果当前在文件页面,重新加载文件列表 if (this.currentView === 'files') { await this.loadFiles(this.currentPath); } } } } catch (error) { console.error('加载用户资料失败:', error); } }, // 加载OSS空间使用统计 async loadOssUsage() { // 检查是否有可用的OSS配置(个人配置或系统级统一配置) if (!this.user || this.user?.oss_config_source === 'none') { this.ossUsage = null; return; } this.ossUsageLoading = true; this.ossUsageError = null; try { const response = await axios.get( `${this.apiBase}/api/user/oss-usage`, ); if (response.data.success) { this.ossUsage = response.data.usage; if (this.user && this.ossUsage && Number.isFinite(Number(this.ossUsage.totalSize))) { this.user.storage_used = Number(this.ossUsage.totalSize); } } } catch (error) { console.error('获取OSS空间使用情况失败:', error); this.ossUsageError = error.response?.data?.message || '获取失败'; } finally { this.ossUsageLoading = false; } }, async loadDownloadTrafficReport(days = this.downloadTrafficReport.days) { if (!this.isLoggedIn || !this.user || this.user.is_admin) { return; } const allowedDays = [7, 30, 90, 180]; const normalizedDays = allowedDays.includes(Number(days)) ? Number(days) : 30; this.downloadTrafficReport.days = normalizedDays; this.downloadTrafficReport.loading = true; this.downloadTrafficReport.error = null; try { const response = await axios.get( `${this.apiBase}/api/user/download-traffic-report?days=${normalizedDays}`, ); if (response.data.success) { const quota = response.data.quota || null; const report = response.data.report || {}; this.downloadTrafficReport.quota = quota; this.downloadTrafficReport.daily = Array.isArray(report.daily) ? report.daily : []; this.downloadTrafficReport.summary = report.summary || null; this.downloadTrafficReport.lastUpdatedAt = new Date().toISOString(); // 同步到 user 对象,保证文件页/设置页显示一致 if (this.user && quota) { this.user.download_traffic_quota = Number(quota.quota || 0); this.user.download_traffic_used = Number(quota.used || 0); this.user.download_traffic_reset_cycle = quota.reset_cycle || 'none'; this.user.download_traffic_quota_expires_at = quota.expires_at || null; this.user.download_traffic_last_reset_at = quota.last_reset_at || null; } } else { this.downloadTrafficReport.error = response.data.message || '获取报表失败'; } } catch (error) { console.error('获取下载流量报表失败:', error); this.downloadTrafficReport.error = error.response?.data?.message || '获取报表失败'; } finally { this.downloadTrafficReport.loading = false; } }, setDownloadTrafficReportDays(days) { const nextDays = Number(days); if (this.downloadTrafficReport.loading || nextDays === this.downloadTrafficReport.days) { return; } this.loadDownloadTrafficReport(nextDays); }, formatReportDateLabel(dateKey) { if (!dateKey) return '-'; const match = String(dateKey).match(/^(\d{4})-(\d{2})-(\d{2})$/); if (match) { return `${match[2]}-${match[3]}`; } return dateKey; }, // 刷新存储空间使用统计(根据当前存储类型) async refreshStorageUsage() { if (this.storageType === 'oss' && this.user?.oss_config_source !== 'none') { // 刷新 OSS 空间统计 await this.loadOssUsage(); } else if (this.storageType === 'local') { // 刷新本地存储统计(通过重新获取用户信息) await this.loadUserProfile(); } }, // 启动定期检查用户配置 startProfileSync() { // 清除已有的定时器 if (this.profileCheckInterval) { clearInterval(this.profileCheckInterval); } // 每30秒检查一次用户配置是否有更新 this.profileCheckInterval = setInterval(() => { // 注意:token 通过 HttpOnly Cookie 管理,仅检查 isLoggedIn if (this.isLoggedIn) { 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; } const confirmMessage = type === 'local' ? '切换到本地存储不会自动迁移 OSS 文件。切换后只会显示本地文件,确认继续?' : '切换到 OSS 存储不会自动迁移本地文件。切换后只会显示 OSS 文件,确认继续?'; if (!window.confirm(confirmMessage)) { return; } // 不再弹出配置引导弹窗,直接尝试切换 // 如果后端检测到没有OSS配置,会返回错误提示 this.storageSwitching = true; this.storageSwitchTarget = type; try { const response = await axios.post( `${this.apiBase}/api/user/switch-storage`, { storage_type: type }, ); if (response.data.success) { this.storageType = type; // 用户主动切换后,下一次配置同步不提示管理员修改 this.suppressStorageToast = true; this.showToast('success', '成功', `已切换到${type === 'local' ? '本地存储' : 'OSS存储'}`); // 重新加载文件列表 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; } }, ensureOssConfigSection() { this.openOssConfigModal(); }, openOssGuideModal() { this.showOssGuideModal = true; }, closeOssGuideModal() { this.showOssGuideModal = false; }, proceedOssGuide() { this.showOssGuideModal = false; this.ensureOssConfigSection(); }, openOssConfigModal() { // 只有管理员才能配置OSS if (!this.user?.is_admin) { this.showToast('error', '权限不足', '只有管理员才能配置OSS服务'); return; } this.showOssGuideModal = false; this.showOssConfigModal = true; if (this.user && !this.user.is_admin) { this.loadOssConfig(); } }, closeOssConfigModal() { this.showOssConfigModal = 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.refreshShareResources(); break; case 'admin': // 切换到管理后台时,重新加载用户列表、健康检测和系统日志 if (this.user && this.user.is_admin) { this.loadUsers(); this.loadServerStorageStats(); if (this.adminTab === 'monitor') { this.initMonitorTab(); } else { this.loadHealthCheck(); this.loadSystemLogs(1); } } break; case 'settings': if (this.user && !this.user.is_admin) { this.loadDownloadTrafficReport(); } break; } }, // 管理员:打开编辑用户存储权限模态框 openEditStorageModal(user) { this.editStorageForm.userId = user.id; this.editStorageForm.username = user.username; this.editStorageForm.storage_permission = user.storage_permission || 'oss_only'; // 智能识别本地配额单位 const localQuotaBytes = Number(user.local_storage_quota || DEFAULT_LOCAL_STORAGE_QUOTA_BYTES); const localQuotaMB = localQuotaBytes / 1024 / 1024; const localQuotaGB = localQuotaMB / 1024; if (localQuotaMB >= 1024 && localQuotaMB % 1024 === 0) { this.editStorageForm.local_storage_quota_value = localQuotaGB; this.editStorageForm.quota_unit = 'GB'; } else { this.editStorageForm.local_storage_quota_value = Math.max(1, Math.round(localQuotaMB)); this.editStorageForm.quota_unit = 'MB'; } // 智能识别 OSS 配额单位(未配置时默认 1GB) const ossQuotaBytes = Number(user.oss_storage_quota || DEFAULT_OSS_STORAGE_QUOTA_BYTES); const effectiveOssQuotaBytes = Number.isFinite(ossQuotaBytes) && ossQuotaBytes > 0 ? ossQuotaBytes : DEFAULT_OSS_STORAGE_QUOTA_BYTES; const mb = 1024 * 1024; const gb = 1024 * 1024 * 1024; const tb = 1024 * 1024 * 1024 * 1024; this.editStorageForm.oss_quota_unlimited = false; if (effectiveOssQuotaBytes >= tb && effectiveOssQuotaBytes % tb === 0) { this.editStorageForm.oss_storage_quota_value = effectiveOssQuotaBytes / tb; this.editStorageForm.oss_quota_unit = 'TB'; } else if (effectiveOssQuotaBytes >= gb && effectiveOssQuotaBytes % gb === 0) { this.editStorageForm.oss_storage_quota_value = effectiveOssQuotaBytes / gb; this.editStorageForm.oss_quota_unit = 'GB'; } else { this.editStorageForm.oss_storage_quota_value = Math.max(1, Math.round(effectiveOssQuotaBytes / mb)); this.editStorageForm.oss_quota_unit = 'MB'; } // 下载流量配额(-1 表示不限,0 表示禁止下载) const downloadQuotaBytes = Number(user.download_traffic_quota || 0); const isDownloadUnlimited = Number.isFinite(downloadQuotaBytes) && downloadQuotaBytes < 0; const effectiveDownloadQuotaBytes = Number.isFinite(downloadQuotaBytes) && downloadQuotaBytes > 0 ? downloadQuotaBytes : 0; const downloadUsedBytes = Number(user.download_traffic_used || 0); this.editStorageForm.download_traffic_used = Number.isFinite(downloadUsedBytes) && downloadUsedBytes > 0 ? Math.floor(downloadUsedBytes) : 0; this.editStorageForm.download_quota_operation = 'set'; this.editStorageForm.download_quota_adjust_value = 1; this.editStorageForm.download_quota_adjust_unit = 'GB'; this.editStorageForm.download_quota_reset_cycle = user.download_traffic_reset_cycle || 'none'; this.editStorageForm.download_quota_expires_at = this.toDateTimeLocalInput(user.download_traffic_quota_expires_at); this.editStorageForm.reset_download_used_now = false; this.editStorageForm.download_quota_unlimited = isDownloadUnlimited; if (isDownloadUnlimited) { this.editStorageForm.download_traffic_quota_value = 1; this.editStorageForm.download_quota_unit = 'GB'; } else if (effectiveDownloadQuotaBytes <= 0) { this.editStorageForm.download_traffic_quota_value = 0; this.editStorageForm.download_quota_unit = 'MB'; } else if (effectiveDownloadQuotaBytes >= tb && effectiveDownloadQuotaBytes % tb === 0) { this.editStorageForm.download_traffic_quota_value = effectiveDownloadQuotaBytes / tb; this.editStorageForm.download_quota_unit = 'TB'; } else if (effectiveDownloadQuotaBytes >= gb && effectiveDownloadQuotaBytes % gb === 0) { this.editStorageForm.download_traffic_quota_value = effectiveDownloadQuotaBytes / gb; this.editStorageForm.download_quota_unit = 'GB'; } else { this.editStorageForm.download_traffic_quota_value = Math.max(1, Math.round(effectiveDownloadQuotaBytes / mb)); this.editStorageForm.download_quota_unit = 'MB'; } this.showEditStorageModal = true; }, // 管理员:更新用户存储权限 async updateUserStorage() { try { // 计算本地配额(字节) if (!this.editStorageForm.local_storage_quota_value || this.editStorageForm.local_storage_quota_value < 1) { this.showToast('error', '参数错误', '本地配额必须大于 0'); return; } let localQuotaBytes; if (this.editStorageForm.quota_unit === 'GB') { localQuotaBytes = this.editStorageForm.local_storage_quota_value * 1024 * 1024 * 1024; } else { localQuotaBytes = this.editStorageForm.local_storage_quota_value * 1024 * 1024; } // 计算 OSS 配额(字节) if (!this.editStorageForm.oss_storage_quota_value || this.editStorageForm.oss_storage_quota_value < 1) { this.showToast('error', '参数错误', 'OSS 配额必须大于 0'); return; } let ossQuotaBytes; if (this.editStorageForm.oss_quota_unit === 'TB') { ossQuotaBytes = this.editStorageForm.oss_storage_quota_value * 1024 * 1024 * 1024 * 1024; } else if (this.editStorageForm.oss_quota_unit === 'GB') { ossQuotaBytes = this.editStorageForm.oss_storage_quota_value * 1024 * 1024 * 1024; } else { ossQuotaBytes = this.editStorageForm.oss_storage_quota_value * 1024 * 1024; } const toBytes = (value, unit) => { if (unit === 'TB') return value * 1024 * 1024 * 1024 * 1024; if (unit === 'GB') return value * 1024 * 1024 * 1024; return value * 1024 * 1024; }; // 下载流量:支持直接设置 / 增加 / 删减 let downloadQuotaBytes = null; let downloadTrafficDelta = null; if (this.editStorageForm.download_quota_operation === 'set') { if (this.editStorageForm.download_quota_unlimited) { downloadQuotaBytes = -1; } else { if (this.editStorageForm.download_traffic_quota_value === null || this.editStorageForm.download_traffic_quota_value === undefined || this.editStorageForm.download_traffic_quota_value < 0) { this.showToast('error', '参数错误', '下载流量配额必须大于等于 0,或选择不限流量'); return; } downloadQuotaBytes = toBytes( this.editStorageForm.download_traffic_quota_value, this.editStorageForm.download_quota_unit ); } } else { if (!this.editStorageForm.download_quota_adjust_value || this.editStorageForm.download_quota_adjust_value < 1) { this.showToast('error', '参数错误', '下载流量增减值必须大于 0'); return; } const adjustBytes = toBytes( this.editStorageForm.download_quota_adjust_value, this.editStorageForm.download_quota_adjust_unit ); downloadTrafficDelta = this.editStorageForm.download_quota_operation === 'increase' ? adjustBytes : -adjustBytes; } const downloadQuotaExpiresAt = this.normalizeDateTimeLocalToApi(this.editStorageForm.download_quota_expires_at); if (this.editStorageForm.download_quota_expires_at && !downloadQuotaExpiresAt) { this.showToast('error', '参数错误', '下载流量到期时间格式无效'); return; } const payload = { storage_permission: this.editStorageForm.storage_permission, local_storage_quota: localQuotaBytes, oss_storage_quota: ossQuotaBytes, download_traffic_quota_expires_at: downloadQuotaExpiresAt, download_traffic_reset_cycle: this.editStorageForm.download_quota_reset_cycle || 'none', reset_download_traffic_used: !!this.editStorageForm.reset_download_used_now }; if (downloadQuotaBytes !== null) { payload.download_traffic_quota = downloadQuotaBytes; } if (downloadTrafficDelta !== null) { payload.download_traffic_delta = downloadTrafficDelta; } const response = await axios.post( `${this.apiBase}/api/admin/users/${this.editStorageForm.userId}/storage-permission`, payload, ); if (response.data.success) { this.showToast('success', '成功', '存储权限已更新'); this.showEditStorageModal = false; this.loadUsers(); } } catch (error) { console.error('更新存储权限失败:', error); this.showToast('error', '错误', error.response?.data?.message || '更新失败'); } }, // ===== 工具函数 ===== setAdminUsersPage(page) { const nextPage = Number(page) || 1; if (nextPage < 1) { if (this.adminUsersPage !== 1) { this.adminUsersPage = 1; this.loadUsers(); } return; } const maxPage = this.adminUsersTotalPages; const targetPage = Math.min(nextPage, maxPage); if (targetPage === this.adminUsersPage) return; this.adminUsersPage = targetPage; this.loadUsers(); }, triggerAdminUsersKeywordSearch() { this.adminUsersPage = 1; if (!this._debouncedAdminUsersQuery) { this._debouncedAdminUsersQuery = this.debounce(() => { this.loadUsers(); }, 260); } this._debouncedAdminUsersQuery(); }, handleAdminUsersFilterChange() { this.adminUsersPage = 1; this.loadUsers(); }, handleAdminUsersPageSizeChange() { this.adminUsersPage = 1; this.loadUsers(); }, resetAdminUserFilters() { this.adminUserFilters = { keyword: '', role: 'all', status: 'all', storage: 'all', sort: 'created_desc' }; this.adminUsersPageSize = 20; this.adminUsersPage = 1; this.loadUsers(); }, getAdminUserStatusTag(user) { if (user?.is_banned) return 'banned'; if (!user?.is_verified) return 'unverified'; const quota = Number(user?.download_traffic_quota); const used = Number(user?.download_traffic_used || 0); if (Number.isFinite(quota) && quota >= 0 && (quota === 0 || used >= quota)) { return 'download_blocked'; } return 'active'; }, getAdminUserStatusLabel(user) { const tag = this.getAdminUserStatusTag(user); if (tag === 'banned') return '已封禁'; if (tag === 'unverified') return '未激活'; if (tag === 'download_blocked') return '下载受限'; return '正常'; }, escapeHtml(value) { return String(value ?? '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); }, escapeRegExp(value) { return String(value ?? '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }, getHighlightedText(value, keyword) { const text = this.escapeHtml(value || '-'); const search = String(keyword || '').trim(); if (!search) return text; const reg = new RegExp(this.escapeRegExp(search), 'ig'); return text.replace(reg, (match) => `${match}`); }, 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]; }, getAdminUserQuotaPercentage(user) { const quota = Number(user?.local_storage_quota || 0); const used = Number(user?.local_storage_used || 0); if (!Number.isFinite(quota) || quota <= 0) return 0; if (!Number.isFinite(used) || used <= 0) return 0; return Math.round((used / quota) * 100); }, getAdminUserOssQuotaPercentage(user) { const quota = Number(user?.oss_storage_quota || DEFAULT_OSS_STORAGE_QUOTA_BYTES); const used = Number(user?.storage_used || 0); if (!Number.isFinite(quota) || quota <= 0) return 0; if (!Number.isFinite(used) || used <= 0) return 0; return Math.min(100, Math.round((used / quota) * 100)); }, getAdminUserDownloadQuotaPercentage(user) { const quota = Number(user?.download_traffic_quota || 0); const used = Number(user?.download_traffic_used || 0); if (!Number.isFinite(quota)) return 0; if (quota < 0) return 0; if (quota === 0) return 100; if (!Number.isFinite(used) || used <= 0) return 0; return Math.min(100, Math.round((used / quota) * 100)); }, getDownloadResetCycleText(cycle) { if (cycle === 'daily') return '每日重置'; if (cycle === 'weekly') return '每周重置'; if (cycle === 'monthly') return '每月重置'; return '不自动重置'; }, toDateTimeLocalInput(dateString) { if (!dateString) return ''; const normalized = String(dateString).trim().replace(' ', 'T'); const match = normalized.match(/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2})/); if (match) return match[1]; const parsed = new Date(normalized); if (Number.isNaN(parsed.getTime())) return ''; const year = parsed.getFullYear(); const month = String(parsed.getMonth() + 1).padStart(2, '0'); const day = String(parsed.getDate()).padStart(2, '0'); const hours = String(parsed.getHours()).padStart(2, '0'); const minutes = String(parsed.getMinutes()).padStart(2, '0'); return `${year}-${month}-${day}T${hours}:${minutes}`; }, normalizeDateTimeLocalToApi(localValue) { if (!localValue) return null; const normalized = String(localValue).trim(); if (!normalized) return null; const fullMatch = normalized.match(/^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2})(?::(\d{2}))?$/); if (!fullMatch) return null; const seconds = fullMatch[3] || '00'; return `${fullMatch[1]} ${fullMatch[2]}:${seconds}`; }, 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`); 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`); 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 ); 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 }, ); this.showToast('success', '成功', response.data.message || '测试邮件已发送'); } catch (error) { console.error('测试SMTP失败:', error); this.showToast('error', '错误', error.response?.data?.message || '测试失败'); } }, // 打开监控标签页(带整体loading遮罩) openMonitorTab() { this.adminTab = 'monitor'; this.initMonitorTab(); }, // 统一加载监控数据,避免初次渲染空态闪烁 async initMonitorTab() { this.monitorTabLoading = true; this.healthCheck.loading = true; this.systemLogs.loading = true; this.reservationMonitor.loading = true; try { await Promise.all([ this.loadHealthCheck(), this.loadSystemLogs(1), this.loadDownloadReservationMonitor(1) ]); } catch (e) { // 子方法内部已处理错误 } finally { this.monitorTabLoading = false; } }, // ===== 健康检测 ===== async loadHealthCheck() { this.healthCheck.loading = true; try { const response = await axios.get(`${this.apiBase}/api/admin/health-check`); 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}`); 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 }, ); if (response.data.success) { this.showToast('success', '成功', response.data.message); this.loadSystemLogs(1); } } catch (error) { console.error('清理日志失败:', error); this.showToast('error', '错误', '清理日志失败'); } }, async loadDownloadReservationMonitor(page = this.reservationMonitor.page) { this.reservationMonitor.loading = true; try { const response = await axios.get(`${this.apiBase}/api/admin/download-reservations`, { params: { page: Math.max(1, Number(page) || 1), pageSize: this.reservationMonitor.pageSize, status: this.reservationMonitor.filters.status || undefined, keyword: (this.reservationMonitor.filters.keyword || '').trim() || undefined, user_id: (this.reservationMonitor.filters.userId || '').trim() || undefined } }); if (response.data?.success) { const rows = Array.isArray(response.data.reservations) ? response.data.reservations : []; const pagination = response.data.pagination || {}; this.reservationMonitor.rows = rows; this.reservationMonitor.summary = response.data.summary || null; this.reservationMonitor.total = Number(pagination.total || rows.length); this.reservationMonitor.totalPages = Number(pagination.totalPages || 1); this.reservationMonitor.page = Number(pagination.page || page || 1); } } catch (error) { console.error('加载预扣监控失败:', error); this.showToast('error', '错误', error.response?.data?.message || '加载预扣监控失败'); } finally { this.reservationMonitor.loading = false; } }, triggerReservationKeywordSearch() { this.reservationMonitor.page = 1; if (!this._debouncedReservationQuery) { this._debouncedReservationQuery = this.debounce(() => { this.loadDownloadReservationMonitor(1); }, 260); } this._debouncedReservationQuery(); }, async changeReservationPage(nextPage) { const page = Math.max(1, Number(nextPage) || 1); if (page === this.reservationMonitor.page) return; await this.loadDownloadReservationMonitor(page); }, getReservationStatusText(status) { if (status === 'pending') return '待确认'; if (status === 'confirmed') return '已确认'; if (status === 'expired') return '已过期'; if (status === 'cancelled') return '已取消'; return status || '-'; }, getReservationStatusColor(status) { if (status === 'pending') return '#f59e0b'; if (status === 'confirmed') return '#22c55e'; if (status === 'expired') return '#ef4444'; if (status === 'cancelled') return '#94a3b8'; return 'var(--text-secondary)'; }, async cancelReservation(row) { if (!row || !row.id) return; if (!confirm(`确认释放预扣 #${row.id} 吗?`)) return; try { const response = await axios.post(`${this.apiBase}/api/admin/download-reservations/${row.id}/cancel`); if (response.data?.success) { this.showToast('success', '成功', response.data.message || '预扣额度已释放'); await this.loadDownloadReservationMonitor(this.reservationMonitor.page); } } catch (error) { console.error('释放预扣失败:', error); this.showToast('error', '错误', error.response?.data?.message || '释放预扣失败'); } }, async cleanupReservations() { if (this.reservationMonitor.cleaning) return; if (!confirm('确认清理过期/历史预扣记录吗?')) return; this.reservationMonitor.cleaning = true; try { const response = await axios.post(`${this.apiBase}/api/admin/download-reservations/cleanup`, { keep_days: 7 }); if (response.data?.success) { this.showToast('success', '成功', response.data.message || '预扣清理完成'); await this.loadDownloadReservationMonitor(1); } } catch (error) { console.error('清理预扣失败:', error); this.showToast('error', '错误', error.response?.data?.message || '清理预扣失败'); } finally { this.reservationMonitor.cleaning = false; } }, // ===== 调试模式管理 ===== // 切换调试模式 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; // 设置 axios 请求拦截器,自动添加 CSRF Token axios.interceptors.request.use(config => { // 从 Cookie 中读取 CSRF token const csrfToken = document.cookie .split('; ') .find(row => row.startsWith('csrf_token=')) ?.split('=')[1]; if (csrfToken && ['POST', 'PUT', 'DELETE', 'PATCH'].includes(config.method?.toUpperCase())) { config.headers['X-CSRF-Token'] = csrfToken; } return config; }); // 初始化调试模式状态 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(); // 如果用户在监控页面刷新,提前设置loading状态(防止显示"无数据"闪烁) if (this.adminTab === 'monitor') { this.healthCheck.loading = true; this.systemLogs.loading = true; this.monitorTabLoading = true; } // 检查登录状态 this.checkLoginStatus(); }, watch: { currentView(newView) { if (newView === 'shares') { this.refreshShareResources(); } else if (newView === 'admin' && this.user?.is_admin) { this.loadUsers(); this.loadSystemSettings(); this.loadServerStorageStats(); } else if (newView === 'settings' && this.user && !this.user.is_admin) { // 普通用户进入设置页面时加载OSS配置 this.loadOssConfig(); this.loadDownloadTrafficReport(); } // 记住最后停留的视图(需合法且已登录) if (this.isLoggedIn && this.isViewAllowed(newView)) { localStorage.setItem('lastView', newView); } }, // 记住管理员当前标签页 adminTab(newTab) { if (this.isLoggedIn && this.user?.is_admin) { localStorage.setItem('adminTab', newTab); if (newTab === 'users') { this.loadUsers(); } } } } }).mount('#app');