新增功能: - 在分享列表表格中添加"到期时间"列,清晰显示每个分享的有效期 - 在分享创建成功后显示到期时间信息 - 添加友好的时间格式化显示(永久有效/今天过期/明天过期/X天后过期) - 使用颜色区分不同状态: * 绿色: 永久有效 * 蓝色: 正常有效期 * 黄色: 即将过期(3天内) * 红色: 已过期 技术实现: - frontend/app.html: 添加到期时间显示UI(表格列+分享结果) - frontend/app.js: 添加3个辅助方法 * formatExpireTime(): 格式化到期时间显示 * isExpiringSoon(): 判断是否即将过期(3天内) * isExpired(): 判断是否已过期 改进用户体验: - 用户可以直观看到分享何时过期 - 即将过期的分享会有醒目的黄色提醒 - 鼠标悬停显示完整的ISO时间戳 - 时间显示本地化,使用中文格式 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
2171 lines
88 KiB
HTML
2171 lines
88 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<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>
|
||
* { 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;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div id="app">
|
||
<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>
|
||
<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 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">
|
||
</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 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()">
|
||
<!-- 拖拽提示层 -->
|
||
<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>
|
||
<i class="fas fa-upload"></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;">
|
||
<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; display: flex; align-items: center; gap: 10px;">
|
||
<!-- 图片缩略图 -->
|
||
<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;"></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;"></i>
|
||
<i v-else-if="file.name.match(/\.(pdf)$/i)" class="fas fa-file-pdf" style="font-size: 20px; color: #F44336;"></i>
|
||
<i v-else-if="file.name.match(/\.(doc|docx)$/i)" class="fas fa-file-word" style="font-size: 20px; color: #2196F3;"></i>
|
||
<i v-else-if="file.name.match(/\.(xls|xlsx)$/i)" class="fas fa-file-excel" style="font-size: 20px; color: #4CAF50;"></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;"></i>
|
||
<i v-else class="fas fa-file" style="font-size: 20px; color: #9E9E9E;"></i>
|
||
<span>{{ file.name }}</span>
|
||
</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" @click="showRenameModal = false">
|
||
<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="showShareAllModal" class="modal-overlay" @click="showShareAllModal = false">
|
||
<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" @click="showShareFileModal = false">
|
||
<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>
|
||
|
||
<!-- 设置视图 -->
|
||
<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: #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;">{{ storageTypeText }}</span>
|
||
</div>
|
||
|
||
<div v-if="storageType === 'local'" 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="display: flex; gap: 15px; flex-wrap: wrap;">
|
||
<button
|
||
class="btn"
|
||
:class="storageType === 'local' ? 'btn-primary' : 'btn-secondary'"
|
||
@click="switchStorage('local')"
|
||
:disabled="storageType === 'local'">
|
||
<i class="fas fa-hard-drive"></i> 本地存储
|
||
</button>
|
||
<button
|
||
class="btn"
|
||
:class="storageType === 'sftp' ? 'btn-primary' : 'btn-secondary'"
|
||
@click="switchStorage('sftp')"
|
||
:disabled="storageType === 'sftp'">
|
||
<i class="fas fa-server"></i> SFTP存储
|
||
</button>
|
||
</div>
|
||
|
||
<div style="margin-top: 15px; padding: 10px; background: #fff3cd; border-radius: 6px; font-size: 13px; color: #856404;">
|
||
<i class="fas fa-info-circle"></i>
|
||
<strong>提示:</strong> 本地存储速度快但有配额限制;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' && user.has_ftp_config" 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: 8px;">
|
||
<div style="margin-bottom: 15px;">
|
||
<span style="font-weight: 600; color: #333;">存储方式: </span>
|
||
<span style="color: #667eea; font-weight: 600;">SFTP存储</span>
|
||
<span style="margin-left: 10px; padding: 4px 12px; background: #17a2b8; color: white; border-radius: 12px; font-size: 12px;">
|
||
<i class="fas fa-lock"></i> 仅SFTP
|
||
</span>
|
||
</div>
|
||
|
||
<div style="margin-bottom: 15px;">
|
||
<span style="font-weight: 600; color: #333;">服务器: </span>
|
||
<span>{{ user.ftp_host }}:{{ user.ftp_port }}</span>
|
||
</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存储",您的文件存储在远程SFTP服务器上。如需使用本地存储,请联系管理员修改权限设置。
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- SFTP配置 - 仅普通用户且权限允许SFTP -->
|
||
<div v-if="user && !user.is_admin && (storagePermission === 'sftp_only' || (storagePermission === 'user_choice' && (storageType === 'sftp' || forceSftpConfigVisible)))" id="sftp-config-section">
|
||
<h3 style="margin-bottom: 20px;">SFTP配置</h3>
|
||
<div v-if="user && !user.has_ftp_config" class="alert alert-info">
|
||
请配置SFTP服务器
|
||
</div>
|
||
|
||
<!-- 上传配置文件 -->
|
||
<div style="margin-bottom: 25px;">
|
||
<div style="border: 2px dashed #667eea; border-radius: 8px; padding: 20px; text-align: center; background: #f8f9ff; cursor: pointer; transition: all 0.3s;"
|
||
@click="$refs.configFileInput.click()"
|
||
@dragover.prevent="$event.currentTarget.style.background='#e8ecf7'"
|
||
@dragleave.prevent="$event.currentTarget.style.background='#f8f9ff'"
|
||
@drop.prevent="handleConfigFileDrop">
|
||
<i class="fas fa-cloud-upload-alt" style="font-size: 48px; color: #667eea; margin-bottom: 10px;"></i>
|
||
<p style="margin: 10px 0; font-size: 16px; color: #333; font-weight: 500;">
|
||
快速导入配置文件
|
||
</p>
|
||
<p style="margin: 5px 0; font-size: 14px; color: #666;">
|
||
点击选择或拖拽 .inf 文件到此处
|
||
</p>
|
||
<p style="margin: 5px 0; font-size: 12px; color: #999;">
|
||
导入后请检查配置信息并点击保存按钮
|
||
</p>
|
||
<input type="file" accept=".inf" @change="handleConfigFileUpload" ref="configFileInput" style="display: none;">
|
||
</div>
|
||
</div>
|
||
|
||
<form @submit.prevent="updateFtpConfig">
|
||
<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格式: 基础URL/文件路径,例如: http://example.com/files/test.exe
|
||
</small>
|
||
</div>
|
||
<button type="submit" class="btn btn-primary">
|
||
<i class="fas fa-save"></i> 保存SFTP配置
|
||
</button>
|
||
</form>
|
||
</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;">
|
||
<thead>
|
||
<tr style="border-bottom: 2px solid #ddd;">
|
||
<th style="padding: 10px; text-align: left;">文件路径</th>
|
||
<th style="padding: 10px; text-align: left;">分享链接</th>
|
||
<th style="padding: 10px; text-align: center;">访问次数</th>
|
||
<th style="padding: 10px; text-align: center;">下载次数</th>
|
||
<th style="padding: 10px; text-align: center;">到期时间</th>
|
||
<th style="padding: 10px; text-align: center;">操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="share in shares" :key="share.id" style="border-bottom: 1px solid #eee;">
|
||
<td style="padding: 10px;">{{ share.share_path }}</td>
|
||
<td style="padding: 10px;">
|
||
<a :href="share.share_url" target="_blank" style="color: #667eea;">{{ 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">
|
||
<!-- 服务器存储统计 -->
|
||
<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>
|
||
|
||
<h3 style="margin-bottom: 20px;">用户管理</h3>
|
||
<div style="overflow-x: auto;">
|
||
<table style="width: 100%; border-collapse: collapse; min-width: 900px;">
|
||
<thead>
|
||
<tr style="border-bottom: 2px solid #ddd; background: #f5f5f5;">
|
||
<th style="padding: 10px; text-align: left;">ID</th>
|
||
<th style="padding: 10px; text-align: left;">用户名</th>
|
||
<th style="padding: 10px; text-align: left;">邮箱</th>
|
||
<th style="padding: 10px; text-align: center;">存储权限</th>
|
||
<th style="padding: 10px; text-align: center;">当前存储</th>
|
||
<th style="padding: 10px; text-align: center;">配额使用</th>
|
||
<th style="padding: 10px; text-align: center;">状态</th>
|
||
<th style="padding: 10px; text-align: center;">操作</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;">
|
||
{{ u.username }}
|
||
<span v-if="u.is_admin" style="color: #28a745; margin-left: 5px;" title="管理员">
|
||
<i class="fas fa-crown"></i>
|
||
</span>
|
||
</td>
|
||
<td style="padding: 10px; font-size: 12px; color: #666;">{{ 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 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 class="card" style="margin-top: 30px;">
|
||
<h3 style="margin-bottom: 20px;">密码重置审核</h3>
|
||
<div v-if="passwordResetRequests.length === 0" class="alert alert-info">
|
||
暂无待审核的密码重置请求
|
||
</div>
|
||
<table v-else style="width: 100%; border-collapse: collapse;">
|
||
<thead>
|
||
<tr style="border-bottom: 2px solid #ddd;">
|
||
<th style="padding: 10px; text-align: left;">用户名</th>
|
||
<th style="padding: 10px; text-align: left;">邮箱</th>
|
||
<th style="padding: 10px; text-align: left;">提交时间</th>
|
||
<th style="padding: 10px; text-align: center;">操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="req in passwordResetRequests" :key="req.id" style="border-bottom: 1px solid #eee;">
|
||
<td style="padding: 10px;">{{ req.username }}</td>
|
||
<td style="padding: 10px;">{{ req.email }}</td>
|
||
<td style="padding: 10px;">{{ formatDate(req.created_at) }}</td>
|
||
<td style="padding: 10px; text-align: center;">
|
||
<button class="btn" style="background: #28a745; color: white; margin: 2px;" @click="reviewPasswordReset(req.id, true)">
|
||
<i class="fas fa-check"></i> 批准
|
||
</button>
|
||
<button class="btn" style="background: #dc3545; color: white; margin: 2px;" @click="reviewPasswordReset(req.id, false)">
|
||
<i class="fas fa-times"></i> 拒绝
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<!-- 上传工具管理区域 -->
|
||
<div class="card" style="margin-top: 30px;">
|
||
<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 v-if="showForgotPasswordModal" class="modal-overlay" @click="showForgotPasswordModal = false">
|
||
<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="text" class="form-input" v-model="forgotPasswordForm.username" placeholder="请输入用户名" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">新密码 (至少6字符)</label>
|
||
<input type="password" class="form-input" v-model="forgotPasswordForm.new_password" placeholder="输入新密码" minlength="6" 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 = {username: '', new_password: ''}" style="flex: 1;">
|
||
<i class="fas fa-times"></i> 取消
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 文件审查模态框 -->
|
||
<div v-if="showFileInspectionModal" class="modal-overlay" @click="showFileInspectionModal = false">
|
||
<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;">
|
||
<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; display: flex; align-items: center; gap: 10px;">
|
||
<i v-if="file.isDirectory" class="fas fa-folder" style="font-size: 20px; 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: 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;"></i>
|
||
<i v-else-if="file.name.match(/\.(pdf)$/i)" class="fas fa-file-pdf" style="font-size: 20px; color: #F44336;"></i>
|
||
<i v-else-if="file.name.match(/\.(doc|docx)$/i)" class="fas fa-file-word" style="font-size: 20px; color: #2196F3;"></i>
|
||
<i v-else-if="file.name.match(/\.(xls|xlsx)$/i)" class="fas fa-file-excel" style="font-size: 20px; color: #4CAF50;"></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;"></i>
|
||
<i v-else class="fas fa-file" style="font-size: 20px; color: #9E9E9E;"></i>
|
||
<span>{{ file.name }}</span>
|
||
</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 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 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" @click="showEditStorageModal = false">
|
||
<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=20251110001"></script>
|
||
|
||
<!-- 开发者工具保护 -->
|
||
<script>
|
||
// 禁用右键菜单
|
||
document.addEventListener('contextmenu', function(e) {
|
||
e.preventDefault();
|
||
return false;
|
||
});
|
||
|
||
// 禁用F12和常见开发者工具快捷键
|
||
document.addEventListener('keydown', function(e) {
|
||
// F12
|
||
if (e.key === 'F12' || e.keyCode === 123) {
|
||
e.preventDefault();
|
||
return false;
|
||
}
|
||
// Ctrl+Shift+I (开发者工具)
|
||
if (e.ctrlKey && e.shiftKey && (e.key === 'I' || e.keyCode === 73)) {
|
||
e.preventDefault();
|
||
return false;
|
||
}
|
||
// Ctrl+Shift+J (控制台)
|
||
if (e.ctrlKey && e.shiftKey && (e.key === 'J' || e.keyCode === 74)) {
|
||
e.preventDefault();
|
||
return false;
|
||
}
|
||
// Ctrl+U (查看源代码)
|
||
if (e.ctrlKey && (e.key === 'U' || e.keyCode === 85)) {
|
||
e.preventDefault();
|
||
return false;
|
||
}
|
||
// Ctrl+Shift+C (元素选择器)
|
||
if (e.ctrlKey && e.shiftKey && (e.key === 'C' || e.keyCode === 67)) {
|
||
e.preventDefault();
|
||
return false;
|
||
}
|
||
});
|
||
|
||
// 检测开发者工具是否打开
|
||
(function() {
|
||
const threshold = 160;
|
||
let isDevToolsOpen = false;
|
||
|
||
setInterval(function() {
|
||
const widthThreshold = window.outerWidth - window.innerWidth > threshold;
|
||
const heightThreshold = window.outerHeight - window.innerHeight > threshold;
|
||
const orientation = widthThreshold ? 'vertical' : 'horizontal';
|
||
|
||
if (!(heightThreshold && widthThreshold) &&
|
||
((window.Firebug && window.Firebug.chrome && window.Firebug.chrome.isInitialized) || widthThreshold || heightThreshold)) {
|
||
if (!isDevToolsOpen) {
|
||
isDevToolsOpen = true;
|
||
// 检测到开发者工具打开,可以选择清空页面或重定向
|
||
// document.body.innerHTML = '<h1 style="text-align:center;margin-top:20%;">请关闭开发者工具</h1>';
|
||
console.clear();
|
||
}
|
||
} else {
|
||
isDevToolsOpen = false;
|
||
}
|
||
}, 500);
|
||
})();
|
||
|
||
// 禁用console输出(可选)
|
||
if (window.location.hostname !== 'localhost' && window.location.hostname !== '127.0.0.1') {
|
||
console.log = function() {};
|
||
console.info = function() {};
|
||
console.warn = function() {};
|
||
console.error = function() {};
|
||
console.debug = function() {};
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|