Files
vue-driven-cloud-storage/frontend/app.js
yuyx 3045c354f4 优化上传体验:上传前检查文件大小限制
问题:原来文件上传完成后才提示超过大小限制,浪费用户时间

修复:
- 后端:添加 /api/config 公开接口返回上传大小限制
- 前端:页面加载时获取配置
- 前端:uploadFile 函数开始时检查文件大小,超限立即提示

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-25 10:46:10 +08:00

2370 lines
75 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const { createApp } = Vue;
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, // 调试模式(管理员可切换)
// 表单数据
loginForm: {
username: '',
password: '',
captcha: ''
},
registerForm: {
username: '',
email: '',
password: ''
},
// 验证码相关
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: ''
},
showResetPasswordModal: false,
resetPasswordForm: {
token: '',
new_password: ''
},
showResendVerify: false,
resendVerifyEmail: '',
// 系统设置
systemSettings: {
maxUploadSizeMB: 100,
smtp: {
host: '',
port: 465,
secure: true,
user: '',
from: '',
password: '',
has_password: false
}
},
// 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
};
},
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: {
// 提取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();
// 管理员直接跳转到管理后台
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()}`;
},
async resendVerification() {
if (!this.resendVerifyEmail) {
this.showToast('error', '错误', '请输入邮箱或用户名后再重试');
return;
}
try {
const payload = {};
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', '成功', '验证邮件已发送,请查收');
}
} catch (error) {
console.error('重发验证邮件失败:', error);
this.showToast('error', '错误', error.response?.data?.message || '发送失败');
}
},
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: ''
};
}
} 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.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);
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();
// 读取上次停留的视图(需合法才生效)
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);
}
},
// 检查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);
const url = file.httpDownloadUrl
? file.httpDownloadUrl
: `${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'
},
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还是HTMLNginx返回HTMLBackend返回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;
}
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: '' };
}
} catch (error) {
console.error('提交密码重置请求失败:', error);
this.showToast('error', '错误', error.response?.data?.message || '提交失败');
}
},
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);
}
},
// 启动定期检查用户配置
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();
}
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));
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 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';
// 处理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;
}
});
// 检查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');