add size only compare

This commit is contained in:
Julian Freeman
2026-02-26 18:12:04 -04:00
parent b2cf655dfb
commit 02b3697978
2 changed files with 203 additions and 0 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
rclone.exe
md5_files/
combined_files/
man_files/

202
Size_Comparator.html Normal file
View File

@@ -0,0 +1,202 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Rclone 尺寸对比器</title>
<style>
:root { --success: #28a745; --danger: #dc3545; --warning: #ffc107; --bg: #f8f9fa; --primary: #0984e3; }
body { font-family: 'Segoe UI', system-ui, sans-serif; background: var(--bg); color: #333; padding: 20px; line-height: 1.5; }
.container { max-width: 1000px; margin: auto; }
/* 拖拽区 */
.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.loaded { border-color: var(--success); background: #f6ffed; }
.drop-box.hover { border-color: var(--primary); background: #e6f7ff; }
/* 统计卡片 */
.stat-grid { display: grid; grid-template-columns: repeat(3, 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-val { font-size: 22px; font-weight: bold; display: block; }
/* 树状视图核心修复 */
.report-card { background: white; padding: 25px; border-radius: 12px; box-shadow: 0 4px 15px rgba(0,0,0,0.08); display: none; }
ul { list-style: none; padding-left: 20px; border-left: 1px solid #eee; margin: 5px 0; }
li { margin: 4px 0; }
/* 修正对齐:使用 Flexbox */
.item-row { display: flex; align-items: center; padding: 3px 6px; border-radius: 4px; }
.item-row:hover { background: #f0f7ff; }
summary { cursor: pointer; font-weight: 600; outline: none; list-style: none; }
summary::-webkit-details-marker { display: none; }
.badge {
display: inline-block; min-width: 22px; height: 18px; line-height: 18px;
text-align: center; border-radius: 3px; color: white; font-size: 11px;
font-weight: bold; margin-right: 10px; flex-shrink: 0;
}
.folder-icon { margin-right: 8px; font-size: 1.1em; }
.error-dot { color: var(--danger); margin-left: 6px; font-size: 14px; }
/* 过滤逻辑 */
.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>
</head>
<body>
<div class="container">
<h2 style="text-align:center;">🎬 Rclone 极速尺寸对比</h2>
<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-local" class="drop-box"><p id="t-local">2. 拖入本地清单</p><input type="file" id="file-local" style="display:none"></div>
</div>
<div id="report-section" style="display: none;">
<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="color:var(--danger)">不同 (Diff)<span id="cnt-diff" class="stat-val">0</span></div>
<div class="stat-card" style="color:var(--warning)">缺失 (Missing)<span id="cnt-miss" class="stat-val">0</span></div>
</div>
<div class="report-card" id="report-card">
<div class="btn-group">
<button onclick="toggleFilter()" id="filterBtn" class="btn">只看异常</button>
<button onclick="toggleAll(true)" class="btn">全部展开</button>
<button onclick="toggleAll(false)" class="btn">全部收起</button>
</div>
<div id="treeContainer"></div>
</div>
</div>
</div>
<script>
let remoteData = null, localData = null, errorMode = false;
function setupBox(id, fileId, type) {
const box = document.getElementById(id);
const input = document.getElementById(fileId);
box.onclick = () => input.click();
box.ondragover = (e) => { e.preventDefault(); box.classList.add('hover'); };
box.ondragleave = () => box.classList.remove('hover');
box.ondrop = (e) => { e.preventDefault(); box.classList.remove('hover'); loadFile(e.dataTransfer.files[0], type); };
input.onchange = (e) => loadFile(e.target.files[0], type);
}
setupBox('drop-remote', 'file-remote', 'remote');
setupBox('drop-local', 'file-local', 'local');
function loadFile(file, type) {
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
const map = new Map();
e.target.result.split(/\r?\n/).forEach(line => {
if (!line.trim()) return;
const parts = line.split('|');
if (parts.length >= 2) {
const size = parts[0].trim();
// 修复:标准化路径,去掉前后斜杠
const path = parts.slice(1).join('|').replace(/^\/+|\/+$/g, '');
map.set(path, size);
}
});
if (type === 'remote') { remoteData = 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'); }
if (remoteData && localData) compare();
};
reader.readAsText(file);
}
function compare() {
const tree = {}, stats = {match:0, diff:0, miss:0}, folderErrors = new Set();
// 以云端清单为基准
remoteData.forEach((rSize, path) => {
const lSize = localData.get(path);
let symbol = '';
if (lSize === undefined) { symbol = '-'; stats.miss++; }
else if (lSize !== rSize) { symbol = '*'; stats.diff++; }
else { symbol = '='; stats.match++; }
// 填充树形结构
let curr = tree;
const parts = path.split('/');
parts.forEach((part, i) => {
if (i === parts.length - 1) {
curr[part] = symbol;
} else {
if (!curr[part] || typeof curr[part] === 'string') curr[part] = {};
curr = curr[part];
}
});
// 冒泡标记文件夹错误
if (symbol !== '=') {
let acc = "";
parts.slice(0, -1).forEach(p => {
acc = acc ? `${acc}/${p}` : p;
folderErrors.add(acc);
});
}
});
document.getElementById('cnt-match').innerText = stats.match;
document.getElementById('cnt-diff').innerText = stats.diff;
document.getElementById('cnt-miss').innerText = stats.miss;
document.getElementById('report-section').style.display = 'block';
document.getElementById('stat-bar').style.display = 'grid';
document.getElementById('report-card').style.display = 'block';
document.getElementById('treeContainer').innerHTML = renderTree(tree, "", folderErrors);
}
function renderTree(node, currentPath, errors) {
let html = "<ul>";
const sortedKeys = Object.keys(node).sort((a, b) => {
const aIsFolder = typeof node[a] === 'object';
const bIsFolder = typeof node[b] === 'object';
return aIsFolder === bIsFolder ? a.localeCompare(b) : (aIsFolder ? -1 : 1);
});
sortedKeys.forEach(name => {
const val = node[name];
const thisPath = currentPath ? `${currentPath}/${name}` : name;
if (typeof val === 'object') {
const hasErr = errors.has(thisPath);
html += `<li class="folder ${hasErr?'has-error':'is-pure-match'}">
<details>
<summary><div class="item-row"><span class="folder-icon">📁</span>${name}/ ${hasErr?'<span class="error-dot">●</span>':''}</div></summary>
${renderTree(val, thisPath, errors)}
</details>
</li>`;
} else {
const colors = {'=': 'var(--success)', '*': 'var(--danger)', '-': 'var(--warning)'};
html += `<li class="${val==='='?'is-match':'is-error'}">
<div class="item-row">
<span class="badge" style="background:${colors[val]}">${val}</span>
<span class="file-name">${name}</span>
</div>
</li>`;
}
});
return html + "</ul>";
}
function toggleFilter() {
errorMode = !errorMode;
document.getElementById('treeContainer').classList.toggle('filter-mode', errorMode);
const btn = document.getElementById('filterBtn');
btn.innerText = errorMode ? "显示全部" : "只看异常";
btn.classList.toggle('btn-active', errorMode);
if(errorMode) document.querySelectorAll('.has-error > details').forEach(d => d.open = true);
}
function toggleAll(open) { document.querySelectorAll('details').forEach(d => d.open = open); }
</script>
</body>
</html>