pl-subpack-slip-template.ts 96 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273
  1. import type { Plan, PlanProduct } from '@/api/pl'
  2. import { formatRemarkForSlipHtml } from '@/utils/pl-batch-print'
  3. import { applyPdfStableTableCellBorders } from '@/utils/pl-slip-pdf-render'
  4. /** 分装单打印默认公司信息(可在富文本中修改) */
  5. export const SUBPACK_SLIP_DEFAULTS = {
  6. companyName: '安庆市山峡化工有限公司',
  7. receivingUnit: '安庆市山峡化工有限公司',
  8. companyAddress: '安庆市高新区霞虹路5号',
  9. companyPhone: '0556-5999710',
  10. contactPhone: '方总13395560066'
  11. } as const
  12. const DOC_FONT = "SimSun, 'Songti SC', serif"
  13. const FONT_TITLE = 'font-size:26px;font-weight:bold;'
  14. const FONT_BODY = 'font-size:20px;'
  15. const FONT_SIGN = 'font-size:18px;'
  16. const FONT_SIDE = 'font-size:18px;'
  17. const DOC_PADDING = '8px 10px 24px'
  18. /** 单据内容区最大宽度(编辑预览居中,略窄于 A4 像素宽) */
  19. export const SUBPACK_SLIP_DOC_MAX_WIDTH = '960px'
  20. const DOC_WRAP_EXTRA = `max-width:${SUBPACK_SLIP_DOC_MAX_WIDTH};margin:0 auto;box-sizing:border-box;`
  21. const LOGO_HEIGHT = 36
  22. const LOGO_LEFT = 50
  23. const TITLE_STYLE = `${FONT_TITLE}font-weight:700 !important;letter-spacing:6px;line-height:1.35;`
  24. const TITLE_SUB_STYLE = `${TITLE_STYLE}margin-top:2px;letter-spacing:10px;`
  25. const TABLE_ROW_PADDING = '4px'
  26. /** 信息表等宽列 */
  27. const INFO_COLGROUP = `<colgroup>
  28. <col span="7" style="width:14.28%;" />
  29. </colgroup>`
  30. /**
  31. * 物料表列宽:加宽名称/型号,收窄包装规格/单位。
  32. * 序号 / 产品名称 / 产品型号 / 包装规格 / 单位 / 数量 / 是否完成
  33. */
  34. const PRODUCT_COLGROUP = `<colgroup>
  35. <col style="width:8%;" />
  36. <col style="width:26%;" />
  37. <col style="width:18%;" />
  38. <col style="width:10%;" />
  39. <col style="width:7%;" />
  40. <col style="width:15%;" />
  41. <col style="width:16%;" />
  42. </colgroup>`
  43. /** 物料网格 padding(编辑器预览) */
  44. const PRODUCT_GRID_CELL_PADDING = '8px 6px'
  45. const PRODUCT_GRID_TH_PADDING = '10px 6px'
  46. /** PDF 专用:padding 直接写在 td/th(html2canvas 对 flex/vertical-align 支持差,底部多留白) */
  47. const PRODUCT_GRID_PDF_TD_PADDING = '8px 6px 12px 6px'
  48. const PRODUCT_GRID_PDF_TH_PADDING = '10px 6px 16px 6px'
  49. const PRODUCT_GRID_PDF_LINE_HEIGHT = '24px'
  50. /** PDF 专用:备注/条款/地址等左对齐合并单元格 */
  51. const PRODUCT_FOOTER_PDF_PADDING = '10px 6px 16px 6px'
  52. const PRODUCT_FOOTER_PDF_LINE_HEIGHT = '1.35'
  53. /** 物料表格边框线宽(备注/条款等合并单元格,与网格一致便于 PDF 衔接) */
  54. const PRODUCT_TABLE_BORDER = '0.3px solid #000'
  55. /** 物料明细网格线(与合同 PDF 一致:全边框 inline + 0.3px,html2canvas 渲染稳定) */
  56. const PRODUCT_GRID_BORDER = '0.3px solid #000'
  57. /** 物料表格单元格:长文本自动换行 */
  58. const PRODUCT_CELL_WRAP = 'white-space:normal;word-wrap:break-word;overflow-wrap:break-word;word-break:break-word;'
  59. 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}`
  60. 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}`
  61. 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}`
  62. 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;`
  63. /** 条款单元格:行高随内容,避免被右侧印章区 min-height 撑出底部空白 */
  64. 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;`
  65. const TERMS_TEXT_STYLE = 'line-height:1.35;'
  66. const SEAL_BLOCK_LINE_STYLE = 'line-height:1.35;'
  67. /** 分装单印章:public/sxshzyz.png,定位在地址单元格右上角(相对原位置向上 100px) */
  68. const SUBPACK_SLIP_SEAL_IMAGE_SRC = '/sxshzyz.png'
  69. const SUBPACK_SLIP_SEAL_TOP = -96
  70. 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;`
  71. const HANDWRITE_BLANK_INNER = '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'
  72. const HANDWRITE_BLANK = `<span class="subpack-slip-handwrite-blank">(${HANDWRITE_BLANK_INNER})</span>`
  73. const PLAIN_CELL = `border:none !important;padding:${TABLE_ROW_PADDING};${FONT_BODY}line-height:1.8;vertical-align:top;background:transparent !important;`
  74. const NO_BORDER = `${PLAIN_CELL} vertical-align:bottom;`
  75. /** 基本信息:label 与值同一行,值区域占满剩余宽度并带下划线 */
  76. const INFO_ROW = `display:flex;align-items:flex-end;width:100%;box-sizing:border-box;`
  77. const INFO_LABEL = `white-space:nowrap;flex-shrink:0;padding-right:2px;`
  78. const INFO_UNDERLINE = '1px solid #000'
  79. const SIGN_UNDERLINE = '1px solid #000'
  80. /** 签字值下划线与文字间距(底部 padding 把 border-bottom 推离字模) */
  81. const SIGN_VALUE_PADDING = '0 2px 6px 2px'
  82. const SIGN_VALUE_PDF_PADDING = '0 2px 8px 2px'
  83. 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;`
  84. 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;`
  85. const INFO_CELL_LEFT = `${NO_BORDER}text-align:left;`
  86. const INFO_CELL_RIGHT = `${NO_BORDER}text-align:left;`
  87. const INFO_GAP_BEFORE_PRODUCT = 12
  88. 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;`
  89. const INFO_GAP_BEFORE_SIGN = 12
  90. 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;`
  91. const SIDE_LABELS_COL_WIDTH = 36
  92. /** 签字区下划线最小宽度 */
  93. const SUBPACK_SLIP_SIGN_VALUE_MIN_WIDTH = '120px'
  94. function buildSubpackSlipSignItem(label: string, valueContent: string, minWidth: string = SUBPACK_SLIP_SIGN_VALUE_MIN_WIDTH): string {
  95. 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>`
  96. }
  97. function buildSubpackSlipSignRowHtml(): string {
  98. return `<tr class="subpack-slip-sign-row">
  99. <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;">
  100. <div class="subpack-slip-sign-inner">
  101. ${buildSubpackSlipSignItem('制单人:', '{{preparerUser}}&nbsp;')}
  102. ${buildSubpackSlipSignItem('审核:', '&nbsp;')}
  103. ${buildSubpackSlipSignItem('生产主管:', '&nbsp;')}
  104. </div>
  105. </td>
  106. </tr>`
  107. }
  108. export interface SubpackSlipRenderContext {
  109. /** 单位字典 code -> 展示文案 */
  110. getUnitLabel?: (unitCode: string) => string
  111. /** 分装单号 NO:SXFZYYYYMMDD-### */
  112. subpackSlipNo?: string
  113. /** 下单日期 YYYY-MM-DD */
  114. orderDate?: string
  115. /** 下单人(当前登录用户姓名) */
  116. orderUser?: string
  117. /** 制单人(默认当前登录用户姓名) */
  118. preparerUser?: string
  119. }
  120. function resolveSlipUnitLabel(unitCode: string | undefined, ctx?: SubpackSlipRenderContext): string {
  121. const code = String(unitCode || '').trim()
  122. if (!code) return ''
  123. return ctx?.getUnitLabel?.(code) || code
  124. }
  125. export function canPrintSubpackSlip(plan?: Pick<Plan, 'planType' | 'transportStatus'> | null): boolean {
  126. return plan?.planType !== 'inbound'
  127. }
  128. function formatPlanDateChinese(planDate?: string): string {
  129. if (!planDate) return ''
  130. const date = new Date(planDate)
  131. if (Number.isNaN(date.getTime())) return planDate
  132. return `${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日`
  133. }
  134. function formatQty(value?: number | string | null): string {
  135. const num = parseFloat(String(value ?? ''))
  136. if (Number.isNaN(num)) return ''
  137. return num.toFixed(3)
  138. }
  139. function formatSubpackSlipProductGridCellContent(content: string): string {
  140. const plain = content
  141. .trim()
  142. .replace(/<div class="subpack-slip-product-cell-inner"[^>]*>([\s\S]*?)<\/div>/gi, '$1')
  143. .replace(/<div class="subpack-slip-pdf-grid-inner"[^>]*>([\s\S]*?)<\/div>/gi, '$1')
  144. .replace(/<span class="subpack-slip-pdf-grid-text"[^>]*>([\s\S]*?)<\/span>/gi, '$1')
  145. .replace(/<span class="subpack-slip-product-grid-align"[^>]*>([\s\S]*?)<\/span>/gi, '$1')
  146. .replace(/^<p[^>]*>([\s\S]*?)<\/p>$/i, '$1')
  147. .trim()
  148. return plain || '&nbsp;'
  149. }
  150. function formatDateIso(value?: string | Date | null): string {
  151. const date = value instanceof Date ? value : value ? new Date(value) : new Date()
  152. if (Number.isNaN(date.getTime())) return ''
  153. const y = date.getFullYear()
  154. const m = String(date.getMonth() + 1).padStart(2, '0')
  155. const d = String(date.getDate()).padStart(2, '0')
  156. return `${y}-${m}-${d}`
  157. }
  158. /** 分装单默认下单日期(当天 YYYY-MM-DD) */
  159. export function formatSubpackSlipDateIso(value?: string | Date | null): string {
  160. return formatDateIso(value)
  161. }
  162. function buildSubpackSlipCompleteCheckHtml(): string {
  163. return `<label class="subpack-slip-check-item subpack-slip-complete-check-item" contenteditable="false"><input type="checkbox" class="subpack-slip-check-input" />完成</label>`
  164. }
  165. const SUBPACK_SLIP_PRODUCT_MIN_ROWS = 5
  166. function buildSubpackSlipProductRow(
  167. rowIndex: number,
  168. product: PlanProduct | undefined,
  169. ctx?: SubpackSlipRenderContext
  170. ): string {
  171. if (product) {
  172. const qty = product.outboundQty ?? product.outboundEstimateQty ?? product.quantity ?? ''
  173. return `<tr>
  174. <td class="subpack-slip-product-cell subpack-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">${formatSubpackSlipProductGridCellContent(String(rowIndex))}</td>
  175. <td class="subpack-slip-product-cell subpack-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">${formatSubpackSlipProductGridCellContent(product.productName || '')}</td>
  176. <td class="subpack-slip-product-cell subpack-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">${formatSubpackSlipProductGridCellContent(product.model || '')}</td>
  177. <td class="subpack-slip-product-cell subpack-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">${formatSubpackSlipProductGridCellContent(product.packageSpec || '')}</td>
  178. <td class="subpack-slip-product-cell subpack-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">${formatSubpackSlipProductGridCellContent(resolveSlipUnitLabel(product.unit, ctx))}</td>
  179. <td class="subpack-slip-product-cell subpack-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">${formatSubpackSlipProductGridCellContent(formatQty(qty))}</td>
  180. <td class="subpack-slip-product-cell subpack-slip-product-grid-cell subpack-slip-complete-cell" style="${PRODUCT_GRID_CELL}">${buildSubpackSlipCompleteCheckHtml()}</td>
  181. </tr>`
  182. }
  183. return `<tr>
  184. <td class="subpack-slip-product-cell subpack-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">${formatSubpackSlipProductGridCellContent(String(rowIndex))}</td>
  185. <td class="subpack-slip-product-cell subpack-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">${formatSubpackSlipProductGridCellContent('')}</td>
  186. <td class="subpack-slip-product-cell subpack-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">${formatSubpackSlipProductGridCellContent('')}</td>
  187. <td class="subpack-slip-product-cell subpack-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">${formatSubpackSlipProductGridCellContent('')}</td>
  188. <td class="subpack-slip-product-cell subpack-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">${formatSubpackSlipProductGridCellContent('')}</td>
  189. <td class="subpack-slip-product-cell subpack-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">${formatSubpackSlipProductGridCellContent('')}</td>
  190. <td class="subpack-slip-product-cell subpack-slip-product-grid-cell subpack-slip-complete-cell" style="${PRODUCT_GRID_CELL}">&nbsp;</td>
  191. </tr>`
  192. }
  193. function buildProductRows(products: PlanProduct[] = [], ctx?: SubpackSlipRenderContext): string {
  194. const rowCount = Math.max(products.length, SUBPACK_SLIP_PRODUCT_MIN_ROWS)
  195. const rows: string[] = []
  196. for (let index = 0; index < rowCount; index += 1) {
  197. rows.push(buildSubpackSlipProductRow(index + 1, products[index], ctx))
  198. }
  199. return rows.join('')
  200. }
  201. export const SUBPACK_SLIP_PRINT_CSS = `
  202. .subpack-slip-doc {
  203. padding: ${DOC_PADDING} !important;
  204. max-width: ${SUBPACK_SLIP_DOC_MAX_WIDTH} !important;
  205. margin-left: auto !important;
  206. margin-right: auto !important;
  207. box-sizing: border-box !important;
  208. }
  209. .subpack-slip-doc .subpack-slip-title-heading {
  210. font-weight: 700 !important;
  211. line-height: 1.35 !important;
  212. }
  213. .subpack-slip-doc .subpack-slip-title-heading + .subpack-slip-title-heading {
  214. margin-top: 2px !important;
  215. }
  216. .subpack-slip-doc .subpack-slip-info-row {
  217. display: flex !important;
  218. align-items: flex-end !important;
  219. width: 100% !important;
  220. box-sizing: border-box !important;
  221. }
  222. .subpack-slip-doc .subpack-slip-slip-no-display {
  223. font-size: 20px !important;
  224. line-height: 1.35 !important;
  225. letter-spacing: 0.5px !important;
  226. white-space: nowrap !important;
  227. }
  228. .subpack-slip-doc .subpack-slip-slip-no-anchor {
  229. position: relative !important;
  230. flex-shrink: 0 !important;
  231. }
  232. .subpack-slip-doc .subpack-slip-slip-no-anchor-text {
  233. visibility: hidden !important;
  234. }
  235. .subpack-slip-doc .subpack-slip-slip-no-anchor .subpack-slip-slip-no-display {
  236. position: absolute !important;
  237. right: 0 !important;
  238. top: 0 !important;
  239. text-align: right !important;
  240. margin-top: 0 !important;
  241. }
  242. .subpack-slip-doc .subpack-slip-slip-no-field .subpack-slip-info-value-left {
  243. visibility: hidden !important;
  244. border-bottom: none !important;
  245. min-height: 0 !important;
  246. padding-bottom: 0 !important;
  247. }
  248. .subpack-slip-doc .subpack-slip-basic-info-spacer {
  249. visibility: hidden !important;
  250. min-height: 0 !important;
  251. height: 0 !important;
  252. overflow: hidden !important;
  253. margin: 0 !important;
  254. padding: 0 !important;
  255. }
  256. .subpack-slip-doc .subpack-slip-basic-info-inner {
  257. display: flex !important;
  258. flex-wrap: wrap !important;
  259. justify-content: space-between !important;
  260. align-items: flex-end !important;
  261. gap: 8px 12px !important;
  262. width: 100% !important;
  263. box-sizing: border-box !important;
  264. }
  265. .subpack-slip-doc .subpack-slip-basic-info-item {
  266. flex: 1 !important;
  267. min-width: 0 !important;
  268. }
  269. /* 下单/交货日期略宽;下单人与经手人等宽(单号行 spacer 同步,保持右缘对齐) */
  270. .subpack-slip-doc .subpack-slip-basic-info-row .subpack-slip-basic-info-inner > .subpack-slip-basic-info-item:nth-child(1),
  271. .subpack-slip-doc .subpack-slip-basic-info-row .subpack-slip-basic-info-inner > .subpack-slip-basic-info-item:nth-child(2),
  272. .subpack-slip-doc .subpack-slip-slip-no-row .subpack-slip-basic-info-inner > .subpack-slip-basic-info-item:nth-child(1),
  273. .subpack-slip-doc .subpack-slip-slip-no-row .subpack-slip-basic-info-inner > .subpack-slip-basic-info-item:nth-child(2) {
  274. flex: 1.1 !important;
  275. }
  276. .subpack-slip-doc .subpack-slip-basic-info-row .subpack-slip-basic-info-inner > .subpack-slip-basic-info-item:nth-child(3),
  277. .subpack-slip-doc .subpack-slip-basic-info-row .subpack-slip-basic-info-inner > .subpack-slip-basic-info-item:nth-child(4),
  278. .subpack-slip-doc .subpack-slip-slip-no-row .subpack-slip-basic-info-inner > .subpack-slip-basic-info-item:nth-child(3),
  279. .subpack-slip-doc .subpack-slip-slip-no-row .subpack-slip-basic-info-inner > .subpack-slip-basic-info-item:nth-child(4) {
  280. flex: 1 !important;
  281. }
  282. .subpack-slip-doc .subpack-slip-remark-row-inner {
  283. display: flex !important;
  284. align-items: center !important;
  285. width: 100% !important;
  286. box-sizing: border-box !important;
  287. gap: 12px !important;
  288. }
  289. .subpack-slip-doc .subpack-slip-remark-label {
  290. flex-shrink: 0 !important;
  291. white-space: nowrap !important;
  292. }
  293. /* 备注正文 + 补充输入共用一条连续下划线(底部 padding 把线推离字模,避免 PDF/html2canvas 遮字) */
  294. .subpack-slip-doc .subpack-slip-remark-underline-group {
  295. display: inline-flex !important;
  296. align-items: baseline !important;
  297. flex: 1 !important;
  298. min-width: 200px !important;
  299. max-width: 420px !important;
  300. box-sizing: border-box !important;
  301. border-bottom: ${INFO_UNDERLINE} !important;
  302. padding: ${SIGN_VALUE_PADDING} !important;
  303. gap: 0 !important;
  304. line-height: 1.35 !important;
  305. overflow: visible !important;
  306. }
  307. .subpack-slip-doc .subpack-slip-remark-value {
  308. display: inline-block !important;
  309. flex: 0 1 auto !important;
  310. min-width: 0 !important;
  311. max-width: 260px !important;
  312. box-sizing: border-box !important;
  313. border-bottom: none !important;
  314. padding: 0 2px !important;
  315. min-height: 1.3em !important;
  316. line-height: 1.35 !important;
  317. overflow: visible !important;
  318. white-space: nowrap !important;
  319. vertical-align: baseline !important;
  320. }
  321. .subpack-slip-doc .subpack-slip-remark-extra-wrap {
  322. flex: 1 !important;
  323. min-width: 80px !important;
  324. display: inline-block !important;
  325. vertical-align: bottom !important;
  326. -webkit-user-modify: read-only !important;
  327. }
  328. .subpack-slip-doc .subpack-slip-remark-extra-input {
  329. width: 100% !important;
  330. min-width: 80px !important;
  331. height: 28px !important;
  332. padding: 0 2px 0 !important;
  333. margin: 0 !important;
  334. border: none !important;
  335. border-bottom: none !important;
  336. border-radius: 0 !important;
  337. background: transparent !important;
  338. font-family: inherit !important;
  339. font-size: 20px !important;
  340. line-height: 1.35 !important;
  341. box-sizing: border-box !important;
  342. outline: none !important;
  343. color: #000 !important;
  344. pointer-events: auto !important;
  345. }
  346. .subpack-slip-doc .subpack-slip-bucket-check-row {
  347. flex: 1 !important;
  348. min-width: 0 !important;
  349. display: flex !important;
  350. flex-wrap: wrap !important;
  351. justify-content: center !important;
  352. align-items: center !important;
  353. gap: 14px 18px !important;
  354. box-sizing: border-box !important;
  355. font-size: 20px !important;
  356. line-height: 1.35 !important;
  357. }
  358. .subpack-slip-doc .subpack-slip-complete-cell {
  359. text-align: center !important;
  360. }
  361. .subpack-slip-doc .subpack-slip-complete-check-item {
  362. justify-content: center !important;
  363. margin: 0 auto !important;
  364. }
  365. .subpack-slip-doc .subpack-slip-check-item {
  366. display: inline-flex !important;
  367. align-items: center !important;
  368. gap: 4px !important;
  369. white-space: nowrap !important;
  370. cursor: pointer !important;
  371. user-select: none !important;
  372. -webkit-user-modify: read-only !important;
  373. }
  374. .subpack-slip-doc .subpack-slip-check-input {
  375. width: 16px !important;
  376. height: 16px !important;
  377. margin: 0 !important;
  378. flex-shrink: 0 !important;
  379. vertical-align: middle !important;
  380. cursor: pointer !important;
  381. pointer-events: auto !important;
  382. appearance: none !important;
  383. -webkit-appearance: none !important;
  384. background: #fff !important;
  385. border: 1px solid #000 !important;
  386. border-radius: 0 !important;
  387. box-sizing: border-box !important;
  388. print-color-adjust: exact !important;
  389. -webkit-print-color-adjust: exact !important;
  390. position: relative !important;
  391. }
  392. .subpack-slip-doc .subpack-slip-check-input:checked::after {
  393. content: '' !important;
  394. position: absolute !important;
  395. left: 4px !important;
  396. top: 1px !important;
  397. width: 5px !important;
  398. height: 10px !important;
  399. border: solid #000 !important;
  400. border-width: 0 2.5px 2.5px 0 !important;
  401. transform: rotate(45deg) !important;
  402. box-sizing: border-box !important;
  403. }
  404. .subpack-slip-doc .subpack-slip-check-item-pdf {
  405. display: inline-block !important;
  406. white-space: nowrap !important;
  407. font-family: SimSun, 'Songti SC', serif !important;
  408. font-size: 20px !important;
  409. line-height: 20px !important;
  410. height: 20px !important;
  411. vertical-align: middle !important;
  412. margin-right: 18px !important;
  413. }
  414. .subpack-slip-doc .subpack-slip-check-item-pdf:last-child {
  415. margin-right: 0 !important;
  416. }
  417. .subpack-slip-doc .subpack-slip-check-img-pdf {
  418. display: inline-block !important;
  419. width: 16px !important;
  420. height: 16px !important;
  421. margin: 0 4px 0 0 !important;
  422. padding: 0 !important;
  423. border: none !important;
  424. vertical-align: middle !important;
  425. }
  426. .subpack-slip-doc .subpack-slip-check-text-pdf {
  427. display: inline-block !important;
  428. font-size: 20px !important;
  429. line-height: 20px !important;
  430. vertical-align: middle !important;
  431. position: relative !important;
  432. top: -12px !important;
  433. }
  434. .subpack-slip-doc .subpack-slip-check-item-pdf.subpack-slip-complete-check-item-pdf {
  435. margin-right: 0 !important;
  436. }
  437. .subpack-slip-doc .subpack-slip-info-label {
  438. white-space: nowrap !important;
  439. flex-shrink: 0 !important;
  440. }
  441. .subpack-slip-doc table.subpack-slip-layout-table tr.subpack-slip-gap-before-product td.subpack-slip-gap-before-product-cell,
  442. .subpack-slip-doc .subpack-slip-section-gap.subpack-slip-gap-before-product {
  443. border: none !important;
  444. background: transparent !important;
  445. padding: 0 !important;
  446. line-height: 0 !important;
  447. font-size: 0 !important;
  448. height: ${INFO_GAP_BEFORE_PRODUCT}px !important;
  449. min-height: ${INFO_GAP_BEFORE_PRODUCT}px !important;
  450. }
  451. .subpack-slip-doc table.subpack-slip-layout-table tr.subpack-slip-gap-before-sign td.subpack-slip-gap-before-sign-cell,
  452. .subpack-slip-doc .subpack-slip-section-gap.subpack-slip-gap-before-sign {
  453. border: none !important;
  454. background: transparent !important;
  455. padding: 0 !important;
  456. line-height: 0 !important;
  457. font-size: 0 !important;
  458. height: ${INFO_GAP_BEFORE_SIGN}px !important;
  459. min-height: ${INFO_GAP_BEFORE_SIGN}px !important;
  460. }
  461. .subpack-slip-doc .subpack-slip-info-value-left {
  462. flex: 1 !important;
  463. min-width: 0 !important;
  464. box-sizing: border-box !important;
  465. text-align: left !important;
  466. border-bottom: ${INFO_UNDERLINE} !important;
  467. min-height: 1.3em !important;
  468. }
  469. .subpack-slip-doc .subpack-slip-info-value-center {
  470. flex: 1 !important;
  471. min-width: 0 !important;
  472. box-sizing: border-box !important;
  473. text-align: center !important;
  474. border-bottom: ${INFO_UNDERLINE} !important;
  475. min-height: 1.3em !important;
  476. }
  477. .subpack-slip-doc .subpack-slip-sign-value {
  478. display: inline-block !important;
  479. min-width: ${SUBPACK_SLIP_SIGN_VALUE_MIN_WIDTH} !important;
  480. line-height: 1.35 !important;
  481. padding: ${SIGN_VALUE_PADDING} !important;
  482. margin: 0 !important;
  483. vertical-align: baseline !important;
  484. border: none !important;
  485. border-bottom: ${SIGN_UNDERLINE} !important;
  486. box-sizing: border-box !important;
  487. }
  488. .subpack-slip-doc .subpack-slip-sign-label {
  489. line-height: 1.35 !important;
  490. vertical-align: baseline !important;
  491. white-space: nowrap !important;
  492. }
  493. .subpack-slip-doc table.subpack-slip-outer-table {
  494. table-layout: fixed !important;
  495. width: 100% !important;
  496. border-collapse: collapse !important;
  497. }
  498. .subpack-slip-doc td.subpack-slip-main-col {
  499. overflow: hidden !important;
  500. box-sizing: border-box !important;
  501. vertical-align: top !important;
  502. }
  503. .subpack-slip-doc td.subpack-slip-side-labels-col {
  504. width: ${SIDE_LABELS_COL_WIDTH}px !important;
  505. min-width: ${SIDE_LABELS_COL_WIDTH}px !important;
  506. max-width: ${SIDE_LABELS_COL_WIDTH}px !important;
  507. }
  508. .subpack-slip-doc .subpack-slip-sign-row-cell,
  509. .subpack-slip-doc .subpack-slip-sign-inner,
  510. .subpack-slip-doc .subpack-slip-sign-item {
  511. font-size: 18px !important;
  512. }
  513. .subpack-slip-doc .subpack-slip-sign-row-cell {
  514. overflow: hidden !important;
  515. max-width: 100% !important;
  516. box-sizing: border-box !important;
  517. }
  518. .subpack-slip-doc .subpack-slip-sign-inner {
  519. display: flex !important;
  520. flex-wrap: wrap !important;
  521. justify-content: center !important;
  522. align-items: baseline !important;
  523. column-gap: 10px !important;
  524. row-gap: 6px !important;
  525. max-width: 100% !important;
  526. box-sizing: border-box !important;
  527. margin: 0 auto !important;
  528. white-space: normal !important;
  529. line-height: 1.35 !important;
  530. }
  531. .subpack-slip-doc .subpack-slip-sign-item {
  532. display: inline !important;
  533. white-space: nowrap !important;
  534. line-height: 1.35 !important;
  535. vertical-align: baseline !important;
  536. }
  537. .subpack-slip-doc .subpack-slip-terms-text {
  538. line-height: 1.35 !important;
  539. margin: 0 !important;
  540. padding: 0 !important;
  541. }
  542. .subpack-slip-doc td.subpack-slip-terms-cell {
  543. padding: 10px 6px 14px 6px !important;
  544. line-height: 1.35 !important;
  545. vertical-align: top !important;
  546. }
  547. .subpack-slip-doc .subpack-slip-handwrite-blank {
  548. display: inline-block !important;
  549. min-width: 3.6em !important;
  550. text-align: center !important;
  551. letter-spacing: 0.08em !important;
  552. }
  553. .subpack-slip-doc .subpack-slip-seal-block {
  554. position: relative !important;
  555. line-height: 1.35 !important;
  556. min-height: 0 !important;
  557. height: auto !important;
  558. }
  559. .subpack-slip-doc .subpack-slip-seal-block img[alt="印章"] {
  560. position: absolute !important;
  561. right: 4px !important;
  562. top: ${SUBPACK_SLIP_SEAL_TOP}px !important;
  563. left: auto !important;
  564. transform: none !important;
  565. width: 140px !important;
  566. height: 140px !important;
  567. opacity: 0.85 !important;
  568. z-index: 10 !important;
  569. }
  570. .subpack-slip-doc .subpack-slip-seal-block .subpack-slip-seal-lines {
  571. position: relative !important;
  572. z-index: 1 !important;
  573. line-height: 1.35 !important;
  574. margin: 0 !important;
  575. padding: 0 !important;
  576. }
  577. .subpack-slip-doc table.subpack-slip-outer-table > tbody > tr > td,
  578. .subpack-slip-doc table.subpack-slip-outer-table > tbody > tr > th,
  579. .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),
  580. .subpack-slip-doc table.subpack-slip-layout-table th:not(.subpack-slip-product-cell),
  581. .subpack-slip-doc table.subpack-slip-info-table td,
  582. .subpack-slip-doc table.subpack-slip-info-table th,
  583. .subpack-slip-doc table.subpack-slip-sign-table td,
  584. .subpack-slip-doc table.subpack-slip-sign-table th {
  585. border: none !important;
  586. background: transparent !important;
  587. padding: ${TABLE_ROW_PADDING} !important;
  588. }
  589. .subpack-slip-doc table.subpack-slip-layout-table,
  590. .subpack-slip-doc table.subpack-slip-info-table,
  591. .subpack-slip-doc table.subpack-slip-product-table,
  592. .subpack-slip-doc table.subpack-slip-footer-table,
  593. .subpack-slip-doc table.subpack-slip-sign-table {
  594. border-collapse: collapse !important;
  595. border-spacing: 0 !important;
  596. table-layout: fixed !important;
  597. width: 100% !important;
  598. }
  599. .subpack-slip-doc .subpack-slip-product-grid-cell {
  600. border: ${PRODUCT_GRID_BORDER} !important;
  601. text-align: center !important;
  602. vertical-align: middle !important;
  603. /* padding/line-height 勿 !important:与合同一致,允许 tr 行高拖拽生效 */
  604. padding: ${PRODUCT_GRID_CELL_PADDING};
  605. box-sizing: border-box !important;
  606. line-height: normal;
  607. overflow: visible !important;
  608. white-space: normal !important;
  609. word-wrap: break-word !important;
  610. overflow-wrap: break-word !important;
  611. word-break: break-word !important;
  612. background-color: #fff !important;
  613. }
  614. .subpack-slip-doc th.subpack-slip-product-grid-cell {
  615. padding: ${PRODUCT_GRID_TH_PADDING};
  616. font-weight: bold !important;
  617. }
  618. .subpack-slip-doc .subpack-slip-product-grid-cell > p {
  619. margin: 0 !important;
  620. padding: 0 !important;
  621. text-align: center !important;
  622. line-height: 1.35;
  623. }
  624. .subpack-slip-doc .sx-excel-row-spacer {
  625. display: block !important;
  626. float: left !important;
  627. width: 0 !important;
  628. max-width: 0 !important;
  629. margin: 0 !important;
  630. padding: 0 !important;
  631. border: 0 !important;
  632. overflow: hidden !important;
  633. visibility: hidden !important;
  634. pointer-events: none !important;
  635. line-height: 0 !important;
  636. font-size: 0 !important;
  637. }
  638. .subpack-slip-doc .subpack-slip-product-cell:not(.subpack-slip-product-grid-cell) {
  639. border: ${PRODUCT_GRID_BORDER} !important;
  640. background-color: #fff !important;
  641. padding: 10px 6px 14px 6px !important;
  642. line-height: 1.35 !important;
  643. vertical-align: top !important;
  644. white-space: normal !important;
  645. word-wrap: break-word !important;
  646. overflow-wrap: break-word !important;
  647. word-break: break-word !important;
  648. }
  649. .subpack-slip-doc .subpack-slip-seal-block,
  650. .subpack-slip-doc .subpack-slip-seal-lines,
  651. .subpack-slip-doc .subpack-slip-terms-text {
  652. margin: 0 !important;
  653. padding: 0 !important;
  654. line-height: 1.35 !important;
  655. }
  656. .subpack-slip-doc table.subpack-slip-side-labels {
  657. border: none !important;
  658. border-collapse: collapse !important;
  659. width: auto !important;
  660. margin: 0 auto !important;
  661. }
  662. .subpack-slip-doc td.subpack-slip-side-labels-col,
  663. .subpack-slip-doc table.subpack-slip-side-labels td,
  664. .subpack-slip-doc table.subpack-slip-side-labels th {
  665. border: none !important;
  666. background: transparent !important;
  667. padding: 0 !important;
  668. vertical-align: top !important;
  669. text-align: center !important;
  670. font-size: 18px !important;
  671. line-height: 1.35 !important;
  672. letter-spacing: 0 !important;
  673. white-space: normal !important;
  674. }
  675. `
  676. export const SUBPACK_SLIP_STYLE_BLOCK = `<style type="text/css">${SUBPACK_SLIP_PRINT_CSS}</style>`
  677. /** 生成公司印章 HTML(地址/电话单元格右上角) */
  678. export function buildSubpackSlipCompanySealHtml(): string {
  679. return `<img src="${SUBPACK_SLIP_SEAL_IMAGE_SRC}" alt="印章" style="${SUBPACK_SLIP_SEAL_IMAGE_STYLE}" />`
  680. }
  681. const SUBPACK_SLIP_BUCKET_CHECK_INNER_STYLE =
  682. '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;'
  683. function buildSubpackSlipProductHeaderRowHtml(): string {
  684. return `<tr class="subpack-slip-product-header">
  685. <th class="subpack-slip-product-cell subpack-slip-product-grid-cell" style="${PRODUCT_TH}">序号</th>
  686. <th class="subpack-slip-product-cell subpack-slip-product-grid-cell" style="${PRODUCT_TH}">产品名称</th>
  687. <th class="subpack-slip-product-cell subpack-slip-product-grid-cell" style="${PRODUCT_TH}">产品型号</th>
  688. <th class="subpack-slip-product-cell subpack-slip-product-grid-cell" style="${PRODUCT_TH}">包装规格</th>
  689. <th class="subpack-slip-product-cell subpack-slip-product-grid-cell" style="${PRODUCT_TH}">单位</th>
  690. <th class="subpack-slip-product-cell subpack-slip-product-grid-cell" style="${PRODUCT_TH}">数量</th>
  691. <th class="subpack-slip-product-cell subpack-slip-product-grid-cell" style="${PRODUCT_TH}">是否完成</th>
  692. </tr>`
  693. }
  694. function buildSubpackSlipBucketCheckItem(label: string): string {
  695. return `<label class="subpack-slip-check-item subpack-slip-bucket-check-item" contenteditable="false"><input type="checkbox" class="subpack-slip-check-input" />${label}</label>`
  696. }
  697. function buildSubpackSlipBucketCheckboxesHtml(): string {
  698. return `${buildSubpackSlipBucketCheckItem('全新桶')}${buildSubpackSlipBucketCheckItem('是否回收桶')}${buildSubpackSlipBucketCheckItem('桶循环')}`
  699. }
  700. function buildSubpackSlipSlipNoDisplayHtml(): string {
  701. return `<span class="subpack-slip-slip-no-display">{{subpackSlipNo}}&nbsp;</span>`
  702. }
  703. /** 基本信息列宽:日期略宽;下单人/经手人等宽(与 content_style nth-child 一致) */
  704. const BASIC_INFO_FLEX_DATE = '1.1'
  705. const BASIC_INFO_FLEX_PERSON = '1'
  706. function buildSubpackSlipSlipNoRowHtml(): string {
  707. const spacer = (flex: string) =>
  708. `<div class="subpack-slip-basic-info-item subpack-slip-basic-info-spacer" style="flex:${flex};min-width:0;">&nbsp;</div>`
  709. return `<tr class="subpack-slip-slip-no-row">
  710. <td colspan="7" style="${INFO_CELL_LEFT}">
  711. <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;">
  712. ${spacer(BASIC_INFO_FLEX_DATE)}${spacer(BASIC_INFO_FLEX_DATE)}${spacer(BASIC_INFO_FLEX_PERSON)}
  713. <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;">
  714. <span class="subpack-slip-info-label subpack-slip-slip-no-anchor" style="${INFO_LABEL}">
  715. <span class="subpack-slip-slip-no-anchor-text">经手人:</span>
  716. ${buildSubpackSlipSlipNoDisplayHtml()}
  717. </span>
  718. <span class="subpack-slip-info-value-left" style="${INFO_VALUE_INLINE_LEFT}">&nbsp;</span>
  719. </div>
  720. </div>
  721. </td>
  722. </tr>`
  723. }
  724. function buildSubpackSlipBasicInfoRowHtml(): string {
  725. const field = (label: string, placeholder: string, flex: string = BASIC_INFO_FLEX_PERSON) =>
  726. `<div class="subpack-slip-info-row subpack-slip-basic-info-item" style="${INFO_ROW}flex:${flex};min-width:0;">
  727. <span class="subpack-slip-info-label" style="${INFO_LABEL}">${label}</span>
  728. <span class="subpack-slip-info-value-left" style="${INFO_VALUE_INLINE_LEFT}">${placeholder}&nbsp;</span>
  729. </div>`
  730. return `<tr class="subpack-slip-basic-info-row">
  731. <td colspan="7" style="${INFO_CELL_LEFT}">
  732. <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;">
  733. ${field('下单日期:', '{{orderDate}}', BASIC_INFO_FLEX_DATE)}
  734. ${field('交货日期:', '{{deliveryDate}}', BASIC_INFO_FLEX_DATE)}
  735. ${field('下单人:', '{{orderUser}}', BASIC_INFO_FLEX_PERSON)}
  736. ${field('经手人:', '{{handlerName}}', BASIC_INFO_FLEX_PERSON)}
  737. </div>
  738. </td>
  739. </tr>`
  740. }
  741. function buildSubpackSlipRemarkExtraInputHtml(): string {
  742. return `<span class="subpack-slip-remark-extra-wrap" contenteditable="false"><input type="text" class="subpack-slip-remark-extra-input" value="{{remarkExtra}}" /></span>`
  743. }
  744. function buildSubpackSlipRemarkUnderlineGroupHtml(remarkContent: string = '{{remark}}'): string {
  745. return `<span class="subpack-slip-remark-underline-group"><span class="subpack-slip-remark-value">${remarkContent}&nbsp;</span>${buildSubpackSlipRemarkExtraInputHtml()}</span>`
  746. }
  747. function buildSubpackSlipRemarkRowHtml(): string {
  748. return `<div class="subpack-slip-remark-row-inner">
  749. <span class="subpack-slip-remark-label">备注:</span>
  750. ${buildSubpackSlipRemarkUnderlineGroupHtml()}
  751. <div class="subpack-slip-bucket-check-row" contenteditable="false" style="${SUBPACK_SLIP_BUCKET_CHECK_INNER_STYLE}">${buildSubpackSlipBucketCheckboxesHtml()}</div>
  752. </div>`
  753. }
  754. /** 修复旧模板中 label / 桶选项容器缺少 contenteditable="false" 导致 TinyMCE 内多选失效 */
  755. function ensureSubpackSlipInteractiveMarkup(html: string): string {
  756. let result = html.replace(/<label\b([^>]*\bsubpack-slip-check-item\b[^>]*)>/gi, (match, attrs: string) => {
  757. if (/contenteditable=/i.test(attrs)) return match
  758. return `<label${attrs} contenteditable="false">`
  759. })
  760. result = result.replace(/<div class="subpack-slip-bucket-check-row"([^>]*)>/g, (match, attrs: string) => {
  761. if (/contenteditable=/i.test(attrs)) return match
  762. return `<div class="subpack-slip-bucket-check-row"${attrs} contenteditable="false">`
  763. })
  764. return result
  765. }
  766. /** @deprecated 分装单已移除右侧联次,保留空实现兼容旧引用 */
  767. export function buildSubpackSlipSideLabelsHtml(): string {
  768. return ''
  769. }
  770. /** 生成带占位符的分装单 HTML 模板(方案A:信息 / 物料 / 页脚+签字 分表) */
  771. export function generateSubpackSlipTemplate(_plan?: Plan): string {
  772. 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}">
  773. <!-- 上部分:标题区 -->
  774. <div class="subpack-slip-title-section" style="position:relative;min-height:56px;margin-bottom:14px;">
  775. <img src="/logo.gif" alt="logo" style="position:absolute;left:${LOGO_LEFT}px;top:50%;transform:translateY(-50%);height:${LOGO_HEIGHT}px;width:auto;" />
  776. <div style="text-align:center;padding:4px 60px 0 100px;">
  777. <div class="subpack-slip-title-heading" style="${TITLE_STYLE}">{{companyName}}</div>
  778. <div class="subpack-slip-title-heading" style="${TITLE_SUB_STYLE}">分&nbsp;装&nbsp;单</div>
  779. </div>
  780. </div>
  781. <!-- 1. 基本信息(独立表,不可拖宽高) -->
  782. <table class="subpack-slip-info-table" style="width:100%;border-collapse:collapse;table-layout:fixed;border:none !important;">
  783. ${INFO_COLGROUP}
  784. <!-- 分装单号(右缘与经手人标签右缘对齐) -->
  785. ${buildSubpackSlipSlipNoRowHtml()}
  786. <!-- 基本信息一行四列 -->
  787. ${buildSubpackSlipBasicInfoRowHtml()}
  788. </table>
  789. <div class="subpack-slip-section-gap subpack-slip-gap-before-product" style="${GAP_BEFORE_PRODUCT_CELL}">&nbsp;</div>
  790. <!-- 2. 物料明细(含备注+桶选项) -->
  791. <table class="subpack-slip-product-table" style="width:100%;border-collapse:collapse;table-layout:fixed;border:none !important;">
  792. ${PRODUCT_COLGROUP}
  793. ${buildSubpackSlipProductHeaderRowHtml()}
  794. {{productTable}}
  795. <tr>
  796. <td colspan="7" class="subpack-slip-product-cell subpack-slip-remark-cell" style="${PRODUCT_TABLE_CELL_LEFT}">
  797. ${buildSubpackSlipRemarkRowHtml()}
  798. </td>
  799. </tr>
  800. </table>
  801. <div class="subpack-slip-section-gap subpack-slip-gap-before-sign" style="${GAP_BEFORE_SIGN_CELL}">&nbsp;</div>
  802. <!-- 3. 签字区(独立表) -->
  803. <table class="subpack-slip-sign-table" style="width:100%;border-collapse:collapse;table-layout:fixed;border:none !important;">
  804. ${buildSubpackSlipSignRowHtml()}
  805. </table>
  806. </div>`
  807. }
  808. /** 移除印章、地址/电话行(分装单不再展示) */
  809. function normalizeSubpackSlipRemoveAddressAndSeal(html: string): string {
  810. if (!html.includes('subpack-slip-doc')) return html
  811. let result = html.replace(/<img[^>]*alt="印章"[^>]*>/gi, '')
  812. result = result.replace(/\{\{companySeal\}\}/g, '')
  813. result = result.replace(/<!--\s*印章图片\s*-->\s*/g, '')
  814. result = result.replace(/<!--\s*地址\/电话[\s\S]*?-->\s*/g, '')
  815. result = result.replace(/<!--\s*地址\/印章[\s\S]*?-->\s*/g, '')
  816. result = result.replace(/<div class="subpack-slip-address-block"[\s\S]*?<\/div>/g, '')
  817. result = result.replace(/<div class="subpack-slip-seal-block"[\s\S]*?<\/div>/g, '')
  818. result = result.replace(
  819. /<tr>\s*<td colspan="7"[^>]*>\s*<div class="subpack-slip-address-block"[\s\S]*?<\/tr>\s*/g,
  820. ''
  821. )
  822. result = result.replace(
  823. /<tr>\s*<td colspan="7"[^>]*>\s*<div class="subpack-slip-seal-block"[\s\S]*?<\/tr>\s*/g,
  824. ''
  825. )
  826. result = result.replace(
  827. /<tr>\s*<td colspan="4"[^>]*>\s*&nbsp;\s*<\/td>\s*<td colspan="3"[^>]*>\s*<div class="subpack-slip-seal-block"[\s\S]*?<\/tr>\s*/g,
  828. ''
  829. )
  830. result = result.replace(
  831. /<tr>\s*<!--\s*地址[\s\S]*?<\/tr>\s*/g,
  832. ''
  833. )
  834. result = result.replace(
  835. /<tr>\s*<td colspan="7"[^>]*>[\s\S]*?地址:[\s\S]*?联系人电话:[\s\S]*?<\/tr>\s*/g,
  836. ''
  837. )
  838. return result.replace(/min-height:76px;/g, 'min-height:0;')
  839. }
  840. /** @deprecated 请使用 normalizeSubpackSlipRemoveAddressAndSeal */
  841. function normalizeSubpackSlipSealPlaceholder(html: string): string {
  842. return normalizeSubpackSlipRemoveAddressAndSeal(html)
  843. }
  844. function applyProductGridBorderToStyle(style: string): string {
  845. // 保留拖拽产生的 height / min-height / line-height,避免打开模板时被抹掉
  846. const keptHeight = style.match(/height:\s*[^;]+;?/gi)?.join('') || ''
  847. const keptMinHeight = style.match(/min-height:\s*[^;]+;?/gi)?.join('') || ''
  848. const keptLineHeight = style.match(/line-height:\s*[^;]+;?/gi)?.join('') || ''
  849. let next = style
  850. .replace(/border:[^;]*;?/gi, '')
  851. .replace(/border-right:[^;]*;?/gi, '')
  852. .replace(/border-bottom:[^;]*;?/gi, '')
  853. .replace(/border-left:[^;]*;?/gi, '')
  854. .replace(/border-top:[^;]*;?/gi, '')
  855. .replace(/height:\s*[^;]+;?/gi, '')
  856. .replace(/min-height:\s*[^;]+;?/gi, '')
  857. .replace(/line-height:\s*[^;]+;?/gi, '')
  858. .replace(/text-align:left;/g, 'text-align:center;')
  859. .replace(/vertical-align:top;/g, '')
  860. .trim()
  861. const lineHeight = keptLineHeight || 'line-height:normal;'
  862. return `border:${PRODUCT_GRID_BORDER} !important;text-align:center;padding:${PRODUCT_GRID_CELL_PADDING};box-sizing:border-box;${lineHeight}${keptHeight}${keptMinHeight}${next}`
  863. }
  864. function patchProductFooterCellAttrs(attrs: string, classes: string, style: string): string {
  865. let next = attrs
  866. if (next.includes('class="')) {
  867. next = next.replace(/class="([^"]*)"/, (_, cls: string) => {
  868. let updated = cls.trim()
  869. for (const item of classes.split(/\s+/)) {
  870. if (item && !updated.includes(item)) updated += ` ${item}`
  871. }
  872. return `class="${updated.trim()}"`
  873. })
  874. } else {
  875. next = `${next} class="${classes.trim()}"`
  876. }
  877. if (next.includes('style="')) {
  878. next = next.replace(/style="[^"]*"/, `style="${style}"`)
  879. } else {
  880. next = `${next} style="${style}"`
  881. }
  882. return next
  883. }
  884. /** 物料明细网格:全边框 inline + 原生居中(兼容已保存模板,对齐合同 PDF) */
  885. function patchSubpackSlipProductGridCellAttrs(attrs: string): string {
  886. let next = attrs.includes('class="')
  887. ? attrs.replace(/class="([^"]*)"/, (_, cls: string) => {
  888. let updated = cls
  889. if (!updated.includes('subpack-slip-product-cell')) updated += ' subpack-slip-product-cell'
  890. if (!updated.includes('subpack-slip-product-grid-cell')) updated += ' subpack-slip-product-grid-cell'
  891. return `class="${updated.trim()}"`
  892. })
  893. : `${attrs} class="subpack-slip-product-cell subpack-slip-product-grid-cell"`
  894. if (next.includes('style="')) {
  895. next = next.replace(/style="([^"]*)"/, (_, style: string) => `style="${applyProductGridBorderToStyle(style)}"`)
  896. } else {
  897. next = `${next} style="${PRODUCT_GRID_CELL}"`
  898. }
  899. return next
  900. }
  901. function normalizeSubpackSlipProductGridCell(html: string): string {
  902. if (!html.includes('subpack-slip-doc') || !html.includes('subpack-slip-product-cell')) return html
  903. let result = html.replace(/<tr([^>]*)>\s*(<th[\s\S]*?<\/th>\s*){7}\s*<\/tr>/g, (rowMatch) => {
  904. if (
  905. (!rowMatch.includes('物料编号') && !rowMatch.includes('序号')) ||
  906. rowMatch.includes('colspan')
  907. ) {
  908. return rowMatch
  909. }
  910. let row = rowMatch.includes('subpack-slip-product-header')
  911. ? rowMatch
  912. : rowMatch.replace('<tr', '<tr class="subpack-slip-product-header"')
  913. return row.replace(/<(th)([^>]*)>([\s\S]*?)<\/th>/g, (_m, tag, attrs, content) => {
  914. return `<${tag}${patchSubpackSlipProductGridCellAttrs(attrs)}>${formatSubpackSlipProductGridCellContent(content)}</${tag}>`
  915. })
  916. })
  917. result = result.replace(/<tr([^>]*)>\s*(<td(?![^>]*colspan)[^>]*>[\s\S]*?<\/td>\s*){7}\s*<\/tr>/g, (rowMatch) => {
  918. if (
  919. rowMatch.includes('备注') ||
  920. rowMatch.includes('1.本送货单') ||
  921. rowMatch.includes('制单人:') ||
  922. rowMatch.includes('仓库:') ||
  923. rowMatch.includes('subpack-slip-complete-check-item') ||
  924. rowMatch.includes('暂无产品信息')
  925. ) {
  926. return rowMatch
  927. }
  928. return rowMatch.replace(/<(td)([^>]*)>([\s\S]*?)<\/td>/g, (_m, tag, attrs, content) => {
  929. if (attrs.includes('colspan')) return `<${tag}${attrs}>${content}</td>`
  930. return `<${tag}${patchSubpackSlipProductGridCellAttrs(attrs)}>${formatSubpackSlipProductGridCellContent(content)}</${tag}>`
  931. })
  932. })
  933. return normalizeSubpackSlipGridCellWrappers(result)
  934. }
  935. /** 去掉 TinyMCE / 历史包裹层,单元格内保留纯文本(对齐合同 PDF) */
  936. function normalizeSubpackSlipGridCellWrappers(html: string): string {
  937. if (!html.includes('subpack-slip-product-grid-cell')) return html
  938. const unwrap = (tag: string, attrs: string, content: string) =>
  939. `<${tag}${attrs}>${formatSubpackSlipProductGridCellContent(content)}</${tag}>`
  940. let result = html.replace(
  941. /<(td|th)([^>]*subpack-slip-product-grid-cell[^>]*)>\s*<span class="subpack-slip-product-grid-align"[^>]*>([\s\S]*?)<\/span>\s*<\/\1>/gi,
  942. (_m, tag, attrs, content) => unwrap(tag, attrs, content)
  943. )
  944. result = result.replace(
  945. /<(td|th)([^>]*subpack-slip-product-grid-cell[^>]*)>\s*<span class="subpack-slip-pdf-grid-text"[^>]*>([\s\S]*?)<\/span>\s*<\/\1>/gi,
  946. (_m, tag, attrs, content) => unwrap(tag, attrs, content)
  947. )
  948. result = result.replace(
  949. /<(td|th)([^>]*subpack-slip-product-grid-cell[^>]*)>\s*<div class="subpack-slip-pdf-grid-inner"[^>]*>([\s\S]*?)<\/div>\s*<\/\1>/gi,
  950. (_m, tag, attrs, content) => unwrap(tag, attrs, content)
  951. )
  952. result = result.replace(
  953. /<(td|th)([^>]*subpack-slip-product-grid-cell[^>]*)>\s*<p[^>]*>([\s\S]*?)<\/p>\s*<\/\1>/gi,
  954. (_m, tag, attrs, content) => unwrap(tag, attrs, content)
  955. )
  956. result = result.replace(
  957. /<(td|th)([^>]*subpack-slip-product-grid-cell[^>]*)>\s*<div(?![^>]*class="subpack-slip)[^>]*>([\s\S]*?)<\/div>\s*<\/\1>/gi,
  958. (_m, tag, attrs, content) => unwrap(tag, attrs, content)
  959. )
  960. result = result.replace(/<(td|th)([^>]*subpack-slip-product-grid-cell[^>]*)\s+height="[^"]*"/gi, '<$1$2')
  961. result = result.replace(/<(tr)([^>]*class="[^"]*subpack-slip-product[^"]*"[^>]*)\s+height="[^"]*"/gi, '<$1$2')
  962. return result
  963. }
  964. function extractSubpackSlipGridCellContent(cell: HTMLTableCellElement): string {
  965. let content = cell.innerHTML.trim()
  966. const onlyChild = cell.firstElementChild
  967. if (onlyChild && cell.childElementCount === 1) {
  968. const tag = onlyChild.tagName
  969. if (
  970. tag === 'P' ||
  971. tag === 'DIV' ||
  972. tag === 'SPAN' ||
  973. onlyChild.classList.contains('subpack-slip-pdf-grid-inner') ||
  974. onlyChild.classList.contains('subpack-slip-pdf-grid-text')
  975. ) {
  976. content = onlyChild.innerHTML.trim()
  977. }
  978. }
  979. return formatSubpackSlipProductGridCellContent(content)
  980. }
  981. function applySubpackSlipPdfGridCellStyles(cell: HTMLTableCellElement, isHeader: boolean): void {
  982. const content = extractSubpackSlipGridCellContent(cell)
  983. cell.innerHTML = content || '&nbsp;'
  984. cell.removeAttribute('height')
  985. cell.style.removeProperty('height')
  986. cell.style.removeProperty('min-height')
  987. cell.style.setProperty('padding', isHeader ? PRODUCT_GRID_PDF_TH_PADDING : PRODUCT_GRID_PDF_TD_PADDING, 'important')
  988. cell.style.setProperty('text-align', 'center', 'important')
  989. cell.style.setProperty('font-size', '20px', 'important')
  990. cell.style.setProperty('line-height', PRODUCT_GRID_PDF_LINE_HEIGHT, 'important')
  991. cell.style.setProperty('box-sizing', 'border-box', 'important')
  992. cell.style.setProperty('overflow', 'visible', 'important')
  993. cell.style.setProperty('vertical-align', 'middle', 'important')
  994. if (isHeader) {
  995. cell.style.setProperty('font-weight', 'bold', 'important')
  996. }
  997. }
  998. function applySubpackSlipPdfFooterCellStyles(cell: HTMLTableCellElement): void {
  999. cell.removeAttribute('height')
  1000. cell.style.removeProperty('height')
  1001. cell.style.removeProperty('min-height')
  1002. cell.style.setProperty('padding', PRODUCT_FOOTER_PDF_PADDING, 'important')
  1003. cell.style.setProperty('font-size', '20px', 'important')
  1004. cell.style.setProperty('line-height', PRODUCT_FOOTER_PDF_LINE_HEIGHT, 'important')
  1005. cell.style.setProperty('box-sizing', 'border-box', 'important')
  1006. cell.style.setProperty('overflow', 'visible', 'important')
  1007. cell.style.setProperty('vertical-align', 'top', 'important')
  1008. cell.style.setProperty('text-align', 'left', 'important')
  1009. }
  1010. function unwrapSubpackSlipPdfCellParagraph(cell: HTMLTableCellElement): void {
  1011. const onlyChild = cell.firstElementChild
  1012. if (onlyChild?.tagName === 'P' && cell.childElementCount === 1) {
  1013. cell.innerHTML = onlyChild.innerHTML.trim() || '&nbsp;'
  1014. }
  1015. }
  1016. function prepareSubpackSlipPdfRemarkCell(cell: HTMLTableCellElement): void {
  1017. unwrapSubpackSlipPdfCellParagraph(cell)
  1018. applySubpackSlipPdfFooterCellStyles(cell)
  1019. cell.querySelectorAll('.subpack-slip-remark-underline-group').forEach((node) => {
  1020. const group = node as HTMLElement
  1021. group.style.setProperty('display', 'inline-flex', 'important')
  1022. group.style.setProperty('align-items', 'baseline', 'important')
  1023. group.style.setProperty('flex', '1', 'important')
  1024. group.style.setProperty('min-width', '200px', 'important')
  1025. group.style.setProperty('max-width', '420px', 'important')
  1026. group.style.setProperty('border', 'none', 'important')
  1027. group.style.setProperty('border-bottom', INFO_UNDERLINE, 'important')
  1028. // 与签字区一致:加大底部 padding,避免 html2canvas 把下划线叠在文字上
  1029. group.style.setProperty('padding', SIGN_VALUE_PDF_PADDING, 'important')
  1030. group.style.setProperty('gap', '0', 'important')
  1031. group.style.setProperty('line-height', '1.35', 'important')
  1032. group.style.setProperty('overflow', 'visible', 'important')
  1033. group.style.setProperty('box-sizing', 'border-box', 'important')
  1034. })
  1035. cell.querySelectorAll('.subpack-slip-remark-value').forEach((node) => {
  1036. const el = node as HTMLElement
  1037. el.style.setProperty('display', 'inline-block', 'important')
  1038. el.style.setProperty('border-bottom', 'none', 'important')
  1039. el.style.setProperty('padding', '0 2px', 'important')
  1040. el.style.setProperty('line-height', '1.35', 'important')
  1041. el.style.setProperty('overflow', 'visible', 'important')
  1042. el.style.setProperty('vertical-align', 'baseline', 'important')
  1043. })
  1044. cell.querySelectorAll('input.subpack-slip-remark-extra-input').forEach((node) => {
  1045. const input = node as HTMLInputElement
  1046. const text = (input.value || '').trim()
  1047. const span = document.createElement('span')
  1048. span.className = 'subpack-slip-remark-extra-text-pdf'
  1049. span.textContent = text || '\u00a0'
  1050. span.style.setProperty('display', 'inline-block', 'important')
  1051. span.style.setProperty('min-width', '80px', 'important')
  1052. span.style.setProperty('flex', '1', 'important')
  1053. span.style.setProperty('padding', '0 2px', 'important')
  1054. span.style.setProperty('border-bottom', 'none', 'important')
  1055. span.style.setProperty('font-size', '20px', 'important')
  1056. span.style.setProperty('line-height', '1.35', 'important')
  1057. span.style.setProperty('vertical-align', 'baseline', 'important')
  1058. input.replaceWith(span)
  1059. })
  1060. }
  1061. function prepareSubpackSlipPdfTermsCell(cell: HTMLTableCellElement): void {
  1062. applySubpackSlipPdfFooterCellStyles(cell)
  1063. const terms = cell.querySelector('.subpack-slip-terms-text') as HTMLElement | null
  1064. if (terms) {
  1065. terms.style.setProperty('margin', '0', 'important')
  1066. terms.style.setProperty('padding', '0', 'important')
  1067. terms.style.setProperty('line-height', PRODUCT_FOOTER_PDF_LINE_HEIGHT, 'important')
  1068. }
  1069. }
  1070. function prepareSubpackSlipPdfSealCell(cell: HTMLTableCellElement): void {
  1071. applySubpackSlipPdfFooterCellStyles(cell)
  1072. const block =
  1073. (cell.querySelector('.subpack-slip-address-block') as HTMLElement | null) ||
  1074. (cell.querySelector('.subpack-slip-seal-block') as HTMLElement | null)
  1075. const lines = cell.querySelector('.subpack-slip-seal-lines') as HTMLElement | null
  1076. if (block) {
  1077. block.style.setProperty('padding', '0', 'important')
  1078. block.style.setProperty('margin', '0', 'important')
  1079. block.style.setProperty('position', 'relative', 'important')
  1080. block.style.setProperty('line-height', PRODUCT_FOOTER_PDF_LINE_HEIGHT, 'important')
  1081. }
  1082. if (lines) {
  1083. lines.style.setProperty('margin', '0', 'important')
  1084. lines.style.setProperty('padding', '0', 'important')
  1085. lines.style.setProperty('line-height', PRODUCT_FOOTER_PDF_LINE_HEIGHT, 'important')
  1086. }
  1087. }
  1088. function prepareSubpackSlipPdfSignValues(root: HTMLElement): void {
  1089. root.querySelectorAll('.subpack-slip-sign-value').forEach((node) => {
  1090. const el = node as HTMLElement
  1091. el.style.setProperty('display', 'inline-block', 'important')
  1092. el.style.setProperty('padding', SIGN_VALUE_PDF_PADDING, 'important')
  1093. el.style.setProperty('line-height', '1.35', 'important')
  1094. el.style.setProperty('vertical-align', 'baseline', 'important')
  1095. el.style.setProperty('border', 'none', 'important')
  1096. el.style.setProperty('border-bottom', SIGN_UNDERLINE, 'important')
  1097. el.style.setProperty('box-sizing', 'border-box', 'important')
  1098. el.style.setProperty('min-width', SUBPACK_SLIP_SIGN_VALUE_MIN_WIDTH, 'important')
  1099. })
  1100. }
  1101. /** PDF 截图前:将原生 checkbox 替换为 canvas 方框图 + 文字(html2canvas 对 flex/SVG 对齐不稳定) */
  1102. const subpackSlipPdfCheckboxImgCache: { empty?: string; checked?: string } = {}
  1103. function buildSubpackSlipPdfCheckboxImgDataUrl(checked: boolean): string {
  1104. const key = checked ? 'checked' : 'empty'
  1105. if (subpackSlipPdfCheckboxImgCache[key]) return subpackSlipPdfCheckboxImgCache[key]!
  1106. const size = 32
  1107. const canvas = document.createElement('canvas')
  1108. canvas.width = size
  1109. canvas.height = size
  1110. const ctx = canvas.getContext('2d')
  1111. if (!ctx) return ''
  1112. ctx.fillStyle = '#ffffff'
  1113. ctx.fillRect(0, 0, size, size)
  1114. ctx.strokeStyle = '#000000'
  1115. ctx.lineWidth = 2
  1116. ctx.strokeRect(1, 1, size - 2, size - 2)
  1117. if (checked) {
  1118. ctx.beginPath()
  1119. ctx.moveTo(8, 17)
  1120. ctx.lineTo(14, 23)
  1121. ctx.lineTo(24, 8)
  1122. ctx.strokeStyle = '#000000'
  1123. ctx.lineWidth = 3.5
  1124. ctx.lineCap = 'round'
  1125. ctx.lineJoin = 'round'
  1126. ctx.stroke()
  1127. }
  1128. const url = canvas.toDataURL('image/png')
  1129. subpackSlipPdfCheckboxImgCache[key] = url
  1130. return url
  1131. }
  1132. function buildSubpackSlipPdfCheckItemHtml(checked: boolean, labelText: string, extraClass = ''): string {
  1133. const imgSrc = buildSubpackSlipPdfCheckboxImgDataUrl(checked)
  1134. const className = `subpack-slip-check-item-pdf${extraClass ? ` ${extraClass}` : ''}`
  1135. if (!imgSrc) return `<span class="${className}">${labelText}</span>`
  1136. 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>`
  1137. }
  1138. function applySubpackSlipPdfCheckboxLayout(root: HTMLElement): void {
  1139. root.querySelectorAll('.subpack-slip-bucket-check-row').forEach((node) => {
  1140. const row = node as HTMLElement
  1141. row.style.setProperty('display', 'flex', 'important')
  1142. row.style.setProperty('flex', '1', 'important')
  1143. row.style.setProperty('justify-content', 'center', 'important')
  1144. row.style.setProperty('align-items', 'center', 'important')
  1145. row.style.setProperty('flex-wrap', 'wrap', 'important')
  1146. row.style.setProperty('gap', '14px 18px', 'important')
  1147. row.style.setProperty('line-height', '20px', 'important')
  1148. row.style.setProperty('font-size', '20px', 'important')
  1149. })
  1150. root.querySelectorAll('.subpack-slip-remark-row-inner').forEach((node) => {
  1151. const row = node as HTMLElement
  1152. row.style.setProperty('display', 'flex', 'important')
  1153. row.style.setProperty('align-items', 'center', 'important')
  1154. row.style.setProperty('width', '100%', 'important')
  1155. row.style.setProperty('gap', '12px', 'important')
  1156. })
  1157. root.querySelectorAll('.subpack-slip-check-item-pdf').forEach((node) => {
  1158. const item = node as HTMLElement
  1159. const isComplete = item.classList.contains('subpack-slip-complete-check-item-pdf')
  1160. item.style.setProperty('display', 'inline-block', 'important')
  1161. item.style.setProperty('white-space', 'nowrap', 'important')
  1162. item.style.setProperty('font-family', "SimSun, 'Songti SC', serif", 'important')
  1163. item.style.setProperty('font-size', '20px', 'important')
  1164. item.style.setProperty('line-height', '20px', 'important')
  1165. item.style.setProperty('height', '20px', 'important')
  1166. item.style.setProperty('margin-right', isComplete ? '0' : '18px', 'important')
  1167. item.style.setProperty('vertical-align', 'middle', 'important')
  1168. const img = item.querySelector('img.subpack-slip-check-img-pdf') as HTMLImageElement | null
  1169. if (img) {
  1170. img.style.setProperty('display', 'inline-block', 'important')
  1171. img.style.setProperty('width', '16px', 'important')
  1172. img.style.setProperty('height', '16px', 'important')
  1173. img.style.setProperty('margin', '0 4px 0 0', 'important')
  1174. img.style.setProperty('padding', '0', 'important')
  1175. img.style.setProperty('border', 'none', 'important')
  1176. img.style.setProperty('vertical-align', 'middle', 'important')
  1177. }
  1178. const text = item.querySelector('.subpack-slip-check-text-pdf') as HTMLElement | null
  1179. if (text) {
  1180. text.style.setProperty('display', 'inline-block', 'important')
  1181. text.style.setProperty('font-size', '20px', 'important')
  1182. text.style.setProperty('line-height', '20px', 'important')
  1183. text.style.setProperty('vertical-align', 'middle', 'important')
  1184. text.style.setProperty('position', 'relative', 'important')
  1185. text.style.setProperty('top', '-12px', 'important')
  1186. }
  1187. })
  1188. }
  1189. function prepareSubpackSlipPdfCheckboxes(root: HTMLElement): void {
  1190. root.querySelectorAll('label.subpack-slip-check-item').forEach((node) => {
  1191. const label = node as HTMLElement
  1192. const input = label.querySelector('input.subpack-slip-check-input') as HTMLInputElement | null
  1193. const checked = input?.checked ?? false
  1194. const labelText = (label.textContent || '').replace(/[✓☑□☐]/g, '').trim()
  1195. const extraClass = label.classList.contains('subpack-slip-complete-check-item')
  1196. ? 'subpack-slip-complete-check-item-pdf'
  1197. : label.classList.contains('subpack-slip-bucket-check-item')
  1198. ? 'subpack-slip-bucket-check-item-pdf'
  1199. : ''
  1200. const html = buildSubpackSlipPdfCheckItemHtml(checked, labelText, extraClass)
  1201. const wrapper = document.createElement('div')
  1202. wrapper.innerHTML = html.trim()
  1203. const replacement = wrapper.firstElementChild
  1204. if (replacement) {
  1205. label.replaceWith(replacement)
  1206. }
  1207. })
  1208. applySubpackSlipPdfCheckboxLayout(root)
  1209. }
  1210. /** PDF 截图前:物料网格 + 备注/条款/地址行 + 签字区统一样式 */
  1211. export function prepareSubpackSlipPdfGridCells(root: HTMLElement): void {
  1212. root.querySelectorAll('td.subpack-slip-product-grid-cell, th.subpack-slip-product-grid-cell').forEach((node) => {
  1213. applySubpackSlipPdfGridCellStyles(node as HTMLTableCellElement, node.tagName === 'TH')
  1214. })
  1215. root.querySelectorAll('td.subpack-slip-terms-cell').forEach((node) => {
  1216. prepareSubpackSlipPdfTermsCell(node as HTMLTableCellElement)
  1217. })
  1218. root.querySelectorAll('td.subpack-slip-product-cell:not(.subpack-slip-product-grid-cell)').forEach((node) => {
  1219. const cell = node as HTMLTableCellElement
  1220. if (cell.classList.contains('subpack-slip-terms-cell')) return
  1221. if (cell.querySelector('.subpack-slip-address-block, .subpack-slip-seal-block')) {
  1222. prepareSubpackSlipPdfSealCell(cell)
  1223. return
  1224. }
  1225. if (/备注\s*[::]/.test(cell.textContent || '')) {
  1226. prepareSubpackSlipPdfRemarkCell(cell)
  1227. }
  1228. })
  1229. root.querySelectorAll('tr').forEach((row) => {
  1230. if (!row.querySelector('.subpack-slip-product-cell')) return
  1231. row.removeAttribute('height')
  1232. row.style.removeProperty('height')
  1233. row.style.removeProperty('min-height')
  1234. })
  1235. prepareSubpackSlipPdfSignValues(root)
  1236. prepareSubpackSlipPdfCheckboxes(root)
  1237. // 亚像素 border 在 Windows DPI 下易变粗;截图前改为 1px 背景单线
  1238. applyPdfStableTableCellBorders(root.querySelectorAll('.subpack-slip-product-cell'))
  1239. }
  1240. /** 备注、条款、印章行:统一 0.3px 全边框(兼容已保存模板,与物料网格衔接) */
  1241. function normalizeSubpackSlipProductTableFooter(html: string): string {
  1242. if (!html.includes('subpack-slip-doc')) return html
  1243. let result = html
  1244. result = result.replace(/<td([^>]*colspan="7"[^>]*)>(\s*)备注:/g, (_match, attrs, space) => {
  1245. const patched = patchProductFooterCellAttrs(
  1246. attrs,
  1247. 'subpack-slip-product-cell subpack-slip-remark-cell',
  1248. PRODUCT_TABLE_CELL_LEFT
  1249. )
  1250. if (_match.includes('subpack-slip-remark-row-inner')) return _match
  1251. return `<td${patched}>${space}${buildSubpackSlipRemarkRowHtml()}`
  1252. })
  1253. result = result.replace(
  1254. /<td([^>]*colspan="4"[^>]*)>\s*(<div class="subpack-slip-terms-text"|\s*1\.本送货单)/g,
  1255. (_match, attrs, contentStart) => {
  1256. const patched = patchProductFooterCellAttrs(
  1257. attrs,
  1258. 'subpack-slip-product-cell subpack-slip-terms-cell',
  1259. PRODUCT_TERMS_CELL_LEFT
  1260. )
  1261. return `<td${patched}>\n ${contentStart}`
  1262. }
  1263. )
  1264. result = result.replace(
  1265. /<td([^>]*colspan="3"[^>]*)>\s*<div class="subpack-slip-seal-block"/g,
  1266. (_match, attrs) => {
  1267. const patched = patchProductFooterCellAttrs(attrs, 'subpack-slip-product-cell', PRODUCT_TABLE_CELL_LEFT)
  1268. return `<td${patched}>\n <div class="subpack-slip-seal-block"`
  1269. }
  1270. )
  1271. return result
  1272. }
  1273. /** 条款单元格行高随内容(兼容已保存模板) */
  1274. function normalizeSubpackSlipTermsCell(html: string): string {
  1275. if (!html.includes('subpack-slip-doc')) return html
  1276. const patchTermsTd = (attrs: string) => {
  1277. let nextAttrs = attrs
  1278. if (nextAttrs.includes('class="')) {
  1279. nextAttrs = nextAttrs.replace(/class="([^"]*)"/, (_, cls: string) =>
  1280. cls.includes('subpack-slip-terms-cell') ? `class="${cls}"` : `class="${cls} subpack-slip-terms-cell"`
  1281. )
  1282. } else {
  1283. nextAttrs = `${nextAttrs} class="subpack-slip-product-cell subpack-slip-terms-cell"`
  1284. }
  1285. if (nextAttrs.includes('style="')) {
  1286. nextAttrs = nextAttrs.replace(/style="[^"]*"/, `style="${PRODUCT_TERMS_CELL_LEFT}"`)
  1287. } else {
  1288. nextAttrs = `${nextAttrs} style="${PRODUCT_TERMS_CELL_LEFT}"`
  1289. }
  1290. return nextAttrs
  1291. }
  1292. let result = html.replace(
  1293. /<td colspan="4"([^>]*)>\s*<div class="subpack-slip-terms-text"/g,
  1294. (match, attrs) => {
  1295. if (attrs.includes('subpack-slip-terms-cell') && attrs.includes('border:0.3px solid #000')) return match
  1296. return `<td colspan="4"${patchTermsTd(attrs)}>\n <div class="subpack-slip-terms-text"`
  1297. }
  1298. )
  1299. result = result.replace(
  1300. /<div class="subpack-slip-seal-block" style="([^"]*)"/g,
  1301. (match, style) => {
  1302. if (style.includes('padding:0;') && !style.includes('min-height')) return match
  1303. let nextStyle = style.replace(/min-height:\d+px;/g, '').replace(/padding:[^;]*;/g, '')
  1304. nextStyle = `padding:0;${nextStyle}`
  1305. return `<div class="subpack-slip-seal-block" style="${nextStyle}"`
  1306. }
  1307. )
  1308. return result
  1309. }
  1310. /** 移除旧版送货条款行(兼容已保存模板) */
  1311. function normalizeSubpackSlipRemoveTermsRow(html: string): string {
  1312. if (!html.includes('subpack-slip-doc')) return html
  1313. if (!html.includes('1.本送货单') && !html.includes('subpack-slip-terms-text')) return html
  1314. let result = html.replace(
  1315. /<td colspan="4"([^>]*class="[^"]*subpack-slip-terms-cell[^"]*"[^>]*)>[\s\S]*?<\/td>/g,
  1316. `<td colspan="4" class="subpack-slip-product-cell" style="${PRODUCT_TABLE_CELL_LEFT}">&nbsp;</td>`
  1317. )
  1318. result = result.replace(
  1319. /<td colspan="4"([^>]*)>\s*(<div class="subpack-slip-terms-text"[\s\S]*?<\/div>|1\.本送货单[\s\S]*?)<\/td>/g,
  1320. `<td colspan="4" class="subpack-slip-product-cell" style="${PRODUCT_TABLE_CELL_LEFT}">&nbsp;</td>`
  1321. )
  1322. return result
  1323. }
  1324. /** 刷新已有间距行的 inline 样式(TinyMCE 保存后可能丢失) */
  1325. function refreshSubpackSlipGapBeforeProduct(html: string): string {
  1326. return html
  1327. .replace(
  1328. /<tr class="subpack-slip-gap-before-product">[\s\S]*?<\/tr>/,
  1329. `<tr class="subpack-slip-gap-before-product">\n <td colspan="7" class="subpack-slip-gap-before-product-cell" style="${GAP_BEFORE_PRODUCT_CELL}">&nbsp;</td>\n </tr>`
  1330. )
  1331. .replace(
  1332. /<div class="subpack-slip-section-gap subpack-slip-gap-before-product"[^>]*>[\s\S]*?<\/div>/,
  1333. `<div class="subpack-slip-section-gap subpack-slip-gap-before-product" style="${GAP_BEFORE_PRODUCT_CELL}">&nbsp;</div>`
  1334. )
  1335. }
  1336. /** 在收货联系方式行与物料表之间插入间距行(兼容已保存模板) */
  1337. function normalizeSubpackSlipGapBeforeProduct(html: string): string {
  1338. if (!html.includes('subpack-slip-doc')) {
  1339. return html
  1340. }
  1341. if (html.includes('subpack-slip-gap-before-product')) {
  1342. return refreshSubpackSlipGapBeforeProduct(html)
  1343. }
  1344. const gapRow = `<tr class="subpack-slip-gap-before-product"><td colspan="7" class="subpack-slip-gap-before-product-cell" style="${GAP_BEFORE_PRODUCT_CELL}">&nbsp;</td></tr>\n `
  1345. const withComment = html.replace(
  1346. /(<span class="subpack-slip-info-label"[^>]*>经(?:&nbsp;)?手人:<\/span>[\s\S]*?<\/tr>\s*)(<!--\s*中:产品明细表[\s\S]*?<tr[^>]*>\s*<th[^>]*>(?:序号|物料编号))/,
  1347. `$1${gapRow}$2`
  1348. )
  1349. if (withComment !== html) return withComment
  1350. const withHeader = html.replace(
  1351. /(<span class="subpack-slip-info-label"[^>]*>经(?:&nbsp;)?手人:<\/span>[\s\S]*?<\/tr>\s*)(<tr[^>]*>\s*<th[^>]*>(?:序号|物料编号))/,
  1352. `$1${gapRow}$2`
  1353. )
  1354. if (withHeader !== html) return withHeader
  1355. return html.replace(
  1356. /(下单人:[\s\S]*?<\/tr>\s*)(<!--\s*中:产品明细表[\s\S]*?<tr[^>]*>\s*<th[^>]*>(?:序号|物料编号)|<tr[^>]*>\s*<th[^>]*>(?:序号|物料编号))/,
  1357. `$1${gapRow}$2`
  1358. )
  1359. }
  1360. /** 刷新签字区上方间距行 inline 样式 */
  1361. function refreshSubpackSlipGapBeforeSign(html: string): string {
  1362. return html
  1363. .replace(
  1364. /<tr class="subpack-slip-gap-before-sign">[\s\S]*?<\/tr>/,
  1365. `<tr class="subpack-slip-gap-before-sign">\n <td colspan="7" class="subpack-slip-gap-before-sign-cell" style="${GAP_BEFORE_SIGN_CELL}">&nbsp;</td>\n </tr>`
  1366. )
  1367. .replace(
  1368. /<div class="subpack-slip-section-gap subpack-slip-gap-before-sign"[^>]*>[\s\S]*?<\/div>/,
  1369. `<div class="subpack-slip-section-gap subpack-slip-gap-before-sign" style="${GAP_BEFORE_SIGN_CELL}">&nbsp;</div>`
  1370. )
  1371. }
  1372. /** 在物料表与签字区之间插入间距行(兼容已保存模板) */
  1373. function normalizeSubpackSlipGapBeforeSign(html: string): string {
  1374. if (!html.includes('subpack-slip-doc')) return html
  1375. if (html.includes('subpack-slip-gap-before-sign')) {
  1376. return refreshSubpackSlipGapBeforeSign(html)
  1377. }
  1378. const gapRow = `<tr class="subpack-slip-gap-before-sign"><td colspan="7" class="subpack-slip-gap-before-sign-cell" style="${GAP_BEFORE_SIGN_CELL}">&nbsp;</td></tr>\n\n `
  1379. const withComment = html.replace(
  1380. /(<\/tr>\s*)(<!--\s*下:签字区[\s\S]*?<tr[^>]*>\s*<td colspan="7"[^>]*>\s*(?:<div class="subpack-slip-sign-inner">\s*)?制单人:)/,
  1381. `$1${gapRow}$2`
  1382. )
  1383. if (withComment !== html) return withComment
  1384. const withLegacy = html.replace(
  1385. /(<\/tr>\s*)(<!--\s*下:签字区[\s\S]*?<tr[^>]*>\s*<td colspan="7"[^>]*>\s*(?:<div class="subpack-slip-sign-inner">\s*)?仓库:)/,
  1386. `$1${gapRow}$2`
  1387. )
  1388. if (withLegacy !== html) return withLegacy
  1389. return html.replace(
  1390. /(subpack-slip-remark-cell[\s\S]*?<\/tr>\s*)(<tr[^>]*>\s*<td colspan="7"[^>]*>\s*(?:<div class="subpack-slip-sign-inner">\s*)?(?:制单人:|仓库:))/,
  1391. `$1${gapRow}$2`
  1392. )
  1393. }
  1394. /** 签字区标签/下划线 baseline 对齐(兼容已保存模板) */
  1395. function normalizeSubpackSlipSignLabelMarkup(html: string): string {
  1396. if (!html.includes('subpack-slip-sign-value')) return html
  1397. let result = html.replace(
  1398. /<span class="subpack-slip-sign-item">(制单人:|审核:|生产主管:|仓库:|客户签收:|司机\/送货人\(附车牌\):)\s*(<span class="subpack-slip-sign-value")/g,
  1399. '<span class="subpack-slip-sign-item"><span class="subpack-slip-sign-label">$1</span>$2'
  1400. )
  1401. result = result.replace(/<span class="subpack-slip-sign-value" style="([^"]*)"/g, (match, style) => {
  1402. const nextStyle = style
  1403. .replace(/display:inline-block;?/g, '')
  1404. .replace(/padding:[^;]*;?/g, '')
  1405. .replace(/vertical-align:[^;]*;?/g, '')
  1406. .replace(/border:none !important;?/g, '')
  1407. .replace(/border-bottom:[^;]*;?/g, '')
  1408. .replace(/min-width:\s*[^;]*;?/g, '')
  1409. .replace(/;\s*;/g, ';')
  1410. .replace(/^;|;$/g, '')
  1411. .trim()
  1412. const minWidthStyle = `min-width:${SUBPACK_SLIP_SIGN_VALUE_MIN_WIDTH};`
  1413. const mergedStyle = nextStyle ? `${nextStyle};${minWidthStyle}` : minWidthStyle
  1414. return `<span class="subpack-slip-sign-value" style="${mergedStyle}"`
  1415. })
  1416. result = result.replace(
  1417. /(<span class="subpack-slip-sign-label">制单人:<\/span>\s*<span class="subpack-slip-sign-value"[^>]*>)\s*(?:&nbsp;|\s)*<\/span>/g,
  1418. '$1{{preparerUser}}&nbsp;</span>'
  1419. )
  1420. return result
  1421. }
  1422. /** 签字区 flex 布局(兼容已保存模板,避免打印预览溢出到右侧联次列) */
  1423. function normalizeSubpackSlipSignRow(html: string): string {
  1424. if (!html.includes('subpack-slip-doc')) return html
  1425. if (
  1426. !html.includes('制单人:') &&
  1427. !html.includes('仓库:') &&
  1428. !html.includes('客户签收:') &&
  1429. !html.includes('司机/送货人')
  1430. ) {
  1431. return html
  1432. }
  1433. if (html.includes('制单人:') && html.includes('subpack-slip-sign-inner') && !html.includes('司机/送货人')) {
  1434. return normalizeSubpackSlipSignLabelMarkup(html)
  1435. }
  1436. let result = html
  1437. if (html.includes('仓库:') || html.includes('客户签收:') || html.includes('司机/送货人')) {
  1438. result = result.replace(
  1439. /<tr class="subpack-slip-sign-row"[\s\S]*?<\/tr>/,
  1440. buildSubpackSlipSignRowHtml()
  1441. )
  1442. result = result.replace(
  1443. /<tr>\s*<td colspan="7"[^>]*>[\s\S]*?(?:仓库:|制单人:)[\s\S]*?(?:司机\/送货人|生产主管)[\s\S]*?<\/td>\s*<\/tr>/,
  1444. buildSubpackSlipSignRowHtml()
  1445. )
  1446. }
  1447. result = result.replace(
  1448. /<tr([^>]*)>\s*<td colspan="7"([^>]*)>([\s\S]*?(?:制单人:|仓库:)[\s\S]*?)<\/td>\s*<\/tr>/g,
  1449. (match, trAttrs, tdAttrs, content) => {
  1450. if (!content.includes('subpack-slip-sign-value') || content.includes('subpack-slip-sign-inner')) {
  1451. return match
  1452. }
  1453. const wrappedContent = content
  1454. .replace(/&nbsp;&nbsp;/g, '')
  1455. .replace(
  1456. /(制单人:|审核:|生产主管:|仓库:|客户签收:|司机\/送货人\(附车牌\):)\s*(<span class="subpack-slip-sign-value"[\s\S]*?<\/span>)/g,
  1457. '<span class="subpack-slip-sign-item"><span class="subpack-slip-sign-label">$1</span>$2</span>'
  1458. )
  1459. const trClassAttr = trAttrs.includes('class="')
  1460. ? trAttrs.replace(/class="([^"]*)"/, (_, cls: string) =>
  1461. cls.includes('subpack-slip-sign-row') ? `class="${cls}"` : `class="${cls} subpack-slip-sign-row"`
  1462. )
  1463. : `${trAttrs} class="subpack-slip-sign-row"`
  1464. let nextTdAttrs = tdAttrs
  1465. .replace(/white-space:nowrap;?/g, '')
  1466. .replace(/font-size:(?:14|16)px;?/g, 'font-size:18px;')
  1467. if (nextTdAttrs.includes('class="')) {
  1468. nextTdAttrs = nextTdAttrs.replace(/class="([^"]*)"/, (_, cls: string) =>
  1469. cls.includes('subpack-slip-sign-row-cell') ? `class="${cls}"` : `class="${cls} subpack-slip-sign-row-cell"`
  1470. )
  1471. } else {
  1472. nextTdAttrs = `${nextTdAttrs} class="subpack-slip-sign-row-cell"`
  1473. }
  1474. return `<tr${trClassAttr}>\n <td colspan="7"${nextTdAttrs}>\n <div class="subpack-slip-sign-inner">${wrappedContent.trim()}</div>\n </td>\n </tr>`
  1475. }
  1476. )
  1477. result = result.replace(
  1478. /<td(?![^>]*class="[^"]*subpack-slip-main-col)([^>]*style="[^"]*vertical-align:top;padding-right:6px)/g,
  1479. '<td class="subpack-slip-main-col"$1'
  1480. )
  1481. return normalizeSubpackSlipSignLabelMarkup(result)
  1482. }
  1483. /**
  1484. * 旧版一体 layout-table / 曾拆出的 footer-table → 信息表 + 物料表(含备注) + 签字表。
  1485. */
  1486. function migrateSubpackSlipSplitTables(html: string): string {
  1487. if (!html.includes('subpack-slip-doc')) return html
  1488. if (
  1489. html.includes('subpack-slip-product-table') &&
  1490. !html.includes('subpack-slip-footer-table') &&
  1491. /subpack-slip-product-table[\s\S]*?(?:subpack-slip-remark-cell|备注:)/.test(html)
  1492. ) {
  1493. return html
  1494. }
  1495. if (html.includes('subpack-slip-footer-table') && html.includes('subpack-slip-product-table')) {
  1496. return html.replace(
  1497. /(<\/tr>\s*)<\/table>\s*<table class="subpack-slip-footer-table"[^>]*>\s*(?:<colgroup>[\s\S]*?<\/colgroup>\s*)?([\s\S]*?)<\/table>/i,
  1498. (_m, beforeClose, footerInner) => {
  1499. const rows = footerInner.replace(/^\s*<colgroup>[\s\S]*?<\/colgroup>\s*/i, '').trim()
  1500. return `${beforeClose}${rows}\n </table>`
  1501. }
  1502. )
  1503. }
  1504. if (!html.includes('subpack-slip-layout-table')) return html
  1505. return html.replace(
  1506. /<table class="subpack-slip-layout-table"[^>]*>([\s\S]*?)<\/table>/i,
  1507. (_full, inner: string) => {
  1508. const headerMatch =
  1509. inner.match(/<tr[^>]*class="[^"]*subpack-slip-product-header[^"]*"[\s\S]*?<\/tr>/i) ||
  1510. inner.match(/<tr>\s*<th[^>]*>(?:序号|物料编号)[\s\S]*?<\/tr>/i)
  1511. if (!headerMatch) return _full
  1512. const headerIdx = inner.indexOf(headerMatch[0])
  1513. let before = inner.slice(0, headerIdx)
  1514. before = before.replace(/<colgroup>[\s\S]*?<\/colgroup>/i, '')
  1515. before = before.replace(/<tr class="subpack-slip-gap-before-product">[\s\S]*?<\/tr>/i, '')
  1516. const afterHeader = inner.slice(headerIdx + headerMatch[0].length)
  1517. const signMatch =
  1518. afterHeader.match(/<tr[^>]*class="[^"]*subpack-slip-sign-row[^"]*"[\s\S]*?<\/tr>/i) ||
  1519. afterHeader.match(/<tr>\s*<td[^>]*>[\s\S]*?(?:制单人:|仓库:)[\s\S]*?<\/tr>/i)
  1520. let productAndFooter = afterHeader
  1521. let signPart = buildSubpackSlipSignRowHtml()
  1522. if (signMatch) {
  1523. const signAt = afterHeader.indexOf(signMatch[0])
  1524. productAndFooter = afterHeader.slice(0, signAt)
  1525. signPart = signMatch[0]
  1526. }
  1527. productAndFooter = productAndFooter.replace(
  1528. /<tr class="subpack-slip-gap-before-sign">[\s\S]*?<\/tr>/i,
  1529. ''
  1530. )
  1531. return `<table class="subpack-slip-info-table" style="width:100%;border-collapse:collapse;table-layout:fixed;border:none !important;">
  1532. ${INFO_COLGROUP}
  1533. ${before.trim()}
  1534. </table>
  1535. <div class="subpack-slip-section-gap subpack-slip-gap-before-product" style="${GAP_BEFORE_PRODUCT_CELL}">&nbsp;</div>
  1536. <table class="subpack-slip-product-table" style="width:100%;border-collapse:collapse;table-layout:fixed;border:none !important;">
  1537. ${PRODUCT_COLGROUP}
  1538. ${headerMatch[0]}
  1539. ${productAndFooter.trim()}
  1540. </table>
  1541. <div class="subpack-slip-section-gap subpack-slip-gap-before-sign" style="${GAP_BEFORE_SIGN_CELL}">&nbsp;</div>
  1542. <table class="subpack-slip-sign-table" style="width:100%;border-collapse:collapse;table-layout:fixed;border:none !important;">
  1543. ${signPart.trim()}
  1544. </table>`
  1545. }
  1546. )
  1547. }
  1548. /** 物料表列宽(兼容已保存模板) */
  1549. function normalizeSubpackSlipProductColWidths(html: string): string {
  1550. if (!html.includes('subpack-slip-product-table')) return html
  1551. return html.replace(
  1552. /(<table class="subpack-slip-product-table"[^>]*>\s*)<colgroup>[\s\S]*?<\/colgroup>/i,
  1553. `$1${PRODUCT_COLGROUP}`
  1554. )
  1555. }
  1556. /** 统一单据左右 10px 间距、内容区宽度(兼容已保存模板) */
  1557. function normalizeSubpackSlipDocLayout(html: string): string {
  1558. let result = migrateSubpackSlipSplitTables(html)
  1559. .replace(/padding:8px 12px 24px 4px;/g, `padding:${DOC_PADDING};`)
  1560. .replace(/padding:8px 12px 24px;/g, `padding:${DOC_PADDING};`)
  1561. .replace(/padding-left: 4px !important;/g, `padding: ${DOC_PADDING} !important;`)
  1562. .replace(/left:0;top:50%/g, `left:${LOGO_LEFT}px;top:50%`)
  1563. .replace(/left:20px;/g, `left:${LOGO_LEFT}px;`)
  1564. .replace(/padding:4px 80px 0 120px;/g, 'padding:4px 60px 0 100px;')
  1565. .replace(/padding:4px 72px 0 88px;/g, 'padding:4px 60px 0 100px;')
  1566. .replace(/max-width:720px/g, `max-width:${SUBPACK_SLIP_DOC_MAX_WIDTH}`)
  1567. .replace(/max-width:760px/g, `max-width:${SUBPACK_SLIP_DOC_MAX_WIDTH}`)
  1568. .replace(/max-width:794px/g, `max-width:${SUBPACK_SLIP_DOC_MAX_WIDTH}`)
  1569. .replace(/height:46px;/g, `height:${LOGO_HEIGHT}px;`)
  1570. .replace(/height:23px;/g, `height:${LOGO_HEIGHT}px;`)
  1571. .replace(/min-height:72px;/g, 'min-height:56px;')
  1572. .replace(/min-height:48px;/g, 'min-height:56px;')
  1573. .replace(/padding:4px 60px 0 68px;/g, 'padding:4px 60px 0 100px;')
  1574. .replace(/padding:4px 60px 0 52px;/g, 'padding:4px 60px 0 100px;')
  1575. .replace(/padding:1px 6px;/g, `padding:${TABLE_ROW_PADDING};`)
  1576. .replace(/padding:4px 6px;/g, `padding:${TABLE_ROW_PADDING};`)
  1577. .replace(/padding-bottom:5px;/g, '')
  1578. .replace(/padding-bottom:10px;/g, '')
  1579. .replace(/padding:18px 6px 8px;/g, `padding:${TABLE_ROW_PADDING};`)
  1580. .replace(/margin-top:6px;/g, 'margin-top:2px;')
  1581. .replace(/line-height:1\.6;/g, 'line-height:1.35;')
  1582. .replace(/line-height:1\.9;/g, 'line-height:1.35;')
  1583. .replace(/min-height:88px;/g, 'min-height:0;')
  1584. .replace(/min-height:36px;/g, '')
  1585. .replace(/min-height:76px;/g, 'min-height:0;')
  1586. .replace(/min-height:120px;/g, 'min-height:0;')
  1587. .replace(/top:4px !important;/g, `top:${SUBPACK_SLIP_SEAL_TOP}px !important;`)
  1588. .replace(/top:4px;/g, `top:${SUBPACK_SLIP_SEAL_TOP}px;`)
  1589. .replace(/>日期:</g, '>日&nbsp;期:<')
  1590. .replace(/border-bottom:2px solid #000 !important;/g, `border-bottom:${INFO_UNDERLINE} !important;`)
  1591. .replace(/border-bottom:1px solid #000 !important;/g, `border-bottom:${INFO_UNDERLINE} !important;`)
  1592. .replace(/border:2px solid #000 !important;/g, `border:${PRODUCT_GRID_BORDER} !important;`)
  1593. .replace(/border:1px solid #000 !important;/g, `border:${PRODUCT_GRID_BORDER} !important;`)
  1594. .replace(
  1595. /display:inline-block;(?:min-width:\d+px;)?padding:0 2px 1px;vertical-align:bottom;border:none !important;/g,
  1596. ''
  1597. )
  1598. .replace(
  1599. /display:inline-block;padding:0 2px 2px;vertical-align:bottom;border:none !important;border-bottom:1px solid #000 !important;/g,
  1600. ''
  1601. )
  1602. .replace(
  1603. /(制单人:|审核:|生产主管:|仓库:|客户签收:|司机\/送货人\(附车牌\):)<span(?![^>]*class="subpack-slip-sign-value") style="/g,
  1604. '$1<span class="subpack-slip-sign-value" style="'
  1605. )
  1606. .replace(
  1607. /(<span class="subpack-slip-sign-value" style="[^"]*)border-bottom:2px solid #000 !important;/g,
  1608. `$1border-bottom:${SIGN_UNDERLINE} !important;`
  1609. )
  1610. .replace(/class="subpack-slip-info-before-product"/g, '')
  1611. .replace(/padding-bottom:12px;/g, '')
  1612. .replace(/padding-bottom:24px;/g, '')
  1613. if (result.includes('subpack-slip-doc') && !result.includes('max-width:')) {
  1614. result = result.replace(
  1615. /(<div class="subpack-slip-doc" style=")([^"]*)"/,
  1616. `$1$2;${DOC_WRAP_EXTRA}"`
  1617. )
  1618. }
  1619. if (result.includes('subpack-slip-doc')) {
  1620. result = result.replace(/<style type="text\/css">[\s\S]*?<\/style>/, SUBPACK_SLIP_STYLE_BLOCK)
  1621. result = normalizeSubpackSlipSignLabelMarkup(result)
  1622. result = normalizeSubpackSlipProductGridCell(result)
  1623. result = normalizeSubpackSlipProductTableFooter(result)
  1624. result = normalizeSubpackSlipSlipNoRow(result)
  1625. }
  1626. return result
  1627. }
  1628. /** 基本信息字段与标题(兼容从送(提)货单复制的旧模板) */
  1629. function normalizeSubpackSlipBasicInfoFields(html: string): string {
  1630. if (!html.includes('subpack-slip-doc')) return html
  1631. let result = html
  1632. result = result.replace(/送&nbsp;\(提\)&nbsp;货&nbsp;单/g, '分&nbsp;装&nbsp;单')
  1633. result = result.replace(/送\(提\)货单/g, '分装单')
  1634. result = result.replace(/收货单位:/g, '下单日期:')
  1635. result = result.replace(/单据编号:/g, '交货日期:')
  1636. result = result.replace(/收货联系方式:/g, '下单人:')
  1637. result = result.replace(/日&nbsp;期:/g, '经手人:')
  1638. result = result.replace(/>日期:</g, '>经手人:<')
  1639. result = result.replace(/\{\{receivingUnit\}\}/g, '{{orderDate}}')
  1640. result = result.replace(/\{\{planNo\}\}/g, '{{deliveryDate}}')
  1641. result = result.replace(/\{\{receiveContact\}\}/g, '{{orderUser}}')
  1642. result = result.replace(/\{\{planDateChinese\}\}/g, '{{handlerName}}')
  1643. result = result.replace(/\{\{driverInfo\}\}/g, '{{handlerName}}')
  1644. result = result.replace(
  1645. /<div class="subpack-slip-info-row subpack-slip-type-check-wrap"[\s\S]*?<\/div>\s*/g,
  1646. ''
  1647. )
  1648. result = result.replace(/<div class="subpack-slip-type-check-row"[\s\S]*?<\/div>\s*/g, '')
  1649. return result
  1650. }
  1651. /** 物料表头(兼容旧版 物料编号/实收数量 表头) */
  1652. function normalizeSubpackSlipProductHeader(html: string): string {
  1653. if (!html.includes('subpack-slip-doc')) return html
  1654. if (html.includes('>序号<') && html.includes('>是否完成<')) return html
  1655. const headerHtml = buildSubpackSlipProductHeaderRowHtml()
  1656. const withClass = html.replace(/<tr class="subpack-slip-product-header"[\s\S]*?<\/tr>/, headerHtml)
  1657. if (withClass !== html) return withClass
  1658. return html.replace(
  1659. /<tr>\s*<th[^>]*>物料编号<\/th>[\s\S]*?<th[^>]*>实收数量<\/th>\s*<\/tr>/,
  1660. headerHtml
  1661. )
  1662. }
  1663. /** 备注行 + 桶选项多选框(兼容旧版纯文本备注行) */
  1664. function normalizeSubpackSlipRemarkRow(html: string): string {
  1665. if (!html.includes('subpack-slip-doc')) return html
  1666. if (html.includes('subpack-slip-remark-row-inner')) {
  1667. let result = html
  1668. // 旧版:备注正文与补充输入分开,升级为同一条连续下划线
  1669. if (!result.includes('subpack-slip-remark-underline-group')) {
  1670. const extractRemark = (raw: string) =>
  1671. String(raw || '')
  1672. .replace(/<[^>]+>/g, '')
  1673. .replace(/备注:/g, '')
  1674. .replace(/&nbsp;/g, '')
  1675. .trim() || '{{remark}}'
  1676. result = result.replace(
  1677. /<div class="subpack-slip-remark-row-inner">([\s\S]*?)<div class="subpack-slip-bucket-check-row"/,
  1678. (_m, middle: string) => {
  1679. const valueMatch = String(middle).match(
  1680. /class="subpack-slip-remark-value"[^>]*>([\s\S]*?)<\/span>/
  1681. )
  1682. const plainMatch = String(middle).match(/备注:([^<]*)/)
  1683. const content = extractRemark(valueMatch?.[1] ?? plainMatch?.[1] ?? '{{remark}}')
  1684. const extraMatch = String(middle).match(
  1685. /<input type="text" class="subpack-slip-remark-extra-input"([^>]*)\/?>/
  1686. )
  1687. const extraHtml = extraMatch
  1688. ? `<span class="subpack-slip-remark-extra-wrap" contenteditable="false"><input type="text" class="subpack-slip-remark-extra-input"${extraMatch[1]} /></span>`
  1689. : buildSubpackSlipRemarkExtraInputHtml()
  1690. return `<div class="subpack-slip-remark-row-inner">
  1691. <span class="subpack-slip-remark-label">备注:</span>
  1692. <span class="subpack-slip-remark-underline-group"><span class="subpack-slip-remark-value">${content}&nbsp;</span>${extraHtml}</span>
  1693. <div class="subpack-slip-bucket-check-row"`
  1694. }
  1695. )
  1696. }
  1697. if (!result.includes('subpack-slip-remark-extra-input')) {
  1698. result = result.replace(
  1699. /(<div class="subpack-slip-remark-row-inner">[\s\S]*?)(\s*<div class="subpack-slip-bucket-check-row")/,
  1700. `$1\n ${buildSubpackSlipRemarkUnderlineGroupHtml()}$2`
  1701. )
  1702. } else if (!result.includes('subpack-slip-remark-extra-wrap')) {
  1703. result = result.replace(
  1704. /<input type="text" class="subpack-slip-remark-extra-input"([^>]*)\/?>/g,
  1705. `<span class="subpack-slip-remark-extra-wrap" contenteditable="false"><input type="text" class="subpack-slip-remark-extra-input"$1 /></span>`
  1706. )
  1707. }
  1708. return ensureSubpackSlipInteractiveMarkup(result)
  1709. }
  1710. return html.replace(
  1711. /(<td[^>]*colspan="7"[^>]*>\s*)备注:(\{\{remark\}\}|[^<]*?)(\s*<\/td>)/,
  1712. `$1${buildSubpackSlipRemarkRowHtml()}$3`
  1713. )
  1714. }
  1715. /** 移除右侧联次列,并解包旧版 outer-table 布局 */
  1716. function normalizeSubpackSlipRemoveSideLabels(html: string): string {
  1717. if (!html.includes('subpack-slip-doc')) return html
  1718. let result = html
  1719. result = result.replace(/<table class="subpack-slip-side-labels"[\s\S]*?<\/table>/g, '')
  1720. result = result.replace(/\{\{sideLabels\}\}/g, '')
  1721. result = result.replace(
  1722. /<!--\s*最右侧纵向联次\s*-->\s*<td class="subpack-slip-side-labels-col"[\s\S]*?<\/td>/g,
  1723. ''
  1724. )
  1725. result = result.replace(/<td class="subpack-slip-side-labels-col"[^>]*>[\s\S]*?<\/td>/g, '')
  1726. if (result.includes('subpack-slip-outer-table') && result.includes('subpack-slip-main-col')) {
  1727. result = result.replace(
  1728. /<table class="subpack-slip-outer-table" style="[^"]*">\s*<tr>\s*<td class="subpack-slip-main-col" style="[^"]*">/,
  1729. ''
  1730. )
  1731. result = result.replace(/<\/td>\s*<\/tr>\s*<\/table>(?=\s*<\/div>)/, '')
  1732. }
  1733. return result
  1734. }
  1735. /** 基本信息合并为一行四列(兼容旧版两行布局) */
  1736. function normalizeSubpackSlipBasicInfoOneRow(html: string): string {
  1737. if (!html.includes('subpack-slip-doc')) return html
  1738. if (html.includes('subpack-slip-basic-info-row')) return html
  1739. const basicInfoRow = buildSubpackSlipBasicInfoRowHtml()
  1740. const replaced = html.replace(
  1741. /<!--\s*上:基本信息两行\s*-->\s*<tr>[\s\S]*?<\/tr>\s*<tr>[\s\S]*?<\/tr>/,
  1742. `<!-- 基本信息一行 -->\n ${basicInfoRow}`
  1743. )
  1744. if (replaced !== html) return replaced
  1745. const fallback = html.replace(
  1746. /<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>/,
  1747. basicInfoRow
  1748. )
  1749. return fallback
  1750. }
  1751. /** 分装单号与经手人标签右缘对齐(兼容旧版标题区居中布局) */
  1752. function normalizeSubpackSlipSlipNoRow(html: string): string {
  1753. if (!html.includes('subpack-slip-doc')) return html
  1754. let result = html
  1755. result = result.replace(/<div class="subpack-slip-info-row subpack-slip-slip-no-wrap"[\s\S]*?<\/div>\s*/g, '')
  1756. result = result.replace(/<span class="subpack-slip-info-label"[^>]*>分装单号:<\/span>\s*/g, '')
  1757. result = result.replace(
  1758. /<div class="subpack-slip-slip-no-display"[^>]*>([\s\S]*?)<\/div>/g,
  1759. '<span class="subpack-slip-slip-no-display">$1</span>'
  1760. )
  1761. if (result.includes('subpack-slip-title-section') && result.includes('subpack-slip-slip-no-display')) {
  1762. result = result.replace(
  1763. /\s*<span class="subpack-slip-slip-no-display"[^>]*>[\s\S]*?<\/span>\s*(?=<\/div>\s*<\/div>\s*<table class="subpack-slip-(?:layout|info)-table")/,
  1764. '\n'
  1765. )
  1766. result = result.replace(
  1767. /<div class="subpack-slip-slip-no-display"[^>]*>[\s\S]*?<\/div>\s*(?=<\/div>\s*<\/div>\s*<table class="subpack-slip-(?:layout|info)-table")/,
  1768. ''
  1769. )
  1770. }
  1771. if (!result.includes('subpack-slip-slip-no-row')) {
  1772. result = result.replace(
  1773. /(<table class="subpack-slip-(?:layout|info)-table"[\s\S]*?<colgroup>[\s\S]*?<\/colgroup>\s*)/,
  1774. `$1\n ${buildSubpackSlipSlipNoRowHtml()}\n\n `
  1775. )
  1776. }
  1777. if (result.includes('subpack-slip-slip-no-row') && !result.includes('subpack-slip-slip-no-anchor')) {
  1778. result = result.replace(
  1779. /<tr class="subpack-slip-slip-no-row"[\s\S]*?<\/tr>\s*/,
  1780. `${buildSubpackSlipSlipNoRowHtml()}\n`
  1781. )
  1782. }
  1783. return ensureSubpackSlipInteractiveMarkup(result)
  1784. }
  1785. const SUBPACK_SLIP_CHECK_DELEGATE_ATTR = 'data-subpack-slip-check-delegate'
  1786. const SUBPACK_SLIP_REMARK_EXTRA_BOUND_ATTR = 'data-subpack-slip-remark-extra-bound'
  1787. function syncSubpackSlipRemarkExtraInputValue(input: HTMLInputElement): void {
  1788. input.setAttribute('value', input.value)
  1789. }
  1790. /** 富文本编辑器内:允许备注补充输入框获得焦点并输入(TinyMCE 默认会拦截) */
  1791. export function bindSubpackSlipRemarkExtraInputInteraction(
  1792. root: HTMLElement | null | undefined,
  1793. onChange?: () => void
  1794. ): void {
  1795. if (!root) return
  1796. root.querySelectorAll('input.subpack-slip-remark-extra-input').forEach((node) => {
  1797. const input = node as HTMLInputElement
  1798. const wrap = input.closest('.subpack-slip-remark-extra-wrap') as HTMLElement | null
  1799. if (wrap) {
  1800. wrap.setAttribute('contenteditable', 'false')
  1801. }
  1802. input.removeAttribute('readonly')
  1803. input.removeAttribute('disabled')
  1804. syncSubpackSlipRemarkExtraInputValue(input)
  1805. if (input.getAttribute(SUBPACK_SLIP_REMARK_EXTRA_BOUND_ATTR) === '1') return
  1806. input.setAttribute(SUBPACK_SLIP_REMARK_EXTRA_BOUND_ATTR, '1')
  1807. const stopBubble = (e: Event) => {
  1808. e.stopPropagation()
  1809. }
  1810. input.addEventListener('mousedown', stopBubble, true)
  1811. input.addEventListener('click', stopBubble, true)
  1812. input.addEventListener('keydown', stopBubble, true)
  1813. input.addEventListener('keyup', stopBubble, true)
  1814. input.addEventListener('keypress', stopBubble, true)
  1815. input.addEventListener('focus', stopBubble, true)
  1816. input.addEventListener('input', () => {
  1817. syncSubpackSlipRemarkExtraInputValue(input)
  1818. onChange?.()
  1819. })
  1820. input.addEventListener('blur', () => {
  1821. syncSubpackSlipRemarkExtraInputValue(input)
  1822. onChange?.()
  1823. })
  1824. })
  1825. }
  1826. function syncSubpackSlipCheckboxInput(input: HTMLInputElement): void {
  1827. if (input.checked) {
  1828. input.setAttribute('checked', 'checked')
  1829. } else {
  1830. input.removeAttribute('checked')
  1831. }
  1832. }
  1833. /** 富文本编辑器内:绑定多选框点击(TinyMCE 默认可编辑区会拦截原生 checkbox) */
  1834. export function bindSubpackSlipCheckboxInteraction(
  1835. root: HTMLElement | null | undefined,
  1836. onChange?: () => void
  1837. ): void {
  1838. if (!root) return
  1839. root.querySelectorAll('label.subpack-slip-check-item').forEach((node) => {
  1840. const label = node as HTMLElement
  1841. label.setAttribute('contenteditable', 'false')
  1842. const input = label.querySelector('input.subpack-slip-check-input') as HTMLInputElement | null
  1843. if (input) syncSubpackSlipCheckboxInput(input)
  1844. })
  1845. if (root.getAttribute(SUBPACK_SLIP_CHECK_DELEGATE_ATTR) === '1') return
  1846. root.setAttribute(SUBPACK_SLIP_CHECK_DELEGATE_ATTR, '1')
  1847. root.addEventListener(
  1848. 'mousedown',
  1849. (event) => {
  1850. if (root.isContentEditable === false) return
  1851. const target = event.target as HTMLElement | null
  1852. if (!target) return
  1853. const label = target.closest('label.subpack-slip-check-item')
  1854. if (!label || !root.contains(label)) return
  1855. const input = label.querySelector('input.subpack-slip-check-input') as HTMLInputElement | null
  1856. if (!input) return
  1857. event.preventDefault()
  1858. event.stopPropagation()
  1859. input.checked = !input.checked
  1860. syncSubpackSlipCheckboxInput(input)
  1861. onChange?.()
  1862. },
  1863. true
  1864. )
  1865. }
  1866. /** 分装单 TinyMCE:初始化与内容变更时绑定多选项与备注输入框交互 */
  1867. export function setupSubpackSlipEditorCheckboxes(editor: {
  1868. getBody: () => HTMLElement
  1869. on: (event: string, callback: (...args: unknown[]) => void) => void
  1870. fire: (event: string) => void
  1871. }): void {
  1872. const bind = () => {
  1873. const body = editor.getBody()
  1874. bindSubpackSlipCheckboxInteraction(body, () => editor.fire('change'))
  1875. bindSubpackSlipRemarkExtraInputInteraction(body, () => editor.fire('change'))
  1876. }
  1877. const syncRemarkExtraValues = () => {
  1878. editor.getBody().querySelectorAll('input.subpack-slip-remark-extra-input').forEach((node) => {
  1879. syncSubpackSlipRemarkExtraInputValue(node as HTMLInputElement)
  1880. })
  1881. }
  1882. editor.on('init', () => {
  1883. window.setTimeout(bind, 0)
  1884. })
  1885. editor.on('SetContent', () => {
  1886. window.setTimeout(bind, 0)
  1887. })
  1888. editor.on('focus', () => {
  1889. window.setTimeout(bind, 0)
  1890. })
  1891. editor.on('BeforeGetContent', () => {
  1892. syncRemarkExtraValues()
  1893. })
  1894. }
  1895. /** @deprecated 兼容旧引用,请使用 setupSubpackSlipEditorCheckboxes */
  1896. export const setupSubpackSlipEditorTypeCheckboxes = setupSubpackSlipEditorCheckboxes
  1897. /** @deprecated 兼容旧引用,请使用 normalizeSubpackSlipRemoveSideLabels */
  1898. function normalizeSubpackSlipSideLabelsPlaceholder(html: string): string {
  1899. return normalizeSubpackSlipRemoveSideLabels(html)
  1900. }
  1901. /** 将占位符替换为计划最新数据 */
  1902. export function replaceSubpackSlipPlaceholders(
  1903. htmlContent: string,
  1904. plan: Plan,
  1905. ctx?: SubpackSlipRenderContext,
  1906. options?: { blank?: boolean }
  1907. ): string {
  1908. if (!htmlContent || !plan) return htmlContent
  1909. const blank = Boolean(options?.blank)
  1910. const orderDate = blank ? '' : ctx?.orderDate || formatDateIso()
  1911. const orderUser = blank ? '' : ctx?.orderUser || ''
  1912. const preparerUser = blank ? '' : ctx?.preparerUser || orderUser
  1913. const replacements: Record<string, string> = {
  1914. '{{companyName}}': SUBPACK_SLIP_DEFAULTS.companyName,
  1915. '{{subpackSlipNo}}': blank ? '' : ctx?.subpackSlipNo || '',
  1916. '{{orderDate}}': orderDate,
  1917. '{{deliveryDate}}': '',
  1918. '{{orderUser}}': orderUser,
  1919. '{{preparerUser}}': preparerUser,
  1920. '{{handlerName}}': '',
  1921. '{{receivingUnit}}': blank ? '' : orderDate,
  1922. '{{planNo}}': '',
  1923. '{{receiveContact}}': blank ? '' : orderUser,
  1924. '{{driverInfo}}': '',
  1925. '{{planDateChinese}}': '',
  1926. '{{productTable}}': buildProductRows(blank ? [] : plan.products, ctx),
  1927. '{{remark}}': blank ? '' : formatRemarkForSlipHtml(plan.remark),
  1928. '{{remarkExtra}}': '',
  1929. '{{companyAddress}}': '',
  1930. '{{companyPhone}}': '',
  1931. '{{contactPhone}}': '',
  1932. '{{companySeal}}': '',
  1933. '{{sideLabels}}': ''
  1934. }
  1935. let result = normalizeSubpackSlipDocLayout(
  1936. normalizeSubpackSlipRemoveAddressAndSeal(
  1937. normalizeSubpackSlipSignRow(
  1938. normalizeSubpackSlipTermsCell(
  1939. normalizeSubpackSlipRemoveTermsRow(
  1940. normalizeSubpackSlipGapBeforeSign(
  1941. normalizeSubpackSlipProductTableFooter(
  1942. normalizeSubpackSlipProductColWidths(
  1943. normalizeSubpackSlipRemarkRow(
  1944. normalizeSubpackSlipProductGridCell(
  1945. normalizeSubpackSlipProductHeader(
  1946. normalizeSubpackSlipGapBeforeProduct(
  1947. normalizeSubpackSlipSlipNoRow(
  1948. normalizeSubpackSlipBasicInfoOneRow(
  1949. normalizeSubpackSlipBasicInfoFields(
  1950. normalizeSubpackSlipRemoveSideLabels(htmlContent)
  1951. )
  1952. )
  1953. )
  1954. )
  1955. )
  1956. )
  1957. )
  1958. )
  1959. )
  1960. )
  1961. )
  1962. )
  1963. )
  1964. )
  1965. )
  1966. for (const [placeholder, value] of Object.entries(replacements)) {
  1967. result = result.split(placeholder).join(value)
  1968. }
  1969. if (!blank && ctx?.subpackSlipNo) {
  1970. result = result.replace(
  1971. /(<span class="subpack-slip-slip-no-display"[^>]*>)[\s\S]*?(&nbsp;<\/span>)/g,
  1972. `$1${ctx.subpackSlipNo}$2`
  1973. )
  1974. }
  1975. if (!blank && preparerUser) {
  1976. result = result.replace(
  1977. /(<span class="subpack-slip-sign-label">制单人:<\/span>\s*<span class="subpack-slip-sign-value"[^>]*>)[\s\S]*?(<\/span>)/g,
  1978. `$1${preparerUser}$2`
  1979. )
  1980. }
  1981. return result
  1982. }
  1983. /** 空白分装单 HTML:默认模板 + 业务字段全空(含分装单号)+ 最小空产品行 */
  1984. export function renderBlankSubpackSlipHtml(): string {
  1985. const blankPlan = {
  1986. id: '',
  1987. planNo: '',
  1988. products: [],
  1989. customerName: '',
  1990. remark: ''
  1991. } as Plan
  1992. const html = generateSubpackSlipTemplate(blankPlan)
  1993. return replaceSubpackSlipPlaceholders(html, blankPlan, undefined, { blank: true })
  1994. }
  1995. /** 保存模板前:将动态字段还原为占位符 */
  1996. export function restoreSubpackSlipPlaceholders(template: string, plan: Plan): string {
  1997. let result = template
  1998. const dynamicReplacements: Record<string, string> = {
  1999. '{{companyName}}': SUBPACK_SLIP_DEFAULTS.companyName,
  2000. '{{orderDate}}': formatDateIso(),
  2001. '{{remark}}': plan.remark || '',
  2002. '{{receivingUnit}}': formatDateIso(),
  2003. '{{planDateChinese}}': formatPlanDateChinese(plan.planDate)
  2004. }
  2005. for (const [placeholder, currentValue] of Object.entries(dynamicReplacements)) {
  2006. if (!currentValue) continue
  2007. const escaped = currentValue.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
  2008. result = result.replace(new RegExp(escaped, 'g'), placeholder)
  2009. }
  2010. result = result.replace(/NO:SXFZ\d{8}-\d{3}/g, '{{subpackSlipNo}}')
  2011. result = result.replace(/SXFZ\d{8}-\d{3}/g, '{{subpackSlipNo}}')
  2012. result = result.replace(/SXFZ-\d{4}-\d{2}-\d{2}-\d{3}/g, '{{subpackSlipNo}}')
  2013. result = result.replace(/NO:SXFZ-\d{4}-\d{2}-\d{2}-\d{3}/g, '{{subpackSlipNo}}')
  2014. result = result.replace(
  2015. /(<span class="subpack-slip-slip-no-display"[^>]*>)[^<]*(?=&nbsp;<\/span>)/,
  2016. '$1{{subpackSlipNo}}'
  2017. )
  2018. result = result.replace(
  2019. /(<div class="subpack-slip-slip-no-display"[^>]*>)[^<]*(?=&nbsp;<\/div>)/,
  2020. '$1{{subpackSlipNo}}'
  2021. )
  2022. result = result.replace(
  2023. /(<span class="subpack-slip-slip-no-value[^"]*"[^>]*>)[^<]*(?=&nbsp;<\/span>)/,
  2024. '$1{{subpackSlipNo}}'
  2025. )
  2026. result = result.replace(
  2027. /(<span class="subpack-slip-info-label"[^>]*>下单日期:<\/span>\s*<span class="subpack-slip-info-value-left"[^>]*>)[^<]*(?=&nbsp;<\/span>)/,
  2028. '$1{{orderDate}}'
  2029. )
  2030. result = result.replace(
  2031. /(<span class="subpack-slip-info-label"[^>]*>下单人:<\/span>\s*<span class="subpack-slip-info-value-left"[^>]*>)[^<]*(?=&nbsp;<\/span>)/,
  2032. '$1{{orderUser}}'
  2033. )
  2034. result = result.replace(
  2035. /(<span class="subpack-slip-info-label"[^>]*>交货日期:<\/span>\s*<span class="subpack-slip-info-value-center"[^>]*>)[^<]*(?=&nbsp;<\/span>)/,
  2036. '$1{{deliveryDate}}'
  2037. )
  2038. result = result.replace(
  2039. /(<span class="subpack-slip-info-label"[^>]*>经手人:<\/span>\s*<span class="subpack-slip-info-value-center"[^>]*>)[^<]*(?=&nbsp;<\/span>)/,
  2040. '$1{{handlerName}}'
  2041. )
  2042. result = result.replace(
  2043. /(<span class="subpack-slip-sign-label">制单人:<\/span>\s*<span class="subpack-slip-sign-value"[^>]*>)[^<]*(?=<\/span>)/,
  2044. '$1{{preparerUser}}'
  2045. )
  2046. // 产品明细行还原为占位符(表头之后、备注行之前)
  2047. result = result.replace(
  2048. /(<th[^>]*>是否完成<\/th>\s*<\/tr>\s*)([\s\S]*?)(\s*<tr>\s*<td colspan="7"[^>]*>\s*(?:<div class="subpack-slip-remark-row-inner">|备注:))/,
  2049. '$1{{productTable}}$3'
  2050. )
  2051. result = result.replace(
  2052. /(<th[^>]*>实收数量<\/th>\s*<\/tr>\s*)([\s\S]*?)(\s*<tr>\s*<td colspan="7"[^>]*>\s*(?:<div class="subpack-slip-remark-row-inner">|备注:))/,
  2053. '$1{{productTable}}$3'
  2054. )
  2055. // 移除右侧联次、印章与地址/电话行
  2056. result = normalizeSubpackSlipRemoveSideLabels(result)
  2057. result = normalizeSubpackSlipRemoveAddressAndSeal(result)
  2058. return result
  2059. }