| | |
| | | import QRCode from "qrcode"; |
| | | |
| | | const PRINT_TITLE = "销售发货单"; |
| | | |
| | | const escapeHtml = (value) => |
| | |
| | | const getOrderNo = (data, row, item) => |
| | | item?.salesContractNo || item?.orderNo || data?.salesContractNo || row?.salesContractNo || ""; |
| | | |
| | | const PRODUCT_NAME_FIELD_KEYS = [ |
| | | "productDescription", |
| | | "productName", |
| | | "name", |
| | | "title", |
| | | "goodsName", |
| | | "materialName", |
| | | "glassName", |
| | | ]; |
| | | |
| | | const PRODUCT_NAME_LIST_FIELD_KEYS = [ |
| | | "productDescriptionList", |
| | | "productNameList", |
| | | "productDescriptions", |
| | | "productNames", |
| | | "nameList", |
| | | "goodsNameList", |
| | | "detailProductNames", |
| | | "productInfoList", |
| | | ]; |
| | | |
| | | const normalizeNameList = (value) => { |
| | | if (Array.isArray(value)) { |
| | | return value |
| | | .flatMap((item) => normalizeNameList(item)) |
| | | .map((item) => String(item).trim()) |
| | | .filter(Boolean); |
| | | } |
| | | if (value && typeof value === "object") { |
| | | const objectListNames = PRODUCT_NAME_LIST_FIELD_KEYS.flatMap((key) => normalizeNameList(value?.[key])); |
| | | if (objectListNames.length) return objectListNames; |
| | | return PRODUCT_NAME_FIELD_KEYS.flatMap((key) => normalizeNameList(value?.[key])); |
| | | } |
| | | if (typeof value === "string") { |
| | | const text = value.trim(); |
| | | if (!text) return []; |
| | | const parts = text |
| | | .split(/[,\n,、;;]/) |
| | | .map((item) => item.trim()) |
| | | .filter(Boolean); |
| | | return parts.length > 1 ? parts : [text]; |
| | | } |
| | | if (value === null || value === undefined) return []; |
| | | const text = String(value).trim(); |
| | | return text ? [text] : []; |
| | | }; |
| | | |
| | | const extractNameListByKeys = (source, keys) => { |
| | | if (!source || typeof source !== "object") return []; |
| | | return keys.flatMap((key) => normalizeNameList(source?.[key])); |
| | | }; |
| | | |
| | | const resolveProductName = (item, fallbackNames, index) => { |
| | | const itemNames = [ |
| | | ...extractNameListByKeys(item, PRODUCT_NAME_LIST_FIELD_KEYS), |
| | | ...extractNameListByKeys(item, PRODUCT_NAME_FIELD_KEYS), |
| | | ]; |
| | | if (itemNames.length > 1) { |
| | | return itemNames[index] || itemNames[0] || ""; |
| | | } |
| | | return itemNames[0] || fallbackNames[index] || fallbackNames[0] || ""; |
| | | }; |
| | | |
| | | const splitItemsByPage = (items, pageSize) => { |
| | | const list = Array.isArray(items) ? items : []; |
| | | if (list.length === 0) return [[]]; |
| | |
| | | |
| | | const normalizeInvoiceData = (raw, selectedRow) => { |
| | | const data = raw ?? {}; |
| | | const groups = Array.isArray(data.groups) ? data.groups : []; |
| | | if (!groups.length) return data; |
| | | const groups = Array.isArray(data.groups) |
| | | ? data.groups |
| | | : Array.isArray(data.groupList) |
| | | ? data.groupList |
| | | : []; |
| | | const dataLevelNames = [ |
| | | ...extractNameListByKeys(data, PRODUCT_NAME_LIST_FIELD_KEYS), |
| | | ...extractNameListByKeys(data, PRODUCT_NAME_FIELD_KEYS), |
| | | ]; |
| | | |
| | | const items = groups.flatMap((group) => |
| | | (Array.isArray(group?.items) ? group.items : []).map((item) => ({ |
| | | ...item, |
| | | productDescription: group?.productName || item?.productDescription || "", |
| | | salesContractNo: group?.salesContractNo || item?.salesContractNo || "", |
| | | widthHeight: item?.widthHeight || "", |
| | | })) |
| | | ); |
| | | const items = groups.length |
| | | ? groups.flatMap((group) => { |
| | | const groupItems = Array.isArray(group?.items) ? group.items : []; |
| | | const groupNames = [ |
| | | ...extractNameListByKeys(group, PRODUCT_NAME_LIST_FIELD_KEYS), |
| | | ...extractNameListByKeys(group, PRODUCT_NAME_FIELD_KEYS), |
| | | ]; |
| | | return groupItems.map((item, index) => ({ |
| | | ...item, |
| | | // 优先使用明细自身产品名,兼容“名称数组/分隔字符串”的接口格式 |
| | | productDescription: resolveProductName(item, groupNames, index), |
| | | salesContractNo: group?.salesContractNo || item?.salesContractNo || "", |
| | | widthHeight: item?.widthHeight || "", |
| | | })); |
| | | }) |
| | | : (Array.isArray(data.items) ? data.items : []).map((item, index) => ({ |
| | | ...item, |
| | | productDescription: resolveProductName(item, dataLevelNames, index), |
| | | widthHeight: item?.widthHeight || "", |
| | | })); |
| | | |
| | | return { |
| | | ...data, |
| | |
| | | }) |
| | | .join(""); |
| | | |
| | | export const printSalesDeliveryNote = (rawData, selectedRow = {}) => { |
| | | export const printSalesDeliveryNote = async (rawData, selectedRow = {}, ledgerIds = null) => { |
| | | const data = normalizeInvoiceData(rawData, selectedRow); |
| | | const allItems = Array.isArray(data.items) ? data.items : []; |
| | | const pageSize = 18; |
| | | const itemPages = splitItemsByPage(allItems, pageSize); |
| | | const totalPages = itemPages.length; |
| | | |
| | | const ids = |
| | | Array.isArray(ledgerIds) && ledgerIds.length > 0 |
| | | ? ledgerIds |
| | | : selectedRow?.id !== undefined && selectedRow?.id !== null && selectedRow?.id !== "" |
| | | ? [selectedRow.id] |
| | | : []; |
| | | const shipmentRef = String( |
| | | data.shipmentNo || data.deliveryNo || data.externalOrderNo || selectedRow?.expressNumber || "" |
| | | ).trim(); |
| | | const qrPayload = JSON.stringify({ |
| | | type: "FH", |
| | | shipmentNo: shipmentRef, |
| | | ledgerIds: ids, |
| | | }); |
| | | let qrDataUrl = ""; |
| | | try { |
| | | qrDataUrl = await QRCode.toDataURL(qrPayload, { width: 160, margin: 1 }); |
| | | } catch { |
| | | qrDataUrl = ""; |
| | | } |
| | | |
| | | const printWindow = window.open("", "_blank", "width=1200,height=900"); |
| | | if (!printWindow) { |
| | |
| | | font-size: 12px; |
| | | margin-right: 8mm; |
| | | margin-bottom: 1px; |
| | | position: relative; |
| | | top: 6mm; |
| | | } |
| | | .head-mid { |
| | | display: grid; |
| | | grid-template-columns: 1fr auto 1fr; |
| | | align-items: center; |
| | | margin-bottom: 2px; |
| | | margin-top: 6mm; |
| | | margin-bottom: 0; |
| | | position: relative; |
| | | } |
| | | .head-mid-left { font-size: 13px; text-align: left; } |
| | | .head-mid-title { font-size: 20px; font-weight: 700; text-align: center; } |
| | | .head-mid-title-wrap { grid-column: 2; } |
| | | .head-mid-title { font-size: 20px; font-weight: 700; text-align: center; line-height: 1; } |
| | | .head-qr { |
| | | width: 18mm; |
| | | height: 18mm; |
| | | object-fit: contain; |
| | | display: block; |
| | | position: absolute; |
| | | left: calc(50% + 30mm); |
| | | top: calc(50% - 4mm); |
| | | transform: translateY(-50%); |
| | | } |
| | | .head-mid-right { font-size: 13px; text-align: right; padding-right: 8mm; } |
| | | table { width: 100%; border-collapse: collapse; table-layout: fixed; border: 1px solid #222; } |
| | | table { width: 100%; margin-top: 3mm; border-collapse: collapse; table-layout: fixed; border: 1px solid #222; } |
| | | td, th { border: 1px solid #222; padding: 2px 4px; font-size: 13px; text-align: center; vertical-align: middle; } |
| | | .left { text-align: left; } |
| | | .group-title td { font-weight: 700; } |
| | |
| | | </div> |
| | | <div class="head-mid"> |
| | | <div class="head-mid-left">对方单号: ${escapeHtml(data.deliveryNo || data.shippingNo || selectedRow.expressNumber || "")}</div> |
| | | <div class="head-mid-title">销售发货单</div> |
| | | <div class="head-mid-title-wrap"> |
| | | <div class="head-mid-title">销售发货单</div> |
| | | </div> |
| | | ${qrDataUrl ? `<img class="head-qr" src="${qrDataUrl}" alt="二维码" />` : ""} |
| | | <div class="head-mid-right">发货单号: ${escapeHtml(data.shipmentNo || data.deliveryNo || "")}</div> |
| | | </div> |
| | | <table> |