- 添加CSS变量系统(背景色、边框色、文字颜色、强调色) - 页面背景改为深色渐变效果 - 卡片、弹窗使用毛玻璃效果(backdrop-filter blur) - 导航栏改为暗色玻璃样式 - 表单、按钮、表格全部适配暗色主题 - 添加自定义滚动条样式 - 统一警告/成功/错误提示框为半透明风格 - 全局替换浅色背景和文字颜色 与index.html保持一致的暗色毛玻璃设计风格 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
3064 lines
138 KiB
HTML
3064 lines
138 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<!-- 开发者工具检测 - 必须放在最前面,优先执行 -->
|
||
<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;
|
||
}
|
||
|
||
/* 防止 Vue 初始化前显示原始模板 */
|
||
[v-cloak] { display: none !important; }
|
||
|
||
* { 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);
|
||
}
|
||
|
||
/* ========== 危险按钮 ========== */
|
||
.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);
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div id="app" v-cloak>
|
||
<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;">
|
||
邮箱未验证?<a style="color:#667eea; cursor: pointer;" @click="resendVerification">点击重发激活邮件</a>
|
||
</div>
|
||
<div style="text-align: right; margin-bottom: 15px;">
|
||
<a @click="showForgotPasswordModal = true" style="color: #667eea; cursor: pointer; font-size: 14px; text-decoration: none;">
|
||
忘记密码?
|
||
</a>
|
||
</div>
|
||
<button type="submit" class="btn btn-primary">
|
||
<i class="fas fa-right-to-bracket"></i> 登录
|
||
</button>
|
||
</form>
|
||
<form v-else @submit.prevent="handleRegister">
|
||
<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>
|
||
<button type="submit" class="btn btn-primary">
|
||
<i class="fas fa-user-plus"></i> 注册
|
||
</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;">
|
||
<!-- 本地存储:显示网页上传按钮 -->
|
||
<button v-if="storageType === 'local'" class="btn btn-primary" @click="$refs.fileUploadInput.click()">
|
||
<i class="fas fa-upload"></i> 上传文件
|
||
</button>
|
||
<button v-if="storageType === 'local'" class="btn btn-primary" @click="showCreateFolderModal = true">
|
||
<i class="fas fa-folder-plus"></i> 新建文件夹
|
||
</button>
|
||
<!-- SFTP存储:显示下载上传工具按钮 -->
|
||
<button v-else class="btn btn-primary" @click="downloadUploadTool" :disabled="downloadingTool">
|
||
<i :class="downloadingTool ? 'fas fa-spinner fa-spin' : 'fas fa-download'"></i>
|
||
{{ downloadingTool ? '生成中...' : '下载上传工具' }}
|
||
</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)" @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="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: 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"
|
||
style="border-bottom: 1px solid #eee; cursor: pointer;"
|
||
@click="handleFileClick(file)"
|
||
@contextmenu.prevent="showFileContextMenu(file, $event)"
|
||
@touchstart="handleLongPressStart(file, $event)"
|
||
@touchend="handleLongPressEnd"
|
||
@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;">
|
||
<!-- 图片缩略图 -->
|
||
<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="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.modifiedTime) }}</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
<!-- 重命名模态框 -->
|
||
<div v-if="showRenameModal" class="modal-overlay" @mousedown.self="handleModalMouseDown" @mouseup.self="handleModalMouseUp('showRenameModal')">
|
||
<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')">
|
||
<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()" style="flex: 1;">
|
||
<i class="fas fa-check"></i> 创建
|
||
</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')">
|
||
<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')">
|
||
<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">{{ shareResult.share_url }}</a>
|
||
<div v-if="shareResult.expires_at" style="margin-top: 10px; padding-top: 10px; border-top: 1px solid #c3e6cb;">
|
||
<strong>到期时间:</strong>
|
||
<span :style="{color: isExpiringSoon(shareResult.expires_at) ? '#ffc107' : '#28a745'}"><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 #c3e6cb;">
|
||
<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()" style="flex: 1;">
|
||
<i class="fas fa-share"></i> 创建分享
|
||
</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')">
|
||
<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">{{ shareResult.share_url }}</a>
|
||
<div v-if="shareResult.expires_at" style="margin-top: 10px; padding-top: 10px; border-top: 1px solid #c3e6cb;">
|
||
<strong>到期时间:</strong>
|
||
<span :style="{color: isExpiringSoon(shareResult.expires_at) ? '#ffc107' : '#28a745'}"><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 #c3e6cb;">
|
||
<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()" style="flex: 1;">
|
||
<i class="fas fa-share"></i> 创建分享
|
||
</button>
|
||
<button class="btn btn-secondary" @click="showShareFileModal = false; shareResult = null" style="flex: 1;">
|
||
<i class="fas fa-times"></i> 关闭
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- SFTP 配置引导弹窗 -->
|
||
<div v-if="showSftpGuideModal" class="modal-overlay" @mousedown.self="handleModalMouseDown" @mouseup.self="handleModalMouseUp('showSftpGuideModal')">
|
||
<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-server" style="font-size: 20px;"></i>
|
||
<h3 style="margin: 0; font-size: 20px;">切换到 SFTP 存储</h3>
|
||
</div>
|
||
<p style="margin: 8px 0 0 0; opacity: 0.9; font-size: 14px;">先配置连接信息,再切换到你的专属 SFTP 空间。</p>
|
||
</div>
|
||
<div style="padding: 18px;">
|
||
<p style="color: #4b5563; line-height: 1.6; margin-bottom: 16px;">
|
||
我们会在你填写完成后再切换,确保过程平滑无干扰。
|
||
</p>
|
||
<div style="display: flex; gap: 10px; justify-content: flex-end;">
|
||
<button class="btn btn-secondary" @click="closeSftpGuideModal">稍后再说</button>
|
||
<button class="btn btn-primary" @click="proceedSftpGuide">
|
||
<i class="fas fa-tools"></i> 去配置 SFTP
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- SFTP 配置弹窗 -->
|
||
<div v-if="showSftpConfigModal" class="modal-overlay" @mousedown.self="handleModalMouseDown" @mouseup.self="handleModalMouseUp('showSftpConfigModal')">
|
||
<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;">配置 SFTP 存储</h3>
|
||
<p style="margin: 0; color: var(--text-muted); font-size: 13px;">填写连接信息或导入 .inf 配置,保存后即可切换到 SFTP 模式。</p>
|
||
</div>
|
||
<button class="btn btn-secondary" style="padding: 6px 10px;" @click="closeSftpConfigModal">
|
||
<i class="fas fa-times"></i>
|
||
</button>
|
||
</div>
|
||
|
||
<div style="display: grid; grid-template-columns: 1fr; gap: 14px;">
|
||
<div style="border: 1px dashed var(--glass-border); border-radius: 12px; padding: 16px; background: rgba(255,255,255,0.03); text-align: center; cursor: pointer; transition: all .3s;"
|
||
@click="$refs.configFileInput.click()"
|
||
@dragover.prevent="$event.currentTarget.style.background='#eef2ff'"
|
||
@dragleave.prevent="$event.currentTarget.style.background='#f8fafc'"
|
||
@drop.prevent="handleConfigFileDrop">
|
||
<i class="fas fa-cloud-upload-alt" style="font-size: 36px; color: #667eea; margin-bottom: 8px;"></i>
|
||
<div style="font-weight: 600; color: var(--text-primary);">导入配置文件</div>
|
||
<div style="color: var(--text-muted); font-size: 13px; margin-top: 4px;">点击选择或拖拽 .inf 文件</div>
|
||
<input type="file" accept=".inf" @change="handleConfigFileUpload" ref="configFileInput" style="display: none;">
|
||
</div>
|
||
|
||
<form @submit.prevent="updateFtpConfig" style="display: grid; gap: 12px;">
|
||
<div class="form-group">
|
||
<label class="form-label">主机地址</label>
|
||
<input type="text" class="form-input" v-model="ftpConfigForm.ftp_host" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">端口</label>
|
||
<input type="number" class="form-input" v-model="ftpConfigForm.ftp_port" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">用户名</label>
|
||
<input type="text" class="form-input" v-model="ftpConfigForm.ftp_user" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">密码 (留空保留现有密码)</label>
|
||
<input type="password" class="form-input" v-model="ftpConfigForm.ftp_password" placeholder="留空保留现有密码">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">HTTP下载基础URL (可选)</label>
|
||
<input type="text" class="form-input" v-model="ftpConfigForm.http_download_base_url" placeholder="例如: http://example.com/files">
|
||
<small style="color: var(--text-secondary); font-size: 12px; margin-top: 5px; display: block;">
|
||
配置后可通过 HTTP 直接下载,例如: 基础URL/文件路径。
|
||
</small>
|
||
</div>
|
||
<div style="display: flex; gap: 10px; justify-content: flex-end; margin-top: 4px;">
|
||
<button type="button" class="btn btn-secondary" @click="closeSftpConfigModal">取消</button>
|
||
<button type="submit" class="btn btn-primary">
|
||
<i class="fas fa-save"></i> 保存配置
|
||
</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: linear-gradient(135deg, #f3f5ff 0%, #eef7ff 100%); padding: 22px; border-radius: 14px; box-shadow: 0 10px 30px rgba(102,126,234,0.12); border: 1px solid #e3e9ff;">
|
||
<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 === 'sftp' ? 'SFTP 存储' : '本地存储' }}...
|
||
</div>
|
||
<div v-else style="color: var(--text-secondary); font-size: 13px;">本地存储适合快速读写,SFTP 适合独立服务器空间</div>
|
||
</div>
|
||
|
||
<div style="margin-top: 16px; background: white; 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: white; border: 1px solid var(--glass-border); border-radius: 12px; padding: 16px; box-shadow: 0 6px 20px rgba(0,0,0,0.04); 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"></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: white; border: 1px solid var(--glass-border); border-radius: 12px; padding: 16px; box-shadow: 0 6px 20px rgba(0,0,0,0.04); 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-server"></i> SFTP 存储
|
||
</div>
|
||
<span v-if="storageType === 'sftp'" style="font-size: 12px; color: #4b5fc9; 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?.has_ftp_config" style="font-size: 13px; color: var(--text-primary); margin-bottom: 10px;">
|
||
已配置: {{ user.ftp_host }}:{{ user.ftp_port }}
|
||
</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> 先填写 SFTP 连接信息再切换
|
||
</div>
|
||
<!-- SFTP空间使用统计(user_choice模式) -->
|
||
<div v-if="user?.has_ftp_config" 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="loadSftpUsage()"
|
||
:disabled="sftpUsageLoading">
|
||
<i :class="sftpUsageLoading ? 'fas fa-sync-alt fa-spin' : 'fas fa-sync-alt'"></i>
|
||
</button>
|
||
</div>
|
||
<div v-if="sftpUsageLoading && !sftpUsage" style="text-align: center; color: #667eea; font-size: 12px;">
|
||
<i class="fas fa-spinner fa-spin"></i> 统计中...
|
||
</div>
|
||
<div v-else-if="sftpUsageError" style="font-size: 12px; color: #ef4444;">
|
||
<i class="fas fa-exclamation-triangle"></i> {{ sftpUsageError }}
|
||
</div>
|
||
<div v-else-if="sftpUsage" style="font-size: 13px; font-weight: 600; color: #4b5fc9;">
|
||
{{ sftpUsage.totalSizeFormatted }}
|
||
<span style="font-weight: 400; color: var(--text-muted); font-size: 12px;">({{ sftpUsage.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?.has_ftp_config ? 'btn-primary' : 'btn-secondary'"
|
||
style="width: 100%; border-radius: 10px;"
|
||
:disabled="storageType === 'sftp' || storageSwitching"
|
||
@click="switchStorage('sftp')">
|
||
<i class="fas fa-random"></i>
|
||
{{ user?.has_ftp_config ? '切到 SFTP 存储' : '去配置 SFTP' }}
|
||
</button>
|
||
<div style="margin-top: 8px; text-align: center;">
|
||
<a style="color: #4b5fc9; font-size: 13px; text-decoration: none; cursor: pointer;" @click.prevent="openSftpConfigModal">
|
||
<i class="fas fa-tools"></i> 配置 / 修改 SFTP
|
||
</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>
|
||
本地存储速度快但受配额限制;SFTP 需先配置连接,切换过程中可继续查看文件列表。
|
||
</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> 管理员已将您的存储权限设置为"仅本地存储",您的文件存储在服务器本地,速度快但有配额限制。如需使用SFTP存储,请联系管理员修改权限设置。
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- SFTP 概览 / 配置入口 - 仅SFTP权限 -->
|
||
<div v-if="user && !user.is_admin && storagePermission === 'sftp_only'" style="margin-bottom: 40px;">
|
||
<h3 style="margin-bottom: 20px;">
|
||
<i class="fas fa-server"></i> SFTP存储
|
||
</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>
|
||
仅 SFTP 模式
|
||
</div>
|
||
<div style="color: var(--text-secondary); font-size: 13px; margin-top: 6px;">
|
||
{{ user.has_ftp_config ? '已配置服务器,可正常使用 SFTP 存储。' : '还未配置 SFTP,请先填写连接信息。' }}
|
||
</div>
|
||
</div>
|
||
<button class="btn btn-primary" @click="openSftpConfigModal()" style="border-radius: 10px;">
|
||
<i class="fas fa-tools"></i> 配置 / 修改 SFTP
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 服务器信息 -->
|
||
<div v-if="user.has_ftp_config" style="margin-bottom: 12px; padding: 12px; background: white; 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-server" style="color: #667eea;"></i> 服务器信息
|
||
</div>
|
||
<div style="color: var(--text-secondary); font-size: 14px;">
|
||
{{ user.ftp_host }}:{{ user.ftp_port }}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- SFTP空间使用统计 -->
|
||
<div v-if="user.has_ftp_config" style="margin-bottom: 12px; padding: 12px; background: white; 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="loadSftpUsage()"
|
||
:disabled="sftpUsageLoading">
|
||
<i :class="sftpUsageLoading ? 'fas fa-sync-alt fa-spin' : 'fas fa-sync-alt'"></i>
|
||
{{ sftpUsageLoading ? '统计中...' : '刷新' }}
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 加载中 -->
|
||
<div v-if="sftpUsageLoading && !sftpUsage" 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;">正在统计 SFTP 空间使用情况...</div>
|
||
<div style="margin-top: 4px; font-size: 12px; color: var(--text-muted);">(文件较多时可能需要一些时间)</div>
|
||
</div>
|
||
|
||
<!-- 错误提示 -->
|
||
<div v-else-if="sftpUsageError" 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> {{ sftpUsageError }}
|
||
</div>
|
||
|
||
<!-- 统计结果 -->
|
||
<div v-else-if="sftpUsage" 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;">{{ sftpUsage.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;">{{ sftpUsage.fileCount }}</div>
|
||
<div style="font-size: 12px; color: var(--text-muted); margin-top: 4px;">文件数</div>
|
||
</div>
|
||
<div style="text-align: center; padding: 12px; background: rgba(245, 158, 11, 0.1); border-radius: 10px; border: 1px solid rgba(245,158,11,0.3);">
|
||
<div style="font-size: 20px; font-weight: 700; color: #f59e0b;">{{ sftpUsage.dirCount }}</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>
|
||
点击"刷新"按钮统计 SFTP 空间使用情况
|
||
</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>
|
||
数据存储在你的 SFTP 服务器上,如需切换回本地请联系管理员调整权限。
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 账号设置 -->
|
||
<h3 style="margin: 40px 0 20px 0;">账号设置</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">
|
||
<i class="fas fa-save"></i> 修改用户名
|
||
</button>
|
||
</form>
|
||
|
||
<!-- 所有用户都可以改密码 -->
|
||
<form @submit.prevent="changePassword">
|
||
<div class="form-group">
|
||
<div class="form-group">
|
||
<label class="form-label">当前密码</label>
|
||
<input type="password" class="form-input" v-model="changePasswordForm.current_password" placeholder="输入当前密码" required>
|
||
</div>
|
||
<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">
|
||
<i class="fas fa-key"></i> 修改密码
|
||
</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 分享视图 -->
|
||
<div v-if="isLoggedIn && currentView === 'shares'" class="main-container">
|
||
<div class="card">
|
||
<!-- 标题和工具栏 -->
|
||
<div style="margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center;">
|
||
<h3 style="margin: 0;">我的分享</h3>
|
||
<div style="display: flex; gap: 10px;">
|
||
<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>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 空状态 -->
|
||
<div v-if="shares.length === 0" class="alert alert-info">
|
||
还没有创建任何分享
|
||
</div>
|
||
|
||
<!-- 大图标视图 -->
|
||
<div v-else-if="shareViewMode === 'grid'" class="file-grid">
|
||
<div v-for="share in shares" :key="share.id" class="file-grid-item">
|
||
<div class="file-icon">
|
||
<i class="fas fa-share-alt" style="font-size: 64px; color: #667eea;"></i>
|
||
</div>
|
||
<div class="file-name" :title="share.share_path">{{ share.share_path }}</div>
|
||
<div class="file-size" style="font-size: 12px; color: var(--text-secondary);">
|
||
访问: {{ share.view_count }} | 下载: {{ share.download_count }}
|
||
</div>
|
||
<div class="file-actions">
|
||
<button class="btn-icon" @click.stop="window.open(share.share_url, '_blank')" title="打开分享">
|
||
<i class="fas fa-external-link-alt"></i>
|
||
</button>
|
||
<button class="btn-icon" @click.stop="copyShareLink(share.share_url)" title="复制链接">
|
||
<i class="fas fa-copy"></i>
|
||
</button>
|
||
<button class="btn-icon" style="color: #ef4444;" @click.stop="deleteShare(share.id)" title="删除">
|
||
<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 shares" :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="adminTab = 'monitor'"
|
||
: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>
|
||
<button @click="adminTab = 'tools'"
|
||
:style="{ padding: '15px 25px', border: 'none', background: adminTab === 'tools' ? '#667eea' : 'transparent', color: adminTab === 'tools' ? 'white' : '#666', cursor: 'pointer', fontWeight: '600', fontSize: '14px', borderRadius: adminTab === 'tools' ? '8px 8px 0 0' : '0', transition: 'all 0.2s' }">
|
||
<i class="fas fa-tools"></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;">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;">使用 SSL(465 通常需要)</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>
|
||
</div><!-- 设置标签页结束 -->
|
||
|
||
<!-- ========== 监控标签页 ========== -->
|
||
<div v-show="adminTab === 'monitor'">
|
||
<!-- 健康检测 -->
|
||
<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 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: 10px;">
|
||
<button class="btn btn-secondary" @click="cleanupLogs" title="清理90天前的日志">
|
||
<i class="fas fa-trash"></i> 清理旧日志
|
||
</button>
|
||
<button class="btn btn-primary" @click="loadSystemLogs(1)" :disabled="systemLogs.loading">
|
||
<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-clipboard" style="font-size: 48px; margin-bottom: 15px;"></i>
|
||
<p>暂无日志记录</p>
|
||
</div>
|
||
|
||
<!-- 加载中 -->
|
||
<div v-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-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>
|
||
</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 === 'sftp_only'" style="background: #6c757d; color: white; padding: 3px 8px; border-radius: 4px;">仅SFTP</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-server"></i> SFTP
|
||
</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.has_ftp_config" 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 v-show="adminTab === 'tools'">
|
||
<!-- 上传工具管理区域 -->
|
||
<div class="card">
|
||
<h3 style="margin-bottom: 20px;">
|
||
<i class="fas fa-cloud-upload-alt"></i> 上传工具管理
|
||
</h3>
|
||
|
||
<!-- 工具状态显示 -->
|
||
<div v-if="uploadToolStatus !== null">
|
||
<div v-if="uploadToolStatus.exists" style="padding: 15px; background: rgba(34, 197, 94, 0.15); border-left: 4px solid #22c55e; border-radius: 6px; margin-bottom: 20px;">
|
||
<div style="display: flex; align-items: center; justify-content: space-between;">
|
||
<div>
|
||
<div style="color: #86efac; font-weight: 600; margin-bottom: 5px;">
|
||
<i class="fas fa-check-circle"></i> 上传工具已存在
|
||
</div>
|
||
<div style="color: #86efac; font-size: 13px;">
|
||
文件大小: {{ uploadToolStatus.fileInfo.sizeMB }} MB
|
||
</div>
|
||
<div style="color: #86efac; font-size: 12px; margin-top: 3px;">
|
||
最后修改: {{ formatDate(uploadToolStatus.fileInfo.modifiedAt) }}
|
||
</div>
|
||
</div>
|
||
<button class="btn btn-primary" @click="checkUploadTool" style="background: #22c55e;">
|
||
<i class="fas fa-sync-alt"></i> 重新检测
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-else style="padding: 15px; background: rgba(245, 158, 11, 0.15); border-left: 4px solid #f59e0b; border-radius: 6px; margin-bottom: 20px;">
|
||
<div style="color: #fbbf24; font-weight: 600; margin-bottom: 5px;">
|
||
<i class="fas fa-exclamation-triangle"></i> 上传工具不存在
|
||
</div>
|
||
<div style="color: #fbbf24; font-size: 13px;">
|
||
普通用户将无法下载上传工具,请上传工具文件
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 操作按钮组 -->
|
||
<div style="display: flex; gap: 10px; margin-top: 15px; flex-wrap: wrap;">
|
||
<button class="btn btn-primary" @click="checkUploadTool" :disabled="checkingUploadTool" style="background: #3b82f6;">
|
||
<i class="fas fa-search" v-if="!checkingUploadTool"></i>
|
||
<i class="fas fa-spinner fa-spin" v-else></i>
|
||
{{ checkingUploadTool ? '检测中...' : '检测上传工具' }}
|
||
</button>
|
||
|
||
<button v-if="uploadToolStatus && !uploadToolStatus.exists" class="btn btn-primary" @click="$refs.uploadToolInput.click()" :disabled="uploadingTool" style="background: #22c55e;">
|
||
<i class="fas fa-upload" v-if="!uploadingTool"></i>
|
||
<i class="fas fa-spinner fa-spin" v-else></i>
|
||
{{ uploadingTool ? '上传中...' : '上传工具文件' }}
|
||
</button>
|
||
|
||
<input ref="uploadToolInput" type="file" accept=".exe" style="display: none;" @change="handleUploadToolFile">
|
||
</div>
|
||
|
||
<!-- 使用说明 -->
|
||
<div style="margin-top: 20px; padding: 12px; background: rgba(59, 130, 246, 0.1); border-left: 4px solid #3b82f6; border-radius: 6px;">
|
||
<div style="color: #93c5fd; font-size: 13px; line-height: 1.6;">
|
||
<strong><i class="fas fa-info-circle"></i> 说明:</strong>
|
||
<ul style="margin: 8px 0 0 20px; padding-left: 0;">
|
||
<li>上传工具文件应为 .exe 格式,大小通常在 20-50 MB</li>
|
||
<li>上传后,普通用户可以在设置页面下载该工具</li>
|
||
<li>如果安装脚本下载失败,可以在这里手动上传</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div><!-- 工具标签页结束 -->
|
||
</div><!-- 管理员视图结束 -->
|
||
|
||
<!-- 忘记密码模态框 -->
|
||
<div v-if="showForgotPasswordModal" class="modal-overlay" @mousedown.self="handleModalMouseDown" @mouseup.self="handleModalMouseUp('showForgotPasswordModal')">
|
||
<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">邮箱</label>
|
||
<input type="email" class="form-input" v-model="forgotPasswordForm.email" placeholder="请输入注册邮箱" required>
|
||
</div>
|
||
<div style="display: flex; gap: 10px; margin-top: 20px;">
|
||
<button class="btn btn-primary" @click="requestPasswordReset" style="flex: 1;">
|
||
<i class="fas fa-paper-plane"></i> 发送重置邮件
|
||
</button>
|
||
<button class="btn btn-secondary" @click="showForgotPasswordModal = false; forgotPasswordForm = {email: ''}" 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')">
|
||
<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" style="flex: 1;">
|
||
<i class="fas fa-unlock"></i> 重置密码
|
||
</button>
|
||
<button class="btn btn-secondary" @click="showResetPasswordModal = false; resetPasswordForm = {token: '', new_password: ''}" 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')">
|
||
<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: white; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.15); 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: #667eea;"></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: #667eea;">{{ uploadProgress }}%</div>
|
||
</div>
|
||
<div style="width: 100%; height: 8px; background: rgba(255,255,255,0.1); 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')">
|
||
<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="sftp_only">仅SFTP存储</option>
|
||
<option value="user_choice">用户选择</option>
|
||
</select>
|
||
<small style="color: var(--text-secondary); font-size: 12px; margin-top: 5px; display: block;">
|
||
仅本地:用户只能使用本地存储 | 仅SFTP:用户只能使用SFTP | 用户选择:用户可自由切换
|
||
</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>
|
||
• 配额仅影响本地存储,SFTP存储不受此限制
|
||
</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>
|
||
</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: white;
|
||
border-radius: 8px;
|
||
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
|
||
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(255,255,255,0.03);
|
||
}
|
||
|
||
.context-menu-item i {
|
||
width: 16px;
|
||
text-align: center;
|
||
}
|
||
|
||
.context-menu-item-danger {
|
||
color: #ef4444;
|
||
}
|
||
|
||
.context-menu-item-danger:hover {
|
||
background: rgba(239, 68, 68, 0.1);
|
||
}
|
||
|
||
.context-menu-divider {
|
||
height: 1px;
|
||
background: rgba(255,255,255,0.1);
|
||
margin: 4px 0;
|
||
}
|
||
|
||
/* 移动端优化 */
|
||
@media (max-width: 768px) {
|
||
.context-menu {
|
||
min-width: 180px;
|
||
box-shadow: 0 8px 32px rgba(0,0,0,0.25);
|
||
}
|
||
|
||
.context-menu-item {
|
||
padding: 14px 18px;
|
||
font-size: 15px;
|
||
}
|
||
}
|
||
|
||
/* 媒体预览器样式 */
|
||
.media-viewer-content {
|
||
background: white;
|
||
border-radius: 12px;
|
||
max-width: 90vw;
|
||
max-height: 90vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
box-shadow: 0 10px 40px rgba(0,0,0,0.3);
|
||
}
|
||
|
||
.media-viewer-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 15px 20px;
|
||
background: #667eea;
|
||
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>
|