This commit is contained in:
zlwu committed 2026-07-20 10:50:26 +08:00
1 parent 6d9354a194
commit aff71d296b
2759 files changed
+860425 -13211

No files matched your search

@@ -1,4 +1,5 @@
import type { Plan, PlanProduct } from '@/api/pl'
import { applyPdfStableTableCellBorders } from '@/utils/pl-slip-pdf-render'
/** 送(提)货单打印默认公司信息(可在富文本中修改) */
export const DELIVERY_SLIP_DEFAULTS = {
@@ -47,9 +48,9 @@ const PRODUCT_TABLE_CELL_LEFT = `padding:10px 6px 14px 6px;border:${PRODUCT_TABL
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) */
/** 送(提)货单印章:public/sxshzyz.png,定位在地址单元格右上角(相对原位置向上 66px) */
const DELIVERY_SLIP_SEAL_IMAGE_SRC = '/sxshzyz.png'
const DELIVERY_SLIP_SEAL_TOP = -96
const DELIVERY_SLIP_SEAL_TOP = -66
const DELIVERY_SLIP_SEAL_IMAGE_STYLE = `position:absolute;right:4px;top:${DELIVERY_SLIP_SEAL_TOP}px;width:140px;height:140px;opacity:0.85;z-index:10;`
const HANDWRITE_BLANK_INNER = '        '
const HANDWRITE_BLANK = `<span class="delivery-slip-handwrite-blank">(${HANDWRITE_BLANK_INNER})</span>`
@@ -1065,6 +1066,9 @@ export function prepareDeliverySlipPdfGridCells(root: HTMLElement): void {
prepareDeliverySlipPdfSignValues(root)
prepareDeliverySlipPdfTypeCheckboxes(root)
// 亚像素 border 在 Windows DPI 下易变粗;截图前改为 1px 背景单线
applyPdfStableTableCellBorders(root.querySelectorAll('.delivery-slip-product-cell'))
}
/** 备注、条款、印章行:统一 0.3px 全边框(兼容已保存模板,与物料网格衔接) */
@@ -1338,6 +1342,8 @@ function normalizeDeliverySlipDocLayout(html: string): string {
.replace(/min-height:120px;/g, 'min-height:0;')
.replace(/top:4px !important;/g, `top:${DELIVERY_SLIP_SEAL_TOP}px !important;`)
.replace(/top:4px;/g, `top:${DELIVERY_SLIP_SEAL_TOP}px;`)
.replace(/top:-96px !important;/g, `top:${DELIVERY_SLIP_SEAL_TOP}px !important;`)
.replace(/top:-96px;/g, `top:${DELIVERY_SLIP_SEAL_TOP}px;`)
.replace(/>日期:</g, '>日&nbsp;期:<')
.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;`)
@@ -1,4 +1,5 @@
import type { Plan, PlanProduct } from '@/api/pl'
import { applyPdfStableTableCellBorders } from '@/utils/pl-slip-pdf-render'
/** 入库单打印默认公司信息(可在富文本中修改) */
export const INBOUND_SLIP_DEFAULTS = {
@@ -47,9 +48,9 @@ const PRODUCT_TABLE_CELL_LEFT = `padding:10px 6px 14px 6px;border:${PRODUCT_TABL
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) */
/** 入库单印章:public/sxshzyz.png,定位在地址单元格右上角(相对原位置向上 66px) */
const INBOUND_SLIP_SEAL_IMAGE_SRC = '/sxshzyz.png'
const INBOUND_SLIP_SEAL_TOP = -96
const INBOUND_SLIP_SEAL_TOP = -66
const INBOUND_SLIP_SEAL_IMAGE_STYLE = `position:absolute;right:4px;top:${INBOUND_SLIP_SEAL_TOP}px;width:140px;height:140px;opacity:0.85;z-index:10;`
const HANDWRITE_BLANK_INNER = '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'
const HANDWRITE_BLANK = `<span class="inbound-slip-handwrite-blank">(${HANDWRITE_BLANK_INNER})</span>`
@@ -807,6 +808,9 @@ export function prepareInboundSlipPdfGridCells(root: HTMLElement): void {
})
prepareInboundSlipPdfSignValues(root)
// 亚像素 border 在 Windows DPI 下易变粗;截图前改为 1px 背景单线
applyPdfStableTableCellBorders(root.querySelectorAll('.inbound-slip-product-cell'))
}
/** 备注、条款、印章行:统一 0.3px 全边框(兼容已保存模板,与物料网格衔接) */
@@ -1080,6 +1084,8 @@ function normalizeInboundSlipDocLayout(html: string): string {
.replace(/min-height:120px;/g, 'min-height:0;')
.replace(/top:4px !important;/g, `top:${INBOUND_SLIP_SEAL_TOP}px !important;`)
.replace(/top:4px;/g, `top:${INBOUND_SLIP_SEAL_TOP}px;`)
.replace(/top:-96px !important;/g, `top:${INBOUND_SLIP_SEAL_TOP}px !important;`)
.replace(/top:-96px;/g, `top:${INBOUND_SLIP_SEAL_TOP}px;`)
.replace(/>日期:</g, '>日&nbsp;期:<')
.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;`)
+56
View File
@@ -0,0 +1,56 @@
/**
* 运输计划单据(入库单 / 送货单 / 分装单)PDF 截图共用渲染策略。
* html2canvas 对 0.3px 边框与 border-collapse 在 Windows DPI 缩放下易画成粗线。
*/
/** 固定像素倍率,不跟 devicePixelRatio,保证 Mac / Windows 栅格化尺度一致 */
export function getPlSlipPdfCanvasScale(): number {
return 3
}
/**
* 将表格单元格边框改为「仅右+底」的 1px 背景线(首行补顶、首列补左)。
* 相邻格只画一条共享边,避免全边框叠成双线。
*/
export function applyPdfStableTableCellBorders(cells: Iterable<Element>): void {
const cellList = Array.from(cells) as HTMLElement[]
if (!cellList.length) return
const rows = new Map<HTMLTableRowElement, HTMLElement[]>()
for (const cell of cellList) {
const tr = cell.closest('tr')
if (!tr) continue
const list = rows.get(tr)
if (list) list.push(cell)
else rows.set(tr, [cell])
}
let rowIndex = 0
for (const [, rowCells] of rows) {
rowCells.forEach((cell, colIndex) => {
const gradients = ['linear-gradient(#000,#000)', 'linear-gradient(#000,#000)']
const sizes = ['100% 1px', '1px 100%']
const positions = ['left bottom', 'right top']
if (rowIndex === 0) {
gradients.push('linear-gradient(#000,#000)')
sizes.push('100% 1px')
positions.push('left top')
}
if (colIndex === 0) {
gradients.push('linear-gradient(#000,#000)')
sizes.push('1px 100%')
positions.push('left top')
}
cell.style.setProperty('border', 'none', 'important')
cell.style.setProperty('border-width', '0', 'important')
cell.style.setProperty('background-color', '#ffffff', 'important')
cell.style.setProperty('background-image', gradients.join(','), 'important')
cell.style.setProperty('background-size', sizes.join(','), 'important')
cell.style.setProperty('background-position', positions.join(','), 'important')
cell.style.setProperty('background-repeat', 'no-repeat', 'important')
})
rowIndex += 1
}
}
+134 -26
View File
@@ -1,4 +1,5 @@
import type { Plan, PlanProduct } from '@/api/pl'
import { applyPdfStableTableCellBorders } from '@/utils/pl-slip-pdf-render'
/** 分装单打印默认公司信息(可在富文本中修改) */
export const SUBPACK_SLIP_DEFAULTS = {
@@ -269,6 +270,19 @@ export const SUBPACK_SLIP_PRINT_CSS = `
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;
@@ -276,27 +290,53 @@ export const SUBPACK_SLIP_PRINT_CSS = `
box-sizing: border-box !important;
gap: 12px !important;
}
.subpack-slip-doc .subpack-slip-remark-text {
.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: 120px !important;
max-width: 320px !important;
min-width: 80px !important;
display: inline-block !important;
vertical-align: middle !important;
vertical-align: bottom !important;
-webkit-user-modify: read-only !important;
}
.subpack-slip-doc .subpack-slip-remark-extra-input {
width: 100% !important;
min-width: 120px !important;
max-width: 320px !important;
height: 32px !important;
padding: 0 6px 2px !important;
min-width: 80px !important;
height: 28px !important;
padding: 0 2px 0 !important;
margin: 0 !important;
border: none !important;
border-bottom: ${INFO_UNDERLINE} !important;
border-bottom: none !important;
border-radius: 0 !important;
background: transparent !important;
font-family: inherit !important;
@@ -647,13 +687,18 @@ function buildSubpackSlipSlipNoDisplayHtml(): string {
return `<span class="subpack-slip-slip-no-display">{{subpackSlipNo}}&nbsp;</span>`
}
/** 基本信息列宽:日期略宽;下单人/经手人等宽(与 content_style nth-child 一致) */
const BASIC_INFO_FLEX_DATE = '1.1'
const BASIC_INFO_FLEX_PERSON = '1'
function buildSubpackSlipSlipNoRowHtml(): string {
const spacer = `<div class="subpack-slip-basic-info-item subpack-slip-basic-info-spacer" style="flex:1;min-width:0;">&nbsp;</div>`
const spacer = (flex: string) =>
`<div class="subpack-slip-basic-info-item subpack-slip-basic-info-spacer" style="flex:${flex};min-width:0;">&nbsp;</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}${spacer}${spacer}
<div class="subpack-slip-info-row subpack-slip-basic-info-item subpack-slip-slip-no-field" style="${INFO_ROW}flex:1;min-width:0;">
${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()}
@@ -666,18 +711,18 @@ function buildSubpackSlipSlipNoRowHtml(): string {
}
function buildSubpackSlipBasicInfoRowHtml(): string {
const field = (label: string, placeholder: string) =>
`<div class="subpack-slip-info-row subpack-slip-basic-info-item" style="${INFO_ROW}flex:1;min-width:0;">
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}&nbsp;</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}}')}
${field('交货日期:', '{{deliveryDate}}')}
${field('下单人:', '{{orderUser}}')}
${field('经手人:', '{{handlerName}}')}
${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>`
@@ -687,10 +732,14 @@ 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}&nbsp;</span>${buildSubpackSlipRemarkExtraInputHtml()}</span>`
}
function buildSubpackSlipRemarkRowHtml(): string {
return `<div class="subpack-slip-remark-row-inner">
<span class="subpack-slip-remark-text">备注:{{remark}}</span>
${buildSubpackSlipRemarkExtraInputHtml()}
<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>`
}
@@ -991,6 +1040,31 @@ function unwrapSubpackSlipPdfCellParagraph(cell: HTMLTableCellElement): void {
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()
@@ -998,11 +1072,12 @@ function prepareSubpackSlipPdfRemarkCell(cell: HTMLTableCellElement): void {
span.className = 'subpack-slip-remark-extra-text-pdf'
span.textContent = text || '\u00a0'
span.style.setProperty('display', 'inline-block', 'important')
span.style.setProperty('min-width', '120px', 'important')
span.style.setProperty('padding', '0 6px 2px', 'important')
span.style.setProperty('border-bottom', INFO_UNDERLINE, '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', PRODUCT_FOOTER_PDF_LINE_HEIGHT, 'important')
span.style.setProperty('line-height', '1.35', 'important')
span.style.setProperty('vertical-align', 'baseline', 'important')
input.replaceWith(span)
})
@@ -1206,6 +1281,9 @@ export function prepareSubpackSlipPdfGridCells(root: HTMLElement): void {
prepareSubpackSlipPdfSignValues(root)
prepareSubpackSlipPdfCheckboxes(root)
// 亚像素 border 在 Windows DPI 下易变粗;截图前改为 1px 背景单线
applyPdfStableTableCellBorders(root.querySelectorAll('.subpack-slip-product-cell'))
}
/** 备注、条款、印章行:统一 0.3px 全边框(兼容已保存模板,与物料网格衔接) */
@@ -1605,10 +1683,40 @@ function normalizeSubpackSlipRemarkRow(html: string): string {
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(/&nbsp;/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}&nbsp;</span>${extraHtml}</span>
<div class="subpack-slip-bucket-check-row"`
}
)
}
if (!result.includes('subpack-slip-remark-extra-input')) {
result = result.replace(
/(<span class="subpack-slip-remark-text">[\s\S]*?<\/span>)(\s*<div class="subpack-slip-bucket-check-row")/,
`$1\n ${buildSubpackSlipRemarkExtraInputHtml()}$2`
/(<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(