fix(intake): keep barcode print as reserved entry

This commit is contained in:
WuYongJie 2026-06-11 13:38:15 +08:00
parent 90ff111283
commit 2ff31032c6
3 changed files with 16 additions and 1631 deletions

View File

@ -1,437 +0,0 @@
import type { BarcodePrintRow } from "./barcodePrintRows";
export type BarcodePrintImportKind = "barcode" | "brandSku";
type BarcodePrintImportUpdates = Partial<
Omit<BarcodePrintRow, "id" | "importRecordIds">
> & {
skuCode: string;
dataSource: string;
};
export type BarcodePrintImportRecord = {
id: string;
kind: BarcodePrintImportKind;
skuCode: string;
sourceFileName: string;
rowNumber: number;
importedAt: number;
updates: BarcodePrintImportUpdates;
};
export type BarcodePrintImportResult = {
kind: BarcodePrintImportKind;
fileName: string;
records: BarcodePrintImportRecord[];
};
export const BARCODE_PRINT_IMPORT_ACCEPT =
".xls,.xlsx,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
const BARCODE_IMPORT_HEADERS = [
"SPU",
"型号",
"货号",
"条码",
"工厂型号",
"规格名称",
"商品类目",
"尺寸",
"颜色",
"执行标准",
"吊牌材质",
"供应商名称",
];
const BRAND_SKU_IMPORT_HEADERS = [
"货号",
"品牌货号",
"品名",
"规格(长*宽*高)",
"合格证的主材*基材",
];
const IMPORT_LABEL_BY_KIND: Record<BarcodePrintImportKind, string> = {
barcode: "条码导入",
brandSku: "品牌货号导入",
};
const TEMPLATE_HEADERS_BY_KIND: Record<BarcodePrintImportKind, string[]> = {
barcode: BARCODE_IMPORT_HEADERS,
brandSku: BRAND_SKU_IMPORT_HEADERS,
};
const TEMPLATE_FILE_NAME_BY_KIND: Record<BarcodePrintImportKind, string> = {
barcode: "条码导入模板.xlsx",
brandSku: "品牌货号导入模板.xlsx",
};
const EMPTY_STANDALONE_ROW: Omit<BarcodePrintRow, "id"> = {
spu: "",
model: "",
specCode: "",
setCount: "",
skuCode: "",
brandSkuCode: "",
productName: "",
specification: "",
certificateMaterial: "",
barcode: "",
factoryModel: "",
attributeCode: "",
drawingId: "",
barcodeSpecName: "",
productCategory: "",
salesCategory: "",
materialType: "",
size: "",
color: "",
material: "",
executeStandard: "",
tagMaterial: "",
supplierName: "",
warehouse: "",
installationService: "",
lengthCm: "",
widthCm: "",
heightCm: "",
volumeCm3: "",
weightKg: "",
goodsShape: "",
costPrice: "",
productPlanning: "",
visualPlanning: "",
spuOwner: "",
operationsOwner: "",
purchaseOwner: "",
demandDepartment: "",
manualProducer: "",
isSelfDevelopedPatentProduct: "",
patentOwner: "",
profitCenter: "",
salesChannel: "",
salesPlatform: "",
productionType: "",
shippingMode: "",
supplierCategory: "",
purchaseOrderMethod: "",
assemblyDetail: "",
locked: "",
createdAt: "",
dataSource: "导入",
};
function normalizeHeader(value: unknown) {
return String(value ?? "").replace(/^\uFEFF/, "").trim();
}
function normalizeSkuCode(value: string) {
return value.trim().toUpperCase();
}
function cellText(value: unknown) {
return String(value ?? "").trim();
}
function importRecordKey(record: BarcodePrintImportRecord) {
return `${record.kind}:${normalizeSkuCode(record.skuCode)}`;
}
function buildHeaderIndex(headers: unknown[]) {
const headerIndex = new Map<string, number>();
headers.forEach((header, index) => {
const normalizedHeader = normalizeHeader(header);
if (normalizedHeader) {
headerIndex.set(normalizedHeader, index);
}
});
return headerIndex;
}
function requireHeaders(
headerIndex: Map<string, number>,
kind: BarcodePrintImportKind,
) {
const missingHeaders = TEMPLATE_HEADERS_BY_KIND[kind].filter(
(header) => !headerIndex.has(header),
);
if (missingHeaders.length > 0) {
throw new Error(
`${IMPORT_LABEL_BY_KIND[kind]}模板缺少字段:${missingHeaders.join("、")}`,
);
}
}
function getCell(
row: unknown[],
headerIndex: Map<string, number>,
headerName: string,
) {
const index = headerIndex.get(headerName);
return typeof index === "number" ? cellText(row[index]) : "";
}
function setIfPresent<
K extends keyof Omit<BarcodePrintRow, "id" | "importRecordIds">,
>(
updates: Partial<Record<K, string>>,
key: K,
value: string,
) {
if (value.trim()) {
updates[key] = value;
}
}
function createRecordId(
kind: BarcodePrintImportKind,
skuCode: string,
rowNumber: number,
) {
const normalizedSkuCode = normalizeSkuCode(skuCode).replace(
/[^A-Z0-9_-]/g,
"_",
);
return `barcode-print-import:${kind}:${Date.now()}:${rowNumber}:${normalizedSkuCode || "row"}`;
}
function buildBarcodeUpdates(
row: unknown[],
headerIndex: Map<string, number>,
) {
const skuCode = getCell(row, headerIndex, "货号");
const updates: BarcodePrintImportRecord["updates"] = {
skuCode,
dataSource: "导入",
};
setIfPresent(updates, "spu", getCell(row, headerIndex, "SPU"));
setIfPresent(updates, "model", getCell(row, headerIndex, "型号"));
setIfPresent(updates, "barcode", getCell(row, headerIndex, "条码"));
setIfPresent(updates, "factoryModel", getCell(row, headerIndex, "工厂型号"));
setIfPresent(
updates,
"barcodeSpecName",
getCell(row, headerIndex, "规格名称"),
);
setIfPresent(
updates,
"productCategory",
getCell(row, headerIndex, "商品类目"),
);
setIfPresent(updates, "size", getCell(row, headerIndex, "尺寸"));
setIfPresent(updates, "color", getCell(row, headerIndex, "颜色"));
setIfPresent(
updates,
"executeStandard",
getCell(row, headerIndex, "执行标准"),
);
setIfPresent(updates, "tagMaterial", getCell(row, headerIndex, "吊牌材质"));
setIfPresent(
updates,
"supplierName",
getCell(row, headerIndex, "供应商名称"),
);
return updates;
}
function buildBrandSkuUpdates(
row: unknown[],
headerIndex: Map<string, number>,
) {
const skuCode = getCell(row, headerIndex, "货号");
const updates: BarcodePrintImportRecord["updates"] = {
skuCode,
dataSource: "导入",
};
setIfPresent(
updates,
"brandSkuCode",
getCell(row, headerIndex, "品牌货号"),
);
setIfPresent(updates, "productName", getCell(row, headerIndex, "品名"));
setIfPresent(
updates,
"specification",
getCell(row, headerIndex, "规格(长*宽*高)"),
);
setIfPresent(
updates,
"certificateMaterial",
getCell(row, headerIndex, "合格证的主材*基材"),
);
return updates;
}
function buildImportRecord({
row,
headerIndex,
kind,
fileName,
rowNumber,
}: {
row: unknown[];
headerIndex: Map<string, number>;
kind: BarcodePrintImportKind;
fileName: string;
rowNumber: number;
}) {
const updates =
kind === "barcode"
? buildBarcodeUpdates(row, headerIndex)
: buildBrandSkuUpdates(row, headerIndex);
if (!updates.skuCode.trim()) {
throw new Error(`第 ${rowNumber} 行缺少货号。`);
}
return {
id: createRecordId(kind, updates.skuCode, rowNumber),
kind,
skuCode: updates.skuCode,
sourceFileName: fileName,
rowNumber,
importedAt: Date.now(),
updates,
};
}
export async function parseBarcodePrintImportFile(
file: File,
kind: BarcodePrintImportKind,
): Promise<BarcodePrintImportResult> {
const XLSX = await import("xlsx");
const arrayBuffer = await file.arrayBuffer();
const workbook = XLSX.read(arrayBuffer, { type: "array", cellDates: false });
const sheetName = workbook.SheetNames[0];
if (!sheetName) {
throw new Error("导入文件里没有可读取的工作表。");
}
const worksheet = workbook.Sheets[sheetName];
const rows = XLSX.utils.sheet_to_json(worksheet, {
header: 1,
defval: "",
raw: false,
blankrows: false,
}) as unknown[][];
const headerRow = rows[0] ?? [];
const headerIndex = buildHeaderIndex(headerRow);
requireHeaders(headerIndex, kind);
const dataRows = rows
.slice(1)
.map((row, index) => ({ row, rowNumber: index + 2 }))
.filter(({ row }) => row.some((cell) => cellText(cell)));
if (dataRows.length === 0) {
throw new Error(`${IMPORT_LABEL_BY_KIND[kind]}模板里没有可导入的数据。`);
}
return {
kind,
fileName: file.name,
records: dataRows.map(({ row, rowNumber }) =>
buildImportRecord({
row,
headerIndex,
kind,
fileName: file.name,
rowNumber,
}),
),
};
}
export function upsertBarcodePrintImportRecords(
currentRecords: BarcodePrintImportRecord[],
incomingRecords: BarcodePrintImportRecord[],
) {
const incomingKeys = new Set(incomingRecords.map(importRecordKey));
return [
...currentRecords.filter(
(record) => !incomingKeys.has(importRecordKey(record)),
),
...incomingRecords,
];
}
export function mergeBarcodePrintImportRows(
baseRows: BarcodePrintRow[],
importRecords: BarcodePrintImportRecord[],
): BarcodePrintRow[] {
const recordsBySkuCode = new Map<string, BarcodePrintImportRecord[]>();
importRecords.forEach((record) => {
const skuCodeKey = normalizeSkuCode(record.skuCode);
if (!skuCodeKey) return;
const records = recordsBySkuCode.get(skuCodeKey) ?? [];
records.push(record);
recordsBySkuCode.set(skuCodeKey, records);
});
const matchedSkuCodes = new Set<string>();
const mergedRows = baseRows.map((row) => {
const skuCodeKey = normalizeSkuCode(row.skuCode);
const records = skuCodeKey ? recordsBySkuCode.get(skuCodeKey) : undefined;
if (!records?.length) return row;
matchedSkuCodes.add(skuCodeKey);
const updates = Object.assign({}, ...records.map((record) => record.updates));
return {
...row,
...updates,
id: row.id,
skuCode: row.skuCode || updates.skuCode || "",
dataSource: "导入",
importRecordIds: records.map((record) => record.id),
};
});
const standaloneRows = Array.from(recordsBySkuCode.entries())
.filter(([skuCodeKey]) => !matchedSkuCodes.has(skuCodeKey))
.map(([skuCodeKey, records]) => {
const updates = Object.assign(
{},
...records.map((record) => record.updates),
);
return {
...EMPTY_STANDALONE_ROW,
...updates,
id: `barcode-print-standalone:${skuCodeKey}`,
skuCode: updates.skuCode || records.at(-1)?.skuCode || "",
dataSource: "导入",
importRecordIds: records.map((record) => record.id),
};
});
return [...mergedRows, ...standaloneRows];
}
export async function downloadBarcodePrintImportTemplate(
kind: BarcodePrintImportKind,
) {
const XLSX = await import("xlsx");
const headers = TEMPLATE_HEADERS_BY_KIND[kind];
const workbook = XLSX.utils.book_new();
const worksheet = XLSX.utils.aoa_to_sheet([headers]);
worksheet["!cols"] = headers.map((header) => ({
wch: Math.max(12, header.length + 4),
}));
XLSX.utils.book_append_sheet(workbook, worksheet, "模板");
XLSX.writeFile(workbook, TEMPLATE_FILE_NAME_BY_KIND[kind], {
bookType: "xlsx",
});
}

