You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

650 lines
23 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Sese Upload</title>
<style>
:root {
--bg-primary: #0f0f14;
--bg-secondary: #18181f;
--bg-card: #1e1e28;
--bg-hover: #252532;
--border: #2a2a3a;
--text-primary: #e8e8f0;
--text-secondary: #8888a0;
--accent: #7c6af5;
--accent-glow: rgba(124, 106, 245, 0.3);
--danger: #f0566a;
--danger-glow: rgba(240, 86, 106, 0.2);
--success: #3dd68c;
--radius: 14px;
--radius-sm: 8px;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
min-height: 100vh;
padding: 16px;
padding-bottom: 100px;
}
/* Header */
.header {
text-align: center;
padding: 20px 0 30px;
}
.header h1 {
font-size: 1.6rem;
font-weight: 600;
background: linear-gradient(135deg, #fff 0%, #a8a4f0 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
letter-spacing: -0.5px;
}
.header p {
color: var(--text-secondary);
font-size: 0.85rem;
margin-top: 6px;
}
/* Path Selector */
.path-selector {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 14px 16px;
margin-bottom: 20px;
}
.path-selector label {
display: block;
font-size: 0.75rem;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 8px;
}
.path-selector select {
width: 100%;
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
color: var(--text-primary);
font-size: 0.95rem;
padding: 10px 12px;
outline: none;
appearance: none;
cursor: pointer;
}
.path-selector select:focus {
border-color: var(--accent);
}
/* Upload Zone */
.upload-zone {
position: relative;
background: var(--bg-card);
border: 2px dashed var(--border);
border-radius: var(--radius);
padding: 50px 20px;
text-align: center;
margin-bottom: 20px;
transition: all 0.2s;
cursor: pointer;
overflow: hidden;
z-index: 1;
}
.upload-zone.dragover {
border-color: var(--accent);
background: rgba(124, 106, 245, 0.05);
}
.upload-zone input[type="file"] {
display: none;
}
.upload-icon {
font-size: 2.5rem;
margin-bottom: 12px;
opacity: 0.6;
}
.upload-zone p {
color: var(--text-secondary);
font-size: 0.9rem;
}
.upload-zone .hint {
font-size: 0.75rem;
margin-top: 6px;
opacity: 0.5;
}
/* Upload Button */
.upload-btn {
width: 100%;
background: linear-gradient(135deg, var(--accent) 0%, #9d8fff 100%);
color: #fff;
border: none;
border-radius: var(--radius);
padding: 14px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
margin-bottom: 16px;
transition: opacity 0.2s, transform 0.1s;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.upload-btn:active { transform: scale(0.98); }
.upload-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.progress-bar {
width: 100%;
height: 4px;
background: var(--bg-secondary);
border-radius: 2px;
margin-top: 10px;
display: none;
overflow: hidden;
}
.progress-bar.active { display: block; }
.progress-bar-fill {
height: 100%;
background: var(--accent);
width: 0%;
transition: width 0.3s ease;
}
/* Message Box */
.message {
padding: 12px 16px;
border-radius: var(--radius-sm);
font-size: 0.88rem;
margin-bottom: 20px;
display: none;
}
.message.success {
background: rgba(61, 214, 140, 0.1);
border: 1px solid rgba(61, 214, 140, 0.3);
color: var(--success);
}
.message.error {
background: var(--danger-glow);
border: 1px solid rgba(240, 86, 106, 0.3);
color: var(--danger);
}
.message.show { display: block; }
/* Files Section */
.files-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 14px;
}
.files-header h2 {
font-size: 1rem;
font-weight: 600;
color: var(--text-secondary);
}
.files-count {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 20px;
padding: 4px 12px;
font-size: 0.75rem;
color: var(--text-secondary);
}
.file-list {
list-style: none;
}
.file-item {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius);
margin-bottom: 10px;
overflow: hidden;
transition: border-color 0.2s;
}
.file-item:hover { border-color: var(--accent); }
.file-row {
display: flex;
align-items: center;
padding: 14px 16px;
cursor: pointer;
}
.file-thumb {
width: 44px;
height: 44px;
border-radius: 8px;
background: var(--bg-secondary);
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2rem;
margin-right: 14px;
flex-shrink: 0;
overflow: hidden;
}
.file-thumb img {
width: 100%;
height: 100%;
object-fit: cover;
}
.file-info {
flex: 1;
min-width: 0;
}
.file-name {
font-size: 0.9rem;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: var(--text-primary);
}
.file-meta {
font-size: 0.75rem;
color: var(--text-secondary);
margin-top: 3px;
}
.expand-icon {
color: var(--text-secondary);
font-size: 1.2rem;
padding: 0 4px;
transition: transform 0.2s, color 0.2s;
display: inline-block;
}
.file-actions {
display: flex;
gap: 8px;
padding: 10px 16px;
background: var(--bg-secondary);
border-top: 1px solid var(--border);
}
.action-btn {
flex: 1;
padding: 8px;
border: none;
border-radius: var(--radius-sm);
font-size: 0.82rem;
cursor: pointer;
transition: opacity 0.2s;
text-align: center;
}
.action-btn:active { opacity: 0.7; }
.action-btn.preview {
background: var(--accent);
color: #fff;
}
.action-btn.delete {
background: var(--danger-glow);
color: var(--danger);
border: 1px solid rgba(240, 86, 106, 0.3);
}
.toggle-btn {
width: 100%;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius);
color: var(--text-secondary);
padding: 12px;
font-size: 0.88rem;
cursor: pointer;
margin-top: 14px;
transition: background 0.2s;
}
.toggle-btn:hover { background: var(--bg-hover); }
.empty-state {
text-align: center;
padding: 60px 20px;
color: var(--text-secondary);
}
.empty-state .icon { font-size: 3rem; opacity: 0.3; margin-bottom: 16px; }
.empty-state p { font-size: 0.9rem; }
/* Preview Overlay */
#previewOverlay {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0, 0, 0, 0.92);
display: none;
justify-content: center;
align-items: center;
z-index: 1000;
padding: 20px;
}
#previewOverlay.show { display: flex; }
.preview-box {
background: var(--bg-card);
border-radius: var(--radius);
max-width: 100%;
max-height: 100%;
overflow: hidden;
position: relative;
}
.preview-close {
position: absolute;
top: 12px; right: 12px;
width: 36px; height: 36px;
border-radius: 50%;
background: rgba(255,255,255,0.1);
border: none;
color: #fff;
font-size: 1.2rem;
cursor: pointer;
z-index: 10;
display: flex;
align-items: center;
justify-content: center;
}
.preview-box img, .preview-box video {
max-width: 90vw;
max-height: 85vh;
display: block;
}
.preview-box video { width: 100%; }
/* Loading spinner */
.spinner {
display: inline-block;
width: 16px; height: 16px;
border: 2px solid rgba(255,255,255,0.3);
border-top-color: #fff;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* Responsive */
@media (max-width: 480px) {
body { padding: 12px; }
.header h1 { font-size: 1.4rem; }
.upload-zone { padding: 24px 16px; }
}
</style>
<script>
let allFiles = [];
let showAll = false;
const currentPath = '/mnt/sssss';
function getFileIcon(filename) {
const ext = filename.split('.').pop().toLowerCase();
const imgExts = ['jpg','jpeg','png','gif','webp','bmp','heic'];
const vidExts = ['mp4','mov','avi','wmv','m4v','mpg','flv','mkv','3gp','webm'];
if (imgExts.includes(ext)) return '🖼';
if (vidExts.includes(ext)) return '🎬';
if (ext === 'pdf') return '📄';
if (['zip','rar','7z','tar','gz'].includes(ext)) return '📦';
return '📁';
}
function formatFileSize(filename) {
// This is approximate since we don't have real size data
return '';
}
async function uploadFile(event) {
event.preventDefault();
const fileInput = document.getElementById('fileInput');
const files = fileInput.files;
if (files.length === 0) {
showMessage('请至少选择一个文件', 'error');
return;
}
const formData = new FormData();
for (let i = 0; i < files.length; i++) formData.append('files', files[i]);
formData.append('uploadPath', document.getElementById('uploadPath').value);
const btn = document.getElementById('uploadBtn');
const progressBar = document.getElementById('progressBar');
btn.disabled = true;
btn.innerHTML = '<span class="spinner"></span> 上传中...';
progressBar.classList.add('active');
try {
console.log('Starting upload, files:', files.length);
const response = await fetch('/upload', { method: 'POST', body: formData });
console.log('Response status:', response.status, 'ok:', response.ok);
const data = await response.json();
console.log('Upload success:', data);
if (data.status === 'success') {
allFiles = data.files;
renderFiles();
fileInput.value = '';
showMessage(`上传成功:${data.message}`, 'success');
} else {
showMessage(`上传失败:${data.message}`, 'error');
}
} catch (e) {
console.error('Upload error:', e);
showMessage('网络错误,上传失败', 'error');
} finally {
btn.disabled = false;
btn.innerHTML = '🚀 开始上传';
progressBar.classList.remove('active');
}
}
function showMessage(text, type) {
const el = document.getElementById('messageBox');
el.className = `message ${type} show`;
el.textContent = text;
setTimeout(() => { el.className = 'message'; }, 4000);
}
async function fetchFiles() {
const uploadPath = document.getElementById('uploadPath').value || currentPath;
try {
const resp = await fetch(`/files?path=${encodeURIComponent(uploadPath)}`);
const data = await resp.json();
if (data.files) {
allFiles = data.files;
renderFiles();
}
} catch (e) { console.error(e); }
}
function renderFiles() {
const list = document.getElementById('fileList');
const countEl = document.getElementById('filesCount');
const toggleBtn = document.getElementById('toggleBtn');
list.innerHTML = '';
countEl.textContent = allFiles.length + ' 个文件';
if (allFiles.length === 0) {
list.innerHTML = '<div class="empty-state"><div class="icon">📂</div><p>暂无文件</p></div>';
toggleBtn.style.display = 'none';
return;
}
const filesToShow = showAll ? allFiles : allFiles.slice(0, 20);
filesToShow.forEach((file, idx) => {
const item = document.createElement('div');
item.className = 'file-item';
item.innerHTML = `
<div class="file-row" onclick="toggleFileActions(${idx})">
<div class="file-thumb">${getFileIcon(file)}</div>
<div class="file-info">
<div class="file-name" title="${file}">${file}</div>
<div class="file-meta">点击展开</div>
</div>
<div style="display:flex;align-items:center;gap:6px">
<button class="action-btn delete" style="padding:6px 10px;font-size:0.8rem;background:var(--danger-glow);color:var(--danger);border:1px solid rgba(240,86,106,0.3);border-radius:6px" onclick="event.stopPropagation(); confirmDelete('${encodeURIComponent(file)}')">🗑</button>
<span class="expand-icon" id="expandIcon${idx}" style="color:var(--text-secondary);font-size:1.2rem;padding:0 4px"></span>
</div>
</div>
<div class="file-actions" id="actions${idx}" style="display:none">
<button class="action-btn preview" onclick="event.stopPropagation(); previewFile('${encodeURIComponent(file)}')">👁 预览</button>
</div>
`;
list.appendChild(item);
});
toggleBtn.style.display = allFiles.length > 20 ? 'block' : 'none';
toggleBtn.textContent = showAll ? '🔼 收起列表' : '🔽 展开全部';
}
function toggleFileActions(idx) {
const actions = document.getElementById('actions' + idx);
const expandIcon = document.getElementById('expandIcon' + idx);
const currentlyVisible = actions.style.display !== 'none';
document.querySelectorAll('.file-actions').forEach(el => { el.style.display = 'none'; });
document.querySelectorAll('.expand-icon').forEach(el => { el.textContent = ''; el.style.color = 'var(--text-secondary)'; });
if (!currentlyVisible) {
actions.style.display = 'flex';
if (expandIcon) { expandIcon.textContent = '∧'; expandIcon.style.color = 'var(--accent)'; }
}
}
function toggleShowAll() {
showAll = !showAll;
renderFiles();
}
async function confirmDelete(filename) {
if (!confirm('确定要删除这个文件吗?')) return;
const uploadPath = document.getElementById('uploadPath').value || currentPath;
try {
const resp = await fetch(`/delete?file=${filename}&path=${encodeURIComponent(uploadPath)}`, { method: 'DELETE' });
const data = await resp.json();
if (data.status === 'success') {
allFiles = allFiles.filter(f => decodeURIComponent(filename) !== f);
renderFiles();
showMessage('删除成功', 'success');
} else {
showMessage('删除失败:' + data.message, 'error');
}
} catch (e) { showMessage('删除失败', 'error'); }
}
function previewFile(filename) {
const uploadPath = document.getElementById('uploadPath').value || currentPath;
const overlay = document.getElementById('previewOverlay');
const box = document.getElementById('previewBox');
const ext = decodeURIComponent(filename).split('.').pop().toLowerCase();
const imgExts = ['jpg','jpeg','png','gif','webp','bmp','heic'];
const vidExts = ['mp4','mov','avi','wmv','m4v','mpg','flv','mkv','3gp','webm'];
const src = `/static/${filename}?path=${encodeURIComponent(uploadPath)}`;
box.innerHTML = '<button class="preview-close" onclick="closePreview()">✕</button>';
if (imgExts.includes(ext)) {
const img = document.createElement('img');
img.src = src;
img.onerror = () => { box.innerHTML = '<p style="padding:40px;color:#888">图片加载失败</p>'; };
box.appendChild(img);
} else if (vidExts.includes(ext)) {
const vid = document.createElement('video');
vid.src = src;
vid.controls = true;
vid.autoplay = true;
box.appendChild(vid);
} else {
box.innerHTML += '<p style="padding:40px;color:#888">该文件类型不支持预览</p>';
}
overlay.classList.add('show');
}
function closePreview() {
document.getElementById('previewOverlay').classList.remove('show');
document.getElementById('previewBox').innerHTML = '';
}
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('uploadPath').addEventListener('change', fetchFiles);
const zone = document.getElementById('uploadZone');
const input = document.getElementById('fileInput');
zone.addEventListener('click', e => { e.stopPropagation(); input.click(); });
zone.addEventListener('dragover', e => { e.preventDefault(); zone.classList.add('dragover'); });
zone.addEventListener('dragleave', () => zone.classList.remove('dragover'));
zone.addEventListener('drop', e => {
e.preventDefault();
zone.classList.remove('dragover');
input.files = e.dataTransfer.files;
showFileCount();
});
input.addEventListener('change', showFileCount);
function showFileCount() {
const files = input.files;
const zoneText = document.getElementById('zoneText');
const zoneHint = document.getElementById('zoneHint');
if (files.length > 0) {
zoneText.textContent = `已选 ${files.length} 个文件`;
zoneText.style.color = 'var(--accent)';
zoneHint.textContent = files.length > 3 ? '继续添加或直接点上传' : '点击上传或继续添加';
} else {
zoneText.textContent = '点击或拖拽文件到此处';
zoneText.style.color = '';
zoneHint.textContent = '支持图片、视频文件';
}
}
document.getElementById('previewOverlay').addEventListener('click', e => {
if (e.target.id === 'previewOverlay') closePreview();
});
fetchFiles();
});
</script>
</head>
<body>
<div class="header">
<h1>📁 文件管理器</h1>
<p>私有隐私空间</p>
</div>
<form id="uploadForm" onsubmit="uploadFile(event)">
<div class="path-selector">
<label>存储路径</label>
<select id="uploadPath" name="uploadPath">
<option value="/mnt/sssss">/mnt/sssss</option>
</select>
</div>
<div class="upload-zone" id="uploadZone">
<div class="upload-icon">📤</div>
<p id="zoneText">点击或拖拽文件到此处</p>
<div class="hint" id="zoneHint">支持图片、视频文件</div>
</div>
<input type="file" id="fileInput" name="files" multiple accept="image/*,video/*" style="display:none">
<button type="submit" class="upload-btn" id="uploadBtn">🚀 开始上传</button>
<div class="progress-bar" id="progressBar"><div class="progress-bar-fill"></div></div>
</form>
<div id="messageBox" class="message"></div>
<div class="files-header">
<h2>📂 文件列表</h2>
<span class="files-count" id="filesCount">0 个文件</span>
</div>
<ul class="file-list" id="fileList"></ul>
<button class="toggle-btn" id="toggleBtn" onclick="toggleShowAll()" style="display:none">🔽 展开全部</button>
<div id="previewOverlay">
<div class="preview-box" id="previewBox"></div>
</div>
</body>
</html>