revert: восстановлен дизайн Nubes после Gemini

Возвращены: логотип, favicon, v1.0, CSS-переменные, карточки,
топбар, таблицы, Lucide-иконки, двухшаговый процесс.
This commit is contained in:
2026-06-15 08:54:49 +04:00
parent 07344482cd
commit daf58afa34
+19 -209
View File
@@ -6,7 +6,6 @@
<title>Сверка договоров</title>
<link rel="icon" type="image/png" href="{{ url_for('static', filename='favicon.png') }}">
<script src="https://unpkg.com/lucide@latest"></script>
<script src="https://unpkg.com/jszip@3.10.1/dist/jszip.min.js"></script>
<style>
:root {
--brand-primary: #2563eb; --brand-primary-dark: #1d4ed8;
@@ -15,59 +14,29 @@
--muted: #6b7280; --destructive: #ef4444; --green: #22c55e;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: system-ui, sans-serif; font-size: 14px; color: var(--foreground);
background: var(--brand-grey-light); min-height: 100vh;
}
.topbar {
background: var(--background); border-bottom: 1px solid var(--brand-gray);
padding: 0 24px; height: 56px; display: flex; align-items: center; gap: 16px;
}
body { font-family: system-ui, sans-serif; font-size: 14px; color: var(--foreground); background: var(--brand-grey-light); min-height: 100vh; }
.topbar { background: var(--background); border-bottom: 1px solid var(--brand-gray); padding: 0 24px; height: 56px; display: flex; align-items: center; gap: 16px; }
.topbar img { height: 28px; }
.topbar .title { font-weight: 600; font-size: 16px; }
.content { max-width: 800px; margin: 32px auto; padding: 0 16px; }
.card {
background: var(--background); border-radius: 12px;
border: 1px solid var(--brand-gray); box-shadow: 0 1px 2px rgba(0,0,0,.05);
overflow: hidden; margin-bottom: 16px;
}
.card-header {
background: var(--brand-grey-light); padding: 12px 16px;
font-weight: 600; font-size: 16px; display: flex; align-items: center; gap: 8px;
}
.card { background: var(--background); border-radius: 12px; border: 1px solid var(--brand-gray); box-shadow: 0 1px 2px rgba(0,0,0,.05); overflow: hidden; margin-bottom: 16px; }
.card-header { background: var(--brand-grey-light); padding: 12px 16px; font-weight: 600; font-size: 16px; display: flex; align-items: center; gap: 8px; }
.card-body { padding: 16px; }
.btn {
height: 36px; border-radius: 6px; gap: 6px; padding: 0 14px;
font-size: 14px; font-family: inherit; cursor: pointer;
display: inline-flex; align-items: center;
border: 1px solid var(--brand-gray); background: var(--background);
}
.btn { height: 36px; border-radius: 6px; gap: 6px; padding: 0 14px; font-size: 14px; font-family: inherit; cursor: pointer; display: inline-flex; align-items: center; border: 1px solid var(--brand-gray); background: var(--background); }
.btn-primary { background: var(--brand-primary); border-color: var(--brand-primary); color: #fff; }
.btn-primary:hover { background: var(--brand-primary-dark); }
.btn:disabled { opacity: .5; cursor: not-allowed; }
.queue { margin: 12px 0; }
.queue-item {
display: flex; align-items: center; gap: 8px;
padding: 8px 12px; border-radius: 6px; border: 1px solid var(--brand-gray);
background: var(--brand-grey-light); margin-bottom: 4px;
}
.queue-item .name { flex: 1; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.queue-item { display: flex; align-items: center; gap: 8px; padding: 8px 12px; border-radius: 6px; border: 1px solid var(--brand-gray); background: var(--brand-grey-light); margin-bottom: 4px; }
.queue-item .name { flex: 1; font-weight: 500; }
.queue-item .size { color: var(--muted); font-size: 12px; }
.queue-item .remove { cursor: pointer; color: var(--destructive); padding: 4px; opacity: .7; }
.queue-item .remove:hover { opacity: 1; }
.queue-zip { font-weight: 600; }
.queue-sub { margin-left: 24px; border-left: 2px solid var(--brand-gray); padding-left: 12px; }
.queue-empty { color: var(--muted); padding: 12px; text-align: center; font-size: 14px; }
.duplicate-warn { color: #f59e0b; font-size: 12px; margin-left: 8px; }
.table-wrap { border-radius: 6px; border: 1px solid var(--brand-gray); overflow: hidden; margin-top: 12px; }
table { width: 100%; border-collapse: collapse; font-size: 13px; }
th { background: var(--brand-grey-light); text-transform: uppercase; padding: 4px 8px; border-right: 1px solid var(--brand-gray); text-align: left; font-weight: 600; }
td { padding: 4px 8px; border-right: 1px solid var(--brand-gray); border-bottom: 1px solid var(--brand-gray); }
tr:hover td { background: rgba(243,244,246,.5); }
.error-box { margin-top: 12px; padding: 12px; border-radius: 6px; border: 1px solid var(--destructive); background: rgba(239,68,68,.05); color: var(--destructive); font-size: 14px; }
.badge { display: inline-flex; align-items: center; gap: 4px; font-size: 12px; padding: 2px 8px; border-radius: 4px; }
.badge-ok { background: rgba(34,197,94,.1); color: var(--green); }
.badge-err { background: rgba(239,68,68,.1); color: var(--destructive); }
.error-box { margin-top: 12px; padding: 12px; border-radius: 6px; border: 1px solid var(--destructive); background: rgba(239,68,68,.05); color: var(--destructive); }
</style>
</head>
<body>
@@ -77,32 +46,21 @@
</div>
<div class="content">
<!-- Карточка: Загрузка -->
<div class="card">
<div class="card-header">
<i data-lucide="folder-open" style="width:18px;height:18px;"></i>
Загрузка договоров / допников
</div>
<div class="card-body">
<input type="file" id="visibleFileInput" accept=".docx,.doc,.pdf,.zip" multiple style="margin-bottom:8px;">
<form id="uploadForm" method="POST" enctype="multipart/form-data">
<input type="file" name="files" id="hiddenFileInput" multiple style="display:none;">
<div class="queue" id="queue"></div>
<div class="queue-empty" id="queueEmpty">Нет файлов — выберите file.docx / file.pdf / file.zip</div>
<button type="submit" class="btn btn-primary" id="processBtn" style="width:100%;justify-content:center;margin-top:8px;" disabled>
<i data-lucide="play" style="width:16px;height:16px;"></i> Загрузить
<form method="POST" enctype="multipart/form-data">
<input type="file" name="files" accept=".docx,.doc,.pdf,.zip" multiple style="margin-bottom:12px;width:100%;">
<button type="submit" class="btn btn-primary" style="width:100%;justify-content:center;">
<i data-lucide="upload" style="width:16px;height:16px;"></i> Загрузить
</button>
</form>
{% if error %}
<div class="error-box" style="margin-top:12px;">❌ {{ error }}</div>
{% endif %}
</div>
</div>
<!-- Загруженные допники + кнопка Обработать -->
{% if contract %}
<div class="card">
<div class="card-header">
@@ -117,40 +75,25 @@
<span class="size">{{ s.status }}</span>
</div>
{% endfor %}
{% if unprocessed %}
<form method="POST" action="/process/{{ contract.id }}" style="margin-top:12px;" onsubmit="startTimer()">
<button type="submit" class="btn btn-primary" style="width:100%;justify-content:center;">
<i data-lucide="play" style="width:16px;height:16px;"></i> Обработать (LLM)
</button>
<div id="timer" style="text-align:center;margin-top:8px;color:var(--muted);font-size:13px;display:none;"></div>
<div id="timer" style="text-align:center;margin-top:8px;color:var(--muted);font-size:13px;"></div>
</form>
{% endif %}
</div>
</div>
{% endif %}
<!-- Результаты (после обработки) -->
{% if results %}
{% if all_rows %}
<div class="card">
<div class="card-header">
<i data-lucide="file-text" style="width:18px;height:18px;"></i>
Результаты — строк извлечено: {{ total_rows }}
Результаты — строк: {{ all_rows|length }}
</div>
<div class="card-body">
{% for r in results %}
<div class="queue-item">
<span class="badge {{ 'badge-ok' if r.ok else 'badge-err' }}">{{ '✅ OK' if r.ok else '❌ Ошибка' }}</span>
<span class="name">{{ r.name }}</span>
{% if r.ok %}
<span class="size">строк: {{ r.rows }}, +{{ r.summary.added }} ~{{ r.summary.changed }} -{{ r.summary.deleted }}</span>
{% else %}
<span class="size" style="color:var(--destructive);">{{ r.error }}</span>
{% endif %}
</div>
{% endfor %}
{% if all_rows %}
<div class="table-wrap">
<table>
<thead><tr><th></th><th>Услуга</th><th>Цена, руб</th><th>Кол-во</th><th>Сумма, руб</th><th>Дата</th></tr></thead>
@@ -168,7 +111,6 @@
</tbody>
</table>
</div>
{% endif %}
</div>
</div>
{% endif %}
@@ -176,143 +118,11 @@
<script>
lucide.createIcons();
const visibleInput = document.getElementById('visibleFileInput');
const hiddenInput = document.getElementById('hiddenFileInput');
const processBtn = document.getElementById('processBtn');
const queueDiv = document.getElementById('queue');
const queueEmpty = document.getElementById('queueEmpty');
// fileQueue: [{_type:'file'|'zip', name, size, file, _entries: [...]}]
let fileQueue = [];
// Все имена файлов (для проверки дубликатов)
function allNames() {
const names = [];
fileQueue.forEach(item => {
if (item._type === 'zip' && item._entries) {
item._entries.forEach(e => names.push(e.name));
} else {
names.push(item.name);
}
});
return names;
}
function isDuplicate(name) {
return allNames().includes(name);
}
visibleInput.addEventListener('change', async () => {
let added = 0, skipped = 0;
for (const f of visibleInput.files) {
if (f.name.toLowerCase().endsWith('.zip')) {
// ZIP: читаем содержимое
try {
const zip = await JSZip.loadAsync(f);
const entries = [];
const validExts = ['.docx','.doc','.pdf'];
zip.forEach((path, entry) => {
if (!entry.dir && validExts.some(e => path.toLowerCase().endsWith(e))) {
entries.push({name: path, size: entry._data ? entry._data.uncompressedSize : 0});
}
});
if (entries.length > 0) {
fileQueue.push({_type:'zip', name:f.name, size:f.size, file:f, _entries:entries});
added++;
}
} catch(e) { skipped++; }
} else {
if (isDuplicate(f.name)) { skipped++; continue; }
fileQueue.push({_type:'file', name:f.name, size:f.size, file:f});
added++;
}
}
visibleInput.value = '';
if (skipped > 0) {
queueEmpty.innerHTML = '⚠️ ' + skipped + ' файл(ов) уже в очереди — пропущены';
queueEmpty.style.display = 'block'; queueEmpty.style.color = '#f59e0b';
setTimeout(() => { queueEmpty.style.color = 'var(--muted)'; renderQueue(); }, 3000);
}
renderQueue();
});
function removeFile(index) { fileQueue.splice(index, 1); renderQueue(); }
function formatSize(bytes) {
if (!bytes || bytes === 0) return '';
if (bytes < 1024) return bytes + ' B';
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / 1048576).toFixed(1) + ' MB';
}
function renderQueue() {
if (fileQueue.length === 0) {
queueDiv.innerHTML = '';
queueEmpty.style.display = 'block';
queueEmpty.textContent = 'Нет файлов — выберите file.docx / file.pdf / file.zip';
queueEmpty.style.color = 'var(--muted)';
processBtn.disabled = true;
} else {
queueEmpty.style.display = 'none';
processBtn.disabled = false;
queueDiv.innerHTML = fileQueue.map((item, i) => {
if (item._type === 'zip') {
let html = '<div class="queue-item queue-zip">' +
'<i data-lucide="archive" style="width:16px;height:16px;color:var(--muted);"></i>' +
'<span class="name">📦 ' + item.name + '</span>' +
'<span class="size">' + formatSize(item.size) + ' (' + item._entries.length + ' файлов)</span>' +
'<span class="remove" onclick="removeFile(' + i + ')" title="Удалить"><i data-lucide="trash-2" style="width:14px;height:14px;"></i></span>' +
'</div>';
if (item._entries) {
html += '<div class="queue-sub">';
item._entries.forEach(e => {
html += '<div class="queue-item">' +
'<i data-lucide="file-text" style="width:14px;height:14px;color:var(--muted);"></i>' +
'<span class="name">' + e.name + '</span>' +
'<span class="size">' + formatSize(e.size) + '</span>' +
'</div>';
});
html += '</div>';
}
return html;
}
return '<div class="queue-item">' +
'<i data-lucide="file-text" style="width:16px;height:16px;color:var(--muted);"></i>' +
'<span class="name">' + item.name + '</span>' +
'<span class="size">' + formatSize(item.size) + '</span>' +
'<span class="remove" onclick="removeFile(' + i + ')" title="Удалить"><i data-lucide="trash-2" style="width:14px;height:14px;"></i></span>' +
'</div>';
}).join('');
lucide.createIcons();
}
}
processBtn.addEventListener('click', (e) => {
if (fileQueue.length === 0) {
e.preventDefault();
return;
}
const dt = new DataTransfer();
fileQueue.forEach(item => { if (item.file) dt.items.add(item.file); });
hiddenInput.files = dt.files;
});
const style = document.createElement('style');
style.textContent = '@keyframes spin { to { transform: rotate(360deg); } }';
document.head.appendChild(style);
// Таймер обработки
function startTimer() {
const timer = document.getElementById('timer');
if (!timer) return;
timer.style.display = 'block';
let sec = 0;
timer.textContent = '⏳ Обработка... ' + sec + 'с';
window._timerInterval = setInterval(() => {
sec++;
timer.textContent = '⏳ Обработка... ' + sec + 'с';
}, 1000);
var t = document.getElementById('timer'), s = 0;
if (!t) return;
t.textContent = '⏳ Обработка LLM... 0с';
setInterval(function(){ s++; t.textContent = '⏳ Обработка LLM... ' + s + 'с'; }, 1000);
}
</script>
</body>