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
+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>