test: 完成 DOCX 全主题视觉验收

This commit is contained in:
SkyJourney
2026-07-30 19:04:12 +08:00
parent aff2830c28
commit e7c688df6e
10 changed files with 804 additions and 63 deletions
@@ -10,6 +10,7 @@ import {
WORD_NAMESPACE,
appendElement,
colorValue,
millimetersToTwips,
parseXmlPart,
pointsToHalfPoints,
serializeXmlPart,
@@ -173,37 +174,50 @@ function appendFooterContent(
}
}
function appendThreeColumnTable(
root: XmlElement,
function appendParagraphProperties(
paragraph: XmlElement,
options: {
alignment?: "left" | "center" | "right";
position: "header" | "footer";
divider: boolean;
dividerColor: string;
alignments: readonly ["left", "center", "right"];
render: (
paragraph: XmlElement,
alignment: "left" | "center" | "right"
) => void;
centerTabTwips?: number;
rightTabTwips?: number;
}
) {
const table = appendElement(root, WORD_NAMESPACE, "w:tbl");
const tableProperties = appendElement(
table,
const properties = appendElement(
paragraph,
WORD_NAMESPACE,
"w:tblPr"
"w:pPr"
);
appendElement(tableProperties, WORD_NAMESPACE, "w:tblW", {
"w:w": "5000",
"w:type": "pct"
});
appendElement(tableProperties, WORD_NAMESPACE, "w:tblLayout", {
"w:type": "fixed"
appendElement(properties, WORD_NAMESPACE, "w:spacing", {
"w:before": "0",
"w:after": "0"
});
if (options.alignment) {
appendElement(properties, WORD_NAMESPACE, "w:jc", {
"w:val": options.alignment
});
}
if (
options.centerTabTwips !== undefined &&
options.rightTabTwips !== undefined
) {
const tabs = appendElement(properties, WORD_NAMESPACE, "w:tabs");
appendElement(tabs, WORD_NAMESPACE, "w:tab", {
"w:val": "center",
"w:pos": String(options.centerTabTwips)
});
appendElement(tabs, WORD_NAMESPACE, "w:tab", {
"w:val": "right",
"w:pos": String(options.rightTabTwips)
});
}
if (options.divider) {
const borders = appendElement(
tableProperties,
properties,
WORD_NAMESPACE,
"w:tblBorders"
"w:pBdr"
);
appendElement(
borders,
@@ -217,41 +231,46 @@ function appendThreeColumnTable(
}
);
}
const grid = appendElement(table, WORD_NAMESPACE, "w:tblGrid");
for (let index = 0; index < 3; index += 1) {
appendElement(grid, WORD_NAMESPACE, "w:gridCol", {
"w:w": "2400"
});
}
const row = appendElement(table, WORD_NAMESPACE, "w:tr");
for (const alignment of options.alignments) {
const cell = appendElement(row, WORD_NAMESPACE, "w:tc");
const cellProperties = appendElement(
cell,
WORD_NAMESPACE,
"w:tcPr"
);
appendElement(cellProperties, WORD_NAMESPACE, "w:tcW", {
"w:w": "1667",
"w:type": "pct"
});
const paragraph = appendElement(cell, WORD_NAMESPACE, "w:p");
const paragraphProperties = appendElement(
paragraph,
WORD_NAMESPACE,
"w:pPr"
);
appendElement(paragraphProperties, WORD_NAMESPACE, "w:jc", {
"w:val": alignment
});
options.render(paragraph, alignment);
}
function appendTab(paragraph: XmlElement) {
const run = appendElement(paragraph, WORD_NAMESPACE, "w:r");
appendElement(run, WORD_NAMESPACE, "w:tab");
}
function appendHeaderParagraph(
root: XmlElement,
options: {
divider: boolean;
dividerColor: string;
contentWidthMm: number;
render: (
paragraph: XmlElement,
alignment: "left" | "center" | "right"
) => void;
}
) {
const paragraph = appendElement(root, WORD_NAMESPACE, "w:p");
const contentWidthTwips = millimetersToTwips(options.contentWidthMm);
appendParagraphProperties(paragraph, {
position: "header",
divider: options.divider,
dividerColor: options.dividerColor,
centerTabTwips: Math.round(contentWidthTwips / 2),
rightTabTwips: contentWidthTwips
});
options.render(paragraph, "left");
appendTab(paragraph);
options.render(paragraph, "center");
appendTab(paragraph);
options.render(paragraph, "right");
}
function createHeaderPart(
header: HeaderConfig,
options: DynamicReferenceDocxOptions,
fallbackFont: string
fallbackFont: string,
contentWidthMm: number
) {
const document = createWordPart("hdr");
const style: RunStyle = {
@@ -259,11 +278,10 @@ function createHeaderPart(
sizePt: (lengthToMillimeters(header.fontSize) * 72) / 25.4,
color: header.color
};
appendThreeColumnTable(document.documentElement!, {
position: "header",
appendHeaderParagraph(document.documentElement!, {
divider: header.showDivider,
dividerColor: header.color,
alignments: ["left", "center", "right"],
contentWidthMm,
render: (paragraph, alignment) => {
const slot = header[alignment];
if (slot.enabled) {
@@ -290,17 +308,20 @@ function createFooterPart(
sizePt: (lengthToMillimeters(footer.fontSize) * 72) / 25.4,
color: footer.color
};
appendThreeColumnTable(document.documentElement!, {
const paragraph = appendElement(
document.documentElement!,
WORD_NAMESPACE,
"w:p"
);
appendParagraphProperties(paragraph, {
position: "footer",
divider: footer.showDivider,
dividerColor: footer.color,
alignments: ["left", "center", "right"],
render: (paragraph, cellAlignment) => {
if (!empty && cellAlignment === alignment) {
appendFooterContent(paragraph, footer, style);
}
}
alignment
});
if (!empty) {
appendFooterContent(paragraph, footer, style);
}
return serializeXmlPart(document);
}
@@ -427,7 +448,8 @@ export function createHeaderFooterParts(
options: DynamicReferenceDocxOptions,
header: HeaderConfig,
footer: FooterConfig,
fallbackFont: string
fallbackFont: string,
contentWidthMm: number
): HeaderFooterTransformResult {
const entries = new Map(sourceEntries);
removeExistingHeaderFooterParts(entries);
@@ -447,7 +469,8 @@ export function createHeaderFooterParts(
const headerContent = createHeaderPart(
header,
options,
fallbackFont
fallbackFont,
contentWidthMm
);
for (const type of [
"default",
@@ -91,7 +91,8 @@ export function createDynamicReferenceDocx(
options,
page.header,
page.footer,
style.body.fonts.eastAsia
style.body.fonts.eastAsia,
page.dimensions.width - page.margins.left - page.margins.right
);
const entries = withDecorations.entries;
entries.set(
+7
View File
@@ -191,6 +191,13 @@ function validateReferencedParts(
`${relationship.targetPart} 的页眉页脚根元素无效`
);
}
if (
root.getElementsByTagNameNS(WORD_NAMESPACE, "tbl").length > 0
) {
throw new Error(
`${relationship.targetPart} 不得使用表格模拟页眉页脚布局`
);
}
}
return references.length;
}