From 0dc7ce0de893850beaa0dd897edd6ec5971b806c Mon Sep 17 00:00:00 2001 From: zlwu Date: Sat, 5 Sep 2026 15:33:47 +0800 Subject: [PATCH] fix(web-admin): polish Word header layout and floating seals Use float text rows for header meta without table borders; rewrite seal images to float over party fields while keeping title underlines. Co-authored-by: Cursor --- .../utils/crm/contract-html-to-docx.test.ts | 28 +++++++++++++++---- .../src/utils/crm/contract-html-to-docx.ts | 25 ++++++++++++++--- 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/web-admin/src/utils/crm/contract-html-to-docx.test.ts b/web-admin/src/utils/crm/contract-html-to-docx.test.ts index ca20cd182..fff917f59 100644 --- a/web-admin/src/utils/crm/contract-html-to-docx.test.ts +++ b/web-admin/src/utils/crm/contract-html-to-docx.test.ts @@ -5,6 +5,7 @@ import { inlineRemoteImagesAsDataUrls, normalizeHeaderMetaFlexLayoutForDocx, normalizePartyFlexLayoutForDocx, + normalizeSealImagesForDocx, parseContractSupplierCode, prepareContractHtmlForDocx, stripContractLogoHtml, @@ -59,7 +60,7 @@ describe('toAbsoluteAssetUrls', () => { }) describe('normalizeHeaderMetaFlexLayoutForDocx', () => { - it('converts supplier/contractNo and demander/signPlace rows into side-by-side tables', () => { + it('converts supplier/contractNo and demander/signPlace rows into float text rows', () => { const html = `
供方:安庆市山峡化工有限公司
@@ -70,8 +71,8 @@ describe('normalizeHeaderMetaFlexLayoutForDocx', () => {
签订地点:安徽安庆
` const out = normalizeHeaderMetaFlexLayoutForDocx(html) - assert.equal((out.match(/ { }) }) +describe('normalizeSealImagesForDocx', () => { + it('rewrites absolute seal images to float-over-text styles', () => { + const html = + '印章' + const out = normalizeSealImagesForDocx(html) + assert.equal(out.includes('position: absolute'), false) + assert.equal(out.includes('float:left'), true) + assert.equal(out.includes('alt="印章"'), true) + }) +}) + describe('normalizePartyFlexLayoutForDocx', () => { it('converts bottom party flex columns into a two-cell table', () => { const html = ` @@ -144,18 +156,22 @@ describe('prepareContractHtmlForDocx', () => {
供方:甲
合同编号:NO1
+ 印章
-
供方
A
-
需方
B
+
供方
A
+
需方
B
` const out = prepareContractHtmlForDocx({ html, contractNo: 'SX/X-JT1', origin: 'http://localhost:3006' }) - assert.equal((out.match(/
= 2, true) + assert.equal(out.includes('float:right'), true) + assert.equal(out.includes(']*style\s*=\s*["'][^"']*display:\s*flex)[^>]*>/gi @@ -247,13 +247,29 @@ export function normalizeHeaderMetaFlexLayoutForDocx(html: string): string { // 避免误伤底部签署区(含「单位名称」等字段) if (left.inner.includes('单位名称') || right.inner.includes('单位名称')) continue - const table = `
${left.inner}${right.inner}
` - out = out.slice(0, index) + table + out.slice(flex.end) + const leftText = left.inner.replace(/<[^>]+>/g, '').trim() + const rightText = right.inner.replace(/<[^>]+>/g, '').trim() + const row = `

${leftText}${rightText}

` + out = out.slice(0, index) + row + out.slice(flex.end) } return out } +/** + * 印章在预览为 absolute 居中叠字;Word 不认 absolute,改为 float + 负 margin 浮于后续文字上方。 + * 供方/需方标题下的横向分割线(border-bottom)保持不变。 + */ +export function normalizeSealImagesForDocx(html: string): string { + return html.replace(/]*alt\s*=\s*["']印章["'][^>]*)>/gi, (_full, attrs: string) => { + let rest = String(attrs).replace(/\s*style\s*=\s*(["'])[\s\S]*?\1/i, '') + rest = rest.replace(/\s*style\s*=\s*{[^}]*}/i, '') + const style = + 'float:left;width:140px;height:140px;margin:4px 12px -120px 24px;opacity:0.85;' + return `` + }) +} + /** * 合同底部供方/需方在预览里是 flex 左右栏;Word 不认 flex,导出前改为双列表格。 */ @@ -276,7 +292,7 @@ export function normalizePartyFlexLayoutForDocx(html: string): string { if (children.length < 2) continue if (!children[0].inner.includes('供方') || !children[1].inner.includes('需方')) continue - const table = `
${children[0].inner}${children[1].inner}
` + const table = `
${children[0].inner}${children[1].inner}
` out = out.slice(0, index) + table + out.slice(flex.end) } @@ -300,6 +316,7 @@ export function prepareContractHtmlForDocx(options: { } html = toAbsoluteAssetUrls(html, origin) + html = normalizeSealImagesForDocx(html) html = normalizeHeaderMetaFlexLayoutForDocx(html) html = normalizePartyFlexLayoutForDocx(html)