загрузка при выборе файла: прогресс, таймер, блокировка input, v1.13
This commit is contained in:
+112
-121
@@ -32,7 +32,7 @@
|
||||
th { background: var(--brand-grey-light); text-transform: uppercase; padding: 6px 10px; border-right: 1px solid var(--brand-gray); text-align: left; font-weight: 600; font-size: 11px; color: var(--muted); }
|
||||
td { padding: 6px 10px; border-right: 1px solid var(--brand-gray); border-bottom: 1px solid var(--brand-gray); }
|
||||
tr:hover td { background: rgba(243,244,246,.5); }
|
||||
.name-cell { max-width: 360px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.name-cell { max-width: 340px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.zip-child .name-cell { padding-left: 28px; }
|
||||
.zip-child .name-cell::before { content: "└ "; color: var(--muted); }
|
||||
.num-cell { text-align: right; white-space: nowrap; }
|
||||
@@ -45,7 +45,6 @@
|
||||
.info-btn { cursor: pointer; color: var(--muted); background: none; border: none; padding: 2px 4px; font-size: 14px; display: none; }
|
||||
.info-btn:hover { color: var(--brand-primary); }
|
||||
.info-btn.visible { display: inline; }
|
||||
/* Modal */
|
||||
.modal-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,.4); z-index: 100; justify-content: center; align-items: center; }
|
||||
.modal-overlay.open { display: flex; }
|
||||
.modal { background: var(--background); border-radius: 12px; border: 1px solid var(--brand-gray); box-shadow: 0 4px 24px rgba(0,0,0,.15); max-width: 520px; width: 90%; max-height: 80vh; overflow-y: auto; }
|
||||
@@ -60,7 +59,7 @@
|
||||
<body>
|
||||
<div class="topbar">
|
||||
<img src="{{ url_for('static', filename='nubes-logo.svg') }}" alt="Nubes">
|
||||
<span class="title">Сверка договоров <span style="font-weight:400;color:var(--muted);font-size:12px;">v1.12</span></span>
|
||||
<span class="title">Сверка договоров <span style="font-weight:400;color:var(--muted);font-size:12px;">v1.13</span></span>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
@@ -88,7 +87,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно Инфо -->
|
||||
<div class="modal-overlay" id="modalOverlay" onclick="closeModal(event)">
|
||||
<div class="modal" onclick="event.stopPropagation()">
|
||||
<div class="modal-header">
|
||||
@@ -108,6 +106,7 @@
|
||||
const fileTable = document.getElementById('fileTable');
|
||||
|
||||
let fileQueue = [];
|
||||
let contractId = null;
|
||||
|
||||
function formatSize(bytes) {
|
||||
if (!bytes || bytes === 0) return '—';
|
||||
@@ -127,14 +126,13 @@
|
||||
fileTable.innerHTML = '<tr class="empty-row"><td colspan="5">Нет файлов — выберите .docx / .pdf / .zip</td></tr>';
|
||||
parseBtn.disabled = true;
|
||||
} else {
|
||||
parseBtn.disabled = false;
|
||||
fileTable.innerHTML = fileQueue.map(function(f, i) {
|
||||
var cls = f.isZipChild ? 'zip-child' : '';
|
||||
return '<tr class="' + cls + '" id="row_' + i + '">' +
|
||||
'<td class="name-cell">' + f.name + '</td>' +
|
||||
'<td style="font-size:12px;color:var(--muted);">' + formatDate(f.lastModified) + '</td>' +
|
||||
'<td class="num-cell" style="font-size:12px;color:var(--muted);">' + formatSize(f.size) + '</td>' +
|
||||
'<td class="status-cell"></td>' +
|
||||
'<td class="status-cell">' + (f.status || '') + '</td>' +
|
||||
'<td><button class="remove-btn" onclick="removeFile(' + i + ')" title="Удалить">×</button>' +
|
||||
'<button class="info-btn" id="info_' + i + '" onclick="showInfo(' + i + ')" title="Инфо">ℹ</button></td>' +
|
||||
'</tr>';
|
||||
@@ -145,34 +143,123 @@
|
||||
function removeFile(index) {
|
||||
var f = fileQueue[index];
|
||||
if (!f) return;
|
||||
if (!f.isZipChild && f.file && (f.file.type === 'application/zip' || f.name.toLowerCase().endsWith('.zip'))) {
|
||||
if (!f.isZipChild && (f.name||'').toLowerCase().endsWith('.zip')) {
|
||||
for (var i = fileQueue.length - 1; i >= 0; i--) {
|
||||
if (fileQueue[i].zipName === f.name) fileQueue.splice(i, 1);
|
||||
}
|
||||
}
|
||||
fileQueue.splice(index, 1);
|
||||
if (fileQueue.filter(function(x){return !x.isZipChild;}).length === 0) contractId = null;
|
||||
renderTable();
|
||||
}
|
||||
|
||||
window.removeFile = removeFile;
|
||||
|
||||
// ── Загрузка файла (XHR с прогрессом) ──────────────────────
|
||||
|
||||
function uploadFile(file, cid) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
var url = '/' + (cid ? '?cid=' + cid : '');
|
||||
xhr.open('POST', url);
|
||||
xhr.timeout = 120000;
|
||||
|
||||
xhr.upload.onprogress = function(e) {
|
||||
if (e.lengthComputable) {
|
||||
var pct = Math.round(e.loaded / e.total * 100);
|
||||
resolve({_progress: true, pct: pct});
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onload = function() {
|
||||
if (xhr.status === 200) {
|
||||
try { resolve(JSON.parse(xhr.responseText)); }
|
||||
catch(e) { reject(new Error('Bad JSON')); }
|
||||
} else {
|
||||
reject(new Error('HTTP ' + xhr.status));
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onerror = function() { reject(new Error('Сеть')); };
|
||||
xhr.ontimeout = function() { reject(new Error('Таймаут')); };
|
||||
|
||||
var fd = new FormData();
|
||||
fd.append('files', file);
|
||||
xhr.send(fd);
|
||||
});
|
||||
}
|
||||
|
||||
// ── Выбор файла → сразу загрузка ────────────────────────────
|
||||
|
||||
fileInput.addEventListener('change', async function() {
|
||||
var newFiles = Array.from(fileInput.files);
|
||||
if (newFiles.length === 0) return;
|
||||
|
||||
fileInput.disabled = true;
|
||||
parseBtn.disabled = true;
|
||||
parseBtn.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> Загрузка...';
|
||||
|
||||
for (var i = 0; i < newFiles.length; i++) {
|
||||
var f = newFiles[i];
|
||||
if (fileQueue.find(function(q) { return q.name === f.name && q.size === f.size; })) continue;
|
||||
if (fileQueue.find(function(q) { return q.name === f.name && q.size === f.size && !q.isZipChild; })) continue;
|
||||
|
||||
fileQueue.push({
|
||||
name: f.name,
|
||||
lastModified: f.lastModified,
|
||||
size: f.size,
|
||||
file: f
|
||||
// Добавить строку с прогрессом
|
||||
var entry = { name: f.name, lastModified: f.lastModified, size: f.size, status: '<span style="color:var(--muted);">↑ 0%</span>' };
|
||||
fileQueue.push(entry);
|
||||
var rowIdx = fileQueue.length - 1;
|
||||
renderTable();
|
||||
var row = document.getElementById('row_' + rowIdx);
|
||||
|
||||
var startTime = Date.now();
|
||||
|
||||
// Загружаем
|
||||
var lastPct = 0;
|
||||
var done = false;
|
||||
|
||||
uploadFile(f, contractId).then(
|
||||
function(res) {
|
||||
if (done) return;
|
||||
if (res._progress) {
|
||||
var pct = res.pct;
|
||||
if (pct !== lastPct) {
|
||||
lastPct = pct;
|
||||
fileQueue[rowIdx].status = '<span style="color:var(--muted);">↑ ' + pct + '%</span>';
|
||||
renderTable();
|
||||
}
|
||||
return; // ждём следующий прогресс или завершение
|
||||
}
|
||||
// Завершено
|
||||
done = true;
|
||||
contractId = res.contract_id;
|
||||
var elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
||||
fileQueue[rowIdx].status = '<span class="status-ok">✓ ' + elapsed + 'с</span>';
|
||||
fileQueue[rowIdx].uploaded = true;
|
||||
renderTable();
|
||||
|
||||
// ZIP: показать содержимое
|
||||
if (f.type === 'application/zip' || f.name.toLowerCase().endsWith('.zip')) {
|
||||
showZipContents(f);
|
||||
}
|
||||
},
|
||||
function(err) {
|
||||
if (done) return;
|
||||
done = true;
|
||||
fileQueue[rowIdx].status = '<span class="status-err">✗ ' + err.message + '</span>';
|
||||
renderTable();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
fileInput.value = '';
|
||||
fileInput.disabled = false;
|
||||
parseBtn.disabled = false;
|
||||
parseBtn.innerHTML = '<i data-lucide="play" style="width:16px;height:16px;"></i> Парсинг';
|
||||
lucide.createIcons();
|
||||
});
|
||||
|
||||
if (f.type === 'application/zip' || f.name.toLowerCase().endsWith('.zip')) {
|
||||
async function showZipContents(zipFile) {
|
||||
try {
|
||||
var zip = await JSZip.loadAsync(f);
|
||||
var zip = await JSZip.loadAsync(zipFile);
|
||||
zip.forEach(function(relativePath, zipEntry) {
|
||||
if (!zipEntry.dir) {
|
||||
var ext = relativePath.toLowerCase().split('.').pop();
|
||||
@@ -182,108 +269,24 @@
|
||||
name: relativePath,
|
||||
lastModified: zipEntry.date ? zipEntry.date.getTime() : 0,
|
||||
size: zsize,
|
||||
status: '',
|
||||
isZipChild: true,
|
||||
zipName: f.name
|
||||
zipName: zipFile.name
|
||||
});
|
||||
}
|
||||
});
|
||||
renderTable();
|
||||
} catch(e) {
|
||||
console.log('JSZip error:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileInput.value = '';
|
||||
renderTable();
|
||||
});
|
||||
// ── Парсинг (SSE) ────────────────────────────────────────────
|
||||
|
||||
// ── Загрузка по одному файлу ──────────────────────────────
|
||||
|
||||
function uploadOne(file, cid, row) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
var url = '/' + (cid ? '?cid=' + cid : '');
|
||||
xhr.open('POST', url);
|
||||
|
||||
xhr.upload.onprogress = function(e) {
|
||||
if (e.lengthComputable && row) {
|
||||
var pct = Math.round(e.loaded / e.total * 100);
|
||||
row.querySelector('.status-cell').innerHTML = '<span style="color:var(--muted);">↑ ' + pct + '%</span>';
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onload = function() {
|
||||
if (xhr.status === 200) {
|
||||
try {
|
||||
resolve(JSON.parse(xhr.responseText));
|
||||
} catch(e) {
|
||||
reject(new Error('Bad JSON'));
|
||||
}
|
||||
} else {
|
||||
reject(new Error('HTTP ' + xhr.status));
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onerror = function() {
|
||||
reject(new Error('Сеть'));
|
||||
};
|
||||
|
||||
xhr.ontimeout = function() {
|
||||
reject(new Error('Таймаут'));
|
||||
};
|
||||
|
||||
xhr.timeout = 60000;
|
||||
|
||||
var fd = new FormData();
|
||||
fd.append('files', file);
|
||||
xhr.send(fd);
|
||||
});
|
||||
}
|
||||
|
||||
// ── Парсинг ────────────────────────────────────────────────
|
||||
|
||||
parseBtn.addEventListener('click', async function() {
|
||||
if (fileQueue.length === 0) return;
|
||||
|
||||
var realFiles = fileQueue.filter(function(f) { return !f.isZipChild; });
|
||||
if (realFiles.length === 0) return;
|
||||
parseBtn.addEventListener('click', function() {
|
||||
if (!contractId) return;
|
||||
|
||||
parseBtn.disabled = true;
|
||||
parseBtn.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> Загрузка...';
|
||||
|
||||
var contractId = null;
|
||||
|
||||
// Загружаем каждый файл отдельно
|
||||
for (var i = 0; i < realFiles.length; i++) {
|
||||
var f = realFiles[i];
|
||||
var row = findRowByName(f.name);
|
||||
|
||||
if (row) {
|
||||
row.querySelector('.status-cell').innerHTML = '<span style="color:var(--muted);">↑ 0%</span>';
|
||||
}
|
||||
|
||||
try {
|
||||
var resp = await uploadOne(f.file, contractId, row);
|
||||
contractId = resp.contract_id;
|
||||
|
||||
if (row) {
|
||||
row.querySelector('.status-cell').innerHTML = '<span style="color:var(--green);">✓ загружен</span>';
|
||||
}
|
||||
} catch(e) {
|
||||
if (row) {
|
||||
row.querySelector('.status-cell').innerHTML = '<span class="status-err">✗ ' + e.message + '</span>';
|
||||
}
|
||||
resetBtn();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!contractId) {
|
||||
resetBtn();
|
||||
return;
|
||||
}
|
||||
|
||||
// Фаза парсинга — SSE
|
||||
parseBtn.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> Парсинг...';
|
||||
|
||||
var es = new EventSource('/parse/' + contractId);
|
||||
@@ -301,8 +304,7 @@
|
||||
if (d.type === 'file_start') {
|
||||
var row = findRowByName(d.name);
|
||||
if (row) {
|
||||
row.querySelector('.status-cell').innerHTML = '<span class="proc-timer" data-start="' + Date.now() + '">⏳ 0с</span>';
|
||||
// Обновить размер если был неизвестен
|
||||
row.querySelector('.status-cell').innerHTML = '<span class="proc-timer" data-start="' + Date.now() + '">⏳ 0.0с</span>';
|
||||
if (d.bytes > 0) {
|
||||
var sizeCell = row.querySelector('.num-cell');
|
||||
if (sizeCell && sizeCell.textContent.trim() === '—') {
|
||||
@@ -316,15 +318,12 @@
|
||||
var row = findRowByName(d.name);
|
||||
if (row) {
|
||||
row.querySelector('.status-cell').innerHTML = '<span class="status-ok">✓ ' + d.time_s + 'с</span>';
|
||||
// Сохранить детали в fileQueue и показать ℹ
|
||||
var idx = findIndexByName(d.name);
|
||||
if (idx >= 0) {
|
||||
fileQueue[idx].parseInfo = d;
|
||||
var infoBtn = document.getElementById('info_' + idx);
|
||||
if (idx >= 0) { fileQueue[idx].parseInfo = d; }
|
||||
var infoBtn = document.getElementById('info_' + (idx >= 0 ? idx : ''));
|
||||
if (infoBtn) infoBtn.classList.add('visible');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else if (d.type === 'file_error') {
|
||||
var row = findRowByName(d.name);
|
||||
@@ -343,8 +342,6 @@
|
||||
else if (d.type === 'summary') {
|
||||
clearInterval(timerInterval);
|
||||
es.close();
|
||||
|
||||
// Идемпотентность: больше нельзя запустить
|
||||
parseBtn.disabled = true;
|
||||
parseBtn.innerHTML = '<i data-lucide="check" style="width:16px;height:16px;"></i> ✓ Готово';
|
||||
lucide.createIcons();
|
||||
@@ -363,14 +360,10 @@
|
||||
es.addEventListener('error', function() {
|
||||
clearInterval(timerInterval);
|
||||
es.close();
|
||||
resetBtn();
|
||||
});
|
||||
|
||||
function resetBtn() {
|
||||
parseBtn.disabled = false;
|
||||
parseBtn.innerHTML = '<i data-lucide="play" style="width:16px;height:16px;"></i> Парсинг';
|
||||
lucide.createIcons();
|
||||
}
|
||||
});
|
||||
|
||||
function findRowByName(name) {
|
||||
for (var i = 0; i < fileQueue.length; i++) {
|
||||
@@ -399,10 +392,8 @@
|
||||
document.getElementById('modalTitle').textContent = f.name;
|
||||
|
||||
var rows = [
|
||||
['Тип', f.file ? f.file.type : (f.name.split('.').pop() || '—')],
|
||||
['Размер', formatSize(f.size)],
|
||||
['В архиве', f.zipName || '—'],
|
||||
['Статус', 'parsed'],
|
||||
['Время парсинга', d.time_s + 'с'],
|
||||
['Элементов всего', d.elements],
|
||||
['Параграфов', d.paragraphs],
|
||||
|
||||
Reference in New Issue
Block a user