View File

@ -1,168 +0,0 @@
import type { NonIntegratedProductRecord } from "@/app/intake/non-integrated/nonIntegratedStorage";
export type BarcodePrintRow = {
id: string;
importRecordIds?: string[];
spu: string;
model: string;
specCode: string;
setCount: string;
skuCode: string;
brandSkuCode: string;
productName: string;
specification: string;
certificateMaterial: string;
barcode: string;
factoryModel: string;
attributeCode: string;
drawingId: string;
barcodeSpecName: string;
productCategory: string;
salesCategory: string;
materialType: string;
size: string;
color: string;
material: string;
executeStandard: string;
tagMaterial: string;
supplierName: string;
warehouse: string;
installationService: string;
lengthCm: string;
widthCm: string;
heightCm: string;
volumeCm3: string;
weightKg: string;
goodsShape: string;
costPrice: string;
productPlanning: string;
visualPlanning: string;
spuOwner: string;
operationsOwner: string;
purchaseOwner: string;
demandDepartment: string;
manualProducer: string;
isSelfDevelopedPatentProduct: string;
patentOwner: string;
profitCenter: string;
salesChannel: string;
salesPlatform: string;
productionType: string;
shippingMode: string;
supplierCategory: string;
purchaseOrderMethod: string;
assemblyDetail: string;
locked: string;
createdAt: string;
dataSource: string;
};
function firstText(...values: Array<string | undefined | null>) {
return values.find((value) => value?.trim())?.trim() ?? "";
}
function calculateVolumeCm3(lengthCm: string, widthCm: string, heightCm: string) {
const length = Number.parseFloat(lengthCm);
const width = Number.parseFloat(widthCm);
const height = Number.parseFloat(heightCm);
if (!Number.isFinite(length) || !Number.isFinite(width) || !Number.isFinite(height)) {
return "";
}
const volume = length * width * height;
if (!Number.isFinite(volume)) return "";
return Number.isInteger(volume) ? String(volume) : volume.toFixed(2);
}
function normalizeCreatedAt(value: string) {
if (!value.trim()) return "";
const parsed = new Date(value);
if (Number.isNaN(parsed.getTime())) return value;
return parsed.toISOString().slice(0, 10);
}
export function buildBarcodePrintRows(
products: NonIntegratedProductRecord[],
): BarcodePrintRow[] {
return products.flatMap((product) => {
const skuItems = product.skuItems.length > 0 ? product.skuItems : [null];
return skuItems.map((sku, index) => {
const skuId = sku?.id ?? `${product.id}-empty-sku`;
const factoryRow = product.factoryRows.find((row) => row.skuId === skuId);
const measurementRow = product.sampleMeasurementRows.find(
(row) => row.skuId === skuId,
);
const pricingRow = product.pricingRows.find((row) => row.skuId === skuId);
const lengthCm = measurementRow?.lengthCm?.trim() ?? "";
const widthCm = measurementRow?.widthCm?.trim() ?? "";
const heightCm = measurementRow?.heightCm?.trim() ?? "";
const barcode = firstText(factoryRow?.barcode, sku?.barcode);
const skuCode = firstText(factoryRow?.skuCode, sku?.skuCode);
return {
id: `${product.id}:${skuId}:${index}`,
spu: product.spu,
model: firstText(sku?.model, factoryRow?.model, product.productCode),
specCode: firstText(sku?.specName, skuCode),
setCount: firstText(factoryRow?.setCount, sku?.setCount),
skuCode,
brandSkuCode: firstText(product.itemId, product.productCode),
productName: firstText(
product.openingReference.productName,
product.productTag,
product.spu,
),
specification: firstText(sku?.size, product.size),
certificateMaterial: product.packageDetail.trim(),
barcode,
factoryModel: firstText(factoryRow?.model, sku?.model),
attributeCode: sku?.attributeCode?.trim() ?? "",
drawingId: sku?.drawingId?.trim() ?? "",
barcodeSpecName: firstText(factoryRow?.specName, sku?.specName),
productCategory: product.productCategory,
salesCategory: product.salesCategory,
materialType: firstText(product.materialType, sku?.boardMaterialType),
size: firstText(sku?.size, product.size),
color: firstText(sku?.color, product.color),
material: firstText(sku?.material, product.openingReference.material),
executeStandard: firstText(factoryRow?.executeStandard, sku?.executeStandard),
tagMaterial: firstText(sku?.tagMaterial, product.tagMaterial),
supplierName: firstText(factoryRow?.supplierName, sku?.manufacturer),
warehouse: factoryRow?.warehouse?.trim() ?? "",
installationService: factoryRow?.installationService?.trim() ?? "",
lengthCm,
widthCm,
heightCm,
volumeCm3: calculateVolumeCm3(lengthCm, widthCm, heightCm),
weightKg: measurementRow?.weightKg?.trim() ?? "",
goodsShape: product.goodsType.trim(),
costPrice: pricingRow?.costPrice?.trim() ?? "",
productPlanning: product.productPlanning,
visualPlanning: product.visualPlanning,
spuOwner: product.spuOwner,
operationsOwner: product.operationsOwner,
purchaseOwner: product.purchaseOwner,
demandDepartment: product.demandDepartment,
manualProducer: product.manualProducer,
isSelfDevelopedPatentProduct: product.isSelfDevelopedPatentProduct,
patentOwner: product.patentOwner,
profitCenter: product.profitCenter,
salesChannel: product.salesChannel,
salesPlatform: product.salesPlatform,
productionType: firstText(factoryRow?.productionType, product.productionType),
shippingMode: firstText(factoryRow?.shippingMethod, product.shippingMode),
supplierCategory: factoryRow?.supplierCategory?.trim() ?? "",
purchaseOrderMethod: factoryRow?.purchaseOrderMethod?.trim() ?? "",
assemblyDetail: product.installSteps.trim(),
locked: sku?.contentLocked ? "是" : "否",
createdAt: normalizeCreatedAt(product.createdAt),
dataSource: firstText(sku?.sourceTable, sku?.source, "流程"),
};
});
});
}