| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import html2canvas from 'html2canvas' |
| | | |
| | | function esc(s) { |
| | | if (!s) return '' |
| | | return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"') |
| | | } |
| | | |
| | | function fmtDate(v) { |
| | | if (!v) return '' |
| | | const d = new Date(v) |
| | | const y = d.getFullYear() |
| | | const m = String(d.getMonth() + 1).padStart(2, '0') |
| | | const day = String(d.getDate()).padStart(2, '0') |
| | | return y + '-' + m + '-' + day |
| | | } |
| | | |
| | | function fmtMoney(v) { |
| | | if (v == null) return '0.00' |
| | | return Number(v).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) |
| | | } |
| | | |
| | | function digitUppercase(n) { |
| | | const digits = ['é¶', '壹', 'è´°', 'å', 'è', 'ä¼', 'é', 'æ', 'æ', 'ç'] |
| | | const radices = ['', 'æ¾', 'ä½°', 'ä»', 'ä¸', 'æ¾', 'ä½°', 'ä»', '亿'] |
| | | let num = Math.abs(n) |
| | | let result = '' |
| | | const intPart = Math.floor(num) |
| | | const intStr = String(intPart) |
| | | const len = intStr.length |
| | | if (intPart === 0) { |
| | | result = 'é¶' |
| | | } else { |
| | | let zeroFlag = false |
| | | for (let i = 0; i < len; i++) { |
| | | const d = parseInt(intStr[i]) |
| | | const pos = len - 1 - i |
| | | if (d === 0) { |
| | | zeroFlag = true |
| | | if (pos % 4 === 0) { |
| | | result += radices[pos] |
| | | zeroFlag = false |
| | | } |
| | | } else { |
| | | if (zeroFlag) { |
| | | result += 'é¶' |
| | | zeroFlag = false |
| | | } |
| | | result += digits[d] + radices[pos] |
| | | } |
| | | } |
| | | if (result.endsWith('é¶')) result = result.slice(0, -1) |
| | | } |
| | | result += 'å
' |
| | | const decPart = Math.round((num - intPart) * 100) |
| | | if (decPart === 0) { |
| | | result += 'æ´' |
| | | } else { |
| | | const jiao = Math.floor(decPart / 10) |
| | | const fen = decPart % 10 |
| | | if (jiao > 0) result += digits[jiao] + 'è§' |
| | | if (fen > 0) result += digits[fen] + 'å' |
| | | } |
| | | return result |
| | | } |
| | | |
| | | function coreCss() { |
| | | return `*{margin:0;padding:0;box-sizing:border-box}body{font-family:"Microsoft YaHei","SimSun",sans-serif;padding:30px 36px;color:#333;background:#fff}.title{text-align:center;font-size:18px;font-weight:bold;letter-spacing:6px;margin-bottom:4px}.sub{text-align:center;font-size:12px;color:#999;margin-bottom:18px}.info-top{display:flex;justify-content:space-between;font-size:13px;margin-bottom:14px;padding-bottom:10px;border-bottom:1px dashed #ccc}.info-top .left,.info-top .right{line-height:2}.info-grid{display:grid;grid-template-columns:1fr 1fr;gap:4px 36px;font-size:13px;margin-bottom:14px;padding-bottom:8px;border-bottom:1px solid #ccc}.info-grid .l{color:#666}.info-grid .v{font-weight:500}table{width:100%;border-collapse:collapse;font-size:13px;margin-bottom:14px}th,td{border:1px solid #333;padding:5px 6px;text-align:center}th{background:#eee;font-weight:500}td.r{text-align:right}.summary{text-align:right;font-size:13px;margin-bottom:16px;line-height:2.2}.summary .cn{font-weight:bold;color:#cf1322}` |
| | | } |
| | | |
| | | function productRows(products) { |
| | | if (!products || !products.length) return '' |
| | | let html = '' |
| | | products.forEach((p, i) => { |
| | | const name = p.productCategory || '' |
| | | const spec = p.specificationModel || '' |
| | | html += '<tr>' |
| | | html += '<td>' + (i + 1) + '</td>' |
| | | html += '<td style="text-align:left">' + esc(name) |
| | | if (spec) html += '<br><span style="color:#888;font-size:12px">' + esc(spec) + '</span>' |
| | | html += '</td>' |
| | | html += '<td>' + esc(p.unit || '') + '</td>' |
| | | html += '<td class="r">' + (p.quantity != null ? Number(p.quantity).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) : '') + '</td>' |
| | | html += '<td class="r">' + fmtMoney(p.taxInclusiveUnitPrice) + '</td>' |
| | | html += '<td class="r">' + fmtMoney(p.taxInclusiveTotalPrice) + '</td>' |
| | | html += '<td>' + (p.taxRate != null ? p.taxRate + '%' : '') + '</td>' |
| | | html += '<td>' + esc(p.invoiceType || '') + '</td>' |
| | | html += '</tr>' |
| | | }) |
| | | return html |
| | | } |
| | | |
| | | function calcTotals(products) { |
| | | let total = 0, totalNoTax = 0 |
| | | if (products) { |
| | | products.forEach(p => { |
| | | if (p.taxInclusiveTotalPrice != null) total += Number(p.taxInclusiveTotalPrice) |
| | | if (p.taxExclusiveTotalPrice != null) totalNoTax += Number(p.taxExclusiveTotalPrice) |
| | | }) |
| | | } |
| | | return { total, totalNoTax } |
| | | } |
| | | |
| | | function buildSalesHtml(companyName, ledger, forPrint) { |
| | | const products = ledger.productData || [] |
| | | const { total, totalNoTax } = calcTotals(products) |
| | | const toolbar = forPrint ? '<div class="toolbar"><button onclick="window.print()">æå° / å¦å为 PDF</button></div>' : '' |
| | | const printCss = forPrint ? '@media print{body{padding:15px 24px}.toolbar{display:none}}' : '' |
| | | const printScript = forPrint ? '<script>window.onload=function(){setTimeout(function(){window.print();},300);};<\/script>' : '' |
| | | |
| | | return `<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"> |
| | | <title>éå®å - ${esc(ledger.salesContractNo)} - ${esc(companyName)}</title> |
| | | <style>.toolbar{text-align:center;margin-bottom:16px}.toolbar button{padding:6px 20px;font-size:14px;cursor:pointer;background:#1890ff;color:#fff;border:none;border-radius:4px}.toolbar button:hover{background:#40a9ff}${coreCss()}${printCss}</style></head><body> |
| | | ${toolbar} |
| | | <div class="title">${esc(companyName)}</div> |
| | | <div class="sub">éå®åå / éå®å</div> |
| | | <div class="info-top"> |
| | | <div class="left"><div><strong>éæ¹åç§°ï¼</strong>${esc(companyName)}</div></div> |
| | | <div class="right"><div><strong>ååç¼å·ï¼</strong>${esc(ledger.salesContractNo)}</div><div><strong>ç¾è®¢æ¥æï¼</strong>${fmtDate(ledger.executionDate)}</div></div> |
| | | </div> |
| | | <div class="info-grid"> |
| | | <div><span class="l">客æ·åç§°ï¼</span><span class="v">${esc(ledger.customerName)}</span></div> |
| | | <div><span class="l">项ç®åç§°ï¼</span><span class="v">${esc(ledger.projectName)}</span></div> |
| | | <div><span class="l">ä¸å¡åï¼</span><span class="v">${esc(ledger.salesman)}</span></div> |
| | | <div><span class="l">仿¬¾æ¹å¼ï¼</span><span class="v">${esc(ledger.paymentMethod)}</span></div> |
| | | <div><span class="l">å½å
¥æ¥æï¼</span><span class="v">${fmtDate(ledger.entryDate)}</span></div> |
| | | <div><span class="l">äº¤è´§æ¥æï¼</span><span class="v">${fmtDate(ledger.deliveryDate)}</span></div> |
| | | ${ledger.remarks ? `<div style="grid-column:1/-1"><span class="l">夿³¨ï¼</span><span class="v">${esc(ledger.remarks)}</span></div>` : ''} |
| | | </div> |
| | | <table><thead><tr><th>åºå·</th><th>产ååç§° / è§æ ¼åå·</th><th>åä½</th><th>æ°é</th><th>å«ç¨åä»·</th><th>å«ç¨æ»ä»·</th><th>ç¨ç</th><th>å票类å</th></tr></thead><tbody> |
| | | ${productRows(products)} |
| | | </tbody></table> |
| | | <div class="summary"> |
| | | <div>ä¸å«ç¨å计ï¼<strong>${fmtMoney(totalNoTax)}</strong></div> |
| | | <div>å«ç¨å计ï¼å¤§åï¼ï¼<span class="cn">${digitUppercase(total)}</span></div> |
| | | <div>å«ç¨å计ï¼å°åï¼ï¼<strong>Â¥${fmtMoney(total)}</strong></div> |
| | | </div> |
| | | ${printScript} |
| | | </body></html>` |
| | | } |
| | | |
| | | function buildPurchaseHtml(companyName, ledger, products, forPrint) { |
| | | const { total, totalNoTax } = calcTotals(products) |
| | | const contractAmount = ledger.contractAmount != null ? Number(ledger.contractAmount) : total |
| | | const toolbar = forPrint ? '<div class="toolbar"><button onclick="window.print()">æå° / å¦å为 PDF</button></div>' : '' |
| | | const printCss = forPrint ? '@media print{body{padding:15px 24px}.toolbar{display:none}}' : '' |
| | | const printScript = forPrint ? '<script>window.onload=function(){setTimeout(function(){window.print();},300);};<\/script>' : '' |
| | | |
| | | return `<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"> |
| | | <title>éè´å - ${esc(ledger.purchaseContractNumber)} - ${esc(companyName)}</title> |
| | | <style>.toolbar{text-align:center;margin-bottom:16px}.toolbar button{padding:6px 20px;font-size:14px;cursor:pointer;background:#1890ff;color:#fff;border:none;border-radius:4px}.toolbar button:hover{background:#40a9ff}${coreCss()}${printCss}</style></head><body> |
| | | ${toolbar} |
| | | <div class="title">${esc(companyName)}</div> |
| | | <div class="info-top"> |
| | | <div class="left"><div><strong>è´æ¹åç§°ï¼</strong>${esc(companyName)}</div></div> |
| | | <div class="right"><div><strong>ååç¼å·ï¼</strong>${esc(ledger.purchaseContractNumber)}</div><div><strong>ç¾è®¢æ¥æï¼</strong>${fmtDate(ledger.executionDate)}</div></div> |
| | | </div> |
| | | <div class="info-grid"> |
| | | <div><span class="l">ä¾åºååç§°ï¼</span><span class="v">${esc(ledger.supplierName)}</span></div> |
| | | <div><span class="l">项ç®åç§°ï¼</span><span class="v">${esc(ledger.projectName)}</span></div> |
| | | <div><span class="l">å½å
¥äººï¼</span><span class="v">${esc(ledger.recorderName)}</span></div> |
| | | <div><span class="l">仿¬¾æ¹å¼ï¼</span><span class="v">${esc(ledger.paymentMethod)}</span></div> |
| | | <div><span class="l">å½å
¥æ¥æï¼</span><span class="v">${fmtDate(ledger.entryDate)}</span></div> |
| | | ${ledger.salesContractNo ? `<div><span class="l">å
³èéå®ååå·ï¼</span><span class="v">${esc(ledger.salesContractNo)}</span></div>` : ''} |
| | | ${ledger.remarks ? `<div style="grid-column:1/-1"><span class="l">夿³¨ï¼</span><span class="v">${esc(ledger.remarks)}</span></div>` : ''} |
| | | </div> |
| | | <table><thead><tr><th>åºå·</th><th>产ååç§° / è§æ ¼åå·</th><th>åä½</th><th>æ°é</th><th>å«ç¨åä»·</th><th>å«ç¨æ»ä»·</th><th>ç¨ç</th><th>å票类å</th></tr></thead><tbody> |
| | | ${productRows(products)} |
| | | </tbody></table> |
| | | <div class="summary"> |
| | | <div>ä¸å«ç¨å计ï¼<strong>${fmtMoney(totalNoTax)}</strong></div> |
| | | <div>å«ç¨å计ï¼å¤§åï¼ï¼<span class="cn">${digitUppercase(total)}</span></div> |
| | | <div>å«ç¨å计ï¼å°åï¼ï¼<strong>Â¥${fmtMoney(total)}</strong></div> |
| | | <div>ååéé¢ï¼å¤§åï¼ï¼<span class="cn">${digitUppercase(contractAmount)}</span></div> |
| | | <div>ååéé¢ï¼å°åï¼ï¼<strong>Â¥${fmtMoney(contractAmount)}</strong></div> |
| | | </div> |
| | | ${printScript} |
| | | </body></html>` |
| | | } |
| | | |
| | | export function buildSalesDocument(companyName, ledger) { |
| | | return buildSalesHtml(companyName, ledger, false) |
| | | } |
| | | |
| | | export function buildPurchaseDocument(companyName, ledger, products) { |
| | | return buildPurchaseHtml(companyName, ledger, products, false) |
| | | } |
| | | |
| | | function buildSalesPrintHtml(companyName, ledger) { |
| | | return buildSalesHtml(companyName, ledger, true) |
| | | } |
| | | |
| | | function buildPurchasePrintHtml(companyName, ledger, products) { |
| | | return buildPurchaseHtml(companyName, ledger, products, true) |
| | | } |
| | | |
| | | export function openPrintWindow(html) { |
| | | const w = window.open('', '_blank', 'width=900,height=700') |
| | | if (!w) return |
| | | w.document.write(html) |
| | | w.document.close() |
| | | } |
| | | |
| | | export async function downloadAsJpg(html, filename) { |
| | | const iframe = document.createElement('iframe') |
| | | iframe.style.position = 'fixed' |
| | | iframe.style.left = '0' |
| | | iframe.style.top = '0' |
| | | iframe.style.width = '900px' |
| | | iframe.style.height = '600px' |
| | | iframe.style.opacity = '0' |
| | | iframe.style.pointerEvents = 'none' |
| | | iframe.style.zIndex = '99999' |
| | | document.body.appendChild(iframe) |
| | | |
| | | return new Promise((resolve, reject) => { |
| | | const iframeDoc = iframe.contentDocument || iframe.contentWindow.document |
| | | iframeDoc.open() |
| | | iframeDoc.write(html) |
| | | iframeDoc.close() |
| | | |
| | | const doCapture = async () => { |
| | | try { |
| | | const body = iframeDoc.body |
| | | iframe.style.height = Math.max(body.scrollHeight, 600) + 'px' |
| | | await new Promise(r => setTimeout(r, 100)) |
| | | const canvas = await html2canvas(body, { |
| | | scale: 2, |
| | | useCORS: true, |
| | | backgroundColor: '#ffffff', |
| | | windowWidth: 900 |
| | | }) |
| | | document.body.removeChild(iframe) |
| | | canvas.toBlob(blob => { |
| | | const url = URL.createObjectURL(blob) |
| | | const a = document.createElement('a') |
| | | a.href = url |
| | | a.download = filename |
| | | a.click() |
| | | URL.revokeObjectURL(url) |
| | | resolve() |
| | | }, 'image/jpeg', 0.95) |
| | | } catch (e) { |
| | | document.body.removeChild(iframe) |
| | | reject(e) |
| | | } |
| | | } |
| | | |
| | | iframe.onload = () => setTimeout(doCapture, 300) |
| | | setTimeout(doCapture, 1000) |
| | | }) |
| | | } |
| | | |
| | | export function downloadSalesJpg(companyName, ledger, filename) { |
| | | const html = buildSalesHtml(companyName, ledger, false) |
| | | return downloadAsJpg(html, filename) |
| | | } |
| | | |
| | | export function downloadPurchaseJpg(companyName, ledger, products, filename) { |
| | | const html = buildPurchaseHtml(companyName, ledger, products, false) |
| | | return downloadAsJpg(html, filename) |
| | | } |
| | | |
| | | export function printSales(companyName, ledger) { |
| | | const html = buildSalesPrintHtml(companyName, ledger) |
| | | openPrintWindow(html) |
| | | } |
| | | |
| | | export function printPurchase(companyName, ledger, products) { |
| | | const html = buildPurchasePrintHtml(companyName, ledger, products) |
| | | openPrintWindow(html) |
| | | } |
| | | |
| | | export function printSalesMulti(companyName, orders) { |
| | | let bodies = '' |
| | | orders.forEach((ledger, i) => { |
| | | const html = buildSalesHtml(companyName, ledger, true) |
| | | const bodyMatch = html.match(/<body>([\s\S]*)<\/body>/) |
| | | if (bodyMatch) { |
| | | const body = bodyMatch[1].replace(/<script>[\s\S]*?<\/script>/g, '') |
| | | bodies += '<div class="print-page' + (i < orders.length - 1 ? ' page-break' : '') + '">' + body + '</div>' |
| | | } |
| | | }) |
| | | const combined = `<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"> |
| | | <title>éå®å - ${esc(companyName)}</title> |
| | | <style> |
| | | *{margin:0;padding:0;box-sizing:border-box} |
| | | body{font-family:"Microsoft YaHei","SimSun",sans-serif;padding:20px 36px;color:#333;background:#fff} |
| | | .toolbar{text-align:center;margin-bottom:20px} |
| | | .toolbar button{padding:6px 24px;font-size:14px;cursor:pointer;background:#1890ff;color:#fff;border:none;border-radius:4px} |
| | | .toolbar button:hover{background:#40a9ff} |
| | | ${coreCss()} |
| | | .print-page{page-break-inside:avoid} |
| | | .page-break{page-break-after:always} |
| | | @media print{body{padding:15px 24px}.toolbar{display:none}} |
| | | </style></head><body> |
| | | <div class="toolbar"><button onclick="window.print()">æå°å
¨é¨ (å
±${orders.length}份)</button></div> |
| | | ${bodies} |
| | | <script>window.onload=function(){setTimeout(function(){window.print();},300);};<\/script> |
| | | </body></html>` |
| | | openPrintWindow(combined) |
| | | } |
| | | |
| | | export function printPurchaseMulti(companyName, orders) { |
| | | let bodies = '' |
| | | orders.forEach((item, i) => { |
| | | const html = buildPurchaseHtml(companyName, item.ledger, item.products, true) |
| | | const bodyMatch = html.match(/<body>([\s\S]*)<\/body>/) |
| | | if (bodyMatch) { |
| | | const body = bodyMatch[1].replace(/<script>[\s\S]*?<\/script>/g, '') |
| | | bodies += '<div class="print-page' + (i < orders.length - 1 ? ' page-break' : '') + '">' + body + '</div>' |
| | | } |
| | | }) |
| | | const combined = `<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"> |
| | | <title>éè´å - ${esc(companyName)}</title> |
| | | <style> |
| | | *{margin:0;padding:0;box-sizing:border-box} |
| | | body{font-family:"Microsoft YaHei","SimSun",sans-serif;padding:20px 36px;color:#333;background:#fff} |
| | | .toolbar{text-align:center;margin-bottom:20px} |
| | | .toolbar button{padding:6px 24px;font-size:14px;cursor:pointer;background:#1890ff;color:#fff;border:none;border-radius:4px} |
| | | .toolbar button:hover{background:#40a9ff} |
| | | ${coreCss()} |
| | | .print-page{page-break-inside:avoid} |
| | | .page-break{page-break-after:always} |
| | | @media print{body{padding:15px 24px}.toolbar{display:none}} |
| | | </style></head><body> |
| | | <div class="toolbar"><button onclick="window.print()">æå°å
¨é¨ (å
±${orders.length}份)</button></div> |
| | | ${bodies} |
| | | <script>window.onload=function(){setTimeout(function(){window.print();},300);};<\/script> |
| | | </body></html>` |
| | | openPrintWindow(combined) |
| | | } |