init
This commit is contained in:
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
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>
|
||||
Reference in New Issue
Block a user