From c48476691b8dad4db5964837e7cdcef253b9e9e7 Mon Sep 17 00:00:00 2001
From: yuan <123@>
Date: 星期四, 23 七月 2026 16:13:38 +0800
Subject: [PATCH] fix: 采购和销售优化公司名称显示

---
 src/utils/documentPrint.js |  331 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 331 insertions(+), 0 deletions(-)

diff --git a/src/utils/documentPrint.js b/src/utils/documentPrint.js
new file mode 100644
index 0000000..2db84f3
--- /dev/null
+++ b/src/utils/documentPrint.js
@@ -0,0 +1,331 @@
+import html2canvas from 'html2canvas'
+
+function esc(s) {
+  if (!s) return ''
+  return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')
+}
+
+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)
+}

--
Gitblit v1.9.3