|
|
@@ -158,12 +158,12 @@ export function toAbsoluteAssetUrls(html: string, origin: string): string {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 从 openIdx 处的 `<div` 起做括号匹配,返回整段与 innerHTML。
|
|
|
+ * 从 openIdx 处的 `<div` 起做括号匹配,返回整段、开标签与 innerHTML。
|
|
|
*/
|
|
|
function takeBalancedDiv(
|
|
|
html: string,
|
|
|
openIdx: number
|
|
|
-): { full: string; inner: string; end: number } | null {
|
|
|
+): { full: string; openTag: string; inner: string; end: number } | null {
|
|
|
const slice = html.slice(openIdx)
|
|
|
if (!/^<div\b/i.test(slice)) return null
|
|
|
const openTagEnd = html.indexOf('>', openIdx)
|
|
|
@@ -186,6 +186,7 @@ function takeBalancedDiv(
|
|
|
const end = nextClose + 6
|
|
|
return {
|
|
|
full: html.slice(openIdx, end),
|
|
|
+ openTag: html.slice(openIdx, openTagEnd + 1),
|
|
|
inner: html.slice(openTagEnd + 1, nextClose),
|
|
|
end
|
|
|
}
|
|
|
@@ -196,6 +197,63 @@ function takeBalancedDiv(
|
|
|
return null
|
|
|
}
|
|
|
|
|
|
+function collectTopLevelDivChildren(
|
|
|
+ innerHtml: string
|
|
|
+): Array<{ openTag: string; inner: string }> {
|
|
|
+ const children: Array<{ openTag: string; inner: string }> = []
|
|
|
+ let pos = 0
|
|
|
+ const lower = innerHtml.toLowerCase()
|
|
|
+ while (pos < innerHtml.length) {
|
|
|
+ const divIdx = lower.indexOf('<div', pos)
|
|
|
+ if (divIdx < 0) break
|
|
|
+ const child = takeBalancedDiv(innerHtml, divIdx)
|
|
|
+ if (!child) break
|
|
|
+ children.push({ openTag: child.openTag, inner: child.inner })
|
|
|
+ pos = child.end
|
|
|
+ }
|
|
|
+ return children
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 顶部元信息(供方/合同编号、需方/签订地点)在预览里用 flex + absolute 右对齐;
|
|
|
+ * Word 不认,导出前改为无边框双列表格(左左文、右右对齐)。
|
|
|
+ */
|
|
|
+export function normalizeHeaderMetaFlexLayoutForDocx(html: string): string {
|
|
|
+ const openRe = /<div\b(?=[^>]*style\s*=\s*["'][^"']*display:\s*flex)[^>]*>/gi
|
|
|
+ const opens: number[] = []
|
|
|
+ let m: RegExpExecArray | null
|
|
|
+ while ((m = openRe.exec(html)) !== null) {
|
|
|
+ opens.push(m.index)
|
|
|
+ }
|
|
|
+
|
|
|
+ let out = html
|
|
|
+ for (let oi = opens.length - 1; oi >= 0; oi -= 1) {
|
|
|
+ const index = opens[oi]
|
|
|
+ const flex = takeBalancedDiv(out, index)
|
|
|
+ if (!flex) continue
|
|
|
+ // 底部供需方双栏用 space-between,交给 normalizePartyFlexLayoutForDocx
|
|
|
+ if (/justify-content:\s*space-between/i.test(flex.openTag)) continue
|
|
|
+
|
|
|
+ const children = collectTopLevelDivChildren(flex.inner)
|
|
|
+ if (children.length !== 2) continue
|
|
|
+
|
|
|
+ const [left, right] = children
|
|
|
+ const absoluteRight =
|
|
|
+ /position:\s*absolute/i.test(right.openTag) && /right:\s*/i.test(right.openTag)
|
|
|
+ const labelPair =
|
|
|
+ (left.inner.includes('供方') && right.inner.includes('合同编号')) ||
|
|
|
+ (left.inner.includes('需方') && right.inner.includes('签订地点'))
|
|
|
+ if (!absoluteRight && !labelPair) continue
|
|
|
+ // 避免误伤底部签署区(含「单位名称」等字段)
|
|
|
+ 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)
|
|
|
+ }
|
|
|
+
|
|
|
+ return out
|
|
|
+}
|
|
|
+
|
|
|
/**
|
|
|
* 合同底部供方/需方在预览里是 flex 左右栏;Word 不认 flex,导出前改为双列表格。
|
|
|
*/
|
|
|
@@ -214,22 +272,11 @@ export function normalizePartyFlexLayoutForDocx(html: string): string {
|
|
|
const flex = takeBalancedDiv(out, index)
|
|
|
if (!flex) continue
|
|
|
|
|
|
- const children: string[] = []
|
|
|
- let pos = 0
|
|
|
- const innerLower = flex.inner.toLowerCase()
|
|
|
- while (pos < flex.inner.length && children.length < 2) {
|
|
|
- const divIdx = innerLower.indexOf('<div', pos)
|
|
|
- if (divIdx < 0) break
|
|
|
- const child = takeBalancedDiv(flex.inner, divIdx)
|
|
|
- if (!child) break
|
|
|
- children.push(child.inner)
|
|
|
- pos = child.end
|
|
|
- }
|
|
|
-
|
|
|
+ const children = collectTopLevelDivChildren(flex.inner)
|
|
|
if (children.length < 2) continue
|
|
|
- if (!children[0].includes('供方') || !children[1].includes('需方')) 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]}</td><td style="width:50%;vertical-align:top;border:1px solid #000;padding:8px;line-height:1.3;font-size:12px;">${children[1]}</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;">${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>`
|
|
|
out = out.slice(0, index) + table + out.slice(flex.end)
|
|
|
}
|
|
|
|
|
|
@@ -253,6 +300,7 @@ export function prepareContractHtmlForDocx(options: {
|
|
|
}
|
|
|
|
|
|
html = toAbsoluteAssetUrls(html, origin)
|
|
|
+ html = normalizeHeaderMetaFlexLayoutForDocx(html)
|
|
|
html = normalizePartyFlexLayoutForDocx(html)
|
|
|
|
|
|
return html
|