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:
Mikhail Chusavitin
2026-06-16 09:16:55 +03:00
parent 6f2c261350
commit 24c34eb0e1
6 changed files with 235 additions and 58 deletions

View File

@@ -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: [] };