fix: текстовый BOM работает в пасте конфигуратора через единый серверный парсер
Паста BOM на странице конфигурирования теперь распознаёт текстовый и
Inspur-форматы: вместо дублирования парсера на JS добавлен stateless
эндпоинт POST /api/vendor-spec/parse-text, который использует те же
детекторы и парсеры, что и импорт файла (KISS — один парсер на оба
входа). JS-копии _parseInspurBOMText/_isInspurBOMText удалены.
Заголовок конфигурации определяется по маркеру ", в составе:" с любым
префиксом ("Сервер X3" и "Вычислительный GPU сервер X3" → модель X3);
строки тримятся, пробел в начале не попадает в P/N; запятые и дефисы
внутри описания сохраняются (RAID0,1,10; 8-GPU-2304GB).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -3054,62 +3054,44 @@ function _normalizeBomRawRows(rows) {
|
||||
});
|
||||
}
|
||||
|
||||
function _parseInspurBOMText(text) {
|
||||
const lines = text.split(/\r?\n/);
|
||||
const result = [];
|
||||
for (const raw of lines) {
|
||||
const line = raw.trim();
|
||||
if (!line) continue;
|
||||
const clean = line.startsWith('|') ? line.slice(1).trim() : line;
|
||||
if (!clean) continue;
|
||||
const starIdx = clean.lastIndexOf('*');
|
||||
if (starIdx > 0) {
|
||||
const suffix = clean.slice(starIdx + 1).trim();
|
||||
if (/^\d+$/.test(suffix)) {
|
||||
result.push([clean.slice(0, starIdx).trim(), suffix]);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
result.push([clean, '1']);
|
||||
}
|
||||
return result;
|
||||
function _applyParsedBOMRows(parsed) {
|
||||
if (!Array.isArray(parsed) || !parsed.length) return false;
|
||||
bomImportRaw = {
|
||||
mode: 'raw',
|
||||
rows: parsed,
|
||||
columnTypes: ['pn', 'qty'],
|
||||
ignoredRows: {},
|
||||
rowErrors: {},
|
||||
uiError: ''
|
||||
};
|
||||
bomRows = [];
|
||||
_setBomUIError('');
|
||||
rebuildBOMRowsFromRaw();
|
||||
renderBOMTable();
|
||||
return true;
|
||||
}
|
||||
|
||||
function _isInspurBOMText(text) {
|
||||
const lines = text.split(/\r?\n/).filter(l => l.trim().length > 0);
|
||||
if (!lines.length) return false;
|
||||
let matches = 0;
|
||||
for (const line of lines) {
|
||||
const t = line.trim();
|
||||
const idx = t.lastIndexOf('*');
|
||||
if (idx > 0 && /^\d+$/.test(t.slice(idx + 1).trim())) matches++;
|
||||
// Detection and parsing of known single-column text BOM formats (Inspur, Russian
|
||||
// text BOM) lives only on the server (internal/services/vendor_workspace_import.go),
|
||||
// shared with the vendor file-import path. The paste handler asks the server to
|
||||
// parse; an unrecognized payload falls back to the generic Excel column grid below.
|
||||
async function _serverParseBOMText(text) {
|
||||
try {
|
||||
const resp = await fetch('/api/vendor-spec/parse-text', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({text})
|
||||
});
|
||||
if (!resp.ok) return null;
|
||||
const data = await resp.json();
|
||||
if (!Array.isArray(data.rows) || !data.rows.length) return null;
|
||||
return data.rows.map(r => [r.vendor_partnumber || '', String(r.quantity || '')]);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
return matches > 0 && matches >= Math.ceil(lines.length * 0.5);
|
||||
}
|
||||
|
||||
function handleBOMPaste(event) {
|
||||
event.preventDefault();
|
||||
const text = event.clipboardData.getData('text/plain');
|
||||
if (!text || !text.trim()) return;
|
||||
|
||||
if (_isInspurBOMText(text)) {
|
||||
const parsed = _parseInspurBOMText(text);
|
||||
if (!parsed.length) return;
|
||||
bomImportRaw = {
|
||||
mode: 'raw',
|
||||
rows: parsed,
|
||||
columnTypes: ['pn', 'qty'],
|
||||
ignoredRows: {},
|
||||
rowErrors: {},
|
||||
uiError: ''
|
||||
};
|
||||
bomRows = [];
|
||||
_setBomUIError('');
|
||||
rebuildBOMRowsFromRaw();
|
||||
renderBOMTable();
|
||||
return;
|
||||
}
|
||||
|
||||
function _applyGenericBOMPaste(text) {
|
||||
const lines = text.split(/\r?\n/).filter(l => l.length > 0);
|
||||
if (!lines.length) return;
|
||||
const rows = lines.map(l => l.split('\t').map(c => c.trim()));
|
||||
@@ -3129,6 +3111,20 @@ function handleBOMPaste(event) {
|
||||
renderBOMTable();
|
||||
}
|
||||
|
||||
async function handleBOMPaste(event) {
|
||||
event.preventDefault();
|
||||
const text = event.clipboardData.getData('text/plain');
|
||||
if (!text || !text.trim()) return;
|
||||
|
||||
// Tabs mean a real spreadsheet table — go straight to the column grid.
|
||||
if (!text.includes('\t')) {
|
||||
const parsed = await _serverParseBOMText(text);
|
||||
if (_applyParsedBOMRows(parsed)) return;
|
||||
}
|
||||
|
||||
_applyGenericBOMPaste(text);
|
||||
}
|
||||
|
||||
function _getBomColumnTypeIndexes() {
|
||||
if (!bomImportRaw || !Array.isArray(bomImportRaw.columnTypes)) return null;
|
||||
const idx = { ignore: [], pn: [], qty: [], price: [], description: [] };
|
||||
|
||||
Reference in New Issue
Block a user