diff --git a/furniture-3d-system/src/app/intake/barcode-print/BarcodePrintWorkspace.tsx b/furniture-3d-system/src/app/intake/barcode-print/BarcodePrintWorkspace.tsx index 0d39748e..54620f6b 100644 --- a/furniture-3d-system/src/app/intake/barcode-print/BarcodePrintWorkspace.tsx +++ b/furniture-3d-system/src/app/intake/barcode-print/BarcodePrintWorkspace.tsx @@ -1,1037 +1,27 @@ "use client"; -import { - useEffect, - useMemo, - useRef, - useState, - type MouseEvent as ReactMouseEvent, -} from "react"; -import { - Barcode, - Download, - FileText, - RotateCcw, - Search, - Trash2, - Upload, -} from "lucide-react"; +import { Barcode } from "lucide-react"; import type { NonIntegratedProductRecord } from "@/app/intake/non-integrated/nonIntegratedStorage"; -import { - BARCODE_PRINT_IMPORT_ACCEPT, - downloadBarcodePrintImportTemplate, - mergeBarcodePrintImportRows, - parseBarcodePrintImportFile, - upsertBarcodePrintImportRecords, - type BarcodePrintImportKind, - type BarcodePrintImportRecord, -} from "./barcodePrintImport"; -import { - buildBarcodePrintRows, - type BarcodePrintRow, -} from "./barcodePrintRows"; +import { BARCODE_PRINT_LABEL } from "./barcodePrintConfig"; -type BarcodePrintFilterState = { - spu: string; - model: string; - skuCode: string; - brandSkuCode: string; - barcode: string; - demandDepartment: string; - operationsOwner: string; - spuOwner: string; - productPlanning: string; - purchaseOwner: string; - productCategory: string; - salesCategory: string; - supplierName: string; - createdFrom: string; - createdTo: string; +type BarcodePrintWorkspaceProps = { + products?: NonIntegratedProductRecord[]; }; -type BarcodePrintColumnKey = Exclude< - keyof BarcodePrintRow, - "id" | "importRecordIds" ->; - -type BarcodePrintColumn = { - key: BarcodePrintColumnKey; - label: string; - width: number; -}; - -type BarcodePrintColumnWidths = Record; - -type ImportProgressState = { - active: boolean; - label: string; - percent: number; - status: "idle" | "reading" | "validating" | "importing" | "success" | "error"; - message: string; -}; - -const EMPTY_FILTERS: BarcodePrintFilterState = { - spu: "", - model: "", - skuCode: "", - brandSkuCode: "", - barcode: "", - demandDepartment: "", - operationsOwner: "", - spuOwner: "", - productPlanning: "", - purchaseOwner: "", - productCategory: "", - salesCategory: "", - supplierName: "", - createdFrom: "", - createdTo: "", -}; - -const EMPTY_IMPORT_PROGRESS: ImportProgressState = { - active: false, - label: "", - percent: 0, - status: "idle", - message: "", -}; - -const BARCODE_PRINT_PAGE_SIZE = 20; -const SELECTION_COLUMN_WIDTH = 34; -const ROW_NUMBER_COLUMN_WIDTH = 46; -const ROW_NUMBER_COLUMN_MIN_WIDTH = 46; -const OPERATION_COLUMN_WIDTH = 132; -const BARCODE_PRINT_COLUMN_MIN_WIDTH = 56; -const FILTER_FIELD_CLASS = - "flex h-7 shrink-0 items-center gap-1.5 text-[12px] text-[var(--obs-shell-muted)]"; -const FILTER_LABEL_CLASS = - "shrink-0 whitespace-nowrap font-apple-semibold text-[var(--obs-shell-title)]"; -const FILTER_CONTROL_CLASS = - "h-7 w-[132px] min-w-[132px] rounded-[5px] border border-[var(--obs-shell-border)] bg-white px-2 text-[11px] font-apple-medium text-[var(--obs-shell-title)] outline-none transition placeholder:text-[var(--obs-shell-subtle)] focus:border-[var(--obs-shell-title)]"; - -const BARCODE_PRINT_COLUMNS: BarcodePrintColumn[] = [ - { key: "spu", label: "SPU", width: 92 }, - { key: "model", label: "型号", width: 104 }, - { key: "specCode", label: "规格码", width: 112 }, - { key: "setCount", label: "套件量", width: 68 }, - { key: "skuCode", label: "货号", width: 112 }, - { key: "brandSkuCode", label: "品牌货号", width: 112 }, - { key: "productName", label: "品名", width: 120 }, - { key: "specification", label: "规格(长*宽*高)", width: 136 }, - { key: "certificateMaterial", label: "合格证的主体*辅材", width: 150 }, - { key: "barcode", label: "条码", width: 132 }, - { key: "factoryModel", label: "工厂型号", width: 120 }, - { key: "attributeCode", label: "属性代码", width: 84 }, - { key: "drawingId", label: "图纸ID", width: 124 }, - { key: "barcodeSpecName", label: "条码规格名称", width: 170 }, - { key: "productCategory", label: "商品类目", width: 96 }, - { key: "salesCategory", label: "销售类目", width: 96 }, - { key: "materialType", label: "材质类型", width: 96 }, - { key: "size", label: "尺寸", width: 112 }, - { key: "color", label: "颜色", width: 104 }, - { key: "material", label: "材质", width: 150 }, - { key: "executeStandard", label: "执行标准", width: 126 }, - { key: "tagMaterial", label: "吊牌材质", width: 104 }, - { key: "supplierName", label: "供应商名称", width: 126 }, - { key: "warehouse", label: "发货仓库", width: 126 }, - { key: "installationService", label: "安装服务", width: 88 }, - { key: "lengthCm", label: "长(CM)", width: 78 }, - { key: "widthCm", label: "宽(CM)", width: 78 }, - { key: "heightCm", label: "高(CM)", width: 78 }, - { key: "volumeCm3", label: "体积(CM)", width: 96 }, - { key: "weightKg", label: "重量(KG)", width: 88 }, - { key: "goodsShape", label: "商品外形", width: 92 }, - { key: "costPrice", label: "成本价", width: 92 }, - { key: "productPlanning", label: "商品策划", width: 112 }, - { key: "visualPlanning", label: "视觉规划", width: 112 }, - { key: "spuOwner", label: "SPU负责人", width: 112 }, - { key: "operationsOwner", label: "运营部门负责人", width: 128 }, - { key: "purchaseOwner", label: "采购负责人", width: 112 }, - { key: "demandDepartment", label: "需求部门", width: 104 }, - { key: "manualProducer", label: "说明书制作方", width: 118 }, - { key: "isSelfDevelopedPatentProduct", label: "是否自研专利产品", width: 136 }, - { key: "patentOwner", label: "专利所有方", width: 112 }, - { key: "profitCenter", label: "利润中心", width: 112 }, - { key: "salesChannel", label: "销售渠道", width: 104 }, - { key: "salesPlatform", label: "销售平台", width: 104 }, - { key: "productionType", label: "产品生产类型", width: 112 }, - { key: "shippingMode", label: "发货模式", width: 92 }, - { key: "supplierCategory", label: "供应商分类", width: 112 }, - { key: "purchaseOrderMethod", label: "采购下单方式", width: 112 }, - { key: "assemblyDetail", label: "整装明细", width: 92 }, - { key: "locked", label: "是否锁定商品", width: 112 }, - { key: "createdAt", label: "创建时间", width: 112 }, - { key: "dataSource", label: "数据源", width: 90 }, -]; - -function textIncludes(value: string, query: string) { - return value - .trim() - .toLowerCase() - .includes(query.trim().toLowerCase()); -} - -function rowMatchesFilters(row: BarcodePrintRow, filters: BarcodePrintFilterState) { - if (filters.spu && !textIncludes(row.spu, filters.spu)) return false; - if (filters.model && !textIncludes(row.model, filters.model)) return false; - if (filters.skuCode && !textIncludes(row.skuCode, filters.skuCode)) return false; - if (filters.brandSkuCode && !textIncludes(row.brandSkuCode, filters.brandSkuCode)) { - return false; - } - if (filters.barcode && !textIncludes(row.barcode, filters.barcode)) return false; - if (filters.demandDepartment && row.demandDepartment !== filters.demandDepartment) { - return false; - } - if (filters.operationsOwner && !textIncludes(row.operationsOwner, filters.operationsOwner)) { - return false; - } - if (filters.spuOwner && !textIncludes(row.spuOwner, filters.spuOwner)) return false; - if (filters.productPlanning && !textIncludes(row.productPlanning, filters.productPlanning)) { - return false; - } - if (filters.purchaseOwner && !textIncludes(row.purchaseOwner, filters.purchaseOwner)) { - return false; - } - if (filters.productCategory && row.productCategory !== filters.productCategory) { - return false; - } - if (filters.salesCategory && row.salesCategory !== filters.salesCategory) { - return false; - } - if (filters.supplierName && !textIncludes(row.supplierName, filters.supplierName)) { - return false; - } - if (filters.createdFrom && row.createdAt && row.createdAt < filters.createdFrom) { - return false; - } - if (filters.createdTo && row.createdAt && row.createdAt > filters.createdTo) { - return false; - } - - return true; -} - -function getUniqueOptions(rows: BarcodePrintRow[], key: keyof BarcodePrintRow) { - return Array.from( - new Set(rows.map((row) => String(row[key]).trim()).filter(Boolean)), - ).sort((left, right) => left.localeCompare(right, "zh-CN")); -} - -function BarcodePrintInput({ - label, - value, - placeholder, - onChange, -}: { - label: string; - value: string; - placeholder: string; - onChange: (value: string) => void; -}) { +export function BarcodePrintWorkspace(_props: BarcodePrintWorkspaceProps) { return ( - - ); -} - -function BarcodePrintSelect({ - label, - value, - placeholder, - options, - onChange, -}: { - label: string; - value: string; - placeholder: string; - options: string[]; - onChange: (value: string) => void; -}) { - return ( - - ); -} - -function BarcodePrintActionButton({ - children, - intent = "default", - onClick, -}: { - children: React.ReactNode; - intent?: "default" | "primary" | "danger"; - onClick: () => void; -}) { - const className = - intent === "primary" - ? "border-[var(--obs-shell-action-border)] bg-[var(--obs-shell-action-bg)] text-[var(--obs-shell-action-text)] hover:bg-[var(--obs-shell-action-hover-bg)]" - : intent === "danger" - ? "border-[var(--obs-shell-danger-border)] bg-[var(--obs-shell-danger-bg)] text-[var(--obs-shell-danger)] hover:bg-[rgba(179,69,61,0.11)]" - : "border-[var(--obs-shell-border)] bg-white text-[var(--obs-shell-control-text)] hover:bg-[var(--obs-shell-hover-bg)] hover:text-[var(--obs-shell-title)]"; - - return ( - - ); -} - -function formatCell(value: string) { - return value.trim() || "-"; -} - -function waitForProgressFrame() { - return new Promise((resolve) => window.setTimeout(resolve, 120)); -} - -function buildInitialColumnWidths(): BarcodePrintColumnWidths { - return Object.fromEntries( - BARCODE_PRINT_COLUMNS.map((column) => [column.key, column.width]), - ) as BarcodePrintColumnWidths; -} - -function getColumnMinWidth(key: BarcodePrintColumnKey) { - const column = BARCODE_PRINT_COLUMNS.find((item) => item.key === key); - return Math.min( - column?.width ?? BARCODE_PRINT_COLUMN_MIN_WIDTH, - BARCODE_PRINT_COLUMN_MIN_WIDTH, - ); -} - -export function BarcodePrintWorkspace({ - products, -}: { - products: NonIntegratedProductRecord[]; -}) { - const barcodeImportInputRef = useRef(null); - const brandSkuImportInputRef = useRef(null); - const baseRows = useMemo(() => buildBarcodePrintRows(products), [products]); - const [importRecords, setImportRecords] = useState( - [], - ); - const [draftFilters, setDraftFilters] = - useState(EMPTY_FILTERS); - const [appliedFilters, setAppliedFilters] = - useState(EMPTY_FILTERS); - const [selectedRowIds, setSelectedRowIds] = useState>( - () => new Set(), - ); - const [currentPage, setCurrentPage] = useState(1); - const [statusMessage, setStatusMessage] = useState(""); - const [importProgress, setImportProgress] = - useState(EMPTY_IMPORT_PROGRESS); - const [rowNumberColumnWidth, setRowNumberColumnWidth] = useState( - ROW_NUMBER_COLUMN_WIDTH, - ); - const [columnWidths, setColumnWidths] = useState( - () => buildInitialColumnWidths(), - ); - const rows = useMemo( - () => mergeBarcodePrintImportRows(baseRows, importRecords), - [baseRows, importRecords], - ); - const tableWidth = useMemo( - () => - SELECTION_COLUMN_WIDTH + - rowNumberColumnWidth + - OPERATION_COLUMN_WIDTH + - BARCODE_PRINT_COLUMNS.reduce( - (totalWidth, column) => totalWidth + columnWidths[column.key], - 0, - ), - [columnWidths, rowNumberColumnWidth], - ); - - const demandDepartmentOptions = useMemo( - () => getUniqueOptions(rows, "demandDepartment"), - [rows], - ); - const productCategoryOptions = useMemo( - () => getUniqueOptions(rows, "productCategory"), - [rows], - ); - const salesCategoryOptions = useMemo( - () => getUniqueOptions(rows, "salesCategory"), - [rows], - ); - const filteredRows = useMemo( - () => rows.filter((row) => rowMatchesFilters(row, appliedFilters)), - [appliedFilters, rows], - ); - const totalPages = Math.max( - 1, - Math.ceil(filteredRows.length / BARCODE_PRINT_PAGE_SIZE), - ); - const pagedRows = useMemo(() => { - const safePage = Math.min(currentPage, totalPages); - const startIndex = (safePage - 1) * BARCODE_PRINT_PAGE_SIZE; - return filteredRows.slice(startIndex, startIndex + BARCODE_PRINT_PAGE_SIZE); - }, [currentPage, filteredRows, totalPages]); - const allVisibleSelected = - pagedRows.length > 0 && pagedRows.every((row) => selectedRowIds.has(row.id)); - - useEffect(() => { - setCurrentPage(1); - }, [appliedFilters, rows.length]); - - useEffect(() => { - if (currentPage > totalPages) { - setCurrentPage(totalPages); - } - }, [currentPage, totalPages]); - - useEffect(() => { - if (!importProgress.active) return; - if (importProgress.status !== "success" && importProgress.status !== "error") { - return; - } - - const timer = window.setTimeout(() => { - setImportProgress(EMPTY_IMPORT_PROGRESS); - }, 3000); - - return () => window.clearTimeout(timer); - }, [importProgress.active, importProgress.status]); - - function updateDraftFilter( - key: K, - value: BarcodePrintFilterState[K], - ) { - setDraftFilters((current) => ({ ...current, [key]: value })); - } - - function toggleVisibleRows() { - setSelectedRowIds((current) => { - const next = new Set(current); - if (allVisibleSelected) { - pagedRows.forEach((row) => next.delete(row.id)); - } else { - pagedRows.forEach((row) => next.add(row.id)); - } - return next; - }); - } - - function toggleRow(rowId: string) { - setSelectedRowIds((current) => { - const next = new Set(current); - if (next.has(rowId)) { - next.delete(rowId); - } else { - next.add(rowId); - } - return next; - }); - } - - async function handleImportFile(kind: BarcodePrintImportKind, file: File | null) { - if (!file) return; - - const label = kind === "barcode" ? "条码导入" : "品牌货号导入"; - - setStatusMessage(""); - setImportProgress({ - active: true, - label, - percent: 15, - status: "reading", - message: `正在读取 ${file.name}`, - }); - - try { - await waitForProgressFrame(); - - const result = await parseBarcodePrintImportFile(file, kind); - - setImportProgress({ - active: true, - label, - percent: 45, - status: "validating", - message: "字段校验通过,准备写入页面数据。", - }); - await waitForProgressFrame(); - - setImportProgress({ - active: true, - label, - percent: 75, - status: "importing", - message: "正在合并导入数据。", - }); - await waitForProgressFrame(); - - setImportRecords((current) => - upsertBarcodePrintImportRecords(current, result.records), - ); - setSelectedRowIds(new Set()); - setCurrentPage(1); - - const successMessage = `${label}成功,共导入 ${result.records.length} 条,数据源已标记为“导入”。`; - setImportProgress({ - active: true, - label, - percent: 100, - status: "success", - message: successMessage, - }); - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : `${label}失败,请检查模板。`; - setImportProgress({ - active: true, - label, - percent: 100, - status: "error", - message: errorMessage, - }); - } - } - - async function handleDownloadTemplate(kind: BarcodePrintImportKind) { - const label = kind === "barcode" ? "条码导入模板" : "品牌货号导入模板"; - - setStatusMessage(""); - setImportProgress({ - active: true, - label, - percent: 50, - status: "importing", - message: `正在生成${label}。`, - }); - - try { - await waitForProgressFrame(); - await downloadBarcodePrintImportTemplate(kind); - - const successMessage = `${label}已生成下载。`; - setImportProgress({ - active: true, - label, - percent: 100, - status: "success", - message: successMessage, - }); - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : `${label}下载失败。`; - setImportProgress({ - active: true, - label, - percent: 100, - status: "error", - message: errorMessage, - }); - } - } - - function deleteImportedData(importRecordIds: string[] | undefined) { - if (!importRecordIds?.length) return; - - const deleteRecordIdSet = new Set(importRecordIds); - setImportRecords((current) => - current.filter((record) => !deleteRecordIdSet.has(record.id)), - ); - setSelectedRowIds(new Set()); - setImportProgress(EMPTY_IMPORT_PROGRESS); - setStatusMessage("已删除该行导入数据。"); - } - - function startRowNumberColumnResize( - event: ReactMouseEvent, - ) { - startColumnResize({ - event, - minWidth: ROW_NUMBER_COLUMN_MIN_WIDTH, - startWidth: rowNumberColumnWidth, - onResize: setRowNumberColumnWidth, - }); - } - - function startDataColumnResize( - event: ReactMouseEvent, - key: BarcodePrintColumnKey, - ) { - startColumnResize({ - event, - minWidth: getColumnMinWidth(key), - startWidth: columnWidths[key], - onResize: (width) => - setColumnWidths((current) => ({ ...current, [key]: width })), - }); - } - - function startColumnResize({ - event, - minWidth, - startWidth, - onResize, - }: { - event: ReactMouseEvent; - minWidth: number; - startWidth: number; - onResize: (width: number) => void; - }) { - event.preventDefault(); - event.stopPropagation(); - - const startX = event.clientX; - - const handleMouseMove = (mouseEvent: MouseEvent) => { - const nextWidth = Math.max( - minWidth, - Math.round(startWidth + mouseEvent.clientX - startX), - ); - onResize(nextWidth); - }; - - const handleMouseUp = () => { - window.removeEventListener("mousemove", handleMouseMove); - }; - - window.addEventListener("mousemove", handleMouseMove); - window.addEventListener("mouseup", handleMouseUp, { once: true }); - } - - function markDeferredAction(actionName: string) { - setImportProgress(EMPTY_IMPORT_PROGRESS); - setStatusMessage(`${actionName}规则待确认,当前先保留入口和字段。`); - } - - return ( -
- { - const file = event.currentTarget.files?.[0] ?? null; - event.currentTarget.value = ""; - void handleImportFile("barcode", file); - }} - /> - { - const file = event.currentTarget.files?.[0] ?? null; - event.currentTarget.value = ""; - void handleImportFile("brandSku", file); - }} - /> -
-
- barcodeImportInputRef.current?.click()} - > - - void handleDownloadTemplate("barcode")} - > - - brandSkuImportInputRef.current?.click()} - > - - void handleDownloadTemplate("brandSku")} - > - -
- -
- updateDraftFilter("spu", value)} - /> - updateDraftFilter("model", value)} - /> - updateDraftFilter("skuCode", value)} - /> - updateDraftFilter("brandSkuCode", value)} - /> - updateDraftFilter("barcode", value)} - /> - updateDraftFilter("demandDepartment", value)} - /> - updateDraftFilter("operationsOwner", value)} - /> - updateDraftFilter("spuOwner", value)} - /> - updateDraftFilter("productPlanning", value)} - /> - updateDraftFilter("purchaseOwner", value)} - /> - updateDraftFilter("productCategory", value)} - /> - updateDraftFilter("salesCategory", value)} - /> - updateDraftFilter("supplierName", value)} - /> - - - - -
- {statusMessage ? ( -
- {statusMessage} -
- ) : null} +
+
+ + +

