重构static的系统前端

This commit is contained in:
qing committed 2026-07-23 08:27:27 +08:00
1 parent 167a458e5a
commit fc6ce0e7ff
4 files changed
+544 -450

No files matched your search

+5 -442
View File
@@ -4,271 +4,14 @@
<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>
:root {
--bg: #fff8f0;
--fg: #5a3e2b;
--card: #fff5e6;
--card-hover: #ffe8cc;
--border: #f0d9b5;
--border-light: #f5e6d3;
--muted: #b08968;
--subtle: #8b6f47;
--accent: #e67e22;
--accent-dark: #d35400;
--accent-bg: rgba(255,255,255,0.25);
--accent-bg-active: rgba(255,255,255,0.45);
--play: #f39c12;
--error: #c0392b;
--shadow: 0 2px 6px rgba(211, 84, 0, 0.1);
--shadow-hover: 0 4px 12px rgba(211, 84, 0, 0.2);
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #2b1d14;
--fg: #f5e6d3;
--card: #3d2817;
--card-hover: #4a3020;
--border: #5a3e2b;
--border-light: #4a3020;
--muted: #a08868;
--subtle: #c4a67a;
--accent: #e67e22;
--accent-dark: #d35400;
--accent-bg: rgba(255,255,255,0.15);
--accent-bg-active: rgba(255,255,255,0.3);
--shadow: 0 2px 6px rgba(0,0,0,0.3);
--shadow-hover: 0 4px 12px rgba(0,0,0,0.5);
}
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif;
background: var(--bg);
color: var(--fg);
min-height: 100vh;
}
.header {
background: linear-gradient(135deg, var(--accent), var(--accent-dark));
color: white;
padding: 14px 16px;
position: sticky;
top: 0;
z-index: 10;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: var(--shadow);
}
.header h1 { font-size: 18px; font-weight: 600; }
.header-actions { display: flex; gap: 6px; }
.header-actions a {
color: white;
text-decoration: none;
padding: 6px 14px;
background: var(--accent-bg);
border-radius: 20px;
font-size: 14px;
transition: background 0.15s;
}
.header-actions a:active { background: var(--accent-bg-active); }
.breadcrumb {
padding: 10px 16px;
background: var(--card);
font-size: 13px;
overflow-x: auto;
white-space: nowrap;
border-bottom: 1px solid var(--border);
}
.breadcrumb a {
color: var(--accent);
text-decoration: none;
padding: 3px 10px;
border-radius: 12px;
transition: background 0.15s;
}
.breadcrumb a:hover { background: var(--card-hover); }
.breadcrumb .sep { color: var(--muted); margin: 0 4px; }
.breadcrumb .current { color: var(--fg); font-weight: 500; padding: 3px 10px; }
.search-box {
padding: 10px 16px;
background: var(--card);
border-bottom: 1px solid var(--border);
}
.search-box input {
width: 100%;
padding: 10px 16px;
border: 1px solid var(--border-light);
border-radius: 24px;
font-size: 14px;
outline: none;
background: var(--bg);
color: var(--fg);
transition: border-color 0.15s, box-shadow 0.15s;
}
.search-box input:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(230, 126, 34, 0.2);
}
.stats {
padding: 8px 16px;
background: var(--bg);
font-size: 12px;
color: var(--subtle);
border-bottom: 1px solid var(--border);
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.stats span {
padding: 3px 10px;
background: var(--card);
border-radius: 12px;
border: 1px solid var(--border-light);
}
.container {
padding: 14px;
max-width: 1200px;
margin: 0 auto;
}
/* 方块网格布局 */
.file-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 14px;
}
.item {
background: var(--card);
border-radius: 20px;
padding: 16px 12px;
cursor: pointer;
border: 1px solid var(--border-light);
box-shadow: var(--shadow);
transition: background 0.15s, box-shadow 0.15s, transform 0.1s;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: 8px;
}
.item:hover { background: var(--card-hover); box-shadow: var(--shadow-hover); }
.item:active { transform: scale(0.97); }
.item .icon {
font-size: 42px;
width: 64px;
height: 64px;
line-height: 64px;
text-align: center;
background: var(--bg);
border-radius: 50%;
border: 1px solid var(--border-light);
}
.item .info {
width: 100%;
display: flex;
flex-direction: column;
gap: 4px;
}
.item .name {
font-size: 14px;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
}
.item .meta {
font-size: 11px;
color: var(--muted);
display: flex;
justify-content: center;
gap: 6px;
flex-wrap: wrap;
}
.item .actions {
display: flex;
gap: 6px;
justify-content: center;
margin-top: 6px;
}
.item .actions button {
padding: 6px 14px;
font-size: 12px;
border: none;
border-radius: 16px;
cursor: pointer;
color: white;
transition: opacity 0.15s, transform 0.1s;
}
.item .actions button:hover { opacity: 0.85; }
.item .actions button:active { transform: scale(0.92); }
.btn-play { background: var(--play); }
.btn-download { background: var(--accent); }
.loading { text-align: center; padding: 50px; color: var(--muted); }
.loading .spinner {
display: inline-block;
animation: spin 1s linear infinite;
font-size: 32px;
width: 56px;
height: 56px;
line-height: 56px;
background: var(--card);
border-radius: 50%;
box-shadow: var(--shadow);
margin-bottom: 8px;
}
@keyframes spin { 100% { transform: rotate(360deg); } }
.empty { text-align: center; padding: 70px 20px; color: var(--muted); }
.empty .icon {
font-size: 56px;
display: inline-block;
width: 80px;
height: 80px;
line-height: 80px;
background: var(--card);
border-radius: 50%;
box-shadow: var(--shadow);
margin-bottom: 16px;
}
.error {
text-align: center;
padding: 50px 20px;
color: var(--error);
background: var(--card);
border-radius: 20px;
margin: 20px 0;
}
@media (max-width: 480px) {
.file-grid { grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); gap: 10px; }
.item { padding: 12px 8px; border-radius: 16px; }
.item .icon { font-size: 32px; width: 52px; height: 52px; line-height: 52px; }
.item .name { font-size: 13px; }
.item .meta { font-size: 10px; gap: 4px; }
.item .actions button { padding: 5px 10px; font-size: 11px; border-radius: 14px; }
.header h1 { font-size: 16px; }
.search-box input { border-radius: 20px; }
}
</style>
<link rel="stylesheet" href="css/view.css">
</head>
<body>
<header class="header">
<h1>📁 文件浏览</h1>
<nav class="header-actions">
<a href="/">🏠</a>
<a href="#" id="btnRefresh">🔄</a>
<a href="/" title="根目录">🏠</a>
<a href="#" id="btnRefresh" title="刷新">🔄</a>
</nav>
</header>
@@ -277,7 +20,7 @@
</nav>
<div class="search-box">
<input type="text" id="searchInput" placeholder="搜索文件..." autocomplete="off">
<input type="text" id="searchInput" placeholder="搜索文件…(快捷键 / 或 Ctrl+F)" autocomplete="off">
</div>
<div class="stats">
@@ -300,186 +43,6 @@
</div>
</main>
<script>
// ===== 路径解析:从 URL 取当前目录(去除首尾斜杠) =====
const currentPath = decodeURIComponent(window.location.pathname).replace(/^\/+|\/+$/g, '');
// ===== DOM 引用(id 与原版本完全一致,保持后端/前端契约不变) =====
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");
// ===== 状态 =====
let items = [];
let searchQuery = "";
// ===== 工具函数 =====
const SIZE_UNITS = ["B", "KB", "MB", "GB"];
function formatSize(bytes) {
if (bytes === 0) return "0B";
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return (bytes / Math.pow(1024, i)).toFixed(1) + SIZE_UNITS[i];
}
const FILE_ICONS = {
pdf: "📄", doc: "📝", docx: "📝", xls: "📊", xlsx: "📊",
zip: "📦", rar: "📦", "7z": "📦",
jpg: "🖼️", jpeg: "🖼️", png: "🖼️", gif: "🖼️",
txt: "📃", json: "📋", html: "🌐", css: "🎨", js: "⚡"
};
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();
return FILE_ICONS[ext] || "📄";
}
// 转义文本,防止文件名含特殊字符导致 XSS 或模板断裂
function esc(str) {
return String(str)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
// ===== 导航行为(与原版本一致) =====
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"; }
// ===== 面包屑 =====
function renderBreadcrumb() {
const parts = currentPath ? currentPath.split('/') : [];
const frags = ['<a href="/">根目录</a>'];
let acc = '';
parts.forEach((part, i) => {
if (i === parts.length - 1) {
frags.push(`<span class="sep">›</span><span class="current">${esc(part)}</span>`);
} else {
acc += (i === 0 ? '' : '/') + part;
frags.push(`<span class="sep">›</span><a href="/${encodeURIComponent(acc)}/">${esc(part)}</a>`);
}
});
breadcrumb.innerHTML = frags.join('');
}
// ===== 列表渲染(使用转义,避免文件名注入) =====
function render() {
const q = searchQuery.toLowerCase();
const filtered = searchQuery
? items.filter(i => i.name.toLowerCase().includes(q))
: items;
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 => {
const path = esc(item.path);
const type = esc(item.type);
const name = esc(item.name);
const sizeOrDir = item.type !== "dir"
? `<span>${formatSize(item.size)}</span>`
: '<span>📁 文件夹</span>';
const playBtn = (item.type === "video" || item.type === "audio")
? `<button class="btn-play" onclick="event.stopPropagation(); playMedia('${path}')">▶</button>`
: '';
return `
<div class="item">
<span class="icon">${getIcon(item)}</span>
<div class="info" onclick="navigate('${path}', '${type}')">
<div class="name">${name}</div>
<div class="meta">
${sizeOrDir}
<span>${esc(item.modified)}</span>
</div>
</div>
<div class="actions">
${playBtn}
<button class="btn-download" onclick="event.stopPropagation(); downloadFile('${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">❌ 加载失败: ${esc(e.message)}</div>`;
loading.style.display = "none";
}
}
// ===== 事件绑定(快捷键 / 搜索 / 刷新 与原版本一致) =====
let searchTimer;
searchInput.addEventListener("input", function () {
clearTimeout(searchTimer);
searchTimer = 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>
<script src="js/view.js"></script>
</body>
</html>
+360
View File
@@ -0,0 +1,360 @@
:root {
--bg: #fdf6ee;
--fg: #4a3728;
--fg-secondary: #8c7260;
--card: #ffffff;
--card-hover: #fff7ef;
--card-active: #ffeedd;
--border: #f0e0cc;
--border-light: #f7ecdd;
--muted: #b89b7a;
--accent: #e8853a;
--accent-dark: #cf6b1f;
--accent-soft: rgba(232, 133, 58, 0.10);
--accent-glow: rgba(232, 133, 58, 0.25);
--play: #f0a030;
--download: #e8853a;
--error: #d94f3d;
--error-bg: #fef0ee;
--icon-bg: #faf0e4;
--icon-border: #f0e0cc;
--shadow-sm: 0 1px 3px rgba(74, 55, 40, 0.06);
--shadow-md: 0 4px 12px rgba(74, 55, 40, 0.08);
--shadow-lg: 0 8px 24px rgba(74, 55, 40, 0.12);
--radius-sm: 10px;
--radius-md: 14px;
--radius-lg: 20px;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #1e1712;
--fg: #f0e4d8;
--fg-secondary: #a08c78;
--card: #2a2119;
--card-hover: #342a20;
--card-active: #3e3228;
--border: #3e3228;
--border-light: #342a20;
--muted: #8c7a66;
--accent: #e8934a;
--accent-dark: #f0a060;
--accent-soft: rgba(232, 147, 74, 0.12);
--accent-glow: rgba(232, 147, 74, 0.20);
--play: #f0a848;
--download: #e8934a;
--error: #e8685a;
--error-bg: #3a2220;
--icon-bg: #342a20;
--icon-border: #4a3c30;
--shadow-sm: 0 1px 3px rgba(0,0,0,0.2);
--shadow-md: 0 4px 12px rgba(0,0,0,0.3);
--shadow-lg: 0 8px 24px rgba(0,0,0,0.4);
}
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', Roboto, Arial, sans-serif;
background: var(--bg);
color: var(--fg);
min-height: 100vh;
-webkit-font-smoothing: antialiased;
}
/* ===== 顶栏 ===== */
.header {
background: linear-gradient(135deg, var(--accent) 0%, var(--accent-dark) 100%);
color: #fff;
padding: 0 20px;
height: 54px;
position: sticky;
top: 0;
z-index: 100;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 2px 12px rgba(207, 107, 31, 0.3);
}
.header h1 {
font-size: 17px;
font-weight: 600;
letter-spacing: 0.3px;
display: flex;
align-items: center;
gap: 8px;
}
.header-actions { display: flex; gap: 4px; }
.header-actions a {
color: #fff;
text-decoration: none;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-sm);
font-size: 16px;
transition: background 0.15s;
}
.header-actions a:hover { background: rgba(255,255,255,0.18); }
.header-actions a:active { background: rgba(255,255,255,0.30); }
/* ===== 面包屑 ===== */
.breadcrumb {
padding: 10px 20px;
background: var(--card);
font-size: 13px;
overflow-x: auto;
white-space: nowrap;
border-bottom: 1px solid var(--border-light);
scrollbar-width: none;
}
.breadcrumb::-webkit-scrollbar { display: none; }
.breadcrumb a {
color: var(--accent);
text-decoration: none;
padding: 4px 10px;
border-radius: 8px;
transition: background 0.15s, color 0.15s;
font-weight: 500;
}
.breadcrumb a:hover { background: var(--accent-soft); }
.breadcrumb .sep { color: var(--muted); margin: 0 2px; font-size: 11px; }
.breadcrumb .current {
color: var(--fg);
font-weight: 600;
padding: 4px 10px;
}
/* ===== 搜索栏 ===== */
.search-box {
padding: 12px 20px;
background: var(--card);
border-bottom: 1px solid var(--border-light);
}
.search-box input {
width: 100%;
max-width: 560px;
padding: 10px 18px;
border: 1.5px solid var(--border);
border-radius: 12px;
font-size: 14px;
outline: none;
background: var(--bg);
color: var(--fg);
transition: border-color 0.2s, box-shadow 0.2s;
}
.search-box input::placeholder { color: var(--muted); }
.search-box input:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-glow);
}
/* ===== 统计条 ===== */
.stats {
padding: 10px 20px;
font-size: 12px;
color: var(--fg-secondary);
display: flex;
gap: 8px;
flex-wrap: wrap;
align-items: center;
}
.stats span {
padding: 4px 12px;
background: var(--card);
border-radius: 20px;
border: 1px solid var(--border-light);
box-shadow: var(--shadow-sm);
font-variant-numeric: tabular-nums;
}
/* ===== 主容器 ===== */
.container {
padding: 16px 20px 40px;
max-width: 1200px;
margin: 0 auto;
}
/* ===== 文件网格 ===== */
.file-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(155px, 1fr));
gap: 14px;
}
.item {
background: var(--card);
border-radius: var(--radius-lg);
padding: 18px 14px 14px;
cursor: pointer;
border: 1px solid var(--border-light);
box-shadow: var(--shadow-sm);
transition: box-shadow 0.2s, transform 0.15s, border-color 0.2s, background 0.15s;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: 10px;
position: relative;
}
.item:hover {
box-shadow: var(--shadow-md);
transform: translateY(-2px);
border-color: var(--border);
background: var(--card-hover);
}
.item:active {
transform: translateY(0) scale(0.98);
background: var(--card-active);
}
/* 方块图标 */
.item .icon {
font-size: 34px;
width: 62px;
height: 62px;
display: flex;
align-items: center;
justify-content: center;
background: var(--icon-bg);
border-radius: var(--radius-md);
border: 1px solid var(--icon-border);
transition: transform 0.15s;
flex-shrink: 0;
}
.item:hover .icon { transform: scale(1.06); }
.item .info {
width: 100%;
display: flex;
flex-direction: column;
gap: 4px;
}
.item .name {
font-size: 13px;
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
line-height: 1.3;
}
.item .meta {
font-size: 11px;
color: var(--muted);
display: flex;
justify-content: center;
gap: 6px;
flex-wrap: wrap;
line-height: 1.4;
}
/* 操作按钮 */
.item .actions {
display: flex;
gap: 6px;
justify-content: center;
margin-top: 2px;
}
.item .actions button {
padding: 5px 14px;
font-size: 12px;
border: none;
border-radius: 10px;
cursor: pointer;
color: #fff;
font-weight: 500;
transition: opacity 0.15s, transform 0.1s;
line-height: 1.4;
}
.item .actions button:hover { opacity: 0.85; }
.item .actions button:active { transform: scale(0.93); }
.btn-play { background: var(--play); }
.btn-download { background: var(--download); }
/* ===== 加载态 ===== */
.loading {
text-align: center;
padding: 60px 20px;
color: var(--muted);
font-size: 14px;
}
.loading .spinner {
display: inline-flex;
align-items: center;
justify-content: center;
animation: spin 1.2s cubic-bezier(0.45, 0.05, 0.55, 0.95) infinite;
font-size: 28px;
width: 56px;
height: 56px;
background: var(--card);
border-radius: var(--radius-md);
border: 1px solid var(--border-light);
box-shadow: var(--shadow-sm);
margin-bottom: 12px;
}
@keyframes spin { 100% { transform: rotate(360deg); } }
/* ===== 空态 ===== */
.empty {
text-align: center;
padding: 80px 20px;
color: var(--muted);
font-size: 14px;
}
.empty .icon {
font-size: 44px;
display: inline-flex;
align-items: center;
justify-content: center;
width: 80px;
height: 80px;
background: var(--card);
border-radius: var(--radius-lg);
border: 1px solid var(--border-light);
box-shadow: var(--shadow-sm);
margin-bottom: 16px;
}
/* ===== 错误态 ===== */
.error {
text-align: center;
padding: 40px 24px;
color: var(--error);
background: var(--error-bg);
border-radius: var(--radius-lg);
border: 1px solid var(--error);
margin: 20px 0;
font-size: 14px;
line-height: 1.6;
}
/* ===== 响应式 ===== */
@media (max-width: 480px) {
.header { padding: 0 14px; height: 50px; }
.header h1 { font-size: 15px; }
.breadcrumb { padding: 8px 14px; }
.search-box { padding: 10px 14px; }
.stats { padding: 8px 14px; gap: 6px; }
.container { padding: 12px 14px 32px; }
.file-grid {
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 10px;
}
.item {
padding: 14px 10px 12px;
border-radius: 16px;
gap: 8px;
}
.item .icon {
font-size: 28px;
width: 52px;
height: 52px;
border-radius: var(--radius-sm);
}
.item .name { font-size: 12px; }
.item .meta { font-size: 10px; gap: 4px; }
.item .actions button { padding: 4px 10px; font-size: 11px; border-radius: 8px; }
}
+179
View File
@@ -0,0 +1,179 @@
// ===== 路径解析:从 URL 取当前目录(去除首尾斜杠) =====
const currentPath = decodeURIComponent(window.location.pathname).replace(/^\/+|\/+$/g, '');
// ===== DOM 引用(id 与原版本完全一致,保持后端/前端契约不变) =====
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");
// ===== 状态 =====
let items = [];
let searchQuery = "";
// ===== 工具函数 =====
const SIZE_UNITS = ["B", "KB", "MB", "GB"];
function formatSize(bytes) {
if (bytes === 0) return "0B";
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return (bytes / Math.pow(1024, i)).toFixed(1) + SIZE_UNITS[i];
}
const FILE_ICONS = {
pdf: "📄", doc: "📝", docx: "📝", xls: "📊", xlsx: "📊",
zip: "📦", rar: "📦", "7z": "📦",
jpg: "🖼️", jpeg: "🖼️", png: "🖼️", gif: "🖼️",
txt: "📃", json: "📋", html: "🌐", css: "🎨", js: "⚡"
};
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();
return FILE_ICONS[ext] || "📄";
}
// 转义文本,防止文件名含特殊字符导致 XSS 或模板断裂
function esc(str) {
return String(str)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
// ===== 导航行为(与原版本一致) =====
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"; }
// ===== 面包屑 =====
function renderBreadcrumb() {
const parts = currentPath ? currentPath.split('/') : [];
const frags = ['<a href="/">根目录</a>'];
let acc = '';
parts.forEach((part, i) => {
if (i === parts.length - 1) {
frags.push(`<span class="sep">›</span><span class="current">${esc(part)}</span>`);
} else {
acc += (i === 0 ? '' : '/') + part;
frags.push(`<span class="sep">›</span><a href="/${encodeURIComponent(acc)}/">${esc(part)}</a>`);
}
});
breadcrumb.innerHTML = frags.join('');
}
// ===== 列表渲染(使用转义,避免文件名注入) =====
function render() {
const q = searchQuery.toLowerCase();
const filtered = searchQuery
? items.filter(i => i.name.toLowerCase().includes(q))
: items;
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 => {
const path = esc(item.path);
const type = esc(item.type);
const name = esc(item.name);
const sizeOrDir = item.type !== "dir"
? `<span>${formatSize(item.size)}</span>`
: '<span>📁 文件夹</span>';
const playBtn = (item.type === "video" || item.type === "audio")
? `<button class="btn-play" onclick="event.stopPropagation(); playMedia('${path}')">▶</button>`
: '';
return `
<div class="item">
<span class="icon">${getIcon(item)}</span>
<div class="info" onclick="navigate('${path}', '${type}')">
<div class="name">${name}</div>
<div class="meta">
${sizeOrDir}
<span>${esc(item.modified)}</span>
</div>
</div>
<div class="actions">
${playBtn}
<button class="btn-download" onclick="event.stopPropagation(); downloadFile('${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">❌ 加载失败: ${esc(e.message)}</div>`;
loading.style.display = "none";
}
}
// ===== 事件绑定(快捷键 / 搜索 / 刷新 与原版本一致) =====
let searchTimer;
searchInput.addEventListener("input", function () {
clearTimeout(searchTimer);
searchTimer = 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();
-8
View File
@@ -1,11 +1,3 @@
nuitka
Flask>=2.3.0,<3.0.0
Flask-CORS>=4.0.0,<5.0.0
Flask-SQLAlchemy>=3.1.0,<4.0.0
Flask-Limiter>=3.3.0,<4.0.0
Flask-SocketIO>=5.3.0,<6.0.0
python-socketio>=5.10.0,<6.0.0
Werkzeug>=2.3.0,<3.0.0
requests>=2.31.0,<3.0.0
websocket-client>=1.6.0,<2.0.0
python-engineio>=4.7.0,<5.0.0