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 <cursoragent@cursor.com>
This commit is contained in:
1 parent
da2f36af7f
commit
0dc7ce0de8
2 files changed
+43
-10
No files matched your search
@@ -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 = `
|
||||
<div style="display: flex; margin-bottom: 0; position: relative;">
|
||||
<div style="flex: 1;">供方:安庆市山峡化工有限公司</div>
|
||||
@@ -70,8 +71,8 @@ describe('normalizeHeaderMetaFlexLayoutForDocx', () => {
|
||||
<div style="position: absolute; right: 0px; min-width: 300px;">签订地点:安徽安庆</div>
|
||||
</div>`
|
||||
const out = normalizeHeaderMetaFlexLayoutForDocx(html)
|
||||
assert.equal((out.match(/<table/g) || []).length, 2)
|
||||
assert.equal(out.includes('text-align:right'), true)
|
||||
assert.equal(out.includes('<table'), false)
|
||||
assert.equal(out.includes('float:right'), true)
|
||||
assert.equal(out.includes('position: absolute'), false)
|
||||
assert.equal(out.includes('供方:安庆市山峡化工有限公司'), true)
|
||||
assert.equal(out.includes('合同编号:SX/X-JT20260828004'), true)
|
||||
@@ -91,6 +92,17 @@ describe('normalizeHeaderMetaFlexLayoutForDocx', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('normalizeSealImagesForDocx', () => {
|
||||
it('rewrites absolute seal images to float-over-text styles', () => {
|
||||
const html =
|
||||
'<img src="/yz.png" alt="印章" style="position: absolute; top: 50%; left: 50%; width: 140px; height: 140px;" />'
|
||||
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', () => {
|
||||
<div style="flex: 1;">供方:甲</div>
|
||||
<div style="position: absolute; right: 0px;">合同编号:NO1</div>
|
||||
</div>
|
||||
<img src="/yz.png" alt="印章" style="position: absolute; top: 50%; left: 50%;" />
|
||||
<div style="display: flex; justify-content: space-between; margin-top: 10px;">
|
||||
<div style="flex: 1;"><div>供方</div><div>A</div></div>
|
||||
<div style="flex: 1;"><div>需方</div><div>B</div></div>
|
||||
<div style="flex: 1;"><div style="border-bottom: 1px solid #000;">供方</div><div>A</div></div>
|
||||
<div style="flex: 1;"><div style="border-bottom: 1px solid #000;">需方</div><div>B</div></div>
|
||||
</div>`
|
||||
const out = prepareContractHtmlForDocx({
|
||||
html,
|
||||
contractNo: 'SX/X-JT1',
|
||||
origin: 'http://localhost:3006'
|
||||
})
|
||||
assert.equal((out.match(/<table/g) || []).length >= 2, true)
|
||||
assert.equal(out.includes('float:right'), true)
|
||||
assert.equal(out.includes('<table'), true)
|
||||
assert.equal(out.includes('float:left'), true)
|
||||
assert.equal(/position:\s*absolute/.test(out), false)
|
||||
assert.equal(/justify-content:\s*space-between/.test(out), false)
|
||||
assert.equal(out.includes('border-bottom: 1px solid #000'), true)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -216,7 +216,7 @@ function collectTopLevelDivChildren(
|
||||
|
||||
/**
|
||||
* 顶部元信息(供方/合同编号、需方/签订地点)在预览里用 flex + absolute 右对齐;
|
||||
* Word 不认,导出前改为无边框双列表格(左左文、右右对齐)。
|
||||
* Word 不认且表格易出线框,导出前改为段落 + float:right(纯文本左右排布)。
|
||||
*/
|
||||
export function normalizeHeaderMetaFlexLayoutForDocx(html: string): string {
|
||||
const openRe = /<div\b(?=[^>]*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 = `<table style="width:100%;border-collapse:collapse;margin:0;table-layout:fixed;font-size:12px;line-height:1.5;"><tbody><tr><td style="width:50%;vertical-align:top;border:none;padding:0;">${left.inner}</td><td style="width:50%;vertical-align:top;border:none;padding:0;text-align:right;">${right.inner}</td></tr></tbody></table>`
|
||||
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 = `<p style="margin:0;font-size:12px;line-height:1.5;"><span>${leftText}</span><span style="float:right;">${rightText}</span></p>`
|
||||
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(/<img\b([^>]*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 `<img${rest} style="${style}">`
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 合同底部供方/需方在预览里是 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 = `<table style="width:100%;border-collapse:collapse;margin-top:10px;table-layout:fixed;"><tbody><tr><td style="width:50%;vertical-align:top;border:1px solid #000;padding:8px;line-height:1.3;font-size:12px;">${children[0].inner}</td><td style="width:50%;vertical-align:top;border:1px solid #000;padding:8px;line-height:1.3;font-size:12px;">${children[1].inner}</td></tr></tbody></table>`
|
||||
const table = `<table style="width:100%;border-collapse:collapse;margin-top:10px;table-layout:fixed;"><tbody><tr><td style="width:50%;vertical-align:top;border:1px solid #000;padding:8px;line-height:1.3;font-size:12px;position:relative;">${children[0].inner}</td><td style="width:50%;vertical-align:top;border:1px solid #000;padding:8px;line-height:1.3;font-size:12px;position:relative;">${children[1].inner}</td></tr></tbody></table>`
|
||||
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)
|
||||
|
||||
|
||||
Reference in new issue
Block a user