Files
micro_servers/static/public/__view.html
T
2026-07-22 20:18:45 +08:00

332 lines
13 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-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=yes">
<title>文件浏览</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif;
background: #f5f5f5;
color: #333;
padding: 0;
min-height: 100vh;
}
.header {
background: #4A90D9;
color: white;
padding: 12px 16px;
position: sticky;
top: 0;
z-index: 10;
display: flex;
justify-content: space-between;
align-items: center;
}
.header h1 { font-size: 18px; font-weight: 600; }
.header a {
color: white;
text-decoration: none;
padding: 4px 10px;
background: rgba(255,255,255,0.2);
border-radius: 4px;
font-size: 14px;
}
.header a:active { background: rgba(255,255,255,0.4); }
.breadcrumb {
padding: 8px 16px;
background: white;
font-size: 13px;
overflow-x: auto;
white-space: nowrap;
border-bottom: 1px solid #e0e0e0;
}
.breadcrumb a { color: #4A90D9; text-decoration: none; padding: 2px 6px; }
.breadcrumb .sep { color: #999; margin: 0 4px; }
.breadcrumb .current { color: #333; font-weight: 500; }
.search-box {
padding: 8px 16px;
background: white;
border-bottom: 1px solid #e0e0e0;
}
.search-box input {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
outline: none;
}
.search-box input:focus { border-color: #4A90D9; }
.stats {
padding: 6px 16px;
background: #fafafa;
font-size: 12px;
color: #666;
border-bottom: 1px solid #e0e0e0;
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.container {
padding: 8px 12px 60px 12px;
max-width: 800px;
margin: 0 auto;
}
.item {
background: white;
border-radius: 8px;
padding: 10px 14px;
margin-bottom: 4px;
display: flex;
align-items: center;
gap: 12px;
cursor: pointer;
border-bottom: 1px solid #f0f0f0;
}
.item:active { background: #f0f0f0; }
.item .icon { font-size: 24px; flex-shrink: 0; width: 36px; text-align: center; }
.item .info { flex: 1; min-width: 0; }
.item .name { font-size: 15px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.item .meta { font-size: 12px; color: #999; display: flex; gap: 10px; flex-wrap: wrap; }
.item .actions { display: flex; gap: 4px; flex-shrink: 0; }
.item .actions button {
padding: 4px 10px;
font-size: 12px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.item .actions button:active { transform: scale(0.95); }
.btn-play { background: #4CAF50; color: white; }
.btn-download { background: #4A90D9; color: white; }
.loading { text-align: center; padding: 40px; color: #999; }
.loading .spinner { display: inline-block; animation: spin 1s linear infinite; font-size: 28px; }
@keyframes spin { 100% { transform: rotate(360deg); } }
.empty { text-align: center; padding: 60px 20px; color: #999; }
.empty .icon { font-size: 48px; display: block; margin-bottom: 12px; }
.error { text-align: center; padding: 40px 20px; color: #e74c3c; }
@media (max-width: 480px) {
.item { padding: 8px 10px; }
.item .icon { font-size: 20px; width: 30px; }
.item .name { font-size: 13px; }
.item .meta { font-size: 11px; gap: 6px; }
.item .actions button { padding: 3px 8px; font-size: 11px; }
.header h1 { font-size: 16px; }
}
@media (prefers-color-scheme: dark) {
body { background: #1a1a2e; color: #e0e0e0; }
.header { background: #2d2d5e; }
.breadcrumb { background: #16213e; border-color: #2a2a4a; }
.breadcrumb .current { color: #e0e0e0; }
.stats { background: #16213e; color: #aaa; border-color: #2a2a4a; }
.search-box { background: #16213e; border-color: #2a2a4a; }
.search-box input { background: #2a2a4a; border-color: #3a3a5a; color: #e0e0e0; }
.item { background: #16213e; border-color: #2a2a4a; }
.item:active { background: #2a2a4a; }
.item .meta { color: #888; }
}
</style>
</head>
<body>
<div class="header">
<h1>📁 文件浏览</h1>
<div>
<a href="/">🏠</a>
<a href="#" id="btnRefresh" style="margin-left:6px;">🔄</a>
</div>
</div>
<div class="breadcrumb" id="breadcrumb">
<a href="/">根目录</a>
</div>
<div class="search-box">
<input type="text" id="searchInput" placeholder="搜索文件..." autocomplete="off">
</div>
<div class="stats">
<span>📊 共 <span id="totalItems">0</span> 项</span>
<span>📁 <span id="totalDirs">0</span></span>
<span>📄 <span id="totalFiles">0</span></span>
<span>🎬 <span id="totalVideos">0</span></span>
<span>🎵 <span id="totalAudios">0</span></span>
</div>
<div class="container">
<div class="loading" id="loading">
<span class="spinner">⏳</span>
<div>加载中...</div>
</div>
<div id="fileList"></div>
<div class="empty" id="empty" style="display:none;">
<span class="icon">📭</span>
<div>此目录为空</div>
</div>
</div>
<script>
// 从 URL 解析当前路径(去除首尾斜杠),不再依赖后端注入
const currentPath = decodeURIComponent(window.location.pathname).replace(/^\/+|\/+$/g, '');
let items = [];
let searchQuery = "";
const fileList = document.getElementById("fileList");
const loading = document.getElementById("loading");
const empty = document.getElementById("empty");
const searchInput = document.getElementById("searchInput");
const totalItems = document.getElementById("totalItems");
const totalDirs = document.getElementById("totalDirs");
const totalFiles = document.getElementById("totalFiles");
const totalVideos = document.getElementById("totalVideos");
const totalAudios = document.getElementById("totalAudios");
const btnRefresh = document.getElementById("btnRefresh");
const breadcrumb = document.getElementById("breadcrumb");
function formatSize(bytes) {
if (bytes === 0) return "0B";
const k = 1024;
const sizes = ["B", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return (bytes / Math.pow(k, i)).toFixed(1) + sizes[i];
}
function getIcon(item) {
if (item.type === "dir") return "📁";
if (item.type === "video") return "🎬";
if (item.type === "audio") return "🎵";
const ext = item.name.split(".").pop().toLowerCase();
const icons = {
"pdf": "📄", "doc": "📝", "docx": "📝", "xls": "📊", "xlsx": "📊",
"zip": "📦", "rar": "📦", "7z": "📦",
"jpg": "🖼️", "jpeg": "🖼️", "png": "🖼️", "gif": "🖼️",
"txt": "📃", "json": "📋", "html": "🌐", "css": "🎨", "js": "⚡"
};
return icons[ext] || "📄";
}
function navigate(path, type) {
if (type === "dir") window.location.href = "/" + path;
else if (type === "video" || type === "audio") window.location.href = "/video/" + path;
else window.location.href = "/" + path;
}
function playMedia(path) { window.location.href = "/video/" + path; }
function downloadFile(path) { window.location.href = "/" + path + "?download=true"; }
// 面包屑由 JS 根据当前路径动态生成
function renderBreadcrumb() {
let html = '<a href="/">根目录</a>';
if (currentPath) {
const parts = currentPath.split('/');
let acc = '';
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
if (i === parts.length - 1) {
html += `<span class="sep">›</span><span class="current">${part}</span>`;
} else {
acc += (i === 0 ? '' : '/') + part;
html += `<span class="sep">›</span><a href="/${acc}/">${part}</a>`;
}
}
}
breadcrumb.innerHTML = html;
}
function render() {
const filtered = items.filter(item => {
if (!searchQuery) return true;
return item.name.toLowerCase().includes(searchQuery.toLowerCase());
});
totalItems.textContent = filtered.length;
totalDirs.textContent = filtered.filter(i => i.type === "dir").length;
totalFiles.textContent = filtered.filter(i => i.type === "file").length;
totalVideos.textContent = filtered.filter(i => i.type === "video").length;
totalAudios.textContent = filtered.filter(i => i.type === "audio").length;
if (filtered.length === 0) {
fileList.innerHTML = "";
empty.style.display = "block";
loading.style.display = "none";
return;
}
empty.style.display = "none";
loading.style.display = "none";
fileList.innerHTML = filtered.map(item => `
<div class="item">
<span class="icon">${getIcon(item)}</span>
<div class="info" onclick="navigate('${item.path}', '${item.type}')">
<div class="name">${item.name}</div>
<div class="meta">
${item.type !== "dir" ? `<span>${formatSize(item.size)}</span>` : '<span>📁 文件夹</span>'}
<span>${item.modified}</span>
</div>
</div>
<div class="actions">
${(item.type === "video" || item.type === "audio") ?
`<button class="btn-play" onclick="event.stopPropagation(); playMedia('${item.path}')">▶</button>` : ''}
<button class="btn-download" onclick="event.stopPropagation(); downloadFile('${item.path}')">⬇</button>
</div>
</div>
`).join("");
}
async function loadData() {
loading.style.display = "block";
fileList.innerHTML = "";
empty.style.display = "none";
try {
const url = `/api/filelist?path=${encodeURIComponent(currentPath)}`;
const res = await fetch(url);
const data = await res.json();
if (data.success) {
items = data.items || [];
render();
} else {
throw new Error(data.error || "加载失败");
}
} catch (e) {
fileList.innerHTML = `<div class="error">❌ 加载失败: ${e.message}</div>`;
loading.style.display = "none";
}
}
let timeout;
searchInput.addEventListener("input", function() {
clearTimeout(timeout);
timeout = setTimeout(() => {
searchQuery = this.value.trim();
render();
}, 200);
});
document.addEventListener("keydown", function(e) {
if ((e.ctrlKey && e.key === "f") || (e.key === "/" && !e.ctrlKey && !e.metaKey)) {
e.preventDefault();
searchInput.focus();
searchInput.select();
}
if (e.key === "Escape") {
searchInput.value = "";
searchQuery = "";
render();
searchInput.blur();
}
});
btnRefresh.addEventListener("click", loadData);
renderBreadcrumb();
loadData();
</script>
</body>
</html>