1457 lines
64 KiB
TypeScript
1457 lines
64 KiB
TypeScript
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 INBOUND_SLIP_DEFAULTS = {
|
||
companyName: '安庆市山峡化工有限公司',
|
||
companyAddress: '安庆市高新区霞虹路5号',
|
||
companyPhone: '0556-5999710',
|
||
contactPhone: '方总13395560066'
|
||
} as const
|
||
|
||
/** 旧模板曾把送货单位写死为本公司名,保存还原时一并识别 */
|
||
const LEGACY_RECEIVING_UNIT = '安庆市山峡化工有限公司'
|
||
|
||
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 INBOUND_SLIP_DOC_MAX_WIDTH = '960px'
|
||
const DOC_WRAP_EXTRA = `max-width:${INBOUND_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 = `<colgroup>
|
||
<col span="7" style="width:14.28%;" />
|
||
</colgroup>`
|
||
/**
|
||
* 物料表列宽:单据编号 + 加宽编号/名称,收窄规格/单位。
|
||
* 单据编号 / 物料编号 / 物料名称 / 规格 / 单位 / 数量 / 产品规格 / 实收数量
|
||
*/
|
||
const PRODUCT_COLGROUP = `<colgroup>
|
||
<col style="width:14%;" />
|
||
<col style="width:14%;" />
|
||
<col style="width:20%;" />
|
||
<col style="width:8%;" />
|
||
<col style="width:6%;" />
|
||
<col style="width:10%;" />
|
||
<col style="width:14%;" />
|
||
<col style="width:14%;" />
|
||
</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;`
|
||
/** 条款单元格样式(兼容已保存模板归一化) */
|
||
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;`
|
||
/** 入库单印章:public/sxshzyz.png(兼容已保存模板的红章开关) */
|
||
const INBOUND_SLIP_SEAL_IMAGE_SRC = '/sxshzyz.png'
|
||
const INBOUND_SLIP_SEAL_TOP = -66
|
||
const INBOUND_SLIP_SEAL_IMAGE_STYLE = `position:absolute;right:4px;top:${INBOUND_SLIP_SEAL_TOP}px;width:140px;height:140px;opacity:0.85;z-index:10;`
|
||
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 buildInboundSlipSignItem(label: string, valueContent: string, minWidth: string): string {
|
||
return `<span class="inbound-slip-sign-item"><span class="inbound-slip-sign-label">${label}</span><span class="inbound-slip-sign-value" style="min-width:${minWidth};">${valueContent}</span></span>`
|
||
}
|
||
|
||
function buildInboundSlipSignRowHtml(): string {
|
||
return `<tr class="inbound-slip-sign-row">
|
||
<td colspan="8" class="inbound-slip-sign-row-cell" style="border:none !important;padding:${TABLE_ROW_PADDING};text-align:center;${FONT_SIGN}background:transparent !important;">
|
||
<div class="inbound-slip-sign-inner">
|
||
${buildInboundSlipSignItem('仓库:', ' ', '80px')}
|
||
${buildInboundSlipSignItem('审核:', ' ', '80px')}
|
||
${buildInboundSlipSignItem('客户签收:', ' ', '80px')}
|
||
${buildInboundSlipSignItem('司机/送货人(附车牌):', ' ', '120px')}
|
||
</div>
|
||
</td>
|
||
</tr>`
|
||
}
|
||
|
||
export interface InboundSlipRenderContext {
|
||
/** CRM 产品 id -> 物料编码 productNo */
|
||
productCodeById?: Record<string, string>
|
||
/** 单位字典 code -> 展示文案 */
|
||
getUnitLabel?: (unitCode: string) => string
|
||
}
|
||
|
||
function resolveSlipProductCode(product: PlanProduct, ctx?: InboundSlipRenderContext): 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?: InboundSlipRenderContext): string {
|
||
const code = String(unitCode || '').trim()
|
||
if (!code) return ''
|
||
return ctx?.getUnitLabel?.(code) || code
|
||
}
|
||
|
||
export function canPrintInboundSlip(plan?: Pick<Plan, 'planType' | 'transportStatus'> | 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 buildDeliveryUnit(plan: Plan): string {
|
||
return plan.customerName?.trim() || plan.destinationName?.trim() || ''
|
||
}
|
||
|
||
function formatInboundSlipProductGridCellContent(content: string): string {
|
||
const plain = content
|
||
.trim()
|
||
.replace(/<div class="inbound-slip-product-cell-inner"[^>]*>([\s\S]*?)<\/div>/gi, '$1')
|
||
.replace(/<div class="inbound-slip-pdf-grid-inner"[^>]*>([\s\S]*?)<\/div>/gi, '$1')
|
||
.replace(/<span class="inbound-slip-pdf-grid-text"[^>]*>([\s\S]*?)<\/span>/gi, '$1')
|
||
.replace(/<span class="inbound-slip-product-grid-align"[^>]*>([\s\S]*?)<\/span>/gi, '$1')
|
||
.replace(/^<p[^>]*>([\s\S]*?)<\/p>$/i, '$1')
|
||
.trim()
|
||
return plain || ' '
|
||
}
|
||
|
||
function buildProductRows(
|
||
products: PlanProduct[] = [],
|
||
planNo = '',
|
||
ctx?: InboundSlipRenderContext
|
||
): string {
|
||
if (!products.length) {
|
||
return `<tr><td class="inbound-slip-product-cell" colspan="8" style="${PRODUCT_CELL}">暂无产品信息</td></tr>`
|
||
}
|
||
|
||
const dataRows = products
|
||
.map((product) => {
|
||
const rowPlanNo = (product as SlipPlanProduct).slipPlanNo || planNo
|
||
const planNoText = formatInboundSlipProductGridCellContent(rowPlanNo)
|
||
const qty = product.inboundQty ?? product.inboundEstimateQty ?? product.quantity ?? ''
|
||
return `<tr>
|
||
<td class="inbound-slip-product-cell inbound-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">${planNoText}</td>
|
||
<td class="inbound-slip-product-cell inbound-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">${formatInboundSlipProductGridCellContent(resolveSlipProductCode(product, ctx))}</td>
|
||
<td class="inbound-slip-product-cell inbound-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">${formatInboundSlipProductGridCellContent(product.productName || '')}</td>
|
||
<td class="inbound-slip-product-cell inbound-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">${formatInboundSlipProductGridCellContent(product.model || '')}</td>
|
||
<td class="inbound-slip-product-cell inbound-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">${formatInboundSlipProductGridCellContent(resolveSlipUnitLabel(product.unit, ctx))}</td>
|
||
<td class="inbound-slip-product-cell inbound-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">${formatInboundSlipProductGridCellContent(formatQty(qty))}</td>
|
||
<td class="inbound-slip-product-cell inbound-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">${formatInboundSlipProductGridCellContent(product.packageSpec || '')}</td>
|
||
<td class="inbound-slip-product-cell inbound-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}"> </td>
|
||
</tr>`
|
||
})
|
||
.join('')
|
||
|
||
return dataRows
|
||
}
|
||
|
||
export const INBOUND_SLIP_PRINT_CSS = `
|
||
.inbound-slip-doc {
|
||
padding: ${DOC_PADDING} !important;
|
||
max-width: ${INBOUND_SLIP_DOC_MAX_WIDTH} !important;
|
||
margin-left: auto !important;
|
||
margin-right: auto !important;
|
||
box-sizing: border-box !important;
|
||
}
|
||
.inbound-slip-doc .inbound-slip-title-heading {
|
||
font-weight: 700 !important;
|
||
line-height: 1.35 !important;
|
||
}
|
||
.inbound-slip-doc .inbound-slip-title-heading + .inbound-slip-title-heading {
|
||
margin-top: 2px !important;
|
||
}
|
||
.inbound-slip-doc .inbound-slip-info-row {
|
||
display: flex !important;
|
||
align-items: flex-end !important;
|
||
width: 100% !important;
|
||
box-sizing: border-box !important;
|
||
}
|
||
.inbound-slip-doc .inbound-slip-info-label {
|
||
white-space: nowrap !important;
|
||
flex-shrink: 0 !important;
|
||
}
|
||
.inbound-slip-doc table.inbound-slip-layout-table tr.inbound-slip-gap-before-product td.inbound-slip-gap-before-product-cell,
|
||
.inbound-slip-doc .inbound-slip-section-gap.inbound-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;
|
||
}
|
||
.inbound-slip-doc table.inbound-slip-layout-table tr.inbound-slip-gap-before-sign td.inbound-slip-gap-before-sign-cell,
|
||
.inbound-slip-doc .inbound-slip-section-gap.inbound-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;
|
||
}
|
||
.inbound-slip-doc .inbound-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;
|
||
}
|
||
.inbound-slip-doc .inbound-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;
|
||
}
|
||
.inbound-slip-doc .inbound-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;
|
||
}
|
||
.inbound-slip-doc .inbound-slip-sign-label {
|
||
line-height: 1.35 !important;
|
||
vertical-align: baseline !important;
|
||
white-space: nowrap !important;
|
||
}
|
||
.inbound-slip-doc table.inbound-slip-outer-table {
|
||
table-layout: fixed !important;
|
||
width: 100% !important;
|
||
border-collapse: collapse !important;
|
||
}
|
||
.inbound-slip-doc td.inbound-slip-main-col {
|
||
overflow: hidden !important;
|
||
box-sizing: border-box !important;
|
||
vertical-align: top !important;
|
||
}
|
||
.inbound-slip-doc td.inbound-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;
|
||
}
|
||
.inbound-slip-doc .inbound-slip-sign-row-cell,
|
||
.inbound-slip-doc .inbound-slip-sign-inner,
|
||
.inbound-slip-doc .inbound-slip-sign-item {
|
||
font-size: 14px !important;
|
||
}
|
||
.inbound-slip-doc .inbound-slip-sign-row-cell {
|
||
overflow: hidden !important;
|
||
max-width: 100% !important;
|
||
box-sizing: border-box !important;
|
||
}
|
||
.inbound-slip-doc .inbound-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;
|
||
}
|
||
.inbound-slip-doc .inbound-slip-sign-item {
|
||
display: inline !important;
|
||
white-space: nowrap !important;
|
||
line-height: 1.35 !important;
|
||
vertical-align: baseline !important;
|
||
}
|
||
.inbound-slip-doc .inbound-slip-terms-text {
|
||
line-height: 1.35 !important;
|
||
margin: 0 !important;
|
||
padding: 0 !important;
|
||
}
|
||
.inbound-slip-doc td.inbound-slip-terms-cell {
|
||
padding: 10px 6px 14px 6px !important;
|
||
line-height: 1.35 !important;
|
||
vertical-align: top !important;
|
||
}
|
||
.inbound-slip-doc .inbound-slip-handwrite-blank {
|
||
display: inline-block !important;
|
||
min-width: 3.6em !important;
|
||
text-align: center !important;
|
||
letter-spacing: 0.08em !important;
|
||
}
|
||
.inbound-slip-doc .inbound-slip-seal-block {
|
||
position: relative !important;
|
||
line-height: 1.35 !important;
|
||
min-height: 0 !important;
|
||
height: auto !important;
|
||
}
|
||
.inbound-slip-doc .inbound-slip-seal-block img[alt="印章"] {
|
||
position: absolute !important;
|
||
right: 4px !important;
|
||
top: ${INBOUND_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;
|
||
}
|
||
.inbound-slip-doc .inbound-slip-seal-block .inbound-slip-seal-lines {
|
||
position: relative !important;
|
||
z-index: 1 !important;
|
||
line-height: 1.35 !important;
|
||
margin: 0 !important;
|
||
padding: 0 !important;
|
||
}
|
||
.inbound-slip-doc table.inbound-slip-outer-table > tbody > tr > td,
|
||
.inbound-slip-doc table.inbound-slip-outer-table > tbody > tr > th,
|
||
.inbound-slip-doc table.inbound-slip-layout-table td:not(.inbound-slip-product-cell):not(.inbound-slip-gap-before-product-cell):not(.inbound-slip-gap-before-sign-cell),
|
||
.inbound-slip-doc table.inbound-slip-layout-table th:not(.inbound-slip-product-cell),
|
||
.inbound-slip-doc table.inbound-slip-info-table td,
|
||
.inbound-slip-doc table.inbound-slip-info-table th,
|
||
.inbound-slip-doc table.inbound-slip-sign-table td,
|
||
.inbound-slip-doc table.inbound-slip-sign-table th {
|
||
border: none !important;
|
||
background: transparent !important;
|
||
padding: ${TABLE_ROW_PADDING} !important;
|
||
}
|
||
.inbound-slip-doc table.inbound-slip-layout-table,
|
||
.inbound-slip-doc table.inbound-slip-info-table,
|
||
.inbound-slip-doc table.inbound-slip-product-table,
|
||
.inbound-slip-doc table.inbound-slip-footer-table,
|
||
.inbound-slip-doc table.inbound-slip-sign-table {
|
||
border-collapse: collapse !important;
|
||
border-spacing: 0 !important;
|
||
table-layout: fixed !important;
|
||
width: 100% !important;
|
||
}
|
||
.inbound-slip-doc .inbound-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;
|
||
}
|
||
.inbound-slip-doc th.inbound-slip-product-grid-cell {
|
||
padding: ${PRODUCT_GRID_TH_PADDING};
|
||
font-weight: bold !important;
|
||
}
|
||
.inbound-slip-doc .inbound-slip-product-grid-cell > p {
|
||
margin: 0 !important;
|
||
padding: 0 !important;
|
||
text-align: center !important;
|
||
line-height: 1.35;
|
||
}
|
||
.inbound-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;
|
||
}
|
||
.inbound-slip-doc .inbound-slip-product-cell:not(.inbound-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;
|
||
}
|
||
.inbound-slip-doc .inbound-slip-seal-block,
|
||
.inbound-slip-doc .inbound-slip-seal-lines,
|
||
.inbound-slip-doc .inbound-slip-terms-text {
|
||
margin: 0 !important;
|
||
padding: 0 !important;
|
||
line-height: 1.35 !important;
|
||
}
|
||
.inbound-slip-doc table.inbound-slip-side-labels {
|
||
border: none !important;
|
||
border-collapse: collapse !important;
|
||
width: auto !important;
|
||
margin: 0 auto !important;
|
||
}
|
||
.inbound-slip-doc td.inbound-slip-side-labels-col,
|
||
.inbound-slip-doc table.inbound-slip-side-labels td,
|
||
.inbound-slip-doc table.inbound-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 INBOUND_SLIP_STYLE_BLOCK = `<style type="text/css">${INBOUND_SLIP_PRINT_CSS}</style>`
|
||
|
||
/** 生成公司印章 HTML(地址/电话单元格右上角) */
|
||
export function buildInboundSlipCompanySealHtml(): string {
|
||
return `<img src="${INBOUND_SLIP_SEAL_IMAGE_SRC}" alt="印章" style="${INBOUND_SLIP_SEAL_IMAGE_STYLE}" />`
|
||
}
|
||
|
||
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;`
|
||
|
||
/** 右侧联次:单列纵向,带圈序号 + 每字一行依次向下排列 */
|
||
export function buildInboundSlipSideLabelsHtml(): string {
|
||
return `<table class="inbound-slip-side-labels" style="border:none !important;border-collapse:collapse;margin:0 auto;width:auto;">
|
||
<tr>
|
||
<td class="inbound-slip-side-label-item" style="${SIDE_LABEL_CELL}">①<br />白<br />留<br />存<br />②<br />绿<br />仓<br />库<br />③<br />红<br />客<br />户<br />④<br />黄<br />财<br />务<br />/<br />销<br />售</td>
|
||
</tr>
|
||
</table>`
|
||
}
|
||
|
||
/** 生成带占位符的入库单 HTML 模板(方案A:信息 / 物料 / 页脚+签字 分表) */
|
||
export function generateInboundSlipTemplate(_plan?: Plan): string {
|
||
return `${INBOUND_SLIP_STYLE_BLOCK}<div class="inbound-slip-doc" style="font-family:${DOC_FONT};${FONT_BODY}color:#000;line-height:1.5;background:#fff;padding:${DOC_PADDING};${DOC_WRAP_EXTRA}">
|
||
|
||
<!-- 上部分:标题区 -->
|
||
<div class="inbound-slip-title-section" style="position:relative;min-height:56px;margin-bottom:14px;">
|
||
<img src="/logo.gif" alt="logo" style="position:absolute;left:${LOGO_LEFT}px;top:50%;transform:translateY(-50%);height:${LOGO_HEIGHT}px;width:auto;" />
|
||
<div style="text-align:center;padding:4px 60px 0 100px;">
|
||
<div class="inbound-slip-title-heading" style="${TITLE_STYLE}">{{companyName}}</div>
|
||
<div class="inbound-slip-title-heading" style="${TITLE_SUB_STYLE}">入 库 单</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 下部分:内容区(左正文 + 右侧联次) -->
|
||
<table class="inbound-slip-outer-table" style="width:100%;border-collapse:collapse;table-layout:fixed;border:none !important;">
|
||
<tr>
|
||
<td class="inbound-slip-main-col" style="vertical-align:top;padding-right:6px;border:none !important;background:transparent !important;">
|
||
<!-- 1. 基本信息(独立表,不可拖宽高) -->
|
||
<table class="inbound-slip-info-table" style="width:100%;border-collapse:collapse;table-layout:fixed;border:none !important;">
|
||
${INFO_COLGROUP}
|
||
<tr>
|
||
<td colspan="4" style="${INFO_CELL_LEFT}">
|
||
<div class="inbound-slip-info-row" style="${INFO_ROW}">
|
||
<span class="inbound-slip-info-label" style="${INFO_LABEL}">送货单位:</span>
|
||
<span class="inbound-slip-info-value-left" style="${INFO_VALUE_INLINE_LEFT}">{{receivingUnit}} </span>
|
||
</div>
|
||
</td>
|
||
<td colspan="3" style="${INFO_CELL_RIGHT}">
|
||
<div class="inbound-slip-info-row" style="${INFO_ROW}">
|
||
<span class="inbound-slip-info-label" style="${INFO_LABEL}">日 期:</span>
|
||
<span class="inbound-slip-info-value-center" style="${INFO_VALUE_INLINE_CENTER}">{{planDateChinese}} </span>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
</table>
|
||
|
||
<div class="inbound-slip-section-gap inbound-slip-gap-before-product" style="${GAP_BEFORE_PRODUCT_CELL}"> </div>
|
||
|
||
<!-- 2. 物料明细表(含备注;可拖表头/明细列宽行高) -->
|
||
<table class="inbound-slip-product-table" style="width:100%;border-collapse:collapse;table-layout:fixed;border:none !important;">
|
||
${PRODUCT_COLGROUP}
|
||
<tr class="inbound-slip-product-header">
|
||
<th class="inbound-slip-product-cell inbound-slip-product-grid-cell" style="${PRODUCT_TH}">单据编号</th>
|
||
<th class="inbound-slip-product-cell inbound-slip-product-grid-cell" style="${PRODUCT_TH}">物料编号</th>
|
||
<th class="inbound-slip-product-cell inbound-slip-product-grid-cell" style="${PRODUCT_TH}">物料名称</th>
|
||
<th class="inbound-slip-product-cell inbound-slip-product-grid-cell" style="${PRODUCT_TH}">规格</th>
|
||
<th class="inbound-slip-product-cell inbound-slip-product-grid-cell" style="${PRODUCT_TH}">单位</th>
|
||
<th class="inbound-slip-product-cell inbound-slip-product-grid-cell" style="${PRODUCT_TH}">数量</th>
|
||
<th class="inbound-slip-product-cell inbound-slip-product-grid-cell" style="${PRODUCT_TH}">产品规格</th>
|
||
<th class="inbound-slip-product-cell inbound-slip-product-grid-cell" style="${PRODUCT_TH}">实收数量</th>
|
||
</tr>
|
||
{{productTable}}
|
||
<tr>
|
||
<td colspan="8" class="inbound-slip-product-cell" style="${PRODUCT_TABLE_CELL_LEFT}">
|
||
备注:{{remark}}
|
||
</td>
|
||
</tr>
|
||
</table>
|
||
|
||
<div class="inbound-slip-section-gap inbound-slip-gap-before-sign" style="${GAP_BEFORE_SIGN_CELL}"> </div>
|
||
|
||
<!-- 3. 签字区(独立表) -->
|
||
<table class="inbound-slip-sign-table" style="width:100%;border-collapse:collapse;table-layout:fixed;border:none !important;">
|
||
${buildInboundSlipSignRowHtml()}
|
||
</table>
|
||
</td>
|
||
|
||
<!-- 最右侧纵向联次 -->
|
||
<td class="inbound-slip-side-labels-col" style="width:36px;min-width:36px;vertical-align:top;text-align:center;color:#333;padding:0 !important;border:none !important;background:transparent !important;">
|
||
{{sideLabels}}
|
||
</td>
|
||
</tr>
|
||
</table>
|
||
</div>`
|
||
}
|
||
|
||
/** 印章占位符与样式(兼容已保存模板,打开时还原为右上角定位) */
|
||
function normalizeInboundSlipSealPlaceholder(html: string): string {
|
||
if (!html.includes('inbound-slip-doc')) return html
|
||
|
||
let result = html.replace(/<img[^>]*alt="印章"[^>]*>/gi, '{{companySeal}}')
|
||
|
||
if (result.includes('inbound-slip-seal-block') && !result.includes('{{companySeal}}')) {
|
||
result = result.replace(
|
||
/(<div class="inbound-slip-seal-block"[^>]*>)/,
|
||
'$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 patchInboundSlipProductGridCellAttrs(attrs: string): string {
|
||
let next = attrs.includes('class="')
|
||
? attrs.replace(/class="([^"]*)"/, (_, cls: string) => {
|
||
let updated = cls
|
||
if (!updated.includes('inbound-slip-product-cell')) updated += ' inbound-slip-product-cell'
|
||
if (!updated.includes('inbound-slip-product-grid-cell')) updated += ' inbound-slip-product-grid-cell'
|
||
return `class="${updated.trim()}"`
|
||
})
|
||
: `${attrs} class="inbound-slip-product-cell inbound-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 normalizeInboundSlipProductGridCell(html: string): string {
|
||
if (!html.includes('inbound-slip-doc') || !html.includes('inbound-slip-product-cell')) return html
|
||
|
||
let result = html.replace(/<tr([^>]*)>\s*(<th[\s\S]*?<\/th>\s*){8}\s*<\/tr>/g, (rowMatch) => {
|
||
if (!rowMatch.includes('物料编号') || rowMatch.includes('colspan')) return rowMatch
|
||
let row = rowMatch.includes('inbound-slip-product-header')
|
||
? rowMatch
|
||
: rowMatch.replace('<tr', '<tr class="inbound-slip-product-header"')
|
||
return row.replace(/<(th)([^>]*)>([\s\S]*?)<\/th>/g, (_m, tag, attrs, content) => {
|
||
return `<${tag}${patchInboundSlipProductGridCellAttrs(attrs)}>${formatInboundSlipProductGridCellContent(content)}</${tag}>`
|
||
})
|
||
})
|
||
|
||
result = result.replace(/<tr([^>]*)>\s*(<td(?![^>]*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}</td>`
|
||
return `<${tag}${patchInboundSlipProductGridCellAttrs(attrs)}>${formatInboundSlipProductGridCellContent(content)}</${tag}>`
|
||
})
|
||
})
|
||
|
||
return normalizeInboundSlipGridCellWrappers(result)
|
||
}
|
||
|
||
/** 去掉 TinyMCE / 历史包裹层,单元格内保留纯文本(对齐合同 PDF) */
|
||
function normalizeInboundSlipGridCellWrappers(html: string): string {
|
||
if (!html.includes('inbound-slip-product-grid-cell')) return html
|
||
|
||
const unwrap = (tag: string, attrs: string, content: string) =>
|
||
`<${tag}${attrs}>${formatInboundSlipProductGridCellContent(content)}</${tag}>`
|
||
|
||
let result = html.replace(
|
||
/<(td|th)([^>]*inbound-slip-product-grid-cell[^>]*)>\s*<span class="inbound-slip-product-grid-align"[^>]*>([\s\S]*?)<\/span>\s*<\/\1>/gi,
|
||
(_m, tag, attrs, content) => unwrap(tag, attrs, content)
|
||
)
|
||
|
||
result = result.replace(
|
||
/<(td|th)([^>]*inbound-slip-product-grid-cell[^>]*)>\s*<span class="inbound-slip-pdf-grid-text"[^>]*>([\s\S]*?)<\/span>\s*<\/\1>/gi,
|
||
(_m, tag, attrs, content) => unwrap(tag, attrs, content)
|
||
)
|
||
|
||
result = result.replace(
|
||
/<(td|th)([^>]*inbound-slip-product-grid-cell[^>]*)>\s*<div class="inbound-slip-pdf-grid-inner"[^>]*>([\s\S]*?)<\/div>\s*<\/\1>/gi,
|
||
(_m, tag, attrs, content) => unwrap(tag, attrs, content)
|
||
)
|
||
|
||
result = result.replace(
|
||
/<(td|th)([^>]*inbound-slip-product-grid-cell[^>]*)>\s*<p[^>]*>([\s\S]*?)<\/p>\s*<\/\1>/gi,
|
||
(_m, tag, attrs, content) => unwrap(tag, attrs, content)
|
||
)
|
||
|
||
result = result.replace(
|
||
/<(td|th)([^>]*inbound-slip-product-grid-cell[^>]*)>\s*<div(?![^>]*class="inbound-slip)[^>]*>([\s\S]*?)<\/div>\s*<\/\1>/gi,
|
||
(_m, tag, attrs, content) => unwrap(tag, attrs, content)
|
||
)
|
||
|
||
result = result.replace(/<(td|th)([^>]*inbound-slip-product-grid-cell[^>]*)\s+height="[^"]*"/gi, '<$1$2')
|
||
result = result.replace(/<(tr)([^>]*class="[^"]*inbound-slip-product[^"]*"[^>]*)\s+height="[^"]*"/gi, '<$1$2')
|
||
|
||
return result
|
||
}
|
||
|
||
function extractInboundSlipGridCellContent(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('inbound-slip-pdf-grid-inner') ||
|
||
onlyChild.classList.contains('inbound-slip-pdf-grid-text')
|
||
) {
|
||
content = onlyChild.innerHTML.trim()
|
||
}
|
||
}
|
||
return formatInboundSlipProductGridCellContent(content)
|
||
}
|
||
|
||
function applyInboundSlipPdfGridCellStyles(cell: HTMLTableCellElement, isHeader: boolean): void {
|
||
const content = extractInboundSlipGridCellContent(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 applyInboundSlipPdfFooterCellStyles(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 unwrapInboundSlipPdfCellParagraph(cell: HTMLTableCellElement): void {
|
||
const onlyChild = cell.firstElementChild
|
||
if (onlyChild?.tagName === 'P' && cell.childElementCount === 1) {
|
||
cell.innerHTML = onlyChild.innerHTML.trim() || ' '
|
||
}
|
||
}
|
||
|
||
function prepareInboundSlipPdfRemarkCell(cell: HTMLTableCellElement): void {
|
||
unwrapInboundSlipPdfCellParagraph(cell)
|
||
applyInboundSlipPdfFooterCellStyles(cell)
|
||
}
|
||
|
||
function prepareInboundSlipPdfTermsCell(cell: HTMLTableCellElement): void {
|
||
applyInboundSlipPdfFooterCellStyles(cell)
|
||
const terms = cell.querySelector('.inbound-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 prepareInboundSlipPdfSealCell(cell: HTMLTableCellElement): void {
|
||
applyInboundSlipPdfFooterCellStyles(cell)
|
||
const block = cell.querySelector('.inbound-slip-seal-block') as HTMLElement | null
|
||
const lines = cell.querySelector('.inbound-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 prepareInboundSlipPdfSignValues(root: HTMLElement): void {
|
||
root.querySelectorAll('.inbound-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 截图前:物料网格 + 备注/条款/地址行 + 签字区统一样式 */
|
||
export function prepareInboundSlipPdfGridCells(root: HTMLElement): void {
|
||
root.querySelectorAll('td.inbound-slip-product-grid-cell, th.inbound-slip-product-grid-cell').forEach((node) => {
|
||
applyInboundSlipPdfGridCellStyles(node as HTMLTableCellElement, node.tagName === 'TH')
|
||
})
|
||
|
||
root.querySelectorAll('td.inbound-slip-terms-cell').forEach((node) => {
|
||
prepareInboundSlipPdfTermsCell(node as HTMLTableCellElement)
|
||
})
|
||
|
||
root.querySelectorAll('td.inbound-slip-product-cell:not(.inbound-slip-product-grid-cell)').forEach((node) => {
|
||
const cell = node as HTMLTableCellElement
|
||
if (cell.classList.contains('inbound-slip-terms-cell')) return
|
||
if (cell.querySelector('.inbound-slip-seal-block')) {
|
||
prepareInboundSlipPdfSealCell(cell)
|
||
return
|
||
}
|
||
if (/备注\s*[::]/.test(cell.textContent || '')) {
|
||
prepareInboundSlipPdfRemarkCell(cell)
|
||
}
|
||
})
|
||
|
||
root.querySelectorAll('tr').forEach((row) => {
|
||
if (!row.querySelector('.inbound-slip-product-cell')) return
|
||
row.removeAttribute('height')
|
||
row.style.removeProperty('height')
|
||
row.style.removeProperty('min-height')
|
||
})
|
||
|
||
prepareInboundSlipPdfSignValues(root)
|
||
|
||
// 亚像素 border 在 Windows DPI 下易变粗;截图前改为 1px 背景单线
|
||
applyPdfStableTableCellBorders(root.querySelectorAll('.inbound-slip-product-cell'))
|
||
}
|
||
|
||
/** 备注、条款、印章行:统一 0.3px 全边框(兼容已保存模板,与物料网格衔接) */
|
||
function normalizeInboundSlipProductTableFooter(html: string): string {
|
||
if (!html.includes('inbound-slip-doc')) return html
|
||
|
||
let result = html
|
||
|
||
result = result.replace(/<td([^>]*colspan="[78]"[^>]*)>(\s*)备注:/g, (_match, attrs, space) => {
|
||
const withSpan = String(attrs).replace(/colspan="[78]"/, 'colspan="8"')
|
||
const patched = patchProductFooterCellAttrs(
|
||
withSpan,
|
||
'inbound-slip-product-cell',
|
||
PRODUCT_TABLE_CELL_LEFT
|
||
)
|
||
return `<td${patched}>${space}备注:`
|
||
})
|
||
|
||
result = result.replace(
|
||
/<td([^>]*colspan="[45]"[^>]*)>\s*(<div class="inbound-slip-terms-text"|\s*1\.本送货单)/g,
|
||
(_match, attrs, contentStart) => {
|
||
const withSpan = String(attrs).replace(/colspan="[45]"/, 'colspan="5"')
|
||
const patched = patchProductFooterCellAttrs(
|
||
withSpan,
|
||
'inbound-slip-product-cell inbound-slip-terms-cell',
|
||
PRODUCT_TERMS_CELL_LEFT
|
||
)
|
||
return `<td${patched}>\n ${contentStart}`
|
||
}
|
||
)
|
||
|
||
result = result.replace(
|
||
/<td([^>]*colspan="[23]"[^>]*)>\s*<div class="inbound-slip-seal-block"/g,
|
||
(_match, attrs) => {
|
||
const withSpan = String(attrs).replace(/colspan="[23]"/, 'colspan="2"')
|
||
const patched = patchProductFooterCellAttrs(withSpan, 'inbound-slip-product-cell', PRODUCT_TABLE_CELL_LEFT)
|
||
return `<td${patched}>\n <div class="inbound-slip-seal-block"`
|
||
}
|
||
)
|
||
|
||
return result
|
||
}
|
||
|
||
/** 条款单元格行高随内容(兼容已保存模板) */
|
||
function normalizeInboundSlipTermsCell(html: string): string {
|
||
if (!html.includes('inbound-slip-doc')) return html
|
||
|
||
const patchTermsTd = (attrs: string) => {
|
||
let nextAttrs = attrs
|
||
if (nextAttrs.includes('class="')) {
|
||
nextAttrs = nextAttrs.replace(/class="([^"]*)"/, (_, cls: string) =>
|
||
cls.includes('inbound-slip-terms-cell') ? `class="${cls}"` : `class="${cls} inbound-slip-terms-cell"`
|
||
)
|
||
} else {
|
||
nextAttrs = `${nextAttrs} class="inbound-slip-product-cell inbound-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(
|
||
/<td colspan="[45]"([^>]*)>\s*<div class="inbound-slip-terms-text"/g,
|
||
(match, attrs) => {
|
||
if (attrs.includes('inbound-slip-terms-cell') && attrs.includes('border:0.3px solid #000') && /colspan="5"/.test(match)) {
|
||
return match
|
||
}
|
||
return `<td colspan="5"${patchTermsTd(attrs)}>\n <div class="inbound-slip-terms-text"`
|
||
}
|
||
)
|
||
|
||
result = result.replace(
|
||
/<div class="inbound-slip-seal-block" style="([^"]*)"/g,
|
||
(match, style) => {
|
||
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 `<div class="inbound-slip-seal-block" style="${nextStyle}"`
|
||
}
|
||
)
|
||
|
||
return result
|
||
}
|
||
|
||
/** 物料表列宽 + 条款区跨列(兼容已保存模板) */
|
||
function normalizeInboundSlipProductColWidths(html: string): string {
|
||
if (!html.includes('inbound-slip-product-table')) return html
|
||
return html.replace(
|
||
/(<table class="inbound-slip-product-table"[^>]*>\s*)<colgroup>[\s\S]*?<\/colgroup>/i,
|
||
`$1${PRODUCT_COLGROUP}`
|
||
)
|
||
}
|
||
|
||
/** 移除条款+地址印章行(兼容已保存模板) */
|
||
function normalizeInboundSlipTermsText(html: string): string {
|
||
if (!html.includes('inbound-slip-doc')) return html
|
||
if (
|
||
!html.includes('1.本送货单') &&
|
||
!html.includes('inbound-slip-terms-text') &&
|
||
!html.includes('inbound-slip-seal-block')
|
||
) {
|
||
return html
|
||
}
|
||
|
||
// 条款+印章整行删除(同一 tr 内左右两格)
|
||
let result = html.replace(
|
||
/<tr[^>]*>\s*<td[^>]*colspan="[45]"[^>]*>[\s\S]*?(?:inbound-slip-terms-text|1\.本送货单)[\s\S]*?<\/tr>\s*/gi,
|
||
''
|
||
)
|
||
// 兜底:独立印章行
|
||
result = result.replace(
|
||
/<tr[^>]*>\s*<td[^>]*>[\s\S]*?inbound-slip-seal-block[\s\S]*?<\/tr>\s*/gi,
|
||
''
|
||
)
|
||
return result
|
||
}
|
||
|
||
/** 刷新已有间距行的 inline 样式(TinyMCE 保存后可能丢失) */
|
||
function refreshInboundSlipGapBeforeProduct(html: string): string {
|
||
return html
|
||
.replace(
|
||
/<tr class="inbound-slip-gap-before-product">[\s\S]*?<\/tr>/,
|
||
`<tr class="inbound-slip-gap-before-product">\n <td colspan="8" class="inbound-slip-gap-before-product-cell" style="${GAP_BEFORE_PRODUCT_CELL}"> </td>\n </tr>`
|
||
)
|
||
.replace(
|
||
/<div class="inbound-slip-section-gap inbound-slip-gap-before-product"[^>]*>[\s\S]*?<\/div>/,
|
||
`<div class="inbound-slip-section-gap inbound-slip-gap-before-product" style="${GAP_BEFORE_PRODUCT_CELL}"> </div>`
|
||
)
|
||
}
|
||
|
||
/** 在信息区与物料表之间插入间距(兼容已保存模板) */
|
||
function normalizeInboundSlipGapBeforeProduct(html: string): string {
|
||
if (!html.includes('inbound-slip-doc')) {
|
||
return html
|
||
}
|
||
|
||
if (html.includes('inbound-slip-gap-before-product')) {
|
||
return refreshInboundSlipGapBeforeProduct(html)
|
||
}
|
||
|
||
const gapRow = `<tr class="inbound-slip-gap-before-product"><td colspan="8" class="inbound-slip-gap-before-product-cell" style="${GAP_BEFORE_PRODUCT_CELL}"> </td></tr>\n `
|
||
const productHeader = `(?:单据编号|物料编号)`
|
||
|
||
const withComment = html.replace(
|
||
new RegExp(
|
||
`(<span class="inbound-slip-info-label"[^>]*>日(?: )?期:</span>[\\s\\S]*?</tr>\\s*)(<!--\\s*中:产品明细表[\\s\\S]*?<tr>\\s*<th[^>]*>${productHeader})`
|
||
),
|
||
`$1${gapRow}$2`
|
||
)
|
||
if (withComment !== html) return withComment
|
||
|
||
const withHeader = html.replace(
|
||
new RegExp(
|
||
`(<span class="inbound-slip-info-label"[^>]*>日(?: )?期:</span>[\\s\\S]*?</tr>\\s*)(<tr>\\s*<th[^>]*>${productHeader})`
|
||
),
|
||
`$1${gapRow}$2`
|
||
)
|
||
if (withHeader !== html) return withHeader
|
||
|
||
return html.replace(
|
||
/(收货联系方式:[\s\S]*?<\/tr>\s*)(<!--\s*中:产品明细表[\s\S]*?<tr>\s*<th[^>]*>物料编号|<tr>\s*<th[^>]*>物料编号)/,
|
||
`$1${gapRow}$2`
|
||
)
|
||
}
|
||
|
||
/** 刷新签字区上方间距行 inline 样式 */
|
||
function refreshInboundSlipGapBeforeSign(html: string): string {
|
||
return html
|
||
.replace(
|
||
/<tr class="inbound-slip-gap-before-sign">[\s\S]*?<\/tr>/,
|
||
`<tr class="inbound-slip-gap-before-sign">\n <td colspan="8" class="inbound-slip-gap-before-sign-cell" style="${GAP_BEFORE_SIGN_CELL}"> </td>\n </tr>`
|
||
)
|
||
.replace(
|
||
/<div class="inbound-slip-section-gap inbound-slip-gap-before-sign"[^>]*>[\s\S]*?<\/div>/,
|
||
`<div class="inbound-slip-section-gap inbound-slip-gap-before-sign" style="${GAP_BEFORE_SIGN_CELL}"> </div>`
|
||
)
|
||
}
|
||
|
||
/** 在物料表与签字区之间插入间距行(兼容已保存模板) */
|
||
function normalizeInboundSlipGapBeforeSign(html: string): string {
|
||
if (!html.includes('inbound-slip-doc')) return html
|
||
|
||
if (html.includes('inbound-slip-gap-before-sign')) {
|
||
return refreshInboundSlipGapBeforeSign(html)
|
||
}
|
||
|
||
const gapRow = `<tr class="inbound-slip-gap-before-sign"><td colspan="8" class="inbound-slip-gap-before-sign-cell" style="${GAP_BEFORE_SIGN_CELL}"> </td></tr>\n\n `
|
||
|
||
const withComment = html.replace(
|
||
/(<\/tr>\s*)(<!--\s*下:签字区[\s\S]*?<tr>\s*<td colspan="[78]"[^>]*>\s*仓库:)/,
|
||
`$1${gapRow}$2`
|
||
)
|
||
if (withComment !== html) return withComment
|
||
|
||
return html.replace(
|
||
/(inbound-slip-seal-block[\s\S]*?<\/tr>\s*)(<tr>\s*<td colspan="[78]"[^>]*>\s*仓库:)/,
|
||
`$1${gapRow}$2`
|
||
)
|
||
}
|
||
|
||
/** 签字区标签/下划线 baseline 对齐(兼容已保存模板) */
|
||
function normalizeInboundSlipSignLabelMarkup(html: string): string {
|
||
if (!html.includes('inbound-slip-sign-value')) return html
|
||
|
||
let result = html.replace(
|
||
/<span class="inbound-slip-sign-item">(仓库:|审核:|客户签收:|司机\/送货人\(附车牌\):)\s*(<span class="inbound-slip-sign-value")/g,
|
||
'<span class="inbound-slip-sign-item"><span class="inbound-slip-sign-label">$1</span>$2'
|
||
)
|
||
|
||
result = result.replace(/<span class="inbound-slip-sign-value" style="([^"]*)"/g, (match, style) => {
|
||
const nextStyle = style
|
||
.replace(/display:inline-block;?/g, '')
|
||
.replace(/padding:[^;]*;?/g, '')
|
||
.replace(/vertical-align:[^;]*;?/g, '')
|
||
.replace(/border:none !important;?/g, '')
|
||
.replace(/border-bottom:[^;]*;?/g, '')
|
||
.replace(/;\s*;/g, ';')
|
||
.replace(/^;|;$/g, '')
|
||
.trim()
|
||
return nextStyle ? `<span class="inbound-slip-sign-value" style="${nextStyle}"` : `<span class="inbound-slip-sign-value"`
|
||
})
|
||
|
||
return result
|
||
}
|
||
|
||
/** 签字区 flex 布局(兼容已保存模板,避免打印预览溢出到右侧联次列) */
|
||
function normalizeInboundSlipSignRow(html: string): string {
|
||
if (!html.includes('inbound-slip-doc') || !html.includes('仓库:')) return html
|
||
|
||
let result = html.replace(
|
||
/<tr([^>]*)>\s*<td colspan="[78]"([^>]*)>([\s\S]*?仓库:[\s\S]*?司机\/送货人\(附车牌\)[^<]*[\s\S]*?)<\/td>\s*<\/tr>/g,
|
||
(match, trAttrs, tdAttrs, content) => {
|
||
if (!content.includes('inbound-slip-sign-value') || content.includes('inbound-slip-sign-inner')) {
|
||
return match
|
||
}
|
||
|
||
const wrappedContent = content
|
||
.replace(/ /g, '')
|
||
.replace(
|
||
/(仓库:|审核:|客户签收:|司机\/送货人\(附车牌\):)\s*(<span class="inbound-slip-sign-value"[\s\S]*?<\/span>)/g,
|
||
'<span class="inbound-slip-sign-item"><span class="inbound-slip-sign-label">$1</span>$2</span>'
|
||
)
|
||
.replace(
|
||
/<span class="inbound-slip-sign-item">(仓库:|审核:|客户签收:|司机\/送货人\(附车牌\):)\s*(<span class="inbound-slip-sign-value")/g,
|
||
'<span class="inbound-slip-sign-item"><span class="inbound-slip-sign-label">$1</span>$2'
|
||
)
|
||
|
||
const trClassAttr = trAttrs.includes('class="')
|
||
? trAttrs.replace(/class="([^"]*)"/, (_, cls: string) =>
|
||
cls.includes('inbound-slip-sign-row') ? `class="${cls}"` : `class="${cls} inbound-slip-sign-row"`
|
||
)
|
||
: `${trAttrs} class="inbound-slip-sign-row"`
|
||
|
||
let nextTdAttrs = tdAttrs.replace(/white-space:nowrap;?/g, '').replace(/font-size:16px;?/g, 'font-size:14px;')
|
||
if (nextTdAttrs.includes('class="')) {
|
||
nextTdAttrs = nextTdAttrs.replace(/class="([^"]*)"/, (_, cls: string) =>
|
||
cls.includes('inbound-slip-sign-row-cell') ? `class="${cls}"` : `class="${cls} inbound-slip-sign-row-cell"`
|
||
)
|
||
} else {
|
||
nextTdAttrs = `${nextTdAttrs} class="inbound-slip-sign-row-cell"`
|
||
}
|
||
|
||
return `<tr${trClassAttr}>\n <td colspan="8"${nextTdAttrs}>\n <div class="inbound-slip-sign-inner">${wrappedContent.trim()}</div>\n </td>\n </tr>`
|
||
}
|
||
)
|
||
|
||
result = result.replace(
|
||
/<td(?![^>]*class="[^"]*inbound-slip-main-col)([^>]*style="[^"]*vertical-align:top;padding-right:6px)/g,
|
||
'<td class="inbound-slip-main-col"$1'
|
||
)
|
||
|
||
return normalizeInboundSlipSignLabelMarkup(result)
|
||
}
|
||
|
||
/** 信息区:送货单位 + 日期(兼容旧「收货单位/单据编号/收货联系方式」布局) */
|
||
function normalizeInboundSlipInfoLayout(html: string): string {
|
||
if (!html.includes('inbound-slip-info-table')) return html
|
||
|
||
const infoMatch = html.match(/<table class="inbound-slip-info-table"[^>]*>[\s\S]*?<\/table>/i)
|
||
if (!infoMatch) return html
|
||
|
||
const infoHtml = infoMatch[0]
|
||
const needsMigrate =
|
||
infoHtml.includes('收货单位:') ||
|
||
infoHtml.includes('收货联系方式:') ||
|
||
infoHtml.includes('单据编号:') ||
|
||
!infoHtml.includes('送货单位:') ||
|
||
(infoHtml.match(/<tr[\s>]/g) || []).length > 1
|
||
|
||
if (!needsMigrate) return html
|
||
|
||
const newInfoTable = `<table class="inbound-slip-info-table" style="width:100%;border-collapse:collapse;table-layout:fixed;border:none !important;">
|
||
${INFO_COLGROUP}
|
||
<tr>
|
||
<td colspan="4" style="${INFO_CELL_LEFT}">
|
||
<div class="inbound-slip-info-row" style="${INFO_ROW}">
|
||
<span class="inbound-slip-info-label" style="${INFO_LABEL}">送货单位:</span>
|
||
<span class="inbound-slip-info-value-left" style="${INFO_VALUE_INLINE_LEFT}">{{receivingUnit}} </span>
|
||
</div>
|
||
</td>
|
||
<td colspan="3" style="${INFO_CELL_RIGHT}">
|
||
<div class="inbound-slip-info-row" style="${INFO_ROW}">
|
||
<span class="inbound-slip-info-label" style="${INFO_LABEL}">日 期:</span>
|
||
<span class="inbound-slip-info-value-center" style="${INFO_VALUE_INLINE_CENTER}">{{planDateChinese}} </span>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
</table>`
|
||
|
||
return html.replace(infoMatch[0], newInfoTable)
|
||
}
|
||
|
||
/** 物料表:表头补「单据编号」列,并把旧 7 列数据行升为 8 列(兼容已保存模板) */
|
||
function normalizeInboundSlipProductPlanNoColumn(html: string): string {
|
||
if (!html.includes('inbound-slip-product-table')) return html
|
||
|
||
let result = html
|
||
|
||
if (!/>单据编号<\/th>/.test(result)) {
|
||
result = result.replace(
|
||
/(<tr[^>]*class="[^"]*inbound-slip-product-header[^"]*"[^>]*>\s*)(<th[^>]*>物料编号<\/th>)/,
|
||
`$1<th class="inbound-slip-product-cell inbound-slip-product-grid-cell" style="${PRODUCT_TH}">单据编号</th>\n $2`
|
||
)
|
||
result = result.replace(
|
||
/(<tr>\s*)(<th[^>]*>物料编号<\/th>)/,
|
||
`$1<th class="inbound-slip-product-cell inbound-slip-product-grid-cell" style="${PRODUCT_TH}">单据编号</th>\n $2`
|
||
)
|
||
}
|
||
|
||
// 旧 7 列明细行:在首格前插入单据编号占位
|
||
result = result.replace(
|
||
/<tr([^>]*)>\s*((?:<td(?![^>]*colspan)[^>]*>[\s\S]*?<\/td>\s*){7})<\/tr>/g,
|
||
(rowMatch, trAttrs, cells) => {
|
||
if (
|
||
rowMatch.includes('备注') ||
|
||
rowMatch.includes('1.本送货单') ||
|
||
rowMatch.includes('仓库:') ||
|
||
rowMatch.includes('暂无产品信息') ||
|
||
rowMatch.includes('单据编号')
|
||
) {
|
||
return rowMatch
|
||
}
|
||
return `<tr${trAttrs}>\n <td class="inbound-slip-product-cell inbound-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">{{planNo}}</td>\n ${cells}</tr>`
|
||
}
|
||
)
|
||
|
||
result = result.replace(/colspan="7"/g, 'colspan="8"')
|
||
return result
|
||
}
|
||
|
||
/**
|
||
* 旧版一体 layout-table / 曾拆出的 footer-table → 方案A:
|
||
* 信息表 + 物料表(含备注条款) + 签字表。
|
||
*/
|
||
function migrateInboundSlipSplitTables(html: string): string {
|
||
if (!html.includes('inbound-slip-doc')) return html
|
||
|
||
// 已正确:物料表内含备注,且无独立 footer-table
|
||
if (
|
||
html.includes('inbound-slip-product-table') &&
|
||
!html.includes('inbound-slip-footer-table') &&
|
||
/inbound-slip-product-table[\s\S]*?备注:/.test(html)
|
||
) {
|
||
return html
|
||
}
|
||
|
||
// 把独立 footer-table 并回 product-table
|
||
if (html.includes('inbound-slip-footer-table') && html.includes('inbound-slip-product-table')) {
|
||
return html.replace(
|
||
/(<\/tr>\s*)<\/table>\s*<table class="inbound-slip-footer-table"[^>]*>\s*(?:<colgroup>[\s\S]*?<\/colgroup>\s*)?([\s\S]*?)<\/table>/i,
|
||
(_m, beforeClose, footerInner) => {
|
||
const rows = footerInner.replace(/^\s*<colgroup>[\s\S]*?<\/colgroup>\s*/i, '').trim()
|
||
return `${beforeClose}${rows}\n </table>`
|
||
}
|
||
)
|
||
}
|
||
|
||
if (!html.includes('inbound-slip-layout-table')) return html
|
||
|
||
return html.replace(
|
||
/<table class="inbound-slip-layout-table"[^>]*>([\s\S]*?)<\/table>/i,
|
||
(_full, inner: string) => {
|
||
const headerMatch =
|
||
inner.match(/<tr[^>]*class="[^"]*inbound-slip-product-header[^"]*"[\s\S]*?<\/tr>/i) ||
|
||
inner.match(/<tr>\s*<th[^>]*>单据编号[\s\S]*?<\/tr>/i) ||
|
||
inner.match(/<tr>\s*<th[^>]*>物料编号[\s\S]*?<\/tr>/i)
|
||
if (!headerMatch) return _full
|
||
|
||
const headerIdx = inner.indexOf(headerMatch[0])
|
||
let before = inner.slice(0, headerIdx)
|
||
before = before.replace(/<colgroup>[\s\S]*?<\/colgroup>/i, '')
|
||
before = before.replace(/<tr class="inbound-slip-gap-before-product">[\s\S]*?<\/tr>/i, '')
|
||
|
||
const afterHeader = inner.slice(headerIdx + headerMatch[0].length)
|
||
const signMatch =
|
||
afterHeader.match(/<tr[^>]*class="[^"]*inbound-slip-sign-row[^"]*"[\s\S]*?<\/tr>/i) ||
|
||
afterHeader.match(/<tr>\s*<td[^>]*>[\s\S]*?仓库:[\s\S]*?<\/tr>/i)
|
||
|
||
let productAndFooter = afterHeader
|
||
let signPart = buildInboundSlipSignRowHtml()
|
||
if (signMatch) {
|
||
const signAt = afterHeader.indexOf(signMatch[0])
|
||
productAndFooter = afterHeader.slice(0, signAt)
|
||
signPart = signMatch[0]
|
||
}
|
||
productAndFooter = productAndFooter.replace(
|
||
/<tr class="inbound-slip-gap-before-sign">[\s\S]*?<\/tr>/i,
|
||
''
|
||
)
|
||
|
||
return `<table class="inbound-slip-info-table" style="width:100%;border-collapse:collapse;table-layout:fixed;border:none !important;">
|
||
${INFO_COLGROUP}
|
||
${before.trim()}
|
||
</table>
|
||
<div class="inbound-slip-section-gap inbound-slip-gap-before-product" style="${GAP_BEFORE_PRODUCT_CELL}"> </div>
|
||
<table class="inbound-slip-product-table" style="width:100%;border-collapse:collapse;table-layout:fixed;border:none !important;">
|
||
${PRODUCT_COLGROUP}
|
||
${headerMatch[0]}
|
||
${productAndFooter.trim()}
|
||
</table>
|
||
<div class="inbound-slip-section-gap inbound-slip-gap-before-sign" style="${GAP_BEFORE_SIGN_CELL}"> </div>
|
||
<table class="inbound-slip-sign-table" style="width:100%;border-collapse:collapse;table-layout:fixed;border:none !important;">
|
||
${signPart.trim()}
|
||
</table>`
|
||
}
|
||
)
|
||
}
|
||
|
||
/** 统一单据左右 10px 间距、内容区宽度(兼容已保存模板) */
|
||
function normalizeInboundSlipDocLayout(html: string): string {
|
||
let result = migrateInboundSlipSplitTables(html)
|
||
.replace(/padding:8px 12px 24px 4px;/g, `padding:${DOC_PADDING};`)
|
||
.replace(/padding:8px 12px 24px;/g, `padding:${DOC_PADDING};`)
|
||
.replace(/padding-left: 4px !important;/g, `padding: ${DOC_PADDING} !important;`)
|
||
.replace(/left:0;top:50%/g, `left:${LOGO_LEFT}px;top:50%`)
|
||
.replace(/left:20px;/g, `left:${LOGO_LEFT}px;`)
|
||
.replace(/padding:4px 80px 0 120px;/g, 'padding:4px 60px 0 100px;')
|
||
.replace(/padding:4px 72px 0 88px;/g, 'padding:4px 60px 0 100px;')
|
||
.replace(/max-width:720px/g, `max-width:${INBOUND_SLIP_DOC_MAX_WIDTH}`)
|
||
.replace(/max-width:760px/g, `max-width:${INBOUND_SLIP_DOC_MAX_WIDTH}`)
|
||
.replace(/max-width:794px/g, `max-width:${INBOUND_SLIP_DOC_MAX_WIDTH}`)
|
||
.replace(/height:46px;/g, `height:${LOGO_HEIGHT}px;`)
|
||
.replace(/height:23px;/g, `height:${LOGO_HEIGHT}px;`)
|
||
.replace(/min-height:72px;/g, 'min-height:56px;')
|
||
.replace(/min-height:48px;/g, 'min-height:56px;')
|
||
.replace(/padding:4px 60px 0 68px;/g, 'padding:4px 60px 0 100px;')
|
||
.replace(/padding:4px 60px 0 52px;/g, 'padding:4px 60px 0 100px;')
|
||
.replace(/padding:1px 6px;/g, `padding:${TABLE_ROW_PADDING};`)
|
||
.replace(/padding:4px 6px;/g, `padding:${TABLE_ROW_PADDING};`)
|
||
.replace(/padding-bottom:5px;/g, '')
|
||
.replace(/padding-bottom:10px;/g, '')
|
||
.replace(/padding:18px 6px 8px;/g, `padding:${TABLE_ROW_PADDING};`)
|
||
.replace(/margin-top:6px;/g, 'margin-top:2px;')
|
||
.replace(/line-height:1\.6;/g, 'line-height:1.35;')
|
||
.replace(/line-height:1\.9;/g, 'line-height:1.35;')
|
||
.replace(/min-height:88px;/g, 'min-height:0;')
|
||
.replace(/min-height:36px;/g, '')
|
||
.replace(/min-height:76px;/g, 'min-height:0;')
|
||
.replace(/min-height:120px;/g, 'min-height:0;')
|
||
.replace(/top:4px !important;/g, `top:${INBOUND_SLIP_SEAL_TOP}px !important;`)
|
||
.replace(/top:4px;/g, `top:${INBOUND_SLIP_SEAL_TOP}px;`)
|
||
.replace(/top:-96px !important;/g, `top:${INBOUND_SLIP_SEAL_TOP}px !important;`)
|
||
.replace(/top:-96px;/g, `top:${INBOUND_SLIP_SEAL_TOP}px;`)
|
||
.replace(/>日期:</g, '>日 期:<')
|
||
.replace(/border-bottom:2px solid #000 !important;/g, `border-bottom:${INFO_UNDERLINE} !important;`)
|
||
.replace(/border-bottom:1px solid #000 !important;/g, `border-bottom:${INFO_UNDERLINE} !important;`)
|
||
.replace(/border:2px solid #000 !important;/g, `border:${PRODUCT_GRID_BORDER} !important;`)
|
||
.replace(/border:1px solid #000 !important;/g, `border:${PRODUCT_GRID_BORDER} !important;`)
|
||
.replace(
|
||
/display:inline-block;(?:min-width:\d+px;)?padding:0 2px 1px;vertical-align:bottom;border:none !important;/g,
|
||
''
|
||
)
|
||
.replace(
|
||
/display:inline-block;padding:0 2px 2px;vertical-align:bottom;border:none !important;border-bottom:1px solid #000 !important;/g,
|
||
''
|
||
)
|
||
.replace(
|
||
/(仓库:|审核:|客户签收:|司机\/送货人\(附车牌\):)<span(?![^>]*class="inbound-slip-sign-value") style="/g,
|
||
'$1<span class="inbound-slip-sign-value" style="'
|
||
)
|
||
.replace(
|
||
/(<span class="inbound-slip-sign-value" style="[^"]*)border-bottom:2px solid #000 !important;/g,
|
||
`$1border-bottom:${SIGN_UNDERLINE} !important;`
|
||
)
|
||
.replace(/class="inbound-slip-info-before-product"/g, '')
|
||
.replace(/padding-bottom:12px;/g, '')
|
||
.replace(/padding-bottom:24px;/g, '')
|
||
|
||
if (result.includes('inbound-slip-doc') && !result.includes('max-width:')) {
|
||
result = result.replace(
|
||
/(<div class="inbound-slip-doc" style=")([^"]*)"/,
|
||
`$1$2;${DOC_WRAP_EXTRA}"`
|
||
)
|
||
}
|
||
|
||
if (result.includes('inbound-slip-doc')) {
|
||
result = result.replace(/<style type="text\/css">[\s\S]*?<\/style>/, INBOUND_SLIP_STYLE_BLOCK)
|
||
result = normalizeInboundSlipSignLabelMarkup(result)
|
||
result = normalizeInboundSlipProductGridCell(result)
|
||
result = normalizeInboundSlipProductTableFooter(result)
|
||
}
|
||
|
||
return result
|
||
}
|
||
|
||
/** 将旧版右侧联次 HTML 统一还原为占位符,便于每次打开注入最新纵向布局 */
|
||
function normalizeInboundSlipSideLabelsPlaceholder(html: string): string {
|
||
if (html.includes('{{sideLabels}}')) return html
|
||
|
||
let result = html
|
||
result = result.replace(/<table class="inbound-slip-side-labels"[\s\S]*?<\/table>/, '{{sideLabels}}')
|
||
result = result.replace(/<div class="inbound-slip-side-labels"[\s\S]*?<\/div>/, '{{sideLabels}}')
|
||
result = result.replace(
|
||
/(<!--\s*最右侧纵向联次\s*-->\s*<td[^>]*>)[\s\S]*?(<\/td>)/,
|
||
'$1{{sideLabels}}$2'
|
||
)
|
||
return result
|
||
}
|
||
|
||
/** 将占位符替换为计划最新数据 */
|
||
export function replaceInboundSlipPlaceholders(
|
||
htmlContent: string,
|
||
plan: Plan,
|
||
ctx?: InboundSlipRenderContext
|
||
): string {
|
||
if (!htmlContent || !plan) return htmlContent
|
||
|
||
const replacements: Record<string, string> = {
|
||
'{{companyName}}': INBOUND_SLIP_DEFAULTS.companyName,
|
||
'{{receivingUnit}}': buildDeliveryUnit(plan),
|
||
'{{planNo}}': plan.planNo || '',
|
||
'{{receiveContact}}': '',
|
||
'{{driverInfo}}': '',
|
||
'{{planDateChinese}}': formatPlanDateChinese(plan.planDate),
|
||
'{{productTable}}': buildProductRows(plan.products, plan.planNo || '', ctx),
|
||
'{{remark}}': formatRemarkForSlipHtml(plan.remark),
|
||
'{{companyAddress}}': INBOUND_SLIP_DEFAULTS.companyAddress,
|
||
'{{companyPhone}}': INBOUND_SLIP_DEFAULTS.companyPhone,
|
||
'{{contactPhone}}': INBOUND_SLIP_DEFAULTS.contactPhone,
|
||
'{{companySeal}}': buildInboundSlipCompanySealHtml(),
|
||
'{{sideLabels}}': buildInboundSlipSideLabelsHtml()
|
||
}
|
||
|
||
let result = normalizeInboundSlipDocLayout(
|
||
normalizeInboundSlipSealPlaceholder(
|
||
normalizeInboundSlipSignRow(
|
||
normalizeInboundSlipTermsCell(
|
||
normalizeInboundSlipTermsText(
|
||
normalizeInboundSlipGapBeforeSign(
|
||
normalizeInboundSlipProductTableFooter(
|
||
normalizeInboundSlipProductColWidths(
|
||
normalizeInboundSlipProductGridCell(
|
||
normalizeInboundSlipProductPlanNoColumn(
|
||
normalizeInboundSlipInfoLayout(
|
||
normalizeInboundSlipGapBeforeProduct(
|
||
normalizeInboundSlipSideLabelsPlaceholder(htmlContent)
|
||
)
|
||
)
|
||
)
|
||
)
|
||
)
|
||
)
|
||
)
|
||
)
|
||
)
|
||
)
|
||
)
|
||
)
|
||
for (const [placeholder, value] of Object.entries(replacements)) {
|
||
result = result.split(placeholder).join(value)
|
||
}
|
||
return result
|
||
}
|
||
|
||
/** 保存模板前:将动态字段还原为占位符 */
|
||
export function restoreInboundSlipPlaceholders(template: string, plan: Plan): string {
|
||
let result = template
|
||
|
||
const deliveryUnit = buildDeliveryUnit(plan)
|
||
const dynamicReplacements: Record<string, string> = {
|
||
'{{companyName}}': INBOUND_SLIP_DEFAULTS.companyName,
|
||
'{{receivingUnit}}': deliveryUnit,
|
||
'{{planNo}}': plan.planNo || '',
|
||
'{{planDateChinese}}': formatPlanDateChinese(plan.planDate),
|
||
'{{remark}}': plan.remark || '',
|
||
'{{companyAddress}}': INBOUND_SLIP_DEFAULTS.companyAddress,
|
||
'{{companyPhone}}': INBOUND_SLIP_DEFAULTS.companyPhone,
|
||
'{{contactPhone}}': INBOUND_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)
|
||
}
|
||
|
||
// 旧模板写死的本公司名 → 送货单位占位符
|
||
if (deliveryUnit !== LEGACY_RECEIVING_UNIT) {
|
||
result = result.replace(new RegExp(LEGACY_RECEIVING_UNIT.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), '{{receivingUnit}}')
|
||
}
|
||
|
||
// 产品明细行还原为占位符(表头「实收数量」与「备注」行之间)
|
||
result = result.replace(
|
||
/(<th[^>]*>实收数量<\/th>\s*<\/tr>\s*)([\s\S]*?)(\s*<tr>\s*<td colspan="[78]"[^>]*>\s*备注:)/,
|
||
'$1{{productTable}}$3'
|
||
)
|
||
|
||
// 右侧联次还原为占位符
|
||
result = result.replace(/<table class="inbound-slip-side-labels"[\s\S]*?<\/table>/, '{{sideLabels}}')
|
||
result = result.replace(/<div class="inbound-slip-side-labels"[\s\S]*?<\/div>/, '{{sideLabels}}')
|
||
|
||
// 印章还原为占位符(对齐合同管理打印弹窗)
|
||
result = result.replace(/<img[^>]*alt="印章"[^>]*>/gi, '')
|
||
if (result.includes('inbound-slip-seal-block') && !result.includes('{{companySeal}}')) {
|
||
result = result.replace(
|
||
/(<div class="inbound-slip-seal-block"[^>]*>)/,
|
||
'$1\n <!-- 印章图片 -->\n {{companySeal}}\n '
|
||
)
|
||
}
|
||
result = result.replace(/(\{\{companySeal\}\}\s*){2,}/g, '{{companySeal}}\n')
|
||
|
||
return result
|
||
}
|