From 9dd1afd832a2222fa1aa1731ef92ee19372af299 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Thu, 25 Jun 2026 08:54:39 +0400 Subject: [PATCH] =?UTF-8?q?v1.0.178:=20=D0=A4=D0=B0=D0=B7=D0=B0=201.1=20?= =?UTF-8?q?=E2=80=94=20statusToHTML.=20=D0=A1=D1=82=D0=B0=D1=82=D1=83?= =?UTF-8?q?=D1=81=D1=8B=20=D0=B8=D0=B7=20HTML-=D1=81=D1=82=D1=80=D0=BE?= =?UTF-8?q?=D0=BA=20=D0=B2=20=D1=81=D1=82=D1=80=D1=83=D0=BA=D1=82=D1=83?= =?UTF-8?q?=D1=80=D1=8B=20{kind,pct,text,count,elapsed}.=20=D0=A7=D0=B8?= =?UTF-8?q?=D1=81=D1=82=D0=B0=D1=8F=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8?= =?UTF-8?q?=D1=8F=20statusToHTML(st).=20renderTable=20=D0=B8=D1=81=D0=BF?= =?UTF-8?q?=D0=BE=D0=BB=D1=8C=D0=B7=D1=83=D0=B5=D1=82=20statusToHTML(f.sta?= =?UTF-8?q?tus).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/app.js | 51 +++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 37 insertions(+), 14 deletions(-) diff --git a/deploy/app.js b/deploy/app.js index 3d5aa01..bec846d 100644 --- a/deploy/app.js +++ b/deploy/app.js @@ -72,6 +72,29 @@ function resetStepper(fromId) { // Утилиты (formatSize, formatDate, moveUp, moveDown, escHtml, fmtDate) — // вынесены в app_utils.js для облегчения анализа и отладки. +/** + * statusToHTML(status) — Чистая функция: структура → HTML (Фаза 1). + * + * Вход: { kind, pct?, text?, count?, elapsed? } — ни одного HTML-тега. + * kind = 'uploading' | 'uploaded' | 'unzipping' | 'parsing' | 'parsed' | 'error' | '' + * Выход: безопасная HTML-строка (статусы не содержат пользовательских данных). + * + * ПАТТЕРН (decoupling-final-plan.md): отделяем данные от представления. + * state хранит чистые данные, statusToHTML — чистая функция рендеринга. + */ +function statusToHTML(st) { + if (!st || !st.kind) return ''; + switch (st.kind) { + case 'uploading': return '↑ ' + (st.pct || 0) + '%'; + case 'uploaded': return ''; + case 'unzipping': return '⏳ распаковка...'; + case 'parsing': return '⏳ парсинг...'; + case 'parsed': return '✓ ' + (st.count || 0) + ' эл.' + (st.elapsed ? ' (' + st.elapsed + 'с)' : '') + ''; + case 'error': return '✗ ' + (st.text || 'Неизвестная ошибка') + ''; + default: return ''; + } +} + function renderTable() { if (state.files.length === 0) { fileTable.innerHTML = 'Нет файлов — выберите .docx / .pdf'; @@ -83,7 +106,7 @@ function renderTable() { escHtml(f.name) + '' + '' + formatDate(f.lastModified) + '' + '' + formatSize(f.size) + '' + - '' + (f.status || '') + '' + + '' + statusToHTML(f.status) + '' + '' + '' + '' + @@ -182,7 +205,7 @@ fileInput.addEventListener('change', async function() { if (isZip) { // ZIP: показать временную строку, распаковать, убрать - var zipEntry = { name: f.name, lastModified: f.lastModified, size: f.size, status: '⏳ распаковка...' }; + var zipEntry = { name: f.name, lastModified: f.lastModified, size: f.size, status: { kind: 'unzipping' } }; state.files.push(zipEntry); var zipIdx = state.files.length - 1; render(state); @@ -220,7 +243,7 @@ fileInput.addEventListener('change', async function() { var zEntry = { name: zf.filename, size: zf.size, doc_id: zf.doc_id, uploaded: true, - status: '⏳ парсинг...' + status: { kind: 'parsing' } }; state.files.push(zEntry); var zIdx = state.files.length - 1; @@ -231,23 +254,23 @@ fileInput.addEventListener('change', async function() { var zpr = zf.parsed; if (zpr && zpr.status === 'parsed') { state.files[zIdx].parsed = true; - state.files[zIdx].status = '✓ ' + zpr.element_count + ' эл.'; + state.files[zIdx].status = { kind: 'parsed', count: zpr.element_count }; } else if (zpr && zpr.status === 'error') { - state.files[zIdx].status = '✗ ' + (zpr.error || 'ошибка парсинга') + ''; + state.files[zIdx].status = { kind: 'error', text: zpr.error || 'ошибка парсинга' }; } else { - state.files[zIdx].status = '✗ Неизвестная ошибка'; + state.files[zIdx].status = { kind: 'error', text: 'Неизвестная ошибка' }; } render(state); } } catch(ze) { - state.files[zipIdx].status = '✗ ZIP: ' + ze.message + ''; + state.files[zipIdx].status = { kind: 'error', text: 'ZIP: ' + ze.message }; render(state); } continue; // ZIP обработан, переходим к следующему файлу } // Обычный файл (не ZIP) - var entry = { name: f.name, lastModified: f.lastModified, size: f.size, file: f, status: '↑ 0%' }; + var entry = { name: f.name, lastModified: f.lastModified, size: f.size, file: f, status: { kind: 'uploading', pct: 0 } }; var existingIdx = -1; for (var j = 0; j < state.files.length; j++) { if (state.files[j].name === f.name) { existingIdx = j; break; } @@ -264,13 +287,13 @@ fileInput.addEventListener('change', async function() { try { var resp = await uploadFile(f, function(pct) { - state.files[rowIdx].status = '↑ ' + pct + '%'; + state.files[rowIdx].status = { kind: 'uploading', pct: pct }; render(state); }); // Python API returns lowercase keys (doc_id, contract_id, parsed) if (resp && resp.contract_id) state.contractId = resp.contract_id; state.files[rowIdx].doc_id = resp.doc_id; - state.files[rowIdx].status = ''; + state.files[rowIdx].status = { kind: 'uploaded' }; state.files[rowIdx].uploaded = true; // Авто-парсинг: VM Python парсит ВСЁ (PDF + DOCX + DOC) @@ -280,15 +303,15 @@ fileInput.addEventListener('change', async function() { var elapsed = ((Date.now() - t0) / 1000).toFixed(1); state.files[rowIdx].parsed = true; state.files[rowIdx].parseInfo = pr; - state.files[rowIdx].status = '✓ ' + pr.element_count + ' эл. (' + elapsed + 'с)'; + state.files[rowIdx].status = { kind: 'parsed', count: pr.element_count, elapsed: elapsed }; } else if (pr && pr.status === 'error') { - state.files[rowIdx].status = '✗ ' + (pr.error || 'ошибка парсинга') + ''; + state.files[rowIdx].status = { kind: 'error', text: pr.error || 'ошибка парсинга' }; state.files[rowIdx].parseInfo = pr; } else { - state.files[rowIdx].status = '✗ Неизвестная ошибка'; + state.files[rowIdx].status = { kind: 'error', text: 'Неизвестная ошибка' }; } } catch(err) { - state.files[rowIdx].status = '✗ ' + err.message + ''; + state.files[rowIdx].status = { kind: 'error', text: err.message }; } render(state); }