211 lines
11 KiB
HTML
211 lines
11 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="ru" data-theme="dark">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<title>Сравнение Excel</title>
|
|
<link rel="stylesheet" href="design-system/kt-ai-fonts.css">
|
|
<link rel="stylesheet" href="design-system/kt-ai-tokens.css">
|
|
<link rel="stylesheet" href="design-system/kt-ai-components.css">
|
|
<link rel="stylesheet" href="design-system/kt-ai-page.css">
|
|
<style>
|
|
.drop-zone{border:2px dashed var(--kt-ai-border);border-radius:var(--kt-ai-radius-xl);padding:var(--kt-ai-space-8) var(--kt-ai-space-6);text-align:center;cursor:pointer;transition:all .2s;background:var(--kt-ai-card-bg)}
|
|
.drop-zone:hover,.drop-zone.drag{border-color:var(--kt-ai-primary);background:var(--kt-ai-bg-soft)}
|
|
.drop-zone input{display:none}
|
|
.drop-zone .icon{font-size:48px;margin-bottom:var(--kt-ai-space-3);opacity:.6}
|
|
.drop-zone .label{font-size:var(--kt-ai-text-lg);color:var(--kt-ai-fg);font-weight:var(--kt-ai-weight-medium)}
|
|
.drop-zone .hint{font-size:var(--kt-ai-text-sm);color:var(--kt-ai-fg-muted);margin-top:var(--kt-ai-space-2)}
|
|
.file-name{margin-top:var(--kt-ai-space-3);font-size:var(--kt-ai-text-sm);color:var(--kt-ai-primary);font-weight:var(--kt-ai-weight-medium)}
|
|
.compare-btn{width:100%;margin-top:var(--kt-ai-space-4)}
|
|
.result-box{background:var(--kt-ai-card-bg);border:1px solid var(--kt-ai-card-border);border-radius:var(--kt-ai-radius-xl);padding:var(--kt-ai-space-6);margin-top:var(--kt-ai-space-6);white-space:pre-wrap;font-family:var(--kt-ai-font-mono);font-size:var(--kt-ai-text-sm);line-height:1.7;color:var(--kt-ai-fg);max-height:60vh;overflow-y:auto}
|
|
.result-box:empty{display:none}
|
|
.stats{display:flex;gap:var(--kt-ai-space-3);flex-wrap:wrap;margin-bottom:var(--kt-ai-space-4)}
|
|
.stat{background:var(--kt-ai-bg-soft);border:1px solid var(--kt-ai-card-border);border-radius:var(--kt-ai-radius-lg);padding:var(--kt-ai-space-3) var(--kt-ai-space-5);text-align:center;flex:1;min-width:120px}
|
|
.stat .num{font-size:var(--kt-ai-text-2xl);font-weight:var(--kt-ai-weight-bold);color:var(--kt-ai-primary)}
|
|
.stat .lbl{font-size:var(--kt-ai-text-xs);color:var(--kt-ai-fg-muted);margin-top:var(--kt-ai-space-1)}
|
|
.tabs{display:flex;gap:var(--kt-ai-space-2);margin-bottom:var(--kt-ai-space-5)}
|
|
.tabs button{flex:1;padding:var(--kt-ai-space-3);border:1px solid var(--kt-ai-card-border);background:var(--kt-ai-card-bg);color:var(--kt-ai-fg);border-radius:var(--kt-ai-radius-lg);cursor:pointer;font-size:var(--kt-ai-text-sm);font-weight:var(--kt-ai-weight-medium);transition:all .2s}
|
|
.tabs button.active{background:var(--kt-ai-primary);color:#fff;border-color:var(--kt-ai-primary)}
|
|
.upload-row{display:grid;grid-template-columns:1fr 1fr;gap:var(--kt-ai-space-4)}
|
|
@media(max-width:640px){.upload-row{grid-template-columns:1fr}}
|
|
.spinner{display:inline-block;width:20px;height:20px;border:2px solid var(--kt-ai-border);border-top-color:var(--kt-ai-primary);border-radius:50%;animation:spin .6s linear infinite;margin-right:var(--kt-ai-space-2);vertical-align:middle}
|
|
@keyframes spin{to{transform:rotate(360deg)}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<header class="kt-ai-pagehead">
|
|
<div class="kt-ai-wrap kt-ai-pagehead-in">
|
|
<strong>Excel Compare</strong>
|
|
<span class="kt-ai-spacer"></span>
|
|
</div>
|
|
</header>
|
|
|
|
<section class="kt-ai-hero" data-align="center">
|
|
<div class="kt-ai-wrap">
|
|
<h1>Сравнение и анализ Excel</h1>
|
|
<p class="kt-ai-hero-sub">Загрузите два файла — получите подробный отчёт по всем отличиям в каждом листе и ячейке.</p>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="kt-ai-section" data-divider="true">
|
|
<div class="kt-ai-wrap">
|
|
|
|
<div class="tabs">
|
|
<button class="active" onclick="switchTab('compare')">Сравнение двух файлов</button>
|
|
<button onclick="switchTab('analyze')">Анализ одного файла</button>
|
|
</div>
|
|
|
|
<div id="tab-compare">
|
|
<div class="upload-row">
|
|
<div class="drop-zone" id="drop1" onclick="document.getElementById('file1').click()">
|
|
<div class="icon">📄</div>
|
|
<div class="label">Файл 1 (оригинал)</div>
|
|
<div class="hint">Нажмите или перетащите .xlsx / .xls</div>
|
|
<div class="file-name" id="name1"></div>
|
|
<input type="file" id="file1" accept=".xlsx,.xls,.csv" onchange="pickFile(1,this)">
|
|
</div>
|
|
<div class="drop-zone" id="drop2" onclick="document.getElementById('file2').click()">
|
|
<div class="icon">📄</div>
|
|
<div class="label">Файл 2 (новая версия)</div>
|
|
<div class="hint">Нажмите или перетащите .xlsx / .xls</div>
|
|
<div class="file-name" id="name2"></div>
|
|
<input type="file" id="file2" accept=".xlsx,.xls,.csv" onchange="pickFile(2,this)">
|
|
</div>
|
|
</div>
|
|
<button class="kt-ai-btn compare-btn" data-variant="primary" data-size="lg" id="compareBtn" onclick="doCompare()" disabled>Сравнить файлы</button>
|
|
</div>
|
|
|
|
<div id="tab-analyze" style="display:none">
|
|
<div class="drop-zone" id="dropA" onclick="document.getElementById('fileA').click()" style="max-width:480px;margin:0 auto">
|
|
<div class="icon">📊</div>
|
|
<div class="label">Загрузите Excel-файл</div>
|
|
<div class="hint">Нажмите или перетащите .xlsx / .xls</div>
|
|
<div class="file-name" id="nameA"></div>
|
|
<input type="file" id="fileA" accept=".xlsx,.xls,.csv" onchange="pickFile('A',this)">
|
|
</div>
|
|
<button class="kt-ai-btn compare-btn" data-variant="primary" data-size="lg" id="analyzeBtn" onclick="doAnalyze()" disabled style="max-width:480px;margin:var(--kt-ai-space-4) auto 0;display:block">Анализировать</button>
|
|
</div>
|
|
|
|
<div class="stats" id="stats" style="display:none">
|
|
<div class="stat"><div class="num" id="statSheets">0</div><div class="lbl">Листов</div></div>
|
|
<div class="stat"><div class="num" id="statChanges">0</div><div class="lbl">Изменений</div></div>
|
|
<div class="stat"><div class="num" id="statAdded">0</div><div class="lbl">Добавлено</div></div>
|
|
<div class="stat"><div class="num" id="statRemoved">0</div><div class="lbl">Удалено</div></div>
|
|
</div>
|
|
|
|
<div class="result-box" id="result"></div>
|
|
</div>
|
|
</section>
|
|
|
|
<footer class="kt-ai-wrap kt-ai-pagefoot">
|
|
<span>Excel Compare</span>
|
|
<span class="kt-ai-spacer"></span>
|
|
<span>Бесплатно, без ограничений</span>
|
|
</footer>
|
|
|
|
<script>
|
|
var files = {};
|
|
var currentTab = 'compare';
|
|
|
|
function switchTab(tab) {
|
|
currentTab = tab;
|
|
document.querySelectorAll('.tabs button').forEach(function(b,i){
|
|
b.classList.toggle('active', (i===0 && tab==='compare') || (i===1 && tab==='analyze'));
|
|
});
|
|
document.getElementById('tab-compare').style.display = tab==='compare' ? '' : 'none';
|
|
document.getElementById('tab-analyze').style.display = tab==='analyze' ? '' : 'none';
|
|
document.getElementById('result').textContent = '';
|
|
document.getElementById('stats').style.display = 'none';
|
|
}
|
|
|
|
function pickFile(n, input) {
|
|
var f = input.files[0];
|
|
if (!f) return;
|
|
files[n] = f;
|
|
var el = document.getElementById('name' + n);
|
|
el.textContent = f.name + ' (' + (f.size/1024).toFixed(1) + ' КБ)';
|
|
var dz = document.getElementById('drop' + n);
|
|
dz.style.borderColor = 'var(--kt-ai-primary)';
|
|
if (n === 1 || n === 2) {
|
|
document.getElementById('compareBtn').disabled = !(files[1] && files[2]);
|
|
}
|
|
if (n === 'A') {
|
|
document.getElementById('analyzeBtn').disabled = !files['A'];
|
|
}
|
|
}
|
|
|
|
function setupDrop(id, n) {
|
|
var dz = document.getElementById(id);
|
|
dz.addEventListener('dragover', function(e){ e.preventDefault(); dz.classList.add('drag'); });
|
|
dz.addEventListener('dragleave', function(){ dz.classList.remove('drag'); });
|
|
dz.addEventListener('drop', function(e){
|
|
e.preventDefault(); dz.classList.remove('drag');
|
|
var f = e.dataTransfer.files[0];
|
|
if (f && (f.name.endsWith('.xlsx') || f.name.endsWith('.xls') || f.name.endsWith('.csv'))) {
|
|
files[n] = f;
|
|
var input = document.getElementById(n === 'A' ? 'fileA' : 'file' + n);
|
|
var dt = new DataTransfer(); dt.items.add(f); input.files = dt.files;
|
|
pickFile(n, input);
|
|
}
|
|
});
|
|
}
|
|
setupDrop('drop1', 1);
|
|
setupDrop('drop2', 2);
|
|
setupDrop('dropA', 'A');
|
|
|
|
function doCompare() {
|
|
if (!files[1] || !files[2]) return;
|
|
var btn = document.getElementById('compareBtn');
|
|
btn.innerHTML = '<span class="spinner"></span> Сравниваю...';
|
|
btn.disabled = true;
|
|
var fd = new FormData();
|
|
fd.append('file1', files[1]);
|
|
fd.append('file2', files[2]);
|
|
fetch('/api/compare', { method: 'POST', body: fd })
|
|
.then(function(r){ return r.json(); })
|
|
.then(function(j){
|
|
btn.innerHTML = 'Сравнить файлы'; btn.disabled = false;
|
|
if (!j.ok) { document.getElementById('result').textContent = 'Ошибка: ' + j.error; return; }
|
|
var d = j.data;
|
|
document.getElementById('result').textContent = d.report;
|
|
var changes = (d.report.match(/ [~+-]/g) || []).length;
|
|
var added = (d.report.match(/ \+/g) || []).length;
|
|
var removed = (d.report.match(/ -/g) || []).length;
|
|
var sheets = Math.max(d.sheetsA, d.sheetsB);
|
|
document.getElementById('statSheets').textContent = sheets;
|
|
document.getElementById('statChanges').textContent = changes;
|
|
document.getElementById('statAdded').textContent = added;
|
|
document.getElementById('statRemoved').textContent = removed;
|
|
document.getElementById('stats').style.display = '';
|
|
})
|
|
.catch(function(e){
|
|
btn.innerHTML = 'Сравнить файлы'; btn.disabled = false;
|
|
document.getElementById('result').textContent = 'Ошибка сети: ' + e.message;
|
|
});
|
|
}
|
|
|
|
function doAnalyze() {
|
|
if (!files['A']) return;
|
|
var btn = document.getElementById('analyzeBtn');
|
|
btn.innerHTML = '<span class="spinner"></span> Анализирую...';
|
|
btn.disabled = true;
|
|
var fd = new FormData();
|
|
fd.append('file', files['A']);
|
|
fetch('/api/analyze', { method: 'POST', body: fd })
|
|
.then(function(r){ return r.json(); })
|
|
.then(function(j){
|
|
btn.innerHTML = 'Анализировать'; btn.disabled = false;
|
|
if (!j.ok) { document.getElementById('result').textContent = 'Ошибка: ' + j.error; return; }
|
|
document.getElementById('result').textContent = j.data.report;
|
|
document.getElementById('stats').style.display = 'none';
|
|
})
|
|
.catch(function(e){
|
|
btn.innerHTML = 'Анализировать'; btn.disabled = false;
|
|
document.getElementById('result').textContent = 'Ошибка сети: ' + e.message;
|
|
});
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|