Files
users/lib/documentation/docx-generator.ts
v0 2408d50cb0 refactor: overhaul UI for streamlined user experience
Redesign navigation, home overview, user portrait, and valuation pages
with improved functionality and responsive design.

Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
2025-07-18 13:47:12 +00:00

469 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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<string> {
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
}
}