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
+
`
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)