1aaef71f52
Document-driven MVP with FastAPI backend, Vue H5, WeChat mini shell, product demo, and Docker dev stack. Co-authored-by: Cursor <cursoragent@cursor.com>
144 lines
4.7 KiB
Python
144 lines
4.7 KiB
Python
"""Server-side PDF export for posture reports."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from io import BytesIO
|
|
|
|
from reportlab.lib import colors
|
|
from reportlab.lib.pagesizes import A4
|
|
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
|
|
from reportlab.lib.units import mm
|
|
from reportlab.platypus import Paragraph, SimpleDocTemplate, Spacer, Table, TableStyle
|
|
|
|
from app.db.models import Child, Report
|
|
from app.services.reports import METRIC_LEVEL_LABELS, RISK_LABELS, report_to_dict
|
|
|
|
LEVEL_COLORS = {
|
|
"normal": colors.HexColor("#2bb673"),
|
|
"low": colors.HexColor("#84cc16"),
|
|
"medium": colors.HexColor("#f59e0b"),
|
|
"high": colors.HexColor("#ef4444"),
|
|
}
|
|
|
|
|
|
def build_report_pdf(report: Report, child: Child | None = None) -> bytes:
|
|
payload = report_to_dict(report)
|
|
buffer = BytesIO()
|
|
doc = SimpleDocTemplate(
|
|
buffer,
|
|
pagesize=A4,
|
|
leftMargin=18 * mm,
|
|
rightMargin=18 * mm,
|
|
topMargin=16 * mm,
|
|
bottomMargin=16 * mm,
|
|
title=f"Happy Up Report #{report.id}",
|
|
)
|
|
|
|
styles = getSampleStyleSheet()
|
|
title_style = ParagraphStyle(
|
|
"ReportTitle",
|
|
parent=styles["Heading1"],
|
|
fontName="Helvetica-Bold",
|
|
fontSize=18,
|
|
textColor=colors.HexColor("#1a6fb5"),
|
|
spaceAfter=8,
|
|
)
|
|
body = styles["BodyText"]
|
|
muted = ParagraphStyle("Muted", parent=body, textColor=colors.HexColor("#6b7280"), fontSize=9)
|
|
|
|
child_name = child.name if child else f"儿童 #{report.child_id}"
|
|
story = [
|
|
Paragraph("儿童 AI 体态管理 · 筛查报告", title_style),
|
|
Paragraph(f"儿童:{child_name} · 报告编号 #{report.id}", muted),
|
|
Spacer(1, 8),
|
|
]
|
|
|
|
if report.report_type == "movement_scoring":
|
|
story.extend(_movement_sections(payload, body, muted))
|
|
else:
|
|
story.extend(_screening_sections(payload, body, muted))
|
|
|
|
story.append(Spacer(1, 10))
|
|
story.append(Paragraph(payload.get("disclaimer", ""), muted))
|
|
|
|
doc.build(story)
|
|
return buffer.getvalue()
|
|
|
|
|
|
def _screening_sections(payload: dict, body, muted) -> list:
|
|
risk = payload.get("riskLevel", "medium")
|
|
risk_label = RISK_LABELS.get(risk, risk)
|
|
sections = [
|
|
Paragraph(f"<b>风险等级:</b>{risk_label}", body),
|
|
Paragraph(f"<b>摘要:</b>{payload.get('summary', '')}", body),
|
|
Spacer(1, 8),
|
|
Paragraph("<b>指标明细</b>", body),
|
|
]
|
|
|
|
rows = [["指标", "数值", "等级", "置信度"]]
|
|
for metric in payload.get("metrics", []):
|
|
rows.append(
|
|
[
|
|
metric.get("name", ""),
|
|
f"{metric.get('value', 0)}%",
|
|
METRIC_LEVEL_LABELS.get(metric.get("level", ""), metric.get("level", "")),
|
|
f"{metric.get('confidence', 0):.0%}",
|
|
]
|
|
)
|
|
|
|
table = Table(rows, colWidths=[80, 60, 60, 60])
|
|
table.setStyle(
|
|
TableStyle(
|
|
[
|
|
("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#eef2f7")),
|
|
("TEXTCOLOR", (0, 0), (-1, 0), colors.HexColor("#1f2937")),
|
|
("GRID", (0, 0), (-1, -1), 0.5, colors.HexColor("#e5e7eb")),
|
|
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
|
|
("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, colors.HexColor("#fafafa")]),
|
|
]
|
|
)
|
|
)
|
|
sections.append(table)
|
|
sections.append(Spacer(1, 8))
|
|
sections.append(Paragraph("<b>建议</b>", body))
|
|
for idx, item in enumerate(payload.get("recommendations", []), start=1):
|
|
sections.append(Paragraph(f"{idx}. {item}", body))
|
|
return sections
|
|
|
|
|
|
def _movement_sections(payload: dict, body, muted) -> list:
|
|
score = payload.get("score", 0)
|
|
sections = [
|
|
Paragraph(f"<b>跟练得分:</b>{score}", body),
|
|
Paragraph(
|
|
f"完成 {payload.get('repsCompleted', 0)} 组 · "
|
|
f"时长 {payload.get('durationSeconds', 0)} 秒",
|
|
muted,
|
|
),
|
|
Spacer(1, 8),
|
|
Paragraph("<b>维度评分</b>", body),
|
|
]
|
|
dims = payload.get("dimensions", {})
|
|
rows = [["维度", "得分"]]
|
|
for key, label in (
|
|
("trajectory", "轨迹"),
|
|
("angle", "角度"),
|
|
("rhythm", "节奏"),
|
|
("stability", "稳定性"),
|
|
("completion", "完成度"),
|
|
):
|
|
rows.append([label, str(dims.get(key, "-"))])
|
|
|
|
table = Table(rows, colWidths=[120, 80])
|
|
table.setStyle(
|
|
TableStyle(
|
|
[
|
|
("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#eef2f7")),
|
|
("GRID", (0, 0), (-1, -1), 0.5, colors.HexColor("#e5e7eb")),
|
|
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
|
|
]
|
|
)
|
|
)
|
|
sections.append(table)
|
|
return sections
|