Files
vue-driven-cloud-storage/frontend/app.html
Dev Team 355c5940d4 fix: 隐藏系统级统一OSS用户的OSS配置按钮
## 问题
用户权限为 oss_only 时仍显示"配置/修改OSS"按钮,但用户使用的是系统级统一OSS配置,
不需要也无法修改个人OSS配置。

## 修复
- app.html:1894 - 添加条件判断 `v-if="user?.has_oss_config"`
- 仅在用户有个人OSS配置时显示"修改个人OSS配置"按钮
- 修改按钮文本:"配置/修改OSS" → "修改个人OSS配置"
- 修改说明文本:"已配置云服务" → "已配置系统级OSS"

## 影响
-  系统级统一OSS用户不再看到误导性的配置按钮
-  有个人OSS配置的用户仍可以修改个人配置
-  提升用户体验,避免混淆

**Bug数量:** 1个UI问题
**修改文件:** 1个
2026-01-20 22:52:09 +08:00

3509 lines
158 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<!-- 邮件链接重定向到独立页面 -->
<script>
(function() {
const search = window.location.search;
if (search.includes('verifyToken')) {
window.location.replace('verify.html' + search);
} else if (search.includes('resetToken')) {
window.location.replace('reset-password.html' + search);
}
})();
</script>
<!-- 开发者工具检测 - 必须放在最前面,优先执行 -->
<script>
(function() {
'use strict';
// 检查调试模式
const isDebugMode = localStorage.getItem('debugMode') === 'true';
if (isDebugMode) return; // 调试模式下跳过检测
let devtoolsOpen = false;
let checkCount = 0; // 检测次数计数器
// 方法1: Console对象toString检测最可靠
const checkElement = /./;
checkElement.toString = function() {
devtoolsOpen = true;
};
// 方法2: debugger暂停检测可能误判需要多次确认
function detectDebugger() {
const start = performance.now();
debugger;
const end = performance.now();
return (end - start) > 100;
}
// 方法3: 窗口尺寸检测(辅助判断)
function detectWindowSize() {
const widthDiff = window.outerWidth - window.innerWidth;
const heightDiff = window.outerHeight - window.innerHeight;
// 提高阈值,避免误判
return widthDiff > 200 || heightDiff > 200;
}
// 综合检测(需要多个条件同时满足才判定)
function checkDevTools() {
devtoolsOpen = false;
console.log('%c', checkElement);
console.clear();
const consoleOpen = devtoolsOpen;
const debuggerPause = detectDebugger();
const windowSizeAbnormal = detectWindowSize();
// 严格判定console检测为主其他为辅助
// 只有console明确检测到才触发警告
return consoleOpen && (debuggerPause || windowSizeAbnormal);
}
// 立即检测(页面加载时)
const initialCheck = checkDevTools();
if (initialCheck) {
// 检测到开发者工具,显示警告
document.write('<style>body{margin:0;overflow:hidden;}</style>');
document.write('<div style="position:fixed;top:0;left:0;width:100%;height:100%;background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);display:flex;align-items:center;justify-content:center;font-family:Arial,sans-serif;z-index:999999;">');
document.write('<div style="background:white;padding:60px;border-radius:20px;text-align:center;max-width:500px;box-shadow:0 20px 60px rgba(0,0,0,0.3);">');
document.write('<div style="font-size:80px;color:#667eea;margin-bottom:30px;">🛡️</div>');
document.write('<h1 style="color:#333;margin-bottom:20px;font-size:32px;">检测到开发者工具</h1>');
document.write('<p style="color:#666;font-size:18px;margin-bottom:30px;line-height:1.6;">为保护系统安全,请关闭浏览器开发者工具后访问</p>');
document.write('<button onclick="location.reload()" style="background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);color:white;border:none;padding:15px 40px;font-size:16px;border-radius:10px;cursor:pointer;font-weight:600;box-shadow:0 4px 15px rgba(102,126,234,0.4);">🔄 刷新页面</button>');
document.write('<p style="color:#999;font-size:14px;margin-top:20px;">如需使用开发者工具,请联系管理员开启调试模式</p>');
document.write('</div></div>');
document.close();
throw new Error('DevTools detected');
}
// 禁用右键和快捷键
document.addEventListener('contextmenu', e => e.preventDefault());
document.addEventListener('keydown', function(e) {
// F12, Ctrl+Shift+I, Ctrl+Shift+J, Ctrl+U, Ctrl+Shift+C
if (e.keyCode === 123 ||
(e.ctrlKey && e.shiftKey && (e.keyCode === 73 || e.keyCode === 74 || e.keyCode === 67)) ||
(e.ctrlKey && e.keyCode === 85)) {
e.preventDefault();
return false;
}
});
// 持续监控每2秒检测一次避免频繁检测
setInterval(function() {
checkCount++;
// 每次检测都要确认
if (checkDevTools()) {
// 检测到开发者工具打开,刷新页面
location.reload();
}
}, 2000);
// 禁用console非localhost
if (window.location.hostname !== 'localhost' && window.location.hostname !== '127.0.0.1') {
const noop = function() {};
['log', 'info', 'warn', 'error', 'debug'].forEach(method => {
console[method] = noop;
});
}
})();
</script>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>玩玩云 - 文件管理平台</title>
<script src="libs/vue.global.prod.js"></script>
<script src="libs/axios.min.js"></script>
<link rel="stylesheet" href="libs/fontawesome/css/all.min.css">
<style>
/* ========== 暗色主题 CSS 变量(默认) ========== */
:root {
--bg-primary: #0a0a0f;
--bg-secondary: #12121a;
--bg-card: rgba(255, 255, 255, 0.03);
--bg-card-hover: rgba(255, 255, 255, 0.06);
--glass-border: rgba(255, 255, 255, 0.08);
--glass-border-hover: rgba(102, 126, 234, 0.3);
--text-primary: #ffffff;
--text-secondary: rgba(255, 255, 255, 0.6);
--text-muted: rgba(255, 255, 255, 0.4);
--accent-1: #667eea;
--accent-2: #764ba2;
--accent-3: #f093fb;
--glow: rgba(102, 126, 234, 0.4);
--danger: #ef4444;
--success: #22c55e;
--warning: #f59e0b;
--info: #3b82f6;
}
/* ========== 亮色玻璃主题 ========== */
.light-theme {
--bg-primary: #f0f4f8;
--bg-secondary: #ffffff;
--bg-card: rgba(255, 255, 255, 0.7);
--bg-card-hover: rgba(255, 255, 255, 0.9);
--glass-border: rgba(102, 126, 234, 0.2);
--glass-border-hover: rgba(102, 126, 234, 0.4);
--text-primary: #1a1a2e;
--text-secondary: rgba(26, 26, 46, 0.7);
--text-muted: rgba(26, 26, 46, 0.5);
--accent-1: #5a67d8;
--accent-2: #6b46c1;
--accent-3: #d53f8c;
--glow: rgba(90, 103, 216, 0.3);
}
/* 亮色主题背景渐变 */
.light-theme body::before,
body.light-theme::before {
background:
radial-gradient(ellipse at top right, rgba(102, 126, 234, 0.12) 0%, transparent 50%),
radial-gradient(ellipse at bottom left, rgba(118, 75, 162, 0.12) 0%, transparent 50%),
linear-gradient(135deg, #e0e7ff 0%, #f0f4f8 50%, #fdf2f8 100%);
}
/* 亮色主题导航栏 */
body.light-theme .navbar {
background: rgba(255, 255, 255, 0.85);
border-bottom: 1px solid rgba(102, 126, 234, 0.15);
}
body.light-theme .nav-item {
color: #1a1a2e;
}
body.light-theme .nav-item:hover {
background: rgba(102, 126, 234, 0.1);
color: #5a67d8;
}
body.light-theme .nav-item.active {
color: white;
}
body.light-theme .user-info {
background: rgba(102, 126, 234, 0.08);
border-color: rgba(102, 126, 234, 0.2);
color: #1a1a2e;
}
body.light-theme .user-info .user-avatar {
color: #5a67d8;
}
/* 亮色主题卡片 */
body.light-theme .card {
background: rgba(255, 255, 255, 0.7);
box-shadow: 0 8px 32px rgba(102, 126, 234, 0.1);
}
/* 亮色主题表格 */
body.light-theme table tr:hover {
background: rgba(102, 126, 234, 0.05);
}
/* 亮色主题模态框 */
body.light-theme .modal-content {
background: rgba(255, 255, 255, 0.95);
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
}
/* 亮色主题输入框 */
body.light-theme .form-input {
background: rgba(255, 255, 255, 0.8);
border-color: rgba(102, 126, 234, 0.2);
}
body.light-theme .form-input:focus {
background: rgba(255, 255, 255, 0.95);
border-color: var(--accent-1);
}
/* 亮色主题按钮 */
body.light-theme .btn-secondary {
background: rgba(102, 126, 234, 0.1);
border-color: rgba(102, 126, 234, 0.3);
color: #1a1a2e;
}
body.light-theme .btn-secondary:hover {
background: rgba(102, 126, 234, 0.2);
border-color: rgba(102, 126, 234, 0.5);
}
body.light-theme .btn-icon {
color: #1a1a2e;
}
body.light-theme .btn-icon:hover {
background: rgba(102, 126, 234, 0.1);
}
/* 防止 Vue 初始化前显示原始模板 */
[v-cloak] { display: none !important; }
/* 应用加载占位符 */
.app-loading {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: var(--bg-primary);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.app-loading .loading-spinner {
font-size: 48px;
color: var(--accent-primary);
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 0.4; transform: scale(0.95); }
50% { opacity: 1; transform: scale(1.05); }
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
min-height: 100vh;
}
/* 动态背景 */
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background:
radial-gradient(ellipse at top right, rgba(102, 126, 234, 0.15) 0%, transparent 50%),
radial-gradient(ellipse at bottom left, rgba(118, 75, 162, 0.15) 0%, transparent 50%);
z-index: -1;
}
#app { min-height: 100vh; }
/* ========== 认证页面 ========== */
.auth-container {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 20px;
}
.auth-box {
background: var(--bg-card);
backdrop-filter: blur(20px);
border: 1px solid var(--glass-border);
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
padding: 40px;
width: 100%;
max-width: 450px;
}
.auth-title {
font-size: 28px;
font-weight: 700;
text-align: center;
margin-bottom: 30px;
background: linear-gradient(135deg, var(--accent-1), var(--accent-3));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.form-group { margin-bottom: 20px; }
.form-label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: var(--text-secondary);
font-size: 14px;
}
.form-input {
width: 100%;
padding: 14px 16px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--glass-border);
border-radius: 12px;
font-size: 15px;
color: var(--text-primary);
transition: all 0.3s;
}
.form-input:focus {
outline: none;
border-color: var(--accent-1);
background: rgba(255, 255, 255, 0.08);
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2);
}
.form-input::placeholder {
color: var(--text-muted);
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 10px;
cursor: pointer;
font-size: 15px;
font-weight: 600;
transition: all 0.3s;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.btn-primary {
background: linear-gradient(135deg, var(--accent-1), var(--accent-2));
color: white;
width: 100%;
box-shadow: 0 4px 20px var(--glow);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 8px 30px var(--glow);
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none !important;
}
.alert {
padding: 14px 16px;
border-radius: 12px;
margin-bottom: 20px;
font-size: 14px;
border: 1px solid transparent;
}
.alert-error {
background: rgba(239, 68, 68, 0.15);
border-color: rgba(239, 68, 68, 0.3);
color: #fca5a5;
}
.alert-success {
background: rgba(34, 197, 94, 0.15);
border-color: rgba(34, 197, 94, 0.3);
color: #86efac;
}
.alert-info {
background: rgba(59, 130, 246, 0.15);
border-color: rgba(59, 130, 246, 0.3);
color: #93c5fd;
}
.auth-switch {
text-align: center;
margin-top: 24px;
color: var(--text-secondary);
font-size: 14px;
}
.auth-switch a {
color: var(--accent-3);
cursor: pointer;
text-decoration: none;
font-weight: 500;
}
.auth-switch a:hover {
text-decoration: underline;
}
/* ========== 导航栏 ========== */
.navbar {
background: rgba(10, 10, 15, 0.85);
backdrop-filter: blur(20px);
border-bottom: 1px solid var(--glass-border);
padding: 15px 30px;
display: flex;
justify-content: space-between;
align-items: center;
}
.navbar-brand {
font-size: 24px;
font-weight: 700;
color: #667eea;
display: flex;
align-items: center;
gap: 10px;
}
.navbar-menu {
display: flex;
gap: 20px;
align-items: center;
}
.nav-item {
padding: 8px 16px;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
color: var(--text-secondary);
font-weight: 500;
}
.nav-item:hover {
background: rgba(255, 255, 255, 0.08);
color: var(--accent-1);
}
.nav-item.active {
background: linear-gradient(135deg, var(--accent-1), var(--accent-2));
color: white;
box-shadow: 0 4px 15px var(--glow);
}
.user-info {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 16px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--glass-border);
border-radius: 20px;
color: var(--text-primary);
}
.main-container {
max-width: 1200px;
margin: 30px auto;
padding: 0 20px;
}
.card {
background: var(--bg-card);
backdrop-filter: blur(20px);
border: 1px solid var(--glass-border);
border-radius: 16px;
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
padding: 30px;
}
.loading {
text-align: center;
padding: 40px;
color: var(--text-secondary);
}
.spinner {
border: 3px solid rgba(255, 255, 255, 0.1);
border-top: 3px solid var(--accent-1);
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto 16px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 文件网格视图 */
.file-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 20px;
padding: 10px;
}
.file-grid-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 15px 10px 10px 10px;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s;
text-align: center;
min-height: 180px;
position: relative;
border: 1px solid transparent;
}
.file-grid-item:hover {
background: var(--bg-card-hover);
border-color: var(--glass-border-hover);
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(102, 126, 234, 0.15);
}
.file-icon {
margin-bottom: 10px;
}
.file-thumbnail {
width: 64px;
height: 64px;
object-fit: cover;
border-radius: 8px;
border: 1px solid var(--glass-border);
}
.file-name {
font-size: 13px;
color: var(--text-primary);
word-break: break-all;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
min-height: 34px;
}
.file-size {
font-size: 12px;
color: var(--text-muted);
margin-top: 5px;
margin-bottom: 8px;
}
.file-actions {
display: flex;
gap: 5px;
justify-content: center;
align-items: center;
position: absolute;
bottom: 10px;
left: 50%;
transform: translateX(-50%);
}
.empty-hint {
text-align: center;
color: var(--text-muted);
padding: 40px;
font-size: 14px;
}
.btn-secondary {
background: rgba(255, 255, 255, 0.1);
border: 1px solid var(--glass-border);
color: var(--text-primary);
}
.btn-secondary:hover {
background: rgba(255, 255, 255, 0.15);
border-color: var(--glass-border-hover);
}
.btn-icon {
background: none;
border: none;
padding: 8px;
cursor: pointer;
border-radius: 6px;
transition: all 0.3s;
color: var(--accent-1);
}
.btn-icon:hover {
background: rgba(102, 126, 234, 0.2);
color: var(--accent-3);
}
.file-actions {
display: flex;
gap: 5px;
margin-top: 8px;
justify-content: center;
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
background: var(--bg-secondary);
border: 1px solid var(--glass-border);
padding: 30px;
border-radius: 16px;
width: 500px;
max-width: 90%;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
}
.modal-content h3 {
color: var(--text-primary);
margin-bottom: 20px;
}
/* 移动端适配 */
@media (max-width: 768px) {
/* 导航栏移动端优化 */
.navbar {
padding: 10px 15px;
flex-wrap: wrap;
}
.navbar-brand {
font-size: 18px;
gap: 8px;
}
.navbar-menu {
width: 100%;
margin-top: 10px;
gap: 8px;
flex-wrap: wrap;
justify-content: space-between;
}
.nav-item {
padding: 6px 10px;
font-size: 13px;
flex: 1;
text-align: center;
min-width: auto;
}
.user-info {
padding: 6px 12px;
font-size: 13px;
gap: 6px;
}
.btn-danger {
padding: 6px 12px;
font-size: 13px;
}
/* 主容器移动端优化 */
.main-container {
margin: 15px auto;
padding: 0 10px;
}
.card {
padding: 15px;
border-radius: 8px;
}
/* 文件网格视图移动端优化 */
.file-grid {
grid-template-columns: repeat(auto-fill, minmax(90px, 1fr));
gap: 12px;
padding: 5px;
}
.file-grid-item {
padding: 10px 5px;
}
.file-icon i {
font-size: 48px !important;
}
/* 图片缩略图移动端适配 */
.file-thumbnail {
width: 48px !important;
height: 48px !important;
}
/* 视频图标容器移动端适配 */
.file-icon div[style*="background: linear-gradient"] {
width: 48px !important;
height: 48px !important;
}
.file-icon div[style*="background: linear-gradient"] i {
font-size: 24px !important;
}
.file-name {
font-size: 12px;
}
.file-size {
font-size: 11px;
}
/* 文件操作按钮移动端优化 */
.file-actions {
gap: 3px;
margin-top: 5px;
}
.btn-icon {
padding: 5px;
font-size: 12px;
}
/* 工具栏移动端优化 - 修正版 */
.card > div[style*="display: flex"][style*="justify-content: space-between"] {
flex-direction: column !important;
gap: 15px !important;
}
.card > div[style*="display: flex"][style*="justify-content: space-between"] > div {
width: 100% !important;
justify-content: center !important;
}
.card > div[style*="display: flex"][style*="justify-content: space-between"] > div button {
flex: 1;
max-width: calc(50% - 5px);
}
.file-list table {
font-size: 13px;
}
.file-list th,
.file-list td {
padding: 8px 5px !important;
}
.file-list th:nth-child(3),
.file-list td:nth-child(3) {
display: none; /* 隐藏修改时间列 */
}
.file-list .file-icon {
font-size: 16px !important;
}
/* 列表视图视频图标容器移动端适配 */
.file-list div[style*="background: linear-gradient"] {
width: 28px !important;
height: 28px !important;
}
.file-list div[style*="background: linear-gradient"] i {
font-size: 14px !important;
}
/* 模态框移动端优化 */
.modal-content {
padding: 20px;
width: 95%;
}
.modal-content h3 {
font-size: 18px;
margin-bottom: 15px;
}
.form-group {
margin-bottom: 15px;
}
.form-input {
padding: 10px 12px;
font-size: 14px;
}
.btn {
padding: 8px 16px;
font-size: 14px;
}
/* 认证页面移动端优化 */
.auth-box {
padding: 25px;
}
.auth-title {
font-size: 22px;
margin-bottom: 20px;
}
/* 管理员表格移动端优化 */
.card table {
font-size: 12px;
}
.card table th,
.card table td {
padding: 6px 4px !important;
}
.card table button {
padding: 4px 8px;
font-size: 11px;
margin: 1px;
}
.card table button i {
font-size: 10px;
}
}
/* 超小屏幕优化 (手机竖屏) */
@media (max-width: 480px) {
.navbar-brand {
font-size: 16px;
}
.nav-item {
font-size: 11px;
padding: 5px 8px;
}
.file-grid {
grid-template-columns: repeat(auto-fill, minmax(75px, 1fr));
gap: 8px;
}
.file-icon i {
font-size: 40px !important;
}
/* 视频图标容器超小屏幕适配 */
.file-icon div[style*="background: linear-gradient"] {
width: 40px !important;
height: 40px !important;
}
.file-icon div[style*="background: linear-gradient"] i {
font-size: 20px !important;
}
.file-name {
font-size: 11px;
}
/* 列表视图在超小屏幕隐藏文件大小列 */
.file-list th:nth-child(2),
.file-list td:nth-child(2) {
font-size: 11px;
}
}
/* 拖拽上传样式 */
.files-container {
position: relative;
min-height: 200px;
}
.drag-drop-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(102, 126, 234, 0.1);
backdrop-filter: blur(2px);
border: 3px dashed #667eea;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
pointer-events: none;
}
.drag-drop-content {
text-align: center;
padding: 40px;
}
.files-container.drag-over {
background: rgba(102, 126, 234, 0.05);
border-radius: 12px;
}
/* 拖拽上传样式 */
.files-container {
position: relative;
min-height: 200px;
}
.drag-drop-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(102, 126, 234, 0.1);
backdrop-filter: blur(2px);
border: 3px dashed #667eea;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
pointer-events: none;
}
.drag-drop-content {
text-align: center;
padding: 40px;
}
.files-container.drag-over {
background: rgba(102, 126, 234, 0.05);
border-radius: 12px;
}
/* 健康检测状态颜色 */
.text-green-600 { color: #22c55e; }
.text-yellow-600 { color: #f59e0b; }
.text-red-600 { color: #ef4444; }
.text-blue-600 { color: #3b82f6; }
/* ========== 表格样式 ========== */
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 12px;
text-align: left;
}
th {
background: rgba(255, 255, 255, 0.05);
color: var(--text-secondary);
font-weight: 600;
border-bottom: 1px solid var(--glass-border);
}
tr {
border-bottom: 1px solid var(--glass-border);
}
tr:hover {
background: rgba(255, 255, 255, 0.02);
}
td {
color: var(--text-secondary);
}
/* 文件列表行样式 */
.file-list-row {
border-bottom: 1px solid var(--glass-border);
cursor: pointer;
transition: background 0.15s ease;
}
.file-list-row:hover {
background: rgba(255, 255, 255, 0.05);
}
body.light-theme .file-list-row:hover {
background: rgba(0, 0, 0, 0.04);
}
/* ========== 危险按钮 ========== */
.btn-danger {
background: linear-gradient(135deg, #ef4444, #dc2626);
color: white;
border: none;
}
.btn-danger:hover {
box-shadow: 0 4px 15px rgba(239, 68, 68, 0.4);
}
/* ========== 信息面板样式 ========== */
.info-panel {
background: rgba(255, 255, 255, 0.03);
border: 1px solid var(--glass-border);
border-radius: 12px;
padding: 16px;
}
.info-label {
color: var(--text-muted);
font-size: 12px;
}
.info-value {
color: var(--text-primary);
font-weight: 600;
}
/* ========== 状态标签 ========== */
.status-success {
color: #22c55e;
}
.status-warning {
color: #f59e0b;
}
.status-danger {
color: #ef4444;
}
.status-info {
color: #3b82f6;
}
/* ========== 通知栏样式 ========== */
.notice-info {
background: rgba(59, 130, 246, 0.1);
border-left: 4px solid var(--info);
border-radius: 8px;
padding: 12px 16px;
color: var(--text-secondary);
}
.notice-warning {
background: rgba(245, 158, 11, 0.1);
border-left: 4px solid var(--warning);
border-radius: 8px;
padding: 12px 16px;
color: var(--text-secondary);
}
.notice-success {
background: rgba(34, 197, 94, 0.1);
border-left: 4px solid var(--success);
border-radius: 8px;
padding: 12px 16px;
color: var(--text-secondary);
}
.notice-danger {
background: rgba(239, 68, 68, 0.1);
border-left: 4px solid var(--danger);
border-radius: 8px;
padding: 12px 16px;
color: var(--text-secondary);
}
/* ========== 选择框和下拉样式 ========== */
select {
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--glass-border);
border-radius: 8px;
padding: 8px 12px;
color: var(--text-primary);
cursor: pointer;
}
select:focus {
outline: none;
border-color: var(--accent-1);
}
select option {
background: var(--bg-secondary);
color: var(--text-primary);
}
/* ========== 滚动条样式 ========== */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.02);
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.15);
}
/* 分享卡片布局 */
.share-card-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 14px;
}
.share-card {
background: var(--bg-card);
border: 1px solid var(--glass-border);
border-radius: 12px;
padding: 14px;
box-shadow: 0 10px 30px rgba(0,0,0,0.12);
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
}
.share-card:hover {
transform: translateY(-2px);
border-color: var(--glass-border-hover);
box-shadow: 0 14px 36px rgba(0,0,0,0.18);
}
.share-card__title {
display: flex;
align-items: center;
gap: 10px;
font-weight: 600;
font-size: 15px;
color: var(--text-primary);
margin-bottom: 8px;
word-break: break-all;
}
.share-card__chips {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 10px;
}
.share-chip {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
border-radius: 999px;
font-size: 12px;
border: 1px solid var(--glass-border);
background: rgba(255,255,255,0.04);
color: var(--text-secondary);
}
.share-chip.success { color: #22c55e; background: rgba(34,197,94,0.12); border-color: rgba(34,197,94,0.25); }
.share-chip.warn { color: #f59e0b; background: rgba(245,158,11,0.14); border-color: rgba(245,158,11,0.25); }
.share-chip.danger { color: #ef4444; background: rgba(239,68,68,0.14); border-color: rgba(239,68,68,0.25); }
.share-chip.info { color: var(--accent-1); background: rgba(102,126,234,0.14); border-color: rgba(102,126,234,0.25); }
.share-card__meta {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 10px;
font-size: 12px;
color: var(--text-secondary);
margin-bottom: 12px;
}
.share-card__meta span {
display: inline-flex;
align-items: center;
gap: 6px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.share-card__actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.share-card__actions .btn {
padding: 8px 12px;
font-size: 13px;
}
.share-toolbar {
display: flex;
gap: 10px;
flex-wrap: wrap;
align-items: center;
}
.share-toolbar input,
.share-toolbar select {
background: rgba(255,255,255,0.05);
border: 1px solid var(--glass-border);
border-radius: 8px;
padding: 8px 12px;
color: var(--text-primary);
}
.share-toolbar input::placeholder {
color: var(--text-secondary);
}
.share-toolbar select {
min-width: 140px;
cursor: pointer;
}
body.light-theme .share-card {
box-shadow: 0 10px 30px rgba(0,0,0,0.08);
}
body.light-theme .share-card:hover {
box-shadow: 0 14px 36px rgba(0,0,0,0.12);
}
</style>
</head>
<body>
<div id="app" v-cloak>
<!-- 应用加载占位符防止UI闪烁 -->
<div v-if="!appReady" class="app-loading">
<div class="loading-spinner">
<i class="fas fa-cloud"></i>
</div>
</div>
<!-- 应用主体内容 -->
<template v-else>
<div class="auth-container" v-if="!isLoggedIn">
<div class="auth-box">
<div class="auth-title">
<i class="fas fa-cloud"></i>
{{ isLogin ? '登录' : '注册' }}
</div>
<div v-if="errorMessage" class="alert alert-error">{{ errorMessage }}</div>
<div v-if="successMessage" class="alert alert-success">{{ successMessage }}</div>
<div v-if="verifyMessage" class="alert alert-info">{{ verifyMessage }}</div>
<form v-if="isLogin" @submit.prevent="handleLogin">
<div class="form-group">
<label class="form-label">用户名</label>
<input type="text" class="form-input" v-model="loginForm.username" required>
</div>
<div class="form-group">
<label class="form-label">密码</label>
<input type="password" class="form-input" v-model="loginForm.password" required>
</div>
<div v-if="showCaptcha" class="form-group">
<label class="form-label">验证码</label>
<div style="display: flex; gap: 10px; align-items: center;">
<input type="text" class="form-input" v-model="loginForm.captcha" required style="flex: 1;" placeholder="请输入验证码">
<div style="cursor: pointer; border: 1px solid var(--glass-border); border-radius: 8px; padding: 5px; background: rgba(255,255,255,0.05);" @click="refreshCaptcha">
<img :src="captchaUrl" alt="验证码" style="display: block; width: 120px; height: 40px;" />
</div>
</div>
<small style="color: var(--text-muted); font-size: 12px;">点击图片刷新验证码</small>
</div>
<div v-if="showResendVerify" class="alert alert-info" style="margin-bottom: 10px;">
<div>邮箱未验证?请输入验证码后重发激活邮件</div>
<div style="display: flex; gap: 10px; align-items: center; margin-top: 8px;">
<input type="text" class="form-input" v-model="resendVerifyCaptcha" placeholder="验证码" style="flex: 1; height: 40px;" @focus="!resendVerifyCaptchaUrl && refreshResendVerifyCaptcha()">
<img v-if="resendVerifyCaptchaUrl" :src="resendVerifyCaptchaUrl" @click="refreshResendVerifyCaptcha" style="height: 40px; cursor: pointer; border-radius: 4px;" title="点击刷新验证码">
<button type="button" class="btn btn-primary" @click="resendVerification" :disabled="resendingVerify" style="height: 40px; white-space: nowrap;">
<i v-if="resendingVerify" class="fas fa-spinner fa-spin"></i> {{ resendingVerify ? '发送中...' : '重发邮件' }}
</button>
</div>
</div>
<div style="text-align: right; margin-bottom: 15px;">
<a @click="showForgotPasswordModal = true; refreshForgotPasswordCaptcha()" style="color: #667eea; cursor: pointer; font-size: 14px; text-decoration: none;">
忘记密码?
</a>
</div>
<button type="submit" class="btn btn-primary" :disabled="loginLoading">
<i :class="loginLoading ? 'fas fa-spinner fa-spin' : 'fas fa-right-to-bracket'"></i> {{ loginLoading ? '登录中...' : '登录' }}
</button>
</form>
<form v-else @submit.prevent="handleRegister" @focusin="!registerCaptchaUrl && refreshRegisterCaptcha()">
<div class="form-group">
<label class="form-label">用户名(3-20字符)</label>
<input type="text" class="form-input" v-model="registerForm.username" required minlength="3" maxlength="20">
</div>
<div class="form-group">
<label class="form-label">邮箱 (必填,用于激活)</label>
<input type="email" class="form-input" v-model="registerForm.email" required>
</div>
<div class="form-group">
<label class="form-label">密码 (至少6字符)</label>
<input type="password" class="form-input" v-model="registerForm.password" required minlength="6">
</div>
<div class="form-group">
<label class="form-label">验证码</label>
<div style="display: flex; gap: 10px; align-items: center;">
<input type="text" class="form-input" v-model="registerForm.captcha" placeholder="请输入验证码" required style="flex: 1;">
<img v-if="registerCaptchaUrl" :src="registerCaptchaUrl" @click="refreshRegisterCaptcha" style="height: 44px; cursor: pointer; border-radius: 4px;" title="点击刷新验证码">
</div>
</div>
<button type="submit" class="btn btn-primary" :disabled="registerLoading">
<i :class="registerLoading ? 'fas fa-spinner fa-spin' : 'fas fa-user-plus'"></i> {{ registerLoading ? '注册中...' : '注册' }}
</button>
</form>
<div class="auth-switch">
{{ isLogin ? '还没有账号?' : '已有账号?' }}
<a @click="toggleAuthMode">{{ isLogin ? '立即注册' : '去登录' }}</a>
</div>
</div>
</div>
<!-- 导航栏 -->
<div class="navbar" v-if="isLoggedIn">
<div class="navbar-brand">
<i class="fas fa-cloud"></i> 玩玩云
</div>
<div class="navbar-menu">
<div v-if="user && !user.is_admin" class="nav-item" :class="{active: currentView === 'files'}" @click="switchView('files')">
<i class="fas fa-folder"></i> 我的文件
</div>
<div v-if="user && !user.is_admin" class="nav-item" :class="{active: currentView === 'shares'}" @click="switchView('shares')">
<i class="fas fa-share-alt"></i> 我的分享
</div>
<div v-if="user && user.is_admin" class="nav-item" :class="{active: currentView === 'admin'}" @click="switchView('admin')">
<i class="fas fa-user-shield"></i> 管理员
</div>
<div class="nav-item" :class="{active: currentView === 'settings'}" @click="switchView('settings')">
<i class="fas fa-cog"></i> 设置
</div>
<div class="user-info">
<i class="fas fa-user-circle"></i>
<span>{{ user.username }}</span>
</div>
<button class="btn btn-danger" @click="logout">
<i class="fas fa-power-off"></i> 退出
</button>
</div>
</div>
<!-- 文件视图 -->
<div v-if="isLoggedIn && currentView === 'files'" class="main-container">
<div class="card">
<!-- 存储信息显示 -->
<div style="margin-bottom: 20px; padding: 15px; background: rgba(255,255,255,0.03); border: 1px solid var(--glass-border); border-radius: 12px; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 15px;">
<div style="display: flex; align-items: center; gap: 10px;">
<span style="font-weight: 600; color: var(--accent-1);">
<i :class="storageType === 'local' ? 'fas fa-hard-drive' : 'fas fa-server'"></i>
当前存储: {{ storageTypeText }}
</span>
</div>
<div v-if="storageType === 'local'" style="flex: 1; max-width: 400px;">
<div style="margin-bottom: 5px; font-size: 12px; color: var(--text-secondary); display: flex; justify-content: space-between;">
<span>配额使用情况</span>
<span>{{ localUsedFormatted }} / {{ localQuotaFormatted }} ({{ quotaPercentage }}%)</span>
</div>
<div style="width: 100%; height: 20px; background: rgba(255,255,255,0.1); border-radius: 10px; overflow: hidden;">
<div :style="{
width: quotaPercentage + '%',
height: '100%',
background: quotaPercentage > 90 ? '#ef4444' : quotaPercentage > 75 ? '#f59e0b' : '#22c55e',
transition: 'width 0.3s'
}"></div>
</div>
</div>
</div>
<!-- 路径导航 (面包屑) -->
<div v-if="currentPath !== '/'" style="margin-bottom: 15px; padding: 10px 15px; background: rgba(255,255,255,0.03); border: 1px solid var(--glass-border); border-radius: 10px; display: flex; align-items: center; gap: 8px; flex-wrap: wrap;">
<button class="btn-icon" @click="loadFiles('/')" title="返回根目录" style="padding: 5px 10px;">
<i class="fas fa-home"></i>
</button>
<span style="color: var(--text-muted);">/</span>
<template v-for="(part, index) in pathParts">
<span v-if="index < pathParts.length - 1"
@click="navigateToIndex(index)"
style="color: #667eea; cursor: pointer; font-weight: 500;"
:title="'进入 ' + part">
{{ part }}
</span>
<span v-else style="color: var(--text-primary); font-weight: 600;">{{ part }}</span>
<span v-if="index < pathParts.length - 1" style="color: var(--text-muted);">/</span>
</template>
<button class="btn-icon" @click="navigateUp()" title="返回上一级" style="margin-left: auto; padding: 5px 10px;">
<i class="fas fa-level-up-alt"></i> 返回上一级
</button>
</div>
<!-- 工具栏 -->
<div style="margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center;">
<div style="display: flex; gap: 10px;">
<!-- 网页上传按钮支持本地和OSS存储 -->
<button class="btn btn-primary" @click="$refs.fileUploadInput.click()">
<i class="fas fa-upload"></i> 上传文件
</button>
<button class="btn btn-primary" @click="showCreateFolderModal = true">
<i class="fas fa-folder-plus"></i> 新建文件夹
</button>
<button class="btn btn-primary" @click="showShareAllModal = true">
<i class="fas fa-share-nodes"></i> 分享所有文件
</button>
</div>
<div style="display: flex; gap: 10px;">
<button class="btn" :class="fileViewMode === 'grid' ? 'btn-primary' : 'btn-secondary'" @click="fileViewMode = 'grid'">
<i class="fas fa-th-large"></i> 大图标
</button>
<button class="btn" :class="fileViewMode === 'list' ? 'btn-primary' : 'btn-secondary'" @click="fileViewMode = 'list'">
<i class="fas fa-list"></i> 列表
</button>
</div>
</div>
<!-- 隐藏的文件上传input -->
<input type="file" ref="fileUploadInput" @change="handleFileSelect" style="display: none;" multiple>
<!-- 加载中 -->
<div v-if="loading" class="loading">
<div class="spinner"></div>
<p>加载中...</p>
</div>
<!-- 文件列表 -->
<div v-else @dragenter="handleDragEnter" @dragover="handleDragOver" @dragleave="handleDragLeave" @drop="handleDrop" class="files-container" :class="{ 'drag-over': isDragging }">
<p v-if="files.length === 0" class="empty-hint">文件夹是空的</p>
<!-- 拖拽提示层 -->
<div v-if="isDragging && storageType === 'local'" class="drag-drop-overlay">
<div class="drag-drop-content">
<i class="fas fa-cloud-upload-alt" style="font-size: 64px; color: #667eea; margin-bottom: 20px;"></i>
<div style="font-size: 24px; font-weight: 600; color: var(--text-primary); margin-bottom: 10px;">拖放文件到这里上传</div>
<div style="font-size: 14px; color: var(--text-secondary);">松开鼠标即可开始上传</div>
</div>
</div>
<!-- 大图标视图 -->
<div v-else-if="fileViewMode === 'grid'" class="file-grid">
<div v-for="file in files" :key="file.name" class="file-grid-item" @click="handleFileClick(file)" @contextmenu.prevent="showFileContextMenu(file, $event)" @touchstart="handleLongPressStart(file, $event)" @touchmove="handleLongPressMove($event)" @touchend="handleLongPressEnd">
<div class="file-icon">
<!-- 图片缩略图 -->
<img v-if="file.name.match(/\.(jpg|jpeg|png|gif|bmp|svg|webp)$/i) && getThumbnailUrl(file)"
:src="getThumbnailUrl(file)"
:alt="file.name"
class="file-thumbnail">
<!-- 视频图标(不预加载,避免慢) -->
<div v-else-if="file.name.match(/\.(mp4|avi|mov|wmv|flv|mkv|webm)$/i)"
style="width: 64px; height: 64px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 8px; display: flex; align-items: center; justify-content: center; position: relative;">
<i class="fas fa-play-circle" style="font-size: 32px; color: white;"></i>
</div>
<!-- 文件夹图标 -->
<i v-else-if="file.isDirectory" class="fas fa-folder" style="font-size: 64px; color: #FFC107;"></i>
<!-- 其他文件类型图标 -->
<i v-else-if="file.name.match(/\.(mp3|wav|flac|aac|ogg)$/i)" class="fas fa-file-audio" style="font-size: 64px; color: #FF5722;"></i>
<i v-else-if="file.name.match(/\.(pdf)$/i)" class="fas fa-file-pdf" style="font-size: 64px; color: #F44336;"></i>
<i v-else-if="file.name.match(/\.(doc|docx)$/i)" class="fas fa-file-word" style="font-size: 64px; color: #2196F3;"></i>
<i v-else-if="file.name.match(/\.(xls|xlsx)$/i)" class="fas fa-file-excel" style="font-size: 64px; color: #4CAF50;"></i>
<i v-else-if="file.name.match(/\.(zip|rar|7z|tar|gz)$/i)" class="fas fa-file-archive" style="font-size: 64px; color: #795548;"></i>
<i v-else class="fas fa-file" style="font-size: 64px; color: #9E9E9E;"></i>
</div>
<div class="file-name" :title="getFileDisplayName(file)">{{ getFileDisplayName(file) }}</div>
<div class="file-size">{{ file.isDirectory ? '文件夹' : file.sizeFormatted }}</div>
</div>
</div>
<!-- 列表视图 -->
<div v-else class="file-list">
<table style="width: 100%; border-collapse: collapse; table-layout: fixed;">
<thead>
<tr style="background: rgba(255,255,255,0.05);">
<th style="padding: 12px; text-align: left; width: 40%;">文件名</th>
<th style="padding: 12px; text-align: left; width: 15%;">大小</th>
<th style="padding: 12px; text-align: left; width: 25%;">修改时间</th>
</tr>
</thead>
<tbody>
<tr v-for="file in files" :key="file.name"
class="file-list-row"
@click="handleFileClick(file)"
@contextmenu.prevent="showFileContextMenu(file, $event)"
@touchstart="handleLongPressStart(file, $event)"
@touchmove="handleLongPressMove($event)"
@touchend="handleLongPressEnd">
<td style="padding: 10px;">
<div style="display: flex; align-items: center; gap: 10px; overflow: hidden;">
<!-- 图片缩略图 -->
<img v-if="file.name.match(/\.(jpg|jpeg|png|gif|bmp|svg|webp)$/i) && getThumbnailUrl(file)"
:src="getThumbnailUrl(file)"
:alt="file.name"
style="width: 32px; height: 32px; object-fit: cover; border-radius: 4px; flex-shrink: 0;">
<!-- 视频图标 -->
<div v-else-if="file.name.match(/\.(mp4|avi|mov|wmv|flv|mkv|webm)$/i)"
style="width: 32px; height: 32px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 4px; display: flex; align-items: center; justify-content: center; flex-shrink: 0;">
<i class="fas fa-play" style="font-size: 14px; color: white;"></i>
</div>
<!-- 文件夹图标 -->
<i v-else-if="file.isDirectory" class="fas fa-folder" style="font-size: 20px; color: #FFC107; flex-shrink: 0;"></i>
<!-- 其他文件类型图标 -->
<i v-else-if="file.name.match(/\.(mp3|wav|flac|aac|ogg)$/i)" class="fas fa-file-audio" style="font-size: 20px; color: #FF5722; flex-shrink: 0;"></i>
<i v-else-if="file.name.match(/\.(pdf)$/i)" class="fas fa-file-pdf" style="font-size: 20px; color: #F44336; flex-shrink: 0;"></i>
<i v-else-if="file.name.match(/\.(doc|docx)$/i)" class="fas fa-file-word" style="font-size: 20px; color: #2196F3; flex-shrink: 0;"></i>
<i v-else-if="file.name.match(/\.(xls|xlsx)$/i)" class="fas fa-file-excel" style="font-size: 20px; color: #4CAF50; flex-shrink: 0;"></i>
<i v-else-if="file.name.match(/\.(zip|rar|7z|tar|gz)$/i)" class="fas fa-file-archive" style="font-size: 20px; color: #795548; flex-shrink: 0;"></i>
<i v-else class="fas fa-file" style="font-size: 20px; color: #9E9E9E; flex-shrink: 0;"></i>
<span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; min-width: 0;" :title="getFileDisplayName(file)">{{ getFileDisplayName(file) }}</span>
</div>
</td>
<td style="padding: 10px; color: var(--text-secondary);">{{ file.isDirectory ? '-' : file.sizeFormatted }}</td>
<td style="padding: 10px; color: var(--text-secondary);">{{ formatDate(file.modifiedTime) }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- 重命名模态框 -->
<div v-if="showRenameModal" class="modal-overlay" @mousedown.self="handleModalMouseDown" @mouseup.self="handleModalMouseUp('showRenameModal', $event)">
<div class="modal-content" @click.stop>
<h3 style="margin-bottom: 20px;">重命名文件</h3>
<div class="form-group">
<label class="form-label">新文件名</label>
<input type="text" class="form-input" v-model="renameForm.newName" @keyup.enter="renameFile()">
</div>
<div style="display: flex; gap: 10px; margin-top: 20px;">
<button class="btn btn-primary" @click="renameFile()" style="flex: 1;">
<i class="fas fa-check"></i> 确定
</button>
<button class="btn btn-secondary" @click="showRenameModal = false" style="flex: 1;">
<i class="fas fa-times"></i> 取消
</button>
</div>
</div>
</div>
<!-- 新建文件夹模态框 -->
<div v-if="showCreateFolderModal" class="modal-overlay" @mousedown.self="handleModalMouseDown" @mouseup.self="handleModalMouseUp('showCreateFolderModal', $event)">
<div class="modal-content" @click.stop>
<h3 style="margin-bottom: 20px;">
<i class="fas fa-folder-plus"></i> 新建文件夹
</h3>
<div class="form-group">
<label class="form-label">文件夹名称</label>
<input type="text" class="form-input" v-model="createFolderForm.folderName" @keyup.enter="createFolder()" placeholder="请输入文件夹名称" autofocus>
</div>
<div style="display: flex; gap: 10px; margin-top: 20px;">
<button class="btn btn-primary" @click="createFolder()" :disabled="creatingFolder" style="flex: 1;">
<i class="fas" :class="creatingFolder ? 'fa-spinner fa-spin' : 'fa-check'"></i> {{ creatingFolder ? '创建中...' : '创建' }}
</button>
<button class="btn btn-secondary" @click="showCreateFolderModal = false; createFolderForm.folderName = ''" style="flex: 1;">
<i class="fas fa-times"></i> 取消
</button>
</div>
</div>
</div>
<!-- 文件夹详情模态框 -->
<div v-if="showFolderInfoModal" class="modal-overlay" @mousedown.self="handleModalMouseDown" @mouseup.self="handleModalMouseUp('showFolderInfoModal', $event)">
<div class="modal-content" @click.stop>
<h3 style="margin-bottom: 20px;">
<i class="fas fa-folder"></i> 文件夹详情
</h3>
<div v-if="folderInfo" style="background: rgba(255,255,255,0.03); padding: 20px; border-radius: 8px;">
<div style="margin-bottom: 15px;">
<strong style="color: var(--text-secondary);">名称:</strong>
<div style="margin-top: 5px; font-size: 16px;">{{ folderInfo.name }}</div>
</div>
<div style="margin-bottom: 15px;">
<strong style="color: var(--text-secondary);">路径:</strong>
<div style="margin-top: 5px; color: #667eea;">{{ folderInfo.path }}</div>
</div>
<div style="margin-bottom: 15px;">
<strong style="color: var(--text-secondary);">总大小:</strong>
<div style="margin-top: 5px; font-size: 18px; font-weight: 600; color: #667eea;">
{{ formatFileSize(folderInfo.size) }}
</div>
</div>
<div style="display: flex; gap: 20px;">
<div style="flex: 1;">
<strong style="color: var(--text-secondary);">文件数:</strong>
<div style="margin-top: 5px; font-size: 16px;">{{ folderInfo.fileCount }} 个</div>
</div>
<div style="flex: 1;">
<strong style="color: var(--text-secondary);">子文件夹:</strong>
<div style="margin-top: 5px; font-size: 16px;">{{ folderInfo.folderCount }} 个</div>
</div>
</div>
</div>
<div v-else style="text-align: center; padding: 40px; color: var(--text-muted);">
<i class="fas fa-spinner fa-spin" style="font-size: 32px;"></i>
<div style="margin-top: 10px;">加载中...</div>
</div>
<div style="margin-top: 20px;">
<button class="btn btn-secondary" @click="showFolderInfoModal = false; folderInfo = null" style="width: 100%;">
<i class="fas fa-times"></i> 关闭
</button>
</div>
</div>
</div>
<!-- 分享所有文件模态框 -->
<div v-if="showShareAllModal" class="modal-overlay" @mousedown.self="handleModalMouseDown" @mouseup.self="handleModalMouseUp('showShareAllModal', $event)">
<div class="modal-content" @click.stop>
<h3 style="margin-bottom: 20px;">分享所有文件</h3>
<div class="form-group">
<label class="form-label">密码保护(可选)</label>
<input type="password" class="form-input" v-model="shareAllForm.password" placeholder="留空则无需密码">
</div>
<div class="form-group">
<label class="form-label">有效期</label>
<select class="form-input" v-model="shareAllForm.expiryType">
<option value="never">永久</option>
<option value="7">7天</option>
<option value="30">30天</option>
<option value="custom">自定义</option>
</select>
</div>
<div v-if="shareAllForm.expiryType === 'custom'" class="form-group">
<label class="form-label">自定义天数</label>
<input type="number" class="form-input" v-model.number="shareAllForm.customDays" min="1" max="365">
</div>
<div v-if="shareResult" class="alert alert-success" style="margin-top: 15px;">
<strong>分享链接:</strong><br>
<a :href="shareResult.share_url" target="_blank" style="color: #22c55e; word-break: break-all;">{{ shareResult.share_url }}</a>
<div v-if="shareResult.expires_at" style="margin-top: 10px; padding-top: 10px; border-top: 1px solid rgba(34, 197, 94, 0.3);">
<strong>到期时间:</strong>
<span :style="{color: isExpiringSoon(shareResult.expires_at) ? '#f59e0b' : '#22c55e'}"><i class="fas fa-clock"></i> {{ formatExpireTime(shareResult.expires_at) }}</span>
</div>
<div v-else style="margin-top: 10px; padding-top: 10px; border-top: 1px solid rgba(34, 197, 94, 0.3);">
<strong>有效期:</strong>
<span style="color: #22c55e;"><i class="fas fa-infinity"></i> 永久有效</span>
</div>
</div>
<div style="display: flex; gap: 10px; margin-top: 20px;">
<button class="btn btn-primary" @click="createShareAll()" :disabled="creatingShare" style="flex: 1;">
<i class="fas" :class="creatingShare ? 'fa-spinner fa-spin' : 'fa-share'"></i> {{ creatingShare ? '创建中...' : '创建分享' }}
</button>
<button class="btn btn-secondary" @click="showShareAllModal = false; shareResult = null" style="flex: 1;">
<i class="fas fa-times"></i> 关闭
</button>
</div>
</div>
</div>
<!-- 分享单个文件模态框 -->
<div v-if="showShareFileModal" class="modal-overlay" @mousedown.self="handleModalMouseDown" @mouseup.self="handleModalMouseUp('showShareFileModal', $event)">
<div class="modal-content" @click.stop>
<h3 style="margin-bottom: 20px;">分享文件</h3>
<p style="color: var(--text-secondary); margin-bottom: 15px;">文件: <strong>{{ shareFileForm.fileName }}</strong></p>
<div class="form-group">
<label class="form-label">密码保护(可选)</label>
<input type="password" class="form-input" v-model="shareFileForm.password" placeholder="留空则无需密码">
</div>
<div class="form-group">
<label class="form-label">有效期</label>
<select class="form-input" v-model="shareFileForm.expiryType">
<option value="never">永久</option>
<option value="7">7天</option>
<option value="30">30天</option>
<option value="custom">自定义</option>
</select>
</div>
<div v-if="shareFileForm.expiryType === 'custom'" class="form-group">
<label class="form-label">自定义天数</label>
<input type="number" class="form-input" v-model.number="shareFileForm.customDays" min="1" max="365">
</div>
<div v-if="shareResult" class="alert alert-success" style="margin-top: 15px;">
<strong>分享链接:</strong><br>
<a :href="shareResult.share_url" target="_blank" style="color: #22c55e; word-break: break-all;">{{ shareResult.share_url }}</a>
<div v-if="shareResult.expires_at" style="margin-top: 10px; padding-top: 10px; border-top: 1px solid rgba(34, 197, 94, 0.3);">
<strong>到期时间:</strong>
<span :style="{color: isExpiringSoon(shareResult.expires_at) ? '#f59e0b' : '#22c55e'}"><i class="fas fa-clock"></i> {{ formatExpireTime(shareResult.expires_at) }}</span>
</div>
<div v-else style="margin-top: 10px; padding-top: 10px; border-top: 1px solid rgba(34, 197, 94, 0.3);">
<strong>有效期:</strong>
<span style="color: #22c55e;"><i class="fas fa-infinity"></i> 永久有效</span>
</div>
</div>
<div style="display: flex; gap: 10px; margin-top: 20px;">
<button class="btn btn-primary" @click="createShareFile()" :disabled="creatingShare" style="flex: 1;">
<i class="fas" :class="creatingShare ? 'fa-spinner fa-spin' : 'fa-share'"></i> {{ creatingShare ? '创建中...' : '创建分享' }}
</button>
<button class="btn btn-secondary" @click="showShareFileModal = false; shareResult = null" style="flex: 1;">
<i class="fas fa-times"></i> 关闭
</button>
</div>
</div>
</div>
<!-- OSS 配置引导弹窗 -->
<div v-if="showOssGuideModal" class="modal-overlay" @mousedown.self="handleModalMouseDown" @mouseup.self="handleModalMouseUp('showOssGuideModal', $event)">
<div class="modal-content" @click.stop style="max-width: 520px; border-radius: 16px; overflow: hidden;">
<div style="background: linear-gradient(135deg,#667eea,#764ba2); color: white; padding: 18px;">
<div style="display: flex; align-items: center; gap: 10px;">
<i class="fas fa-cloud" style="font-size: 20px;"></i>
<h3 style="margin: 0; font-size: 20px;">切换到 OSS 存储</h3>
</div>
<p style="margin: 8px 0 0 0; opacity: 0.9; font-size: 14px;">先配置云服务信息,再切换到你的专属 OSS 空间。</p>
</div>
<div style="padding: 18px;">
<p style="color: var(--text-secondary); line-height: 1.6; margin-bottom: 16px;">
支持阿里云 OSS、腾讯云 COS、AWS S3 等兼容 S3 协议的云存储服务。
</p>
<div style="display: flex; gap: 10px; justify-content: flex-end;">
<button class="btn btn-secondary" @click="closeOssGuideModal">稍后再说</button>
<button class="btn btn-primary" @click="proceedOssGuide">
<i class="fas fa-tools"></i> 去配置 OSS
</button>
</div>
</div>
</div>
</div>
<!-- OSS 配置弹窗 -->
<div v-if="showOssConfigModal" class="modal-overlay" @mousedown.self="handleModalMouseDown" @mouseup.self="handleModalMouseUp('showOssConfigModal', $event)">
<div class="modal-content" @click.stop style="max-width: 720px; border-radius: 16px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
<div>
<h3 style="margin: 0 0 6px 0;">配置 OSS 存储</h3>
<p style="margin: 0; color: var(--text-muted); font-size: 13px;">填写云服务配置信息,保存后即可切换到 OSS 模式。</p>
</div>
<button class="btn btn-secondary" style="padding: 6px 10px;" @click="closeOssConfigModal">
<i class="fas fa-times"></i>
</button>
</div>
<form @submit.prevent="updateOssConfig" style="display: grid; gap: 12px;">
<div class="form-group">
<label class="form-label">云服务商</label>
<select class="form-input" v-model="ossConfigForm.oss_provider" required style="cursor: pointer;">
<option value="aliyun">阿里云 OSS</option>
<option value="tencent">腾讯云 COS</option>
<option value="aws">AWS S3</option>
</select>
</div>
<div class="form-group">
<label class="form-label">地域</label>
<input type="text" class="form-input" v-model="ossConfigForm.oss_region" placeholder="如: oss-cn-hangzhou / ap-guangzhou / us-east-1" required>
<small style="color: var(--text-secondary); font-size: 12px; margin-top: 5px; display: block;">
阿里云: oss-cn-hangzhou, 腾讯云: ap-guangzhou, AWS: us-east-1
</small>
</div>
<div class="form-group">
<label class="form-label">Access Key ID</label>
<input type="text" class="form-input" v-model="ossConfigForm.oss_access_key_id" required>
</div>
<div class="form-group">
<label class="form-label">Access Key Secret (留空保留现有密钥)</label>
<input type="password" class="form-input" v-model="ossConfigForm.oss_access_key_secret" placeholder="留空保留现有密钥">
</div>
<div class="form-group">
<label class="form-label">存储桶名称</label>
<input type="text" class="form-input" v-model="ossConfigForm.oss_bucket" placeholder="如: my-storage-bucket" required>
</div>
<div class="form-group">
<label class="form-label">自定义 Endpoint (可选)</label>
<input type="text" class="form-input" v-model="ossConfigForm.oss_endpoint" placeholder="兼容 S3 的服务可填写自定义地址">
<small style="color: var(--text-secondary); font-size: 12px; margin-top: 5px; display: block;">
一般不需要填写,仅在使用自定义 S3 兼容服务时需要。
</small>
</div>
<div style="display: flex; gap: 10px; justify-content: flex-end; margin-top: 4px;">
<button type="button" class="btn btn-secondary" @click="closeOssConfigModal">取消</button>
<button type="submit" class="btn btn-primary" :disabled="ossConfigSaving" :style="{ opacity: ossConfigSaving ? 0.7 : 1 }">
<i class="fas" :class="ossConfigSaving ? 'fa-spinner fa-spin' : 'fa-save'"></i>
{{ ossConfigSaving ? '保存中...' : '保存配置' }}
</button>
</div>
</form>
</div>
</div>
</div>
<!-- 设置视图 -->
<div v-if="isLoggedIn && currentView === 'settings'" class="main-container">
<div class="card">
<!-- 存储管理 - 仅用户可选择 -->
<div v-if="user && !user.is_admin && storagePermission === 'user_choice'" style="margin-bottom: 40px;">
<h3 style="margin-bottom: 20px;">
<i class="fas fa-database"></i> 存储管理
</h3>
<div style="background: var(--bg-card); backdrop-filter: blur(20px); padding: 22px; border-radius: 14px; box-shadow: 0 10px 30px rgba(0,0,0,0.2); border: 1px solid var(--glass-border);">
<div style="display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap;">
<div style="display: flex; align-items: center; gap: 10px;">
<span style="font-weight: 700; color: var(--text-primary);">当前模式</span>
<span :style="{
padding: '6px 12px',
borderRadius: '999px',
background: storageType === 'local' ? 'rgba(40,167,69,0.12)' : 'rgba(102,126,234,0.12)',
color: storageType === 'local' ? '#1c7c3d' : '#4b5fc9',
fontWeight: 700,
display: 'inline-flex',
alignItems: 'center',
gap: '6px'
}">
<i :class="storageType === 'local' ? 'fas fa-hard-drive' : 'fas fa-server'"></i>
{{ storageTypeText }}
</span>
</div>
<div v-if="storageSwitching" style="color: #4b5fc9; font-weight: 600; display: inline-flex; align-items: center; gap: 8px;">
<i class="fas fa-sync-alt fa-spin"></i>
正在切换到 {{ storageSwitchTarget === 'oss' ? 'OSS 存储' : '本地存储' }}...
</div>
<div v-else style="color: var(--text-secondary); font-size: 13px;">本地存储适合快速读写OSS 适合云存储扩展</div>
</div>
<div style="margin-top: 16px; background: var(--bg-secondary); border-radius: 12px; padding: 12px; border: 1px solid var(--glass-border);">
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; align-items: center;">
<div style="height: 4px; background: rgba(255,255,255,0.1); border-radius: 999px; position: relative; overflow: hidden;">
<div :style="{
position: 'absolute',
left: storageType === 'local' ? '6%' : '52%',
width: '42%',
height: '100%',
background: 'linear-gradient(90deg,#667eea,#764ba2)',
borderRadius: '999px',
transition: 'left .35s ease'
}"></div>
</div>
</div>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 14px; margin-top: 14px; align-items: stretch;">
<div style="background: var(--bg-secondary); border: 1px solid var(--glass-border); border-radius: 12px; padding: 16px; box-shadow: 0 6px 20px rgba(0,0,0,0.15); display: flex; flex-direction: column; height: 100%;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<div style="font-weight: 700; color: var(--text-primary); display: flex; gap: 8px; align-items: center;">
<i class="fas fa-hard-drive" style="color: var(--accent-1);"></i> 本地存储
</div>
<span v-if="storageType === 'local'" style="font-size: 12px; color: #22c55e; background: rgba(40,167,69,0.12); padding: 4px 8px; border-radius: 999px;">当前</span>
</div>
<div style="color: var(--text-secondary); font-size: 13px; margin-bottom: 10px;">更快的读写,适合日常上传下载。</div>
<div style="margin-bottom: 10px;">
<div style="font-size: 12px; color: var(--text-muted); margin-bottom: 6px;">配额使用</div>
<div style="font-weight: 600; color: var(--text-primary);">{{ localUsedFormatted }} / {{ localQuotaFormatted }}</div>
<div style="margin-top: 6px; width: 100%; height: 10px; background: rgba(255,255,255,0.1); border-radius: 5px; overflow: hidden;">
<div :style="{
width: quotaPercentage + '%',
height: '100%',
background: quotaPercentage > 90 ? '#dc3545' : quotaPercentage > 75 ? '#ffc107' : '#28a745',
transition: 'width 0.35s ease'
}"></div>
</div>
</div>
<div style="margin-top: auto;">
<button class="btn btn-primary" style="width: 100%; border-radius: 10px;" :disabled="storageType === 'local' || storageSwitching" @click="switchStorage('local')">
<i class="fas fa-bolt"></i> 用本地存储
</button>
</div>
</div>
<div style="background: var(--bg-secondary); border: 1px solid var(--glass-border); border-radius: 12px; padding: 16px; box-shadow: 0 6px 20px rgba(0,0,0,0.15); display: flex; flex-direction: column; height: 100%;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<div style="font-weight: 700; color: var(--text-primary); display: flex; gap: 8px; align-items: center;">
<i class="fas fa-cloud" style="color: var(--accent-1);"></i> OSS 存储
</div>
<span v-if="storageType === 'oss'" style="font-size: 12px; color: var(--accent-1); background: rgba(102,126,234,0.15); padding: 4px 8px; border-radius: 999px;">当前</span>
</div>
<div style="color: var(--text-secondary); font-size: 13px; margin-bottom: 10px;">使用云存储服务,安全可靠扩展性强。</div>
<div v-if="user?.oss_config_source !== 'none'" style="font-size: 13px; color: var(--text-primary); margin-bottom: 10px;">
<i class="fas fa-check-circle" style="color: var(--accent-1);"></i>
<span v-if="user?.oss_config_source === 'unified'">系统级OSS配置已启用</span>
<span v-else>已配置: {{ user.oss_provider }} / {{ user.oss_bucket }}</span>
</div>
<div v-else style="font-size: 13px; color: #f59e0b; background: rgba(245, 158, 11, 0.1); border: 1px dashed rgba(245,158,11,0.4); padding: 10px; border-radius: 8px; margin-bottom: 10px;">
<i class="fas fa-exclamation-circle"></i> 先填写 OSS 配置信息再切换
</div>
<!-- OSS空间使用统计user_choice模式 -->
<div v-if="user?.oss_config_source !== 'none'" style="margin-bottom: 10px; padding: 10px; background: rgba(255,255,255,0.03); border-radius: 8px; border: 1px solid var(--glass-border);">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
<span style="font-size: 12px; color: var(--text-muted);">空间统计</span>
<button
style="background: none; border: none; color: #4b5fc9; cursor: pointer; font-size: 12px; padding: 2px 6px;"
@click.stop="loadOssUsage()"
:disabled="ossUsageLoading">
<i :class="ossUsageLoading ? 'fas fa-sync-alt fa-spin' : 'fas fa-sync-alt'"></i>
</button>
</div>
<div v-if="ossUsageLoading && !ossUsage" style="text-align: center; color: #667eea; font-size: 12px;">
<i class="fas fa-spinner fa-spin"></i> 统计中...
</div>
<div v-else-if="ossUsageError" style="font-size: 12px; color: #ef4444;">
<i class="fas fa-exclamation-triangle"></i> {{ ossUsageError }}
</div>
<div v-else-if="ossUsage" style="font-size: 13px; font-weight: 600; color: #4b5fc9;">
{{ ossUsage.totalSizeFormatted }}
<span style="font-weight: 400; color: var(--text-muted); font-size: 12px;">{{ ossUsage.fileCount }} 文件)</span>
</div>
<div v-else style="font-size: 12px; color: var(--text-muted);">点击刷新查看</div>
</div>
<div style="margin-top: auto;">
<button
class="btn"
:class="user?.oss_config_source !== 'none' ? 'btn-primary' : 'btn-secondary'"
style="width: 100%; border-radius: 10px;"
:disabled="storageType === 'oss' || storageSwitching"
@click="switchStorage('oss')">
<i class="fas fa-random"></i>
{{ user?.oss_config_source !== 'none' ? '切到 OSS 存储' : '去配置 OSS' }}
</button>
<div v-if="user?.is_admin" style="margin-top: 8px; text-align: center;">
<a style="color: #4b5fc9; font-size: 13px; text-decoration: none; cursor: pointer;" @click.prevent="openOssConfigModal">
<i class="fas fa-tools"></i> 配置 / 修改 OSS
</a>
</div>
</div>
</div>
</div>
<div style="margin-top: 12px; padding: 10px 12px; background: rgba(255,255,255,0.05); border-radius: 10px; font-size: 13px; color: var(--text-secondary);">
<i class="fas fa-info-circle" style="color: #4b5fc9;"></i>
本地存储速度快但受配额限制OSS 支持多家云服务商,切换过程中可继续查看文件列表。
</div>
</div>
</div>
<!-- 本地存储信息 - 仅本地存储权限 -->
<div v-if="user && !user.is_admin && storagePermission === 'local_only'" style="margin-bottom: 40px;">
<h3 style="margin-bottom: 20px;">
<i class="fas fa-hard-drive"></i> 本地存储
</h3>
<div style="background: rgba(255,255,255,0.03); padding: 20px; border-radius: 8px;">
<div style="margin-bottom: 15px;">
<span style="font-weight: 600; color: var(--text-primary);">存储方式: </span>
<span style="color: #667eea; font-weight: 600;">本地存储</span>
<span style="margin-left: 10px; padding: 4px 12px; background: #22c55e; color: white; border-radius: 12px; font-size: 12px;">
<i class="fas fa-lock"></i> 仅本地
</span>
</div>
<div style="margin-bottom: 15px;">
<span style="font-weight: 600; color: var(--text-primary);">配额使用: </span>
<span>{{ localUsedFormatted }} / {{ localQuotaFormatted }} ({{ quotaPercentage }}%)</span>
<div style="margin-top: 8px; width: 100%; height: 18px; background: rgba(255,255,255,0.1); border-radius: 9px; overflow: hidden;">
<div :style="{
width: quotaPercentage + '%',
height: '100%',
background: quotaPercentage > 90 ? '#dc3545' : quotaPercentage > 75 ? '#ffc107' : '#28a745',
transition: 'width 0.3s'
}"></div>
</div>
</div>
<div style="padding: 10px; background: rgba(59, 130, 246, 0.15); border-left: 4px solid #3b82f6; border-radius: 6px; font-size: 13px; color: #93c5fd;">
<i class="fas fa-info-circle"></i>
<strong>说明:</strong> 管理员已将您的存储权限设置为"仅本地存储"您的文件存储在服务器本地速度快但有配额限制。如需使用OSS存储请联系管理员修改权限设置。
</div>
</div>
</div>
<!-- OSS 概览 / 配置入口 - 仅OSS权限 -->
<div v-if="user && !user.is_admin && storagePermission === 'oss_only'" style="margin-bottom: 40px;">
<h3 style="margin-bottom: 20px;">
<i class="fas fa-cloud"></i> OSS存储
</h3>
<div style="background: rgba(255,255,255,0.03); padding: 20px; border-radius: 12px; border: 1px solid var(--glass-border);">
<div style="display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-bottom: 12px;">
<div>
<div style="font-weight: 700; color: var(--text-primary); display: flex; gap: 8px; align-items: center;">
<i class="fas fa-shield-alt"></i>
仅 OSS 模式
</div>
<div style="color: var(--text-secondary); font-size: 13px; margin-top: 6px;">
{{ user?.oss_config_source !== 'none' ? '已配置系统级 OSS可正常使用 OSS 存储。' : '还未配置 OSS请先填写配置信息。' }}
</div>
</div>
<!-- 仅在用户有个人OSS配置时显示修改按钮 -->
<button v-if="user?.has_oss_config" class="btn btn-primary" @click="openOssConfigModal()" style="border-radius: 10px;">
<i class="fas fa-tools"></i> 修改个人 OSS 配置
</button>
</div>
<!-- OSS服务器信息 -->
<div v-if="user?.oss_config_source !== 'none'" style="margin-bottom: 12px; padding: 12px; background: var(--bg-secondary); border-radius: 10px; border: 1px solid var(--glass-border);">
<div style="font-weight: 600; color: var(--text-primary); margin-bottom: 8px;">
<i class="fas fa-cloud" style="color: var(--accent-1);"></i> 云服务信息
</div>
<div style="color: var(--text-secondary); font-size: 14px;">
{{ user.oss_provider }} / {{ user.oss_bucket }}
</div>
</div>
<!-- OSS空间使用统计 -->
<div v-if="user?.oss_config_source !== 'none'" style="margin-bottom: 12px; padding: 12px; background: var(--bg-secondary); border-radius: 10px; border: 1px solid var(--glass-border);">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<div style="font-weight: 600; color: var(--text-primary);">
<i class="fas fa-chart-pie" style="color: #667eea;"></i> 空间使用统计
</div>
<button
class="btn btn-secondary"
style="padding: 4px 10px; font-size: 12px; border-radius: 6px;"
@click="loadOssUsage()"
:disabled="ossUsageLoading">
<i :class="ossUsageLoading ? 'fas fa-sync-alt fa-spin' : 'fas fa-sync-alt'"></i>
{{ ossUsageLoading ? '统计中...' : '刷新' }}
</button>
</div>
<!-- 加载中 -->
<div v-if="ossUsageLoading && !ossUsage" style="text-align: center; padding: 20px; color: #667eea;">
<i class="fas fa-spinner fa-spin" style="font-size: 24px;"></i>
<div style="margin-top: 8px; font-size: 13px;">正在统计 OSS 空间使用情况...</div>
<div style="margin-top: 4px; font-size: 12px; color: var(--text-muted);">(文件较多时可能需要一些时间)</div>
</div>
<!-- 错误提示 -->
<div v-else-if="ossUsageError" style="padding: 12px; background: rgba(239, 68, 68, 0.1); border-radius: 8px; color: #ef4444; font-size: 13px;">
<i class="fas fa-exclamation-triangle"></i> {{ ossUsageError }}
</div>
<!-- 统计结果 -->
<div v-else-if="ossUsage" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 12px;">
<div style="text-align: center; padding: 12px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 10px; color: white;">
<div style="font-size: 20px; font-weight: 700;">{{ ossUsage.totalSizeFormatted }}</div>
<div style="font-size: 12px; opacity: 0.9; margin-top: 4px;">总使用空间</div>
</div>
<div style="text-align: center; padding: 12px; background: rgba(59, 130, 246, 0.1); border-radius: 10px; border: 1px solid rgba(59,130,246,0.2);">
<div style="font-size: 20px; font-weight: 700; color: #3b82f6;">{{ ossUsage.fileCount }}</div>
<div style="font-size: 12px; color: var(--text-muted); margin-top: 4px;">对象数</div>
</div>
</div>
<!-- 未统计提示 -->
<div v-else style="text-align: center; padding: 16px; color: var(--text-muted); font-size: 13px;">
<i class="fas fa-database" style="font-size: 24px; color: var(--text-muted); margin-bottom: 8px; display: block;"></i>
点击"刷新"按钮统计 OSS 空间使用情况
</div>
</div>
<div style="padding: 10px; background: rgba(102, 126, 234, 0.1); border-radius: 10px; color: var(--text-secondary); font-size: 13px;">
<i class="fas fa-info-circle" style="color: #4b5fc9;"></i>
数据存储在云服务上,安全可靠扩展性强。如需切换回本地请联系管理员调整权限。
</div>
</div>
</div>
<!-- 界面设置 -->
<h3 style="margin: 40px 0 20px 0;"><i class="fas fa-palette"></i> 界面设置</h3>
<div style="background: var(--bg-card); padding: 20px; border-radius: 12px; border: 1px solid var(--glass-border); margin-bottom: 30px;">
<div style="margin-bottom: 15px;">
<span style="font-weight: 600; color: var(--text-primary);">主题模式</span>
<span style="color: var(--text-secondary); font-size: 13px; margin-left: 10px;">选择你喜欢的界面风格</span>
</div>
<div style="display: flex; gap: 12px; flex-wrap: wrap;">
<button
class="btn"
:class="userThemePreference === null ? 'btn-primary' : 'btn-secondary'"
@click="setUserTheme(null)"
style="flex: 1; min-width: 120px; padding: 12px 16px;">
<i class="fas fa-globe"></i> 跟随全局
<span v-if="userThemePreference === null" style="margin-left: 8px; font-size: 12px; opacity: 0.8;">({{ globalTheme === 'dark' ? '暗色' : '亮色' }})</span>
</button>
<button
class="btn"
:class="userThemePreference === 'dark' ? 'btn-primary' : 'btn-secondary'"
@click="setUserTheme('dark')"
style="flex: 1; min-width: 120px; padding: 12px 16px;">
<i class="fas fa-moon"></i> 暗色主题
</button>
<button
class="btn"
:class="userThemePreference === 'light' ? 'btn-primary' : 'btn-secondary'"
@click="setUserTheme('light')"
style="flex: 1; min-width: 120px; padding: 12px 16px;">
<i class="fas fa-sun"></i> 亮色主题
</button>
</div>
<div style="margin-top: 12px; font-size: 13px; color: var(--text-muted);">
<i class="fas fa-info-circle"></i> 主题设置会影响你的文件页面和分享页面的外观
</div>
</div>
<!-- 账号设置 -->
<h3 style="margin: 40px 0 20px 0;"><i class="fas fa-user-cog"></i> 账号设置</h3>
<!-- 管理员可以改用户名 -->
<form v-if="user && user.is_admin" @submit.prevent="updateUsername" style="margin-bottom: 30px;">
<div class="form-group">
<label class="form-label">用户名</label>
<input type="text" class="form-input" v-model="usernameForm.newUsername" :placeholder="user.username" minlength="3" maxlength="20" required>
</div>
<button type="submit" class="btn btn-primary" :disabled="usernameChanging">
<i :class="usernameChanging ? 'fas fa-spinner fa-spin' : 'fas fa-save'"></i> {{ usernameChanging ? '保存中...' : '修改用户名' }}
</button>
</form>
<!-- 所有用户都可以改密码 -->
<form @submit.prevent="changePassword">
<div class="form-group">
<label class="form-label">当前密码</label>
<input type="password" class="form-input" v-model="changePasswordForm.current_password" placeholder="输入当前密码" required>
</div>
<div class="form-group">
<label class="form-label">新密码 (至少6字符)</label>
<input type="password" class="form-input" v-model="changePasswordForm.new_password" placeholder="输入新密码" minlength="6" required>
</div>
<button type="submit" class="btn btn-primary" :disabled="passwordChanging">
<i :class="passwordChanging ? 'fas fa-spinner fa-spin' : 'fas fa-key'"></i> {{ passwordChanging ? '修改中...' : '修改密码' }}
</button>
</form>
</div>
</div>
<!-- 分享视图 -->
<div v-if="isLoggedIn && currentView === 'shares'" class="main-container">
<div class="card">
<!-- 标题和工具栏 -->
<div style="margin-bottom: 16px; display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap;">
<h3 style="margin: 0; display: flex; align-items: center; gap: 8px;">
<i class="fas fa-share-alt"></i> 我的分享
</h3>
<div style="display: flex; gap: 8px;">
<button class="btn" :class="shareViewMode === 'grid' ? 'btn-primary' : 'btn-secondary'" @click="shareViewMode = 'grid'">
<i class="fas fa-th-large"></i> 卡片
</button>
<button class="btn" :class="shareViewMode === 'list' ? 'btn-primary' : 'btn-secondary'" @click="shareViewMode = 'list'">
<i class="fas fa-list"></i> 列表
</button>
<button class="btn btn-secondary" @click="loadShares">
<i class="fas fa-sync-alt"></i> 刷新
</button>
</div>
</div>
<!-- 筛选/搜索 -->
<div class="share-toolbar" style="margin-bottom: 14px;">
<input type="text" v-model="shareFilters.keyword" placeholder="搜索路径 / 链接 / 分享码" style="flex: 1; min-width: 180px;">
<select v-model="shareFilters.type">
<option value="all">全部类型</option>
<option value="file">文件</option>
<option value="directory">文件夹</option>
<option value="all_files">全部文件</option>
</select>
<select v-model="shareFilters.status">
<option value="all">全部状态</option>
<option value="active">有效</option>
<option value="expiring">即将到期</option>
<option value="expired">已过期</option>
<option value="protected">已加密</option>
<option value="public">公开</option>
</select>
<select v-model="shareFilters.sort">
<option value="created_desc">最新创建</option>
<option value="created_asc">最早创建</option>
<option value="views_desc">访问最多</option>
<option value="downloads_desc">下载最多</option>
<option value="expire_asc">最早到期</option>
</select>
</div>
<!-- 空状态 -->
<div v-if="shares.length === 0" class="alert alert-info">
还没有创建任何分享
</div>
<div v-else-if="filteredShares.length === 0" class="alert alert-warning">
没有符合筛选条件的分享,试试清空搜索/筛选。
</div>
<!-- 大图标视图 -->
<div v-else-if="shareViewMode === 'grid'" class="share-card-grid">
<div v-for="share in filteredShares" :key="share.id" class="share-card">
<div class="share-card__title">
<i class="fas" :class="{
'fa-file-alt': share.share_type === 'file',
'fa-folder': share.share_type === 'directory',
'fa-layer-group': share.share_type === 'all'
}" style="color: var(--accent-1);"></i>
<span :title="share.share_path">{{ share.share_path }}</span>
</div>
<div class="share-card__chips">
<span :class="['share-chip', getShareStatus(share).class]">
<i class="fas" :class="getShareStatus(share).icon"></i> {{ getShareStatus(share).text }}
</span>
<span class="share-chip info">
<i class="fas fa-tag"></i> {{ getShareTypeLabel(share.share_type) }}
</span>
<span class="share-chip info">
<i class="fas" :class="getShareProtection(share).icon"></i> {{ getShareProtection(share).text }}
</span>
<span class="share-chip info" v-if="share.storage_type">
<i class="fas fa-hdd"></i> {{ getStorageLabel(share.storage_type) }}
</span>
<span class="share-chip info">
<i class="fas fa-barcode"></i> {{ share.share_code }}
</span>
</div>
<div style="font-size: 13px; color: var(--text-secondary); margin-bottom: 10px; word-break: break-all;">
<i class="fas fa-link"></i>
<a :href="share.share_url" target="_blank" style="color: var(--accent-1); word-break: break-all;">{{ share.share_url }}</a>
</div>
<div class="share-card__meta">
<span><i class="fas fa-eye"></i> 访问 {{ share.view_count }}</span>
<span><i class="fas fa-download"></i> 下载 {{ share.download_count }}</span>
<span><i class="fas fa-clock"></i> {{ share.expires_at ? formatExpireTime(share.expires_at) : '永久有效' }}</span>
<span><i class="fas fa-calendar-alt"></i> 创建 {{ formatDateTime(share.created_at) }}</span>
</div>
<div class="share-card__actions">
<button class="btn btn-secondary" @click.stop="openShare(share.share_url)">
<i class="fas fa-external-link-alt"></i> 打开
</button>
<button class="btn btn-secondary" @click.stop="copyShareLink(share.share_url)">
<i class="fas fa-copy"></i> 复制链接
</button>
<button class="btn" style="background: #ef4444; color: white;" @click.stop="deleteShare(share.id)">
<i class="fas fa-trash"></i> 删除
</button>
</div>
</div>
</div>
<!-- 列表视图 -->
<table v-else style="width: 100%; border-collapse: collapse; table-layout: fixed;">
<thead>
<tr style="border-bottom: 2px solid #ddd;">
<th style="padding: 10px; text-align: left; width: 20%;">文件路径</th>
<th style="padding: 10px; text-align: left; width: 30%;">分享链接</th>
<th style="padding: 10px; text-align: center; width: 10%;">访问次数</th>
<th style="padding: 10px; text-align: center; width: 10%;">下载次数</th>
<th style="padding: 10px; text-align: center; width: 20%;">到期时间</th>
<th style="padding: 10px; text-align: center; width: 10%;">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="share in filteredShares" :key="share.id" style="border-bottom: 1px solid #eee;">
<td style="padding: 10px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" :title="share.share_path">{{ share.share_path }}</td>
<td style="padding: 10px; overflow: hidden;">
<a :href="share.share_url" target="_blank" style="color: #667eea; display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" :title="share.share_url">{{ share.share_url }}</a>
</td>
<td style="padding: 10px; text-align: center;">{{ share.view_count }}</td>
<td style="padding: 10px; text-align: center;">{{ share.download_count }}</td>
<td style="padding: 10px; text-align: center;">
<span v-if="!share.expires_at" style="color: #22c55e;"><i class="fas fa-infinity"></i> 永久有效</span>
<span v-else :style="{color: isExpiringSoon(share.expires_at) ? '#ffc107' : isExpired(share.expires_at) ? '#dc3545' : '#667eea'}" :title="share.expires_at"><i class="fas fa-clock"></i> {{ formatExpireTime(share.expires_at) }}</span>
</td>
<td style="padding: 10px; text-align: center;">
<button class="btn" style="background: #ef4444; color: white;" @click="deleteShare(share.id)">
<i class="fas fa-trash"></i> 删除
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 管理员视图 -->
<div v-if="isLoggedIn && currentView === 'admin' && user && user.is_admin" class="main-container">
<!-- 管理员标签页导航 -->
<div class="card" style="margin-bottom: 20px; padding: 0;">
<div style="display: flex; flex-wrap: wrap; border-bottom: 2px solid #eee;">
<button @click="adminTab = 'overview'"
:style="{ padding: '15px 25px', border: 'none', background: adminTab === 'overview' ? '#667eea' : 'transparent', color: adminTab === 'overview' ? 'white' : '#666', cursor: 'pointer', fontWeight: '600', fontSize: '14px', borderRadius: adminTab === 'overview' ? '8px 8px 0 0' : '0', transition: 'all 0.2s' }">
<i class="fas fa-tachometer-alt"></i> 概览
</button>
<button @click="adminTab = 'settings'"
:style="{ padding: '15px 25px', border: 'none', background: adminTab === 'settings' ? '#667eea' : 'transparent', color: adminTab === 'settings' ? 'white' : '#666', cursor: 'pointer', fontWeight: '600', fontSize: '14px', borderRadius: adminTab === 'settings' ? '8px 8px 0 0' : '0', transition: 'all 0.2s' }">
<i class="fas fa-cog"></i> 设置
</button>
<button @click="openMonitorTab"
:style="{ padding: '15px 25px', border: 'none', background: adminTab === 'monitor' ? '#667eea' : 'transparent', color: adminTab === 'monitor' ? 'white' : '#666', cursor: 'pointer', fontWeight: '600', fontSize: '14px', borderRadius: adminTab === 'monitor' ? '8px 8px 0 0' : '0', transition: 'all 0.2s' }">
<i class="fas fa-chart-line"></i> 监控
</button>
<button @click="adminTab = 'users'"
:style="{ padding: '15px 25px', border: 'none', background: adminTab === 'users' ? '#667eea' : 'transparent', color: adminTab === 'users' ? 'white' : '#666', cursor: 'pointer', fontWeight: '600', fontSize: '14px', borderRadius: adminTab === 'users' ? '8px 8px 0 0' : '0', transition: 'all 0.2s' }">
<i class="fas fa-users"></i> 用户
</button>
</div>
</div>
<!-- ========== 概览标签页 ========== -->
<div v-show="adminTab === 'overview'">
<!-- 调试模式开关 -->
<div class="card" style="margin-bottom: 30px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white;">
<div style="display: flex; align-items: center; justify-content: space-between;">
<div>
<h3 style="margin-bottom: 10px; color: white;">
<i class="fas fa-bug"></i> 调试模式
</h3>
<p style="margin: 0; font-size: 14px; opacity: 0.9;">
{{ debugMode ? '已启用 - F12和开发者工具已解锁' : '已禁用 - F12和开发者工具被锁定' }}
</p>
</div>
<button @click="toggleDebugMode" class="btn" :style="{background: debugMode ? '#28a745' : '#dc3545', color: 'white', border: 'none', padding: '12px 24px', fontSize: '16px', fontWeight: '600', cursor: 'pointer', borderRadius: '8px', boxShadow: '0 4px 12px rgba(0,0,0,0.2)'}">
<i :class="debugMode ? 'fas fa-toggle-on' : 'fas fa-toggle-off'"></i>
{{ debugMode ? '关闭调试' : '开启调试' }}
</button>
</div>
</div>
<!-- 服务器存储统计 -->
<div class="card" style="margin-bottom: 30px;">
<h3 style="margin-bottom: 20px;">
<i class="fas fa-hdd"></i> 服务器存储统计
</h3>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px;">
<!-- 磁盘总容量 -->
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 20px; border-radius: 12px; color: white;">
<div style="display: flex; align-items: center; justify-content: space-between;">
<div>
<div style="font-size: 14px; opacity: 0.9; margin-bottom: 8px;">磁盘总容量</div>
<div style="font-size: 28px; font-weight: 700;">{{ formatBytes(serverStorageStats.totalDisk) }}</div>
</div>
<i class="fas fa-database" style="font-size: 48px; opacity: 0.3;"></i>
</div>
</div>
<!-- 已使用空间 -->
<div style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); padding: 20px; border-radius: 12px; color: white;">
<div style="display: flex; align-items: center; justify-content: space-between;">
<div>
<div style="font-size: 14px; opacity: 0.9; margin-bottom: 8px;">已使用空间</div>
<div style="font-size: 28px; font-weight: 700;">{{ formatBytes(serverStorageStats.usedDisk) }}</div>
<div style="font-size: 12px; opacity: 0.8; margin-top: 4px;">
{{ serverStorageStats.totalDisk > 0 ? Math.round((serverStorageStats.usedDisk / serverStorageStats.totalDisk) * 100) : 0 }}% 使用率
</div>
</div>
<i class="fas fa-chart-pie" style="font-size: 48px; opacity: 0.3;"></i>
</div>
</div>
<!-- 可用空间 -->
<div style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); padding: 20px; border-radius: 12px; color: white;">
<div style="display: flex; align-items: center; justify-content: space-between;">
<div>
<div style="font-size: 14px; opacity: 0.9; margin-bottom: 8px;">可用空间</div>
<div style="font-size: 28px; font-weight: 700;">{{ formatBytes(serverStorageStats.availableDisk) }}</div>
</div>
<i class="fas fa-folder-open" style="font-size: 48px; opacity: 0.3;"></i>
</div>
</div>
<!-- 用户配额总和 -->
<div style="background: linear-gradient(135deg, #fa709a 0%, #fee140 100%); padding: 20px; border-radius: 12px; color: white;">
<div style="display: flex; align-items: center; justify-content: space-between;">
<div>
<div style="font-size: 14px; opacity: 0.9; margin-bottom: 8px;">用户配额总和</div>
<div style="font-size: 28px; font-weight: 700;">{{ formatBytes(serverStorageStats.totalUserQuotas) }}</div>
<div style="font-size: 12px; opacity: 0.8; margin-top: 4px;">
{{ serverStorageStats.totalUsers }} 个用户
</div>
</div>
<i class="fas fa-users" style="font-size: 48px; opacity: 0.3;"></i>
</div>
</div>
<!-- 用户实际使用 -->
<div style="background: linear-gradient(135deg, #30cfd0 0%, #330867 100%); padding: 20px; border-radius: 12px; color: white;">
<div style="display: flex; align-items: center; justify-content: space-between;">
<div>
<div style="font-size: 14px; opacity: 0.9; margin-bottom: 8px;">用户实际使用</div>
<div style="font-size: 28px; font-weight: 700;">{{ formatBytes(serverStorageStats.totalUserUsed) }}</div>
<div style="font-size: 12px; opacity: 0.8; margin-top: 4px;">
{{ serverStorageStats.totalUserQuotas > 0 ? Math.round((serverStorageStats.totalUserUsed / serverStorageStats.totalUserQuotas) * 100) : 0 }}% 配额使用率
</div>
</div>
<i class="fas fa-cloud-upload-alt" style="font-size: 48px; opacity: 0.3;"></i>
</div>
</div>
<!-- 配额剩余 -->
<div style="background: linear-gradient(135deg, rgba(102, 126, 234, 0.2), rgba(118, 75, 162, 0.2)); padding: 20px; border-radius: 12px; color: var(--text-primary); border: 1px solid var(--glass-border);">
<div style="display: flex; align-items: center; justify-content: space-between;">
<div>
<div style="font-size: 14px; opacity: 0.8; margin-bottom: 8px;">安全可分配配额</div>
<div style="font-size: 28px; font-weight: 700;">
{{ formatBytes(Math.max(0, serverStorageStats.availableDisk - (serverStorageStats.totalUserQuotas - serverStorageStats.totalUserUsed))) }}
</div>
<div style="font-size: 12px; opacity: 0.7; margin-top: 4px;">
可用空间 - 未使用的配额
</div>
</div>
<i class="fas fa-boxes" style="font-size: 48px; opacity: 0.2;"></i>
</div>
</div>
</div>
<!-- 存储警告提示 -->
<div v-if="serverStorageStats.totalDisk > 0 && ((serverStorageStats.usedDisk / serverStorageStats.totalDisk) > 0.9)"
style="margin-top: 20px; padding: 15px; background: rgba(245, 158, 11, 0.15); border-left: 4px solid #f59e0b; border-radius: 6px; color: #fbbf24;">
<i class="fas fa-exclamation-triangle"></i>
<strong>警告:</strong> 磁盘使用率已超过90%,建议及时清理空间或扩容!
</div>
<div v-if="serverStorageStats.totalUserQuotas > serverStorageStats.totalDisk"
style="margin-top: 20px; padding: 15px; background: rgba(239, 68, 68, 0.15); border-left: 4px solid #ef4444; border-radius: 6px; color: #fca5a5;">
<i class="fas fa-exclamation-circle"></i>
<strong>配额超分配:</strong> 用户配额总和 ({{ formatBytes(serverStorageStats.totalUserQuotas) }}) 已超过磁盘总容量 ({{ formatBytes(serverStorageStats.totalDisk) }})
</div>
</div>
</div><!-- 概览标签页结束 -->
<!-- ========== 设置标签页 ========== -->
<div v-show="adminTab === 'settings'">
<!-- 系统设置 -->
<div class="card" style="margin-bottom: 30px;">
<h3 style="margin-bottom: 20px;">
<i class="fas fa-sliders-h"></i> 系统设置
</h3>
<div style="display: flex; align-items: flex-end; gap: 16px; flex-wrap: wrap;">
<div class="form-group" style="margin-bottom: 0;">
<label class="form-label">最大上传大小 (MB)</label>
<input type="number" class="form-input" v-model.number="systemSettings.maxUploadSizeMB" min="1" style="width: 150px;">
</div>
<button class="btn btn-primary" @click="updateSystemSettings" style="height: 40px;">
<i class="fas fa-save"></i> 保存
</button>
<span style="color: var(--text-secondary); font-size: 13px;">修改后需要重启服务才能生效</span>
</div>
<hr style="margin: 20px 0;">
<h4 style="margin-bottom: 12px;"><i class="fas fa-palette"></i> 全局主题设置</h4>
<div style="margin-bottom: 15px;">
<span style="color: var(--text-secondary); font-size: 13px;">设置系统默认主题,用户可以在个人设置中覆盖此设置</span>
</div>
<div style="display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 20px;">
<button
class="btn"
:class="globalTheme === 'dark' ? 'btn-primary' : 'btn-secondary'"
@click="setGlobalTheme('dark')"
style="padding: 12px 24px;">
<i class="fas fa-moon"></i> 暗色主题(默认)
</button>
<button
class="btn"
:class="globalTheme === 'light' ? 'btn-primary' : 'btn-secondary'"
@click="setGlobalTheme('light')"
style="padding: 12px 24px;">
<i class="fas fa-sun"></i> 亮色主题
</button>
</div>
<div style="font-size: 13px; color: var(--text-muted); background: var(--bg-card); padding: 12px; border-radius: 8px; border: 1px solid var(--glass-border);">
<i class="fas fa-info-circle"></i> 当前全局主题: <strong>{{ globalTheme === 'dark' ? '暗色' : '亮色' }}</strong>
未设置个人偏好的用户将使用此主题。分享页面也会默认使用分享者的主题设置。
</div>
<hr style="margin: 20px 0;">
<h4 style="margin-bottom: 12px;"><i class="fas fa-envelope"></i> SMTP 邮件配置(用于注册激活和找回密码)</h4>
<div class="alert alert-info" style="margin-bottom: 15px;">
支持 QQ/163/企业邮箱等。QQ 邮箱示例:主机 <code>smtp.qq.com</code>,端口 <code>465</code>,勾选 SSL用户名=邮箱地址,密码=授权码。
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 16px;">
<div class="form-group">
<label class="form-label">SMTP 主机</label>
<input type="text" class="form-input" v-model="systemSettings.smtp.host" placeholder="如 smtp.qq.com">
</div>
<div class="form-group">
<label class="form-label">端口</label>
<input type="number" class="form-input" v-model.number="systemSettings.smtp.port" placeholder="465/587">
</div>
<div class="form-group">
<label class="form-label">SSL/TLS</label>
<div style="display: flex; align-items: center; gap: 8px;">
<input type="checkbox" id="smtp-secure" v-model="systemSettings.smtp.secure">
<label for="smtp-secure" style="margin: 0;">使用 SSL465 通常需要)</label>
</div>
</div>
<div class="form-group">
<label class="form-label">用户名(邮箱)</label>
<input type="text" class="form-input" v-model="systemSettings.smtp.user" placeholder="your@qq.com">
</div>
<div class="form-group">
<label class="form-label">发件人 From可选</label>
<input type="text" class="form-input" v-model="systemSettings.smtp.from" placeholder="显示名称 <your@qq.com>">
</div>
<div class="form-group">
<label class="form-label">密码/授权码</label>
<input type="password" class="form-input" v-model="systemSettings.smtp.password" :placeholder="systemSettings.smtp.has_password ? '已配置,留空则不修改' : '请输入授权码'">
</div>
</div>
<div style="display: flex; gap: 10px; flex-wrap: wrap; margin-top: 15px;">
<button class="btn btn-primary" @click="updateSystemSettings">
<i class="fas fa-save"></i> 保存设置
</button>
<button class="btn btn-secondary" @click="testSmtp">
<i class="fas fa-envelope"></i> 发送测试邮件
</button>
</div>
</div>
<!-- 管理员 OSS 配置 -->
<div class="card" style="margin-bottom: 30px;">
<h3 style="margin-bottom: 20px;">
<i class="fas fa-cloud"></i> 管理员 OSS 配置
</h3>
<div style="margin-bottom: 15px;">
<span style="color: var(--text-secondary); font-size: 13px;">配置管理员账号的 OSS 云存储,用于文件存储和管理。</span>
</div>
<!-- OSS 配置状态 -->
<div v-if="user?.oss_config_source !== 'none'" style="margin-bottom: 20px; padding: 15px; background: rgba(34, 197, 94, 0.1); border-left: 4px solid #22c55e; border-radius: 8px;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<div>
<div style="font-weight: 600; color: var(--text-primary); margin-bottom: 4px;">
<i class="fas fa-check-circle" style="color: #22c55e;"></i> OSS 已配置
</div>
<div style="font-size: 13px; color: var(--text-secondary);">
{{ user.oss_provider }} / {{ user.oss_bucket }}
</div>
</div>
<button class="btn btn-primary" @click="openOssConfigModal" style="padding: 8px 16px;">
<i class="fas fa-edit"></i> 修改配置
</button>
</div>
</div>
<!-- OSS 空间统计 -->
<div v-if="user?.oss_config_source !== 'none'" style="margin-bottom: 20px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<span style="font-weight: 600; color: var(--text-primary);">
<i class="fas fa-chart-pie"></i> 空间使用统计
</span>
<button class="btn btn-secondary" @click="loadOssUsage" :disabled="ossUsageLoading" style="padding: 6px 12px; font-size: 12px;">
<i :class="ossUsageLoading ? 'fas fa-spinner fa-spin' : 'fas fa-sync-alt'"></i> 刷新
</button>
</div>
<div v-if="ossUsage" style="padding: 12px; background: var(--bg-card); border-radius: 8px; border: 1px solid var(--glass-border);">
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 15px; font-size: 13px;">
<div>
<span style="color: var(--text-muted);">总大小</span>
<div style="font-weight: 600; color: var(--text-primary);">{{ ossUsage.totalSizeFormatted || '-' }}</div>
</div>
<div>
<span style="color: var(--text-muted);">文件数</span>
<div style="font-weight: 600; color: var(--text-primary);">{{ ossUsage.fileCount || '-' }}</div>
</div>
<div>
<span style="color: var(--text-muted);">文件夹数</span>
<div style="font-weight: 600; color: var(--text-primary);">{{ ossUsage.dirCount || '-' }}</div>
</div>
</div>
</div>
<div v-else-if="ossUsageLoading" style="padding: 12px; text-align: center; color: var(--text-muted);">
<i class="fas fa-spinner fa-spin"></i> 正在加载...
</div>
<div v-else style="padding: 12px; text-align: center; color: var(--text-muted);">
暂无数据,点击刷新查看
</div>
</div>
<!-- 未配置 OSS 时显示配置按钮 -->
<div v-if="user?.oss_config_source === 'none'" style="padding: 30px; text-align: center; background: var(--bg-card); border-radius: 12px; border: 1px solid var(--glass-border);">
<i class="fas fa-cloud-upload-alt" style="font-size: 48px; color: var(--text-muted); margin-bottom: 15px;"></i>
<div style="margin-bottom: 15px; color: var(--text-secondary);">尚未配置 OSS 存储</div>
<button class="btn btn-primary" @click="openOssConfigModal" style="padding: 12px 30px;">
<i class="fas fa-plus"></i> 配置 OSS 存储
</button>
</div>
<!-- 存储模式切换 -->
<div v-if="user" style="margin-top: 20px; padding: 15px; background: var(--bg-card); border-radius: 12px; border: 1px solid var(--glass-border);">
<div style="display: flex; justify-content: space-between; align-items: center;">
<div>
<div style="font-weight: 600; color: var(--text-primary); margin-bottom: 5px;">
<i class="fas fa-database"></i> 当前存储模式
</div>
<div style="font-size: 13px; color: var(--text-secondary);">
<span v-if="user.current_storage_type === 'local'" style="color: #667eea;">
<i class="fas fa-hard-drive"></i> 本地存储
</span>
<span v-else style="color: #6c757d;">
<i class="fas fa-cloud"></i> OSS 存储
</span>
</div>
</div>
<button v-if="user?.oss_config_source !== 'none'" class="btn" :class="user.current_storage_type === 'oss' ? 'btn-secondary' : 'btn-primary'"
@click="switchStorage(user.current_storage_type === 'local' ? 'oss' : 'local')"
:disabled="storageSwitching" style="padding: 8px 16px;">
<i :class="storageSwitching ? 'fas fa-spinner fa-spin' : 'fas fa-random'"></i>
{{ storageSwitching ? '切换中...' : (user.current_storage_type === 'local' ? '切换到 OSS' : '切换到本地') }}
</button>
</div>
</div>
</div>
</div><!-- 设置标签页结束 -->
<!-- ========== 监控标签页 ========== -->
<div v-show="adminTab === 'monitor'">
<div v-if="monitorTabLoading" class="card" style="margin-bottom: 30px; text-align: center; padding: 40px;">
<i class="fas fa-spinner fa-spin" style="font-size: 36px; margin-bottom: 10px;"></i>
<div style="color: var(--text-secondary); font-size: 14px;">正在加载监控数据...</div>
</div>
<template v-else>
<!-- 健康检测 -->
<div class="card" style="margin-bottom: 30px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h3 style="margin: 0;">
<i class="fas fa-heartbeat"></i> 系统健康检测
</h3>
<button class="btn btn-primary" @click="loadHealthCheck" :disabled="healthCheck.loading">
<i class="fas" :class="healthCheck.loading ? 'fa-spinner fa-spin' : 'fa-sync'"></i>
{{ healthCheck.loading ? '检测中...' : '刷新检测' }}
</button>
</div>
<!-- 整体状态 -->
<div v-if="healthCheck.overallStatus" style="margin-bottom: 20px;">
<div style="display: flex; align-items: center; gap: 15px; flex-wrap: wrap;">
<div style="font-size: 18px; font-weight: bold;" :class="getOverallStatusColor(healthCheck.overallStatus)">
<i class="fas" :class="{
'fa-check-circle': healthCheck.overallStatus === 'healthy',
'fa-exclamation-triangle': healthCheck.overallStatus === 'warning',
'fa-times-circle': healthCheck.overallStatus === 'critical'
}"></i>
{{ getOverallStatusText(healthCheck.overallStatus) }}
</div>
<div style="display: flex; gap: 12px; font-size: 13px;">
<span style="color: #22c55e;"><i class="fas fa-check"></i> 通过: {{ healthCheck.summary.pass }}</span>
<span style="color: #ffc107;"><i class="fas fa-exclamation"></i> 警告: {{ healthCheck.summary.warning }}</span>
<span style="color: #ef4444;"><i class="fas fa-times"></i> 失败: {{ healthCheck.summary.fail }}</span>
<span style="color: #17a2b8;"><i class="fas fa-info"></i> 信息: {{ healthCheck.summary.info }}</span>
</div>
</div>
<div v-if="healthCheck.lastCheck" style="font-size: 12px; color: var(--text-muted); margin-top: 8px;">
上次检测: {{ new Date(healthCheck.lastCheck).toLocaleString() }}
</div>
</div>
<!-- 检测项列表 -->
<div v-if="healthCheck.checks.length > 0">
<!-- 按分类分组 -->
<div style="margin-bottom: 20px;">
<h4 style="margin-bottom: 12px; color: var(--text-primary);"><i class="fas fa-shield-alt"></i> 安全配置</h4>
<div style="display: grid; gap: 10px;">
<div v-for="check in healthCheck.checks.filter(c => c.category === 'security')" :key="check.name"
style="display: flex; align-items: flex-start; gap: 12px; padding: 12px; background: rgba(255,255,255,0.03); border-radius: 8px;">
<span style="font-size: 18px; width: 24px; text-align: center;"
:class="{
'text-green-600': check.status === 'pass',
'text-yellow-600': check.status === 'warning',
'text-red-600': check.status === 'fail',
'text-blue-600': check.status === 'info'
}">
{{ getHealthStatusIcon(check.status) }}
</span>
<div style="flex: 1;">
<div style="font-weight: 500; margin-bottom: 4px;">{{ check.name }}</div>
<div style="font-size: 13px; color: var(--text-secondary);">{{ check.message }}</div>
<div v-if="check.suggestion" style="font-size: 12px; color: #e67e22; margin-top: 4px;">
<i class="fas fa-lightbulb"></i> {{ check.suggestion }}
</div>
</div>
</div>
</div>
</div>
<div style="margin-bottom: 20px;">
<h4 style="margin-bottom: 12px; color: var(--text-primary);"><i class="fas fa-server"></i> 服务状态</h4>
<div style="display: grid; gap: 10px;">
<div v-for="check in healthCheck.checks.filter(c => c.category === 'service')" :key="check.name"
style="display: flex; align-items: flex-start; gap: 12px; padding: 12px; background: rgba(255,255,255,0.03); border-radius: 8px;">
<span style="font-size: 18px; width: 24px; text-align: center;"
:class="{
'text-green-600': check.status === 'pass',
'text-yellow-600': check.status === 'warning',
'text-red-600': check.status === 'fail',
'text-blue-600': check.status === 'info'
}">
{{ getHealthStatusIcon(check.status) }}
</span>
<div style="flex: 1;">
<div style="font-weight: 500; margin-bottom: 4px;">{{ check.name }}</div>
<div style="font-size: 13px; color: var(--text-secondary);">{{ check.message }}</div>
<div v-if="check.suggestion" style="font-size: 12px; color: #e67e22; margin-top: 4px;">
<i class="fas fa-lightbulb"></i> {{ check.suggestion }}
</div>
</div>
</div>
</div>
</div>
<div>
<h4 style="margin-bottom: 12px; color: var(--text-primary);"><i class="fas fa-cog"></i> 运行配置</h4>
<div style="display: grid; gap: 10px;">
<div v-for="check in healthCheck.checks.filter(c => c.category === 'config')" :key="check.name"
style="display: flex; align-items: flex-start; gap: 12px; padding: 12px; background: rgba(255,255,255,0.03); border-radius: 8px;">
<span style="font-size: 18px; width: 24px; text-align: center;"
:class="{
'text-green-600': check.status === 'pass',
'text-yellow-600': check.status === 'warning',
'text-red-600': check.status === 'fail',
'text-blue-600': check.status === 'info'
}">
{{ getHealthStatusIcon(check.status) }}
</span>
<div style="flex: 1;">
<div style="font-weight: 500; margin-bottom: 4px;">{{ check.name }}</div>
<div style="font-size: 13px; color: var(--text-secondary);">{{ check.message }}</div>
<div v-if="check.suggestion" style="font-size: 12px; color: #e67e22; margin-top: 4px;">
<i class="fas fa-lightbulb"></i> {{ check.suggestion }}
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 加载中 -->
<div v-else-if="healthCheck.loading" style="text-align: center; padding: 40px; color: var(--text-muted);">
<i class="fas fa-spinner fa-spin" style="font-size: 48px; margin-bottom: 15px;"></i>
<p>正在检测系统健康状态...</p>
</div>
<!-- 未检测提示 -->
<div v-else style="text-align: center; padding: 40px; color: var(--text-muted);">
<i class="fas fa-stethoscope" style="font-size: 48px; margin-bottom: 15px;"></i>
<p>点击"刷新检测"按钮开始系统健康检测</p>
</div>
</div>
<!-- 系统日志 -->
<div class="card" style="margin-bottom: 30px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h3 style="margin: 0;">
<i class="fas fa-clipboard-list"></i> 系统日志
</h3>
<div style="display: flex; gap: 8px; align-items: center;">
<button class="btn btn-secondary" @click="cleanupLogs" title="清理90天前的日志" style="padding: 6px 12px; font-size: 12px;">
<i class="fas fa-trash"></i> 清理
</button>
<button class="btn btn-primary" @click="loadSystemLogs(1)" :disabled="systemLogs.loading" style="padding: 6px 12px; font-size: 12px;">
<i class="fas" :class="systemLogs.loading ? 'fa-spinner fa-spin' : 'fa-sync'"></i>
刷新
</button>
</div>
</div>
<!-- 筛选器 -->
<div style="display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 15px; padding: 15px; background: rgba(255,255,255,0.03); border-radius: 8px;">
<div style="display: flex; align-items: center; gap: 8px;">
<label style="font-size: 13px; color: var(--text-secondary);">级别:</label>
<select v-model="systemLogs.filters.level" @change="filterLogs" style="padding: 6px 10px; border: 1px solid #ddd; border-radius: 4px;">
<option value="">全部</option>
<option value="info">信息</option>
<option value="warn">警告</option>
<option value="error">错误</option>
</select>
</div>
<div style="display: flex; align-items: center; gap: 8px;">
<label style="font-size: 13px; color: var(--text-secondary);">分类:</label>
<select v-model="systemLogs.filters.category" @change="filterLogs" style="padding: 6px 10px; border: 1px solid #ddd; border-radius: 4px;">
<option value="">全部</option>
<option value="auth">认证</option>
<option value="user">用户</option>
<option value="file">文件</option>
<option value="share">分享</option>
<option value="system">系统</option>
<option value="security">安全</option>
</select>
</div>
<div style="display: flex; align-items: center; gap: 8px; flex: 1; min-width: 200px;">
<label style="font-size: 13px; color: var(--text-secondary);">搜索:</label>
<input type="text" v-model="systemLogs.filters.keyword" @keyup.enter="filterLogs"
placeholder="搜索日志内容..." style="flex: 1; padding: 6px 10px; border: 1px solid #ddd; border-radius: 4px;">
</div>
<button class="btn btn-secondary" @click="clearLogFilters" style="padding: 6px 12px;">
<i class="fas fa-times"></i> 清除筛选
</button>
</div>
<!-- 日志统计 -->
<div v-if="systemLogs.total > 0" style="margin-bottom: 15px; font-size: 13px; color: var(--text-secondary);">
共 {{ systemLogs.total }} 条日志,第 {{ systemLogs.page }}/{{ systemLogs.totalPages }} 页
</div>
<!-- 日志列表 -->
<div v-if="systemLogs.logs.length > 0" style="max-height: 500px; overflow-y: auto;">
<div v-for="log in systemLogs.logs" :key="log.id"
style="display: flex; gap: 12px; padding: 12px; border-bottom: 1px solid #eee; align-items: flex-start;">
<!-- 时间 -->
<div style="width: 140px; flex-shrink: 0; font-size: 12px; color: var(--text-muted);">
{{ formatLogTime(log.created_at) }}
</div>
<!-- 级别标签 -->
<div style="width: 50px; flex-shrink: 0;">
<span :style="getLogLevelColor(log.level)" style="padding: 2px 8px; border-radius: 4px; font-size: 11px;">
{{ getLogLevelText(log.level) }}
</span>
</div>
<!-- 分类图标 -->
<div style="width: 70px; flex-shrink: 0; display: flex; align-items: center; gap: 4px; font-size: 12px; color: var(--text-secondary);">
<i class="fas" :class="getLogCategoryIcon(log.category)"></i>
{{ getLogCategoryText(log.category) }}
</div>
<!-- 内容 -->
<div style="flex: 1; min-width: 0;">
<div style="font-weight: 500; margin-bottom: 4px;">{{ log.action }}</div>
<div style="font-size: 13px; color: var(--text-secondary);">{{ log.message }}</div>
<div v-if="log.username || log.ip_address" style="font-size: 11px; color: var(--text-muted); margin-top: 4px;">
<span v-if="log.username"><i class="fas fa-user"></i> {{ log.username }}</span>
<span v-if="log.ip_address" style="margin-left: 10px;"><i class="fas fa-globe"></i> {{ log.ip_address }}</span>
</div>
</div>
</div>
</div>
<!-- 加载中 -->
<div v-else-if="systemLogs.loading" style="text-align: center; padding: 40px; color: var(--text-muted);">
<i class="fas fa-spinner fa-spin" style="font-size: 24px;"></i>
<p>加载中...</p>
</div>
<!-- 空状态 -->
<div v-else style="text-align: center; padding: 40px; color: var(--text-muted);">
<i class="fas fa-clipboard" style="font-size: 48px; margin-bottom: 15px;"></i>
<p>暂无日志记录</p>
</div>
<!-- 分页 -->
<div v-if="systemLogs.totalPages > 1" style="display: flex; justify-content: center; gap: 8px; margin-top: 15px;">
<button class="btn btn-secondary" @click="loadSystemLogs(systemLogs.page - 1)" :disabled="systemLogs.page <= 1" style="padding: 6px 12px;">
<i class="fas fa-chevron-left"></i> 上一页
</button>
<span style="display: flex; align-items: center; padding: 0 15px; color: var(--text-secondary);">
{{ systemLogs.page }} / {{ systemLogs.totalPages }}
</span>
<button class="btn btn-secondary" @click="loadSystemLogs(systemLogs.page + 1)" :disabled="systemLogs.page >= systemLogs.totalPages" style="padding: 6px 12px;">
下一页 <i class="fas fa-chevron-right"></i>
</button>
</div>
</div>
</template>
</div><!-- 监控标签页结束 -->
<!-- ========== 用户标签页 ========== -->
<div v-show="adminTab === 'users'">
<div class="card">
<h3 style="margin-bottom: 20px;">用户管理</h3>
<div style="overflow-x: auto;">
<table style="width: 100%; border-collapse: collapse; table-layout: fixed; min-width: 900px;">
<thead>
<tr style="background: rgba(255,255,255,0.05);">
<th style="padding: 10px; text-align: left; width: 4%;">ID</th>
<th style="padding: 10px; text-align: left; width: 10%;">用户名</th>
<th style="padding: 10px; text-align: center; width: 10%;">角色</th>
<th style="padding: 10px; text-align: left; width: 14%;">邮箱</th>
<th style="padding: 10px; text-align: center; width: 9%;">存储权限</th>
<th style="padding: 10px; text-align: center; width: 9%;">当前存储</th>
<th style="padding: 10px; text-align: center; width: 12%;">配额使用</th>
<th style="padding: 10px; text-align: center; width: 8%;">状态</th>
<th style="padding: 10px; text-align: center; width: 24%;">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="u in adminUsers" :key="u.id" style="border-bottom: 1px solid #eee;">
<td style="padding: 10px;">{{ u.id }}</td>
<td style="padding: 10px; overflow: hidden;">
<div style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" :title="u.username">
{{ u.username }}
</div>
</td>
<td style="padding: 10px; text-align: center; font-size: 12px;">
<span v-if="u.is_admin" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 3px 10px; border-radius: 4px; white-space: nowrap;">
<i class="fas fa-crown"></i> 管理员
</span>
<span v-else style="background: rgba(255,255,255,0.1); color: var(--text-secondary); padding: 3px 10px; border-radius: 4px; white-space: nowrap;">
<i class="fas fa-user"></i> 用户
</span>
</td>
<td style="padding: 10px; font-size: 12px; color: var(--text-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" :title="u.email">{{ u.email }}</td>
<td style="padding: 10px; text-align: center; font-size: 12px;">
<span v-if="u.storage_permission === 'local_only'" style="background: #667eea; color: white; padding: 3px 8px; border-radius: 4px;">仅本地</span>
<span v-else-if="u.storage_permission === 'oss_only'" style="background: #6c757d; color: white; padding: 3px 8px; border-radius: 4px;">仅OSS</span>
<span v-else style="background: #22c55e; color: white; padding: 3px 8px; border-radius: 4px;">用户选择</span>
</td>
<td style="padding: 10px; text-align: center; font-size: 12px;">
<span v-if="u.current_storage_type === 'local'" style="color: #667eea;">
<i class="fas fa-hard-drive"></i> 本地
</span>
<span v-else style="color: #6c757d;">
<i class="fas fa-cloud"></i> OSS
</span>
</td>
<td style="padding: 10px; text-align: center; font-size: 12px;">
<div v-if="u.current_storage_type === 'local'">
<div>{{ formatBytes(u.local_storage_used) }} / {{ formatBytes(u.local_storage_quota) }}</div>
<div style="font-size: 11px; color: var(--text-muted);">
{{ Math.round((u.local_storage_used / u.local_storage_quota) * 100) }}%
</div>
</div>
<span v-else style="color: var(--text-muted);">-</span>
</td>
<td style="padding: 10px; text-align: center;">
<span v-if="u.is_banned" style="color: #ef4444; font-weight: 600;">已封禁</span>
<span v-else-if="!u.is_verified" style="color: #f59e0b; font-weight: 600;">未激活</span>
<span v-else style="color: #22c55e;">正常</span>
</td>
<td style="padding: 10px; text-align: center;">
<div style="display: flex; gap: 3px; justify-content: center; flex-wrap: wrap;">
<button class="btn" style="background: #667eea; color: white; font-size: 11px; padding: 5px 10px;" @click="openEditStorageModal(u)" title="存储设置">
<i class="fas fa-database"></i> 存储
</button>
<button v-if="!u.is_banned" class="btn" style="background: #f59e0b; color: #000; font-size: 11px; padding: 5px 10px;" @click="banUser(u.id, true)">
<i class="fas fa-ban"></i> 封禁
</button>
<button v-else class="btn" style="background: #22c55e; color: white; font-size: 11px; padding: 5px 10px;" @click="banUser(u.id, false)">
<i class="fas fa-check"></i> 解封
</button>
<button v-if="u.oss_config_source !== 'none'" class="btn" style="background: #3b82f6; color: white; font-size: 11px; padding: 5px 10px;" @click="openFileInspection(u)">
<i class="fas fa-folder-open"></i> 文件
</button>
<button class="btn" style="background: #ef4444; color: white; font-size: 11px; padding: 5px 10px;" @click="deleteUser(u.id)">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div><!-- 用户标签页结束 -->
</div><!-- 管理员视图结束 -->
<!-- 忘记密码模态框 -->
<div v-if="showForgotPasswordModal" class="modal-overlay" @mousedown.self="handleModalMouseDown" @mouseup.self="handleModalMouseUp('showForgotPasswordModal', $event)">
<div class="modal-content" @click.stop @focusin="!forgotPasswordCaptchaUrl && refreshForgotPasswordCaptcha()">
<h3 style="margin-bottom: 20px;">忘记密码 - 邮箱重置</h3>
<p style="color: var(--text-secondary); margin-bottom: 15px; font-size: 14px;">
请输入注册邮箱,我们会发送重置链接到您的邮箱
</p>
<div class="form-group">
<label class="form-label">邮箱</label>
<input type="email" class="form-input" v-model="forgotPasswordForm.email" placeholder="请输入注册邮箱" required>
</div>
<div class="form-group">
<label class="form-label">验证码</label>
<div style="display: flex; gap: 10px; align-items: center;">
<input type="text" class="form-input" v-model="forgotPasswordForm.captcha" placeholder="请输入验证码" required style="flex: 1;">
<img v-if="forgotPasswordCaptchaUrl" :src="forgotPasswordCaptchaUrl" @click="refreshForgotPasswordCaptcha" style="height: 44px; cursor: pointer; border-radius: 4px;" title="点击刷新验证码">
</div>
</div>
<div style="display: flex; gap: 10px; margin-top: 20px;">
<button class="btn btn-primary" @click="requestPasswordReset" :disabled="passwordResetting" style="flex: 1;">
<i :class="passwordResetting ? 'fas fa-spinner fa-spin' : 'fas fa-paper-plane'"></i> {{ passwordResetting ? '发送中...' : '发送重置邮件' }}
</button>
<button class="btn btn-secondary" @click="showForgotPasswordModal = false; forgotPasswordForm = {email: '', captcha: ''}; forgotPasswordCaptchaUrl = ''" :disabled="passwordResetting" style="flex: 1;">
<i class="fas fa-times"></i> 取消
</button>
</div>
</div>
</div>
<!-- 邮件重置密码模态框 -->
<div v-if="showResetPasswordModal" class="modal-overlay" @mousedown.self="handleModalMouseDown" @mouseup.self="handleModalMouseUp('showResetPasswordModal', $event)">
<div class="modal-content" @click.stop>
<h3 style="margin-bottom: 20px;">设置新密码</h3>
<p style="color: var(--text-secondary); margin-bottom: 15px; font-size: 14px;">
重置链接已验证,请输入新密码
</p>
<div class="form-group">
<label class="form-label">新密码 (至少6字符)</label>
<input type="password" class="form-input" v-model="resetPasswordForm.new_password" placeholder="输入新密码" minlength="6" required>
</div>
<div style="display: flex; gap: 10px; margin-top: 20px;">
<button class="btn btn-primary" @click="submitResetPassword" :disabled="passwordResetting" style="flex: 1;">
<i :class="passwordResetting ? 'fas fa-spinner fa-spin' : 'fas fa-unlock'"></i> {{ passwordResetting ? '重置中...' : '重置密码' }}
</button>
<button class="btn btn-secondary" @click="showResetPasswordModal = false; resetPasswordForm = {token: '', new_password: ''}" :disabled="passwordResetting" style="flex: 1;">
<i class="fas fa-times"></i> 取消
</button>
</div>
</div>
</div>
<!-- 文件审查模态框 -->
<div v-if="showFileInspectionModal" class="modal-overlay" @mousedown.self="handleModalMouseDown" @mouseup.self="handleModalMouseUp('showFileInspectionModal', $event)">
<div class="modal-content" @click.stop style="max-width: 900px; max-height: 80vh; overflow-y: auto;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h3 style="margin: 0;">
<i class="fas fa-eye"></i> 文件审查 - {{ inspectionUser?.username }}
<span style="background: #f59e0b; color: #000; padding: 4px 8px; border-radius: 4px; font-size: 12px; margin-left: 10px;">只读模式</span>
</h3>
<button class="btn-icon" @click="showFileInspectionModal = false" style="font-size: 20px;">
<i class="fas fa-times"></i>
</button>
</div>
<!-- 路径导航和视图切换 -->
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; gap: 15px;">
<div style="background: rgba(255,255,255,0.03); padding: 10px; border-radius: 6px; display: flex; align-items: center; gap: 10px; flex: 1;">
<button class="btn-icon" @click="navigateInspectionToRoot" title="返回根目录">
<i class="fas fa-home"></i>
</button>
<button class="btn-icon" @click="navigateInspectionUp" :disabled="inspectionPath === '/'" title="上一级">
<i class="fas fa-arrow-up"></i>
</button>
<span style="flex: 1; color: var(--text-secondary); font-family: monospace;">{{ inspectionPath }}</span>
</div>
<div style="display: flex; gap: 10px;">
<button class="btn" :class="inspectionViewMode === 'grid' ? 'btn-primary' : 'btn-secondary'" @click="inspectionViewMode = 'grid'">
<i class="fas fa-th-large"></i> 大图标
</button>
<button class="btn" :class="inspectionViewMode === 'list' ? 'btn-primary' : 'btn-secondary'" @click="inspectionViewMode = 'list'">
<i class="fas fa-list"></i> 列表
</button>
</div>
</div>
<!-- 加载中 -->
<div v-if="inspectionLoading" class="loading">
<div class="spinner"></div>
<p>加载中...</p>
</div>
<!-- 文件列表 -->
<div v-else>
<p v-if="inspectionFiles.length === 0" class="empty-hint">文件夹是空的</p>
<!-- 大图标视图 -->
<div v-else-if="inspectionViewMode === 'grid'" class="file-grid">
<div v-for="file in inspectionFiles" :key="file.name" class="file-grid-item" @dblclick="handleInspectionFileClick(file)">
<div class="file-icon">
<i v-if="file.isDirectory" class="fas fa-folder" style="font-size: 64px; color: #FFC107;"></i>
<i v-else-if="file.name.match(/\.(jpg|jpeg|png|gif|bmp|svg)$/i)" class="fas fa-file-image" style="font-size: 64px; color: #4CAF50;"></i>
<i v-else-if="file.name.match(/\.(mp4|avi|mov|wmv|flv|mkv)$/i)" class="fas fa-file-video" style="font-size: 64px; color: #9C27B0;"></i>
<i v-else-if="file.name.match(/\.(mp3|wav|flac|aac|ogg)$/i)" class="fas fa-file-audio" style="font-size: 64px; color: #FF5722;"></i>
<i v-else-if="file.name.match(/\.(pdf)$/i)" class="fas fa-file-pdf" style="font-size: 64px; color: #F44336;"></i>
<i v-else-if="file.name.match(/\.(doc|docx)$/i)" class="fas fa-file-word" style="font-size: 64px; color: #2196F3;"></i>
<i v-else-if="file.name.match(/\.(xls|xlsx)$/i)" class="fas fa-file-excel" style="font-size: 64px; color: #4CAF50;"></i>
<i v-else-if="file.name.match(/\.(zip|rar|7z|tar|gz)$/i)" class="fas fa-file-archive" style="font-size: 64px; color: #795548;"></i>
<i v-else class="fas fa-file" style="font-size: 64px; color: #9E9E9E;"></i>
</div>
<div class="file-name" :title="file.name">{{ file.name }}</div>
<div class="file-size">{{ file.isDirectory ? '文件夹' : file.sizeFormatted }}</div>
</div>
</div>
<!-- 列表视图 -->
<div v-else class="file-list">
<table style="width: 100%; border-collapse: collapse; table-layout: fixed;">
<thead>
<tr style="background: rgba(255,255,255,0.05);">
<th style="padding: 12px; text-align: left; width: 50%;">文件名</th>
<th style="padding: 12px; text-align: left; width: 25%;">大小</th>
<th style="padding: 12px; text-align: left; width: 25%;">修改时间</th>
</tr>
</thead>
<tbody>
<tr v-for="file in inspectionFiles" :key="file.name"
style="border-bottom: 1px solid #eee; cursor: pointer;"
@dblclick="handleInspectionFileClick(file)"
@mouseover="$event.currentTarget.style.background='#f9f9f9'"
@mouseout="$event.currentTarget.style.background='white'">
<td style="padding: 10px;">
<div style="display: flex; align-items: center; gap: 10px; overflow: hidden;">
<i v-if="file.isDirectory" class="fas fa-folder" style="font-size: 20px; color: #FFC107; flex-shrink: 0;"></i>
<i v-else-if="file.name.match(/\.(jpg|jpeg|png|gif|bmp|svg)$/i)" class="fas fa-file-image" style="font-size: 20px; color: #4CAF50;"></i>
<i v-else-if="file.name.match(/\.(mp4|avi|mov|wmv|flv|mkv)$/i)" class="fas fa-file-video" style="font-size: 20px; color: #9C27B0;"></i>
<i v-else-if="file.name.match(/\.(mp3|wav|flac|aac|ogg)$/i)" class="fas fa-file-audio" style="font-size: 20px; color: #FF5722; flex-shrink: 0;"></i>
<i v-else-if="file.name.match(/\.(pdf)$/i)" class="fas fa-file-pdf" style="font-size: 20px; color: #F44336; flex-shrink: 0;"></i>
<i v-else-if="file.name.match(/\.(doc|docx)$/i)" class="fas fa-file-word" style="font-size: 20px; color: #2196F3; flex-shrink: 0;"></i>
<i v-else-if="file.name.match(/\.(xls|xlsx)$/i)" class="fas fa-file-excel" style="font-size: 20px; color: #4CAF50; flex-shrink: 0;"></i>
<i v-else-if="file.name.match(/\.(zip|rar|7z|tar|gz)$/i)" class="fas fa-file-archive" style="font-size: 20px; color: #795548; flex-shrink: 0;"></i>
<i v-else class="fas fa-file" style="font-size: 20px; color: #9E9E9E; flex-shrink: 0;"></i>
<span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; min-width: 0;" :title="file.name">{{ file.name }}</span>
</div>
</td>
<td style="padding: 10px; color: var(--text-secondary);">{{ file.isDirectory ? '-' : file.sizeFormatted }}</td>
<td style="padding: 10px; color: var(--text-secondary);">{{ formatDate(file.modifiedAt) }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<div style="margin-top: 15px; padding: 10px; background: rgba(245, 158, 11, 0.15); border-radius: 6px; color: #fbbf24;">
<i class="fas fa-info-circle"></i> 只读模式:双击文件夹可进入,无法下载、修改或删除文件
</div>
</div>
</div>
<!-- Toast 通知容器 -->
<div style="position: fixed; top: 20px; right: 20px; z-index: 2000; max-width: 350px;">
<div v-for="toast in toasts" :key="toast.id"
:style="{
background: toast.type === 'error' ? '#f8d7da' : toast.type === 'success' ? '#d4edda' : '#d1ecf1',
color: toast.type === 'error' ? '#721c24' : toast.type === 'success' ? '#155724' : '#0c5460',
padding: '15px',
borderRadius: '8px',
marginBottom: '10px',
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
display: 'flex',
alignItems: 'start',
gap: '12px',
animation: toast.hiding ? 'slideOut 0.5s ease-out forwards' : 'slideIn 0.5s ease-out',
opacity: toast.hiding ? 0 : 1,
transform: toast.hiding ? 'translateX(400px)' : 'translateX(0)'
}">
<i :class="toast.icon" style="font-size: 20px; margin-top: 2px;"></i>
<div style="flex: 1;">
<div style="font-weight: 600; margin-bottom: 4px;">{{ toast.title }}</div>
<div style="font-size: 14px;">{{ toast.message }}</div>
</div>
</div>
</div>
<!-- 上传进度条 -->
<div v-if="uploadProgress > 0 && uploadProgress < 100"
style="position: fixed; bottom: 20px; right: 20px; z-index: 2000; width: 350px; background: var(--bg-secondary); backdrop-filter: blur(20px); border: 1px solid var(--glass-border); border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.3); padding: 20px; animation: slideIn 0.3s ease-out;">
<div style="display: flex; align-items: center; gap: 12px; margin-bottom: 12px;">
<i class="fas fa-cloud-upload-alt" style="font-size: 24px; color: var(--accent-1);"></i>
<div style="flex: 1;">
<div style="font-weight: 600; color: var(--text-primary); margin-bottom: 4px;">正在上传文件</div>
<div style="font-size: 13px; color: var(--text-secondary);">{{ uploadingFileName }}</div>
<div v-if="totalBytes > 0" style="font-size: 12px; color: var(--text-muted); margin-top: 2px;"> {{ formatFileSize(uploadedBytes) }} / {{ formatFileSize(totalBytes) }} </div>
</div>
<div style="font-size: 20px; font-weight: 700; color: var(--accent-1);">{{ uploadProgress }}%</div>
</div>
<div style="width: 100%; height: 8px; background: rgba(102,126,234,0.2); border-radius: 4px; overflow: hidden;">
<div :style="{
width: uploadProgress + '%',
height: '100%',
background: 'linear-gradient(90deg, #667eea 0%, #764ba2 100%)',
transition: 'width 0.3s ease',
borderRadius: '4px'
}"></div>
</div>
</div>
<!-- 右键菜单 -->
<div v-if="showContextMenu" class="context-menu" :style="{
left: contextMenuX + 'px',
top: contextMenuY + 'px'
}" @click.stop>
<div v-if="isPreviewable(contextMenuFile)" class="context-menu-item" @click="contextMenuAction('preview')">
<i class="fas fa-eye"></i> 预览
</div>
<!-- 文件夹不显示下载和分享按钮 -->
<div v-if="!contextMenuFile.isDirectory" class="context-menu-item" @click="contextMenuAction('download')">
<i class="fas fa-download"></i> 下载
</div>
<div class="context-menu-item" @click="contextMenuAction('rename')">
<i class="fas fa-edit"></i> 重命名
</div>
<div v-if="contextMenuFile.isDirectory" class="context-menu-item" @click="contextMenuAction('info')">
<i class="fas fa-info-circle"></i> 查看详情
</div>
<div class="context-menu-item" @click="contextMenuAction('share')">
<i class="fas fa-share"></i> 分享
</div>
<div class="context-menu-divider"></div>
<div class="context-menu-item context-menu-item-danger" @click="contextMenuAction('delete')">
<i class="fas fa-trash"></i> 删除
</div>
</div>
<!-- 管理员:编辑用户存储权限模态框 -->
<div v-if="showEditStorageModal" class="modal-overlay" @mousedown.self="handleModalMouseDown" @mouseup.self="handleModalMouseUp('showEditStorageModal', $event)">
<div class="modal-content" @click.stop>
<h3 style="margin-bottom: 20px;">
<i class="fas fa-database"></i> 存储权限设置 - {{ editStorageForm.username }}
</h3>
<div class="form-group">
<label class="form-label">存储权限</label>
<select class="form-input" v-model="editStorageForm.storage_permission">
<option value="local_only">仅本地存储</option>
<option value="oss_only">仅OSS存储</option>
<option value="user_choice">用户选择</option>
</select>
<small style="color: var(--text-secondary); font-size: 12px; margin-top: 5px; display: block;">
仅本地:用户只能使用本地存储 | 仅OSS用户只能使用OSS | 用户选择:用户可自由切换
</small>
</div>
<div class="form-group">
<label class="form-label">本地存储配额</label>
<div style="display: flex; gap: 10px;">
<input type="number" class="form-input" v-model.number="editStorageForm.local_storage_quota_value" min="1" max="102400" step="1" style="flex: 1;">
<select class="form-input" v-model="editStorageForm.quota_unit" style="width: 100px;">
<option value="MB">MB</option>
<option value="GB">GB</option>
</select>
</div>
<small style="color: var(--text-secondary); font-size: 12px; margin-top: 5px; display: block;">
配额范围: 1MB - 100GB | 建议: 大配额使用GB小配额使用MB
</small>
</div>
<div style="padding: 12px; background: rgba(255,255,255,0.03); border-radius: 6px; margin-bottom: 20px;">
<div style="font-size: 13px; color: var(--text-secondary); line-height: 1.6;">
<strong style="color: var(--text-primary);">配额说明:</strong><br>
• 默认配额: 1GB<br>
• 当前配额: {{ editStorageForm.local_storage_quota_value }} {{ editStorageForm.quota_unit }}
({{ editStorageForm.quota_unit === 'GB' ? (editStorageForm.local_storage_quota_value * 1024).toFixed(0) : editStorageForm.local_storage_quota_value }} MB)<br>
• 配额仅影响本地存储OSS存储不受此限制
</div>
</div>
<div style="display: flex; gap: 10px; margin-top: 20px;">
<button class="btn btn-primary" @click="updateUserStorage" style="flex: 1;">
<i class="fas fa-save"></i> 保存设置
</button>
<button class="btn btn-secondary" @click="showEditStorageModal = false" style="flex: 1;">
<i class="fas fa-times"></i> 取消
</button>
</div>
</div>
</div>
<!-- 图片预览模态框 -->
<div v-if="showImageViewer" class="modal-overlay" @click="closeMediaViewer">
<div class="media-viewer-content" @click.stop>
<div class="media-viewer-header">
<span class="media-viewer-title">{{ currentMediaName }}</span>
<div class="media-viewer-actions">
<button class="media-viewer-btn" @click="downloadCurrentMedia" title="下载">
<i class="fas fa-download"></i>
</button>
<button class="media-viewer-btn" @click="closeMediaViewer" title="关闭">
<i class="fas fa-times"></i>
</button>
</div>
</div>
<div class="media-viewer-body">
<img :src="currentMediaUrl" :alt="currentMediaName" class="media-viewer-image">
</div>
</div>
</div>
<!-- 视频播放器模态框 -->
<div v-if="showVideoPlayer" class="modal-overlay" @click="closeMediaViewer">
<div class="media-viewer-content" @click.stop>
<div class="media-viewer-header">
<span class="media-viewer-title">{{ currentMediaName }}</span>
<div class="media-viewer-actions">
<button class="media-viewer-btn" @click="downloadCurrentMedia" title="下载">
<i class="fas fa-download"></i>
</button>
<button class="media-viewer-btn" @click="closeMediaViewer" title="关闭">
<i class="fas fa-times"></i>
</button>
</div>
</div>
<div class="media-viewer-body">
<video controls :src="currentMediaUrl" class="media-viewer-video">
您的浏览器不支持视频播放
</video>
</div>
</div>
</div>
<!-- 音频播放器模态框 -->
<div v-if="showAudioPlayer" class="modal-overlay" @click="closeMediaViewer">
<div class="media-viewer-content audio-player" @click.stop>
<div class="media-viewer-header">
<span class="media-viewer-title">{{ currentMediaName }}</span>
<div class="media-viewer-actions">
<button class="media-viewer-btn" @click="downloadCurrentMedia" title="下载">
<i class="fas fa-download"></i>
</button>
<button class="media-viewer-btn" @click="closeMediaViewer" title="关闭">
<i class="fas fa-times"></i>
</button>
</div>
</div>
<div class="media-viewer-body">
<div class="audio-player-icon">
<i class="fas fa-music"></i>
</div>
<audio controls :src="currentMediaUrl" class="media-viewer-audio">
您的浏览器不支持音频播放
</audio>
</div>
</div>
</div>
</template>
</div>
<style>
@keyframes slideIn {
from {
opacity: 0;
transform: translateX(400px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes slideOut {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(400px);
}
}
/* 右键菜单样式 */
.context-menu {
position: fixed;
background: var(--bg-secondary);
backdrop-filter: blur(20px);
border: 1px solid var(--glass-border);
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
min-width: 160px;
z-index: 10000;
overflow: hidden;
animation: contextMenuFadeIn 0.15s ease-out;
}
@keyframes contextMenuFadeIn {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
.context-menu-item {
padding: 12px 16px;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 12px;
color: var(--text-primary);
font-size: 14px;
}
.context-menu-item:hover {
background: rgba(102, 126, 234, 0.15);
color: var(--accent-1);
}
.context-menu-item i {
width: 16px;
text-align: center;
color: var(--accent-1);
}
.context-menu-item-danger {
color: #ef4444;
}
.context-menu-item-danger i {
color: #ef4444;
}
.context-menu-item-danger:hover {
background: rgba(239, 68, 68, 0.15);
color: #ef4444;
}
.context-menu-divider {
height: 1px;
background: var(--glass-border);
margin: 4px 0;
}
/* 移动端优化 */
@media (max-width: 768px) {
.context-menu {
min-width: 180px;
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
}
.context-menu-item {
padding: 14px 18px;
font-size: 15px;
}
}
/* 媒体预览器样式 */
.media-viewer-content {
background: var(--bg-secondary);
border: 1px solid var(--glass-border);
border-radius: 16px;
max-width: 90vw;
max-height: 90vh;
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow: 0 20px 60px rgba(0,0,0,0.5);
}
.media-viewer-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 20px;
background: linear-gradient(135deg, var(--accent-1), var(--accent-2));
color: white;
}
.media-viewer-title {
font-size: 16px;
font-weight: 600;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-right: 15px;
}
.media-viewer-actions {
display: flex;
gap: 10px;
}
.media-viewer-btn {
background: rgba(255,255,255,0.2);
border: none;
color: white;
width: 36px;
height: 36px;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
font-size: 16px;
}
.media-viewer-btn:hover {
background: rgba(255,255,255,0.3);
}
.media-viewer-body {
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
background: rgba(255,255,255,0.03);
flex: 1;
overflow: auto;
}
.media-viewer-image {
max-width: 100%;
max-height: 75vh;
object-fit: contain;
border-radius: 8px;
}
.media-viewer-video {
max-width: 100%;
max-height: 75vh;
border-radius: 8px;
}
.media-viewer-audio {
width: 100%;
max-width: 500px;
}
.audio-player .media-viewer-body {
flex-direction: column;
gap: 30px;
}
.audio-player-icon {
font-size: 80px;
color: #667eea;
}
.audio-player-icon i {
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.6;
}
}
/* 移动端适配 */
@media (max-width: 768px) {
.media-viewer-content {
max-width: 95vw;
max-height: 95vh;
}
.media-viewer-header {
padding: 12px 15px;
}
.media-viewer-title {
font-size: 14px;
}
.media-viewer-btn {
width: 32px;
height: 32px;
font-size: 14px;
}
.media-viewer-body {
padding: 15px;
}
.media-viewer-image,
.media-viewer-video {
max-height: 70vh;
}
.audio-player-icon {
font-size: 64px;
}
}
/* 拖拽上传样式 */
.files-container {
position: relative;
min-height: 200px;
}
.drag-drop-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(102, 126, 234, 0.1);
backdrop-filter: blur(2px);
border: 3px dashed #667eea;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
pointer-events: none;
}
.drag-drop-content {
text-align: center;
padding: 40px;
}
.files-container.drag-over {
background: rgba(102, 126, 234, 0.05);
border-radius: 12px;
}
/* 拖拽上传样式 */
.files-container {
position: relative;
min-height: 200px;
}
.drag-drop-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(102, 126, 234, 0.1);
backdrop-filter: blur(2px);
border: 3px dashed #667eea;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
pointer-events: none;
}
.drag-drop-content {
text-align: center;
padding: 40px;
}
.files-container.drag-over {
background: rgba(102, 126, 234, 0.05);
border-radius: 12px;
}
</style>
<script src="app.js?v=20251127003"></script>
</body>
</html>