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)
|
}
|