v1.0.178: Фаза 1.1 — statusToHTML. Статусы из HTML-строк в структуры {kind,pct,text,count,elapsed}. Чистая функция statusToHTML(st). renderTable использует statusToHTML(f.status).

This commit is contained in:
2026-06-25 08:54:39 +04:00
parent 7490568b4b
commit 9dd1afd832
+37 -14
View File
@@ -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 '<span class="status-ok">✓</span>';
case 'unzipping': return '⏳ распаковка...';
case 'parsing': return '⏳ парсинг...';
case 'parsed': return '<span class="status-ok">✓ ' + (st.count || 0) + ' эл.' + (st.elapsed ? ' (' + st.elapsed + 'с)' : '') + '</span>';
case 'error': return '<span class="status-err">✗ ' + (st.text || 'Неизвестная ошибка') + '</span>';
default: return '';
}
}
function renderTable() {
if (state.files.length === 0) {
fileTable.innerHTML = '<tr class="empty-row"><td colspan="7">Нет файлов — выберите .docx / .pdf</td></tr>';
@@ -83,7 +106,7 @@ function renderTable() {
escHtml(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">' + (f.status || '') + '</td>' +
'<td class="status-cell">' + statusToHTML(f.status) + '</td>' +
'<td><button class="info-btn' + (f.doc_id ? ' visible' : '') + '" onclick="showText(' + i + ')" title="Текст / Распарсено"><i data-lucide="file-text" style="width:16px;height:16px;"></i></button></td>' +
'<td><button class="remove-btn" onclick="removeFile(' + i + ')" title="Удалить"><i data-lucide="trash-2" style="width:16px;height:16px;"></i></button></td>' +
'</tr>' +
@@ -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 = '<span class="status-ok">✓ ' + zpr.element_count + ' эл.</span>';
state.files[zIdx].status = { kind: 'parsed', count: zpr.element_count };
} else if (zpr && zpr.status === 'error') {
state.files[zIdx].status = '<span class="status-err">✗ ' + (zpr.error || 'ошибка парсинга') + '</span>';
state.files[zIdx].status = { kind: 'error', text: zpr.error || 'ошибка парсинга' };
} else {
state.files[zIdx].status = '<span class="status-err">✗ Неизвестная ошибка</span>';
state.files[zIdx].status = { kind: 'error', text: 'Неизвестная ошибка' };
}
render(state);
}
} catch(ze) {
state.files[zipIdx].status = '<span class="status-err">✗ ZIP: ' + ze.message + '</span>';
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 = '<span class="status-ok">✓</span>';
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 = '<span class="status-ok">✓ ' + pr.element_count + ' эл. (' + elapsed + 'с)</span>';
state.files[rowIdx].status = { kind: 'parsed', count: pr.element_count, elapsed: elapsed };
} else if (pr && pr.status === 'error') {
state.files[rowIdx].status = '<span class="status-err">✗ ' + (pr.error || 'ошибка парсинга') + '</span>';
state.files[rowIdx].status = { kind: 'error', text: pr.error || 'ошибка парсинга' };
state.files[rowIdx].parseInfo = pr;
} else {
state.files[rowIdx].status = '<span class="status-err">✗ Неизвестная ошибка</span>';
state.files[rowIdx].status = { kind: 'error', text: 'Неизвестная ошибка' };
}
} catch(err) {
state.files[rowIdx].status = '<span class="status-err">✗ ' + err.message + '</span>';
state.files[rowIdx].status = { kind: 'error', text: err.message };
}
render(state);
}