feat: v3.1.0 OSS直连优化与代码质量提升
- 🚀 OSS 直连上传下载(用户直连OSS,不经过后端) - ✨ 新增 Presigned URL 签名接口 - ✨ 支持自定义 OSS endpoint 配置 - 🐛 修复 buildS3Config 不支持自定义 endpoint 的问题 - 🐛 清理残留的 basic-ftp 依赖 - ♻️ 更新 package.json 项目描述和版本号 - 📝 完善 README.md 更新日志和 CORS 配置说明 - 🔒 安全性增强:签名 URL 15分钟/1小时有效期 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
564
frontend/app.js
564
frontend/app.js
@@ -48,15 +48,17 @@ createApp({
|
||||
showCaptcha: false,
|
||||
captchaUrl: '',
|
||||
|
||||
// SFTP配置表单
|
||||
ftpConfigForm: {
|
||||
ftp_host: '',
|
||||
ftp_port: 22,
|
||||
ftp_user: '',
|
||||
ftp_password: '',
|
||||
http_download_base_url: ''
|
||||
// OSS配置表单
|
||||
ossConfigForm: {
|
||||
oss_provider: 'aliyun',
|
||||
oss_region: '',
|
||||
oss_access_key_id: '',
|
||||
oss_access_key_secret: '',
|
||||
oss_bucket: '',
|
||||
oss_endpoint: ''
|
||||
},
|
||||
showFtpConfigModal: false,
|
||||
showOssConfigModal: false,
|
||||
ossConfigSaving: false, // OSS 配置保存中状态
|
||||
|
||||
// 修改密码表单
|
||||
changePasswordForm: {
|
||||
@@ -211,8 +213,8 @@ createApp({
|
||||
verifyMessage: '',
|
||||
|
||||
// 存储相关
|
||||
storageType: 'sftp', // 当前使用的存储类型
|
||||
storagePermission: 'sftp_only', // 存储权限
|
||||
storageType: 'oss', // 当前使用的存储类型
|
||||
storagePermission: 'oss_only', // 存储权限
|
||||
localQuota: 0, // 本地存储配额(字节)
|
||||
localUsed: 0, // 本地存储已使用(字节)
|
||||
|
||||
@@ -222,7 +224,7 @@ createApp({
|
||||
contextMenuX: 0,
|
||||
contextMenuY: 0,
|
||||
contextMenuFile: null,
|
||||
|
||||
|
||||
// 长按检测
|
||||
longPressTimer: null,
|
||||
longPressStartX: 0,
|
||||
@@ -242,7 +244,7 @@ createApp({
|
||||
editStorageForm: {
|
||||
userId: null,
|
||||
username: '',
|
||||
storage_permission: 'sftp_only',
|
||||
storage_permission: 'oss_only',
|
||||
local_storage_quota_value: 1, // 配额数值
|
||||
quota_unit: 'GB' // 配额单位:MB 或 GB
|
||||
},
|
||||
@@ -271,14 +273,14 @@ createApp({
|
||||
suppressStorageToast: false,
|
||||
profileInitialized: false,
|
||||
|
||||
// SFTP配置引导弹窗
|
||||
showSftpGuideModal: false,
|
||||
showSftpConfigModal: false,
|
||||
// OSS配置引导弹窗
|
||||
showOssGuideModal: false,
|
||||
showOssConfigModal: false,
|
||||
|
||||
// SFTP空间使用统计
|
||||
sftpUsage: null, // { totalSize, totalSizeFormatted, fileCount, dirCount }
|
||||
sftpUsageLoading: false,
|
||||
sftpUsageError: null,
|
||||
// OSS空间使用统计
|
||||
ossUsage: null, // { totalSize, totalSizeFormatted, fileCount, dirCount }
|
||||
ossUsageLoading: false,
|
||||
ossUsageError: null,
|
||||
|
||||
// 主题设置
|
||||
currentTheme: 'dark', // 当前生效的主题: 'dark' 或 'light'
|
||||
@@ -309,7 +311,7 @@ createApp({
|
||||
|
||||
// 存储类型显示文本
|
||||
storageTypeText() {
|
||||
return this.storageType === 'local' ? '本地存储' : 'SFTP存储';
|
||||
return this.storageType === 'local' ? '本地存储' : 'OSS存储';
|
||||
},
|
||||
|
||||
// 分享筛选+排序后的列表
|
||||
@@ -613,17 +615,17 @@ handleDragLeave(e) {
|
||||
this.startTokenRefresh(expiresIn);
|
||||
|
||||
// 直接从登录响应中获取存储信息
|
||||
this.storagePermission = this.user.storage_permission || 'sftp_only';
|
||||
this.storageType = this.user.current_storage_type || 'sftp';
|
||||
this.storagePermission = this.user.storage_permission || 'oss_only';
|
||||
this.storageType = this.user.current_storage_type || 'oss';
|
||||
this.localQuota = this.user.local_storage_quota || 0;
|
||||
this.localUsed = this.user.local_storage_used || 0;
|
||||
|
||||
console.log('[登录] 存储权限:', this.storagePermission, '存储类型:', this.storageType, 'SFTP配置:', this.user.has_ftp_config);
|
||||
console.log('[登录] 存储权限:', this.storagePermission, '存储类型:', this.storageType, 'OSS配置:', this.user.has_oss_config);
|
||||
|
||||
// 智能存储类型修正:如果当前是SFTP但未配置,且用户有本地存储权限,自动切换到本地
|
||||
if (this.storageType === 'sftp' && !this.user.has_ftp_config) {
|
||||
// 智能存储类型修正:如果当前是OSS但未配置,且用户有本地存储权限,自动切换到本地
|
||||
if (this.storageType === 'oss' && !this.user.has_oss_config) {
|
||||
if (this.storagePermission === 'local_only' || this.storagePermission === 'user_choice') {
|
||||
console.log('[登录] SFTP未配置但用户有本地存储权限,自动切换到本地存储');
|
||||
console.log('[登录] OSS未配置但用户有本地存储权限,自动切换到本地存储');
|
||||
this.storageType = 'local';
|
||||
// 异步更新到后端(不等待,避免阻塞登录流程)
|
||||
axios.post(`${this.apiBase}/api/user/switch-storage`, { storage_type: 'local' })
|
||||
@@ -646,15 +648,15 @@ handleDragLeave(e) {
|
||||
this.currentView = 'files';
|
||||
this.loadFiles('/');
|
||||
}
|
||||
// 如果仅SFTP模式,需要检查是否配置了SFTP
|
||||
else if (this.storagePermission === 'sftp_only') {
|
||||
if (this.user.has_ftp_config) {
|
||||
// 如果仅OSS模式,需要检查是否配置了OSS
|
||||
else if (this.storagePermission === 'oss_only') {
|
||||
if (this.user.has_oss_config) {
|
||||
this.currentView = 'files';
|
||||
this.loadFiles('/');
|
||||
} else {
|
||||
this.currentView = 'settings';
|
||||
alert('欢迎!请先配置您的SFTP服务器');
|
||||
this.openSftpConfigModal();
|
||||
alert('欢迎!请先配置您的OSS服务');
|
||||
this.openOssConfigModal();
|
||||
}
|
||||
} else {
|
||||
// 默认行为:跳转到文件页面
|
||||
@@ -808,44 +810,56 @@ handleDragLeave(e) {
|
||||
}
|
||||
},
|
||||
|
||||
async updateFtpConfig() {
|
||||
async updateOssConfig() {
|
||||
// 防止重复提交
|
||||
if (this.ossConfigSaving) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.ossConfigSaving = true;
|
||||
|
||||
try {
|
||||
const response = await axios.post(
|
||||
`${this.apiBase}/api/user/update-ftp`,
|
||||
this.ftpConfigForm,
|
||||
`${this.apiBase}/api/user/update-oss`,
|
||||
this.ossConfigForm,
|
||||
);
|
||||
|
||||
if (response.data.success) {
|
||||
alert('SFTP配置已保存!');
|
||||
// 更新用户信息
|
||||
this.user.has_ftp_config = 1;
|
||||
|
||||
// 如果用户有 user_choice 权限,自动切换到 SFTP 存储
|
||||
if (this.storagePermission === 'user_choice' || this.storagePermission === 'sftp_only') {
|
||||
this.user.has_oss_config = 1;
|
||||
|
||||
// 如果用户有 user_choice 权限,自动切换到 OSS 存储
|
||||
if (this.storagePermission === 'user_choice' || this.storagePermission === 'oss_only') {
|
||||
try {
|
||||
const switchResponse = await axios.post(
|
||||
`${this.apiBase}/api/user/switch-storage`,
|
||||
{ storage_type: 'sftp' },
|
||||
);
|
||||
|
||||
{ storage_type: 'oss' },
|
||||
);
|
||||
|
||||
if (switchResponse.data.success) {
|
||||
this.storageType = 'sftp';
|
||||
console.log('[SFTP配置] 已自动切换到SFTP存储模式');
|
||||
this.storageType = 'oss';
|
||||
console.log('[OSS配置] 已自动切换到OSS存储模式');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[SFTP配置] 自动切换存储模式失败:', err);
|
||||
console.error('[OSS配置] 自动切换存储模式失败:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭配置弹窗
|
||||
this.showSftpConfigModal = false;
|
||||
this.showOssConfigModal = false;
|
||||
|
||||
// 刷新到文件页面
|
||||
this.currentView = 'files';
|
||||
this.loadFiles('/');
|
||||
|
||||
// 显示成功提示
|
||||
this.showToast('success', '配置成功', 'OSS存储配置已保存!');
|
||||
}
|
||||
} catch (error) {
|
||||
alert('配置失败: ' + (error.response?.data?.message || error.message));
|
||||
console.error('OSS配置保存失败:', error);
|
||||
this.showToast('error', '配置失败', error.response?.data?.message || error.message || '请检查配置信息后重试');
|
||||
} finally {
|
||||
this.ossConfigSaving = false;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -905,7 +919,7 @@ handleDragLeave(e) {
|
||||
}
|
||||
},
|
||||
|
||||
async loadFtpConfig() {
|
||||
async loadOssConfig() {
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`${this.apiBase}/api/user/profile`,
|
||||
@@ -913,105 +927,20 @@ handleDragLeave(e) {
|
||||
|
||||
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 || '';
|
||||
// 填充OSS配置表单(密钥不回显)
|
||||
this.ossConfigForm.oss_provider = user.oss_provider || 'aliyun';
|
||||
this.ossConfigForm.oss_region = user.oss_region || '';
|
||||
this.ossConfigForm.oss_access_key_id = user.oss_access_key_id || '';
|
||||
this.ossConfigForm.oss_access_key_secret = ''; // 密钥不回显
|
||||
this.ossConfigForm.oss_bucket = user.oss_bucket || '';
|
||||
this.ossConfigForm.oss_endpoint = user.oss_endpoint || '';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载SFTP配置失败:', error);
|
||||
console.error('加载OSS配置失败:', 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;
|
||||
},
|
||||
// 上传工具配置引导已移除(OSS 不需要配置文件导入)
|
||||
|
||||
async updateUsername() {
|
||||
if (!this.usernameForm.newUsername || this.usernameForm.newUsername.length < 3) {
|
||||
@@ -1107,8 +1036,8 @@ handleDragLeave(e) {
|
||||
localStorage.setItem('user', JSON.stringify(this.user));
|
||||
|
||||
// 从最新的用户信息初始化存储相关字段
|
||||
this.storagePermission = this.user.storage_permission || 'sftp_only';
|
||||
this.storageType = this.user.current_storage_type || 'sftp';
|
||||
this.storagePermission = this.user.storage_permission || 'oss_only';
|
||||
this.storageType = this.user.current_storage_type || 'oss';
|
||||
this.localQuota = this.user.local_storage_quota || 0;
|
||||
this.localUsed = this.user.local_storage_used || 0;
|
||||
|
||||
@@ -1129,7 +1058,7 @@ handleDragLeave(e) {
|
||||
targetView = savedView;
|
||||
} else if (this.user.is_admin) {
|
||||
targetView = 'admin';
|
||||
} else if (this.storagePermission === 'sftp_only' && !this.user.has_ftp_config) {
|
||||
} else if (this.storagePermission === 'oss_only' && !this.user.has_oss_config) {
|
||||
targetView = 'settings';
|
||||
} else {
|
||||
targetView = 'files';
|
||||
@@ -1311,17 +1240,42 @@ handleDragLeave(e) {
|
||||
downloadFile(file) {
|
||||
console.log("[DEBUG] 下载文件:", file);
|
||||
|
||||
// SFTP存储且有HTTP直链,新窗口打开直接下载(避免Mixed Content问题)
|
||||
if (file.httpDownloadUrl) {
|
||||
window.open(file.httpDownloadUrl, '_blank');
|
||||
return;
|
||||
}
|
||||
// 构建文件路径
|
||||
const filePath = this.currentPath === '/' ? `/${file.name}` : `${this.currentPath}/${file.name}`;
|
||||
|
||||
// 本地存储,使用隐藏链接触发下载
|
||||
const url = `${this.apiBase}/api/files/download?path=${encodeURIComponent(this.currentPath === '/' ? `/${file.name}` : `${this.currentPath}/${file.name}`)}`;
|
||||
// OSS 模式:使用签名 URL 直连下载(不经过后端)
|
||||
if (this.storageType === 'oss' && this.user?.has_oss_config) {
|
||||
this.downloadFromOSS(filePath);
|
||||
} else {
|
||||
// 本地存储模式:通过后端下载
|
||||
this.downloadFromLocal(filePath);
|
||||
}
|
||||
},
|
||||
|
||||
// OSS 直连下载
|
||||
async downloadFromOSS(filePath) {
|
||||
try {
|
||||
// 获取签名 URL
|
||||
const { data } = await axios.get(`${this.apiBase}/api/files/download-url`, {
|
||||
params: { path: filePath }
|
||||
});
|
||||
|
||||
if (data.success) {
|
||||
// 直连 OSS 下载
|
||||
window.open(data.downloadUrl, '_blank');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取下载链接失败:', error);
|
||||
this.showToast('error', '错误', '获取下载链接失败');
|
||||
}
|
||||
},
|
||||
|
||||
// 本地存储下载
|
||||
downloadFromLocal(filePath) {
|
||||
const url = `${this.apiBase}/api/files/download?path=${encodeURIComponent(filePath)}`;
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.setAttribute('download', file.name);
|
||||
link.setAttribute('download', '');
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
@@ -1385,6 +1339,7 @@ handleDragLeave(e) {
|
||||
this.showCreateFolderModal = false;
|
||||
this.createFolderForm.folderName = '';
|
||||
await this.loadFiles(this.currentPath); // 刷新文件列表
|
||||
await this.refreshStorageUsage(); // 刷新空间统计(OSS会增加空对象)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[创建文件夹失败]', error);
|
||||
@@ -1540,18 +1495,26 @@ handleDragLeave(e) {
|
||||
|
||||
// ===== 媒体预览功能 =====
|
||||
|
||||
// 获取媒体文件URL
|
||||
getMediaUrl(file) {
|
||||
// 获取媒体文件URL(OSS直连或后端代理)
|
||||
async getMediaUrl(file) {
|
||||
const filePath = this.currentPath === '/'
|
||||
? `/${file.name}`
|
||||
: `${this.currentPath}/${file.name}`;
|
||||
|
||||
// SFTP存储且配置了HTTP下载URL,使用HTTP直接访问;否则使用API下载
|
||||
if (file.httpDownloadUrl) {
|
||||
return file.httpDownloadUrl;
|
||||
// OSS 模式:返回签名 URL(用于媒体预览)
|
||||
if (this.storageType === 'oss' && this.user?.has_oss_config) {
|
||||
try {
|
||||
const { data } = await axios.get(`${this.apiBase}/api/files/download-url`, {
|
||||
params: { path: filePath }
|
||||
});
|
||||
return data.success ? data.downloadUrl : null;
|
||||
} catch (error) {
|
||||
console.error('获取媒体URL失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 本地存储或未配置HTTP URL,使用API下载(同域 Cookie 验证)
|
||||
// 本地存储模式:通过后端 API
|
||||
return `${this.apiBase}/api/files/download?path=${encodeURIComponent(filePath)}`;
|
||||
},
|
||||
|
||||
@@ -1634,7 +1597,9 @@ handleDragLeave(e) {
|
||||
|
||||
if (response.data.success) {
|
||||
this.showToast('success', '成功', '文件已删除');
|
||||
this.loadFiles(this.currentPath);
|
||||
// 刷新文件列表和空间统计
|
||||
await this.loadFiles(this.currentPath);
|
||||
await this.refreshStorageUsage();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error);
|
||||
@@ -1764,7 +1729,7 @@ handleDragLeave(e) {
|
||||
},
|
||||
|
||||
async uploadFile(file) {
|
||||
// 文件大小限制预检查(在上传前检查,避免用户等待上传完才发现超限)
|
||||
// 文件大小限制预检查
|
||||
if (file.size > this.maxUploadSize) {
|
||||
const fileSizeMB = Math.round(file.size / (1024 * 1024));
|
||||
const maxSizeMB = Math.round(this.maxUploadSize / (1024 * 1024));
|
||||
@@ -1776,99 +1741,129 @@ handleDragLeave(e) {
|
||||
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);
|
||||
// 设置上传状态
|
||||
this.uploadingFileName = file.name;
|
||||
this.uploadProgress = 0;
|
||||
this.uploadedBytes = 0;
|
||||
this.totalBytes = file.size;
|
||||
|
||||
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: {
|
||||
'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);
|
||||
if (this.storageType === 'oss' && this.user?.has_oss_config) {
|
||||
// ===== OSS 直连上传(不经过后端) =====
|
||||
await this.uploadToOSSDirect(file);
|
||||
} else {
|
||||
// ===== 本地存储上传(经过后端) =====
|
||||
await this.uploadToLocal(file);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('上传失败:', error);
|
||||
|
||||
// 重置上传进度
|
||||
this.uploadProgress = 0;
|
||||
this.uploadedBytes = 0;
|
||||
this.totalBytes = 0;
|
||||
this.uploadedBytes = 0;
|
||||
this.totalBytes = 0;
|
||||
this.uploadingFileName = '';
|
||||
|
||||
// 处理文件大小超限错误
|
||||
if (error.response?.status === 413) {
|
||||
const errorData = error.response.data;
|
||||
this.showToast('error', '上传失败', error.message || '上传失败,请重试');
|
||||
}
|
||||
},
|
||||
|
||||
// 判断响应是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 超过系统限制,请联系管理员`
|
||||
);
|
||||
// OSS 直连上传
|
||||
async uploadToOSSDirect(file) {
|
||||
try {
|
||||
// 1. 获取签名 URL
|
||||
const { data: signData } = await axios.get(`${this.apiBase}/api/files/upload-signature`, {
|
||||
params: {
|
||||
filename: file.name,
|
||||
contentType: file.type || 'application/octet-stream'
|
||||
}
|
||||
} else {
|
||||
this.showToast('error', '上传失败', error.response?.data?.message || error.message);
|
||||
});
|
||||
|
||||
if (!signData.success) {
|
||||
throw new Error(signData.message || '获取上传签名失败');
|
||||
}
|
||||
|
||||
// 2. 直连 OSS 上传(不经过后端!)
|
||||
await axios.put(signData.uploadUrl, file, {
|
||||
headers: {
|
||||
'Content-Type': file.type || 'application/octet-stream'
|
||||
},
|
||||
onUploadProgress: (progressEvent) => {
|
||||
this.uploadProgress = Math.round((progressEvent.loaded * 100) / progressEvent.total);
|
||||
this.uploadedBytes = progressEvent.loaded;
|
||||
this.totalBytes = progressEvent.total;
|
||||
},
|
||||
timeout: 30 * 60 * 1000 // 30分钟超时
|
||||
});
|
||||
|
||||
// 3. 通知后端上传完成
|
||||
await axios.post(`${this.apiBase}/api/files/upload-complete`, {
|
||||
objectKey: signData.objectKey,
|
||||
size: file.size,
|
||||
path: this.currentPath
|
||||
});
|
||||
|
||||
// 4. 显示成功提示
|
||||
this.showToast('success', '上传成功', `文件 ${file.name} 已上传到 OSS`);
|
||||
|
||||
// 5. 重置上传进度
|
||||
this.uploadProgress = 0;
|
||||
this.uploadedBytes = 0;
|
||||
this.totalBytes = 0;
|
||||
this.uploadingFileName = '';
|
||||
|
||||
// 6. 刷新文件列表和空间统计
|
||||
await this.loadFiles(this.currentPath);
|
||||
await this.refreshStorageUsage();
|
||||
|
||||
} catch (error) {
|
||||
// 处理 CORS 错误
|
||||
if (error.message?.includes('CORS') || error.message?.includes('Cross-Origin')) {
|
||||
throw new Error('OSS 跨域配置错误,请联系管理员检查 Bucket CORS 设置');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 本地存储上传(经过后端)
|
||||
async uploadToLocal(file) {
|
||||
// 本地存储配额预检查
|
||||
const estimatedUsage = this.localUsed + file.size;
|
||||
if (estimatedUsage > this.localQuota) {
|
||||
this.showToast(
|
||||
'error',
|
||||
'配额不足',
|
||||
`文件大小 ${this.formatBytes(file.size)},剩余配额 ${this.formatBytes(this.localQuota - this.localUsed)}`
|
||||
);
|
||||
this.uploadProgress = 0;
|
||||
this.uploadedBytes = 0;
|
||||
this.totalBytes = 0;
|
||||
this.uploadingFileName = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('path', this.currentPath);
|
||||
|
||||
const response = await axios.post(`${this.apiBase}/api/upload`, formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
timeout: 30 * 60 * 1000,
|
||||
onUploadProgress: (progressEvent) => {
|
||||
this.uploadProgress = Math.round((progressEvent.loaded * 100) / progressEvent.total);
|
||||
this.uploadedBytes = progressEvent.loaded;
|
||||
this.totalBytes = progressEvent.total;
|
||||
}
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
this.showToast('success', '上传成功', `文件 ${file.name} 已上传`);
|
||||
this.uploadProgress = 0;
|
||||
this.uploadedBytes = 0;
|
||||
this.totalBytes = 0;
|
||||
this.uploadingFileName = '';
|
||||
await this.loadFiles(this.currentPath);
|
||||
await this.refreshStorageUsage();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2266,14 +2261,14 @@ handleDragLeave(e) {
|
||||
|
||||
if (response.data.success && response.data.user) {
|
||||
const user = response.data.user;
|
||||
// 同步用户信息(含 has_ftp_config)
|
||||
// 同步用户信息(含 has_oss_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';
|
||||
const newStorageType = user.current_storage_type || 'oss';
|
||||
const newStoragePermission = user.storage_permission || 'oss_only';
|
||||
|
||||
// 更新本地数据
|
||||
this.localQuota = user.local_storage_quota || 0;
|
||||
@@ -2293,7 +2288,7 @@ handleDragLeave(e) {
|
||||
console.log('[存储配置更新] 旧权限:', oldStoragePermission, '新权限:', newStoragePermission);
|
||||
|
||||
if (!this.suppressStorageToast) {
|
||||
this.showToast('info', '存储配置已更新', `管理员已将您的存储方式更改为${newStorageType === 'local' ? '本地存储' : 'SFTP存储'}`);
|
||||
this.showToast('info', '存储配置已更新', `管理员已将您的存储方式更改为${newStorageType === 'local' ? '本地存储' : 'OSS存储'}`);
|
||||
} else {
|
||||
this.suppressStorageToast = false;
|
||||
}
|
||||
@@ -2309,30 +2304,41 @@ handleDragLeave(e) {
|
||||
}
|
||||
},
|
||||
|
||||
// 加载SFTP空间使用统计
|
||||
async loadSftpUsage() {
|
||||
// 仅在用户已配置SFTP时才加载
|
||||
if (!this.user?.has_ftp_config) {
|
||||
this.sftpUsage = null;
|
||||
// 加载OSS空间使用统计
|
||||
async loadOssUsage() {
|
||||
// 仅在用户已配置OSS时才加载
|
||||
if (!this.user?.has_oss_config) {
|
||||
this.ossUsage = null;
|
||||
return;
|
||||
}
|
||||
|
||||
this.sftpUsageLoading = true;
|
||||
this.sftpUsageError = null;
|
||||
this.ossUsageLoading = true;
|
||||
this.ossUsageError = null;
|
||||
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`${this.apiBase}/api/user/sftp-usage`,
|
||||
`${this.apiBase}/api/user/oss-usage`,
|
||||
);
|
||||
|
||||
if (response.data.success) {
|
||||
this.sftpUsage = response.data.usage;
|
||||
this.ossUsage = response.data.usage;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取SFTP空间使用情况失败:', error);
|
||||
this.sftpUsageError = error.response?.data?.message || '获取失败';
|
||||
console.error('获取OSS空间使用情况失败:', error);
|
||||
this.ossUsageError = error.response?.data?.message || '获取失败';
|
||||
} finally {
|
||||
this.sftpUsageLoading = false;
|
||||
this.ossUsageLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// 刷新存储空间使用统计(根据当前存储类型)
|
||||
async refreshStorageUsage() {
|
||||
if (this.storageType === 'oss' && this.user?.has_oss_config) {
|
||||
// 刷新 OSS 空间统计
|
||||
await this.loadOssUsage();
|
||||
} else if (this.storageType === 'local') {
|
||||
// 刷新本地存储统计(通过重新获取用户信息)
|
||||
await this.loadUserProfile();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2368,9 +2374,9 @@ handleDragLeave(e) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 切到SFTP但还未配置,引导弹窗
|
||||
if (type === 'sftp' && (!this.user?.has_ftp_config)) {
|
||||
this.showSftpGuideModal = true;
|
||||
// 切到OSS但还未配置,引导弹窗
|
||||
if (type === 'oss' && (!this.user?.has_oss_config)) {
|
||||
this.showOssGuideModal = true;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2387,7 +2393,7 @@ handleDragLeave(e) {
|
||||
this.storageType = type;
|
||||
// 用户主动切换后,下一次配置同步不提示管理员修改
|
||||
this.suppressStorageToast = true;
|
||||
this.showToast('success', '成功', `已切换到${type === 'local' ? '本地存储' : 'SFTP存储'}`);
|
||||
this.showToast('success', '成功', `已切换到${type === 'local' ? '本地存储' : 'OSS存储'}`);
|
||||
|
||||
// 重新加载文件列表
|
||||
if (this.currentView === 'files') {
|
||||
@@ -2403,33 +2409,33 @@ handleDragLeave(e) {
|
||||
}
|
||||
},
|
||||
|
||||
ensureSftpConfigSection() {
|
||||
this.openSftpConfigModal();
|
||||
ensureOssConfigSection() {
|
||||
this.openOssConfigModal();
|
||||
},
|
||||
|
||||
openSftpGuideModal() {
|
||||
this.showSftpGuideModal = true;
|
||||
openOssGuideModal() {
|
||||
this.showOssGuideModal = true;
|
||||
},
|
||||
|
||||
closeSftpGuideModal() {
|
||||
this.showSftpGuideModal = false;
|
||||
closeOssGuideModal() {
|
||||
this.showOssGuideModal = false;
|
||||
},
|
||||
|
||||
proceedSftpGuide() {
|
||||
this.showSftpGuideModal = false;
|
||||
this.ensureSftpConfigSection();
|
||||
proceedOssGuide() {
|
||||
this.showOssGuideModal = false;
|
||||
this.ensureOssConfigSection();
|
||||
},
|
||||
|
||||
openSftpConfigModal() {
|
||||
this.showSftpGuideModal = false;
|
||||
this.showSftpConfigModal = true;
|
||||
openOssConfigModal() {
|
||||
this.showOssGuideModal = false;
|
||||
this.showOssConfigModal = true;
|
||||
if (this.user && !this.user.is_admin) {
|
||||
this.loadFtpConfig();
|
||||
this.loadOssConfig();
|
||||
}
|
||||
},
|
||||
|
||||
closeSftpConfigModal() {
|
||||
this.showSftpConfigModal = false;
|
||||
closeOssConfigModal() {
|
||||
this.showOssConfigModal = false;
|
||||
},
|
||||
|
||||
// 检查视图权限
|
||||
@@ -2487,7 +2493,7 @@ handleDragLeave(e) {
|
||||
openEditStorageModal(user) {
|
||||
this.editStorageForm.userId = user.id;
|
||||
this.editStorageForm.username = user.username;
|
||||
this.editStorageForm.storage_permission = user.storage_permission || 'sftp_only';
|
||||
this.editStorageForm.storage_permission = user.storage_permission || 'oss_only';
|
||||
|
||||
// 智能识别配额单位
|
||||
const quotaBytes = user.local_storage_quota || 1073741824;
|
||||
@@ -3082,8 +3088,8 @@ handleDragLeave(e) {
|
||||
this.loadSystemSettings();
|
||||
this.loadServerStorageStats();
|
||||
} else if (newView === 'settings' && this.user && !this.user.is_admin) {
|
||||
// 普通用户进入设置页面时加载SFTP配置
|
||||
this.loadFtpConfig();
|
||||
// 普通用户进入设置页面时加载OSS配置
|
||||
this.loadOssConfig();
|
||||
}
|
||||
|
||||
// 记住最后停留的视图(需合法且已登录)
|
||||
|
||||
Reference in New Issue
Block a user