| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271 |
- import type { Plan, PlanProduct } from '@/api/pl'
- import { formatRemarkForSlipHtml } from '@/utils/pl-batch-print'
- import { applyPdfStableTableCellBorders } from '@/utils/pl-slip-pdf-render'
- /** 分装单打印默认公司信息(可在富文本中修改) */
- export const SUBPACK_SLIP_DEFAULTS = {
- companyName: '安庆市山峡化工有限公司',
- receivingUnit: '安庆市山峡化工有限公司',
- companyAddress: '安庆市高新区霞虹路5号',
- companyPhone: '0556-5999710',
- contactPhone: '方总13395560066'
- } as const
- const DOC_FONT = "SimSun, 'Songti SC', serif"
- const FONT_TITLE = 'font-size:26px;font-weight:bold;'
- const FONT_BODY = 'font-size:20px;'
- const FONT_SIGN = 'font-size:14px;'
- const FONT_SIDE = 'font-size:18px;'
- const DOC_PADDING = '8px 10px 24px'
- /** 单据内容区最大宽度(编辑预览居中,略窄于 A4 像素宽) */
- export const SUBPACK_SLIP_DOC_MAX_WIDTH = '960px'
- const DOC_WRAP_EXTRA = `max-width:${SUBPACK_SLIP_DOC_MAX_WIDTH};margin:0 auto;box-sizing:border-box;`
- const LOGO_HEIGHT = 36
- const LOGO_LEFT = 50
- const TITLE_STYLE = `${FONT_TITLE}font-weight:700 !important;letter-spacing:6px;line-height:1.35;`
- const TITLE_SUB_STYLE = `${TITLE_STYLE}margin-top:2px;letter-spacing:10px;`
- const TABLE_ROW_PADDING = '4px'
- /** 信息表等宽列 */
- const INFO_COLGROUP = `<colgroup>
- <col span="7" style="width:14.28%;" />
- </colgroup>`
- /**
- * 物料表列宽:加宽名称/型号,收窄包装规格/单位。
- * 序号 / 产品名称 / 产品型号 / 包装规格 / 单位 / 数量 / 是否完成
- */
- const PRODUCT_COLGROUP = `<colgroup>
- <col style="width:8%;" />
- <col style="width:26%;" />
- <col style="width:18%;" />
- <col style="width:10%;" />
- <col style="width:7%;" />
- <col style="width:15%;" />
- <col style="width:16%;" />
- </colgroup>`
- /** 物料网格 padding(编辑器预览) */
- const PRODUCT_GRID_CELL_PADDING = '8px 6px'
- const PRODUCT_GRID_TH_PADDING = '10px 6px'
- /** PDF 专用:padding 直接写在 td/th(html2canvas 对 flex/vertical-align 支持差,底部多留白) */
- const PRODUCT_GRID_PDF_TD_PADDING = '8px 6px 12px 6px'
- const PRODUCT_GRID_PDF_TH_PADDING = '10px 6px 16px 6px'
- const PRODUCT_GRID_PDF_LINE_HEIGHT = '24px'
- /** PDF 专用:备注/条款/地址等左对齐合并单元格 */
- const PRODUCT_FOOTER_PDF_PADDING = '10px 6px 16px 6px'
- const PRODUCT_FOOTER_PDF_LINE_HEIGHT = '1.35'
- /** 物料表格边框线宽(备注/条款等合并单元格,与网格一致便于 PDF 衔接) */
- const PRODUCT_TABLE_BORDER = '0.3px solid #000'
- /** 物料明细网格线(与合同 PDF 一致:全边框 inline + 0.3px,html2canvas 渲染稳定) */
- const PRODUCT_GRID_BORDER = '0.3px solid #000'
- /** 物料表格单元格:长文本自动换行 */
- const PRODUCT_CELL_WRAP = 'white-space:normal;word-wrap:break-word;overflow-wrap:break-word;word-break:break-word;'
- const PRODUCT_GRID_CELL = `padding:${PRODUCT_GRID_CELL_PADDING};box-sizing:border-box;border:${PRODUCT_GRID_BORDER} !important;text-align:center;${FONT_BODY}line-height:normal;background-color:#fff !important;${PRODUCT_CELL_WRAP}`
- const PRODUCT_CELL = `padding:4px 6px;border:${PRODUCT_TABLE_BORDER} !important;text-align:center;${FONT_BODY}line-height:1.35;background-color:#fff !important;${PRODUCT_CELL_WRAP}`
- const PRODUCT_TH = `padding:${PRODUCT_GRID_TH_PADDING};box-sizing:border-box;border:${PRODUCT_GRID_BORDER} !important;text-align:center;${FONT_BODY}line-height:normal;font-weight:bold;background-color:#fff !important;${PRODUCT_CELL_WRAP}`
- const PRODUCT_TABLE_CELL_LEFT = `padding:10px 6px 14px 6px;border:${PRODUCT_TABLE_BORDER} !important;text-align:left;${FONT_BODY}line-height:1.35;vertical-align:top;background-color:#fff !important;`
- /** 条款单元格:行高随内容,避免被右侧印章区 min-height 撑出底部空白 */
- const PRODUCT_TERMS_CELL_LEFT = `padding:10px 6px 14px 6px;border:${PRODUCT_TABLE_BORDER} !important;text-align:left;${FONT_BODY}line-height:1.35;vertical-align:top;background-color:#fff !important;`
- const TERMS_TEXT_STYLE = 'line-height:1.35;'
- const SEAL_BLOCK_LINE_STYLE = 'line-height:1.35;'
- /** 分装单印章:public/sxshzyz.png,定位在地址单元格右上角(相对原位置向上 100px) */
- const SUBPACK_SLIP_SEAL_IMAGE_SRC = '/sxshzyz.png'
- const SUBPACK_SLIP_SEAL_TOP = -96
- const SUBPACK_SLIP_SEAL_IMAGE_STYLE = `position:absolute;right:4px;top:${SUBPACK_SLIP_SEAL_TOP}px;width:140px;height:140px;opacity:0.85;z-index:10;`
- const HANDWRITE_BLANK_INNER = ' '
- const HANDWRITE_BLANK = `<span class="subpack-slip-handwrite-blank">(${HANDWRITE_BLANK_INNER})</span>`
- const PLAIN_CELL = `border:none !important;padding:${TABLE_ROW_PADDING};${FONT_BODY}line-height:1.8;vertical-align:top;background:transparent !important;`
- const NO_BORDER = `${PLAIN_CELL} vertical-align:bottom;`
- /** 基本信息:label 与值同一行,值区域占满剩余宽度并带下划线 */
- const INFO_ROW = `display:flex;align-items:flex-end;width:100%;box-sizing:border-box;`
- const INFO_LABEL = `white-space:nowrap;flex-shrink:0;padding-right:2px;`
- const INFO_UNDERLINE = '1px solid #000'
- const SIGN_UNDERLINE = '1px solid #000'
- /** 签字值下划线与文字间距(底部 padding 把 border-bottom 推离字模) */
- const SIGN_VALUE_PADDING = '0 2px 6px 2px'
- const SIGN_VALUE_PDF_PADDING = '0 2px 8px 2px'
- const INFO_VALUE_INLINE_LEFT = `flex:1;min-width:0;box-sizing:border-box;border-bottom:${INFO_UNDERLINE} !important;padding:0 2px 2px;text-align:left;min-height:1.3em;`
- const INFO_VALUE_INLINE_CENTER = `flex:1;min-width:0;box-sizing:border-box;border-bottom:${INFO_UNDERLINE} !important;padding:0 2px 2px;text-align:center;min-height:1.3em;`
- const INFO_CELL_LEFT = `${NO_BORDER}text-align:left;`
- const INFO_CELL_RIGHT = `${NO_BORDER}text-align:left;`
- const INFO_GAP_BEFORE_PRODUCT = 12
- const GAP_BEFORE_PRODUCT_CELL = `border:none !important;padding:0 !important;line-height:0 !important;font-size:0 !important;background:transparent !important;height:${INFO_GAP_BEFORE_PRODUCT}px !important;min-height:${INFO_GAP_BEFORE_PRODUCT}px !important;`
- const INFO_GAP_BEFORE_SIGN = 12
- const GAP_BEFORE_SIGN_CELL = `border:none !important;padding:0 !important;line-height:0 !important;font-size:0 !important;background:transparent !important;height:${INFO_GAP_BEFORE_SIGN}px !important;min-height:${INFO_GAP_BEFORE_SIGN}px !important;`
- const SIDE_LABELS_COL_WIDTH = 36
- /** 签字区下划线最小宽度 */
- const SUBPACK_SLIP_SIGN_VALUE_MIN_WIDTH = '120px'
- function buildSubpackSlipSignItem(label: string, valueContent: string, minWidth: string = SUBPACK_SLIP_SIGN_VALUE_MIN_WIDTH): string {
- return `<span class="subpack-slip-sign-item"><span class="subpack-slip-sign-label">${label}</span><span class="subpack-slip-sign-value" style="min-width:${minWidth};">${valueContent}</span></span>`
- }
- function buildSubpackSlipSignRowHtml(): string {
- return `<tr class="subpack-slip-sign-row">
- <td colspan="7" class="subpack-slip-sign-row-cell" style="border:none !important;padding:${TABLE_ROW_PADDING};text-align:center;${FONT_SIGN}background:transparent !important;">
- <div class="subpack-slip-sign-inner">
- ${buildSubpackSlipSignItem('制单人:', '{{preparerUser}} ')}
- ${buildSubpackSlipSignItem('审核:', ' ')}
- ${buildSubpackSlipSignItem('生产主管:', ' ')}
- </div>
- </td>
- </tr>`
- }
- export interface SubpackSlipRenderContext {
- /** 单位字典 code -> 展示文案 */
- getUnitLabel?: (unitCode: string) => string
- /** 分装单号 NO:SXFZYYYYMMDD-### */
- subpackSlipNo?: string
- /** 下单日期 YYYY-MM-DD */
- orderDate?: string
- /** 下单人(当前登录用户姓名) */
- orderUser?: string
- /** 制单人(默认当前登录用户姓名) */
- preparerUser?: string
- }
- function resolveSlipUnitLabel(unitCode: string | undefined, ctx?: SubpackSlipRenderContext): string {
- const code = String(unitCode || '').trim()
- if (!code) return ''
- return ctx?.getUnitLabel?.(code) || code
- }
- export function canPrintSubpackSlip(plan?: Pick<Plan, 'planType' | 'transportStatus'> | null): boolean {
- return plan?.planType !== 'inbound'
- }
- function formatPlanDateChinese(planDate?: string): string {
- if (!planDate) return ''
- const date = new Date(planDate)
- if (Number.isNaN(date.getTime())) return planDate
- return `${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日`
- }
- function formatQty(value?: number | string | null): string {
- const num = parseFloat(String(value ?? ''))
- if (Number.isNaN(num)) return ''
- return num.toFixed(3)
- }
- function formatSubpackSlipProductGridCellContent(content: string): string {
- const plain = content
- .trim()
- .replace(/<div class="subpack-slip-product-cell-inner"[^>]*>([\s\S]*?)<\/div>/gi, '$1')
- .replace(/<div class="subpack-slip-pdf-grid-inner"[^>]*>([\s\S]*?)<\/div>/gi, '$1')
- .replace(/<span class="subpack-slip-pdf-grid-text"[^>]*>([\s\S]*?)<\/span>/gi, '$1')
- .replace(/<span class="subpack-slip-product-grid-align"[^>]*>([\s\S]*?)<\/span>/gi, '$1')
- .replace(/^<p[^>]*>([\s\S]*?)<\/p>$/i, '$1')
- .trim()
- return plain || ' '
- }
- function formatDateIso(value?: string | Date | null): string {
- const date = value instanceof Date ? value : value ? new Date(value) : new Date()
- if (Number.isNaN(date.getTime())) return ''
- const y = date.getFullYear()
- const m = String(date.getMonth() + 1).padStart(2, '0')
- const d = String(date.getDate()).padStart(2, '0')
- return `${y}-${m}-${d}`
- }
- /** 分装单默认下单日期(当天 YYYY-MM-DD) */
- export function formatSubpackSlipDateIso(value?: string | Date | null): string {
- return formatDateIso(value)
- }
- function buildSubpackSlipCompleteCheckHtml(): string {
- return `<label class="subpack-slip-check-item subpack-slip-complete-check-item" contenteditable="false"><input type="checkbox" class="subpack-slip-check-input" />完成</label>`
- }
- const SUBPACK_SLIP_PRODUCT_MIN_ROWS = 5
- function buildSubpackSlipProductRow(
- rowIndex: number,
- product: PlanProduct | undefined,
- ctx?: SubpackSlipRenderContext
- ): string {
- if (product) {
- const qty = product.outboundQty ?? product.outboundEstimateQty ?? product.quantity ?? ''
- return `<tr>
- <td class="subpack-slip-product-cell subpack-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">${formatSubpackSlipProductGridCellContent(String(rowIndex))}</td>
- <td class="subpack-slip-product-cell subpack-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">${formatSubpackSlipProductGridCellContent(product.productName || '')}</td>
- <td class="subpack-slip-product-cell subpack-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">${formatSubpackSlipProductGridCellContent(product.model || '')}</td>
- <td class="subpack-slip-product-cell subpack-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">${formatSubpackSlipProductGridCellContent(product.packageSpec || '')}</td>
- <td class="subpack-slip-product-cell subpack-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">${formatSubpackSlipProductGridCellContent(resolveSlipUnitLabel(product.unit, ctx))}</td>
- <td class="subpack-slip-product-cell subpack-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">${formatSubpackSlipProductGridCellContent(formatQty(qty))}</td>
- <td class="subpack-slip-product-cell subpack-slip-product-grid-cell subpack-slip-complete-cell" style="${PRODUCT_GRID_CELL}">${buildSubpackSlipCompleteCheckHtml()}</td>
- </tr>`
- }
- return `<tr>
- <td class="subpack-slip-product-cell subpack-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">${formatSubpackSlipProductGridCellContent(String(rowIndex))}</td>
- <td class="subpack-slip-product-cell subpack-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">${formatSubpackSlipProductGridCellContent('')}</td>
- <td class="subpack-slip-product-cell subpack-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">${formatSubpackSlipProductGridCellContent('')}</td>
- <td class="subpack-slip-product-cell subpack-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">${formatSubpackSlipProductGridCellContent('')}</td>
- <td class="subpack-slip-product-cell subpack-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">${formatSubpackSlipProductGridCellContent('')}</td>
- <td class="subpack-slip-product-cell subpack-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">${formatSubpackSlipProductGridCellContent('')}</td>
- <td class="subpack-slip-product-cell subpack-slip-product-grid-cell subpack-slip-complete-cell" style="${PRODUCT_GRID_CELL}"> </td>
- </tr>`
- }
- function buildProductRows(products: PlanProduct[] = [], ctx?: SubpackSlipRenderContext): string {
- const rowCount = Math.max(products.length, SUBPACK_SLIP_PRODUCT_MIN_ROWS)
- const rows: string[] = []
- for (let index = 0; index < rowCount; index += 1) {
- rows.push(buildSubpackSlipProductRow(index + 1, products[index], ctx))
- }
- return rows.join('')
- }
- export const SUBPACK_SLIP_PRINT_CSS = `
- .subpack-slip-doc {
- padding: ${DOC_PADDING} !important;
- max-width: ${SUBPACK_SLIP_DOC_MAX_WIDTH} !important;
- margin-left: auto !important;
- margin-right: auto !important;
- box-sizing: border-box !important;
- }
- .subpack-slip-doc .subpack-slip-title-heading {
- font-weight: 700 !important;
- line-height: 1.35 !important;
- }
- .subpack-slip-doc .subpack-slip-title-heading + .subpack-slip-title-heading {
- margin-top: 2px !important;
- }
- .subpack-slip-doc .subpack-slip-info-row {
- display: flex !important;
- align-items: flex-end !important;
- width: 100% !important;
- box-sizing: border-box !important;
- }
- .subpack-slip-doc .subpack-slip-slip-no-display {
- font-size: 20px !important;
- line-height: 1.35 !important;
- letter-spacing: 0.5px !important;
- white-space: nowrap !important;
- }
- .subpack-slip-doc .subpack-slip-slip-no-anchor {
- position: relative !important;
- flex-shrink: 0 !important;
- }
- .subpack-slip-doc .subpack-slip-slip-no-anchor-text {
- visibility: hidden !important;
- }
- .subpack-slip-doc .subpack-slip-slip-no-anchor .subpack-slip-slip-no-display {
- position: absolute !important;
- right: 0 !important;
- top: 0 !important;
- text-align: right !important;
- margin-top: 0 !important;
- }
- .subpack-slip-doc .subpack-slip-slip-no-field .subpack-slip-info-value-left {
- visibility: hidden !important;
- border-bottom: none !important;
- min-height: 0 !important;
- padding-bottom: 0 !important;
- }
- .subpack-slip-doc .subpack-slip-basic-info-spacer {
- visibility: hidden !important;
- min-height: 0 !important;
- height: 0 !important;
- overflow: hidden !important;
- margin: 0 !important;
- padding: 0 !important;
- }
- .subpack-slip-doc .subpack-slip-basic-info-inner {
- display: flex !important;
- flex-wrap: wrap !important;
- justify-content: space-between !important;
- align-items: flex-end !important;
- gap: 8px 12px !important;
- width: 100% !important;
- box-sizing: border-box !important;
- }
- .subpack-slip-doc .subpack-slip-basic-info-item {
- flex: 1 !important;
- min-width: 0 !important;
- }
- /* 下单/交货日期略宽;下单人与经手人等宽(单号行 spacer 同步,保持右缘对齐) */
- .subpack-slip-doc .subpack-slip-basic-info-row .subpack-slip-basic-info-inner > .subpack-slip-basic-info-item:nth-child(1),
- .subpack-slip-doc .subpack-slip-basic-info-row .subpack-slip-basic-info-inner > .subpack-slip-basic-info-item:nth-child(2),
- .subpack-slip-doc .subpack-slip-slip-no-row .subpack-slip-basic-info-inner > .subpack-slip-basic-info-item:nth-child(1),
- .subpack-slip-doc .subpack-slip-slip-no-row .subpack-slip-basic-info-inner > .subpack-slip-basic-info-item:nth-child(2) {
- flex: 1.1 !important;
- }
- .subpack-slip-doc .subpack-slip-basic-info-row .subpack-slip-basic-info-inner > .subpack-slip-basic-info-item:nth-child(3),
- .subpack-slip-doc .subpack-slip-basic-info-row .subpack-slip-basic-info-inner > .subpack-slip-basic-info-item:nth-child(4),
- .subpack-slip-doc .subpack-slip-slip-no-row .subpack-slip-basic-info-inner > .subpack-slip-basic-info-item:nth-child(3),
- .subpack-slip-doc .subpack-slip-slip-no-row .subpack-slip-basic-info-inner > .subpack-slip-basic-info-item:nth-child(4) {
- flex: 1 !important;
- }
- .subpack-slip-doc .subpack-slip-remark-row-inner {
- display: flex !important;
- align-items: center !important;
- width: 100% !important;
- box-sizing: border-box !important;
- gap: 12px !important;
- }
- .subpack-slip-doc .subpack-slip-remark-label {
- flex-shrink: 0 !important;
- white-space: nowrap !important;
- }
- /* 备注正文 + 补充输入共用一条连续下划线(底部 padding 把线推离字模,避免 PDF/html2canvas 遮字) */
- .subpack-slip-doc .subpack-slip-remark-underline-group {
- display: inline-flex !important;
- align-items: baseline !important;
- flex: 1 !important;
- min-width: 200px !important;
- max-width: 420px !important;
- box-sizing: border-box !important;
- border-bottom: ${INFO_UNDERLINE} !important;
- padding: ${SIGN_VALUE_PADDING} !important;
- gap: 0 !important;
- line-height: 1.35 !important;
- overflow: visible !important;
- }
- .subpack-slip-doc .subpack-slip-remark-value {
- display: inline-block !important;
- flex: 0 1 auto !important;
- min-width: 0 !important;
- max-width: 260px !important;
- box-sizing: border-box !important;
- border-bottom: none !important;
- padding: 0 2px !important;
- min-height: 1.3em !important;
- line-height: 1.35 !important;
- overflow: visible !important;
- white-space: nowrap !important;
- vertical-align: baseline !important;
- }
- .subpack-slip-doc .subpack-slip-remark-extra-wrap {
- flex: 1 !important;
- min-width: 80px !important;
- display: inline-block !important;
- vertical-align: bottom !important;
- -webkit-user-modify: read-only !important;
- }
- .subpack-slip-doc .subpack-slip-remark-extra-input {
- width: 100% !important;
- min-width: 80px !important;
- height: 28px !important;
- padding: 0 2px 0 !important;
- margin: 0 !important;
- border: none !important;
- border-bottom: none !important;
- border-radius: 0 !important;
- background: transparent !important;
- font-family: inherit !important;
- font-size: 20px !important;
- line-height: 1.35 !important;
- box-sizing: border-box !important;
- outline: none !important;
- color: #000 !important;
- pointer-events: auto !important;
- }
- .subpack-slip-doc .subpack-slip-bucket-check-row {
- flex: 1 !important;
- min-width: 0 !important;
- display: flex !important;
- flex-wrap: wrap !important;
- justify-content: center !important;
- align-items: center !important;
- gap: 14px 18px !important;
- box-sizing: border-box !important;
- font-size: 20px !important;
- line-height: 1.35 !important;
- }
- .subpack-slip-doc .subpack-slip-complete-cell {
- text-align: center !important;
- }
- .subpack-slip-doc .subpack-slip-complete-check-item {
- justify-content: center !important;
- margin: 0 auto !important;
- }
- .subpack-slip-doc .subpack-slip-check-item {
- display: inline-flex !important;
- align-items: center !important;
- gap: 4px !important;
- white-space: nowrap !important;
- cursor: pointer !important;
- user-select: none !important;
- -webkit-user-modify: read-only !important;
- }
- .subpack-slip-doc .subpack-slip-check-input {
- width: 16px !important;
- height: 16px !important;
- margin: 0 !important;
- flex-shrink: 0 !important;
- vertical-align: middle !important;
- cursor: pointer !important;
- pointer-events: auto !important;
- appearance: none !important;
- -webkit-appearance: none !important;
- background: #fff !important;
- border: 1px solid #000 !important;
- border-radius: 0 !important;
- box-sizing: border-box !important;
- print-color-adjust: exact !important;
- -webkit-print-color-adjust: exact !important;
- position: relative !important;
- }
- .subpack-slip-doc .subpack-slip-check-input:checked::after {
- content: '' !important;
- position: absolute !important;
- left: 4px !important;
- top: 1px !important;
- width: 5px !important;
- height: 10px !important;
- border: solid #000 !important;
- border-width: 0 2.5px 2.5px 0 !important;
- transform: rotate(45deg) !important;
- box-sizing: border-box !important;
- }
- .subpack-slip-doc .subpack-slip-check-item-pdf {
- display: inline-block !important;
- white-space: nowrap !important;
- font-family: SimSun, 'Songti SC', serif !important;
- font-size: 20px !important;
- line-height: 20px !important;
- height: 20px !important;
- vertical-align: middle !important;
- margin-right: 18px !important;
- }
- .subpack-slip-doc .subpack-slip-check-item-pdf:last-child {
- margin-right: 0 !important;
- }
- .subpack-slip-doc .subpack-slip-check-img-pdf {
- display: inline-block !important;
- width: 16px !important;
- height: 16px !important;
- margin: 0 4px 0 0 !important;
- padding: 0 !important;
- border: none !important;
- vertical-align: middle !important;
- }
- .subpack-slip-doc .subpack-slip-check-text-pdf {
- display: inline-block !important;
- font-size: 20px !important;
- line-height: 20px !important;
- vertical-align: middle !important;
- position: relative !important;
- top: -12px !important;
- }
- .subpack-slip-doc .subpack-slip-check-item-pdf.subpack-slip-complete-check-item-pdf {
- margin-right: 0 !important;
- }
- .subpack-slip-doc .subpack-slip-info-label {
- white-space: nowrap !important;
- flex-shrink: 0 !important;
- }
- .subpack-slip-doc table.subpack-slip-layout-table tr.subpack-slip-gap-before-product td.subpack-slip-gap-before-product-cell,
- .subpack-slip-doc .subpack-slip-section-gap.subpack-slip-gap-before-product {
- border: none !important;
- background: transparent !important;
- padding: 0 !important;
- line-height: 0 !important;
- font-size: 0 !important;
- height: ${INFO_GAP_BEFORE_PRODUCT}px !important;
- min-height: ${INFO_GAP_BEFORE_PRODUCT}px !important;
- }
- .subpack-slip-doc table.subpack-slip-layout-table tr.subpack-slip-gap-before-sign td.subpack-slip-gap-before-sign-cell,
- .subpack-slip-doc .subpack-slip-section-gap.subpack-slip-gap-before-sign {
- border: none !important;
- background: transparent !important;
- padding: 0 !important;
- line-height: 0 !important;
- font-size: 0 !important;
- height: ${INFO_GAP_BEFORE_SIGN}px !important;
- min-height: ${INFO_GAP_BEFORE_SIGN}px !important;
- }
- .subpack-slip-doc .subpack-slip-info-value-left {
- flex: 1 !important;
- min-width: 0 !important;
- box-sizing: border-box !important;
- text-align: left !important;
- border-bottom: ${INFO_UNDERLINE} !important;
- min-height: 1.3em !important;
- }
- .subpack-slip-doc .subpack-slip-info-value-center {
- flex: 1 !important;
- min-width: 0 !important;
- box-sizing: border-box !important;
- text-align: center !important;
- border-bottom: ${INFO_UNDERLINE} !important;
- min-height: 1.3em !important;
- }
- .subpack-slip-doc .subpack-slip-sign-value {
- display: inline-block !important;
- min-width: ${SUBPACK_SLIP_SIGN_VALUE_MIN_WIDTH} !important;
- line-height: 1.35 !important;
- padding: ${SIGN_VALUE_PADDING} !important;
- margin: 0 !important;
- vertical-align: baseline !important;
- border: none !important;
- border-bottom: ${SIGN_UNDERLINE} !important;
- box-sizing: border-box !important;
- }
- .subpack-slip-doc .subpack-slip-sign-label {
- line-height: 1.35 !important;
- vertical-align: baseline !important;
- white-space: nowrap !important;
- }
- .subpack-slip-doc table.subpack-slip-outer-table {
- table-layout: fixed !important;
- width: 100% !important;
- border-collapse: collapse !important;
- }
- .subpack-slip-doc td.subpack-slip-main-col {
- overflow: hidden !important;
- box-sizing: border-box !important;
- vertical-align: top !important;
- }
- .subpack-slip-doc td.subpack-slip-side-labels-col {
- width: ${SIDE_LABELS_COL_WIDTH}px !important;
- min-width: ${SIDE_LABELS_COL_WIDTH}px !important;
- max-width: ${SIDE_LABELS_COL_WIDTH}px !important;
- }
- .subpack-slip-doc .subpack-slip-sign-row-cell,
- .subpack-slip-doc .subpack-slip-sign-inner,
- .subpack-slip-doc .subpack-slip-sign-item {
- font-size: 14px !important;
- }
- .subpack-slip-doc .subpack-slip-sign-row-cell {
- overflow: hidden !important;
- max-width: 100% !important;
- box-sizing: border-box !important;
- }
- .subpack-slip-doc .subpack-slip-sign-inner {
- display: flex !important;
- flex-wrap: wrap !important;
- justify-content: center !important;
- align-items: baseline !important;
- column-gap: 10px !important;
- row-gap: 6px !important;
- max-width: 100% !important;
- box-sizing: border-box !important;
- margin: 0 auto !important;
- white-space: normal !important;
- line-height: 1.35 !important;
- }
- .subpack-slip-doc .subpack-slip-sign-item {
- display: inline !important;
- white-space: nowrap !important;
- line-height: 1.35 !important;
- vertical-align: baseline !important;
- }
- .subpack-slip-doc .subpack-slip-terms-text {
- line-height: 1.35 !important;
- margin: 0 !important;
- padding: 0 !important;
- }
- .subpack-slip-doc td.subpack-slip-terms-cell {
- padding: 10px 6px 14px 6px !important;
- line-height: 1.35 !important;
- vertical-align: top !important;
- }
- .subpack-slip-doc .subpack-slip-handwrite-blank {
- display: inline-block !important;
- min-width: 3.6em !important;
- text-align: center !important;
- letter-spacing: 0.08em !important;
- }
- .subpack-slip-doc .subpack-slip-seal-block {
- position: relative !important;
- line-height: 1.35 !important;
- min-height: 0 !important;
- height: auto !important;
- }
- .subpack-slip-doc .subpack-slip-seal-block img[alt="印章"] {
- position: absolute !important;
- right: 4px !important;
- top: ${SUBPACK_SLIP_SEAL_TOP}px !important;
- left: auto !important;
- transform: none !important;
- width: 140px !important;
- height: 140px !important;
- opacity: 0.85 !important;
- z-index: 10 !important;
- }
- .subpack-slip-doc .subpack-slip-seal-block .subpack-slip-seal-lines {
- position: relative !important;
- z-index: 1 !important;
- line-height: 1.35 !important;
- margin: 0 !important;
- padding: 0 !important;
- }
- .subpack-slip-doc table.subpack-slip-outer-table > tbody > tr > td,
- .subpack-slip-doc table.subpack-slip-outer-table > tbody > tr > th,
- .subpack-slip-doc table.subpack-slip-layout-table td:not(.subpack-slip-product-cell):not(.subpack-slip-gap-before-product-cell):not(.subpack-slip-gap-before-sign-cell),
- .subpack-slip-doc table.subpack-slip-layout-table th:not(.subpack-slip-product-cell),
- .subpack-slip-doc table.subpack-slip-info-table td,
- .subpack-slip-doc table.subpack-slip-info-table th,
- .subpack-slip-doc table.subpack-slip-sign-table td,
- .subpack-slip-doc table.subpack-slip-sign-table th {
- border: none !important;
- background: transparent !important;
- padding: ${TABLE_ROW_PADDING} !important;
- }
- .subpack-slip-doc table.subpack-slip-layout-table,
- .subpack-slip-doc table.subpack-slip-info-table,
- .subpack-slip-doc table.subpack-slip-product-table,
- .subpack-slip-doc table.subpack-slip-footer-table,
- .subpack-slip-doc table.subpack-slip-sign-table {
- border-collapse: collapse !important;
- border-spacing: 0 !important;
- table-layout: fixed !important;
- width: 100% !important;
- }
- .subpack-slip-doc .subpack-slip-product-grid-cell {
- border: ${PRODUCT_GRID_BORDER} !important;
- text-align: center !important;
- vertical-align: middle !important;
- /* padding/line-height 勿 !important:与合同一致,允许 tr 行高拖拽生效 */
- padding: ${PRODUCT_GRID_CELL_PADDING};
- box-sizing: border-box !important;
- line-height: normal;
- overflow: visible !important;
- white-space: normal !important;
- word-wrap: break-word !important;
- overflow-wrap: break-word !important;
- word-break: break-word !important;
- background-color: #fff !important;
- }
- .subpack-slip-doc th.subpack-slip-product-grid-cell {
- padding: ${PRODUCT_GRID_TH_PADDING};
- font-weight: bold !important;
- }
- .subpack-slip-doc .subpack-slip-product-grid-cell > p {
- margin: 0 !important;
- padding: 0 !important;
- text-align: center !important;
- line-height: 1.35;
- }
- .subpack-slip-doc .sx-excel-row-spacer {
- display: block !important;
- float: left !important;
- width: 0 !important;
- max-width: 0 !important;
- margin: 0 !important;
- padding: 0 !important;
- border: 0 !important;
- overflow: hidden !important;
- visibility: hidden !important;
- pointer-events: none !important;
- line-height: 0 !important;
- font-size: 0 !important;
- }
- .subpack-slip-doc .subpack-slip-product-cell:not(.subpack-slip-product-grid-cell) {
- border: ${PRODUCT_GRID_BORDER} !important;
- background-color: #fff !important;
- padding: 10px 6px 14px 6px !important;
- line-height: 1.35 !important;
- vertical-align: top !important;
- white-space: normal !important;
- word-wrap: break-word !important;
- overflow-wrap: break-word !important;
- word-break: break-word !important;
- }
- .subpack-slip-doc .subpack-slip-seal-block,
- .subpack-slip-doc .subpack-slip-seal-lines,
- .subpack-slip-doc .subpack-slip-terms-text {
- margin: 0 !important;
- padding: 0 !important;
- line-height: 1.35 !important;
- }
- .subpack-slip-doc table.subpack-slip-side-labels {
- border: none !important;
- border-collapse: collapse !important;
- width: auto !important;
- margin: 0 auto !important;
- }
- .subpack-slip-doc td.subpack-slip-side-labels-col,
- .subpack-slip-doc table.subpack-slip-side-labels td,
- .subpack-slip-doc table.subpack-slip-side-labels th {
- border: none !important;
- background: transparent !important;
- padding: 0 !important;
- vertical-align: top !important;
- text-align: center !important;
- font-size: 18px !important;
- line-height: 1.35 !important;
- letter-spacing: 0 !important;
- white-space: normal !important;
- }
- `
- export const SUBPACK_SLIP_STYLE_BLOCK = `<style type="text/css">${SUBPACK_SLIP_PRINT_CSS}</style>`
- /** 生成公司印章 HTML(地址/电话单元格右上角) */
- export function buildSubpackSlipCompanySealHtml(): string {
- return `<img src="${SUBPACK_SLIP_SEAL_IMAGE_SRC}" alt="印章" style="${SUBPACK_SLIP_SEAL_IMAGE_STYLE}" />`
- }
- const SUBPACK_SLIP_BUCKET_CHECK_INNER_STYLE =
- 'flex:1;min-width:0;display:flex;flex-wrap:wrap;justify-content:center;align-items:center;gap:14px 18px;box-sizing:border-box;font-size:20px;line-height:1.35;'
- function buildSubpackSlipProductHeaderRowHtml(): string {
- return `<tr class="subpack-slip-product-header">
- <th class="subpack-slip-product-cell subpack-slip-product-grid-cell" style="${PRODUCT_TH}">序号</th>
- <th class="subpack-slip-product-cell subpack-slip-product-grid-cell" style="${PRODUCT_TH}">产品名称</th>
- <th class="subpack-slip-product-cell subpack-slip-product-grid-cell" style="${PRODUCT_TH}">产品型号</th>
- <th class="subpack-slip-product-cell subpack-slip-product-grid-cell" style="${PRODUCT_TH}">包装规格</th>
- <th class="subpack-slip-product-cell subpack-slip-product-grid-cell" style="${PRODUCT_TH}">单位</th>
- <th class="subpack-slip-product-cell subpack-slip-product-grid-cell" style="${PRODUCT_TH}">数量</th>
- <th class="subpack-slip-product-cell subpack-slip-product-grid-cell" style="${PRODUCT_TH}">是否完成</th>
- </tr>`
- }
- function buildSubpackSlipBucketCheckItem(label: string): string {
- return `<label class="subpack-slip-check-item subpack-slip-bucket-check-item" contenteditable="false"><input type="checkbox" class="subpack-slip-check-input" />${label}</label>`
- }
- function buildSubpackSlipBucketCheckboxesHtml(): string {
- return `${buildSubpackSlipBucketCheckItem('全新桶')}${buildSubpackSlipBucketCheckItem('是否回收桶')}${buildSubpackSlipBucketCheckItem('桶循环')}`
- }
- function buildSubpackSlipSlipNoDisplayHtml(): string {
- return `<span class="subpack-slip-slip-no-display">{{subpackSlipNo}} </span>`
- }
- /** 基本信息列宽:日期略宽;下单人/经手人等宽(与 content_style nth-child 一致) */
- const BASIC_INFO_FLEX_DATE = '1.1'
- const BASIC_INFO_FLEX_PERSON = '1'
- function buildSubpackSlipSlipNoRowHtml(): string {
- const spacer = (flex: string) =>
- `<div class="subpack-slip-basic-info-item subpack-slip-basic-info-spacer" style="flex:${flex};min-width:0;"> </div>`
- return `<tr class="subpack-slip-slip-no-row">
- <td colspan="7" style="${INFO_CELL_LEFT}">
- <div class="subpack-slip-basic-info-inner" style="display:flex;flex-wrap:wrap;justify-content:space-between;align-items:flex-end;gap:8px 12px;width:100%;box-sizing:border-box;">
- ${spacer(BASIC_INFO_FLEX_DATE)}${spacer(BASIC_INFO_FLEX_DATE)}${spacer(BASIC_INFO_FLEX_PERSON)}
- <div class="subpack-slip-info-row subpack-slip-basic-info-item subpack-slip-slip-no-field" style="${INFO_ROW}flex:${BASIC_INFO_FLEX_PERSON};min-width:0;">
- <span class="subpack-slip-info-label subpack-slip-slip-no-anchor" style="${INFO_LABEL}">
- <span class="subpack-slip-slip-no-anchor-text">经手人:</span>
- ${buildSubpackSlipSlipNoDisplayHtml()}
- </span>
- <span class="subpack-slip-info-value-left" style="${INFO_VALUE_INLINE_LEFT}"> </span>
- </div>
- </div>
- </td>
- </tr>`
- }
- function buildSubpackSlipBasicInfoRowHtml(): string {
- const field = (label: string, placeholder: string, flex: string = BASIC_INFO_FLEX_PERSON) =>
- `<div class="subpack-slip-info-row subpack-slip-basic-info-item" style="${INFO_ROW}flex:${flex};min-width:0;">
- <span class="subpack-slip-info-label" style="${INFO_LABEL}">${label}</span>
- <span class="subpack-slip-info-value-left" style="${INFO_VALUE_INLINE_LEFT}">${placeholder} </span>
- </div>`
- return `<tr class="subpack-slip-basic-info-row">
- <td colspan="7" style="${INFO_CELL_LEFT}">
- <div class="subpack-slip-basic-info-inner" style="display:flex;flex-wrap:wrap;justify-content:space-between;align-items:flex-end;gap:8px 12px;width:100%;box-sizing:border-box;">
- ${field('下单日期:', '{{orderDate}}', BASIC_INFO_FLEX_DATE)}
- ${field('交货日期:', '{{deliveryDate}}', BASIC_INFO_FLEX_DATE)}
- ${field('下单人:', '{{orderUser}}', BASIC_INFO_FLEX_PERSON)}
- ${field('经手人:', '{{handlerName}}', BASIC_INFO_FLEX_PERSON)}
- </div>
- </td>
- </tr>`
- }
- function buildSubpackSlipRemarkExtraInputHtml(): string {
- return `<span class="subpack-slip-remark-extra-wrap" contenteditable="false"><input type="text" class="subpack-slip-remark-extra-input" value="{{remarkExtra}}" /></span>`
- }
- function buildSubpackSlipRemarkUnderlineGroupHtml(remarkContent: string = '{{remark}}'): string {
- return `<span class="subpack-slip-remark-underline-group"><span class="subpack-slip-remark-value">${remarkContent} </span>${buildSubpackSlipRemarkExtraInputHtml()}</span>`
- }
- function buildSubpackSlipRemarkRowHtml(): string {
- return `<div class="subpack-slip-remark-row-inner">
- <span class="subpack-slip-remark-label">备注:</span>
- ${buildSubpackSlipRemarkUnderlineGroupHtml()}
- <div class="subpack-slip-bucket-check-row" contenteditable="false" style="${SUBPACK_SLIP_BUCKET_CHECK_INNER_STYLE}">${buildSubpackSlipBucketCheckboxesHtml()}</div>
- </div>`
- }
- /** 修复旧模板中 label / 桶选项容器缺少 contenteditable="false" 导致 TinyMCE 内多选失效 */
- function ensureSubpackSlipInteractiveMarkup(html: string): string {
- let result = html.replace(/<label\b([^>]*\bsubpack-slip-check-item\b[^>]*)>/gi, (match, attrs: string) => {
- if (/contenteditable=/i.test(attrs)) return match
- return `<label${attrs} contenteditable="false">`
- })
- result = result.replace(/<div class="subpack-slip-bucket-check-row"([^>]*)>/g, (match, attrs: string) => {
- if (/contenteditable=/i.test(attrs)) return match
- return `<div class="subpack-slip-bucket-check-row"${attrs} contenteditable="false">`
- })
- return result
- }
- /** @deprecated 分装单已移除右侧联次,保留空实现兼容旧引用 */
- export function buildSubpackSlipSideLabelsHtml(): string {
- return ''
- }
- /** 生成带占位符的分装单 HTML 模板(方案A:信息 / 物料 / 页脚+签字 分表) */
- export function generateSubpackSlipTemplate(_plan?: Plan): string {
- return `${SUBPACK_SLIP_STYLE_BLOCK}<div class="subpack-slip-doc" style="font-family:${DOC_FONT};${FONT_BODY}color:#000;line-height:1.5;background:#fff;padding:${DOC_PADDING};${DOC_WRAP_EXTRA}">
- <!-- 上部分:标题区 -->
- <div class="subpack-slip-title-section" style="position:relative;min-height:56px;margin-bottom:14px;">
- <img src="/logo.gif" alt="logo" style="position:absolute;left:${LOGO_LEFT}px;top:50%;transform:translateY(-50%);height:${LOGO_HEIGHT}px;width:auto;" />
- <div style="text-align:center;padding:4px 60px 0 100px;">
- <div class="subpack-slip-title-heading" style="${TITLE_STYLE}">{{companyName}}</div>
- <div class="subpack-slip-title-heading" style="${TITLE_SUB_STYLE}">分 装 单</div>
- </div>
- </div>
- <!-- 1. 基本信息(独立表,不可拖宽高) -->
- <table class="subpack-slip-info-table" style="width:100%;border-collapse:collapse;table-layout:fixed;border:none !important;">
- ${INFO_COLGROUP}
- <!-- 分装单号(右缘与经手人标签右缘对齐) -->
- ${buildSubpackSlipSlipNoRowHtml()}
- <!-- 基本信息一行四列 -->
- ${buildSubpackSlipBasicInfoRowHtml()}
- </table>
- <div class="subpack-slip-section-gap subpack-slip-gap-before-product" style="${GAP_BEFORE_PRODUCT_CELL}"> </div>
- <!-- 2. 物料明细(含备注+桶选项) -->
- <table class="subpack-slip-product-table" style="width:100%;border-collapse:collapse;table-layout:fixed;border:none !important;">
- ${PRODUCT_COLGROUP}
- ${buildSubpackSlipProductHeaderRowHtml()}
- {{productTable}}
- <tr>
- <td colspan="7" class="subpack-slip-product-cell subpack-slip-remark-cell" style="${PRODUCT_TABLE_CELL_LEFT}">
- ${buildSubpackSlipRemarkRowHtml()}
- </td>
- </tr>
- </table>
- <div class="subpack-slip-section-gap subpack-slip-gap-before-sign" style="${GAP_BEFORE_SIGN_CELL}"> </div>
- <!-- 3. 签字区(独立表) -->
- <table class="subpack-slip-sign-table" style="width:100%;border-collapse:collapse;table-layout:fixed;border:none !important;">
- ${buildSubpackSlipSignRowHtml()}
- </table>
- </div>`
- }
- /** 移除印章、地址/电话行(分装单不再展示) */
- function normalizeSubpackSlipRemoveAddressAndSeal(html: string): string {
- if (!html.includes('subpack-slip-doc')) return html
- let result = html.replace(/<img[^>]*alt="印章"[^>]*>/gi, '')
- result = result.replace(/\{\{companySeal\}\}/g, '')
- result = result.replace(/<!--\s*印章图片\s*-->\s*/g, '')
- result = result.replace(/<!--\s*地址\/电话[\s\S]*?-->\s*/g, '')
- result = result.replace(/<!--\s*地址\/印章[\s\S]*?-->\s*/g, '')
- result = result.replace(/<div class="subpack-slip-address-block"[\s\S]*?<\/div>/g, '')
- result = result.replace(/<div class="subpack-slip-seal-block"[\s\S]*?<\/div>/g, '')
- result = result.replace(
- /<tr>\s*<td colspan="7"[^>]*>\s*<div class="subpack-slip-address-block"[\s\S]*?<\/tr>\s*/g,
- ''
- )
- result = result.replace(
- /<tr>\s*<td colspan="7"[^>]*>\s*<div class="subpack-slip-seal-block"[\s\S]*?<\/tr>\s*/g,
- ''
- )
- result = result.replace(
- /<tr>\s*<td colspan="4"[^>]*>\s* \s*<\/td>\s*<td colspan="3"[^>]*>\s*<div class="subpack-slip-seal-block"[\s\S]*?<\/tr>\s*/g,
- ''
- )
- result = result.replace(
- /<tr>\s*<!--\s*地址[\s\S]*?<\/tr>\s*/g,
- ''
- )
- result = result.replace(
- /<tr>\s*<td colspan="7"[^>]*>[\s\S]*?地址:[\s\S]*?联系人电话:[\s\S]*?<\/tr>\s*/g,
- ''
- )
- return result.replace(/min-height:76px;/g, 'min-height:0;')
- }
- /** @deprecated 请使用 normalizeSubpackSlipRemoveAddressAndSeal */
- function normalizeSubpackSlipSealPlaceholder(html: string): string {
- return normalizeSubpackSlipRemoveAddressAndSeal(html)
- }
- function applyProductGridBorderToStyle(style: string): string {
- // 保留拖拽产生的 height / min-height / line-height,避免打开模板时被抹掉
- const keptHeight = style.match(/height:\s*[^;]+;?/gi)?.join('') || ''
- const keptMinHeight = style.match(/min-height:\s*[^;]+;?/gi)?.join('') || ''
- const keptLineHeight = style.match(/line-height:\s*[^;]+;?/gi)?.join('') || ''
- let next = style
- .replace(/border:[^;]*;?/gi, '')
- .replace(/border-right:[^;]*;?/gi, '')
- .replace(/border-bottom:[^;]*;?/gi, '')
- .replace(/border-left:[^;]*;?/gi, '')
- .replace(/border-top:[^;]*;?/gi, '')
- .replace(/height:\s*[^;]+;?/gi, '')
- .replace(/min-height:\s*[^;]+;?/gi, '')
- .replace(/line-height:\s*[^;]+;?/gi, '')
- .replace(/text-align:left;/g, 'text-align:center;')
- .replace(/vertical-align:top;/g, '')
- .trim()
- const lineHeight = keptLineHeight || 'line-height:normal;'
- return `border:${PRODUCT_GRID_BORDER} !important;text-align:center;padding:${PRODUCT_GRID_CELL_PADDING};box-sizing:border-box;${lineHeight}${keptHeight}${keptMinHeight}${next}`
- }
- function patchProductFooterCellAttrs(attrs: string, classes: string, style: string): string {
- let next = attrs
- if (next.includes('class="')) {
- next = next.replace(/class="([^"]*)"/, (_, cls: string) => {
- let updated = cls.trim()
- for (const item of classes.split(/\s+/)) {
- if (item && !updated.includes(item)) updated += ` ${item}`
- }
- return `class="${updated.trim()}"`
- })
- } else {
- next = `${next} class="${classes.trim()}"`
- }
- if (next.includes('style="')) {
- next = next.replace(/style="[^"]*"/, `style="${style}"`)
- } else {
- next = `${next} style="${style}"`
- }
- return next
- }
- /** 物料明细网格:全边框 inline + 原生居中(兼容已保存模板,对齐合同 PDF) */
- function patchSubpackSlipProductGridCellAttrs(attrs: string): string {
- let next = attrs.includes('class="')
- ? attrs.replace(/class="([^"]*)"/, (_, cls: string) => {
- let updated = cls
- if (!updated.includes('subpack-slip-product-cell')) updated += ' subpack-slip-product-cell'
- if (!updated.includes('subpack-slip-product-grid-cell')) updated += ' subpack-slip-product-grid-cell'
- return `class="${updated.trim()}"`
- })
- : `${attrs} class="subpack-slip-product-cell subpack-slip-product-grid-cell"`
- if (next.includes('style="')) {
- next = next.replace(/style="([^"]*)"/, (_, style: string) => `style="${applyProductGridBorderToStyle(style)}"`)
- } else {
- next = `${next} style="${PRODUCT_GRID_CELL}"`
- }
- return next
- }
- function normalizeSubpackSlipProductGridCell(html: string): string {
- if (!html.includes('subpack-slip-doc') || !html.includes('subpack-slip-product-cell')) return html
- let result = html.replace(/<tr([^>]*)>\s*(<th[\s\S]*?<\/th>\s*){7}\s*<\/tr>/g, (rowMatch) => {
- if (
- (!rowMatch.includes('物料编号') && !rowMatch.includes('序号')) ||
- rowMatch.includes('colspan')
- ) {
- return rowMatch
- }
- let row = rowMatch.includes('subpack-slip-product-header')
- ? rowMatch
- : rowMatch.replace('<tr', '<tr class="subpack-slip-product-header"')
- return row.replace(/<(th)([^>]*)>([\s\S]*?)<\/th>/g, (_m, tag, attrs, content) => {
- return `<${tag}${patchSubpackSlipProductGridCellAttrs(attrs)}>${formatSubpackSlipProductGridCellContent(content)}</${tag}>`
- })
- })
- result = result.replace(/<tr([^>]*)>\s*(<td(?![^>]*colspan)[^>]*>[\s\S]*?<\/td>\s*){7}\s*<\/tr>/g, (rowMatch) => {
- if (
- rowMatch.includes('备注') ||
- rowMatch.includes('1.本送货单') ||
- rowMatch.includes('制单人:') ||
- rowMatch.includes('仓库:') ||
- rowMatch.includes('subpack-slip-complete-check-item') ||
- rowMatch.includes('暂无产品信息')
- ) {
- return rowMatch
- }
- return rowMatch.replace(/<(td)([^>]*)>([\s\S]*?)<\/td>/g, (_m, tag, attrs, content) => {
- if (attrs.includes('colspan')) return `<${tag}${attrs}>${content}</td>`
- return `<${tag}${patchSubpackSlipProductGridCellAttrs(attrs)}>${formatSubpackSlipProductGridCellContent(content)}</${tag}>`
- })
- })
- return normalizeSubpackSlipGridCellWrappers(result)
- }
- /** 去掉 TinyMCE / 历史包裹层,单元格内保留纯文本(对齐合同 PDF) */
- function normalizeSubpackSlipGridCellWrappers(html: string): string {
- if (!html.includes('subpack-slip-product-grid-cell')) return html
- const unwrap = (tag: string, attrs: string, content: string) =>
- `<${tag}${attrs}>${formatSubpackSlipProductGridCellContent(content)}</${tag}>`
- let result = html.replace(
- /<(td|th)([^>]*subpack-slip-product-grid-cell[^>]*)>\s*<span class="subpack-slip-product-grid-align"[^>]*>([\s\S]*?)<\/span>\s*<\/\1>/gi,
- (_m, tag, attrs, content) => unwrap(tag, attrs, content)
- )
- result = result.replace(
- /<(td|th)([^>]*subpack-slip-product-grid-cell[^>]*)>\s*<span class="subpack-slip-pdf-grid-text"[^>]*>([\s\S]*?)<\/span>\s*<\/\1>/gi,
- (_m, tag, attrs, content) => unwrap(tag, attrs, content)
- )
- result = result.replace(
- /<(td|th)([^>]*subpack-slip-product-grid-cell[^>]*)>\s*<div class="subpack-slip-pdf-grid-inner"[^>]*>([\s\S]*?)<\/div>\s*<\/\1>/gi,
- (_m, tag, attrs, content) => unwrap(tag, attrs, content)
- )
- result = result.replace(
- /<(td|th)([^>]*subpack-slip-product-grid-cell[^>]*)>\s*<p[^>]*>([\s\S]*?)<\/p>\s*<\/\1>/gi,
- (_m, tag, attrs, content) => unwrap(tag, attrs, content)
- )
- result = result.replace(
- /<(td|th)([^>]*subpack-slip-product-grid-cell[^>]*)>\s*<div(?![^>]*class="subpack-slip)[^>]*>([\s\S]*?)<\/div>\s*<\/\1>/gi,
- (_m, tag, attrs, content) => unwrap(tag, attrs, content)
- )
- result = result.replace(/<(td|th)([^>]*subpack-slip-product-grid-cell[^>]*)\s+height="[^"]*"/gi, '<$1$2')
- result = result.replace(/<(tr)([^>]*class="[^"]*subpack-slip-product[^"]*"[^>]*)\s+height="[^"]*"/gi, '<$1$2')
- return result
- }
- function extractSubpackSlipGridCellContent(cell: HTMLTableCellElement): string {
- let content = cell.innerHTML.trim()
- const onlyChild = cell.firstElementChild
- if (onlyChild && cell.childElementCount === 1) {
- const tag = onlyChild.tagName
- if (
- tag === 'P' ||
- tag === 'DIV' ||
- tag === 'SPAN' ||
- onlyChild.classList.contains('subpack-slip-pdf-grid-inner') ||
- onlyChild.classList.contains('subpack-slip-pdf-grid-text')
- ) {
- content = onlyChild.innerHTML.trim()
- }
- }
- return formatSubpackSlipProductGridCellContent(content)
- }
- function applySubpackSlipPdfGridCellStyles(cell: HTMLTableCellElement, isHeader: boolean): void {
- const content = extractSubpackSlipGridCellContent(cell)
- cell.innerHTML = content || ' '
- cell.removeAttribute('height')
- cell.style.removeProperty('height')
- cell.style.removeProperty('min-height')
- cell.style.setProperty('padding', isHeader ? PRODUCT_GRID_PDF_TH_PADDING : PRODUCT_GRID_PDF_TD_PADDING, 'important')
- cell.style.setProperty('text-align', 'center', 'important')
- cell.style.setProperty('font-size', '20px', 'important')
- cell.style.setProperty('line-height', PRODUCT_GRID_PDF_LINE_HEIGHT, 'important')
- cell.style.setProperty('box-sizing', 'border-box', 'important')
- cell.style.setProperty('overflow', 'visible', 'important')
- cell.style.setProperty('vertical-align', 'middle', 'important')
- if (isHeader) {
- cell.style.setProperty('font-weight', 'bold', 'important')
- }
- }
- function applySubpackSlipPdfFooterCellStyles(cell: HTMLTableCellElement): void {
- cell.removeAttribute('height')
- cell.style.removeProperty('height')
- cell.style.removeProperty('min-height')
- cell.style.setProperty('padding', PRODUCT_FOOTER_PDF_PADDING, 'important')
- cell.style.setProperty('font-size', '20px', 'important')
- cell.style.setProperty('line-height', PRODUCT_FOOTER_PDF_LINE_HEIGHT, 'important')
- cell.style.setProperty('box-sizing', 'border-box', 'important')
- cell.style.setProperty('overflow', 'visible', 'important')
- cell.style.setProperty('vertical-align', 'top', 'important')
- cell.style.setProperty('text-align', 'left', 'important')
- }
- function unwrapSubpackSlipPdfCellParagraph(cell: HTMLTableCellElement): void {
- const onlyChild = cell.firstElementChild
- if (onlyChild?.tagName === 'P' && cell.childElementCount === 1) {
- cell.innerHTML = onlyChild.innerHTML.trim() || ' '
- }
- }
- function prepareSubpackSlipPdfRemarkCell(cell: HTMLTableCellElement): void {
- unwrapSubpackSlipPdfCellParagraph(cell)
- applySubpackSlipPdfFooterCellStyles(cell)
- cell.querySelectorAll('.subpack-slip-remark-underline-group').forEach((node) => {
- const group = node as HTMLElement
- group.style.setProperty('display', 'inline-flex', 'important')
- group.style.setProperty('align-items', 'baseline', 'important')
- group.style.setProperty('flex', '1', 'important')
- group.style.setProperty('min-width', '200px', 'important')
- group.style.setProperty('max-width', '420px', 'important')
- group.style.setProperty('border', 'none', 'important')
- group.style.setProperty('border-bottom', INFO_UNDERLINE, 'important')
- // 与签字区一致:加大底部 padding,避免 html2canvas 把下划线叠在文字上
- group.style.setProperty('padding', SIGN_VALUE_PDF_PADDING, 'important')
- group.style.setProperty('gap', '0', 'important')
- group.style.setProperty('line-height', '1.35', 'important')
- group.style.setProperty('overflow', 'visible', 'important')
- group.style.setProperty('box-sizing', 'border-box', 'important')
- })
- cell.querySelectorAll('.subpack-slip-remark-value').forEach((node) => {
- const el = node as HTMLElement
- el.style.setProperty('display', 'inline-block', 'important')
- el.style.setProperty('border-bottom', 'none', 'important')
- el.style.setProperty('padding', '0 2px', 'important')
- el.style.setProperty('line-height', '1.35', 'important')
- el.style.setProperty('overflow', 'visible', 'important')
- el.style.setProperty('vertical-align', 'baseline', 'important')
- })
- cell.querySelectorAll('input.subpack-slip-remark-extra-input').forEach((node) => {
- const input = node as HTMLInputElement
- const text = (input.value || '').trim()
- const span = document.createElement('span')
- span.className = 'subpack-slip-remark-extra-text-pdf'
- span.textContent = text || '\u00a0'
- span.style.setProperty('display', 'inline-block', 'important')
- span.style.setProperty('min-width', '80px', 'important')
- span.style.setProperty('flex', '1', 'important')
- span.style.setProperty('padding', '0 2px', 'important')
- span.style.setProperty('border-bottom', 'none', 'important')
- span.style.setProperty('font-size', '20px', 'important')
- span.style.setProperty('line-height', '1.35', 'important')
- span.style.setProperty('vertical-align', 'baseline', 'important')
- input.replaceWith(span)
- })
- }
- function prepareSubpackSlipPdfTermsCell(cell: HTMLTableCellElement): void {
- applySubpackSlipPdfFooterCellStyles(cell)
- const terms = cell.querySelector('.subpack-slip-terms-text') as HTMLElement | null
- if (terms) {
- terms.style.setProperty('margin', '0', 'important')
- terms.style.setProperty('padding', '0', 'important')
- terms.style.setProperty('line-height', PRODUCT_FOOTER_PDF_LINE_HEIGHT, 'important')
- }
- }
- function prepareSubpackSlipPdfSealCell(cell: HTMLTableCellElement): void {
- applySubpackSlipPdfFooterCellStyles(cell)
- const block =
- (cell.querySelector('.subpack-slip-address-block') as HTMLElement | null) ||
- (cell.querySelector('.subpack-slip-seal-block') as HTMLElement | null)
- const lines = cell.querySelector('.subpack-slip-seal-lines') as HTMLElement | null
- if (block) {
- block.style.setProperty('padding', '0', 'important')
- block.style.setProperty('margin', '0', 'important')
- block.style.setProperty('position', 'relative', 'important')
- block.style.setProperty('line-height', PRODUCT_FOOTER_PDF_LINE_HEIGHT, 'important')
- }
- if (lines) {
- lines.style.setProperty('margin', '0', 'important')
- lines.style.setProperty('padding', '0', 'important')
- lines.style.setProperty('line-height', PRODUCT_FOOTER_PDF_LINE_HEIGHT, 'important')
- }
- }
- function prepareSubpackSlipPdfSignValues(root: HTMLElement): void {
- root.querySelectorAll('.subpack-slip-sign-value').forEach((node) => {
- const el = node as HTMLElement
- el.style.setProperty('display', 'inline-block', 'important')
- el.style.setProperty('padding', SIGN_VALUE_PDF_PADDING, 'important')
- el.style.setProperty('line-height', '1.35', 'important')
- el.style.setProperty('vertical-align', 'baseline', 'important')
- el.style.setProperty('border', 'none', 'important')
- el.style.setProperty('border-bottom', SIGN_UNDERLINE, 'important')
- el.style.setProperty('box-sizing', 'border-box', 'important')
- el.style.setProperty('min-width', SUBPACK_SLIP_SIGN_VALUE_MIN_WIDTH, 'important')
- })
- }
- /** PDF 截图前:将原生 checkbox 替换为 canvas 方框图 + 文字(html2canvas 对 flex/SVG 对齐不稳定) */
- const subpackSlipPdfCheckboxImgCache: { empty?: string; checked?: string } = {}
- function buildSubpackSlipPdfCheckboxImgDataUrl(checked: boolean): string {
- const key = checked ? 'checked' : 'empty'
- if (subpackSlipPdfCheckboxImgCache[key]) return subpackSlipPdfCheckboxImgCache[key]!
- const size = 32
- const canvas = document.createElement('canvas')
- canvas.width = size
- canvas.height = size
- const ctx = canvas.getContext('2d')
- if (!ctx) return ''
- ctx.fillStyle = '#ffffff'
- ctx.fillRect(0, 0, size, size)
- ctx.strokeStyle = '#000000'
- ctx.lineWidth = 2
- ctx.strokeRect(1, 1, size - 2, size - 2)
- if (checked) {
- ctx.beginPath()
- ctx.moveTo(8, 17)
- ctx.lineTo(14, 23)
- ctx.lineTo(24, 8)
- ctx.strokeStyle = '#000000'
- ctx.lineWidth = 3.5
- ctx.lineCap = 'round'
- ctx.lineJoin = 'round'
- ctx.stroke()
- }
- const url = canvas.toDataURL('image/png')
- subpackSlipPdfCheckboxImgCache[key] = url
- return url
- }
- function buildSubpackSlipPdfCheckItemHtml(checked: boolean, labelText: string, extraClass = ''): string {
- const imgSrc = buildSubpackSlipPdfCheckboxImgDataUrl(checked)
- const className = `subpack-slip-check-item-pdf${extraClass ? ` ${extraClass}` : ''}`
- if (!imgSrc) return `<span class="${className}">${labelText}</span>`
- return `<span class="${className}"><img class="subpack-slip-check-img-pdf" src="${imgSrc}" width="16" height="16" alt="" /><span class="subpack-slip-check-text-pdf">${labelText}</span></span>`
- }
- function applySubpackSlipPdfCheckboxLayout(root: HTMLElement): void {
- root.querySelectorAll('.subpack-slip-bucket-check-row').forEach((node) => {
- const row = node as HTMLElement
- row.style.setProperty('display', 'flex', 'important')
- row.style.setProperty('flex', '1', 'important')
- row.style.setProperty('justify-content', 'center', 'important')
- row.style.setProperty('align-items', 'center', 'important')
- row.style.setProperty('flex-wrap', 'wrap', 'important')
- row.style.setProperty('gap', '14px 18px', 'important')
- row.style.setProperty('line-height', '20px', 'important')
- row.style.setProperty('font-size', '20px', 'important')
- })
- root.querySelectorAll('.subpack-slip-remark-row-inner').forEach((node) => {
- const row = node as HTMLElement
- row.style.setProperty('display', 'flex', 'important')
- row.style.setProperty('align-items', 'center', 'important')
- row.style.setProperty('width', '100%', 'important')
- row.style.setProperty('gap', '12px', 'important')
- })
- root.querySelectorAll('.subpack-slip-check-item-pdf').forEach((node) => {
- const item = node as HTMLElement
- const isComplete = item.classList.contains('subpack-slip-complete-check-item-pdf')
- item.style.setProperty('display', 'inline-block', 'important')
- item.style.setProperty('white-space', 'nowrap', 'important')
- item.style.setProperty('font-family', "SimSun, 'Songti SC', serif", 'important')
- item.style.setProperty('font-size', '20px', 'important')
- item.style.setProperty('line-height', '20px', 'important')
- item.style.setProperty('height', '20px', 'important')
- item.style.setProperty('margin-right', isComplete ? '0' : '18px', 'important')
- item.style.setProperty('vertical-align', 'middle', 'important')
- const img = item.querySelector('img.subpack-slip-check-img-pdf') as HTMLImageElement | null
- if (img) {
- img.style.setProperty('display', 'inline-block', 'important')
- img.style.setProperty('width', '16px', 'important')
- img.style.setProperty('height', '16px', 'important')
- img.style.setProperty('margin', '0 4px 0 0', 'important')
- img.style.setProperty('padding', '0', 'important')
- img.style.setProperty('border', 'none', 'important')
- img.style.setProperty('vertical-align', 'middle', 'important')
- }
- const text = item.querySelector('.subpack-slip-check-text-pdf') as HTMLElement | null
- if (text) {
- text.style.setProperty('display', 'inline-block', 'important')
- text.style.setProperty('font-size', '20px', 'important')
- text.style.setProperty('line-height', '20px', 'important')
- text.style.setProperty('vertical-align', 'middle', 'important')
- text.style.setProperty('position', 'relative', 'important')
- text.style.setProperty('top', '-12px', 'important')
- }
- })
- }
- function prepareSubpackSlipPdfCheckboxes(root: HTMLElement): void {
- root.querySelectorAll('label.subpack-slip-check-item').forEach((node) => {
- const label = node as HTMLElement
- const input = label.querySelector('input.subpack-slip-check-input') as HTMLInputElement | null
- const checked = input?.checked ?? false
- const labelText = (label.textContent || '').replace(/[✓☑□☐]/g, '').trim()
- const extraClass = label.classList.contains('subpack-slip-complete-check-item')
- ? 'subpack-slip-complete-check-item-pdf'
- : label.classList.contains('subpack-slip-bucket-check-item')
- ? 'subpack-slip-bucket-check-item-pdf'
- : ''
- const html = buildSubpackSlipPdfCheckItemHtml(checked, labelText, extraClass)
- const wrapper = document.createElement('div')
- wrapper.innerHTML = html.trim()
- const replacement = wrapper.firstElementChild
- if (replacement) {
- label.replaceWith(replacement)
- }
- })
- applySubpackSlipPdfCheckboxLayout(root)
- }
- /** PDF 截图前:物料网格 + 备注/条款/地址行 + 签字区统一样式 */
- export function prepareSubpackSlipPdfGridCells(root: HTMLElement): void {
- root.querySelectorAll('td.subpack-slip-product-grid-cell, th.subpack-slip-product-grid-cell').forEach((node) => {
- applySubpackSlipPdfGridCellStyles(node as HTMLTableCellElement, node.tagName === 'TH')
- })
- root.querySelectorAll('td.subpack-slip-terms-cell').forEach((node) => {
- prepareSubpackSlipPdfTermsCell(node as HTMLTableCellElement)
- })
- root.querySelectorAll('td.subpack-slip-product-cell:not(.subpack-slip-product-grid-cell)').forEach((node) => {
- const cell = node as HTMLTableCellElement
- if (cell.classList.contains('subpack-slip-terms-cell')) return
- if (cell.querySelector('.subpack-slip-address-block, .subpack-slip-seal-block')) {
- prepareSubpackSlipPdfSealCell(cell)
- return
- }
- if (/备注\s*[::]/.test(cell.textContent || '')) {
- prepareSubpackSlipPdfRemarkCell(cell)
- }
- })
- root.querySelectorAll('tr').forEach((row) => {
- if (!row.querySelector('.subpack-slip-product-cell')) return
- row.removeAttribute('height')
- row.style.removeProperty('height')
- row.style.removeProperty('min-height')
- })
- prepareSubpackSlipPdfSignValues(root)
- prepareSubpackSlipPdfCheckboxes(root)
- // 亚像素 border 在 Windows DPI 下易变粗;截图前改为 1px 背景单线
- applyPdfStableTableCellBorders(root.querySelectorAll('.subpack-slip-product-cell'))
- }
- /** 备注、条款、印章行:统一 0.3px 全边框(兼容已保存模板,与物料网格衔接) */
- function normalizeSubpackSlipProductTableFooter(html: string): string {
- if (!html.includes('subpack-slip-doc')) return html
- let result = html
- result = result.replace(/<td([^>]*colspan="7"[^>]*)>(\s*)备注:/g, (_match, attrs, space) => {
- const patched = patchProductFooterCellAttrs(
- attrs,
- 'subpack-slip-product-cell subpack-slip-remark-cell',
- PRODUCT_TABLE_CELL_LEFT
- )
- if (_match.includes('subpack-slip-remark-row-inner')) return _match
- return `<td${patched}>${space}${buildSubpackSlipRemarkRowHtml()}`
- })
- result = result.replace(
- /<td([^>]*colspan="4"[^>]*)>\s*(<div class="subpack-slip-terms-text"|\s*1\.本送货单)/g,
- (_match, attrs, contentStart) => {
- const patched = patchProductFooterCellAttrs(
- attrs,
- 'subpack-slip-product-cell subpack-slip-terms-cell',
- PRODUCT_TERMS_CELL_LEFT
- )
- return `<td${patched}>\n ${contentStart}`
- }
- )
- result = result.replace(
- /<td([^>]*colspan="3"[^>]*)>\s*<div class="subpack-slip-seal-block"/g,
- (_match, attrs) => {
- const patched = patchProductFooterCellAttrs(attrs, 'subpack-slip-product-cell', PRODUCT_TABLE_CELL_LEFT)
- return `<td${patched}>\n <div class="subpack-slip-seal-block"`
- }
- )
- return result
- }
- /** 条款单元格行高随内容(兼容已保存模板) */
- function normalizeSubpackSlipTermsCell(html: string): string {
- if (!html.includes('subpack-slip-doc')) return html
- const patchTermsTd = (attrs: string) => {
- let nextAttrs = attrs
- if (nextAttrs.includes('class="')) {
- nextAttrs = nextAttrs.replace(/class="([^"]*)"/, (_, cls: string) =>
- cls.includes('subpack-slip-terms-cell') ? `class="${cls}"` : `class="${cls} subpack-slip-terms-cell"`
- )
- } else {
- nextAttrs = `${nextAttrs} class="subpack-slip-product-cell subpack-slip-terms-cell"`
- }
- if (nextAttrs.includes('style="')) {
- nextAttrs = nextAttrs.replace(/style="[^"]*"/, `style="${PRODUCT_TERMS_CELL_LEFT}"`)
- } else {
- nextAttrs = `${nextAttrs} style="${PRODUCT_TERMS_CELL_LEFT}"`
- }
- return nextAttrs
- }
- let result = html.replace(
- /<td colspan="4"([^>]*)>\s*<div class="subpack-slip-terms-text"/g,
- (match, attrs) => {
- if (attrs.includes('subpack-slip-terms-cell') && attrs.includes('border:0.3px solid #000')) return match
- return `<td colspan="4"${patchTermsTd(attrs)}>\n <div class="subpack-slip-terms-text"`
- }
- )
- result = result.replace(
- /<div class="subpack-slip-seal-block" style="([^"]*)"/g,
- (match, style) => {
- if (style.includes('padding:0;') && !style.includes('min-height')) return match
- let nextStyle = style.replace(/min-height:\d+px;/g, '').replace(/padding:[^;]*;/g, '')
- nextStyle = `padding:0;${nextStyle}`
- return `<div class="subpack-slip-seal-block" style="${nextStyle}"`
- }
- )
- return result
- }
- /** 移除旧版送货条款行(兼容已保存模板) */
- function normalizeSubpackSlipRemoveTermsRow(html: string): string {
- if (!html.includes('subpack-slip-doc')) return html
- if (!html.includes('1.本送货单') && !html.includes('subpack-slip-terms-text')) return html
- let result = html.replace(
- /<td colspan="4"([^>]*class="[^"]*subpack-slip-terms-cell[^"]*"[^>]*)>[\s\S]*?<\/td>/g,
- `<td colspan="4" class="subpack-slip-product-cell" style="${PRODUCT_TABLE_CELL_LEFT}"> </td>`
- )
- result = result.replace(
- /<td colspan="4"([^>]*)>\s*(<div class="subpack-slip-terms-text"[\s\S]*?<\/div>|1\.本送货单[\s\S]*?)<\/td>/g,
- `<td colspan="4" class="subpack-slip-product-cell" style="${PRODUCT_TABLE_CELL_LEFT}"> </td>`
- )
- return result
- }
- /** 刷新已有间距行的 inline 样式(TinyMCE 保存后可能丢失) */
- function refreshSubpackSlipGapBeforeProduct(html: string): string {
- return html
- .replace(
- /<tr class="subpack-slip-gap-before-product">[\s\S]*?<\/tr>/,
- `<tr class="subpack-slip-gap-before-product">\n <td colspan="7" class="subpack-slip-gap-before-product-cell" style="${GAP_BEFORE_PRODUCT_CELL}"> </td>\n </tr>`
- )
- .replace(
- /<div class="subpack-slip-section-gap subpack-slip-gap-before-product"[^>]*>[\s\S]*?<\/div>/,
- `<div class="subpack-slip-section-gap subpack-slip-gap-before-product" style="${GAP_BEFORE_PRODUCT_CELL}"> </div>`
- )
- }
- /** 在收货联系方式行与物料表之间插入间距行(兼容已保存模板) */
- function normalizeSubpackSlipGapBeforeProduct(html: string): string {
- if (!html.includes('subpack-slip-doc')) {
- return html
- }
- if (html.includes('subpack-slip-gap-before-product')) {
- return refreshSubpackSlipGapBeforeProduct(html)
- }
- const gapRow = `<tr class="subpack-slip-gap-before-product"><td colspan="7" class="subpack-slip-gap-before-product-cell" style="${GAP_BEFORE_PRODUCT_CELL}"> </td></tr>\n `
- const withComment = html.replace(
- /(<span class="subpack-slip-info-label"[^>]*>经(?: )?手人:<\/span>[\s\S]*?<\/tr>\s*)(<!--\s*中:产品明细表[\s\S]*?<tr[^>]*>\s*<th[^>]*>(?:序号|物料编号))/,
- `$1${gapRow}$2`
- )
- if (withComment !== html) return withComment
- const withHeader = html.replace(
- /(<span class="subpack-slip-info-label"[^>]*>经(?: )?手人:<\/span>[\s\S]*?<\/tr>\s*)(<tr[^>]*>\s*<th[^>]*>(?:序号|物料编号))/,
- `$1${gapRow}$2`
- )
- if (withHeader !== html) return withHeader
- return html.replace(
- /(下单人:[\s\S]*?<\/tr>\s*)(<!--\s*中:产品明细表[\s\S]*?<tr[^>]*>\s*<th[^>]*>(?:序号|物料编号)|<tr[^>]*>\s*<th[^>]*>(?:序号|物料编号))/,
- `$1${gapRow}$2`
- )
- }
- /** 刷新签字区上方间距行 inline 样式 */
- function refreshSubpackSlipGapBeforeSign(html: string): string {
- return html
- .replace(
- /<tr class="subpack-slip-gap-before-sign">[\s\S]*?<\/tr>/,
- `<tr class="subpack-slip-gap-before-sign">\n <td colspan="7" class="subpack-slip-gap-before-sign-cell" style="${GAP_BEFORE_SIGN_CELL}"> </td>\n </tr>`
- )
- .replace(
- /<div class="subpack-slip-section-gap subpack-slip-gap-before-sign"[^>]*>[\s\S]*?<\/div>/,
- `<div class="subpack-slip-section-gap subpack-slip-gap-before-sign" style="${GAP_BEFORE_SIGN_CELL}"> </div>`
- )
- }
- /** 在物料表与签字区之间插入间距行(兼容已保存模板) */
- function normalizeSubpackSlipGapBeforeSign(html: string): string {
- if (!html.includes('subpack-slip-doc')) return html
- if (html.includes('subpack-slip-gap-before-sign')) {
- return refreshSubpackSlipGapBeforeSign(html)
- }
- const gapRow = `<tr class="subpack-slip-gap-before-sign"><td colspan="7" class="subpack-slip-gap-before-sign-cell" style="${GAP_BEFORE_SIGN_CELL}"> </td></tr>\n\n `
- const withComment = html.replace(
- /(<\/tr>\s*)(<!--\s*下:签字区[\s\S]*?<tr[^>]*>\s*<td colspan="7"[^>]*>\s*(?:<div class="subpack-slip-sign-inner">\s*)?制单人:)/,
- `$1${gapRow}$2`
- )
- if (withComment !== html) return withComment
- const withLegacy = html.replace(
- /(<\/tr>\s*)(<!--\s*下:签字区[\s\S]*?<tr[^>]*>\s*<td colspan="7"[^>]*>\s*(?:<div class="subpack-slip-sign-inner">\s*)?仓库:)/,
- `$1${gapRow}$2`
- )
- if (withLegacy !== html) return withLegacy
- return html.replace(
- /(subpack-slip-remark-cell[\s\S]*?<\/tr>\s*)(<tr[^>]*>\s*<td colspan="7"[^>]*>\s*(?:<div class="subpack-slip-sign-inner">\s*)?(?:制单人:|仓库:))/,
- `$1${gapRow}$2`
- )
- }
- /** 签字区标签/下划线 baseline 对齐(兼容已保存模板) */
- function normalizeSubpackSlipSignLabelMarkup(html: string): string {
- if (!html.includes('subpack-slip-sign-value')) return html
- let result = html.replace(
- /<span class="subpack-slip-sign-item">(制单人:|审核:|生产主管:|仓库:|客户签收:|司机\/送货人\(附车牌\):)\s*(<span class="subpack-slip-sign-value")/g,
- '<span class="subpack-slip-sign-item"><span class="subpack-slip-sign-label">$1</span>$2'
- )
- result = result.replace(/<span class="subpack-slip-sign-value" style="([^"]*)"/g, (match, style) => {
- const nextStyle = style
- .replace(/display:inline-block;?/g, '')
- .replace(/padding:[^;]*;?/g, '')
- .replace(/vertical-align:[^;]*;?/g, '')
- .replace(/border:none !important;?/g, '')
- .replace(/border-bottom:[^;]*;?/g, '')
- .replace(/min-width:\s*[^;]*;?/g, '')
- .replace(/;\s*;/g, ';')
- .replace(/^;|;$/g, '')
- .trim()
- const minWidthStyle = `min-width:${SUBPACK_SLIP_SIGN_VALUE_MIN_WIDTH};`
- const mergedStyle = nextStyle ? `${nextStyle};${minWidthStyle}` : minWidthStyle
- return `<span class="subpack-slip-sign-value" style="${mergedStyle}"`
- })
- result = result.replace(
- /(<span class="subpack-slip-sign-label">制单人:<\/span>\s*<span class="subpack-slip-sign-value"[^>]*>)\s*(?: |\s)*<\/span>/g,
- '$1{{preparerUser}} </span>'
- )
- return result
- }
- /** 签字区 flex 布局(兼容已保存模板,避免打印预览溢出到右侧联次列) */
- function normalizeSubpackSlipSignRow(html: string): string {
- if (!html.includes('subpack-slip-doc')) return html
- if (
- !html.includes('制单人:') &&
- !html.includes('仓库:') &&
- !html.includes('客户签收:') &&
- !html.includes('司机/送货人')
- ) {
- return html
- }
- if (html.includes('制单人:') && html.includes('subpack-slip-sign-inner') && !html.includes('司机/送货人')) {
- return normalizeSubpackSlipSignLabelMarkup(html)
- }
- let result = html
- if (html.includes('仓库:') || html.includes('客户签收:') || html.includes('司机/送货人')) {
- result = result.replace(
- /<tr class="subpack-slip-sign-row"[\s\S]*?<\/tr>/,
- buildSubpackSlipSignRowHtml()
- )
- result = result.replace(
- /<tr>\s*<td colspan="7"[^>]*>[\s\S]*?(?:仓库:|制单人:)[\s\S]*?(?:司机\/送货人|生产主管)[\s\S]*?<\/td>\s*<\/tr>/,
- buildSubpackSlipSignRowHtml()
- )
- }
- result = result.replace(
- /<tr([^>]*)>\s*<td colspan="7"([^>]*)>([\s\S]*?(?:制单人:|仓库:)[\s\S]*?)<\/td>\s*<\/tr>/g,
- (match, trAttrs, tdAttrs, content) => {
- if (!content.includes('subpack-slip-sign-value') || content.includes('subpack-slip-sign-inner')) {
- return match
- }
- const wrappedContent = content
- .replace(/ /g, '')
- .replace(
- /(制单人:|审核:|生产主管:|仓库:|客户签收:|司机\/送货人\(附车牌\):)\s*(<span class="subpack-slip-sign-value"[\s\S]*?<\/span>)/g,
- '<span class="subpack-slip-sign-item"><span class="subpack-slip-sign-label">$1</span>$2</span>'
- )
- const trClassAttr = trAttrs.includes('class="')
- ? trAttrs.replace(/class="([^"]*)"/, (_, cls: string) =>
- cls.includes('subpack-slip-sign-row') ? `class="${cls}"` : `class="${cls} subpack-slip-sign-row"`
- )
- : `${trAttrs} class="subpack-slip-sign-row"`
- let nextTdAttrs = tdAttrs.replace(/white-space:nowrap;?/g, '').replace(/font-size:16px;?/g, 'font-size:14px;')
- if (nextTdAttrs.includes('class="')) {
- nextTdAttrs = nextTdAttrs.replace(/class="([^"]*)"/, (_, cls: string) =>
- cls.includes('subpack-slip-sign-row-cell') ? `class="${cls}"` : `class="${cls} subpack-slip-sign-row-cell"`
- )
- } else {
- nextTdAttrs = `${nextTdAttrs} class="subpack-slip-sign-row-cell"`
- }
- return `<tr${trClassAttr}>\n <td colspan="7"${nextTdAttrs}>\n <div class="subpack-slip-sign-inner">${wrappedContent.trim()}</div>\n </td>\n </tr>`
- }
- )
- result = result.replace(
- /<td(?![^>]*class="[^"]*subpack-slip-main-col)([^>]*style="[^"]*vertical-align:top;padding-right:6px)/g,
- '<td class="subpack-slip-main-col"$1'
- )
- return normalizeSubpackSlipSignLabelMarkup(result)
- }
- /**
- * 旧版一体 layout-table / 曾拆出的 footer-table → 信息表 + 物料表(含备注) + 签字表。
- */
- function migrateSubpackSlipSplitTables(html: string): string {
- if (!html.includes('subpack-slip-doc')) return html
- if (
- html.includes('subpack-slip-product-table') &&
- !html.includes('subpack-slip-footer-table') &&
- /subpack-slip-product-table[\s\S]*?(?:subpack-slip-remark-cell|备注:)/.test(html)
- ) {
- return html
- }
- if (html.includes('subpack-slip-footer-table') && html.includes('subpack-slip-product-table')) {
- return html.replace(
- /(<\/tr>\s*)<\/table>\s*<table class="subpack-slip-footer-table"[^>]*>\s*(?:<colgroup>[\s\S]*?<\/colgroup>\s*)?([\s\S]*?)<\/table>/i,
- (_m, beforeClose, footerInner) => {
- const rows = footerInner.replace(/^\s*<colgroup>[\s\S]*?<\/colgroup>\s*/i, '').trim()
- return `${beforeClose}${rows}\n </table>`
- }
- )
- }
- if (!html.includes('subpack-slip-layout-table')) return html
- return html.replace(
- /<table class="subpack-slip-layout-table"[^>]*>([\s\S]*?)<\/table>/i,
- (_full, inner: string) => {
- const headerMatch =
- inner.match(/<tr[^>]*class="[^"]*subpack-slip-product-header[^"]*"[\s\S]*?<\/tr>/i) ||
- inner.match(/<tr>\s*<th[^>]*>(?:序号|物料编号)[\s\S]*?<\/tr>/i)
- if (!headerMatch) return _full
- const headerIdx = inner.indexOf(headerMatch[0])
- let before = inner.slice(0, headerIdx)
- before = before.replace(/<colgroup>[\s\S]*?<\/colgroup>/i, '')
- before = before.replace(/<tr class="subpack-slip-gap-before-product">[\s\S]*?<\/tr>/i, '')
- const afterHeader = inner.slice(headerIdx + headerMatch[0].length)
- const signMatch =
- afterHeader.match(/<tr[^>]*class="[^"]*subpack-slip-sign-row[^"]*"[\s\S]*?<\/tr>/i) ||
- afterHeader.match(/<tr>\s*<td[^>]*>[\s\S]*?(?:制单人:|仓库:)[\s\S]*?<\/tr>/i)
- let productAndFooter = afterHeader
- let signPart = buildSubpackSlipSignRowHtml()
- if (signMatch) {
- const signAt = afterHeader.indexOf(signMatch[0])
- productAndFooter = afterHeader.slice(0, signAt)
- signPart = signMatch[0]
- }
- productAndFooter = productAndFooter.replace(
- /<tr class="subpack-slip-gap-before-sign">[\s\S]*?<\/tr>/i,
- ''
- )
- return `<table class="subpack-slip-info-table" style="width:100%;border-collapse:collapse;table-layout:fixed;border:none !important;">
- ${INFO_COLGROUP}
- ${before.trim()}
- </table>
- <div class="subpack-slip-section-gap subpack-slip-gap-before-product" style="${GAP_BEFORE_PRODUCT_CELL}"> </div>
- <table class="subpack-slip-product-table" style="width:100%;border-collapse:collapse;table-layout:fixed;border:none !important;">
- ${PRODUCT_COLGROUP}
- ${headerMatch[0]}
- ${productAndFooter.trim()}
- </table>
- <div class="subpack-slip-section-gap subpack-slip-gap-before-sign" style="${GAP_BEFORE_SIGN_CELL}"> </div>
- <table class="subpack-slip-sign-table" style="width:100%;border-collapse:collapse;table-layout:fixed;border:none !important;">
- ${signPart.trim()}
- </table>`
- }
- )
- }
- /** 物料表列宽(兼容已保存模板) */
- function normalizeSubpackSlipProductColWidths(html: string): string {
- if (!html.includes('subpack-slip-product-table')) return html
- return html.replace(
- /(<table class="subpack-slip-product-table"[^>]*>\s*)<colgroup>[\s\S]*?<\/colgroup>/i,
- `$1${PRODUCT_COLGROUP}`
- )
- }
- /** 统一单据左右 10px 间距、内容区宽度(兼容已保存模板) */
- function normalizeSubpackSlipDocLayout(html: string): string {
- let result = migrateSubpackSlipSplitTables(html)
- .replace(/padding:8px 12px 24px 4px;/g, `padding:${DOC_PADDING};`)
- .replace(/padding:8px 12px 24px;/g, `padding:${DOC_PADDING};`)
- .replace(/padding-left: 4px !important;/g, `padding: ${DOC_PADDING} !important;`)
- .replace(/left:0;top:50%/g, `left:${LOGO_LEFT}px;top:50%`)
- .replace(/left:20px;/g, `left:${LOGO_LEFT}px;`)
- .replace(/padding:4px 80px 0 120px;/g, 'padding:4px 60px 0 100px;')
- .replace(/padding:4px 72px 0 88px;/g, 'padding:4px 60px 0 100px;')
- .replace(/max-width:720px/g, `max-width:${SUBPACK_SLIP_DOC_MAX_WIDTH}`)
- .replace(/max-width:760px/g, `max-width:${SUBPACK_SLIP_DOC_MAX_WIDTH}`)
- .replace(/max-width:794px/g, `max-width:${SUBPACK_SLIP_DOC_MAX_WIDTH}`)
- .replace(/height:46px;/g, `height:${LOGO_HEIGHT}px;`)
- .replace(/height:23px;/g, `height:${LOGO_HEIGHT}px;`)
- .replace(/min-height:72px;/g, 'min-height:56px;')
- .replace(/min-height:48px;/g, 'min-height:56px;')
- .replace(/padding:4px 60px 0 68px;/g, 'padding:4px 60px 0 100px;')
- .replace(/padding:4px 60px 0 52px;/g, 'padding:4px 60px 0 100px;')
- .replace(/padding:1px 6px;/g, `padding:${TABLE_ROW_PADDING};`)
- .replace(/padding:4px 6px;/g, `padding:${TABLE_ROW_PADDING};`)
- .replace(/padding-bottom:5px;/g, '')
- .replace(/padding-bottom:10px;/g, '')
- .replace(/padding:18px 6px 8px;/g, `padding:${TABLE_ROW_PADDING};`)
- .replace(/margin-top:6px;/g, 'margin-top:2px;')
- .replace(/line-height:1\.6;/g, 'line-height:1.35;')
- .replace(/line-height:1\.9;/g, 'line-height:1.35;')
- .replace(/min-height:88px;/g, 'min-height:0;')
- .replace(/min-height:36px;/g, '')
- .replace(/min-height:76px;/g, 'min-height:0;')
- .replace(/min-height:120px;/g, 'min-height:0;')
- .replace(/top:4px !important;/g, `top:${SUBPACK_SLIP_SEAL_TOP}px !important;`)
- .replace(/top:4px;/g, `top:${SUBPACK_SLIP_SEAL_TOP}px;`)
- .replace(/>日期:</g, '>日 期:<')
- .replace(/border-bottom:2px solid #000 !important;/g, `border-bottom:${INFO_UNDERLINE} !important;`)
- .replace(/border-bottom:1px solid #000 !important;/g, `border-bottom:${INFO_UNDERLINE} !important;`)
- .replace(/border:2px solid #000 !important;/g, `border:${PRODUCT_GRID_BORDER} !important;`)
- .replace(/border:1px solid #000 !important;/g, `border:${PRODUCT_GRID_BORDER} !important;`)
- .replace(
- /display:inline-block;(?:min-width:\d+px;)?padding:0 2px 1px;vertical-align:bottom;border:none !important;/g,
- ''
- )
- .replace(
- /display:inline-block;padding:0 2px 2px;vertical-align:bottom;border:none !important;border-bottom:1px solid #000 !important;/g,
- ''
- )
- .replace(
- /(制单人:|审核:|生产主管:|仓库:|客户签收:|司机\/送货人\(附车牌\):)<span(?![^>]*class="subpack-slip-sign-value") style="/g,
- '$1<span class="subpack-slip-sign-value" style="'
- )
- .replace(
- /(<span class="subpack-slip-sign-value" style="[^"]*)border-bottom:2px solid #000 !important;/g,
- `$1border-bottom:${SIGN_UNDERLINE} !important;`
- )
- .replace(/class="subpack-slip-info-before-product"/g, '')
- .replace(/padding-bottom:12px;/g, '')
- .replace(/padding-bottom:24px;/g, '')
- if (result.includes('subpack-slip-doc') && !result.includes('max-width:')) {
- result = result.replace(
- /(<div class="subpack-slip-doc" style=")([^"]*)"/,
- `$1$2;${DOC_WRAP_EXTRA}"`
- )
- }
- if (result.includes('subpack-slip-doc')) {
- result = result.replace(/<style type="text\/css">[\s\S]*?<\/style>/, SUBPACK_SLIP_STYLE_BLOCK)
- result = normalizeSubpackSlipSignLabelMarkup(result)
- result = normalizeSubpackSlipProductGridCell(result)
- result = normalizeSubpackSlipProductTableFooter(result)
- result = normalizeSubpackSlipSlipNoRow(result)
- }
- return result
- }
- /** 基本信息字段与标题(兼容从送(提)货单复制的旧模板) */
- function normalizeSubpackSlipBasicInfoFields(html: string): string {
- if (!html.includes('subpack-slip-doc')) return html
- let result = html
- result = result.replace(/送 \(提\) 货 单/g, '分 装 单')
- result = result.replace(/送\(提\)货单/g, '分装单')
- result = result.replace(/收货单位:/g, '下单日期:')
- result = result.replace(/单据编号:/g, '交货日期:')
- result = result.replace(/收货联系方式:/g, '下单人:')
- result = result.replace(/日 期:/g, '经手人:')
- result = result.replace(/>日期:</g, '>经手人:<')
- result = result.replace(/\{\{receivingUnit\}\}/g, '{{orderDate}}')
- result = result.replace(/\{\{planNo\}\}/g, '{{deliveryDate}}')
- result = result.replace(/\{\{receiveContact\}\}/g, '{{orderUser}}')
- result = result.replace(/\{\{planDateChinese\}\}/g, '{{handlerName}}')
- result = result.replace(/\{\{driverInfo\}\}/g, '{{handlerName}}')
- result = result.replace(
- /<div class="subpack-slip-info-row subpack-slip-type-check-wrap"[\s\S]*?<\/div>\s*/g,
- ''
- )
- result = result.replace(/<div class="subpack-slip-type-check-row"[\s\S]*?<\/div>\s*/g, '')
- return result
- }
- /** 物料表头(兼容旧版 物料编号/实收数量 表头) */
- function normalizeSubpackSlipProductHeader(html: string): string {
- if (!html.includes('subpack-slip-doc')) return html
- if (html.includes('>序号<') && html.includes('>是否完成<')) return html
- const headerHtml = buildSubpackSlipProductHeaderRowHtml()
- const withClass = html.replace(/<tr class="subpack-slip-product-header"[\s\S]*?<\/tr>/, headerHtml)
- if (withClass !== html) return withClass
- return html.replace(
- /<tr>\s*<th[^>]*>物料编号<\/th>[\s\S]*?<th[^>]*>实收数量<\/th>\s*<\/tr>/,
- headerHtml
- )
- }
- /** 备注行 + 桶选项多选框(兼容旧版纯文本备注行) */
- function normalizeSubpackSlipRemarkRow(html: string): string {
- if (!html.includes('subpack-slip-doc')) return html
- if (html.includes('subpack-slip-remark-row-inner')) {
- let result = html
- // 旧版:备注正文与补充输入分开,升级为同一条连续下划线
- if (!result.includes('subpack-slip-remark-underline-group')) {
- const extractRemark = (raw: string) =>
- String(raw || '')
- .replace(/<[^>]+>/g, '')
- .replace(/备注:/g, '')
- .replace(/ /g, '')
- .trim() || '{{remark}}'
- result = result.replace(
- /<div class="subpack-slip-remark-row-inner">([\s\S]*?)<div class="subpack-slip-bucket-check-row"/,
- (_m, middle: string) => {
- const valueMatch = String(middle).match(
- /class="subpack-slip-remark-value"[^>]*>([\s\S]*?)<\/span>/
- )
- const plainMatch = String(middle).match(/备注:([^<]*)/)
- const content = extractRemark(valueMatch?.[1] ?? plainMatch?.[1] ?? '{{remark}}')
- const extraMatch = String(middle).match(
- /<input type="text" class="subpack-slip-remark-extra-input"([^>]*)\/?>/
- )
- const extraHtml = extraMatch
- ? `<span class="subpack-slip-remark-extra-wrap" contenteditable="false"><input type="text" class="subpack-slip-remark-extra-input"${extraMatch[1]} /></span>`
- : buildSubpackSlipRemarkExtraInputHtml()
- return `<div class="subpack-slip-remark-row-inner">
- <span class="subpack-slip-remark-label">备注:</span>
- <span class="subpack-slip-remark-underline-group"><span class="subpack-slip-remark-value">${content} </span>${extraHtml}</span>
- <div class="subpack-slip-bucket-check-row"`
- }
- )
- }
- if (!result.includes('subpack-slip-remark-extra-input')) {
- result = result.replace(
- /(<div class="subpack-slip-remark-row-inner">[\s\S]*?)(\s*<div class="subpack-slip-bucket-check-row")/,
- `$1\n ${buildSubpackSlipRemarkUnderlineGroupHtml()}$2`
- )
- } else if (!result.includes('subpack-slip-remark-extra-wrap')) {
- result = result.replace(
- /<input type="text" class="subpack-slip-remark-extra-input"([^>]*)\/?>/g,
- `<span class="subpack-slip-remark-extra-wrap" contenteditable="false"><input type="text" class="subpack-slip-remark-extra-input"$1 /></span>`
- )
- }
- return ensureSubpackSlipInteractiveMarkup(result)
- }
- return html.replace(
- /(<td[^>]*colspan="7"[^>]*>\s*)备注:(\{\{remark\}\}|[^<]*?)(\s*<\/td>)/,
- `$1${buildSubpackSlipRemarkRowHtml()}$3`
- )
- }
- /** 移除右侧联次列,并解包旧版 outer-table 布局 */
- function normalizeSubpackSlipRemoveSideLabels(html: string): string {
- if (!html.includes('subpack-slip-doc')) return html
- let result = html
- result = result.replace(/<table class="subpack-slip-side-labels"[\s\S]*?<\/table>/g, '')
- result = result.replace(/\{\{sideLabels\}\}/g, '')
- result = result.replace(
- /<!--\s*最右侧纵向联次\s*-->\s*<td class="subpack-slip-side-labels-col"[\s\S]*?<\/td>/g,
- ''
- )
- result = result.replace(/<td class="subpack-slip-side-labels-col"[^>]*>[\s\S]*?<\/td>/g, '')
- if (result.includes('subpack-slip-outer-table') && result.includes('subpack-slip-main-col')) {
- result = result.replace(
- /<table class="subpack-slip-outer-table" style="[^"]*">\s*<tr>\s*<td class="subpack-slip-main-col" style="[^"]*">/,
- ''
- )
- result = result.replace(/<\/td>\s*<\/tr>\s*<\/table>(?=\s*<\/div>)/, '')
- }
- return result
- }
- /** 基本信息合并为一行四列(兼容旧版两行布局) */
- function normalizeSubpackSlipBasicInfoOneRow(html: string): string {
- if (!html.includes('subpack-slip-doc')) return html
- if (html.includes('subpack-slip-basic-info-row')) return html
- const basicInfoRow = buildSubpackSlipBasicInfoRowHtml()
- const replaced = html.replace(
- /<!--\s*上:基本信息两行\s*-->\s*<tr>[\s\S]*?<\/tr>\s*<tr>[\s\S]*?<\/tr>/,
- `<!-- 基本信息一行 -->\n ${basicInfoRow}`
- )
- if (replaced !== html) return replaced
- const fallback = html.replace(
- /<tr>\s*<td colspan="4"[^>]*>\s*<div class="subpack-slip-info-row"[^>]*>\s*<span class="subpack-slip-info-label"[^>]*>下单日期:[\s\S]*?<\/tr>\s*<tr>\s*<td colspan="4"[^>]*>\s*<div class="subpack-slip-info-row"[^>]*>\s*<span class="subpack-slip-info-label"[^>]*>下单人:[\s\S]*?<\/tr>/,
- basicInfoRow
- )
- return fallback
- }
- /** 分装单号与经手人标签右缘对齐(兼容旧版标题区居中布局) */
- function normalizeSubpackSlipSlipNoRow(html: string): string {
- if (!html.includes('subpack-slip-doc')) return html
- let result = html
- result = result.replace(/<div class="subpack-slip-info-row subpack-slip-slip-no-wrap"[\s\S]*?<\/div>\s*/g, '')
- result = result.replace(/<span class="subpack-slip-info-label"[^>]*>分装单号:<\/span>\s*/g, '')
- result = result.replace(
- /<div class="subpack-slip-slip-no-display"[^>]*>([\s\S]*?)<\/div>/g,
- '<span class="subpack-slip-slip-no-display">$1</span>'
- )
- if (result.includes('subpack-slip-title-section') && result.includes('subpack-slip-slip-no-display')) {
- result = result.replace(
- /\s*<span class="subpack-slip-slip-no-display"[^>]*>[\s\S]*?<\/span>\s*(?=<\/div>\s*<\/div>\s*<table class="subpack-slip-(?:layout|info)-table")/,
- '\n'
- )
- result = result.replace(
- /<div class="subpack-slip-slip-no-display"[^>]*>[\s\S]*?<\/div>\s*(?=<\/div>\s*<\/div>\s*<table class="subpack-slip-(?:layout|info)-table")/,
- ''
- )
- }
- if (!result.includes('subpack-slip-slip-no-row')) {
- result = result.replace(
- /(<table class="subpack-slip-(?:layout|info)-table"[\s\S]*?<colgroup>[\s\S]*?<\/colgroup>\s*)/,
- `$1\n ${buildSubpackSlipSlipNoRowHtml()}\n\n `
- )
- }
- if (result.includes('subpack-slip-slip-no-row') && !result.includes('subpack-slip-slip-no-anchor')) {
- result = result.replace(
- /<tr class="subpack-slip-slip-no-row"[\s\S]*?<\/tr>\s*/,
- `${buildSubpackSlipSlipNoRowHtml()}\n`
- )
- }
- return ensureSubpackSlipInteractiveMarkup(result)
- }
- const SUBPACK_SLIP_CHECK_DELEGATE_ATTR = 'data-subpack-slip-check-delegate'
- const SUBPACK_SLIP_REMARK_EXTRA_BOUND_ATTR = 'data-subpack-slip-remark-extra-bound'
- function syncSubpackSlipRemarkExtraInputValue(input: HTMLInputElement): void {
- input.setAttribute('value', input.value)
- }
- /** 富文本编辑器内:允许备注补充输入框获得焦点并输入(TinyMCE 默认会拦截) */
- export function bindSubpackSlipRemarkExtraInputInteraction(
- root: HTMLElement | null | undefined,
- onChange?: () => void
- ): void {
- if (!root) return
- root.querySelectorAll('input.subpack-slip-remark-extra-input').forEach((node) => {
- const input = node as HTMLInputElement
- const wrap = input.closest('.subpack-slip-remark-extra-wrap') as HTMLElement | null
- if (wrap) {
- wrap.setAttribute('contenteditable', 'false')
- }
- input.removeAttribute('readonly')
- input.removeAttribute('disabled')
- syncSubpackSlipRemarkExtraInputValue(input)
- if (input.getAttribute(SUBPACK_SLIP_REMARK_EXTRA_BOUND_ATTR) === '1') return
- input.setAttribute(SUBPACK_SLIP_REMARK_EXTRA_BOUND_ATTR, '1')
- const stopBubble = (e: Event) => {
- e.stopPropagation()
- }
- input.addEventListener('mousedown', stopBubble, true)
- input.addEventListener('click', stopBubble, true)
- input.addEventListener('keydown', stopBubble, true)
- input.addEventListener('keyup', stopBubble, true)
- input.addEventListener('keypress', stopBubble, true)
- input.addEventListener('focus', stopBubble, true)
- input.addEventListener('input', () => {
- syncSubpackSlipRemarkExtraInputValue(input)
- onChange?.()
- })
- input.addEventListener('blur', () => {
- syncSubpackSlipRemarkExtraInputValue(input)
- onChange?.()
- })
- })
- }
- function syncSubpackSlipCheckboxInput(input: HTMLInputElement): void {
- if (input.checked) {
- input.setAttribute('checked', 'checked')
- } else {
- input.removeAttribute('checked')
- }
- }
- /** 富文本编辑器内:绑定多选框点击(TinyMCE 默认可编辑区会拦截原生 checkbox) */
- export function bindSubpackSlipCheckboxInteraction(
- root: HTMLElement | null | undefined,
- onChange?: () => void
- ): void {
- if (!root) return
- root.querySelectorAll('label.subpack-slip-check-item').forEach((node) => {
- const label = node as HTMLElement
- label.setAttribute('contenteditable', 'false')
- const input = label.querySelector('input.subpack-slip-check-input') as HTMLInputElement | null
- if (input) syncSubpackSlipCheckboxInput(input)
- })
- if (root.getAttribute(SUBPACK_SLIP_CHECK_DELEGATE_ATTR) === '1') return
- root.setAttribute(SUBPACK_SLIP_CHECK_DELEGATE_ATTR, '1')
- root.addEventListener(
- 'mousedown',
- (event) => {
- if (root.isContentEditable === false) return
- const target = event.target as HTMLElement | null
- if (!target) return
- const label = target.closest('label.subpack-slip-check-item')
- if (!label || !root.contains(label)) return
- const input = label.querySelector('input.subpack-slip-check-input') as HTMLInputElement | null
- if (!input) return
- event.preventDefault()
- event.stopPropagation()
- input.checked = !input.checked
- syncSubpackSlipCheckboxInput(input)
- onChange?.()
- },
- true
- )
- }
- /** 分装单 TinyMCE:初始化与内容变更时绑定多选项与备注输入框交互 */
- export function setupSubpackSlipEditorCheckboxes(editor: {
- getBody: () => HTMLElement
- on: (event: string, callback: (...args: unknown[]) => void) => void
- fire: (event: string) => void
- }): void {
- const bind = () => {
- const body = editor.getBody()
- bindSubpackSlipCheckboxInteraction(body, () => editor.fire('change'))
- bindSubpackSlipRemarkExtraInputInteraction(body, () => editor.fire('change'))
- }
- const syncRemarkExtraValues = () => {
- editor.getBody().querySelectorAll('input.subpack-slip-remark-extra-input').forEach((node) => {
- syncSubpackSlipRemarkExtraInputValue(node as HTMLInputElement)
- })
- }
- editor.on('init', () => {
- window.setTimeout(bind, 0)
- })
- editor.on('SetContent', () => {
- window.setTimeout(bind, 0)
- })
- editor.on('focus', () => {
- window.setTimeout(bind, 0)
- })
- editor.on('BeforeGetContent', () => {
- syncRemarkExtraValues()
- })
- }
- /** @deprecated 兼容旧引用,请使用 setupSubpackSlipEditorCheckboxes */
- export const setupSubpackSlipEditorTypeCheckboxes = setupSubpackSlipEditorCheckboxes
- /** @deprecated 兼容旧引用,请使用 normalizeSubpackSlipRemoveSideLabels */
- function normalizeSubpackSlipSideLabelsPlaceholder(html: string): string {
- return normalizeSubpackSlipRemoveSideLabels(html)
- }
- /** 将占位符替换为计划最新数据 */
- export function replaceSubpackSlipPlaceholders(
- htmlContent: string,
- plan: Plan,
- ctx?: SubpackSlipRenderContext,
- options?: { blank?: boolean }
- ): string {
- if (!htmlContent || !plan) return htmlContent
- const blank = Boolean(options?.blank)
- const orderDate = blank ? '' : ctx?.orderDate || formatDateIso()
- const orderUser = blank ? '' : ctx?.orderUser || ''
- const preparerUser = blank ? '' : ctx?.preparerUser || orderUser
- const replacements: Record<string, string> = {
- '{{companyName}}': SUBPACK_SLIP_DEFAULTS.companyName,
- '{{subpackSlipNo}}': blank ? '' : ctx?.subpackSlipNo || '',
- '{{orderDate}}': orderDate,
- '{{deliveryDate}}': '',
- '{{orderUser}}': orderUser,
- '{{preparerUser}}': preparerUser,
- '{{handlerName}}': '',
- '{{receivingUnit}}': blank ? '' : orderDate,
- '{{planNo}}': '',
- '{{receiveContact}}': blank ? '' : orderUser,
- '{{driverInfo}}': '',
- '{{planDateChinese}}': '',
- '{{productTable}}': buildProductRows(blank ? [] : plan.products, ctx),
- '{{remark}}': blank ? '' : formatRemarkForSlipHtml(plan.remark),
- '{{remarkExtra}}': '',
- '{{companyAddress}}': '',
- '{{companyPhone}}': '',
- '{{contactPhone}}': '',
- '{{companySeal}}': '',
- '{{sideLabels}}': ''
- }
- let result = normalizeSubpackSlipDocLayout(
- normalizeSubpackSlipRemoveAddressAndSeal(
- normalizeSubpackSlipSignRow(
- normalizeSubpackSlipTermsCell(
- normalizeSubpackSlipRemoveTermsRow(
- normalizeSubpackSlipGapBeforeSign(
- normalizeSubpackSlipProductTableFooter(
- normalizeSubpackSlipProductColWidths(
- normalizeSubpackSlipRemarkRow(
- normalizeSubpackSlipProductGridCell(
- normalizeSubpackSlipProductHeader(
- normalizeSubpackSlipGapBeforeProduct(
- normalizeSubpackSlipSlipNoRow(
- normalizeSubpackSlipBasicInfoOneRow(
- normalizeSubpackSlipBasicInfoFields(
- normalizeSubpackSlipRemoveSideLabels(htmlContent)
- )
- )
- )
- )
- )
- )
- )
- )
- )
- )
- )
- )
- )
- )
- )
- for (const [placeholder, value] of Object.entries(replacements)) {
- result = result.split(placeholder).join(value)
- }
- if (!blank && ctx?.subpackSlipNo) {
- result = result.replace(
- /(<span class="subpack-slip-slip-no-display"[^>]*>)[\s\S]*?( <\/span>)/g,
- `$1${ctx.subpackSlipNo}$2`
- )
- }
- if (!blank && preparerUser) {
- result = result.replace(
- /(<span class="subpack-slip-sign-label">制单人:<\/span>\s*<span class="subpack-slip-sign-value"[^>]*>)[\s\S]*?(<\/span>)/g,
- `$1${preparerUser}$2`
- )
- }
- return result
- }
- /** 空白分装单 HTML:默认模板 + 业务字段全空(含分装单号)+ 最小空产品行 */
- export function renderBlankSubpackSlipHtml(): string {
- const blankPlan = {
- id: '',
- planNo: '',
- products: [],
- customerName: '',
- remark: ''
- } as Plan
- const html = generateSubpackSlipTemplate(blankPlan)
- return replaceSubpackSlipPlaceholders(html, blankPlan, undefined, { blank: true })
- }
- /** 保存模板前:将动态字段还原为占位符 */
- export function restoreSubpackSlipPlaceholders(template: string, plan: Plan): string {
- let result = template
- const dynamicReplacements: Record<string, string> = {
- '{{companyName}}': SUBPACK_SLIP_DEFAULTS.companyName,
- '{{orderDate}}': formatDateIso(),
- '{{remark}}': plan.remark || '',
- '{{receivingUnit}}': formatDateIso(),
- '{{planDateChinese}}': formatPlanDateChinese(plan.planDate)
- }
- for (const [placeholder, currentValue] of Object.entries(dynamicReplacements)) {
- if (!currentValue) continue
- const escaped = currentValue.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
- result = result.replace(new RegExp(escaped, 'g'), placeholder)
- }
- result = result.replace(/NO:SXFZ\d{8}-\d{3}/g, '{{subpackSlipNo}}')
- result = result.replace(/SXFZ\d{8}-\d{3}/g, '{{subpackSlipNo}}')
- result = result.replace(/SXFZ-\d{4}-\d{2}-\d{2}-\d{3}/g, '{{subpackSlipNo}}')
- result = result.replace(/NO:SXFZ-\d{4}-\d{2}-\d{2}-\d{3}/g, '{{subpackSlipNo}}')
- result = result.replace(
- /(<span class="subpack-slip-slip-no-display"[^>]*>)[^<]*(?= <\/span>)/,
- '$1{{subpackSlipNo}}'
- )
- result = result.replace(
- /(<div class="subpack-slip-slip-no-display"[^>]*>)[^<]*(?= <\/div>)/,
- '$1{{subpackSlipNo}}'
- )
- result = result.replace(
- /(<span class="subpack-slip-slip-no-value[^"]*"[^>]*>)[^<]*(?= <\/span>)/,
- '$1{{subpackSlipNo}}'
- )
- result = result.replace(
- /(<span class="subpack-slip-info-label"[^>]*>下单日期:<\/span>\s*<span class="subpack-slip-info-value-left"[^>]*>)[^<]*(?= <\/span>)/,
- '$1{{orderDate}}'
- )
- result = result.replace(
- /(<span class="subpack-slip-info-label"[^>]*>下单人:<\/span>\s*<span class="subpack-slip-info-value-left"[^>]*>)[^<]*(?= <\/span>)/,
- '$1{{orderUser}}'
- )
- result = result.replace(
- /(<span class="subpack-slip-info-label"[^>]*>交货日期:<\/span>\s*<span class="subpack-slip-info-value-center"[^>]*>)[^<]*(?= <\/span>)/,
- '$1{{deliveryDate}}'
- )
- result = result.replace(
- /(<span class="subpack-slip-info-label"[^>]*>经手人:<\/span>\s*<span class="subpack-slip-info-value-center"[^>]*>)[^<]*(?= <\/span>)/,
- '$1{{handlerName}}'
- )
- result = result.replace(
- /(<span class="subpack-slip-sign-label">制单人:<\/span>\s*<span class="subpack-slip-sign-value"[^>]*>)[^<]*(?=<\/span>)/,
- '$1{{preparerUser}}'
- )
- // 产品明细行还原为占位符(表头之后、备注行之前)
- result = result.replace(
- /(<th[^>]*>是否完成<\/th>\s*<\/tr>\s*)([\s\S]*?)(\s*<tr>\s*<td colspan="7"[^>]*>\s*(?:<div class="subpack-slip-remark-row-inner">|备注:))/,
- '$1{{productTable}}$3'
- )
- result = result.replace(
- /(<th[^>]*>实收数量<\/th>\s*<\/tr>\s*)([\s\S]*?)(\s*<tr>\s*<td colspan="7"[^>]*>\s*(?:<div class="subpack-slip-remark-row-inner">|备注:))/,
- '$1{{productTable}}$3'
- )
- // 移除右侧联次、印章与地址/电话行
- result = normalizeSubpackSlipRemoveSideLabels(result)
- result = normalizeSubpackSlipRemoveAddressAndSeal(result)
- return result
- }
|