загрузка по одному файлу с XHR-прогрессом, ZIP-размеры, v1.6

This commit is contained in:
2026-06-16 15:08:09 +04:00
parent 435f617a25
commit 64b2295d8c
2 changed files with 108 additions and 33 deletions
+89 -19
View File
@@ -32,22 +32,22 @@
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: 380px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.name-cell { max-width: 360px; 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; }
.status-ok { color: var(--green); }
.status-err { color: var(--destructive); }
.summary-row td { background: rgba(34,197,94,.05); font-weight: 600; }
.empty-row td { color: var(--muted); text-align: center; padding: 24px; }
.remove-btn { cursor: pointer; color: var(--muted); background: none; border: none; padding: 2px 4px; font-size: 16px; line-height: 1; }
.remove-btn:hover { 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>
<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.5</span></span>
<span class="title">Сверка договоров <span style="font-weight:400;color:var(--muted);font-size:12px;">v1.6</span></span>
</div>
<div class="content">
@@ -62,7 +62,7 @@
<div class="table-wrap">
<table>
<thead>
<tr><th>Имя</th><th style="width:130px;">Изменён</th><th style="width:80px;">Размер</th><th style="width:100px;">Статус</th><th style="width:30px;"></th></tr>
<tr><th>Имя</th><th style="width:130px;">Изменён</th><th style="width:90px;">Размер</th><th style="width:115px;">Статус</th><th style="width:30px;"></th></tr>
</thead>
<tbody id="fileTable"></tbody>
</table>
@@ -85,6 +85,7 @@
let fileQueue = [];
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';
@@ -107,7 +108,7 @@
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 style="font-size:12px;color:var(--muted);">' + (f.size ? formatSize(f.size) : '—') + '</td>' +
'<td class="num-cell" style="font-size:12px;color:var(--muted);">' + formatSize(f.size) + '</td>' +
'<td class="status-cell"></td>' +
'<td><button class="remove-btn" onclick="removeFile(' + i + ')" title="Удалить">×</button></td>' +
'</tr>';
@@ -118,7 +119,6 @@
function removeFile(index) {
var f = fileQueue[index];
if (!f) return;
// Если удаляем ZIP — удалить и всех его детей
if (!f.isZipChild && f.file && (f.file.type === 'application/zip' || f.name.toLowerCase().endsWith('.zip'))) {
for (var i = fileQueue.length - 1; i >= 0; i--) {
if (fileQueue[i].zipName === f.name) fileQueue.splice(i, 1);
@@ -144,7 +144,6 @@
file: f
});
// ZIP: показать содержимое
if (f.type === 'application/zip' || f.name.toLowerCase().endsWith('.zip')) {
try {
var zip = await JSZip.loadAsync(f);
@@ -152,10 +151,11 @@
if (!zipEntry.dir) {
var ext = relativePath.toLowerCase().split('.').pop();
if (['docx','doc','pdf'].indexOf(ext) === -1) return;
var zsize = (zipEntry._data && zipEntry._data.uncompressedSize) ? zipEntry._data.uncompressedSize : 0;
fileQueue.push({
name: relativePath,
lastModified: zipEntry.date ? zipEntry.date.getTime() : 0,
size: 0,
size: zsize,
isZipChild: true,
zipName: f.name
});
@@ -171,6 +171,51 @@
renderTable();
});
// ── Загрузка по одному файлу ──────────────────────────────
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;
@@ -180,21 +225,39 @@
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 formData = new FormData();
realFiles.forEach(function(f) { formData.append('files', f.file); });
var contractId = null;
var contractId;
try {
var uploadResp = await fetch('/', { method: 'POST', body: formData });
var uploadJson = await uploadResp.json();
if (uploadJson.error) { alert(uploadJson.error); resetBtn(); return; }
contractId = uploadJson.contract_id;
} catch(e) {
alert('Ошибка загрузки: ' + e.message);
// Загружаем каждый файл отдельно
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);
@@ -213,6 +276,13 @@
var row = findRowByName(d.name);
if (row) {
row.querySelector('.status-cell').innerHTML = '<span class="proc-timer" data-start="' + Date.now() + '">⏳ 0с</span>';
// Обновить размер если был неизвестен
if (d.bytes > 0) {
var sizeCell = row.querySelector('.num-cell');
if (sizeCell && sizeCell.textContent.trim() === '—') {
sizeCell.textContent = formatSize(d.bytes);
}
}
}
}
@@ -246,7 +316,7 @@
summaryRow.className = 'summary-row';
summaryRow.innerHTML = '<td>✓ Готово: ' + d.files_processed + ' файлов</td>' +
'<td></td>' +
'<td>' + formatSize(d.total_bytes) + '</td>' +
'<td class="num-cell">' + formatSize(d.total_bytes) + '</td>' +
'<td><strong>' + d.total_time_s + 'с</strong></td>' +
'<td></td>';
fileTable.appendChild(summaryRow);