1. 登录时智能修正存储类型: - 当用户为SFTP模式但未配置SFTP时,如果有本地存储权限,自动切换到本地存储 - 避免管理员更改用户权限后,用户登录时出现"加载文件失败"错误 2. SFTP配置后自动切换存储模式: - 用户成功配置SFTP后,自动切换到SFTP存储模式 - 无需用户手动再次切换,提升用户体验 3. 改进存储切换提示信息: - 当用户尝试切换到未配置的SFTP时,显示更友好的提示 - 明确告知用户配置完成后将自动切换到SFTP存储 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1935 lines
61 KiB
JavaScript
1935 lines
61 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 列表
|
||
|
||
// 表单数据
|
||
loginForm: {
|
||
username: '',
|
||
password: ''
|
||
},
|
||
registerForm: {
|
||
username: '',
|
||
email: '',
|
||
password: ''
|
||
},
|
||
|
||
// 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: "",
|
||
password: "",
|
||
expiryType: "never",
|
||
customDays: 7
|
||
},
|
||
shareResult: null,
|
||
|
||
// 文件重命名
|
||
showRenameModal: false,
|
||
renameForm: {
|
||
oldName: "",
|
||
newName: "",
|
||
path: ""
|
||
},
|
||
|
||
// 上传
|
||
showUploadModal: false,
|
||
uploadProgress: 0,
|
||
uploadedBytes: 0,
|
||
totalBytes: 0,
|
||
uploadingFileName: '',
|
||
isDragging: false,
|
||
|
||
// 上传工具下载
|
||
downloadingTool: false,
|
||
|
||
// 管理员
|
||
adminUsers: [],
|
||
showResetPwdModal: false,
|
||
resetPwdUser: {},
|
||
newPassword: '',
|
||
|
||
// 密码重置审核
|
||
passwordResetRequests: [],
|
||
|
||
// 文件审查
|
||
showFileInspectionModal: false,
|
||
inspectionUser: null,
|
||
inspectionFiles: [],
|
||
inspectionPath: '/',
|
||
inspectionLoading: false,
|
||
inspectionViewMode: 'grid', // 文件审查显示模式: grid 大图标, list 列表
|
||
|
||
// 忘记密码
|
||
showForgotPasswordModal: false,
|
||
forgotPasswordForm: {
|
||
username: '',
|
||
new_password: ''
|
||
},
|
||
|
||
// 系统设置
|
||
systemSettings: {
|
||
maxUploadSizeMB: 100
|
||
},
|
||
|
||
// Toast通知
|
||
toasts: [],
|
||
toastIdCounter: 0,
|
||
|
||
// 提示信息
|
||
errorMessage: '',
|
||
successMessage: '',
|
||
|
||
// 存储相关
|
||
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 // 是否正在上传工具
|
||
};
|
||
},
|
||
|
||
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: {
|
||
// 格式化文件大小
|
||
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;
|
||
|
||
// 保存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();
|
||
// 管理员直接跳转到管理后台
|
||
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服务器');
|
||
}
|
||
} else {
|
||
// 默认行为:跳转到文件页面
|
||
this.currentView = 'files';
|
||
this.loadFiles('/');
|
||
}
|
||
}
|
||
}
|
||
} catch (error) {
|
||
this.errorMessage = 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: ''
|
||
};
|
||
}
|
||
} catch (error) {
|
||
const errorData = error.response?.data;
|
||
if (errorData?.errors) {
|
||
this.errorMessage = errorData.errors.map(e => e.msg).join(', ');
|
||
} else {
|
||
this.errorMessage = errorData?.message || '注册失败';
|
||
}
|
||
}
|
||
},
|
||
|
||
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.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');
|
||
|
||
// 停止定期检查
|
||
this.stopProfileSync();
|
||
},
|
||
|
||
// 检查本地存储的登录状态
|
||
async checkLoginStatus() {
|
||
const token = localStorage.getItem('token');
|
||
const user = localStorage.getItem('user');
|
||
|
||
if (token && user) {
|
||
this.token = token;
|
||
this.user = JSON.parse(user);
|
||
this.isLoggedIn = true;
|
||
|
||
// 从localStorage中的用户信息初始化存储相关字段
|
||
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);
|
||
|
||
// 加载最新的用户信息(异步更新)
|
||
this.loadUserProfile();
|
||
|
||
// 启动定期检查用户配置
|
||
this.startProfileSync();
|
||
// 管理员跳转到管理后台
|
||
if (this.user.is_admin) {
|
||
this.currentView = 'admin';
|
||
}
|
||
// 普通用户:根据存储权限决定跳转
|
||
else {
|
||
// 如果用户可以使用本地存储,直接加载文件
|
||
if (this.storagePermission === 'local_only' || this.storagePermission === 'user_choice') {
|
||
this.loadFiles('/');
|
||
}
|
||
// 如果仅SFTP模式,需要检查是否配置了SFTP
|
||
else if (this.storagePermission === 'sftp_only') {
|
||
if (this.user.has_ftp_config) {
|
||
this.loadFiles('/');
|
||
} else {
|
||
this.currentView = 'settings';
|
||
}
|
||
} else {
|
||
// 默认加载文件
|
||
this.loadFiles('/');
|
||
}
|
||
}
|
||
}
|
||
},
|
||
|
||
// 检查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;
|
||
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);
|
||
},
|
||
|
||
downloadFile(file) {
|
||
console.log("[DEBUG] 下载文件:", file);
|
||
if (file.httpDownloadUrl) {
|
||
// 如果配置了HTTP下载URL,使用HTTP直接下载
|
||
console.log("[DEBUG] 使用HTTP下载:", file.httpDownloadUrl);
|
||
window.open(file.httpDownloadUrl, "_blank");
|
||
} else {
|
||
// 如果没有配置HTTP URL,通过后端SFTP下载
|
||
console.log("[DEBUG] 使用SFTP下载");
|
||
const filePath = this.currentPath === '/'
|
||
? `/${file.name}`
|
||
: `${this.currentPath}/${file.name}`;
|
||
|
||
// 使用<a>标签下载,通过URL参数传递token
|
||
const link = document.createElement('a');
|
||
link.href = `${this.apiBase}/api/files/download?path=${encodeURIComponent(filePath)}&token=${this.token}`;
|
||
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 || '重命名失败');
|
||
}
|
||
},
|
||
|
||
confirmDeleteFile(file) {
|
||
const fileType = file.isDirectory ? '文件夹' : '文件';
|
||
const warning = file.isDirectory ? "\n注意:只能删除空文件夹!" : "";
|
||
if (confirm(`确定要删除${fileType} "${file.name}" 吗?此操作无法撤销!${warning}`)) {
|
||
this.deleteFile(file);
|
||
}
|
||
},
|
||
|
||
// ===== 右键菜单和长按功能 =====
|
||
|
||
// 显示右键菜单(PC端)
|
||
showFileContextMenu(file, event) {
|
||
if (file.isDirectory) return; // 文件夹不显示菜单
|
||
|
||
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 '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直接访问
|
||
if (file.httpDownloadUrl) {
|
||
return file.httpDownloadUrl;
|
||
}
|
||
|
||
// 本地存储或未配置HTTP URL,使用API下载
|
||
return `${this.apiBase}/api/files/download?path=${encodeURIComponent(filePath)}&token=${this.token}`;
|
||
},
|
||
|
||
// 获取文件缩略图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?token=${this.token}`;
|
||
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.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);
|
||
|
||
const response = await axios.post(
|
||
`${this.apiBase}/api/share/create`,
|
||
{
|
||
share_type: 'file',
|
||
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;
|
||
this.showToast('success', '成功', '文件分享链接已创建');
|
||
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 (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'
|
||
},
|
||
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));
|
||
}
|
||
},
|
||
|
||
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.username) {
|
||
this.showToast('error', '错误', '请输入用户名');
|
||
return;
|
||
}
|
||
if (!this.forgotPasswordForm.new_password || this.forgotPasswordForm.new_password.length < 6) {
|
||
this.showToast('error', '错误', '新密码至少6个字符');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await axios.post(
|
||
`${this.apiBase}/api/password-reset/request`,
|
||
this.forgotPasswordForm
|
||
);
|
||
|
||
if (response.data.success) {
|
||
this.showToast('success', '成功', '密码重置请求已提交,请等待管理员审核');
|
||
this.showForgotPasswordModal = false;
|
||
this.forgotPasswordForm = { username: '', new_password: '' };
|
||
}
|
||
} catch (error) {
|
||
console.error('提交密码重置请求失败:', error);
|
||
this.showToast('error', '错误', error.response?.data?.message || '提交失败');
|
||
}
|
||
},
|
||
|
||
// ===== 管理员:密码重置审核 =====
|
||
|
||
async loadPasswordResetRequests() {
|
||
try {
|
||
const response = await axios.get(`${this.apiBase}/api/admin/password-reset/pending`, {
|
||
headers: { Authorization: `Bearer ${this.token}` }
|
||
});
|
||
|
||
if (response.data.success) {
|
||
this.passwordResetRequests = response.data.requests;
|
||
}
|
||
} catch (error) {
|
||
console.error('加载密码重置请求失败:', error);
|
||
this.showToast('error', '错误', '加载密码重置请求失败');
|
||
}
|
||
},
|
||
|
||
async reviewPasswordReset(requestId, approved) {
|
||
const action = approved ? '批准' : '拒绝';
|
||
if (!confirm(`确定要${action}这个密码重置请求吗?`)) return;
|
||
|
||
try {
|
||
const response = await axios.post(
|
||
`${this.apiBase}/api/admin/password-reset/${requestId}/review`,
|
||
{ approved },
|
||
{ headers: { Authorization: `Bearer ${this.token}` } }
|
||
);
|
||
|
||
if (response.data.success) {
|
||
this.showToast('success', '成功', response.data.message);
|
||
this.loadPasswordResetRequests();
|
||
}
|
||
} 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;
|
||
|
||
// 检测存储配置是否被管理员更改
|
||
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 (oldStorageType !== newStorageType || oldStoragePermission !== newStoragePermission) {
|
||
console.log('[存储配置更新] 旧类型:', oldStorageType, '新类型:', newStorageType);
|
||
console.log('[存储配置更新] 旧权限:', oldStoragePermission, '新权限:', newStoragePermission);
|
||
|
||
this.showToast('info', '存储配置已更新', `管理员已将您的存储方式更改为${newStorageType === 'local' ? '本地存储' : 'SFTP存储'}`);
|
||
|
||
// 如果当前在文件页面,重新加载文件列表
|
||
if (this.currentView === 'files') {
|
||
await this.loadFiles(this.currentPath);
|
||
}
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('加载用户资料失败:', error);
|
||
}
|
||
},
|
||
// 启动定期检查用户配置
|
||
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) {
|
||
// 检查是否尝试切换到SFTP但未配置
|
||
if (type === 'sftp' && !this.user.has_ftp_config) {
|
||
const goToSettings = confirm('您还未配置SFTP服务器。\n\n是否现在前往设置页面进行配置?配置完成后将自动切换到SFTP存储。');
|
||
if (goToSettings) {
|
||
this.currentView = 'settings';
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (!confirm(`确定要切换到${type === 'local' ? '本地存储' : 'SFTP存储'}吗?`)) {
|
||
return;
|
||
}
|
||
|
||
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.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 || '切换存储失败');
|
||
}
|
||
},
|
||
|
||
// 切换视图并自动刷新数据
|
||
switchView(view) {
|
||
// 如果已经在当前视图,不重复刷新
|
||
if (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();
|
||
}
|
||
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 '-';
|
||
const date = new Date(dateString);
|
||
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.push(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));
|
||
}
|
||
} 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 response = await axios.post(
|
||
`${this.apiBase}/api/admin/settings`,
|
||
{ max_upload_size: maxUploadSize },
|
||
{ headers: { Authorization: `Bearer ${this.token}` } }
|
||
);
|
||
|
||
if (response.data.success) {
|
||
this.showToast('success', '成功', '系统设置已更新');
|
||
}
|
||
} 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,允许重复上传
|
||
}
|
||
} },
|
||
|
||
mounted() {
|
||
// 阻止全局拖拽默认行为(防止拖到区域外打开新页面)
|
||
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;
|
||
}
|
||
});
|
||
|
||
// 检查URL参数
|
||
this.checkUrlParams();
|
||
// 检查登录状态
|
||
this.checkLoginStatus();
|
||
},
|
||
|
||
watch: {
|
||
currentView(newView) {
|
||
if (newView === 'shares') {
|
||
this.loadShares();
|
||
} else if (newView === 'admin' && this.user?.is_admin) {
|
||
this.loadUsers();
|
||
this.loadSystemSettings();
|
||
this.loadPasswordResetRequests();
|
||
this.loadServerStorageStats();
|
||
} else if (newView === 'settings' && this.user && !this.user.is_admin) {
|
||
// 普通用户进入设置页面时加载SFTP配置
|
||
this.loadFtpConfig();
|
||
}
|
||
}
|
||
}
|
||
}).mount('#app');
|