add size only compare
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
rclone.exe
|
||||
md5_files/
|
||||
combined_files/
|
||||
man_files/
|
||||
|
||||
202
Size_Comparator.html
Normal file
202
Size_Comparator.html
Normal 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>
|
||||
Reference in New Issue
Block a user