功能新增: - OSS 存储使用情况显示(文件页面) - OSS 当日流量统计(阿里云云监控API) - 分享页面路由修复(/s/xxx 格式支持) Bug修复: - 修复分享页面资源路径(相对路径改绝对路径) - 修复分享码获取逻辑(支持路径格式) - 修复OSS配额undefined显示问题 - 修复登录流程OSS配置检查 - 修复文件数为null时的显示问题 依赖更新: - 添加 @alicloud/cms20190101 云监控SDK - 添加 @alicloud/openapi-client Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
3258 lines
106 KiB
JavaScript
3258 lines
106 KiB
JavaScript
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,
|
||
ossTraffic: null, // 当日流量统计
|
||
ossTrafficLoading: false,
|
||
|
||
// 主题设置
|
||
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.loadOssUsage();
|
||
this.loadOssTraffic(); // 加载当日流量 // 加载OSS使用情况
|
||
this.loadFiles('/');
|
||
}
|
||
// 如果仅OSS模式,需要检查是否配置了OSS(包括系统级统一配置)
|
||
else if (this.storagePermission === 'oss_only') {
|
||
if (this.user?.oss_config_source !== 'none') {
|
||
this.currentView = "files";
|
||
this.loadOssUsage();
|
||
this.loadOssTraffic(); // 加载当日流量 // 加载OSS使用情况
|
||
this.loadFiles('/');
|
||
} else {
|
||
this.currentView = 'settings';
|
||
this.showToast('warning', 'OSS未配置', '系统尚未配置OSS存储服务,请联系管理员进行配置');
|
||
// 普通用户不需要打开配置弹窗,等待管理员配置
|
||
}
|
||
} else {
|
||
// 默认行为:跳转到文件页面
|
||
this.currentView = "files";
|
||
this.loadOssUsage();
|
||
this.loadOssTraffic(); // 加载当日流量 // 加载OSS使用情况
|
||
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();
|
||
},
|
||
|
||
// ===== 媒体预览功能 =====
|
||
|
||
// 获取媒体文件URL(OSS直连或后端代理)
|
||
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;
|
||
}
|
||
},
|
||
|
||
// 加载 OSS 当日流量统计
|
||
async loadOssTraffic() {
|
||
if (this.ossTrafficLoading) return;
|
||
if (!this.user || this.user?.oss_config_source === "none") return;
|
||
this.ossTrafficLoading = true;
|
||
try {
|
||
const response = await axios.get(`${this.apiBase}/api/oss/traffic`);
|
||
if (response.data.success) {
|
||
this.ossTraffic = response.data.traffic;
|
||
}
|
||
} catch (error) {
|
||
console.error("[OSS流量] 加载失败:", error);
|
||
this.ossTraffic = null;
|
||
} finally {
|
||
this.ossTrafficLoading = false;
|
||
}
|
||
},
|
||
|
||
// 刷新存储空间使用统计(根据当前存储类型)
|
||
async refreshStorageUsage() {
|
||
if (this.storageType === 'oss' && this.user?.oss_config_source !== 'none') {
|
||
// 刷新 OSS 空间统计
|
||
await this.loadOssUsage();
|
||
this.loadOssTraffic(); // 加载当日流量
|
||
} 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':
|
||
// 如果是 OSS 存储,加载使用情况
|
||
if (this.storageType === 'oss') {
|
||
this.loadOssUsage();
|
||
this.loadOssTraffic(); // 加载当日流量
|
||
}
|
||
// 切换到文件视图时,重新加载文件列表
|
||
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');
|
||
|