2025-12-14 18:19:41 +08:00
|
|
|
|
#!/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
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-12-14 19:47:17 +08:00
|
|
|
|
def parse_args():
|
2025-12-14 18:19:41 +08:00
|
|
|
|
"""解析命令行参数"""
|
|
|
|
|
|
parser = argparse.ArgumentParser(description='PDF Report Generation Script')
|
2025-12-14 19:47:17 +08:00
|
|
|
|
parser.add_argument('--score', '--grade', required=True, help='最终成绩文件')
|
2025-12-14 18:19:41 +08:00
|
|
|
|
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='学生姓名')
|
2025-12-14 19:47:17 +08:00
|
|
|
|
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')
|
2025-12-14 18:19:41 +08:00
|
|
|
|
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()
|