upgrade html viewer

This commit is contained in:
Julian Freeman
2026-02-26 11:58:35 -04:00
parent 9d96680a75
commit 053ee6686f
2 changed files with 114 additions and 68 deletions

View File

@@ -2,72 +2,79 @@
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Rclone 校验报告在线查看器</title> <title>Rclone 报告查看器</title>
<script src="languages.js"></script>
<style> <style>
:root { :root {
--success: #28a745; --danger: #dc3545; --warning: #ffc107; --info: #17a2b8; --success: #28a745; --danger: #dc3545; --warning: #ffc107; --info: #17a2b8;
--bg: #f0f2f5; --card-bg: #ffffff; --text: #2d3436; --bg: #f0f2f5; --card-bg: #ffffff; --text: #2d3436; --primary: #0984e3;
} }
body { font-family: 'Segoe UI', system-ui, sans-serif; background: var(--bg); color: var(--text); padding: 40px 20px; margin: 0; } body { font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; background: var(--bg); color: var(--text); padding: 20px; margin: 0; }
.container { max-width: 1100px; margin: auto; } .container { max-width: 1100px; margin: auto; padding-top: 20px; }
/* 顶部语言选择框 */
.lang-switch { position: absolute; top: 20px; right: 20px; z-index: 100; display: flex; align-items: center; gap: 8px; }
.select-lang { padding: 6px 12px; border-radius: 20px; border: 1px solid #ddd; outline: none; cursor: pointer; font-size: 13px; }
/* 拖拽区 UI */
#drop-zone { #drop-zone {
border: 3px dashed #bdc3c7; border-radius: 15px; padding: 15px 40px; text-align: center; 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; background: white; margin-bottom: 30px; transition: all 0.2s; cursor: pointer;
} }
#drop-zone.hover { border-color: #0984e3; background: #e1f5fe; transform: scale(1.01); } #drop-zone.hover { border-color: var(--primary); background: #e1f5fe; }
#drop-zone p { font-size: 1.3em; color: #7f8c8d; pointer-events: none; } #drop-zone p { font-size: 1.3em; color: #7f8c8d; pointer-events: none; margin-bottom: 10px; }
/* 统计卡片 */ #report-content { display: none; animation: fadeIn 0.4s ease-out; }
#report-content { display: none; animation: fadeIn 0.5s; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } @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-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-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-value { display: block; font-size: 28px; font-weight: 800; margin-bottom: 5px; }
.stat-label { font-size: 12px; color: #636e72; text-transform: uppercase; } .stat-label { font-size: 11px; color: #636e72; text-transform: uppercase; font-weight: 600; }
/* 树状结构 UI */
.report-card { background: var(--card-bg); padding: 30px; border-radius: 15px; box-shadow: 0 10px 30px rgba(0,0,0,0.08); } .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; } .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 { padding: 10px 20px; border: none; border-radius: 8px; font-weight: 600; cursor: pointer; background: #dfe6e9; transition: 0.2s; font-size: 14px; }
.btn:hover { filter: brightness(0.9); } .btn:hover { filter: brightness(0.95); }
.btn-danger { background: var(--danger); color: white; } .btn-danger { background: var(--danger); color: white; }
.btn-primary { background: #0984e3; color: white; } .btn-primary { background: var(--primary); color: white; }
ul { list-style: none; padding-left: 25px; border-left: 1px dashed #dfe6e9; } ul { list-style: none; padding-left: 25px; border-left: 1px dashed #dfe6e9; }
li { margin: 8px 0; } li { margin: 8px 0; }
summary { cursor: pointer; padding: 6px; border-radius: 6px; font-weight: 600; display: flex; align-items: center; } summary { cursor: pointer; padding: 6px; border-radius: 6px; font-weight: 600; display: flex; align-items: center; }
summary:hover { background: #f1f2f6; color: #0984e3; } summary:hover { background: #f1f2f6; color: var(--primary); }
.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; } .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; } .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; } } @keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }
/* 过滤逻辑 */
.filter-mode-errors .is-match, .filter-mode-errors .is-pure-match { display: none; } .filter-mode-errors .is-match, .filter-mode-errors .is-pure-match { display: none; }
</style> </style>
</head> </head>
<body> <body>
<div class="lang-switch">
<select class="select-lang" id="langSelect" onchange="changeLang(this.value)">
</select>
</div>
<div class="container"> <div class="container">
<div id="drop-zone"> <div id="drop-zone">
<p>📂 <b>拖入</b>生成的 <b>report.txt</b> 文件到这里</p> <p id="t-drop-hint"></p>
<span style="color: #95a5a6;">或者 点击此处手动选择</span> <span id="t-click-hint" style="color: #95a5a6;"></span>
<input type="file" id="fileInput" style="display: none;" accept=".txt"> <input type="file" id="fileInput" style="display: none;" accept=".txt">
</div> </div>
<div id="report-content"> <div id="report-content">
<div class="stat-grid"> <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(--success)"><span id="s-match" class="stat-value" style="color: var(--success)">0</span><span class="stat-label" id="t-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(--danger)"><span id="s-mismatch" class="stat-value" style="color: var(--danger)">0</span><span class="stat-label" id="t-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(--warning)"><span id="s-missing" class="stat-value" style="color: var(--warning)">0</span><span class="stat-label" id="t-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 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" id="t-label-extra"></span></div>
</div> </div>
<div class="actions"> <div class="actions">
<button id="toggleFilterBtn" class="btn btn-primary" onclick="toggleFilter()">显示模式:完整清单</button> <button id="toggleFilterBtn" class="btn btn-primary" onclick="toggleFilter()"></button>
<button class="btn" onclick="toggleAll(true)">全部展开</button> <button class="btn" id="t-btn-expand" onclick="toggleAll(true)"></button>
<button class="btn" onclick="toggleAll(false)">全部收起</button> <button class="btn" id="t-btn-collapse" onclick="toggleAll(false)"></button>
</div> </div>
<div class="report-card"> <div class="report-card">
@@ -77,35 +84,62 @@
</div> </div>
<script> <script>
const dropZone = document.getElementById('drop-zone'); // 内置默认语言
const fileInput = document.getElementById('fileInput'); const defaultI18n = {
const treeContainer = document.getElementById('treeContainer'); zh: { name: "简体中文", dropHint: "📂 <b>拖入</b>生成的文件到这里", clickHint: "或者 点击此处手动选择", labelMatch: "一致 (Match)", labelMismatch: "损坏 (Mismatch)", labelMissing: "缺失 (Missing)", labelExtra: "多余 (Extra)", btnExpand: "全部展开", btnCollapse: "全部收起", modeFull: "显示模式:完整清单", modeError: "显示模式:只看异常", loadSuccess: "✅ 已加载:", files: " 个文件" },
en: { name: "English", dropHint: "📂 <b>Drag</b> your file here", clickHint: "or click to select manually", labelMatch: "Match", labelMismatch: "Mismatch", labelMissing: "Missing", labelExtra: "Extra", btnExpand: "Expand All", btnCollapse: "Collapse All", modeFull: "View: All Files", modeError: "View: Errors Only", loadSuccess: "✅ Loaded: ", files: " files" }
};
// 核心修复:阻止浏览器默认行为 // 合并外部语言
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { const allI18n = Object.assign({}, defaultI18n, window.externalI18n || {});
dropZone.addEventListener(eventName, preventDefaults, false); let currentLang = 'zh';
document.body.addEventListener(eventName, preventDefaults, false); let globalData = "";
let errorMode = false;
// 初始化语言选择器
const langSelect = document.getElementById('langSelect');
Object.keys(allI18n).forEach(key => {
const opt = document.createElement('option');
opt.value = key;
opt.innerText = allI18n[key].name;
langSelect.appendChild(opt);
}); });
function preventDefaults(e) { function changeLang(lang) {
e.preventDefault(); currentLang = lang;
e.stopPropagation(); updateUI();
if (globalData) processData(globalData);
} }
// 视觉反馈 function updateUI() {
['dragenter', 'dragover'].forEach(eventName => { const t = allI18n[currentLang];
dropZone.addEventListener(eventName, () => dropZone.classList.add('hover'), false); document.getElementById('t-drop-hint').innerHTML = t.dropHint;
}); document.getElementById('t-click-hint').innerText = t.clickHint;
['dragleave', 'drop'].forEach(eventName => { document.getElementById('t-label-match').innerText = t.labelMatch;
dropZone.addEventListener(eventName, () => dropZone.classList.remove('hover'), false); document.getElementById('t-label-mismatch').innerText = t.labelMismatch;
document.getElementById('t-label-missing').innerText = t.labelMissing;
document.getElementById('t-label-extra').innerText = t.labelExtra;
document.getElementById('t-btn-expand').innerText = t.btnExpand;
document.getElementById('t-btn-collapse').innerText = t.btnCollapse;
const filterBtn = document.getElementById('toggleFilterBtn');
filterBtn.innerText = errorMode ? t.modeError : t.modeFull;
filterBtn.className = errorMode ? 'btn btn-danger' : 'btn btn-primary';
}
// --- Rclone 处理逻辑 ---
const dropZone = document.getElementById('drop-zone');
const fileInput = document.getElementById('fileInput');
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(e => {
dropZone.addEventListener(e, (ev) => { ev.preventDefault(); ev.stopPropagation(); });
}); });
// 真正的拖拽处理 dropZone.addEventListener('dragover', () => dropZone.classList.add('hover'));
dropZone.addEventListener('dragleave', () => dropZone.classList.remove('hover'));
dropZone.addEventListener('drop', (e) => { dropZone.addEventListener('drop', (e) => {
const dt = e.dataTransfer; dropZone.classList.remove('hover');
const files = dt.files; handleFile(e.dataTransfer.files[0]);
handleFile(files[0]); });
}, false);
dropZone.onclick = () => fileInput.click(); dropZone.onclick = () => fileInput.click();
fileInput.onchange = (e) => handleFile(e.target.files[0]); fileInput.onchange = (e) => handleFile(e.target.files[0]);
@@ -113,7 +147,7 @@
function handleFile(file) { function handleFile(file) {
if (!file) return; if (!file) return;
const reader = new FileReader(); const reader = new FileReader();
reader.onload = (e) => processData(e.target.result); reader.onload = (e) => { globalData = e.target.result; processData(globalData); };
reader.readAsText(file, 'UTF-8'); reader.readAsText(file, 'UTF-8');
} }
@@ -129,14 +163,12 @@
const path = line.substring(2).replace(/\\/g, '/'); const path = line.substring(2).replace(/\\/g, '/');
const parts = path.split('/'); const parts = path.split('/');
if (stats[symbol] !== undefined) stats[symbol]++; if (stats[symbol] !== undefined) stats[symbol]++;
let curr = tree; let curr = tree;
let pathAcc = ""; let pathAcc = "";
parts.forEach((part, i) => { parts.forEach((part, i) => {
pathAcc = pathAcc ? `${pathAcc}/${part}` : part; pathAcc = pathAcc ? `${pathAcc}/${part}` : part;
if (i === parts.length - 1) { if (i === parts.length - 1) { curr[part] = symbol; }
curr[part] = symbol; else {
} else {
if (symbol !== '=') folderErrors.add(pathAcc); if (symbol !== '=') folderErrors.add(pathAcc);
if (!curr[part]) curr[part] = {}; if (!curr[part]) curr[part] = {};
curr = curr[part]; curr = curr[part];
@@ -149,10 +181,9 @@
document.getElementById('s-missing').innerText = stats['-']; document.getElementById('s-missing').innerText = stats['-'];
document.getElementById('s-extra').innerText = stats['+']; document.getElementById('s-extra').innerText = stats['+'];
document.getElementById('report-content').style.display = 'block'; document.getElementById('report-content').style.display = 'block';
document.getElementById('treeContainer').innerHTML = buildHtml(tree, "", folderErrors);
treeContainer.innerHTML = buildHtml(tree, "", folderErrors); const t = allI18n[currentLang];
// 提示用户加载成功 dropZone.querySelector('p').innerHTML = t.loadSuccess + (stats['=']+stats['*']+stats['-']+stats['+']) + t.files;
dropZone.querySelector('p').innerHTML = "✅ 已加载报告:" + stats['='] + " 个文件已校验";
} }
function buildHtml(node, currentPath, folderErrors) { function buildHtml(node, currentPath, folderErrors) {
@@ -162,15 +193,13 @@
const bIsFile = typeof node[b] === 'string'; const bIsFile = typeof node[b] === 'string';
return aIsFile === bIsFile ? a.localeCompare(b) : aIsFile - bIsFile; return aIsFile === bIsFile ? a.localeCompare(b) : aIsFile - bIsFile;
}); });
keys.forEach(name => { keys.forEach(name => {
const val = node[name]; const val = node[name];
const thisPath = currentPath ? `${currentPath}/${name}` : name; const thisPath = currentPath ? `${currentPath}/${name}` : name;
if (typeof val === 'object') { if (typeof val === 'object') {
const hasErr = folderErrors.has(thisPath); const hasErr = folderErrors.has(thisPath);
const dot = hasErr ? '<span class="error-dot">●</span>' : '';
html += `<li class="folder ${hasErr?'has-error-branch':'is-pure-match'}"> html += `<li class="folder ${hasErr?'has-error-branch':'is-pure-match'}">
<details><summary>📁 ${name}/ ${dot}</summary>${buildHtml(val, thisPath, folderErrors)}</details> <details><summary>📁 ${name}/ ${hasErr?'<span class="error-dot">●</span>':''}</summary>${buildHtml(val, thisPath, folderErrors)}</details>
</li>`; </li>`;
} else { } else {
const colors = {'=':'#28a745', '*':'#dc3545', '-':'#ffc107', '+':'#17a2b8'}; const colors = {'=':'#28a745', '*':'#dc3545', '-':'#ffc107', '+':'#17a2b8'};
@@ -183,17 +212,16 @@
} }
function toggleFilter() { function toggleFilter() {
const container = document.getElementById('treeContainer'); errorMode = !errorMode;
const btn = document.getElementById('toggleFilterBtn'); document.getElementById('treeContainer').classList.toggle('filter-mode-errors', errorMode);
const active = container.classList.toggle('filter-mode-errors'); updateUI();
btn.innerText = active ? "显示模式:只看异常" : "显示模式:完整清单"; if(errorMode) document.querySelectorAll('.has-error-branch > details').forEach(d => d.open = true);
btn.classList.toggle('btn-danger', active);
if(active) document.querySelectorAll('.has-error-branch > details').forEach(d => d.open = true);
} }
function toggleAll(open) { function toggleAll(open) { document.querySelectorAll('details').forEach(d => d.open = open); }
document.querySelectorAll('details').forEach(d => d.open = open);
} // 首次加载初始化
updateUI();
</script> </script>
</body> </body>
</html> </html>

18
languages.js Normal file
View File

@@ -0,0 +1,18 @@
// languages.js
window.externalI18n = {
"es": {
"name": "Español",
"dropHint": "📂 <b>Arrastre</b> el archivo aquí",
"clickHint": "o haga clic para seleccionar manualmente",
"labelMatch": "Coincidencia (Match)",
"labelMismatch": "Error (Mismatch)",
"labelMissing": "Faltante (Missing)",
"labelExtra": "Extra",
"btnExpand": "Expandir todo",
"btnCollapse": "Contraer todo",
"modeFull": "Vista: Todos los archivos",
"modeError": "Vista: Solo errores",
"loadSuccess": "✅ Informe cargado: ",
"files": " archivos"
}
};