v1.0.178: Фаза 1.1 — statusToHTML. Статусы из HTML-строк в структуры {kind,pct,text,count,elapsed}. Чистая функция statusToHTML(st). renderTable использует statusToHTML(f.status).
This commit is contained in:
+37
-14
@@ -72,6 +72,29 @@ function resetStepper(fromId) {
|
|||||||
// Утилиты (formatSize, formatDate, moveUp, moveDown, escHtml, fmtDate) —
|
// Утилиты (formatSize, formatDate, moveUp, moveDown, escHtml, fmtDate) —
|
||||||
// вынесены в app_utils.js для облегчения анализа и отладки.
|
// вынесены в 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() {
|
function renderTable() {
|
||||||
if (state.files.length === 0) {
|
if (state.files.length === 0) {
|
||||||
fileTable.innerHTML = '<tr class="empty-row"><td colspan="7">Нет файлов — выберите .docx / .pdf</td></tr>';
|
fileTable.innerHTML = '<tr class="empty-row"><td colspan="7">Нет файлов — выберите .docx / .pdf</td></tr>';
|
||||||
@@ -83,7 +106,7 @@ function renderTable() {
|
|||||||
escHtml(f.name) + '</td>' +
|
escHtml(f.name) + '</td>' +
|
||||||
'<td style="font-size:12px;color:var(--muted);">' + formatDate(f.lastModified) + '</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="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="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>' +
|
'<td><button class="remove-btn" onclick="removeFile(' + i + ')" title="Удалить"><i data-lucide="trash-2" style="width:16px;height:16px;"></i></button></td>' +
|
||||||
'</tr>' +
|
'</tr>' +
|
||||||
@@ -182,7 +205,7 @@ fileInput.addEventListener('change', async function() {
|
|||||||
|
|
||||||
if (isZip) {
|
if (isZip) {
|
||||||
// ZIP: показать временную строку, распаковать, убрать
|
// 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);
|
state.files.push(zipEntry);
|
||||||
var zipIdx = state.files.length - 1;
|
var zipIdx = state.files.length - 1;
|
||||||
render(state);
|
render(state);
|
||||||
@@ -220,7 +243,7 @@ fileInput.addEventListener('change', async function() {
|
|||||||
var zEntry = {
|
var zEntry = {
|
||||||
name: zf.filename, size: zf.size,
|
name: zf.filename, size: zf.size,
|
||||||
doc_id: zf.doc_id, uploaded: true,
|
doc_id: zf.doc_id, uploaded: true,
|
||||||
status: '⏳ парсинг...'
|
status: { kind: 'parsing' }
|
||||||
};
|
};
|
||||||
state.files.push(zEntry);
|
state.files.push(zEntry);
|
||||||
var zIdx = state.files.length - 1;
|
var zIdx = state.files.length - 1;
|
||||||
@@ -231,23 +254,23 @@ fileInput.addEventListener('change', async function() {
|
|||||||
var zpr = zf.parsed;
|
var zpr = zf.parsed;
|
||||||
if (zpr && zpr.status === 'parsed') {
|
if (zpr && zpr.status === 'parsed') {
|
||||||
state.files[zIdx].parsed = true;
|
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') {
|
} 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 {
|
} else {
|
||||||
state.files[zIdx].status = '<span class="status-err">✗ Неизвестная ошибка</span>';
|
state.files[zIdx].status = { kind: 'error', text: 'Неизвестная ошибка' };
|
||||||
}
|
}
|
||||||
render(state);
|
render(state);
|
||||||
}
|
}
|
||||||
} catch(ze) {
|
} 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);
|
render(state);
|
||||||
}
|
}
|
||||||
continue; // ZIP обработан, переходим к следующему файлу
|
continue; // ZIP обработан, переходим к следующему файлу
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обычный файл (не 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;
|
var existingIdx = -1;
|
||||||
for (var j = 0; j < state.files.length; j++) {
|
for (var j = 0; j < state.files.length; j++) {
|
||||||
if (state.files[j].name === f.name) { existingIdx = j; break; }
|
if (state.files[j].name === f.name) { existingIdx = j; break; }
|
||||||
@@ -264,13 +287,13 @@ fileInput.addEventListener('change', async function() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
var resp = await uploadFile(f, function(pct) {
|
var resp = await uploadFile(f, function(pct) {
|
||||||
state.files[rowIdx].status = '↑ ' + pct + '%';
|
state.files[rowIdx].status = { kind: 'uploading', pct: pct };
|
||||||
render(state);
|
render(state);
|
||||||
});
|
});
|
||||||
// Python API returns lowercase keys (doc_id, contract_id, parsed)
|
// Python API returns lowercase keys (doc_id, contract_id, parsed)
|
||||||
if (resp && resp.contract_id) state.contractId = resp.contract_id;
|
if (resp && resp.contract_id) state.contractId = resp.contract_id;
|
||||||
state.files[rowIdx].doc_id = resp.doc_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;
|
state.files[rowIdx].uploaded = true;
|
||||||
|
|
||||||
// Авто-парсинг: VM Python парсит ВСЁ (PDF + DOCX + DOC)
|
// Авто-парсинг: VM Python парсит ВСЁ (PDF + DOCX + DOC)
|
||||||
@@ -280,15 +303,15 @@ fileInput.addEventListener('change', async function() {
|
|||||||
var elapsed = ((Date.now() - t0) / 1000).toFixed(1);
|
var elapsed = ((Date.now() - t0) / 1000).toFixed(1);
|
||||||
state.files[rowIdx].parsed = true;
|
state.files[rowIdx].parsed = true;
|
||||||
state.files[rowIdx].parseInfo = pr;
|
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') {
|
} 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;
|
state.files[rowIdx].parseInfo = pr;
|
||||||
} else {
|
} else {
|
||||||
state.files[rowIdx].status = '<span class="status-err">✗ Неизвестная ошибка</span>';
|
state.files[rowIdx].status = { kind: 'error', text: 'Неизвестная ошибка' };
|
||||||
}
|
}
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
state.files[rowIdx].status = '<span class="status-err">✗ ' + err.message + '</span>';
|
state.files[rowIdx].status = { kind: 'error', text: err.message };
|
||||||
}
|
}
|
||||||
render(state);
|
render(state);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user