Files
vue-driven-cloud-storage/frontend/app.html
yuyx 4f29bbe631 feat(admin): 管理员页面改为标签页模式
- 添加5个标签页:概览、设置、监控、用户、工具
- 概览:调试模式开关、服务器存储统计
- 设置:系统设置、SMTP邮件配置
- 监控:健康检测、系统日志
- 用户:用户管理列表
- 工具:上传工具管理

优化管理员页面布局,减少滚动,提升使用体验

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-27 20:31:26 +08:00

2827 lines
129 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<!-- 开发者工具检测 - 必须放在最前面,优先执行 -->
<script>
(function() {
'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>
/* 防止 Vue 初始化前显示原始模板 */
[v-cloak] { display: none !important; }
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
#app { min-height: 100vh; }
.auth-container {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 20px;
}
.auth-box {
background: white;
border-radius: 16px;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
padding: 40px;
width: 100%;
max-width: 500px;
}
.auth-title {
font-size: 28px;
font-weight: 700;
text-align: center;
margin-bottom: 30px;
color: #667eea;
}
.form-group { margin-bottom: 20px; }
.form-label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #555;
}
.form-input {
width: 100%;
padding: 12px 16px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
}
.form-input:focus {
outline: none;
border-color: #667eea;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
}
.btn-primary {
background: #667eea;
color: white;
width: 100%;
}
.btn-primary:hover { background: #5568d3; }
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-primary:disabled:hover {
background: #667eea;
}
.alert { padding: 12px; border-radius: 8px; margin-bottom: 15px; }
.alert-error { background: #f8d7da; color: #721c24; }
.alert-success { background: #d4edda; color: #155724; }
.auth-switch {
text-align: center;
margin-top: 20px;
color: #666;
}
.auth-switch a {
color: #667eea;
cursor: pointer;
text-decoration: none;
}
.navbar {
background: white;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
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: 6px;
cursor: pointer;
transition: all 0.2s;
color: #666;
font-weight: 500;
}
.nav-item:hover {
background: #f0f0f0;
color: #667eea;
}
.nav-item.active {
background: #667eea;
color: white;
}
.user-info {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 16px;
background: #f8f9fa;
border-radius: 20px;
}
.main-container {
max-width: 1200px;
margin: 30px auto;
padding: 0 20px;
}
.card {
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
padding: 30px;
}
.loading {
text-align: center;
padding: 40px;
color: #999;
}
.spinner {
border: 3px solid #f3f3f3;
border-top: 3px solid #667eea;
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); }
}
.alert-info { background: #d1ecf1; color: #0c5460; }
/* 文件网格视图 */
.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: 8px;
cursor: pointer;
transition: all 0.2s;
text-align: center;
min-height: 180px;
position: relative;
}
.file-grid-item:hover {
background: #f5f5f5;
transform: translateY(-2px);
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.file-icon {
margin-bottom: 10px;
}
.file-thumbnail {
width: 64px;
height: 64px;
object-fit: cover;
border-radius: 8px;
}
.file-name {
font-size: 13px;
color: #333;
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: #999;
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: #999;
padding: 40px;
font-size: 14px;
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-secondary:hover {
background: #5a6268;
}
.btn-icon {
background: none;
border: none;
padding: 8px;
cursor: pointer;
border-radius: 4px;
transition: all 0.2s;
color: #667eea;
}
.btn-icon:hover {
background: #f0f0f0;
color: #5568d3;
}
.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.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
background: white;
padding: 30px;
border-radius: 12px;
width: 500px;
max-width: 90%;
max-height: 90vh;
overflow-y: auto;
}
/* 移动端适配 */
@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: #16a34a; }
.text-yellow-600 { color: #ca8a04; }
.text-red-600 { color: #dc2626; }
.text-blue-600 { color: #2563eb; }
</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 #ddd; border-radius: 4px; padding: 5px; background: #f0f0f0;" @click="refreshCaptcha">
<img :src="captchaUrl" alt="验证码" style="display: block; width: 120px; height: 40px;" />
</div>
</div>
<small style="color: #666; 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: #f8f9fa; border-radius: 8px; 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: #667eea;">
<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: #666; display: flex; justify-content: space-between;">
<span>配额使用情况</span>
<span>{{ localUsedFormatted }} / {{ localQuotaFormatted }} ({{ quotaPercentage }}%)</span>
</div>
<div style="width: 100%; height: 20px; background: #e0e0e0; border-radius: 10px; overflow: hidden;">
<div :style="{
width: quotaPercentage + '%',
height: '100%',
background: quotaPercentage > 90 ? '#dc3545' : quotaPercentage > 75 ? '#ffc107' : '#28a745',
transition: 'width 0.3s'
}"></div>
</div>
</div>
</div>
<!-- 路径导航 (面包屑) -->
<div v-if="currentPath !== '/'" style="margin-bottom: 15px; padding: 10px 15px; background: #f5f5f5; border-radius: 8px; 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: #999;">/</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: #333; font-weight: 600;">{{ part }}</span>
<span v-if="index < pathParts.length - 1" style="color: #999;">/</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: #333; margin-bottom: 10px;">拖放文件到这里上传</div>
<div style="font-size: 14px; color: #666;">松开鼠标即可开始上传</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="border-bottom: 2px solid #ddd; background: #f5f5f5;">
<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: #666;">{{ file.isDirectory ? '-' : file.sizeFormatted }}</td>
<td style="padding: 10px; color: #666;">{{ 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: #f9f9f9; padding: 20px; border-radius: 8px;">
<div style="margin-bottom: 15px;">
<strong style="color: #666;">名称:</strong>
<div style="margin-top: 5px; font-size: 16px;">{{ folderInfo.name }}</div>
</div>
<div style="margin-bottom: 15px;">
<strong style="color: #666;">路径:</strong>
<div style="margin-top: 5px; color: #667eea;">{{ folderInfo.path }}</div>
</div>
<div style="margin-bottom: 15px;">
<strong style="color: #666;">总大小:</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: #666;">文件数:</strong>
<div style="margin-top: 5px; font-size: 16px;">{{ folderInfo.fileCount }} 个</div>
</div>
<div style="flex: 1;">
<strong style="color: #666;">子文件夹:</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: #999;">
<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: #28a745;"><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: #666; 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: #28a745;"><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: #64748b; 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 #cbd5e1; border-radius: 12px; padding: 16px; background: #f8fafc; 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: #1f2937;">导入配置文件</div>
<div style="color: #6b7280; 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: #666; 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: #334155;">当前模式</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: #666; font-size: 13px;">本地存储适合快速读写SFTP 适合独立服务器空间</div>
</div>
<div style="margin-top: 16px; background: white; border-radius: 12px; padding: 12px; border: 1px solid #e2e8f0;">
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; align-items: center;">
<div style="height: 4px; background: #e2e8f0; 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 #e2e8f0; 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: #0f172a; 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: #28a745; background: rgba(40,167,69,0.12); padding: 4px 8px; border-radius: 999px;">当前</span>
</div>
<div style="color: #475569; font-size: 13px; margin-bottom: 10px;">更快的读写,适合日常上传下载。</div>
<div style="margin-bottom: 10px;">
<div style="font-size: 12px; color: #64748b; margin-bottom: 6px;">配额使用</div>
<div style="font-weight: 600; color: #0f172a;">{{ localUsedFormatted }} / {{ localQuotaFormatted }}</div>
<div style="margin-top: 6px; width: 100%; height: 10px; background: #e2e8f0; 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 #e2e8f0; 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: #0f172a; 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: #475569; font-size: 13px; margin-bottom: 10px;">使用你自己的服务器空间,独立存储更灵活。</div>
<div v-if="user?.has_ftp_config" style="font-size: 13px; color: #0f172a; margin-bottom: 10px;">
已配置: {{ user.ftp_host }}:{{ user.ftp_port }}
</div>
<div v-else style="font-size: 13px; color: #b45309; background: #fff7ed; border: 1px dashed #fcd34d; 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: #f8fafc; border-radius: 8px; border: 1px solid #e2e8f0;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
<span style="font-size: 12px; color: #64748b;">空间统计</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: #c53030;">
<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: #64748b; font-size: 12px;">{{ sftpUsage.fileCount }} 文件)</span>
</div>
<div v-else style="font-size: 12px; color: #94a3b8;">点击刷新查看</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: #f1f5f9; border-radius: 10px; font-size: 13px; color: #475569;">
<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: #f8f9fa; padding: 20px; border-radius: 8px;">
<div style="margin-bottom: 15px;">
<span style="font-weight: 600; color: #333;">存储方式: </span>
<span style="color: #667eea; font-weight: 600;">本地存储</span>
<span style="margin-left: 10px; padding: 4px 12px; background: #28a745; 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: #333;">配额使用: </span>
<span>{{ localUsedFormatted }} / {{ localQuotaFormatted }} ({{ quotaPercentage }}%)</span>
<div style="margin-top: 8px; width: 100%; height: 18px; background: #e0e0e0; 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: #d1ecf1; border-left: 4px solid #0c5460; border-radius: 6px; font-size: 13px; color: #0c5460;">
<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: #f8f9fa; padding: 20px; border-radius: 12px; border: 1px solid #e5e7eb;">
<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: #0f172a; display: flex; gap: 8px; align-items: center;">
<i class="fas fa-shield-alt"></i>
仅 SFTP 模式
</div>
<div style="color: #475569; 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 #e2e8f0;">
<div style="font-weight: 600; color: #333; margin-bottom: 8px;">
<i class="fas fa-server" style="color: #667eea;"></i> 服务器信息
</div>
<div style="color: #475569; 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 #e2e8f0;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<div style="font-weight: 600; color: #333;">
<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: #999;">(文件较多时可能需要一些时间)</div>
</div>
<!-- 错误提示 -->
<div v-else-if="sftpUsageError" style="padding: 12px; background: #fff5f5; border-radius: 8px; color: #c53030; 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: #f0f9ff; border-radius: 10px; border: 1px solid #e0f2fe;">
<div style="font-size: 20px; font-weight: 700; color: #0369a1;">{{ sftpUsage.fileCount }}</div>
<div style="font-size: 12px; color: #64748b; margin-top: 4px;">文件数</div>
</div>
<div style="text-align: center; padding: 12px; background: #fefce8; border-radius: 10px; border: 1px solid #fef08a;">
<div style="font-size: 20px; font-weight: 700; color: #a16207;">{{ sftpUsage.dirCount }}</div>
<div style="font-size: 12px; color: #64748b; margin-top: 4px;">文件夹数</div>
</div>
</div>
<!-- 未统计提示 -->
<div v-else style="text-align: center; padding: 16px; color: #64748b; font-size: 13px;">
<i class="fas fa-database" style="font-size: 24px; color: #cbd5e1; margin-bottom: 8px; display: block;"></i>
点击"刷新"按钮统计 SFTP 空间使用情况
</div>
</div>
<div style="padding: 10px; background: #eef2ff; border-radius: 10px; color: #374151; 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: #666;">
访问: {{ 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: #dc3545;" @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: #28a745;"><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: #dc3545; 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, #a8edea 0%, #fed6e3 100%); padding: 20px; border-radius: 12px; color: #333;">
<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: #fff3cd; border-left: 4px solid #ffc107; border-radius: 6px; color: #856404;">
<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: #f8d7da; border-left: 4px solid #dc3545; border-radius: 6px; color: #721c24;">
<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: #666; 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;">使用 SSL465 通常需要)</label>
</div>
</div>
<div class="form-group">
<label class="form-label">用户名(邮箱)</label>
<input type="text" class="form-input" v-model="systemSettings.smtp.user" placeholder="your@qq.com">
</div>
<div class="form-group">
<label class="form-label">发件人 From可选</label>
<input type="text" class="form-input" v-model="systemSettings.smtp.from" placeholder="显示名称 <your@qq.com>">
</div>
<div class="form-group">
<label class="form-label">密码/授权码</label>
<input type="password" class="form-input" v-model="systemSettings.smtp.password" :placeholder="systemSettings.smtp.has_password ? '已配置,留空则不修改' : '请输入授权码'">
</div>
</div>
<div style="display: flex; gap: 10px; flex-wrap: wrap; margin-top: 15px;">
<button class="btn btn-primary" @click="updateSystemSettings">
<i class="fas fa-save"></i> 保存设置
</button>
<button class="btn btn-secondary" @click="testSmtp">
<i class="fas fa-envelope"></i> 发送测试邮件
</button>
</div>
</div>
</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: #28a745;"><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: #dc3545;"><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: #888; 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: #333;"><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: #f8f9fa; 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: #666;">{{ 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: #333;"><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: #f8f9fa; 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: #666;">{{ 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: #333;"><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: #f8f9fa; 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: #666;">{{ 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: #888;">
<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: #f8f9fa; border-radius: 8px;">
<div style="display: flex; align-items: center; gap: 8px;">
<label style="font-size: 13px; color: #666;">级别:</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: #666;">分类:</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: #666;">搜索:</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: #666;">
共 {{ 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: #888;">
{{ 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: #666;">
<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: #555;">{{ log.message }}</div>
<div v-if="log.username || log.ip_address" style="font-size: 11px; color: #888; 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: #888;">
<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: #888;">
<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: #666;">
{{ 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="border-bottom: 2px solid #ddd; background: #f5f5f5;">
<th style="padding: 10px; text-align: left; width: 5%;">ID</th>
<th style="padding: 10px; text-align: left; width: 12%;">用户名</th>
<th style="padding: 10px; text-align: left; width: 15%;">邮箱</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: 13%;">配额使用</th>
<th style="padding: 10px; text-align: center; width: 10%;">状态</th>
<th style="padding: 10px; text-align: center; width: 25%;">操作</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">
<span>{{ u.username }}</span>
<span v-if="u.is_admin" style="color: #28a745; margin-left: 5px; white-space: nowrap;" title="管理员">
<i class="fas fa-crown"></i>
</span>
</div>
</td>
<td style="padding: 10px; font-size: 12px; color: #666; 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: #28a745; 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: #999;">
{{ Math.round((u.local_storage_used / u.local_storage_quota) * 100) }}%
</div>
</div>
<span v-else style="color: #999;">-</span>
</td>
<td style="padding: 10px; text-align: center;">
<span v-if="u.is_banned" style="color: #dc3545; font-weight: 600;">已封禁</span>
<span v-else-if="!u.is_verified" style="color: #fd7e14; font-weight: 600;">未激活</span>
<span v-else style="color: #28a745;">正常</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: #ffc107; 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: #28a745; 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: #17a2b8; color: white; font-size: 11px; padding: 5px 10px;" @click="openFileInspection(u)">
<i class="fas fa-folder-open"></i> 文件
</button>
<button class="btn" style="background: #dc3545; 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: #d4edda; border-left: 4px solid #28a745; border-radius: 6px; margin-bottom: 20px;">
<div style="display: flex; align-items: center; justify-content: space-between;">
<div>
<div style="color: #155724; font-weight: 600; margin-bottom: 5px;">
<i class="fas fa-check-circle"></i> 上传工具已存在
</div>
<div style="color: #155724; font-size: 13px;">
文件大小: {{ uploadToolStatus.fileInfo.sizeMB }} MB
</div>
<div style="color: #155724; font-size: 12px; margin-top: 3px;">
最后修改: {{ formatDate(uploadToolStatus.fileInfo.modifiedAt) }}
</div>
</div>
<button class="btn btn-primary" @click="checkUploadTool" style="background: #28a745;">
<i class="fas fa-sync-alt"></i> 重新检测
</button>
</div>
</div>
<div v-else style="padding: 15px; background: #fff3cd; border-left: 4px solid #ffc107; border-radius: 6px; margin-bottom: 20px;">
<div style="color: #856404; font-weight: 600; margin-bottom: 5px;">
<i class="fas fa-exclamation-triangle"></i> 上传工具不存在
</div>
<div style="color: #856404; 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: #17a2b8;">
<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: #28a745;">
<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: #e7f3ff; border-left: 4px solid #2196F3; border-radius: 6px;">
<div style="color: #0c5460; 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: #666; 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: #666; 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: #ffc107; 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: #f5f5f5; 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: #666; 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="border-bottom: 2px solid #ddd; background: #f5f5f5;">
<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: #666;">{{ file.isDirectory ? '-' : file.sizeFormatted }}</td>
<td style="padding: 10px; color: #666;">{{ formatDate(file.modifiedAt) }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<div style="margin-top: 15px; padding: 10px; background: #fff3cd; border-radius: 6px; color: #856404;">
<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: #333; margin-bottom: 4px;">正在上传文件</div>
<div style="font-size: 13px; color: #666;">{{ uploadingFileName }}</div>
<div v-if="totalBytes > 0" style="font-size: 12px; color: #999; 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: #e0e0e0; 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: #666; 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: #666; font-size: 12px; margin-top: 5px; display: block;">
配额范围: 1MB - 100GB | 建议: 大配额使用GB小配额使用MB
</small>
</div>
<div style="padding: 12px; background: #f8f9fa; border-radius: 6px; margin-bottom: 20px;">
<div style="font-size: 13px; color: #666; line-height: 1.6;">
<strong style="color: #333;">配额说明:</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: #333;
font-size: 14px;
}
.context-menu-item:hover {
background: #f5f5f5;
}
.context-menu-item i {
width: 16px;
text-align: center;
}
.context-menu-item-danger {
color: #dc3545;
}
.context-menu-item-danger:hover {
background: #fff5f5;
}
.context-menu-divider {
height: 1px;
background: #e0e0e0;
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: #f5f5f5;
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>