Initial commit: Nginx Autoindex文件列表系统
This commit is contained in:
369
footer.html
Normal file
369
footer.html
Normal file
@@ -0,0 +1,369 @@
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 添加搜索框
|
||||
const searchBox = document.createElement('div');
|
||||
searchBox.innerHTML = `
|
||||
<input type="text" id="search" placeholder="搜索文件..."
|
||||
style="width: 100%; padding: 0.6rem; margin-bottom: 1rem;
|
||||
border: 2px solid #e2e8f0; border-radius: 6px;
|
||||
font-size: 0.9rem; background: white; color: #1e293b;">
|
||||
`;
|
||||
document.body.insertBefore(searchBox, document.querySelector('pre'));
|
||||
|
||||
const pre = document.querySelector('pre');
|
||||
if (!pre) return;
|
||||
|
||||
// 添加表格头部
|
||||
const headerDiv = document.createElement('div');
|
||||
headerDiv.innerHTML = `
|
||||
<div style="display: flex; background: #f8fafc; padding: 0.3rem 1rem;
|
||||
border-bottom: 1px solid #e2e8f0; font-weight: 600; font-size: 0.8rem; color: #64748b;">
|
||||
<div style="flex: 1; line-height: 1.5;">📋 文件名</div>
|
||||
<div style="width: 100px; text-align: right; margin-right: 1rem; line-height: 1.5;">大小</div>
|
||||
<div style="width: 140px; text-align: right; margin-right: 1rem; line-height: 1.5;">修改时间</div>
|
||||
<div style="width: 50px; text-align: center; line-height: 1.5;">操作</div>
|
||||
</div>
|
||||
`;
|
||||
pre.parentNode.insertBefore(headerDiv, pre);
|
||||
|
||||
// 获取原始HTML并解析
|
||||
const originalHTML = pre.innerHTML;
|
||||
|
||||
// 使用正则表达式匹配链接和后续的日期大小信息
|
||||
const regex = /<a href="([^"]*)"[^>]*>([^<]*)<\/a>\s*([^<]*?)(?=<a|$)/g;
|
||||
|
||||
let newHTML = '';
|
||||
let match;
|
||||
|
||||
while ((match = regex.exec(originalHTML)) !== null) {
|
||||
const href = match[1];
|
||||
const filename = match[2];
|
||||
const afterText = match[3].trim();
|
||||
|
||||
// 解析日期和大小 - nginx格式是: DD-MMM-YYYY HH:MM size
|
||||
const parts = afterText.split(/\s+/).filter(p => p && p !== '-');
|
||||
let date = '-', size = '-';
|
||||
|
||||
if (parts.length >= 2) {
|
||||
date = parts[0] + ' ' + parts[1]; // 日期和时间
|
||||
if (parts.length >= 3 && parts[2] !== '-') {
|
||||
size = parts[2]; // 文件大小
|
||||
}
|
||||
}
|
||||
|
||||
const icon = getFileIcon(filename);
|
||||
|
||||
// 根据是否为文件夹设置不同颜色
|
||||
const isFolder = filename.endsWith('/');
|
||||
const linkColor = isFolder ? '#3b82f6' : '#1e293b'; // 文件夹蓝色,文件黑色
|
||||
|
||||
newHTML += `
|
||||
<div style="display: flex; align-items: center; padding: 0.15rem 1rem;
|
||||
background: white; transition: background-color 0.2s;"
|
||||
onmouseenter="this.style.backgroundColor='#f8fafc'"
|
||||
onmouseleave="this.style.backgroundColor='white'">
|
||||
<a href="${href}" style="flex: 1; display: flex; text-decoration: none;
|
||||
color: ${linkColor}; overflow: hidden;"
|
||||
data-filename="${filename.toLowerCase()}">
|
||||
<span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 0.9rem; line-height: 1.2;">${filename}</span>
|
||||
</a>
|
||||
<div style="width: 100px; text-align: right; color: #64748b; font-size: 0.85rem; line-height: 1.2; margin-right: 1rem;">${formatSize(size)}</div>
|
||||
<div style="width: 140px; text-align: right; color: #64748b; font-size: 0.85rem; line-height: 1.2; margin-right: 1rem;">${formatDate(date)}</div>
|
||||
<div style="width: 50px; text-align: center;"><button onclick="copyFileUrl(event, '${href}')" title="复制链接" style="padding: 0.15rem 0.4rem; border: 1px solid #d1d5db; border-radius: 3px; background: white; color: #374151; cursor: pointer; font-size: 0.75rem; line-height: 1.3; font-weight: 500; transition: all 0.2s;" onmouseenter="this.style.backgroundColor='#eff6ff'; this.style.borderColor='#3b82f6'; this.style.color='#2563eb';" onmouseleave="this.style.backgroundColor='white'; this.style.borderColor='#d1d5db'; this.style.color='#374151';">复制</button></div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 彻底清理pre元素,移除所有子节点
|
||||
while (pre.firstChild) {
|
||||
pre.removeChild(pre.firstChild);
|
||||
}
|
||||
|
||||
// 设置pre样式,容器字体为0消除空白,但子元素会有自己的字体大小
|
||||
pre.style.cssText = `
|
||||
background: white;
|
||||
border-radius: 6px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
overflow: hidden;
|
||||
display: block;
|
||||
font-size: 0;
|
||||
line-height: 0;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
|
||||
`;
|
||||
|
||||
// 直接设置innerHTML,让浏览器自动解析
|
||||
pre.innerHTML = newHTML;
|
||||
|
||||
// 搜索功能
|
||||
// 存储所有文件(包括子目录)
|
||||
let allFiles = [];
|
||||
let isSearching = false;
|
||||
|
||||
// 递归获取所有子目录的文件
|
||||
async function fetchAllFiles(path = '', depth = 0, maxDepth = 10) {
|
||||
if (depth > maxDepth) return [];
|
||||
|
||||
try {
|
||||
const response = await fetch(path || './');
|
||||
const html = await response.text();
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, 'text/html');
|
||||
const links = doc.querySelectorAll('pre a');
|
||||
const files = [];
|
||||
|
||||
for (const link of links) {
|
||||
const href = link.getAttribute('href');
|
||||
const filename = link.textContent;
|
||||
|
||||
if (href === '../') continue; // 跳过父目录
|
||||
|
||||
const fullPath = path ? path + href : href;
|
||||
const afterText = link.nextSibling ? link.nextSibling.textContent.trim() : '';
|
||||
const parts = afterText.split(/\s+/).filter(p => p && p !== '-');
|
||||
|
||||
let date = '-', size = '-';
|
||||
if (parts.length >= 2) {
|
||||
date = parts[0] + ' ' + parts[1];
|
||||
if (parts.length >= 3 && parts[2] !== '-') {
|
||||
size = parts[2];
|
||||
}
|
||||
}
|
||||
|
||||
files.push({
|
||||
href: fullPath,
|
||||
filename: filename,
|
||||
fullPath: fullPath,
|
||||
date: date,
|
||||
size: size,
|
||||
isFolder: filename.endsWith('/')
|
||||
});
|
||||
|
||||
// 如果是文件夹,递归获取
|
||||
if (filename.endsWith('/')) {
|
||||
const subFiles = await fetchAllFiles(fullPath, depth + 1, maxDepth);
|
||||
files.push(...subFiles);
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
} catch (e) {
|
||||
console.error('获取目录失败:', path, e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索功能
|
||||
const searchInput = document.getElementById('search');
|
||||
let searchTimeout;
|
||||
|
||||
searchInput.addEventListener('input', async function(e) {
|
||||
const term = e.target.value.toLowerCase().trim();
|
||||
|
||||
// 清除之前的延时
|
||||
clearTimeout(searchTimeout);
|
||||
|
||||
if (!term) {
|
||||
// 如果搜索框为空,显示当前目录文件
|
||||
const rows = pre.querySelectorAll('div[onmouseenter]');
|
||||
rows.forEach(row => row.style.display = 'flex');
|
||||
return;
|
||||
}
|
||||
|
||||
// 延时300ms再搜索,避免频繁请求
|
||||
searchTimeout = setTimeout(async () => {
|
||||
// 如果还没有加载所有文件,先加载
|
||||
if (allFiles.length === 0 && !isSearching) {
|
||||
isSearching = true;
|
||||
searchInput.placeholder = '正在索引所有文件...';
|
||||
searchInput.disabled = true;
|
||||
|
||||
allFiles = await fetchAllFiles('', 0, 5); // 最多5层深度
|
||||
|
||||
searchInput.disabled = false;
|
||||
searchInput.placeholder = '搜索所有文件...';
|
||||
isSearching = false;
|
||||
}
|
||||
|
||||
// 过滤匹配的文件
|
||||
const matchedFiles = allFiles.filter(f =>
|
||||
f.filename.toLowerCase().includes(term) ||
|
||||
f.fullPath.toLowerCase().includes(term)
|
||||
);
|
||||
|
||||
// 隐藏当前所有行
|
||||
const rows = pre.querySelectorAll('div[onmouseenter]');
|
||||
rows.forEach(row => row.style.display = 'none');
|
||||
|
||||
// 如果有匹配结果,显示
|
||||
if (matchedFiles.length > 0) {
|
||||
let searchHTML = '';
|
||||
matchedFiles.forEach(file => {
|
||||
const linkColor = file.isFolder ? '#3b82f6' : '#1e293b';
|
||||
searchHTML += `
|
||||
<div style="display: flex; align-items: center; padding: 0.15rem 1rem;
|
||||
background: white; transition: background-color 0.2s;"
|
||||
onmouseenter="this.style.backgroundColor='#f8fafc'"
|
||||
onmouseleave="this.style.backgroundColor='white'">
|
||||
<a href="${file.href}" style="flex: 1; display: flex; text-decoration: none;
|
||||
color: ${linkColor}; overflow: hidden;"
|
||||
data-filename="${file.filename.toLowerCase()}">
|
||||
<span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 0.9rem; line-height: 1.2;" title="${file.fullPath}">${file.fullPath}</span>
|
||||
</a>
|
||||
<div style="width: 100px; text-align: right; color: #64748b; font-size: 0.85rem; line-height: 1.2; margin-right: 1rem;">${formatSize(file.size)}</div>
|
||||
<div style="width: 140px; text-align: right; color: #64748b; font-size: 0.85rem; line-height: 1.2; margin-right: 1rem;">${formatDate(file.date)}</div>
|
||||
<div style="width: 50px; text-align: center;"><button onclick="copyFileUrl(event, '${file.href}')" title="复制链接" style="padding: 0.15rem 0.4rem; border: 1px solid #d1d5db; border-radius: 3px; background: white; color: #374151; cursor: pointer; font-size: 0.75rem; line-height: 1.3; font-weight: 500; transition: all 0.2s;" onmouseenter="this.style.backgroundColor='#eff6ff'; this.style.borderColor='#3b82f6'; this.style.color='#2563eb';" onmouseleave="this.style.backgroundColor='white'; this.style.borderColor='#d1d5db'; this.style.color='#374151';">复制</button></div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
// 添加搜索结果到页面
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = searchHTML;
|
||||
while (tempDiv.firstChild) {
|
||||
pre.appendChild(tempDiv.firstChild);
|
||||
}
|
||||
}
|
||||
}, 300);
|
||||
});
|
||||
});
|
||||
|
||||
function getFileIcon(filename) {
|
||||
if (filename.endsWith('/')) return '📁';
|
||||
const ext = filename.split('.').pop().toLowerCase();
|
||||
const icons = {
|
||||
'pdf': '📕', 'doc': '📘', 'docx': '📘',
|
||||
'jpg': '🖼️', 'jpeg': '🖼️', 'png': '🖼️', 'gif': '🖼️',
|
||||
'mp4': '🎬', 'avi': '🎬', 'mov': '🎬', 'mkv': '🎬',
|
||||
'mp3': '🎵', 'wav': '🎵', 'ogg': '🎵',
|
||||
'zip': '📦', 'rar': '📦', '7z': '📦', 'tar': '📦',
|
||||
'exe': '⚙️', 'msi': '⚙️'
|
||||
};
|
||||
return icons[ext] || '📄';
|
||||
}
|
||||
|
||||
function formatSize(size) {
|
||||
if (!size || size === '-') return '-';
|
||||
if (size.match(/[KMGT]/i)) return size;
|
||||
|
||||
const num = parseFloat(size);
|
||||
if (isNaN(num)) return size;
|
||||
|
||||
if (num < 1024) return num + 'B';
|
||||
if (num < 1048576) return (num/1024).toFixed(1) + 'K';
|
||||
if (num < 1073741824) return (num/1048576).toFixed(1) + 'M';
|
||||
return (num/1073741824).toFixed(1) + 'G';
|
||||
}
|
||||
|
||||
function formatDate(dateStr) {
|
||||
if (!dateStr || dateStr === '-') return '-';
|
||||
return dateStr.replace(/(\d{2})-(\w{3})-(\d{4}) (\d{2}):(\d{2})/, '$3-$2-$1 $4:$5');
|
||||
}
|
||||
|
||||
// 复制文件URL到剪贴板
|
||||
function copyFileUrl(event, href) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
// 构建完整URL - 处理路径拼接
|
||||
let basePath = window.location.pathname;
|
||||
// 如果路径以/结尾,去掉末尾的/
|
||||
if (basePath.endsWith('/')) {
|
||||
basePath = basePath.slice(0, -1);
|
||||
}
|
||||
// 如果href不以/开头,添加/
|
||||
if (!href.startsWith('/')) {
|
||||
href = '/' + href;
|
||||
}
|
||||
|
||||
// 解码URL中的中文字符,使其更易读
|
||||
let decodedHref = href;
|
||||
try {
|
||||
decodedHref = decodeURIComponent(href);
|
||||
} catch (e) {
|
||||
// 如果解码失败,使用原始href
|
||||
}
|
||||
|
||||
const fullUrl = window.location.origin + basePath + decodedHref;
|
||||
|
||||
console.log('复制URL:', fullUrl); // 调试信息
|
||||
|
||||
// 复制到剪贴板
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(fullUrl).then(() => {
|
||||
// 显示复制成功提示
|
||||
const button = event.target.closest('button');
|
||||
if (button) {
|
||||
const originalText = button.textContent;
|
||||
button.textContent = '已复制';
|
||||
button.style.color = '#10b981';
|
||||
button.style.borderColor = '#10b981';
|
||||
button.style.backgroundColor = '#f0fdf4';
|
||||
|
||||
setTimeout(() => {
|
||||
button.textContent = originalText;
|
||||
button.style.color = '#374151';
|
||||
button.style.borderColor = '#d1d5db';
|
||||
button.style.backgroundColor = 'white';
|
||||
}, 1500);
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error('复制失败:', err);
|
||||
// 降级方案:使用传统方法
|
||||
const button = event.target.closest('button');
|
||||
fallbackCopyTextToClipboard(fullUrl, button);
|
||||
});
|
||||
} else {
|
||||
// 不支持clipboard API,使用降级方案
|
||||
const button = event.target.closest('button');
|
||||
fallbackCopyTextToClipboard(fullUrl, button);
|
||||
}
|
||||
}
|
||||
|
||||
// 降级复制方案
|
||||
function fallbackCopyTextToClipboard(text, button) {
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = text;
|
||||
textArea.style.position = 'fixed';
|
||||
textArea.style.top = '0';
|
||||
textArea.style.left = '0';
|
||||
textArea.style.width = '2em';
|
||||
textArea.style.height = '2em';
|
||||
textArea.style.padding = '0';
|
||||
textArea.style.border = 'none';
|
||||
textArea.style.outline = 'none';
|
||||
textArea.style.boxShadow = 'none';
|
||||
textArea.style.background = 'transparent';
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
|
||||
try {
|
||||
const successful = document.execCommand('copy');
|
||||
if (successful && button) {
|
||||
// 显示复制成功,不使用alert
|
||||
const originalText = button.textContent;
|
||||
button.textContent = '已复制';
|
||||
button.style.color = '#10b981';
|
||||
button.style.borderColor = '#10b981';
|
||||
button.style.backgroundColor = '#f0fdf4';
|
||||
|
||||
setTimeout(() => {
|
||||
button.textContent = originalText;
|
||||
button.style.color = '#374151';
|
||||
button.style.borderColor = '#d1d5db';
|
||||
button.style.backgroundColor = 'white';
|
||||
}, 1500);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('复制失败:', err);
|
||||
}
|
||||
|
||||
document.body.removeChild(textArea);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div style="margin-top: 2rem; text-align: center; color: #64748b; font-size: 0.875rem;">
|
||||
<p>由 nginx autoindex 提供支持</p>
|
||||
</div>
|
||||
Reference in New Issue
Block a user