#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ generate_pdf_report.py - PDF报告生成脚本 将评分结果和报告内容整合生成PDF文件 """ import json import argparse import sys import os from datetime import datetime # 尝试导入reportlab,如果不存在则显示错误 try: from reportlab.lib.pagesizes import A4 from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_JUSTIFY from reportlab.platypus import (SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, Image, PageBreak) from reportlab.lib import colors from reportlab.lib.units import cm REPORTLAB_AVAILABLE = True except ImportError: REPORTLAB_AVAILABLE = False def parse_args(): """解析命令行参数""" parser = argparse.ArgumentParser(description='PDF Report Generation Script') parser.add_argument('--score', '--grade', required=True, help='最终成绩文件') parser.add_argument('--report', required=True, help='REPORT.md文件路径') parser.add_argument('--frontend', required=True, help='FRONTEND.md文件路径') parser.add_argument('--out', required=True, help='输出PDF文件路径') parser.add_argument('--student-id', required=True, help='学生ID') parser.add_argument('--student-name', required=True, help='学生姓名') parser.add_argument('--class-id', '--class-name', help='班级ID/班级名称') parser.add_argument('--assignment-id', default='2311061111', help='作业ID') parser.add_argument('--images', help='图片目录路径') parser.add_argument('--commit-sha', help='提交SHA') return parser.parse_args() def load_file_content(file_path: str) -> str: """加载文件内容""" with open(file_path, 'r', encoding='utf-8') as f: return f.read() def load_json_file(file_path: str) -> dict: """加载JSON文件内容""" with open(file_path, 'r', encoding='utf-8') as f: return json.load(f) def markdown_to_paragraphs(markdown_text: str) -> list: """将Markdown文本转换为ReportLab可处理的段落列表""" # 简单的Markdown处理,主要处理标题和段落 paragraphs = [] lines = markdown_text.split('\n') current_paragraph = "" for line in lines: line = line.strip() if not line: if current_paragraph: paragraphs.append(('paragraph', current_paragraph)) current_paragraph = "" continue # 处理标题 if line.startswith('# '): if current_paragraph: paragraphs.append(('paragraph', current_paragraph)) current_paragraph = "" paragraphs.append(('h1', line[2:])) elif line.startswith('## '): if current_paragraph: paragraphs.append(('paragraph', current_paragraph)) current_paragraph = "" paragraphs.append(('h2', line[3:])) elif line.startswith('### '): if current_paragraph: paragraphs.append(('paragraph', current_paragraph)) current_paragraph = "" paragraphs.append(('h3', line[4:])) elif line.startswith('#### '): if current_paragraph: paragraphs.append(('paragraph', current_paragraph)) current_paragraph = "" paragraphs.append(('h4', line[5:])) # 处理列表 elif line.startswith('- '): if current_paragraph: paragraphs.append(('paragraph', current_paragraph)) current_paragraph = "" paragraphs.append(('list_item', line[2:])) elif line.startswith('* '): if current_paragraph: paragraphs.append(('paragraph', current_paragraph)) current_paragraph = "" paragraphs.append(('list_item', line[2:])) elif line.startswith('1. '): if current_paragraph: paragraphs.append(('paragraph', current_paragraph)) current_paragraph = "" paragraphs.append(('list_item', line[3:])) # 处理代码块 elif line.startswith('```'): if current_paragraph: paragraphs.append(('paragraph', current_paragraph)) current_paragraph = "" paragraphs.append(('code_block_start', '')) elif paragraphs and paragraphs[-1][0] == 'code_block_start': if line.startswith('```'): paragraphs.append(('code_block_end', '')) else: paragraphs.append(('code_block', line)) # 处理引用 elif line.startswith('> '): if current_paragraph: paragraphs.append(('paragraph', current_paragraph)) current_paragraph = "" paragraphs.append(('quote', line[2:])) # 处理粗体和斜体 else: current_paragraph += ' ' + line if current_paragraph else line if current_paragraph: paragraphs.append(('paragraph', current_paragraph)) return paragraphs def generate_pdf_report(args, score_data, report_content, frontend_content): """生成PDF报告""" if not REPORTLAB_AVAILABLE: print("❌ ERROR: reportlab库未安装,无法生成PDF报告") print("请运行: pip install reportlab") return False # 创建PDF文档 doc = SimpleDocTemplate(args.out, pagesize=A4) story = [] # 获取样式 styles = getSampleStyleSheet() # 自定义样式 styles.add(ParagraphStyle( name='CustomHeading1', parent=styles['Heading1'], fontSize=24, alignment=TA_CENTER, textColor=colors.HexColor('#2c3e50'), spaceAfter=30 )) styles.add(ParagraphStyle( name='CustomHeading2', parent=styles['Heading2'], fontSize=18, textColor=colors.HexColor('#34495e'), spaceAfter=15, spaceBefore=15 )) styles.add(ParagraphStyle( name='CustomHeading3', parent=styles['Heading3'], fontSize=14, textColor=colors.HexColor('#7f8c8d'), spaceAfter=10 )) styles.add(ParagraphStyle( name='CustomParagraph', parent=styles['Normal'], fontSize=12, leading=18, alignment=TA_JUSTIFY, textColor=colors.HexColor('#000000') )) styles.add(ParagraphStyle( name='CustomListItem', parent=styles['CustomParagraph'], leftIndent=20, bulletIndent=10 )) styles.add(ParagraphStyle( name='CustomCentered', parent=styles['CustomParagraph'], alignment=TA_CENTER )) styles.add(ParagraphStyle( name='ScoreStyle', parent=styles['CustomCentered'], fontSize=36, textColor=colors.HexColor('#e74c3c'), fontWeight='bold', spaceAfter=20 )) # 封面 story.append(Spacer(1, 5*cm)) story.append(Paragraph("作业成绩报告", styles['CustomHeading1'])) story.append(Spacer(1, 2*cm)) # 学生信息 student_info = [ ['学生姓名:', args.student_name], ['学生ID:', args.student_id], ['班级ID:', args.class_id], ['作业ID:', args.assignment_id], ['提交日期:', datetime.now().strftime('%Y-%m-%d')] ] table_style = TableStyle([ ('ALIGN', (0, 0), (-1, -1), 'LEFT'), ('FONTNAME', (0, 0), (-1, -1), 'Helvetica'), ('FONTSIZE', (0, 0), (-1, -1), 12), ('BOTTOMPADDING', (0, 0), (-1, -1), 10), ('RIGHTPADDING', (0, 0), (0, -1), 20), ]) info_table = Table(student_info, colWidths=[100, 200]) info_table.setStyle(table_style) story.append(Spacer(1, 1*cm)) story.append(info_table) # 最终成绩 story.append(Spacer(1, 3*cm)) story.append(Paragraph("最终成绩", styles['CustomHeading2'])) story.append(Paragraph(f"{score_data['final_score']:.2f}/100", styles['ScoreStyle'])) # 成绩等级 if score_data['final_score'] >= 90: grade = "优秀" grade_color = colors.HexColor('#27ae60') elif score_data['final_score'] >= 80: grade = "良好" grade_color = colors.HexColor('#f39c12') elif score_data['final_score'] >= 70: grade = "中等" grade_color = colors.HexColor('#e67e22') elif score_data['final_score'] >= 60: grade = "及格" grade_color = colors.HexColor('#d35400') else: grade = "不及格" grade_color = colors.HexColor('#c0392b') story.append(Paragraph(f"等级: {grade}", ParagraphStyle(name='GradeStyle', parent=styles['CustomCentered'], fontSize=20, textColor=grade_color))) story.append(PageBreak()) # 成绩分布 story.append(Paragraph("成绩分布", styles['CustomHeading2'])) distribution_data = [ ['部分', '得分', '权重', '占总分百分比'] ] for part, data in score_data['breakdown'].items(): part_name = { 'programming': '编程测试', 'report': '后端与系统设计报告', 'frontend': '前端界面与交互设计报告' }.get(part, part) distribution_data.append([ part_name, f"{data['score']:.2f}", f"{data['weight']*100:.0f}%", f"{data['percentage']:.2f}" ]) distribution_table = Table(distribution_data, colWidths=[150, 60, 60, 100]) distribution_table.setStyle(TableStyle([ ('ALIGN', (0, 0), (-1, -1), 'CENTER'), ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'), ('FONTSIZE', (0, 0), (-1, -1), 12), ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#f8f9fa')), ('TEXTCOLOR', (0, 0), (-1, 0), colors.HexColor('#2c3e50')), ('BOTTOMPADDING', (0, 0), (-1, -1), 10), ('TOPPADDING', (0, 0), (-1, -1), 10), ('GRID', (0, 0), (-1, -1), 1, colors.HexColor('#dee2e6')) ])) story.append(Spacer(1, 1*cm)) story.append(distribution_table) story.append(Spacer(1, 2*cm)) # 编程测试详情 story.append(Paragraph("编程测试详情", styles['CustomHeading2'])) programming = score_data['details']['programming'] programming_groups = programming.get('groups', []) if programming_groups: prog_data = [['测试组', '通过测试', '总测试数', '得分', '通过率']] for group in programming_groups: prog_data.append([ group['name'], group['passed'], group['total'], f"{group['score']:.2f}", f"{group['passed']/group['total']*100:.1f}%" ]) prog_table = Table(prog_data, colWidths=[100, 70, 70, 60, 70]) prog_table.setStyle(TableStyle([ ('ALIGN', (0, 0), (-1, -1), 'CENTER'), ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'), ('FONTSIZE', (0, 0), (-1, -1), 12), ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#f8f9fa')), ('TEXTCOLOR', (0, 0), (-1, 0), colors.HexColor('#2c3e50')), ('BOTTOMPADDING', (0, 0), (-1, -1), 8), ('TOPPADDING', (0, 0), (-1, -1), 8), ('GRID', (0, 0), (-1, -1), 1, colors.HexColor('#dee2e6')) ])) story.append(Spacer(1, 1*cm)) story.append(prog_table) else: story.append(Paragraph("暂无编程测试详情", styles['CustomParagraph'])) story.append(Spacer(1, 2*cm)) story.append(PageBreak()) # 后端与系统设计报告 story.append(Paragraph("后端与系统设计报告", styles['CustomHeading2'])) story.append(Spacer(1, 1*cm)) report_paragraphs = markdown_to_paragraphs(report_content) in_code_block = False for para_type, content in report_paragraphs: if para_type == 'h1': story.append(Paragraph(content, styles['CustomHeading1'])) elif para_type == 'h2': story.append(Paragraph(content, styles['CustomHeading2'])) elif para_type == 'h3': story.append(Paragraph(content, styles['CustomHeading3'])) elif para_type == 'paragraph': story.append(Paragraph(content, styles['CustomParagraph'])) story.append(Spacer(1, 0.5*cm)) elif para_type == 'list_item': story.append(Paragraph(f'• {content}', styles['CustomListItem'])) story.append(Spacer(1, 0.2*cm)) elif para_type == 'quote': story.append(Paragraph(content, ParagraphStyle(name='QuoteStyle', parent=styles['CustomParagraph'], leftIndent=20, rightIndent=20, borderLeftWidth=3, borderLeftColor=colors.HexColor('#bdc3c7')))) story.append(Spacer(1, 0.5*cm)) elif para_type == 'code_block_start': in_code_block = True story.append(Paragraph('```', styles['CustomParagraph'])) elif para_type == 'code_block_end': in_code_block = False story.append(Paragraph('```', styles['CustomParagraph'])) story.append(Spacer(1, 0.5*cm)) elif para_type == 'code_block' and in_code_block: story.append(Paragraph(content, ParagraphStyle(name='CodeStyle', parent=styles['CustomParagraph'], fontName='Courier', fontSize=10, leftIndent=20, rightIndent=20))) story.append(PageBreak()) # 前端界面与交互设计报告 story.append(Paragraph("前端界面与交互设计报告", styles['CustomHeading2'])) story.append(Spacer(1, 1*cm)) frontend_paragraphs = markdown_to_paragraphs(frontend_content) in_code_block = False for para_type, content in frontend_paragraphs: if para_type == 'h1': story.append(Paragraph(content, styles['CustomHeading1'])) elif para_type == 'h2': story.append(Paragraph(content, styles['CustomHeading2'])) elif para_type == 'h3': story.append(Paragraph(content, styles['CustomHeading3'])) elif para_type == 'paragraph': story.append(Paragraph(content, styles['CustomParagraph'])) story.append(Spacer(1, 0.5*cm)) elif para_type == 'list_item': story.append(Paragraph(f'• {content}', styles['CustomListItem'])) story.append(Spacer(1, 0.2*cm)) elif para_type == 'quote': story.append(Paragraph(content, ParagraphStyle(name='QuoteStyle', parent=styles['CustomParagraph'], leftIndent=20, rightIndent=20, borderLeftWidth=3, borderLeftColor=colors.HexColor('#bdc3c7')))) story.append(Spacer(1, 0.5*cm)) elif para_type == 'code_block_start': in_code_block = True story.append(Paragraph('```', styles['CustomParagraph'])) elif para_type == 'code_block_end': in_code_block = False story.append(Paragraph('```', styles['CustomParagraph'])) story.append(Spacer(1, 0.5*cm)) elif para_type == 'code_block' and in_code_block: story.append(Paragraph(content, ParagraphStyle(name='CodeStyle', parent=styles['CustomParagraph'], fontName='Courier', fontSize=10, leftIndent=20, rightIndent=20))) # 构建并保存PDF try: doc.build(story) return True except Exception as e: print(f"❌ PDF生成失败: {str(e)}") return False def generate_text_report(args, score_data, report_content, frontend_content): """生成纯文本报告作为备选方案""" text_report = [] text_report.append("=" * 80) text_report.append(" 作业成绩报告 ") text_report.append("=" * 80) text_report.append("") # 学生信息 text_report.append(f"学生姓名: {args.student_name}") text_report.append(f"学生ID: {args.student_id}") text_report.append(f"班级ID: {args.class_id}") text_report.append(f"作业ID: {args.assignment_id}") text_report.append(f"提交日期: {datetime.now().strftime('%Y-%m-%d')}") text_report.append("") # 最终成绩 text_report.append("-" * 80) text_report.append(f"最终成绩: {score_data['final_score']:.2f}/100") # 成绩等级 if score_data['final_score'] >= 90: grade = "优秀" elif score_data['final_score'] >= 80: grade = "良好" elif score_data['final_score'] >= 70: grade = "中等" elif score_data['final_score'] >= 60: grade = "及格" else: grade = "不及格" text_report.append(f"等级: {grade}") text_report.append("") # 成绩分布 text_report.append("成绩分布:") text_report.append("| 部分 | 得分 | 权重 | 占总分百分比 |") text_report.append("|------|------|------|------------|") for part, data in score_data['breakdown'].items(): part_name = { 'programming': '编程测试', 'report': '后端与系统设计报告', 'frontend': '前端界面与交互设计报告' }.get(part, part) text_report.append(f"| {part_name} | {data['score']:.2f} | {data['weight']*100:.0f}% | {data['percentage']:.2f} |") text_report.append("") text_report.append("=" * 80) text_report.append("") # 保存文本报告 text_report_path = args.out.replace('.pdf', '.txt') with open(text_report_path, 'w', encoding='utf-8') as f: f.write('\n'.join(text_report)) print(f"✅ 已生成纯文本报告: {text_report_path}") return True def main(): """主函数""" args = parse_args() print("📁 加载评分数据...") score_data = load_json_file(args.score) print("📁 加载后端报告内容...") report_content = load_file_content(args.report) print("📁 加载前端报告内容...") frontend_content = load_file_content(args.frontend) print("📄 生成PDF报告...") pdf_success = generate_pdf_report(args, score_data, report_content, frontend_content) # 如果PDF生成失败,生成纯文本报告 if not pdf_success: print("🔄 尝试生成纯文本报告...") generate_text_report(args, score_data, report_content, frontend_content) else: print(f"✅ PDF报告生成成功: {args.out}") # 生成一个简单的总结文件 summary = { "student_info": { "name": args.student_name, "id": args.student_id, "class_id": args.class_id, "assignment_id": args.assignment_id }, "final_score": score_data["final_score"], "report_path": args.out, "generated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "status": "completed" } summary_path = args.out.replace('.pdf', '_summary.json') with open(summary_path, 'w', encoding='utf-8') as f: json.dump(summary, f, ensure_ascii=False, indent=2) print(f"✅ 总结文件生成成功: {summary_path}") if __name__ == '__main__': main()