Files
host_message/templates/index.html
2026-02-13 12:33:43 +08:00

6359 lines
222 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh">
<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>