From 818792abacdfb75c10a32a09d755dbff7942def9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AF=92=E5=AF=92?= <2596194220@qq.com> Date: Mon, 13 Apr 2026 00:01:52 +0800 Subject: [PATCH] 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 --- app/page.tsx | 172 ++++++++++++++++++++++++++++++++++----------------- 1 file changed, 114 insertions(+), 58 deletions(-) diff --git a/app/page.tsx b/app/page.tsx index 39ecf63..f223e3a 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -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,33 +402,76 @@ export default function RecipeTranslationPage() { height: pageHeight, }, margin: { - top: 700, // 约1.27厘米 + top: 700, bottom: 700, - left: 900, // 约1.9厘米 + left: 900, right: 900, }, }, }, children: [ - new Paragraph({ - children: [ - new TextRun({ - text: t(data.title?.zh || "食谱") || data.title?.zh || "食谱", - bold: true, - size: 32, - }), - ], - alignment: AlignmentType.CENTER, - }), - new Paragraph({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: tableRows, + 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, + }, + }, }, - ], + children: [ + 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(), ...dayRows], + width: {size: 100, type: WidthType.PERCENTAGE}, + layout: TableLayoutType.FIXED, + }), + ], + }); + } + + const doc = new Document({ + sections, }); // 6. 生成并下载