feat: implement second page header in export

- Add multi-section support when secondPageHeader is enabled
- Each page gets its own header row when content spans multiple pages
- Only applies when days exceed estimated page capacity
This commit is contained in:
2026-04-13 00:01:52 +08:00
parent 4586649131
commit 818792abac
+104 -48
View File
@@ -226,8 +226,9 @@ export default function RecipeTranslationPage() {
const exportConfig = configResult.success ? configResult.data?.export : {
untranslatedColor: "CC0000",
orientation: "landscape",
secondPageHeader: false,
};
const { untranslatedColor, orientation } = exportConfig;
const { untranslatedColor, orientation, secondPageHeader } = exportConfig;
// 2. 收集所有需要翻译的内容(星期、餐食类型、菜品)
const allNames: string[] = [];
@@ -273,43 +274,8 @@ export default function RecipeTranslationPage() {
// 3. 构建翻译映射函数
const t = (zh: string): string => translations[zh] || "";
// 4. 构建表格数据
const tableRows: TableRow[] = [];
// 表头行 - 星期列头 + 餐食类型都要上中下英
const headerCells: TableCell[] = [];
headerCells.push(
new TableCell({
children: [
new Paragraph({children: [new TextRun({text: "星期 / Week", bold: true})]}),
].filter((p): p is Paragraph => p !== null) as unknown as Paragraph[],
width: {size: 12, type: WidthType.PERCENTAGE},
}),
);
for (const mealType of MEAL_TYPES) {
const zhText = mealType.label;
const enText = t(zhText);
headerCells.push(
new TableCell({
children: [
new Paragraph({children: [new TextRun({text: enText || zhText, bold: true})]}),
enText ? new Paragraph({
children: [new TextRun({
text: zhText,
color: "666666",
size: 18
})]
}) : null,
].filter((p): p is Paragraph => p !== null) as unknown as Paragraph[],
width: {size: 17.6, type: WidthType.PERCENTAGE},
})
);
}
tableRows.push(new TableRow({children: headerCells, tableHeader: true}));
// 数据行(使用实际数据中的星期)- 上中下英
// 4. 构建表格数据 - 生成所有数据行
const dayRows: TableRow[] = [];
for (const day of data.days) {
const dayZh = day.name.zh || "";
const dayEn = t(dayZh) || translations[dayZh] || "";
@@ -370,17 +336,64 @@ export default function RecipeTranslationPage() {
);
}
tableRows.push(new TableRow({children: dayCells}));
dayRows.push(new TableRow({children: dayCells}));
}
// 5. 创建文档 - 根据设置选择页面方向
// 5. 创建文档 - 根据设置选择页面方向和分页
const isLandscape = orientation === "landscape";
const pageWidth = isLandscape ? 11906 : 16838;
const pageHeight = isLandscape ? 16838 : 11906;
const doc = new Document({
sections: [
{
// 构建表头行
const buildHeaderRow = (): TableRow => {
const headerCells: TableCell[] = [];
headerCells.push(
new TableCell({
children: [
new Paragraph({children: [new TextRun({text: "星期 / Week", bold: true})]}),
].filter((p): p is Paragraph => p !== null) as unknown as Paragraph[],
width: {size: 12, type: WidthType.PERCENTAGE},
}),
);
for (const mealType of MEAL_TYPES) {
const zhText = mealType.label;
const enText = t(zhText);
headerCells.push(
new TableCell({
children: [
new Paragraph({children: [new TextRun({text: enText || zhText, bold: true})]}),
enText ? new Paragraph({
children: [new TextRun({
text: zhText,
color: "666666",
size: 18
})]
}) : null,
].filter((p): p is Paragraph => p !== null) as unknown as Paragraph[],
width: {size: 17.6, type: WidthType.PERCENTAGE},
})
);
}
return new TableRow({children: headerCells, tableHeader: true});
};
// 估算每页能放多少天(横向 A4 约 27cm 高度,减去标题和边距约 25cm,每行约 1cm)
const daysPerPage = 5;
// 构建 sections
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const sections: any[] = [];
if (secondPageHeader && dayRows.length > daysPerPage) {
// 分页模式:每页有表头
const pageCount = Math.ceil(dayRows.length / daysPerPage);
for (let page = 0; page < pageCount; page++) {
const startIdx = page * daysPerPage;
const endIdx = Math.min(startIdx + daysPerPage, dayRows.length);
const pageRows = dayRows.slice(startIdx, endIdx);
sections.push({
properties: {
page: {
size: {
@@ -389,9 +402,49 @@ export default function RecipeTranslationPage() {
height: pageHeight,
},
margin: {
top: 700, // 约1.27厘米
top: 700,
bottom: 700,
left: 900, // 约1.9厘米
left: 900,
right: 900,
},
},
},
children: [
...(page === 0 ? [
new Paragraph({
children: [
new TextRun({
text: t(data.title?.zh || "食谱") || data.title?.zh || "食谱",
bold: true,
size: 32,
}),
],
alignment: AlignmentType.CENTER,
}),
new Paragraph({children: []}),
] : []),
new Table({
rows: [buildHeaderRow(), ...pageRows],
width: {size: 100, type: WidthType.PERCENTAGE},
layout: TableLayoutType.FIXED,
}),
],
});
}
} else {
// 普通模式:单表格无分页表头
sections.push({
properties: {
page: {
size: {
orientation: isLandscape ? PageOrientation.LANDSCAPE : PageOrientation.PORTRAIT,
width: pageWidth,
height: pageHeight,
},
margin: {
top: 700,
bottom: 700,
left: 900,
right: 900,
},
},
@@ -409,13 +462,16 @@ export default function RecipeTranslationPage() {
}),
new Paragraph({children: []}),
new Table({
rows: tableRows,
rows: [buildHeaderRow(), ...dayRows],
width: {size: 100, type: WidthType.PERCENTAGE},
layout: TableLayoutType.FIXED,
}),
],
},
],
});
}
const doc = new Document({
sections,
});
// 6. 生成并下载