#!/usr/bin/env python3 """ 专业报告模版生成脚本 V2.2 修复循环表格语法(poi-tl 需要标记行 + 模板行的结构) """ from docx import Document from docx.shared import Pt, Cm from docx.enum.text import WD_ALIGN_PARAGRAPH from docx.enum.table import WD_TABLE_ALIGNMENT from docx.oxml.ns import qn from docx.oxml import OxmlElement import os def set_font(run, name='宋体', size=12, bold=False): """设置字体""" run.font.name = name run._element.rPr.rFonts.set(qn('w:eastAsia'), name) run.font.size = Pt(size) run.bold = bold def add_heading(doc, text, size=14): """添加章节标题""" para = doc.add_paragraph() run = para.add_run(text) set_font(run, '黑体', size, bold=True) para.paragraph_format.space_before = Pt(12) para.paragraph_format.space_after = Pt(6) return para def add_body(doc, text, indent=True): """添加正文段落""" para = doc.add_paragraph() run = para.add_run(text) set_font(run, '宋体', 12) if indent: para.paragraph_format.first_line_indent = Cm(0.74) return para def add_label_para(doc, label, value): """添加 标签:值 段落""" para = doc.add_paragraph() label_run = para.add_run(label) set_font(label_run, '宋体', 12, bold=True) value_run = para.add_run(value) set_font(value_run, '宋体', 12) return para def set_cell_background(cell, color): """设置单元格背景色""" shading = OxmlElement('w:shd') shading.set(qn('w:val'), 'clear') shading.set(qn('w:fill'), color) cell._tc.get_or_add_tcPr().append(shading) def set_cell_vertical_center(cell): """单元格内容垂直居中""" vAlign = OxmlElement('w:vAlign') vAlign.set(qn('w:val'), 'center') cell._tc.get_or_add_tcPr().append(vAlign) def style_header_cell(cell, bg_color='DCE6F1'): """表头单元格样式:加粗 + 居中 + 浅蓝底纹 + 垂直居中""" set_cell_background(cell, bg_color) set_cell_vertical_center(cell) cell.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER for run in cell.paragraphs[0].runs: run.bold = True def style_body_cell(cell, align=WD_ALIGN_PARAGRAPH.CENTER): """数据单元格样式:垂直居中""" set_cell_vertical_center(cell) cell.paragraphs[0].alignment = align def set_table_widths(table, widths_cm): """设置表格列宽(厘米)""" for row in table.rows: for i, width in enumerate(widths_cm): if i < len(row.cells): row.cells[i].width = Cm(width) def create_professional_template(template_name, device_type, template_dir): """创建专业报告模版""" doc = Document() # 页面设置:A4 + 标准页边距 section = doc.sections[0] section.page_width = Cm(21.0) section.page_height = Cm(29.7) section.left_margin = Cm(2.54) section.right_margin = Cm(2.54) section.top_margin = Cm(2.54) section.bottom_margin = Cm(2.54) # 设置文档默认样式:宋体小四、1.5 倍行距 style = doc.styles['Normal'] style.font.name = '宋体' style._element.rPr.rFonts.set(qn('w:eastAsia'), '宋体') style.font.size = Pt(12) style.paragraph_format.line_spacing = 1.5 # ===== 1. 标题 ===== title = doc.add_paragraph() title.alignment = WD_ALIGN_PARAGRAPH.CENTER run = title.add_run(f'{device_type}监测分析报告') set_font(run, '黑体', 18, bold=True) title.paragraph_format.space_after = Pt(12) # ===== 2. 基本信息 ===== info_para = doc.add_paragraph() r1 = info_para.add_run('报告编号:') set_font(r1, '宋体', 12, bold=True) r2 = info_para.add_run('{{report.no}}') set_font(r2, '宋体', 12) r3 = info_para.add_run(' 生成时间:') set_font(r3, '宋体', 12, bold=True) r4 = info_para.add_run('{{report.generatedAt}}') set_font(r4, '宋体', 12) add_label_para(doc, '报告周期:', '{{report.periodDesc}}') add_label_para(doc, '设备信息:', '设备名称 {{device.name}}|设备编号 {{device.serialNumber}}|所属产品 {{device.productName}}') add_label_para(doc, '监测指标:', '{{device.metricName}}') add_label_para(doc, '控制值:', '{{controlValue}} {{controlValueUnit}}') # ===== 3. 项目概况 ===== add_heading(doc, '一、项目概况') add_body(doc, '{{summary}}') # ===== 4. 安全评估 ===== add_heading(doc, '二、安全评估') add_label_para(doc, '安全等级:', '{{safetyLevel}}') add_label_para(doc, '评估说明:', '{{safetyDescription}}') # ===== 5. 统计指标 ===== add_heading(doc, '三、统计指标') table = doc.add_table(rows=10, cols=2) table.style = 'Table Grid' table.alignment = WD_TABLE_ALIGNMENT.CENTER header_cells = table.rows[0].cells header_cells[0].text = '统计指标' header_cells[1].text = '数值' style_header_cell(header_cells[0]) style_header_cell(header_cells[1]) metrics = [ ('当前值', '{{analysis.statistics.currentWithUnit}}'), ('初始值', '{{analysis.statistics.initialWithUnit}}'), ('最大值', '{{analysis.statistics.maxWithUnit}}'), ('最小值', '{{analysis.statistics.minWithUnit}}'), ('平均值', '{{analysis.statistics.avgWithUnit}}'), ('中位数', '{{analysis.statistics.medianWithUnit}}'), ('变化量', '{{analysis.statistics.deltaWithUnit}}'), ('累计变化', '{{analysis.statistics.totalDeltaWithUnit}}'), ('数据量', '{{analysis.statistics.count}}'), ] for i, (label, value) in enumerate(metrics, start=1): row = table.rows[i] row.cells[0].text = label row.cells[1].text = value style_body_cell(row.cells[0]) style_body_cell(row.cells[1]) set_table_widths(table, [4.5, 11.0]) # ===== 6. 测点数据(循环表格) ===== add_heading(doc, '四、测点数据') # poi-tl LoopRowTableRenderPolicy 需要的结构: # 第1行:表头 # 第2行:{{listName}} 标记行(可以是空单元格) # 第3行:模板行,使用 [field] 语法 points_table = doc.add_table(rows=3, cols=8) points_table.style = 'Table Grid' points_table.alignment = WD_TABLE_ALIGNMENT.CENTER # 表头 points_headers = ['测点', '初始值', '当前值', '变化量', '变化速率', '最大值', '最小值', '预警状态'] points_header_row = points_table.rows[0] for i, header in enumerate(points_headers): points_header_row.cells[i].text = header style_header_cell(points_header_row.cells[i]) # 标记行:{{metricPoints}} points_marker_row = points_table.rows[1] points_marker_row.cells[0].text = '{{metricPoints}}' # 模板行:[field] 语法 points_data_row = points_table.rows[2] points_data = [ '[modelName]', '[initialValue]', '[currentValue]', '[changeValue]', '[changeRate]', '[maxValue]', '[minValue]', '[warningStatus]', ] for i, data in enumerate(points_data): points_data_row.cells[i].text = data style_body_cell(points_data_row.cells[i]) set_table_widths(points_table, [2.6, 1.84, 1.84, 1.84, 1.84, 1.84, 1.84, 1.84]) # ===== 7. 趋势分析 ===== add_heading(doc, '五、趋势分析') add_body(doc, '{{analysis.trend.description}}') # ===== 8. 数据质量 ===== add_heading(doc, '六、数据质量') add_body(doc, '{{analysis.quality.description}}') # ===== 9. 预警状态 ===== add_heading(doc, '七、预警状态') add_label_para(doc, '状态:', '{{analysis.warning.status}}') add_body(doc, '{{analysis.warning.description}}', indent=False) # ===== 10. 时程曲线(主指标 + 分组指标) ===== add_heading(doc, '八、时程曲线') chart_para1 = doc.add_paragraph() chart_para1.alignment = WD_ALIGN_PARAGRAPH.CENTER chart_para1.add_run('{{@charts.timeSeriesChart}}') chart_para2 = doc.add_paragraph() chart_para2.alignment = WD_ALIGN_PARAGRAPH.CENTER chart_para2.add_run('{{@charts.chart2}}') chart_para3 = doc.add_paragraph() chart_para3.alignment = WD_ALIGN_PARAGRAPH.CENTER chart_para3.add_run('{{@charts.chart3}}') # ===== 11. 专业分析建议 ===== add_heading(doc, '九、专业分析建议') add_body(doc, '{{professionalAdvice}}') # ===== 12. 结论(循环表格) ===== add_heading(doc, '十、结论') # poi-tl LoopRowTableRenderPolicy 结构 conclusion_table = doc.add_table(rows=3, cols=2) conclusion_table.style = 'Table Grid' conclusion_table.alignment = WD_TABLE_ALIGNMENT.CENTER # 表头 conclusion_header_row = conclusion_table.rows[0] conclusion_header_row.cells[0].text = '序号' conclusion_header_row.cells[1].text = '结论内容' style_header_cell(conclusion_header_row.cells[0]) style_header_cell(conclusion_header_row.cells[1]) # 标记行 conclusion_marker_row = conclusion_table.rows[1] conclusion_marker_row.cells[0].text = '{{conclusions}}' # 模板行 conclusion_data_row = conclusion_table.rows[2] conclusion_data_row.cells[0].text = '[seq]' conclusion_data_row.cells[1].text = '[text]' style_body_cell(conclusion_data_row.cells[0]) style_body_cell(conclusion_data_row.cells[1], WD_ALIGN_PARAGRAPH.LEFT) set_table_widths(conclusion_table, [1.5, 14.0]) # 保存文档 output_path = os.path.join(template_dir, f'{template_name}_V2.docx') doc.save(output_path) print(f'已生成模版: {output_path}') return output_path def main(): """主函数""" template_dir = '/Users/admin/IdeaProjects/fastbee/fastbee-report/src/main/resources/templates' device_types = { 'DEVICE_CRACK_METER': '裂缝计', 'DEVICE_DEEP_DISPLACEMENT': '深部位移', 'DEVICE_AXIAL_FORCE': '轴力/应力', 'DEVICE_STATIC_LEVEL': '静力水准', 'DEVICE_VIBRATING_WIRE': '振弦', 'DEVICE_STRAIN_GAUGE': '应变计', 'DEVICE_WATER_LEVEL': '水位计', 'DEVICE_RANGING_METER': '测距仪', 'DEVICE_INCLINOMETER': '倾角计', 'DEVICE_THERMOMETER': '温度计', 'DEVICE_RAIN_GAUGE': '雨量计', } print('开始生成专业报告模版 V2.2(修复循环表格语法)...') print(f'模版目录: {template_dir}') print() for template_name, device_type in device_types.items(): create_professional_template(template_name, device_type, template_dir) print() print('所有模版生成完成!') if __name__ == '__main__': main()