| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273 |
- import type { Plan, PlanProduct } from '@/api/pl'
- import { formatRemarkForSlipHtml } from '@/utils/pl-batch-print'
- 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:18px;'
- 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 = 36
- const LOGO_LEFT = 50
- const TITLE_STYLE = `${FONT_TITLE}font-weight:700 !important;letter-spacing:6px;line-height:1.35;`
- const TITLE_SUB_STYLE = `${TITLE_STYLE}margin-top:2px;letter-spacing:10px;`
- const TABLE_ROW_PADDING = '4px'
- /** 信息表等宽列 */
- const INFO_COLGROUP = `<colgroup>
- <col span="7" style="width:14.28%;" />
- </colgroup>`
- /**
- * 物料表列宽:加宽名称/型号,收窄包装规格/单位。
- * 序号 / 产品名称 / 产品型号 / 包装规格 / 单位 / 数量 / 是否完成
- */
- const PRODUCT_COLGROUP = `<colgroup>
- <col style="width:8%;" />
- <col style="width:26%;" />
- <col style="width:18%;" />
- <col style="width:10%;" />
- <col style="width:7%;" />
- <col style="width:15%;" />
- <col style="width:16%;" />
- </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,定位在地址单元格右上角(相对原位置向上 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 = `<span class="subpack-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
- /** 签字区下划线最小宽度 */
- const SUBPACK_SLIP_SIGN_VALUE_MIN_WIDTH = '120px'
- function buildSubpackSlipSignItem(label: string, valueContent: string, minWidth: string = SUBPACK_SLIP_SIGN_VALUE_MIN_WIDTH): string {
- return `<span class="subpack-slip-sign-item"><span class="subpack-slip-sign-label">${label}</span><span class="subpack-slip-sign-value" style="min-width:${minWidth};">${valueContent}</span></span>`
- }
- function buildSubpackSlipSignRowHtml(): string {
- return `<tr class="subpack-slip-sign-row">
- <td colspan="7" class="subpack-slip-sign-row-cell" style="border:none !important;padding:${TABLE_ROW_PADDING};text-align:center;${FONT_SIGN}background:transparent !important;">
- <div class="subpack-slip-sign-inner">
- ${buildSubpackSlipSignItem('制单人:', '{{preparerUser}} ')}
- ${buildSubpackSlipSignItem('审核:', ' ')}
- ${buildSubpackSlipSignItem('生产主管:', ' ')}
- </div>
- </td>
- </tr>`
- }
- 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<Plan, 'planType' | 'transportStatus'> | 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(/<div class="subpack-slip-product-cell-inner"[^>]*>([\s\S]*?)<\/div>/gi, '$1')
- .replace(/<div class="subpack-slip-pdf-grid-inner"[^>]*>([\s\S]*?)<\/div>/gi, '$1')
- .replace(/<span class="subpack-slip-pdf-grid-text"[^>]*>([\s\S]*?)<\/span>/gi, '$1')
- .replace(/<span class="subpack-slip-product-grid-align"[^>]*>([\s\S]*?)<\/span>/gi, '$1')
- .replace(/^<p[^>]*>([\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 `<label class="subpack-slip-check-item subpack-slip-complete-check-item" contenteditable="false"><input type="checkbox" class="subpack-slip-check-input" />完成</label>`
- }
- 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 `<tr>
- <td class="subpack-slip-product-cell subpack-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">${formatSubpackSlipProductGridCellContent(String(rowIndex))}</td>
- <td class="subpack-slip-product-cell subpack-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">${formatSubpackSlipProductGridCellContent(product.productName || '')}</td>
- <td class="subpack-slip-product-cell subpack-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">${formatSubpackSlipProductGridCellContent(product.model || '')}</td>
- <td class="subpack-slip-product-cell subpack-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">${formatSubpackSlipProductGridCellContent(product.packageSpec || '')}</td>
- <td class="subpack-slip-product-cell subpack-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">${formatSubpackSlipProductGridCellContent(resolveSlipUnitLabel(product.unit, ctx))}</td>
- <td class="subpack-slip-product-cell subpack-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">${formatSubpackSlipProductGridCellContent(formatQty(qty))}</td>
- <td class="subpack-slip-product-cell subpack-slip-product-grid-cell subpack-slip-complete-cell" style="${PRODUCT_GRID_CELL}">${buildSubpackSlipCompleteCheckHtml()}</td>
- </tr>`
- }
- return `<tr>
- <td class="subpack-slip-product-cell subpack-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">${formatSubpackSlipProductGridCellContent(String(rowIndex))}</td>
- <td class="subpack-slip-product-cell subpack-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">${formatSubpackSlipProductGridCellContent('')}</td>
- <td class="subpack-slip-product-cell subpack-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">${formatSubpackSlipProductGridCellContent('')}</td>
- <td class="subpack-slip-product-cell subpack-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">${formatSubpackSlipProductGridCellContent('')}</td>
- <td class="subpack-slip-product-cell subpack-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">${formatSubpackSlipProductGridCellContent('')}</td>
- <td class="subpack-slip-product-cell subpack-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">${formatSubpackSlipProductGridCellContent('')}</td>
- <td class="subpack-slip-product-cell subpack-slip-product-grid-cell subpack-slip-complete-cell" style="${PRODUCT_GRID_CELL}"> </td>
- </tr>`
- }
- 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,
- .subpack-slip-doc .subpack-slip-section-gap.subpack-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;
- }
- .subpack-slip-doc table.subpack-slip-layout-table tr.subpack-slip-gap-before-sign td.subpack-slip-gap-before-sign-cell,
- .subpack-slip-doc .subpack-slip-section-gap.subpack-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;
- }
- .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: 18px !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;
- }
- .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),
- .subpack-slip-doc table.subpack-slip-info-table td,
- .subpack-slip-doc table.subpack-slip-info-table th,
- .subpack-slip-doc table.subpack-slip-sign-table td,
- .subpack-slip-doc table.subpack-slip-sign-table th {
- border: none !important;
- background: transparent !important;
- padding: ${TABLE_ROW_PADDING} !important;
- }
- .subpack-slip-doc table.subpack-slip-layout-table,
- .subpack-slip-doc table.subpack-slip-info-table,
- .subpack-slip-doc table.subpack-slip-product-table,
- .subpack-slip-doc table.subpack-slip-footer-table,
- .subpack-slip-doc table.subpack-slip-sign-table {
- border-collapse: collapse !important;
- border-spacing: 0 !important;
- table-layout: fixed !important;
- width: 100% !important;
- }
- .subpack-slip-doc .subpack-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;
- }
- .subpack-slip-doc th.subpack-slip-product-grid-cell {
- padding: ${PRODUCT_GRID_TH_PADDING};
- 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;
- }
- .subpack-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;
- }
- .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 = `<style type="text/css">${SUBPACK_SLIP_PRINT_CSS}</style>`
- /** 生成公司印章 HTML(地址/电话单元格右上角) */
- export function buildSubpackSlipCompanySealHtml(): string {
- return `<img src="${SUBPACK_SLIP_SEAL_IMAGE_SRC}" alt="印章" style="${SUBPACK_SLIP_SEAL_IMAGE_STYLE}" />`
- }
- 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 `<tr class="subpack-slip-product-header">
- <th class="subpack-slip-product-cell subpack-slip-product-grid-cell" style="${PRODUCT_TH}">序号</th>
- <th class="subpack-slip-product-cell subpack-slip-product-grid-cell" style="${PRODUCT_TH}">产品名称</th>
- <th class="subpack-slip-product-cell subpack-slip-product-grid-cell" style="${PRODUCT_TH}">产品型号</th>
- <th class="subpack-slip-product-cell subpack-slip-product-grid-cell" style="${PRODUCT_TH}">包装规格</th>
- <th class="subpack-slip-product-cell subpack-slip-product-grid-cell" style="${PRODUCT_TH}">单位</th>
- <th class="subpack-slip-product-cell subpack-slip-product-grid-cell" style="${PRODUCT_TH}">数量</th>
- <th class="subpack-slip-product-cell subpack-slip-product-grid-cell" style="${PRODUCT_TH}">是否完成</th>
- </tr>`
- }
- function buildSubpackSlipBucketCheckItem(label: string): string {
- return `<label class="subpack-slip-check-item subpack-slip-bucket-check-item" contenteditable="false"><input type="checkbox" class="subpack-slip-check-input" />${label}</label>`
- }
- function buildSubpackSlipBucketCheckboxesHtml(): string {
- return `${buildSubpackSlipBucketCheckItem('全新桶')}${buildSubpackSlipBucketCheckItem('是否回收桶')}${buildSubpackSlipBucketCheckItem('桶循环')}`
- }
- function buildSubpackSlipSlipNoDisplayHtml(): string {
- return `<span class="subpack-slip-slip-no-display">{{subpackSlipNo}} </span>`
- }
- /** 基本信息列宽:日期略宽;下单人/经手人等宽(与 content_style nth-child 一致) */
- const BASIC_INFO_FLEX_DATE = '1.1'
- const BASIC_INFO_FLEX_PERSON = '1'
- function buildSubpackSlipSlipNoRowHtml(): string {
- const spacer = (flex: string) =>
- `<div class="subpack-slip-basic-info-item subpack-slip-basic-info-spacer" style="flex:${flex};min-width:0;"> </div>`
- return `<tr class="subpack-slip-slip-no-row">
- <td colspan="7" style="${INFO_CELL_LEFT}">
- <div class="subpack-slip-basic-info-inner" style="display:flex;flex-wrap:wrap;justify-content:space-between;align-items:flex-end;gap:8px 12px;width:100%;box-sizing:border-box;">
- ${spacer(BASIC_INFO_FLEX_DATE)}${spacer(BASIC_INFO_FLEX_DATE)}${spacer(BASIC_INFO_FLEX_PERSON)}
- <div class="subpack-slip-info-row subpack-slip-basic-info-item subpack-slip-slip-no-field" style="${INFO_ROW}flex:${BASIC_INFO_FLEX_PERSON};min-width:0;">
- <span class="subpack-slip-info-label subpack-slip-slip-no-anchor" style="${INFO_LABEL}">
- <span class="subpack-slip-slip-no-anchor-text">经手人:</span>
- ${buildSubpackSlipSlipNoDisplayHtml()}
- </span>
- <span class="subpack-slip-info-value-left" style="${INFO_VALUE_INLINE_LEFT}"> </span>
- </div>
- </div>
- </td>
- </tr>`
- }
- function buildSubpackSlipBasicInfoRowHtml(): string {
- const field = (label: string, placeholder: string, flex: string = BASIC_INFO_FLEX_PERSON) =>
- `<div class="subpack-slip-info-row subpack-slip-basic-info-item" style="${INFO_ROW}flex:${flex};min-width:0;">
- <span class="subpack-slip-info-label" style="${INFO_LABEL}">${label}</span>
- <span class="subpack-slip-info-value-left" style="${INFO_VALUE_INLINE_LEFT}">${placeholder} </span>
- </div>`
- return `<tr class="subpack-slip-basic-info-row">
- <td colspan="7" style="${INFO_CELL_LEFT}">
- <div class="subpack-slip-basic-info-inner" style="display:flex;flex-wrap:wrap;justify-content:space-between;align-items:flex-end;gap:8px 12px;width:100%;box-sizing:border-box;">
- ${field('下单日期:', '{{orderDate}}', BASIC_INFO_FLEX_DATE)}
- ${field('交货日期:', '{{deliveryDate}}', BASIC_INFO_FLEX_DATE)}
- ${field('下单人:', '{{orderUser}}', BASIC_INFO_FLEX_PERSON)}
- ${field('经手人:', '{{handlerName}}', BASIC_INFO_FLEX_PERSON)}
- </div>
- </td>
- </tr>`
- }
- function buildSubpackSlipRemarkExtraInputHtml(): string {
- return `<span class="subpack-slip-remark-extra-wrap" contenteditable="false"><input type="text" class="subpack-slip-remark-extra-input" value="{{remarkExtra}}" /></span>`
- }
- function buildSubpackSlipRemarkUnderlineGroupHtml(remarkContent: string = '{{remark}}'): string {
- return `<span class="subpack-slip-remark-underline-group"><span class="subpack-slip-remark-value">${remarkContent} </span>${buildSubpackSlipRemarkExtraInputHtml()}</span>`
- }
- function buildSubpackSlipRemarkRowHtml(): string {
- return `<div class="subpack-slip-remark-row-inner">
- <span class="subpack-slip-remark-label">备注:</span>
- ${buildSubpackSlipRemarkUnderlineGroupHtml()}
- <div class="subpack-slip-bucket-check-row" contenteditable="false" style="${SUBPACK_SLIP_BUCKET_CHECK_INNER_STYLE}">${buildSubpackSlipBucketCheckboxesHtml()}</div>
- </div>`
- }
- /** 修复旧模板中 label / 桶选项容器缺少 contenteditable="false" 导致 TinyMCE 内多选失效 */
- function ensureSubpackSlipInteractiveMarkup(html: string): string {
- let result = html.replace(/<label\b([^>]*\bsubpack-slip-check-item\b[^>]*)>/gi, (match, attrs: string) => {
- if (/contenteditable=/i.test(attrs)) return match
- return `<label${attrs} contenteditable="false">`
- })
- result = result.replace(/<div class="subpack-slip-bucket-check-row"([^>]*)>/g, (match, attrs: string) => {
- if (/contenteditable=/i.test(attrs)) return match
- return `<div class="subpack-slip-bucket-check-row"${attrs} contenteditable="false">`
- })
- return result
- }
- /** @deprecated 分装单已移除右侧联次,保留空实现兼容旧引用 */
- export function buildSubpackSlipSideLabelsHtml(): string {
- return ''
- }
- /** 生成带占位符的分装单 HTML 模板(方案A:信息 / 物料 / 页脚+签字 分表) */
- export function generateSubpackSlipTemplate(_plan?: Plan): string {
- return `${SUBPACK_SLIP_STYLE_BLOCK}<div class="subpack-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="subpack-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="subpack-slip-title-heading" style="${TITLE_STYLE}">{{companyName}}</div>
- <div class="subpack-slip-title-heading" style="${TITLE_SUB_STYLE}">分 装 单</div>
- </div>
- </div>
- <!-- 1. 基本信息(独立表,不可拖宽高) -->
- <table class="subpack-slip-info-table" style="width:100%;border-collapse:collapse;table-layout:fixed;border:none !important;">
- ${INFO_COLGROUP}
- <!-- 分装单号(右缘与经手人标签右缘对齐) -->
- ${buildSubpackSlipSlipNoRowHtml()}
- <!-- 基本信息一行四列 -->
- ${buildSubpackSlipBasicInfoRowHtml()}
- </table>
- <div class="subpack-slip-section-gap subpack-slip-gap-before-product" style="${GAP_BEFORE_PRODUCT_CELL}"> </div>
- <!-- 2. 物料明细(含备注+桶选项) -->
- <table class="subpack-slip-product-table" style="width:100%;border-collapse:collapse;table-layout:fixed;border:none !important;">
- ${PRODUCT_COLGROUP}
- ${buildSubpackSlipProductHeaderRowHtml()}
- {{productTable}}
- <tr>
- <td colspan="7" class="subpack-slip-product-cell subpack-slip-remark-cell" style="${PRODUCT_TABLE_CELL_LEFT}">
- ${buildSubpackSlipRemarkRowHtml()}
- </td>
- </tr>
- </table>
- <div class="subpack-slip-section-gap subpack-slip-gap-before-sign" style="${GAP_BEFORE_SIGN_CELL}"> </div>
- <!-- 3. 签字区(独立表) -->
- <table class="subpack-slip-sign-table" style="width:100%;border-collapse:collapse;table-layout:fixed;border:none !important;">
- ${buildSubpackSlipSignRowHtml()}
- </table>
- </div>`
- }
- /** 移除印章、地址/电话行(分装单不再展示) */
- function normalizeSubpackSlipRemoveAddressAndSeal(html: string): string {
- if (!html.includes('subpack-slip-doc')) return html
- let result = html.replace(/<img[^>]*alt="印章"[^>]*>/gi, '')
- result = result.replace(/\{\{companySeal\}\}/g, '')
- result = result.replace(/<!--\s*印章图片\s*-->\s*/g, '')
- result = result.replace(/<!--\s*地址\/电话[\s\S]*?-->\s*/g, '')
- result = result.replace(/<!--\s*地址\/印章[\s\S]*?-->\s*/g, '')
- result = result.replace(/<div class="subpack-slip-address-block"[\s\S]*?<\/div>/g, '')
- result = result.replace(/<div class="subpack-slip-seal-block"[\s\S]*?<\/div>/g, '')
- result = result.replace(
- /<tr>\s*<td colspan="7"[^>]*>\s*<div class="subpack-slip-address-block"[\s\S]*?<\/tr>\s*/g,
- ''
- )
- result = result.replace(
- /<tr>\s*<td colspan="7"[^>]*>\s*<div class="subpack-slip-seal-block"[\s\S]*?<\/tr>\s*/g,
- ''
- )
- result = result.replace(
- /<tr>\s*<td colspan="4"[^>]*>\s* \s*<\/td>\s*<td colspan="3"[^>]*>\s*<div class="subpack-slip-seal-block"[\s\S]*?<\/tr>\s*/g,
- ''
- )
- result = result.replace(
- /<tr>\s*<!--\s*地址[\s\S]*?<\/tr>\s*/g,
- ''
- )
- result = result.replace(
- /<tr>\s*<td colspan="7"[^>]*>[\s\S]*?地址:[\s\S]*?联系人电话:[\s\S]*?<\/tr>\s*/g,
- ''
- )
- return result.replace(/min-height:76px;/g, 'min-height:0;')
- }
- /** @deprecated 请使用 normalizeSubpackSlipRemoveAddressAndSeal */
- function normalizeSubpackSlipSealPlaceholder(html: string): string {
- return normalizeSubpackSlipRemoveAddressAndSeal(html)
- }
- 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 patchSubpackSlipProductGridCellAttrs(attrs: string): string {
- let next = attrs.includes('class="')
- ? attrs.replace(/class="([^"]*)"/, (_, cls: string) => {
- let updated = cls
- if (!updated.includes('subpack-slip-product-cell')) updated += ' subpack-slip-product-cell'
- if (!updated.includes('subpack-slip-product-grid-cell')) updated += ' subpack-slip-product-grid-cell'
- return `class="${updated.trim()}"`
- })
- : `${attrs} class="subpack-slip-product-cell subpack-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 normalizeSubpackSlipProductGridCell(html: string): string {
- if (!html.includes('subpack-slip-doc') || !html.includes('subpack-slip-product-cell')) return html
- let result = html.replace(/<tr([^>]*)>\s*(<th[\s\S]*?<\/th>\s*){7}\s*<\/tr>/g, (rowMatch) => {
- if (
- (!rowMatch.includes('物料编号') && !rowMatch.includes('序号')) ||
- rowMatch.includes('colspan')
- ) {
- return rowMatch
- }
- let row = rowMatch.includes('subpack-slip-product-header')
- ? rowMatch
- : rowMatch.replace('<tr', '<tr class="subpack-slip-product-header"')
- return row.replace(/<(th)([^>]*)>([\s\S]*?)<\/th>/g, (_m, tag, attrs, content) => {
- return `<${tag}${patchSubpackSlipProductGridCellAttrs(attrs)}>${formatSubpackSlipProductGridCellContent(content)}</${tag}>`
- })
- })
- result = result.replace(/<tr([^>]*)>\s*(<td(?![^>]*colspan)[^>]*>[\s\S]*?<\/td>\s*){7}\s*<\/tr>/g, (rowMatch) => {
- if (
- rowMatch.includes('备注') ||
- rowMatch.includes('1.本送货单') ||
- rowMatch.includes('制单人:') ||
- rowMatch.includes('仓库:') ||
- rowMatch.includes('subpack-slip-complete-check-item') ||
- 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}${patchSubpackSlipProductGridCellAttrs(attrs)}>${formatSubpackSlipProductGridCellContent(content)}</${tag}>`
- })
- })
- return normalizeSubpackSlipGridCellWrappers(result)
- }
- /** 去掉 TinyMCE / 历史包裹层,单元格内保留纯文本(对齐合同 PDF) */
- function normalizeSubpackSlipGridCellWrappers(html: string): string {
- if (!html.includes('subpack-slip-product-grid-cell')) return html
- const unwrap = (tag: string, attrs: string, content: string) =>
- `<${tag}${attrs}>${formatSubpackSlipProductGridCellContent(content)}</${tag}>`
- let result = html.replace(
- /<(td|th)([^>]*subpack-slip-product-grid-cell[^>]*)>\s*<span class="subpack-slip-product-grid-align"[^>]*>([\s\S]*?)<\/span>\s*<\/\1>/gi,
- (_m, tag, attrs, content) => unwrap(tag, attrs, content)
- )
- result = result.replace(
- /<(td|th)([^>]*subpack-slip-product-grid-cell[^>]*)>\s*<span class="subpack-slip-pdf-grid-text"[^>]*>([\s\S]*?)<\/span>\s*<\/\1>/gi,
- (_m, tag, attrs, content) => unwrap(tag, attrs, content)
- )
- result = result.replace(
- /<(td|th)([^>]*subpack-slip-product-grid-cell[^>]*)>\s*<div class="subpack-slip-pdf-grid-inner"[^>]*>([\s\S]*?)<\/div>\s*<\/\1>/gi,
- (_m, tag, attrs, content) => unwrap(tag, attrs, content)
- )
- result = result.replace(
- /<(td|th)([^>]*subpack-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)([^>]*subpack-slip-product-grid-cell[^>]*)>\s*<div(?![^>]*class="subpack-slip)[^>]*>([\s\S]*?)<\/div>\s*<\/\1>/gi,
- (_m, tag, attrs, content) => unwrap(tag, attrs, content)
- )
- result = result.replace(/<(td|th)([^>]*subpack-slip-product-grid-cell[^>]*)\s+height="[^"]*"/gi, '<$1$2')
- result = result.replace(/<(tr)([^>]*class="[^"]*subpack-slip-product[^"]*"[^>]*)\s+height="[^"]*"/gi, '<$1$2')
- return result
- }
- function extractSubpackSlipGridCellContent(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('subpack-slip-pdf-grid-inner') ||
- onlyChild.classList.contains('subpack-slip-pdf-grid-text')
- ) {
- content = onlyChild.innerHTML.trim()
- }
- }
- return formatSubpackSlipProductGridCellContent(content)
- }
- function applySubpackSlipPdfGridCellStyles(cell: HTMLTableCellElement, isHeader: boolean): void {
- const content = extractSubpackSlipGridCellContent(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 applySubpackSlipPdfFooterCellStyles(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 unwrapSubpackSlipPdfCellParagraph(cell: HTMLTableCellElement): void {
- const onlyChild = cell.firstElementChild
- if (onlyChild?.tagName === 'P' && cell.childElementCount === 1) {
- cell.innerHTML = onlyChild.innerHTML.trim() || ' '
- }
- }
- function prepareSubpackSlipPdfRemarkCell(cell: HTMLTableCellElement): void {
- unwrapSubpackSlipPdfCellParagraph(cell)
- applySubpackSlipPdfFooterCellStyles(cell)
- cell.querySelectorAll('.subpack-slip-remark-underline-group').forEach((node) => {
- const group = node as HTMLElement
- group.style.setProperty('display', 'inline-flex', 'important')
- group.style.setProperty('align-items', 'baseline', 'important')
- group.style.setProperty('flex', '1', 'important')
- group.style.setProperty('min-width', '200px', 'important')
- group.style.setProperty('max-width', '420px', 'important')
- group.style.setProperty('border', 'none', 'important')
- group.style.setProperty('border-bottom', INFO_UNDERLINE, 'important')
- // 与签字区一致:加大底部 padding,避免 html2canvas 把下划线叠在文字上
- group.style.setProperty('padding', SIGN_VALUE_PDF_PADDING, 'important')
- group.style.setProperty('gap', '0', 'important')
- group.style.setProperty('line-height', '1.35', 'important')
- group.style.setProperty('overflow', 'visible', 'important')
- group.style.setProperty('box-sizing', 'border-box', 'important')
- })
- cell.querySelectorAll('.subpack-slip-remark-value').forEach((node) => {
- const el = node as HTMLElement
- el.style.setProperty('display', 'inline-block', 'important')
- el.style.setProperty('border-bottom', 'none', 'important')
- el.style.setProperty('padding', '0 2px', 'important')
- el.style.setProperty('line-height', '1.35', 'important')
- el.style.setProperty('overflow', 'visible', 'important')
- el.style.setProperty('vertical-align', 'baseline', 'important')
- })
- cell.querySelectorAll('input.subpack-slip-remark-extra-input').forEach((node) => {
- const input = node as HTMLInputElement
- const text = (input.value || '').trim()
- const span = document.createElement('span')
- span.className = 'subpack-slip-remark-extra-text-pdf'
- span.textContent = text || '\u00a0'
- span.style.setProperty('display', 'inline-block', 'important')
- span.style.setProperty('min-width', '80px', 'important')
- span.style.setProperty('flex', '1', 'important')
- span.style.setProperty('padding', '0 2px', 'important')
- span.style.setProperty('border-bottom', 'none', 'important')
- span.style.setProperty('font-size', '20px', 'important')
- span.style.setProperty('line-height', '1.35', 'important')
- span.style.setProperty('vertical-align', 'baseline', 'important')
- input.replaceWith(span)
- })
- }
- function prepareSubpackSlipPdfTermsCell(cell: HTMLTableCellElement): void {
- applySubpackSlipPdfFooterCellStyles(cell)
- const terms = cell.querySelector('.subpack-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 prepareSubpackSlipPdfSealCell(cell: HTMLTableCellElement): void {
- applySubpackSlipPdfFooterCellStyles(cell)
- const block =
- (cell.querySelector('.subpack-slip-address-block') as HTMLElement | null) ||
- (cell.querySelector('.subpack-slip-seal-block') as HTMLElement | null)
- const lines = cell.querySelector('.subpack-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')
- block.style.setProperty('line-height', PRODUCT_FOOTER_PDF_LINE_HEIGHT, '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 prepareSubpackSlipPdfSignValues(root: HTMLElement): void {
- root.querySelectorAll('.subpack-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')
- el.style.setProperty('min-width', SUBPACK_SLIP_SIGN_VALUE_MIN_WIDTH, 'important')
- })
- }
- /** PDF 截图前:将原生 checkbox 替换为 canvas 方框图 + 文字(html2canvas 对 flex/SVG 对齐不稳定) */
- const subpackSlipPdfCheckboxImgCache: { empty?: string; checked?: string } = {}
- function buildSubpackSlipPdfCheckboxImgDataUrl(checked: boolean): string {
- const key = checked ? 'checked' : 'empty'
- if (subpackSlipPdfCheckboxImgCache[key]) return subpackSlipPdfCheckboxImgCache[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')
- subpackSlipPdfCheckboxImgCache[key] = url
- return url
- }
- function buildSubpackSlipPdfCheckItemHtml(checked: boolean, labelText: string, extraClass = ''): string {
- const imgSrc = buildSubpackSlipPdfCheckboxImgDataUrl(checked)
- const className = `subpack-slip-check-item-pdf${extraClass ? ` ${extraClass}` : ''}`
- if (!imgSrc) return `<span class="${className}">${labelText}</span>`
- return `<span class="${className}"><img class="subpack-slip-check-img-pdf" src="${imgSrc}" width="16" height="16" alt="" /><span class="subpack-slip-check-text-pdf">${labelText}</span></span>`
- }
- function applySubpackSlipPdfCheckboxLayout(root: HTMLElement): void {
- root.querySelectorAll('.subpack-slip-bucket-check-row').forEach((node) => {
- const row = node as HTMLElement
- row.style.setProperty('display', 'flex', 'important')
- row.style.setProperty('flex', '1', 'important')
- row.style.setProperty('justify-content', 'center', 'important')
- row.style.setProperty('align-items', 'center', 'important')
- row.style.setProperty('flex-wrap', 'wrap', 'important')
- row.style.setProperty('gap', '14px 18px', 'important')
- row.style.setProperty('line-height', '20px', 'important')
- row.style.setProperty('font-size', '20px', 'important')
- })
- root.querySelectorAll('.subpack-slip-remark-row-inner').forEach((node) => {
- const row = node as HTMLElement
- row.style.setProperty('display', 'flex', 'important')
- row.style.setProperty('align-items', 'center', 'important')
- row.style.setProperty('width', '100%', 'important')
- row.style.setProperty('gap', '12px', 'important')
- })
- root.querySelectorAll('.subpack-slip-check-item-pdf').forEach((node) => {
- const item = node as HTMLElement
- const isComplete = item.classList.contains('subpack-slip-complete-check-item-pdf')
- 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', isComplete ? '0' : '18px', 'important')
- item.style.setProperty('vertical-align', 'middle', 'important')
- const img = item.querySelector('img.subpack-slip-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('.subpack-slip-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 prepareSubpackSlipPdfCheckboxes(root: HTMLElement): void {
- root.querySelectorAll('label.subpack-slip-check-item').forEach((node) => {
- const label = node as HTMLElement
- const input = label.querySelector('input.subpack-slip-check-input') as HTMLInputElement | null
- const checked = input?.checked ?? false
- const labelText = (label.textContent || '').replace(/[✓☑□☐]/g, '').trim()
- const extraClass = label.classList.contains('subpack-slip-complete-check-item')
- ? 'subpack-slip-complete-check-item-pdf'
- : label.classList.contains('subpack-slip-bucket-check-item')
- ? 'subpack-slip-bucket-check-item-pdf'
- : ''
- const html = buildSubpackSlipPdfCheckItemHtml(checked, labelText, extraClass)
- const wrapper = document.createElement('div')
- wrapper.innerHTML = html.trim()
- const replacement = wrapper.firstElementChild
- if (replacement) {
- label.replaceWith(replacement)
- }
- })
- applySubpackSlipPdfCheckboxLayout(root)
- }
- /** PDF 截图前:物料网格 + 备注/条款/地址行 + 签字区统一样式 */
- export function prepareSubpackSlipPdfGridCells(root: HTMLElement): void {
- root.querySelectorAll('td.subpack-slip-product-grid-cell, th.subpack-slip-product-grid-cell').forEach((node) => {
- applySubpackSlipPdfGridCellStyles(node as HTMLTableCellElement, node.tagName === 'TH')
- })
- root.querySelectorAll('td.subpack-slip-terms-cell').forEach((node) => {
- prepareSubpackSlipPdfTermsCell(node as HTMLTableCellElement)
- })
- root.querySelectorAll('td.subpack-slip-product-cell:not(.subpack-slip-product-grid-cell)').forEach((node) => {
- const cell = node as HTMLTableCellElement
- if (cell.classList.contains('subpack-slip-terms-cell')) return
- if (cell.querySelector('.subpack-slip-address-block, .subpack-slip-seal-block')) {
- prepareSubpackSlipPdfSealCell(cell)
- return
- }
- if (/备注\s*[::]/.test(cell.textContent || '')) {
- prepareSubpackSlipPdfRemarkCell(cell)
- }
- })
- root.querySelectorAll('tr').forEach((row) => {
- if (!row.querySelector('.subpack-slip-product-cell')) return
- row.removeAttribute('height')
- row.style.removeProperty('height')
- row.style.removeProperty('min-height')
- })
- prepareSubpackSlipPdfSignValues(root)
- prepareSubpackSlipPdfCheckboxes(root)
- // 亚像素 border 在 Windows DPI 下易变粗;截图前改为 1px 背景单线
- applyPdfStableTableCellBorders(root.querySelectorAll('.subpack-slip-product-cell'))
- }
- /** 备注、条款、印章行:统一 0.3px 全边框(兼容已保存模板,与物料网格衔接) */
- function normalizeSubpackSlipProductTableFooter(html: string): string {
- if (!html.includes('subpack-slip-doc')) return html
- let result = html
- result = result.replace(/<td([^>]*colspan="7"[^>]*)>(\s*)备注:/g, (_match, attrs, space) => {
- const patched = patchProductFooterCellAttrs(
- attrs,
- 'subpack-slip-product-cell subpack-slip-remark-cell',
- PRODUCT_TABLE_CELL_LEFT
- )
- if (_match.includes('subpack-slip-remark-row-inner')) return _match
- return `<td${patched}>${space}${buildSubpackSlipRemarkRowHtml()}`
- })
- result = result.replace(
- /<td([^>]*colspan="4"[^>]*)>\s*(<div class="subpack-slip-terms-text"|\s*1\.本送货单)/g,
- (_match, attrs, contentStart) => {
- const patched = patchProductFooterCellAttrs(
- attrs,
- 'subpack-slip-product-cell subpack-slip-terms-cell',
- PRODUCT_TERMS_CELL_LEFT
- )
- return `<td${patched}>\n ${contentStart}`
- }
- )
- result = result.replace(
- /<td([^>]*colspan="3"[^>]*)>\s*<div class="subpack-slip-seal-block"/g,
- (_match, attrs) => {
- const patched = patchProductFooterCellAttrs(attrs, 'subpack-slip-product-cell', PRODUCT_TABLE_CELL_LEFT)
- return `<td${patched}>\n <div class="subpack-slip-seal-block"`
- }
- )
- return result
- }
- /** 条款单元格行高随内容(兼容已保存模板) */
- function normalizeSubpackSlipTermsCell(html: string): string {
- if (!html.includes('subpack-slip-doc')) return html
- const patchTermsTd = (attrs: string) => {
- let nextAttrs = attrs
- if (nextAttrs.includes('class="')) {
- nextAttrs = nextAttrs.replace(/class="([^"]*)"/, (_, cls: string) =>
- cls.includes('subpack-slip-terms-cell') ? `class="${cls}"` : `class="${cls} subpack-slip-terms-cell"`
- )
- } else {
- nextAttrs = `${nextAttrs} class="subpack-slip-product-cell subpack-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="4"([^>]*)>\s*<div class="subpack-slip-terms-text"/g,
- (match, attrs) => {
- if (attrs.includes('subpack-slip-terms-cell') && attrs.includes('border:0.3px solid #000')) return match
- return `<td colspan="4"${patchTermsTd(attrs)}>\n <div class="subpack-slip-terms-text"`
- }
- )
- result = result.replace(
- /<div class="subpack-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="subpack-slip-seal-block" style="${nextStyle}"`
- }
- )
- return result
- }
- /** 移除旧版送货条款行(兼容已保存模板) */
- function normalizeSubpackSlipRemoveTermsRow(html: string): string {
- if (!html.includes('subpack-slip-doc')) return html
- if (!html.includes('1.本送货单') && !html.includes('subpack-slip-terms-text')) return html
- let result = html.replace(
- /<td colspan="4"([^>]*class="[^"]*subpack-slip-terms-cell[^"]*"[^>]*)>[\s\S]*?<\/td>/g,
- `<td colspan="4" class="subpack-slip-product-cell" style="${PRODUCT_TABLE_CELL_LEFT}"> </td>`
- )
- result = result.replace(
- /<td colspan="4"([^>]*)>\s*(<div class="subpack-slip-terms-text"[\s\S]*?<\/div>|1\.本送货单[\s\S]*?)<\/td>/g,
- `<td colspan="4" class="subpack-slip-product-cell" style="${PRODUCT_TABLE_CELL_LEFT}"> </td>`
- )
- return result
- }
- /** 刷新已有间距行的 inline 样式(TinyMCE 保存后可能丢失) */
- function refreshSubpackSlipGapBeforeProduct(html: string): string {
- return html
- .replace(
- /<tr class="subpack-slip-gap-before-product">[\s\S]*?<\/tr>/,
- `<tr class="subpack-slip-gap-before-product">\n <td colspan="7" class="subpack-slip-gap-before-product-cell" style="${GAP_BEFORE_PRODUCT_CELL}"> </td>\n </tr>`
- )
- .replace(
- /<div class="subpack-slip-section-gap subpack-slip-gap-before-product"[^>]*>[\s\S]*?<\/div>/,
- `<div class="subpack-slip-section-gap subpack-slip-gap-before-product" style="${GAP_BEFORE_PRODUCT_CELL}"> </div>`
- )
- }
- /** 在收货联系方式行与物料表之间插入间距行(兼容已保存模板) */
- function normalizeSubpackSlipGapBeforeProduct(html: string): string {
- if (!html.includes('subpack-slip-doc')) {
- return html
- }
- if (html.includes('subpack-slip-gap-before-product')) {
- return refreshSubpackSlipGapBeforeProduct(html)
- }
- const gapRow = `<tr class="subpack-slip-gap-before-product"><td colspan="7" class="subpack-slip-gap-before-product-cell" style="${GAP_BEFORE_PRODUCT_CELL}"> </td></tr>\n `
- const withComment = html.replace(
- /(<span class="subpack-slip-info-label"[^>]*>经(?: )?手人:<\/span>[\s\S]*?<\/tr>\s*)(<!--\s*中:产品明细表[\s\S]*?<tr[^>]*>\s*<th[^>]*>(?:序号|物料编号))/,
- `$1${gapRow}$2`
- )
- if (withComment !== html) return withComment
- const withHeader = html.replace(
- /(<span class="subpack-slip-info-label"[^>]*>经(?: )?手人:<\/span>[\s\S]*?<\/tr>\s*)(<tr[^>]*>\s*<th[^>]*>(?:序号|物料编号))/,
- `$1${gapRow}$2`
- )
- if (withHeader !== html) return withHeader
- return html.replace(
- /(下单人:[\s\S]*?<\/tr>\s*)(<!--\s*中:产品明细表[\s\S]*?<tr[^>]*>\s*<th[^>]*>(?:序号|物料编号)|<tr[^>]*>\s*<th[^>]*>(?:序号|物料编号))/,
- `$1${gapRow}$2`
- )
- }
- /** 刷新签字区上方间距行 inline 样式 */
- function refreshSubpackSlipGapBeforeSign(html: string): string {
- return html
- .replace(
- /<tr class="subpack-slip-gap-before-sign">[\s\S]*?<\/tr>/,
- `<tr class="subpack-slip-gap-before-sign">\n <td colspan="7" class="subpack-slip-gap-before-sign-cell" style="${GAP_BEFORE_SIGN_CELL}"> </td>\n </tr>`
- )
- .replace(
- /<div class="subpack-slip-section-gap subpack-slip-gap-before-sign"[^>]*>[\s\S]*?<\/div>/,
- `<div class="subpack-slip-section-gap subpack-slip-gap-before-sign" style="${GAP_BEFORE_SIGN_CELL}"> </div>`
- )
- }
- /** 在物料表与签字区之间插入间距行(兼容已保存模板) */
- function normalizeSubpackSlipGapBeforeSign(html: string): string {
- if (!html.includes('subpack-slip-doc')) return html
- if (html.includes('subpack-slip-gap-before-sign')) {
- return refreshSubpackSlipGapBeforeSign(html)
- }
- const gapRow = `<tr class="subpack-slip-gap-before-sign"><td colspan="7" class="subpack-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="7"[^>]*>\s*(?:<div class="subpack-slip-sign-inner">\s*)?制单人:)/,
- `$1${gapRow}$2`
- )
- if (withComment !== html) return withComment
- const withLegacy = html.replace(
- /(<\/tr>\s*)(<!--\s*下:签字区[\s\S]*?<tr[^>]*>\s*<td colspan="7"[^>]*>\s*(?:<div class="subpack-slip-sign-inner">\s*)?仓库:)/,
- `$1${gapRow}$2`
- )
- if (withLegacy !== html) return withLegacy
- return html.replace(
- /(subpack-slip-remark-cell[\s\S]*?<\/tr>\s*)(<tr[^>]*>\s*<td colspan="7"[^>]*>\s*(?:<div class="subpack-slip-sign-inner">\s*)?(?:制单人:|仓库:))/,
- `$1${gapRow}$2`
- )
- }
- /** 签字区标签/下划线 baseline 对齐(兼容已保存模板) */
- function normalizeSubpackSlipSignLabelMarkup(html: string): string {
- if (!html.includes('subpack-slip-sign-value')) return html
- let result = html.replace(
- /<span class="subpack-slip-sign-item">(制单人:|审核:|生产主管:|仓库:|客户签收:|司机\/送货人\(附车牌\):)\s*(<span class="subpack-slip-sign-value")/g,
- '<span class="subpack-slip-sign-item"><span class="subpack-slip-sign-label">$1</span>$2'
- )
- result = result.replace(/<span class="subpack-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(/min-width:\s*[^;]*;?/g, '')
- .replace(/;\s*;/g, ';')
- .replace(/^;|;$/g, '')
- .trim()
- const minWidthStyle = `min-width:${SUBPACK_SLIP_SIGN_VALUE_MIN_WIDTH};`
- const mergedStyle = nextStyle ? `${nextStyle};${minWidthStyle}` : minWidthStyle
- return `<span class="subpack-slip-sign-value" style="${mergedStyle}"`
- })
- result = result.replace(
- /(<span class="subpack-slip-sign-label">制单人:<\/span>\s*<span class="subpack-slip-sign-value"[^>]*>)\s*(?: |\s)*<\/span>/g,
- '$1{{preparerUser}} </span>'
- )
- return result
- }
- /** 签字区 flex 布局(兼容已保存模板,避免打印预览溢出到右侧联次列) */
- function normalizeSubpackSlipSignRow(html: string): string {
- if (!html.includes('subpack-slip-doc')) return html
- if (
- !html.includes('制单人:') &&
- !html.includes('仓库:') &&
- !html.includes('客户签收:') &&
- !html.includes('司机/送货人')
- ) {
- return html
- }
- if (html.includes('制单人:') && html.includes('subpack-slip-sign-inner') && !html.includes('司机/送货人')) {
- return normalizeSubpackSlipSignLabelMarkup(html)
- }
- let result = html
- if (html.includes('仓库:') || html.includes('客户签收:') || html.includes('司机/送货人')) {
- result = result.replace(
- /<tr class="subpack-slip-sign-row"[\s\S]*?<\/tr>/,
- buildSubpackSlipSignRowHtml()
- )
- result = result.replace(
- /<tr>\s*<td colspan="7"[^>]*>[\s\S]*?(?:仓库:|制单人:)[\s\S]*?(?:司机\/送货人|生产主管)[\s\S]*?<\/td>\s*<\/tr>/,
- buildSubpackSlipSignRowHtml()
- )
- }
- result = result.replace(
- /<tr([^>]*)>\s*<td colspan="7"([^>]*)>([\s\S]*?(?:制单人:|仓库:)[\s\S]*?)<\/td>\s*<\/tr>/g,
- (match, trAttrs, tdAttrs, content) => {
- if (!content.includes('subpack-slip-sign-value') || content.includes('subpack-slip-sign-inner')) {
- return match
- }
- const wrappedContent = content
- .replace(/ /g, '')
- .replace(
- /(制单人:|审核:|生产主管:|仓库:|客户签收:|司机\/送货人\(附车牌\):)\s*(<span class="subpack-slip-sign-value"[\s\S]*?<\/span>)/g,
- '<span class="subpack-slip-sign-item"><span class="subpack-slip-sign-label">$1</span>$2</span>'
- )
- const trClassAttr = trAttrs.includes('class="')
- ? trAttrs.replace(/class="([^"]*)"/, (_, cls: string) =>
- cls.includes('subpack-slip-sign-row') ? `class="${cls}"` : `class="${cls} subpack-slip-sign-row"`
- )
- : `${trAttrs} class="subpack-slip-sign-row"`
- let nextTdAttrs = tdAttrs
- .replace(/white-space:nowrap;?/g, '')
- .replace(/font-size:(?:14|16)px;?/g, 'font-size:18px;')
- if (nextTdAttrs.includes('class="')) {
- nextTdAttrs = nextTdAttrs.replace(/class="([^"]*)"/, (_, cls: string) =>
- cls.includes('subpack-slip-sign-row-cell') ? `class="${cls}"` : `class="${cls} subpack-slip-sign-row-cell"`
- )
- } else {
- nextTdAttrs = `${nextTdAttrs} class="subpack-slip-sign-row-cell"`
- }
- return `<tr${trClassAttr}>\n <td colspan="7"${nextTdAttrs}>\n <div class="subpack-slip-sign-inner">${wrappedContent.trim()}</div>\n </td>\n </tr>`
- }
- )
- result = result.replace(
- /<td(?![^>]*class="[^"]*subpack-slip-main-col)([^>]*style="[^"]*vertical-align:top;padding-right:6px)/g,
- '<td class="subpack-slip-main-col"$1'
- )
- return normalizeSubpackSlipSignLabelMarkup(result)
- }
- /**
- * 旧版一体 layout-table / 曾拆出的 footer-table → 信息表 + 物料表(含备注) + 签字表。
- */
- function migrateSubpackSlipSplitTables(html: string): string {
- if (!html.includes('subpack-slip-doc')) return html
- if (
- html.includes('subpack-slip-product-table') &&
- !html.includes('subpack-slip-footer-table') &&
- /subpack-slip-product-table[\s\S]*?(?:subpack-slip-remark-cell|备注:)/.test(html)
- ) {
- return html
- }
- if (html.includes('subpack-slip-footer-table') && html.includes('subpack-slip-product-table')) {
- return html.replace(
- /(<\/tr>\s*)<\/table>\s*<table class="subpack-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('subpack-slip-layout-table')) return html
- return html.replace(
- /<table class="subpack-slip-layout-table"[^>]*>([\s\S]*?)<\/table>/i,
- (_full, inner: string) => {
- const headerMatch =
- inner.match(/<tr[^>]*class="[^"]*subpack-slip-product-header[^"]*"[\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="subpack-slip-gap-before-product">[\s\S]*?<\/tr>/i, '')
- const afterHeader = inner.slice(headerIdx + headerMatch[0].length)
- const signMatch =
- afterHeader.match(/<tr[^>]*class="[^"]*subpack-slip-sign-row[^"]*"[\s\S]*?<\/tr>/i) ||
- afterHeader.match(/<tr>\s*<td[^>]*>[\s\S]*?(?:制单人:|仓库:)[\s\S]*?<\/tr>/i)
- let productAndFooter = afterHeader
- let signPart = buildSubpackSlipSignRowHtml()
- if (signMatch) {
- const signAt = afterHeader.indexOf(signMatch[0])
- productAndFooter = afterHeader.slice(0, signAt)
- signPart = signMatch[0]
- }
- productAndFooter = productAndFooter.replace(
- /<tr class="subpack-slip-gap-before-sign">[\s\S]*?<\/tr>/i,
- ''
- )
- return `<table class="subpack-slip-info-table" style="width:100%;border-collapse:collapse;table-layout:fixed;border:none !important;">
- ${INFO_COLGROUP}
- ${before.trim()}
- </table>
- <div class="subpack-slip-section-gap subpack-slip-gap-before-product" style="${GAP_BEFORE_PRODUCT_CELL}"> </div>
- <table class="subpack-slip-product-table" style="width:100%;border-collapse:collapse;table-layout:fixed;border:none !important;">
- ${PRODUCT_COLGROUP}
- ${headerMatch[0]}
- ${productAndFooter.trim()}
- </table>
- <div class="subpack-slip-section-gap subpack-slip-gap-before-sign" style="${GAP_BEFORE_SIGN_CELL}"> </div>
- <table class="subpack-slip-sign-table" style="width:100%;border-collapse:collapse;table-layout:fixed;border:none !important;">
- ${signPart.trim()}
- </table>`
- }
- )
- }
- /** 物料表列宽(兼容已保存模板) */
- function normalizeSubpackSlipProductColWidths(html: string): string {
- if (!html.includes('subpack-slip-product-table')) return html
- return html.replace(
- /(<table class="subpack-slip-product-table"[^>]*>\s*)<colgroup>[\s\S]*?<\/colgroup>/i,
- `$1${PRODUCT_COLGROUP}`
- )
- }
- /** 统一单据左右 10px 间距、内容区宽度(兼容已保存模板) */
- function normalizeSubpackSlipDocLayout(html: string): string {
- let result = migrateSubpackSlipSplitTables(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:${SUBPACK_SLIP_DOC_MAX_WIDTH}`)
- .replace(/max-width:760px/g, `max-width:${SUBPACK_SLIP_DOC_MAX_WIDTH}`)
- .replace(/max-width:794px/g, `max-width:${SUBPACK_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:${SUBPACK_SLIP_SEAL_TOP}px !important;`)
- .replace(/top:4px;/g, `top:${SUBPACK_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="subpack-slip-sign-value") style="/g,
- '$1<span class="subpack-slip-sign-value" style="'
- )
- .replace(
- /(<span class="subpack-slip-sign-value" style="[^"]*)border-bottom:2px solid #000 !important;/g,
- `$1border-bottom:${SIGN_UNDERLINE} !important;`
- )
- .replace(/class="subpack-slip-info-before-product"/g, '')
- .replace(/padding-bottom:12px;/g, '')
- .replace(/padding-bottom:24px;/g, '')
- if (result.includes('subpack-slip-doc') && !result.includes('max-width:')) {
- result = result.replace(
- /(<div class="subpack-slip-doc" style=")([^"]*)"/,
- `$1$2;${DOC_WRAP_EXTRA}"`
- )
- }
- if (result.includes('subpack-slip-doc')) {
- result = result.replace(/<style type="text\/css">[\s\S]*?<\/style>/, SUBPACK_SLIP_STYLE_BLOCK)
- result = normalizeSubpackSlipSignLabelMarkup(result)
- result = normalizeSubpackSlipProductGridCell(result)
- result = normalizeSubpackSlipProductTableFooter(result)
- result = normalizeSubpackSlipSlipNoRow(result)
- }
- return result
- }
- /** 基本信息字段与标题(兼容从送(提)货单复制的旧模板) */
- function normalizeSubpackSlipBasicInfoFields(html: string): string {
- if (!html.includes('subpack-slip-doc')) return html
- let result = html
- result = result.replace(/送 \(提\) 货 单/g, '分 装 单')
- result = result.replace(/送\(提\)货单/g, '分装单')
- result = result.replace(/收货单位:/g, '下单日期:')
- result = result.replace(/单据编号:/g, '交货日期:')
- result = result.replace(/收货联系方式:/g, '下单人:')
- result = result.replace(/日 期:/g, '经手人:')
- result = result.replace(/>日期:</g, '>经手人:<')
- result = result.replace(/\{\{receivingUnit\}\}/g, '{{orderDate}}')
- result = result.replace(/\{\{planNo\}\}/g, '{{deliveryDate}}')
- result = result.replace(/\{\{receiveContact\}\}/g, '{{orderUser}}')
- result = result.replace(/\{\{planDateChinese\}\}/g, '{{handlerName}}')
- result = result.replace(/\{\{driverInfo\}\}/g, '{{handlerName}}')
- result = result.replace(
- /<div class="subpack-slip-info-row subpack-slip-type-check-wrap"[\s\S]*?<\/div>\s*/g,
- ''
- )
- result = result.replace(/<div class="subpack-slip-type-check-row"[\s\S]*?<\/div>\s*/g, '')
- return result
- }
- /** 物料表头(兼容旧版 物料编号/实收数量 表头) */
- function normalizeSubpackSlipProductHeader(html: string): string {
- if (!html.includes('subpack-slip-doc')) return html
- if (html.includes('>序号<') && html.includes('>是否完成<')) return html
- const headerHtml = buildSubpackSlipProductHeaderRowHtml()
- const withClass = html.replace(/<tr class="subpack-slip-product-header"[\s\S]*?<\/tr>/, headerHtml)
- if (withClass !== html) return withClass
- return html.replace(
- /<tr>\s*<th[^>]*>物料编号<\/th>[\s\S]*?<th[^>]*>实收数量<\/th>\s*<\/tr>/,
- headerHtml
- )
- }
- /** 备注行 + 桶选项多选框(兼容旧版纯文本备注行) */
- function normalizeSubpackSlipRemarkRow(html: string): string {
- if (!html.includes('subpack-slip-doc')) return html
- if (html.includes('subpack-slip-remark-row-inner')) {
- let result = html
- // 旧版:备注正文与补充输入分开,升级为同一条连续下划线
- if (!result.includes('subpack-slip-remark-underline-group')) {
- const extractRemark = (raw: string) =>
- String(raw || '')
- .replace(/<[^>]+>/g, '')
- .replace(/备注:/g, '')
- .replace(/ /g, '')
- .trim() || '{{remark}}'
- result = result.replace(
- /<div class="subpack-slip-remark-row-inner">([\s\S]*?)<div class="subpack-slip-bucket-check-row"/,
- (_m, middle: string) => {
- const valueMatch = String(middle).match(
- /class="subpack-slip-remark-value"[^>]*>([\s\S]*?)<\/span>/
- )
- const plainMatch = String(middle).match(/备注:([^<]*)/)
- const content = extractRemark(valueMatch?.[1] ?? plainMatch?.[1] ?? '{{remark}}')
- const extraMatch = String(middle).match(
- /<input type="text" class="subpack-slip-remark-extra-input"([^>]*)\/?>/
- )
- const extraHtml = extraMatch
- ? `<span class="subpack-slip-remark-extra-wrap" contenteditable="false"><input type="text" class="subpack-slip-remark-extra-input"${extraMatch[1]} /></span>`
- : buildSubpackSlipRemarkExtraInputHtml()
- return `<div class="subpack-slip-remark-row-inner">
- <span class="subpack-slip-remark-label">备注:</span>
- <span class="subpack-slip-remark-underline-group"><span class="subpack-slip-remark-value">${content} </span>${extraHtml}</span>
- <div class="subpack-slip-bucket-check-row"`
- }
- )
- }
- if (!result.includes('subpack-slip-remark-extra-input')) {
- result = result.replace(
- /(<div class="subpack-slip-remark-row-inner">[\s\S]*?)(\s*<div class="subpack-slip-bucket-check-row")/,
- `$1\n ${buildSubpackSlipRemarkUnderlineGroupHtml()}$2`
- )
- } else if (!result.includes('subpack-slip-remark-extra-wrap')) {
- result = result.replace(
- /<input type="text" class="subpack-slip-remark-extra-input"([^>]*)\/?>/g,
- `<span class="subpack-slip-remark-extra-wrap" contenteditable="false"><input type="text" class="subpack-slip-remark-extra-input"$1 /></span>`
- )
- }
- return ensureSubpackSlipInteractiveMarkup(result)
- }
- return html.replace(
- /(<td[^>]*colspan="7"[^>]*>\s*)备注:(\{\{remark\}\}|[^<]*?)(\s*<\/td>)/,
- `$1${buildSubpackSlipRemarkRowHtml()}$3`
- )
- }
- /** 移除右侧联次列,并解包旧版 outer-table 布局 */
- function normalizeSubpackSlipRemoveSideLabels(html: string): string {
- if (!html.includes('subpack-slip-doc')) return html
- let result = html
- result = result.replace(/<table class="subpack-slip-side-labels"[\s\S]*?<\/table>/g, '')
- result = result.replace(/\{\{sideLabels\}\}/g, '')
- result = result.replace(
- /<!--\s*最右侧纵向联次\s*-->\s*<td class="subpack-slip-side-labels-col"[\s\S]*?<\/td>/g,
- ''
- )
- result = result.replace(/<td class="subpack-slip-side-labels-col"[^>]*>[\s\S]*?<\/td>/g, '')
- if (result.includes('subpack-slip-outer-table') && result.includes('subpack-slip-main-col')) {
- result = result.replace(
- /<table class="subpack-slip-outer-table" style="[^"]*">\s*<tr>\s*<td class="subpack-slip-main-col" style="[^"]*">/,
- ''
- )
- 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*<tr>[\s\S]*?<\/tr>\s*<tr>[\s\S]*?<\/tr>/,
- `<!-- 基本信息一行 -->\n ${basicInfoRow}`
- )
- if (replaced !== html) return replaced
- const fallback = html.replace(
- /<tr>\s*<td colspan="4"[^>]*>\s*<div class="subpack-slip-info-row"[^>]*>\s*<span class="subpack-slip-info-label"[^>]*>下单日期:[\s\S]*?<\/tr>\s*<tr>\s*<td colspan="4"[^>]*>\s*<div class="subpack-slip-info-row"[^>]*>\s*<span class="subpack-slip-info-label"[^>]*>下单人:[\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(/<div class="subpack-slip-info-row subpack-slip-slip-no-wrap"[\s\S]*?<\/div>\s*/g, '')
- result = result.replace(/<span class="subpack-slip-info-label"[^>]*>分装单号:<\/span>\s*/g, '')
- result = result.replace(
- /<div class="subpack-slip-slip-no-display"[^>]*>([\s\S]*?)<\/div>/g,
- '<span class="subpack-slip-slip-no-display">$1</span>'
- )
- if (result.includes('subpack-slip-title-section') && result.includes('subpack-slip-slip-no-display')) {
- result = result.replace(
- /\s*<span class="subpack-slip-slip-no-display"[^>]*>[\s\S]*?<\/span>\s*(?=<\/div>\s*<\/div>\s*<table class="subpack-slip-(?:layout|info)-table")/,
- '\n'
- )
- result = result.replace(
- /<div class="subpack-slip-slip-no-display"[^>]*>[\s\S]*?<\/div>\s*(?=<\/div>\s*<\/div>\s*<table class="subpack-slip-(?:layout|info)-table")/,
- ''
- )
- }
- if (!result.includes('subpack-slip-slip-no-row')) {
- result = result.replace(
- /(<table class="subpack-slip-(?:layout|info)-table"[\s\S]*?<colgroup>[\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(
- /<tr class="subpack-slip-slip-no-row"[\s\S]*?<\/tr>\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,
- options?: { blank?: boolean }
- ): string {
- if (!htmlContent || !plan) return htmlContent
- const blank = Boolean(options?.blank)
- const orderDate = blank ? '' : ctx?.orderDate || formatDateIso()
- const orderUser = blank ? '' : ctx?.orderUser || ''
- const preparerUser = blank ? '' : ctx?.preparerUser || orderUser
- const replacements: Record<string, string> = {
- '{{companyName}}': SUBPACK_SLIP_DEFAULTS.companyName,
- '{{subpackSlipNo}}': blank ? '' : ctx?.subpackSlipNo || '',
- '{{orderDate}}': orderDate,
- '{{deliveryDate}}': '',
- '{{orderUser}}': orderUser,
- '{{preparerUser}}': preparerUser,
- '{{handlerName}}': '',
- '{{receivingUnit}}': blank ? '' : orderDate,
- '{{planNo}}': '',
- '{{receiveContact}}': blank ? '' : orderUser,
- '{{driverInfo}}': '',
- '{{planDateChinese}}': '',
- '{{productTable}}': buildProductRows(blank ? [] : plan.products, ctx),
- '{{remark}}': blank ? '' : formatRemarkForSlipHtml(plan.remark),
- '{{remarkExtra}}': '',
- '{{companyAddress}}': '',
- '{{companyPhone}}': '',
- '{{contactPhone}}': '',
- '{{companySeal}}': '',
- '{{sideLabels}}': ''
- }
- let result = normalizeSubpackSlipDocLayout(
- normalizeSubpackSlipRemoveAddressAndSeal(
- normalizeSubpackSlipSignRow(
- normalizeSubpackSlipTermsCell(
- normalizeSubpackSlipRemoveTermsRow(
- normalizeSubpackSlipGapBeforeSign(
- normalizeSubpackSlipProductTableFooter(
- normalizeSubpackSlipProductColWidths(
- normalizeSubpackSlipRemarkRow(
- normalizeSubpackSlipProductGridCell(
- normalizeSubpackSlipProductHeader(
- normalizeSubpackSlipGapBeforeProduct(
- normalizeSubpackSlipSlipNoRow(
- normalizeSubpackSlipBasicInfoOneRow(
- normalizeSubpackSlipBasicInfoFields(
- normalizeSubpackSlipRemoveSideLabels(htmlContent)
- )
- )
- )
- )
- )
- )
- )
- )
- )
- )
- )
- )
- )
- )
- )
- for (const [placeholder, value] of Object.entries(replacements)) {
- result = result.split(placeholder).join(value)
- }
- if (!blank && ctx?.subpackSlipNo) {
- result = result.replace(
- /(<span class="subpack-slip-slip-no-display"[^>]*>)[\s\S]*?( <\/span>)/g,
- `$1${ctx.subpackSlipNo}$2`
- )
- }
- if (!blank && preparerUser) {
- result = result.replace(
- /(<span class="subpack-slip-sign-label">制单人:<\/span>\s*<span class="subpack-slip-sign-value"[^>]*>)[\s\S]*?(<\/span>)/g,
- `$1${preparerUser}$2`
- )
- }
- return result
- }
- /** 空白分装单 HTML:默认模板 + 业务字段全空(含分装单号)+ 最小空产品行 */
- export function renderBlankSubpackSlipHtml(): string {
- const blankPlan = {
- id: '',
- planNo: '',
- products: [],
- customerName: '',
- remark: ''
- } as Plan
- const html = generateSubpackSlipTemplate(blankPlan)
- return replaceSubpackSlipPlaceholders(html, blankPlan, undefined, { blank: true })
- }
- /** 保存模板前:将动态字段还原为占位符 */
- export function restoreSubpackSlipPlaceholders(template: string, plan: Plan): string {
- let result = template
- const dynamicReplacements: Record<string, string> = {
- '{{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 class="subpack-slip-slip-no-display"[^>]*>)[^<]*(?= <\/span>)/,
- '$1{{subpackSlipNo}}'
- )
- result = result.replace(
- /(<div class="subpack-slip-slip-no-display"[^>]*>)[^<]*(?= <\/div>)/,
- '$1{{subpackSlipNo}}'
- )
- result = result.replace(
- /(<span class="subpack-slip-slip-no-value[^"]*"[^>]*>)[^<]*(?= <\/span>)/,
- '$1{{subpackSlipNo}}'
- )
- result = result.replace(
- /(<span class="subpack-slip-info-label"[^>]*>下单日期:<\/span>\s*<span class="subpack-slip-info-value-left"[^>]*>)[^<]*(?= <\/span>)/,
- '$1{{orderDate}}'
- )
- result = result.replace(
- /(<span class="subpack-slip-info-label"[^>]*>下单人:<\/span>\s*<span class="subpack-slip-info-value-left"[^>]*>)[^<]*(?= <\/span>)/,
- '$1{{orderUser}}'
- )
- result = result.replace(
- /(<span class="subpack-slip-info-label"[^>]*>交货日期:<\/span>\s*<span class="subpack-slip-info-value-center"[^>]*>)[^<]*(?= <\/span>)/,
- '$1{{deliveryDate}}'
- )
- result = result.replace(
- /(<span class="subpack-slip-info-label"[^>]*>经手人:<\/span>\s*<span class="subpack-slip-info-value-center"[^>]*>)[^<]*(?= <\/span>)/,
- '$1{{handlerName}}'
- )
- result = result.replace(
- /(<span class="subpack-slip-sign-label">制单人:<\/span>\s*<span class="subpack-slip-sign-value"[^>]*>)[^<]*(?=<\/span>)/,
- '$1{{preparerUser}}'
- )
- // 产品明细行还原为占位符(表头之后、备注行之前)
- result = result.replace(
- /(<th[^>]*>是否完成<\/th>\s*<\/tr>\s*)([\s\S]*?)(\s*<tr>\s*<td colspan="7"[^>]*>\s*(?:<div class="subpack-slip-remark-row-inner">|备注:))/,
- '$1{{productTable}}$3'
- )
- result = result.replace(
- /(<th[^>]*>实收数量<\/th>\s*<\/tr>\s*)([\s\S]*?)(\s*<tr>\s*<td colspan="7"[^>]*>\s*(?:<div class="subpack-slip-remark-row-inner">|备注:))/,
- '$1{{productTable}}$3'
- )
- // 移除右侧联次、印章与地址/电话行
- result = normalizeSubpackSlipRemoveSideLabels(result)
- result = normalizeSubpackSlipRemoveAddressAndSeal(result)
- return result
- }
|