This commit is contained in:
condor
2026-07-24 19:05:07 +08:00
commit b46270264b
10 changed files with 901 additions and 0 deletions
+13
View File
@@ -0,0 +1,13 @@
## 2026-07-17 工作记录
- 修复搜索结果表格显示异常:
- 问题:搜索只显示文件名(如 SimHei.ttf),缺少 Size、Last modified 列,且点击后 404。
- 根因:`render()` 直接把抓取到的 `tr.outerHTML` 拼进新表格,浏览器解析时 `<tr>` 不能作为 `<div>` 子元素,导致 `div.firstChild` 是文本节点,`href` 未正确替换,最终只渲染出孤立文件名。
- 修复:在 `theme/header.html``theme-preview.html``crawl()` 中改为提取结构化数据 `{ name, full, isDir, size, date }``render()` 手动生成完整 4 列表格行,并用 `escapeHtml()` 处理输出;链接统一用绝对路径 `full`,兼容 href 已以 `/` 开头的情况。
- 修复搜索结果中目录名双斜杠:原 `render()` 对目录无条件追加 `/`,而 fancyindex 的链接文本本身已带 `/`,导致 `client-v1.3.7//`。改为仅在 `e.name` 不以 `/` 结尾时才追加 `/`。同步更新 `theme/header.html``theme-preview.html`
- 说明:Directory 面包屑显示的是当前页面 URL 路径,搜索时 URL 不变,所以面包屑保持当前目录路径是正确行为;点击搜索结果进入子目录后才会更新。
- 新增「返回根目录」按钮:位于搜索框右侧,`.search-form` 改为 flex 布局,`.home-btn`accent 色胶囊按钮 + home 图标 + 文字),href="/";窄屏 (<=480px) 只显示图标。改动在 `theme/header.html``theme-preview.html``theme/css/styles.css`
- 新增「返回上一级目录」按钮:位于搜索框左侧,`initUpBtn()` 用 JS 根据 `location.pathname` 计算父目录并写入 `href`(如 /a/b/ -> /a/)。`.up-btn` 用 outline 风格(透明底 + border)与根目录实心按钮区分;窄屏只显示图标。改动同上三文件。
- 修复「上一级」按钮不显示:`initUpBtn()` 原在 `file://` 预览或站点根目录时 `display:none`,导致预览和根目录页看不到按钮。改为始终显示:预览时 `href="#"` 仅展示;http(s) 下根目录 `href="/"`、子目录指向上级 `path.slice(0, idx+1)`
- 关键部署修正:`theme/header.html` 第 1 行的 CSS 链接原本指向远程 `https://repo.52or.com/fancyindex/styles.css`,导致本地 `theme/css/styles.css` 的样式(含 `.up-btn`/`.home-btn`)在服务器上不生效。已改为 `/theme/css/styles.css`,与 `fancyindex.conf``/theme/...` 约定一致,主题自包含。
- 修复按钮被旧 CSS 缓存导致换行:截图显示「上一级」与「根目录」垂直堆叠、且输入框未同行,说明浏览器仍在用旧的(或未同步的)CSS,`.search-form {display:flex}` 未生效。给 CSS 链接加 `?v=3` 缓存戳,并在 `<form>` 上内联 `style="display:flex;justify-content:center;align-items:center;gap:10px;"` 作为兜底。同步更新 `theme/header.html``theme-preview.html`
+18
View File
@@ -0,0 +1,18 @@
# 2026-07-18 工作日志
## 1. 诊断:部分 .zip 文件不显示
- 现象:`Nginx-Fancyindex-Theme.zip` 不显示,`ServerStack.zip` 正常显示。
- 根因:`fancyindex.conf` 第 8 行 `fancyindex_ignore "theme";` 本意是隐藏 `/theme/` 资源目录,
`fancyindex_ignore` 在 PCRE 下是**不区分大小写 + 未锚定的子串正则**(`ngx_http_fancyindex_module.c`
`NGX_REGEX_CASELESS` 编译,且按裸文件名匹配、无 `^$` 锚点),因此也命中了文件名含 `Theme` 的 zip。
- 建议修复(尚未改动配置):改为 `fancyindex_ignore "^theme$";` 精确匹配目录名,避免误伤含 theme 的文件。
## 2. 修改:列标题改为 File Name / File Size / Date
- nginx 的 `ngx-fancyindex` 模块自行生成 `table#list``<thead>`,主题无法用静态 HTML 改表头文字;
`styles.css``thead th { text-transform: uppercase }` 会把标题强制大写。
- 方案:在 `theme/header.html` 的 IIFE 中新增 `renameHeaders()`,于 `onReady()` 调用,
用 JS 改写 `table#list``table.results``<th>` 文本为
`['File Name','File Size','Date','']`,并通过内联 `th.style.textTransform='none'` 覆盖大写(内联优先级高于外部 CDN 样式,无需改 CSS 文件)。
- 同步更新 `theme-preview.html` 的静态 thead 与脚本,保持预览一致。
- 注:`header.html:1` 仍引用外部 `https://repo.52or.com/fancyindex/styles.css?v=3`,本地 `theme/css/styles.css` 不一定线上生效;JS 内联方案不受此影响。
- **回退**:用户要求"回退到上一个版本",已将 `theme/header.html``theme-preview.html` 中本次的列标题改动全部撤销,恢复为 `Name / Size / Last modified`。主题文件当前回到改动前状态。
+10
View File
@@ -0,0 +1,10 @@
# 项目长期记忆:Nginx-Fancyindex-Theme
## 部署约定
- 主题资源放在 nginx `root` 下的 `theme/``/theme/header.html``/theme/footer.html``/theme/css/styles.css`
- 依赖第三方 `ngx-fancyindex` 模块;未编译则 `fancyindex` 指令报错。
- `fancyindex_ignore "theme"` 仅隐藏列表显示,不阻止对 `/theme/...` 的直接访问。
## 已知坑
- 搜索结果显示需手动构建表格行:不可依赖抓取到的 `tr.outerHTML` 直接插入新表格,否则会出现列缺失、链接错误、只显示文件名等问题。应提取 `{ name, full, isDir, size, date }` 结构化数据后重新渲染 4 列表格(Name / Size / Last modified / 空)。
- `header.html` 中 CSS 链接、目录路径清理、搜索递归均用前端 JS 实现,修改后刷新页面即可生效,无需 reload nginx。
+5
View File
@@ -0,0 +1,5 @@
# Nginx-Fancyindex-Theme
A responsive theme for Nginx Fancyindex module. Minimal, modern and simple.
Comes with a search form, aims to handle thousands of files without any problems.
The fancyindex module can be found [here](https://github.com/aperezdc/ngx-fancyindex).
+20
View File
@@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright © 2016 Lilian Besson (Naereen), https://github.com/naereen/ <naereen at crans dot org>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the “Software”), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+9
View File
@@ -0,0 +1,9 @@
fancyindex on;
fancyindex_localtime on;
fancyindex_time_format "%Y-%m-%d %H:%M";
fancyindex_exact_size off;
fancyindex_header "/theme/header.html";
fancyindex_footer "/theme/footer.html";
fancyindex_ignore "examplefile.html"; # Ignored files will not show up in the directory listing, but will still be public.
fancyindex_ignore "^theme$"; # Making sure folder where files are don't show up in the listing.
fancyindex_name_length 255; # Maximum file name length in bytes, change as you like.
+246
View File
@@ -0,0 +1,246 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Nginx-Fancyindex-Theme · 预览</title>
<link rel="stylesheet" href="theme/css/styles.css?v=3">
</head>
<body>
<!-- ===== header.html (fragment) ===== -->
<div class="wrapper">
<header class="site-header">
<form class="search-form" role="search" onsubmit="return false" style="display:flex;justify-content:center;align-items:center;gap:10px;">
<a class="up-btn" id="up-btn" href="#" aria-label="返回上一级目录" title="返回上一级目录">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path fill="currentColor" d="M13.414 5.414 7.828 11l5.586 5.586a1 1 0 0 1-1.414 1.414l-6.293-6.293a1 1 0 0 1 0-1.414l6.293-6.293a1 1 0 0 1 1.414 1.414Z"/></svg>
<span>上一级</span>
</a>
<input name="filter" id="s" type="search" placeholder="输入关键字筛选文件…" aria-label="搜索文件">
<a class="home-btn" href="/" aria-label="返回根目录" title="返回根目录">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path fill="currentColor" d="M12 3.172 2.808 11.02a1 1 0 0 0 .65 1.758H5v7.25a1 1 0 0 0 1 1h3.5a1 1 0 0 0 1-1v-4.25h3v4.25a1 1 0 0 0 1 1H18a1 1 0 0 0 1-1v-7.25h1.542a1 1 0 0 0 .65-1.758L12 3.172Z"/></svg>
<span>根目录</span>
</a>
</form>
</header>
<h2 class="dir-title">
<span class="dir-label">Directory</span>
<nav class="breadcrumb" id="dir-path" aria-label="当前路径">/</nav>
</h2>
<script>
(function () {
function renderBreadcrumb() {
var host = document.getElementById('dir-path');
if (!host) return;
var path;
if (location.protocol === 'file:') {
path = '/';
} else {
path = decodeURIComponent(location.pathname);
if (path.charAt(path.length - 1) !== '/') path += '/';
}
var parts = path.split('/').filter(function (p) { return p !== ''; });
host.innerHTML = '';
var root = document.createElement('a');
root.href = '/';
root.textContent = '/';
host.appendChild(root);
var acc = '';
parts.forEach(function (p, i) {
acc += '/' + p;
var sep = document.createElement('span');
sep.className = 'sep';
sep.textContent = (i === 0) ? ' ' : ' / ';
host.appendChild(sep);
var a = document.createElement('a');
a.href = acc + '/';
a.textContent = p;
host.appendChild(a);
});
}
renderBreadcrumb();
// 递归搜索:抓取整棵目录树(当前目录 + 子目录),构建扁平索引后按关键字过滤
function initSearch() {
var list = document.getElementById('list');
var input = document.getElementById('s');
if (!list || !input) return;
var baseDir = location.pathname;
if (baseDir.charAt(baseDir.length - 1) !== '/') baseDir += '/';
var MAX_DEPTH = 8;
var indexPromise = null;
var resultsEl = null;
function ensureResults() {
if (resultsEl) return resultsEl;
resultsEl = document.createElement('div');
resultsEl.id = 'search-results';
resultsEl.hidden = true;
list.parentNode.insertBefore(resultsEl, list.nextSibling);
return resultsEl;
}
function crawl(dir, depth, results, seen) {
if (depth > MAX_DEPTH || seen[dir]) return Promise.resolve();
seen[dir] = true;
return fetch(dir, { credentials: 'same-origin' })
.then(function (r) { return r.ok ? r.text() : ''; })
.then(function (html) {
if (!html) return;
var doc = new DOMParser().parseFromString(html, 'text/html');
var trs = doc.querySelectorAll('table#list tbody tr');
var tasks = [];
Array.prototype.forEach.call(trs, function (tr) {
var a = tr.querySelector('td a');
if (!a) return;
var name = (a.textContent || '').trim();
var href = a.getAttribute('href');
if (!href || href === '../' || name === 'Parent Directory') return;
var isDir = /\/$/.test(href);
var full = href.charAt(0) === '/' ? href : dir + href;
// 提取 Size / Last modified(与表头列保持一致)
var cells = tr.querySelectorAll('td');
var size = cells.length > 1 ? (cells[1].textContent || '').trim() : '-';
var date = cells.length > 2 ? (cells[2].textContent || '').trim() : '-';
results.push({ name: name, full: full, isDir: isDir, size: size, date: date });
if (isDir) tasks.push(crawl(full, depth + 1, results, seen));
});
return Promise.all(tasks);
})
.catch(function () { /* 忽略不可访问的目录 */ });
}
function demoIndex() {
return Promise.resolve([
{ name: 'ubuntu-24.04.iso', full: '/ubuntu-24.04.iso', isDir: false, size: '5.1 GiB', date: '2026-01-12 09:24' },
{ name: 'docs', full: '/docs/', isDir: true, size: '-', date: '2026-03-04 18:02' },
{ name: 'readme.txt', full: '/docs/readme.txt', isDir: false, size: '12 KiB', date: '2026-03-10 10:00' },
{ name: 'guide.pdf', full: '/docs/guide.pdf', isDir: false, size: '1.4 MiB', date: '2026-03-12 14:30' },
{ name: 'backup.tar.gz', full: '/backup.tar.gz', isDir: false, size: '842 MiB', date: '2026-05-21 22:47' },
{ name: 'README.md', full: '/README.md', isDir: false, size: '3.3 KiB', date: '2026-06-30 11:15' },
{ name: 'scripts', full: '/scripts/', isDir: true, size: '-', date: '2026-07-01 07:53' },
{ name: 'deploy.sh', full: '/scripts/deploy.sh', isDir: false, size: '3 KiB', date: '2026-07-02 08:11' },
{ name: 'setup.sh', full: '/scripts/setup.sh', isDir: false, size: '5 KiB', date: '2026-07-03 09:40' }
]);
}
function buildIndex() {
if (indexPromise) return indexPromise;
if (location.protocol === 'file:') {
indexPromise = demoIndex();
} else {
var results = [];
var seen = {};
indexPromise = crawl(baseDir, 0, results, seen).then(function () { return results; });
}
return indexPromise;
}
function render(entries, q) {
var el = ensureResults();
if (!entries.length) {
el.innerHTML = '<p class="search-status">未找到匹配 “' + q + '” 的文件(已包含子目录)</p>';
return;
}
var rows = entries.map(function (e) {
var displayName = e.name + (e.isDir && !/[\/]$/.test(e.name) ? '/' : '');
return '<tr>' +
'<td><a href="' + escapeHtml(e.full) + '">' + escapeHtml(displayName) + '</a></td>' +
'<td>' + escapeHtml(e.size) + '</td>' +
'<td>' + escapeHtml(e.date) + '</td>' +
'<td> </td>' +
'</tr>';
}).join('');
el.innerHTML =
'<p class="search-status">找到 ' + entries.length + ' 个匹配项(含子目录)</p>' +
'<table class="results"><thead><tr><th>Name</th><th>Size</th><th>Last modified</th><th> </th></tr></thead>' +
'<tbody>' + rows + '</tbody></table>';
}
function escapeHtml(str) {
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
input.addEventListener('input', function () {
var q = input.value.trim().toLowerCase();
if (!q) {
list.style.display = '';
if (resultsEl) resultsEl.hidden = true;
return;
}
list.style.display = 'none';
var el = ensureResults();
el.hidden = false;
if (!indexPromise) el.innerHTML = '<p class="search-status">正在索引子目录…</p>';
buildIndex().then(function (entries) {
var matched = entries.filter(function (e) {
return e.name.toLowerCase().indexOf(q) !== -1;
});
render(matched, q);
});
});
}
// 移除 ngx-fancyindex 在表格前输出的裸文本路径(如 /bitwarden/chrome-plugins/
function cleanupStrayText() {
var roots = [document.querySelector('.wrapper'), document.body];
roots.forEach(function (root) {
if (!root) return;
var nodes = root.childNodes;
for (var i = nodes.length - 1; i >= 0; i--) {
var n = nodes[i];
if (n.nodeType === 3 && n.textContent.trim() !== '') {
n.parentNode.removeChild(n);
}
}
});
}
function initUpBtn() {
var btn = document.getElementById('up-btn');
if (!btn) return;
var parent;
if (location.protocol === 'file:') {
parent = '#'; // 本地预览无真实目录层级,仅展示按钮
} else {
var path = location.pathname.replace(/\/+$/, '');
var idx = path.lastIndexOf('/');
parent = (idx <= 0) ? '/' : path.slice(0, idx + 1);
}
btn.setAttribute('href', parent);
}
function onReady() {
initSearch();
initUpBtn();
cleanupStrayText();
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', onReady);
else onReady();
})();
</script>
<!-- ===== injected listing (simulated) ===== -->
<table id="list">
<thead><tr><th>Name</th><th>Size</th><th>Last modified</th><th> </th></tr></thead>
<tbody>
<tr class="parent"><td><a href="../">../</a></td><td>-</td><td>-</td><td>-</td></tr>
<tr><td><a href="ubuntu-24.04.iso">ubuntu-24.04.iso</a></td><td>5.1 GiB</td><td>2026-01-12 09:24</td><td> </td></tr>
<tr><td><a href="docs/">docs/</a></td><td>-</td><td>2026-03-04 18:02</td><td> </td></tr>
<tr><td><a href="backup.tar.gz">backup.tar.gz</a></td><td>842 MiB</td><td>2026-05-21 22:47</td><td> </td></tr>
<tr><td><a href="README.md">README.md</a></td><td>3.3 KiB</td><td>2026-06-30 11:15</td><td> </td></tr>
<tr><td><a href="scripts/">scripts/</a></td><td>-</td><td>2026-07-01 07:53</td><td> </td></tr>
</tbody>
</table>
<!-- ===== footer.html (fragment) ===== -->
</div><!-- /.wrapper -->
</body>
</html>
+348
View File
@@ -0,0 +1,348 @@
/* ============================================================
Nginx-Fancyindex-Theme — modernized stylesheet
- System font stack (no external font dependency → works offline)
- CSS variables for light / dark mode
- Card-style listing, refined search box, smooth hover states
============================================================ */
:root {
--bg: #f6f7f9;
--surface: #ffffff;
--text: #2b2f36;
--text-muted: #8a929e;
--border: #e8ebf0;
--accent: #3b82f6;
--accent-strong: #2563eb;
--row-hover: #f1f5fb;
--row-stripe: #eef2f8;
--shadow: 0 1px 2px rgba(16, 24, 40, .06), 0 10px 30px rgba(16, 24, 40, .05);
--radius: 14px;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #0f1115;
--surface: #171a21;
--text: #e6e9ef;
--text-muted: #8b93a3;
--border: #262b34;
--accent: #60a5fa;
--accent-strong: #93c5fd;
--row-hover: #1d212a;
--row-stripe: #1c212b;
--shadow: 0 1px 2px rgba(0, 0, 0, .5), 0 10px 30px rgba(0, 0, 0, .35);
}
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
font-size: 16px;
color: var(--text);
line-height: 1.6;
background: var(--bg);
-webkit-font-smoothing: antialiased;
font-family: 'Open Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI',
Roboto, 'Helvetica Neue', Arial, 'PingFang SC', 'Microsoft YaHei', sans-serif;
}
body {
padding: 40px 16px 56px;
background: var(--bg);
}
a {
color: var(--accent-strong);
text-decoration: none;
transition: color .2s ease;
}
a:hover {
color: var(--accent);
text-decoration: underline;
}
/* ---------- Layout ---------- */
.wrapper {
max-width: 880px;
margin: 0 auto;
}
.site-header {
text-align: center;
margin-bottom: 6px;
}
/* ---------- Directory breadcrumb (same line) ---------- */
.dir-title {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: .5rem;
margin: 24px 6px 14px;
padding: 10px 14px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow);
font-size: .95rem;
}
.dir-label {
font-size: .68rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: .09em;
color: var(--text-muted);
}
.breadcrumb {
display: flex;
flex-wrap: wrap;
align-items: center;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
font-size: .9rem;
line-height: 1.4;
word-break: break-all;
}
.breadcrumb a {
color: var(--accent-strong);
}
.breadcrumb a:hover {
color: var(--accent);
text-decoration: underline;
}
.breadcrumb .sep {
color: var(--text-muted);
user-select: none;
white-space: pre;
}
/* ---------- Search ---------- */
.search-form {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
margin: 16px 0 4px;
}
input[type="search"] {
display: block;
width: min(380px, 100%);
margin: 0;
padding: 11px 18px;
font-size: 14px;
color: var(--text);
background: var(--surface);
border: 1px solid var(--border);
border-radius: 999px;
outline: none;
box-shadow: var(--shadow);
transition: border-color .2s ease, box-shadow .2s ease;
}
input[type="search"]::placeholder {
color: var(--text-muted);
}
input[type="search"]:focus {
border-color: var(--accent);
box-shadow: 0 0 0 4px rgba(59, 130, 246, .18);
}
input[type="search"]::-webkit-search-cancel-button {
-webkit-appearance: none;
}
/* 返回根目录按钮 */
.home-btn {
display: inline-flex;
align-items: center;
gap: 6px;
flex: none;
padding: 10px 16px;
font-size: 13px;
font-weight: 600;
color: #fff;
background: var(--accent);
border: 1px solid transparent;
border-radius: 999px;
box-shadow: var(--shadow);
white-space: nowrap;
transition: background .2s ease, transform .1s ease;
}
.home-btn:hover {
color: #fff;
background: var(--accent-strong);
text-decoration: none;
}
.home-btn:active {
transform: translateY(1px);
}
.home-btn svg {
display: block;
}
/* 返回上一级目录按钮(outline 风格,与根目录按钮区分) */
.up-btn {
display: inline-flex;
align-items: center;
gap: 6px;
flex: none;
padding: 10px 16px;
font-size: 13px;
font-weight: 600;
color: var(--accent);
background: var(--surface);
border: 1px solid var(--border);
border-radius: 999px;
box-shadow: var(--shadow);
white-space: nowrap;
transition: background .2s ease, transform .1s ease, border-color .2s ease;
}
.up-btn:hover {
color: var(--accent-strong);
border-color: var(--accent);
background: var(--row-hover);
text-decoration: none;
}
.up-btn:active {
transform: translateY(1px);
}
.up-btn svg {
display: block;
}
@media (max-width: 480px) {
.home-btn span {
display: none; /* 窄屏只留图标 */
}
.home-btn {
padding: 10px 12px;
}
.up-btn span {
display: none;
}
.up-btn {
padding: 10px 12px;
}
}
/* ---------- Search results (recursive) ---------- */
#search-results {
margin-top: 4px;
}
.search-status {
margin: 6px 6px 12px;
font-size: .82rem;
color: var(--text-muted);
}
table.results {
margin-top: 10px;
}
/* ---------- Listing table ---------- */
table {
width: 100%;
border-collapse: collapse;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
box-shadow: var(--shadow);
font-size: .92rem;
}
thead th {
text-align: left;
font-weight: 600;
font-size: .72rem;
letter-spacing: .06em;
text-transform: uppercase;
color: var(--text-muted);
padding: 14px 16px;
background: color-mix(in srgb, var(--surface) 92%, var(--border));
border-bottom: 1px solid var(--border);
}
th + th { width: 22%; }
th + th + th + th { width: 6%; }
tr {
outline: 0;
border: 0;
transition: background .2s ease;
}
/* 隔行底色(斑马纹) */
tbody tr:nth-child(even) td {
background: var(--row-stripe);
}
tbody tr:hover td {
background: var(--row-hover);
}
/* Parent directory link: replace nginx's "../" text with a readable label */
tr.parent a {
color: var(--text-muted);
font-weight: 500;
font-size: 0; /* hide the original "../" text */
}
tr.parent a::before {
content: "Parent directory/";
font-size: 0.92rem; /* restore a readable size */
}
tr.parent a:hover {
color: var(--accent-strong);
}
td {
padding: 11px 16px;
border-bottom: 1px solid var(--border);
vertical-align: middle;
text-align: left;
color: var(--text);
}
tr:last-child td {
border-bottom: 0;
}
td a {
display: block;
font-weight: 500;
}
td:last-child,
th:last-child {
text-align: right;
padding-right: 16px;
color: var(--text-muted);
font-variant-numeric: tabular-nums;
}
/* ---------- Responsive ---------- */
@media (max-width: 600px) {
body { padding: 28px 12px 40px; }
table { font-size: .85rem; }
td, thead th { padding: 10px 12px; }
}
+1
View File
@@ -0,0 +1 @@
</div><!-- /.wrapper -->
+231
View File
@@ -0,0 +1,231 @@
<link rel="stylesheet" href="https://repo.52or.com/fancyindex/styles.css?v=3">
<div class="wrapper">
<header class="site-header">
<form class="search-form" role="search" onsubmit="return false" style="display:flex;justify-content:center;align-items:center;gap:10px;">
<a class="up-btn" id="up-btn" href="#" aria-label="返回上一级目录" title="返回上一级目录">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path fill="currentColor" d="M13.414 5.414 7.828 11l5.586 5.586a1 1 0 0 1-1.414 1.414l-6.293-6.293a1 1 0 0 1 0-1.414l6.293-6.293a1 1 0 0 1 1.414 1.414Z"/></svg>
<span>上一级</span>
</a>
<input name="filter" id="s" type="search" placeholder="输入关键字筛选文件…" aria-label="搜索文件">
<a class="home-btn" href="/" aria-label="返回根目录" title="返回根目录">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path fill="currentColor" d="M12 3.172 2.808 11.02a1 1 0 0 0 .65 1.758H5v7.25a1 1 0 0 0 1 1h3.5a1 1 0 0 0 1-1v-4.25h3v4.25a1 1 0 0 0 1 1H18a1 1 0 0 0 1-1v-7.25h1.542a1 1 0 0 0 .65-1.758L12 3.172Z"/></svg>
<span>根目录</span>
</a>
</form>
</header>
<h2 class="dir-title">
<span class="dir-label">Directory</span>
<nav class="breadcrumb" id="dir-path" aria-label="当前路径">/</nav>
</h2>
<script>
(function () {
// 把当前 URL 路径渲染成可点击的面包屑,并与 "Directory" 显示在同一行
function renderBreadcrumb() {
var host = document.getElementById('dir-path');
if (!host) return;
// 本地 file:// 预览时无法拿到真实站点路径,直接显示根目录 "/"
var path;
if (location.protocol === 'file:') {
path = '/';
} else {
path = decodeURIComponent(location.pathname);
if (path.charAt(path.length - 1) !== '/') path += '/';
}
var parts = path.split('/').filter(function (p) { return p !== ''; });
host.innerHTML = '';
var root = document.createElement('a');
root.href = '/';
root.textContent = '/';
host.appendChild(root);
var acc = '';
parts.forEach(function (p, i) {
acc += '/' + p;
var sep = document.createElement('span');
sep.className = 'sep';
sep.textContent = (i === 0) ? ' ' : ' / ';
host.appendChild(sep);
var a = document.createElement('a');
a.href = acc + '/';
a.textContent = p;
host.appendChild(a);
});
}
renderBreadcrumb();
// 递归搜索:抓取整棵目录树(当前目录 + 子目录),构建扁平索引后按关键字过滤
function initSearch() {
var list = document.getElementById('list');
var input = document.getElementById('s');
if (!list || !input) return;
var baseDir = location.pathname;
if (baseDir.charAt(baseDir.length - 1) !== '/') baseDir += '/';
var MAX_DEPTH = 8;
var indexPromise = null;
var resultsEl = null;
function ensureResults() {
if (resultsEl) return resultsEl;
resultsEl = document.createElement('div');
resultsEl.id = 'search-results';
resultsEl.hidden = true;
list.parentNode.insertBefore(resultsEl, list.nextSibling);
return resultsEl;
}
// 从抓取到的目录页 HTML 中提取行,递归收集;dir 必须是带结尾 "/" 的绝对路径
function crawl(dir, depth, results, seen) {
if (depth > MAX_DEPTH || seen[dir]) return Promise.resolve();
seen[dir] = true;
return fetch(dir, { credentials: 'same-origin' })
.then(function (r) { return r.ok ? r.text() : ''; })
.then(function (html) {
if (!html) return;
var doc = new DOMParser().parseFromString(html, 'text/html');
var trs = doc.querySelectorAll('table#list tbody tr');
var tasks = [];
Array.prototype.forEach.call(trs, function (tr) {
var a = tr.querySelector('td a');
if (!a) return;
var name = (a.textContent || '').trim();
var href = a.getAttribute('href');
if (!href || href === '../' || name === 'Parent Directory') return;
var isDir = /\/$/.test(href);
var full = href.charAt(0) === '/' ? href : dir + href;
// 提取 Size / Last modified(与表头列保持一致)
var cells = tr.querySelectorAll('td');
var size = cells.length > 1 ? (cells[1].textContent || '').trim() : '-';
var date = cells.length > 2 ? (cells[2].textContent || '').trim() : '-';
results.push({ name: name, full: full, isDir: isDir, size: size, date: date });
if (isDir) tasks.push(crawl(full, depth + 1, results, seen));
});
return Promise.all(tasks);
})
.catch(function () { /* 忽略不可访问的目录 */ });
}
// 本地 file:// 预览用的模拟目录树(含子目录里的文件)
function demoIndex() {
return Promise.resolve([
{ name: 'ubuntu-24.04.iso', full: '/ubuntu-24.04.iso', isDir: false, size: '5.1 GiB', date: '2026-01-12 09:24' },
{ name: 'docs', full: '/docs/', isDir: true, size: '-', date: '2026-03-04 18:02' },
{ name: 'readme.txt', full: '/docs/readme.txt', isDir: false, size: '12 KiB', date: '2026-03-10 10:00' },
{ name: 'guide.pdf', full: '/docs/guide.pdf', isDir: false, size: '1.4 MiB', date: '2026-03-12 14:30' },
{ name: 'backup.tar.gz', full: '/backup.tar.gz', isDir: false, size: '842 MiB', date: '2026-05-21 22:47' },
{ name: 'README.md', full: '/README.md', isDir: false, size: '3.3 KiB', date: '2026-06-30 11:15' },
{ name: 'scripts', full: '/scripts/', isDir: true, size: '-', date: '2026-07-01 07:53' },
{ name: 'deploy.sh', full: '/scripts/deploy.sh', isDir: false, size: '3 KiB', date: '2026-07-02 08:11' },
{ name: 'setup.sh', full: '/scripts/setup.sh', isDir: false, size: '5 KiB', date: '2026-07-03 09:40' }
]);
}
// 构建索引(只抓一次,之后复用);file:// 预览用模拟数据
function buildIndex() {
if (indexPromise) return indexPromise;
if (location.protocol === 'file:') {
indexPromise = demoIndex();
} else {
var results = [];
var seen = {};
indexPromise = crawl(baseDir, 0, results, seen).then(function () { return results; });
}
return indexPromise;
}
function render(entries, q) {
var el = ensureResults();
if (!entries.length) {
el.innerHTML = '<p class="search-status">未找到匹配 “' + q + '” 的文件(已包含子目录)</p>';
return;
}
var rows = entries.map(function (e) {
var displayName = e.name + (e.isDir && !/[\/]$/.test(e.name) ? '/' : '');
return '<tr>' +
'<td><a href="' + escapeHtml(e.full) + '">' + escapeHtml(displayName) + '</a></td>' +
'<td>' + escapeHtml(e.size) + '</td>' +
'<td>' + escapeHtml(e.date) + '</td>' +
'<td> </td>' +
'</tr>';
}).join('');
el.innerHTML =
'<p class="search-status">找到 ' + entries.length + ' 个匹配项(含子目录)</p>' +
'<table class="results"><thead><tr><th>Name</th><th>Size</th><th>Last modified</th><th> </th></tr></thead>' +
'<tbody>' + rows + '</tbody></table>';
}
function escapeHtml(str) {
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
input.addEventListener('input', function () {
var q = input.value.trim().toLowerCase();
if (!q) {
list.style.display = '';
if (resultsEl) resultsEl.hidden = true;
return;
}
list.style.display = 'none';
var el = ensureResults();
el.hidden = false;
if (!indexPromise) el.innerHTML = '<p class="search-status">正在索引子目录…</p>';
buildIndex().then(function (entries) {
var matched = entries.filter(function (e) {
return e.name.toLowerCase().indexOf(q) !== -1;
});
render(matched, q);
});
});
}
// 移除 ngx-fancyindex 在表格前输出的裸文本路径(如 /bitwarden/chrome-plugins/
// CSS 无法隐藏没有标签包裹的文本节点,故用 JS 在 DOM 就绪后删除
function cleanupStrayText() {
var roots = [document.querySelector('.wrapper'), document.body];
roots.forEach(function (root) {
if (!root) return;
var nodes = root.childNodes;
for (var i = nodes.length - 1; i >= 0; i--) {
var n = nodes[i];
if (n.nodeType === 3 && n.textContent.trim() !== '') {
n.parentNode.removeChild(n);
}
}
});
}
function initUpBtn() {
var btn = document.getElementById('up-btn');
if (!btn) return;
var parent;
if (location.protocol === 'file:') {
parent = '#'; // 本地预览无真实目录层级,仅展示按钮
} else {
// 当前路径去掉末尾斜杠后求父目录
var path = location.pathname.replace(/\/+$/, '');
var idx = path.lastIndexOf('/');
parent = (idx <= 0) ? '/' : path.slice(0, idx + 1); // 如 /a/b/ -> /a/
}
btn.setAttribute('href', parent);
}
function onReady() {
initSearch();
initUpBtn();
cleanupStrayText();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', onReady);
} else {
onReady();
}
})();
</script>