import { Document, Packer, Paragraph, TextRun, Table, TableRow, TableCell, WidthType, AlignmentType, HeadingLevel, PageBreak, ImageRun, TableOfContents, VerticalAlign, FileChild, } from 'docx' type Heading = (typeof HeadingLevel)[keyof typeof HeadingLevel] type HAlign = (typeof AlignmentType)[keyof typeof AlignmentType] import type { MonthlyReportData, Chapter3FaultEntry, Chapter3OtherEntry, } from '@/types/report' // ---- helpers ---- function createHeaderCell(text: string, width?: number): TableCell { return new TableCell({ children: [new Paragraph({ children: [new TextRun({ text, bold: true, size: 20, font: 'SimSun', color: 'FFFFFF' })], alignment: AlignmentType.CENTER, spacing: { line: 360 }, })], shading: { fill: '1F4E79' }, width: width ? { size: width, type: WidthType.PERCENTAGE } : undefined, verticalAlign: VerticalAlign.CENTER, }) } function createCell(text: string, align: HAlign = AlignmentType.CENTER): TableCell { return new TableCell({ children: [new Paragraph({ children: [new TextRun({ text: text || '', size: 18, font: 'SimSun' })], alignment: align, spacing: { line: 276 }, })], verticalAlign: VerticalAlign.CENTER, }) } // 标题段落 function chapterTitle(text: string, heading: Heading, spaceBefore = 400, spaceAfter = 200): Paragraph { return new Paragraph({ children: [new TextRun({ text, bold: true, size: 28, font: 'SimSun' })], heading, spacing: { before: spaceBefore, after: spaceAfter, line: 360 }, }) } // 小节标题 function sectionTitle(text: string): Paragraph { return new Paragraph({ children: [new TextRun({ text, bold: true, size: 24, font: 'SimSun' })], heading: HeadingLevel.HEADING_2, spacing: { before: 200, after: 100, line: 360 }, }) } // 正文段落(首行缩进2字符) function bodyPara(text: string): Paragraph { return new Paragraph({ children: [new TextRun({ text, size: 22, font: 'SimSun' })], spacing: { after: 80, line: 360 }, indent: { firstLine: 480 }, }) } // 页面分隔 function pageBreak(): Paragraph { return new Paragraph({ children: [new PageBreak()] }) } // ---- 表格构建 ---- function buildFaultTable(entries: Chapter3FaultEntry[]): Table { const headerTexts = ['工单编号', '故障节点', '故障日期', '故障问题', '故障原因', '处理时长(分钟)', '是否计入SLA'] const headerRow = new TableRow({ children: headerTexts.map(t => createHeaderCell(t)) }) const dataRows = entries.map(e => new TableRow({ children: [ createCell(String(e.ticketId)), createCell(e.nodeIp), createCell(e.faultDate), createCell(e.faultProblem), createCell(e.faultCause), createCell(e.isOngoing ? '进行中' : String(e.durationMinutes)), createCell(e.isOngoing ? '—' : e.countedInSla), ], })) return new Table({ width: { size: 100, type: WidthType.PERCENTAGE }, rows: [headerRow, ...dataRows] }) } function buildOtherTable(entries: Chapter3OtherEntry[]): Table { const headerTexts = ['工单编号', '设备IP地址', '工单日期', '工单内容', '工单结论', '处理时长(分钟)', '是否计入SLA'] const headerRow = new TableRow({ children: headerTexts.map(t => createHeaderCell(t)) }) const dataRows = entries.map(e => new TableRow({ children: [ createCell(String(e.ticketId)), createCell(e.deviceIp), createCell(e.ticketDate), createCell(e.ticketContent, AlignmentType.LEFT), createCell(e.ticketConclusion, AlignmentType.LEFT), createCell(e.isOngoing ? '进行中' : String(e.durationMinutes)), createCell(e.isOngoing ? '—' : e.countedInSla), ], })) return new Table({ width: { size: 100, type: WidthType.PERCENTAGE }, rows: [headerRow, ...dataRows] }) } // ---- 主入口 ---- export async function buildMonthlyReportDocx( data: MonthlyReportData, charts: { gpuPng: Buffer; storagePng: Buffer }, weeklyLabels: string[] = [], ): Promise { const children: FileChild[] = [] // ========== 封面页 ========== // 上方留白 children.push(new Paragraph({ children: [], spacing: { before: 2400 } })) children.push(new Paragraph({ children: [new TextRun({ text: '图灵引擎&腾讯公司算力服务框架IT基础设施运营月报', bold: true, size: 44, font: 'SimSun', })], alignment: AlignmentType.CENTER, spacing: { after: 200, line: 360 }, })) children.push(new Paragraph({ children: [new TextRun({ text: `(${data.monthLabel.replace('年', '年').replace('月', '月')}1日-${new Date(data.periodEnd).getDate()}日)`, bold: true, size: 52, font: 'SimHei', })], alignment: AlignmentType.CENTER, spacing: { after: 800, line: 360 }, })) // 公司 + 生成月份(页面下方,标题下方留 8 空行) children.push(new Paragraph({ children: [], spacing: { before: 600, line: 360 } })) children.push(new Paragraph({ children: [], spacing: { before: 200, line: 360 } })) children.push(new Paragraph({ children: [], spacing: { before: 200, line: 360 } })) children.push(new Paragraph({ children: [], spacing: { before: 200, line: 360 } })) children.push(new Paragraph({ children: [], spacing: { before: 200, line: 360 } })) children.push(new Paragraph({ children: [], spacing: { before: 200, line: 360 } })) children.push(new Paragraph({ children: [], spacing: { before: 200, line: 360 } })) children.push(new Paragraph({ children: [], spacing: { before: 200, line: 360 } })) children.push(new Paragraph({ children: [], spacing: { before: 200 } })) children.push(new Paragraph({ children: [new TextRun({ text: '杭州图灵引擎科技有限公司', size: 32, font: 'SimSun' })], alignment: AlignmentType.CENTER, spacing: { line: 360 }, })) children.push(new Paragraph({ children: [new TextRun({ text: `${data.generationLabel}`, size: 32, font: 'SimSun' })], alignment: AlignmentType.CENTER, spacing: { line: 360 }, })) // ========== 目录页 ========== children.push(pageBreak()) children.push(new Paragraph({ children: [new TextRun({ text: '目录', bold: true, size: 36, font: 'SimSun' })], alignment: AlignmentType.CENTER, spacing: { after: 400, line: 360 }, })) children.push(new TableOfContents('目录', { hyperlink: true, headingStyleRange: '1-2', })) // ========== 第一章:总体运营概况 ========== children.push(pageBreak()) children.push(chapterTitle('一、总体运营概况', HeadingLevel.HEADING_1)) // GPU 图表 children.push(new Paragraph({ children: [new ImageRun({ data: charts.gpuPng, transformation: { width: 550, height: 275 }, type: 'png', })], alignment: AlignmentType.CENTER, spacing: { before: 200, after: 300 }, })) // 存储图表 children.push(new Paragraph({ children: [new ImageRun({ data: charts.storagePng, transformation: { width: 550, height: 275 }, type: 'png', })], alignment: AlignmentType.CENTER, spacing: { before: 200, after: 200 }, })) // ========== 第二章:运营数据总览 ========== children.push(chapterTitle('二、运营数据总览', HeadingLevel.HEADING_1)) // 按 device_type 分组 const gpuChapter2 = data.chapter2.filter(e => e.device_type === 'gpu') const storageChapter2 = data.chapter2.filter(e => e.device_type === 'storage') // 2.1 GPU children.push(sectionTitle('2.1 GPU服务器运行状态')) if (gpuChapter2.length === 0) { children.push(bodyPara('故障记录:无。')) } else { for (const entry of gpuChapter2) { for (const f of entry.faults) { const dateParts = entry.date.split('-') const monthDay = `${parseInt(dateParts[1])}月${parseInt(dateParts[2])}日` const statusText = f.isOngoing ? '处理中。' : `${f.recoveryDays === 0 ? '当日' : f.recoveryDays === 1 ? '次日' : `${f.recoveryDays}日后`}恢复。` children.push(bodyPara( `${monthDay}发生1次${f.fault_subcategory},故障节点为${f.ip},${statusText}` )) } } } // 2.2 存储 children.push(sectionTitle('2.2 存储服务器运行状态')) if (storageChapter2.length === 0) { children.push(bodyPara('故障记录:无。')) } else { for (const entry of storageChapter2) { for (const f of entry.faults) { const dateParts = entry.date.split('-') const monthDay = `${parseInt(dateParts[1])}月${parseInt(dateParts[2])}日` const statusText = f.isOngoing ? '处理中。' : `${f.recoveryDays === 0 ? '当日' : f.recoveryDays === 1 ? '次日' : `${f.recoveryDays}日后`}恢复。` children.push(bodyPara( `${monthDay}发生1次${f.fault_subcategory},故障节点为${f.ip},${statusText}` )) } } } // ========== 第三章:运营故障概览 ========== children.push(chapterTitle('三、运营故障概览', HeadingLevel.HEADING_1)) const { gpuFaults, storageFaults, otherTickets } = data.chapter3 // 3.1 GPU children.push(sectionTitle( gpuFaults.length > 0 ? `3.1 GPU服务器故障(${gpuFaults.length}个)` : '3.1 GPU服务器故障(无)' )) if (gpuFaults.length > 0) children.push(buildFaultTable(gpuFaults)) // 3.2 存储 children.push(sectionTitle( storageFaults.length > 0 ? `3.2 存储服务器故障(${storageFaults.length}个)` : '3.2 存储服务器故障(无)' )) if (storageFaults.length > 0) children.push(buildFaultTable(storageFaults)) // 3.3 其他工单 children.push(sectionTitle( otherTickets.length > 0 ? `3.3 其他工单(${otherTickets.length}个)` : '3.3 其他工单(无)' )) if (otherTickets.length > 0) children.push(buildOtherTable(otherTickets)) // ========== 第四章:服务可用性说明 ========== children.push(chapterTitle('四、服务可用性说明', HeadingLevel.HEADING_1)) if (data.chapter4.length === 0) { children.push(bodyPara('无。')) } else { for (const entry of data.chapter4) { const totalMinutes = entry.monthDays * 24 * 60 const formula = `${entry.ip}服务可用性=(${entry.monthDays}*24*60-${entry.totalDurationMinutes})/(${entry.monthDays}*24*60)*100%=` const percent = `${entry.availabilityPercent.toFixed(2)}%` const below99 = entry.availabilityPercent < 99 const children_: TextRun[] = [ new TextRun({ text: formula, size: 22, font: 'SimSun' }), new TextRun(below99 ? { text: percent, size: 22, font: 'SimSun', bold: true, color: 'FF0000', highlight: 'yellow' } : { text: percent, size: 22, font: 'SimSun' } ), ] if (entry.hasOngoing) { children_.push(new TextRun({ text: '(故障处理中,仅计本月部分)', size: 22, font: 'SimSun', color: 'FF6600' })) } children.push(new Paragraph({ children: children_, spacing: { after: 80, line: 360 }, indent: { firstLine: 480 }, })) } } // ========== 第五章:附件 ========== if (weeklyLabels.length > 0) { children.push(chapterTitle('五、附件', HeadingLevel.HEADING_1)) children.push(bodyPara(`本月共 ${weeklyLabels.length} 份周报,双击图标可打开查看:`)) } // ========== 组装文档 ========== const doc = new Document({ sections: [{ children, properties: { page: { margin: { top: 1440, // ~1 inch in twips bottom: 1440, left: 1440, right: 1440, }, }, }, }], styles: { default: { document: { run: { font: 'SimSun', size: 22 } }, heading1: { run: { font: 'SimSun', size: 28, bold: true }, paragraph: { spacing: { line: 360 } } }, heading2: { run: { font: 'SimSun', size: 24, bold: true }, paragraph: { spacing: { line: 360 } } }, }, }, }) return Packer.toBuffer(doc) }