feat(web-admin): add contract HTML prep helpers for Word export
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
e2b676121a
commit
17b42dc06c
2 files changed
+152
No files matched your search
@@ -0,0 +1,88 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { describe, it } from 'node:test'
|
||||
import {
|
||||
buildContractDocxFileName,
|
||||
injectSxWatermarkHtml,
|
||||
parseContractSupplierCode,
|
||||
prepareContractHtmlForDocx,
|
||||
stripContractLogoHtml,
|
||||
toAbsoluteAssetUrls
|
||||
} from './contract-html-to-docx'
|
||||
|
||||
describe('parseContractSupplierCode', () => {
|
||||
it('parses supplier code from contractNo', () => {
|
||||
assert.equal(parseContractSupplierCode('SX/X-JT20250926002'), 'SX')
|
||||
assert.equal(parseContractSupplierCode('YD/X-JT20250926001'), 'YD')
|
||||
})
|
||||
|
||||
it('defaults to SX when missing or unmatched', () => {
|
||||
assert.equal(parseContractSupplierCode(undefined), 'SX')
|
||||
assert.equal(parseContractSupplierCode(''), 'SX')
|
||||
assert.equal(parseContractSupplierCode('NO-MATCH'), 'SX')
|
||||
})
|
||||
})
|
||||
|
||||
describe('stripContractLogoHtml', () => {
|
||||
it('removes logo img and logo-only wrapper div', () => {
|
||||
const html =
|
||||
'<div><img src="/logo.gif" alt="logo" /></div><p>正文</p><img src="/watermask.jpg" alt="水印" />'
|
||||
const out = stripContractLogoHtml(html)
|
||||
assert.equal(out.includes('logo.gif'), false)
|
||||
assert.equal(out.includes('正文'), true)
|
||||
assert.equal(out.includes('watermask.jpg'), true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('toAbsoluteAssetUrls', () => {
|
||||
it('prefixes root-relative src with origin', () => {
|
||||
const out = toAbsoluteAssetUrls(
|
||||
'<img src="/seals/a.png" /><img src="https://cdn.example/x.png" />',
|
||||
'http://localhost:3006'
|
||||
)
|
||||
assert.equal(out.includes('src="http://localhost:3006/seals/a.png"'), true)
|
||||
assert.equal(out.includes('src="https://cdn.example/x.png"'), true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('injectSxWatermarkHtml', () => {
|
||||
it('prepends watermark image with absolute url', () => {
|
||||
const out = injectSxWatermarkHtml('<p>合同</p>', 'http://localhost:3006/watermask.jpg')
|
||||
assert.equal(out.includes('http://localhost:3006/watermask.jpg'), true)
|
||||
assert.equal(out.includes('alt="水印"'), true)
|
||||
assert.equal(out.includes('<p>合同</p>'), true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('prepareContractHtmlForDocx', () => {
|
||||
it('adds SX watermark and keeps logo', () => {
|
||||
const out = prepareContractHtmlForDocx({
|
||||
html: '<div><img src="/logo.gif" /></div><p>正文</p>',
|
||||
contractNo: 'SX/X-JT20250926002',
|
||||
origin: 'http://localhost:3006'
|
||||
})
|
||||
assert.equal(out.includes('watermask.jpg'), true)
|
||||
assert.equal(out.includes('logo.gif'), true)
|
||||
assert.equal(out.includes('http://localhost:3006/logo.gif'), true)
|
||||
})
|
||||
|
||||
it('strips logo and skips watermark for non-SX', () => {
|
||||
const out = prepareContractHtmlForDocx({
|
||||
html: '<div><img src="/logo.gif" /></div><p>正文</p>',
|
||||
contractNo: 'YD/X-JT20250926001',
|
||||
origin: 'http://localhost:3006'
|
||||
})
|
||||
assert.equal(out.includes('watermask'), false)
|
||||
assert.equal(out.includes('logo.gif'), false)
|
||||
assert.equal(out.includes('正文'), true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildContractDocxFileName', () => {
|
||||
it('uses contract name and sanitizes illegal chars', () => {
|
||||
assert.equal(buildContractDocxFileName('甲/乙合同', 'SX/X-JT1'), '甲-乙合同合同.docx')
|
||||
})
|
||||
|
||||
it('falls back to contractNo', () => {
|
||||
assert.equal(buildContractDocxFileName('', 'SX/X-JT1'), 'SX-X-JT1合同.docx')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,64 @@
|
||||
/** 合同打印弹窗:HTML → DOCX 预处理与生成 */
|
||||
|
||||
export function parseContractSupplierCode(contractNo?: string | null): string {
|
||||
if (!contractNo) return 'SX'
|
||||
const match = String(contractNo).match(/^([A-Z]+)\/X-JT/)
|
||||
return match?.[1] || 'SX'
|
||||
}
|
||||
|
||||
export function stripContractLogoHtml(html: string): string {
|
||||
let result = html.replace(/<div[^>]*>\s*<img[^>]*(?:logo\.gif|logo)[^>]*>[\s\S]*?<\/div>/gi, (match) => {
|
||||
if (match.includes('logo.gif') || match.includes('/logo') || (match.includes('logo') && !match.includes('watermask'))) {
|
||||
return ''
|
||||
}
|
||||
return match
|
||||
})
|
||||
result = result.replace(/<img[^>]*(?:logo\.gif|logo)[^>]*(?![^<]*watermask)[^>]*>/gi, '')
|
||||
return result
|
||||
}
|
||||
|
||||
export function toAbsoluteAssetUrls(html: string, origin: string): string {
|
||||
const base = origin.replace(/\/$/, '')
|
||||
return html.replace(/\bsrc=(["'])(\/[^"']+)\1/gi, (_m, quote: string, path: string) => {
|
||||
return `src=${quote}${base}${path}${quote}`
|
||||
})
|
||||
}
|
||||
|
||||
export function injectSxWatermarkHtml(html: string, watermarkUrl: string): string {
|
||||
const watermarkBlock = `<div style="text-align:center;margin:8px 0 16px;">
|
||||
<img src="${watermarkUrl}" alt="水印" style="width:50%;max-width:400px;opacity:0.15;" />
|
||||
</div>`
|
||||
return `${watermarkBlock}${html}`
|
||||
}
|
||||
|
||||
export function prepareContractHtmlForDocx(options: {
|
||||
html: string
|
||||
contractNo?: string | null
|
||||
origin: string
|
||||
}): string {
|
||||
const origin = options.origin.replace(/\/$/, '')
|
||||
const supplierCode = parseContractSupplierCode(options.contractNo)
|
||||
const isSX = supplierCode === 'SX'
|
||||
|
||||
let html = options.html || ''
|
||||
if (!isSX) {
|
||||
html = stripContractLogoHtml(html)
|
||||
}
|
||||
|
||||
html = toAbsoluteAssetUrls(html, origin)
|
||||
|
||||
if (isSX) {
|
||||
html = injectSxWatermarkHtml(html, `${origin}/watermask.jpg`)
|
||||
}
|
||||
|
||||
return html
|
||||
}
|
||||
|
||||
export function buildContractDocxFileName(
|
||||
contractName?: string | null,
|
||||
contractNo?: string | null
|
||||
): string {
|
||||
const raw = (contractName && String(contractName).trim()) || (contractNo && String(contractNo).trim()) || '合同'
|
||||
const safe = raw.replace(/[/\\:*?"<>|]/g, '-')
|
||||
return `${safe}合同.docx`
|
||||
}
|
||||
Reference in new issue
Block a user