feat: 添加 AI 入库预处理功能,支持数据结构化和异常行导出

This commit is contained in:
2026-03-12 14:59:02 +08:00
parent d2dc25eb09
commit ef0af75193
4 changed files with 472 additions and 9 deletions

View File

@@ -76,12 +76,34 @@
<h2 id="quick-inbound-title">快速入库</h2>
<button class="btn btn-light" type="button" id="close-quick-inbound">关闭</button>
</div>
<p class="hint">每行一条: 料号, 名称, 数量, 规格, 备注。支持英文逗号或Tab分隔检测到同料号或同参数时不会自动合并,需要人工确认</p>
<form method="post" action="{{ url_for('quick_inbound', box_id=box.id) }}">
<textarea class="batch-input" name="lines" rows="8" placeholder="10K-0603, 电阻10K 0603, 500, 1%, 常用\n100nF-0603, 电容100nF 0603, 300, 50V X7R, 去耦"></textarea>
<p class="hint">每行一条: 料号, 名称, 数量, 规格, 备注。支持英文逗号或Tab分隔先点“AI预处理”查看结构化结果再确认导入</p>
<form method="post" id="quick-inbound-form" action="{% if box.box_type == 'bag' %}{{ url_for('add_bag_items_batch', box_id=box.id) }}{% else %}{{ url_for('quick_inbound', box_id=box.id) }}{% endif %}">
<input type="hidden" id="ai-inbound-mode" value="{% if box.box_type == 'bag' %}bag{% else %}box{% endif %}">
<textarea class="batch-input" id="quick-inbound-lines" name="lines" rows="8" placeholder="10K-0603, 电阻10K 0603, 500, 1%, 常用\n100nF-0603, 电容100nF 0603, 300, 50V X7R, 去耦"></textarea>
<p class="hint">建议: part_no 用厂家型号name 用品类+型号specification 只写关键参数。</p>
<p class="hint" id="ai-inbound-status" aria-live="polite"></p>
<section class="ai-preview" id="ai-inbound-preview" hidden>
<div class="table-wrap">
<table>
<thead>
<tr>
<th></th>
<th>料号</th>
<th>名称</th>
<th>数量</th>
<th>规格</th>
<th>备注</th>
<th>识别提示</th>
</tr>
</thead>
<tbody id="ai-inbound-preview-body"></tbody>
</table>
</div>
</section>
<div class="actions">
<button class="btn" type="submit">批量快速入库</button>
<button class="btn btn-light" type="button" id="ai-inbound-parse-btn">AI预处理并预览</button>
<button class="btn btn-light" type="button" id="ai-inbound-export-invalid-btn" disabled>导出异常行</button>
<button class="btn" type="submit">确认导入</button>
</div>
</form>
</div>
@@ -217,6 +239,184 @@
});
});
})();
(function () {
var parseBtn = document.getElementById('ai-inbound-parse-btn');
var exportInvalidBtn = document.getElementById('ai-inbound-export-invalid-btn');
var textarea = document.getElementById('quick-inbound-lines');
var modeInput = document.getElementById('ai-inbound-mode');
var status = document.getElementById('ai-inbound-status');
var preview = document.getElementById('ai-inbound-preview');
var previewBody = document.getElementById('ai-inbound-preview-body');
var latestRows = [];
if (!parseBtn || !textarea || !status || !preview || !previewBody || !exportInvalidBtn) {
return;
}
function escapeHtml(text) {
return String(text || '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function renderRows(rows) {
latestRows = Array.isArray(rows) ? rows : [];
var invalidCount = latestRows.filter(function (row) {
return !row.is_valid;
}).length;
exportInvalidBtn.disabled = invalidCount <= 0;
if (!Array.isArray(rows) || !rows.length) {
preview.hidden = true;
previewBody.innerHTML = '';
return;
}
var html = rows.map(function (row) {
var messages = [];
(row.errors || []).forEach(function (msg) {
messages.push('错误: ' + msg);
});
(row.warnings || []).forEach(function (msg) {
messages.push('提示: ' + msg);
});
var tips = messages.length ? messages.join('<br>') : '正常';
var rowClass = row.is_valid ? '' : ' class="row-invalid"';
return '<tr' + rowClass + '>'
+ '<td>' + escapeHtml(row.line_no) + '</td>'
+ '<td>' + escapeHtml(row.part_no) + '</td>'
+ '<td>' + escapeHtml(row.name) + '</td>'
+ '<td>' + escapeHtml(row.quantity) + '</td>'
+ '<td>' + escapeHtml(row.specification) + '</td>'
+ '<td>' + escapeHtml(row.note) + '</td>'
+ '<td>' + tips + '</td>'
+ '</tr>';
}).join('');
previewBody.innerHTML = html;
preview.hidden = false;
}
parseBtn.addEventListener('click', function () {
var lines = (textarea.value || '').trim();
if (!lines) {
status.textContent = '请先粘贴至少一行内容';
status.classList.remove('ok');
return;
}
parseBtn.disabled = true;
exportInvalidBtn.disabled = true;
status.textContent = '正在进行 AI 预处理...';
status.classList.remove('ok');
var payload = new URLSearchParams();
payload.set('lines', lines);
payload.set('mode', modeInput ? modeInput.value : 'box');
fetch('{{ url_for('ai_inbound_parse') }}', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
},
body: payload.toString()
}).then(function (resp) {
return resp.json().then(function (data) {
if (!resp.ok || !data.ok) {
throw new Error(data.message || '预处理失败');
}
return data;
});
}).then(function (data) {
renderRows(data.rows || []);
if (data.normalized_lines) {
textarea.value = data.normalized_lines;
}
var baseText = '预处理完成: 有效 ' + (data.valid_count || 0) + ' 行';
if (data.invalid_count) {
baseText += ',异常 ' + data.invalid_count + ' 行';
}
if (data.parse_notice) {
baseText += '。' + data.parse_notice;
}
status.textContent = baseText;
status.classList.add('ok');
}).catch(function (error) {
status.textContent = '预处理失败: ' + error.message;
status.classList.remove('ok');
}).finally(function () {
parseBtn.disabled = false;
});
});
exportInvalidBtn.addEventListener('click', function () {
var invalidRows = latestRows.filter(function (row) {
return !row.is_valid;
});
if (!invalidRows.length) {
status.textContent = '当前没有异常行可导出';
status.classList.remove('ok');
return;
}
var headers = ['line_no', 'raw', 'part_no', 'name', 'quantity', 'specification', 'note', 'errors', 'warnings'];
var rows = [headers];
invalidRows.forEach(function (row) {
rows.push([
row.line_no || '',
row.raw || '',
row.part_no || '',
row.name || '',
row.quantity || 0,
row.specification || '',
row.note || '',
(row.errors || []).join(' | '),
(row.warnings || []).join(' | ')
]);
});
function csvCell(value) {
var text = String(value == null ? '' : value);
if (/[",\n]/.test(text)) {
return '"' + text.replace(/"/g, '""') + '"';
}
return text;
}
var csv = rows.map(function (cols) {
return cols.map(csvCell).join(',');
}).join('\n');
var blob = new Blob(['\ufeff' + csv], { type: 'text/csv;charset=utf-8;' });
var url = URL.createObjectURL(blob);
var now = new Date();
var stamp = now.getFullYear()
+ String(now.getMonth() + 1).padStart(2, '0')
+ String(now.getDate()).padStart(2, '0')
+ '_'
+ String(now.getHours()).padStart(2, '0')
+ String(now.getMinutes()).padStart(2, '0')
+ String(now.getSeconds()).padStart(2, '0');
var link = document.createElement('a');
link.href = url;
link.download = 'inbound_invalid_rows_' + stamp + '.csv';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
status.textContent = '已导出异常行 ' + invalidRows.length + ' 条';
status.classList.add('ok');
});
})();
</script>
</body>
</html>