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