upgrade size only compare

This commit is contained in:
Julian Freeman
2026-02-26 18:20:46 -04:00
parent 02b3697978
commit 5ae8d48261

View File

@@ -4,63 +4,70 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Rclone 尺寸对比器</title> <title>Rclone 尺寸对比器</title>
<style> <style>
:root { --success: #28a745; --danger: #dc3545; --warning: #ffc107; --bg: #f8f9fa; --primary: #0984e3; } :root {
body { font-family: 'Segoe UI', system-ui, sans-serif; background: var(--bg); color: #333; padding: 20px; line-height: 1.5; } --success: #28a745; --danger: #dc3545; --warning: #ffc107; --info: #17a2b8;
.container { max-width: 1000px; margin: auto; } --bg: #f8f9fa; --card-bg: #ffffff; --text: #2d3436; --primary: #0984e3;
}
body { font-family: 'Segoe UI', system-ui, sans-serif; background: var(--bg); color: var(--text); padding: 20px; margin: 0; }
.container { max-width: 1100px; margin: auto; padding-top: 5px; }
/* 拖拽区 */ /* 拖拽区 */
.drop-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 25px; } .drop-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 25px; }
.drop-box { border: 2px dashed #ccc; border-radius: 10px; padding: 30px 15px; text-align: center; background: white; cursor: pointer; transition: 0.2s; } .drop-box { border: 2px dashed #bdc3c7; border-radius: 12px; padding: 35px 15px; text-align: center; background: white; cursor: pointer; transition: 0.2s; }
.drop-box.loaded { border-color: var(--success); background: #f6ffed; } .drop-box.loaded { border-color: var(--success); background: #f6ffed; }
.drop-box.hover { border-color: var(--primary); background: #e6f7ff; } .drop-box.hover { border-color: var(--primary); background: #e6f7ff; }
/* 统计卡片 */ /* 统计卡片 - 增加到4项 */
.stat-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 15px; margin-bottom: 25px; display: none; } .stat-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 15px; margin-bottom: 25px; display: none; }
.stat-card { background: white; padding: 15px; border-radius: 8px; text-align: center; box-shadow: 0 2px 5px rgba(0,0,0,0.05); } .stat-card { background: white; padding: 18px; border-radius: 10px; text-align: center; box-shadow: 0 4px 6px rgba(0,0,0,0.05); border-bottom: 4px solid #ddd; }
.stat-val { font-size: 22px; font-weight: bold; display: block; } .stat-val { font-size: 24px; font-weight: 800; display: block; margin-bottom: 4px; }
.stat-label { font-size: 12px; color: #636e72; font-weight: 600; text-transform: uppercase; }
/* 树状视图核心修复 */ /* 报告区域 */
.report-card { background: white; padding: 25px; border-radius: 12px; box-shadow: 0 4px 15px rgba(0,0,0,0.08); display: none; } .report-card { background: white; padding: 30px; border-radius: 15px; box-shadow: 0 10px 30px rgba(0,0,0,0.08); display: none; }
ul { list-style: none; padding-left: 20px; border-left: 1px solid #eee; margin: 5px 0; } .btn-group { margin-bottom: 20px; display: flex; gap: 10px; }
li { margin: 4px 0; } .btn { padding: 9px 18px; border: 1px solid #dfe6e9; border-radius: 8px; background: white; cursor: pointer; font-weight: 600; transition: 0.2s; }
.btn:hover { background: #f1f2f6; }
.btn-active { background: var(--danger) !important; color: white; border-color: var(--danger); }
/* 修正对齐:使用 Flexbox */ /* 树状 UI */
.item-row { display: flex; align-items: center; padding: 3px 6px; border-radius: 4px; } ul { list-style: none; padding-left: 22px; border-left: 1px dashed #dfe6e9; margin: 6px 0; }
.item-row:hover { background: #f0f7ff; } li { margin: 5px 0; }
.item-row { display: flex; align-items: center; padding: 4px 8px; border-radius: 5px; transition: 0.1s; }
.item-row:hover { background: #f8f9fa; }
summary { cursor: pointer; font-weight: 600; outline: none; list-style: none; } summary { cursor: pointer; outline: none; list-style: none; }
summary::-webkit-details-marker { display: none; } summary::-webkit-details-marker { display: none; }
.badge { .badge {
display: inline-block; min-width: 22px; height: 18px; line-height: 18px; display: inline-block; min-width: 22px; height: 18px; line-height: 18px;
text-align: center; border-radius: 3px; color: white; font-size: 11px; text-align: center; border-radius: 4px; color: white; font-size: 11px;
font-weight: bold; margin-right: 10px; flex-shrink: 0; font-weight: bold; margin-right: 12px; flex-shrink: 0;
} }
.folder-icon { margin-right: 8px; font-size: 1.1em; } .folder-icon { margin-right: 8px; font-size: 1.1em; opacity: 0.7; }
.error-dot { color: var(--danger); margin-left: 6px; font-size: 14px; } .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 .is-match, .filter-mode .is-pure-match { display: none; } .filter-mode .is-match, .filter-mode .is-pure-match { display: none; }
.btn-group { margin-bottom: 15px; display: flex; gap: 10px; }
.btn { padding: 8px 16px; border: 1px solid #ddd; border-radius: 6px; background: white; cursor: pointer; font-weight: 600; }
.btn-active { background: var(--danger); color: white; border-color: var(--danger); }
</style> </style>
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<h2 style="text-align:center;">🎬 Rclone 极速尺寸对比</h2> <h2 style="text-align:center; margin-bottom:30px;">🎬 Rclone 双向对比工具</h2>
<div class="drop-grid"> <div class="drop-grid">
<div id="drop-remote" class="drop-box"><p id="t-remote">1. 拖入云端清单</p><input type="file" id="file-remote" style="display:none"></div> <div id="drop-remote" class="drop-box"><p id="t-remote">1. 拖入云端清单</p><input type="file" id="file-remote" style="display:none"></div>
<div id="drop-local" class="drop-box"><p id="t-local">2. 拖入本地清单</p><input type="file" id="file-local" style="display:none"></div> <div id="drop-local" class="drop-box"><p id="t-local">2. 拖入本地清单</p><input type="file" id="file-local" style="display:none"></div>
</div> </div>
<div id="report-section" style="display: none;"> <div id="report-section">
<div class="stat-grid" id="stat-bar"> <div class="stat-grid" id="stat-bar">
<div class="stat-card" style="color:var(--success)">一致 (Match)<span id="cnt-match" class="stat-val">0</span></div> <div class="stat-card" style="border-color: var(--success)"><span class="stat-label">一致 (Match)</span><span id="cnt-match" class="stat-val" style="color: var(--success)">0</span></div>
<div class="stat-card" style="color:var(--danger)">不同 (Diff)<span id="cnt-diff" class="stat-val">0</span></div> <div class="stat-card" style="border-color: var(--danger)"><span class="stat-label">不同 (Diff)</span><span id="cnt-diff" class="stat-val" style="color: var(--danger)">0</span></div>
<div class="stat-card" style="color:var(--warning)">缺失 (Missing)<span id="cnt-miss" class="stat-val">0</span></div> <div class="stat-card" style="border-color: var(--warning)"><span class="stat-label">缺失 (Missing)</span><span id="cnt-miss" class="stat-val" style="color: var(--warning)">0</span></div>
<div class="stat-card" style="border-color: var(--info)"><span class="stat-label">多余 (Extra)</span><span id="cnt-extra" class="stat-val" style="color: var(--info)">0</span></div>
</div> </div>
<div class="report-card" id="report-card"> <div class="report-card" id="report-card">
@@ -75,7 +82,7 @@
</div> </div>
<script> <script>
let remoteData = null, localData = null, errorMode = false; let remoteMap = null, localMap = null, errorMode = false;
function setupBox(id, fileId, type) { function setupBox(id, fileId, type) {
const box = document.getElementById(id); const box = document.getElementById(id);
@@ -83,14 +90,14 @@
box.onclick = () => input.click(); box.onclick = () => input.click();
box.ondragover = (e) => { e.preventDefault(); box.classList.add('hover'); }; box.ondragover = (e) => { e.preventDefault(); box.classList.add('hover'); };
box.ondragleave = () => box.classList.remove('hover'); box.ondragleave = () => box.classList.remove('hover');
box.ondrop = (e) => { e.preventDefault(); box.classList.remove('hover'); loadFile(e.dataTransfer.files[0], type); }; box.ondrop = (e) => { e.preventDefault(); box.classList.remove('hover'); handleFile(e.dataTransfer.files[0], type); };
input.onchange = (e) => loadFile(e.target.files[0], type); input.onchange = (e) => handleFile(e.target.files[0], type);
} }
setupBox('drop-remote', 'file-remote', 'remote'); setupBox('drop-remote', 'file-remote', 'remote');
setupBox('drop-local', 'file-local', 'local'); setupBox('drop-local', 'file-local', 'local');
function loadFile(file, type) { function handleFile(file, type) {
if (!file) return; if (!file) return;
const reader = new FileReader(); const reader = new FileReader();
reader.onload = (e) => { reader.onload = (e) => {
@@ -100,30 +107,38 @@
const parts = line.split('|'); const parts = line.split('|');
if (parts.length >= 2) { if (parts.length >= 2) {
const size = parts[0].trim(); const size = parts[0].trim();
// 修复:标准化路径,去掉前后斜杠
const path = parts.slice(1).join('|').replace(/^\/+|\/+$/g, ''); const path = parts.slice(1).join('|').replace(/^\/+|\/+$/g, '');
map.set(path, size); map.set(path, size);
} }
}); });
if (type === 'remote') { remoteData = map; document.getElementById('t-remote').innerText = "✅ 云端加载成功"; document.getElementById('drop-remote').classList.add('loaded'); } if (type === 'remote') { remoteMap = map; document.getElementById('t-remote').innerText = "✅ 云端清单已加载"; document.getElementById('drop-remote').classList.add('loaded'); }
else { localData = map; document.getElementById('t-local').innerText = "✅ 本地加载成功"; document.getElementById('drop-local').classList.add('loaded'); } else { localMap = map; document.getElementById('t-local').innerText = "✅ 本地清单已加载"; document.getElementById('drop-local').classList.add('loaded'); }
if (remoteData && localData) compare(); if (remoteMap && localMap) startComparison();
}; };
reader.readAsText(file); reader.readAsText(file);
} }
function compare() { function startComparison() {
const tree = {}, stats = {match:0, diff:0, miss:0}, folderErrors = new Set(); const tree = {}, stats = {match:0, diff:0, miss:0, extra:0}, folderErrors = new Set();
// 以云端清单为基准 // 获取所有路径的并集
remoteData.forEach((rSize, path) => { const allPaths = new Set([...remoteMap.keys(), ...localMap.keys()]);
const lSize = localData.get(path);
allPaths.forEach(path => {
const rSize = remoteMap.get(path);
const lSize = localMap.get(path);
let symbol = ''; let symbol = '';
if (lSize === undefined) { symbol = '-'; stats.miss++; }
else if (lSize !== rSize) { symbol = '*'; stats.diff++; }
else { symbol = '='; stats.match++; }
// 填充树形结构 if (rSize !== undefined && lSize !== undefined) {
if (rSize === lSize) { symbol = '='; stats.match++; }
else { symbol = '*'; stats.diff++; }
} else if (rSize !== undefined && lSize === undefined) {
symbol = '-'; stats.miss++; // 缺失
} else {
symbol = '+'; stats.extra++; // 多余
}
// 构建树状结构
let curr = tree; let curr = tree;
const parts = path.split('/'); const parts = path.split('/');
parts.forEach((part, i) => { parts.forEach((part, i) => {
@@ -135,7 +150,7 @@
} }
}); });
// 冒泡标记文件夹错误 // 标记含异常的文件夹
if (symbol !== '=') { if (symbol !== '=') {
let acc = ""; let acc = "";
parts.slice(0, -1).forEach(p => { parts.slice(0, -1).forEach(p => {
@@ -145,10 +160,12 @@
} }
}); });
// 更新统计 UI
document.getElementById('cnt-match').innerText = stats.match; document.getElementById('cnt-match').innerText = stats.match;
document.getElementById('cnt-diff').innerText = stats.diff; document.getElementById('cnt-diff').innerText = stats.diff;
document.getElementById('cnt-miss').innerText = stats.miss; document.getElementById('cnt-miss').innerText = stats.miss;
document.getElementById('report-section').style.display = 'block'; document.getElementById('cnt-extra').innerText = stats.extra;
document.getElementById('stat-bar').style.display = 'grid'; document.getElementById('stat-bar').style.display = 'grid';
document.getElementById('report-card').style.display = 'block'; document.getElementById('report-card').style.display = 'block';
document.getElementById('treeContainer').innerHTML = renderTree(tree, "", folderErrors); document.getElementById('treeContainer').innerHTML = renderTree(tree, "", folderErrors);
@@ -175,10 +192,16 @@
</details> </details>
</li>`; </li>`;
} else { } else {
const colors = {'=': 'var(--success)', '*': 'var(--danger)', '-': 'var(--warning)'}; const cfg = {
'=': {c: 'var(--success)', s: '='},
'*': {c: 'var(--danger)', s: '*'},
'-': {c: 'var(--warning)', s: '-'},
'+': {c: 'var(--info)', s: '+'}
}[val];
html += `<li class="${val==='='?'is-match':'is-error'}"> html += `<li class="${val==='='?'is-match':'is-error'}">
<div class="item-row"> <div class="item-row">
<span class="badge" style="background:${colors[val]}">${val}</span> <span class="badge" style="background:${cfg.c}">${cfg.s}</span>
<span class="file-name">${name}</span> <span class="file-name">${name}</span>
</div> </div>
</li>`; </li>`;