\s*]*>\s*\s*/g,
''
)
result = result.replace(
/ \s*]*>\s*\s*/g,
''
)
result = result.replace(
/ \s*| ]*>\s* \s*<\/td>\s* | ]*>\s*\s*/g,
''
)
result = result.replace(
/ \s*\s*| /g,
''
)
result = result.replace(/ | ]*>[\s\S]*?<\/td>/g, '')
if (result.includes('subpack-slip-outer-table') && result.includes('subpack-slip-main-col')) {
result = result.replace(
/\s*\s*| /,
''
)
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\S]*?<\/tr>/,
`\n ${basicInfoRow}`
)
if (replaced !== html) return replaced
const fallback = html.replace(
/ \s*]*>\s*]*>\s* ]*>下单日期:[\s\S]*?<\/tr>\s*\s*]*>\s*]*>\s* ]*>下单人:[\s\S]*?<\/tr>/,
basicInfoRow
)
return fallback
}
/** 分装单号与经手人标签右缘对齐(兼容旧版标题区居中布局) */
function normalizeSubpackSlipSlipNoRow(html: string): string {
if (!html.includes('subpack-slip-doc')) return html
let result = html
result = result.replace(/\s*/g, '')
result = result.replace(/ ]*>分装单号:<\/span>\s*/g, '')
result = result.replace(
/]*>([\s\S]*?)<\/div>/g,
' $1'
)
if (result.includes('subpack-slip-title-section') && result.includes('subpack-slip-slip-no-display')) {
result = result.replace(
/\s* ]*>[\s\S]*?<\/span>\s*(?=<\/div>\s*<\/div>\s*]*>[\s\S]*?<\/div>\s*(?=<\/div>\s*<\/div>\s*[\s\S]*?<\/colgroup>\s*)/,
`$1\n ${buildSubpackSlipSlipNoRowHtml()}\n\n `
)
}
if (result.includes('subpack-slip-slip-no-row') && !result.includes('subpack-slip-slip-no-anchor')) {
result = result.replace(
/\s*/,
`${buildSubpackSlipSlipNoRowHtml()}\n`
)
}
return ensureSubpackSlipInteractiveMarkup(result)
}
const SUBPACK_SLIP_CHECK_DELEGATE_ATTR = 'data-subpack-slip-check-delegate'
const SUBPACK_SLIP_REMARK_EXTRA_BOUND_ATTR = 'data-subpack-slip-remark-extra-bound'
function syncSubpackSlipRemarkExtraInputValue(input: HTMLInputElement): void {
input.setAttribute('value', input.value)
}
/** 富文本编辑器内:允许备注补充输入框获得焦点并输入(TinyMCE 默认会拦截) */
export function bindSubpackSlipRemarkExtraInputInteraction(
root: HTMLElement | null | undefined,
onChange?: () => void
): void {
if (!root) return
root.querySelectorAll('input.subpack-slip-remark-extra-input').forEach((node) => {
const input = node as HTMLInputElement
const wrap = input.closest('.subpack-slip-remark-extra-wrap') as HTMLElement | null
if (wrap) {
wrap.setAttribute('contenteditable', 'false')
}
input.removeAttribute('readonly')
input.removeAttribute('disabled')
syncSubpackSlipRemarkExtraInputValue(input)
if (input.getAttribute(SUBPACK_SLIP_REMARK_EXTRA_BOUND_ATTR) === '1') return
input.setAttribute(SUBPACK_SLIP_REMARK_EXTRA_BOUND_ATTR, '1')
const stopBubble = (e: Event) => {
e.stopPropagation()
}
input.addEventListener('mousedown', stopBubble, true)
input.addEventListener('click', stopBubble, true)
input.addEventListener('keydown', stopBubble, true)
input.addEventListener('keyup', stopBubble, true)
input.addEventListener('keypress', stopBubble, true)
input.addEventListener('focus', stopBubble, true)
input.addEventListener('input', () => {
syncSubpackSlipRemarkExtraInputValue(input)
onChange?.()
})
input.addEventListener('blur', () => {
syncSubpackSlipRemarkExtraInputValue(input)
onChange?.()
})
})
}
function syncSubpackSlipCheckboxInput(input: HTMLInputElement): void {
if (input.checked) {
input.setAttribute('checked', 'checked')
} else {
input.removeAttribute('checked')
}
}
/** 富文本编辑器内:绑定多选框点击(TinyMCE 默认可编辑区会拦截原生 checkbox) */
export function bindSubpackSlipCheckboxInteraction(
root: HTMLElement | null | undefined,
onChange?: () => void
): void {
if (!root) return
root.querySelectorAll('label.subpack-slip-check-item').forEach((node) => {
const label = node as HTMLElement
label.setAttribute('contenteditable', 'false')
const input = label.querySelector('input.subpack-slip-check-input') as HTMLInputElement | null
if (input) syncSubpackSlipCheckboxInput(input)
})
if (root.getAttribute(SUBPACK_SLIP_CHECK_DELEGATE_ATTR) === '1') return
root.setAttribute(SUBPACK_SLIP_CHECK_DELEGATE_ATTR, '1')
root.addEventListener(
'mousedown',
(event) => {
if (root.isContentEditable === false) return
const target = event.target as HTMLElement | null
if (!target) return
const label = target.closest('label.subpack-slip-check-item')
if (!label || !root.contains(label)) return
const input = label.querySelector('input.subpack-slip-check-input') as HTMLInputElement | null
if (!input) return
event.preventDefault()
event.stopPropagation()
input.checked = !input.checked
syncSubpackSlipCheckboxInput(input)
onChange?.()
},
true
)
}
/** 分装单 TinyMCE:初始化与内容变更时绑定多选项与备注输入框交互 */
export function setupSubpackSlipEditorCheckboxes(editor: {
getBody: () => HTMLElement
on: (event: string, callback: (...args: unknown[]) => void) => void
fire: (event: string) => void
}): void {
const bind = () => {
const body = editor.getBody()
bindSubpackSlipCheckboxInteraction(body, () => editor.fire('change'))
bindSubpackSlipRemarkExtraInputInteraction(body, () => editor.fire('change'))
}
const syncRemarkExtraValues = () => {
editor.getBody().querySelectorAll('input.subpack-slip-remark-extra-input').forEach((node) => {
syncSubpackSlipRemarkExtraInputValue(node as HTMLInputElement)
})
}
editor.on('init', () => {
window.setTimeout(bind, 0)
})
editor.on('SetContent', () => {
window.setTimeout(bind, 0)
})
editor.on('focus', () => {
window.setTimeout(bind, 0)
})
editor.on('BeforeGetContent', () => {
syncRemarkExtraValues()
})
}
/** @deprecated 兼容旧引用,请使用 setupSubpackSlipEditorCheckboxes */
export const setupSubpackSlipEditorTypeCheckboxes = setupSubpackSlipEditorCheckboxes
/** @deprecated 兼容旧引用,请使用 normalizeSubpackSlipRemoveSideLabels */
function normalizeSubpackSlipSideLabelsPlaceholder(html: string): string {
return normalizeSubpackSlipRemoveSideLabels(html)
}
/** 将占位符替换为计划最新数据 */
export function replaceSubpackSlipPlaceholders(
htmlContent: string,
plan: Plan,
ctx?: SubpackSlipRenderContext
): string {
if (!htmlContent || !plan) return htmlContent
const orderDate = ctx?.orderDate || formatDateIso()
const orderUser = ctx?.orderUser || ''
const preparerUser = ctx?.preparerUser || orderUser
const replacements: Record = {
'{{companyName}}': SUBPACK_SLIP_DEFAULTS.companyName,
'{{subpackSlipNo}}': ctx?.subpackSlipNo || '',
'{{orderDate}}': orderDate,
'{{deliveryDate}}': '',
'{{orderUser}}': orderUser,
'{{preparerUser}}': preparerUser,
'{{handlerName}}': '',
'{{receivingUnit}}': orderDate,
'{{planNo}}': '',
'{{receiveContact}}': orderUser,
'{{driverInfo}}': '',
'{{planDateChinese}}': '',
'{{productTable}}': buildProductRows(plan.products, ctx),
'{{remark}}': 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 (ctx?.subpackSlipNo) {
result = result.replace(
/(]*>)[\s\S]*?( <\/span>)/g,
`$1${ctx.subpackSlipNo}$2`
)
}
if (preparerUser) {
result = result.replace(
/(制单人:<\/span>\s*]*>)[\s\S]*?(<\/span>)/g,
`$1${preparerUser}$2`
)
}
return result
}
/** 保存模板前:将动态字段还原为占位符 */
export function restoreSubpackSlipPlaceholders(template: string, plan: Plan): string {
let result = template
const dynamicReplacements: Record = {
'{{companyName}}': SUBPACK_SLIP_DEFAULTS.companyName,
'{{orderDate}}': formatDateIso(),
'{{remark}}': plan.remark || '',
'{{receivingUnit}}': formatDateIso(),
'{{planDateChinese}}': formatPlanDateChinese(plan.planDate)
}
for (const [placeholder, currentValue] of Object.entries(dynamicReplacements)) {
if (!currentValue) continue
const escaped = currentValue.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
result = result.replace(new RegExp(escaped, 'g'), placeholder)
}
result = result.replace(/NO:SXFZ\d{8}-\d{3}/g, '{{subpackSlipNo}}')
result = result.replace(/SXFZ\d{8}-\d{3}/g, '{{subpackSlipNo}}')
result = result.replace(/SXFZ-\d{4}-\d{2}-\d{2}-\d{3}/g, '{{subpackSlipNo}}')
result = result.replace(/NO:SXFZ-\d{4}-\d{2}-\d{2}-\d{3}/g, '{{subpackSlipNo}}')
result = result.replace(
/(]*>)[^<]*(?= <\/span>)/,
'$1{{subpackSlipNo}}'
)
result = result.replace(
/(]*>)[^<]*(?= <\/div>)/,
'$1{{subpackSlipNo}}'
)
result = result.replace(
/(]*>)[^<]*(?= <\/span>)/,
'$1{{subpackSlipNo}}'
)
result = result.replace(
/(]*>下单日期:<\/span>\s*]*>)[^<]*(?= <\/span>)/,
'$1{{orderDate}}'
)
result = result.replace(
/(]*>下单人:<\/span>\s*]*>)[^<]*(?= <\/span>)/,
'$1{{orderUser}}'
)
result = result.replace(
/(]*>交货日期:<\/span>\s*]*>)[^<]*(?= <\/span>)/,
'$1{{deliveryDate}}'
)
result = result.replace(
/(]*>经手人:<\/span>\s*]*>)[^<]*(?= <\/span>)/,
'$1{{handlerName}}'
)
result = result.replace(
/(制单人:<\/span>\s*]*>)[^<]*(?=<\/span>)/,
'$1{{preparerUser}}'
)
// 产品明细行还原为占位符(表头之后、备注行之前)
result = result.replace(
/( ]*>是否完成<\/th>\s*<\/tr>\s*)([\s\S]*?)(\s* | \s*| ]*>\s*(?: | | | | | | |