/** * Word文档生成器 * 使用docx库生成专业的Word文档 */ import { Document, Paragraph, TextRun, HeadingLevel, ImageRun, TableOfContents, PageBreak, AlignmentType, Table, TableRow, TableCell, WidthType, BorderStyle, } from "docx" interface DocumentSection { title: string path: string description: string screenshot: string } interface DocumentData { title: string author: string date: string sections: DocumentSection[] } /** * 生成Word文档 * @param data 文档数据 * @returns 文档的blob URL */ export async function generateDocx(data: DocumentData): Promise { console.log("开始生成Word文档...") try { // 验证输入数据 if (!data) { throw new Error("文档数据不能为空") } if (!Array.isArray(data.sections)) { console.warn("sections不是数组,使用空数组代替") data.sections = [] } console.log(`文档标题: ${data.title}`) console.log(`作者: ${data.author}`) console.log(`日期: ${data.date}`) console.log(`部分数量: ${data.sections.length}`) // 创建文档 const doc = new Document({ title: data.title, description: "由文档生成工具自动生成", creator: data.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: data.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: `作者: ${data.author}`, size: 24, }), ], }), new Paragraph({ alignment: AlignmentType.CENTER, children: [ new TextRun({ text: `生成日期: ${data.date}`, 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 < data.sections.length; i++) { try { const section = data.sections[i] console.log(`处理部分 ${i + 1}/${data.sections.length}: ${section.title}`) // 验证部分数据 if (!section.title) { console.warn(`部分 ${i + 1} 缺少标题,使用默认标题`) section.title = `页面 ${i + 1}` } if (!section.description) { console.warn(`部分 ${i + 1} 缺少描述,使用默认描述`) section.description = `这是 ${section.title} 页面的描述。` } // 添加标题 contentSection.children.push( new Paragraph({ text: `2.${i + 1} ${section.title}`, heading: HeadingLevel.HEADING_2, }), ) // 添加描述 const descriptionParagraphs = section.description.split("\n") for (const paragraph of descriptionParagraphs) { contentSection.children.push( new Paragraph({ text: paragraph, }), ) } // 添加截图 try { if (section.screenshot && section.screenshot.startsWith("data:image/")) { console.log(`处理截图: ${section.title}`) // 从base64数据URL中提取图像数据 const base64Data = section.screenshot.split(",")[1] if (!base64Data) { throw new Error("无效的base64数据") } // 将base64转换为二进制数据 const imageBuffer = Buffer.from(base64Data, "base64") // 添加图像 contentSection.children.push( new Paragraph({ children: [ new ImageRun({ data: imageBuffer, transformation: { width: 600, height: 400, }, }), ], alignment: AlignmentType.CENTER, }), new Paragraph({ text: `图 ${i + 1}: ${section.title} 页面截图`, 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(data.title)], }), ], }), new TableRow({ children: [ new TableCell({ children: [new Paragraph("作者")], }), new TableCell({ children: [new Paragraph(data.author)], }), ], }), new TableRow({ children: [ new TableCell({ children: [new Paragraph("生成日期")], }), new TableCell({ children: [new Paragraph(data.date)], }), ], }), new TableRow({ children: [ new TableCell({ children: [new Paragraph("页面数量")], }), new TableCell({ children: [new Paragraph(String(data.sections.length))], }), ], }), ], }) contentSection.children.push(infoTable) // 添加内容部分到文档 sections.push(contentSection) // 设置文档部分 doc.addSection({ children: [...sections[0].children, ...sections[1].children, ...contentSection.children], }) console.log("文档生成完成,准备导出...") // 生成blob const buffer = await doc.save() const blob = new Blob([buffer], { type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document" }) // 创建URL const url = URL.createObjectURL(blob) console.log("文档URL已创建:", url) return url } catch (error) { console.error("生成Word文档时出错:", error) throw error } } /** * 将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 } }