add html viewer
This commit is contained in:
199
Report_Viewer.html
Normal file
199
Report_Viewer.html
Normal file
@@ -0,0 +1,199 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Rclone 校验报告在线查看器</title>
|
||||
<style>
|
||||
:root {
|
||||
--success: #28a745; --danger: #dc3545; --warning: #ffc107; --info: #17a2b8;
|
||||
--bg: #f0f2f5; --card-bg: #ffffff; --text: #2d3436;
|
||||
}
|
||||
body { font-family: 'Segoe UI', system-ui, sans-serif; background: var(--bg); color: var(--text); padding: 40px 20px; margin: 0; }
|
||||
.container { max-width: 1100px; margin: auto; }
|
||||
|
||||
/* 拖拽区 UI */
|
||||
#drop-zone {
|
||||
border: 3px dashed #bdc3c7; border-radius: 15px; padding: 15px 40px; text-align: center;
|
||||
background: white; margin-bottom: 30px; transition: all 0.2s ease-in-out; cursor: pointer;
|
||||
}
|
||||
#drop-zone.hover { border-color: #0984e3; background: #e1f5fe; transform: scale(1.01); }
|
||||
#drop-zone p { font-size: 1.3em; color: #7f8c8d; pointer-events: none; }
|
||||
|
||||
/* 统计卡片 */
|
||||
#report-content { display: none; animation: fadeIn 0.5s; }
|
||||
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
|
||||
|
||||
.stat-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 15px; margin-bottom: 30px; }
|
||||
.stat-card { background: var(--card-bg); padding: 20px; border-radius: 12px; box-shadow: 0 4px 6px rgba(0,0,0,0.05); text-align: center; border-bottom: 4px solid #ddd; }
|
||||
.stat-value { display: block; font-size: 28px; font-weight: 800; margin-bottom: 5px; }
|
||||
.stat-label { font-size: 12px; color: #636e72; text-transform: uppercase; }
|
||||
|
||||
/* 树状结构 UI */
|
||||
.report-card { background: var(--card-bg); padding: 30px; border-radius: 15px; box-shadow: 0 10px 30px rgba(0,0,0,0.08); }
|
||||
.actions { display: flex; gap: 10px; margin-bottom: 20px; }
|
||||
.btn { padding: 10px 20px; border: none; border-radius: 8px; font-weight: 600; cursor: pointer; background: #dfe6e9; transition: 0.2s; }
|
||||
.btn:hover { filter: brightness(0.9); }
|
||||
.btn-danger { background: var(--danger); color: white; }
|
||||
.btn-primary { background: #0984e3; color: white; }
|
||||
|
||||
ul { list-style: none; padding-left: 25px; border-left: 1px dashed #dfe6e9; }
|
||||
li { margin: 8px 0; }
|
||||
summary { cursor: pointer; padding: 6px; border-radius: 6px; font-weight: 600; display: flex; align-items: center; }
|
||||
summary:hover { background: #f1f2f6; color: #0984e3; }
|
||||
.badge { display: inline-block; min-width: 22px; padding: 2px 6px; text-align: center; border-radius: 4px; color: white; font-size: 11px; margin-right: 10px; font-weight: bold; }
|
||||
.error-dot { color: var(--danger); margin-left: 8px; font-size: 14px; animation: blink 2s infinite; }
|
||||
@keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }
|
||||
|
||||
/* 过滤逻辑 */
|
||||
.filter-mode-errors .is-match, .filter-mode-errors .is-pure-match { display: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div id="drop-zone">
|
||||
<p>📂 <b>拖入</b>生成的 <b>report.txt</b> 文件到这里</p>
|
||||
<span style="color: #95a5a6;">或者 点击此处手动选择</span>
|
||||
<input type="file" id="fileInput" style="display: none;" accept=".txt">
|
||||
</div>
|
||||
|
||||
<div id="report-content">
|
||||
<div class="stat-grid">
|
||||
<div class="stat-card" style="border-color: var(--success)"><span id="s-match" class="stat-value" style="color: var(--success)">0</span><span class="stat-label">一致 (Match)</span></div>
|
||||
<div class="stat-card" style="border-color: var(--danger)"><span id="s-mismatch" class="stat-value" style="color: var(--danger)">0</span><span class="stat-label">损坏 (Mismatch)</span></div>
|
||||
<div class="stat-card" style="border-color: var(--warning)"><span id="s-missing" class="stat-value" style="color: var(--warning)">0</span><span class="stat-label">缺失 (Missing)</span></div>
|
||||
<div class="stat-card" style="border-color: var(--info)"><span id="s-extra" class="stat-value" style="color: var(--info)">0</span><span class="stat-label">多余 (Extra)</span></div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button id="toggleFilterBtn" class="btn btn-primary" onclick="toggleFilter()">显示模式:完整清单</button>
|
||||
<button class="btn" onclick="toggleAll(true)">全部展开</button>
|
||||
<button class="btn" onclick="toggleAll(false)">全部收起</button>
|
||||
</div>
|
||||
|
||||
<div class="report-card">
|
||||
<div id="treeContainer"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const fileInput = document.getElementById('fileInput');
|
||||
const treeContainer = document.getElementById('treeContainer');
|
||||
|
||||
// 核心修复:阻止浏览器默认行为
|
||||
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
||||
dropZone.addEventListener(eventName, preventDefaults, false);
|
||||
document.body.addEventListener(eventName, preventDefaults, false);
|
||||
});
|
||||
|
||||
function preventDefaults(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
// 视觉反馈
|
||||
['dragenter', 'dragover'].forEach(eventName => {
|
||||
dropZone.addEventListener(eventName, () => dropZone.classList.add('hover'), false);
|
||||
});
|
||||
['dragleave', 'drop'].forEach(eventName => {
|
||||
dropZone.addEventListener(eventName, () => dropZone.classList.remove('hover'), false);
|
||||
});
|
||||
|
||||
// 真正的拖拽处理
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
const dt = e.dataTransfer;
|
||||
const files = dt.files;
|
||||
handleFile(files[0]);
|
||||
}, false);
|
||||
|
||||
dropZone.onclick = () => fileInput.click();
|
||||
fileInput.onchange = (e) => handleFile(e.target.files[0]);
|
||||
|
||||
function handleFile(file) {
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => processData(e.target.result);
|
||||
reader.readAsText(file, 'UTF-8');
|
||||
}
|
||||
|
||||
function processData(text) {
|
||||
const lines = text.split(/\r?\n/);
|
||||
const tree = {};
|
||||
const stats = {'=':0, '*':0, '-':0, '+':0};
|
||||
const folderErrors = new Set();
|
||||
|
||||
lines.forEach(line => {
|
||||
if (line.length < 3) return;
|
||||
const symbol = line[0];
|
||||
const path = line.substring(2).replace(/\\/g, '/');
|
||||
const parts = path.split('/');
|
||||
if (stats[symbol] !== undefined) stats[symbol]++;
|
||||
|
||||
let curr = tree;
|
||||
let pathAcc = "";
|
||||
parts.forEach((part, i) => {
|
||||
pathAcc = pathAcc ? `${pathAcc}/${part}` : part;
|
||||
if (i === parts.length - 1) {
|
||||
curr[part] = symbol;
|
||||
} else {
|
||||
if (symbol !== '=') folderErrors.add(pathAcc);
|
||||
if (!curr[part]) curr[part] = {};
|
||||
curr = curr[part];
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('s-match').innerText = stats['='];
|
||||
document.getElementById('s-mismatch').innerText = stats['*'];
|
||||
document.getElementById('s-missing').innerText = stats['-'];
|
||||
document.getElementById('s-extra').innerText = stats['+'];
|
||||
document.getElementById('report-content').style.display = 'block';
|
||||
|
||||
treeContainer.innerHTML = buildHtml(tree, "", folderErrors);
|
||||
// 提示用户加载成功
|
||||
dropZone.querySelector('p').innerHTML = "✅ 已加载报告:" + stats['='] + " 个文件已校验";
|
||||
}
|
||||
|
||||
function buildHtml(node, currentPath, folderErrors) {
|
||||
let html = "<ul>";
|
||||
const keys = Object.keys(node).sort((a, b) => {
|
||||
const aIsFile = typeof node[a] === 'string';
|
||||
const bIsFile = typeof node[b] === 'string';
|
||||
return aIsFile === bIsFile ? a.localeCompare(b) : aIsFile - bIsFile;
|
||||
});
|
||||
|
||||
keys.forEach(name => {
|
||||
const val = node[name];
|
||||
const thisPath = currentPath ? `${currentPath}/${name}` : name;
|
||||
if (typeof val === 'object') {
|
||||
const hasErr = folderErrors.has(thisPath);
|
||||
const dot = hasErr ? '<span class="error-dot">●</span>' : '';
|
||||
html += `<li class="folder ${hasErr?'has-error-branch':'is-pure-match'}">
|
||||
<details><summary>📁 ${name}/ ${dot}</summary>${buildHtml(val, thisPath, folderErrors)}</details>
|
||||
</li>`;
|
||||
} else {
|
||||
const colors = {'=':'#28a745', '*':'#dc3545', '-':'#ffc107', '+':'#17a2b8'};
|
||||
html += `<li class="file ${val==='='?'is-match':'is-error'}">
|
||||
<span class="badge" style="background:${colors[val]}">${val}</span>${name}
|
||||
</li>`;
|
||||
}
|
||||
});
|
||||
return html + "</ul>";
|
||||
}
|
||||
|
||||
function toggleFilter() {
|
||||
const container = document.getElementById('treeContainer');
|
||||
const btn = document.getElementById('toggleFilterBtn');
|
||||
const active = container.classList.toggle('filter-mode-errors');
|
||||
btn.innerText = active ? "显示模式:只看异常" : "显示模式:完整清单";
|
||||
btn.classList.toggle('btn-danger', active);
|
||||
if(active) document.querySelectorAll('.has-error-branch > details').forEach(d => d.open = true);
|
||||
}
|
||||
|
||||
function toggleAll(open) {
|
||||
document.querySelectorAll('details').forEach(d => d.open = open);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user