fastbee/generate_professional_templates.py
2026-09-10 11:47:39 +08:00

316 lines
10 KiB
Python
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.

#!/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()