1887 lines
81 KiB
TypeScript
1887 lines
81 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 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 = `<colgroup>
|
||
<col span="7" style="width:14.28%;" />
|
||
</colgroup>`
|
||
/**
|
||
* 物料表列宽:单据编号 + 加宽编号/名称,收窄规格/单位;前 5 列合计供条款区使用。
|
||
* 单据编号 / 物料编号 / 物料名称 / 规格 / 单位 / 数量 / 产品规格 / 实收数量
|
||
*/
|
||
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;`
|
||
/** 条款单元格:行高随内容,避免被右侧印章区 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 = `<span class="delivery-slip-handwrite-blank">(${HANDWRITE_BLANK_INNER})</span>`
|
||
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 `<span class="delivery-slip-sign-item"><span class="delivery-slip-sign-label">${label}</span><span class="delivery-slip-sign-value" style="min-width:${minWidth};">${valueContent}</span></span>`
|
||
}
|
||
|
||
function buildDeliverySlipSignRowHtml(): string {
|
||
return `<tr class="delivery-slip-sign-row">
|
||
<td colspan="8" class="delivery-slip-sign-row-cell" style="border:none !important;padding:${TABLE_ROW_PADDING};text-align:center;${FONT_SIGN}background:transparent !important;">
|
||
<div class="delivery-slip-sign-inner">
|
||
${buildDeliverySlipSignItem('仓库:', ' ', '80px')}
|
||
${buildDeliverySlipSignItem('审核:', ' ', '80px')}
|
||
${buildDeliverySlipSignItem('客户签收:', ' ', '80px')}
|
||
${buildDeliverySlipSignItem('司机/送货人(附车牌):', ' ', '120px')}
|
||
</div>
|
||
</td>
|
||
</tr>`
|
||
}
|
||
|
||
export interface DeliverySlipRenderContext {
|
||
/** CRM 产品 id -> 物料编码 productNo */
|
||
productCodeById?: Record<string, string>
|
||
/** 单位字典 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<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 buildReceivingUnit(plan: Plan): string {
|
||
return plan.customerName?.trim() || plan.destinationName?.trim() || ''
|
||
}
|
||
|
||
function formatDeliverySlipProductGridCellContent(content: string): string {
|
||
const plain = content
|
||
.trim()
|
||
.replace(/<div class="delivery-slip-product-cell-inner"[^>]*>([\s\S]*?)<\/div>/gi, '$1')
|
||
.replace(/<div class="delivery-slip-pdf-grid-inner"[^>]*>([\s\S]*?)<\/div>/gi, '$1')
|
||
.replace(/<span class="delivery-slip-pdf-grid-text"[^>]*>([\s\S]*?)<\/span>/gi, '$1')
|
||
.replace(/<span class="delivery-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?: DeliverySlipRenderContext
|
||
): string {
|
||
if (!products.length) {
|
||
return `<tr><td class="delivery-slip-product-cell" colspan="8" style="${PRODUCT_CELL}">暂无产品信息</td></tr>`
|
||
}
|
||
|
||
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 `<tr>
|
||
<td class="delivery-slip-product-cell delivery-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">${planNoText}</td>
|
||
<td class="delivery-slip-product-cell delivery-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">${formatDeliverySlipProductGridCellContent(resolveSlipProductCode(product, ctx))}</td>
|
||
<td class="delivery-slip-product-cell delivery-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">${formatDeliverySlipProductGridCellContent(product.productName || '')}</td>
|
||
<td class="delivery-slip-product-cell delivery-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">${formatDeliverySlipProductGridCellContent(product.model || '')}</td>
|
||
<td class="delivery-slip-product-cell delivery-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">${formatDeliverySlipProductGridCellContent(resolveSlipUnitLabel(product.unit, ctx))}</td>
|
||
<td class="delivery-slip-product-cell delivery-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">${formatDeliverySlipProductGridCellContent(formatQty(qty))}</td>
|
||
<td class="delivery-slip-product-cell delivery-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">${formatDeliverySlipProductGridCellContent(product.packageSpec || '')}</td>
|
||
<td class="delivery-slip-product-cell delivery-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}"> </td>
|
||
</tr>`
|
||
})
|
||
.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 = `<style type="text/css">${DELIVERY_SLIP_PRINT_CSS}</style>`
|
||
|
||
/** 生成公司印章 HTML(地址/电话单元格右上角) */
|
||
export function buildDeliverySlipCompanySealHtml(): string {
|
||
return `<img src="${DELIVERY_SLIP_SEAL_IMAGE_SRC}" alt="印章" style="${DELIVERY_SLIP_SEAL_IMAGE_STYLE}" />`
|
||
}
|
||
|
||
const SEAL_BLOCK = `<div class="delivery-slip-seal-block" style="position:relative;padding:0;${FONT_BODY}${SEAL_BLOCK_LINE_STYLE}">
|
||
<!-- 印章图片 -->
|
||
{{companySeal}}
|
||
<div class="delivery-slip-seal-lines" style="position:relative;z-index:1;${SEAL_BLOCK_LINE_STYLE}margin:0;padding:0;">
|
||
地址:{{companyAddress}}<br />
|
||
送货单位固话:{{companyPhone}}<br />
|
||
联系人电话:{{contactPhone}}
|
||
</div>
|
||
</div>`
|
||
|
||
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) =>
|
||
`<label class="delivery-slip-type-check-item" contenteditable="false"><input type="checkbox" class="delivery-slip-type-check-input" />${label}</label>`
|
||
return `<div class="delivery-slip-info-row delivery-slip-type-check-wrap" style="${DELIVERY_SLIP_TYPE_CHECK_WRAP_STYLE}">
|
||
<span class="delivery-slip-info-label delivery-slip-type-check-spacer" style="${INFO_LABEL}visibility:hidden;">日 期:</span>
|
||
<div class="delivery-slip-type-check-row" style="${DELIVERY_SLIP_TYPE_CHECK_INNER_STYLE}">${item('自提')}${item('出库')}${item('直销')}</div>
|
||
</div>`
|
||
}
|
||
|
||
function buildDeliverySlipInfoTableHtml(): string {
|
||
return `<table class="delivery-slip-info-table" style="width:100%;border-collapse:collapse;table-layout:fixed;border:none !important;">
|
||
${INFO_COLGROUP}
|
||
<tr>
|
||
<td colspan="2" style="${INFO_CELL_LEFT}">
|
||
<div class="delivery-slip-info-row" style="${INFO_ROW}">
|
||
<span class="delivery-slip-info-label" style="${INFO_LABEL}">收货单位:</span>
|
||
<span class="delivery-slip-info-value-left" style="${INFO_VALUE_INLINE_LEFT}">{{receivingUnit}} </span>
|
||
</div>
|
||
</td>
|
||
<td colspan="2" style="${INFO_CELL_LEFT}">
|
||
<div class="delivery-slip-info-row" style="${INFO_ROW}">
|
||
<span class="delivery-slip-info-label" style="${INFO_LABEL}">收货联系方式:</span>
|
||
<span class="delivery-slip-info-value-left" style="${INFO_VALUE_INLINE_LEFT}">{{receiveContact}} </span>
|
||
</div>
|
||
</td>
|
||
<td colspan="3" style="${INFO_CELL_RIGHT}">
|
||
${buildDeliverySlipTypeCheckboxesHtml()}
|
||
<div class="delivery-slip-info-row" style="${INFO_ROW}">
|
||
<span class="delivery-slip-info-label" style="${INFO_LABEL}">日 期:</span>
|
||
<span class="delivery-slip-info-value-center" style="${INFO_VALUE_INLINE_CENTER}">{{planDateChinese}} </span>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
</table>`
|
||
}
|
||
|
||
function buildDeliverySlipTermsHtml(): string {
|
||
return `<div class="delivery-slip-terms-text" style="${TERMS_TEXT_STYLE}">
|
||
1.本送货单如购销双方未签订合同,视为有效;<br />
|
||
2.本次送货含桶${HANDWRITE_BLANK}个,回收${HANDWRITE_BLANK}个,结存${HANDWRITE_BLANK}个;
|
||
</div>`
|
||
}
|
||
|
||
/** 右侧联次:单列纵向,带圈序号 + 每字一行依次向下排列 */
|
||
export function buildDeliverySlipSideLabelsHtml(): string {
|
||
return `<table class="delivery-slip-side-labels" style="border:none !important;border-collapse:collapse;margin:0 auto;width:auto;">
|
||
<tr>
|
||
<td class="delivery-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 generateDeliverySlipTemplate(_plan?: Plan): string {
|
||
return `${DELIVERY_SLIP_STYLE_BLOCK}<div class="delivery-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="delivery-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="delivery-slip-title-heading" style="${TITLE_STYLE}">{{companyName}}</div>
|
||
<div class="delivery-slip-title-heading" style="${TITLE_SUB_STYLE}">送 (提) 货 单</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 下部分:内容区(左正文 + 右侧联次) -->
|
||
<table class="delivery-slip-outer-table" style="width:100%;border-collapse:collapse;table-layout:fixed;border:none !important;">
|
||
<tr>
|
||
<td class="delivery-slip-main-col" style="vertical-align:top;padding-right:6px;border:none !important;background:transparent !important;">
|
||
<!-- 1. 基本信息(独立表,不可拖宽高) -->
|
||
${buildDeliverySlipInfoTableHtml()}
|
||
|
||
<div class="delivery-slip-section-gap delivery-slip-gap-before-product" style="${GAP_BEFORE_PRODUCT_CELL}"> </div>
|
||
|
||
<!-- 2. 物料明细表(含备注、条款+印章) -->
|
||
<table class="delivery-slip-product-table" style="width:100%;border-collapse:collapse;table-layout:fixed;border:none !important;">
|
||
${PRODUCT_COLGROUP}
|
||
<tr class="delivery-slip-product-header">
|
||
<th class="delivery-slip-product-cell delivery-slip-product-grid-cell" style="${PRODUCT_TH}">单据编号</th>
|
||
<th class="delivery-slip-product-cell delivery-slip-product-grid-cell" style="${PRODUCT_TH}">物料编号</th>
|
||
<th class="delivery-slip-product-cell delivery-slip-product-grid-cell" style="${PRODUCT_TH}">物料名称</th>
|
||
<th class="delivery-slip-product-cell delivery-slip-product-grid-cell" style="${PRODUCT_TH}">规格</th>
|
||
<th class="delivery-slip-product-cell delivery-slip-product-grid-cell" style="${PRODUCT_TH}">单位</th>
|
||
<th class="delivery-slip-product-cell delivery-slip-product-grid-cell" style="${PRODUCT_TH}">数量</th>
|
||
<th class="delivery-slip-product-cell delivery-slip-product-grid-cell" style="${PRODUCT_TH}">产品规格</th>
|
||
<th class="delivery-slip-product-cell delivery-slip-product-grid-cell" style="${PRODUCT_TH}">实收数量</th>
|
||
</tr>
|
||
{{productTable}}
|
||
<tr>
|
||
<td colspan="8" class="delivery-slip-product-cell" style="${PRODUCT_TABLE_CELL_LEFT}">
|
||
备注:{{remark}}
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td colspan="5" class="delivery-slip-product-cell delivery-slip-terms-cell" style="${PRODUCT_TERMS_CELL_LEFT}">
|
||
${buildDeliverySlipTermsHtml()}
|
||
</td>
|
||
<td colspan="3" class="delivery-slip-product-cell" style="${PRODUCT_TABLE_CELL_LEFT}">
|
||
${SEAL_BLOCK}
|
||
</td>
|
||
</tr>
|
||
</table>
|
||
|
||
<div class="delivery-slip-section-gap delivery-slip-gap-before-sign" style="${GAP_BEFORE_SIGN_CELL}"> </div>
|
||
|
||
<!-- 3. 签字区(独立表) -->
|
||
<table class="delivery-slip-sign-table" style="width:100%;border-collapse:collapse;table-layout:fixed;border:none !important;">
|
||
${buildDeliverySlipSignRowHtml()}
|
||
</table>
|
||
</td>
|
||
|
||
<!-- 最右侧纵向联次 -->
|
||
<td class="delivery-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 normalizeDeliverySlipSealPlaceholder(html: string): string {
|
||
if (!html.includes('delivery-slip-doc')) return html
|
||
|
||
let result = html.replace(/<img[^>]*alt="印章"[^>]*>/gi, '{{companySeal}}')
|
||
|
||
if (result.includes('delivery-slip-seal-block') && !result.includes('{{companySeal}}')) {
|
||
result = result.replace(
|
||
/(<div class="delivery-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 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(/<tr([^>]*)>\s*(<th[\s\S]*?<\/th>\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('<tr', '<tr class="delivery-slip-product-header"')
|
||
return row.replace(/<(th)([^>]*)>([\s\S]*?)<\/th>/g, (_m, tag, attrs, content) => {
|
||
return `<${tag}${patchDeliverySlipProductGridCellAttrs(attrs)}>${formatDeliverySlipProductGridCellContent(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}${patchDeliverySlipProductGridCellAttrs(attrs)}>${formatDeliverySlipProductGridCellContent(content)}</${tag}>`
|
||
})
|
||
})
|
||
|
||
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)}</${tag}>`
|
||
|
||
let result = html.replace(
|
||
/<(td|th)([^>]*delivery-slip-product-grid-cell[^>]*)>\s*<span class="delivery-slip-product-grid-align"[^>]*>([\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*<span class="delivery-slip-pdf-grid-text"[^>]*>([\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*<div class="delivery-slip-pdf-grid-inner"[^>]*>([\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*<p[^>]*>([\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*<div(?![^>]*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 `<span class="delivery-slip-type-check-item-pdf">${labelText}</span>`
|
||
|
||
return `<span class="delivery-slip-type-check-item-pdf"><img class="delivery-slip-type-check-img-pdf" src="${imgSrc}" width="16" height="16" alt="" /><span class="delivery-slip-type-check-text-pdf">${labelText}</span></span>`
|
||
}
|
||
|
||
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(/<td([^>]*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 `<td${patched}>${space}备注:`
|
||
})
|
||
|
||
result = result.replace(
|
||
/<td([^>]*colspan="[456]"[^>]*)>\s*(<div class="delivery-slip-terms-text"|\s*1\.本送货单)/g,
|
||
(_match, attrs, contentStart) => {
|
||
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 `<td${patched}>\n ${contentStart}`
|
||
}
|
||
)
|
||
|
||
result = result.replace(
|
||
/<td([^>]*colspan="[23]"[^>]*)>\s*<div class="delivery-slip-seal-block"/g,
|
||
(_match, attrs) => {
|
||
const withSpan = String(attrs).replace(/colspan="[23]"/, 'colspan="3"')
|
||
const patched = patchProductFooterCellAttrs(withSpan, 'delivery-slip-product-cell', PRODUCT_TABLE_CELL_LEFT)
|
||
return `<td${patched}>\n <div class="delivery-slip-seal-block"`
|
||
}
|
||
)
|
||
|
||
return result
|
||
}
|
||
|
||
/** 条款单元格行高随内容(兼容已保存模板) */
|
||
function normalizeDeliverySlipTermsCell(html: string): string {
|
||
if (!html.includes('delivery-slip-doc')) return html
|
||
|
||
const patchTermsTd = (attrs: string) => {
|
||
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(
|
||
/<td colspan="[456]"([^>]*)>\s*<div class="delivery-slip-terms-text"/g,
|
||
(match, attrs) => {
|
||
if (attrs.includes('delivery-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="delivery-slip-terms-text"`
|
||
}
|
||
)
|
||
|
||
result = result.replace(
|
||
/<div class="delivery-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="delivery-slip-seal-block" style="${nextStyle}"`
|
||
}
|
||
)
|
||
|
||
return result
|
||
}
|
||
|
||
/** 物料表列宽 + 条款区跨列(兼容已保存模板) */
|
||
function normalizeDeliverySlipProductColWidths(html: string): string {
|
||
if (!html.includes('delivery-slip-product-table')) return html
|
||
return html.replace(
|
||
/(<table class="delivery-slip-product-table"[^>]*>\s*)<colgroup>[\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(/<div class="delivery-slip-terms-text"[\s\S]*?<\/div>/, termsHtml.trim())
|
||
}
|
||
|
||
return html.replace(
|
||
/1\.本送货单如购销双方未签订合同,视为有效;[\s\S]*?结存\([^)]*\)个;/,
|
||
termsHtml.trim()
|
||
)
|
||
}
|
||
|
||
/** 刷新已有间距行的 inline 样式(TinyMCE 保存后可能丢失) */
|
||
function refreshDeliverySlipGapBeforeProduct(html: string): string {
|
||
return html
|
||
.replace(
|
||
/<tr class="delivery-slip-gap-before-product">[\s\S]*?<\/tr>/,
|
||
`<tr class="delivery-slip-gap-before-product">\n <td colspan="8" class="delivery-slip-gap-before-product-cell" style="${GAP_BEFORE_PRODUCT_CELL}"> </td>\n </tr>`
|
||
)
|
||
.replace(
|
||
/<div class="delivery-slip-section-gap delivery-slip-gap-before-product"[^>]*>[\s\S]*?<\/div>/,
|
||
`<div class="delivery-slip-section-gap delivery-slip-gap-before-product" style="${GAP_BEFORE_PRODUCT_CELL}"> </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 = `<tr class="delivery-slip-gap-before-product"><td colspan="8" class="delivery-slip-gap-before-product-cell" style="${GAP_BEFORE_PRODUCT_CELL}"> </td></tr>\n `
|
||
const productHeader = `(?:单据编号|物料编号)`
|
||
|
||
const withComment = html.replace(
|
||
new RegExp(
|
||
`(收货联系方式:[\\s\\S]*?<\\/tr>\\s*)(<!--\\s*中:产品明细表[\\s\\S]*?<tr[\\s\\S]*?<th[^>]*>${productHeader})`
|
||
),
|
||
`$1${gapRow}$2`
|
||
)
|
||
if (withComment !== html) return withComment
|
||
|
||
const withHeader = html.replace(
|
||
new RegExp(`(收货联系方式:[\\s\\S]*?<\\/tr>\\s*)(<tr[\\s\\S]*?<th[^>]*>${productHeader})`),
|
||
`$1${gapRow}$2`
|
||
)
|
||
if (withHeader !== html) return withHeader
|
||
|
||
return html.replace(
|
||
new RegExp(
|
||
`(<span class="delivery-slip-info-label"[^>]*>日(?: )?期:<\\/span>[\\s\\S]*?<\\/tr>\\s*)(<!--\\s*中:产品明细表[\\s\\S]*?<tr[\\s\\S]*?<th[^>]*>${productHeader}|<tr[\\s\\S]*?<th[^>]*>${productHeader})`
|
||
),
|
||
`$1${gapRow}$2`
|
||
)
|
||
}
|
||
|
||
/** 刷新签字区上方间距行 inline 样式 */
|
||
function refreshDeliverySlipGapBeforeSign(html: string): string {
|
||
return html
|
||
.replace(
|
||
/<tr class="delivery-slip-gap-before-sign">[\s\S]*?<\/tr>/,
|
||
`<tr class="delivery-slip-gap-before-sign">\n <td colspan="8" class="delivery-slip-gap-before-sign-cell" style="${GAP_BEFORE_SIGN_CELL}"> </td>\n </tr>`
|
||
)
|
||
.replace(
|
||
/<div class="delivery-slip-section-gap delivery-slip-gap-before-sign"[^>]*>[\s\S]*?<\/div>/,
|
||
`<div class="delivery-slip-section-gap delivery-slip-gap-before-sign" style="${GAP_BEFORE_SIGN_CELL}"> </div>`
|
||
)
|
||
}
|
||
|
||
/** 在物料表与签字区之间插入间距行(兼容已保存模板) */
|
||
function normalizeDeliverySlipGapBeforeSign(html: string): string {
|
||
if (!html.includes('delivery-slip-doc')) return html
|
||
|
||
if (html.includes('delivery-slip-gap-before-sign')) {
|
||
return refreshDeliverySlipGapBeforeSign(html)
|
||
}
|
||
|
||
const gapRow = `<tr class="delivery-slip-gap-before-sign"><td colspan="8" class="delivery-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(
|
||
/(delivery-slip-seal-block[\s\S]*?<\/tr>\s*)(<tr>\s*<td colspan="[78]"[^>]*>\s*仓库:)/,
|
||
`$1${gapRow}$2`
|
||
)
|
||
}
|
||
|
||
/** 签字区标签/下划线 baseline 对齐(兼容已保存模板) */
|
||
function normalizeDeliverySlipSignLabelMarkup(html: string): string {
|
||
if (!html.includes('delivery-slip-sign-value')) return html
|
||
|
||
let result = html.replace(
|
||
/<span class="delivery-slip-sign-item">(仓库:|审核:|客户签收:|司机\/送货人\(附车牌\):)\s*(<span class="delivery-slip-sign-value")/g,
|
||
'<span class="delivery-slip-sign-item"><span class="delivery-slip-sign-label">$1</span>$2'
|
||
)
|
||
|
||
result = result.replace(/<span class="delivery-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="delivery-slip-sign-value" style="${nextStyle}"` : `<span class="delivery-slip-sign-value"`
|
||
})
|
||
|
||
return result
|
||
}
|
||
|
||
/** 签字区 flex 布局(兼容已保存模板,避免打印预览溢出到右侧联次列) */
|
||
function normalizeDeliverySlipSignRow(html: string): string {
|
||
if (!html.includes('delivery-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('delivery-slip-sign-value') || content.includes('delivery-slip-sign-inner')) {
|
||
return match
|
||
}
|
||
|
||
const wrappedContent = content
|
||
.replace(/ /g, '')
|
||
.replace(
|
||
/(仓库:|审核:|客户签收:|司机\/送货人\(附车牌\):)\s*(<span class="delivery-slip-sign-value"[\s\S]*?<\/span>)/g,
|
||
'<span class="delivery-slip-sign-item"><span class="delivery-slip-sign-label">$1</span>$2</span>'
|
||
)
|
||
.replace(
|
||
/<span class="delivery-slip-sign-item">(仓库:|审核:|客户签收:|司机\/送货人\(附车牌\):)\s*(<span class="delivery-slip-sign-value")/g,
|
||
'<span class="delivery-slip-sign-item"><span class="delivery-slip-sign-label">$1</span>$2'
|
||
)
|
||
|
||
const trClassAttr = trAttrs.includes('class="')
|
||
? trAttrs.replace(/class="([^"]*)"/, (_, cls: string) =>
|
||
cls.includes('delivery-slip-sign-row') ? `class="${cls}"` : `class="${cls} delivery-slip-sign-row"`
|
||
)
|
||
: `${trAttrs} class="delivery-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('delivery-slip-sign-row-cell') ? `class="${cls}"` : `class="${cls} delivery-slip-sign-row-cell"`
|
||
)
|
||
} else {
|
||
nextTdAttrs = `${nextTdAttrs} class="delivery-slip-sign-row-cell"`
|
||
}
|
||
|
||
return `<tr${trClassAttr}>\n <td colspan="8"${nextTdAttrs}>\n <div class="delivery-slip-sign-inner">${wrappedContent.trim()}</div>\n </td>\n </tr>`
|
||
}
|
||
)
|
||
|
||
result = result.replace(
|
||
/<td(?![^>]*class="[^"]*delivery-slip-main-col)([^>]*style="[^"]*vertical-align:top;padding-right:6px)/g,
|
||
'<td class="delivery-slip-main-col"$1'
|
||
)
|
||
|
||
return normalizeDeliverySlipSignLabelMarkup(result)
|
||
}
|
||
|
||
/** 信息区:收货单位 / 收货联系方式 / 日期 同一行(兼容旧多行布局) */
|
||
function normalizeDeliverySlipInfoLayout(html: string): string {
|
||
if (!html.includes('delivery-slip-info-table')) return html
|
||
|
||
const infoMatch = html.match(/<table class="delivery-slip-info-table"[^>]*>[\s\S]*?<\/table>/i)
|
||
if (!infoMatch) return html
|
||
|
||
const infoHtml = infoMatch[0]
|
||
const rowCount = (infoHtml.match(/<tr[\s>]/g) || []).length
|
||
const hasPlanNoInInfo = infoHtml.includes('单据编号:')
|
||
const dateColWideEnough = /<td[^>]*colspan="3"[^>]*>[\s\S]*?日(?: )?期:/.test(infoHtml)
|
||
const oneRowThreeFields =
|
||
rowCount === 1 &&
|
||
infoHtml.includes('收货单位:') &&
|
||
infoHtml.includes('收货联系方式:') &&
|
||
/日(?: )?期:/.test(infoHtml) &&
|
||
!hasPlanNoInInfo &&
|
||
dateColWideEnough
|
||
|
||
if (oneRowThreeFields) return html
|
||
|
||
return html.replace(infoMatch[0], buildDeliverySlipInfoTableHtml())
|
||
}
|
||
|
||
/** 物料表:表头补「单据编号」列,并把旧 7 列数据行升为 8 列(兼容已保存模板) */
|
||
function normalizeDeliverySlipProductPlanNoColumn(html: string): string {
|
||
if (!html.includes('delivery-slip-product-table')) return html
|
||
|
||
let result = html
|
||
|
||
if (!/>单据编号<\/th>/.test(result)) {
|
||
result = result.replace(
|
||
/(<tr[^>]*class="[^"]*delivery-slip-product-header[^"]*"[^>]*>\s*)(<th[^>]*>物料编号<\/th>)/,
|
||
`$1<th class="delivery-slip-product-cell delivery-slip-product-grid-cell" style="${PRODUCT_TH}">单据编号</th>\n $2`
|
||
)
|
||
result = result.replace(
|
||
/(<tr>\s*)(<th[^>]*>物料编号<\/th>)/,
|
||
`$1<th class="delivery-slip-product-cell delivery-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="delivery-slip-product-cell delivery-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">{{planNo}}</td>\n ${cells}</tr>`
|
||
}
|
||
)
|
||
|
||
// 仅升级物料表内备注等跨列,条款区保持 5 列、印章区 3 列
|
||
result = result.replace(
|
||
/(<table class="delivery-slip-product-table"[^>]*>)([\s\S]*?)(<\/table>)/i,
|
||
(_m, open, inner, close) => {
|
||
let nextInner = String(inner).replace(/colspan="7"/g, 'colspan="8"')
|
||
nextInner = nextInner.replace(
|
||
/(<td[^>]*colspan=")6("[^>]*>\s*(?:<div class="delivery-slip-terms-text"|[\s]*1\.本送货单))/g,
|
||
'$15$2'
|
||
)
|
||
nextInner = nextInner.replace(
|
||
/(<td[^>]*colspan=")2("[^>]*>\s*<div class="delivery-slip-seal-block")/g,
|
||
'$13$2'
|
||
)
|
||
return `${open}${nextInner}${close}`
|
||
}
|
||
)
|
||
|
||
result = result.replace(
|
||
/(<table class="delivery-slip-sign-table"[^>]*>)([\s\S]*?)(<\/table>)/i,
|
||
(_m, open, inner, close) => `${open}${String(inner).replace(/colspan="7"/g, 'colspan="8"')}${close}`
|
||
)
|
||
|
||
return result
|
||
}
|
||
|
||
/**
|
||
* 旧版一体 layout-table / 曾拆出的 footer-table → 信息表 + 物料表(含备注条款) + 签字表。
|
||
*/
|
||
function migrateDeliverySlipSplitTables(html: string): string {
|
||
if (!html.includes('delivery-slip-doc')) return html
|
||
|
||
if (
|
||
html.includes('delivery-slip-product-table') &&
|
||
!html.includes('delivery-slip-footer-table') &&
|
||
/delivery-slip-product-table[\s\S]*?备注:/.test(html)
|
||
) {
|
||
return html
|
||
}
|
||
|
||
if (html.includes('delivery-slip-footer-table') && html.includes('delivery-slip-product-table')) {
|
||
return html.replace(
|
||
/(<\/tr>\s*)<\/table>\s*<table class="delivery-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('delivery-slip-layout-table')) return html
|
||
|
||
return html.replace(
|
||
/<table class="delivery-slip-layout-table"[^>]*>([\s\S]*?)<\/table>/i,
|
||
(_full, inner: string) => {
|
||
const headerMatch =
|
||
inner.match(/<tr[^>]*class="[^"]*delivery-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="delivery-slip-gap-before-product">[\s\S]*?<\/tr>/i, '')
|
||
|
||
const afterHeader = inner.slice(headerIdx + headerMatch[0].length)
|
||
const signMatch =
|
||
afterHeader.match(/<tr[^>]*class="[^"]*delivery-slip-sign-row[^"]*"[\s\S]*?<\/tr>/i) ||
|
||
afterHeader.match(/<tr>\s*<td[^>]*>[\s\S]*?仓库:[\s\S]*?<\/tr>/i)
|
||
|
||
let productAndFooter = afterHeader
|
||
let signPart = buildDeliverySlipSignRowHtml()
|
||
if (signMatch) {
|
||
const signAt = afterHeader.indexOf(signMatch[0])
|
||
productAndFooter = afterHeader.slice(0, signAt)
|
||
signPart = signMatch[0]
|
||
}
|
||
productAndFooter = productAndFooter.replace(
|
||
/<tr class="delivery-slip-gap-before-sign">[\s\S]*?<\/tr>/i,
|
||
''
|
||
)
|
||
|
||
return `<table class="delivery-slip-info-table" style="width:100%;border-collapse:collapse;table-layout:fixed;border:none !important;">
|
||
${INFO_COLGROUP}
|
||
${before.trim()}
|
||
</table>
|
||
<div class="delivery-slip-section-gap delivery-slip-gap-before-product" style="${GAP_BEFORE_PRODUCT_CELL}"> </div>
|
||
<table class="delivery-slip-product-table" style="width:100%;border-collapse:collapse;table-layout:fixed;border:none !important;">
|
||
${PRODUCT_COLGROUP}
|
||
${headerMatch[0]}
|
||
${productAndFooter.trim()}
|
||
</table>
|
||
<div class="delivery-slip-section-gap delivery-slip-gap-before-sign" style="${GAP_BEFORE_SIGN_CELL}"> </div>
|
||
<table class="delivery-slip-sign-table" style="width:100%;border-collapse:collapse;table-layout:fixed;border:none !important;">
|
||
${signPart.trim()}
|
||
</table>`
|
||
}
|
||
)
|
||
}
|
||
|
||
/** 统一单据左右 10px 间距、内容区宽度(兼容已保存模板) */
|
||
function normalizeDeliverySlipDocLayout(html: string): string {
|
||
let result = migrateDeliverySlipSplitTables(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:${DELIVERY_SLIP_DOC_MAX_WIDTH}`)
|
||
.replace(/max-width:760px/g, `max-width:${DELIVERY_SLIP_DOC_MAX_WIDTH}`)
|
||
.replace(/max-width:794px/g, `max-width:${DELIVERY_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:${DELIVERY_SLIP_SEAL_TOP}px !important;`)
|
||
.replace(/top:4px;/g, `top:${DELIVERY_SLIP_SEAL_TOP}px;`)
|
||
.replace(/top:-96px !important;/g, `top:${DELIVERY_SLIP_SEAL_TOP}px !important;`)
|
||
.replace(/top:-96px;/g, `top:${DELIVERY_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="delivery-slip-sign-value") style="/g,
|
||
'$1<span class="delivery-slip-sign-value" style="'
|
||
)
|
||
.replace(
|
||
/(<span class="delivery-slip-sign-value" style="[^"]*)border-bottom:2px solid #000 !important;/g,
|
||
`$1border-bottom:${SIGN_UNDERLINE} !important;`
|
||
)
|
||
.replace(/class="delivery-slip-info-before-product"/g, '')
|
||
.replace(/padding-bottom:12px;/g, '')
|
||
.replace(/padding-bottom:24px;/g, '')
|
||
|
||
if (result.includes('delivery-slip-doc') && !result.includes('max-width:')) {
|
||
result = result.replace(
|
||
/(<div class="delivery-slip-doc" style=")([^"]*)"/,
|
||
`$1$2;${DOC_WRAP_EXTRA}"`
|
||
)
|
||
}
|
||
|
||
if (result.includes('delivery-slip-doc')) {
|
||
result = result.replace(/<style type="text\/css">[\s\S]*?<\/style>/, DELIVERY_SLIP_STYLE_BLOCK)
|
||
result = normalizeDeliverySlipInfoLayout(result)
|
||
result = normalizeDeliverySlipProductPlanNoColumn(result)
|
||
result = normalizeDeliverySlipSignLabelMarkup(result)
|
||
result = normalizeDeliverySlipProductGridCell(result)
|
||
result = normalizeDeliverySlipProductTableFooter(result)
|
||
result = normalizeDeliverySlipTypeCheckRow(result)
|
||
}
|
||
|
||
return result
|
||
}
|
||
|
||
/** 单据编号上方:自提/出库/直销多选(兼容已保存模板) */
|
||
function refreshDeliverySlipTypeCheckRowStyle(html: string): string {
|
||
if (!html.includes('delivery-slip-type-check-row')) return html
|
||
|
||
let result = html
|
||
|
||
if (!result.includes('delivery-slip-type-check-wrap')) {
|
||
result = result.replace(
|
||
/<div class="delivery-slip-type-check-row"[^>]*>([\s\S]*?)<\/div>/,
|
||
(_match, inner) =>
|
||
`<div class="delivery-slip-info-row delivery-slip-type-check-wrap" style="${DELIVERY_SLIP_TYPE_CHECK_WRAP_STYLE}"><span class="delivery-slip-info-label delivery-slip-type-check-spacer" style="${INFO_LABEL}visibility:hidden;">日 期:</span><div class="delivery-slip-type-check-row" style="${DELIVERY_SLIP_TYPE_CHECK_INNER_STYLE}">${inner}</div></div>`
|
||
)
|
||
}
|
||
|
||
result = result.replace(
|
||
/<div class="delivery-slip-info-row delivery-slip-type-check-wrap"[^>]*>/g,
|
||
`<div class="delivery-slip-info-row delivery-slip-type-check-wrap" style="${DELIVERY_SLIP_TYPE_CHECK_WRAP_STYLE}">`
|
||
)
|
||
result = result.replace(
|
||
/<span class="delivery-slip-info-label delivery-slip-type-check-spacer"[^>]*>[\s\S]*?<\/span>/g,
|
||
`<span class="delivery-slip-info-label delivery-slip-type-check-spacer" style="${INFO_LABEL}visibility:hidden;">日 期:</span>`
|
||
)
|
||
result = result.replace(
|
||
/<div class="delivery-slip-type-check-row"[^>]*>/g,
|
||
`<div class="delivery-slip-type-check-row" style="${DELIVERY_SLIP_TYPE_CHECK_INNER_STYLE}">`
|
||
)
|
||
result = result.replace(
|
||
/<label class="delivery-slip-type-check-item"(?![^>]*contenteditable=)/g,
|
||
'<label class="delivery-slip-type-check-item" contenteditable="false"'
|
||
)
|
||
|
||
return result
|
||
}
|
||
|
||
function normalizeDeliverySlipTypeCheckRow(html: string): string {
|
||
if (!html.includes('delivery-slip-doc')) return html
|
||
|
||
let result = html
|
||
if (!result.includes('delivery-slip-type-check-row')) {
|
||
const checkRowHtml = buildDeliverySlipTypeCheckboxesHtml()
|
||
|
||
const withClass = result.replace(
|
||
/(<td colspan="3"[^>]*style="[^"]*"[^>]*>\s*)(<div class="delivery-slip-info-row"[^>]*>\s*<span class="delivery-slip-info-label"[^>]*>日(?: )?期:)/,
|
||
`$1${checkRowHtml}\n $2`
|
||
)
|
||
if (withClass !== result) {
|
||
result = withClass
|
||
} else {
|
||
result = result.replace(
|
||
/(<td colspan="3"[^>]*>\s*)(<div class="delivery-slip-info-row"[^>]*>\s*<span class="delivery-slip-info-label"[^>]*>日(?: )?期:)/,
|
||
`$1${checkRowHtml}\n $2`
|
||
)
|
||
}
|
||
// 兼容旧模板:日期在 colspan=2 单元格时仍插多选
|
||
if (!result.includes('delivery-slip-type-check-row')) {
|
||
result = result.replace(
|
||
/(<td colspan="2"[^>]*>\s*)(<div class="delivery-slip-info-row"[^>]*>\s*<span class="delivery-slip-info-label"[^>]*>日(?: )?期:)/,
|
||
`$1${checkRowHtml}\n $2`
|
||
)
|
||
}
|
||
// 兼容更旧模板:单据编号位置仍插多选,随后由 infoLayout 迁移
|
||
if (!result.includes('delivery-slip-type-check-row')) {
|
||
result = result.replace(
|
||
/(<td colspan="3"[^>]*>\s*)(<div class="delivery-slip-info-row"[^>]*>\s*<span class="delivery-slip-info-label"[^>]*>单据编号:)/,
|
||
`$1${checkRowHtml}\n $2`
|
||
)
|
||
}
|
||
}
|
||
|
||
return refreshDeliverySlipTypeCheckRowStyle(result)
|
||
}
|
||
|
||
const DELIVERY_SLIP_CHECK_BOUND_ATTR = 'data-delivery-slip-check-bound'
|
||
|
||
function syncDeliverySlipTypeCheckboxInput(input: HTMLInputElement): void {
|
||
if (input.checked) {
|
||
input.setAttribute('checked', 'checked')
|
||
} else {
|
||
input.removeAttribute('checked')
|
||
}
|
||
}
|
||
|
||
/** 富文本编辑器内:绑定自提/出库/直销多选点击(TinyMCE 默认可编辑区会拦截原生 checkbox) */
|
||
export function bindDeliverySlipTypeCheckboxInteraction(
|
||
root: HTMLElement | null | undefined,
|
||
onChange?: () => void
|
||
): void {
|
||
if (!root) return
|
||
|
||
root.querySelectorAll('label.delivery-slip-type-check-item').forEach((node) => {
|
||
const label = node as HTMLElement
|
||
label.setAttribute('contenteditable', 'false')
|
||
|
||
const input = label.querySelector('input.delivery-slip-type-check-input') as HTMLInputElement | null
|
||
if (!input) return
|
||
|
||
syncDeliverySlipTypeCheckboxInput(input)
|
||
|
||
if (label.getAttribute(DELIVERY_SLIP_CHECK_BOUND_ATTR) === '1') return
|
||
label.setAttribute(DELIVERY_SLIP_CHECK_BOUND_ATTR, '1')
|
||
|
||
const toggle = (e: Event) => {
|
||
if (root.isContentEditable === false) return
|
||
e.preventDefault()
|
||
e.stopPropagation()
|
||
input.checked = !input.checked
|
||
syncDeliverySlipTypeCheckboxInput(input)
|
||
onChange?.()
|
||
}
|
||
|
||
label.addEventListener('mousedown', toggle, true)
|
||
input.addEventListener('mousedown', (e) => {
|
||
e.preventDefault()
|
||
e.stopPropagation()
|
||
}, true)
|
||
input.addEventListener('click', (e) => {
|
||
e.preventDefault()
|
||
e.stopPropagation()
|
||
}, true)
|
||
})
|
||
}
|
||
|
||
/** 送(提)货单 TinyMCE:初始化与内容变更时绑定多选项交互 */
|
||
export function setupDeliverySlipEditorTypeCheckboxes(editor: {
|
||
getBody: () => HTMLElement
|
||
on: (event: string, callback: () => void) => void
|
||
fire: (event: string) => void
|
||
}): void {
|
||
const bind = () => {
|
||
bindDeliverySlipTypeCheckboxInteraction(editor.getBody(), () => editor.fire('change'))
|
||
}
|
||
|
||
editor.on('init', () => {
|
||
window.setTimeout(bind, 0)
|
||
})
|
||
editor.on('SetContent', () => {
|
||
window.setTimeout(bind, 0)
|
||
})
|
||
editor.on('focus', () => {
|
||
window.setTimeout(bind, 0)
|
||
})
|
||
}
|
||
|
||
/** 将旧版右侧联次 HTML 统一还原为占位符,便于每次打开注入最新纵向布局 */
|
||
function normalizeDeliverySlipSideLabelsPlaceholder(html: string): string {
|
||
if (html.includes('{{sideLabels}}')) return html
|
||
|
||
let result = html
|
||
result = result.replace(/<table class="delivery-slip-side-labels"[\s\S]*?<\/table>/, '{{sideLabels}}')
|
||
result = result.replace(/<div class="delivery-slip-side-labels"[\s\S]*?<\/div>/, '{{sideLabels}}')
|
||
result = result.replace(
|
||
/(<!--\s*最右侧纵向联次\s*-->\s*<td[^>]*>)[\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<string, string> = {
|
||
'{{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<string, string> = {
|
||
'{{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[^>]*>实收数量<\/th>\s*<\/tr>\s*)([\s\S]*?)(\s*<tr>\s*<td colspan="[78]"[^>]*>\s*备注:)/,
|
||
'$1{{productTable}}$3'
|
||
)
|
||
|
||
// 右侧联次还原为占位符
|
||
result = result.replace(/<table class="delivery-slip-side-labels"[\s\S]*?<\/table>/, '{{sideLabels}}')
|
||
result = result.replace(/<div class="delivery-slip-side-labels"[\s\S]*?<\/div>/, '{{sideLabels}}')
|
||
|
||
// 印章还原为占位符(对齐合同管理打印弹窗)
|
||
result = result.replace(/<img[^>]*alt="印章"[^>]*>/gi, '')
|
||
if (result.includes('delivery-slip-seal-block') && !result.includes('{{companySeal}}')) {
|
||
result = result.replace(
|
||
/(<div class="delivery-slip-seal-block"[^>]*>)/,
|
||
'$1\n <!-- 印章图片 -->\n {{companySeal}}\n '
|
||
)
|
||
}
|
||
result = result.replace(/(\{\{companySeal\}\}\s*){2,}/g, '{{companySeal}}\n')
|
||
|
||
return result
|
||
}
|