+ {BARCODE_PRINT_LABEL} +

+

+ 入口已预留,完整条码打印工作区等待后续合并。 +

- -
- - - - - - {BARCODE_PRINT_COLUMNS.map((column) => ( - - ))} - - - - - {filteredRows.length === 0 ? ( - - - - ) : ( - pagedRows.map((row, index) => ( - - - - {BARCODE_PRINT_COLUMNS.map((column) => ( - - ))} - - - )) - )} - -
- - - 序号 - - {column.label} - - 操作 -
- 当前没有符合条件的条码资料。 -
- toggleRow(row.id)} - className="h-3.5 w-3.5" - /> - - {(currentPage - 1) * BARCODE_PRINT_PAGE_SIZE + index + 1} - - - {formatCell(String(row[column.key]))} - - -
- - - {row.importRecordIds?.length ? ( - - ) : null} -
-
-
- -
- -
- 共 {filteredRows.length} 条 - 第 - - {currentPage} - - / {totalPages} 页 - 每页 {BARCODE_PRINT_PAGE_SIZE} 条 -
- -
- - {importProgress.active ? ( -
-
- {importProgress.label} - {importProgress.percent}% -
-
-
-
-
- {importProgress.message} -
-
- ) : null}
); } diff --git a/furniture-3d-system/src/app/intake/barcode-print/barcodePrintImport.ts b/furniture-3d-system/src/app/intake/barcode-print/barcodePrintImport.ts deleted file mode 100644 index ab8aaf43..00000000 --- a/furniture-3d-system/src/app/intake/barcode-print/barcodePrintImport.ts +++ /dev/null @@ -1,437 +0,0 @@ -import type { BarcodePrintRow } from "./barcodePrintRows"; - -export type BarcodePrintImportKind = "barcode" | "brandSku"; - -type BarcodePrintImportUpdates = Partial< - Omit -> & { - 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 = { - barcode: "条码导入", - brandSku: "品牌货号导入", -}; - -const TEMPLATE_HEADERS_BY_KIND: Record = { - barcode: BARCODE_IMPORT_HEADERS, - brandSku: BRAND_SKU_IMPORT_HEADERS, -}; - -const TEMPLATE_FILE_NAME_BY_KIND: Record = { - barcode: "条码导入模板.xlsx", - brandSku: "品牌货号导入模板.xlsx", -}; - -const EMPTY_STANDALONE_ROW: Omit = { - 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(); - headers.forEach((header, index) => { - const normalizedHeader = normalizeHeader(header); - if (normalizedHeader) { - headerIndex.set(normalizedHeader, index); - } - }); - return headerIndex; -} - -function requireHeaders( - headerIndex: Map, - 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, - headerName: string, -) { - const index = headerIndex.get(headerName); - return typeof index === "number" ? cellText(row[index]) : ""; -} - -function setIfPresent< - K extends keyof Omit, ->( - updates: Partial>, - 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, -) { - 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, -) { - 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; - 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 { - 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(); - - 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(); - 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", - }); -} diff --git a/furniture-3d-system/src/app/intake/barcode-print/barcodePrintRows.ts b/furniture-3d-system/src/app/intake/barcode-print/barcodePrintRows.ts deleted file mode 100644 index 72286ab6..00000000 --- a/furniture-3d-system/src/app/intake/barcode-print/barcodePrintRows.ts +++ /dev/null @@ -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) { - 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, "流程"), - }; - }); - }); -}