Files
vue-driven-cloud-storage/frontend/app.js
WanWanYun 0f133962dc Initial commit - 玩玩云文件管理系统 v1.0.0
- 完整的前后端代码
- 支持本地存储和SFTP存储
- 文件分享功能
- 上传工具源代码
- 完整的部署文档
- Nginx配置模板

技术栈:
- 后端: Node.js + Express + SQLite
- 前端: Vue.js 3 + Axios
- 存储: 本地存储 / SFTP远程存储
2025-11-10 21:50:16 +08:00

1709 lines
53 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
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配置 - 动态适配localhost或生产环境
apiBase: window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'
? 'http://localhost:40001'
: 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: {
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
}
};
},
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);
// 管理员直接跳转到管理后台
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;
// 刷新到文件页面
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.new_password.length < 6) {
alert('新密码至少6个字符');
return;
}
try {
const response = await axios.post(
`${this.apiBase}/api/user/change-password`,
{
new_password: this.changePasswordForm.new_password
},
{ headers: { Authorization: `Bearer ${this.token}` } }
);
if (response.data.success) {
alert('密码修改成功!');
this.changePasswordForm.new_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');
},
// 检查本地存储的登录状态
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();
// 管理员跳转到管理后台
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还是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));
}
},
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;
this.localQuota = user.local_storage_quota || 0;
this.localUsed = user.local_storage_used || 0;
this.storagePermission = user.storage_permission || 'sftp_only';
this.storageType = user.current_storage_type || 'sftp';
}
} catch (error) {
console.error('加载用户资料失败:', error);
}
},
// 用户切换存储方式
async switchStorage(type) {
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 || '切换存储失败');
}
},
// 管理员:打开编辑用户存储权限模态框
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', '错误', '更新系统设置失败');
}
}
},
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');