Files
vue-driven-cloud-storage/frontend/app.js

4727 lines
158 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
};
},
// 获取媒体文件URLOSS直连或后端代理
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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
},
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) => `<mark class="admin-search-hit">${match}</mark>`);
},
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');