Files
vue-driven-cloud-storage/frontend/app.js
237899745 a86903fcdc fix: 修复普通用户登录和访问文件时的OSS配置检查
- 登录流程:当没有OSS配置时显示友好警告而不是权限错误
- 文件访问:在调用API前检查OSS配置,避免后端报错
- 更新JS版本号强制刷新缓存

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 19:50:29 +08:00

3225 lines
105 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() {
// 预先确定管理员标签页,避免刷新时状态丢失导致闪烁
const initialAdminTab = (() => {
const saved = localStorage.getItem('adminTab');
return (saved && ['overview', 'settings', 'monitor', 'users'].includes(saved)) ? saved : 'overview';
})();
return {
// API配置
// API配置 - 通过nginx代理访问
apiBase: window.location.protocol + '//' + window.location.host,
// 应用状态
appReady: false, // 应用是否初始化完成防止UI闪烁
// 用户状态
isLoggedIn: false,
user: null,
token: null, // 仅用于内部状态跟踪,实际认证通过 HttpOnly Cookie
tokenRefreshTimer: null,
// 视图状态
currentView: 'files',
isLogin: true,
fileViewMode: 'grid', // 文件显示模式: grid 大图标, list 列表
shareViewMode: 'list', // 分享显示模式: grid 大图标, list 列表
debugMode: false, // 调试模式(管理员可切换)
adminTab: initialAdminTab, // 管理员页面当前标签overview, settings, monitor, users
// 表单数据
loginForm: {
username: '',
password: '',
captcha: ''
},
registerForm: {
username: '',
email: '',
password: '',
captcha: ''
},
registerCaptchaUrl: '',
// 验证码相关
showCaptcha: false,
captchaUrl: '',
// OSS配置表单
ossConfigForm: {
oss_provider: 'aliyun',
oss_region: '',
oss_access_key_id: '',
oss_access_key_secret: '',
oss_bucket: '',
oss_endpoint: ''
},
showOssConfigModal: false,
ossConfigSaving: false, // OSS 配置保存中状态
ossConfigTesting: false, // OSS 配置测试中状态
// 修改密码表单
changePasswordForm: {
current_password: '',
new_password: ''
},
// 用户名修改表单
usernameForm: {
newUsername: ''
},
// 用户资料表单
profileForm: {
email: ''
},
// 管理员资料表单
adminProfileForm: {
username: ''
},
// 分享表单(通用)
shareForm: {
path: '',
password: '',
expiryDays: null
},
currentPath: '/',
files: [],
loading: false,
// 分享管理
shares: [],
showShareAllModal: false,
showShareFileModal: false,
creatingShare: false, // 创建分享中状态
shareAllForm: {
password: "",
expiryType: "never",
customDays: 7
},
shareFileForm: {
fileName: "",
filePath: "",
isDirectory: false, // 新增:标记是否为文件夹
password: "",
expiryType: "never",
customDays: 7
},
shareResult: null,
shareFilters: {
keyword: '',
type: 'all', // all/file/directory/all_files
status: 'all', // all/active/expiring/expired/protected/public
sort: 'created_desc' // created_desc/created_asc/views_desc/downloads_desc/expire_asc
},
// 文件重命名
showRenameModal: false,
renameForm: {
oldName: "",
newName: "",
path: ""
},
// 创建文件夹
showCreateFolderModal: false,
creatingFolder: false, // 创建文件夹中状态
createFolderForm: {
folderName: ""
},
// 文件夹详情
showFolderInfoModal: false,
folderInfo: null,
// 上传
showUploadModal: false,
uploadProgress: 0,
uploadedBytes: 0,
totalBytes: 0,
uploadingFileName: '',
isDragging: false,
modalMouseDownTarget: null, // 模态框鼠标按下的目标
// 管理员
adminUsers: [],
showResetPwdModal: false,
resetPwdUser: {},
newPassword: '',
// 文件审查
showFileInspectionModal: false,
inspectionUser: null,
inspectionFiles: [],
inspectionPath: '/',
inspectionLoading: false,
inspectionViewMode: 'grid', // 文件审查显示模式: grid 大图标, list 列表
// 忘记密码
showForgotPasswordModal: false,
forgotPasswordForm: {
email: '',
captcha: ''
},
forgotPasswordCaptchaUrl: '',
showResetPasswordModal: false,
resetPasswordForm: {
token: '',
new_password: ''
},
showResendVerify: false,
resendVerifyEmail: '',
resendVerifyCaptcha: '',
resendVerifyCaptchaUrl: '',
// 加载状态
loginLoading: false, // 登录中
registerLoading: false, // 注册中
passwordChanging: false, // 修改密码中
usernameChanging: false, // 修改用户名中
passwordResetting: false, // 重置密码中
resendingVerify: false, // 重发验证邮件中
// 系统设置
systemSettings: {
maxUploadSizeMB: 100,
smtp: {
host: '',
port: 465,
secure: true,
user: '',
from: '',
password: '',
has_password: false
}
},
// 健康检测
healthCheck: {
loading: initialAdminTab === 'monitor',
lastCheck: null,
overallStatus: null, // healthy, warning, critical
summary: { total: 0, pass: 0, warning: 0, fail: 0, info: 0 },
checks: []
},
// 系统日志
systemLogs: {
loading: initialAdminTab === 'monitor',
logs: [],
total: 0,
page: 1,
pageSize: 30,
totalPages: 0,
filters: {
level: '',
category: '',
keyword: ''
}
},
// 监控页整体加载遮罩(避免刷新时闪一下空态)
monitorTabLoading: initialAdminTab === 'monitor',
// Toast通知
toasts: [],
toastIdCounter: 0,
// 上传限制字节默认10GB
maxUploadSize: 10737418240,
// 提示信息
errorMessage: '',
successMessage: '',
verifyMessage: '',
// 存储相关
storageType: 'oss', // 当前使用的存储类型
storagePermission: 'oss_only', // 存储权限
localQuota: 0, // 本地存储配额(字节)
localUsed: 0, // 本地存储已使用(字节)
ossQuota: 0, // OSS 存储配额字节0表示无限制
// 右键菜单
showContextMenu: false,
contextMenuX: 0,
contextMenuY: 0,
contextMenuFile: null,
// 长按检测
longPressTimer: null,
longPressStartX: 0,
longPressStartY: 0,
longPressFile: null,
// 媒体预览
showImageViewer: false,
showVideoPlayer: false,
showAudioPlayer: false,
currentMediaUrl: '',
currentMediaName: '',
currentMediaType: '', // 'image', 'video', 'audio'
longPressDuration: 500, // 长按时间(毫秒)
// 管理员编辑用户存储权限
showEditStorageModal: false,
editStorageForm: {
userId: null,
username: '',
storage_permission: 'oss_only',
local_storage_quota_value: 1, // 配额数值
quota_unit: 'GB', // 配额单位MB 或 GB
oss_storage_quota_value: 0, // OSS配额数值
oss_quota_unit: 'GB' // OSS配额单位
},
// 服务器存储统计
serverStorageStats: {
totalDisk: 0,
usedDisk: 0,
availableDisk: 0,
totalUserQuotas: 0,
totalUserUsed: 0,
totalUsers: 0
},
// 定期检查用户配置更新的定时器
profileCheckInterval: null,
// 存储切换状态
storageSwitching: false,
storageSwitchTarget: null,
suppressStorageToast: false,
profileInitialized: false,
// OSS配置引导弹窗
showOssGuideModal: false,
// OSS空间使用统计
ossUsage: null, // { totalSize, totalSizeFormatted, fileCount, dirCount }
ossUsageLoading: false,
ossUsageError: null,
// 主题设置
currentTheme: 'dark', // 当前生效的主题: 'dark' 或 'light'
globalTheme: 'dark', // 全局默认主题(管理员设置)
userThemePreference: null // 用户主题偏好: 'dark', 'light', 或 null(跟随全局)
};
},
computed: {
pathParts() {
return this.currentPath.split('/').filter(p => p !== '');
},
// 格式化配额显示
localQuotaFormatted() {
return this.formatBytes(this.localQuota);
},
localUsedFormatted() {
return this.formatBytes(this.localUsed);
},
// 配额使用百分比
quotaPercentage() {
if (this.localQuota === 0) return 0;
return Math.round((this.localUsed / this.localQuota) * 100);
},
// 存储类型显示文本
storageTypeText() {
return this.storageType === 'local' ? '本地存储' : 'OSS存储';
},
// 分享筛选+排序后的列表
filteredShares() {
let list = [...this.shares];
const keyword = this.shareFilters.keyword.trim().toLowerCase();
if (keyword) {
list = list.filter(s =>
(s.share_path || '').toLowerCase().includes(keyword) ||
(s.share_code || '').toLowerCase().includes(keyword) ||
(s.share_url || '').toLowerCase().includes(keyword)
);
}
if (this.shareFilters.type !== 'all') {
const targetType = this.shareFilters.type === 'all_files' ? 'all' : this.shareFilters.type;
list = list.filter(s => (s.share_type || 'file') === targetType);
}
if (this.shareFilters.status !== 'all') {
list = list.filter(s => {
if (this.shareFilters.status === 'expired') return this.isExpired(s.expires_at);
if (this.shareFilters.status === 'expiring') return this.isExpiringSoon(s.expires_at) && !this.isExpired(s.expires_at);
if (this.shareFilters.status === 'active') return !this.isExpired(s.expires_at);
if (this.shareFilters.status === 'protected') return !!s.share_password;
if (this.shareFilters.status === 'public') return !s.share_password;
return true;
});
}
list.sort((a, b) => {
const getTime = s => s.created_at ? new Date(s.created_at).getTime() : 0;
const getExpire = s => s.expires_at ? new Date(s.expires_at).getTime() : Number.MAX_SAFE_INTEGER;
switch (this.shareFilters.sort) {
case 'created_asc':
return getTime(a) - getTime(b);
case 'views_desc':
return (b.view_count || 0) - (a.view_count || 0);
case 'downloads_desc':
return (b.download_count || 0) - (a.download_count || 0);
case 'expire_asc':
return getExpire(a) - getExpire(b);
default:
return getTime(b) - getTime(a); // created_desc
}
});
return list;
}
},
methods: {
// ========== 工具函数 ==========
// 防抖函数 - 避免频繁调用
debounce(fn, delay) {
let timer = null;
return function(...args) {
if (timer) clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
},
// 创建防抖版本的 loadUserProfile延迟2秒避免频繁请求
debouncedLoadUserProfile() {
if (!this._debouncedLoadUserProfile) {
this._debouncedLoadUserProfile = this.debounce(() => {
this.loadUserProfile();
}, 2000);
}
this._debouncedLoadUserProfile();
},
// ========== 主题管理 ==========
// 初始化主题
async initTheme() {
// 先从localStorage读取避免页面闪烁
const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
this.applyTheme(savedTheme);
}
// 如果没有登录从公开API获取全局主题
if (!this.token) {
try {
const res = await axios.get(`${this.apiBase}/api/public/theme`);
if (res.data.success) {
this.globalTheme = res.data.theme;
this.applyTheme(res.data.theme);
}
} catch (e) {
console.log('无法加载全局主题');
}
}
},
// 加载用户主题设置(登录后调用)
async loadUserTheme() {
try {
const res = await axios.get(`${this.apiBase}/api/user/theme`);
if (res.data.success) {
this.globalTheme = res.data.theme.global;
this.userThemePreference = res.data.theme.user;
this.currentTheme = res.data.theme.effective;
this.applyTheme(this.currentTheme);
localStorage.setItem('theme', this.currentTheme);
}
} catch (error) {
console.error('加载主题设置失败:', error);
}
},
// 应用主题到DOM
applyTheme(theme) {
this.currentTheme = theme;
if (theme === 'light') {
document.body.classList.add('light-theme');
} else {
document.body.classList.remove('light-theme');
}
},
// 切换用户主题偏好
async setUserTheme(theme) {
try {
const res = await axios.post(`${this.apiBase}/api/user/theme`,
{ theme },
);
if (res.data.success) {
this.userThemePreference = res.data.theme.user;
this.currentTheme = res.data.theme.effective;
this.applyTheme(this.currentTheme);
localStorage.setItem('theme', this.currentTheme);
this.showToast('success', '主题已更新', theme === null ? '已设为跟随全局' : (theme === 'dark' ? '已切换到暗色主题' : '已切换到亮色主题'));
}
} catch (error) {
this.showToast('error', '主题更新失败', error.response?.data?.message || '请稍后重试');
}
},
// 获取主题显示文本
getThemeText(theme) {
if (theme === null) return '跟随全局';
return theme === 'dark' ? '暗色主题' : '亮色主题';
},
// 设置全局主题(管理员)
async setGlobalTheme(theme) {
try {
console.log('[主题] 设置全局主题:', theme);
const res = await axios.post(`${this.apiBase}/api/admin/settings`,
{ global_theme: theme },
);
console.log('[主题] API响应:', res.data);
if (res.data.success) {
this.globalTheme = theme;
console.log('[主题] globalTheme已更新为:', this.globalTheme);
// 如果用户没有设置个人偏好,则跟随全局
if (this.userThemePreference === null) {
this.currentTheme = theme;
this.applyTheme(theme);
localStorage.setItem('theme', theme);
} else {
console.log('[主题] 用户有个人偏好,不更改当前显示主题:', this.userThemePreference);
}
// 提示信息
let toastMsg = theme === 'dark' ? '默认暗色主题' : '默认亮色主题';
if (this.userThemePreference !== null) {
toastMsg += '(你设置了个人偏好,不受全局影响)';
}
this.showToast('success', '全局主题已更新', toastMsg);
} else {
console.error('[主题] API返回失败:', res.data);
this.showToast('error', '设置失败', res.data.message || '未知错误');
}
} catch (error) {
console.error('[主题] 设置全局主题失败:', error);
this.showToast('error', '设置失败', error.response?.data?.message || '请稍后重试');
}
},
// 提取URL中的token兼容缺少 ? 的场景)
getTokenFromUrl(key) {
const currentHref = window.location.href;
const url = new URL(currentHref);
let token = url.searchParams.get(key);
if (!token) {
const match = currentHref.match(new RegExp(`${key}=([\\w-]+)`));
if (match && match[1]) {
token = match[1];
}
}
return token;
},
// 清理URL中的token同时处理路径和查询参数
sanitizeUrlToken(key) {
const url = new URL(window.location.href);
url.searchParams.delete(key);
if (url.pathname.includes(`${key}=`)) {
url.pathname = url.pathname.split(`${key}=`)[0];
}
window.history.replaceState({}, document.title, url.toString());
},
// 模态框点击外部关闭优化 - 防止拖动选择文本时误关闭
handleModalMouseDown(e) {
// 记录鼠标按下时的目标
this.modalMouseDownTarget = e.target;
},
handleModalMouseUp(modalName, e) {
// 只有在同一个overlay元素上按下和释放鼠标时才关闭
if (e && 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 = '';
// 切换到注册模式时加载验证码
if (!this.isLogin) {
this.refreshRegisterCaptcha();
}
},
async handleLogin() {
this.errorMessage = '';
this.loginLoading = true;
try {
const response = await axios.post(`${this.apiBase}/api/login`, this.loginForm);
if (response.data.success) {
// token 和 refreshToken 都通过 HttpOnly Cookie 自动管理
this.user = response.data.user;
this.isLoggedIn = true;
this.showResendVerify = false;
this.resendVerifyEmail = '';
// 登录成功后隐藏验证码并清空验证码输入
this.showCaptcha = false;
this.loginForm.captcha = '';
// 保存用户信息到localStorage非敏感信息用于页面刷新后恢复
// 注意token 通过 HttpOnly Cookie 传递,不再存储在 localStorage
localStorage.setItem('user', JSON.stringify(this.user));
// 启动token自动刷新在过期前5分钟刷新
const expiresIn = response.data.expiresIn || (2 * 60 * 60 * 1000);
this.startTokenRefresh(expiresIn);
// 直接从登录响应中获取存储信息
this.storagePermission = this.user.storage_permission || 'oss_only';
this.storageType = this.user.current_storage_type || 'oss';
this.localQuota = this.user.local_storage_quota || 0;
this.localUsed = this.user.local_storage_used || 0;
this.ossQuota = this.user.oss_storage_quota || 0;
console.log('[登录] 存储权限:', this.storagePermission, '存储类型:', this.storageType, 'OSS配置:', this.user.oss_config_source);
// 智能存储类型修正如果当前是OSS但未配置包括个人配置和系统级配置且用户有本地存储权限自动切换到本地
if (this.storageType === 'oss' && (!this.user || this.user.oss_config_source === 'none')) {
if (this.storagePermission === 'local_only' || this.storagePermission === 'user_choice') {
console.log('[登录] OSS未配置但用户有本地存储权限自动切换到本地存储');
this.storageType = 'local';
// 异步更新到后端(不等待,避免阻塞登录流程)
axios.post(`${this.apiBase}/api/user/switch-storage`, { storage_type: 'local' })
.catch(err => console.error('[登录] 自动切换存储类型失败:', err));
}
}
// 启动定期检查用户配置
this.startProfileSync();
// 加载用户主题设置
this.loadUserTheme();
// 管理员直接跳转到管理后台
if (this.user.is_admin) {
this.currentView = 'admin';
}
// 普通用户:检查存储权限
else {
// 如果用户可以使用本地存储,直接进入文件页面
if (this.storagePermission === 'local_only' || this.storagePermission === 'user_choice') {
this.currentView = 'files';
this.loadFiles('/');
}
// 如果仅OSS模式需要检查是否配置了OSS包括系统级统一配置
else if (this.storagePermission === 'oss_only') {
if (this.user?.oss_config_source !== 'none') {
this.currentView = 'files';
this.loadFiles('/');
} else {
this.currentView = 'settings';
this.showToast('warning', 'OSS未配置', '系统尚未配置OSS存储服务请联系管理员进行配置');
// 普通用户不需要打开配置弹窗,等待管理员配置
}
} 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 = '';
}
} finally {
this.loginLoading = false;
}
},
// 通用验证码加载函数(带防抖)
async loadCaptcha(targetField) {
// 防抖2秒内不重复请求
const now = Date.now();
if (this._lastCaptchaTime && (now - this._lastCaptchaTime) < 2000) {
console.log('[验证码] 请求过于频繁,跳过');
return;
}
this._lastCaptchaTime = now;
try {
const response = await axios.get(`${this.apiBase}/api/captcha?t=${now}`, {
responseType: 'blob'
});
this[targetField] = URL.createObjectURL(response.data);
} catch (error) {
console.error('获取验证码失败:', error);
// 如果是429错误不清除已有验证码
if (error.response?.status !== 429) {
this[targetField] = '';
}
}
},
// 刷新验证码(登录)
refreshCaptcha() {
this.loadCaptcha('captchaUrl');
},
// 刷新注册验证码
refreshRegisterCaptcha() {
this.loadCaptcha('registerCaptchaUrl');
},
// 刷新忘记密码验证码
refreshForgotPasswordCaptcha() {
this.loadCaptcha('forgotPasswordCaptchaUrl');
},
// 刷新重发验证邮件验证码
refreshResendVerifyCaptcha() {
this.loadCaptcha('resendVerifyCaptchaUrl');
},
async resendVerification() {
if (!this.resendVerifyEmail) {
this.showToast('error', '错误', '请输入邮箱或用户名后再重试');
return;
}
if (!this.resendVerifyCaptcha) {
this.showToast('error', '错误', '请输入验证码');
return;
}
this.resendingVerify = true;
try {
const payload = { captcha: this.resendVerifyCaptcha };
if (this.resendVerifyEmail.includes('@')) {
payload.email = this.resendVerifyEmail;
} else {
payload.username = this.resendVerifyEmail;
}
const response = await axios.post(`${this.apiBase}/api/resend-verification`, payload);
if (response.data.success) {
this.showToast('success', '成功', '验证邮件已发送,请查收');
this.showResendVerify = false;
this.resendVerifyEmail = '';
this.resendVerifyCaptcha = '';
this.resendVerifyCaptchaUrl = '';
}
} catch (error) {
console.error('重发验证邮件失败:', error);
this.showToast('error', '错误', error.response?.data?.message || '发送失败');
// 刷新验证码
this.resendVerifyCaptcha = '';
this.refreshResendVerifyCaptcha();
} finally {
this.resendingVerify = false;
}
},
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 = '';
this.registerLoading = true;
try {
const response = await axios.post(`${this.apiBase}/api/register`, this.registerForm);
if (response.data.success) {
this.successMessage = '注册成功!请查收邮箱完成验证后再登录';
this.isLogin = true;
// 清空表单
this.registerForm = {
username: '',
email: '',
password: '',
captcha: ''
};
this.registerCaptchaUrl = '';
}
} catch (error) {
const errorData = error.response?.data;
if (errorData?.errors) {
this.errorMessage = errorData.errors.map(e => e.msg).join(', ');
} else {
this.errorMessage = errorData?.message || '注册失败';
}
// 刷新验证码
this.registerForm.captcha = '';
this.refreshRegisterCaptcha();
} finally {
this.registerLoading = false;
}
},
async updateOssConfig() {
// 防止重复提交
if (this.ossConfigSaving) {
return;
}
// 前端验证
if (!this.ossConfigForm.oss_provider || !['aliyun', 'tencent', 'aws'].includes(this.ossConfigForm.oss_provider)) {
this.showToast('error', '配置错误', '请选择有效的 OSS 服务商');
return;
}
if (!this.ossConfigForm.oss_region || this.ossConfigForm.oss_region.trim() === '') {
this.showToast('error', '配置错误', '地域/Region 不能为空');
return;
}
if (!this.ossConfigForm.oss_access_key_id || this.ossConfigForm.oss_access_key_id.trim() === '') {
this.showToast('error', '配置错误', 'Access Key ID 不能为空');
return;
}
if (!this.ossConfigForm.oss_access_key_secret || this.ossConfigForm.oss_access_key_secret.trim() === '') {
this.showToast('error', '配置错误', 'Access Key Secret 不能为空');
return;
}
if (!this.ossConfigForm.oss_bucket || this.ossConfigForm.oss_bucket.trim() === '') {
this.showToast('error', '配置错误', 'Bucket 名称不能为空');
return;
}
this.ossConfigSaving = true;
try {
const response = await axios.post(
`${this.apiBase}/api/user/update-oss`,
this.ossConfigForm,
);
if (response.data.success) {
// 更新用户信息
this.user.has_oss_config = 1;
// 如果用户有 user_choice 权限,自动切换到 OSS 存储
if (this.storagePermission === 'user_choice' || this.storagePermission === 'oss_only') {
try {
const switchResponse = await axios.post(
`${this.apiBase}/api/user/switch-storage`,
{ storage_type: 'oss' },
);
if (switchResponse.data.success) {
this.storageType = 'oss';
console.log('[OSS配置] 已自动切换到OSS存储模式');
}
} catch (err) {
console.error('[OSS配置] 自动切换存储模式失败:', err);
}
}
// 关闭配置弹窗
this.showOssConfigModal = false;
// 管理员配置OSS后留在当前页面不跳转到文件列表
// 显示成功提示
this.showToast('success', '配置成功', 'OSS存储配置已保存');
}
} catch (error) {
console.error('OSS配置保存失败:', error);
this.showToast('error', '配置失败', error.response?.data?.message || error.message || '请检查配置信息后重试');
} finally {
this.ossConfigSaving = false;
}
},
// 测试 OSS 连接(不保存配置)
async testOssConnection() {
// 防止重复提交
if (this.ossConfigTesting) {
return;
}
// 前端验证
if (!this.ossConfigForm.oss_provider || !['aliyun', 'tencent', 'aws'].includes(this.ossConfigForm.oss_provider)) {
this.showToast('error', '配置错误', '请选择有效的 OSS 服务商');
return;
}
if (!this.ossConfigForm.oss_region || this.ossConfigForm.oss_region.trim() === '') {
this.showToast('error', '配置错误', '地域/Region 不能为空');
return;
}
if (!this.ossConfigForm.oss_access_key_id || this.ossConfigForm.oss_access_key_id.trim() === '') {
this.showToast('error', '配置错误', 'Access Key ID 不能为空');
return;
}
// 如果用户已有配置Secret 可以为空(使用现有密钥)
if (this.user?.oss_config_source === 'none' && (!this.ossConfigForm.oss_access_key_secret || this.ossConfigForm.oss_access_key_secret.trim() === '')) {
this.showToast('error', '配置错误', 'Access Key Secret 不能为空');
return;
}
if (!this.ossConfigForm.oss_bucket || this.ossConfigForm.oss_bucket.trim() === '') {
this.showToast('error', '配置错误', 'Bucket 名称不能为空');
return;
}
this.ossConfigTesting = true;
try {
const response = await axios.post(
`${this.apiBase}/api/user/test-oss`,
this.ossConfigForm,
);
if (response.data.success) {
this.showToast('success', '连接成功', 'OSS 配置验证通过,可以保存');
}
} catch (error) {
console.error('OSS连接测试失败:', error);
this.showToast('error', '连接失败', error.response?.data?.message || error.message || '请检查配置信息');
} finally {
this.ossConfigTesting = false;
}
},
async updateAdminProfile() {
try {
const response = await axios.post(
`${this.apiBase}/api/admin/update-profile`,
{
username: this.adminProfileForm.username
},
);
if (response.data.success) {
this.showToast('success', '成功', '用户名已更新!即将重新登录');
// 更新用户信息(后端已通过 Cookie 更新 token
if (response.data.user) {
this.user = response.data.user;
localStorage.setItem('user', JSON.stringify(response.data.user));
}
// 延迟后重新登录
setTimeout(() => this.logout(), 1500);
}
} catch (error) {
this.showToast('error', '错误', '修改失败: ' + (error.response?.data?.message || error.message));
}
},
async changePassword() {
if (!this.changePasswordForm.current_password) {
this.showToast('warning', '提示', '请输入当前密码');
return;
}
if (this.changePasswordForm.new_password.length < 6) {
this.showToast('warning', '提示', '新密码至少6个字符');
return;
}
this.passwordChanging = true;
try {
const response = await axios.post(
`${this.apiBase}/api/user/change-password`,
{
current_password: this.changePasswordForm.current_password,
new_password: this.changePasswordForm.new_password
},
);
if (response.data.success) {
this.showToast('success', '成功', '密码修改成功!');
this.changePasswordForm.new_password = '';
this.changePasswordForm.current_password = '';
}
} catch (error) {
this.showToast('error', '错误', '密码修改失败: ' + (error.response?.data?.message || error.message));
} finally {
this.passwordChanging = false;
}
},
async loadOssConfig() {
try {
const response = await axios.get(
`${this.apiBase}/api/user/profile`,
);
if (response.data.success && response.data.user) {
const user = response.data.user;
// 填充OSS配置表单密钥不回显
this.ossConfigForm.oss_provider = user.oss_provider || 'aliyun';
this.ossConfigForm.oss_region = user.oss_region || '';
this.ossConfigForm.oss_access_key_id = user.oss_access_key_id || '';
this.ossConfigForm.oss_access_key_secret = ''; // 密钥不回显
this.ossConfigForm.oss_bucket = user.oss_bucket || '';
this.ossConfigForm.oss_endpoint = user.oss_endpoint || '';
}
} catch (error) {
console.error('加载OSS配置失败:', error);
}
},
// 上传工具配置引导已移除OSS 不需要配置文件导入)
async updateUsername() {
if (!this.usernameForm.newUsername || this.usernameForm.newUsername.length < 3) {
this.showToast('warning', '提示', '用户名至少3个字符');
return;
}
this.usernameChanging = true;
try {
const response = await axios.post(
`${this.apiBase}/api/user/update-username`,
{ username: this.usernameForm.newUsername },
);
if (response.data.success) {
this.showToast('success', '成功', '用户名修改成功!');
// 更新本地用户信息
this.user.username = this.usernameForm.newUsername;
localStorage.setItem('user', JSON.stringify(this.user));
this.usernameForm.newUsername = '';
}
} catch (error) {
this.showToast('error', '错误', '用户名修改失败: ' + (error.response?.data?.message || error.message));
} finally {
this.usernameChanging = false;
}
},
async updateProfile() {
try {
const response = await axios.post(
`${this.apiBase}/api/user/update-profile`,
{ email: this.profileForm.email },
);
if (response.data.success) {
this.showToast('success', '成功', '邮箱已更新!');
// 更新本地用户信息
if (response.data.user) {
this.user = response.data.user;
localStorage.setItem('user', JSON.stringify(this.user));
}
}
} catch (error) {
this.showToast('error', '错误', '更新失败: ' + (error.response?.data?.message || error.message));
}
},
async logout() {
// 调用后端清除 HttpOnly Cookie
try {
await axios.post(`${this.apiBase}/api/logout`);
} catch (err) {
console.error('[登出] 清除Cookie失败:', err);
}
this.isLoggedIn = false;
this.user = null;
this.token = null;
this.stopTokenRefresh();
localStorage.removeItem('user');
localStorage.removeItem('lastView');
localStorage.removeItem('adminTab');
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);
// 使用默认值
}
},
// 检查登录状态(通过 HttpOnly Cookie 验证)
async checkLoginStatus() {
// 直接调用API验证Cookie会自动携带
try {
const response = await axios.get(`${this.apiBase}/api/user/profile`);
if (response.data.success && response.data.user) {
// Cookie有效用户已登录
this.user = response.data.user;
this.isLoggedIn = true;
// 更新localStorage中的用户信息非敏感信息
localStorage.setItem('user', JSON.stringify(this.user));
// 从最新的用户信息初始化存储相关字段
this.storagePermission = this.user.storage_permission || 'oss_only';
this.storageType = this.user.current_storage_type || 'oss';
this.localQuota = this.user.local_storage_quota || 0;
this.localUsed = this.user.local_storage_used || 0;
this.ossQuota = this.user.oss_storage_quota || 0;
console.log('[页面加载] Cookie验证成功存储权限:', this.storagePermission, '存储类型:', this.storageType);
// 启动token自动刷新假设剩余1.5小时,实际由服务端控制)
this.startTokenRefresh(1.5 * 60 * 60 * 1000);
// 启动定期检查用户配置
this.startProfileSync();
// 加载用户主题设置
this.loadUserTheme();
// 读取上次停留的视图(需合法才生效)
const savedView = localStorage.getItem('lastView');
let targetView = null;
if (savedView && this.isViewAllowed(savedView)) {
targetView = savedView;
} else if (this.user.is_admin) {
targetView = 'admin';
} else if (this.storagePermission === 'oss_only' && this.user?.oss_config_source === 'none') {
targetView = 'settings';
} else {
targetView = 'files';
}
// 强制切换到目标视图并加载数据
this.switchView(targetView, true);
}
} catch (error) {
// 401表示未登录或Cookie过期静默处理用户需要重新登录
if (error.response?.status === 401) {
console.log('[页面加载] 未登录或Cookie已过期');
} else {
console.warn('[页面加载] 验证登录状态失败:', error.message);
}
// 清理可能残留的用户信息
localStorage.removeItem('user');
} finally {
// 无论登录验证成功还是失败,都标记应用已准备就绪
this.appReady = true;
}
},
// 尝试刷新token失败则登出
async tryRefreshOrLogout() {
// refreshToken 通过 Cookie 自动管理,直接尝试刷新
const refreshed = await this.doRefreshToken();
if (refreshed) {
await this.checkLoginStatus();
return;
}
this.handleTokenExpired();
},
// 处理token过期/失效
handleTokenExpired() {
console.log('[认证] Cookie已失效清除登录状态');
this.isLoggedIn = false;
this.user = null;
this.token = null;
this.stopTokenRefresh();
localStorage.removeItem('user');
localStorage.removeItem('lastView');
this.stopProfileSync();
},
// 启动token自动刷新定时器
startTokenRefresh(expiresIn) {
this.stopTokenRefresh(); // 先清除旧的定时器
// 在token过期前5分钟刷新
const refreshTime = Math.max(expiresIn - 5 * 60 * 1000, 60 * 1000);
console.log(`[认证] Token将在 ${Math.round(refreshTime / 60000)} 分钟后刷新`);
this.tokenRefreshTimer = setTimeout(async () => {
await this.doRefreshToken();
}, refreshTime);
},
// 停止token刷新定时器
stopTokenRefresh() {
if (this.tokenRefreshTimer) {
clearTimeout(this.tokenRefreshTimer);
this.tokenRefreshTimer = null;
}
},
// 执行token刷新refreshToken 通过 HttpOnly Cookie 自动发送)
async doRefreshToken() {
try {
console.log('[认证] 正在刷新access token...');
// refreshToken 通过 Cookie 自动携带,无需手动传递
const response = await axios.post(`${this.apiBase}/api/refresh-token`);
if (response.data.success) {
// 后端已自动更新 HttpOnly Cookie 中的 token
console.log('[认证] Token刷新成功Cookie已更新');
// 继续下一次刷新
const expiresIn = response.data.expiresIn || (2 * 60 * 60 * 1000);
this.startTokenRefresh(expiresIn);
return true;
}
} catch (error) {
console.error('[认证] Token刷新失败:', error.response?.data?.message || error.message);
// 刷新失败,需要重新登录
this.handleTokenExpired();
}
return false;
},
// 检查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) {
// 检查是否有可用的存储配置
if (this.storagePermission === 'oss_only' && this.user?.oss_config_source === 'none') {
this.showToast('warning', '无法访问文件', 'OSS存储未配置请联系管理员进行配置');
this.currentView = 'settings';
return;
}
this.loading = true;
// 确保路径不为undefined
this.currentPath = path || '/';
try {
const response = await axios.get(`${this.apiBase}/api/files`, {
params: { path }
});
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;
}
// 更新用户本地存储信息(使用防抖避免频繁请求)
this.debouncedLoadUserProfile();
}
} catch (error) {
console.error('加载文件失败:', error);
this.showToast('error', '加载失败', error.response?.data?.message || error.message);
if (error.response?.status === 401) {
this.logout();
}
} finally {
this.loading = false;
}
},
async 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)) {
await this.openImageViewer(file);
} else if (file.name.match(/\.(mp4|avi|mov|wmv|flv|mkv|webm)$/i)) {
await this.openVideoPlayer(file);
} else if (file.name.match(/\.(mp3|wav|flac|aac|ogg|m4a)$/i)) {
await 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) {
// 构建文件路径
const filePath = this.currentPath === '/' ? `/${file.name}` : `${this.currentPath}/${file.name}`;
// OSS 模式:使用签名 URL 直连下载(不经过后端)
if (this.storageType === 'oss' && this.user?.oss_config_source !== 'none') {
this.downloadFromOSS(filePath);
} else {
// 本地存储模式:通过后端下载
this.downloadFromLocal(filePath);
}
},
// OSS 直连下载使用签名URL不经过后端节省后端带宽
async downloadFromOSS(filePath) {
try {
// 获取签名 URL
const { data } = await axios.get(`${this.apiBase}/api/files/download-url`, {
params: { path: filePath }
});
if (data.success) {
// 直连 OSS 下载不经过后端充分利用OSS带宽和CDN
window.open(data.downloadUrl, '_blank');
} else {
// 处理后端返回的错误
console.error('获取下载链接失败:', data.message);
this.showToast('error', '下载失败', data.message || '获取下载链接失败');
}
} catch (error) {
console.error('获取下载链接失败:', error);
const errorMsg = error.response?.data?.message || error.message || '获取下载链接失败';
this.showToast('error', '下载失败', errorMsg);
}
},
// 本地存储下载
downloadFromLocal(filePath) {
const url = `${this.apiBase}/api/files/download?path=${encodeURIComponent(filePath)}`;
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', '');
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) {
this.showToast('warning', '提示', '请输入新的文件名');
return;
}
try {
const response = await axios.post(
`${this.apiBase}/api/files/rename`,
this.renameForm,
);
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() {
if (this.creatingFolder) return; // 防止重复提交
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;
}
this.creatingFolder = true;
try {
const response = await axios.post(`${this.apiBase}/api/files/mkdir`, {
path: this.currentPath,
folderName: folderName
});
if (response.data.success) {
this.showToast('success', '成功', '文件夹创建成功');
this.showCreateFolderModal = false;
this.createFolderForm.folderName = '';
await this.loadFiles(this.currentPath); // 刷新文件列表
await this.refreshStorageUsage(); // 刷新空间统计OSS会增加空对象
}
} catch (error) {
console.error('[创建文件夹失败]', error);
this.showToast('error', '错误', error.response?.data?.message || '创建文件夹失败');
} finally {
this.creatingFolder = false;
}
},
// 显示文件夹详情
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
});
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; // 文件夹不响应长按
// 记录初始触摸位置,用于检测是否在滑动
const touch = event.touches[0];
this.longPressStartX = touch.clientX;
this.longPressStartY = touch.clientY;
this.longPressFile = file;
this.longPressTimer = setTimeout(() => {
// 触发长按菜单
this.contextMenuFile = file;
// 使用记录的触摸位置
this.contextMenuX = this.longPressStartX;
this.contextMenuY = this.longPressStartY;
this.showContextMenu = true;
// 触摸震动反馈(如果支持)
if (navigator.vibrate) {
navigator.vibrate(50);
}
// 点击其他地方关闭菜单
this.$nextTick(() => {
document.addEventListener('click', this.hideContextMenu, { once: true });
});
}, this.longPressDuration);
},
// 长按移动检测(移动端)- 滑动时取消长按
handleLongPressMove(event) {
if (!this.longPressTimer) return;
const touch = event.touches[0];
const moveX = Math.abs(touch.clientX - this.longPressStartX);
const moveY = Math.abs(touch.clientY - this.longPressStartY);
// 如果移动超过10px认为是滑动取消长按
if (moveX > 10 || moveY > 10) {
clearTimeout(this.longPressTimer);
this.longPressTimer = null;
}
},
// 长按取消(移动端)
handleLongPressEnd() {
if (this.longPressTimer) {
clearTimeout(this.longPressTimer);
this.longPressTimer = null;
}
},
// 从菜单执行操作
async contextMenuAction(action) {
if (!this.contextMenuFile) return;
switch (action) {
case 'preview':
// 根据文件类型打开对应的预览(异步)
if (this.contextMenuFile.name.match(/\.(jpg|jpeg|png|gif|bmp|svg|webp)$/i)) {
await this.openImageViewer(this.contextMenuFile);
} else if (this.contextMenuFile.name.match(/\.(mp4|avi|mov|wmv|flv|mkv|webm)$/i)) {
await this.openVideoPlayer(this.contextMenuFile);
} else if (this.contextMenuFile.name.match(/\.(mp3|wav|flac|aac|ogg|m4a)$/i)) {
await 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();
},
// ===== 媒体预览功能 =====
// 获取媒体文件URLOSS直连或后端代理
async getMediaUrl(file) {
const filePath = this.currentPath === '/'
? `/${file.name}`
: `${this.currentPath}/${file.name}`;
// OSS 模式:返回签名 URL用于媒体预览
if (this.storageType === 'oss' && this.user?.oss_config_source !== 'none') {
try {
const { data } = await axios.get(`${this.apiBase}/api/files/download-url`, {
params: { path: filePath }
});
return data.success ? data.downloadUrl : null;
} catch (error) {
console.error('获取媒体URL失败:', error);
return null;
}
}
// 本地存储模式:通过后端 API
return `${this.apiBase}/api/files/download?path=${encodeURIComponent(filePath)}`;
},
// 获取文件缩略图URL同步方法用于本地存储模式
// 注意OSS 模式下缩略图需要单独处理此处返回本地存储的直接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;
// 本地存储模式:返回同步的下载 URL
// OSS 模式下缩略图功能暂不支持(需要预签名 URL建议点击文件预览
if (this.storageType !== 'oss') {
const filePath = this.currentPath === '/'
? `/${file.name}`
: `${this.currentPath}/${file.name}`;
return `${this.apiBase}/api/files/download?path=${encodeURIComponent(filePath)}`;
}
// OSS 模式暂不支持同步缩略图,返回 null
return null;
},
// 打开图片预览
async openImageViewer(file) {
const url = await this.getMediaUrl(file);
if (url) {
this.currentMediaUrl = url;
this.currentMediaName = file.name;
this.currentMediaType = 'image';
this.showImageViewer = true;
} else {
this.showToast('error', '错误', '无法获取文件预览链接');
}
},
// 打开视频播放器
async openVideoPlayer(file) {
const url = await this.getMediaUrl(file);
if (url) {
this.currentMediaUrl = url;
this.currentMediaName = file.name;
this.currentMediaType = 'video';
this.showVideoPlayer = true;
} else {
this.showToast('error', '错误', '无法获取文件预览链接');
}
},
// 打开音频播放器
async openAudioPlayer(file) {
const url = await this.getMediaUrl(file);
if (url) {
this.currentMediaUrl = url;
this.currentMediaName = file.name;
this.currentMediaType = 'audio';
this.showAudioPlayer = true;
} else {
this.showToast('error', '错误', '无法获取文件预览链接');
}
},
// 关闭媒体预览
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
},
);
if (response.data.success) {
this.showToast('success', '成功', '文件已删除');
// 刷新文件列表和空间统计
await this.loadFiles(this.currentPath);
await this.refreshStorageUsage();
}
} catch (error) {
console.error('删除失败:', error);
this.showToast('error', '错误', error.response?.data?.message || '删除失败');
}
},
// ===== 分享功能 =====
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() {
if (this.creatingShare) return; // 防止重复提交
this.creatingShare = true;
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
},
);
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 || '创建分享失败');
} finally {
this.creatingShare = false;
}
},
async createShareFile() {
if (this.creatingShare) return; // 防止重复提交
this.creatingShare = true;
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
},
);
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 || '创建分享失败');
} finally {
this.creatingShare = false;
}
},
// ===== 文件上传 =====
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;
}
// 设置上传状态
this.uploadingFileName = file.name;
this.uploadProgress = 0;
this.uploadedBytes = 0;
this.totalBytes = file.size;
try {
if (this.storageType === 'oss' && this.user?.oss_config_source !== 'none') {
// ===== OSS 直连上传(不经过后端) =====
await this.uploadToOSSDirect(file);
} else {
// ===== 本地存储上传(经过后端) =====
await this.uploadToLocal(file);
}
} catch (error) {
console.error('上传失败:', error);
// 重置上传进度
this.uploadProgress = 0;
this.uploadedBytes = 0;
this.totalBytes = 0;
this.uploadingFileName = '';
this.showToast('error', '上传失败', error.message || '上传失败,请重试');
}
},
// OSS 直连上传
async uploadToOSSDirect(file) {
try {
// 1. 获取签名 URL传递当前路径
const { data: signData } = await axios.get(`${this.apiBase}/api/files/upload-signature`, {
params: {
filename: file.name,
path: this.currentPath,
contentType: file.type || 'application/octet-stream',
fileSize: file.size
}
});
if (!signData.success) {
throw new Error(signData.message || '获取上传签名失败');
}
// 2. 直连 OSS 上传(不经过后端!)
await axios.put(signData.uploadUrl, file, {
headers: {
'Content-Type': file.type || 'application/octet-stream'
},
onUploadProgress: (progressEvent) => {
this.uploadProgress = Math.round((progressEvent.loaded * 100) / progressEvent.total);
this.uploadedBytes = progressEvent.loaded;
this.totalBytes = progressEvent.total;
},
timeout: 30 * 60 * 1000 // 30分钟超时
});
// 3. 通知后端上传完成
await axios.post(`${this.apiBase}/api/files/upload-complete`, {
objectKey: signData.objectKey,
size: file.size,
path: this.currentPath
});
// 4. 显示成功提示
this.showToast('success', '上传成功', `文件 ${file.name} 已上传到 OSS`);
// 5. 重置上传进度
this.uploadProgress = 0;
this.uploadedBytes = 0;
this.totalBytes = 0;
this.uploadingFileName = '';
// 6. 刷新文件列表和空间统计
await this.loadFiles(this.currentPath);
await this.refreshStorageUsage();
} catch (error) {
// 处理 CORS 错误
if (error.message?.includes('CORS') || error.message?.includes('Cross-Origin')) {
throw new Error('OSS 跨域配置错误,请联系管理员检查 Bucket CORS 设置');
}
throw error;
}
},
// 本地存储上传(经过后端)
async uploadToLocal(file) {
// 本地存储配额预检查
const estimatedUsage = this.localUsed + file.size;
if (estimatedUsage > this.localQuota) {
this.showToast(
'error',
'配额不足',
`文件大小 ${this.formatBytes(file.size)},剩余配额 ${this.formatBytes(this.localQuota - this.localUsed)}`
);
this.uploadProgress = 0;
this.uploadedBytes = 0;
this.totalBytes = 0;
this.uploadingFileName = '';
return;
}
const formData = new FormData();
formData.append('file', file);
formData.append('path', this.currentPath);
const response = await axios.post(`${this.apiBase}/api/upload`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
timeout: 30 * 60 * 1000,
onUploadProgress: (progressEvent) => {
this.uploadProgress = Math.round((progressEvent.loaded * 100) / progressEvent.total);
this.uploadedBytes = progressEvent.loaded;
this.totalBytes = progressEvent.total;
}
});
if (response.data.success) {
this.showToast('success', '上传成功', `文件 ${file.name} 已上传`);
this.uploadProgress = 0;
this.uploadedBytes = 0;
this.totalBytes = 0;
this.uploadingFileName = '';
await this.loadFiles(this.currentPath);
await this.refreshStorageUsage();
}
},
// ===== 分享管理 =====
async loadShares() {
try {
const response = await axios.get(`${this.apiBase}/api/share/my`);
if (response.data.success) {
this.shares = response.data.shares;
}
} catch (error) {
console.error('加载分享列表失败:', error);
this.showToast('error', '加载失败', 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);
if (response.data.success) {
this.shareResult = response.data;
this.loadShares();
}
} catch (error) {
console.error('创建分享失败:', error);
this.showToast('error', '创建失败', error.response?.data?.message || error.message);
}
},
async deleteShare(id) {
if (!confirm('确定要删除这个分享吗?')) return;
try {
const response = await axios.delete(`${this.apiBase}/api/share/${id}`);
if (response.data.success) {
this.showToast('success', '成功', '分享已删除');
this.loadShares();
}
} catch (error) {
console.error('删除分享失败:', error);
this.showToast('error', '删除失败', 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;
},
// 分享类型标签
getShareTypeLabel(type) {
switch (type) {
case 'directory': return '文件夹';
case 'all': return '全部文件';
case 'file':
default: return '文件';
}
},
// 分享状态标签
getShareStatus(share) {
if (this.isExpired(share.expires_at)) {
return { text: '已过期', class: 'danger', icon: 'fa-clock' };
}
if (this.isExpiringSoon(share.expires_at)) {
return { text: '即将到期', class: 'warn', icon: 'fa-hourglass-half' };
}
return { text: '有效', class: 'success', icon: 'fa-check-circle' };
},
// 分享保护标签
getShareProtection(share) {
if (share.share_password) {
return { text: '已加密', class: 'info', icon: 'fa-lock' };
}
return { text: '公开', class: 'info', icon: 'fa-unlock' };
},
// 存储来源
getStorageLabel(storageType) {
if (!storageType) return '默认';
return storageType === 'local' ? '本地存储' : storageType.toUpperCase();
},
// 格式化时间
formatDateTime(value) {
if (!value) return '--';
const d = new Date(value);
if (Number.isNaN(d.getTime())) return value;
return d.toLocaleString();
},
// HTML实体解码前端兜底防止已实体化的文件名显示乱码
decodeHtmlEntities(str) {
if (typeof str !== 'string') return '';
const entityMap = {
amp: '&',
lt: '<',
gt: '>',
quot: '"',
apos: "'",
'#x27': "'",
'#x2F': '/',
'#x60': '`'
};
const decodeOnce = (input) =>
input.replace(/&(#x[0-9a-fA-F]+|#\d+|[a-zA-Z]+);/g, (match, code) => {
if (code[0] === '#') {
const isHex = code[1]?.toLowerCase() === 'x';
const num = isHex ? parseInt(code.slice(2), 16) : parseInt(code.slice(1), 10);
if (!Number.isNaN(num)) {
return String.fromCharCode(num);
}
return match;
}
const mapped = entityMap[code];
return mapped !== undefined ? mapped : match;
});
let output = str;
let decoded = decodeOnce(output);
while (decoded !== output) {
output = decoded;
decoded = decodeOnce(output);
}
return output;
},
getFileDisplayName(file) {
if (!file) return '';
const base = (typeof file.displayName === 'string' && file.displayName !== '')
? file.displayName
: (typeof file.name === 'string' ? file.name : '');
const decoded = this.decodeHtmlEntities(base);
return decoded || base || '';
},
openShare(url) {
if (!url) return;
const newWindow = window.open(url, '_blank', 'noopener');
if (!newWindow || newWindow.closed || typeof newWindow.closed === 'undefined') {
// 弹窗被拦截时提示用户手动打开,避免当前页跳转
this.showToast('info', '提示', '浏览器阻止了新标签页,请允许弹窗或手动打开链接');
}
},
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`);
if (response.data.success) {
this.adminUsers = response.data.users;
}
} catch (error) {
console.error('加载用户列表失败:', error);
this.showToast('error', '加载失败', 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 },
);
if (response.data.success) {
this.showToast('success', '成功', response.data.message);
this.loadUsers();
}
} catch (error) {
console.error('操作失败:', error);
this.showToast('error', '操作失败', error.response?.data?.message || error.message);
}
},
async deleteUser(userId) {
if (!confirm('确定要删除这个用户吗?此操作不可恢复!')) return;
try {
const response = await axios.delete(`${this.apiBase}/api/admin/users/${userId}`);
if (response.data.success) {
this.showToast('success', '成功', '用户已删除');
this.loadUsers();
}
} catch (error) {
console.error('删除用户失败:', error);
this.showToast('error', '删除失败', error.response?.data?.message || error.message);
}
},
// ===== 忘记密码功能 =====
async requestPasswordReset() {
if (!this.forgotPasswordForm.email) {
this.showToast('error', '错误', '请输入注册邮箱');
return;
}
if (!this.forgotPasswordForm.captcha) {
this.showToast('error', '错误', '请输入验证码');
return;
}
this.passwordResetting = true;
try {
const response = await axios.post(
`${this.apiBase}/api/password/forgot`,
this.forgotPasswordForm
);
if (response.data.success) {
this.showToast('success', '成功', '如果邮箱存在,将收到重置邮件');
this.showForgotPasswordModal = false;
this.forgotPasswordForm = { email: '', captcha: '' };
this.forgotPasswordCaptchaUrl = '';
}
} catch (error) {
console.error('提交密码重置请求失败:', error);
this.showToast('error', '错误', error.response?.data?.message || '提交失败');
// 刷新验证码
this.forgotPasswordForm.captcha = '';
this.refreshForgotPasswordCaptcha();
} finally {
this.passwordResetting = false;
}
},
async submitResetPassword() {
if (!this.resetPasswordForm.token || !this.resetPasswordForm.new_password || this.resetPasswordForm.new_password.length < 6) {
this.showToast('error', '错误', '请输入有效的重置链接和新密码至少6位');
return;
}
this.passwordResetting = true;
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 || '重置失败');
} finally {
this.passwordResetting = false;
}
},
// ===== 管理员:文件审查功能 =====
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 }
}
);
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`,
);
if (response.data.success && response.data.user) {
const user = response.data.user;
// 同步用户信息(含 has_oss_config
this.user = { ...(this.user || {}), ...user };
// 检测存储配置是否被管理员更改
const oldStorageType = this.storageType;
const oldStoragePermission = this.storagePermission;
const newStorageType = user.current_storage_type || 'oss';
const newStoragePermission = user.storage_permission || 'oss_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' ? '本地存储' : 'OSS存储'}`);
} else {
this.suppressStorageToast = false;
}
// 如果当前在文件页面,重新加载文件列表
if (this.currentView === 'files') {
await this.loadFiles(this.currentPath);
}
}
}
} catch (error) {
console.error('加载用户资料失败:', error);
}
},
// 加载OSS空间使用统计
async loadOssUsage() {
// 检查是否有可用的OSS配置个人配置或系统级统一配置
if (!this.user || this.user?.oss_config_source === 'none') {
this.ossUsage = null;
return;
}
this.ossUsageLoading = true;
this.ossUsageError = null;
try {
const response = await axios.get(
`${this.apiBase}/api/user/oss-usage`,
);
if (response.data.success) {
this.ossUsage = response.data.usage;
}
} catch (error) {
console.error('获取OSS空间使用情况失败:', error);
this.ossUsageError = error.response?.data?.message || '获取失败';
} finally {
this.ossUsageLoading = false;
}
},
// 刷新存储空间使用统计(根据当前存储类型)
async refreshStorageUsage() {
if (this.storageType === 'oss' && this.user?.oss_config_source !== 'none') {
// 刷新 OSS 空间统计
await this.loadOssUsage();
} else if (this.storageType === 'local') {
// 刷新本地存储统计(通过重新获取用户信息)
await this.loadUserProfile();
}
},
// 启动定期检查用户配置
startProfileSync() {
// 清除已有的定时器
if (this.profileCheckInterval) {
clearInterval(this.profileCheckInterval);
}
// 每30秒检查一次用户配置是否有更新
this.profileCheckInterval = setInterval(() => {
// 注意token 通过 HttpOnly Cookie 管理,仅检查 isLoggedIn
if (this.isLoggedIn) {
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;
}
// 不再弹出配置引导弹窗,直接尝试切换
// 如果后端检测到没有OSS配置会返回错误提示
this.storageSwitching = true;
this.storageSwitchTarget = type;
try {
const response = await axios.post(
`${this.apiBase}/api/user/switch-storage`,
{ storage_type: type },
);
if (response.data.success) {
this.storageType = type;
// 用户主动切换后,下一次配置同步不提示管理员修改
this.suppressStorageToast = true;
this.showToast('success', '成功', `已切换到${type === 'local' ? '本地存储' : 'OSS存储'}`);
// 重新加载文件列表
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;
}
},
ensureOssConfigSection() {
// 普通用户不需要打开配置弹窗,等待管理员配置
},
openOssGuideModal() {
this.showOssGuideModal = true;
},
closeOssGuideModal() {
this.showOssGuideModal = false;
},
proceedOssGuide() {
this.showOssGuideModal = false;
this.ensureOssConfigSection();
},
openOssConfigModal() {
// 只有管理员才能配置OSS
if (!this.user?.is_admin) {
this.showToast('error', '权限不足', '只有管理员才能配置OSS服务');
return;
}
this.showOssGuideModal = false;
this.showOssConfigModal = true;
if (this.user && !this.user.is_admin) {
this.loadOssConfig();
}
},
closeOssConfigModal() {
this.showOssConfigModal = 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();
if (this.adminTab === 'monitor') {
this.initMonitorTab();
} else {
this.loadHealthCheck();
this.loadSystemLogs(1);
}
}
break;
case 'settings':
// 设置页面不需要额外加载数据
break;
}
},
// 管理员:打开编辑用户存储权限模态框
openEditStorageModal(user) {
this.editStorageForm.userId = user.id;
this.editStorageForm.username = user.username;
this.editStorageForm.storage_permission = user.storage_permission || 'oss_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';
}
// OSS 配额初始化
const ossQuotaBytes = user.oss_storage_quota || 0;
if (ossQuotaBytes === 0) {
this.editStorageForm.oss_storage_quota_value = 0;
this.editStorageForm.oss_quota_unit = "GB";
} else {
const ossQuotaMB = ossQuotaBytes / 1024 / 1024;
const ossQuotaGB = ossQuotaMB / 1024;
if (ossQuotaMB >= 1024 && ossQuotaMB % 1024 === 0) {
this.editStorageForm.oss_storage_quota_value = ossQuotaGB;
this.editStorageForm.oss_quota_unit = "GB";
} else {
this.editStorageForm.oss_storage_quota_value = Math.round(ossQuotaMB);
this.editStorageForm.oss_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;
}
// 计算 OSS 配额字节数
let ossQuotaBytes = 0;
if (this.editStorageForm.oss_storage_quota_value > 0) {
if (this.editStorageForm.oss_quota_unit === "GB") {
ossQuotaBytes = this.editStorageForm.oss_storage_quota_value * 1024 * 1024 * 1024;
} else {
ossQuotaBytes = this.editStorageForm.oss_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,
oss_storage_quota: ossQuotaBytes
},
);
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`);
if (response.data.success) {
const settings = response.data.settings;
this.systemSettings.maxUploadSizeMB = Math.round(settings.max_upload_size / (1024 * 1024));
// 加载全局主题设置
console.log('[主题] 从服务器加载全局主题:', settings.global_theme);
if (settings.global_theme) {
this.globalTheme = settings.global_theme;
console.log('[主题] globalTheme已设置为:', this.globalTheme);
}
if (settings.smtp) {
this.systemSettings.smtp.host = settings.smtp.host || '';
this.systemSettings.smtp.port = settings.smtp.port || 465;
this.systemSettings.smtp.secure = !!settings.smtp.secure;
this.systemSettings.smtp.user = settings.smtp.user || '';
this.systemSettings.smtp.from = settings.smtp.from || settings.smtp.user || '';
this.systemSettings.smtp.has_password = !!settings.smtp.has_password;
this.systemSettings.smtp.password = '';
}
}
} catch (error) {
console.error('加载系统设置失败:', error);
this.showToast('error', '错误', '加载系统设置失败');
}
},
async loadServerStorageStats() {
try {
const response = await axios.get(`${this.apiBase}/api/admin/storage-stats`);
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
);
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 },
);
this.showToast('success', '成功', response.data.message || '测试邮件已发送');
} catch (error) {
console.error('测试SMTP失败:', error);
this.showToast('error', '错误', error.response?.data?.message || '测试失败');
}
},
// 打开监控标签页带整体loading遮罩
openMonitorTab() {
this.adminTab = 'monitor';
this.initMonitorTab();
},
// 统一加载监控数据,避免初次渲染空态闪烁
async initMonitorTab() {
this.monitorTabLoading = true;
this.healthCheck.loading = true;
this.systemLogs.loading = true;
try {
await Promise.all([
this.loadHealthCheck(),
this.loadSystemLogs(1)
]);
} catch (e) {
// 子方法内部已处理错误
} finally {
this.monitorTabLoading = false;
}
},
// ===== 健康检测 =====
async loadHealthCheck() {
this.healthCheck.loading = true;
try {
const response = await axios.get(`${this.apiBase}/api/admin/health-check`);
if (response.data.success) {
this.healthCheck.overallStatus = response.data.overallStatus;
this.healthCheck.summary = response.data.summary;
this.healthCheck.checks = response.data.checks;
this.healthCheck.lastCheck = response.data.timestamp;
}
} catch (error) {
console.error('健康检测失败:', error);
this.showToast('error', '错误', '健康检测失败');
} finally {
this.healthCheck.loading = false;
}
},
getHealthStatusColor(status) {
const colors = {
pass: 'bg-green-100 text-green-800',
warning: 'bg-yellow-100 text-yellow-800',
fail: 'bg-red-100 text-red-800',
info: 'bg-blue-100 text-blue-800'
};
return colors[status] || 'bg-gray-100 text-gray-800';
},
getHealthStatusIcon(status) {
const icons = {
pass: '✓',
warning: '⚠',
fail: '✗',
info: ''
};
return icons[status] || '?';
},
getOverallStatusColor(status) {
const colors = {
healthy: 'text-green-600',
warning: 'text-yellow-600',
critical: 'text-red-600'
};
return colors[status] || 'text-gray-600';
},
getOverallStatusText(status) {
const texts = {
healthy: '系统健康',
warning: '存在警告',
critical: '存在问题'
};
return texts[status] || '未知';
},
// ===== 系统日志 =====
async loadSystemLogs(page = 1) {
this.systemLogs.loading = true;
try {
const params = new URLSearchParams({
page: page,
pageSize: this.systemLogs.pageSize
});
if (this.systemLogs.filters.level) {
params.append('level', this.systemLogs.filters.level);
}
if (this.systemLogs.filters.category) {
params.append('category', this.systemLogs.filters.category);
}
if (this.systemLogs.filters.keyword) {
params.append('keyword', this.systemLogs.filters.keyword);
}
const response = await axios.get(`${this.apiBase}/api/admin/logs?${params}`);
if (response.data.success) {
this.systemLogs.logs = response.data.logs;
this.systemLogs.total = response.data.total;
this.systemLogs.page = response.data.page;
this.systemLogs.totalPages = response.data.totalPages;
}
} catch (error) {
console.error('加载系统日志失败:', error);
this.showToast('error', '错误', '加载系统日志失败');
} finally {
this.systemLogs.loading = false;
}
},
filterLogs() {
this.loadSystemLogs(1);
},
clearLogFilters() {
this.systemLogs.filters = { level: '', category: '', keyword: '' };
this.loadSystemLogs(1);
},
getLogLevelColor(level) {
const colors = {
debug: 'background: #6c757d; color: white;',
info: 'background: #17a2b8; color: white;',
warn: 'background: #ffc107; color: black;',
error: 'background: #dc3545; color: white;'
};
return colors[level] || 'background: #6c757d; color: white;';
},
getLogLevelText(level) {
const texts = { debug: '调试', info: '信息', warn: '警告', error: '错误' };
return texts[level] || level;
},
getLogCategoryText(category) {
const texts = {
auth: '认证',
user: '用户',
file: '文件',
share: '分享',
system: '系统',
security: '安全'
};
return texts[category] || category;
},
getLogCategoryIcon(category) {
const icons = {
auth: 'fa-key',
user: 'fa-user',
file: 'fa-file',
share: 'fa-share-alt',
system: 'fa-cog',
security: 'fa-shield-alt'
};
return icons[category] || 'fa-info';
},
formatLogTime(timestamp) {
if (!timestamp) return '';
const date = new Date(timestamp);
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
},
async cleanupLogs() {
if (!confirm('确定要清理90天前的日志吗此操作不可恢复。')) return;
try {
const response = await axios.post(
`${this.apiBase}/api/admin/logs/cleanup`,
{ keepDays: 90 },
);
if (response.data.success) {
this.showToast('success', '成功', response.data.message);
this.loadSystemLogs(1);
}
} catch (error) {
console.error('清理日志失败:', error);
this.showToast('error', '错误', '清理日志失败');
}
},
// ===== 调试模式管理 =====
// 切换调试模式
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;
// 设置 axios 请求拦截器,自动添加 CSRF Token
axios.interceptors.request.use(config => {
// 从 Cookie 中读取 CSRF token
const csrfToken = document.cookie
.split('; ')
.find(row => row.startsWith('csrf_token='))
?.split('=')[1];
if (csrfToken && ['POST', 'PUT', 'DELETE', 'PATCH'].includes(config.method?.toUpperCase())) {
config.headers['X-CSRF-Token'] = csrfToken;
}
return config;
});
// 初始化调试模式状态
this.debugMode = localStorage.getItem('debugMode') === 'true';
// 初始化主题从localStorage加载避免闪烁
this.initTheme();
// 处理URL中的验证/重置token兼容缺少?的旧链接)
const verifyToken = this.getTokenFromUrl('verifyToken');
const resetToken = this.getTokenFromUrl('resetToken');
if (verifyToken) {
this.handleVerifyToken(verifyToken);
this.sanitizeUrlToken('verifyToken');
}
if (resetToken) {
this.resetPasswordForm.token = resetToken;
this.showResetPasswordModal = true;
this.sanitizeUrlToken('resetToken');
}
// 阻止全局拖拽默认行为(防止拖到区域外打开新页面)
window.addEventListener("dragover", (e) => {
e.preventDefault();
});
window.addEventListener("drop", (e) => {
e.preventDefault();
});
// 添加全局 dragend 监听(拖拽结束时总是隐藏覆盖层)
window.addEventListener("dragend", () => {
this.isDragging = false;
});
// 添加 ESC 键监听(按 ESC 关闭拖拽覆盖层)
window.addEventListener("keydown", (e) => {
if (e.key === "Escape" && this.isDragging) {
this.isDragging = false;
}
});
// 设置axios响应拦截器处理401错误token过期/失效)
axios.interceptors.response.use(
response => response,
error => {
if (error.response && error.response.status === 401) {
// 排除登录接口本身的401密码错误等
const isLoginApi = error.config?.url?.includes('/api/login');
if (!isLoginApi && this.isLoggedIn) {
console.warn('[认证] 收到401响应Token已失效');
this.handleTokenExpired();
this.showToast('warning', '登录已过期', '请重新登录');
}
}
return Promise.reject(error);
}
);
// 检查URL参数
this.checkUrlParams();
// 获取系统配置(上传限制等)
this.loadPublicConfig();
// 如果用户在监控页面刷新提前设置loading状态防止显示"无数据"闪烁)
if (this.adminTab === 'monitor') {
this.healthCheck.loading = true;
this.systemLogs.loading = true;
this.monitorTabLoading = true;
}
// 检查登录状态
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) {
// 普通用户进入设置页面时加载OSS配置
this.loadOssConfig();
}
// 记住最后停留的视图(需合法且已登录)
if (this.isLoggedIn && this.isViewAllowed(newView)) {
localStorage.setItem('lastView', newView);
}
},
// 记住管理员当前标签页
adminTab(newTab) {
if (this.isLoggedIn && this.user?.is_admin) {
localStorage.setItem('adminTab', newTab);
}
}
}
}).mount('#app');