<!DOCTYPE html>
|
<html lang="zh-CN">
|
<head>
|
<meta charset="UTF-8">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<title>📄 文档管理系统</title>
|
<link href="/static/css/all.min.css" rel="stylesheet">
|
<style>
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
body {
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
background: #f0f2f5;
|
color: #1a1a2e;
|
min-height: 100vh;
|
}
|
.header {
|
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
color: white;
|
padding: 24px 32px;
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
flex-wrap: wrap;
|
gap: 12px;
|
}
|
.header h1 { font-size: 22px; display: flex; align-items: center; gap: 10px; }
|
.header .badge {
|
font-size: 13px;
|
background: rgba(255,255,255,0.15);
|
padding: 4px 12px;
|
border-radius: 20px;
|
color: #a8b2d1;
|
}
|
.container { max-width: 1100px; margin: 0 auto; padding: 24px; }
|
|
/* 上传区域 */
|
.upload-card {
|
background: white;
|
border-radius: 16px;
|
padding: 40px;
|
margin-bottom: 24px;
|
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
|
text-align: center;
|
border: 2px dashed #d0d5dd;
|
transition: all 0.3s;
|
cursor: pointer;
|
}
|
.upload-card:hover, .upload-card.dragover {
|
border-color: #2563eb;
|
background: #f8faff;
|
}
|
.upload-card .icon { font-size: 48px; color: #2563eb; margin-bottom: 12px; }
|
.upload-card h3 { font-size: 18px; margin-bottom: 8px; color: #333; }
|
.upload-card p { color: #666; font-size: 14px; }
|
.upload-card input[type="file"] { display: none; }
|
|
/* 进度条 */
|
.progress-bar {
|
display: none;
|
margin-top: 16px;
|
width: 100%;
|
max-width: 400px;
|
margin-left: auto;
|
margin-right: auto;
|
}
|
.progress-bar .bar {
|
height: 6px;
|
background: #e5e7eb;
|
border-radius: 3px;
|
overflow: hidden;
|
}
|
.progress-bar .bar .fill {
|
height: 100%;
|
background: linear-gradient(90deg, #2563eb, #7c3aed);
|
width: 0%;
|
transition: width 0.3s;
|
border-radius: 3px;
|
}
|
.progress-bar .text { font-size: 12px; color: #666; margin-top: 6px; }
|
|
/* 文件列表 */
|
.file-card {
|
background: white;
|
border-radius: 16px;
|
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
|
overflow: hidden;
|
}
|
.file-card .list-header {
|
padding: 20px 24px;
|
border-bottom: 1px solid #eef0f4;
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
flex-wrap: wrap;
|
gap: 8px;
|
}
|
.file-card .list-header h2 { font-size: 16px; display: flex; align-items: center; gap: 8px; }
|
.file-card .list-header .count { color: #666; font-size: 14px; }
|
|
.file-item {
|
display: flex;
|
align-items: center;
|
padding: 16px 24px;
|
border-bottom: 1px solid #f3f4f6;
|
transition: background 0.2s;
|
gap: 12px;
|
flex-wrap: wrap;
|
}
|
.file-item:hover { background: #f9fafb; }
|
.file-item:last-child { border-bottom: none; }
|
|
.file-item .file-icon {
|
width: 42px; height: 42px;
|
border-radius: 10px;
|
display: flex; align-items: center; justify-content: center;
|
font-size: 18px;
|
flex-shrink: 0;
|
}
|
.file-icon.archive { background: #fef3c7; color: #d97706; }
|
.file-icon.image { background: #dbeafe; color: #2563eb; }
|
.file-icon.pdf { background: #fee2e2; color: #dc2626; }
|
.file-icon.doc { background: #d1fae5; color: #059669; }
|
.file-icon.other { background: #f3f4f6; color: #6b7280; }
|
|
.file-item .file-info { flex: 1; min-width: 0; }
|
.file-item .file-info .name {
|
font-weight: 500; font-size: 14px;
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
}
|
.file-item .file-info .meta {
|
font-size: 12px; color: #9ca3af; margin-top: 2px;
|
}
|
|
.file-item .actions {
|
display: flex; gap: 6px; flex-wrap: wrap;
|
}
|
.btn {
|
padding: 6px 14px;
|
border-radius: 8px;
|
font-size: 13px;
|
border: none;
|
cursor: pointer;
|
text-decoration: none;
|
display: inline-flex;
|
align-items: center;
|
gap: 5px;
|
transition: all 0.2s;
|
white-space: nowrap;
|
}
|
.btn-primary { background: #2563eb; color: white; }
|
.btn-primary:hover { background: #1d4ed8; }
|
.btn-success { background: #059669; color: white; }
|
.btn-success:hover { background: #047857; }
|
.btn-outline {
|
background: transparent;
|
border: 1px solid #d0d5dd;
|
color: #374151;
|
}
|
.btn-outline:hover { background: #f3f4f6; }
|
.btn-danger { background: #dc2626; color: white; }
|
.btn-danger:hover { background: #b91c1c; }
|
|
/* 弹窗 */
|
.modal-overlay {
|
display: none; position: fixed; inset: 0;
|
background: rgba(0,0,0,0.5);
|
z-index: 1000;
|
align-items: center; justify-content: center;
|
}
|
.modal-overlay.active { display: flex; }
|
.modal {
|
background: white; border-radius: 16px;
|
padding: 32px; max-width: 480px; width: 90%;
|
box-shadow: 0 20px 60px rgba(0,0,0,0.2);
|
}
|
.modal h3 { font-size: 18px; margin-bottom: 20px; display: flex; align-items: center; gap: 8px; }
|
.modal .link-box {
|
background: #f8faff;
|
border: 1px solid #e5e7eb;
|
border-radius: 10px;
|
padding: 12px 16px;
|
margin-bottom: 12px;
|
}
|
.modal .link-box label { font-size: 12px; color: #666; display: block; margin-bottom: 4px; }
|
.modal .link-box .link-value {
|
display: flex; align-items: center; gap: 8px;
|
}
|
.modal .link-box .link-value input {
|
flex: 1; border: none; background: transparent;
|
font-size: 14px; color: #2563eb; font-family: monospace;
|
outline: none;
|
}
|
.modal .link-box .link-value .copy-btn {
|
background: none; border: none; color: #2563eb; cursor: pointer; font-size: 16px;
|
}
|
.modal .qr-container {
|
text-align: center;
|
margin: 16px 0;
|
}
|
.modal .qr-container img {
|
width: 180px; height: 180px;
|
border-radius: 8px;
|
border: 1px solid #eef0f4;
|
}
|
.modal .modal-actions {
|
display: flex; gap: 8px; justify-content: center;
|
margin-top: 16px;
|
}
|
.toast {
|
position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%);
|
background: #1a1a2e; color: white;
|
padding: 12px 24px; border-radius: 10px;
|
font-size: 14px;
|
z-index: 2000;
|
opacity: 0; transition: opacity 0.3s;
|
pointer-events: none;
|
}
|
.toast.show { opacity: 1; }
|
|
.empty-state {
|
text-align: center; padding: 48px 24px; color: #9ca3af;
|
}
|
.empty-state i { font-size: 48px; margin-bottom: 12px; }
|
|
@media (max-width: 640px) {
|
.file-item { padding: 12px 16px; }
|
.btn { font-size: 12px; padding: 5px 10px; }
|
}
|
</style>
|
</head>
|
<body>
|
|
<div class="header">
|
<h1><i class="fa-regular fa-folder-open"></i> 文档管理系统</h1>
|
<div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap;">
|
<span class="badge"><i class="fa-regular fa-user"></i> <%= user.username %></span>
|
<% if (user.username === 'gwalrusadmin') { %>
|
<a href="/users" class="btn btn-outline" style="color:white;border-color:rgba(255,255,255,0.3);font-size:13px;">
|
<i class="fa-regular fa-users"></i> 账号管理
|
</a>
|
<% } %>
|
<span class="badge"><i class="fa-regular fa-server"></i> <span id="baseUrlDisplay"><%= baseUrl %></span></span>
|
<a href="/settings" class="btn btn-outline" style="color:white;border-color:rgba(255,255,255,0.3);font-size:13px;">
|
<i class="fa-solid fa-key"></i> 修改密码
|
</a>
|
<a href="/logout" class="btn btn-outline" style="color:white;border-color:rgba(255,255,255,0.3);font-size:13px;">
|
<i class="fa-solid fa-arrow-right-from-bracket"></i> 退出
|
</a>
|
</div>
|
</div>
|
|
<div class="container">
|
<!-- 上传区域 -->
|
<div class="upload-card" id="uploadZone">
|
<div class="icon"><i class="fa-solid fa-cloud-arrow-up"></i></div>
|
<h3>点击或拖拽文件到此处上传</h3>
|
<p>支持任意格式,单文件最大 500MB</p>
|
<input type="file" id="fileInput">
|
<div class="progress-bar" id="progressBar">
|
<div class="bar"><div class="fill" id="progressFill"></div></div>
|
<div class="text" id="progressText">上传中...</div>
|
</div>
|
</div>
|
|
<!-- 文件列表 -->
|
<div class="file-card">
|
<div class="list-header">
|
<h2><i class="fa-regular fa-list"></i> 文件列表</h2>
|
<span class="count">共 <strong id="fileCount"><%= files.length %></strong> 个文件</span>
|
</div>
|
<div id="fileList">
|
<% if (files.length === 0) { %>
|
<div class="empty-state">
|
<i class="fa-regular fa-folder-open"></i>
|
<p>暂无文件,上传你的第一个文档吧</p>
|
</div>
|
<% } else { %>
|
<% files.forEach(function(f) { %>
|
<div class="file-item" data-code="<%= f.code %>">
|
<div class="file-icon <%= getFileIconClass(f.originalName) %>">
|
<i class="<%= getFileIcon(f.originalName) %>"></i>
|
</div>
|
<div class="file-info">
|
<div class="name"><%= f.originalName %></div>
|
<div class="meta">
|
<%= formatSize(f.size) %> · <%= formatDate(f.uploadedAt) %>
|
· <span style="color:#2563eb;"><%= f.downloads || 0 %> 次下载</span>
|
</div>
|
</div>
|
<div class="actions">
|
<button class="btn btn-outline" onclick="showQR('<%= f.code %>')">
|
<i class="fa-solid fa-qrcode"></i> 二维码
|
</button>
|
<a href="/d/<%= f.code %>" class="btn btn-primary" target="_blank">
|
<i class="fa-solid fa-download"></i> 下载
|
</a>
|
<button class="btn btn-danger" onclick="deleteFile('<%= f.code %>')">
|
<i class="fa-regular fa-trash-can"></i> 删除
|
</button>
|
</div>
|
</div>
|
<% }); %>
|
<% } %>
|
</div>
|
</div>
|
</div>
|
|
<!-- 二维码弹窗 -->
|
<div class="modal-overlay" id="qrModal">
|
<div class="modal">
|
<h3><i class="fa-solid fa-qrcode"></i> 文件分享</h3>
|
<div class="link-box">
|
<label>下载链接</label>
|
<div class="link-value">
|
<input type="text" id="downloadLinkInput" readonly>
|
<button class="copy-btn" onclick="copyLink()"><i class="fa-regular fa-copy"></i></button>
|
</div>
|
</div>
|
<div class="link-box">
|
<label>短链(扫码下载)</label>
|
<div class="link-value">
|
<input type="text" id="shortLinkInput" readonly>
|
<button class="copy-btn" onclick="copyShortLink()"><i class="fa-regular fa-copy"></i></button>
|
</div>
|
</div>
|
<div class="qr-container">
|
<img id="qrImage" src="" alt="二维码">
|
<p style="font-size:13px;color:#9ca3af;margin-top:8px;">扫码即可下载文件</p>
|
</div>
|
<div class="modal-actions">
|
<button class="btn btn-success" onclick="downloadQR()">
|
<i class="fa-regular fa-image"></i> 保存二维码
|
</button>
|
<button class="btn btn-outline" onclick="closeModal()">关闭</button>
|
</div>
|
</div>
|
</div>
|
|
<div class="toast" id="toast"></div>
|
|
<script>
|
const uploadZone = document.getElementById('uploadZone');
|
const fileInput = document.getElementById('fileInput');
|
const progressBar = document.getElementById('progressBar');
|
const progressFill = document.getElementById('progressFill');
|
const progressText = document.getElementById('progressText');
|
let currentQrCode = null;
|
|
// 上传点击
|
uploadZone.addEventListener('click', () => fileInput.click());
|
|
// 拖拽上传
|
uploadZone.addEventListener('dragover', (e) => {
|
e.preventDefault();
|
uploadZone.classList.add('dragover');
|
});
|
uploadZone.addEventListener('dragleave', () => uploadZone.classList.remove('dragover'));
|
uploadZone.addEventListener('drop', (e) => {
|
e.preventDefault();
|
uploadZone.classList.remove('dragover');
|
if (e.dataTransfer.files.length) uploadFile(e.dataTransfer.files[0]);
|
});
|
fileInput.addEventListener('change', () => {
|
if (fileInput.files.length) uploadFile(fileInput.files[0]);
|
});
|
|
function uploadFile(file) {
|
const formData = new FormData();
|
formData.append('file', file);
|
|
progressBar.style.display = 'block';
|
progressFill.style.width = '0%';
|
progressText.textContent = `正在上传 ${file.name}...`;
|
|
const xhr = new XMLHttpRequest();
|
xhr.open('POST', '/api/upload', true);
|
xhr.upload.onprogress = (e) => {
|
if (e.lengthComputable) {
|
const pct = Math.round((e.loaded / e.total) * 100);
|
progressFill.style.width = pct + '%';
|
progressText.textContent = `上传中 ${pct}% - ${file.name}`;
|
}
|
};
|
xhr.onload = () => {
|
if (xhr.status === 200) {
|
const result = JSON.parse(xhr.responseText);
|
showToast(`✅ ${file.name} 上传成功!`);
|
setTimeout(() => location.reload(), 500);
|
} else {
|
progressText.textContent = '❌ 上传失败,请重试';
|
setTimeout(() => { progressBar.style.display = 'none'; }, 2000);
|
}
|
};
|
xhr.onerror = () => {
|
progressText.textContent = '❌ 网络错误';
|
};
|
xhr.send(formData);
|
}
|
|
function showQR(code) {
|
currentQrCode = code;
|
document.getElementById('downloadLinkInput').value = `${document.getElementById('baseUrlDisplay').textContent}/d/${code}`;
|
document.getElementById('shortLinkInput').value = `${document.getElementById('baseUrlDisplay').textContent}/d/${code}`;
|
document.getElementById('qrImage').src = `/qr/${code}?t=${Date.now()}`;
|
document.getElementById('qrModal').classList.add('active');
|
}
|
|
function closeModal() {
|
document.getElementById('qrModal').classList.remove('active');
|
}
|
|
function copyLink() {
|
const input = document.getElementById('downloadLinkInput');
|
input.select();
|
navigator.clipboard.writeText(input.value).then(() => showToast('✅ 链接已复制'));
|
}
|
|
function copyShortLink() {
|
const input = document.getElementById('shortLinkInput');
|
input.select();
|
navigator.clipboard.writeText(input.value).then(() => showToast('✅ 短链接已复制'));
|
}
|
|
function downloadQR() {
|
const link = document.createElement('a');
|
link.download = `qrcode-${currentQrCode}.png`;
|
link.href = document.getElementById('qrImage').src;
|
link.click();
|
showToast('✅ 二维码已保存');
|
}
|
|
function deleteFile(code) {
|
if (!confirm('确定要删除这个文件吗?')) return;
|
fetch(`/api/delete/${code}`, { method: 'POST' })
|
.then(r => r.json())
|
.then(d => {
|
if (d.success) {
|
showToast('🗑️ 文件已删除');
|
location.reload();
|
}
|
});
|
}
|
|
function showToast(msg) {
|
const t = document.getElementById('toast');
|
t.textContent = msg;
|
t.classList.add('show');
|
setTimeout(() => t.classList.remove('show'), 2500);
|
}
|
|
// 点击外部关闭弹窗
|
document.getElementById('qrModal').addEventListener('click', (e) => {
|
if (e.target === document.getElementById('qrModal')) closeModal();
|
});
|
</script>
|
|
</body>
|
</html>
|