import type { Plan, PlanProduct } from '@/api/pl' import type { SlipPlanProduct } from '@/utils/pl-batch-print' import { formatRemarkForSlipHtml } from '@/utils/pl-batch-print' import { applyPdfStableTableCellBorders } from '@/utils/pl-slip-pdf-render' /** 送(提)货单打印默认公司信息(可在富文本中修改) */ export const DELIVERY_SLIP_DEFAULTS = { companyName: '安庆市山峡化工有限公司', receivingUnit: '安庆市山峡化工有限公司', companyAddress: '安庆市高新区霞虹路5号', companyPhone: '0556-5999710', contactPhone: '方总13395560066' } as const const DOC_FONT = "SimSun, 'Songti SC', serif" const FONT_TITLE = 'font-size:26px;font-weight:bold;' const FONT_BODY = 'font-size:20px;' const FONT_SIGN = 'font-size:14px;' const FONT_SIDE = 'font-size:18px;' const DOC_PADDING = '8px 10px 24px' /** 单据内容区最大宽度(编辑预览居中,略窄于 A4 像素宽) */ export const DELIVERY_SLIP_DOC_MAX_WIDTH = '960px' const DOC_WRAP_EXTRA = `max-width:${DELIVERY_SLIP_DOC_MAX_WIDTH};margin:0 auto;box-sizing:border-box;` const LOGO_HEIGHT = 36 const LOGO_LEFT = 50 const TITLE_STYLE = `${FONT_TITLE}font-weight:700 !important;letter-spacing:6px;line-height:1.35;` const TITLE_SUB_STYLE = `${TITLE_STYLE}margin-top:2px;letter-spacing:10px;` const TABLE_ROW_PADDING = '4px' /** 信息表等宽列 */ const INFO_COLGROUP = ` ` /** * 物料表列宽:单据编号 + 加宽编号/名称,收窄规格/单位;前 5 列合计供条款区使用。 * 单据编号 / 物料编号 / 物料名称 / 规格 / 单位 / 数量 / 产品规格 / 实收数量 */ const PRODUCT_COLGROUP = ` ` /** 物料网格 padding(编辑器预览) */ const PRODUCT_GRID_CELL_PADDING = '8px 6px' const PRODUCT_GRID_TH_PADDING = '10px 6px' /** PDF 专用:padding 直接写在 td/th(html2canvas 对 flex/vertical-align 支持差,底部多留白) */ const PRODUCT_GRID_PDF_TD_PADDING = '8px 6px 12px 6px' const PRODUCT_GRID_PDF_TH_PADDING = '10px 6px 16px 6px' const PRODUCT_GRID_PDF_LINE_HEIGHT = '24px' /** PDF 专用:备注/条款/地址等左对齐合并单元格 */ const PRODUCT_FOOTER_PDF_PADDING = '10px 6px 16px 6px' const PRODUCT_FOOTER_PDF_LINE_HEIGHT = '1.35' /** 物料表格边框线宽(备注/条款等合并单元格,与网格一致便于 PDF 衔接) */ const PRODUCT_TABLE_BORDER = '0.3px solid #000' /** 物料明细网格线(与合同 PDF 一致:全边框 inline + 0.3px,html2canvas 渲染稳定) */ const PRODUCT_GRID_BORDER = '0.3px solid #000' /** 物料表格单元格:长文本自动换行 */ const PRODUCT_CELL_WRAP = 'white-space:normal;word-wrap:break-word;overflow-wrap:break-word;word-break:break-word;' const PRODUCT_GRID_CELL = `padding:${PRODUCT_GRID_CELL_PADDING};box-sizing:border-box;border:${PRODUCT_GRID_BORDER} !important;text-align:center;${FONT_BODY}line-height:normal;background-color:#fff !important;${PRODUCT_CELL_WRAP}` const PRODUCT_CELL = `padding:4px 6px;border:${PRODUCT_TABLE_BORDER} !important;text-align:center;${FONT_BODY}line-height:1.35;background-color:#fff !important;${PRODUCT_CELL_WRAP}` const PRODUCT_TH = `padding:${PRODUCT_GRID_TH_PADDING};box-sizing:border-box;border:${PRODUCT_GRID_BORDER} !important;text-align:center;${FONT_BODY}line-height:normal;font-weight:bold;background-color:#fff !important;${PRODUCT_CELL_WRAP}` const PRODUCT_TABLE_CELL_LEFT = `padding:10px 6px 14px 6px;border:${PRODUCT_TABLE_BORDER} !important;text-align:left;${FONT_BODY}line-height:1.35;vertical-align:top;background-color:#fff !important;` /** 条款单元格:行高随内容,避免被右侧印章区 min-height 撑出底部空白 */ const PRODUCT_TERMS_CELL_LEFT = `padding:10px 6px 14px 6px;border:${PRODUCT_TABLE_BORDER} !important;text-align:left;${FONT_BODY}line-height:1.35;vertical-align:top;background-color:#fff !important;` const TERMS_TEXT_STYLE = 'line-height:1.35;' const SEAL_BLOCK_LINE_STYLE = 'line-height:1.35;' /** 送(提)货单印章:public/sxshzyz.png,定位在地址单元格右上角(相对原位置向上 66px) */ const DELIVERY_SLIP_SEAL_IMAGE_SRC = '/sxshzyz.png' const DELIVERY_SLIP_SEAL_TOP = -66 const DELIVERY_SLIP_SEAL_IMAGE_STYLE = `position:absolute;right:4px;top:${DELIVERY_SLIP_SEAL_TOP}px;width:140px;height:140px;opacity:0.85;z-index:10;` const HANDWRITE_BLANK_INNER = '    ' const HANDWRITE_BLANK = `(${HANDWRITE_BLANK_INNER})` const PLAIN_CELL = `border:none !important;padding:${TABLE_ROW_PADDING};${FONT_BODY}line-height:1.8;vertical-align:top;background:transparent !important;` const NO_BORDER = `${PLAIN_CELL} vertical-align:bottom;` /** 基本信息:label 与值同一行,值区域占满剩余宽度并带下划线 */ const INFO_ROW = `display:flex;align-items:flex-end;width:100%;box-sizing:border-box;` const INFO_LABEL = `white-space:nowrap;flex-shrink:0;padding-right:2px;` const INFO_UNDERLINE = '1px solid #000' const SIGN_UNDERLINE = '1px solid #000' /** 签字值下划线与文字间距(底部 padding 把 border-bottom 推离字模) */ const SIGN_VALUE_PADDING = '0 2px 6px 2px' const SIGN_VALUE_PDF_PADDING = '0 2px 8px 2px' const INFO_VALUE_INLINE_LEFT = `flex:1;min-width:0;box-sizing:border-box;border-bottom:${INFO_UNDERLINE} !important;padding:0 2px 2px;text-align:left;min-height:1.3em;` const INFO_VALUE_INLINE_CENTER = `flex:1;min-width:0;box-sizing:border-box;border-bottom:${INFO_UNDERLINE} !important;padding:0 2px 2px;text-align:center;min-height:1.3em;` const INFO_CELL_LEFT = `${NO_BORDER}text-align:left;` const INFO_CELL_RIGHT = `${NO_BORDER}text-align:left;` const INFO_GAP_BEFORE_PRODUCT = 12 const GAP_BEFORE_PRODUCT_CELL = `border:none !important;padding:0 !important;line-height:0 !important;font-size:0 !important;background:transparent !important;height:${INFO_GAP_BEFORE_PRODUCT}px !important;min-height:${INFO_GAP_BEFORE_PRODUCT}px !important;` const INFO_GAP_BEFORE_SIGN = 12 const GAP_BEFORE_SIGN_CELL = `border:none !important;padding:0 !important;line-height:0 !important;font-size:0 !important;background:transparent !important;height:${INFO_GAP_BEFORE_SIGN}px !important;min-height:${INFO_GAP_BEFORE_SIGN}px !important;` const SIDE_LABELS_COL_WIDTH = 36 function buildDeliverySlipSignItem(label: string, valueContent: string, minWidth: string): string { return `${label}${valueContent}` } function buildDeliverySlipSignRowHtml(): string { return `
${buildDeliverySlipSignItem('仓库:', ' ', '80px')} ${buildDeliverySlipSignItem('审核:', ' ', '80px')} ${buildDeliverySlipSignItem('客户签收:', ' ', '80px')} ${buildDeliverySlipSignItem('司机/送货人(附车牌):', ' ', '120px')}
` } export interface DeliverySlipRenderContext { /** CRM 产品 id -> 物料编码 productNo */ productCodeById?: Record /** 单位字典 code -> 展示文案 */ getUnitLabel?: (unitCode: string) => string } function resolveSlipProductCode(product: PlanProduct, ctx?: DeliverySlipRenderContext): string { const productId = String(product.productId || '').trim() const storedNo = String(product.productNo || '').trim() const mappedNo = productId ? String(ctx?.productCodeById?.[productId] || '').trim() : '' if (mappedNo) return mappedNo if (storedNo && productId && storedNo !== productId) return storedNo return storedNo } function resolveSlipUnitLabel(unitCode: string | undefined, ctx?: DeliverySlipRenderContext): string { const code = String(unitCode || '').trim() if (!code) return '' return ctx?.getUnitLabel?.(code) || code } export function canPrintDeliverySlip(plan?: Pick | null): boolean { return plan?.planType !== 'inbound' && plan?.transportStatus === 'arranged' } function formatPlanDateChinese(planDate?: string): string { if (!planDate) return '' const date = new Date(planDate) if (Number.isNaN(date.getTime())) return planDate return `${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日` } function formatQty(value?: number | string | null): string { const num = parseFloat(String(value ?? '')) if (Number.isNaN(num)) return '' return num.toFixed(3) } function buildReceivingUnit(plan: Plan): string { return plan.customerName?.trim() || plan.destinationName?.trim() || '' } function formatDeliverySlipProductGridCellContent(content: string): string { const plain = content .trim() .replace(/
]*>([\s\S]*?)<\/div>/gi, '$1') .replace(/
]*>([\s\S]*?)<\/div>/gi, '$1') .replace(/]*>([\s\S]*?)<\/span>/gi, '$1') .replace(/]*>([\s\S]*?)<\/span>/gi, '$1') .replace(/^]*>([\s\S]*?)<\/p>$/i, '$1') .trim() return plain || ' ' } function buildProductRows( products: PlanProduct[] = [], planNo = '', ctx?: DeliverySlipRenderContext ): string { if (!products.length) { return `暂无产品信息` } const dataRows = products .map((product) => { const rowPlanNo = (product as SlipPlanProduct).slipPlanNo || planNo const planNoText = formatDeliverySlipProductGridCellContent(rowPlanNo) const qty = product.outboundQty ?? product.outboundEstimateQty ?? product.quantity ?? '' return ` ${planNoText} ${formatDeliverySlipProductGridCellContent(resolveSlipProductCode(product, ctx))} ${formatDeliverySlipProductGridCellContent(product.productName || '')} ${formatDeliverySlipProductGridCellContent(product.model || '')} ${formatDeliverySlipProductGridCellContent(resolveSlipUnitLabel(product.unit, ctx))} ${formatDeliverySlipProductGridCellContent(formatQty(qty))} ${formatDeliverySlipProductGridCellContent(product.packageSpec || '')}   ` }) .join('') return dataRows } export const DELIVERY_SLIP_PRINT_CSS = ` .delivery-slip-doc { padding: ${DOC_PADDING} !important; max-width: ${DELIVERY_SLIP_DOC_MAX_WIDTH} !important; margin-left: auto !important; margin-right: auto !important; box-sizing: border-box !important; } .delivery-slip-doc .delivery-slip-title-heading { font-weight: 700 !important; line-height: 1.35 !important; } .delivery-slip-doc .delivery-slip-title-heading + .delivery-slip-title-heading { margin-top: 2px !important; } .delivery-slip-doc .delivery-slip-info-row { display: flex !important; align-items: flex-end !important; width: 100% !important; box-sizing: border-box !important; } .delivery-slip-doc .delivery-slip-type-check-wrap { margin-top: -8px !important; margin-bottom: 20px !important; } .delivery-slip-doc .delivery-slip-type-check-spacer { visibility: hidden !important; } .delivery-slip-doc .delivery-slip-type-check-row { flex: 1 !important; min-width: 0 !important; display: flex !important; flex-wrap: nowrap !important; justify-content: flex-start !important; align-items: center !important; gap: 10px 12px !important; box-sizing: border-box !important; font-size: 20px !important; line-height: 1.35 !important; white-space: nowrap !important; } .delivery-slip-doc .delivery-slip-type-check-item { display: inline-flex !important; align-items: center !important; gap: 4px !important; white-space: nowrap !important; cursor: pointer !important; user-select: none !important; -webkit-user-modify: read-only !important; } .delivery-slip-doc .delivery-slip-type-check-input { width: 16px !important; height: 16px !important; margin: 0 !important; flex-shrink: 0 !important; vertical-align: middle !important; cursor: pointer !important; pointer-events: auto !important; appearance: none !important; -webkit-appearance: none !important; background: #fff !important; border: 1px solid #000 !important; border-radius: 0 !important; box-sizing: border-box !important; print-color-adjust: exact !important; -webkit-print-color-adjust: exact !important; position: relative !important; } .delivery-slip-doc .delivery-slip-type-check-input:checked::after { content: '' !important; position: absolute !important; left: 4px !important; top: 1px !important; width: 5px !important; height: 10px !important; border: solid #000 !important; border-width: 0 2.5px 2.5px 0 !important; transform: rotate(45deg) !important; box-sizing: border-box !important; } .delivery-slip-doc .delivery-slip-type-check-item-pdf { display: inline-block !important; white-space: nowrap !important; font-family: SimSun, 'Songti SC', serif !important; font-size: 20px !important; line-height: 20px !important; height: 20px !important; vertical-align: middle !important; margin-right: 18px !important; } .delivery-slip-doc .delivery-slip-type-check-item-pdf:last-child { margin-right: 0 !important; } .delivery-slip-doc .delivery-slip-type-check-img-pdf { display: inline-block !important; width: 16px !important; height: 16px !important; margin: 0 4px 0 0 !important; padding: 0 !important; border: none !important; vertical-align: middle !important; } .delivery-slip-doc .delivery-slip-type-check-text-pdf { display: inline-block !important; font-size: 20px !important; line-height: 20px !important; vertical-align: middle !important; position: relative !important; top: -12px !important; } .delivery-slip-doc .delivery-slip-type-check-wrap-pdf { display: table !important; width: 100% !important; table-layout: auto !important; border-collapse: collapse !important; } .delivery-slip-doc .delivery-slip-type-check-spacer-pdf { display: table-cell !important; visibility: hidden !important; white-space: nowrap !important; vertical-align: middle !important; padding: 0 !important; border: none !important; } .delivery-slip-doc .delivery-slip-type-check-row-pdf { display: table-cell !important; vertical-align: middle !important; line-height: 20px !important; font-size: 20px !important; } .delivery-slip-doc .delivery-slip-info-label { white-space: nowrap !important; flex-shrink: 0 !important; } .delivery-slip-doc table.delivery-slip-layout-table tr.delivery-slip-gap-before-product td.delivery-slip-gap-before-product-cell, .delivery-slip-doc .delivery-slip-section-gap.delivery-slip-gap-before-product { border: none !important; background: transparent !important; padding: 0 !important; line-height: 0 !important; font-size: 0 !important; height: ${INFO_GAP_BEFORE_PRODUCT}px !important; min-height: ${INFO_GAP_BEFORE_PRODUCT}px !important; } .delivery-slip-doc table.delivery-slip-layout-table tr.delivery-slip-gap-before-sign td.delivery-slip-gap-before-sign-cell, .delivery-slip-doc .delivery-slip-section-gap.delivery-slip-gap-before-sign { border: none !important; background: transparent !important; padding: 0 !important; line-height: 0 !important; font-size: 0 !important; height: ${INFO_GAP_BEFORE_SIGN}px !important; min-height: ${INFO_GAP_BEFORE_SIGN}px !important; } .delivery-slip-doc .delivery-slip-info-value-left { flex: 1 !important; min-width: 0 !important; box-sizing: border-box !important; text-align: left !important; border-bottom: ${INFO_UNDERLINE} !important; min-height: 1.3em !important; } .delivery-slip-doc .delivery-slip-info-value-center { flex: 1 !important; min-width: 0 !important; box-sizing: border-box !important; text-align: center !important; border-bottom: ${INFO_UNDERLINE} !important; min-height: 1.3em !important; } .delivery-slip-doc .delivery-slip-sign-value { display: inline-block !important; line-height: 1.35 !important; padding: ${SIGN_VALUE_PADDING} !important; margin: 0 !important; vertical-align: baseline !important; border: none !important; border-bottom: ${SIGN_UNDERLINE} !important; box-sizing: border-box !important; } .delivery-slip-doc .delivery-slip-sign-label { line-height: 1.35 !important; vertical-align: baseline !important; white-space: nowrap !important; } .delivery-slip-doc table.delivery-slip-outer-table { table-layout: fixed !important; width: 100% !important; border-collapse: collapse !important; } .delivery-slip-doc td.delivery-slip-main-col { overflow: hidden !important; box-sizing: border-box !important; vertical-align: top !important; } .delivery-slip-doc td.delivery-slip-side-labels-col { width: ${SIDE_LABELS_COL_WIDTH}px !important; min-width: ${SIDE_LABELS_COL_WIDTH}px !important; max-width: ${SIDE_LABELS_COL_WIDTH}px !important; } .delivery-slip-doc .delivery-slip-sign-row-cell, .delivery-slip-doc .delivery-slip-sign-inner, .delivery-slip-doc .delivery-slip-sign-item { font-size: 14px !important; } .delivery-slip-doc .delivery-slip-sign-row-cell { overflow: hidden !important; max-width: 100% !important; box-sizing: border-box !important; } .delivery-slip-doc .delivery-slip-sign-inner { display: flex !important; flex-wrap: wrap !important; justify-content: center !important; align-items: baseline !important; column-gap: 10px !important; row-gap: 6px !important; max-width: 100% !important; box-sizing: border-box !important; margin: 0 auto !important; white-space: normal !important; line-height: 1.35 !important; } .delivery-slip-doc .delivery-slip-sign-item { display: inline !important; white-space: nowrap !important; line-height: 1.35 !important; vertical-align: baseline !important; } .delivery-slip-doc .delivery-slip-terms-text { line-height: 1.35 !important; margin: 0 !important; padding: 0 !important; } .delivery-slip-doc td.delivery-slip-terms-cell { padding: 10px 6px 14px 6px !important; line-height: 1.35 !important; vertical-align: top !important; } .delivery-slip-doc .delivery-slip-handwrite-blank { display: inline-block !important; min-width: 3.6em !important; text-align: center !important; letter-spacing: 0.08em !important; } .delivery-slip-doc .delivery-slip-seal-block { position: relative !important; line-height: 1.35 !important; min-height: 0 !important; height: auto !important; } .delivery-slip-doc .delivery-slip-seal-block img[alt="印章"] { position: absolute !important; right: 4px !important; top: ${DELIVERY_SLIP_SEAL_TOP}px !important; left: auto !important; transform: none !important; width: 140px !important; height: 140px !important; opacity: 0.85 !important; z-index: 10 !important; } .delivery-slip-doc .delivery-slip-seal-block .delivery-slip-seal-lines { position: relative !important; z-index: 1 !important; line-height: 1.35 !important; margin: 0 !important; padding: 0 !important; } .delivery-slip-doc table.delivery-slip-outer-table > tbody > tr > td, .delivery-slip-doc table.delivery-slip-outer-table > tbody > tr > th, .delivery-slip-doc table.delivery-slip-layout-table td:not(.delivery-slip-product-cell):not(.delivery-slip-gap-before-product-cell):not(.delivery-slip-gap-before-sign-cell), .delivery-slip-doc table.delivery-slip-layout-table th:not(.delivery-slip-product-cell), .delivery-slip-doc table.delivery-slip-info-table td, .delivery-slip-doc table.delivery-slip-info-table th, .delivery-slip-doc table.delivery-slip-sign-table td, .delivery-slip-doc table.delivery-slip-sign-table th { border: none !important; background: transparent !important; padding: ${TABLE_ROW_PADDING} !important; } .delivery-slip-doc table.delivery-slip-layout-table, .delivery-slip-doc table.delivery-slip-info-table, .delivery-slip-doc table.delivery-slip-product-table, .delivery-slip-doc table.delivery-slip-footer-table, .delivery-slip-doc table.delivery-slip-sign-table { border-collapse: collapse !important; border-spacing: 0 !important; table-layout: fixed !important; width: 100% !important; } .delivery-slip-doc .delivery-slip-product-grid-cell { border: ${PRODUCT_GRID_BORDER} !important; text-align: center !important; vertical-align: middle !important; /* padding/line-height 勿 !important:与合同一致,允许 tr 行高拖拽生效 */ padding: ${PRODUCT_GRID_CELL_PADDING}; box-sizing: border-box !important; line-height: normal; overflow: visible !important; white-space: normal !important; word-wrap: break-word !important; overflow-wrap: break-word !important; word-break: break-word !important; background-color: #fff !important; } .delivery-slip-doc th.delivery-slip-product-grid-cell { padding: ${PRODUCT_GRID_TH_PADDING}; font-weight: bold !important; } .delivery-slip-doc .delivery-slip-product-grid-cell > p { margin: 0 !important; padding: 0 !important; text-align: center !important; line-height: 1.35; } .delivery-slip-doc .sx-excel-row-spacer { display: block !important; float: left !important; width: 0 !important; max-width: 0 !important; margin: 0 !important; padding: 0 !important; border: 0 !important; overflow: hidden !important; visibility: hidden !important; pointer-events: none !important; line-height: 0 !important; font-size: 0 !important; } .delivery-slip-doc .delivery-slip-product-cell:not(.delivery-slip-product-grid-cell) { border: ${PRODUCT_GRID_BORDER} !important; background-color: #fff !important; padding: 10px 6px 14px 6px !important; line-height: 1.35 !important; vertical-align: top !important; white-space: normal !important; word-wrap: break-word !important; overflow-wrap: break-word !important; word-break: break-word !important; } .delivery-slip-doc .delivery-slip-seal-block, .delivery-slip-doc .delivery-slip-seal-lines, .delivery-slip-doc .delivery-slip-terms-text { margin: 0 !important; padding: 0 !important; line-height: 1.35 !important; } .delivery-slip-doc table.delivery-slip-side-labels { border: none !important; border-collapse: collapse !important; width: auto !important; margin: 0 auto !important; } .delivery-slip-doc td.delivery-slip-side-labels-col, .delivery-slip-doc table.delivery-slip-side-labels td, .delivery-slip-doc table.delivery-slip-side-labels th { border: none !important; background: transparent !important; padding: 0 !important; vertical-align: top !important; text-align: center !important; font-size: 18px !important; line-height: 1.35 !important; letter-spacing: 0 !important; white-space: normal !important; } ` export const DELIVERY_SLIP_STYLE_BLOCK = `` /** 生成公司印章 HTML(地址/电话单元格右上角) */ export function buildDeliverySlipCompanySealHtml(): string { return `印章` } const SEAL_BLOCK = `
{{companySeal}}
地址:{{companyAddress}}
送货单位固话:{{companyPhone}}
联系人电话:{{contactPhone}}
` const SIDE_LABEL_CELL = `border:none !important;padding:0 !important;vertical-align:top;text-align:center;${FONT_SIDE}line-height:1.35;background:transparent !important;` const DELIVERY_SLIP_TYPE_CHECK_WRAP_STYLE = `${INFO_ROW}margin-top:-8px;margin-bottom:20px;` const DELIVERY_SLIP_TYPE_CHECK_INNER_STYLE = 'flex:1;min-width:0;display:flex;flex-wrap:nowrap;justify-content:flex-start;align-items:center;gap:10px 12px;box-sizing:border-box;font-size:20px;line-height:1.35;white-space:nowrap;' function buildDeliverySlipTypeCheckboxesHtml(): string { const item = (label: string) => `` return `
日 期:
${item('自提')}${item('出库')}${item('直销')}
` } function buildDeliverySlipInfoTableHtml(): string { return ` ${INFO_COLGROUP}
收货单位: {{receivingUnit}} 
收货联系方式: {{receiveContact}} 
${buildDeliverySlipTypeCheckboxesHtml()}
日 期: {{planDateChinese}} 
` } function buildDeliverySlipTermsHtml(): string { return `
1.本送货单如购销双方未签订合同,视为有效;
2.本次送货含桶${HANDWRITE_BLANK}个,回收${HANDWRITE_BLANK}个,结存${HANDWRITE_BLANK}个;
` } /** 右侧联次:单列纵向,带圈序号 + 每字一行依次向下排列 */ export function buildDeliverySlipSideLabelsHtml(): string { return `
①
白
留
存
②
绿
仓
库
③
红
客
户
④
黄
财
务
/
销
售
` } /** 生成带占位符的送(提)货单 HTML 模板(方案A:信息 / 物料 / 页脚+签字 分表) */ export function generateDeliverySlipTemplate(_plan?: Plan): string { return `${DELIVERY_SLIP_STYLE_BLOCK}
logo
{{companyName}}
送 (提) 货 单
${buildDeliverySlipInfoTableHtml()}
 
${PRODUCT_COLGROUP} {{productTable}}
单据编号 物料编号 物料名称 规格 单位 数量 产品规格 实收数量
备注:{{remark}}
${buildDeliverySlipTermsHtml()} ${SEAL_BLOCK}
 
${buildDeliverySlipSignRowHtml()}
{{sideLabels}}
` } /** 印章占位符与样式(兼容已保存模板,打开时还原为右上角定位) */ function normalizeDeliverySlipSealPlaceholder(html: string): string { if (!html.includes('delivery-slip-doc')) return html let result = html.replace(/]*alt="印章"[^>]*>/gi, '{{companySeal}}') if (result.includes('delivery-slip-seal-block') && !result.includes('{{companySeal}}')) { result = result.replace( /(
]*>)/, '$1\n \n {{companySeal}}\n ' ) } return result.replace(/min-height:76px;/g, 'min-height:0;') } function applyProductGridBorderToStyle(style: string): string { // 保留拖拽产生的 height / min-height / line-height,避免打开模板时被抹掉 const keptHeight = style.match(/height:\s*[^;]+;?/gi)?.join('') || '' const keptMinHeight = style.match(/min-height:\s*[^;]+;?/gi)?.join('') || '' const keptLineHeight = style.match(/line-height:\s*[^;]+;?/gi)?.join('') || '' let next = style .replace(/border:[^;]*;?/gi, '') .replace(/border-right:[^;]*;?/gi, '') .replace(/border-bottom:[^;]*;?/gi, '') .replace(/border-left:[^;]*;?/gi, '') .replace(/border-top:[^;]*;?/gi, '') .replace(/height:\s*[^;]+;?/gi, '') .replace(/min-height:\s*[^;]+;?/gi, '') .replace(/line-height:\s*[^;]+;?/gi, '') .replace(/text-align:left;/g, 'text-align:center;') .replace(/vertical-align:top;/g, '') .trim() const lineHeight = keptLineHeight || 'line-height:normal;' return `border:${PRODUCT_GRID_BORDER} !important;text-align:center;padding:${PRODUCT_GRID_CELL_PADDING};box-sizing:border-box;${lineHeight}${keptHeight}${keptMinHeight}${next}` } function patchProductFooterCellAttrs(attrs: string, classes: string, style: string): string { let next = attrs if (next.includes('class="')) { next = next.replace(/class="([^"]*)"/, (_, cls: string) => { let updated = cls.trim() for (const item of classes.split(/\s+/)) { if (item && !updated.includes(item)) updated += ` ${item}` } return `class="${updated.trim()}"` }) } else { next = `${next} class="${classes.trim()}"` } if (next.includes('style="')) { next = next.replace(/style="[^"]*"/, `style="${style}"`) } else { next = `${next} style="${style}"` } return next } /** 物料明细网格:全边框 inline + 原生居中(兼容已保存模板,对齐合同 PDF) */ function patchDeliverySlipProductGridCellAttrs(attrs: string): string { let next = attrs.includes('class="') ? attrs.replace(/class="([^"]*)"/, (_, cls: string) => { let updated = cls if (!updated.includes('delivery-slip-product-cell')) updated += ' delivery-slip-product-cell' if (!updated.includes('delivery-slip-product-grid-cell')) updated += ' delivery-slip-product-grid-cell' return `class="${updated.trim()}"` }) : `${attrs} class="delivery-slip-product-cell delivery-slip-product-grid-cell"` if (next.includes('style="')) { next = next.replace(/style="([^"]*)"/, (_, style: string) => `style="${applyProductGridBorderToStyle(style)}"`) } else { next = `${next} style="${PRODUCT_GRID_CELL}"` } return next } function normalizeDeliverySlipProductGridCell(html: string): string { if (!html.includes('delivery-slip-doc') || !html.includes('delivery-slip-product-cell')) return html let result = html.replace(/]*)>\s*(\s*){8}\s*<\/tr>/g, (rowMatch) => { if (!rowMatch.includes('物料编号') || rowMatch.includes('colspan')) return rowMatch let row = rowMatch.includes('delivery-slip-product-header') ? rowMatch : rowMatch.replace(']*)>([\s\S]*?)<\/th>/g, (_m, tag, attrs, content) => { return `<${tag}${patchDeliverySlipProductGridCellAttrs(attrs)}>${formatDeliverySlipProductGridCellContent(content)}` }) }) result = result.replace(/]*)>\s*(]*colspan)[^>]*>[\s\S]*?<\/td>\s*){8}\s*<\/tr>/g, (rowMatch) => { if ( rowMatch.includes('备注') || rowMatch.includes('1.本送货单') || rowMatch.includes('仓库:') || rowMatch.includes('暂无产品信息') ) { return rowMatch } return rowMatch.replace(/<(td)([^>]*)>([\s\S]*?)<\/td>/g, (_m, tag, attrs, content) => { if (attrs.includes('colspan')) return `<${tag}${attrs}>${content}` return `<${tag}${patchDeliverySlipProductGridCellAttrs(attrs)}>${formatDeliverySlipProductGridCellContent(content)}` }) }) return normalizeDeliverySlipGridCellWrappers(result) } /** 去掉 TinyMCE / 历史包裹层,单元格内保留纯文本(对齐合同 PDF) */ function normalizeDeliverySlipGridCellWrappers(html: string): string { if (!html.includes('delivery-slip-product-grid-cell')) return html const unwrap = (tag: string, attrs: string, content: string) => `<${tag}${attrs}>${formatDeliverySlipProductGridCellContent(content)}` let result = html.replace( /<(td|th)([^>]*delivery-slip-product-grid-cell[^>]*)>\s*]*>([\s\S]*?)<\/span>\s*<\/\1>/gi, (_m, tag, attrs, content) => unwrap(tag, attrs, content) ) result = result.replace( /<(td|th)([^>]*delivery-slip-product-grid-cell[^>]*)>\s*]*>([\s\S]*?)<\/span>\s*<\/\1>/gi, (_m, tag, attrs, content) => unwrap(tag, attrs, content) ) result = result.replace( /<(td|th)([^>]*delivery-slip-product-grid-cell[^>]*)>\s*
]*>([\s\S]*?)<\/div>\s*<\/\1>/gi, (_m, tag, attrs, content) => unwrap(tag, attrs, content) ) result = result.replace( /<(td|th)([^>]*delivery-slip-product-grid-cell[^>]*)>\s*]*>([\s\S]*?)<\/p>\s*<\/\1>/gi, (_m, tag, attrs, content) => unwrap(tag, attrs, content) ) result = result.replace( /<(td|th)([^>]*delivery-slip-product-grid-cell[^>]*)>\s*]*class="delivery-slip)[^>]*>([\s\S]*?)<\/div>\s*<\/\1>/gi, (_m, tag, attrs, content) => unwrap(tag, attrs, content) ) result = result.replace(/<(td|th)([^>]*delivery-slip-product-grid-cell[^>]*)\s+height="[^"]*"/gi, '<$1$2') result = result.replace(/<(tr)([^>]*class="[^"]*delivery-slip-product[^"]*"[^>]*)\s+height="[^"]*"/gi, '<$1$2') return result } function extractDeliverySlipGridCellContent(cell: HTMLTableCellElement): string { let content = cell.innerHTML.trim() const onlyChild = cell.firstElementChild if (onlyChild && cell.childElementCount === 1) { const tag = onlyChild.tagName if ( tag === 'P' || tag === 'DIV' || tag === 'SPAN' || onlyChild.classList.contains('delivery-slip-pdf-grid-inner') || onlyChild.classList.contains('delivery-slip-pdf-grid-text') ) { content = onlyChild.innerHTML.trim() } } return formatDeliverySlipProductGridCellContent(content) } function applyDeliverySlipPdfGridCellStyles(cell: HTMLTableCellElement, isHeader: boolean): void { const content = extractDeliverySlipGridCellContent(cell) cell.innerHTML = content || ' ' cell.removeAttribute('height') cell.style.removeProperty('height') cell.style.removeProperty('min-height') cell.style.setProperty('padding', isHeader ? PRODUCT_GRID_PDF_TH_PADDING : PRODUCT_GRID_PDF_TD_PADDING, 'important') cell.style.setProperty('text-align', 'center', 'important') cell.style.setProperty('font-size', '20px', 'important') cell.style.setProperty('line-height', PRODUCT_GRID_PDF_LINE_HEIGHT, 'important') cell.style.setProperty('box-sizing', 'border-box', 'important') cell.style.setProperty('overflow', 'visible', 'important') cell.style.setProperty('vertical-align', 'middle', 'important') if (isHeader) { cell.style.setProperty('font-weight', 'bold', 'important') } } function applyDeliverySlipPdfFooterCellStyles(cell: HTMLTableCellElement): void { cell.removeAttribute('height') cell.style.removeProperty('height') cell.style.removeProperty('min-height') cell.style.setProperty('padding', PRODUCT_FOOTER_PDF_PADDING, 'important') cell.style.setProperty('font-size', '20px', 'important') cell.style.setProperty('line-height', PRODUCT_FOOTER_PDF_LINE_HEIGHT, 'important') cell.style.setProperty('box-sizing', 'border-box', 'important') cell.style.setProperty('overflow', 'visible', 'important') cell.style.setProperty('vertical-align', 'top', 'important') cell.style.setProperty('text-align', 'left', 'important') } function unwrapDeliverySlipPdfCellParagraph(cell: HTMLTableCellElement): void { const onlyChild = cell.firstElementChild if (onlyChild?.tagName === 'P' && cell.childElementCount === 1) { cell.innerHTML = onlyChild.innerHTML.trim() || ' ' } } function prepareDeliverySlipPdfRemarkCell(cell: HTMLTableCellElement): void { unwrapDeliverySlipPdfCellParagraph(cell) applyDeliverySlipPdfFooterCellStyles(cell) } function prepareDeliverySlipPdfTermsCell(cell: HTMLTableCellElement): void { applyDeliverySlipPdfFooterCellStyles(cell) const terms = cell.querySelector('.delivery-slip-terms-text') as HTMLElement | null if (terms) { terms.style.setProperty('margin', '0', 'important') terms.style.setProperty('padding', '0', 'important') terms.style.setProperty('line-height', PRODUCT_FOOTER_PDF_LINE_HEIGHT, 'important') } } function prepareDeliverySlipPdfSealCell(cell: HTMLTableCellElement): void { applyDeliverySlipPdfFooterCellStyles(cell) const block = cell.querySelector('.delivery-slip-seal-block') as HTMLElement | null const lines = cell.querySelector('.delivery-slip-seal-lines') as HTMLElement | null if (block) { block.style.setProperty('padding', '0', 'important') block.style.setProperty('margin', '0', 'important') block.style.setProperty('position', 'relative', 'important') } if (lines) { lines.style.setProperty('margin', '0', 'important') lines.style.setProperty('padding', '0', 'important') lines.style.setProperty('line-height', PRODUCT_FOOTER_PDF_LINE_HEIGHT, 'important') } } function prepareDeliverySlipPdfSignValues(root: HTMLElement): void { root.querySelectorAll('.delivery-slip-sign-value').forEach((node) => { const el = node as HTMLElement el.style.setProperty('display', 'inline-block', 'important') el.style.setProperty('padding', SIGN_VALUE_PDF_PADDING, 'important') el.style.setProperty('line-height', '1.35', 'important') el.style.setProperty('vertical-align', 'baseline', 'important') el.style.setProperty('border', 'none', 'important') el.style.setProperty('border-bottom', SIGN_UNDERLINE, 'important') el.style.setProperty('box-sizing', 'border-box', 'important') }) } /** PDF 截图前:将原生 checkbox 替换为 canvas 方框图 + 文字(html2canvas 对 flex/SVG 对齐不稳定) */ const deliverySlipPdfCheckboxImgCache: { empty?: string; checked?: string } = {} function buildDeliverySlipPdfCheckboxImgDataUrl(checked: boolean): string { const key = checked ? 'checked' : 'empty' if (deliverySlipPdfCheckboxImgCache[key]) return deliverySlipPdfCheckboxImgCache[key]! const size = 32 const canvas = document.createElement('canvas') canvas.width = size canvas.height = size const ctx = canvas.getContext('2d') if (!ctx) return '' ctx.fillStyle = '#ffffff' ctx.fillRect(0, 0, size, size) ctx.strokeStyle = '#000000' ctx.lineWidth = 2 ctx.strokeRect(1, 1, size - 2, size - 2) if (checked) { ctx.beginPath() ctx.moveTo(8, 17) ctx.lineTo(14, 23) ctx.lineTo(24, 8) ctx.strokeStyle = '#000000' ctx.lineWidth = 3.5 ctx.lineCap = 'round' ctx.lineJoin = 'round' ctx.stroke() } const url = canvas.toDataURL('image/png') deliverySlipPdfCheckboxImgCache[key] = url return url } function buildDeliverySlipPdfTypeCheckItemHtml(checked: boolean, labelText: string): string { const imgSrc = buildDeliverySlipPdfCheckboxImgDataUrl(checked) if (!imgSrc) return `${labelText}` return `${labelText}` } function applyDeliverySlipPdfTypeCheckboxLayout(root: HTMLElement): void { root.querySelectorAll('.delivery-slip-type-check-wrap').forEach((node) => { const wrap = node as HTMLElement wrap.classList.add('delivery-slip-type-check-wrap-pdf') wrap.style.setProperty('display', 'table', 'important') wrap.style.setProperty('width', '100%', 'important') wrap.style.setProperty('table-layout', 'auto', 'important') wrap.style.setProperty('border-collapse', 'collapse', 'important') wrap.style.setProperty('align-items', 'unset', 'important') }) root.querySelectorAll('.delivery-slip-type-check-spacer').forEach((node) => { const spacer = node as HTMLElement spacer.classList.add('delivery-slip-type-check-spacer-pdf') spacer.style.setProperty('display', 'table-cell', 'important') spacer.style.setProperty('visibility', 'hidden', 'important') spacer.style.setProperty('white-space', 'nowrap', 'important') spacer.style.setProperty('vertical-align', 'middle', 'important') spacer.style.setProperty('padding', '0', 'important') spacer.style.setProperty('border', 'none', 'important') }) root.querySelectorAll('.delivery-slip-type-check-row').forEach((node) => { const row = node as HTMLElement row.classList.add('delivery-slip-type-check-row-pdf') row.style.setProperty('display', 'table-cell', 'important') row.style.setProperty('vertical-align', 'middle', 'important') row.style.setProperty('line-height', '20px', 'important') row.style.setProperty('font-size', '20px', 'important') row.style.setProperty('flex', 'unset', 'important') row.style.removeProperty('gap') row.style.removeProperty('align-items') row.style.removeProperty('justify-content') }) root.querySelectorAll('.delivery-slip-type-check-item-pdf').forEach((node) => { const item = node as HTMLElement item.style.setProperty('display', 'inline-block', 'important') item.style.setProperty('white-space', 'nowrap', 'important') item.style.setProperty('font-family', "SimSun, 'Songti SC', serif", 'important') item.style.setProperty('font-size', '20px', 'important') item.style.setProperty('line-height', '20px', 'important') item.style.setProperty('height', '20px', 'important') item.style.setProperty('margin-right', '18px', 'important') item.style.setProperty('vertical-align', 'middle', 'important') const img = item.querySelector('img.delivery-slip-type-check-img-pdf') as HTMLImageElement | null if (img) { img.style.setProperty('display', 'inline-block', 'important') img.style.setProperty('width', '16px', 'important') img.style.setProperty('height', '16px', 'important') img.style.setProperty('margin', '0 4px 0 0', 'important') img.style.setProperty('padding', '0', 'important') img.style.setProperty('border', 'none', 'important') img.style.setProperty('vertical-align', 'middle', 'important') } const text = item.querySelector('.delivery-slip-type-check-text-pdf') as HTMLElement | null if (text) { text.style.setProperty('display', 'inline-block', 'important') text.style.setProperty('font-size', '20px', 'important') text.style.setProperty('line-height', '20px', 'important') text.style.setProperty('vertical-align', 'middle', 'important') text.style.setProperty('position', 'relative', 'important') text.style.setProperty('top', '-12px', 'important') } }) } function prepareDeliverySlipPdfTypeCheckboxes(root: HTMLElement): void { root.querySelectorAll('label.delivery-slip-type-check-item').forEach((node) => { const label = node as HTMLElement const input = label.querySelector('input.delivery-slip-type-check-input') as HTMLInputElement | null const checked = input?.checked ?? label.querySelector('.delivery-slip-type-check-box')?.textContent?.includes('✓') ?? false const labelText = (label.textContent || '').replace(/[✓☑□☐]/g, '').trim() const html = buildDeliverySlipPdfTypeCheckItemHtml(checked, labelText) const wrapper = document.createElement('div') wrapper.innerHTML = html.trim() const replacement = wrapper.firstElementChild if (replacement) { label.replaceWith(replacement) } }) applyDeliverySlipPdfTypeCheckboxLayout(root) } /** PDF 截图前:物料网格 + 备注/条款/地址行 + 签字区统一样式 */ export function prepareDeliverySlipPdfGridCells(root: HTMLElement): void { root.querySelectorAll('td.delivery-slip-product-grid-cell, th.delivery-slip-product-grid-cell').forEach((node) => { applyDeliverySlipPdfGridCellStyles(node as HTMLTableCellElement, node.tagName === 'TH') }) root.querySelectorAll('td.delivery-slip-terms-cell').forEach((node) => { prepareDeliverySlipPdfTermsCell(node as HTMLTableCellElement) }) root.querySelectorAll('td.delivery-slip-product-cell:not(.delivery-slip-product-grid-cell)').forEach((node) => { const cell = node as HTMLTableCellElement if (cell.classList.contains('delivery-slip-terms-cell')) return if (cell.querySelector('.delivery-slip-seal-block')) { prepareDeliverySlipPdfSealCell(cell) return } if (/备注\s*[::]/.test(cell.textContent || '')) { prepareDeliverySlipPdfRemarkCell(cell) } }) root.querySelectorAll('tr').forEach((row) => { if (!row.querySelector('.delivery-slip-product-cell')) return row.removeAttribute('height') row.style.removeProperty('height') row.style.removeProperty('min-height') }) prepareDeliverySlipPdfSignValues(root) prepareDeliverySlipPdfTypeCheckboxes(root) // 亚像素 border 在 Windows DPI 下易变粗;截图前改为 1px 背景单线 applyPdfStableTableCellBorders(root.querySelectorAll('.delivery-slip-product-cell')) } /** 备注、条款、印章行:统一 0.3px 全边框(兼容已保存模板,与物料网格衔接) */ function normalizeDeliverySlipProductTableFooter(html: string): string { if (!html.includes('delivery-slip-doc')) return html let result = html result = result.replace(/]*colspan="[78]"[^>]*)>(\s*)备注:/g, (_match, attrs, space) => { const withSpan = String(attrs).replace(/colspan="[78]"/, 'colspan="8"') const patched = patchProductFooterCellAttrs( withSpan, 'delivery-slip-product-cell', PRODUCT_TABLE_CELL_LEFT ) return `${space}备注:` }) result = result.replace( /]*colspan="[456]"[^>]*)>\s*(
{ const withSpan = String(attrs).replace(/colspan="[456]"/, 'colspan="5"') const patched = patchProductFooterCellAttrs( withSpan, 'delivery-slip-product-cell delivery-slip-terms-cell', PRODUCT_TERMS_CELL_LEFT ) return `\n ${contentStart}` } ) result = result.replace( /]*colspan="[23]"[^>]*)>\s*
{ const withSpan = String(attrs).replace(/colspan="[23]"/, 'colspan="3"') const patched = patchProductFooterCellAttrs(withSpan, 'delivery-slip-product-cell', PRODUCT_TABLE_CELL_LEFT) return `\n
{ let nextAttrs = attrs if (nextAttrs.includes('class="')) { nextAttrs = nextAttrs.replace(/class="([^"]*)"/, (_, cls: string) => cls.includes('delivery-slip-terms-cell') ? `class="${cls}"` : `class="${cls} delivery-slip-terms-cell"` ) } else { nextAttrs = `${nextAttrs} class="delivery-slip-product-cell delivery-slip-terms-cell"` } if (nextAttrs.includes('style="')) { nextAttrs = nextAttrs.replace(/style="[^"]*"/, `style="${PRODUCT_TERMS_CELL_LEFT}"`) } else { nextAttrs = `${nextAttrs} style="${PRODUCT_TERMS_CELL_LEFT}"` } return nextAttrs } let result = html.replace( /]*)>\s*
{ if (attrs.includes('delivery-slip-terms-cell') && attrs.includes('border:0.3px solid #000') && /colspan="5"/.test(match)) { return match } return `\n
{ if (style.includes('padding:0;') && !style.includes('min-height')) return match let nextStyle = style.replace(/min-height:\d+px;/g, '').replace(/padding:[^;]*;/g, '') nextStyle = `padding:0;${nextStyle}` return `
]*>\s*)[\s\S]*?<\/colgroup>/i, `$1${PRODUCT_COLGROUP}` ) } /** 条款区两行间距与括号内手写空白(兼容已保存模板) */ function normalizeDeliverySlipTermsText(html: string): string { if (!html.includes('delivery-slip-doc') || !html.includes('1.本送货单')) { return html } const termsHtml = buildDeliverySlipTermsHtml() if (html.includes('delivery-slip-terms-text')) { return html.replace(/
/, termsHtml.trim()) } return html.replace( /1\.本送货单如购销双方未签订合同,视为有效;[\s\S]*?结存\([^)]*\)个;/, termsHtml.trim() ) } /** 刷新已有间距行的 inline 样式(TinyMCE 保存后可能丢失) */ function refreshDeliverySlipGapBeforeProduct(html: string): string { return html .replace( /[\s\S]*?<\/tr>/, `\n  \n ` ) .replace( /
]*>[\s\S]*?<\/div>/, `
 
` ) } /** 在收货联系方式行与物料表之间插入间距行(兼容已保存模板) */ function normalizeDeliverySlipGapBeforeProduct(html: string): string { if (!html.includes('delivery-slip-doc')) { return html } if (html.includes('delivery-slip-gap-before-product')) { return refreshDeliverySlipGapBeforeProduct(html) } const gapRow = ` \n ` const productHeader = `(?:单据编号|物料编号)` const withComment = html.replace( new RegExp( `(收货联系方式:[\\s\\S]*?<\\/tr>\\s*)(\s*]*>)[\s\S]*?(<\/td>)/, '$1{{sideLabels}}$2' ) return result } /** 将占位符替换为计划最新数据 */ export function replaceDeliverySlipPlaceholders( htmlContent: string, plan: Plan, ctx?: DeliverySlipRenderContext ): string { if (!htmlContent || !plan) return htmlContent const replacements: Record = { '{{companyName}}': DELIVERY_SLIP_DEFAULTS.companyName, '{{receivingUnit}}': buildReceivingUnit(plan), '{{planNo}}': plan.planNo || '', '{{receiveContact}}': '', '{{driverInfo}}': '', '{{planDateChinese}}': formatPlanDateChinese(plan.planDate), '{{productTable}}': buildProductRows(plan.products, plan.planNo || '', ctx), '{{remark}}': formatRemarkForSlipHtml(plan.remark), '{{companyAddress}}': DELIVERY_SLIP_DEFAULTS.companyAddress, '{{companyPhone}}': DELIVERY_SLIP_DEFAULTS.companyPhone, '{{contactPhone}}': DELIVERY_SLIP_DEFAULTS.contactPhone, '{{companySeal}}': buildDeliverySlipCompanySealHtml(), '{{sideLabels}}': buildDeliverySlipSideLabelsHtml() } let result = normalizeDeliverySlipDocLayout( normalizeDeliverySlipSealPlaceholder( normalizeDeliverySlipSignRow( normalizeDeliverySlipTermsCell( normalizeDeliverySlipTermsText( normalizeDeliverySlipGapBeforeSign( normalizeDeliverySlipProductTableFooter( normalizeDeliverySlipProductColWidths( normalizeDeliverySlipProductGridCell( normalizeDeliverySlipProductPlanNoColumn( normalizeDeliverySlipInfoLayout( normalizeDeliverySlipGapBeforeProduct( normalizeDeliverySlipTypeCheckRow(normalizeDeliverySlipSideLabelsPlaceholder(htmlContent)) ) ) ) ) ) ) ) ) ) ) ) ) for (const [placeholder, value] of Object.entries(replacements)) { result = result.split(placeholder).join(value) } return result } /** 保存模板前:将动态字段还原为占位符 */ export function restoreDeliverySlipPlaceholders(template: string, plan: Plan): string { let result = template const dynamicReplacements: Record = { '{{companyName}}': DELIVERY_SLIP_DEFAULTS.companyName, '{{receivingUnit}}': buildReceivingUnit(plan), '{{planNo}}': plan.planNo || '', '{{planDateChinese}}': formatPlanDateChinese(plan.planDate), '{{remark}}': plan.remark || '', '{{companyAddress}}': DELIVERY_SLIP_DEFAULTS.companyAddress, '{{companyPhone}}': DELIVERY_SLIP_DEFAULTS.companyPhone, '{{contactPhone}}': DELIVERY_SLIP_DEFAULTS.contactPhone } for (const [placeholder, currentValue] of Object.entries(dynamicReplacements)) { if (!currentValue) continue const escaped = currentValue.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') result = result.replace(new RegExp(escaped, 'g'), placeholder) } // 产品明细行还原为占位符(表头「实收数量」之后、页脚「备注」之前;兼容分表与旧一体表) result = result.replace( /(]*>实收数量<\/th>\s*<\/tr>\s*)([\s\S]*?)(\s*\s*]*>\s*备注:)/, '$1{{productTable}}$3' ) // 右侧联次还原为占位符 result = result.replace(//, '{{sideLabels}}') result = result.replace(/
/, '{{sideLabels}}') // 印章还原为占位符(对齐合同管理打印弹窗) result = result.replace(/]*alt="印章"[^>]*>/gi, '') if (result.includes('delivery-slip-seal-block') && !result.includes('{{companySeal}}')) { result = result.replace( /(
]*>)/, '$1\n \n {{companySeal}}\n ' ) } result = result.replace(/(\{\{companySeal\}\}\s*){2,}/g, '{{companySeal}}\n') return result }