feat: 添加 AI 入库预处理功能,支持数据结构化和异常行导出
This commit is contained in:
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user