316 lines
10 KiB
Python
316 lines
10 KiB
Python
#!/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() |