| ¶Ô±ÈÐÂÎļþ |
| | |
| | | const PRINT_TITLE = "éå®è®¢å"; |
| | | |
| | | const formatDisplayDate = (value) => { |
| | | if (!value) return ""; |
| | | const date = new Date(value); |
| | | if (Number.isNaN(date.getTime())) return String(value); |
| | | const year = date.getFullYear(); |
| | | const month = String(date.getMonth() + 1).padStart(2, "0"); |
| | | const day = String(date.getDate()).padStart(2, "0"); |
| | | return `${year}/${month}/${day}`; |
| | | }; |
| | | |
| | | const getCurrentDateTime = () => { |
| | | const date = new Date(); |
| | | const year = date.getFullYear(); |
| | | const month = String(date.getMonth() + 1).padStart(2, "0"); |
| | | const day = String(date.getDate()).padStart(2, "0"); |
| | | return `${year}/${month}/${day}`; |
| | | }; |
| | | |
| | | const escapeHtml = (value) => |
| | | String(value ?? "") |
| | | .replaceAll("&", "&") |
| | | .replaceAll("<", "<") |
| | | .replaceAll(">", ">") |
| | | .replaceAll('"', """) |
| | | .replaceAll("'", "'"); |
| | | |
| | | const toNumber = (value) => { |
| | | const num = Number(value); |
| | | return Number.isFinite(num) ? num : 0; |
| | | }; |
| | | |
| | | const formatMoney = (value) => { |
| | | const num = toNumber(value); |
| | | return num.toFixed(2); |
| | | }; |
| | | |
| | | const formatArea = (value) => { |
| | | const num = toNumber(value); |
| | | return num ? num.toFixed(2) : ""; |
| | | }; |
| | | |
| | | const formatQty = (value) => { |
| | | const num = toNumber(value); |
| | | return num ? String(num) : ""; |
| | | }; |
| | | |
| | | const getItemAmount = (item) => { |
| | | const fromPrice = toNumber(item?.taxInclusiveUnitPrice) * toNumber(item?.quantity); |
| | | const amount = toNumber(item?.taxInclusiveTotalPrice || item?.amount || fromPrice); |
| | | return amount; |
| | | }; |
| | | |
| | | const getItemArea = (item) => |
| | | toNumber(item?.area || item?.settleTotalArea || item?.actualTotalArea); |
| | | |
| | | const getDeliveryAddress = (data) => |
| | | data?.deliveryAddress || data?.shippingAddress || data?.address || data?.shipAddress || ""; |
| | | |
| | | const normalizeRequirementText = (value) => { |
| | | const text = String(value ?? "").trim(); |
| | | if (!text) return ""; |
| | | return text |
| | | .replace(/^å å·¥è¦æ±å夿³¨[:ï¼]\s*/g, "") |
| | | .replace(/^å å·¥è¦æ±[:ï¼]\s*/g, "") |
| | | .replace(/^夿³¨[:ï¼]\s*/g, ""); |
| | | }; |
| | | |
| | | const extractOtherFees = (data, items) => { |
| | | const source = []; |
| | | if (Array.isArray(data?.otherAmounts)) source.push(...data.otherAmounts); |
| | | if (Array.isArray(data?.otherAmountList)) source.push(...data.otherAmountList); |
| | | if (Array.isArray(data?.otherAmountProjects)) source.push(...data.otherAmountProjects); |
| | | if (Array.isArray(data?.salesProductProcessList)) source.push(...data.salesProductProcessList); |
| | | (Array.isArray(items) ? items : []).forEach((item) => { |
| | | if (Array.isArray(item?.salesProductProcessList)) source.push(...item.salesProductProcessList); |
| | | if (Array.isArray(item?.otherAmounts)) source.push(...item.otherAmounts); |
| | | }); |
| | | |
| | | const map = new Map(); |
| | | source.forEach((fee) => { |
| | | const name = String(fee?.processName || fee?.name || fee?.itemName || "").trim(); |
| | | if (!name) return; |
| | | const quantity = toNumber(fee?.quantity || fee?.num); |
| | | const unitPrice = toNumber(fee?.unitPrice || fee?.price); |
| | | const amount = |
| | | toNumber(fee?.amount || fee?.totalPrice) || (quantity && unitPrice ? quantity * unitPrice : 0); |
| | | const key = name; |
| | | if (!map.has(key)) { |
| | | map.set(key, { name, quantity: 0, unitPrice: 0, amount: 0 }); |
| | | } |
| | | const row = map.get(key); |
| | | row.quantity += quantity; |
| | | row.unitPrice = unitPrice || row.unitPrice; |
| | | row.amount += amount; |
| | | }); |
| | | return Array.from(map.values()); |
| | | }; |
| | | |
| | | const renderOtherFeeNames = (rows) => |
| | | (Array.isArray(rows) ? rows : []) |
| | | .map((row) => escapeHtml(row.name)) |
| | | .filter(Boolean) |
| | | .join("<br/>"); |
| | | |
| | | const renderOtherFeeUnitPrices = (rows) => |
| | | (Array.isArray(rows) ? rows : []) |
| | | .map((row) => (row.unitPrice ? formatMoney(row.unitPrice) : "")) |
| | | .filter(Boolean) |
| | | .join("<br/>"); |
| | | |
| | | const renderOtherFeeQuantities = (rows) => |
| | | (Array.isArray(rows) ? rows : []) |
| | | .map((row) => (row.quantity ? String(row.quantity) : "")) |
| | | .filter(Boolean) |
| | | .join("<br/>"); |
| | | |
| | | const renderOtherFeeAmounts = (rows) => |
| | | (Array.isArray(rows) ? rows : []) |
| | | .map((row) => (row.amount ? formatMoney(row.amount) : "")) |
| | | .filter(Boolean) |
| | | .join("<br/>"); |
| | | |
| | | const splitItemsByPage = (items, pageSize) => { |
| | | const list = Array.isArray(items) ? items : []; |
| | | if (list.length === 0) return [[]]; |
| | | const pages = []; |
| | | for (let i = 0; i < list.length; i += pageSize) { |
| | | pages.push(list.slice(i, i + pageSize)); |
| | | } |
| | | return pages; |
| | | }; |
| | | |
| | | const renderRows = (items, startIndex) => { |
| | | const list = Array.isArray(items) ? items : []; |
| | | if (list.length === 0) { |
| | | return ` |
| | | <tr> |
| | | <td colspan="8" class="empty">ææ æç»</td> |
| | | </tr> |
| | | `; |
| | | } |
| | | return list |
| | | .map((item, idx) => { |
| | | const width = escapeHtml(item?.width); |
| | | const height = escapeHtml(item?.height); |
| | | const sizeText = width || height ? `${width}*${height}` : ""; |
| | | const unitPrice = formatMoney(item?.taxInclusiveUnitPrice || item?.unitPrice); |
| | | const amount = formatMoney(getItemAmount(item)); |
| | | return ` |
| | | <tr> |
| | | <td>${startIndex + idx + 1}</td> |
| | | <td>${escapeHtml(item?.floorCode)}</td> |
| | | <td>${sizeText}</td> |
| | | <td>${formatQty(item?.quantity)}</td> |
| | | <td>${formatArea(item?.area || item?.settleTotalArea || item?.actualTotalArea)}</td> |
| | | <td>${unitPrice}</td> |
| | | <td>${amount}</td> |
| | | <td>${escapeHtml(item?.processRequirement)}</td> |
| | | </tr> |
| | | `; |
| | | }) |
| | | .join(""); |
| | | }; |
| | | |
| | | export const printSalesOrder = (orderData) => { |
| | | const data = orderData ?? {}; |
| | | const items = Array.isArray(data.items) ? data.items : []; |
| | | const pageSize = 15; |
| | | const pages = splitItemsByPage(items, pageSize); |
| | | const totalPages = pages.length; |
| | | |
| | | const totalQuantity = toNumber(data.totalQuantity) || items.reduce((sum, item) => sum + toNumber(item?.quantity), 0); |
| | | const totalArea = toNumber(data.totalArea) || items.reduce((sum, item) => sum + getItemArea(item), 0); |
| | | const totalAmount = items.reduce((sum, item) => sum + getItemAmount(item), 0); |
| | | const otherFees = extractOtherFees(data, items); |
| | | |
| | | const printWindow = window.open("", "_blank", "width=1200,height=900"); |
| | | if (!printWindow) { |
| | | throw new Error("æµè§å¨æ¦æªäºå¼¹çªï¼è¯·å
许弹çªåéè¯"); |
| | | } |
| | | |
| | | const html = ` |
| | | <!DOCTYPE html> |
| | | <html> |
| | | <head> |
| | | <meta charset="UTF-8" /> |
| | | <title>${PRINT_TITLE}</title> |
| | | <style> |
| | | body { margin: 0; padding: 0; font-family: "SimSun", serif; color: #111; } |
| | | .page { |
| | | width: 198mm; |
| | | min-height: 186mm; |
| | | margin: 0 auto; |
| | | padding: 5mm 4mm 16mm; |
| | | box-sizing: border-box; |
| | | page-break-after: always; |
| | | position: relative; |
| | | } |
| | | .page:last-child { page-break-after: auto; } |
| | | .title-main { text-align: center; font-size: 22px; font-weight: 700; letter-spacing: 1px; line-height: 1.15; margin-top: 1mm; } |
| | | .title-sub { text-align: center; font-size: 22px; font-weight: 700; letter-spacing: 6px; line-height: 1.15; margin: 1mm 0 4mm; } |
| | | table { width: 100%; border-collapse: collapse; table-layout: fixed; border: 1px solid #222; } |
| | | .detail-table { |
| | | border-right: 1px solid #222 !important; |
| | | box-shadow: inset -1px 0 0 #222; |
| | | } |
| | | .detail-table-wrap { |
| | | position: relative; |
| | | margin-top: -1px; |
| | | } |
| | | .detail-table-wrap::after { |
| | | content: ""; |
| | | position: absolute; |
| | | top: 0; |
| | | right: 0; |
| | | width: 1px; |
| | | height: 100%; |
| | | background: #222; |
| | | pointer-events: none; |
| | | } |
| | | .sheet-wrap { |
| | | position: relative; |
| | | overflow: visible; |
| | | } |
| | | .sheet-wrap::after { |
| | | content: ""; |
| | | position: absolute; |
| | | top: 0; |
| | | right: -0.4px; |
| | | width: 1.2px; |
| | | height: 100%; |
| | | background: #222; |
| | | pointer-events: none; |
| | | z-index: 20; |
| | | } |
| | | td, th { border: 1px solid #222; padding: 2px 4px; font-size: 13px; text-align: center; vertical-align: middle; } |
| | | tr > td:first-child, tr > th:first-child { border-left: 1px solid #222 !important; } |
| | | tr > td:last-child, tr > th:last-child { border-right: 1px solid #222 !important; } |
| | | .left { text-align: left; } |
| | | .bold { font-weight: 700; } |
| | | .cell-title { font-weight: 700; white-space: nowrap; } |
| | | .product-row td { font-size: 13px; font-weight: 700; } |
| | | .empty { height: 140px; color: #777; } |
| | | .large-row td { height: 46px; vertical-align: top; } |
| | | .total-row td { font-weight: 700; font-size: 13px; line-height: 1.2; } |
| | | .footer { margin-top: 5px; display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 6px; font-size: 13px; line-height: 1.7; } |
| | | .footer b { display: inline-block; min-width: 74px; } |
| | | .customer-sign-row { |
| | | margin-top: 6px; |
| | | display: grid; |
| | | grid-template-columns: 1fr 1fr 1fr; |
| | | } |
| | | .customer-sign-cell { text-align: left; } |
| | | .customer-sign { font-size: 13px; font-weight: 700; line-height: 1.2; } |
| | | .right { text-align: right; } |
| | | .footer-page-right { display: block; text-align: right; font-size: 12px; } |
| | | .footer-pair { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | gap: 8px; |
| | | } |
| | | .other-fee-cell { white-space: normal; word-break: break-all; line-height: 1.5; vertical-align: top; } |
| | | .other-fee-header { font-weight: 700; } |
| | | .other-fee-header-grid { |
| | | display: grid; |
| | | grid-template-columns: repeat(4, 1fr); |
| | | align-items: center; |
| | | text-align: center; |
| | | } |
| | | .other-fee-header-row td { border-bottom: none !important; } |
| | | .other-fee-content-row td { border-top: none !important; } |
| | | @media print { |
| | | @page { size: A4 landscape; margin: 6mm; } |
| | | .page { width: 100%; margin: 0; padding: 0 0 14mm; min-height: 0; } |
| | | } |
| | | </style> |
| | | </head> |
| | | <body> |
| | | ${pages |
| | | .map((pageItems, pageIndex) => { |
| | | const isLastPage = pageIndex === totalPages - 1; |
| | | const startIndex = pageIndex * pageSize; |
| | | return ` |
| | | <div class="page"> |
| | | <div class="title-main">鹤å£å¤©æ²é¢åç»çå</div> |
| | | <div class="title-sub">éå®è®¢å</div> |
| | | |
| | | <div class="sheet-wrap"> |
| | | <table> |
| | | <colgroup> |
| | | <col style="width: 14%;" /> |
| | | <col style="width: 20%;" /> |
| | | <col style="width: 20%;" /> |
| | | <col style="width: 14%;" /> |
| | | <col style="width: 32%;" /> |
| | | </colgroup> |
| | | <tr> |
| | | <td class="cell-title">客æ·åç§°:</td> |
| | | <td class="left">${escapeHtml(data.customerName)}</td> |
| | | <td class="cell-title">项ç®åç§°:</td> |
| | | <td class="left">${escapeHtml(data.projectName)}</td> |
| | | <td class="left"><span class="cell-title">ä¸ å¡ å:</span> ${escapeHtml(data.salesman)}</td> |
| | | </tr> |
| | | <tr> |
| | | <td class="cell-title">å¶åæ¥æ:</td> |
| | | <td class="left">${escapeHtml(formatDisplayDate(data.registerDate || data.entryDate))}</td> |
| | | <td class="cell-title">äº¤è´§æ¥æ:</td> |
| | | <td class="left">${escapeHtml(formatDisplayDate(data.deliveryDate))}</td> |
| | | <td class="left"></td> |
| | | </tr> |
| | | <tr> |
| | | <td class="cell-title">éè´§å°å:</td> |
| | | <td colspan="4" class="left">${escapeHtml(getDeliveryAddress(data))}</td> |
| | | </tr> |
| | | </table> |
| | | |
| | | <div class="detail-table-wrap"> |
| | | <table class="detail-table"> |
| | | <colgroup> |
| | | <col style="width: 9%;" /> |
| | | <col style="width: 12%;" /> |
| | | <col style="width: 12%;" /> |
| | | <col style="width: 7%;" /> |
| | | <col style="width: 10%;" /> |
| | | <col style="width: 8%;" /> |
| | | <col style="width: 10%;" /> |
| | | <col style="width: 32%;" /> |
| | | </colgroup> |
| | | <tr> |
| | | <th>åºå·</th> |
| | | <th>楼å±ç¼å·</th> |
| | | <th>宽(å¼§é¿)*é«</th> |
| | | <th>æ°é</th> |
| | | <th>ç»ç®é¢ç§¯</th> |
| | | <th>åä»·</th> |
| | | <th>éé¢</th> |
| | | <th>å å·¥è¦æ±</th> |
| | | </tr> |
| | | <tr class="product-row"> |
| | | <td colspan="6" class="left">产ååç§°: ${escapeHtml(items[0]?.productDescription)}</td> |
| | | <td colspan="2" class="left">订åç¼å·: ${escapeHtml(data.salesContractNo)}</td> |
| | | </tr> |
| | | ${renderRows(pageItems, startIndex)} |
| | | ${ |
| | | isLastPage |
| | | ? ` |
| | | <tr class="total-row"> |
| | | <td colspan="3" class="left">å°è®¡:</td> |
| | | <td>${totalQuantity || ""}</td> |
| | | <td>${totalArea ? totalArea.toFixed(2) : ""}</td> |
| | | <td></td> |
| | | <td>${formatMoney(totalAmount)}</td> |
| | | <td></td> |
| | | </tr> |
| | | <tr class="total-row"> |
| | | <td colspan="3" class="left">å计:</td> |
| | | <td>${totalQuantity || ""}</td> |
| | | <td>${totalArea ? totalArea.toFixed(2) : ""}</td> |
| | | <td></td> |
| | | <td>${formatMoney(totalAmount)}</td> |
| | | <td></td> |
| | | </tr> |
| | | <tr class="other-fee-header-row"> |
| | | <td colspan="5" class="left other-fee-header"> |
| | | <div class="other-fee-header-grid"> |
| | | <span>å
¶ä»è´¹ç¨</span> |
| | | <span>åä»·</span> |
| | | <span>æ°é</span> |
| | | <span>éé¢</span> |
| | | </div> |
| | | </td> |
| | | <td colspan="3" class="left other-fee-header">å å·¥è¦æ±å夿³¨:</td> |
| | | </tr> |
| | | <tr class="large-row other-fee-content-row"> |
| | | <td colspan="5" class="left other-fee-cell"> |
| | | ${ |
| | | renderOtherFeeNames(otherFees) || |
| | | renderOtherFeeUnitPrices(otherFees) || |
| | | renderOtherFeeQuantities(otherFees) || |
| | | renderOtherFeeAmounts(otherFees) |
| | | ? `${renderOtherFeeNames(otherFees)}` |
| | | : "" |
| | | } |
| | | </td> |
| | | <td colspan="3" class="left other-fee-cell">${escapeHtml(normalizeRequirementText(data.orderProcessRequirement))}</td> |
| | | </tr> |
| | | <tr class="total-row"> |
| | | <td colspan="8" class="left">æ»éé¢: ${formatMoney(totalAmount)}å
</td> |
| | | </tr> |
| | | ` |
| | | : ` |
| | | <tr><td colspan="8" class="right">ä¸é¡µç»...</td></tr> |
| | | ` |
| | | } |
| | | </table> |
| | | </div> |
| | | </div> |
| | | |
| | | ${ |
| | | isLastPage |
| | | ? ` |
| | | <div class="customer-sign-row"> |
| | | <span></span> |
| | | <span></span> |
| | | <span class="customer-sign customer-sign-cell">客æ·ç¾å:</span> |
| | | </div> |
| | | <div class="footer"> |
| | | <div><b>å¶åå:</b>${escapeHtml(data.register)}</div> |
| | | <div><b>å®¡æ ¸å:</b>${escapeHtml(data.register)}</div> |
| | | <div><b>æå°äºº:</b>${escapeHtml(data.register)}</div> |
| | | <div><b>å¶åæ¥æ:</b>${escapeHtml(formatDisplayDate(data.registerDate || data.entryDate))}</div> |
| | | <div><b>å®¡æ ¸æ¥æ:</b>${escapeHtml(formatDisplayDate(data.registerDate || data.entryDate))}</div> |
| | | <div class="footer-pair"><span><b>æå°æ¶é´:</b>${getCurrentDateTime()}</span><span class="footer-page-right">第${pageIndex + 1}页,å
±${totalPages}页</span></div> |
| | | </div> |
| | | ` |
| | | : "" |
| | | } |
| | | </div>`; |
| | | }) |
| | | .join("")} |
| | | </body> |
| | | </html> |
| | | `; |
| | | |
| | | printWindow.document.write(html); |
| | | printWindow.document.close(); |
| | | printWindow.onload = () => { |
| | | setTimeout(() => { |
| | | printWindow.focus(); |
| | | printWindow.print(); |
| | | printWindow.close(); |
| | | }, 300); |
| | | }; |
| | | }; |