6359 lines
222 KiB
HTML
6359 lines
222 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>叠加态 - 局域网文件传输</title>
|
||
<link rel="icon" type="image/x-icon" href="/image/logo_w.ico">
|
||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||
<style>
|
||
:root {
|
||
--primary-color: #4a90e2;
|
||
--secondary-color: #357abd;
|
||
--background-color: #f5f7fa;
|
||
--card-bg: #ffffff;
|
||
--text-color: #333;
|
||
--border-radius: 12px;
|
||
--transition-speed: 0.3s;
|
||
}
|
||
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||
background-color: var(--background-color);
|
||
color: var(--text-color);
|
||
line-height: 1.6;
|
||
padding: 20px;
|
||
}
|
||
|
||
.container {
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.nav-bar {
|
||
display: flex;
|
||
flex-direction: column;
|
||
padding: 24px 30px;
|
||
background: linear-gradient(135deg, var(--card-bg) 0%, rgba(245,247,250,0.8) 100%);
|
||
border-radius: var(--border-radius);
|
||
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
|
||
margin-bottom: 30px;
|
||
border: 1px solid rgba(255,255,255,0.3);
|
||
backdrop-filter: blur(10px);
|
||
position: relative;
|
||
overflow: hidden;
|
||
gap: 18px;
|
||
}
|
||
|
||
.nav-top-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
width: 100%;
|
||
}
|
||
|
||
.nav-bottom-row {
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
width: 100%;
|
||
gap: 30px;
|
||
flex-wrap: wrap;
|
||
position: relative;
|
||
padding-top: 18px;
|
||
}
|
||
|
||
.nav-bottom-row::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 0;
|
||
left: 20%;
|
||
right: 20%;
|
||
height: 1px;
|
||
background: linear-gradient(90deg, transparent, rgba(74, 144, 226, 0.2), transparent);
|
||
}
|
||
|
||
@media (max-width: 1024px) {
|
||
.nav-bar {
|
||
padding: 16px 20px;
|
||
gap: 15px;
|
||
}
|
||
|
||
.nav-controls {
|
||
gap: 18px;
|
||
}
|
||
|
||
.mobile-switches {
|
||
gap: 12px;
|
||
}
|
||
|
||
.quick-upload-switch {
|
||
padding: 8px 12px;
|
||
}
|
||
|
||
.switch-label {
|
||
font-size: 0.8em;
|
||
}
|
||
}
|
||
|
||
.nav-bar::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
height: 3px;
|
||
background: linear-gradient(90deg, var(--primary-color), var(--secondary-color), #00d2ff);
|
||
background-size: 200% 100%;
|
||
animation: gradientShift 3s ease-in-out infinite;
|
||
}
|
||
|
||
@keyframes gradientShift {
|
||
0%, 100% { background-position: 0% 50%; }
|
||
50% { background-position: 100% 50%; }
|
||
}
|
||
|
||
/* 添加导航栏悬浮效果 */
|
||
.nav-bar:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 8px 25px rgba(0,0,0,0.12);
|
||
}
|
||
|
||
.nav-bar:hover::before {
|
||
height: 4px;
|
||
animation-duration: 1.5s;
|
||
}
|
||
|
||
/* 导航栏分隔线 */
|
||
.nav-controls::before {
|
||
content: '';
|
||
position: absolute;
|
||
left: 50%;
|
||
top: 50%;
|
||
transform: translate(-50%, -50%);
|
||
width: 1px;
|
||
height: 30px;
|
||
background: linear-gradient(to bottom, transparent, rgba(74, 144, 226, 0.3), transparent);
|
||
opacity: 0;
|
||
transition: opacity 0.3s ease;
|
||
}
|
||
|
||
.nav-bar:hover .nav-controls::before {
|
||
opacity: 1;
|
||
}
|
||
|
||
.user-info {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 15px;
|
||
padding: 12px 20px;
|
||
background: rgba(74, 144, 226, 0.05);
|
||
border-radius: 22px;
|
||
border: 1px solid rgba(74, 144, 226, 0.1);
|
||
transition: all 0.3s ease;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.user-info:hover {
|
||
background: rgba(74, 144, 226, 0.1);
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 4px 12px rgba(74, 144, 226, 0.15);
|
||
}
|
||
|
||
.user-avatar {
|
||
width: 46px;
|
||
height: 46px;
|
||
border-radius: 50%;
|
||
border: 2px solid var(--primary-color);
|
||
object-fit: cover;
|
||
transition: all 0.3s ease;
|
||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 24px;
|
||
line-height: 1;
|
||
background: transparent;
|
||
}
|
||
|
||
.user-avatar.emoji-avatar {
|
||
background: linear-gradient(135deg, rgba(74, 144, 226, 0.1), rgba(74, 144, 226, 0.2));
|
||
}
|
||
|
||
.user-info:hover .user-avatar {
|
||
transform: scale(1.05);
|
||
border-color: var(--secondary-color);
|
||
box-shadow: 0 4px 12px rgba(74, 144, 226, 0.2);
|
||
}
|
||
|
||
.user-avatar {
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.user-avatar:hover {
|
||
transform: scale(1.1);
|
||
border-color: var(--secondary-color);
|
||
box-shadow: 0 4px 15px rgba(74, 144, 226, 0.3);
|
||
}
|
||
|
||
/* Admin登录弹窗样式 */
|
||
.admin-login-modal {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
backdrop-filter: blur(5px);
|
||
display: none;
|
||
justify-content: center;
|
||
align-items: center;
|
||
z-index: 10000;
|
||
}
|
||
|
||
.admin-login-modal.active {
|
||
display: flex;
|
||
}
|
||
|
||
.admin-login-content {
|
||
background: var(--card-bg);
|
||
padding: 30px;
|
||
border-radius: var(--border-radius);
|
||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
||
width: 90%;
|
||
max-width: 400px;
|
||
position: relative;
|
||
animation: modalSlideIn 0.3s ease-out;
|
||
}
|
||
|
||
@keyframes modalSlideIn {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(-20px) scale(0.95);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0) scale(1);
|
||
}
|
||
}
|
||
|
||
.admin-login-header {
|
||
text-align: center;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.admin-login-header h3 {
|
||
color: var(--primary-color);
|
||
margin-bottom: 5px;
|
||
font-size: 1.5em;
|
||
}
|
||
|
||
.admin-login-header p {
|
||
color: #666;
|
||
font-size: 0.9em;
|
||
}
|
||
|
||
.admin-login-form {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 15px;
|
||
}
|
||
|
||
.admin-form-group {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 5px;
|
||
}
|
||
|
||
.admin-form-group label {
|
||
font-weight: 500;
|
||
color: var(--text-color);
|
||
}
|
||
|
||
.admin-form-group input {
|
||
padding: 12px;
|
||
border: 2px solid #e0e0e0;
|
||
border-radius: 8px;
|
||
font-size: 16px;
|
||
transition: border-color 0.3s ease;
|
||
}
|
||
|
||
.admin-form-group input:focus {
|
||
outline: none;
|
||
border-color: var(--primary-color);
|
||
box-shadow: 0 0 0 3px rgba(74, 144, 226, 0.1);
|
||
}
|
||
|
||
.admin-login-buttons {
|
||
display: flex;
|
||
gap: 10px;
|
||
justify-content: space-between;
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.admin-btn {
|
||
padding: 12px 20px;
|
||
border: none;
|
||
border-radius: 8px;
|
||
font-size: 16px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
flex: 1;
|
||
}
|
||
|
||
.admin-btn-primary {
|
||
background: var(--primary-color);
|
||
color: white;
|
||
}
|
||
|
||
.admin-btn-primary:hover {
|
||
background: var(--secondary-color);
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 4px 12px rgba(74, 144, 226, 0.2);
|
||
}
|
||
|
||
.admin-btn-secondary {
|
||
background: #f5f5f5;
|
||
color: var(--text-color);
|
||
border: 1px solid #ddd;
|
||
}
|
||
|
||
.admin-btn-secondary:hover {
|
||
background: #e8e8e8;
|
||
}
|
||
|
||
.admin-close-btn {
|
||
position: absolute;
|
||
top: 15px;
|
||
right: 15px;
|
||
background: none;
|
||
border: none;
|
||
font-size: 20px;
|
||
color: #999;
|
||
cursor: pointer;
|
||
width: 30px;
|
||
height: 30px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 50%;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.admin-close-btn:hover {
|
||
background: #f0f0f0;
|
||
color: #666;
|
||
}
|
||
|
||
.admin-login-error {
|
||
background: #fee;
|
||
color: #c33;
|
||
padding: 10px;
|
||
border-radius: 6px;
|
||
margin-bottom: 15px;
|
||
font-size: 14px;
|
||
border-left: 4px solid #c33;
|
||
}
|
||
|
||
.admin-login-success {
|
||
background: #efe;
|
||
color: #363;
|
||
padding: 10px;
|
||
border-radius: 6px;
|
||
margin-bottom: 15px;
|
||
font-size: 14px;
|
||
border-left: 4px solid #363;
|
||
}
|
||
|
||
.admin-login-info {
|
||
background: #e3f2fd;
|
||
color: #1976d2;
|
||
padding: 10px;
|
||
border-radius: 6px;
|
||
margin-bottom: 15px;
|
||
font-size: 14px;
|
||
border-left: 4px solid #1976d2;
|
||
}
|
||
|
||
/* Admin状态指示器 */
|
||
.admin-indicator {
|
||
position: absolute;
|
||
top: -3px;
|
||
right: -3px;
|
||
width: 16px;
|
||
height: 16px;
|
||
background: #4CAF50;
|
||
border: 2px solid white;
|
||
border-radius: 50%;
|
||
animation: adminPulse 2s infinite;
|
||
}
|
||
|
||
@keyframes adminPulse {
|
||
0% { box-shadow: 0 0 0 0 rgba(76, 175, 80, 0.7); }
|
||
70% { box-shadow: 0 0 0 8px rgba(76, 175, 80, 0); }
|
||
100% { box-shadow: 0 0 0 0 rgba(76, 175, 80, 0); }
|
||
}
|
||
|
||
.admin-badge {
|
||
background: linear-gradient(135deg, #4CAF50, #45a049);
|
||
color: white;
|
||
padding: 2px 8px;
|
||
border-radius: 12px;
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
text-shadow: 0 1px 2px rgba(0,0,0,0.2);
|
||
}
|
||
|
||
.user-details {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
gap: 2px;
|
||
}
|
||
|
||
.user-name {
|
||
font-size: 0.9em;
|
||
font-weight: 600;
|
||
color: var(--primary-color);
|
||
margin: 0;
|
||
line-height: 1.2;
|
||
}
|
||
|
||
.user-role {
|
||
font-size: 0.75em;
|
||
color: #666;
|
||
margin: 0;
|
||
line-height: 1.2;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
|
||
.admin-badge {
|
||
background: linear-gradient(135deg, #ff6b6b, #ee5a24);
|
||
color: white;
|
||
padding: 3px 8px;
|
||
border-radius: 12px;
|
||
font-size: 0.65em;
|
||
font-weight: 600;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
box-shadow: 0 2px 4px rgba(238, 90, 36, 0.3);
|
||
animation: pulse 2s infinite;
|
||
}
|
||
|
||
@keyframes pulse {
|
||
0%, 100% { transform: scale(1); }
|
||
50% { transform: scale(1.05); }
|
||
}
|
||
|
||
.nav-bar h1 {
|
||
color: var(--primary-color);
|
||
font-size: 1.8em;
|
||
margin: 0;
|
||
font-weight: 700;
|
||
text-shadow: 0 1px 2px rgba(0,0,0,0.1);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
white-space: nowrap;
|
||
flex-shrink: 0;
|
||
min-width: 0;
|
||
}
|
||
|
||
.nav-bar h1 img {
|
||
height: 36px !important;
|
||
width: auto !important;
|
||
filter: drop-shadow(0 2px 4px rgba(0,0,0,0.1));
|
||
transition: transform 0.3s ease;
|
||
}
|
||
|
||
.nav-bar h1:hover img {
|
||
transform: scale(1.05) rotate(5deg);
|
||
}
|
||
|
||
.nav-controls {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 30px;
|
||
flex-wrap: wrap;
|
||
justify-content: center;
|
||
}
|
||
|
||
.mobile-switches {
|
||
display: flex;
|
||
gap: 20px;
|
||
align-items: center;
|
||
}
|
||
|
||
.nav-links {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16px;
|
||
}
|
||
|
||
.nav-links a {
|
||
color: var(--primary-color);
|
||
text-decoration: none;
|
||
transition: all var(--transition-speed);
|
||
padding: 12px 20px;
|
||
border-radius: 25px;
|
||
background: rgba(74, 144, 226, 0.08);
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-weight: 500;
|
||
font-size: 0.95em;
|
||
border: 1px solid rgba(74, 144, 226, 0.15);
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.nav-links a::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 0;
|
||
left: -100%;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
|
||
transition: left 0.5s;
|
||
}
|
||
|
||
.nav-links a:hover::before {
|
||
left: 100%;
|
||
}
|
||
|
||
.nav-links a:hover {
|
||
color: white;
|
||
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 15px rgba(74, 144, 226, 0.3);
|
||
border-color: var(--primary-color);
|
||
}
|
||
|
||
.nav-links a i {
|
||
font-size: 1.1em;
|
||
transition: transform 0.3s ease;
|
||
}
|
||
|
||
.nav-links a:hover i {
|
||
transform: scale(1.1);
|
||
}
|
||
|
||
.upload-section {
|
||
background: var(--card-bg);
|
||
padding: 30px;
|
||
border-radius: var(--border-radius);
|
||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||
margin-bottom: 30px;
|
||
text-align: center;
|
||
}
|
||
|
||
.upload-zone {
|
||
background: linear-gradient(145deg, rgba(255,255,255,1) 0%, rgba(245,247,250,1) 100%);
|
||
border: 2px dashed rgba(74, 144, 226, 0.3);
|
||
transition: all 0.3s ease;
|
||
padding: 40px;
|
||
margin: 20px 0;
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.upload-buttons {
|
||
display: flex;
|
||
gap: 15px;
|
||
justify-content: center;
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.upload-btn-style {
|
||
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
|
||
color: white;
|
||
border: none;
|
||
padding: 12px 24px;
|
||
border-radius: 25px;
|
||
cursor: pointer;
|
||
font-size: 0.9em;
|
||
font-weight: 600;
|
||
transition: all 0.3s ease;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
box-shadow: 0 2px 8px rgba(74, 144, 226, 0.2);
|
||
}
|
||
|
||
.upload-btn-style:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 12px rgba(74, 144, 226, 0.3);
|
||
}
|
||
|
||
.upload-btn-style:active {
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
.upload-btn-style:focus {
|
||
outline: 2px solid var(--primary-color);
|
||
outline-offset: 2px;
|
||
}
|
||
|
||
.upload-zone:hover {
|
||
background: linear-gradient(145deg, rgba(255,255,255,1) 0%, rgba(74, 144, 226, 0.1) 100%);
|
||
border-color: var(--primary-color);
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 12px rgba(74, 144, 226, 0.1);
|
||
}
|
||
|
||
.upload-zone.drag-over {
|
||
border-color: var(--primary-color);
|
||
background-color: rgba(74, 144, 226, 0.1);
|
||
transform: scale(1.02);
|
||
}
|
||
|
||
.upload-icon {
|
||
background: rgba(74, 144, 226, 0.1);
|
||
width: 80px;
|
||
height: 80px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 50%;
|
||
margin: 0 auto 20px;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.upload-zone:hover .upload-icon {
|
||
background: rgba(74, 144, 226, 0.2);
|
||
transform: scale(1.1);
|
||
}
|
||
|
||
.file-input {
|
||
position: absolute;
|
||
width: 1px;
|
||
height: 1px;
|
||
opacity: 0;
|
||
overflow: hidden;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.files-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||
gap: 24px;
|
||
padding: 24px 0;
|
||
max-width: 1400px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.file-card {
|
||
background: var(--card-bg);
|
||
border-radius: 16px;
|
||
padding: 24px;
|
||
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
|
||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||
border: 1px solid rgba(0,0,0,0.06);
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.file-card::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
height: 4px;
|
||
background: linear-gradient(90deg, var(--primary-color) 0%, var(--secondary-color) 100%);
|
||
opacity: 0;
|
||
transition: opacity 0.3s ease;
|
||
}
|
||
|
||
.file-card:hover {
|
||
transform: translateY(-6px) scale(1.02);
|
||
box-shadow: 0 12px 32px rgba(0,0,0,0.15);
|
||
border-color: var(--primary-color);
|
||
}
|
||
|
||
.file-card:hover::before {
|
||
opacity: 1;
|
||
}
|
||
|
||
.file-icon {
|
||
background: linear-gradient(145deg, rgba(74, 144, 226, 0.1) 0%, rgba(74, 144, 226, 0.2) 100%);
|
||
border-radius: 12px;
|
||
transition: all 0.3s ease;
|
||
font-size: 24px;
|
||
color: var(--primary-color);
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.file-card:hover .file-icon {
|
||
transform: scale(1.1) rotate(5deg);
|
||
}
|
||
|
||
.file-name {
|
||
font-weight: 700;
|
||
font-size: 1.1em;
|
||
margin-bottom: 8px;
|
||
word-break: break-word;
|
||
color: #2c3e50;
|
||
line-height: 1.3;
|
||
}
|
||
|
||
.file-info {
|
||
color: #666;
|
||
font-size: 0.9em;
|
||
line-height: 1.5;
|
||
margin-top: 16px;
|
||
}
|
||
|
||
.file-info div {
|
||
margin-bottom: 6px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.file-info i {
|
||
width: 16px;
|
||
text-align: center;
|
||
color: var(--primary-color);
|
||
opacity: 0.7;
|
||
}
|
||
|
||
.file-actions {
|
||
margin-top: 20px;
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 12px;
|
||
justify-content: center;
|
||
padding: 0 10px;
|
||
}
|
||
|
||
.download-btn, .delete-btn, .move-btn, .rename-btn {
|
||
flex: 1;
|
||
min-width: 100px;
|
||
max-width: 140px;
|
||
padding: 12px 16px;
|
||
border: none;
|
||
border-radius: 12px;
|
||
font-weight: 600;
|
||
font-size: 0.9em;
|
||
letter-spacing: 0.3px;
|
||
text-decoration: none;
|
||
text-align: center;
|
||
cursor: pointer;
|
||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||
position: relative;
|
||
overflow: hidden;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 8px;
|
||
box-shadow: 0 2px 8px rgba(0,0,0,0.12);
|
||
}
|
||
|
||
.download-btn {
|
||
background: linear-gradient(135deg, #4285f4 0%, #34a853 100%);
|
||
color: white;
|
||
}
|
||
|
||
.delete-btn {
|
||
background: linear-gradient(135deg, #ea4335 0%, #d33b2c 100%);
|
||
color: white;
|
||
}
|
||
|
||
.download-btn:hover, .delete-btn:hover, .move-btn:hover, .rename-btn:hover {
|
||
transform: translateY(-3px) scale(1.02);
|
||
box-shadow: 0 8px 20px rgba(0,0,0,0.2);
|
||
}
|
||
|
||
.download-btn:active, .delete-btn:active, .move-btn:active, .rename-btn:active {
|
||
transform: translateY(-1px) scale(0.98);
|
||
transition: all 0.1s ease;
|
||
}
|
||
|
||
/* 按钮图标优化 */
|
||
.file-actions button i,
|
||
.file-actions a i {
|
||
font-size: 1em;
|
||
margin-right: 6px;
|
||
}
|
||
|
||
.progress-bar {
|
||
height: 4px;
|
||
width: 100%;
|
||
background: #eee;
|
||
border-radius: 2px;
|
||
margin-top: 10px;
|
||
overflow: hidden;
|
||
display: none;
|
||
}
|
||
|
||
.progress {
|
||
height: 100%;
|
||
background: var(--primary-color);
|
||
width: 0%;
|
||
transition: width 0.3s ease;
|
||
}
|
||
|
||
@keyframes fadeIn {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(20px);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
|
||
@keyframes spin {
|
||
0% { transform: rotate(0deg); }
|
||
100% { transform: rotate(360deg); }
|
||
}
|
||
|
||
.loading {
|
||
animation: spin 1s linear infinite;
|
||
}
|
||
|
||
.upload-text {
|
||
margin-top: 10px;
|
||
color: #666;
|
||
}
|
||
|
||
.toast {
|
||
position: fixed;
|
||
bottom: 20px;
|
||
right: 20px;
|
||
padding: 15px 25px;
|
||
background: #333;
|
||
color: white;
|
||
border-radius: var(--border-radius);
|
||
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
|
||
display: none;
|
||
animation: slideIn 0.3s ease-out;
|
||
}
|
||
|
||
@keyframes slideIn {
|
||
from {
|
||
transform: translateX(100%);
|
||
}
|
||
to {
|
||
transform: translateX(0);
|
||
}
|
||
}
|
||
|
||
.file-actions {
|
||
display: flex;
|
||
gap: 10px;
|
||
margin-top: 15px;
|
||
}
|
||
|
||
.file-card {
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.file-card:hover .file-actions {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
|
||
/* 添加删除确认对话框样式 */
|
||
.dialog-overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 1000;
|
||
}
|
||
|
||
.dialog {
|
||
background: white;
|
||
padding: 20px;
|
||
border-radius: var(--border-radius);
|
||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||
max-width: 400px;
|
||
width: 90%;
|
||
}
|
||
|
||
.dialog-buttons {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 10px;
|
||
margin-top: 20px;
|
||
}
|
||
|
||
/* 添加移动端适配样式 */
|
||
@media (max-width: 768px) {
|
||
body {
|
||
padding: 10px;
|
||
}
|
||
|
||
/* 导航栏优化 */
|
||
.nav-bar {
|
||
padding: 18px 15px;
|
||
gap: 16px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.nav-top-row {
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
text-align: center;
|
||
}
|
||
|
||
.nav-bar h1 {
|
||
font-size: 1.4em;
|
||
justify-content: center;
|
||
}
|
||
|
||
.nav-bar h1 img {
|
||
height: 28px !important;
|
||
}
|
||
|
||
.nav-bottom-row {
|
||
flex-direction: column;
|
||
gap: 16px;
|
||
padding-top: 12px;
|
||
}
|
||
|
||
.nav-bottom-row::before {
|
||
display: none;
|
||
}
|
||
|
||
/* 移动端开关组合 */
|
||
.mobile-switches {
|
||
gap: 10px;
|
||
justify-content: center;
|
||
}
|
||
|
||
.quick-upload-switch {
|
||
flex: 1;
|
||
max-width: 160px;
|
||
padding: 10px 14px;
|
||
}
|
||
|
||
.nav-links {
|
||
justify-content: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.nav-links a {
|
||
padding: 10px 16px;
|
||
font-size: 0.9em;
|
||
}
|
||
|
||
.nav-links {
|
||
display: flex;
|
||
justify-content: center;
|
||
flex-wrap: wrap;
|
||
gap: 10px;
|
||
width: 100%;
|
||
}
|
||
|
||
.nav-links a {
|
||
margin: 0;
|
||
font-size: 0.9em;
|
||
padding: 8px 12px;
|
||
flex: 1;
|
||
min-width: 100px;
|
||
text-align: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.user-info {
|
||
justify-content: center;
|
||
margin: 0;
|
||
padding: 10px;
|
||
background: rgba(74, 144, 226, 0.1);
|
||
border-radius: 12px;
|
||
}
|
||
|
||
.user-avatar {
|
||
width: 35px;
|
||
height: 35px;
|
||
}
|
||
|
||
.user-details {
|
||
align-items: center;
|
||
text-align: center;
|
||
}
|
||
|
||
/* 上传区域优化 */
|
||
.upload-section {
|
||
padding: 15px;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.upload-zone {
|
||
padding: 20px 15px;
|
||
margin: 10px 0;
|
||
}
|
||
|
||
.upload-icon {
|
||
font-size: 36px;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.upload-zone h2 {
|
||
font-size: 1.1em;
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
.upload-text {
|
||
font-size: 0.9em;
|
||
}
|
||
|
||
/* 文件列表优化 */
|
||
.files-grid {
|
||
gap: 15px;
|
||
padding: 10px;
|
||
}
|
||
|
||
.file-card {
|
||
margin: 0;
|
||
padding: 15px;
|
||
}
|
||
|
||
.file-name {
|
||
font-size: 0.95em;
|
||
}
|
||
|
||
.file-info {
|
||
font-size: 0.85em;
|
||
}
|
||
|
||
/* 文件操作按钮优化 */
|
||
.file-actions {
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
margin-top: 16px;
|
||
}
|
||
|
||
.download-btn, .delete-btn, .move-btn, .rename-btn {
|
||
flex: none;
|
||
width: 100%;
|
||
max-width: none;
|
||
min-width: auto;
|
||
padding: 14px 16px;
|
||
font-size: 0.95em;
|
||
}
|
||
|
||
/* Toast 提示优化 */
|
||
.toast {
|
||
left: 50%;
|
||
right: auto;
|
||
transform: translateX(-50%);
|
||
width: 90%;
|
||
max-width: 300px;
|
||
text-align: center;
|
||
font-size: 0.9em;
|
||
padding: 12px 20px;
|
||
}
|
||
}
|
||
|
||
/* 超小屏幕优化 */
|
||
@media (max-width: 360px) {
|
||
.nav-bar h1 {
|
||
font-size: 1.2em;
|
||
}
|
||
|
||
.upload-zone {
|
||
padding: 15px 10px;
|
||
}
|
||
|
||
.file-card {
|
||
padding: 12px;
|
||
}
|
||
}
|
||
|
||
/* 添加安全区域适配 */
|
||
@supports (padding: max(0px)) {
|
||
body {
|
||
padding-top: max(20px, env(safe-area-inset-top));
|
||
padding-bottom: max(20px, env(safe-area-inset-bottom));
|
||
padding-left: max(20px, env(safe-area-inset-left));
|
||
padding-right: max(20px, env(safe-area-inset-right));
|
||
}
|
||
}
|
||
|
||
/* 添加触摸反馈 */
|
||
@media (hover: none) {
|
||
.file-card:active {
|
||
transform: scale(0.98);
|
||
}
|
||
|
||
.download-btn:active, .delete-btn:active {
|
||
transform: scale(0.95);
|
||
}
|
||
|
||
.upload-zone:active {
|
||
background-color: rgba(74, 144, 226, 0.1);
|
||
}
|
||
}
|
||
|
||
/* 优化拖放区域 */
|
||
.upload-zone {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-height: 200px;
|
||
}
|
||
|
||
/* 添加文件上传进度显示 */
|
||
.upload-progress {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
height: 3px;
|
||
background: var(--primary-color);
|
||
transform-origin: left;
|
||
transform: scaleX(0);
|
||
transition: transform 0.3s ease;
|
||
z-index: 1000;
|
||
}
|
||
|
||
/* 优化文件卡片布局 */
|
||
.file-card {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.file-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.file-content {
|
||
flex: 1;
|
||
}
|
||
|
||
/* 添加加载动画 */
|
||
.loading-overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(255, 255, 255, 0.8);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 1000;
|
||
backdrop-filter: blur(5px);
|
||
}
|
||
|
||
.loading-spinner {
|
||
width: 40px;
|
||
height: 40px;
|
||
border: 3px solid #f3f3f3;
|
||
border-top: 3px solid var(--primary-color);
|
||
border-radius: 50%;
|
||
animation: spin 1s linear infinite;
|
||
}
|
||
|
||
.file-uploader {
|
||
font-size: 0.9em;
|
||
color: #666;
|
||
margin: 5px 0;
|
||
}
|
||
|
||
.file-comment {
|
||
font-size: 0.9em;
|
||
color: #666;
|
||
margin: 5px 0;
|
||
padding: 5px 10px;
|
||
background: rgba(74, 144, 226, 0.1);
|
||
border-radius: 5px;
|
||
word-break: break-word;
|
||
}
|
||
|
||
.file-header {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 18px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.file-content {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.file-icon {
|
||
width: 48px;
|
||
height: 48px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: linear-gradient(135deg, rgba(74, 144, 226, 0.1) 0%, rgba(74, 144, 226, 0.2) 100%);
|
||
border-radius: 12px;
|
||
flex-shrink: 0;
|
||
font-size: 20px;
|
||
color: var(--primary-color);
|
||
box-shadow: 0 2px 8px rgba(74, 144, 226, 0.1);
|
||
position: relative;
|
||
overflow: hidden;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
/* 文件类型图标样式 */
|
||
.file-icon.image {
|
||
background: linear-gradient(135deg, rgba(76, 175, 80, 0.1) 0%, rgba(76, 175, 80, 0.2) 100%);
|
||
color: #4caf50;
|
||
box-shadow: 0 2px 8px rgba(76, 175, 80, 0.1);
|
||
}
|
||
|
||
.file-icon.video {
|
||
background: linear-gradient(135deg, rgba(244, 67, 54, 0.1) 0%, rgba(244, 67, 54, 0.2) 100%);
|
||
color: #f44336;
|
||
box-shadow: 0 2px 8px rgba(244, 67, 54, 0.1);
|
||
}
|
||
|
||
.file-icon.audio {
|
||
background: linear-gradient(135deg, rgba(156, 39, 176, 0.1) 0%, rgba(156, 39, 176, 0.2) 100%);
|
||
color: #9c27b0;
|
||
box-shadow: 0 2px 8px rgba(156, 39, 176, 0.1);
|
||
}
|
||
|
||
.file-icon.pdf {
|
||
background: linear-gradient(135deg, rgba(244, 67, 54, 0.1) 0%, rgba(244, 67, 54, 0.2) 100%);
|
||
color: #f44336;
|
||
box-shadow: 0 2px 8px rgba(244, 67, 54, 0.1);
|
||
}
|
||
|
||
.file-icon.archive {
|
||
background: linear-gradient(135deg, rgba(255, 152, 0, 0.1) 0%, rgba(255, 152, 0, 0.2) 100%);
|
||
color: #ff9800;
|
||
box-shadow: 0 2px 8px rgba(255, 152, 0, 0.1);
|
||
}
|
||
|
||
.file-icon.code {
|
||
background: linear-gradient(135deg, rgba(96, 125, 139, 0.1) 0%, rgba(96, 125, 139, 0.2) 100%);
|
||
color: #607d8b;
|
||
box-shadow: 0 2px 8px rgba(96, 125, 139, 0.1);
|
||
}
|
||
|
||
.file-icon.document {
|
||
background: linear-gradient(135deg, rgba(33, 150, 243, 0.1) 0%, rgba(33, 150, 243, 0.2) 100%);
|
||
color: #2196f3;
|
||
box-shadow: 0 2px 8px rgba(33, 150, 243, 0.1);
|
||
}
|
||
|
||
/* 文件预览缩略图 */
|
||
.file-thumbnail {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
border-radius: 12px;
|
||
opacity: 0;
|
||
transition: opacity 0.3s ease;
|
||
}
|
||
|
||
.file-thumbnail.loaded {
|
||
opacity: 1;
|
||
}
|
||
|
||
.file-icon:hover {
|
||
transform: scale(1.05);
|
||
}
|
||
|
||
.file-icon.has-preview:hover .file-thumbnail {
|
||
opacity: 0.8;
|
||
}
|
||
|
||
/* 播放按钮覆盖层 */
|
||
.play-overlay {
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
background: rgba(0, 0, 0, 0.6);
|
||
color: white;
|
||
border: none;
|
||
border-radius: 50%;
|
||
width: 24px;
|
||
height: 24px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 10px;
|
||
opacity: 0;
|
||
transition: opacity 0.3s ease;
|
||
cursor: pointer;
|
||
z-index: 2;
|
||
}
|
||
|
||
.file-icon:hover .play-overlay {
|
||
opacity: 1;
|
||
}
|
||
|
||
/* 媒体预览模态框 */
|
||
.media-preview-modal {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.9);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 2000;
|
||
opacity: 0;
|
||
visibility: hidden;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.media-preview-modal.active {
|
||
opacity: 1;
|
||
visibility: visible;
|
||
}
|
||
|
||
.media-preview-content {
|
||
max-width: 90vw;
|
||
max-height: 90vh;
|
||
position: relative;
|
||
background: #fff;
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
.media-preview-content img,
|
||
.media-preview-content video {
|
||
max-width: 100%;
|
||
max-height: 90vh;
|
||
display: block;
|
||
}
|
||
|
||
.media-preview-content audio {
|
||
width: 450px;
|
||
min-width: 350px;
|
||
height: auto;
|
||
background: #f8f9fa;
|
||
border-radius: 8px;
|
||
outline: none;
|
||
}
|
||
|
||
.audio-preview-container {
|
||
padding: 30px;
|
||
background: #fff;
|
||
border-radius: 8px;
|
||
text-align: center;
|
||
min-width: 400px;
|
||
max-width: 500px;
|
||
}
|
||
|
||
.audio-preview-icon {
|
||
font-size: 48px;
|
||
color: #9c27b0;
|
||
margin-bottom: 20px;
|
||
background: linear-gradient(135deg, rgba(156, 39, 176, 0.1) 0%, rgba(156, 39, 176, 0.2) 100%);
|
||
width: 80px;
|
||
height: 80px;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin: 0 auto 20px;
|
||
}
|
||
|
||
.audio-preview-title {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
margin-bottom: 8px;
|
||
word-break: break-word;
|
||
}
|
||
|
||
.audio-preview-details {
|
||
font-size: 14px;
|
||
color: #666;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.audio-preview-player {
|
||
width: 100%;
|
||
margin-top: 20px;
|
||
background: #f8f9fa;
|
||
border-radius: 8px;
|
||
outline: none;
|
||
height: 54px;
|
||
display: block !important;
|
||
}
|
||
|
||
/* 自定义音频控件样式 */
|
||
.audio-preview-player::-webkit-media-controls-panel {
|
||
background-color: #f8f9fa;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.audio-preview-player::-webkit-media-controls-play-button {
|
||
background-color: #9c27b0;
|
||
border-radius: 50%;
|
||
margin: 0 8px;
|
||
}
|
||
|
||
.audio-preview-player::-webkit-media-controls-timeline {
|
||
background-color: rgba(156, 39, 176, 0.2);
|
||
border-radius: 2px;
|
||
margin: 0 8px;
|
||
}
|
||
|
||
.audio-preview-player::-webkit-media-controls-current-time-display,
|
||
.audio-preview-player::-webkit-media-controls-time-remaining-display {
|
||
color: #9c27b0;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.audio-loading {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 20px;
|
||
color: #9c27b0;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.audio-loading i {
|
||
animation: spin 1s linear infinite;
|
||
margin-right: 8px;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.audio-error {
|
||
color: #f44336;
|
||
text-align: center;
|
||
padding: 20px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.audio-error i {
|
||
font-size: 24px;
|
||
margin-bottom: 10px;
|
||
display: block;
|
||
}
|
||
|
||
/* 自定义音频控制按钮 */
|
||
.audio-custom-controls {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 15px;
|
||
margin-top: 15px;
|
||
padding: 15px;
|
||
background: #f8f9fa;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.audio-play-btn {
|
||
width: 50px;
|
||
height: 50px;
|
||
border-radius: 50%;
|
||
background: linear-gradient(135deg, #9c27b0 0%, #7b1fa2 100%);
|
||
border: none;
|
||
color: white;
|
||
font-size: 20px;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: all 0.3s ease;
|
||
box-shadow: 0 2px 8px rgba(156, 39, 176, 0.3);
|
||
}
|
||
|
||
.audio-play-btn:hover {
|
||
background: linear-gradient(135deg, #7b1fa2 0%, #4a148c 100%);
|
||
transform: scale(1.05);
|
||
box-shadow: 0 4px 12px rgba(156, 39, 176, 0.4);
|
||
}
|
||
|
||
.audio-time-display {
|
||
font-family: monospace;
|
||
font-size: 14px;
|
||
color: #666;
|
||
min-width: 100px;
|
||
text-align: center;
|
||
}
|
||
|
||
.audio-volume-control {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.audio-volume-slider {
|
||
width: 80px;
|
||
height: 4px;
|
||
background: #ddd;
|
||
border-radius: 2px;
|
||
outline: none;
|
||
-webkit-appearance: none;
|
||
appearance: none;
|
||
}
|
||
|
||
.audio-volume-slider::-webkit-slider-thumb {
|
||
-webkit-appearance: none;
|
||
width: 16px;
|
||
height: 16px;
|
||
border-radius: 50%;
|
||
background: #9c27b0;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.media-close-btn {
|
||
position: absolute;
|
||
top: 10px;
|
||
right: 10px;
|
||
background: rgba(0, 0, 0, 0.7);
|
||
color: white;
|
||
border: none;
|
||
border-radius: 50%;
|
||
width: 40px;
|
||
height: 40px;
|
||
cursor: pointer;
|
||
font-size: 20px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 2001;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.media-close-btn:hover {
|
||
background: rgba(0, 0, 0, 0.9);
|
||
}
|
||
|
||
.media-info {
|
||
position: absolute;
|
||
bottom: 0;
|
||
left: 0;
|
||
right: 0;
|
||
background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
|
||
color: white;
|
||
padding: 20px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.media-info h3 {
|
||
margin: 0 0 5px 0;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.media-info p {
|
||
margin: 0;
|
||
opacity: 0.8;
|
||
}
|
||
|
||
/* 移动端媒体预览优化 */
|
||
@media (max-width: 768px) {
|
||
.media-preview-content {
|
||
max-width: 95vw;
|
||
max-height: 95vh;
|
||
margin: 20px;
|
||
}
|
||
|
||
.media-preview-content audio {
|
||
width: 100%;
|
||
min-width: 280px;
|
||
padding: 15px;
|
||
}
|
||
|
||
.audio-preview-container {
|
||
padding: 20px;
|
||
min-width: 300px;
|
||
max-width: 90vw;
|
||
margin: 10px;
|
||
}
|
||
|
||
.audio-preview-icon {
|
||
width: 60px;
|
||
height: 60px;
|
||
font-size: 36px;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.audio-preview-title {
|
||
font-size: 14px;
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
.audio-preview-details {
|
||
font-size: 12px;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.audio-preview-player {
|
||
margin-top: 15px;
|
||
}
|
||
|
||
.audio-custom-controls {
|
||
gap: 10px;
|
||
padding: 12px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.audio-play-btn {
|
||
width: 45px;
|
||
height: 45px;
|
||
font-size: 18px;
|
||
}
|
||
|
||
.audio-time-display {
|
||
font-size: 12px;
|
||
min-width: 80px;
|
||
}
|
||
|
||
.audio-volume-slider {
|
||
width: 60px;
|
||
}
|
||
|
||
.media-close-btn {
|
||
width: 44px;
|
||
height: 44px;
|
||
font-size: 18px;
|
||
top: 5px;
|
||
right: 5px;
|
||
}
|
||
|
||
.media-info {
|
||
padding: 15px;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.media-info h3 {
|
||
font-size: 14px;
|
||
}
|
||
|
||
.file-icon {
|
||
width: 40px;
|
||
height: 40px;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.play-overlay {
|
||
width: 20px;
|
||
height: 20px;
|
||
font-size: 8px;
|
||
}
|
||
}
|
||
|
||
/* 移动端优化 */
|
||
@media (max-width: 768px) {
|
||
.file-header {
|
||
gap: 10px;
|
||
}
|
||
|
||
.file-icon {
|
||
width: 35px;
|
||
height: 35px;
|
||
}
|
||
|
||
.file-uploader,
|
||
.file-comment {
|
||
font-size: 0.85em;
|
||
}
|
||
}
|
||
|
||
.upload-preview {
|
||
margin-top: 20px;
|
||
background: white;
|
||
border-radius: var(--border-radius);
|
||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.preview-header {
|
||
padding: 15px 20px;
|
||
background: #f8f9fa;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
border-bottom: 1px solid #eee;
|
||
}
|
||
|
||
.preview-header h3 {
|
||
margin: 0;
|
||
color: #333;
|
||
font-size: 1.1em;
|
||
}
|
||
|
||
.clear-btn {
|
||
background: none;
|
||
border: none;
|
||
color: #666;
|
||
cursor: pointer;
|
||
padding: 5px;
|
||
border-radius: 50%;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.clear-btn:hover {
|
||
background: rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.preview-files {
|
||
max-height: 300px;
|
||
overflow-y: auto;
|
||
padding: 10px;
|
||
}
|
||
|
||
.preview-file {
|
||
position: relative;
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 15px;
|
||
border: 1px solid #eee;
|
||
border-radius: 8px;
|
||
margin-bottom: 10px;
|
||
background: #fff;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.preview-file:hover {
|
||
border-color: var(--primary-color);
|
||
box-shadow: 0 2px 8px rgba(74, 144, 226, 0.1);
|
||
}
|
||
|
||
.remove-file-btn {
|
||
position: absolute;
|
||
right: 10px;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
background: none;
|
||
border: none;
|
||
color: #666;
|
||
cursor: pointer;
|
||
padding: 5px;
|
||
border-radius: 50%;
|
||
opacity: 0;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.preview-file:hover .remove-file-btn {
|
||
opacity: 1;
|
||
}
|
||
|
||
.remove-file-btn:hover {
|
||
background: rgba(220, 53, 69, 0.1);
|
||
color: #dc3545;
|
||
}
|
||
|
||
.preview-file-icon {
|
||
width: 40px;
|
||
height: 40px;
|
||
background: rgba(74, 144, 226, 0.1);
|
||
border-radius: 8px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: var(--primary-color);
|
||
margin-right: 15px;
|
||
flex-shrink: 0;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
/* 预览文件图标类型样式 */
|
||
.preview-file-icon.image {
|
||
background: rgba(76, 175, 80, 0.1);
|
||
color: #4caf50;
|
||
}
|
||
|
||
.preview-file-icon.video {
|
||
background: rgba(244, 67, 54, 0.1);
|
||
color: #f44336;
|
||
}
|
||
|
||
.preview-file-icon.audio {
|
||
background: rgba(156, 39, 176, 0.1);
|
||
color: #9c27b0;
|
||
}
|
||
|
||
.preview-file-icon.pdf {
|
||
background: rgba(244, 67, 54, 0.1);
|
||
color: #f44336;
|
||
}
|
||
|
||
.preview-file-icon.archive {
|
||
background: rgba(255, 152, 0, 0.1);
|
||
color: #ff9800;
|
||
}
|
||
|
||
.preview-file-icon.code {
|
||
background: rgba(96, 125, 139, 0.1);
|
||
color: #607d8b;
|
||
}
|
||
|
||
.preview-file-icon.document {
|
||
background: rgba(33, 150, 243, 0.1);
|
||
color: #2196f3;
|
||
}
|
||
|
||
.preview-file-info {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.preview-file-name {
|
||
font-weight: 500;
|
||
margin-bottom: 5px;
|
||
word-break: break-word;
|
||
}
|
||
|
||
.preview-file-size {
|
||
font-size: 0.85em;
|
||
color: #666;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.preview-file-type {
|
||
font-size: 0.8em;
|
||
color: var(--primary-color);
|
||
font-weight: 500;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.preview-file-name-container {
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.preview-file-name-container label {
|
||
display: block;
|
||
font-size: 0.85em;
|
||
font-weight: 500;
|
||
color: #333;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.preview-file-name-input {
|
||
width: 100%;
|
||
padding: 8px 10px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 4px;
|
||
font-size: 0.9em;
|
||
transition: all 0.3s ease;
|
||
background: #fff;
|
||
}
|
||
|
||
.preview-file-name-input:focus {
|
||
border-color: var(--primary-color);
|
||
outline: none;
|
||
box-shadow: 0 0 0 2px rgba(74, 144, 226, 0.2);
|
||
}
|
||
|
||
.preview-file-name-input:invalid,
|
||
.preview-file-name-input.invalid {
|
||
border-color: #dc3545;
|
||
background-color: rgba(220, 53, 69, 0.05);
|
||
}
|
||
|
||
.filename-error {
|
||
font-size: 0.75em;
|
||
color: #dc3545;
|
||
margin-top: 4px;
|
||
display: block;
|
||
animation: slideDown 0.3s ease;
|
||
}
|
||
|
||
@keyframes slideDown {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(-5px);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
|
||
.preview-file-path {
|
||
font-size: 0.8em;
|
||
color: #666;
|
||
margin-bottom: 6px;
|
||
padding: 4px 8px;
|
||
background: rgba(74, 144, 226, 0.05);
|
||
border-radius: 3px;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.preview-file-comment {
|
||
width: 100%;
|
||
padding: 8px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 4px;
|
||
font-size: 0.9em;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.preview-file-comment:focus {
|
||
border-color: var(--primary-color);
|
||
outline: none;
|
||
}
|
||
|
||
.preview-actions {
|
||
padding: 15px;
|
||
text-align: right;
|
||
background: #f8f9fa;
|
||
border-top: 1px solid #eee;
|
||
}
|
||
|
||
.upload-all-btn {
|
||
background: var(--primary-color);
|
||
color: white;
|
||
border: none;
|
||
padding: 12px 24px;
|
||
border-radius: 24px;
|
||
cursor: pointer;
|
||
font-size: 0.95em;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.upload-all-btn:hover {
|
||
background: var(--secondary-color);
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
/* 移动端优化 */
|
||
@media (max-width: 768px) {
|
||
.preview-file {
|
||
padding: 12px;
|
||
}
|
||
|
||
.preview-file-icon {
|
||
width: 35px;
|
||
height: 35px;
|
||
}
|
||
|
||
.preview-file-name {
|
||
font-size: 0.9em;
|
||
}
|
||
|
||
.preview-file-size {
|
||
font-size: 0.8em;
|
||
}
|
||
|
||
.preview-file-comment {
|
||
padding: 6px;
|
||
font-size: 0.85em;
|
||
}
|
||
|
||
.preview-file-name-input {
|
||
padding: 6px 8px;
|
||
font-size: 0.85em;
|
||
}
|
||
|
||
.preview-file-name-container label {
|
||
font-size: 0.8em;
|
||
}
|
||
|
||
.filename-error {
|
||
font-size: 0.7em;
|
||
}
|
||
|
||
.remove-file-btn {
|
||
opacity: 1;
|
||
padding: 8px;
|
||
}
|
||
}
|
||
|
||
.nav-controls {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 20px;
|
||
}
|
||
|
||
.quick-upload-switch {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
cursor: pointer;
|
||
padding: 12px 18px;
|
||
border-radius: 18px;
|
||
background: rgba(74, 144, 226, 0.05);
|
||
border: 1px solid rgba(74, 144, 226, 0.1);
|
||
transition: all 0.3s ease;
|
||
position: relative;
|
||
white-space: nowrap;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.quick-upload-switch:hover {
|
||
background: rgba(74, 144, 226, 0.08);
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 2px 8px rgba(74, 144, 226, 0.1);
|
||
}
|
||
|
||
.switch-label {
|
||
font-size: 0.85em;
|
||
color: #555;
|
||
font-weight: 500;
|
||
line-height: 1.2;
|
||
user-select: none;
|
||
}
|
||
|
||
.switch-slider {
|
||
position: relative;
|
||
display: inline-block;
|
||
width: 44px;
|
||
height: 22px;
|
||
background: linear-gradient(135deg, #e0e0e0, #f0f0f0);
|
||
border-radius: 22px;
|
||
transition: all 0.3s ease;
|
||
box-shadow: inset 0 1px 3px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.switch-slider::after {
|
||
content: '';
|
||
position: absolute;
|
||
width: 18px;
|
||
height: 18px;
|
||
border-radius: 50%;
|
||
background: linear-gradient(135deg, #fff, #f8f9fa);
|
||
top: 2px;
|
||
left: 2px;
|
||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||
box-shadow: 0 2px 6px rgba(0,0,0,0.15), 0 1px 2px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.quick-upload-switch input {
|
||
display: none;
|
||
}
|
||
|
||
.quick-upload-switch input:checked + .switch-label + .switch-slider {
|
||
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
||
box-shadow: inset 0 1px 3px rgba(0,0,0,0.1), 0 0 10px rgba(74, 144, 226, 0.3);
|
||
}
|
||
|
||
.quick-upload-switch input:checked + .switch-label + .switch-slider::after {
|
||
transform: translateX(22px);
|
||
background: linear-gradient(135deg, #fff, #e3f2fd);
|
||
box-shadow: 0 2px 8px rgba(74, 144, 226, 0.2), 0 1px 3px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.quick-upload-switch input:checked + .switch-label {
|
||
color: var(--primary-color);
|
||
font-weight: 600;
|
||
}
|
||
|
||
/* 移动端适配 */
|
||
@media (max-width: 768px) {
|
||
.nav-controls {
|
||
width: 100%;
|
||
flex-direction: column;
|
||
gap: 15px;
|
||
}
|
||
|
||
.quick-upload-switch {
|
||
width: 100%;
|
||
padding: 12px;
|
||
background: rgba(74, 144, 226, 0.05);
|
||
border-radius: var(--border-radius);
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.switch-label {
|
||
font-size: 0.95em;
|
||
color: #333;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.switch-slider {
|
||
width: 46px;
|
||
height: 24px;
|
||
}
|
||
|
||
.switch-slider::after {
|
||
width: 22px;
|
||
height: 22px;
|
||
}
|
||
|
||
.quick-upload-switch input:checked + .switch-label + .switch-slider::after {
|
||
transform: translateX(22px);
|
||
}
|
||
|
||
/* 添加开关说明文字 */
|
||
.quick-upload-hint {
|
||
font-size: 0.8em;
|
||
color: #666;
|
||
padding: 0 15px;
|
||
margin-top: 4px;
|
||
display: none; /* 默认隐藏 */
|
||
}
|
||
|
||
.quick-upload-switch:active .quick-upload-hint {
|
||
display: block; /* 长按显示提示 */
|
||
}
|
||
|
||
/* 优化导航栏布局 */
|
||
.nav-bar {
|
||
padding: 20px 15px;
|
||
gap: 15px;
|
||
}
|
||
|
||
.nav-top-row {
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
text-align: center;
|
||
}
|
||
|
||
.nav-bar h1 {
|
||
font-size: 1.3em;
|
||
justify-content: center;
|
||
}
|
||
|
||
.nav-bottom-row {
|
||
gap: 20px;
|
||
}
|
||
|
||
.mobile-switches {
|
||
justify-content: center;
|
||
gap: 15px;
|
||
}
|
||
|
||
.quick-upload-switch {
|
||
padding: 10px 14px;
|
||
}
|
||
|
||
.nav-links {
|
||
gap: 14px;
|
||
}
|
||
|
||
.nav-links {
|
||
width: 100%;
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: 15px;
|
||
padding-top: 10px;
|
||
border-top: 1px solid rgba(0,0,0,0.05);
|
||
}
|
||
|
||
.nav-links a {
|
||
padding: 8px 15px;
|
||
background: rgba(74, 144, 226, 0.1);
|
||
border-radius: 20px;
|
||
font-size: 0.9em;
|
||
}
|
||
|
||
.user-info {
|
||
justify-content: center;
|
||
padding: 8px 12px;
|
||
background: rgba(74, 144, 226, 0.1);
|
||
border-radius: 12px;
|
||
margin: 0;
|
||
}
|
||
|
||
/* 添加快速上传状态提示 */
|
||
.upload-status {
|
||
position: fixed;
|
||
bottom: 20px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
background: rgba(0, 0, 0, 0.8);
|
||
color: white;
|
||
padding: 10px 20px;
|
||
border-radius: 20px;
|
||
font-size: 0.9em;
|
||
z-index: 1000;
|
||
display: none;
|
||
animation: slideUp 0.3s ease;
|
||
}
|
||
|
||
@keyframes slideUp {
|
||
from {
|
||
transform: translate(-50%, 100%);
|
||
opacity: 0;
|
||
}
|
||
to {
|
||
transform: translate(-50%, 0);
|
||
opacity: 1;
|
||
}
|
||
}
|
||
|
||
/* 优化上传区域 */
|
||
.upload-zone {
|
||
padding: 25px 15px;
|
||
margin: 10px 0;
|
||
}
|
||
|
||
.upload-buttons {
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
margin-top: 15px;
|
||
}
|
||
|
||
.upload-btn-style {
|
||
width: 100%;
|
||
justify-content: center;
|
||
padding: 14px 20px;
|
||
font-size: 1em;
|
||
}
|
||
|
||
.upload-icon {
|
||
font-size: 36px;
|
||
}
|
||
|
||
.upload-text {
|
||
font-size: 0.85em;
|
||
margin-top: 8px;
|
||
color: #666;
|
||
}
|
||
|
||
/* 快速上传模式下的样式 */
|
||
.quick-upload-mode .upload-zone {
|
||
background: rgba(74, 144, 226, 0.05);
|
||
border-style: solid;
|
||
border-color: var(--primary-color);
|
||
}
|
||
|
||
.quick-upload-mode .upload-icon {
|
||
color: var(--primary-color);
|
||
animation: pulse 1.5s infinite;
|
||
}
|
||
|
||
@keyframes pulse {
|
||
0% { transform: scale(1); }
|
||
50% { transform: scale(1.1); }
|
||
100% { transform: scale(1); }
|
||
}
|
||
}
|
||
|
||
/* 超小屏幕优化 */
|
||
@media (max-width: 360px) {
|
||
.nav-bar h1 {
|
||
font-size: 1.2em;
|
||
}
|
||
|
||
.quick-upload-switch {
|
||
padding: 8px 12px;
|
||
}
|
||
|
||
.switch-label {
|
||
font-size: 0.85em;
|
||
}
|
||
|
||
.nav-links a {
|
||
padding: 6px 12px;
|
||
font-size: 0.85em;
|
||
}
|
||
|
||
.user-info {
|
||
padding: 6px 10px;
|
||
}
|
||
|
||
.user-avatar {
|
||
width: 30px;
|
||
height: 30px;
|
||
}
|
||
|
||
.user-name, .user-role {
|
||
font-size: 0.8em;
|
||
}
|
||
}
|
||
|
||
/* 添加进度条相关样式 */
|
||
.upload-progress-container {
|
||
margin-top: 10px;
|
||
width: 100%;
|
||
display: none;
|
||
}
|
||
|
||
.file-progress {
|
||
background: #f8f9fa;
|
||
border-radius: 8px;
|
||
padding: 12px;
|
||
margin-bottom: 10px;
|
||
border: 1px solid #eee;
|
||
}
|
||
|
||
.progress-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.progress-filename {
|
||
font-weight: 500;
|
||
color: #333;
|
||
flex: 1;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
margin-right: 10px;
|
||
}
|
||
|
||
.progress-stats {
|
||
font-size: 0.9em;
|
||
color: #666;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.progress-bar-outer {
|
||
width: 100%;
|
||
height: 6px;
|
||
background: #eee;
|
||
border-radius: 3px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.progress-bar-inner {
|
||
width: 0%;
|
||
height: 100%;
|
||
background: var(--primary-color);
|
||
border-radius: 3px;
|
||
transition: width 0.3s ease;
|
||
}
|
||
|
||
.upload-speed {
|
||
font-size: 0.85em;
|
||
color: #666;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
/* 快速上传模式的进度条样式 */
|
||
.quick-upload .file-progress {
|
||
position: fixed;
|
||
bottom: 20px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
width: 90%;
|
||
max-width: 400px;
|
||
background: white;
|
||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||
z-index: 1000;
|
||
}
|
||
|
||
.upload-progress-container {
|
||
margin-top: 15px;
|
||
padding: 10px;
|
||
background: #fff;
|
||
border-radius: 8px;
|
||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.file-progress {
|
||
margin-bottom: 15px;
|
||
animation: slideIn 0.3s ease;
|
||
}
|
||
|
||
@keyframes slideIn {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(-10px);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
|
||
.cancel-upload-btn {
|
||
background: none;
|
||
border: none;
|
||
color: #666;
|
||
cursor: pointer;
|
||
padding: 4px;
|
||
margin-left: 8px;
|
||
border-radius: 50%;
|
||
width: 24px;
|
||
height: 24px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.cancel-upload-btn:hover {
|
||
background: rgba(220, 53, 69, 0.1);
|
||
color: #dc3545;
|
||
}
|
||
|
||
.upload-cancelled .progress-bar-inner {
|
||
background: #dc3545;
|
||
}
|
||
|
||
.upload-cancelled .progress-bar-outer {
|
||
opacity: 0.5;
|
||
}
|
||
|
||
.file-progress {
|
||
position: relative;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.upload-cancelled {
|
||
opacity: 0.7;
|
||
}
|
||
|
||
.progress-stats {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.upload-cancelled {
|
||
opacity: 0.7;
|
||
transform: translateX(100%);
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.file-progress {
|
||
position: relative;
|
||
transition: all 0.3s ease;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.upload-all-btn {
|
||
background: var(--primary-color);
|
||
color: white;
|
||
border: none;
|
||
padding: 12px 24px;
|
||
border-radius: 24px;
|
||
cursor: pointer;
|
||
font-size: 0.95em;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.upload-all-btn:not(:disabled):hover {
|
||
background: var(--secondary-color);
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
.upload-all-btn:disabled {
|
||
background: #ccc;
|
||
cursor: not-allowed;
|
||
transform: none;
|
||
}
|
||
|
||
.upload-all-btn i {
|
||
transition: transform 0.3s ease;
|
||
}
|
||
|
||
.upload-all-btn:not(:disabled):hover i {
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.batch-rename-btn {
|
||
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
|
||
color: white;
|
||
border: none;
|
||
padding: 12px 24px;
|
||
border-radius: 24px;
|
||
cursor: pointer;
|
||
font-size: 0.95em;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
transition: all 0.3s ease;
|
||
margin-right: 10px;
|
||
}
|
||
|
||
.batch-rename-btn:hover {
|
||
background: linear-gradient(135deg, #218838 0%, #1e7e34 100%);
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
.rename-preview {
|
||
max-height: 200px;
|
||
overflow-y: auto;
|
||
border: 1px solid #ddd;
|
||
border-radius: 4px;
|
||
padding: 10px;
|
||
background: #f8f9fa;
|
||
font-family: monospace;
|
||
font-size: 0.9em;
|
||
}
|
||
|
||
.rename-preview-item {
|
||
padding: 2px 0;
|
||
border-bottom: 1px solid #eee;
|
||
}
|
||
|
||
.rename-preview-item:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.rename-original {
|
||
color: #666;
|
||
}
|
||
|
||
.rename-arrow {
|
||
color: var(--primary-color);
|
||
margin: 0 8px;
|
||
}
|
||
|
||
.rename-new {
|
||
color: #28a745;
|
||
font-weight: 500;
|
||
}
|
||
|
||
/* 文件管理工具栏样式 */
|
||
.file-toolbar {
|
||
background: var(--card-bg);
|
||
padding: 20px;
|
||
border-radius: var(--border-radius);
|
||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||
margin-bottom: 20px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 15px;
|
||
}
|
||
|
||
.toolbar-left {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 15px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.toolbar-right {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 15px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.breadcrumb {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
font-size: 0.9em;
|
||
}
|
||
|
||
.breadcrumb a {
|
||
color: var(--primary-color);
|
||
text-decoration: none;
|
||
padding: 5px 10px;
|
||
border-radius: 5px;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.breadcrumb a:hover {
|
||
background: rgba(74, 144, 226, 0.1);
|
||
}
|
||
|
||
.breadcrumb .separator {
|
||
color: #666;
|
||
margin: 0 5px;
|
||
}
|
||
|
||
.search-box {
|
||
position: relative;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.search-box input {
|
||
padding: 10px 40px 10px 15px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 25px;
|
||
font-size: 0.9em;
|
||
width: 250px;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.search-box input:focus {
|
||
border-color: var(--primary-color);
|
||
outline: none;
|
||
box-shadow: 0 0 0 2px rgba(74, 144, 226, 0.2);
|
||
}
|
||
|
||
.search-box i {
|
||
position: absolute;
|
||
right: 15px;
|
||
color: #666;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.sort-controls {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.sort-controls select {
|
||
padding: 8px 12px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 5px;
|
||
background: white;
|
||
font-size: 0.9em;
|
||
}
|
||
|
||
.sort-order-btn, .layout-toggle-btn {
|
||
background: none;
|
||
border: 1px solid #ddd;
|
||
padding: 8px 12px;
|
||
border-radius: 5px;
|
||
cursor: pointer;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.sort-order-btn:hover, .layout-toggle-btn:hover {
|
||
background: rgba(74, 144, 226, 0.1);
|
||
border-color: var(--primary-color);
|
||
}
|
||
|
||
.layout-toggle-btn.active {
|
||
background: var(--primary-color);
|
||
color: white;
|
||
border-color: var(--primary-color);
|
||
}
|
||
|
||
.btn {
|
||
padding: 10px 20px;
|
||
border: none;
|
||
border-radius: 5px;
|
||
cursor: pointer;
|
||
font-size: 0.9em;
|
||
transition: all 0.3s;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.btn-primary {
|
||
background: var(--primary-color);
|
||
color: white;
|
||
}
|
||
|
||
.btn-primary:hover {
|
||
background: var(--secondary-color);
|
||
}
|
||
|
||
.btn-secondary {
|
||
background: #6c757d;
|
||
color: white;
|
||
}
|
||
|
||
.btn-secondary:hover {
|
||
background: #5a6268;
|
||
}
|
||
|
||
/* 模态框样式 */
|
||
.modal-overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 1000;
|
||
backdrop-filter: blur(5px);
|
||
}
|
||
|
||
.modal {
|
||
background: white;
|
||
border-radius: var(--border-radius);
|
||
box-shadow: 0 10px 25px rgba(0,0,0,0.2);
|
||
max-width: 500px;
|
||
width: 90%;
|
||
max-height: 80vh;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.modal-header {
|
||
padding: 20px;
|
||
border-bottom: 1px solid #eee;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.modal-header h3 {
|
||
margin: 0;
|
||
color: #333;
|
||
}
|
||
|
||
.close-btn {
|
||
background: none;
|
||
border: none;
|
||
font-size: 1.2em;
|
||
cursor: pointer;
|
||
color: #666;
|
||
padding: 5px;
|
||
border-radius: 50%;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.close-btn:hover {
|
||
background: rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.modal-body {
|
||
padding: 20px;
|
||
}
|
||
|
||
.modal-footer {
|
||
padding: 20px;
|
||
border-top: 1px solid #eee;
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 10px;
|
||
}
|
||
|
||
.form-group {
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.form-group label {
|
||
display: block;
|
||
margin-bottom: 5px;
|
||
font-weight: 500;
|
||
color: #333;
|
||
}
|
||
|
||
.form-group input,
|
||
.form-group select {
|
||
width: 100%;
|
||
padding: 10px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 5px;
|
||
font-size: 0.9em;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.form-group input:focus,
|
||
.form-group select:focus {
|
||
border-color: var(--primary-color);
|
||
outline: none;
|
||
box-shadow: 0 0 0 2px rgba(74, 144, 226, 0.2);
|
||
}
|
||
|
||
/* 文件卡片增强 */
|
||
.file-card {
|
||
position: relative;
|
||
}
|
||
|
||
.file-card.folder {
|
||
border-left: 4px solid #ffc107;
|
||
background: linear-gradient(135deg, rgba(255, 193, 7, 0.02) 0%, rgba(255, 193, 7, 0.05) 100%);
|
||
}
|
||
|
||
.file-card.folder .file-icon {
|
||
background: linear-gradient(135deg, rgba(255, 193, 7, 0.15) 0%, rgba(255, 193, 7, 0.25) 100%);
|
||
color: #ffc107;
|
||
box-shadow: 0 2px 8px rgba(255, 193, 7, 0.2);
|
||
}
|
||
|
||
.file-card.folder:hover {
|
||
border-left-color: #e0a800;
|
||
background: linear-gradient(135deg, rgba(255, 193, 7, 0.05) 0%, rgba(255, 193, 7, 0.08) 100%);
|
||
}
|
||
|
||
.move-btn {
|
||
background: linear-gradient(135deg, #17a2b8 0%, #138496 100%);
|
||
color: white;
|
||
}
|
||
|
||
.rename-btn {
|
||
background: linear-gradient(135deg, #ffc107 0%, #e0a800 100%);
|
||
color: white;
|
||
}
|
||
|
||
/* 紧凑布局样式 */
|
||
.files-grid.compact {
|
||
grid-template-columns: 1fr;
|
||
gap: 8px;
|
||
}
|
||
|
||
.file-card.compact {
|
||
padding: 12px 16px;
|
||
border-radius: 8px;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
gap: 12px;
|
||
min-height: 60px;
|
||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.file-card.compact::before {
|
||
height: 2px;
|
||
}
|
||
|
||
.file-card.compact:hover {
|
||
transform: translateY(-1px) scale(1.005);
|
||
box-shadow: 0 3px 8px rgba(0,0,0,0.15);
|
||
}
|
||
|
||
.file-card.compact .file-header {
|
||
flex: 1;
|
||
margin-bottom: 0;
|
||
gap: 12px;
|
||
align-items: center;
|
||
}
|
||
|
||
.file-card.compact .file-icon {
|
||
width: 32px;
|
||
height: 32px;
|
||
font-size: 14px;
|
||
margin-bottom: 0;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
/* 紧凑模式下的文件类型样式 */
|
||
.file-card.compact .file-icon.image {
|
||
background: linear-gradient(135deg, rgba(76, 175, 80, 0.1) 0%, rgba(76, 175, 80, 0.2) 100%);
|
||
color: #4caf50;
|
||
}
|
||
|
||
.file-card.compact .file-icon.video {
|
||
background: linear-gradient(135deg, rgba(244, 67, 54, 0.1) 0%, rgba(244, 67, 54, 0.2) 100%);
|
||
color: #f44336;
|
||
}
|
||
|
||
.file-card.compact .file-icon.audio {
|
||
background: linear-gradient(135deg, rgba(156, 39, 176, 0.1) 0%, rgba(156, 39, 176, 0.2) 100%);
|
||
color: #9c27b0;
|
||
}
|
||
|
||
.file-card.compact .file-icon.pdf {
|
||
background: linear-gradient(135deg, rgba(244, 67, 54, 0.1) 0%, rgba(244, 67, 54, 0.2) 100%);
|
||
color: #f44336;
|
||
}
|
||
|
||
.file-card.compact .file-icon.archive {
|
||
background: linear-gradient(135deg, rgba(255, 152, 0, 0.1) 0%, rgba(255, 152, 0, 0.2) 100%);
|
||
color: #ff9800;
|
||
}
|
||
|
||
.file-card.compact .file-icon.code {
|
||
background: linear-gradient(135deg, rgba(96, 125, 139, 0.1) 0%, rgba(96, 125, 139, 0.2) 100%);
|
||
color: #607d8b;
|
||
}
|
||
|
||
.file-card.compact .file-icon.document {
|
||
background: linear-gradient(135deg, rgba(33, 150, 243, 0.1) 0%, rgba(33, 150, 243, 0.2) 100%);
|
||
color: #2196f3;
|
||
}
|
||
|
||
.file-card.compact .play-overlay {
|
||
width: 18px;
|
||
height: 18px;
|
||
font-size: 8px;
|
||
}
|
||
|
||
.file-card.compact .file-content {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.file-card.compact .file-name {
|
||
font-size: 0.95em;
|
||
margin-bottom: 2px;
|
||
line-height: 1.2;
|
||
}
|
||
|
||
.file-card.compact .file-uploader {
|
||
font-size: 0.8em;
|
||
margin: 0;
|
||
line-height: 1.2;
|
||
}
|
||
|
||
.file-card.compact .file-comment {
|
||
font-size: 0.8em;
|
||
margin: 2px 0 0 0;
|
||
padding: 2px 6px;
|
||
line-height: 1.2;
|
||
}
|
||
|
||
.file-card.compact .file-info {
|
||
display: flex;
|
||
gap: 15px;
|
||
margin: 0;
|
||
font-size: 0.8em;
|
||
color: #666;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.file-card.compact .file-info div {
|
||
margin: 0;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.file-card.compact .file-actions {
|
||
margin: 0;
|
||
flex-direction: row;
|
||
gap: 6px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.file-card.compact .download-btn,
|
||
.file-card.compact .delete-btn,
|
||
.file-card.compact .move-btn,
|
||
.file-card.compact .rename-btn {
|
||
padding: 6px 10px;
|
||
font-size: 0.8em;
|
||
min-width: auto;
|
||
max-width: auto;
|
||
flex: none;
|
||
}
|
||
|
||
.file-card.compact .download-btn i,
|
||
.file-card.compact .delete-btn i,
|
||
.file-card.compact .move-btn i,
|
||
.file-card.compact .rename-btn i {
|
||
margin-right: 3px;
|
||
font-size: 0.9em;
|
||
}
|
||
|
||
/* 紧凑模式下的文件夹样式 */
|
||
.file-card.compact.folder {
|
||
border-left-width: 3px;
|
||
}
|
||
|
||
/* 移动端适配增强 */
|
||
@media (max-width: 768px) {
|
||
.file-toolbar {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
}
|
||
|
||
.toolbar-left,
|
||
.toolbar-right {
|
||
width: 100%;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.search-box input {
|
||
width: 100%;
|
||
max-width: 200px;
|
||
}
|
||
|
||
.sort-controls {
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.modal {
|
||
margin: 20px;
|
||
width: calc(100% - 40px);
|
||
}
|
||
|
||
/* 移动端紧凑布局优化 */
|
||
.file-card.compact {
|
||
padding: 10px 12px;
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
min-height: auto;
|
||
}
|
||
|
||
.file-card.compact .file-header {
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.file-card.compact .file-info {
|
||
justify-content: flex-start;
|
||
gap: 10px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.file-card.compact .file-actions {
|
||
flex-direction: row;
|
||
justify-content: center;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
}
|
||
|
||
.file-card.compact .download-btn,
|
||
.file-card.compact .delete-btn,
|
||
.file-card.compact .move-btn,
|
||
.file-card.compact .rename-btn {
|
||
flex: 1;
|
||
min-width: 80px;
|
||
max-width: 120px;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<div class="nav-bar">
|
||
<div class="nav-top-row">
|
||
<h1>
|
||
<img src="/image/logo2.png" alt="Logo">
|
||
内网文件传输
|
||
</h1>
|
||
<div class="user-info" id="userInfo" style="display: none;">
|
||
<div class="user-avatar" id="userAvatar"></div>
|
||
<div class="user-details">
|
||
<p class="user-name" id="userName">用户</p>
|
||
<p class="user-role" id="userRole">IP: <span id="userIP">-</span></p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="nav-bottom-row">
|
||
<div class="mobile-switches">
|
||
<label class="quick-upload-switch">
|
||
<input type="checkbox" id="quickUploadToggle">
|
||
<span class="switch-label">快速上传</span>
|
||
<span class="switch-slider"></span>
|
||
</label>
|
||
<label class="quick-upload-switch">
|
||
<input type="checkbox" id="skipValidationToggle">
|
||
<span class="switch-label">跳过文件名检查</span>
|
||
<span class="switch-slider"></span>
|
||
</label>
|
||
</div>
|
||
<div class="nav-links">
|
||
<a href="/"><i class="fas fa-home"></i> 首页</a>
|
||
<a href="/chat"><i class="fas fa-comments"></i> 聊天室</a>
|
||
<!-- //// / -->
|
||
<a href="#" onclick="showInternalAssets()"><i class="fas fa-network-wired"></i> 内网资产</a>
|
||
<a href="http://6.6.6.86:3321" target="_blank"><i class="fas fa-code-branch"></i> gitea代码仓库</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 文件管理工具栏 -->
|
||
<div class="file-toolbar">
|
||
<div class="toolbar-left">
|
||
<div class="breadcrumb" id="breadcrumb">
|
||
<a href="#" onclick="navigateToFolder('')">
|
||
<i class="fas fa-home"></i> 根目录
|
||
</a>
|
||
</div>
|
||
<button class="btn btn-secondary" id="backBtn" onclick="goBack()" style="display: none;">
|
||
<i class="fas fa-arrow-left"></i> 返回上级
|
||
</button>
|
||
<button class="btn btn-primary" onclick="showCreateFolderDialog()">
|
||
<i class="fas fa-folder-plus"></i> 新建文件夹
|
||
</button>
|
||
</div>
|
||
<div class="toolbar-right">
|
||
<div class="search-box">
|
||
<input type="text" id="searchInput" placeholder="搜索文件名或上传者..." onkeyup="handleSearch(event)">
|
||
<i class="fas fa-search"></i>
|
||
</div>
|
||
<div class="sort-controls">
|
||
<select id="sortBy" onchange="applySortAndSearch()">
|
||
<option value="name">按名称排序</option>
|
||
<option value="time">按时间排序</option>
|
||
<option value="uploader_ip">按上传者排序</option>
|
||
<option value="size">按大小排序</option>
|
||
</select>
|
||
<button class="sort-order-btn" id="sortOrderBtn" onclick="toggleSortOrder()">
|
||
<i class="fas fa-sort-alpha-down"></i>
|
||
</button>
|
||
<button class="layout-toggle-btn" id="layoutToggleBtn" onclick="toggleLayout()" title="切换布局模式">
|
||
<i class="fas fa-th-large"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="upload-section">
|
||
<div class="upload-zone" id="dropZone">
|
||
<i class="fas fa-cloud-upload-alt upload-icon"></i>
|
||
<h2>拖拽文件或文件夹到这里上传</h2>
|
||
<p class="upload-text">支持多文件和文件夹上传</p>
|
||
<div class="upload-buttons">
|
||
<button type="button" class="upload-btn-style" onclick="selectFiles()">
|
||
<i class="fas fa-file"></i> 选择文件
|
||
</button>
|
||
<button type="button" class="upload-btn-style" onclick="selectFolder()">
|
||
<i class="fas fa-folder"></i> 选择文件夹
|
||
</button>
|
||
</div>
|
||
<input type="file" id="fileInput" class="file-input" multiple>
|
||
<input type="file" id="folderInput" class="file-input" multiple webkitdirectory style="display: none;">
|
||
</div>
|
||
<div class="upload-preview" id="uploadPreview" style="display: none;">
|
||
<div class="preview-header">
|
||
<h3>准备上传</h3>
|
||
<button class="clear-btn" onclick="clearPreview()">
|
||
<i class="fas fa-times"></i>
|
||
</button>
|
||
</div>
|
||
<div class="preview-files" id="previewFiles"></div>
|
||
<div class="preview-actions">
|
||
<button class="batch-rename-btn" onclick="showBatchRenameDialog()" title="批量重命名">
|
||
<i class="fas fa-edit"></i> 批量重命名
|
||
</button>
|
||
<button class="upload-all-btn" onclick="uploadAllFiles()">
|
||
<i class="fas fa-cloud-upload-alt"></i> 开始上传
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="files-grid" id="fileList">
|
||
<!-- 文件列表将通过 JavaScript 动态添加 -->
|
||
</div>
|
||
</div>
|
||
|
||
<div class="toast" id="toast"></div>
|
||
|
||
<!-- 媒体预览模态框 -->
|
||
<div class="media-preview-modal" id="mediaPreviewModal">
|
||
<div class="media-preview-content" id="mediaPreviewContent">
|
||
<button class="media-close-btn" onclick="closeMediaPreview()">
|
||
<i class="fas fa-times"></i>
|
||
</button>
|
||
<div id="mediaContainer"></div>
|
||
<div class="media-info" id="mediaInfo" style="display: none;">
|
||
<h3 id="mediaTitle"></h3>
|
||
<p id="mediaDetails"></p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 创建文件夹对话框 -->
|
||
<div class="modal-overlay" id="createFolderModal" style="display: none;">
|
||
<div class="modal">
|
||
<div class="modal-header">
|
||
<h3>创建新文件夹</h3>
|
||
<button class="close-btn" onclick="hideCreateFolderDialog()">
|
||
<i class="fas fa-times"></i>
|
||
</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="form-group">
|
||
<label for="folderNameInput">文件夹名称:</label>
|
||
<input type="text" id="folderNameInput" placeholder="请输入文件夹名称" maxlength="50">
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-secondary" onclick="hideCreateFolderDialog()">取消</button>
|
||
<button class="btn btn-primary" onclick="createFolder()">创建</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 移动文件对话框 -->
|
||
<div class="modal-overlay" id="moveFileModal" style="display: none;">
|
||
<div class="modal">
|
||
<div class="modal-header">
|
||
<h3>移动文件</h3>
|
||
<button class="close-btn" onclick="hideMoveFileDialog()">
|
||
<i class="fas fa-times"></i>
|
||
</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<p>将文件 "<span id="moveFileName"></span>" 移动到:</p>
|
||
<div class="form-group">
|
||
<label for="targetFolderSelect">目标文件夹:</label>
|
||
<select id="targetFolderSelect">
|
||
<option value="">根目录</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-secondary" onclick="hideMoveFileDialog()">取消</button>
|
||
<button class="btn btn-primary" onclick="confirmMoveFile()">移动</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 重命名文件对话框 -->
|
||
<div class="modal-overlay" id="renameFileModal" style="display: none;">
|
||
<div class="modal">
|
||
<div class="modal-header">
|
||
<h3>重命名文件</h3>
|
||
<button class="close-btn" onclick="hideRenameFileDialog()">
|
||
<i class="fas fa-times"></i>
|
||
</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<p>当前文件名:<span id="currentFileName"></span></p>
|
||
<div class="form-group">
|
||
<label for="newFileNameInput">新文件名:</label>
|
||
<input type="text" id="newFileNameInput" placeholder="请输入新文件名" maxlength="100">
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-secondary" onclick="hideRenameFileDialog()">取消</button>
|
||
<button class="btn btn-primary" onclick="confirmRenameFile()">重命名</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 重命名文件夹对话框 -->
|
||
<div class="modal-overlay" id="renameFolderModal" style="display: none;">
|
||
<div class="modal">
|
||
<div class="modal-header">
|
||
<h3>重命名文件夹</h3>
|
||
<button class="close-btn" onclick="hideRenameFolderDialog()">
|
||
<i class="fas fa-times"></i>
|
||
</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<p>当前文件夹名:<span id="currentFolderName"></span></p>
|
||
<div class="form-group">
|
||
<label for="newFolderNameInput">新文件夹名:</label>
|
||
<input type="text" id="newFolderNameInput" placeholder="请输入新文件夹名" maxlength="50">
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-secondary" onclick="hideRenameFolderDialog()">取消</button>
|
||
<button class="btn btn-primary" onclick="confirmRenameFolder()">重命名</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 批量重命名对话框 -->
|
||
<div class="modal-overlay" id="batchRenameModal" style="display: none;">
|
||
<div class="modal">
|
||
<div class="modal-header">
|
||
<h3>批量重命名</h3>
|
||
<button class="close-btn" onclick="hideBatchRenameDialog()">
|
||
<i class="fas fa-times"></i>
|
||
</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="form-group">
|
||
<label for="renamePattern">命名模式:</label>
|
||
<select id="renamePattern" onchange="updateRenamePreview()">
|
||
<option value="sequential">序号命名 (文件1, 文件2, ...)</option>
|
||
<option value="timestamp">时间戳命名</option>
|
||
<option value="prefix">添加前缀</option>
|
||
<option value="suffix">添加后缀</option>
|
||
<option value="custom">自定义模式</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group" id="customInputGroup" style="display: none;">
|
||
<label for="customInput">自定义内容:</label>
|
||
<input type="text" id="customInput" placeholder="输入前缀/后缀/自定义模式" oninput="updateRenamePreview()">
|
||
<small>自定义模式支持:{name} = 原文件名, {index} = 序号, {ext} = 扩展名</small>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>预览效果:</label>
|
||
<div id="renamePreview" class="rename-preview"></div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-secondary" onclick="hideBatchRenameDialog()">取消</button>
|
||
<button class="btn btn-primary" onclick="applyBatchRename()">应用</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// 文件类型检测和图标映射
|
||
function getFileType(filename) {
|
||
const ext = filename.toLowerCase().split('.').pop();
|
||
|
||
// 图片文件
|
||
if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'ico'].includes(ext)) {
|
||
return 'image';
|
||
}
|
||
|
||
// 视频文件
|
||
if (['mp4', 'avi', 'mkv', 'mov', 'wmv', 'flv', 'webm', 'm4v', '3gp'].includes(ext)) {
|
||
return 'video';
|
||
}
|
||
|
||
// 音频文件
|
||
if (['mp3', 'wav', 'flac', 'aac', 'ogg', 'wma', 'm4a', 'opus'].includes(ext)) {
|
||
return 'audio';
|
||
}
|
||
|
||
// PDF文件
|
||
if (ext === 'pdf') {
|
||
return 'pdf';
|
||
}
|
||
|
||
// 压缩文件
|
||
if (['zip', 'rar', '7z', 'tar', 'gz', 'bz2', 'xz'].includes(ext)) {
|
||
return 'archive';
|
||
}
|
||
|
||
// 代码文件
|
||
if (['js', 'ts', 'py', 'java', 'cpp', 'c', 'h', 'css', 'html', 'php', 'rb', 'go', 'rs', 'swift'].includes(ext)) {
|
||
return 'code';
|
||
}
|
||
|
||
// 文档文件
|
||
if (['doc', 'docx', 'txt', 'rtf', 'odt', 'xls', 'xlsx', 'ppt', 'pptx'].includes(ext)) {
|
||
return 'document';
|
||
}
|
||
|
||
return 'file';
|
||
}
|
||
|
||
function getFileIcon(fileType) {
|
||
const iconMap = {
|
||
'image': 'fa-image',
|
||
'video': 'fa-video',
|
||
'audio': 'fa-music',
|
||
'pdf': 'fa-file-pdf',
|
||
'archive': 'fa-file-archive',
|
||
'code': 'fa-code',
|
||
'document': 'fa-file-alt',
|
||
'file': 'fa-file'
|
||
};
|
||
return iconMap[fileType] || 'fa-file';
|
||
}
|
||
|
||
function canPreview(fileType) {
|
||
return ['image', 'video', 'audio'].includes(fileType);
|
||
}
|
||
|
||
// 媒体预览功能
|
||
function openMediaPreview(filePath, fileName, fileType) {
|
||
const modal = document.getElementById('mediaPreviewModal');
|
||
const container = document.getElementById('mediaContainer');
|
||
const info = document.getElementById('mediaInfo');
|
||
const title = document.getElementById('mediaTitle');
|
||
const details = document.getElementById('mediaDetails');
|
||
|
||
// 清空容器
|
||
container.innerHTML = '';
|
||
|
||
let mediaElement;
|
||
const fileUrl = `/uploads/${filePath}`;
|
||
|
||
if (fileType === 'image') {
|
||
mediaElement = document.createElement('img');
|
||
mediaElement.src = fileUrl;
|
||
mediaElement.alt = fileName;
|
||
mediaElement.onload = function() {
|
||
details.textContent = `分辨率: ${this.naturalWidth} × ${this.naturalHeight}`;
|
||
};
|
||
} else if (fileType === 'video') {
|
||
mediaElement = document.createElement('video');
|
||
mediaElement.src = fileUrl;
|
||
mediaElement.controls = true;
|
||
mediaElement.autoplay = false;
|
||
mediaElement.onloadedmetadata = function() {
|
||
const duration = Math.round(this.duration);
|
||
const minutes = Math.floor(duration / 60);
|
||
const seconds = duration % 60;
|
||
details.textContent = `时长: ${minutes}:${seconds.toString().padStart(2, '0')} | 分辨率: ${this.videoWidth} × ${this.videoHeight}`;
|
||
};
|
||
} else if (fileType === 'audio') {
|
||
// 创建音频预览容器
|
||
const audioContainer = document.createElement('div');
|
||
audioContainer.className = 'audio-preview-container';
|
||
|
||
// 音频图标
|
||
const audioIcon = document.createElement('div');
|
||
audioIcon.className = 'audio-preview-icon';
|
||
audioIcon.innerHTML = '<i class="fas fa-music"></i>';
|
||
|
||
// 文件标题
|
||
const audioTitle = document.createElement('div');
|
||
audioTitle.className = 'audio-preview-title';
|
||
audioTitle.textContent = fileName;
|
||
|
||
// 文件详情
|
||
const audioDetails = document.createElement('div');
|
||
audioDetails.className = 'audio-preview-details';
|
||
audioDetails.textContent = '正在加载音频信息...';
|
||
|
||
// 音频播放器
|
||
mediaElement = document.createElement('audio');
|
||
mediaElement.className = 'audio-preview-player';
|
||
mediaElement.src = fileUrl;
|
||
mediaElement.controls = true;
|
||
mediaElement.autoplay = false;
|
||
mediaElement.preload = 'metadata';
|
||
mediaElement.controlsList = 'nodownload';
|
||
|
||
// 确保控件显示
|
||
mediaElement.setAttribute('controls', 'controls');
|
||
mediaElement.style.display = 'block';
|
||
mediaElement.style.width = '100%';
|
||
mediaElement.style.height = '54px';
|
||
|
||
// 添加加载状态
|
||
let isLoading = true;
|
||
let loadTimeout = setTimeout(() => {
|
||
if (isLoading) {
|
||
audioDetails.innerHTML = '<i class="fas fa-spinner"></i> 加载中...';
|
||
audioDetails.className = 'audio-preview-details audio-loading';
|
||
}
|
||
}, 500);
|
||
|
||
mediaElement.onloadedmetadata = function() {
|
||
isLoading = false;
|
||
clearTimeout(loadTimeout);
|
||
audioDetails.className = 'audio-preview-details';
|
||
|
||
const duration = Math.round(this.duration);
|
||
const minutes = Math.floor(duration / 60);
|
||
const seconds = duration % 60;
|
||
audioDetails.textContent = `时长: ${minutes}:${seconds.toString().padStart(2, '0')}`;
|
||
};
|
||
|
||
mediaElement.onerror = function(e) {
|
||
isLoading = false;
|
||
clearTimeout(loadTimeout);
|
||
audioDetails.className = 'audio-preview-details audio-error';
|
||
audioDetails.innerHTML = '<i class="fas fa-exclamation-triangle"></i>音频文件加载失败<br><small>请检查文件格式或网络连接</small>';
|
||
|
||
// 隐藏播放器控件
|
||
mediaElement.style.display = 'none';
|
||
};
|
||
|
||
mediaElement.onloadstart = function() {
|
||
audioDetails.textContent = '正在加载音频信息...';
|
||
audioDetails.className = 'audio-preview-details';
|
||
};
|
||
|
||
mediaElement.oncanplay = function() {
|
||
isLoading = false;
|
||
clearTimeout(loadTimeout);
|
||
|
||
if (audioDetails.textContent.includes('加载')) {
|
||
const duration = Math.round(this.duration);
|
||
const minutes = Math.floor(duration / 60);
|
||
const seconds = duration % 60;
|
||
audioDetails.textContent = `时长: ${minutes}:${seconds.toString().padStart(2, '0')}`;
|
||
audioDetails.className = 'audio-preview-details';
|
||
}
|
||
|
||
// 确保控件可见
|
||
setTimeout(() => {
|
||
if (mediaElement.offsetHeight < 30 || !mediaElement.controls) {
|
||
// 如果原生控件有问题,显示自定义控件
|
||
customControls.style.display = 'flex';
|
||
mediaElement.style.display = 'none';
|
||
} else {
|
||
// 原生控件正常,确保它们可见
|
||
mediaElement.style.display = 'block';
|
||
}
|
||
}, 500);
|
||
};
|
||
|
||
// 设置超时处理
|
||
setTimeout(() => {
|
||
if (isLoading && mediaElement.readyState === 0) {
|
||
isLoading = false;
|
||
clearTimeout(loadTimeout);
|
||
audioDetails.className = 'audio-preview-details audio-error';
|
||
audioDetails.innerHTML = '<i class="fas fa-exclamation-triangle"></i>音频加载超时<br><small>请检查网络连接后重试</small>';
|
||
mediaElement.style.display = 'none';
|
||
}
|
||
}, 10000); // 10秒超时
|
||
|
||
// 创建自定义控制按钮
|
||
const customControls = document.createElement('div');
|
||
customControls.className = 'audio-custom-controls';
|
||
customControls.style.display = 'none'; // 默认隐藏,仅在原生控件有问题时显示
|
||
|
||
const playBtn = document.createElement('button');
|
||
playBtn.className = 'audio-play-btn';
|
||
playBtn.innerHTML = '<i class="fas fa-play"></i>';
|
||
|
||
const timeDisplay = document.createElement('div');
|
||
timeDisplay.className = 'audio-time-display';
|
||
timeDisplay.textContent = '0:00 / 0:00';
|
||
|
||
const volumeControl = document.createElement('div');
|
||
volumeControl.className = 'audio-volume-control';
|
||
volumeControl.innerHTML = `
|
||
<i class="fas fa-volume-up" style="color: #9c27b0; font-size: 16px;"></i>
|
||
<input type="range" class="audio-volume-slider" min="0" max="100" value="100">
|
||
`;
|
||
|
||
customControls.appendChild(playBtn);
|
||
customControls.appendChild(timeDisplay);
|
||
customControls.appendChild(volumeControl);
|
||
|
||
// 播放/暂停功能
|
||
let isPlaying = false;
|
||
playBtn.onclick = function() {
|
||
if (isPlaying) {
|
||
mediaElement.pause();
|
||
playBtn.innerHTML = '<i class="fas fa-play"></i>';
|
||
isPlaying = false;
|
||
} else {
|
||
mediaElement.play();
|
||
playBtn.innerHTML = '<i class="fas fa-pause"></i>';
|
||
isPlaying = true;
|
||
}
|
||
};
|
||
|
||
// 音量控制
|
||
const volumeSlider = volumeControl.querySelector('.audio-volume-slider');
|
||
volumeSlider.oninput = function() {
|
||
mediaElement.volume = this.value / 100;
|
||
};
|
||
|
||
// 时间更新
|
||
mediaElement.ontimeupdate = function() {
|
||
const current = Math.floor(this.currentTime);
|
||
const duration = Math.floor(this.duration) || 0;
|
||
const currentMin = Math.floor(current / 60);
|
||
const currentSec = current % 60;
|
||
const durationMin = Math.floor(duration / 60);
|
||
const durationSec = duration % 60;
|
||
timeDisplay.textContent = `${currentMin}:${currentSec.toString().padStart(2, '0')} / ${durationMin}:${durationSec.toString().padStart(2, '0')}`;
|
||
};
|
||
|
||
// 播放结束
|
||
mediaElement.onended = function() {
|
||
playBtn.innerHTML = '<i class="fas fa-play"></i>';
|
||
isPlaying = false;
|
||
};
|
||
|
||
// 检查原生控件是否工作
|
||
setTimeout(() => {
|
||
const computedStyle = window.getComputedStyle(mediaElement);
|
||
if (computedStyle.height === '0px' || mediaElement.offsetHeight < 30) {
|
||
// 原生控件可能有问题,显示自定义控件
|
||
customControls.style.display = 'flex';
|
||
mediaElement.style.display = 'none';
|
||
}
|
||
}, 1000);
|
||
|
||
// 组装容器
|
||
audioContainer.appendChild(audioIcon);
|
||
audioContainer.appendChild(audioTitle);
|
||
audioContainer.appendChild(audioDetails);
|
||
audioContainer.appendChild(mediaElement);
|
||
audioContainer.appendChild(customControls);
|
||
|
||
container.appendChild(audioContainer);
|
||
|
||
// 不使用默认的info显示
|
||
info.style.display = 'none';
|
||
modal.classList.add('active');
|
||
|
||
// 点击模态框背景关闭
|
||
modal.onclick = function(e) {
|
||
if (e.target === modal) {
|
||
closeMediaPreview();
|
||
}
|
||
};
|
||
|
||
// ESC键关闭
|
||
document.addEventListener('keydown', handleEscapeKey);
|
||
return; // 提前返回,不执行下面的通用逻辑
|
||
}
|
||
|
||
if (mediaElement) {
|
||
container.appendChild(mediaElement);
|
||
title.textContent = fileName;
|
||
info.style.display = 'block';
|
||
modal.classList.add('active');
|
||
|
||
// 点击模态框背景关闭
|
||
modal.onclick = function(e) {
|
||
if (e.target === modal) {
|
||
closeMediaPreview();
|
||
}
|
||
};
|
||
|
||
// ESC键关闭
|
||
document.addEventListener('keydown', handleEscapeKey);
|
||
}
|
||
}
|
||
|
||
function closeMediaPreview() {
|
||
const modal = document.getElementById('mediaPreviewModal');
|
||
const container = document.getElementById('mediaContainer');
|
||
|
||
modal.classList.remove('active');
|
||
|
||
// 停止所有媒体播放
|
||
container.querySelectorAll('video, audio').forEach(media => {
|
||
media.pause();
|
||
media.currentTime = 0;
|
||
});
|
||
|
||
// 移除ESC键监听
|
||
document.removeEventListener('keydown', handleEscapeKey);
|
||
|
||
// 延迟清空内容,等待动画完成
|
||
setTimeout(() => {
|
||
container.innerHTML = '';
|
||
}, 300);
|
||
}
|
||
|
||
function handleEscapeKey(e) {
|
||
if (e.key === 'Escape') {
|
||
closeMediaPreview();
|
||
}
|
||
}
|
||
|
||
// 生成文件缩略图
|
||
function generateThumbnail(filePath, fileName, fileType, iconElement) {
|
||
if (fileType === 'image') {
|
||
const img = document.createElement('img');
|
||
img.className = 'file-thumbnail';
|
||
img.src = `/uploads/${filePath}`;
|
||
img.onload = function() {
|
||
this.classList.add('loaded');
|
||
iconElement.classList.add('has-preview');
|
||
};
|
||
img.onerror = function() {
|
||
console.log('Failed to load thumbnail for:', fileName);
|
||
};
|
||
iconElement.appendChild(img);
|
||
} else if (fileType === 'video') {
|
||
// 对于视频,我们可以尝试生成第一帧缩略图
|
||
const video = document.createElement('video');
|
||
video.src = `/uploads/${filePath}`;
|
||
video.muted = true;
|
||
video.preload = 'metadata';
|
||
video.style.display = 'none';
|
||
|
||
video.onloadedmetadata = function() {
|
||
// 设置到视频的10%位置获取缩略图
|
||
this.currentTime = Math.max(1, this.duration * 0.1);
|
||
};
|
||
|
||
video.onseeked = function() {
|
||
try {
|
||
const canvas = document.createElement('canvas');
|
||
const ctx = canvas.getContext('2d');
|
||
canvas.width = 48;
|
||
canvas.height = 48;
|
||
|
||
// 计算缩放比例以适应正方形
|
||
const scale = Math.min(48 / this.videoWidth, 48 / this.videoHeight);
|
||
const width = this.videoWidth * scale;
|
||
const height = this.videoHeight * scale;
|
||
const x = (48 - width) / 2;
|
||
const y = (48 - height) / 2;
|
||
|
||
ctx.drawImage(this, x, y, width, height);
|
||
|
||
const img = document.createElement('img');
|
||
img.className = 'file-thumbnail';
|
||
img.src = canvas.toDataURL();
|
||
img.onload = function() {
|
||
this.classList.add('loaded');
|
||
iconElement.classList.add('has-preview');
|
||
};
|
||
iconElement.appendChild(img);
|
||
|
||
// 添加播放按钮
|
||
const playBtn = document.createElement('button');
|
||
playBtn.className = 'play-overlay';
|
||
playBtn.innerHTML = '<i class="fas fa-play"></i>';
|
||
playBtn.onclick = function(e) {
|
||
e.stopPropagation();
|
||
openMediaPreview(filePath, fileName, fileType);
|
||
};
|
||
iconElement.appendChild(playBtn);
|
||
|
||
// 移除视频元素
|
||
video.remove();
|
||
} catch (error) {
|
||
console.log('Failed to generate video thumbnail:', error);
|
||
video.remove();
|
||
}
|
||
};
|
||
|
||
video.onerror = function() {
|
||
console.log('Failed to load video for thumbnail:', fileName);
|
||
video.remove();
|
||
};
|
||
|
||
// 添加到DOM中(隐藏状态)
|
||
document.body.appendChild(video);
|
||
} else if (fileType === 'audio') {
|
||
// 为音频文件添加播放按钮
|
||
const playBtn = document.createElement('button');
|
||
playBtn.className = 'play-overlay';
|
||
playBtn.innerHTML = '<i class="fas fa-play"></i>';
|
||
playBtn.onclick = function(e) {
|
||
e.stopPropagation();
|
||
openMediaPreview(filePath, fileName, fileType);
|
||
};
|
||
iconElement.appendChild(playBtn);
|
||
iconElement.classList.add('has-preview');
|
||
}
|
||
}
|
||
|
||
const dropZone = document.getElementById('dropZone');
|
||
const fileInput = document.getElementById('fileInput');
|
||
const progressBar = document.getElementById('progressBar');
|
||
const progress = document.getElementById('progress');
|
||
const toast = document.getElementById('toast');
|
||
|
||
// 拖拽相关事件
|
||
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
||
dropZone.addEventListener(eventName, preventDefaults, false);
|
||
document.body.addEventListener(eventName, preventDefaults, false);
|
||
});
|
||
|
||
// 防止按钮区域的拖拽事件影响
|
||
const uploadButtons = document.querySelector('.upload-buttons');
|
||
if (uploadButtons) {
|
||
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
||
uploadButtons.addEventListener(eventName, function(e) {
|
||
e.stopPropagation();
|
||
}, false);
|
||
});
|
||
}
|
||
|
||
function preventDefaults(e) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
}
|
||
|
||
['dragenter', 'dragover'].forEach(eventName => {
|
||
dropZone.addEventListener(eventName, highlight, false);
|
||
});
|
||
|
||
['dragleave', 'drop'].forEach(eventName => {
|
||
dropZone.addEventListener(eventName, unhighlight, false);
|
||
});
|
||
|
||
function highlight(e) {
|
||
dropZone.classList.add('drag-over');
|
||
}
|
||
|
||
function unhighlight(e) {
|
||
dropZone.classList.remove('drag-over');
|
||
}
|
||
|
||
dropZone.addEventListener('drop', handleDrop, false);
|
||
|
||
function handleDrop(e) {
|
||
const dt = e.dataTransfer;
|
||
const files = dt.files;
|
||
|
||
// 检查是否包含文件夹
|
||
const filesArray = Array.from(files);
|
||
const hasDirectoryItems = Array.from(dt.items).some(item =>
|
||
item.webkitGetAsEntry && item.webkitGetAsEntry().isDirectory
|
||
);
|
||
|
||
if (hasDirectoryItems) {
|
||
handleDirectoryDrop(dt);
|
||
} else {
|
||
handleFiles(files);
|
||
}
|
||
}
|
||
|
||
// 处理文件夹拖拽
|
||
async function handleDirectoryDrop(dataTransfer) {
|
||
const items = Array.from(dataTransfer.items);
|
||
const allFiles = [];
|
||
|
||
for (const item of items) {
|
||
if (item.webkitGetAsEntry) {
|
||
const entry = item.webkitGetAsEntry();
|
||
if (entry) {
|
||
const files = await processEntry(entry);
|
||
allFiles.push(...files);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (allFiles.length > 0) {
|
||
handleFolderFiles(allFiles);
|
||
}
|
||
}
|
||
|
||
// 递归处理目录条目
|
||
function processEntry(entry, path = '') {
|
||
return new Promise((resolve) => {
|
||
if (entry.isFile) {
|
||
entry.file((file) => {
|
||
// 设置文件的相对路径
|
||
file.webkitRelativePath = path + entry.name;
|
||
resolve([file]);
|
||
});
|
||
} else if (entry.isDirectory) {
|
||
const dirReader = entry.createReader();
|
||
dirReader.readEntries(async (entries) => {
|
||
const allFiles = [];
|
||
for (const childEntry of entries) {
|
||
const childFiles = await processEntry(childEntry, path + entry.name + '/');
|
||
allFiles.push(...childFiles);
|
||
}
|
||
resolve(allFiles);
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
fileInput.addEventListener('change', function(e) {
|
||
console.log('Files selected, files count:', this.files.length);
|
||
if (this.files.length > 0) {
|
||
handleFiles(this.files);
|
||
// 清空输入,允许重复选择同一文件
|
||
this.value = '';
|
||
}
|
||
});
|
||
|
||
// 添加文件夹输入事件
|
||
document.getElementById('folderInput').addEventListener('change', function(e) {
|
||
console.log('Folder selected, files count:', this.files.length);
|
||
if (this.files.length > 0) {
|
||
handleFolderFiles(this.files);
|
||
// 清空输入,允许重复选择同一文件夹
|
||
this.value = '';
|
||
}
|
||
});
|
||
|
||
// 存储待上传文件的数组
|
||
let pendingFiles = [];
|
||
|
||
// 全局变量
|
||
let currentFolder = '';
|
||
let currentSortBy = 'name';
|
||
let currentSortOrder = 'asc';
|
||
let currentSearch = '';
|
||
let folderList = []; // 用于存储文件夹列表
|
||
let currentMoveFile = null; // 当前要移动的文件
|
||
let currentRenameFile = null; // 当前要重命名的文件
|
||
let currentRenameFolder = null; // 当前要重命名的文件夹
|
||
let isCompactLayout = false; // 是否为紧凑布局
|
||
|
||
// 添加上传进度管理
|
||
class UploadProgress {
|
||
constructor(file, isQuickUpload = false) {
|
||
this.file = file;
|
||
this.startTime = Date.now();
|
||
this.lastLoaded = 0;
|
||
this.lastTime = this.startTime;
|
||
this.aborted = false;
|
||
this.xhr = null; // 存储 XMLHttpRequest 对象
|
||
this.createProgressElement(isQuickUpload);
|
||
}
|
||
|
||
createProgressElement(isQuickUpload) {
|
||
const container = document.createElement('div');
|
||
container.className = 'file-progress';
|
||
container.innerHTML = `
|
||
<div class="progress-header">
|
||
<div class="progress-filename">${this.file.name}</div>
|
||
<div class="progress-stats">
|
||
<span class="progress-percentage">0%</span>
|
||
<span class="progress-size">0/${this.formatSize(this.file.size)}</span>
|
||
<button class="cancel-upload-btn">
|
||
<i class="fas fa-times"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="progress-bar-outer">
|
||
<div class="progress-bar-inner"></div>
|
||
</div>
|
||
<div class="upload-speed">准备上传...</div>
|
||
`;
|
||
|
||
// 添加取消按钮事件监听
|
||
const cancelBtn = container.querySelector('.cancel-upload-btn');
|
||
cancelBtn.addEventListener('click', () => this.cancelUpload());
|
||
|
||
const progressContainer = document.getElementById('uploadProgressContainer');
|
||
if (progressContainer) {
|
||
progressContainer.appendChild(container);
|
||
progressContainer.style.display = 'block';
|
||
}
|
||
|
||
this.element = container;
|
||
this.progressBar = container.querySelector('.progress-bar-inner');
|
||
this.percentageEl = container.querySelector('.progress-percentage');
|
||
this.sizeEl = container.querySelector('.progress-size');
|
||
this.speedEl = container.querySelector('.upload-speed');
|
||
}
|
||
|
||
setXHR(xhr) {
|
||
this.xhr = xhr;
|
||
}
|
||
|
||
cancelUpload() {
|
||
if (this.xhr) {
|
||
this.aborted = true;
|
||
this.xhr.abort();
|
||
this.element.classList.add('upload-cancelled');
|
||
this.speedEl.textContent = '上传已取消';
|
||
|
||
// 使用 Promise 和 setTimeout 确保动画完成后再移除元素
|
||
const removeElement = () => {
|
||
return new Promise(resolve => {
|
||
setTimeout(() => {
|
||
if (this.element && this.element.parentNode) {
|
||
this.element.remove();
|
||
|
||
// 检查是否需要隐藏容器
|
||
const container = document.getElementById('uploadProgressContainer');
|
||
if (container && container.children.length === 0) {
|
||
container.style.display = 'none';
|
||
}
|
||
}
|
||
resolve();
|
||
}, 1000);
|
||
});
|
||
};
|
||
|
||
// 执行移除操作
|
||
removeElement().catch(console.error);
|
||
}
|
||
}
|
||
|
||
updateProgress(loaded) {
|
||
const now = Date.now();
|
||
const timeElapsed = (now - this.lastTime) / 1000;
|
||
const loadedDiff = loaded - this.lastLoaded;
|
||
|
||
// 计算平均速度(使用移动平均)
|
||
const instantSpeed = timeElapsed > 0 ? loadedDiff / timeElapsed : 0;
|
||
this.speeds = this.speeds || [];
|
||
this.speeds.push(instantSpeed);
|
||
if (this.speeds.length > 5) this.speeds.shift();
|
||
const avgSpeed = this.speeds.reduce((a, b) => a + b, 0) / this.speeds.length;
|
||
|
||
// 更新进度显示
|
||
const progress = (loaded / this.file.size) * 100;
|
||
this.progressBar.style.width = `${progress}%`;
|
||
this.percentageEl.textContent = `${Math.round(progress)}%`;
|
||
this.sizeEl.textContent = `${this.formatSize(loaded)}/${this.formatSize(this.file.size)}`;
|
||
|
||
// 计算剩余时间
|
||
const remaining = (this.file.size - loaded) / avgSpeed;
|
||
const remainingText = this.formatTime(remaining);
|
||
|
||
// 更新速度显示
|
||
if (timeElapsed >= 0.5) {
|
||
this.speedEl.textContent = `速度: ${this.formatSpeed(avgSpeed)} - 剩余时间: ${remainingText}`;
|
||
this.lastLoaded = loaded;
|
||
this.lastTime = now;
|
||
}
|
||
}
|
||
|
||
formatTime(seconds) {
|
||
if (!isFinite(seconds) || seconds < 0) return '计算中...';
|
||
if (seconds < 60) return `${Math.round(seconds)}秒`;
|
||
const minutes = Math.floor(seconds / 60);
|
||
return `${minutes}分${Math.round(seconds % 60)}秒`;
|
||
}
|
||
|
||
complete() {
|
||
setTimeout(() => {
|
||
this.element.remove();
|
||
// 如果没有更多进度条,隐藏容器
|
||
const container = document.getElementById('uploadProgressContainer');
|
||
if (container && container.children.length === 0) {
|
||
container.style.display = 'none';
|
||
}
|
||
}, 1000);
|
||
}
|
||
|
||
formatSize(bytes) {
|
||
const units = ['B', 'KB', 'MB', 'GB'];
|
||
let size = bytes;
|
||
let unitIndex = 0;
|
||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||
size /= 1024;
|
||
unitIndex++;
|
||
}
|
||
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
||
}
|
||
|
||
formatSpeed(bytesPerSecond) {
|
||
const units = ['B/s', 'KB/s', 'MB/s', 'GB/s'];
|
||
let speed = bytesPerSecond;
|
||
let unitIndex = 0;
|
||
while (speed >= 1024 && unitIndex < units.length - 1) {
|
||
speed /= 1024;
|
||
unitIndex++;
|
||
}
|
||
return `${speed.toFixed(1)} ${units[unitIndex]}`;
|
||
}
|
||
}
|
||
|
||
// 单文件上传函数
|
||
async function uploadSingleFile(file, isQuickUpload = false, isFromFolder = false, customFilename = null) {
|
||
const formData = new FormData();
|
||
formData.append('file', file);
|
||
|
||
// 如果是文件夹上传,需要保持相对路径结构
|
||
if (isFromFolder && file.webkitRelativePath) {
|
||
formData.append('relative_path', file.webkitRelativePath);
|
||
}
|
||
|
||
// 添加当前目录信息
|
||
if (currentFolder) {
|
||
formData.append('target_folder', currentFolder);
|
||
}
|
||
|
||
// 添加自定义文件名
|
||
if (customFilename) {
|
||
formData.append('custom_filename', customFilename);
|
||
}
|
||
|
||
return uploadFile(formData, file, isQuickUpload);
|
||
}
|
||
|
||
// 修改文件上传函数 - 移除分片上传,直接上传整个文件
|
||
async function uploadFile(formData, file, isQuickUpload = false) {
|
||
return new Promise((resolve, reject) => {
|
||
const progress = new UploadProgress(file, isQuickUpload);
|
||
const xhr = new XMLHttpRequest();
|
||
|
||
// 移除分片上传逻辑,所有文件都直接上传
|
||
// 这样可以避免大文件被切分成多个小文件
|
||
progress.setXHR(xhr);
|
||
|
||
xhr.upload.addEventListener('progress', (event) => {
|
||
if (event.lengthComputable && !progress.aborted) {
|
||
progress.updateProgress(event.loaded);
|
||
}
|
||
});
|
||
|
||
xhr.addEventListener('load', () => {
|
||
if (!progress.aborted) {
|
||
if (xhr.status === 200) {
|
||
const response = JSON.parse(xhr.responseText);
|
||
showToast('文件上传成功!');
|
||
progress.complete();
|
||
resolve(response);
|
||
} else {
|
||
showToast('上传失败:服务器错误');
|
||
progress.element.remove();
|
||
reject(new Error('Upload failed'));
|
||
}
|
||
}
|
||
});
|
||
|
||
xhr.addEventListener('error', () => {
|
||
if (!progress.aborted) {
|
||
showToast('上传失败:网络错误');
|
||
progress.element.remove();
|
||
reject(new Error('Network error'));
|
||
}
|
||
});
|
||
|
||
xhr.addEventListener('abort', () => {
|
||
reject(new Error('Upload cancelled'));
|
||
});
|
||
|
||
// 设置超时时间为30分钟,适用于大文件上传(如700MB视频)
|
||
xhr.timeout = 30 * 60 * 1000; // 30分钟超时,适应大文件传输
|
||
|
||
xhr.addEventListener('timeout', () => {
|
||
if (!progress.aborted) {
|
||
showToast('上传超时:文件过大或网络较慢,请检查网络连接');
|
||
progress.element.remove();
|
||
reject(new Error('Upload timeout'));
|
||
}
|
||
});
|
||
|
||
xhr.open('POST', '/upload', true);
|
||
xhr.send(formData);
|
||
});
|
||
}
|
||
|
||
// 修改文件处理函数
|
||
function handleFiles(files) {
|
||
const quickUpload = document.getElementById('quickUploadToggle').checked;
|
||
|
||
if (quickUpload) {
|
||
// 快速上传模式 - 直接上传,不做文件名验证
|
||
initializeProgressContainer();
|
||
Array.from(files).forEach(file => {
|
||
uploadSingleFile(file, true);
|
||
});
|
||
showToast(`快速上传模式:开始上传 ${files.length} 个文件`);
|
||
} else {
|
||
// 预览模式
|
||
pendingFiles = [...pendingFiles, ...Array.from(files)];
|
||
showPreview();
|
||
}
|
||
}
|
||
|
||
// 处理文件夹上传
|
||
function handleFolderFiles(files) {
|
||
const quickUpload = document.getElementById('quickUploadToggle').checked;
|
||
const filesArray = Array.from(files);
|
||
|
||
console.log('Processing folder files:', filesArray.length);
|
||
console.log('Files with relative paths:', filesArray.map(f => ({name: f.name, path: f.webkitRelativePath})));
|
||
|
||
if (filesArray.length === 0) {
|
||
showToast('未选择任何文件');
|
||
return;
|
||
}
|
||
|
||
// 检查文件是否有相对路径(确认是文件夹上传)
|
||
const hasRelativePaths = filesArray.some(file => file.webkitRelativePath);
|
||
if (!hasRelativePaths) {
|
||
showToast('请选择文件夹而不是单独的文件');
|
||
return;
|
||
}
|
||
|
||
showToast(`检测到文件夹,包含 ${filesArray.length} 个文件`);
|
||
|
||
if (quickUpload) {
|
||
// 快速上传模式
|
||
initializeProgressContainer();
|
||
filesArray.forEach(file => {
|
||
uploadSingleFile(file, true, true);
|
||
});
|
||
} else {
|
||
// 预览模式
|
||
pendingFiles = [...pendingFiles, ...filesArray];
|
||
showPreview();
|
||
}
|
||
}
|
||
|
||
// 选择文件函数
|
||
function selectFiles() {
|
||
console.log('Selecting files...');
|
||
document.getElementById('fileInput').click();
|
||
}
|
||
|
||
// 选择文件夹函数
|
||
function selectFolder() {
|
||
console.log('Selecting folder...');
|
||
document.getElementById('folderInput').click();
|
||
}
|
||
|
||
// 返回上级目录
|
||
function goBack() {
|
||
if (currentFolder) {
|
||
const parts = currentFolder.split('/');
|
||
parts.pop(); // 移除最后一级
|
||
const parentFolder = parts.join('/');
|
||
navigateToFolder(parentFolder);
|
||
}
|
||
}
|
||
|
||
// 自动修复文件名
|
||
function autoFixFilename(filename) {
|
||
// 替换有害字符为安全字符
|
||
let fixed = filename
|
||
.replace(/[\/\\]/g, '-') // 路径分隔符替换为连字符
|
||
.replace(/[|]/g, '_') // 管道符替换为下划线
|
||
.trim();
|
||
|
||
// 如果文件名太长,截断但保留扩展名
|
||
if (fixed.length > 300) {
|
||
const lastDotIndex = fixed.lastIndexOf('.');
|
||
if (lastDotIndex > 0) {
|
||
const extension = fixed.substring(lastDotIndex);
|
||
const nameWithoutExt = fixed.substring(0, lastDotIndex);
|
||
fixed = nameWithoutExt.substring(0, 300 - extension.length) + extension;
|
||
} else {
|
||
fixed = fixed.substring(0, 300);
|
||
}
|
||
}
|
||
|
||
return fixed;
|
||
}
|
||
|
||
// 验证文件名 - 宽松版本,自动修复
|
||
function validateFilename(input) {
|
||
// 检查是否跳过验证
|
||
const skipValidation = document.getElementById('skipValidationToggle').checked;
|
||
if (skipValidation) {
|
||
// 移除任何错误提示
|
||
const existingError = input.parentNode.querySelector('.filename-error');
|
||
if (existingError) {
|
||
existingError.remove();
|
||
}
|
||
input.classList.remove('invalid');
|
||
return true; // 跳过所有验证
|
||
}
|
||
|
||
const originalFilename = input.value.trim();
|
||
const fixedFilename = autoFixFilename(originalFilename);
|
||
|
||
// 移除之前的错误提示
|
||
const existingError = input.parentNode.querySelector('.filename-error');
|
||
if (existingError) {
|
||
existingError.remove();
|
||
}
|
||
|
||
// 重置样式
|
||
input.classList.remove('invalid');
|
||
|
||
if (originalFilename.length === 0) {
|
||
showFilenameError(input, '文件名不能为空');
|
||
return false;
|
||
}
|
||
|
||
// 如果文件名被自动修复了,更新输入框并显示提示
|
||
if (fixedFilename !== originalFilename) {
|
||
input.value = fixedFilename;
|
||
showFilenameInfo(input, '文件名已自动修正');
|
||
return true;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
// 显示文件名信息提示
|
||
function showFilenameInfo(input, message) {
|
||
const infoDiv = document.createElement('div');
|
||
infoDiv.className = 'filename-info';
|
||
infoDiv.textContent = message;
|
||
infoDiv.style.color = '#4caf50';
|
||
infoDiv.style.fontSize = '0.75em';
|
||
infoDiv.style.marginTop = '4px';
|
||
input.parentNode.appendChild(infoDiv);
|
||
|
||
// 3秒后自动移除提示
|
||
setTimeout(() => {
|
||
if (infoDiv.parentNode) {
|
||
infoDiv.remove();
|
||
}
|
||
}, 3000);
|
||
}
|
||
|
||
// 显示文件名错误
|
||
function showFilenameError(input, message) {
|
||
input.classList.add('invalid');
|
||
const errorDiv = document.createElement('div');
|
||
errorDiv.className = 'filename-error';
|
||
errorDiv.textContent = message;
|
||
input.parentNode.appendChild(errorDiv);
|
||
}
|
||
|
||
// 显示批量重命名对话框
|
||
function showBatchRenameDialog() {
|
||
if (pendingFiles.length === 0) {
|
||
showToast('没有文件可以重命名');
|
||
return;
|
||
}
|
||
document.getElementById('batchRenameModal').style.display = 'flex';
|
||
updateRenamePreview();
|
||
}
|
||
|
||
// 隐藏批量重命名对话框
|
||
function hideBatchRenameDialog() {
|
||
document.getElementById('batchRenameModal').style.display = 'none';
|
||
document.getElementById('renamePattern').value = 'sequential';
|
||
document.getElementById('customInput').value = '';
|
||
document.getElementById('customInputGroup').style.display = 'none';
|
||
}
|
||
|
||
// 更新重命名预览
|
||
function updateRenamePreview() {
|
||
const pattern = document.getElementById('renamePattern').value;
|
||
const customInput = document.getElementById('customInput').value;
|
||
const customGroup = document.getElementById('customInputGroup');
|
||
const preview = document.getElementById('renamePreview');
|
||
|
||
// 显示/隐藏自定义输入框
|
||
if (pattern === 'prefix' || pattern === 'suffix' || pattern === 'custom') {
|
||
customGroup.style.display = 'block';
|
||
} else {
|
||
customGroup.style.display = 'none';
|
||
}
|
||
|
||
// 生成预览
|
||
let previewHTML = '';
|
||
const now = new Date();
|
||
const timestamp = now.toISOString().replace(/[:.]/g, '-').slice(0, -5);
|
||
|
||
pendingFiles.forEach((file, index) => {
|
||
const originalName = file.name;
|
||
const nameWithoutExt = originalName.replace(/\.[^/.]+$/, "");
|
||
const ext = originalName.includes('.') ? originalName.split('.').pop() : '';
|
||
let newName = originalName;
|
||
|
||
switch (pattern) {
|
||
case 'sequential':
|
||
newName = `文件${index + 1}${ext ? '.' + ext : ''}`;
|
||
break;
|
||
case 'timestamp':
|
||
newName = `${timestamp}_${index + 1}${ext ? '.' + ext : ''}`;
|
||
break;
|
||
case 'prefix':
|
||
if (customInput) {
|
||
newName = `${customInput}${originalName}`;
|
||
}
|
||
break;
|
||
case 'suffix':
|
||
if (customInput) {
|
||
newName = `${nameWithoutExt}${customInput}${ext ? '.' + ext : ''}`;
|
||
}
|
||
break;
|
||
case 'custom':
|
||
if (customInput) {
|
||
newName = customInput
|
||
.replace(/{name}/g, nameWithoutExt)
|
||
.replace(/{index}/g, (index + 1).toString())
|
||
.replace(/{ext}/g, ext);
|
||
if (ext && !newName.includes('.')) {
|
||
newName += '.' + ext;
|
||
}
|
||
}
|
||
break;
|
||
}
|
||
|
||
previewHTML += `
|
||
<div class="rename-preview-item">
|
||
<span class="rename-original">${originalName}</span>
|
||
<span class="rename-arrow">→</span>
|
||
<span class="rename-new">${newName}</span>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
preview.innerHTML = previewHTML;
|
||
}
|
||
|
||
// 应用批量重命名
|
||
function applyBatchRename() {
|
||
const pattern = document.getElementById('renamePattern').value;
|
||
const customInput = document.getElementById('customInput').value;
|
||
|
||
if ((pattern === 'prefix' || pattern === 'suffix' || pattern === 'custom') && !customInput.trim()) {
|
||
showToast('请输入自定义内容');
|
||
return;
|
||
}
|
||
|
||
const now = new Date();
|
||
const timestamp = now.toISOString().replace(/[:.]/g, '-').slice(0, -5);
|
||
|
||
// 应用重命名到每个文件输入框
|
||
document.querySelectorAll('.preview-file-name-input').forEach((input, inputIndex) => {
|
||
const fileIndex = parseInt(input.dataset.index);
|
||
const file = pendingFiles[fileIndex];
|
||
const originalName = file.name;
|
||
const nameWithoutExt = originalName.replace(/\.[^/.]+$/, "");
|
||
const ext = originalName.includes('.') ? originalName.split('.').pop() : '';
|
||
let newName = originalName;
|
||
|
||
switch (pattern) {
|
||
case 'sequential':
|
||
newName = `文件${fileIndex + 1}${ext ? '.' + ext : ''}`;
|
||
break;
|
||
case 'timestamp':
|
||
newName = `${timestamp}_${fileIndex + 1}${ext ? '.' + ext : ''}`;
|
||
break;
|
||
case 'prefix':
|
||
newName = `${customInput}${originalName}`;
|
||
break;
|
||
case 'suffix':
|
||
newName = `${nameWithoutExt}${customInput}${ext ? '.' + ext : ''}`;
|
||
break;
|
||
case 'custom':
|
||
newName = customInput
|
||
.replace(/{name}/g, nameWithoutExt)
|
||
.replace(/{index}/g, (fileIndex + 1).toString())
|
||
.replace(/{ext}/g, ext);
|
||
if (ext && !newName.includes('.')) {
|
||
newName += '.' + ext;
|
||
}
|
||
break;
|
||
}
|
||
|
||
input.value = newName;
|
||
validateFilename(input);
|
||
});
|
||
|
||
hideBatchRenameDialog();
|
||
showToast('批量重命名已应用');
|
||
}
|
||
|
||
// 修改批量上传函数
|
||
async function uploadAllFiles() {
|
||
// 禁用上传按钮并改变样式
|
||
const uploadBtn = document.querySelector('.upload-all-btn');
|
||
if (uploadBtn) {
|
||
uploadBtn.disabled = true;
|
||
uploadBtn.style.opacity = '0.5';
|
||
uploadBtn.innerHTML = `
|
||
<i class="fas fa-spinner fa-spin"></i>
|
||
正在上传...
|
||
`;
|
||
}
|
||
|
||
initializeProgressContainer();
|
||
const maxConcurrent = 3;
|
||
const comments = {};
|
||
const customFilenames = {};
|
||
|
||
// 获取文件备注
|
||
document.querySelectorAll('.preview-file-comment').forEach(input => {
|
||
const index = input.dataset.index;
|
||
comments[index] = input.value.trim();
|
||
});
|
||
|
||
// 验证所有文件名并获取自定义文件名
|
||
let hasInvalidFilenames = false;
|
||
document.querySelectorAll('.preview-file-name-input').forEach(input => {
|
||
const index = input.dataset.index;
|
||
const customName = input.value.trim();
|
||
|
||
// 验证文件名
|
||
if (!validateFilename(input)) {
|
||
hasInvalidFilenames = true;
|
||
return;
|
||
}
|
||
|
||
if (customName && customName !== pendingFiles[index].name) {
|
||
customFilenames[index] = customName;
|
||
}
|
||
});
|
||
|
||
// 如果有无效文件名,停止上传
|
||
if (hasInvalidFilenames) {
|
||
showToast('发现无效文件名,请检查红色标记的文件名');
|
||
// 恢复上传按钮
|
||
if (uploadBtn) {
|
||
uploadBtn.disabled = false;
|
||
uploadBtn.style.opacity = '1';
|
||
uploadBtn.innerHTML = `
|
||
<i class="fas fa-cloud-upload-alt"></i>
|
||
上传 ${pendingFiles.length} 个文件
|
||
`;
|
||
}
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const uploadTasks = pendingFiles.map((file, index) => {
|
||
return async () => {
|
||
const formData = new FormData();
|
||
formData.append('file', file);
|
||
|
||
// 添加备注
|
||
if (comments[index]) {
|
||
formData.append('comment', comments[index]);
|
||
}
|
||
|
||
// 添加自定义文件名
|
||
if (customFilenames[index]) {
|
||
formData.append('custom_filename', customFilenames[index]);
|
||
}
|
||
|
||
// 处理文件夹路径
|
||
if (file.webkitRelativePath) {
|
||
formData.append('relative_path', file.webkitRelativePath);
|
||
}
|
||
|
||
// 添加当前目录信息
|
||
if (currentFolder) {
|
||
formData.append('target_folder', currentFolder);
|
||
}
|
||
|
||
try {
|
||
await uploadFile(formData, file, false);
|
||
} catch (error) {
|
||
if (error.message !== 'Upload cancelled') {
|
||
console.error('上传失败:', error);
|
||
}
|
||
}
|
||
};
|
||
});
|
||
|
||
while (uploadTasks.length > 0) {
|
||
const batch = uploadTasks.splice(0, maxConcurrent);
|
||
await Promise.all(batch.map(task => task()));
|
||
}
|
||
|
||
// 上传完成后清理预览和进度条
|
||
clearPreview();
|
||
clearProgressContainer();
|
||
loadFiles();
|
||
} catch (error) {
|
||
console.error('上传过程出错:', error);
|
||
if (error.message !== 'Upload cancelled') {
|
||
showToast('部分文件上传失败,请重试');
|
||
}
|
||
}
|
||
}
|
||
|
||
// 添加清理进度条容器的函数
|
||
function clearProgressContainer() {
|
||
const container = document.getElementById('uploadProgressContainer');
|
||
if (container) {
|
||
container.innerHTML = '';
|
||
container.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
// 添加进度条容器的初始化
|
||
function initializeProgressContainer() {
|
||
let container = document.getElementById('uploadProgressContainer');
|
||
|
||
// 如果容器不存在,创建新容器
|
||
if (!container) {
|
||
container = document.createElement('div');
|
||
container.id = 'uploadProgressContainer';
|
||
container.className = 'upload-progress-container';
|
||
document.querySelector('.upload-section').appendChild(container);
|
||
}
|
||
|
||
// 显示容器
|
||
container.style.display = 'block';
|
||
|
||
return container;
|
||
}
|
||
|
||
// 添加检查进度条容器状态的函数
|
||
function checkProgressContainer() {
|
||
const container = document.getElementById('uploadProgressContainer');
|
||
if (container) {
|
||
// 如果容器为空,隐藏它
|
||
if (container.children.length === 0) {
|
||
container.style.display = 'none';
|
||
} else {
|
||
container.style.display = 'block';
|
||
}
|
||
}
|
||
}
|
||
|
||
// 修改预览显示函数
|
||
function showPreview() {
|
||
const previewArea = document.getElementById('uploadPreview');
|
||
const previewFiles = document.getElementById('previewFiles');
|
||
const uploadBtn = document.querySelector('.upload-all-btn');
|
||
|
||
// 清空现有预览
|
||
previewFiles.innerHTML = '';
|
||
|
||
// 显示每个待上传文件
|
||
pendingFiles.forEach((file, index) => {
|
||
const fileDiv = document.createElement('div');
|
||
fileDiv.className = 'preview-file';
|
||
|
||
// 确定显示的文件名(如果是文件夹上传,显示相对路径)
|
||
const displayName = file.webkitRelativePath || file.name;
|
||
const isFromFolder = !!file.webkitRelativePath;
|
||
|
||
// 检测文件类型
|
||
const fileType = getFileType(file.name);
|
||
const fileIcon = getFileIcon(fileType);
|
||
|
||
fileDiv.innerHTML = `
|
||
<div class="preview-file-icon ${fileType}">
|
||
<i class="fas ${isFromFolder ? 'fa-folder-open' : fileIcon}"></i>
|
||
</div>
|
||
<div class="preview-file-info">
|
||
<div class="preview-file-name-container">
|
||
<label>文件名:</label>
|
||
<input type="text"
|
||
class="preview-file-name-input"
|
||
value="${file.name}"
|
||
data-index="${index}"
|
||
placeholder="输入文件名"
|
||
onblur="validateFilename(this)"
|
||
oninput="validateFilename(this)">
|
||
</div>
|
||
${isFromFolder ? `<div class="preview-file-path">路径:${file.webkitRelativePath}</div>` : ''}
|
||
<div class="preview-file-size">${formatFileSize(file.size)}</div>
|
||
${isFromFolder ? '<div class="preview-file-type">来自文件夹</div>' : ''}
|
||
<input type="text"
|
||
class="preview-file-comment"
|
||
placeholder="添加文件说明(可选)"
|
||
data-index="${index}">
|
||
</div>
|
||
<button class="remove-file-btn" onclick="removeFile(${index}, event)">
|
||
<i class="fas fa-times"></i>
|
||
</button>
|
||
`;
|
||
previewFiles.appendChild(fileDiv);
|
||
});
|
||
|
||
// 显示预览区域
|
||
previewArea.style.display = pendingFiles.length > 0 ? 'block' : 'none';
|
||
|
||
// 更新上传按钮状态和文本
|
||
if (uploadBtn) {
|
||
uploadBtn.disabled = pendingFiles.length === 0;
|
||
uploadBtn.style.opacity = pendingFiles.length === 0 ? '0.5' : '1';
|
||
uploadBtn.innerHTML = `
|
||
<i class="fas fa-cloud-upload-alt"></i>
|
||
上传 ${pendingFiles.length} 个文件
|
||
`;
|
||
}
|
||
}
|
||
|
||
// 修改移除文件函数
|
||
function removeFile(index, event) {
|
||
event.preventDefault(); // 阻止事件冒泡
|
||
event.stopPropagation();
|
||
pendingFiles.splice(index, 1);
|
||
showPreview();
|
||
}
|
||
|
||
// 清除预览
|
||
function clearPreview() {
|
||
pendingFiles = [];
|
||
const previewArea = document.getElementById('uploadPreview');
|
||
const uploadBtn = document.querySelector('.upload-all-btn');
|
||
|
||
previewArea.style.display = 'none';
|
||
|
||
if (uploadBtn) {
|
||
uploadBtn.disabled = true;
|
||
uploadBtn.style.opacity = '0.5';
|
||
uploadBtn.innerHTML = `
|
||
<i class="fas fa-cloud-upload-alt"></i>
|
||
上传文件
|
||
`;
|
||
}
|
||
|
||
document.getElementById('fileInput').value = '';
|
||
document.getElementById('folderInput').value = '';
|
||
}
|
||
|
||
function showToast(message) {
|
||
toast.textContent = message;
|
||
toast.style.display = 'block';
|
||
setTimeout(() => {
|
||
toast.style.display = 'none';
|
||
}, 3000);
|
||
}
|
||
|
||
function formatFileSize(bytes) {
|
||
if (bytes === 0) return '0 Bytes';
|
||
const k = 1024;
|
||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||
}
|
||
|
||
function loadFiles() {
|
||
const params = new URLSearchParams({
|
||
folder: currentFolder,
|
||
sort_by: currentSortBy,
|
||
sort_order: currentSortOrder,
|
||
search: currentSearch
|
||
});
|
||
|
||
fetch(`/files?${params}`)
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
const fileList = document.getElementById('fileList');
|
||
fileList.innerHTML = '';
|
||
|
||
// 应用紧凑布局到容器
|
||
if (isCompactLayout) {
|
||
fileList.classList.add('compact');
|
||
} else {
|
||
fileList.classList.remove('compact');
|
||
}
|
||
|
||
// 更新面包屑导航
|
||
updateBreadcrumb(data.current_folder, data.parent_folder);
|
||
|
||
// 更新文件夹列表
|
||
folderList = data.files.filter(file => file.type === 'folder');
|
||
|
||
data.files.forEach(file => {
|
||
const fileCard = document.createElement('div');
|
||
fileCard.className = `file-card ${file.type}${isCompactLayout ? ' compact' : ''}`;
|
||
|
||
if (file.type === 'folder') {
|
||
fileCard.innerHTML = `
|
||
<div class="file-header">
|
||
<div class="file-icon">
|
||
<i class="fas fa-folder"></i>
|
||
</div>
|
||
<div class="file-content">
|
||
<div class="file-name">${file.name}</div>
|
||
<div class="file-uploader">
|
||
<i class="fas fa-calendar"></i> 创建时间: ${file.created}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="file-info">
|
||
<div><i class="fas fa-folder"></i> 类型:文件夹</div>
|
||
</div>
|
||
<div class="file-actions">
|
||
<button class="download-btn" onclick="navigateToFolder('${file.path}')">
|
||
<i class="fas fa-folder-open"></i> 打开
|
||
</button>
|
||
<button class="rename-btn" onclick="showRenameFolderDialog('${file.name}', '${currentFolder}')">
|
||
<i class="fas fa-edit"></i> 重命名
|
||
</button>
|
||
${(currentUserInfo.isAdmin || currentUserInfo.adminSessionId) ? `<button class="delete-btn" onclick="deleteFolder('${file.path}')">
|
||
<i class="fas fa-trash"></i> 删除
|
||
</button>` : ''}
|
||
</div>
|
||
`;
|
||
} else {
|
||
// 检测文件类型
|
||
const fileType = getFileType(file.name);
|
||
const fileIcon = getFileIcon(fileType);
|
||
const canShowPreview = canPreview(fileType);
|
||
|
||
fileCard.innerHTML = `
|
||
<div class="file-header">
|
||
<div class="file-icon ${fileType}" onclick="${canShowPreview ? `openMediaPreview('${file.path}', '${file.name}', '${fileType}')` : ''}">
|
||
<i class="fas ${fileIcon}"></i>
|
||
</div>
|
||
<div class="file-content">
|
||
<div class="file-name">${file.name}</div>
|
||
<div class="file-uploader">
|
||
<i class="fas fa-user"></i> 上传者: ${formatUploaderInfo(file.uploader_username, file.uploader_ip)}
|
||
</div>
|
||
${file.comment ? `
|
||
<div class="file-comment">
|
||
<i class="fas fa-comment"></i> ${file.comment}
|
||
</div>
|
||
` : ''}
|
||
</div>
|
||
</div>
|
||
<div class="file-info">
|
||
<div><i class="fas fa-weight"></i> 大小:${formatFileSize(file.size)}</div>
|
||
<div><i class="fas fa-clock"></i> 上传时间:${file.created}</div>
|
||
<div><i class="fas fa-tag"></i> 类型:${fileType.toUpperCase()}</div>
|
||
</div>
|
||
<div class="file-actions">
|
||
${canShowPreview ? `
|
||
<button class="download-btn" onclick="openMediaPreview('${file.path}', '${file.name}', '${fileType}')">
|
||
<i class="fas fa-eye"></i> 预览
|
||
</button>
|
||
` : ''}
|
||
<a href="/download/${file.path}" class="download-btn" target="_blank">
|
||
<i class="fas fa-download"></i> 下载
|
||
</a>
|
||
<button class="rename-btn" onclick="showRenameFileDialog('${file.name}', '${currentFolder}')">
|
||
<i class="fas fa-edit"></i> 重命名
|
||
</button>
|
||
<button class="move-btn" onclick="showMoveFileDialog('${file.name}', '${currentFolder}')">
|
||
<i class="fas fa-cut"></i> 移动
|
||
</button>
|
||
${(currentUserInfo.isAdmin || currentUserInfo.adminSessionId) ? `<button class="delete-btn" onclick="deleteFile('${file.path}')">
|
||
<i class="fas fa-trash"></i> 删除
|
||
</button>` : ''}
|
||
</div>
|
||
`;
|
||
|
||
// 生成缩略图预览
|
||
const iconElement = fileCard.querySelector('.file-icon');
|
||
if (canShowPreview) {
|
||
generateThumbnail(file.path, file.name, fileType, iconElement);
|
||
}
|
||
}
|
||
fileList.appendChild(fileCard);
|
||
});
|
||
})
|
||
.catch(error => {
|
||
console.error('加载文件列表失败:', error);
|
||
showToast('加载文件列表失败');
|
||
});
|
||
}
|
||
|
||
// 更新面包屑导航
|
||
function updateBreadcrumb(currentFolder, parentFolder) {
|
||
const breadcrumb = document.getElementById('breadcrumb');
|
||
breadcrumb.innerHTML = '<a href="#" onclick="navigateToFolder(\'\')"><i class="fas fa-home"></i> 根目录</a>';
|
||
|
||
if (currentFolder) {
|
||
const parts = currentFolder.split('/');
|
||
let path = '';
|
||
parts.forEach((part, index) => {
|
||
if (index > 0) path += '/';
|
||
path += part;
|
||
breadcrumb.innerHTML += `<span class="separator">/</span><a href="#" onclick="navigateToFolder('${path}')">${part}</a>`;
|
||
});
|
||
}
|
||
}
|
||
|
||
// 导航到文件夹
|
||
function navigateToFolder(folder) {
|
||
currentFolder = folder;
|
||
updateBackButton();
|
||
loadFiles();
|
||
}
|
||
|
||
// 更新返回按钮状态
|
||
function updateBackButton() {
|
||
const backBtn = document.getElementById('backBtn');
|
||
if (currentFolder && currentFolder.trim() !== '') {
|
||
backBtn.style.display = 'inline-flex';
|
||
} else {
|
||
backBtn.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
// 处理搜索
|
||
function handleSearch(event) {
|
||
if (event.key === 'Enter' || event.type === 'input') {
|
||
currentSearch = event.target.value;
|
||
applySortAndSearch();
|
||
}
|
||
}
|
||
|
||
// 应用排序和搜索
|
||
function applySortAndSearch() {
|
||
currentSortBy = document.getElementById('sortBy').value;
|
||
currentSearch = document.getElementById('searchInput').value;
|
||
loadFiles();
|
||
}
|
||
|
||
// 切换排序顺序
|
||
function toggleSortOrder() {
|
||
currentSortOrder = currentSortOrder === 'asc' ? 'desc' : 'asc';
|
||
const btn = document.getElementById('sortOrderBtn');
|
||
btn.innerHTML = currentSortOrder === 'asc' ?
|
||
'<i class="fas fa-sort-alpha-down"></i>' :
|
||
'<i class="fas fa-sort-alpha-up"></i>';
|
||
loadFiles();
|
||
}
|
||
|
||
// 切换布局模式
|
||
function toggleLayout() {
|
||
isCompactLayout = !isCompactLayout;
|
||
const btn = document.getElementById('layoutToggleBtn');
|
||
const fileList = document.getElementById('fileList');
|
||
|
||
if (isCompactLayout) {
|
||
fileList.classList.add('compact');
|
||
btn.classList.add('active');
|
||
btn.innerHTML = '<i class="fas fa-th-list"></i>';
|
||
btn.title = '切换到卡片模式';
|
||
} else {
|
||
fileList.classList.remove('compact');
|
||
btn.classList.remove('active');
|
||
btn.innerHTML = '<i class="fas fa-th-large"></i>';
|
||
btn.title = '切换到紧凑模式';
|
||
}
|
||
|
||
// 应用紧凑样式到现有文件卡片
|
||
applyLayoutToCards();
|
||
|
||
// 保存用户偏好
|
||
localStorage.setItem('compactLayout', isCompactLayout);
|
||
|
||
// 显示提示
|
||
showToast(isCompactLayout ? '已切换到紧凑模式' : '已切换到卡片模式');
|
||
}
|
||
|
||
// 应用布局样式到文件卡片
|
||
function applyLayoutToCards() {
|
||
const fileCards = document.querySelectorAll('.file-card');
|
||
fileCards.forEach(card => {
|
||
if (isCompactLayout) {
|
||
card.classList.add('compact');
|
||
} else {
|
||
card.classList.remove('compact');
|
||
}
|
||
});
|
||
}
|
||
|
||
// 显示创建文件夹对话框
|
||
function showCreateFolderDialog() {
|
||
document.getElementById('createFolderModal').style.display = 'flex';
|
||
document.getElementById('folderNameInput').focus();
|
||
}
|
||
|
||
// 隐藏创建文件夹对话框
|
||
function hideCreateFolderDialog() {
|
||
document.getElementById('createFolderModal').style.display = 'none';
|
||
document.getElementById('folderNameInput').value = '';
|
||
}
|
||
|
||
// 创建文件夹
|
||
function createFolder() {
|
||
const folderName = document.getElementById('folderNameInput').value.trim();
|
||
if (!folderName) {
|
||
showToast('请输入文件夹名称');
|
||
return;
|
||
}
|
||
|
||
const formData = new FormData();
|
||
formData.append('folder_name', folderName);
|
||
formData.append('parent_folder', currentFolder);
|
||
|
||
fetch('/folders', {
|
||
method: 'POST',
|
||
body: formData
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.message) {
|
||
showToast(data.message);
|
||
hideCreateFolderDialog();
|
||
loadFiles();
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('创建文件夹失败:', error);
|
||
showToast('创建文件夹失败');
|
||
});
|
||
}
|
||
|
||
// 显示移动文件对话框
|
||
function showMoveFileDialog(fileName, sourceFolder) {
|
||
currentMoveFile = { name: fileName, source: sourceFolder };
|
||
document.getElementById('moveFileName').textContent = fileName;
|
||
|
||
// 填充目标文件夹选择框
|
||
const select = document.getElementById('targetFolderSelect');
|
||
select.innerHTML = '<option value="">根目录</option>';
|
||
|
||
// 递归添加所有文件夹选项
|
||
loadAllFolders().then(folders => {
|
||
folders.forEach(folder => {
|
||
if (folder.path !== sourceFolder) { // 不能移动到当前文件夹
|
||
select.innerHTML += `<option value="${folder.path}">${folder.path || '根目录'}</option>`;
|
||
}
|
||
});
|
||
});
|
||
|
||
document.getElementById('moveFileModal').style.display = 'flex';
|
||
}
|
||
|
||
// 隐藏移动文件对话框
|
||
function hideMoveFileDialog() {
|
||
document.getElementById('moveFileModal').style.display = 'none';
|
||
currentMoveFile = null;
|
||
}
|
||
|
||
// 确认移动文件
|
||
function confirmMoveFile() {
|
||
if (!currentMoveFile) return;
|
||
|
||
const targetFolder = document.getElementById('targetFolderSelect').value;
|
||
const formData = new FormData();
|
||
formData.append('file_name', currentMoveFile.name);
|
||
formData.append('source_folder', currentMoveFile.source);
|
||
formData.append('target_folder', targetFolder);
|
||
|
||
fetch('/move_file', {
|
||
method: 'POST',
|
||
body: formData
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
showToast(data.message);
|
||
hideMoveFileDialog();
|
||
loadFiles();
|
||
})
|
||
.catch(error => {
|
||
console.error('移动文件失败:', error);
|
||
showToast('移动文件失败');
|
||
});
|
||
}
|
||
|
||
// 加载所有文件夹列表
|
||
async function loadAllFolders(folder = '') {
|
||
try {
|
||
const params = new URLSearchParams({ folder, sort_by: 'name' });
|
||
const response = await fetch(`/files?${params}`);
|
||
const data = await response.json();
|
||
|
||
let allFolders = [];
|
||
const folders = data.files.filter(file => file.type === 'folder');
|
||
|
||
// 添加当前层级的文件夹
|
||
allFolders = allFolders.concat(folders);
|
||
|
||
// 递归加载子文件夹
|
||
for (const childFolder of folders) {
|
||
const subFolders = await loadAllFolders(childFolder.path);
|
||
allFolders = allFolders.concat(subFolders);
|
||
}
|
||
|
||
return allFolders;
|
||
} catch (error) {
|
||
console.error('加载文件夹列表失败:', error);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
// 删除文件夹
|
||
async function deleteFolder(folderPath) {
|
||
// 检查管理员权限 - 支持通过IP或登录获得的管理员权限
|
||
if (!currentUserInfo.isAdmin && !currentUserInfo.adminSessionId) {
|
||
showToast('权限不足,只有管理员才能删除文件夹');
|
||
return;
|
||
}
|
||
|
||
if (!confirm('确定要删除这个文件夹吗?文件夹内的所有文件和子文件夹都将被永久删除。')) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const headers = {};
|
||
// 如果有admin session,添加到请求头
|
||
if (currentUserInfo.adminSessionId) {
|
||
headers['Authorization'] = `AdminSession ${currentUserInfo.adminSessionId}`;
|
||
}
|
||
|
||
const response = await fetch(`/folders/${folderPath}`, {
|
||
method: 'DELETE',
|
||
headers: headers
|
||
});
|
||
|
||
if (response.status === 403) {
|
||
showToast('权限不足,只有管理员才能删除文件夹');
|
||
return;
|
||
}
|
||
|
||
const result = await response.json();
|
||
showToast(result.message);
|
||
loadFiles(); // 重新加载文件列表
|
||
} catch (error) {
|
||
showToast('删除失败:' + error.message);
|
||
}
|
||
}
|
||
|
||
// 显示重命名文件对话框
|
||
function showRenameFileDialog(fileName, folderPath) {
|
||
currentRenameFile = { name: fileName, folder: folderPath };
|
||
document.getElementById('currentFileName').textContent = fileName;
|
||
document.getElementById('newFileNameInput').value = fileName;
|
||
document.getElementById('renameFileModal').style.display = 'flex';
|
||
document.getElementById('newFileNameInput').focus();
|
||
document.getElementById('newFileNameInput').select();
|
||
}
|
||
|
||
// 隐藏重命名文件对话框
|
||
function hideRenameFileDialog() {
|
||
document.getElementById('renameFileModal').style.display = 'none';
|
||
currentRenameFile = null;
|
||
}
|
||
|
||
// 确认重命名文件
|
||
function confirmRenameFile() {
|
||
if (!currentRenameFile) return;
|
||
|
||
const newName = document.getElementById('newFileNameInput').value.trim();
|
||
if (!newName) {
|
||
showToast('请输入新文件名');
|
||
return;
|
||
}
|
||
|
||
if (newName === currentRenameFile.name) {
|
||
showToast('新文件名与原文件名相同');
|
||
return;
|
||
}
|
||
|
||
const formData = new FormData();
|
||
formData.append('old_name', currentRenameFile.name);
|
||
formData.append('new_name', newName);
|
||
formData.append('folder_path', currentRenameFile.folder);
|
||
|
||
fetch('/rename_file', {
|
||
method: 'POST',
|
||
body: formData
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
showToast(data.message);
|
||
hideRenameFileDialog();
|
||
loadFiles();
|
||
})
|
||
.catch(error => {
|
||
console.error('重命名文件失败:', error);
|
||
showToast('重命名文件失败');
|
||
});
|
||
}
|
||
|
||
// 显示重命名文件夹对话框
|
||
function showRenameFolderDialog(folderName, parentFolder) {
|
||
currentRenameFolder = { name: folderName, parent: parentFolder };
|
||
document.getElementById('currentFolderName').textContent = folderName;
|
||
document.getElementById('newFolderNameInput').value = folderName;
|
||
document.getElementById('renameFolderModal').style.display = 'flex';
|
||
document.getElementById('newFolderNameInput').focus();
|
||
document.getElementById('newFolderNameInput').select();
|
||
}
|
||
|
||
// 隐藏重命名文件夹对话框
|
||
function hideRenameFolderDialog() {
|
||
document.getElementById('renameFolderModal').style.display = 'none';
|
||
currentRenameFolder = null;
|
||
}
|
||
|
||
// 确认重命名文件夹
|
||
function confirmRenameFolder() {
|
||
if (!currentRenameFolder) return;
|
||
|
||
const newName = document.getElementById('newFolderNameInput').value.trim();
|
||
if (!newName) {
|
||
showToast('请输入新文件夹名');
|
||
return;
|
||
}
|
||
|
||
if (newName === currentRenameFolder.name) {
|
||
showToast('新文件夹名与原文件夹名相同');
|
||
return;
|
||
}
|
||
|
||
const formData = new FormData();
|
||
formData.append('old_name', currentRenameFolder.name);
|
||
formData.append('new_name', newName);
|
||
formData.append('parent_folder', currentRenameFolder.parent);
|
||
|
||
fetch('/rename_folder', {
|
||
method: 'POST',
|
||
body: formData
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
showToast(data.message);
|
||
hideRenameFolderDialog();
|
||
loadFiles();
|
||
})
|
||
.catch(error => {
|
||
console.error('重命名文件夹失败:', error);
|
||
showToast('重命名文件夹失败');
|
||
});
|
||
}
|
||
|
||
// 添加删除文件的函数
|
||
async function deleteFile(filename) {
|
||
// 检查管理员权限 - 支持通过IP或登录获得的管理员权限
|
||
if (!currentUserInfo.isAdmin && !currentUserInfo.adminSessionId) {
|
||
showToast('权限不足,只有管理员才能删除文件');
|
||
return;
|
||
}
|
||
|
||
if (!confirm('确定要删除这个文件吗?')) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const headers = {};
|
||
// 如果有admin session,添加到请求头
|
||
if (currentUserInfo.adminSessionId) {
|
||
headers['Authorization'] = `AdminSession ${currentUserInfo.adminSessionId}`;
|
||
}
|
||
|
||
const response = await fetch(`/files/${filename}`, {
|
||
method: 'DELETE',
|
||
headers: headers
|
||
});
|
||
|
||
if (response.status === 403) {
|
||
showToast('权限不足,只有管理员才能删除文件');
|
||
return;
|
||
}
|
||
|
||
const result = await response.json();
|
||
showToast(result.message);
|
||
loadFiles(); // 重新加载文件列表
|
||
} catch (error) {
|
||
showToast('删除失败:' + error.message);
|
||
}
|
||
}
|
||
|
||
// 全局用户信息变量
|
||
let currentUserInfo = {
|
||
ip: '',
|
||
name: '',
|
||
avatar: '',
|
||
isAdmin: false,
|
||
adminSessionId: null // 添加admin会话ID
|
||
};
|
||
|
||
// 全局IP名称映射
|
||
let ipNameMap = {};
|
||
let ipAvatarMap = {};
|
||
|
||
// 可用的emoji头像列表
|
||
const availableEmojis = [
|
||
'👨💻', '👩💻', '🧑💼', '👨🔬', '👩🔬', '🧑🎨', '👨🎨', '👩🎨',
|
||
'🧑🚀', '👨🚀', '👩🚀', '🧑⚕️', '👨⚕️', '👩⚕️', '🧑🏫', '👨🏫',
|
||
'👩🏫', '🧑💼', '👨💼', '👩💼', '🧑🔧', '👨🔧', '👩🔧', '🧑🌾',
|
||
'👨🌾', '👩🌾', '🧑🍳', '👨🍳', '👩🍳', '🧑🎓', '👨🎓', '👩🎓',
|
||
'🤖', '👽', '🦸♂️', '🦸♀️', '🦹♂️', '🦹♀️', '🧙♂️', '🧙♀️',
|
||
'🧚♂️', '🧚♀️', '🧞♂️', '🧞♀️', '🧝♂️', '🧝♀️', '🧛♂️', '🧛♀️',
|
||
'🎭', '🎨', '🎪', '🎯', '🎲', '🎸', '🎺', '🎷', '🎻', '🎹'
|
||
];
|
||
|
||
// 为IP随机分配的emoji头像缓存
|
||
let emojiAvatarCache = {};
|
||
|
||
// 智能头像分配函数
|
||
function getAvatarForIP(ip) {
|
||
// 1. 首先检查是否有配置的图片头像
|
||
if (ipAvatarMap[ip]) {
|
||
return {
|
||
type: 'image',
|
||
src: ipAvatarMap[ip]
|
||
};
|
||
}
|
||
|
||
// 2. 检查是否已经为该IP随机分配过emoji头像
|
||
if (emojiAvatarCache[ip]) {
|
||
return {
|
||
type: 'emoji',
|
||
emoji: emojiAvatarCache[ip]
|
||
};
|
||
}
|
||
|
||
// 3. 为新IP随机分配一个emoji头像
|
||
const randomIndex = Math.floor(Math.random() * availableEmojis.length);
|
||
const assignedEmoji = availableEmojis[randomIndex];
|
||
|
||
// 4. 缓存分配结果
|
||
emojiAvatarCache[ip] = assignedEmoji;
|
||
|
||
// 5. 保存到localStorage以便持久化
|
||
try {
|
||
localStorage.setItem('emojiAvatarCache', JSON.stringify(emojiAvatarCache));
|
||
} catch (e) {
|
||
console.warn('无法保存emoji头像缓存到localStorage:', e);
|
||
}
|
||
|
||
return {
|
||
type: 'emoji',
|
||
emoji: assignedEmoji
|
||
};
|
||
}
|
||
|
||
// 从localStorage恢复emoji头像缓存
|
||
function loadEmojiAvatarCache() {
|
||
try {
|
||
const cached = localStorage.getItem('emojiAvatarCache');
|
||
if (cached) {
|
||
emojiAvatarCache = JSON.parse(cached);
|
||
}
|
||
} catch (e) {
|
||
console.warn('无法从localStorage加载emoji头像缓存:', e);
|
||
emojiAvatarCache = {};
|
||
}
|
||
}
|
||
|
||
// 加载用户信息
|
||
async function loadUserInfo() {
|
||
try {
|
||
// 先加载emoji头像缓存
|
||
loadEmojiAvatarCache();
|
||
|
||
// 获取当前用户IP和管理员信息
|
||
const [ipResponse, adminResponse, ipNamesResponse] = await Promise.all([
|
||
fetch('/get_ip'),
|
||
fetch('/admin/info'),
|
||
fetch('/get_ip_names')
|
||
]);
|
||
|
||
const ipData = await ipResponse.json();
|
||
const adminData = await adminResponse.json();
|
||
const ipNamesData = await ipNamesResponse.json();
|
||
|
||
currentUserInfo.ip = ipData.ip;
|
||
currentUserInfo.isAdmin = adminData.is_admin;
|
||
|
||
// 设置用户名称和头像
|
||
ipNameMap = ipNamesData.ip_vs_name || {};
|
||
ipAvatarMap = ipNamesData.ip_vs_avatar || {};
|
||
|
||
currentUserInfo.name = ipNameMap[currentUserInfo.ip] || currentUserInfo.ip;
|
||
// 使用智能头像分配
|
||
currentUserInfo.avatar = getAvatarForIP(currentUserInfo.ip);
|
||
|
||
// 检查admin会话状态
|
||
await checkAdminSession();
|
||
|
||
// 更新UI
|
||
updateUserInfoDisplay();
|
||
|
||
// 设置头像点击事件
|
||
setupAvatarClickEvent();
|
||
|
||
} catch (error) {
|
||
console.error('加载用户信息失败:', error);
|
||
// 使用默认值
|
||
currentUserInfo.ip = 'unknown';
|
||
currentUserInfo.name = '访客';
|
||
currentUserInfo.avatar = {
|
||
type: 'image',
|
||
src: '/image/avatar/server.png'
|
||
};
|
||
currentUserInfo.isAdmin = false;
|
||
updateUserInfoDisplay();
|
||
setupAvatarClickEvent();
|
||
}
|
||
}
|
||
|
||
// 更新用户信息显示
|
||
function updateUserInfoDisplay() {
|
||
const userInfo = document.getElementById('userInfo');
|
||
const userAvatar = document.getElementById('userAvatar');
|
||
const userName = document.getElementById('userName');
|
||
const userRole = document.getElementById('userRole');
|
||
const userIP = document.getElementById('userIP');
|
||
|
||
if (userInfo && userAvatar && userName && userRole && userIP) {
|
||
// 清除之前的admin指示器
|
||
const existingIndicator = userAvatar.querySelector('.admin-indicator');
|
||
if (existingIndicator) {
|
||
existingIndicator.remove();
|
||
}
|
||
|
||
// 根据头像类型显示不同的内容
|
||
if (currentUserInfo.avatar.type === 'image') {
|
||
// 显示图片头像
|
||
userAvatar.innerHTML = `<img src="${currentUserInfo.avatar.src}" alt="用户头像" style="width: 100%; height: 100%; object-fit: cover; border-radius: 50%;">`;
|
||
userAvatar.classList.remove('emoji-avatar');
|
||
} else if (currentUserInfo.avatar.type === 'emoji') {
|
||
// 显示emoji头像
|
||
userAvatar.textContent = currentUserInfo.avatar.emoji;
|
||
userAvatar.classList.add('emoji-avatar');
|
||
}
|
||
|
||
// 如果已经admin登录,添加指示器
|
||
if (currentUserInfo.adminSessionId) {
|
||
const indicator = document.createElement('div');
|
||
indicator.className = 'admin-indicator';
|
||
userAvatar.appendChild(indicator);
|
||
}
|
||
|
||
userName.textContent = currentUserInfo.name;
|
||
userIP.textContent = currentUserInfo.ip;
|
||
|
||
if (currentUserInfo.isAdmin || currentUserInfo.adminSessionId) {
|
||
userRole.innerHTML = `<span class="admin-badge">管理员</span> IP: <span id="userIP">${currentUserInfo.ip}</span>`;
|
||
} else {
|
||
userRole.innerHTML = `IP: <span id="userIP">${currentUserInfo.ip}</span>`;
|
||
}
|
||
|
||
userInfo.style.display = 'flex';
|
||
}
|
||
}
|
||
|
||
// Admin登录相关功能
|
||
function showAdminLogin() {
|
||
// 如果已经是admin登录状态,显示登出选项
|
||
if (currentUserInfo.adminSessionId) {
|
||
if (confirm('您已经是管理员登录状态,是否要登出?')) {
|
||
logoutAdmin();
|
||
}
|
||
return;
|
||
}
|
||
|
||
const modal = document.getElementById('adminLoginModal');
|
||
modal.classList.add('active');
|
||
|
||
// 清空表单
|
||
document.getElementById('adminUsername').value = '';
|
||
document.getElementById('adminPassword').value = '';
|
||
document.getElementById('adminLoginMessage').innerHTML = '';
|
||
|
||
// 聚焦到用户名输入框
|
||
setTimeout(() => {
|
||
document.getElementById('adminUsername').focus();
|
||
}, 100);
|
||
}
|
||
|
||
function closeAdminLogin() {
|
||
const modal = document.getElementById('adminLoginModal');
|
||
if (modal) {
|
||
modal.classList.remove('active');
|
||
}
|
||
}
|
||
|
||
async function submitAdminLogin(event) {
|
||
event.preventDefault();
|
||
|
||
const username = document.getElementById('adminUsername').value;
|
||
const password = document.getElementById('adminPassword').value;
|
||
const messageDiv = document.getElementById('adminLoginMessage');
|
||
|
||
if (!username || !password) {
|
||
showAdminMessage('请输入用户名和密码', 'error');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
showAdminMessage('正在登录...', 'info');
|
||
|
||
const formData = new FormData();
|
||
formData.append('username', username);
|
||
formData.append('password', password);
|
||
|
||
const response = await fetch('/admin/login', {
|
||
method: 'POST',
|
||
body: formData
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (response.ok && result.success) {
|
||
// 登录成功
|
||
currentUserInfo.adminSessionId = result.admin_session_id;
|
||
|
||
// 保存admin会话到localStorage
|
||
localStorage.setItem('adminSessionId', result.admin_session_id);
|
||
|
||
showAdminMessage('登录成功!', 'success');
|
||
|
||
// 更新用户界面
|
||
updateUserInfoDisplay();
|
||
|
||
// 重新加载文件列表以显示删除按钮
|
||
loadFiles();
|
||
|
||
// 1秒后关闭弹窗
|
||
setTimeout(() => {
|
||
closeAdminLogin();
|
||
}, 1000);
|
||
|
||
} else {
|
||
showAdminMessage(result.detail || '登录失败', 'error');
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('Admin登录失败:', error);
|
||
showAdminMessage('网络错误,请稍后重试', 'error');
|
||
}
|
||
}
|
||
|
||
async function logoutAdmin() {
|
||
if (!currentUserInfo.adminSessionId) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const formData = new FormData();
|
||
formData.append('admin_session_id', currentUserInfo.adminSessionId);
|
||
|
||
const response = await fetch('/admin/logout', {
|
||
method: 'POST',
|
||
body: formData
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (response.ok && result.success) {
|
||
console.log('Admin登出成功');
|
||
} else {
|
||
console.warn('Admin登出失败:', result.detail);
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('Admin登出请求失败:', error);
|
||
} finally {
|
||
// 无论服务器响应如何,都清除本地状态
|
||
currentUserInfo.adminSessionId = null;
|
||
localStorage.removeItem('adminSessionId');
|
||
updateUserInfoDisplay();
|
||
|
||
// 重新加载文件列表以隐藏删除按钮
|
||
loadFiles();
|
||
}
|
||
}
|
||
|
||
async function checkAdminSession() {
|
||
const sessionId = localStorage.getItem('adminSessionId');
|
||
if (!sessionId) {
|
||
return false;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`/admin/check_session?admin_session_id=${encodeURIComponent(sessionId)}`);
|
||
const result = await response.json();
|
||
|
||
if (result.valid) {
|
||
currentUserInfo.adminSessionId = sessionId;
|
||
return true;
|
||
} else {
|
||
// 会话无效,清除本地存储
|
||
localStorage.removeItem('adminSessionId');
|
||
return false;
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('检查admin会话失败:', error);
|
||
localStorage.removeItem('adminSessionId');
|
||
return false;
|
||
}
|
||
}
|
||
|
||
function showAdminMessage(message, type) {
|
||
const messageDiv = document.getElementById('adminLoginMessage');
|
||
messageDiv.innerHTML = `<div class="admin-login-${type}">${message}</div>`;
|
||
}
|
||
|
||
// 点击头像事件处理
|
||
function setupAvatarClickEvent() {
|
||
const userAvatar = document.getElementById('userAvatar');
|
||
if (userAvatar) {
|
||
userAvatar.addEventListener('click', showAdminLogin);
|
||
}
|
||
}
|
||
|
||
// 点击弹窗外部关闭
|
||
document.addEventListener('click', function(event) {
|
||
const modal = document.getElementById('adminLoginModal');
|
||
if (event.target === modal) {
|
||
closeAdminLogin();
|
||
}
|
||
});
|
||
|
||
// ESC键关闭弹窗
|
||
document.addEventListener('keydown', function(event) {
|
||
if (event.key === 'Escape') {
|
||
closeAdminLogin();
|
||
}
|
||
});
|
||
|
||
// 格式化上传者信息
|
||
function formatUploaderInfo(uploaderUsername, uploaderIp) {
|
||
// 如果uploader_username包含@符号,说明已经是格式化过的名称
|
||
if (uploaderUsername && uploaderUsername.includes('@')) {
|
||
return uploaderUsername;
|
||
}
|
||
|
||
// 否则尝试从IP映射中获取名称
|
||
if (uploaderIp && ipNameMap[uploaderIp]) {
|
||
return `${ipNameMap[uploaderIp]}@${uploaderIp}`;
|
||
}
|
||
|
||
// fallback:使用原始信息
|
||
return uploaderUsername || uploaderIp || '未知用户';
|
||
}
|
||
|
||
// 初始加载文件列表(在页面完全加载后进行)
|
||
// loadFiles() 现在将在 DOMContentLoaded 事件中调用
|
||
|
||
// 添加开关状态保存
|
||
document.getElementById('quickUploadToggle').addEventListener('change', function(e) {
|
||
const isQuickUpload = e.target.checked;
|
||
document.querySelector('.upload-section').classList.toggle('quick-upload-mode', isQuickUpload);
|
||
|
||
// 保存设置
|
||
localStorage.setItem('quickUpload', isQuickUpload);
|
||
|
||
// 显示状态提示
|
||
showUploadStatus(isQuickUpload ? '已开启快速上传' : '已关闭快速上传');
|
||
});
|
||
|
||
// 添加跳过验证开关
|
||
document.getElementById('skipValidationToggle').addEventListener('change', function(e) {
|
||
const skipValidation = e.target.checked;
|
||
|
||
// 保存设置
|
||
localStorage.setItem('skipValidation', skipValidation);
|
||
|
||
// 显示状态提示
|
||
showUploadStatus(skipValidation ? '已跳过文件名检查' : '已开启文件名检查');
|
||
|
||
// 如果开启跳过验证,清除所有现有的错误提示
|
||
if (skipValidation) {
|
||
document.querySelectorAll('.filename-error').forEach(error => error.remove());
|
||
document.querySelectorAll('.preview-file-name-input').forEach(input => {
|
||
input.classList.remove('invalid');
|
||
});
|
||
}
|
||
});
|
||
|
||
// 添加状态提示函数
|
||
function showUploadStatus(message) {
|
||
let status = document.querySelector('.upload-status');
|
||
if (!status) {
|
||
status = document.createElement('div');
|
||
status.className = 'upload-status';
|
||
document.body.appendChild(status);
|
||
}
|
||
|
||
status.textContent = message;
|
||
status.style.display = 'block';
|
||
|
||
setTimeout(() => {
|
||
status.style.display = 'none';
|
||
}, 2000);
|
||
}
|
||
|
||
// 显示内网资产
|
||
function showInternalAssets() {
|
||
// 创建模态框
|
||
const modal = document.createElement('div');
|
||
modal.className = 'modal-overlay';
|
||
modal.id = 'internalAssetsModal';
|
||
modal.style.display = 'flex';
|
||
|
||
// 创建模态框内容
|
||
const modalContent = document.createElement('div');
|
||
modalContent.className = 'modal';
|
||
modalContent.style.maxWidth = '900px';
|
||
modalContent.style.width = '90%';
|
||
|
||
// 模态框头部
|
||
const modalHeader = document.createElement('div');
|
||
modalHeader.className = 'modal-header';
|
||
modalHeader.innerHTML = `
|
||
<h3><i class="fas fa-network-wired"></i> 内网资产</h3>
|
||
<button class="close-btn" onclick="closeInternalAssetsModal()">
|
||
<i class="fas fa-times"></i>
|
||
</button>
|
||
`;
|
||
|
||
// 模态框主体
|
||
const modalBody = document.createElement('div');
|
||
modalBody.className = 'modal-body';
|
||
modalBody.innerHTML = `
|
||
<div class="internal-assets-container">
|
||
<div class="asset-section">
|
||
<h4><i class="fas fa-desktop"></i> 香橙派资源</h4>
|
||
<div class="asset-list">
|
||
<div class="asset-item" data-asset="orange-1panel">
|
||
<div class="asset-info">
|
||
<div class="asset-main">
|
||
<div class="asset-icon">
|
||
<i class="fas fa-server"></i>
|
||
</div>
|
||
<div class="asset-details">
|
||
<h5>香橙派1Panel</h5>
|
||
<p>6.6.6.86:3333/jeremypi</p>
|
||
</div>
|
||
<div class="asset-actions">
|
||
<a href="http://6.6.6.86:3333/jeremypi" target="_blank" class="asset-link">
|
||
<i class="fas fa-external-link-alt"></i> 访问
|
||
</a>
|
||
<button class="credential-toggle" id="toggle-orange-1panel" onclick="showCredentials('orange-1panel')">
|
||
<i class="fas fa-eye"></i> 查看账号
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="asset-credentials" id="credentials-orange-1panel" style="display: none;">
|
||
<div class="credential-item">
|
||
<span class="credential-label">账号:</span>
|
||
<span class="credential-value">jeremypi</span>
|
||
<button class="copy-btn" onclick="copyToClipboard('jeremypi', 'orange-pi-1panel')">
|
||
<i class="fas fa-copy"></i>
|
||
</button>
|
||
</div>
|
||
<div class="credential-item">
|
||
<span class="credential-label">密码:</span>
|
||
<span class="credential-value">123tangledup-ai</span>
|
||
<button class="copy-btn" onclick="copyToClipboard('123tangledup-ai', 'orange-pi-1panel')">
|
||
<i class="fas fa-copy"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="asset-item" data-asset="orange-vscode">
|
||
<div class="asset-info">
|
||
<div class="asset-main">
|
||
<div class="asset-icon">
|
||
<i class="fas fa-code"></i>
|
||
</div>
|
||
<div class="asset-details">
|
||
<h5>香橙派VScode</h5>
|
||
<p>6.6.6.86:8088</p>
|
||
</div>
|
||
<div class="asset-actions">
|
||
<a href="http://6.6.6.86:8088" target="_blank" class="asset-link">
|
||
<i class="fas fa-external-link-alt"></i> 访问
|
||
</a>
|
||
<button class="credential-toggle" id="toggle-orange-vscode" onclick="showCredentials('orange-vscode')">
|
||
<i class="fas fa-eye"></i> 查看密码
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="asset-credentials" id="credentials-orange-vscode" style="display: none;">
|
||
<div class="credential-item">
|
||
<span class="credential-label">密码:</span>
|
||
<span class="credential-value">jeremypi</span>
|
||
<button class="copy-btn" onclick="copyToClipboard('jeremypi', 'orange-pi-vscode')">
|
||
<i class="fas fa-copy"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="asset-section">
|
||
<h4><i class="fas fa-laptop"></i> 台式机资源</h4>
|
||
<div class="asset-list">
|
||
<div class="asset-item" data-asset="desktop-1panel">
|
||
<div class="asset-info">
|
||
<div class="asset-main">
|
||
<div class="asset-icon">
|
||
<i class="fas fa-server"></i>
|
||
</div>
|
||
<div class="asset-details">
|
||
<h5>台式机ubuntu1Panel</h5>
|
||
<p>6.6.6.66:3333/quantspeed</p>
|
||
</div>
|
||
<div class="asset-actions">
|
||
<a href="http://6.6.6.66:3333/quantspeed" target="_blank" class="asset-link">
|
||
<i class="fas fa-external-link-alt"></i> 访问
|
||
</a>
|
||
<button class="credential-toggle" id="toggle-desktop-1panel" onclick="showCredentials('desktop-1panel')">
|
||
<i class="fas fa-eye"></i> 查看账号
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="asset-credentials" id="credentials-desktop-1panel" style="display: none;">
|
||
<div class="credential-item">
|
||
<span class="credential-label">账号:</span>
|
||
<span class="credential-value">quantspeed</span>
|
||
<button class="copy-btn" onclick="copyToClipboard('quantspeed', 'desktop-1panel')">
|
||
<i class="fas fa-copy"></i>
|
||
</button>
|
||
</div>
|
||
<div class="credential-item">
|
||
<span class="credential-label">密码:</span>
|
||
<span class="credential-value">123quantspeed</span>
|
||
<button class="copy-btn" onclick="copyToClipboard('123quantspeed', 'desktop-1panel')">
|
||
<i class="fas fa-copy"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="asset-item" data-asset="desktop-vscode">
|
||
<div class="asset-info">
|
||
<div class="asset-main">
|
||
<div class="asset-icon">
|
||
<i class="fas fa-code"></i>
|
||
</div>
|
||
<div class="asset-details">
|
||
<h5>台式机VScode</h5>
|
||
<p>6.6.6.66:8080</p>
|
||
</div>
|
||
<div class="asset-actions">
|
||
<a href="http://6.6.6.66:8080" target="_blank" class="asset-link">
|
||
<i class="fas fa-external-link-alt"></i> 访问
|
||
</a>
|
||
<button class="credential-toggle" id="toggle-desktop-vscode" onclick="showCredentials('desktop-vscode')">
|
||
<i class="fas fa-eye"></i> 查看密码
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="asset-credentials" id="credentials-desktop-vscode" style="display: none;">
|
||
<div class="credential-item">
|
||
<span class="credential-label">密码:</span>
|
||
<span class="credential-value">quant-speed</span>
|
||
<button class="copy-btn" onclick="copyToClipboard('quant-speed', 'desktop-vscode')">
|
||
<i class="fas fa-copy"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="asset-section">
|
||
<h4><i class="fas fa-th-large"></i> 应用服务</h4>
|
||
<div class="asset-list">
|
||
<!-- it-tools -->
|
||
<div class="asset-item" data-asset="it-tools">
|
||
<div class="asset-info">
|
||
<div class="asset-main">
|
||
<div class="asset-icon">
|
||
<i class="fas fa-toolbox"></i>
|
||
</div>
|
||
<div class="asset-details">
|
||
<h5>it-tools</h5>
|
||
<p>6.6.6.66:40116</p>
|
||
</div>
|
||
<div class="asset-actions">
|
||
<a href="http://6.6.6.66:40116" target="_blank" class="asset-link">
|
||
<i class="fas fa-external-link-alt"></i> 访问
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- langflow -->
|
||
<div class="asset-item" data-asset="langflow">
|
||
<div class="asset-info">
|
||
<div class="asset-main">
|
||
<div class="asset-icon">
|
||
<i class="fas fa-wind"></i>
|
||
</div>
|
||
<div class="asset-details">
|
||
<h5>langflow</h5>
|
||
<p>6.6.6.66:7860</p>
|
||
</div>
|
||
<div class="asset-actions">
|
||
<a href="http://6.6.6.66:7860" target="_blank" class="asset-link">
|
||
<i class="fas fa-external-link-alt"></i> 访问
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- n8n -->
|
||
<div class="asset-item" data-asset="n8n">
|
||
<div class="asset-info">
|
||
<div class="asset-main">
|
||
<div class="asset-icon">
|
||
<i class="fas fa-project-diagram"></i>
|
||
</div>
|
||
<div class="asset-details">
|
||
<h5>n8n</h5>
|
||
<p>6.6.6.66:5678</p>
|
||
</div>
|
||
<div class="asset-actions">
|
||
<a href="http://6.6.6.66:5678" target="_blank" class="asset-link">
|
||
<i class="fas fa-external-link-alt"></i> 访问
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- linux-command -->
|
||
<div class="asset-item" data-asset="linux-command">
|
||
<div class="asset-info">
|
||
<div class="asset-main">
|
||
<div class="asset-icon">
|
||
<i class="fas fa-terminal"></i>
|
||
</div>
|
||
<div class="asset-details">
|
||
<h5>Linux指令搜索</h5>
|
||
<p>6.6.6.66:40255</p>
|
||
</div>
|
||
<div class="asset-actions">
|
||
<a href="http://6.6.6.66:40255/" target="_blank" class="asset-link">
|
||
<i class="fas fa-external-link-alt"></i> 访问
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- ntfy -->
|
||
<div class="asset-item" data-asset="ntfy">
|
||
<div class="asset-info">
|
||
<div class="asset-main">
|
||
<div class="asset-icon">
|
||
<i class="fas fa-comment-dots"></i>
|
||
</div>
|
||
<div class="asset-details">
|
||
<h5>ntfy</h5>
|
||
<p>6.6.6.66:40265</p>
|
||
</div>
|
||
<div class="asset-actions">
|
||
<a href="http://6.6.6.66:40265" target="_blank" class="asset-link">
|
||
<i class="fas fa-external-link-alt"></i> 访问
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- screego -->
|
||
<div class="asset-item" data-asset="screego">
|
||
<div class="asset-info">
|
||
<div class="asset-main">
|
||
<div class="asset-icon">
|
||
<i class="fas fa-desktop"></i>
|
||
</div>
|
||
<div class="asset-details">
|
||
<h5>screego</h5>
|
||
<p>6.6.6.66:5050</p>
|
||
</div>
|
||
<div class="asset-actions">
|
||
<a href="http://6.6.6.66:5050" target="_blank" class="asset-link">
|
||
<i class="fas fa-external-link-alt"></i> 访问
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- qrcode -->
|
||
<div class="asset-item" data-asset="qrcode">
|
||
<div class="asset-info">
|
||
<div class="asset-main">
|
||
<div class="asset-icon">
|
||
<i class="fas fa-qrcode"></i>
|
||
</div>
|
||
<div class="asset-details">
|
||
<h5>叠加态QR-code工具</h5>
|
||
<p>6.6.6.86:8711</p>
|
||
</div>
|
||
<div class="asset-actions">
|
||
<a href="http://6.6.6.86:8711/" target="_blank" class="asset-link">
|
||
<i class="fas fa-external-link-alt"></i> 访问
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
// 添加样式
|
||
const style = document.createElement('style');
|
||
style.textContent = `
|
||
.internal-assets-container {
|
||
max-height: 70vh;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.asset-section {
|
||
margin-bottom: 25px;
|
||
}
|
||
|
||
.asset-section h4 {
|
||
margin-bottom: 15px;
|
||
color: var(--primary-color);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-size: 18px;
|
||
}
|
||
|
||
.asset-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 15px;
|
||
}
|
||
|
||
.asset-item {
|
||
background: rgba(74, 144, 226, 0.05);
|
||
border: 1px solid rgba(74, 144, 226, 0.1);
|
||
border-radius: 12px;
|
||
overflow: hidden;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.asset-item:hover {
|
||
background: rgba(74, 144, 226, 0.1);
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 5px 15px rgba(74, 144, 226, 0.15);
|
||
}
|
||
|
||
.asset-info {
|
||
padding: 0;
|
||
}
|
||
|
||
.asset-main {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 20px;
|
||
gap: 15px;
|
||
}
|
||
|
||
.asset-icon {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 50px;
|
||
height: 50px;
|
||
background: rgba(74, 144, 226, 0.1);
|
||
border-radius: 10px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.asset-icon i {
|
||
font-size: 24px;
|
||
color: var(--primary-color);
|
||
}
|
||
|
||
.asset-details {
|
||
flex: 1;
|
||
}
|
||
|
||
.asset-details h5 {
|
||
margin: 0 0 5px 0;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: var(--text-color);
|
||
}
|
||
|
||
.asset-details p {
|
||
margin: 0;
|
||
font-size: 14px;
|
||
color: #666;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.asset-actions {
|
||
display: flex;
|
||
gap: 10px;
|
||
align-items: center;
|
||
}
|
||
|
||
.asset-link {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
padding: 8px 15px;
|
||
background: var(--primary-color);
|
||
color: white;
|
||
text-decoration: none;
|
||
border-radius: 6px;
|
||
font-size: 14px;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.asset-link:hover {
|
||
background: var(--secondary-color);
|
||
}
|
||
|
||
.credential-toggle {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
padding: 8px 15px;
|
||
background: #f0f0f0;
|
||
color: var(--text-color);
|
||
border: none;
|
||
border-radius: 6px;
|
||
font-size: 14px;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.credential-toggle:hover {
|
||
background: #e0e0e0;
|
||
}
|
||
|
||
.asset-credentials {
|
||
background: rgba(255, 255, 255, 0.7);
|
||
border-top: 1px solid rgba(74, 144, 226, 0.1);
|
||
padding: 15px 20px;
|
||
animation: slideDown 0.3s ease-in-out;
|
||
}
|
||
|
||
.credential-item {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 10px;
|
||
gap: 10px;
|
||
}
|
||
|
||
.credential-item:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.credential-label {
|
||
font-weight: 600;
|
||
min-width: 50px;
|
||
color: var(--primary-color);
|
||
}
|
||
|
||
.credential-value {
|
||
flex: 1;
|
||
background: rgba(255, 255, 255, 0.8);
|
||
padding: 6px 12px;
|
||
border-radius: 6px;
|
||
font-family: monospace;
|
||
font-size: 14px;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.copy-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 32px;
|
||
height: 32px;
|
||
background: var(--primary-color);
|
||
color: white;
|
||
border: none;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.copy-btn:hover {
|
||
background: var(--secondary-color);
|
||
}
|
||
|
||
.toast-notification {
|
||
position: fixed;
|
||
bottom: 20px;
|
||
right: 20px;
|
||
background: rgba(0, 0, 0, 0.8);
|
||
color: white;
|
||
padding: 12px 20px;
|
||
border-radius: 6px;
|
||
transform: translateY(100px);
|
||
opacity: 0;
|
||
transition: all 0.3s ease;
|
||
z-index: 10000;
|
||
}
|
||
|
||
.toast-notification.show {
|
||
transform: translateY(0);
|
||
opacity: 1;
|
||
}
|
||
|
||
@keyframes slideDown {
|
||
from {
|
||
opacity: 0;
|
||
max-height: 0;
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
max-height: 200px;
|
||
}
|
||
}
|
||
`;
|
||
|
||
// 组装模态框
|
||
modalContent.appendChild(modalHeader);
|
||
modalContent.appendChild(modalBody);
|
||
modal.appendChild(modalContent);
|
||
document.head.appendChild(style);
|
||
document.body.appendChild(modal);
|
||
|
||
// 点击模态框外部关闭
|
||
modal.addEventListener('click', function(event) {
|
||
if (event.target === modal) {
|
||
closeInternalAssetsModal();
|
||
}
|
||
});
|
||
}
|
||
|
||
// 关闭内网资产模态框
|
||
function closeInternalAssetsModal() {
|
||
const modal = document.getElementById('internalAssetsModal');
|
||
if (modal) {
|
||
modal.remove();
|
||
}
|
||
}
|
||
|
||
// 显示账号密码信息
|
||
function showCredentials(assetId) {
|
||
const credentialsSection = document.getElementById(`credentials-${assetId}`);
|
||
const toggleBtn = document.getElementById(`toggle-${assetId}`);
|
||
|
||
if (!credentialsSection || !toggleBtn) {
|
||
console.error('找不到元素:', `credentials-${assetId}`, `toggle-${assetId}`);
|
||
return;
|
||
}
|
||
|
||
if (credentialsSection.style.display === 'none' || !credentialsSection.style.display) {
|
||
// 显示账号密码
|
||
credentialsSection.style.display = 'block';
|
||
toggleBtn.innerHTML = '<i class="fas fa-eye-slash"></i> 隐藏账号';
|
||
} else {
|
||
// 隐藏账号密码
|
||
credentialsSection.style.display = 'none';
|
||
toggleBtn.innerHTML = '<i class="fas fa-eye"></i> 查看账号';
|
||
}
|
||
}
|
||
|
||
// 复制到剪贴板
|
||
function copyToClipboard(text, assetId = null) {
|
||
// 直接复制传入的文本,不再根据assetId复制整个账号密码字符串
|
||
// 创建临时文本区域
|
||
const tempTextArea = document.createElement('textarea');
|
||
tempTextArea.value = text;
|
||
document.body.appendChild(tempTextArea);
|
||
tempTextArea.select();
|
||
|
||
try {
|
||
document.execCommand('copy');
|
||
// 根据文本类型显示不同的提示信息
|
||
if (text === 'jeremypi' || text === 'quant_speed') {
|
||
showToast('账号已复制到剪贴板');
|
||
} else if (text === '123tangledup-ai' || text === '123quantspeed' || text === 'quant-speed') {
|
||
showToast('密码已复制到剪贴板');
|
||
} else {
|
||
showToast('内容已复制到剪贴板');
|
||
}
|
||
} catch (err) {
|
||
showToast('复制失败,请手动复制');
|
||
}
|
||
|
||
// 移除临时文本区域
|
||
document.body.removeChild(tempTextArea);
|
||
}
|
||
|
||
// 显示提示信息
|
||
function showToast(message) {
|
||
const toast = document.createElement('div');
|
||
toast.className = 'toast-notification';
|
||
toast.textContent = message;
|
||
document.body.appendChild(toast);
|
||
|
||
// 显示提示
|
||
setTimeout(() => {
|
||
toast.classList.add('show');
|
||
}, 10);
|
||
|
||
// 3秒后隐藏
|
||
setTimeout(() => {
|
||
toast.classList.remove('show');
|
||
setTimeout(() => {
|
||
document.body.removeChild(toast);
|
||
}, 300);
|
||
}, 3000);
|
||
}
|
||
|
||
// 页面加载时初始化
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
// 恢复快速上传设置
|
||
const quickUpload = localStorage.getItem('quickUpload');
|
||
if (quickUpload !== null) {
|
||
const isQuickUpload = quickUpload === 'true';
|
||
document.getElementById('quickUploadToggle').checked = isQuickUpload;
|
||
document.querySelector('.upload-section').classList.toggle('quick-upload-mode', isQuickUpload);
|
||
}
|
||
|
||
// 恢复跳过验证设置
|
||
const skipValidation = localStorage.getItem('skipValidation');
|
||
if (skipValidation !== null) {
|
||
const isSkipValidation = skipValidation === 'true';
|
||
document.getElementById('skipValidationToggle').checked = isSkipValidation;
|
||
}
|
||
|
||
// 恢复布局设置
|
||
const compactLayout = localStorage.getItem('compactLayout');
|
||
if (compactLayout !== null) {
|
||
isCompactLayout = compactLayout === 'true';
|
||
const btn = document.getElementById('layoutToggleBtn');
|
||
const fileList = document.getElementById('fileList');
|
||
|
||
if (isCompactLayout) {
|
||
fileList.classList.add('compact');
|
||
btn.classList.add('active');
|
||
btn.innerHTML = '<i class="fas fa-th-list"></i>';
|
||
btn.title = '切换到卡片模式';
|
||
} else {
|
||
fileList.classList.remove('compact');
|
||
btn.classList.remove('active');
|
||
btn.innerHTML = '<i class="fas fa-th-large"></i>';
|
||
btn.title = '切换到紧凑模式';
|
||
}
|
||
}
|
||
|
||
// 初始化返回按钮状态
|
||
updateBackButton();
|
||
|
||
// 先加载用户信息,然后加载文件列表
|
||
loadUserInfo().then(() => {
|
||
loadFiles();
|
||
});
|
||
|
||
// 添加长按提示支持
|
||
const quickUploadSwitch = document.querySelector('.quick-upload-switch');
|
||
if (quickUploadSwitch) {
|
||
let pressTimer;
|
||
|
||
quickUploadSwitch.addEventListener('touchstart', () => {
|
||
pressTimer = setTimeout(() => {
|
||
const hint = quickUploadSwitch.querySelector('.quick-upload-hint');
|
||
if (hint) hint.style.display = 'block';
|
||
}, 500);
|
||
});
|
||
|
||
quickUploadSwitch.addEventListener('touchend', () => {
|
||
clearTimeout(pressTimer);
|
||
setTimeout(() => {
|
||
const hint = quickUploadSwitch.querySelector('.quick-upload-hint');
|
||
if (hint) hint.style.display = 'none';
|
||
}, 1000);
|
||
});
|
||
}
|
||
|
||
// 添加搜索框实时搜索
|
||
const searchInput = document.getElementById('searchInput');
|
||
if (searchInput) {
|
||
searchInput.addEventListener('input', function(e) {
|
||
clearTimeout(searchInput.searchTimeout);
|
||
searchInput.searchTimeout = setTimeout(() => {
|
||
currentSearch = e.target.value;
|
||
loadFiles();
|
||
}, 500); // 延迟500ms执行搜索
|
||
});
|
||
}
|
||
|
||
// 添加键盘快捷键
|
||
document.addEventListener('keydown', function(e) {
|
||
// Ctrl+N 或 Cmd+N 创建新文件夹
|
||
if ((e.ctrlKey || e.metaKey) && e.key === 'n') {
|
||
e.preventDefault();
|
||
showCreateFolderDialog();
|
||
}
|
||
|
||
// Escape 关闭模态框
|
||
if (e.key === 'Escape') {
|
||
hideCreateFolderDialog();
|
||
hideMoveFileDialog();
|
||
hideRenameFileDialog();
|
||
hideRenameFolderDialog();
|
||
hideBatchRenameDialog();
|
||
closeInternalAssetsModal();
|
||
}
|
||
|
||
// Enter 在各种对话框中执行相应操作
|
||
if (e.key === 'Enter') {
|
||
if (document.getElementById('createFolderModal').style.display === 'flex') {
|
||
createFolder();
|
||
} else if (document.getElementById('renameFileModal').style.display === 'flex') {
|
||
confirmRenameFile();
|
||
} else if (document.getElementById('renameFolderModal').style.display === 'flex') {
|
||
confirmRenameFolder();
|
||
} else if (document.getElementById('batchRenameModal').style.display === 'flex') {
|
||
applyBatchRename();
|
||
}
|
||
}
|
||
});
|
||
});
|
||
|
||
// 添加上传进度提示
|
||
function showUploadProgress(show) {
|
||
const progressBar = document.querySelector('.upload-progress');
|
||
if (!progressBar) return;
|
||
|
||
if (show) {
|
||
progressBar.style.display = 'block';
|
||
progressBar.style.width = '100%';
|
||
progressBar.style.transition = 'width 0.3s ease';
|
||
} else {
|
||
progressBar.style.display = 'none';
|
||
progressBar.style.width = '0';
|
||
}
|
||
}
|
||
|
||
// 添加样式以优化取消动画
|
||
const style = document.createElement('style');
|
||
style.textContent += `
|
||
.upload-cancelled {
|
||
opacity: 0.7;
|
||
transform: translateX(100%);
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.file-progress {
|
||
position: relative;
|
||
transition: all 0.3s ease;
|
||
overflow: hidden;
|
||
}
|
||
`;
|
||
document.head.appendChild(style);
|
||
</script>
|
||
|
||
<!-- Admin登录弹窗 -->
|
||
<div id="adminLoginModal" class="admin-login-modal">
|
||
<div class="admin-login-content">
|
||
<button class="admin-close-btn" onclick="closeAdminLogin()">×</button>
|
||
<div class="admin-login-header">
|
||
<h3>管理员登录</h3>
|
||
<p>点击头像登录管理员账号</p>
|
||
</div>
|
||
<div id="adminLoginMessage"></div>
|
||
<form class="admin-login-form" onsubmit="submitAdminLogin(event)">
|
||
<div class="admin-form-group">
|
||
<label for="adminUsername">用户名</label>
|
||
<input type="text" id="adminUsername" name="username" required autocomplete="username">
|
||
</div>
|
||
<div class="admin-form-group">
|
||
<label for="adminPassword">密码</label>
|
||
<input type="password" id="adminPassword" name="password" required autocomplete="current-password">
|
||
</div>
|
||
<div class="admin-login-buttons">
|
||
<button type="button" class="admin-btn admin-btn-secondary" onclick="closeAdminLogin()">取消</button>
|
||
<button type="submit" class="admin-btn admin-btn-primary">登录</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</body>
|
||
</html> |