fix(ui): очередь файлов — JS слой, обработка только по кнопке

- Файлы добавляются в очередь при выборе (чистый JS, без сервера)
- Кнопка «Обработать» активна только когда есть файлы
- Скрытая форма отправляется ТОЛЬКО по нажатию кнопки
- Никакой авто-отправки, никакого LLM до нажатия
This commit is contained in:
2026-06-15 07:09:32 +04:00
parent 4e714f5680
commit 17c4841acf
+96 -10
View File
@@ -43,7 +43,8 @@
} }
.btn-primary { background: var(--brand-primary); border-color: var(--brand-primary); color: #fff; } .btn-primary { background: var(--brand-primary); border-color: var(--brand-primary); color: #fff; }
.btn-primary:hover { background: var(--brand-primary-dark); } .btn-primary:hover { background: var(--brand-primary-dark); }
.queue { margin-top: 12px; } .btn:disabled { opacity: .5; cursor: not-allowed; }
.queue { margin: 12px 0; }
.queue-item { .queue-item {
display: flex; align-items: center; gap: 8px; display: flex; align-items: center; gap: 8px;
padding: 8px 12px; border-radius: 6px; border: 1px solid var(--brand-gray); padding: 8px 12px; border-radius: 6px; border: 1px solid var(--brand-gray);
@@ -53,6 +54,7 @@
.queue-item .size { color: var(--muted); font-size: 12px; } .queue-item .size { color: var(--muted); font-size: 12px; }
.queue-item .remove { cursor: pointer; color: var(--muted); padding: 4px; } .queue-item .remove { cursor: pointer; color: var(--muted); padding: 4px; }
.queue-item .remove:hover { color: var(--destructive); } .queue-item .remove:hover { color: var(--destructive); }
.queue-empty { color: var(--muted); padding: 12px; text-align: center; font-size: 14px; }
.table-wrap { border-radius: 6px; border: 1px solid var(--brand-gray); overflow: hidden; margin-top: 12px; } .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; } 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; } 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; }
@@ -71,34 +73,46 @@
</div> </div>
<div class="content"> <div class="content">
<!-- Карточка: Загрузка -->
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<i data-lucide="folder-open" style="width:18px;height:18px;"></i> <i data-lucide="folder-open" style="width:18px;height:18px;"></i>
Загрузка договоров / допников Загрузка договоров / допников
</div> </div>
<div class="card-body"> <div class="card-body">
<form method="POST" enctype="multipart/form-data"> <!-- Скрытая форма — отправляется только по кнопке -->
<input type="file" name="files" accept=".docx,.doc,.pdf,.zip" multiple style="margin-bottom:12px;"> <form id="uploadForm" method="POST" enctype="multipart/form-data" style="display:none;">
<button type="submit" class="btn btn-primary" style="width:100%;justify-content:center;"> <input type="file" name="files" id="hiddenFileInput" multiple>
<i data-lucide="play" style="width:16px;height:16px;"></i> Обработать
</button>
</form> </form>
<!-- Видимый интерфейс -->
<input type="file" id="visibleFileInput" accept=".docx,.doc,.pdf,.zip" multiple style="margin-bottom:8px;">
<!-- Очередь файлов -->
<div class="queue" id="queue"></div>
<div class="queue-empty" id="queueEmpty">Нет файлов — выберите file.docx / file.pdf / file.zip</div>
<button 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> Обработать
</button>
{% if error %} {% if error %}
<div class="error-box" style="margin-top:12px;">❌ {{ error }}</div> <div class="error-box" style="margin-top:12px;">❌ {{ error }}</div>
{% endif %} {% endif %}
</div> </div>
</div> </div>
<!-- Результаты (после обработки) -->
{% if results %} {% if results %}
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<i data-lucide="file-text" style="width:18px;height:18px;"></i> <i data-lucide="file-text" style="width:18px;height:18px;"></i>
Результаты (строк извлечено: {{ total_rows }}) Результаты строк извлечено: {{ total_rows }}
</div> </div>
<div class="card-body"> <div class="card-body">
{% for r in results %} {% for r in results %}
<div class="queue-item"> <div class="queue-item">
<span class="badge {{ 'badge-ok' if r.ok else 'badge-err' }}">{{ '✅' if r.ok else '❌' }} {{ 'OK' if r.ok else 'Ошибка' }}</span> <span class="badge {{ 'badge-ok' if r.ok else 'badge-err' }}">{{ '✅ OK' if r.ok else 'Ошибка' }}</span>
<span class="name">{{ r.name }}</span> <span class="name">{{ r.name }}</span>
{% if r.ok %} {% if r.ok %}
<span class="size">строк: {{ r.rows }}, +{{ r.summary.added }} ~{{ r.summary.changed }} -{{ r.summary.deleted }}</span> <span class="size">строк: {{ r.rows }}, +{{ r.summary.added }} ~{{ r.summary.changed }} -{{ r.summary.deleted }}</span>
@@ -109,7 +123,7 @@
{% endfor %} {% endfor %}
{% if all_rows %} {% if all_rows %}
<div class="table-wrap" style="margin-top:12px;"> <div class="table-wrap">
<table> <table>
<thead><tr><th></th><th>Услуга</th><th>Цена, руб</th><th>Кол-во</th><th>Сумма, руб</th><th>Дата</th></tr></thead> <thead><tr><th></th><th>Услуга</th><th>Цена, руб</th><th>Кол-во</th><th>Сумма, руб</th><th>Дата</th></tr></thead>
<tbody> <tbody>
@@ -131,6 +145,78 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
<script>lucide.createIcons();</script>
<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');
let fileQueue = [];
// Добавление файлов в очередь при выборе
visibleInput.addEventListener('change', () => {
for (const f of visibleInput.files) {
if (!fileQueue.find(q => q.name === f.name && q.size === f.size)) {
fileQueue.push(f);
}
}
visibleInput.value = '';
renderQueue();
});
function removeFile(index) {
fileQueue.splice(index, 1);
renderQueue();
}
function formatSize(bytes) {
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';
processBtn.disabled = true;
} else {
queueEmpty.style.display = 'none';
processBtn.disabled = false;
queueDiv.innerHTML = fileQueue.map((f, i) =>
'<div class="queue-item">' +
'<i data-lucide="file-text" style="width:16px;height:16px;color:var(--muted);"></i>' +
'<span class="name">' + f.name + '</span>' +
'<span class="size">' + formatSize(f.size) + '</span>' +
'<span class="remove" onclick="removeFile(' + i + ')"><i data-lucide="x" style="width:14px;height:14px;"></i></span>' +
'</div>'
).join('');
lucide.createIcons();
}
}
// Отправка формы: переносим файлы из очереди в скрытый input
processBtn.addEventListener('click', () => {
if (fileQueue.length === 0) return;
// Переносим File объекты в скрытый input
const dt = new DataTransfer();
fileQueue.forEach(f => dt.items.add(f));
hiddenInput.files = dt.files;
processBtn.disabled = true;
processBtn.innerHTML = '<span style="display:inline-block;width:16px;height:16px;border:2px solid rgba(255,255,255,.3);border-top-color:#fff;border-radius:50%;animation:spin .6s linear infinite;"></span> Обработка...';
document.getElementById('uploadForm').submit();
});
// Анимация спиннера
const style = document.createElement('style');
style.textContent = '@keyframes spin { to { transform: rotate(360deg); } }';
document.head.appendChild(style);
</script>
</body> </body>
</html> </html>