/** * Word文档生成器 * 使用docx库生成专业的Word文档 */ import { Document, Paragraph, TextRun, HeadingLevel, ImageRun, TableOfContents, PageBreak, AlignmentType, Table, TableRow, TableCell, WidthType, BorderStyle, } from "docx" import { Buffer } from "buffer" interface Screenshot { id: string name: string url: string dataUrl?: string timestamp: Date status: string } interface DocumentSettings { title: string author: string description: string includeTimestamp: boolean includePageUrls: boolean imageFormat: string imageQuality: number pageSize: string orientation: string } interface DocumentSection { title: string path: string description: string screenshot: string } interface DocumentData { title: string author: string date: string sections: DocumentSection[] } /** * 将base64字符串转换为Buffer * @param base64 base64字符串 * @returns Buffer */ function base64ToBuffer(base64: string): Buffer { try { // 移除data URL前缀 const base64Data = base64.includes("base64,") ? base64.split("base64,")[1] : base64 // 转换为Buffer return Buffer.from(base64Data, "base64") } catch (error) { console.error("base64转换为Buffer时出错:", error) throw error } } /** * 生成Word文档 * @param screenshots 截图数据 * @param settings 文档设置 * @returns 文档的ArrayBuffer */ export async function generateDocx(screenshots: Screenshot[], settings: DocumentSettings): Promise { console.log("开始生成Word文档...") try { // 验证输入数据 if (!screenshots || !Array.isArray(screenshots)) { throw new Error("截图数据不能为空或不是数组") } if (!settings) { throw new Error("文档设置不能为空") } console.log(`文档标题: ${settings.title}`) console.log(`作者: ${settings.author}`) console.log(`描述: ${settings.description}`) console.log(`部分数量: ${screenshots.length}`) // 创建文档 const doc = new Document({ title: settings.title, description: settings.description, creator: settings.author, styles: { paragraphStyles: [ { id: "Normal", name: "Normal", run: { size: 24, // 12pt font: "Microsoft YaHei", }, paragraph: { spacing: { line: 360, // 1.5倍行距 before: 240, // 12pt after: 240, // 12pt }, }, }, { id: "Heading1", name: "Heading 1", run: { size: 36, // 18pt bold: true, font: "Microsoft YaHei", }, paragraph: { spacing: { before: 480, // 24pt after: 240, // 12pt }, }, }, { id: "Heading2", name: "Heading 2", run: { size: 32, // 16pt bold: true, font: "Microsoft YaHei", }, paragraph: { spacing: { before: 360, // 18pt after: 240, // 12pt }, }, }, { id: "Caption", name: "Caption", run: { size: 20, // 10pt italic: true, font: "Microsoft YaHei", }, paragraph: { alignment: AlignmentType.CENTER, spacing: { before: 120, // 6pt after: 240, // 12pt }, }, }, ], }) // 文档部分 const sections = [] // 封面 sections.push({ properties: {}, children: [ new Paragraph({ text: "", spacing: {\ before: 3000, // 大约页面1/3处 },\ }),\ new Paragraph({\ text: settings.title,\ heading: HeadingLevel.TITLE, alignment: AlignmentType.CENTER, spacing: {\ after: 400, }, }), new Paragraph({\ text: "",\ spacing: {\ before: 800, }, }), new Paragraph({ alignment: AlignmentType.CENTER,\ children: [ new TextRun({ text: \`作者: ${settings.author}`,\ size: 24,\ }), ], }), new Paragraph({ alignment: AlignmentType.CENTER,\ children: [ new TextRun({ text: `生成日期: ${new Date().toLocaleString()}`,\ size: 24, }), ], }), new Paragraph({ text: "",\ break: PageBreak.AFTER, }), ], }) // 目录 sections.push({ properties: {}, children: [\ new Paragraph({ text: "目录", heading: HeadingLevel.HEADING_1,\ alignment: AlignmentType.CENTER, }), new TableOfContents("目录", { hyperlink: true, headingStyleRange: "1-3",\ }),\ new Paragraph({ text: "", break: PageBreak.AFTER, }),\ ], }) // 正文 const contentSection = { properties: {}, children: [] as any[], } // 添加简介 contentSection.children.push(\ new Paragraph({ text: "1. 简介", heading: HeadingLevel.HEADING_1, }), new Paragraph({ text: "本文档由文档生成工具自动生成,包含应用程序的所有主要页面截图和功能说明。", }), new Paragraph({ text: "文档目的是帮助用户了解系统功能和使用方法,为系统管理员和最终用户提供参考。", }),\ new Paragraph({ text: "", }), ) // 添加页面内容 contentSection.children.push( new Paragraph({ text: "2. 系统功能", heading: HeadingLevel.HEADING_1, }), ) // 处理每个部分 console.log("开始处理文档部分...") // 使用for循环而不是forEach,以便更好地处理错误 for (let i = 0; i < screenshots.length; i++) { try { const screenshot = screenshots[i] console.log(\`处理部分 ${i + 1}/${screenshots.length}: ${screenshot.name}`) // 添加标题 contentSection.children.push( new Paragraph({ text: `2.${i + 1} ${screenshot.name}`, heading: HeadingLevel.HEADING_2, }), ) // 添加页面URL和截图时间 if (settings.includePageUrls) { contentSection.children.push( new Paragraph({ text: `页面URL: ${screenshot.url}`, }), ) } if (settings.includeTimestamp) { contentSection.children.push( new Paragraph({ text: `截图时间: ${screenshot.timestamp.toLocaleString()}`, }), ) } // 添加状态 contentSection.children.push( new Paragraph({ text: `状态: ${screenshot.status}`, }), ) // 添加截图 try { if (screenshot.dataUrl && screenshot.dataUrl.startsWith("data:image/")) { console.log(`处理截图: ${screenshot.name}`) // 从base64数据URL中提取图像数据 const base64Data = screenshot.dataUrl.split(",")[1] if (!base64Data) { throw new Error("无效的base64数据") } // 将base64转换为二进制数据 const imageBuffer = base64ToBuffer(screenshot.dataUrl) // 添加图像 contentSection.children.push( new Paragraph({ children: [ new ImageRun({ data: imageBuffer, transformation: { width: 600, height: 400, }, }), ], alignment: AlignmentType.CENTER, }), new Paragraph({ text: `图 ${i + 1}: ${screenshot.name} 页面截图`, style: "Caption", }), new Paragraph({ text: "", }), ) } else { console.warn(`部分 ${i + 1} 缺少有效的截图`) contentSection.children.push( new Paragraph({ text: "[截图不可用]", alignment: AlignmentType.CENTER, }), new Paragraph({ text: "", }), ) } } catch (imageError) { console.error(`处理部分 ${i + 1} 的截图时出错:`, imageError) contentSection.children.push( new Paragraph({ text: "[处理截图时出错]", alignment: AlignmentType.CENTER, }), new Paragraph({ text: "", }), ) } } catch (sectionError) { console.error(`处理部分 ${i + 1} 时出错:`, sectionError) contentSection.children.push( new Paragraph({ text: `[处理部分 ${i + 1} 时出错: ${sectionError instanceof Error ? sectionError.message : String(sectionError)}]`, alignment: AlignmentType.CENTER, }), new Paragraph({ text: "", }), ) } } // 添加附录 contentSection.children.push( new Paragraph({ text: "3. 附录", heading: HeadingLevel.HEADING_1, }), new Paragraph({ text: "3.1 文档信息", heading: HeadingLevel.HEADING_2, }), ) // 创建文档信息表格 const infoTable = new Table({ width: { size: 100, type: WidthType.PERCENTAGE, }, borders: { top: { style: BorderStyle.SINGLE, size: 1, color: "auto" }, bottom: { style: BorderStyle.SINGLE, size: 1, color: "auto" }, left: { style: BorderStyle.SINGLE, size: 1, color: "auto" }, right: { style: BorderStyle.SINGLE, size: 1, color: "auto" }, insideHorizontal: { style: BorderStyle.SINGLE, size: 1, color: "auto" }, insideVertical: { style: BorderStyle.SINGLE, size: 1, color: "auto" }, }, rows: [ new TableRow({ children: [ new TableCell({ width: { size: 30, type: WidthType.PERCENTAGE, }, children: [new Paragraph("文档标题")], }), new TableCell({ width: { size: 70, type: WidthType.PERCENTAGE, }, children: [new Paragraph(settings.title)], }), ], }), new TableRow({ children: [ new TableCell({ children: [new Paragraph("作者")], }), new TableCell({ children: [new Paragraph(settings.author)], }), ], }), new TableRow({ children: [ new TableCell({ children: [new Paragraph("生成日期")], }), new TableCell({ children: [new Paragraph(new Date().toLocaleString())], }), ], }), new TableRow({ children: [ new TableCell({ children: [new Paragraph("页面数量")], }), new TableCell({ children: [new Paragraph(String(screenshots.length))], }), ], }), ], }) contentSection.children.push(infoTable) // 添加内容部分到文档 doc.addSection({ children: sections[0].children.concat(sections[1].children, contentSection.children), }) console.log("文档生成完成,准备导出...") // 生成blob const buffer = await doc.save() console.log("文档生成完成") return buffer } catch (error) { console.error("生成Word文档时出错:", error) throw error } }