fix(ui): очередь файлов — JS слой, обработка только по кнопке
- Файлы добавляются в очередь при выборе (чистый JS, без сервера) - Кнопка «Обработать» активна только когда есть файлы - Скрытая форма отправляется ТОЛЬКО по нажатию кнопки - Никакой авто-отправки, никакого LLM до нажатия
This commit is contained in:
@@ -43,7 +43,8 @@
|
||||
}
|
||||
.btn-primary { background: var(--brand-primary); border-color: var(--brand-primary); color: #fff; }
|
||||
.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 {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
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 .remove { cursor: pointer; color: var(--muted); padding: 4px; }
|
||||
.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 { 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; }
|
||||
@@ -71,34 +73,46 @@
|
||||
</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">
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
<input type="file" name="files" accept=".docx,.doc,.pdf,.zip" multiple style="margin-bottom:12px;">
|
||||
<button type="submit" class="btn btn-primary" style="width:100%;justify-content:center;">
|
||||
<!-- Скрытая форма — отправляется только по кнопке -->
|
||||
<form id="uploadForm" method="POST" enctype="multipart/form-data" style="display:none;">
|
||||
<input type="file" name="files" id="hiddenFileInput" multiple>
|
||||
</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>
|
||||
</form>
|
||||
|
||||
{% if error %}
|
||||
<div class="error-box" style="margin-top:12px;">❌ {{ error }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Результаты (после обработки) -->
|
||||
{% if results %}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<i data-lucide="file-text" style="width:18px;height:18px;"></i>
|
||||
Результаты (строк извлечено: {{ total_rows }})
|
||||
Результаты — строк извлечено: {{ total_rows }}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% for r in results %}
|
||||
<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>
|
||||
{% if r.ok %}
|
||||
<span class="size">строк: {{ r.rows }}, +{{ r.summary.added }} ~{{ r.summary.changed }} -{{ r.summary.deleted }}</span>
|
||||
@@ -109,7 +123,7 @@
|
||||
{% endfor %}
|
||||
|
||||
{% if all_rows %}
|
||||
<div class="table-wrap" style="margin-top:12px;">
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead><tr><th>№</th><th>Услуга</th><th>Цена, руб</th><th>Кол-во</th><th>Сумма, руб</th><th>Дата</th></tr></thead>
|
||||
<tbody>
|
||||
@@ -131,6 +145,78 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</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>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user