import type { Plan, PlanProduct } from '@/api/pl' import { applyPdfStableTableCellBorders } from '@/utils/pl-slip-pdf-render' /** 分装单打印默认公司信息(可在富文本中修改) */ export const SUBPACK_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 SUBPACK_SLIP_DOC_MAX_WIDTH = '960px' const DOC_WRAP_EXTRA = `max-width:${SUBPACK_SLIP_DOC_MAX_WIDTH};margin:0 auto;box-sizing:border-box;` const LOGO_HEIGHT = 23 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' /** 物料网格 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,定位在地址单元格右上角(相对原位置向上 100px) */ const SUBPACK_SLIP_SEAL_IMAGE_SRC = '/sxshzyz.png' const SUBPACK_SLIP_SEAL_TOP = -96 const SUBPACK_SLIP_SEAL_IMAGE_STYLE = `position:absolute;right:4px;top:${SUBPACK_SLIP_SEAL_TOP}px;width:140px;height:140px;opacity:0.85;z-index:10;` const HANDWRITE_BLANK_INNER = '        ' const HANDWRITE_BLANK = `(${HANDWRITE_BLANK_INNER})` const PLAIN_CELL = `border:none !important;padding:${TABLE_ROW_PADDING};${FONT_BODY}line-height:1.8;vertical-align:top;background:transparent !important;` const NO_BORDER = `${PLAIN_CELL} vertical-align:bottom;` /** 基本信息:label 与值同一行,值区域占满剩余宽度并带下划线 */ const INFO_ROW = `display:flex;align-items:flex-end;width:100%;box-sizing:border-box;` const INFO_LABEL = `white-space:nowrap;flex-shrink:0;padding-right:2px;` const INFO_UNDERLINE = '1px solid #000' const SIGN_UNDERLINE = '1px solid #000' /** 签字值下划线与文字间距(底部 padding 把 border-bottom 推离字模) */ const SIGN_VALUE_PADDING = '0 2px 6px 2px' const SIGN_VALUE_PDF_PADDING = '0 2px 8px 2px' const INFO_VALUE_INLINE_LEFT = `flex:1;min-width:0;box-sizing:border-box;border-bottom:${INFO_UNDERLINE} !important;padding:0 2px 2px;text-align:left;min-height:1.3em;` const INFO_VALUE_INLINE_CENTER = `flex:1;min-width:0;box-sizing:border-box;border-bottom:${INFO_UNDERLINE} !important;padding:0 2px 2px;text-align:center;min-height:1.3em;` const INFO_CELL_LEFT = `${NO_BORDER}text-align:left;` const INFO_CELL_RIGHT = `${NO_BORDER}text-align:left;` const INFO_GAP_BEFORE_PRODUCT = 24 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 = 24 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 /** 签字区下划线最小宽度 */ const SUBPACK_SLIP_SIGN_VALUE_MIN_WIDTH = '120px' function buildSubpackSlipSignItem(label: string, valueContent: string, minWidth: string = SUBPACK_SLIP_SIGN_VALUE_MIN_WIDTH): string { return `${label}${valueContent}` } function buildSubpackSlipSignRowHtml(): string { return `
${buildSubpackSlipSignItem('制单人:', '{{preparerUser}} ')} ${buildSubpackSlipSignItem('审核:', ' ')} ${buildSubpackSlipSignItem('生产主管:', ' ')}
` } export interface SubpackSlipRenderContext { /** 单位字典 code -> 展示文案 */ getUnitLabel?: (unitCode: string) => string /** 分装单号 NO:SXFZYYYYMMDD-### */ subpackSlipNo?: string /** 下单日期 YYYY-MM-DD */ orderDate?: string /** 下单人(当前登录用户姓名) */ orderUser?: string /** 制单人(默认当前登录用户姓名) */ preparerUser?: string } function resolveSlipUnitLabel(unitCode: string | undefined, ctx?: SubpackSlipRenderContext): string { const code = String(unitCode || '').trim() if (!code) return '' return ctx?.getUnitLabel?.(code) || code } export function canPrintSubpackSlip(plan?: Pick | null): boolean { return plan?.planType !== 'inbound' } 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 formatSubpackSlipProductGridCellContent(content: string): string { const plain = content .trim() .replace(/
]*>([\s\S]*?)<\/div>/gi, '$1') .replace(/
]*>([\s\S]*?)<\/div>/gi, '$1') .replace(/]*>([\s\S]*?)<\/span>/gi, '$1') .replace(/]*>([\s\S]*?)<\/span>/gi, '$1') .replace(/^]*>([\s\S]*?)<\/p>$/i, '$1') .trim() return plain || ' ' } function formatDateIso(value?: string | Date | null): string { const date = value instanceof Date ? value : value ? new Date(value) : new Date() if (Number.isNaN(date.getTime())) return '' const y = date.getFullYear() const m = String(date.getMonth() + 1).padStart(2, '0') const d = String(date.getDate()).padStart(2, '0') return `${y}-${m}-${d}` } /** 分装单默认下单日期(当天 YYYY-MM-DD) */ export function formatSubpackSlipDateIso(value?: string | Date | null): string { return formatDateIso(value) } function buildSubpackSlipCompleteCheckHtml(): string { return `` } const SUBPACK_SLIP_PRODUCT_MIN_ROWS = 5 function buildSubpackSlipProductRow( rowIndex: number, product: PlanProduct | undefined, ctx?: SubpackSlipRenderContext ): string { if (product) { const qty = product.outboundQty ?? product.outboundEstimateQty ?? product.quantity ?? '' return ` ${formatSubpackSlipProductGridCellContent(String(rowIndex))} ${formatSubpackSlipProductGridCellContent(product.productName || '')} ${formatSubpackSlipProductGridCellContent(product.model || '')} ${formatSubpackSlipProductGridCellContent(product.packageSpec || '')} ${formatSubpackSlipProductGridCellContent(resolveSlipUnitLabel(product.unit, ctx))} ${formatSubpackSlipProductGridCellContent(formatQty(qty))} ${buildSubpackSlipCompleteCheckHtml()} ` } return ` ${formatSubpackSlipProductGridCellContent(String(rowIndex))} ${formatSubpackSlipProductGridCellContent('')} ${formatSubpackSlipProductGridCellContent('')} ${formatSubpackSlipProductGridCellContent('')} ${formatSubpackSlipProductGridCellContent('')} ${formatSubpackSlipProductGridCellContent('')}   ` } function buildProductRows(products: PlanProduct[] = [], ctx?: SubpackSlipRenderContext): string { const rowCount = Math.max(products.length, SUBPACK_SLIP_PRODUCT_MIN_ROWS) const rows: string[] = [] for (let index = 0; index < rowCount; index += 1) { rows.push(buildSubpackSlipProductRow(index + 1, products[index], ctx)) } return rows.join('') } export const SUBPACK_SLIP_PRINT_CSS = ` .subpack-slip-doc { padding: ${DOC_PADDING} !important; max-width: ${SUBPACK_SLIP_DOC_MAX_WIDTH} !important; margin-left: auto !important; margin-right: auto !important; box-sizing: border-box !important; } .subpack-slip-doc .subpack-slip-title-heading { font-weight: 700 !important; line-height: 1.35 !important; } .subpack-slip-doc .subpack-slip-title-heading + .subpack-slip-title-heading { margin-top: 2px !important; } .subpack-slip-doc .subpack-slip-info-row { display: flex !important; align-items: flex-end !important; width: 100% !important; box-sizing: border-box !important; } .subpack-slip-doc .subpack-slip-slip-no-display { font-size: 20px !important; line-height: 1.35 !important; letter-spacing: 0.5px !important; white-space: nowrap !important; } .subpack-slip-doc .subpack-slip-slip-no-anchor { position: relative !important; flex-shrink: 0 !important; } .subpack-slip-doc .subpack-slip-slip-no-anchor-text { visibility: hidden !important; } .subpack-slip-doc .subpack-slip-slip-no-anchor .subpack-slip-slip-no-display { position: absolute !important; right: 0 !important; top: 0 !important; text-align: right !important; margin-top: 0 !important; } .subpack-slip-doc .subpack-slip-slip-no-field .subpack-slip-info-value-left { visibility: hidden !important; border-bottom: none !important; min-height: 0 !important; padding-bottom: 0 !important; } .subpack-slip-doc .subpack-slip-basic-info-spacer { visibility: hidden !important; min-height: 0 !important; height: 0 !important; overflow: hidden !important; margin: 0 !important; padding: 0 !important; } .subpack-slip-doc .subpack-slip-basic-info-inner { display: flex !important; flex-wrap: wrap !important; justify-content: space-between !important; align-items: flex-end !important; gap: 8px 12px !important; width: 100% !important; box-sizing: border-box !important; } .subpack-slip-doc .subpack-slip-basic-info-item { flex: 1 !important; min-width: 0 !important; } /* 下单/交货日期略宽;下单人与经手人等宽(单号行 spacer 同步,保持右缘对齐) */ .subpack-slip-doc .subpack-slip-basic-info-row .subpack-slip-basic-info-inner > .subpack-slip-basic-info-item:nth-child(1), .subpack-slip-doc .subpack-slip-basic-info-row .subpack-slip-basic-info-inner > .subpack-slip-basic-info-item:nth-child(2), .subpack-slip-doc .subpack-slip-slip-no-row .subpack-slip-basic-info-inner > .subpack-slip-basic-info-item:nth-child(1), .subpack-slip-doc .subpack-slip-slip-no-row .subpack-slip-basic-info-inner > .subpack-slip-basic-info-item:nth-child(2) { flex: 1.1 !important; } .subpack-slip-doc .subpack-slip-basic-info-row .subpack-slip-basic-info-inner > .subpack-slip-basic-info-item:nth-child(3), .subpack-slip-doc .subpack-slip-basic-info-row .subpack-slip-basic-info-inner > .subpack-slip-basic-info-item:nth-child(4), .subpack-slip-doc .subpack-slip-slip-no-row .subpack-slip-basic-info-inner > .subpack-slip-basic-info-item:nth-child(3), .subpack-slip-doc .subpack-slip-slip-no-row .subpack-slip-basic-info-inner > .subpack-slip-basic-info-item:nth-child(4) { flex: 1 !important; } .subpack-slip-doc .subpack-slip-remark-row-inner { display: flex !important; align-items: center !important; width: 100% !important; box-sizing: border-box !important; gap: 12px !important; } .subpack-slip-doc .subpack-slip-remark-label { flex-shrink: 0 !important; white-space: nowrap !important; } /* 备注正文 + 补充输入共用一条连续下划线(底部 padding 把线推离字模,避免 PDF/html2canvas 遮字) */ .subpack-slip-doc .subpack-slip-remark-underline-group { display: inline-flex !important; align-items: baseline !important; flex: 1 !important; min-width: 200px !important; max-width: 420px !important; box-sizing: border-box !important; border-bottom: ${INFO_UNDERLINE} !important; padding: ${SIGN_VALUE_PADDING} !important; gap: 0 !important; line-height: 1.35 !important; overflow: visible !important; } .subpack-slip-doc .subpack-slip-remark-value { display: inline-block !important; flex: 0 1 auto !important; min-width: 0 !important; max-width: 260px !important; box-sizing: border-box !important; border-bottom: none !important; padding: 0 2px !important; min-height: 1.3em !important; line-height: 1.35 !important; overflow: visible !important; white-space: nowrap !important; vertical-align: baseline !important; } .subpack-slip-doc .subpack-slip-remark-extra-wrap { flex: 1 !important; min-width: 80px !important; display: inline-block !important; vertical-align: bottom !important; -webkit-user-modify: read-only !important; } .subpack-slip-doc .subpack-slip-remark-extra-input { width: 100% !important; min-width: 80px !important; height: 28px !important; padding: 0 2px 0 !important; margin: 0 !important; border: none !important; border-bottom: none !important; border-radius: 0 !important; background: transparent !important; font-family: inherit !important; font-size: 20px !important; line-height: 1.35 !important; box-sizing: border-box !important; outline: none !important; color: #000 !important; pointer-events: auto !important; } .subpack-slip-doc .subpack-slip-bucket-check-row { flex: 1 !important; min-width: 0 !important; display: flex !important; flex-wrap: wrap !important; justify-content: center !important; align-items: center !important; gap: 14px 18px !important; box-sizing: border-box !important; font-size: 20px !important; line-height: 1.35 !important; } .subpack-slip-doc .subpack-slip-complete-cell { text-align: center !important; } .subpack-slip-doc .subpack-slip-complete-check-item { justify-content: center !important; margin: 0 auto !important; } .subpack-slip-doc .subpack-slip-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; } .subpack-slip-doc .subpack-slip-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; } .subpack-slip-doc .subpack-slip-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; } .subpack-slip-doc .subpack-slip-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; } .subpack-slip-doc .subpack-slip-check-item-pdf:last-child { margin-right: 0 !important; } .subpack-slip-doc .subpack-slip-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; } .subpack-slip-doc .subpack-slip-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; } .subpack-slip-doc .subpack-slip-check-item-pdf.subpack-slip-complete-check-item-pdf { margin-right: 0 !important; } .subpack-slip-doc .subpack-slip-info-label { white-space: nowrap !important; flex-shrink: 0 !important; } .subpack-slip-doc table.subpack-slip-layout-table tr.subpack-slip-gap-before-product td.subpack-slip-gap-before-product-cell { 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; } .subpack-slip-doc table.subpack-slip-layout-table tr.subpack-slip-gap-before-sign td.subpack-slip-gap-before-sign-cell { 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; } .subpack-slip-doc .subpack-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; } .subpack-slip-doc .subpack-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; } .subpack-slip-doc .subpack-slip-sign-value { display: inline-block !important; min-width: ${SUBPACK_SLIP_SIGN_VALUE_MIN_WIDTH} !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; } .subpack-slip-doc .subpack-slip-sign-label { line-height: 1.35 !important; vertical-align: baseline !important; white-space: nowrap !important; } .subpack-slip-doc table.subpack-slip-outer-table { table-layout: fixed !important; width: 100% !important; border-collapse: collapse !important; } .subpack-slip-doc td.subpack-slip-main-col { overflow: hidden !important; box-sizing: border-box !important; vertical-align: top !important; } .subpack-slip-doc td.subpack-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; } .subpack-slip-doc .subpack-slip-sign-row-cell, .subpack-slip-doc .subpack-slip-sign-inner, .subpack-slip-doc .subpack-slip-sign-item { font-size: 14px !important; } .subpack-slip-doc .subpack-slip-sign-row-cell { overflow: hidden !important; max-width: 100% !important; box-sizing: border-box !important; } .subpack-slip-doc .subpack-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; } .subpack-slip-doc .subpack-slip-sign-item { display: inline !important; white-space: nowrap !important; line-height: 1.35 !important; vertical-align: baseline !important; } .subpack-slip-doc .subpack-slip-terms-text { line-height: 1.35 !important; margin: 0 !important; padding: 0 !important; } .subpack-slip-doc td.subpack-slip-terms-cell { padding: 10px 6px 14px 6px !important; line-height: 1.35 !important; vertical-align: top !important; height: auto !important; } .subpack-slip-doc .subpack-slip-handwrite-blank { display: inline-block !important; min-width: 3.6em !important; text-align: center !important; letter-spacing: 0.08em !important; } .subpack-slip-doc .subpack-slip-seal-block { position: relative !important; line-height: 1.35 !important; min-height: 0 !important; height: auto !important; } .subpack-slip-doc .subpack-slip-seal-block img[alt="印章"] { position: absolute !important; right: 4px !important; top: ${SUBPACK_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; } .subpack-slip-doc .subpack-slip-seal-block .subpack-slip-seal-lines { position: relative !important; z-index: 1 !important; line-height: 1.35 !important; margin: 0 !important; padding: 0 !important; } .subpack-slip-doc table.subpack-slip-outer-table > tbody > tr > td, .subpack-slip-doc table.subpack-slip-outer-table > tbody > tr > th, .subpack-slip-doc table.subpack-slip-layout-table td:not(.subpack-slip-product-cell):not(.subpack-slip-gap-before-product-cell):not(.subpack-slip-gap-before-sign-cell), .subpack-slip-doc table.subpack-slip-layout-table th:not(.subpack-slip-product-cell) { border: none !important; background: transparent !important; padding: ${TABLE_ROW_PADDING} !important; } .subpack-slip-doc table.subpack-slip-layout-table { border-collapse: collapse !important; border-spacing: 0 !important; table-layout: fixed !important; } .subpack-slip-doc .subpack-slip-product-grid-cell { border: ${PRODUCT_GRID_BORDER} !important; text-align: center !important; vertical-align: middle !important; padding: ${PRODUCT_GRID_CELL_PADDING} !important; box-sizing: border-box !important; line-height: normal !important; 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; } .subpack-slip-doc th.subpack-slip-product-grid-cell { padding: ${PRODUCT_GRID_TH_PADDING} !important; font-weight: bold !important; } .subpack-slip-doc .subpack-slip-product-grid-cell > p { margin: 0 !important; padding: 0 !important; text-align: center !important; line-height: 1.35 !important; } .subpack-slip-doc .subpack-slip-product-cell:not(.subpack-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; } .subpack-slip-doc .subpack-slip-seal-block, .subpack-slip-doc .subpack-slip-seal-lines, .subpack-slip-doc .subpack-slip-terms-text { margin: 0 !important; padding: 0 !important; line-height: 1.35 !important; } .subpack-slip-doc table.subpack-slip-side-labels { border: none !important; border-collapse: collapse !important; width: auto !important; margin: 0 auto !important; } .subpack-slip-doc td.subpack-slip-side-labels-col, .subpack-slip-doc table.subpack-slip-side-labels td, .subpack-slip-doc table.subpack-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 SUBPACK_SLIP_STYLE_BLOCK = `` /** 生成公司印章 HTML(地址/电话单元格右上角) */ export function buildSubpackSlipCompanySealHtml(): string { return `印章` } const SUBPACK_SLIP_BUCKET_CHECK_INNER_STYLE = 'flex:1;min-width:0;display:flex;flex-wrap:wrap;justify-content:center;align-items:center;gap:14px 18px;box-sizing:border-box;font-size:20px;line-height:1.35;' function buildSubpackSlipProductHeaderRowHtml(): string { return ` 序号 产品名称 产品型号 包装规格 单位 数量 是否完成 ` } function buildSubpackSlipBucketCheckItem(label: string): string { return `` } function buildSubpackSlipBucketCheckboxesHtml(): string { return `${buildSubpackSlipBucketCheckItem('全新桶')}${buildSubpackSlipBucketCheckItem('是否回收桶')}${buildSubpackSlipBucketCheckItem('桶循环')}` } function buildSubpackSlipSlipNoDisplayHtml(): string { return `{{subpackSlipNo}} ` } /** 基本信息列宽:日期略宽;下单人/经手人等宽(与 content_style nth-child 一致) */ const BASIC_INFO_FLEX_DATE = '1.1' const BASIC_INFO_FLEX_PERSON = '1' function buildSubpackSlipSlipNoRowHtml(): string { const spacer = (flex: string) => `
 
` return `
${spacer(BASIC_INFO_FLEX_DATE)}${spacer(BASIC_INFO_FLEX_DATE)}${spacer(BASIC_INFO_FLEX_PERSON)}
经手人: ${buildSubpackSlipSlipNoDisplayHtml()}  
` } function buildSubpackSlipBasicInfoRowHtml(): string { const field = (label: string, placeholder: string, flex: string = BASIC_INFO_FLEX_PERSON) => `
${label} ${placeholder} 
` return `
${field('下单日期:', '{{orderDate}}', BASIC_INFO_FLEX_DATE)} ${field('交货日期:', '{{deliveryDate}}', BASIC_INFO_FLEX_DATE)} ${field('下单人:', '{{orderUser}}', BASIC_INFO_FLEX_PERSON)} ${field('经手人:', '{{handlerName}}', BASIC_INFO_FLEX_PERSON)}
` } function buildSubpackSlipRemarkExtraInputHtml(): string { return `` } function buildSubpackSlipRemarkUnderlineGroupHtml(remarkContent: string = '{{remark}}'): string { return `${remarkContent} ${buildSubpackSlipRemarkExtraInputHtml()}` } function buildSubpackSlipRemarkRowHtml(): string { return `
备注: ${buildSubpackSlipRemarkUnderlineGroupHtml()}
${buildSubpackSlipBucketCheckboxesHtml()}
` } /** 修复旧模板中 label / 桶选项容器缺少 contenteditable="false" 导致 TinyMCE 内多选失效 */ function ensureSubpackSlipInteractiveMarkup(html: string): string { let result = html.replace(/]*\bsubpack-slip-check-item\b[^>]*)>/gi, (match, attrs: string) => { if (/contenteditable=/i.test(attrs)) return match return `` }) result = result.replace(/
]*)>/g, (match, attrs: string) => { if (/contenteditable=/i.test(attrs)) return match return `
` }) return result } /** @deprecated 分装单已移除右侧联次,保留空实现兼容旧引用 */ export function buildSubpackSlipSideLabelsHtml(): string { return '' } /** 生成带占位符的分装单 HTML 模板 */ export function generateSubpackSlipTemplate(_plan?: Plan): string { return `${SUBPACK_SLIP_STYLE_BLOCK}
logo
{{companyName}}
分 装 单
${buildSubpackSlipSlipNoRowHtml()} ${buildSubpackSlipBasicInfoRowHtml()} ${buildSubpackSlipProductHeaderRowHtml()} {{productTable}} ${buildSubpackSlipSignRowHtml()}
 
${buildSubpackSlipRemarkRowHtml()}
 
` } /** 移除印章、地址/电话行(分装单不再展示) */ function normalizeSubpackSlipRemoveAddressAndSeal(html: string): string { if (!html.includes('subpack-slip-doc')) return html let result = html.replace(/]*alt="印章"[^>]*>/gi, '') result = result.replace(/\{\{companySeal\}\}/g, '') result = result.replace(/\s*/g, '') result = result.replace(/\s*/g, '') result = result.replace(/\s*/g, '') result = result.replace(/
/g, '') result = result.replace(/
/g, '') result = result.replace( /\s*]*>\s*
\s*/g, '' ) result = result.replace( /\s*]*>\s*
\s*/g, '' ) result = result.replace( /\s*]*>\s* \s*<\/td>\s*]*>\s*
\s*/g, '' ) result = result.replace( /\s*\s*/g, '' ) result = result.replace(/]*>[\s\S]*?<\/td>/g, '') if (result.includes('subpack-slip-outer-table') && result.includes('subpack-slip-main-col')) { result = result.replace( /\s*\s*[\s\S]*?<\/tr>\s*[\s\S]*?<\/tr>/, `\n ${basicInfoRow}` ) if (replaced !== html) return replaced const fallback = html.replace( /\s*\s*
/, '' ) result = result.replace(/<\/td>\s*<\/tr>\s*<\/table>(?=\s*<\/div>)/, '') } return result } /** 基本信息合并为一行四列(兼容旧版两行布局) */ function normalizeSubpackSlipBasicInfoOneRow(html: string): string { if (!html.includes('subpack-slip-doc')) return html if (html.includes('subpack-slip-basic-info-row')) return html const basicInfoRow = buildSubpackSlipBasicInfoRowHtml() const replaced = html.replace( /\s*
]*>\s*
]*>\s*]*>下单日期:[\s\S]*?<\/tr>\s*
]*>\s*
]*>\s*]*>下单人:[\s\S]*?<\/tr>/, basicInfoRow ) return fallback } /** 分装单号与经手人标签右缘对齐(兼容旧版标题区居中布局) */ function normalizeSubpackSlipSlipNoRow(html: string): string { if (!html.includes('subpack-slip-doc')) return html let result = html result = result.replace(/
\s*/g, '') result = result.replace(/]*>分装单号:<\/span>\s*/g, '') result = result.replace( /
]*>([\s\S]*?)<\/div>/g, '$1' ) if (result.includes('subpack-slip-title-section') && result.includes('subpack-slip-slip-no-display')) { result = result.replace( /\s*]*>[\s\S]*?<\/span>\s*(?=<\/div>\s*<\/div>\s*]*>[\s\S]*?<\/div>\s*(?=<\/div>\s*<\/div>\s*
[\s\S]*?<\/colgroup>\s*)/, `$1\n ${buildSubpackSlipSlipNoRowHtml()}\n\n ` ) } if (result.includes('subpack-slip-slip-no-row') && !result.includes('subpack-slip-slip-no-anchor')) { result = result.replace( /\s*/, `${buildSubpackSlipSlipNoRowHtml()}\n` ) } return ensureSubpackSlipInteractiveMarkup(result) } const SUBPACK_SLIP_CHECK_DELEGATE_ATTR = 'data-subpack-slip-check-delegate' const SUBPACK_SLIP_REMARK_EXTRA_BOUND_ATTR = 'data-subpack-slip-remark-extra-bound' function syncSubpackSlipRemarkExtraInputValue(input: HTMLInputElement): void { input.setAttribute('value', input.value) } /** 富文本编辑器内:允许备注补充输入框获得焦点并输入(TinyMCE 默认会拦截) */ export function bindSubpackSlipRemarkExtraInputInteraction( root: HTMLElement | null | undefined, onChange?: () => void ): void { if (!root) return root.querySelectorAll('input.subpack-slip-remark-extra-input').forEach((node) => { const input = node as HTMLInputElement const wrap = input.closest('.subpack-slip-remark-extra-wrap') as HTMLElement | null if (wrap) { wrap.setAttribute('contenteditable', 'false') } input.removeAttribute('readonly') input.removeAttribute('disabled') syncSubpackSlipRemarkExtraInputValue(input) if (input.getAttribute(SUBPACK_SLIP_REMARK_EXTRA_BOUND_ATTR) === '1') return input.setAttribute(SUBPACK_SLIP_REMARK_EXTRA_BOUND_ATTR, '1') const stopBubble = (e: Event) => { e.stopPropagation() } input.addEventListener('mousedown', stopBubble, true) input.addEventListener('click', stopBubble, true) input.addEventListener('keydown', stopBubble, true) input.addEventListener('keyup', stopBubble, true) input.addEventListener('keypress', stopBubble, true) input.addEventListener('focus', stopBubble, true) input.addEventListener('input', () => { syncSubpackSlipRemarkExtraInputValue(input) onChange?.() }) input.addEventListener('blur', () => { syncSubpackSlipRemarkExtraInputValue(input) onChange?.() }) }) } function syncSubpackSlipCheckboxInput(input: HTMLInputElement): void { if (input.checked) { input.setAttribute('checked', 'checked') } else { input.removeAttribute('checked') } } /** 富文本编辑器内:绑定多选框点击(TinyMCE 默认可编辑区会拦截原生 checkbox) */ export function bindSubpackSlipCheckboxInteraction( root: HTMLElement | null | undefined, onChange?: () => void ): void { if (!root) return root.querySelectorAll('label.subpack-slip-check-item').forEach((node) => { const label = node as HTMLElement label.setAttribute('contenteditable', 'false') const input = label.querySelector('input.subpack-slip-check-input') as HTMLInputElement | null if (input) syncSubpackSlipCheckboxInput(input) }) if (root.getAttribute(SUBPACK_SLIP_CHECK_DELEGATE_ATTR) === '1') return root.setAttribute(SUBPACK_SLIP_CHECK_DELEGATE_ATTR, '1') root.addEventListener( 'mousedown', (event) => { if (root.isContentEditable === false) return const target = event.target as HTMLElement | null if (!target) return const label = target.closest('label.subpack-slip-check-item') if (!label || !root.contains(label)) return const input = label.querySelector('input.subpack-slip-check-input') as HTMLInputElement | null if (!input) return event.preventDefault() event.stopPropagation() input.checked = !input.checked syncSubpackSlipCheckboxInput(input) onChange?.() }, true ) } /** 分装单 TinyMCE:初始化与内容变更时绑定多选项与备注输入框交互 */ export function setupSubpackSlipEditorCheckboxes(editor: { getBody: () => HTMLElement on: (event: string, callback: (...args: unknown[]) => void) => void fire: (event: string) => void }): void { const bind = () => { const body = editor.getBody() bindSubpackSlipCheckboxInteraction(body, () => editor.fire('change')) bindSubpackSlipRemarkExtraInputInteraction(body, () => editor.fire('change')) } const syncRemarkExtraValues = () => { editor.getBody().querySelectorAll('input.subpack-slip-remark-extra-input').forEach((node) => { syncSubpackSlipRemarkExtraInputValue(node as HTMLInputElement) }) } editor.on('init', () => { window.setTimeout(bind, 0) }) editor.on('SetContent', () => { window.setTimeout(bind, 0) }) editor.on('focus', () => { window.setTimeout(bind, 0) }) editor.on('BeforeGetContent', () => { syncRemarkExtraValues() }) } /** @deprecated 兼容旧引用,请使用 setupSubpackSlipEditorCheckboxes */ export const setupSubpackSlipEditorTypeCheckboxes = setupSubpackSlipEditorCheckboxes /** @deprecated 兼容旧引用,请使用 normalizeSubpackSlipRemoveSideLabels */ function normalizeSubpackSlipSideLabelsPlaceholder(html: string): string { return normalizeSubpackSlipRemoveSideLabels(html) } /** 将占位符替换为计划最新数据 */ export function replaceSubpackSlipPlaceholders( htmlContent: string, plan: Plan, ctx?: SubpackSlipRenderContext ): string { if (!htmlContent || !plan) return htmlContent const orderDate = ctx?.orderDate || formatDateIso() const orderUser = ctx?.orderUser || '' const preparerUser = ctx?.preparerUser || orderUser const replacements: Record = { '{{companyName}}': SUBPACK_SLIP_DEFAULTS.companyName, '{{subpackSlipNo}}': ctx?.subpackSlipNo || '', '{{orderDate}}': orderDate, '{{deliveryDate}}': '', '{{orderUser}}': orderUser, '{{preparerUser}}': preparerUser, '{{handlerName}}': '', '{{receivingUnit}}': orderDate, '{{planNo}}': '', '{{receiveContact}}': orderUser, '{{driverInfo}}': '', '{{planDateChinese}}': '', '{{productTable}}': buildProductRows(plan.products, ctx), '{{remark}}': plan.remark || '', '{{remarkExtra}}': '', '{{companyAddress}}': '', '{{companyPhone}}': '', '{{contactPhone}}': '', '{{companySeal}}': '', '{{sideLabels}}': '' } let result = normalizeSubpackSlipDocLayout( normalizeSubpackSlipRemoveAddressAndSeal( normalizeSubpackSlipSignRow( normalizeSubpackSlipTermsCell( normalizeSubpackSlipRemoveTermsRow( normalizeSubpackSlipGapBeforeSign( normalizeSubpackSlipProductTableFooter( normalizeSubpackSlipRemarkRow( normalizeSubpackSlipProductGridCell( normalizeSubpackSlipProductHeader( normalizeSubpackSlipGapBeforeProduct( normalizeSubpackSlipSlipNoRow( normalizeSubpackSlipBasicInfoOneRow( normalizeSubpackSlipBasicInfoFields( normalizeSubpackSlipRemoveSideLabels(htmlContent) ) ) ) ) ) ) ) ) ) ) ) ) ) ) for (const [placeholder, value] of Object.entries(replacements)) { result = result.split(placeholder).join(value) } if (ctx?.subpackSlipNo) { result = result.replace( /(]*>)[\s\S]*?( <\/span>)/g, `$1${ctx.subpackSlipNo}$2` ) } if (preparerUser) { result = result.replace( /(制单人:<\/span>\s*]*>)[\s\S]*?(<\/span>)/g, `$1${preparerUser}$2` ) } return result } /** 保存模板前:将动态字段还原为占位符 */ export function restoreSubpackSlipPlaceholders(template: string, plan: Plan): string { let result = template const dynamicReplacements: Record = { '{{companyName}}': SUBPACK_SLIP_DEFAULTS.companyName, '{{orderDate}}': formatDateIso(), '{{remark}}': plan.remark || '', '{{receivingUnit}}': formatDateIso(), '{{planDateChinese}}': formatPlanDateChinese(plan.planDate) } 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(/NO:SXFZ\d{8}-\d{3}/g, '{{subpackSlipNo}}') result = result.replace(/SXFZ\d{8}-\d{3}/g, '{{subpackSlipNo}}') result = result.replace(/SXFZ-\d{4}-\d{2}-\d{2}-\d{3}/g, '{{subpackSlipNo}}') result = result.replace(/NO:SXFZ-\d{4}-\d{2}-\d{2}-\d{3}/g, '{{subpackSlipNo}}') result = result.replace( /(]*>)[^<]*(?= <\/span>)/, '$1{{subpackSlipNo}}' ) result = result.replace( /(
]*>)[^<]*(?= <\/div>)/, '$1{{subpackSlipNo}}' ) result = result.replace( /(]*>)[^<]*(?= <\/span>)/, '$1{{subpackSlipNo}}' ) result = result.replace( /(]*>下单日期:<\/span>\s*]*>)[^<]*(?= <\/span>)/, '$1{{orderDate}}' ) result = result.replace( /(]*>下单人:<\/span>\s*]*>)[^<]*(?= <\/span>)/, '$1{{orderUser}}' ) result = result.replace( /(]*>交货日期:<\/span>\s*]*>)[^<]*(?= <\/span>)/, '$1{{deliveryDate}}' ) result = result.replace( /(]*>经手人:<\/span>\s*]*>)[^<]*(?= <\/span>)/, '$1{{handlerName}}' ) result = result.replace( /(制单人:<\/span>\s*]*>)[^<]*(?=<\/span>)/, '$1{{preparerUser}}' ) // 产品明细行还原为占位符(表头「是否完成」与「备注」行之间) result = result.replace( /(]*>是否完成<\/th>\s*<\/tr>\s*)([\s\S]*?)(\s*
\s*\s*
]*>\s*(?:
|备注:))/, '$1{{productTable}}$3' ) result = result.replace( /(]*>实收数量<\/th>\s*<\/tr>\s*)([\s\S]*?)(\s*
]*>\s*(?:
|备注:))/, '$1{{productTable}}$3' ) // 移除右侧联次、印章与地址/电话行 result = normalizeSubpackSlipRemoveSideLabels(result) result = normalizeSubpackSlipRemoveAddressAndSeal(result) return result }