feat(web-admin): TinyMCE 表格类 Excel 调宽调高,并优化出入库/分装/送货单排版
支持合同、报价与入库/分装/送(提)货单在富文本中拖拽调整列宽行高;单据侧加宽物料编号与名称、收窄规格单位、放大 logo、加宽条款区,并弱化调整手柄绿色阴影。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
2380b01a4f
commit
3d5866aa14
13 files changed
+31815
-30418
No files matched your search
@@ -19,11 +19,29 @@ const DOC_PADDING = '8px 10px 24px'
|
||||
/** 单据内容区最大宽度(编辑预览居中,略窄于 A4 像素宽) */
|
||||
export const DELIVERY_SLIP_DOC_MAX_WIDTH = '960px'
|
||||
const DOC_WRAP_EXTRA = `max-width:${DELIVERY_SLIP_DOC_MAX_WIDTH};margin:0 auto;box-sizing:border-box;`
|
||||
const LOGO_HEIGHT = 23
|
||||
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>`
|
||||
/**
|
||||
* 物料表列宽:加宽编号/名称,收窄规格/单位;前 5 列合计供条款区使用。
|
||||
* 物料编号 / 物料名称 / 规格 / 单位 / 数量 / 产品规格 / 实收数量
|
||||
*/
|
||||
const PRODUCT_COLGROUP = `<colgroup>
|
||||
<col style="width:18%;" />
|
||||
<col style="width:24%;" />
|
||||
<col style="width:8%;" />
|
||||
<col style="width:6%;" />
|
||||
<col style="width:12%;" />
|
||||
<col style="width:16%;" />
|
||||
<col style="width:16%;" />
|
||||
</colgroup>`
|
||||
|
||||
/** 物料网格 padding(编辑器预览) */
|
||||
const PRODUCT_GRID_CELL_PADDING = '8px 6px'
|
||||
const PRODUCT_GRID_TH_PADDING = '10px 6px'
|
||||
@@ -52,7 +70,7 @@ const SEAL_BLOCK_LINE_STYLE = 'line-height:1.35;'
|
||||
const DELIVERY_SLIP_SEAL_IMAGE_SRC = '/sxshzyz.png'
|
||||
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_INNER = ' '
|
||||
const HANDWRITE_BLANK = `<span class="delivery-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;`
|
||||
@@ -303,7 +321,8 @@ export const DELIVERY_SLIP_PRINT_CSS = `
|
||||
white-space: nowrap !important;
|
||||
flex-shrink: 0 !important;
|
||||
}
|
||||
.delivery-slip-doc table.delivery-slip-layout-table tr.delivery-slip-gap-before-product td.delivery-slip-gap-before-product-cell {
|
||||
.delivery-slip-doc table.delivery-slip-layout-table tr.delivery-slip-gap-before-product td.delivery-slip-gap-before-product-cell,
|
||||
.delivery-slip-doc .delivery-slip-section-gap.delivery-slip-gap-before-product {
|
||||
border: none !important;
|
||||
background: transparent !important;
|
||||
padding: 0 !important;
|
||||
@@ -312,7 +331,8 @@ export const DELIVERY_SLIP_PRINT_CSS = `
|
||||
height: ${INFO_GAP_BEFORE_PRODUCT}px !important;
|
||||
min-height: ${INFO_GAP_BEFORE_PRODUCT}px !important;
|
||||
}
|
||||
.delivery-slip-doc table.delivery-slip-layout-table tr.delivery-slip-gap-before-sign td.delivery-slip-gap-before-sign-cell {
|
||||
.delivery-slip-doc table.delivery-slip-layout-table tr.delivery-slip-gap-before-sign td.delivery-slip-gap-before-sign-cell,
|
||||
.delivery-slip-doc .delivery-slip-section-gap.delivery-slip-gap-before-sign {
|
||||
border: none !important;
|
||||
background: transparent !important;
|
||||
padding: 0 !important;
|
||||
@@ -405,7 +425,6 @@ export const DELIVERY_SLIP_PRINT_CSS = `
|
||||
padding: 10px 6px 14px 6px !important;
|
||||
line-height: 1.35 !important;
|
||||
vertical-align: top !important;
|
||||
height: auto !important;
|
||||
}
|
||||
.delivery-slip-doc .delivery-slip-handwrite-blank {
|
||||
display: inline-block !important;
|
||||
@@ -440,23 +459,33 @@ export const DELIVERY_SLIP_PRINT_CSS = `
|
||||
.delivery-slip-doc table.delivery-slip-outer-table > tbody > tr > td,
|
||||
.delivery-slip-doc table.delivery-slip-outer-table > tbody > tr > th,
|
||||
.delivery-slip-doc table.delivery-slip-layout-table td:not(.delivery-slip-product-cell):not(.delivery-slip-gap-before-product-cell):not(.delivery-slip-gap-before-sign-cell),
|
||||
.delivery-slip-doc table.delivery-slip-layout-table th:not(.delivery-slip-product-cell) {
|
||||
.delivery-slip-doc table.delivery-slip-layout-table th:not(.delivery-slip-product-cell),
|
||||
.delivery-slip-doc table.delivery-slip-info-table td,
|
||||
.delivery-slip-doc table.delivery-slip-info-table th,
|
||||
.delivery-slip-doc table.delivery-slip-sign-table td,
|
||||
.delivery-slip-doc table.delivery-slip-sign-table th {
|
||||
border: none !important;
|
||||
background: transparent !important;
|
||||
padding: ${TABLE_ROW_PADDING} !important;
|
||||
}
|
||||
.delivery-slip-doc table.delivery-slip-layout-table {
|
||||
.delivery-slip-doc table.delivery-slip-layout-table,
|
||||
.delivery-slip-doc table.delivery-slip-info-table,
|
||||
.delivery-slip-doc table.delivery-slip-product-table,
|
||||
.delivery-slip-doc table.delivery-slip-footer-table,
|
||||
.delivery-slip-doc table.delivery-slip-sign-table {
|
||||
border-collapse: collapse !important;
|
||||
border-spacing: 0 !important;
|
||||
table-layout: fixed !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
.delivery-slip-doc .delivery-slip-product-grid-cell {
|
||||
border: ${PRODUCT_GRID_BORDER} !important;
|
||||
text-align: center !important;
|
||||
vertical-align: middle !important;
|
||||
padding: ${PRODUCT_GRID_CELL_PADDING} !important;
|
||||
/* padding/line-height 勿 !important:与合同一致,允许 tr 行高拖拽生效 */
|
||||
padding: ${PRODUCT_GRID_CELL_PADDING};
|
||||
box-sizing: border-box !important;
|
||||
line-height: normal !important;
|
||||
line-height: normal;
|
||||
overflow: visible !important;
|
||||
white-space: normal !important;
|
||||
word-wrap: break-word !important;
|
||||
@@ -465,14 +494,28 @@ export const DELIVERY_SLIP_PRINT_CSS = `
|
||||
background-color: #fff !important;
|
||||
}
|
||||
.delivery-slip-doc th.delivery-slip-product-grid-cell {
|
||||
padding: ${PRODUCT_GRID_TH_PADDING} !important;
|
||||
padding: ${PRODUCT_GRID_TH_PADDING};
|
||||
font-weight: bold !important;
|
||||
}
|
||||
.delivery-slip-doc .delivery-slip-product-grid-cell > p {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
text-align: center !important;
|
||||
line-height: 1.35 !important;
|
||||
line-height: 1.35;
|
||||
}
|
||||
.delivery-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;
|
||||
}
|
||||
.delivery-slip-doc .delivery-slip-product-cell:not(.delivery-slip-product-grid-cell) {
|
||||
border: ${PRODUCT_GRID_BORDER} !important;
|
||||
@@ -561,12 +604,12 @@ export function buildDeliverySlipSideLabelsHtml(): string {
|
||||
</table>`
|
||||
}
|
||||
|
||||
/** 生成带占位符的送(提)货单 HTML 模板 */
|
||||
/** 生成带占位符的送(提)货单 HTML 模板(方案A:信息 / 物料 / 页脚+签字 分表) */
|
||||
export function generateDeliverySlipTemplate(_plan?: Plan): string {
|
||||
return `${DELIVERY_SLIP_STYLE_BLOCK}<div class="delivery-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="delivery-slip-title-section" style="position:relative;min-height:48px;margin-bottom:14px;">
|
||||
<div class="delivery-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="delivery-slip-title-heading" style="${TITLE_STYLE}">{{companyName}}</div>
|
||||
@@ -578,12 +621,9 @@ export function generateDeliverySlipTemplate(_plan?: Plan): string {
|
||||
<table class="delivery-slip-outer-table" style="width:100%;border-collapse:collapse;table-layout:fixed;border:none !important;">
|
||||
<tr>
|
||||
<td class="delivery-slip-main-col" style="vertical-align:top;padding-right:6px;border:none !important;background:transparent !important;">
|
||||
<table class="delivery-slip-layout-table" style="width:100%;border-collapse:collapse;table-layout:fixed;border:none !important;">
|
||||
<colgroup>
|
||||
<col span="7" style="width:14.28%;" />
|
||||
</colgroup>
|
||||
|
||||
<!-- 上:基本信息两行 -->
|
||||
<!-- 1. 基本信息(独立表,不可拖宽高) -->
|
||||
<table class="delivery-slip-info-table" style="width:100%;border-collapse:collapse;table-layout:fixed;border:none !important;">
|
||||
${INFO_COLGROUP}
|
||||
<tr>
|
||||
<td colspan="4" style="${INFO_CELL_LEFT}">
|
||||
<div class="delivery-slip-info-row" style="${INFO_ROW}">
|
||||
@@ -613,12 +653,13 @@ export function generateDeliverySlipTemplate(_plan?: Plan): string {
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<tr class="delivery-slip-gap-before-product">
|
||||
<td colspan="7" class="delivery-slip-gap-before-product-cell" style="${GAP_BEFORE_PRODUCT_CELL}"> </td>
|
||||
</tr>
|
||||
<div class="delivery-slip-section-gap delivery-slip-gap-before-product" style="${GAP_BEFORE_PRODUCT_CELL}"> </div>
|
||||
|
||||
<!-- 中:产品明细表(含备注、条款,黑色边框) -->
|
||||
<!-- 2. 物料明细表(含备注、条款+印章) -->
|
||||
<table class="delivery-slip-product-table" style="width:100%;border-collapse:collapse;table-layout:fixed;border:none !important;">
|
||||
${PRODUCT_COLGROUP}
|
||||
<tr class="delivery-slip-product-header">
|
||||
<th class="delivery-slip-product-cell delivery-slip-product-grid-cell" style="${PRODUCT_TH}">物料编号</th>
|
||||
<th class="delivery-slip-product-cell delivery-slip-product-grid-cell" style="${PRODUCT_TH}">物料名称</th>
|
||||
@@ -629,29 +670,25 @@ export function generateDeliverySlipTemplate(_plan?: Plan): string {
|
||||
<th class="delivery-slip-product-cell delivery-slip-product-grid-cell" style="${PRODUCT_TH}">实收数量</th>
|
||||
</tr>
|
||||
{{productTable}}
|
||||
|
||||
<!-- 备注(7列合并,物料表边框) -->
|
||||
<tr>
|
||||
<td colspan="7" class="delivery-slip-product-cell" style="${PRODUCT_TABLE_CELL_LEFT}">
|
||||
备注:{{remark}}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- 条款 + 地址(物料表边框) -->
|
||||
<tr>
|
||||
<td colspan="4" class="delivery-slip-product-cell delivery-slip-terms-cell" style="${PRODUCT_TERMS_CELL_LEFT}">
|
||||
<td colspan="5" class="delivery-slip-product-cell delivery-slip-terms-cell" style="${PRODUCT_TERMS_CELL_LEFT}">
|
||||
${buildDeliverySlipTermsHtml()}
|
||||
</td>
|
||||
<td colspan="3" class="delivery-slip-product-cell" style="${PRODUCT_TABLE_CELL_LEFT}">
|
||||
<td colspan="2" class="delivery-slip-product-cell" style="${PRODUCT_TABLE_CELL_LEFT}">
|
||||
${SEAL_BLOCK}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<tr class="delivery-slip-gap-before-sign">
|
||||
<td colspan="7" class="delivery-slip-gap-before-sign-cell" style="${GAP_BEFORE_SIGN_CELL}"> </td>
|
||||
</tr>
|
||||
<div class="delivery-slip-section-gap delivery-slip-gap-before-sign" style="${GAP_BEFORE_SIGN_CELL}"> </div>
|
||||
|
||||
<!-- 下:签字区(居中,flex 换行避免打印时溢出到右侧联次) -->
|
||||
<!-- 3. 签字区(独立表) -->
|
||||
<table class="delivery-slip-sign-table" style="width:100%;border-collapse:collapse;table-layout:fixed;border:none !important;">
|
||||
${buildDeliverySlipSignRowHtml()}
|
||||
</table>
|
||||
</td>
|
||||
@@ -682,18 +719,24 @@ function normalizeDeliverySlipSealPlaceholder(html: string): string {
|
||||
}
|
||||
|
||||
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*\d+px;?/gi, '')
|
||||
.replace(/min-height:\s*[^;]*;?/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()
|
||||
return `border:${PRODUCT_GRID_BORDER} !important;text-align:center;padding:${PRODUCT_GRID_CELL_PADDING};box-sizing:border-box;line-height:normal;${next}`
|
||||
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 {
|
||||
@@ -1087,10 +1130,11 @@ function normalizeDeliverySlipProductTableFooter(html: string): string {
|
||||
})
|
||||
|
||||
result = result.replace(
|
||||
/<td([^>]*colspan="4"[^>]*)>\s*(<div class="delivery-slip-terms-text"|\s*1\.本送货单)/g,
|
||||
/<td([^>]*colspan="[45]"[^>]*)>\s*(<div class="delivery-slip-terms-text"|\s*1\.本送货单)/g,
|
||||
(_match, attrs, contentStart) => {
|
||||
const withSpan = String(attrs).replace(/colspan="[45]"/, 'colspan="5"')
|
||||
const patched = patchProductFooterCellAttrs(
|
||||
attrs,
|
||||
withSpan,
|
||||
'delivery-slip-product-cell delivery-slip-terms-cell',
|
||||
PRODUCT_TERMS_CELL_LEFT
|
||||
)
|
||||
@@ -1099,9 +1143,10 @@ function normalizeDeliverySlipProductTableFooter(html: string): string {
|
||||
)
|
||||
|
||||
result = result.replace(
|
||||
/<td([^>]*colspan="3"[^>]*)>\s*<div class="delivery-slip-seal-block"/g,
|
||||
/<td([^>]*colspan="[23]"[^>]*)>\s*<div class="delivery-slip-seal-block"/g,
|
||||
(_match, attrs) => {
|
||||
const patched = patchProductFooterCellAttrs(attrs, 'delivery-slip-product-cell', PRODUCT_TABLE_CELL_LEFT)
|
||||
const withSpan = String(attrs).replace(/colspan="[23]"/, 'colspan="2"')
|
||||
const patched = patchProductFooterCellAttrs(withSpan, 'delivery-slip-product-cell', PRODUCT_TABLE_CELL_LEFT)
|
||||
return `<td${patched}>\n <div class="delivery-slip-seal-block"`
|
||||
}
|
||||
)
|
||||
@@ -1131,10 +1176,12 @@ function normalizeDeliverySlipTermsCell(html: string): string {
|
||||
}
|
||||
|
||||
let result = html.replace(
|
||||
/<td colspan="4"([^>]*)>\s*<div class="delivery-slip-terms-text"/g,
|
||||
/<td colspan="[45]"([^>]*)>\s*<div class="delivery-slip-terms-text"/g,
|
||||
(match, attrs) => {
|
||||
if (attrs.includes('delivery-slip-terms-cell') && attrs.includes('border:0.3px solid #000')) return match
|
||||
return `<td colspan="4"${patchTermsTd(attrs)}>\n <div class="delivery-slip-terms-text"`
|
||||
if (attrs.includes('delivery-slip-terms-cell') && attrs.includes('border:0.3px solid #000') && /colspan="5"/.test(match)) {
|
||||
return match
|
||||
}
|
||||
return `<td colspan="5"${patchTermsTd(attrs)}>\n <div class="delivery-slip-terms-text"`
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1151,6 +1198,15 @@ function normalizeDeliverySlipTermsCell(html: string): string {
|
||||
return result
|
||||
}
|
||||
|
||||
/** 物料表列宽 + 条款区跨列(兼容已保存模板) */
|
||||
function normalizeDeliverySlipProductColWidths(html: string): string {
|
||||
if (!html.includes('delivery-slip-product-table')) return html
|
||||
return html.replace(
|
||||
/(<table class="delivery-slip-product-table"[^>]*>\s*)<colgroup>[\s\S]*?<\/colgroup>/i,
|
||||
`$1${PRODUCT_COLGROUP}`
|
||||
)
|
||||
}
|
||||
|
||||
/** 条款区两行间距与括号内手写空白(兼容已保存模板) */
|
||||
function normalizeDeliverySlipTermsText(html: string): string {
|
||||
if (!html.includes('delivery-slip-doc') || !html.includes('1.本送货单')) {
|
||||
@@ -1311,9 +1367,83 @@ function normalizeDeliverySlipSignRow(html: string): string {
|
||||
return normalizeDeliverySlipSignLabelMarkup(result)
|
||||
}
|
||||
|
||||
/**
|
||||
* 旧版一体 layout-table / 曾拆出的 footer-table → 信息表 + 物料表(含备注条款) + 签字表。
|
||||
*/
|
||||
function migrateDeliverySlipSplitTables(html: string): string {
|
||||
if (!html.includes('delivery-slip-doc')) return html
|
||||
|
||||
if (
|
||||
html.includes('delivery-slip-product-table') &&
|
||||
!html.includes('delivery-slip-footer-table') &&
|
||||
/delivery-slip-product-table[\s\S]*?备注:/.test(html)
|
||||
) {
|
||||
return html
|
||||
}
|
||||
|
||||
if (html.includes('delivery-slip-footer-table') && html.includes('delivery-slip-product-table')) {
|
||||
return html.replace(
|
||||
/(<\/tr>\s*)<\/table>\s*<table class="delivery-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('delivery-slip-layout-table')) return html
|
||||
|
||||
return html.replace(
|
||||
/<table class="delivery-slip-layout-table"[^>]*>([\s\S]*?)<\/table>/i,
|
||||
(_full, inner: string) => {
|
||||
const headerMatch =
|
||||
inner.match(/<tr[^>]*class="[^"]*delivery-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="delivery-slip-gap-before-product">[\s\S]*?<\/tr>/i, '')
|
||||
|
||||
const afterHeader = inner.slice(headerIdx + headerMatch[0].length)
|
||||
const signMatch =
|
||||
afterHeader.match(/<tr[^>]*class="[^"]*delivery-slip-sign-row[^"]*"[\s\S]*?<\/tr>/i) ||
|
||||
afterHeader.match(/<tr>\s*<td[^>]*>[\s\S]*?仓库:[\s\S]*?<\/tr>/i)
|
||||
|
||||
let productAndFooter = afterHeader
|
||||
let signPart = buildDeliverySlipSignRowHtml()
|
||||
if (signMatch) {
|
||||
const signAt = afterHeader.indexOf(signMatch[0])
|
||||
productAndFooter = afterHeader.slice(0, signAt)
|
||||
signPart = signMatch[0]
|
||||
}
|
||||
productAndFooter = productAndFooter.replace(
|
||||
/<tr class="delivery-slip-gap-before-sign">[\s\S]*?<\/tr>/i,
|
||||
''
|
||||
)
|
||||
|
||||
return `<table class="delivery-slip-info-table" style="width:100%;border-collapse:collapse;table-layout:fixed;border:none !important;">
|
||||
${INFO_COLGROUP}
|
||||
${before.trim()}
|
||||
</table>
|
||||
<div class="delivery-slip-section-gap delivery-slip-gap-before-product" style="${GAP_BEFORE_PRODUCT_CELL}"> </div>
|
||||
<table class="delivery-slip-product-table" style="width:100%;border-collapse:collapse;table-layout:fixed;border:none !important;">
|
||||
${PRODUCT_COLGROUP}
|
||||
${headerMatch[0]}
|
||||
${productAndFooter.trim()}
|
||||
</table>
|
||||
<div class="delivery-slip-section-gap delivery-slip-gap-before-sign" style="${GAP_BEFORE_SIGN_CELL}"> </div>
|
||||
<table class="delivery-slip-sign-table" style="width:100%;border-collapse:collapse;table-layout:fixed;border:none !important;">
|
||||
${signPart.trim()}
|
||||
</table>`
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/** 统一单据左右 10px 间距、内容区宽度(兼容已保存模板) */
|
||||
function normalizeDeliverySlipDocLayout(html: string): string {
|
||||
let result = html
|
||||
let result = migrateDeliverySlipSplitTables(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;`)
|
||||
@@ -1325,7 +1455,9 @@ function normalizeDeliverySlipDocLayout(html: string): string {
|
||||
.replace(/max-width:760px/g, `max-width:${DELIVERY_SLIP_DOC_MAX_WIDTH}`)
|
||||
.replace(/max-width:794px/g, `max-width:${DELIVERY_SLIP_DOC_MAX_WIDTH}`)
|
||||
.replace(/height:46px;/g, `height:${LOGO_HEIGHT}px;`)
|
||||
.replace(/min-height:72px;/g, 'min-height:48px;')
|
||||
.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};`)
|
||||
@@ -1557,9 +1689,11 @@ export function replaceDeliverySlipPlaceholders(
|
||||
normalizeDeliverySlipTermsText(
|
||||
normalizeDeliverySlipGapBeforeSign(
|
||||
normalizeDeliverySlipProductTableFooter(
|
||||
normalizeDeliverySlipProductGridCell(
|
||||
normalizeDeliverySlipGapBeforeProduct(
|
||||
normalizeDeliverySlipTypeCheckRow(normalizeDeliverySlipSideLabelsPlaceholder(htmlContent))
|
||||
normalizeDeliverySlipProductColWidths(
|
||||
normalizeDeliverySlipProductGridCell(
|
||||
normalizeDeliverySlipGapBeforeProduct(
|
||||
normalizeDeliverySlipTypeCheckRow(normalizeDeliverySlipSideLabelsPlaceholder(htmlContent))
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -1596,7 +1730,7 @@ export function restoreDeliverySlipPlaceholders(template: string, plan: Plan): s
|
||||
result = result.replace(new RegExp(escaped, 'g'), placeholder)
|
||||
}
|
||||
|
||||
// 产品明细行还原为占位符(表头「实收数量」与「备注」行之间)
|
||||
// 产品明细行还原为占位符(表头「实收数量」之后、页脚「备注」之前;兼容分表与旧一体表)
|
||||
result = result.replace(
|
||||
/(<th[^>]*>实收数量<\/th>\s*<\/tr>\s*)([\s\S]*?)(\s*<tr>\s*<td colspan="7"[^>]*>\s*备注:)/,
|
||||
'$1{{productTable}}$3'
|
||||
|
||||
@@ -19,11 +19,29 @@ const DOC_PADDING = '8px 10px 24px'
|
||||
/** 单据内容区最大宽度(编辑预览居中,略窄于 A4 像素宽) */
|
||||
export const INBOUND_SLIP_DOC_MAX_WIDTH = '960px'
|
||||
const DOC_WRAP_EXTRA = `max-width:${INBOUND_SLIP_DOC_MAX_WIDTH};margin:0 auto;box-sizing:border-box;`
|
||||
const LOGO_HEIGHT = 23
|
||||
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>`
|
||||
/**
|
||||
* 物料表列宽:加宽编号/名称,收窄规格/单位;前 5 列合计供条款区使用。
|
||||
* 物料编号 / 物料名称 / 规格 / 单位 / 数量 / 产品规格 / 实收数量
|
||||
*/
|
||||
const PRODUCT_COLGROUP = `<colgroup>
|
||||
<col style="width:18%;" />
|
||||
<col style="width:24%;" />
|
||||
<col style="width:8%;" />
|
||||
<col style="width:6%;" />
|
||||
<col style="width:12%;" />
|
||||
<col style="width:16%;" />
|
||||
<col style="width:16%;" />
|
||||
</colgroup>`
|
||||
|
||||
/** 物料网格 padding(编辑器预览) */
|
||||
const PRODUCT_GRID_CELL_PADDING = '8px 6px'
|
||||
const PRODUCT_GRID_TH_PADDING = '10px 6px'
|
||||
@@ -52,7 +70,7 @@ const SEAL_BLOCK_LINE_STYLE = 'line-height:1.35;'
|
||||
const INBOUND_SLIP_SEAL_IMAGE_SRC = '/sxshzyz.png'
|
||||
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 = ' '
|
||||
const HANDWRITE_BLANK_INNER = ' '
|
||||
const HANDWRITE_BLANK = `<span class="inbound-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;`
|
||||
@@ -191,7 +209,8 @@ export const INBOUND_SLIP_PRINT_CSS = `
|
||||
white-space: nowrap !important;
|
||||
flex-shrink: 0 !important;
|
||||
}
|
||||
.inbound-slip-doc table.inbound-slip-layout-table tr.inbound-slip-gap-before-product td.inbound-slip-gap-before-product-cell {
|
||||
.inbound-slip-doc table.inbound-slip-layout-table tr.inbound-slip-gap-before-product td.inbound-slip-gap-before-product-cell,
|
||||
.inbound-slip-doc .inbound-slip-section-gap.inbound-slip-gap-before-product {
|
||||
border: none !important;
|
||||
background: transparent !important;
|
||||
padding: 0 !important;
|
||||
@@ -200,7 +219,8 @@ export const INBOUND_SLIP_PRINT_CSS = `
|
||||
height: ${INFO_GAP_BEFORE_PRODUCT}px !important;
|
||||
min-height: ${INFO_GAP_BEFORE_PRODUCT}px !important;
|
||||
}
|
||||
.inbound-slip-doc table.inbound-slip-layout-table tr.inbound-slip-gap-before-sign td.inbound-slip-gap-before-sign-cell {
|
||||
.inbound-slip-doc table.inbound-slip-layout-table tr.inbound-slip-gap-before-sign td.inbound-slip-gap-before-sign-cell,
|
||||
.inbound-slip-doc .inbound-slip-section-gap.inbound-slip-gap-before-sign {
|
||||
border: none !important;
|
||||
background: transparent !important;
|
||||
padding: 0 !important;
|
||||
@@ -293,7 +313,6 @@ export const INBOUND_SLIP_PRINT_CSS = `
|
||||
padding: 10px 6px 14px 6px !important;
|
||||
line-height: 1.35 !important;
|
||||
vertical-align: top !important;
|
||||
height: auto !important;
|
||||
}
|
||||
.inbound-slip-doc .inbound-slip-handwrite-blank {
|
||||
display: inline-block !important;
|
||||
@@ -328,23 +347,33 @@ export const INBOUND_SLIP_PRINT_CSS = `
|
||||
.inbound-slip-doc table.inbound-slip-outer-table > tbody > tr > td,
|
||||
.inbound-slip-doc table.inbound-slip-outer-table > tbody > tr > th,
|
||||
.inbound-slip-doc table.inbound-slip-layout-table td:not(.inbound-slip-product-cell):not(.inbound-slip-gap-before-product-cell):not(.inbound-slip-gap-before-sign-cell),
|
||||
.inbound-slip-doc table.inbound-slip-layout-table th:not(.inbound-slip-product-cell) {
|
||||
.inbound-slip-doc table.inbound-slip-layout-table th:not(.inbound-slip-product-cell),
|
||||
.inbound-slip-doc table.inbound-slip-info-table td,
|
||||
.inbound-slip-doc table.inbound-slip-info-table th,
|
||||
.inbound-slip-doc table.inbound-slip-sign-table td,
|
||||
.inbound-slip-doc table.inbound-slip-sign-table th {
|
||||
border: none !important;
|
||||
background: transparent !important;
|
||||
padding: ${TABLE_ROW_PADDING} !important;
|
||||
}
|
||||
.inbound-slip-doc table.inbound-slip-layout-table {
|
||||
.inbound-slip-doc table.inbound-slip-layout-table,
|
||||
.inbound-slip-doc table.inbound-slip-info-table,
|
||||
.inbound-slip-doc table.inbound-slip-product-table,
|
||||
.inbound-slip-doc table.inbound-slip-footer-table,
|
||||
.inbound-slip-doc table.inbound-slip-sign-table {
|
||||
border-collapse: collapse !important;
|
||||
border-spacing: 0 !important;
|
||||
table-layout: fixed !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
.inbound-slip-doc .inbound-slip-product-grid-cell {
|
||||
border: ${PRODUCT_GRID_BORDER} !important;
|
||||
text-align: center !important;
|
||||
vertical-align: middle !important;
|
||||
padding: ${PRODUCT_GRID_CELL_PADDING} !important;
|
||||
/* padding/line-height 勿 !important:与合同一致,允许 tr 行高拖拽生效 */
|
||||
padding: ${PRODUCT_GRID_CELL_PADDING};
|
||||
box-sizing: border-box !important;
|
||||
line-height: normal !important;
|
||||
line-height: normal;
|
||||
overflow: visible !important;
|
||||
white-space: normal !important;
|
||||
word-wrap: break-word !important;
|
||||
@@ -353,14 +382,28 @@ export const INBOUND_SLIP_PRINT_CSS = `
|
||||
background-color: #fff !important;
|
||||
}
|
||||
.inbound-slip-doc th.inbound-slip-product-grid-cell {
|
||||
padding: ${PRODUCT_GRID_TH_PADDING} !important;
|
||||
padding: ${PRODUCT_GRID_TH_PADDING};
|
||||
font-weight: bold !important;
|
||||
}
|
||||
.inbound-slip-doc .inbound-slip-product-grid-cell > p {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
text-align: center !important;
|
||||
line-height: 1.35 !important;
|
||||
line-height: 1.35;
|
||||
}
|
||||
.inbound-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;
|
||||
}
|
||||
.inbound-slip-doc .inbound-slip-product-cell:not(.inbound-slip-product-grid-cell) {
|
||||
border: ${PRODUCT_GRID_BORDER} !important;
|
||||
@@ -436,12 +479,12 @@ export function buildInboundSlipSideLabelsHtml(): string {
|
||||
</table>`
|
||||
}
|
||||
|
||||
/** 生成带占位符的入库单 HTML 模板 */
|
||||
/** 生成带占位符的入库单 HTML 模板(方案A:信息 / 物料 / 页脚+签字 分表) */
|
||||
export function generateInboundSlipTemplate(_plan?: Plan): string {
|
||||
return `${INBOUND_SLIP_STYLE_BLOCK}<div class="inbound-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="inbound-slip-title-section" style="position:relative;min-height:48px;margin-bottom:14px;">
|
||||
<div class="inbound-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="inbound-slip-title-heading" style="${TITLE_STYLE}">{{companyName}}</div>
|
||||
@@ -453,12 +496,9 @@ export function generateInboundSlipTemplate(_plan?: Plan): string {
|
||||
<table class="inbound-slip-outer-table" style="width:100%;border-collapse:collapse;table-layout:fixed;border:none !important;">
|
||||
<tr>
|
||||
<td class="inbound-slip-main-col" style="vertical-align:top;padding-right:6px;border:none !important;background:transparent !important;">
|
||||
<table class="inbound-slip-layout-table" style="width:100%;border-collapse:collapse;table-layout:fixed;border:none !important;">
|
||||
<colgroup>
|
||||
<col span="7" style="width:14.28%;" />
|
||||
</colgroup>
|
||||
|
||||
<!-- 上:基本信息两行 -->
|
||||
<!-- 1. 基本信息(独立表,不可拖宽高) -->
|
||||
<table class="inbound-slip-info-table" style="width:100%;border-collapse:collapse;table-layout:fixed;border:none !important;">
|
||||
${INFO_COLGROUP}
|
||||
<tr>
|
||||
<td colspan="4" style="${INFO_CELL_LEFT}">
|
||||
<div class="inbound-slip-info-row" style="${INFO_ROW}">
|
||||
@@ -487,12 +527,13 @@ export function generateInboundSlipTemplate(_plan?: Plan): string {
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<tr class="inbound-slip-gap-before-product">
|
||||
<td colspan="7" class="inbound-slip-gap-before-product-cell" style="${GAP_BEFORE_PRODUCT_CELL}"> </td>
|
||||
</tr>
|
||||
<div class="inbound-slip-section-gap inbound-slip-gap-before-product" style="${GAP_BEFORE_PRODUCT_CELL}"> </div>
|
||||
|
||||
<!-- 中:产品明细表(含备注、条款,黑色边框) -->
|
||||
<!-- 2. 物料明细表(含备注、条款+印章;可拖表头/明细列宽行高) -->
|
||||
<table class="inbound-slip-product-table" style="width:100%;border-collapse:collapse;table-layout:fixed;border:none !important;">
|
||||
${PRODUCT_COLGROUP}
|
||||
<tr class="inbound-slip-product-header">
|
||||
<th class="inbound-slip-product-cell inbound-slip-product-grid-cell" style="${PRODUCT_TH}">物料编号</th>
|
||||
<th class="inbound-slip-product-cell inbound-slip-product-grid-cell" style="${PRODUCT_TH}">物料名称</th>
|
||||
@@ -503,29 +544,25 @@ export function generateInboundSlipTemplate(_plan?: Plan): string {
|
||||
<th class="inbound-slip-product-cell inbound-slip-product-grid-cell" style="${PRODUCT_TH}">实收数量</th>
|
||||
</tr>
|
||||
{{productTable}}
|
||||
|
||||
<!-- 备注(7列合并,物料表边框) -->
|
||||
<tr>
|
||||
<td colspan="7" class="inbound-slip-product-cell" style="${PRODUCT_TABLE_CELL_LEFT}">
|
||||
备注:{{remark}}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- 条款 + 地址(物料表边框) -->
|
||||
<tr>
|
||||
<td colspan="4" class="inbound-slip-product-cell inbound-slip-terms-cell" style="${PRODUCT_TERMS_CELL_LEFT}">
|
||||
<td colspan="5" class="inbound-slip-product-cell inbound-slip-terms-cell" style="${PRODUCT_TERMS_CELL_LEFT}">
|
||||
${buildInboundSlipTermsHtml()}
|
||||
</td>
|
||||
<td colspan="3" class="inbound-slip-product-cell" style="${PRODUCT_TABLE_CELL_LEFT}">
|
||||
<td colspan="2" class="inbound-slip-product-cell" style="${PRODUCT_TABLE_CELL_LEFT}">
|
||||
${SEAL_BLOCK}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<tr class="inbound-slip-gap-before-sign">
|
||||
<td colspan="7" class="inbound-slip-gap-before-sign-cell" style="${GAP_BEFORE_SIGN_CELL}"> </td>
|
||||
</tr>
|
||||
<div class="inbound-slip-section-gap inbound-slip-gap-before-sign" style="${GAP_BEFORE_SIGN_CELL}"> </div>
|
||||
|
||||
<!-- 下:签字区(居中,flex 换行避免打印时溢出到右侧联次) -->
|
||||
<!-- 3. 签字区(独立表) -->
|
||||
<table class="inbound-slip-sign-table" style="width:100%;border-collapse:collapse;table-layout:fixed;border:none !important;">
|
||||
${buildInboundSlipSignRowHtml()}
|
||||
</table>
|
||||
</td>
|
||||
@@ -556,18 +593,24 @@ function normalizeInboundSlipSealPlaceholder(html: string): string {
|
||||
}
|
||||
|
||||
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*\d+px;?/gi, '')
|
||||
.replace(/min-height:\s*[^;]*;?/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()
|
||||
return `border:${PRODUCT_GRID_BORDER} !important;text-align:center;padding:${PRODUCT_GRID_CELL_PADDING};box-sizing:border-box;line-height:normal;${next}`
|
||||
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 {
|
||||
@@ -829,10 +872,11 @@ function normalizeInboundSlipProductTableFooter(html: string): string {
|
||||
})
|
||||
|
||||
result = result.replace(
|
||||
/<td([^>]*colspan="4"[^>]*)>\s*(<div class="inbound-slip-terms-text"|\s*1\.本送货单)/g,
|
||||
/<td([^>]*colspan="[45]"[^>]*)>\s*(<div class="inbound-slip-terms-text"|\s*1\.本送货单)/g,
|
||||
(_match, attrs, contentStart) => {
|
||||
const withSpan = String(attrs).replace(/colspan="[45]"/, 'colspan="5"')
|
||||
const patched = patchProductFooterCellAttrs(
|
||||
attrs,
|
||||
withSpan,
|
||||
'inbound-slip-product-cell inbound-slip-terms-cell',
|
||||
PRODUCT_TERMS_CELL_LEFT
|
||||
)
|
||||
@@ -841,9 +885,10 @@ function normalizeInboundSlipProductTableFooter(html: string): string {
|
||||
)
|
||||
|
||||
result = result.replace(
|
||||
/<td([^>]*colspan="3"[^>]*)>\s*<div class="inbound-slip-seal-block"/g,
|
||||
/<td([^>]*colspan="[23]"[^>]*)>\s*<div class="inbound-slip-seal-block"/g,
|
||||
(_match, attrs) => {
|
||||
const patched = patchProductFooterCellAttrs(attrs, 'inbound-slip-product-cell', PRODUCT_TABLE_CELL_LEFT)
|
||||
const withSpan = String(attrs).replace(/colspan="[23]"/, 'colspan="2"')
|
||||
const patched = patchProductFooterCellAttrs(withSpan, 'inbound-slip-product-cell', PRODUCT_TABLE_CELL_LEFT)
|
||||
return `<td${patched}>\n <div class="inbound-slip-seal-block"`
|
||||
}
|
||||
)
|
||||
@@ -873,10 +918,12 @@ function normalizeInboundSlipTermsCell(html: string): string {
|
||||
}
|
||||
|
||||
let result = html.replace(
|
||||
/<td colspan="4"([^>]*)>\s*<div class="inbound-slip-terms-text"/g,
|
||||
/<td colspan="[45]"([^>]*)>\s*<div class="inbound-slip-terms-text"/g,
|
||||
(match, attrs) => {
|
||||
if (attrs.includes('inbound-slip-terms-cell') && attrs.includes('border:0.3px solid #000')) return match
|
||||
return `<td colspan="4"${patchTermsTd(attrs)}>\n <div class="inbound-slip-terms-text"`
|
||||
if (attrs.includes('inbound-slip-terms-cell') && attrs.includes('border:0.3px solid #000') && /colspan="5"/.test(match)) {
|
||||
return match
|
||||
}
|
||||
return `<td colspan="5"${patchTermsTd(attrs)}>\n <div class="inbound-slip-terms-text"`
|
||||
}
|
||||
)
|
||||
|
||||
@@ -893,6 +940,15 @@ function normalizeInboundSlipTermsCell(html: string): string {
|
||||
return result
|
||||
}
|
||||
|
||||
/** 物料表列宽 + 条款区跨列(兼容已保存模板) */
|
||||
function normalizeInboundSlipProductColWidths(html: string): string {
|
||||
if (!html.includes('inbound-slip-product-table')) return html
|
||||
return html.replace(
|
||||
/(<table class="inbound-slip-product-table"[^>]*>\s*)<colgroup>[\s\S]*?<\/colgroup>/i,
|
||||
`$1${PRODUCT_COLGROUP}`
|
||||
)
|
||||
}
|
||||
|
||||
/** 条款区两行间距与括号内手写空白(兼容已保存模板) */
|
||||
function normalizeInboundSlipTermsText(html: string): string {
|
||||
if (!html.includes('inbound-slip-doc') || !html.includes('1.本送货单')) {
|
||||
@@ -1053,9 +1109,86 @@ function normalizeInboundSlipSignRow(html: string): string {
|
||||
return normalizeInboundSlipSignLabelMarkup(result)
|
||||
}
|
||||
|
||||
/**
|
||||
* 旧版一体 layout-table / 曾拆出的 footer-table → 方案A:
|
||||
* 信息表 + 物料表(含备注条款) + 签字表。
|
||||
*/
|
||||
function migrateInboundSlipSplitTables(html: string): string {
|
||||
if (!html.includes('inbound-slip-doc')) return html
|
||||
|
||||
// 已正确:物料表内含备注,且无独立 footer-table
|
||||
if (
|
||||
html.includes('inbound-slip-product-table') &&
|
||||
!html.includes('inbound-slip-footer-table') &&
|
||||
/inbound-slip-product-table[\s\S]*?备注:/.test(html)
|
||||
) {
|
||||
return html
|
||||
}
|
||||
|
||||
// 把独立 footer-table 并回 product-table
|
||||
if (html.includes('inbound-slip-footer-table') && html.includes('inbound-slip-product-table')) {
|
||||
return html.replace(
|
||||
/(<\/tr>\s*)<\/table>\s*<table class="inbound-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('inbound-slip-layout-table')) return html
|
||||
|
||||
return html.replace(
|
||||
/<table class="inbound-slip-layout-table"[^>]*>([\s\S]*?)<\/table>/i,
|
||||
(_full, inner: string) => {
|
||||
const headerMatch =
|
||||
inner.match(/<tr[^>]*class="[^"]*inbound-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="inbound-slip-gap-before-product">[\s\S]*?<\/tr>/i, '')
|
||||
|
||||
const afterHeader = inner.slice(headerIdx + headerMatch[0].length)
|
||||
const signMatch =
|
||||
afterHeader.match(/<tr[^>]*class="[^"]*inbound-slip-sign-row[^"]*"[\s\S]*?<\/tr>/i) ||
|
||||
afterHeader.match(/<tr>\s*<td[^>]*>[\s\S]*?仓库:[\s\S]*?<\/tr>/i)
|
||||
|
||||
let productAndFooter = afterHeader
|
||||
let signPart = buildInboundSlipSignRowHtml()
|
||||
if (signMatch) {
|
||||
const signAt = afterHeader.indexOf(signMatch[0])
|
||||
productAndFooter = afterHeader.slice(0, signAt)
|
||||
signPart = signMatch[0]
|
||||
}
|
||||
productAndFooter = productAndFooter.replace(
|
||||
/<tr class="inbound-slip-gap-before-sign">[\s\S]*?<\/tr>/i,
|
||||
''
|
||||
)
|
||||
|
||||
return `<table class="inbound-slip-info-table" style="width:100%;border-collapse:collapse;table-layout:fixed;border:none !important;">
|
||||
${INFO_COLGROUP}
|
||||
${before.trim()}
|
||||
</table>
|
||||
<div class="inbound-slip-section-gap inbound-slip-gap-before-product" style="${GAP_BEFORE_PRODUCT_CELL}"> </div>
|
||||
<table class="inbound-slip-product-table" style="width:100%;border-collapse:collapse;table-layout:fixed;border:none !important;">
|
||||
${PRODUCT_COLGROUP}
|
||||
${headerMatch[0]}
|
||||
${productAndFooter.trim()}
|
||||
</table>
|
||||
<div class="inbound-slip-section-gap inbound-slip-gap-before-sign" style="${GAP_BEFORE_SIGN_CELL}"> </div>
|
||||
<table class="inbound-slip-sign-table" style="width:100%;border-collapse:collapse;table-layout:fixed;border:none !important;">
|
||||
${signPart.trim()}
|
||||
</table>`
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/** 统一单据左右 10px 间距、内容区宽度(兼容已保存模板) */
|
||||
function normalizeInboundSlipDocLayout(html: string): string {
|
||||
let result = html
|
||||
let result = migrateInboundSlipSplitTables(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;`)
|
||||
@@ -1067,7 +1200,9 @@ function normalizeInboundSlipDocLayout(html: string): string {
|
||||
.replace(/max-width:760px/g, `max-width:${INBOUND_SLIP_DOC_MAX_WIDTH}`)
|
||||
.replace(/max-width:794px/g, `max-width:${INBOUND_SLIP_DOC_MAX_WIDTH}`)
|
||||
.replace(/height:46px;/g, `height:${LOGO_HEIGHT}px;`)
|
||||
.replace(/min-height:72px;/g, 'min-height:48px;')
|
||||
.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};`)
|
||||
@@ -1173,8 +1308,10 @@ export function replaceInboundSlipPlaceholders(
|
||||
normalizeInboundSlipTermsText(
|
||||
normalizeInboundSlipGapBeforeSign(
|
||||
normalizeInboundSlipProductTableFooter(
|
||||
normalizeInboundSlipProductGridCell(
|
||||
normalizeInboundSlipGapBeforeProduct(normalizeInboundSlipSideLabelsPlaceholder(htmlContent))
|
||||
normalizeInboundSlipProductColWidths(
|
||||
normalizeInboundSlipProductGridCell(
|
||||
normalizeInboundSlipGapBeforeProduct(normalizeInboundSlipSideLabelsPlaceholder(htmlContent))
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -19,11 +19,29 @@ 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 = 23
|
||||
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'
|
||||
@@ -442,7 +460,8 @@ export const SUBPACK_SLIP_PRINT_CSS = `
|
||||
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 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;
|
||||
@@ -451,7 +470,8 @@ export const SUBPACK_SLIP_PRINT_CSS = `
|
||||
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 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;
|
||||
@@ -545,7 +565,6 @@ export const SUBPACK_SLIP_PRINT_CSS = `
|
||||
padding: 10px 6px 14px 6px !important;
|
||||
line-height: 1.35 !important;
|
||||
vertical-align: top !important;
|
||||
height: auto !important;
|
||||
}
|
||||
.subpack-slip-doc .subpack-slip-handwrite-blank {
|
||||
display: inline-block !important;
|
||||
@@ -580,23 +599,33 @@ export const SUBPACK_SLIP_PRINT_CSS = `
|
||||
.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-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-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: ${PRODUCT_GRID_CELL_PADDING} !important;
|
||||
/* padding/line-height 勿 !important:与合同一致,允许 tr 行高拖拽生效 */
|
||||
padding: ${PRODUCT_GRID_CELL_PADDING};
|
||||
box-sizing: border-box !important;
|
||||
line-height: normal !important;
|
||||
line-height: normal;
|
||||
overflow: visible !important;
|
||||
white-space: normal !important;
|
||||
word-wrap: break-word !important;
|
||||
@@ -605,14 +634,28 @@ export const SUBPACK_SLIP_PRINT_CSS = `
|
||||
background-color: #fff !important;
|
||||
}
|
||||
.subpack-slip-doc th.subpack-slip-product-grid-cell {
|
||||
padding: ${PRODUCT_GRID_TH_PADDING} !important;
|
||||
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 !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;
|
||||
@@ -762,12 +805,12 @@ export function buildSubpackSlipSideLabelsHtml(): string {
|
||||
return ''
|
||||
}
|
||||
|
||||
/** 生成带占位符的分装单 HTML 模板 */
|
||||
/** 生成带占位符的分装单 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:48px;margin-bottom:14px;">
|
||||
<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>
|
||||
@@ -775,37 +818,35 @@ export function generateSubpackSlipTemplate(_plan?: Plan): string {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="subpack-slip-layout-table" style="width:100%;border-collapse:collapse;table-layout:fixed;border:none !important;">
|
||||
<colgroup>
|
||||
<col span="7" style="width:14.28%;" />
|
||||
</colgroup>
|
||||
<!-- 1. 基本信息(独立表,不可拖宽高) -->
|
||||
<table class="subpack-slip-info-table" style="width:100%;border-collapse:collapse;table-layout:fixed;border:none !important;">
|
||||
${INFO_COLGROUP}
|
||||
|
||||
<!-- 分装单号(右缘与经手人标签右缘对齐) -->
|
||||
${buildSubpackSlipSlipNoRowHtml()}
|
||||
|
||||
<!-- 基本信息一行四列 -->
|
||||
${buildSubpackSlipBasicInfoRowHtml()}
|
||||
</table>
|
||||
|
||||
<tr class="subpack-slip-gap-before-product">
|
||||
<td colspan="7" class="subpack-slip-gap-before-product-cell" style="${GAP_BEFORE_PRODUCT_CELL}"> </td>
|
||||
</tr>
|
||||
<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}}
|
||||
|
||||
<!-- 备注 + 桶选项(7列合并,物料表边框) -->
|
||||
<tr>
|
||||
<td colspan="7" class="subpack-slip-product-cell subpack-slip-remark-cell" style="${PRODUCT_TABLE_CELL_LEFT}">
|
||||
${buildSubpackSlipRemarkRowHtml()}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<tr class="subpack-slip-gap-before-sign">
|
||||
<td colspan="7" class="subpack-slip-gap-before-sign-cell" style="${GAP_BEFORE_SIGN_CELL}"> </td>
|
||||
</tr>
|
||||
<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>`
|
||||
@@ -852,18 +893,24 @@ function normalizeSubpackSlipSealPlaceholder(html: string): string {
|
||||
}
|
||||
|
||||
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*\d+px;?/gi, '')
|
||||
.replace(/min-height:\s*[^;]*;?/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()
|
||||
return `border:${PRODUCT_GRID_BORDER} !important;text-align:center;padding:${PRODUCT_GRID_CELL_PADDING};box-sizing:border-box;line-height:normal;${next}`
|
||||
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 {
|
||||
@@ -1561,9 +1608,92 @@ function normalizeSubpackSlipSignRow(html: string): string {
|
||||
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 = html
|
||||
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;`)
|
||||
@@ -1575,7 +1705,9 @@ function normalizeSubpackSlipDocLayout(html: string): string {
|
||||
.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(/min-height:72px;/g, 'min-height:48px;')
|
||||
.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};`)
|
||||
@@ -1791,18 +1923,18 @@ function normalizeSubpackSlipSlipNoRow(html: string): string {
|
||||
|
||||
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-table")/,
|
||||
/\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-table")/,
|
||||
/<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-table"[\s\S]*?<colgroup>[\s\S]*?<\/colgroup>\s*)/,
|
||||
/(<table class="subpack-slip-(?:layout|info)-table"[\s\S]*?<colgroup>[\s\S]*?<\/colgroup>\s*)/,
|
||||
`$1\n ${buildSubpackSlipSlipNoRowHtml()}\n\n `
|
||||
)
|
||||
}
|
||||
@@ -1993,14 +2125,16 @@ export function replaceSubpackSlipPlaceholders(
|
||||
normalizeSubpackSlipRemoveTermsRow(
|
||||
normalizeSubpackSlipGapBeforeSign(
|
||||
normalizeSubpackSlipProductTableFooter(
|
||||
normalizeSubpackSlipRemarkRow(
|
||||
normalizeSubpackSlipProductGridCell(
|
||||
normalizeSubpackSlipProductHeader(
|
||||
normalizeSubpackSlipGapBeforeProduct(
|
||||
normalizeSubpackSlipSlipNoRow(
|
||||
normalizeSubpackSlipBasicInfoOneRow(
|
||||
normalizeSubpackSlipBasicInfoFields(
|
||||
normalizeSubpackSlipRemoveSideLabels(htmlContent)
|
||||
normalizeSubpackSlipProductColWidths(
|
||||
normalizeSubpackSlipRemarkRow(
|
||||
normalizeSubpackSlipProductGridCell(
|
||||
normalizeSubpackSlipProductHeader(
|
||||
normalizeSubpackSlipGapBeforeProduct(
|
||||
normalizeSubpackSlipSlipNoRow(
|
||||
normalizeSubpackSlipBasicInfoOneRow(
|
||||
normalizeSubpackSlipBasicInfoFields(
|
||||
normalizeSubpackSlipRemoveSideLabels(htmlContent)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -2093,7 +2227,7 @@ export function restoreSubpackSlipPlaceholders(template: string, plan: Plan): st
|
||||
'$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'
|
||||
|
||||
@@ -0,0 +1,927 @@
|
||||
/**
|
||||
* TinyMCE 表格「类 Excel」拖拽
|
||||
*
|
||||
* 在表格列/行边界叠加可拖调整条(不依赖 snooker、不依赖边线像素命中),
|
||||
* 初始不改模板 HTML;仅开始拖拽时把该表当前视觉尺寸快照为像素。
|
||||
*/
|
||||
|
||||
const HANDLE_THICKNESS = 5
|
||||
const HANDLE_ROW_THICKNESS = 5
|
||||
const MIN_COL_PX = 28
|
||||
const MIN_ROW_PX = 20
|
||||
const HANDLE_CLASS = 'sx-excel-resize-handle'
|
||||
const HANDLE_COL = 'sx-excel-resize-col'
|
||||
const HANDLE_ROW = 'sx-excel-resize-row'
|
||||
/** 强制撑开表格行高的占位(float 撑高,不依赖 tr/td height 是否被浏览器忽略) */
|
||||
const ROW_SPACER_CLASS = 'sx-excel-row-spacer'
|
||||
|
||||
/** 单据外层表 / 联次小表 / 信息页脚签字表:不参与列行拖拽 */
|
||||
const EXCLUDE_TABLE_CLASS_RE =
|
||||
/(?:^|\s)(?:[\w-]+-)?(?:slip-outer-table|slip-side-labels|side-labels|slip-info-table|slip-footer-table|slip-sign-table)(?:\s|$)/i
|
||||
|
||||
type ResizeKind = 'col' | 'row'
|
||||
|
||||
interface ResizeState {
|
||||
kind: ResizeKind
|
||||
table: HTMLTableElement
|
||||
index: number
|
||||
/** 列拖拽时右邻列索引(边界在 index 与 neighborIndex 之间) */
|
||||
neighborIndex?: number
|
||||
startPos: number
|
||||
startSizes: number[]
|
||||
/** 锁定表格总宽,拖列时不得改变 */
|
||||
lockedTableWidth?: number
|
||||
}
|
||||
|
||||
const isExcelResizeExcludedTable = (table: HTMLTableElement): boolean => {
|
||||
const cls = typeof table.className === 'string' ? table.className : String(table.className || '')
|
||||
return EXCLUDE_TABLE_CLASS_RE.test(cls)
|
||||
}
|
||||
|
||||
/** 优先选「单列单元格最多」的行作度量行(入库单布局表首行多为 colspan,物料表头才是真列) */
|
||||
const findMetricRow = (table: HTMLTableElement): HTMLTableRowElement | null => {
|
||||
let best: HTMLTableRowElement | null = null
|
||||
let bestSpan1 = -1
|
||||
Array.from(table.rows).forEach((row) => {
|
||||
let span1 = 0
|
||||
let total = 0
|
||||
Array.from(row.cells).forEach((cell) => {
|
||||
const span = cell.colSpan || 1
|
||||
total += span
|
||||
if (span === 1) span1 += 1
|
||||
})
|
||||
if (span1 > bestSpan1) {
|
||||
bestSpan1 = span1
|
||||
best = row
|
||||
}
|
||||
if (span1 > 0 && span1 === total) {
|
||||
best = row
|
||||
bestSpan1 = span1
|
||||
}
|
||||
})
|
||||
return best || table.rows[0] || null
|
||||
}
|
||||
|
||||
const columnCount = (table: HTMLTableElement): number => {
|
||||
const row = findMetricRow(table)
|
||||
if (!row) return 0
|
||||
let n = 0
|
||||
for (const cell of Array.from(row.cells)) n += cell.colSpan || 1
|
||||
return n
|
||||
}
|
||||
|
||||
const getColumnCells = (table: HTMLTableElement, colIndex: number): HTMLTableCellElement[] => {
|
||||
const cells: HTMLTableCellElement[] = []
|
||||
Array.from(table.rows).forEach((row) => {
|
||||
let idx = 0
|
||||
for (const cell of Array.from(row.cells)) {
|
||||
const span = cell.colSpan || 1
|
||||
if (colIndex >= idx && colIndex < idx + span) {
|
||||
// 跨列表头/合计行不能拿来当单列宽度
|
||||
if (span === 1) cells.push(cell)
|
||||
break
|
||||
}
|
||||
idx += span
|
||||
}
|
||||
})
|
||||
return cells
|
||||
}
|
||||
|
||||
/** 只按「非跨列」单元格量列宽;优先度量行(物料表头) */
|
||||
const measureColWidth = (table: HTMLTableElement, colIndex: number): number => {
|
||||
const row = findMetricRow(table)
|
||||
if (row) {
|
||||
let idx = 0
|
||||
for (const cell of Array.from(row.cells)) {
|
||||
const span = cell.colSpan || 1
|
||||
if (colIndex >= idx && colIndex < idx + span) {
|
||||
const w = cell.getBoundingClientRect().width
|
||||
return Math.max(MIN_COL_PX, span > 1 ? w / span : w)
|
||||
}
|
||||
idx += span
|
||||
}
|
||||
}
|
||||
const cells = getColumnCells(table, colIndex)
|
||||
if (!cells.length) return MIN_COL_PX
|
||||
return Math.max(MIN_COL_PX, ...cells.map((c) => c.getBoundingClientRect().width))
|
||||
}
|
||||
|
||||
const measureRowHeight = (row: HTMLTableRowElement): number => row.getBoundingClientRect().height
|
||||
|
||||
const isGapSpacerRow = (row: HTMLTableRowElement): boolean => {
|
||||
const cls = typeof row.className === 'string' ? row.className : ''
|
||||
if (/gap-before-(product|sign)/i.test(cls)) return true
|
||||
return !!row.querySelector(
|
||||
'[class*="gap-before-product-cell"],[class*="gap-before-sign-cell"]'
|
||||
)
|
||||
}
|
||||
|
||||
/** 物料明细网格单元格(入库/送货/分装单);有此类名的表仅允许在物料区拖宽高 */
|
||||
const isProductGridCell = (el: Element | null): boolean => {
|
||||
if (!el || !(el instanceof Element)) return false
|
||||
const cell = el.closest('td,th')
|
||||
if (!cell) return false
|
||||
const cls = typeof cell.className === 'string' ? cell.className : String(cell.className || '')
|
||||
return /product-grid-cell/i.test(cls)
|
||||
}
|
||||
|
||||
const isProductGridRow = (row: HTMLTableRowElement): boolean =>
|
||||
Array.from(row.cells).some((cell) => isProductGridCell(cell))
|
||||
|
||||
const tableHasProductGrid = (table: HTMLTableElement): boolean =>
|
||||
!!table.querySelector('td.product-grid-cell, th.product-grid-cell, [class*="product-grid-cell"]')
|
||||
|
||||
/** 方案A 独立物料表:整表都是物料区,悬停任意处即可出调整条 */
|
||||
const isProductOnlyTable = (table: HTMLTableElement): boolean =>
|
||||
/slip-product-table/i.test(
|
||||
typeof table.className === 'string' ? table.className : String(table.className || '')
|
||||
)
|
||||
|
||||
const getProductGridRows = (table: HTMLTableElement): HTMLTableRowElement[] =>
|
||||
Array.from(table.rows).filter((row) => isProductGridRow(row))
|
||||
|
||||
/** 物料区整体包围盒(相对 viewport) */
|
||||
const getProductGridRect = (table: HTMLTableElement): DOMRect | null => {
|
||||
const rows = getProductGridRows(table)
|
||||
if (!rows.length) return null
|
||||
let left = Infinity
|
||||
let top = Infinity
|
||||
let right = -Infinity
|
||||
let bottom = -Infinity
|
||||
rows.forEach((row) => {
|
||||
const r = row.getBoundingClientRect()
|
||||
left = Math.min(left, r.left)
|
||||
top = Math.min(top, r.top)
|
||||
right = Math.max(right, r.right)
|
||||
bottom = Math.max(bottom, r.bottom)
|
||||
})
|
||||
if (!Number.isFinite(left) || right - left < 4 || bottom - top < 4) return null
|
||||
return new DOMRect(left, top, right - left, bottom - top)
|
||||
}
|
||||
|
||||
/**
|
||||
* 入库单等模板用 `<col span="7" style="width:14.28%">`,fixed 布局下会盖掉 td 像素宽。
|
||||
* 拖拽时必须改写成「每列一个 col + 像素宽」,否则视觉上拖不动。
|
||||
*/
|
||||
const syncColgroupPixelWidths = (
|
||||
editor: any,
|
||||
table: HTMLTableElement,
|
||||
widths: number[]
|
||||
): void => {
|
||||
const doc = table.ownerDocument
|
||||
table.querySelectorAll('colgroup').forEach((cg) => cg.remove())
|
||||
const colgroup = doc.createElement('colgroup')
|
||||
widths.forEach((w) => {
|
||||
const col = doc.createElement('col')
|
||||
editor.dom.setStyle(col, 'width', `${Math.round(w)}px`)
|
||||
colgroup.appendChild(col)
|
||||
})
|
||||
const first = table.firstElementChild
|
||||
if (first && first.tagName === 'TBODY') {
|
||||
table.insertBefore(colgroup, first)
|
||||
} else {
|
||||
table.insertBefore(colgroup, table.firstChild)
|
||||
}
|
||||
}
|
||||
|
||||
const resolveResizeTargetTable = (from: Element | null): HTMLTableElement | null => {
|
||||
if (!from) return null
|
||||
const product = from.closest('table[class*="slip-product-table"]') as HTMLTableElement | null
|
||||
if (product) return product
|
||||
|
||||
const outer = from.closest('table[class*="slip-outer-table"]') as HTMLTableElement | null
|
||||
if (outer) {
|
||||
const blocked = from.closest(
|
||||
'table[class*="slip-info-table"],table[class*="slip-footer-table"],table[class*="slip-sign-table"]'
|
||||
)
|
||||
if (blocked) return null
|
||||
const innerProduct = outer.querySelector(
|
||||
'table[class*="slip-product-table"]'
|
||||
) as HTMLTableElement | null
|
||||
// 点在外层空白/联次时不误挂整表调整条
|
||||
if (!from.closest('table[class*="slip-product-table"]')) return null
|
||||
if (innerProduct) return innerProduct
|
||||
}
|
||||
|
||||
const table = from.closest('table') as HTMLTableElement | null
|
||||
if (!table || isExcelResizeExcludedTable(table)) return null
|
||||
return table
|
||||
}
|
||||
|
||||
const getEditorContentWidth = (editor: any, body: HTMLElement): number => {
|
||||
const style = editor.getDoc?.()?.defaultView?.getComputedStyle?.(body)
|
||||
const padL = style ? parseFloat(style.paddingLeft) || 0 : 0
|
||||
const padR = style ? parseFloat(style.paddingRight) || 0 : 0
|
||||
return Math.max(0, body.clientWidth - padL - padR)
|
||||
}
|
||||
|
||||
/** 相对编辑器 body 的定位(body 为 position:relative) */
|
||||
const relativeToBody = (body: HTMLElement, rect: DOMRect) => {
|
||||
const br = body.getBoundingClientRect()
|
||||
return {
|
||||
left: rect.left - br.left + body.scrollLeft,
|
||||
top: rect.top - br.top + body.scrollTop
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将当前视觉列宽快照为像素,并钳制在编辑区宽度内(等比缩放,不改变相对比例)。
|
||||
* 返回写入后的列宽与表宽,供拖拽直接使用(避免再次测量造成二次放大)。
|
||||
*/
|
||||
const snapshotTableVisualToPixels = (
|
||||
editor: any,
|
||||
table: HTMLTableElement
|
||||
): { widths: number[]; tableWidth: number } | null => {
|
||||
const body = editor.getBody() as HTMLElement | null
|
||||
if (!body) return null
|
||||
const cols = columnCount(table)
|
||||
if (cols <= 0) return null
|
||||
|
||||
// 先统一 border-box,再测量,保证「量到的宽 = 写回去的宽」
|
||||
Array.from(table.querySelectorAll('td,th')).forEach((cell) => {
|
||||
editor.dom.setStyle(cell, 'box-sizing', 'border-box')
|
||||
})
|
||||
|
||||
const raw: number[] = []
|
||||
for (let i = 0; i < cols; i++) {
|
||||
raw.push(Math.max(MIN_COL_PX, Math.round(measureColWidth(table, i))))
|
||||
}
|
||||
const rawSum = raw.reduce((a, b) => a + b, 0) || cols
|
||||
// 以首行单元格宽度之和为准更稳;再与表宽、编辑区取最小,防止撑破弹窗
|
||||
const visualW = Math.round(table.getBoundingClientRect().width) || rawSum
|
||||
const contentW = getEditorContentWidth(editor, body)
|
||||
const targetW = Math.max(cols * MIN_COL_PX, Math.min(rawSum, visualW, contentW || visualW))
|
||||
|
||||
let used = 0
|
||||
const widths = raw.map((w, i) => {
|
||||
if (i === cols - 1) return Math.max(MIN_COL_PX, targetW - used)
|
||||
const next = Math.max(MIN_COL_PX, Math.round((w / rawSum) * targetW))
|
||||
used += next
|
||||
return next
|
||||
})
|
||||
|
||||
editor.dom.setStyle(table, 'table-layout', 'fixed')
|
||||
editor.dom.setStyle(table, 'width', `${targetW}px`)
|
||||
editor.dom.setStyle(table, 'max-width', '100%')
|
||||
editor.dom.setStyle(table, 'box-sizing', 'border-box')
|
||||
editor.dom.setAttrib(table, 'width', '')
|
||||
|
||||
const applyWidthToCol = (colIndex: number, widthPx: number) => {
|
||||
getColumnCells(table, colIndex).forEach((cell) => {
|
||||
editor.dom.setStyle(cell, 'box-sizing', 'border-box')
|
||||
editor.dom.setStyle(cell, 'width', `${widthPx}px`)
|
||||
editor.dom.setAttrib(cell, 'width', '')
|
||||
})
|
||||
const head = findMetricRow(table)
|
||||
if (!head) return
|
||||
let idx = 0
|
||||
for (const cell of Array.from(head.cells)) {
|
||||
const span = cell.colSpan || 1
|
||||
if (colIndex >= idx && colIndex < idx + span && span === 1) {
|
||||
editor.dom.setStyle(cell, 'box-sizing', 'border-box')
|
||||
editor.dom.setStyle(cell, 'width', `${widthPx}px`)
|
||||
break
|
||||
}
|
||||
idx += span
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < cols; i++) {
|
||||
applyWidthToCol(i, widths[i])
|
||||
}
|
||||
syncColgroupPixelWidths(editor, table, widths)
|
||||
|
||||
return { widths, tableWidth: targetW }
|
||||
}
|
||||
|
||||
const injectHandleStyles = (doc: Document) => {
|
||||
// 版本号:样式规则变更后强制重注入
|
||||
const styleId = 'sx-excel-resize-style-v7'
|
||||
;[
|
||||
'sx-excel-resize-style',
|
||||
'sx-excel-resize-style-v3',
|
||||
'sx-excel-resize-style-v4',
|
||||
'sx-excel-resize-style-v5',
|
||||
'sx-excel-resize-style-v6'
|
||||
].forEach((id) => doc.getElementById(id)?.remove())
|
||||
if (doc.getElementById(styleId)) return
|
||||
const style = doc.createElement('style')
|
||||
style.id = styleId
|
||||
style.textContent = `
|
||||
.${HANDLE_CLASS} {
|
||||
position: absolute;
|
||||
z-index: 100001;
|
||||
background: transparent;
|
||||
box-sizing: border-box;
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
/* 可视:黑线 + 极淡绿阴影(能辨认即可) */
|
||||
.${HANDLE_COL}::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 1px;
|
||||
transform: translateX(-50%);
|
||||
background: #000;
|
||||
box-shadow:
|
||||
-1px 0 1px rgba(16, 185, 129, 0.35),
|
||||
1px 0 1px rgba(16, 185, 129, 0.35);
|
||||
pointer-events: none;
|
||||
}
|
||||
.${HANDLE_ROW}::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
transform: translateY(-50%);
|
||||
background: #000;
|
||||
box-shadow:
|
||||
0 -1px 1px rgba(16, 185, 129, 0.35),
|
||||
0 1px 1px rgba(16, 185, 129, 0.35);
|
||||
pointer-events: none;
|
||||
}
|
||||
.${HANDLE_CLASS}:hover::before,
|
||||
.${HANDLE_CLASS}.is-dragging::before {
|
||||
box-shadow:
|
||||
-1px 0 2px rgba(16, 185, 129, 0.5),
|
||||
1px 0 2px rgba(16, 185, 129, 0.5);
|
||||
}
|
||||
.${HANDLE_ROW}:hover::before,
|
||||
.${HANDLE_ROW}.is-dragging::before {
|
||||
box-shadow:
|
||||
0 -1px 2px rgba(16, 185, 129, 0.5),
|
||||
0 1px 2px rgba(16, 185, 129, 0.5);
|
||||
}
|
||||
.${HANDLE_COL} { cursor: col-resize; z-index: 100001; }
|
||||
.${HANDLE_ROW} { cursor: row-resize; z-index: 100002; }
|
||||
.${ROW_SPACER_CLASS} {
|
||||
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;
|
||||
}
|
||||
`
|
||||
doc.head.appendChild(style)
|
||||
}
|
||||
|
||||
export const setupExcelLikeTableResize = (editor: any) => {
|
||||
let state: ResizeState | null = null
|
||||
let activeTable: HTMLTableElement | null = null
|
||||
let handlesRoot: HTMLElement | null = null
|
||||
let hideTimer: number | null = null
|
||||
|
||||
const setColWidth = (table: HTMLTableElement, colIndex: number, widthPx: number) => {
|
||||
const w = Math.max(MIN_COL_PX, Math.round(widthPx))
|
||||
getColumnCells(table, colIndex).forEach((cell) => {
|
||||
editor.dom.setStyle(cell, 'box-sizing', 'border-box')
|
||||
editor.dom.setStyle(cell, 'width', `${w}px`)
|
||||
})
|
||||
const head = findMetricRow(table)
|
||||
if (head) {
|
||||
let idx = 0
|
||||
for (const cell of Array.from(head.cells)) {
|
||||
const span = cell.colSpan || 1
|
||||
if (colIndex >= idx && colIndex < idx + span && span === 1) {
|
||||
editor.dom.setStyle(cell, 'box-sizing', 'border-box')
|
||||
editor.dom.setStyle(cell, 'width', `${w}px`)
|
||||
break
|
||||
}
|
||||
idx += span
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 按完整列宽数组重写,并锁定表宽(Excel:拖边界只改两列,总宽不变) */
|
||||
const applyColumnWidthsLocked = (
|
||||
table: HTMLTableElement,
|
||||
widths: number[],
|
||||
lockedTableWidth: number
|
||||
) => {
|
||||
editor.dom.setStyle(table, 'table-layout', 'fixed')
|
||||
editor.dom.setStyle(table, 'box-sizing', 'border-box')
|
||||
editor.dom.setStyle(table, 'width', `${Math.round(lockedTableWidth)}px`)
|
||||
editor.dom.setStyle(table, 'max-width', '100%')
|
||||
for (let i = 0; i < widths.length; i++) {
|
||||
setColWidth(table, i, widths[i])
|
||||
}
|
||||
syncColgroupPixelWidths(editor, table, widths)
|
||||
}
|
||||
|
||||
const canResizeRow = (table: HTMLTableElement, row: HTMLTableRowElement): boolean => {
|
||||
if (isGapSpacerRow(row)) return false
|
||||
// 合同/报价等普通表:任意行
|
||||
if (!tableHasProductGrid(table) && !isProductOnlyTable(table)) return true
|
||||
// 物料网格行(表头/明细)
|
||||
if (isProductGridRow(row)) return true
|
||||
// 独立物料表:备注、条款等合并行也允许拖行高(用户要求)
|
||||
if (isProductOnlyTable(table)) return true
|
||||
return false
|
||||
}
|
||||
|
||||
const ensureRowSpacer = (cell: HTMLTableCellElement, heightPx: number) => {
|
||||
const doc = cell.ownerDocument
|
||||
let spacer = cell.querySelector(`:scope > .${ROW_SPACER_CLASS}`) as HTMLElement | null
|
||||
if (!spacer) {
|
||||
spacer = doc.createElement('div')
|
||||
spacer.className = ROW_SPACER_CLASS
|
||||
spacer.setAttribute('contenteditable', 'false')
|
||||
spacer.setAttribute('data-mce-contenteditable', 'false')
|
||||
// 避免被清空:放一个零宽字符
|
||||
spacer.textContent = '\u200b'
|
||||
cell.insertBefore(spacer, cell.firstChild)
|
||||
}
|
||||
spacer.style.setProperty('height', `${heightPx}px`, 'important')
|
||||
spacer.style.setProperty('min-height', `${heightPx}px`, 'important')
|
||||
}
|
||||
|
||||
const setRowHeight = (table: HTMLTableElement, rowIndex: number, heightPx: number) => {
|
||||
const h = Math.max(MIN_ROW_PX, Math.round(heightPx))
|
||||
const row = table.rows[rowIndex]
|
||||
if (!row || !canResizeRow(table, row)) return
|
||||
|
||||
/**
|
||||
* 合同/报价里 TinyMCE 写 tr height 通常够用。
|
||||
* 单据表在部分浏览器 + border-collapse/fixed 下 tr/td height 会被忽略,
|
||||
* 因此用每个单元格内的 float 占位条强制撑高(对布局最稳)。
|
||||
*/
|
||||
row.style.setProperty('height', `${h}px`, 'important')
|
||||
row.setAttribute('height', String(h))
|
||||
|
||||
Array.from(row.cells).forEach((cell) => {
|
||||
ensureRowSpacer(cell, h)
|
||||
cell.style.setProperty('height', `${h}px`, 'important')
|
||||
cell.style.setProperty('min-height', `${h}px`, 'important')
|
||||
cell.setAttribute('height', String(h))
|
||||
})
|
||||
}
|
||||
|
||||
const clearHandles = () => {
|
||||
handlesRoot?.querySelectorAll(`.${HANDLE_CLASS}`).forEach((el) => el.remove())
|
||||
}
|
||||
|
||||
const ensureHandlesRoot = (body: HTMLElement) => {
|
||||
if (handlesRoot && handlesRoot.isConnected) {
|
||||
// setContent 后可能被挤到前面,保持置顶才能盖住表格命中
|
||||
if (handlesRoot.parentElement === body && body.lastElementChild !== handlesRoot) {
|
||||
body.appendChild(handlesRoot)
|
||||
}
|
||||
return handlesRoot
|
||||
}
|
||||
handlesRoot = body.ownerDocument.createElement('div')
|
||||
handlesRoot.id = 'sx-excel-resize-layer'
|
||||
// 铺满编辑区:父级 0×0 时部分浏览器子元素无法命中;层本身不拦截,仅把手可点
|
||||
handlesRoot.style.cssText =
|
||||
'position:absolute;left:0;top:0;right:0;bottom:0;width:auto;height:auto;overflow:visible;z-index:100000;pointer-events:none;'
|
||||
body.appendChild(handlesRoot)
|
||||
return handlesRoot
|
||||
}
|
||||
|
||||
const renderHandles = (table: HTMLTableElement, _preferProductGrid = false) => {
|
||||
const body = editor.getBody() as HTMLElement | null
|
||||
const doc = editor.getDoc() as Document | null
|
||||
if (!body || !doc || editor.mode?.isReadOnly?.() || editor.readonly) return
|
||||
|
||||
injectHandleStyles(doc)
|
||||
const root = ensureHandlesRoot(body)
|
||||
clearHandles()
|
||||
activeTable = table
|
||||
|
||||
const tableRect = table.getBoundingClientRect()
|
||||
if (tableRect.width < 8 || tableRect.height < 8) return
|
||||
|
||||
const hasGrid = tableHasProductGrid(table)
|
||||
// 物料表含备注/条款时:调整条只覆盖表头+明细网格;量不到时回退整表,避免功能“消失”
|
||||
const productRect = hasGrid ? getProductGridRect(table) : null
|
||||
const bandRect = productRect || tableRect
|
||||
const bandPos = relativeToBody(body, bandRect)
|
||||
const cols = columnCount(table)
|
||||
if (cols < 2 && table.rows.length < 1) return
|
||||
|
||||
// 只在列与列之间放竖条;单据表仅覆盖物料明细网格高度
|
||||
for (let i = 0; i < cols - 1; i++) {
|
||||
const cells = getColumnCells(table, i)
|
||||
const sample =
|
||||
cells.find((c) => isProductGridCell(c)) ||
|
||||
cells[0] ||
|
||||
table.rows[0]?.cells?.[i]
|
||||
if (!sample) continue
|
||||
const cr = sample.getBoundingClientRect()
|
||||
const pos = relativeToBody(body, cr)
|
||||
const handle = doc.createElement('div')
|
||||
handle.className = `${HANDLE_CLASS} ${HANDLE_COL}`
|
||||
handle.dataset.kind = 'col'
|
||||
handle.dataset.index = String(i)
|
||||
handle.dataset.neighbor = String(i + 1)
|
||||
handle.style.left = `${pos.left + cr.width - HANDLE_THICKNESS / 2}px`
|
||||
handle.style.top = `${bandPos.top}px`
|
||||
handle.style.width = `${HANDLE_THICKNESS}px`
|
||||
handle.style.height = `${bandRect.height}px`
|
||||
handle.style.pointerEvents = 'auto'
|
||||
root.appendChild(handle)
|
||||
}
|
||||
|
||||
// 行底边:物料网格行(或独立物料表的非合并行)
|
||||
Array.from(table.rows).forEach((row, rowIndex) => {
|
||||
if (!canResizeRow(table, row)) return
|
||||
const rr = row.getBoundingClientRect()
|
||||
const pos = relativeToBody(body, rr)
|
||||
const handle = doc.createElement('div')
|
||||
handle.className = `${HANDLE_CLASS} ${HANDLE_ROW}`
|
||||
handle.dataset.kind = 'row'
|
||||
handle.dataset.index = String(rowIndex)
|
||||
handle.style.left = `${bandPos.left}px`
|
||||
handle.style.top = `${pos.top + rr.height - HANDLE_ROW_THICKNESS / 2}px`
|
||||
handle.style.width = `${bandRect.width}px`
|
||||
handle.style.height = `${HANDLE_ROW_THICKNESS}px`
|
||||
handle.style.pointerEvents = 'auto'
|
||||
root.appendChild(handle)
|
||||
})
|
||||
}
|
||||
|
||||
const scheduleHideHandles = () => {
|
||||
if (hideTimer) window.clearTimeout(hideTimer)
|
||||
hideTimer = window.setTimeout(() => {
|
||||
if (!state) {
|
||||
clearHandles()
|
||||
activeTable = null
|
||||
}
|
||||
}, 250)
|
||||
}
|
||||
|
||||
const cancelHideHandles = () => {
|
||||
if (hideTimer) {
|
||||
window.clearTimeout(hideTimer)
|
||||
hideTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
const onHandleMouseDown = (e: MouseEvent) => {
|
||||
const target = e.target as HTMLElement | null
|
||||
if (!target?.classList?.contains(HANDLE_CLASS)) return
|
||||
if (!activeTable) return
|
||||
if (editor.mode?.isReadOnly?.() || editor.readonly) return
|
||||
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
cancelHideHandles()
|
||||
target.classList.add('is-dragging')
|
||||
|
||||
const kind = (target.dataset.kind || '') as ResizeKind
|
||||
const index = Number(target.dataset.index)
|
||||
if ((kind !== 'col' && kind !== 'row') || Number.isNaN(index)) return
|
||||
|
||||
editor.focus()
|
||||
|
||||
if (kind === 'col') {
|
||||
const cols = columnCount(activeTable)
|
||||
const neighborIndex = Number(target.dataset.neighbor)
|
||||
if (Number.isNaN(neighborIndex) || neighborIndex <= index || neighborIndex >= cols) return
|
||||
|
||||
const snap = snapshotTableVisualToPixels(editor, activeTable)
|
||||
if (!snap) return
|
||||
|
||||
state = {
|
||||
kind: 'col',
|
||||
table: activeTable,
|
||||
index,
|
||||
neighborIndex,
|
||||
startPos: e.clientX,
|
||||
startSizes: snap.widths,
|
||||
lockedTableWidth: snap.tableWidth
|
||||
}
|
||||
} else {
|
||||
const row = activeTable.rows[index]
|
||||
if (!row || !canResizeRow(activeTable, row)) return
|
||||
// 注意:行拖不要走列宽快照(会改写单元格 style,易冲掉行高)
|
||||
state = {
|
||||
kind: 'row',
|
||||
table: activeTable,
|
||||
index,
|
||||
startPos: e.clientY,
|
||||
startSizes: [measureRowHeight(row)]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const onDocMouseMove = (e: MouseEvent) => {
|
||||
if (!state) return
|
||||
e.preventDefault()
|
||||
if (state.kind === 'col') {
|
||||
const neighborIndex = state.neighborIndex
|
||||
const locked = state.lockedTableWidth
|
||||
if (neighborIndex == null || locked == null) return
|
||||
|
||||
const delta = e.clientX - state.startPos
|
||||
const leftStart = state.startSizes[state.index]
|
||||
const rightStart = state.startSizes[neighborIndex]
|
||||
const maxGrow = rightStart - MIN_COL_PX
|
||||
const maxShrink = leftStart - MIN_COL_PX
|
||||
const appliedDelta = Math.min(Math.max(delta, -maxShrink), maxGrow)
|
||||
const leftW = leftStart + appliedDelta
|
||||
const rightW = rightStart - appliedDelta
|
||||
|
||||
const widths = state.startSizes.slice()
|
||||
widths[state.index] = leftW
|
||||
widths[neighborIndex] = rightW
|
||||
applyColumnWidthsLocked(state.table, widths, locked)
|
||||
} else {
|
||||
const delta = e.clientY - state.startPos
|
||||
setRowHeight(state.table, state.index, state.startSizes[0] + delta)
|
||||
}
|
||||
}
|
||||
|
||||
const onDocMouseUp = () => {
|
||||
if (!state) return
|
||||
const table = state.table
|
||||
state = null
|
||||
handlesRoot?.querySelectorAll('.is-dragging').forEach((el) => el.classList.remove('is-dragging'))
|
||||
renderHandles(table, tableHasProductGrid(table))
|
||||
editor.nodeChanged()
|
||||
editor.undoManager?.add?.()
|
||||
editor.fire('change')
|
||||
}
|
||||
|
||||
const onEditorMouseMove = (e: MouseEvent) => {
|
||||
if (state) return
|
||||
const t = e.target as Node | null
|
||||
if (t && (t as HTMLElement).classList?.contains?.(HANDLE_CLASS)) {
|
||||
cancelHideHandles()
|
||||
return
|
||||
}
|
||||
const el = t instanceof Element ? t : t?.parentElement
|
||||
const table = resolveResizeTargetTable(el || null)
|
||||
if (table && editor.getBody()?.contains(table) && !isExcelResizeExcludedTable(table)) {
|
||||
const cls =
|
||||
typeof table.className === 'string' ? table.className : String(table.className || '')
|
||||
if (/slip-(info|footer|sign)-table/i.test(cls)) {
|
||||
scheduleHideHandles()
|
||||
return
|
||||
}
|
||||
// 独立物料表:悬停表内任意处(含备注/条款)都可出调整条,但条只画在明细网格上
|
||||
if (isProductOnlyTable(table)) {
|
||||
cancelHideHandles()
|
||||
if (activeTable !== table || !handlesRoot?.querySelector(`.${HANDLE_CLASS}`)) {
|
||||
renderHandles(table, true)
|
||||
}
|
||||
return
|
||||
}
|
||||
if (tableHasProductGrid(table) && !isProductGridCell(el)) {
|
||||
scheduleHideHandles()
|
||||
return
|
||||
}
|
||||
cancelHideHandles()
|
||||
if (activeTable !== table || !handlesRoot?.querySelector(`.${HANDLE_CLASS}`)) {
|
||||
renderHandles(table, true)
|
||||
}
|
||||
} else {
|
||||
scheduleHideHandles()
|
||||
}
|
||||
}
|
||||
|
||||
const onBodyMouseMove = (e: MouseEvent) => {
|
||||
onEditorMouseMove(e)
|
||||
}
|
||||
|
||||
const bind = () => {
|
||||
const body = editor.getBody() as HTMLElement | null
|
||||
const doc = editor.getDoc() as Document | null
|
||||
if (!body || !doc) return
|
||||
if (body.getAttribute('data-sx-excel-resize') === '1') {
|
||||
ensureHandlesRoot(body)
|
||||
return
|
||||
}
|
||||
|
||||
injectHandleStyles(doc)
|
||||
ensureHandlesRoot(body)
|
||||
|
||||
// 捕获阶段:保证点到调整条 / 悬停检测不依赖 TinyMCE 事件封装
|
||||
body.addEventListener('mousedown', onHandleMouseDown, true)
|
||||
body.addEventListener('mousemove', onBodyMouseMove, true)
|
||||
doc.addEventListener('mousemove', onDocMouseMove, true)
|
||||
doc.addEventListener('mouseup', onDocMouseUp, true)
|
||||
|
||||
body.setAttribute('data-sx-excel-resize', '1')
|
||||
}
|
||||
|
||||
const unbind = () => {
|
||||
const body = editor.getBody?.() as HTMLElement | null
|
||||
const doc = editor.getDoc?.() as Document | null
|
||||
body?.removeEventListener('mousedown', onHandleMouseDown, true)
|
||||
body?.removeEventListener('mousemove', onBodyMouseMove, true)
|
||||
doc?.removeEventListener('mousemove', onDocMouseMove, true)
|
||||
doc?.removeEventListener('mouseup', onDocMouseUp, true)
|
||||
body?.removeAttribute('data-sx-excel-resize')
|
||||
clearHandles()
|
||||
handlesRoot?.remove()
|
||||
handlesRoot = null
|
||||
activeTable = null
|
||||
}
|
||||
|
||||
editor.on('init', () => {
|
||||
// 下一帧再绑,确保 iframe body 可交互
|
||||
requestAnimationFrame(() => bind())
|
||||
})
|
||||
// 载入/切换单据 HTML 后重挂调整层(setContent 会清掉 body 子节点)
|
||||
editor.on('SetContent', () => {
|
||||
requestAnimationFrame(() => {
|
||||
handlesRoot = null
|
||||
const body = editor.getBody() as HTMLElement | null
|
||||
if (body?.getAttribute('data-sx-excel-resize') !== '1') {
|
||||
bind()
|
||||
} else {
|
||||
ensureHandlesRoot(body)
|
||||
}
|
||||
})
|
||||
})
|
||||
editor.on('ScrollContent ScrollWindow', () => {
|
||||
if (activeTable && !state) renderHandles(activeTable, tableHasProductGrid(activeTable))
|
||||
})
|
||||
editor.on('remove', unbind)
|
||||
}
|
||||
|
||||
const parseCssLengthPx = (raw: string | null | undefined): number | null => {
|
||||
if (!raw) return null
|
||||
const s = String(raw).trim()
|
||||
if (!s) return null
|
||||
if (s.endsWith('%')) return null
|
||||
const px = s.match(/^([\d.]+)px$/i)
|
||||
if (px) return parseFloat(px[1])
|
||||
const num = s.match(/^([\d.]+)$/)
|
||||
if (num) return parseFloat(num[1])
|
||||
return null
|
||||
}
|
||||
|
||||
/** 读取声明列宽(col / 单元格 style|attr),避免被 max-width:100% 的视觉宽度误导 */
|
||||
const getDeclaredColWidthPx = (table: HTMLTableElement, colIndex: number): number | null => {
|
||||
const cols = table.querySelectorAll('colgroup col')
|
||||
const col = cols[colIndex] as HTMLElement | undefined
|
||||
if (col) {
|
||||
const fromCol =
|
||||
parseCssLengthPx(col.style.width) ?? parseCssLengthPx(col.getAttribute('width'))
|
||||
if (fromCol && fromCol > 0) return fromCol
|
||||
}
|
||||
|
||||
const row = table.rows[0]
|
||||
if (!row) return null
|
||||
let idx = 0
|
||||
for (const cell of Array.from(row.cells)) {
|
||||
const span = cell.colSpan || 1
|
||||
if (colIndex >= idx && colIndex < idx + span) {
|
||||
const fromStyle =
|
||||
parseCssLengthPx(cell.style.width) ?? parseCssLengthPx(cell.getAttribute('width'))
|
||||
if (fromStyle && fromStyle > 0) return span > 1 ? fromStyle / span : fromStyle
|
||||
return null
|
||||
}
|
||||
idx += span
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 将富文本内表格强制适配到 maxWidthPx(用于 PDF/打印容器)。
|
||||
*
|
||||
* 根因:编辑器 content_style 有 max-width:100%,拖宽后视觉已钳制,但 HTML 仍保留更大的
|
||||
* 单元格/col 像素宽。PDF 临时 DOM 无该样式时,声明宽会撑破 A4,html2canvas 裁切右侧。
|
||||
* 因此必须以「声明列宽之和」为准重写,不能只看 getBoundingClientRect。
|
||||
* 仅改临时 DOM,不回写业务 HTML。
|
||||
*/
|
||||
export function fitRichTextTablesToMaxWidth(root: HTMLElement, maxWidthPx: number): void {
|
||||
if (!root || maxWidthPx <= 0) return
|
||||
|
||||
const targetW = Math.floor(maxWidthPx)
|
||||
|
||||
// 外层模版常带 max-width:794 + padding,叠在 PDF 容器 padding 上会再挤窄;统一吃满内容区
|
||||
root.querySelectorAll<HTMLElement>('[style*="max-width"]').forEach((el) => {
|
||||
if (el.tagName === 'TABLE' || el.tagName === 'IMG') return
|
||||
el.style.maxWidth = '100%'
|
||||
el.style.width = '100%'
|
||||
el.style.boxSizing = 'border-box'
|
||||
})
|
||||
|
||||
root.querySelectorAll('table').forEach((tableEl) => {
|
||||
const table = tableEl as HTMLTableElement
|
||||
Array.from(table.querySelectorAll('td, th')).forEach((cell) => {
|
||||
const el = cell as HTMLTableCellElement
|
||||
el.style.boxSizing = 'border-box'
|
||||
el.style.minWidth = '0'
|
||||
el.removeAttribute('width')
|
||||
el.style.wordBreak = 'break-word'
|
||||
el.style.overflowWrap = 'anywhere'
|
||||
})
|
||||
table.removeAttribute('width')
|
||||
|
||||
const cols = columnCount(table)
|
||||
if (cols <= 0) {
|
||||
table.style.maxWidth = '100%'
|
||||
table.style.width = '100%'
|
||||
table.style.tableLayout = 'fixed'
|
||||
table.style.boxSizing = 'border-box'
|
||||
return
|
||||
}
|
||||
|
||||
const raw: number[] = []
|
||||
for (let i = 0; i < cols; i++) {
|
||||
const declared = getDeclaredColWidthPx(table, i)
|
||||
if (declared && declared > 0) {
|
||||
raw.push(Math.max(MIN_COL_PX, Math.round(declared)))
|
||||
} else {
|
||||
raw.push(Math.max(MIN_COL_PX, Math.round(measureColWidth(table, i))))
|
||||
}
|
||||
}
|
||||
|
||||
const rawSum = raw.reduce((a, b) => a + b, 0) || cols
|
||||
const tableDeclared =
|
||||
parseCssLengthPx(table.style.width) ?? parseCssLengthPx(table.getAttribute('width'))
|
||||
// 任一声明总宽超过目标,或列宽之和超过目标,都必须重写(不能因视觉已钳制而跳过)
|
||||
const needsFit = (tableDeclared != null && tableDeclared > targetW + 1) || rawSum > targetW + 1
|
||||
|
||||
table.style.maxWidth = '100%'
|
||||
table.style.width = '100%'
|
||||
table.style.tableLayout = 'fixed'
|
||||
table.style.boxSizing = 'border-box'
|
||||
|
||||
if (!needsFit && rawSum <= targetW + 1) {
|
||||
// 声明已在范围内:仍统一成百分比,避免 PDF 环境差异
|
||||
let usedPct = 0
|
||||
for (let i = 0; i < cols; i++) {
|
||||
const pct =
|
||||
i === cols - 1
|
||||
? Math.max(1, 100 - usedPct)
|
||||
: Math.max(1, Math.round((raw[i] / rawSum) * 100))
|
||||
if (i < cols - 1) usedPct += pct
|
||||
const pctStr = `${pct}%`
|
||||
getColumnCells(table, i).forEach((cell) => {
|
||||
cell.style.width = pctStr
|
||||
})
|
||||
const head = table.rows[0]
|
||||
if (head) {
|
||||
let idx = 0
|
||||
for (const cell of Array.from(head.cells)) {
|
||||
const span = cell.colSpan || 1
|
||||
if (i >= idx && i < idx + span && span === 1) {
|
||||
cell.style.width = pctStr
|
||||
break
|
||||
}
|
||||
idx += span
|
||||
}
|
||||
}
|
||||
const colEl = table.querySelectorAll('colgroup col')[i] as HTMLElement | undefined
|
||||
if (colEl) {
|
||||
colEl.style.width = pctStr
|
||||
colEl.removeAttribute('width')
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let used = 0
|
||||
const widths = raw.map((w, i) => {
|
||||
if (i === cols - 1) return Math.max(MIN_COL_PX, targetW - used)
|
||||
const next = Math.max(MIN_COL_PX, Math.round((w / rawSum) * targetW))
|
||||
used += next
|
||||
return next
|
||||
})
|
||||
|
||||
let usedPct = 0
|
||||
for (let i = 0; i < cols; i++) {
|
||||
const pct =
|
||||
i === cols - 1
|
||||
? Math.max(1, 100 - usedPct)
|
||||
: Math.max(1, Math.round((widths[i] / targetW) * 100))
|
||||
if (i < cols - 1) usedPct += pct
|
||||
const pctStr = `${pct}%`
|
||||
getColumnCells(table, i).forEach((cell) => {
|
||||
cell.style.width = pctStr
|
||||
})
|
||||
const head = table.rows[0]
|
||||
if (head) {
|
||||
let idx = 0
|
||||
for (const cell of Array.from(head.cells)) {
|
||||
const span = cell.colSpan || 1
|
||||
if (i >= idx && i < idx + span && span === 1) {
|
||||
cell.style.width = pctStr
|
||||
break
|
||||
}
|
||||
idx += span
|
||||
}
|
||||
}
|
||||
const colEl = table.querySelectorAll('colgroup col')[i] as HTMLElement | undefined
|
||||
if (colEl) {
|
||||
colEl.style.width = pctStr
|
||||
colEl.removeAttribute('width')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user