pl-subpack-slip-template.ts 96 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271
  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:14px;'
  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: 14px !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.replace(/white-space:nowrap;?/g, '').replace(/font-size:16px;?/g, 'font-size:14px;')
  1465. if (nextTdAttrs.includes('class="')) {
  1466. nextTdAttrs = nextTdAttrs.replace(/class="([^"]*)"/, (_, cls: string) =>
  1467. cls.includes('subpack-slip-sign-row-cell') ? `class="${cls}"` : `class="${cls} subpack-slip-sign-row-cell"`
  1468. )
  1469. } else {
  1470. nextTdAttrs = `${nextTdAttrs} class="subpack-slip-sign-row-cell"`
  1471. }
  1472. return `<tr${trClassAttr}>\n <td colspan="7"${nextTdAttrs}>\n <div class="subpack-slip-sign-inner">${wrappedContent.trim()}</div>\n </td>\n </tr>`
  1473. }
  1474. )
  1475. result = result.replace(
  1476. /<td(?![^>]*class="[^"]*subpack-slip-main-col)([^>]*style="[^"]*vertical-align:top;padding-right:6px)/g,
  1477. '<td class="subpack-slip-main-col"$1'
  1478. )
  1479. return normalizeSubpackSlipSignLabelMarkup(result)
  1480. }
  1481. /**
  1482. * 旧版一体 layout-table / 曾拆出的 footer-table → 信息表 + 物料表(含备注) + 签字表。
  1483. */
  1484. function migrateSubpackSlipSplitTables(html: string): string {
  1485. if (!html.includes('subpack-slip-doc')) return html
  1486. if (
  1487. html.includes('subpack-slip-product-table') &&
  1488. !html.includes('subpack-slip-footer-table') &&
  1489. /subpack-slip-product-table[\s\S]*?(?:subpack-slip-remark-cell|备注:)/.test(html)
  1490. ) {
  1491. return html
  1492. }
  1493. if (html.includes('subpack-slip-footer-table') && html.includes('subpack-slip-product-table')) {
  1494. return html.replace(
  1495. /(<\/tr>\s*)<\/table>\s*<table class="subpack-slip-footer-table"[^>]*>\s*(?:<colgroup>[\s\S]*?<\/colgroup>\s*)?([\s\S]*?)<\/table>/i,
  1496. (_m, beforeClose, footerInner) => {
  1497. const rows = footerInner.replace(/^\s*<colgroup>[\s\S]*?<\/colgroup>\s*/i, '').trim()
  1498. return `${beforeClose}${rows}\n </table>`
  1499. }
  1500. )
  1501. }
  1502. if (!html.includes('subpack-slip-layout-table')) return html
  1503. return html.replace(
  1504. /<table class="subpack-slip-layout-table"[^>]*>([\s\S]*?)<\/table>/i,
  1505. (_full, inner: string) => {
  1506. const headerMatch =
  1507. inner.match(/<tr[^>]*class="[^"]*subpack-slip-product-header[^"]*"[\s\S]*?<\/tr>/i) ||
  1508. inner.match(/<tr>\s*<th[^>]*>(?:序号|物料编号)[\s\S]*?<\/tr>/i)
  1509. if (!headerMatch) return _full
  1510. const headerIdx = inner.indexOf(headerMatch[0])
  1511. let before = inner.slice(0, headerIdx)
  1512. before = before.replace(/<colgroup>[\s\S]*?<\/colgroup>/i, '')
  1513. before = before.replace(/<tr class="subpack-slip-gap-before-product">[\s\S]*?<\/tr>/i, '')
  1514. const afterHeader = inner.slice(headerIdx + headerMatch[0].length)
  1515. const signMatch =
  1516. afterHeader.match(/<tr[^>]*class="[^"]*subpack-slip-sign-row[^"]*"[\s\S]*?<\/tr>/i) ||
  1517. afterHeader.match(/<tr>\s*<td[^>]*>[\s\S]*?(?:制单人:|仓库:)[\s\S]*?<\/tr>/i)
  1518. let productAndFooter = afterHeader
  1519. let signPart = buildSubpackSlipSignRowHtml()
  1520. if (signMatch) {
  1521. const signAt = afterHeader.indexOf(signMatch[0])
  1522. productAndFooter = afterHeader.slice(0, signAt)
  1523. signPart = signMatch[0]
  1524. }
  1525. productAndFooter = productAndFooter.replace(
  1526. /<tr class="subpack-slip-gap-before-sign">[\s\S]*?<\/tr>/i,
  1527. ''
  1528. )
  1529. return `<table class="subpack-slip-info-table" style="width:100%;border-collapse:collapse;table-layout:fixed;border:none !important;">
  1530. ${INFO_COLGROUP}
  1531. ${before.trim()}
  1532. </table>
  1533. <div class="subpack-slip-section-gap subpack-slip-gap-before-product" style="${GAP_BEFORE_PRODUCT_CELL}">&nbsp;</div>
  1534. <table class="subpack-slip-product-table" style="width:100%;border-collapse:collapse;table-layout:fixed;border:none !important;">
  1535. ${PRODUCT_COLGROUP}
  1536. ${headerMatch[0]}
  1537. ${productAndFooter.trim()}
  1538. </table>
  1539. <div class="subpack-slip-section-gap subpack-slip-gap-before-sign" style="${GAP_BEFORE_SIGN_CELL}">&nbsp;</div>
  1540. <table class="subpack-slip-sign-table" style="width:100%;border-collapse:collapse;table-layout:fixed;border:none !important;">
  1541. ${signPart.trim()}
  1542. </table>`
  1543. }
  1544. )
  1545. }
  1546. /** 物料表列宽(兼容已保存模板) */
  1547. function normalizeSubpackSlipProductColWidths(html: string): string {
  1548. if (!html.includes('subpack-slip-product-table')) return html
  1549. return html.replace(
  1550. /(<table class="subpack-slip-product-table"[^>]*>\s*)<colgroup>[\s\S]*?<\/colgroup>/i,
  1551. `$1${PRODUCT_COLGROUP}`
  1552. )
  1553. }
  1554. /** 统一单据左右 10px 间距、内容区宽度(兼容已保存模板) */
  1555. function normalizeSubpackSlipDocLayout(html: string): string {
  1556. let result = migrateSubpackSlipSplitTables(html)
  1557. .replace(/padding:8px 12px 24px 4px;/g, `padding:${DOC_PADDING};`)
  1558. .replace(/padding:8px 12px 24px;/g, `padding:${DOC_PADDING};`)
  1559. .replace(/padding-left: 4px !important;/g, `padding: ${DOC_PADDING} !important;`)
  1560. .replace(/left:0;top:50%/g, `left:${LOGO_LEFT}px;top:50%`)
  1561. .replace(/left:20px;/g, `left:${LOGO_LEFT}px;`)
  1562. .replace(/padding:4px 80px 0 120px;/g, 'padding:4px 60px 0 100px;')
  1563. .replace(/padding:4px 72px 0 88px;/g, 'padding:4px 60px 0 100px;')
  1564. .replace(/max-width:720px/g, `max-width:${SUBPACK_SLIP_DOC_MAX_WIDTH}`)
  1565. .replace(/max-width:760px/g, `max-width:${SUBPACK_SLIP_DOC_MAX_WIDTH}`)
  1566. .replace(/max-width:794px/g, `max-width:${SUBPACK_SLIP_DOC_MAX_WIDTH}`)
  1567. .replace(/height:46px;/g, `height:${LOGO_HEIGHT}px;`)
  1568. .replace(/height:23px;/g, `height:${LOGO_HEIGHT}px;`)
  1569. .replace(/min-height:72px;/g, 'min-height:56px;')
  1570. .replace(/min-height:48px;/g, 'min-height:56px;')
  1571. .replace(/padding:4px 60px 0 68px;/g, 'padding:4px 60px 0 100px;')
  1572. .replace(/padding:4px 60px 0 52px;/g, 'padding:4px 60px 0 100px;')
  1573. .replace(/padding:1px 6px;/g, `padding:${TABLE_ROW_PADDING};`)
  1574. .replace(/padding:4px 6px;/g, `padding:${TABLE_ROW_PADDING};`)
  1575. .replace(/padding-bottom:5px;/g, '')
  1576. .replace(/padding-bottom:10px;/g, '')
  1577. .replace(/padding:18px 6px 8px;/g, `padding:${TABLE_ROW_PADDING};`)
  1578. .replace(/margin-top:6px;/g, 'margin-top:2px;')
  1579. .replace(/line-height:1\.6;/g, 'line-height:1.35;')
  1580. .replace(/line-height:1\.9;/g, 'line-height:1.35;')
  1581. .replace(/min-height:88px;/g, 'min-height:0;')
  1582. .replace(/min-height:36px;/g, '')
  1583. .replace(/min-height:76px;/g, 'min-height:0;')
  1584. .replace(/min-height:120px;/g, 'min-height:0;')
  1585. .replace(/top:4px !important;/g, `top:${SUBPACK_SLIP_SEAL_TOP}px !important;`)
  1586. .replace(/top:4px;/g, `top:${SUBPACK_SLIP_SEAL_TOP}px;`)
  1587. .replace(/>日期:</g, '>日&nbsp;期:<')
  1588. .replace(/border-bottom:2px solid #000 !important;/g, `border-bottom:${INFO_UNDERLINE} !important;`)
  1589. .replace(/border-bottom:1px solid #000 !important;/g, `border-bottom:${INFO_UNDERLINE} !important;`)
  1590. .replace(/border:2px solid #000 !important;/g, `border:${PRODUCT_GRID_BORDER} !important;`)
  1591. .replace(/border:1px solid #000 !important;/g, `border:${PRODUCT_GRID_BORDER} !important;`)
  1592. .replace(
  1593. /display:inline-block;(?:min-width:\d+px;)?padding:0 2px 1px;vertical-align:bottom;border:none !important;/g,
  1594. ''
  1595. )
  1596. .replace(
  1597. /display:inline-block;padding:0 2px 2px;vertical-align:bottom;border:none !important;border-bottom:1px solid #000 !important;/g,
  1598. ''
  1599. )
  1600. .replace(
  1601. /(制单人:|审核:|生产主管:|仓库:|客户签收:|司机\/送货人\(附车牌\):)<span(?![^>]*class="subpack-slip-sign-value") style="/g,
  1602. '$1<span class="subpack-slip-sign-value" style="'
  1603. )
  1604. .replace(
  1605. /(<span class="subpack-slip-sign-value" style="[^"]*)border-bottom:2px solid #000 !important;/g,
  1606. `$1border-bottom:${SIGN_UNDERLINE} !important;`
  1607. )
  1608. .replace(/class="subpack-slip-info-before-product"/g, '')
  1609. .replace(/padding-bottom:12px;/g, '')
  1610. .replace(/padding-bottom:24px;/g, '')
  1611. if (result.includes('subpack-slip-doc') && !result.includes('max-width:')) {
  1612. result = result.replace(
  1613. /(<div class="subpack-slip-doc" style=")([^"]*)"/,
  1614. `$1$2;${DOC_WRAP_EXTRA}"`
  1615. )
  1616. }
  1617. if (result.includes('subpack-slip-doc')) {
  1618. result = result.replace(/<style type="text\/css">[\s\S]*?<\/style>/, SUBPACK_SLIP_STYLE_BLOCK)
  1619. result = normalizeSubpackSlipSignLabelMarkup(result)
  1620. result = normalizeSubpackSlipProductGridCell(result)
  1621. result = normalizeSubpackSlipProductTableFooter(result)
  1622. result = normalizeSubpackSlipSlipNoRow(result)
  1623. }
  1624. return result
  1625. }
  1626. /** 基本信息字段与标题(兼容从送(提)货单复制的旧模板) */
  1627. function normalizeSubpackSlipBasicInfoFields(html: string): string {
  1628. if (!html.includes('subpack-slip-doc')) return html
  1629. let result = html
  1630. result = result.replace(/送&nbsp;\(提\)&nbsp;货&nbsp;单/g, '分&nbsp;装&nbsp;单')
  1631. result = result.replace(/送\(提\)货单/g, '分装单')
  1632. result = result.replace(/收货单位:/g, '下单日期:')
  1633. result = result.replace(/单据编号:/g, '交货日期:')
  1634. result = result.replace(/收货联系方式:/g, '下单人:')
  1635. result = result.replace(/日&nbsp;期:/g, '经手人:')
  1636. result = result.replace(/>日期:</g, '>经手人:<')
  1637. result = result.replace(/\{\{receivingUnit\}\}/g, '{{orderDate}}')
  1638. result = result.replace(/\{\{planNo\}\}/g, '{{deliveryDate}}')
  1639. result = result.replace(/\{\{receiveContact\}\}/g, '{{orderUser}}')
  1640. result = result.replace(/\{\{planDateChinese\}\}/g, '{{handlerName}}')
  1641. result = result.replace(/\{\{driverInfo\}\}/g, '{{handlerName}}')
  1642. result = result.replace(
  1643. /<div class="subpack-slip-info-row subpack-slip-type-check-wrap"[\s\S]*?<\/div>\s*/g,
  1644. ''
  1645. )
  1646. result = result.replace(/<div class="subpack-slip-type-check-row"[\s\S]*?<\/div>\s*/g, '')
  1647. return result
  1648. }
  1649. /** 物料表头(兼容旧版 物料编号/实收数量 表头) */
  1650. function normalizeSubpackSlipProductHeader(html: string): string {
  1651. if (!html.includes('subpack-slip-doc')) return html
  1652. if (html.includes('>序号<') && html.includes('>是否完成<')) return html
  1653. const headerHtml = buildSubpackSlipProductHeaderRowHtml()
  1654. const withClass = html.replace(/<tr class="subpack-slip-product-header"[\s\S]*?<\/tr>/, headerHtml)
  1655. if (withClass !== html) return withClass
  1656. return html.replace(
  1657. /<tr>\s*<th[^>]*>物料编号<\/th>[\s\S]*?<th[^>]*>实收数量<\/th>\s*<\/tr>/,
  1658. headerHtml
  1659. )
  1660. }
  1661. /** 备注行 + 桶选项多选框(兼容旧版纯文本备注行) */
  1662. function normalizeSubpackSlipRemarkRow(html: string): string {
  1663. if (!html.includes('subpack-slip-doc')) return html
  1664. if (html.includes('subpack-slip-remark-row-inner')) {
  1665. let result = html
  1666. // 旧版:备注正文与补充输入分开,升级为同一条连续下划线
  1667. if (!result.includes('subpack-slip-remark-underline-group')) {
  1668. const extractRemark = (raw: string) =>
  1669. String(raw || '')
  1670. .replace(/<[^>]+>/g, '')
  1671. .replace(/备注:/g, '')
  1672. .replace(/&nbsp;/g, '')
  1673. .trim() || '{{remark}}'
  1674. result = result.replace(
  1675. /<div class="subpack-slip-remark-row-inner">([\s\S]*?)<div class="subpack-slip-bucket-check-row"/,
  1676. (_m, middle: string) => {
  1677. const valueMatch = String(middle).match(
  1678. /class="subpack-slip-remark-value"[^>]*>([\s\S]*?)<\/span>/
  1679. )
  1680. const plainMatch = String(middle).match(/备注:([^<]*)/)
  1681. const content = extractRemark(valueMatch?.[1] ?? plainMatch?.[1] ?? '{{remark}}')
  1682. const extraMatch = String(middle).match(
  1683. /<input type="text" class="subpack-slip-remark-extra-input"([^>]*)\/?>/
  1684. )
  1685. const extraHtml = extraMatch
  1686. ? `<span class="subpack-slip-remark-extra-wrap" contenteditable="false"><input type="text" class="subpack-slip-remark-extra-input"${extraMatch[1]} /></span>`
  1687. : buildSubpackSlipRemarkExtraInputHtml()
  1688. return `<div class="subpack-slip-remark-row-inner">
  1689. <span class="subpack-slip-remark-label">备注:</span>
  1690. <span class="subpack-slip-remark-underline-group"><span class="subpack-slip-remark-value">${content}&nbsp;</span>${extraHtml}</span>
  1691. <div class="subpack-slip-bucket-check-row"`
  1692. }
  1693. )
  1694. }
  1695. if (!result.includes('subpack-slip-remark-extra-input')) {
  1696. result = result.replace(
  1697. /(<div class="subpack-slip-remark-row-inner">[\s\S]*?)(\s*<div class="subpack-slip-bucket-check-row")/,
  1698. `$1\n ${buildSubpackSlipRemarkUnderlineGroupHtml()}$2`
  1699. )
  1700. } else if (!result.includes('subpack-slip-remark-extra-wrap')) {
  1701. result = result.replace(
  1702. /<input type="text" class="subpack-slip-remark-extra-input"([^>]*)\/?>/g,
  1703. `<span class="subpack-slip-remark-extra-wrap" contenteditable="false"><input type="text" class="subpack-slip-remark-extra-input"$1 /></span>`
  1704. )
  1705. }
  1706. return ensureSubpackSlipInteractiveMarkup(result)
  1707. }
  1708. return html.replace(
  1709. /(<td[^>]*colspan="7"[^>]*>\s*)备注:(\{\{remark\}\}|[^<]*?)(\s*<\/td>)/,
  1710. `$1${buildSubpackSlipRemarkRowHtml()}$3`
  1711. )
  1712. }
  1713. /** 移除右侧联次列,并解包旧版 outer-table 布局 */
  1714. function normalizeSubpackSlipRemoveSideLabels(html: string): string {
  1715. if (!html.includes('subpack-slip-doc')) return html
  1716. let result = html
  1717. result = result.replace(/<table class="subpack-slip-side-labels"[\s\S]*?<\/table>/g, '')
  1718. result = result.replace(/\{\{sideLabels\}\}/g, '')
  1719. result = result.replace(
  1720. /<!--\s*最右侧纵向联次\s*-->\s*<td class="subpack-slip-side-labels-col"[\s\S]*?<\/td>/g,
  1721. ''
  1722. )
  1723. result = result.replace(/<td class="subpack-slip-side-labels-col"[^>]*>[\s\S]*?<\/td>/g, '')
  1724. if (result.includes('subpack-slip-outer-table') && result.includes('subpack-slip-main-col')) {
  1725. result = result.replace(
  1726. /<table class="subpack-slip-outer-table" style="[^"]*">\s*<tr>\s*<td class="subpack-slip-main-col" style="[^"]*">/,
  1727. ''
  1728. )
  1729. result = result.replace(/<\/td>\s*<\/tr>\s*<\/table>(?=\s*<\/div>)/, '')
  1730. }
  1731. return result
  1732. }
  1733. /** 基本信息合并为一行四列(兼容旧版两行布局) */
  1734. function normalizeSubpackSlipBasicInfoOneRow(html: string): string {
  1735. if (!html.includes('subpack-slip-doc')) return html
  1736. if (html.includes('subpack-slip-basic-info-row')) return html
  1737. const basicInfoRow = buildSubpackSlipBasicInfoRowHtml()
  1738. const replaced = html.replace(
  1739. /<!--\s*上:基本信息两行\s*-->\s*<tr>[\s\S]*?<\/tr>\s*<tr>[\s\S]*?<\/tr>/,
  1740. `<!-- 基本信息一行 -->\n ${basicInfoRow}`
  1741. )
  1742. if (replaced !== html) return replaced
  1743. const fallback = html.replace(
  1744. /<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>/,
  1745. basicInfoRow
  1746. )
  1747. return fallback
  1748. }
  1749. /** 分装单号与经手人标签右缘对齐(兼容旧版标题区居中布局) */
  1750. function normalizeSubpackSlipSlipNoRow(html: string): string {
  1751. if (!html.includes('subpack-slip-doc')) return html
  1752. let result = html
  1753. result = result.replace(/<div class="subpack-slip-info-row subpack-slip-slip-no-wrap"[\s\S]*?<\/div>\s*/g, '')
  1754. result = result.replace(/<span class="subpack-slip-info-label"[^>]*>分装单号:<\/span>\s*/g, '')
  1755. result = result.replace(
  1756. /<div class="subpack-slip-slip-no-display"[^>]*>([\s\S]*?)<\/div>/g,
  1757. '<span class="subpack-slip-slip-no-display">$1</span>'
  1758. )
  1759. if (result.includes('subpack-slip-title-section') && result.includes('subpack-slip-slip-no-display')) {
  1760. result = result.replace(
  1761. /\s*<span class="subpack-slip-slip-no-display"[^>]*>[\s\S]*?<\/span>\s*(?=<\/div>\s*<\/div>\s*<table class="subpack-slip-(?:layout|info)-table")/,
  1762. '\n'
  1763. )
  1764. result = result.replace(
  1765. /<div class="subpack-slip-slip-no-display"[^>]*>[\s\S]*?<\/div>\s*(?=<\/div>\s*<\/div>\s*<table class="subpack-slip-(?:layout|info)-table")/,
  1766. ''
  1767. )
  1768. }
  1769. if (!result.includes('subpack-slip-slip-no-row')) {
  1770. result = result.replace(
  1771. /(<table class="subpack-slip-(?:layout|info)-table"[\s\S]*?<colgroup>[\s\S]*?<\/colgroup>\s*)/,
  1772. `$1\n ${buildSubpackSlipSlipNoRowHtml()}\n\n `
  1773. )
  1774. }
  1775. if (result.includes('subpack-slip-slip-no-row') && !result.includes('subpack-slip-slip-no-anchor')) {
  1776. result = result.replace(
  1777. /<tr class="subpack-slip-slip-no-row"[\s\S]*?<\/tr>\s*/,
  1778. `${buildSubpackSlipSlipNoRowHtml()}\n`
  1779. )
  1780. }
  1781. return ensureSubpackSlipInteractiveMarkup(result)
  1782. }
  1783. const SUBPACK_SLIP_CHECK_DELEGATE_ATTR = 'data-subpack-slip-check-delegate'
  1784. const SUBPACK_SLIP_REMARK_EXTRA_BOUND_ATTR = 'data-subpack-slip-remark-extra-bound'
  1785. function syncSubpackSlipRemarkExtraInputValue(input: HTMLInputElement): void {
  1786. input.setAttribute('value', input.value)
  1787. }
  1788. /** 富文本编辑器内:允许备注补充输入框获得焦点并输入(TinyMCE 默认会拦截) */
  1789. export function bindSubpackSlipRemarkExtraInputInteraction(
  1790. root: HTMLElement | null | undefined,
  1791. onChange?: () => void
  1792. ): void {
  1793. if (!root) return
  1794. root.querySelectorAll('input.subpack-slip-remark-extra-input').forEach((node) => {
  1795. const input = node as HTMLInputElement
  1796. const wrap = input.closest('.subpack-slip-remark-extra-wrap') as HTMLElement | null
  1797. if (wrap) {
  1798. wrap.setAttribute('contenteditable', 'false')
  1799. }
  1800. input.removeAttribute('readonly')
  1801. input.removeAttribute('disabled')
  1802. syncSubpackSlipRemarkExtraInputValue(input)
  1803. if (input.getAttribute(SUBPACK_SLIP_REMARK_EXTRA_BOUND_ATTR) === '1') return
  1804. input.setAttribute(SUBPACK_SLIP_REMARK_EXTRA_BOUND_ATTR, '1')
  1805. const stopBubble = (e: Event) => {
  1806. e.stopPropagation()
  1807. }
  1808. input.addEventListener('mousedown', stopBubble, true)
  1809. input.addEventListener('click', stopBubble, true)
  1810. input.addEventListener('keydown', stopBubble, true)
  1811. input.addEventListener('keyup', stopBubble, true)
  1812. input.addEventListener('keypress', stopBubble, true)
  1813. input.addEventListener('focus', stopBubble, true)
  1814. input.addEventListener('input', () => {
  1815. syncSubpackSlipRemarkExtraInputValue(input)
  1816. onChange?.()
  1817. })
  1818. input.addEventListener('blur', () => {
  1819. syncSubpackSlipRemarkExtraInputValue(input)
  1820. onChange?.()
  1821. })
  1822. })
  1823. }
  1824. function syncSubpackSlipCheckboxInput(input: HTMLInputElement): void {
  1825. if (input.checked) {
  1826. input.setAttribute('checked', 'checked')
  1827. } else {
  1828. input.removeAttribute('checked')
  1829. }
  1830. }
  1831. /** 富文本编辑器内:绑定多选框点击(TinyMCE 默认可编辑区会拦截原生 checkbox) */
  1832. export function bindSubpackSlipCheckboxInteraction(
  1833. root: HTMLElement | null | undefined,
  1834. onChange?: () => void
  1835. ): void {
  1836. if (!root) return
  1837. root.querySelectorAll('label.subpack-slip-check-item').forEach((node) => {
  1838. const label = node as HTMLElement
  1839. label.setAttribute('contenteditable', 'false')
  1840. const input = label.querySelector('input.subpack-slip-check-input') as HTMLInputElement | null
  1841. if (input) syncSubpackSlipCheckboxInput(input)
  1842. })
  1843. if (root.getAttribute(SUBPACK_SLIP_CHECK_DELEGATE_ATTR) === '1') return
  1844. root.setAttribute(SUBPACK_SLIP_CHECK_DELEGATE_ATTR, '1')
  1845. root.addEventListener(
  1846. 'mousedown',
  1847. (event) => {
  1848. if (root.isContentEditable === false) return
  1849. const target = event.target as HTMLElement | null
  1850. if (!target) return
  1851. const label = target.closest('label.subpack-slip-check-item')
  1852. if (!label || !root.contains(label)) return
  1853. const input = label.querySelector('input.subpack-slip-check-input') as HTMLInputElement | null
  1854. if (!input) return
  1855. event.preventDefault()
  1856. event.stopPropagation()
  1857. input.checked = !input.checked
  1858. syncSubpackSlipCheckboxInput(input)
  1859. onChange?.()
  1860. },
  1861. true
  1862. )
  1863. }
  1864. /** 分装单 TinyMCE:初始化与内容变更时绑定多选项与备注输入框交互 */
  1865. export function setupSubpackSlipEditorCheckboxes(editor: {
  1866. getBody: () => HTMLElement
  1867. on: (event: string, callback: (...args: unknown[]) => void) => void
  1868. fire: (event: string) => void
  1869. }): void {
  1870. const bind = () => {
  1871. const body = editor.getBody()
  1872. bindSubpackSlipCheckboxInteraction(body, () => editor.fire('change'))
  1873. bindSubpackSlipRemarkExtraInputInteraction(body, () => editor.fire('change'))
  1874. }
  1875. const syncRemarkExtraValues = () => {
  1876. editor.getBody().querySelectorAll('input.subpack-slip-remark-extra-input').forEach((node) => {
  1877. syncSubpackSlipRemarkExtraInputValue(node as HTMLInputElement)
  1878. })
  1879. }
  1880. editor.on('init', () => {
  1881. window.setTimeout(bind, 0)
  1882. })
  1883. editor.on('SetContent', () => {
  1884. window.setTimeout(bind, 0)
  1885. })
  1886. editor.on('focus', () => {
  1887. window.setTimeout(bind, 0)
  1888. })
  1889. editor.on('BeforeGetContent', () => {
  1890. syncRemarkExtraValues()
  1891. })
  1892. }
  1893. /** @deprecated 兼容旧引用,请使用 setupSubpackSlipEditorCheckboxes */
  1894. export const setupSubpackSlipEditorTypeCheckboxes = setupSubpackSlipEditorCheckboxes
  1895. /** @deprecated 兼容旧引用,请使用 normalizeSubpackSlipRemoveSideLabels */
  1896. function normalizeSubpackSlipSideLabelsPlaceholder(html: string): string {
  1897. return normalizeSubpackSlipRemoveSideLabels(html)
  1898. }
  1899. /** 将占位符替换为计划最新数据 */
  1900. export function replaceSubpackSlipPlaceholders(
  1901. htmlContent: string,
  1902. plan: Plan,
  1903. ctx?: SubpackSlipRenderContext,
  1904. options?: { blank?: boolean }
  1905. ): string {
  1906. if (!htmlContent || !plan) return htmlContent
  1907. const blank = Boolean(options?.blank)
  1908. const orderDate = blank ? '' : ctx?.orderDate || formatDateIso()
  1909. const orderUser = blank ? '' : ctx?.orderUser || ''
  1910. const preparerUser = blank ? '' : ctx?.preparerUser || orderUser
  1911. const replacements: Record<string, string> = {
  1912. '{{companyName}}': SUBPACK_SLIP_DEFAULTS.companyName,
  1913. '{{subpackSlipNo}}': blank ? '' : ctx?.subpackSlipNo || '',
  1914. '{{orderDate}}': orderDate,
  1915. '{{deliveryDate}}': '',
  1916. '{{orderUser}}': orderUser,
  1917. '{{preparerUser}}': preparerUser,
  1918. '{{handlerName}}': '',
  1919. '{{receivingUnit}}': blank ? '' : orderDate,
  1920. '{{planNo}}': '',
  1921. '{{receiveContact}}': blank ? '' : orderUser,
  1922. '{{driverInfo}}': '',
  1923. '{{planDateChinese}}': '',
  1924. '{{productTable}}': buildProductRows(blank ? [] : plan.products, ctx),
  1925. '{{remark}}': blank ? '' : formatRemarkForSlipHtml(plan.remark),
  1926. '{{remarkExtra}}': '',
  1927. '{{companyAddress}}': '',
  1928. '{{companyPhone}}': '',
  1929. '{{contactPhone}}': '',
  1930. '{{companySeal}}': '',
  1931. '{{sideLabels}}': ''
  1932. }
  1933. let result = normalizeSubpackSlipDocLayout(
  1934. normalizeSubpackSlipRemoveAddressAndSeal(
  1935. normalizeSubpackSlipSignRow(
  1936. normalizeSubpackSlipTermsCell(
  1937. normalizeSubpackSlipRemoveTermsRow(
  1938. normalizeSubpackSlipGapBeforeSign(
  1939. normalizeSubpackSlipProductTableFooter(
  1940. normalizeSubpackSlipProductColWidths(
  1941. normalizeSubpackSlipRemarkRow(
  1942. normalizeSubpackSlipProductGridCell(
  1943. normalizeSubpackSlipProductHeader(
  1944. normalizeSubpackSlipGapBeforeProduct(
  1945. normalizeSubpackSlipSlipNoRow(
  1946. normalizeSubpackSlipBasicInfoOneRow(
  1947. normalizeSubpackSlipBasicInfoFields(
  1948. normalizeSubpackSlipRemoveSideLabels(htmlContent)
  1949. )
  1950. )
  1951. )
  1952. )
  1953. )
  1954. )
  1955. )
  1956. )
  1957. )
  1958. )
  1959. )
  1960. )
  1961. )
  1962. )
  1963. )
  1964. for (const [placeholder, value] of Object.entries(replacements)) {
  1965. result = result.split(placeholder).join(value)
  1966. }
  1967. if (!blank && ctx?.subpackSlipNo) {
  1968. result = result.replace(
  1969. /(<span class="subpack-slip-slip-no-display"[^>]*>)[\s\S]*?(&nbsp;<\/span>)/g,
  1970. `$1${ctx.subpackSlipNo}$2`
  1971. )
  1972. }
  1973. if (!blank && preparerUser) {
  1974. result = result.replace(
  1975. /(<span class="subpack-slip-sign-label">制单人:<\/span>\s*<span class="subpack-slip-sign-value"[^>]*>)[\s\S]*?(<\/span>)/g,
  1976. `$1${preparerUser}$2`
  1977. )
  1978. }
  1979. return result
  1980. }
  1981. /** 空白分装单 HTML:默认模板 + 业务字段全空(含分装单号)+ 最小空产品行 */
  1982. export function renderBlankSubpackSlipHtml(): string {
  1983. const blankPlan = {
  1984. id: '',
  1985. planNo: '',
  1986. products: [],
  1987. customerName: '',
  1988. remark: ''
  1989. } as Plan
  1990. const html = generateSubpackSlipTemplate(blankPlan)
  1991. return replaceSubpackSlipPlaceholders(html, blankPlan, undefined, { blank: true })
  1992. }
  1993. /** 保存模板前:将动态字段还原为占位符 */
  1994. export function restoreSubpackSlipPlaceholders(template: string, plan: Plan): string {
  1995. let result = template
  1996. const dynamicReplacements: Record<string, string> = {
  1997. '{{companyName}}': SUBPACK_SLIP_DEFAULTS.companyName,
  1998. '{{orderDate}}': formatDateIso(),
  1999. '{{remark}}': plan.remark || '',
  2000. '{{receivingUnit}}': formatDateIso(),
  2001. '{{planDateChinese}}': formatPlanDateChinese(plan.planDate)
  2002. }
  2003. for (const [placeholder, currentValue] of Object.entries(dynamicReplacements)) {
  2004. if (!currentValue) continue
  2005. const escaped = currentValue.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
  2006. result = result.replace(new RegExp(escaped, 'g'), placeholder)
  2007. }
  2008. result = result.replace(/NO:SXFZ\d{8}-\d{3}/g, '{{subpackSlipNo}}')
  2009. result = result.replace(/SXFZ\d{8}-\d{3}/g, '{{subpackSlipNo}}')
  2010. result = result.replace(/SXFZ-\d{4}-\d{2}-\d{2}-\d{3}/g, '{{subpackSlipNo}}')
  2011. result = result.replace(/NO:SXFZ-\d{4}-\d{2}-\d{2}-\d{3}/g, '{{subpackSlipNo}}')
  2012. result = result.replace(
  2013. /(<span class="subpack-slip-slip-no-display"[^>]*>)[^<]*(?=&nbsp;<\/span>)/,
  2014. '$1{{subpackSlipNo}}'
  2015. )
  2016. result = result.replace(
  2017. /(<div class="subpack-slip-slip-no-display"[^>]*>)[^<]*(?=&nbsp;<\/div>)/,
  2018. '$1{{subpackSlipNo}}'
  2019. )
  2020. result = result.replace(
  2021. /(<span class="subpack-slip-slip-no-value[^"]*"[^>]*>)[^<]*(?=&nbsp;<\/span>)/,
  2022. '$1{{subpackSlipNo}}'
  2023. )
  2024. result = result.replace(
  2025. /(<span class="subpack-slip-info-label"[^>]*>下单日期:<\/span>\s*<span class="subpack-slip-info-value-left"[^>]*>)[^<]*(?=&nbsp;<\/span>)/,
  2026. '$1{{orderDate}}'
  2027. )
  2028. result = result.replace(
  2029. /(<span class="subpack-slip-info-label"[^>]*>下单人:<\/span>\s*<span class="subpack-slip-info-value-left"[^>]*>)[^<]*(?=&nbsp;<\/span>)/,
  2030. '$1{{orderUser}}'
  2031. )
  2032. result = result.replace(
  2033. /(<span class="subpack-slip-info-label"[^>]*>交货日期:<\/span>\s*<span class="subpack-slip-info-value-center"[^>]*>)[^<]*(?=&nbsp;<\/span>)/,
  2034. '$1{{deliveryDate}}'
  2035. )
  2036. result = result.replace(
  2037. /(<span class="subpack-slip-info-label"[^>]*>经手人:<\/span>\s*<span class="subpack-slip-info-value-center"[^>]*>)[^<]*(?=&nbsp;<\/span>)/,
  2038. '$1{{handlerName}}'
  2039. )
  2040. result = result.replace(
  2041. /(<span class="subpack-slip-sign-label">制单人:<\/span>\s*<span class="subpack-slip-sign-value"[^>]*>)[^<]*(?=<\/span>)/,
  2042. '$1{{preparerUser}}'
  2043. )
  2044. // 产品明细行还原为占位符(表头之后、备注行之前)
  2045. result = result.replace(
  2046. /(<th[^>]*>是否完成<\/th>\s*<\/tr>\s*)([\s\S]*?)(\s*<tr>\s*<td colspan="7"[^>]*>\s*(?:<div class="subpack-slip-remark-row-inner">|备注:))/,
  2047. '$1{{productTable}}$3'
  2048. )
  2049. result = result.replace(
  2050. /(<th[^>]*>实收数量<\/th>\s*<\/tr>\s*)([\s\S]*?)(\s*<tr>\s*<td colspan="7"[^>]*>\s*(?:<div class="subpack-slip-remark-row-inner">|备注:))/,
  2051. '$1{{productTable}}$3'
  2052. )
  2053. // 移除右侧联次、印章与地址/电话行
  2054. result = normalizeSubpackSlipRemoveSideLabels(result)
  2055. result = normalizeSubpackSlipRemoveAddressAndSeal(result)
  2056. return result
  2057. }