v2 4.9 该版本集成了Excel导出功能
This commit is contained in:
1 parent
e5030abf39
commit
86567eece5
16 files changed
+2516
-30
No files matched your search
Whitespace-only changes.
@@ -0,0 +1,226 @@
|
||||
from openpyxl import Workbook
|
||||
from openpyxl.styles import Font, Alignment, PatternFill, Border, Side
|
||||
|
||||
|
||||
class NutritionExcelBuilder:
|
||||
|
||||
def build(self, data, output_path):
|
||||
wb = Workbook()
|
||||
|
||||
self._create_user_sheet(wb, data)
|
||||
self._create_food_details_sheet(wb, data)
|
||||
self._create_category_sheet(wb, data)
|
||||
self._create_nutrition_sheet(wb, data)
|
||||
self._create_suggestions_sheet(wb, data)
|
||||
|
||||
wb.save(output_path)
|
||||
|
||||
def _create_user_sheet(self, wb, data):
|
||||
ws = wb.active
|
||||
ws.title = "个人信息"
|
||||
|
||||
header_fill = PatternFill(start_color="4F75B5", end_color="4F75B5", fill_type="solid")
|
||||
header_font = Font(name="微软雅黑", size=12, bold=True, color="FFFFFF")
|
||||
cell_font = Font(name="微软雅黑", size=11)
|
||||
cell_alignment = Alignment(horizontal="center", vertical="center")
|
||||
thin_border = Border(
|
||||
left=Side(style="thin"),
|
||||
right=Side(style="thin"),
|
||||
top=Side(style="thin"),
|
||||
bottom=Side(style="thin")
|
||||
)
|
||||
|
||||
headers = ["项目", "内容"]
|
||||
for col, header in enumerate(headers, 1):
|
||||
cell = ws.cell(row=1, column=col, value=header)
|
||||
cell.fill = header_fill
|
||||
cell.font = header_font
|
||||
cell.alignment = cell_alignment
|
||||
cell.border = thin_border
|
||||
|
||||
user = data.get("user", {})
|
||||
date = data.get("date", "")
|
||||
|
||||
rows = [
|
||||
("姓名", user.get("name", "")),
|
||||
("年龄", f"{user.get('age', '')} 岁"),
|
||||
("身高", f"{user.get('height', '')} cm"),
|
||||
("体重", f"{user.get('weight', '')} kg"),
|
||||
("日期", date),
|
||||
]
|
||||
|
||||
for row_idx, (label, value) in enumerate(rows, 2):
|
||||
ws.cell(row=row_idx, column=1, value=label).font = cell_font
|
||||
ws.cell(row=row_idx, column=1).alignment = cell_alignment
|
||||
ws.cell(row=row_idx, column=1).border = thin_border
|
||||
ws.cell(row=row_idx, column=2, value=value).font = cell_font
|
||||
ws.cell(row=row_idx, column=2).alignment = cell_alignment
|
||||
ws.cell(row=row_idx, column=2).border = thin_border
|
||||
|
||||
ws.column_dimensions["A"].width = 15
|
||||
ws.column_dimensions["B"].width = 20
|
||||
|
||||
def _create_food_details_sheet(self, wb, data):
|
||||
ws = wb.create_sheet("食物明细")
|
||||
|
||||
header_fill = PatternFill(start_color="4F75B5", end_color="4F75B5", fill_type="solid")
|
||||
header_font = Font(name="微软雅黑", size=11, bold=True, color="FFFFFF")
|
||||
cell_font = Font(name="微软雅黑", size=10)
|
||||
cell_alignment = Alignment(horizontal="center", vertical="center")
|
||||
thin_border = Border(
|
||||
left=Side(style="thin"),
|
||||
right=Side(style="thin"),
|
||||
top=Side(style="thin"),
|
||||
bottom=Side(style="thin")
|
||||
)
|
||||
|
||||
headers = ["食物名称", "分类", "摄入量(g)", "能量(kcal)", "蛋白质(g)", "脂肪(g)", "碳水(g)", "钙(mg)"]
|
||||
for col, header in enumerate(headers, 1):
|
||||
cell = ws.cell(row=1, column=col, value=header)
|
||||
cell.fill = header_fill
|
||||
cell.font = header_font
|
||||
cell.alignment = cell_alignment
|
||||
cell.border = thin_border
|
||||
|
||||
foods = data.get("foods", [])
|
||||
for row_idx, food in enumerate(foods, 2):
|
||||
nutrition = food.get("nutrition", {})
|
||||
row_data = [
|
||||
food.get("name", ""),
|
||||
food.get("category", ""),
|
||||
food.get("amount", ""),
|
||||
nutrition.get("energy", ""),
|
||||
nutrition.get("protein", ""),
|
||||
nutrition.get("fat", ""),
|
||||
nutrition.get("carbohydrate", ""),
|
||||
nutrition.get("calcium", ""),
|
||||
]
|
||||
for col_idx, value in enumerate(row_data, 1):
|
||||
cell = ws.cell(row=row_idx, column=col_idx, value=value)
|
||||
cell.font = cell_font
|
||||
cell.alignment = cell_alignment
|
||||
cell.border = thin_border
|
||||
|
||||
col_widths = [15, 10, 12, 12, 12, 12, 12, 12]
|
||||
for idx, width in enumerate(col_widths, 1):
|
||||
ws.column_dimensions[chr(64 + idx)].width = width
|
||||
|
||||
def _create_category_sheet(self, wb, data):
|
||||
ws = wb.create_sheet("分类评估")
|
||||
|
||||
header_fill = PatternFill(start_color="4F75B5", end_color="4F75B5", fill_type="solid")
|
||||
header_font = Font(name="微软雅黑", size=11, bold=True, color="FFFFFF")
|
||||
cell_font = Font(name="微软雅黑", size=10)
|
||||
cell_alignment = Alignment(horizontal="center", vertical="center")
|
||||
thin_border = Border(
|
||||
left=Side(style="thin"),
|
||||
right=Side(style="thin"),
|
||||
top=Side(style="thin"),
|
||||
bottom=Side(style="thin")
|
||||
)
|
||||
|
||||
headers = ["分类", "摄入量(g)", "评价"]
|
||||
for col, header in enumerate(headers, 1):
|
||||
cell = ws.cell(row=1, column=col, value=header)
|
||||
cell.fill = header_fill
|
||||
cell.font = header_font
|
||||
cell.alignment = cell_alignment
|
||||
cell.border = thin_border
|
||||
|
||||
category_result = data.get("category_result", {})
|
||||
row_idx = 2
|
||||
for cat, info in category_result.items():
|
||||
ws.cell(row=row_idx, column=1, value=cat).font = cell_font
|
||||
ws.cell(row=row_idx, column=1).alignment = cell_alignment
|
||||
ws.cell(row=row_idx, column=1).border = thin_border
|
||||
|
||||
ws.cell(row=row_idx, column=2, value=info.get("value", "")).font = cell_font
|
||||
ws.cell(row=row_idx, column=2).alignment = cell_alignment
|
||||
ws.cell(row=row_idx, column=2).border = thin_border
|
||||
|
||||
ws.cell(row=row_idx, column=3, value=info.get("evaluation", "")).font = cell_font
|
||||
ws.cell(row=row_idx, column=3).alignment = cell_alignment
|
||||
ws.cell(row=row_idx, column=3).border = thin_border
|
||||
row_idx += 1
|
||||
|
||||
ws.column_dimensions["A"].width = 15
|
||||
ws.column_dimensions["B"].width = 15
|
||||
ws.column_dimensions["C"].width = 15
|
||||
|
||||
def _create_nutrition_sheet(self, wb, data):
|
||||
ws = wb.create_sheet("营养总计")
|
||||
|
||||
header_fill = PatternFill(start_color="4F75B5", end_color="4F75B5", fill_type="solid")
|
||||
header_font = Font(name="微软雅黑", size=11, bold=True, color="FFFFFF")
|
||||
cell_font = Font(name="微软雅黑", size=10)
|
||||
cell_alignment = Alignment(horizontal="center", vertical="center")
|
||||
thin_border = Border(
|
||||
left=Side(style="thin"),
|
||||
right=Side(style="thin"),
|
||||
top=Side(style="thin"),
|
||||
bottom=Side(style="thin")
|
||||
)
|
||||
|
||||
headers = ["项目", "摄入量", "单位", "评价"]
|
||||
for col, header in enumerate(headers, 1):
|
||||
cell = ws.cell(row=1, column=col, value=header)
|
||||
cell.fill = header_fill
|
||||
cell.font = header_font
|
||||
cell.alignment = cell_alignment
|
||||
cell.border = thin_border
|
||||
|
||||
nutrition = data.get("nutrition", {})
|
||||
rows = [
|
||||
("总能量", nutrition.get("energy", {}).get("value", ""), "kcal/day", nutrition.get("energy", {}).get("evaluation", "")),
|
||||
("蛋白质", nutrition.get("protein", ""), "g", "-"),
|
||||
("脂肪", nutrition.get("fat", ""), "g", "-"),
|
||||
("碳水化合物", nutrition.get("carbohydrate", ""), "g", "-"),
|
||||
("钙", nutrition.get("calcium", {}).get("value", ""), "mg/day", nutrition.get("calcium", {}).get("evaluation", "")),
|
||||
]
|
||||
|
||||
for row_idx, row_data in enumerate(rows, 2):
|
||||
for col_idx, value in enumerate(row_data, 1):
|
||||
cell = ws.cell(row=row_idx, column=col_idx, value=value)
|
||||
cell.font = cell_font
|
||||
cell.alignment = cell_alignment
|
||||
cell.border = thin_border
|
||||
|
||||
ws.column_dimensions["A"].width = 15
|
||||
ws.column_dimensions["B"].width = 15
|
||||
ws.column_dimensions["C"].width = 12
|
||||
ws.column_dimensions["D"].width = 15
|
||||
|
||||
def _create_suggestions_sheet(self, wb, data):
|
||||
ws = wb.create_sheet("建议")
|
||||
|
||||
header_fill = PatternFill(start_color="4F75B5", end_color="4F75B5", fill_type="solid")
|
||||
header_font = Font(name="微软雅黑", size=11, bold=True, color="FFFFFF")
|
||||
cell_font = Font(name="微软雅黑", size=10)
|
||||
cell_alignment = Alignment(horizontal="left", vertical="center")
|
||||
thin_border = Border(
|
||||
left=Side(style="thin"),
|
||||
right=Side(style="thin"),
|
||||
top=Side(style="thin"),
|
||||
bottom=Side(style="thin")
|
||||
)
|
||||
|
||||
headers = ["序号", "建议内容"]
|
||||
for col, header in enumerate(headers, 1):
|
||||
cell = ws.cell(row=1, column=col, value=header)
|
||||
cell.fill = header_fill
|
||||
cell.font = header_font
|
||||
cell.alignment = cell_alignment
|
||||
cell.border = thin_border
|
||||
|
||||
suggestions = data.get("suggestions", [])
|
||||
for row_idx, suggestion in enumerate(suggestions, 2):
|
||||
ws.cell(row=row_idx, column=1, value=row_idx - 1).font = cell_font
|
||||
ws.cell(row=row_idx, column=1).alignment = cell_alignment
|
||||
ws.cell(row=row_idx, column=1).border = thin_border
|
||||
|
||||
ws.cell(row=row_idx, column=2, value=suggestion).font = cell_font
|
||||
ws.cell(row=row_idx, column=2).alignment = cell_alignment
|
||||
ws.cell(row=row_idx, column=2).border = thin_border
|
||||
|
||||
ws.column_dimensions["A"].width = 10
|
||||
ws.column_dimensions["B"].width = 50
|
||||
@@ -0,0 +1,250 @@
|
||||
import os
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib.font_manager as fm
|
||||
from PIL import Image as PILImage
|
||||
|
||||
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, Image as RLImage, PageBreak
|
||||
from reportlab.lib.styles import ParagraphStyle
|
||||
from reportlab.lib.units import mm
|
||||
from reportlab.lib import colors
|
||||
from reportlab.platypus import TableStyle
|
||||
from reportlab.pdfbase.ttfonts import TTFont
|
||||
from reportlab.pdfbase import pdfmetrics
|
||||
from reportlab.lib.pagesizes import A4
|
||||
|
||||
|
||||
class NutritionPDFBuilder:
|
||||
|
||||
def __init__(self):
|
||||
pdfmetrics.registerFont(TTFont('SimHei', 'C:/Windows/Fonts/simhei.ttf'))
|
||||
|
||||
def build(self, data, output_path):
|
||||
|
||||
doc = SimpleDocTemplate(output_path, pagesize=A4)
|
||||
story = []
|
||||
|
||||
# ===== 统一字体 =====
|
||||
normal = ParagraphStyle(
|
||||
"normal",
|
||||
fontName="SimHei",
|
||||
fontSize=13,
|
||||
leading=18,
|
||||
alignment=1
|
||||
)
|
||||
|
||||
header = ParagraphStyle(
|
||||
"header",
|
||||
fontName="SimHei",
|
||||
fontSize=13,
|
||||
leading=18,
|
||||
alignment=1,
|
||||
textColor=colors.white
|
||||
)
|
||||
|
||||
story.append(Paragraph("营养健康评估报告", normal))
|
||||
story.append(Spacer(1, 10 * mm))
|
||||
|
||||
# ================= 食物表 =================
|
||||
food_table = []
|
||||
|
||||
food_table.append([Paragraph("食物摄入量评估报告", header), "", "", ""])
|
||||
food_table.append([
|
||||
Paragraph("种类", header),
|
||||
Paragraph("图片", header),
|
||||
Paragraph("摄入量", header),
|
||||
Paragraph("评价", header)
|
||||
])
|
||||
|
||||
for cat, info in data["category_result"].items():
|
||||
food_table.append([
|
||||
Paragraph(cat, normal),
|
||||
self._get_image(cat),
|
||||
Paragraph(f"{info['value']} g", normal),
|
||||
Paragraph(info["evaluation"], normal)
|
||||
])
|
||||
|
||||
# ===== 建议(适中行距)=====
|
||||
suggestion_text = "<br/>".join(
|
||||
[f"{i+1}. {s}" for i, s in enumerate(data["suggestions"])]
|
||||
)
|
||||
|
||||
food_table.append([
|
||||
Paragraph("整体评估及建议", normal),
|
||||
Paragraph(suggestion_text, normal),
|
||||
"",
|
||||
""
|
||||
])
|
||||
|
||||
t1 = Table(
|
||||
food_table,
|
||||
colWidths=[40*mm, 55*mm, 40*mm, 30*mm],
|
||||
repeatRows=2
|
||||
)
|
||||
|
||||
t1.setStyle(TableStyle([
|
||||
("SPAN", (0,0), (-1,0)),
|
||||
("SPAN", (1,-1), (-1,-1)),
|
||||
|
||||
("BACKGROUND", (0,0), (-1,1), colors.HexColor("#4F75B5")),
|
||||
("TEXTCOLOR", (0,0), (-1,1), colors.white),
|
||||
|
||||
("GRID",(0,0),(-1,-1),0.5,colors.grey),
|
||||
|
||||
("FONTNAME",(0,0),(-1,-1),"SimHei"),
|
||||
|
||||
("ALIGN",(0,0),(-1,-1),"CENTER"),
|
||||
("VALIGN",(0,0),(-1,-1),"MIDDLE"),
|
||||
|
||||
("TOPPADDING",(0,0),(-1,-1),14),
|
||||
("BOTTOMPADDING",(0,0),(-1,-1),14),
|
||||
|
||||
("ROWHEIGHT",(0,2),(-1,-2),45)
|
||||
]))
|
||||
|
||||
story.append(t1)
|
||||
story.append(PageBreak())
|
||||
|
||||
# ================= 饼图 =================
|
||||
pie_path = "temp_pie.png"
|
||||
self._generate_pie(data, pie_path)
|
||||
|
||||
n = data["nutrition"]
|
||||
|
||||
energy_table = [
|
||||
[Paragraph("能量及钙摄入量评估报告", header), "", ""],
|
||||
|
||||
[
|
||||
Paragraph("项目", header),
|
||||
Paragraph("摄入量", header),
|
||||
Paragraph("评价", header)
|
||||
],
|
||||
|
||||
[
|
||||
Paragraph("总能量", normal),
|
||||
Paragraph(f"{n['energy']['value']} kcal/day", normal),
|
||||
Paragraph(n["energy"]["evaluation"], normal)
|
||||
],
|
||||
|
||||
[
|
||||
Paragraph("三大营养素供能占比", normal),
|
||||
self._get_pie(pie_path),
|
||||
""
|
||||
],
|
||||
|
||||
[
|
||||
Paragraph("钙", normal),
|
||||
Paragraph(f"{n['calcium']['value']} mg/day", normal),
|
||||
Paragraph(n["calcium"]["evaluation"], normal)
|
||||
],
|
||||
]
|
||||
|
||||
# ===== 解读(适中间距)=====
|
||||
interpretation = []
|
||||
|
||||
if n["energy"]["evaluation"] == "不足":
|
||||
interpretation.append("能量摄入不足,应增加碳水化合物摄入")
|
||||
|
||||
if n["calcium"]["evaluation"] == "不足":
|
||||
interpretation.append("钙摄入不足,应增加奶制品摄入")
|
||||
|
||||
text = "<br/>".join(interpretation)
|
||||
|
||||
energy_table.append([
|
||||
Paragraph("解读", normal),
|
||||
Paragraph(text, normal),
|
||||
""
|
||||
])
|
||||
|
||||
t2 = Table(energy_table, colWidths=[50*mm, 70*mm, 40*mm])
|
||||
|
||||
t2.setStyle(TableStyle([
|
||||
("SPAN", (0,0), (-1,0)),
|
||||
("SPAN", (1,3), (2,3)),
|
||||
("SPAN", (1,-1), (2,-1)),
|
||||
|
||||
("BACKGROUND",(0,0),(-1,1),colors.HexColor("#4F75B5")),
|
||||
("TEXTCOLOR",(0,0),(-1,1),colors.white),
|
||||
|
||||
("GRID",(0,0),(-1,-1),0.5,colors.grey),
|
||||
|
||||
("FONTNAME",(0,0),(-1,-1),"SimHei"),
|
||||
|
||||
("ALIGN",(0,0),(-1,-1),"CENTER"),
|
||||
("VALIGN",(0,0),(-1,-1),"MIDDLE"),
|
||||
|
||||
("TOPPADDING",(0,0),(-1,-1),14),
|
||||
("BOTTOMPADDING",(0,0),(-1,-1),14),
|
||||
]))
|
||||
|
||||
story.append(t2)
|
||||
|
||||
doc.build(story)
|
||||
|
||||
# ================= 图片 =================
|
||||
def _get_image(self, category):
|
||||
|
||||
base = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
path = os.path.join(base, "static", "images", f"{category}.png")
|
||||
|
||||
if os.path.exists(path):
|
||||
return RLImage(path, width=35*mm, height=35*mm)
|
||||
|
||||
return ""
|
||||
|
||||
# ================= 饼图 =================
|
||||
def _generate_pie(self, data, path):
|
||||
|
||||
font_path = r"C:\Windows\Fonts\simhei.ttf"
|
||||
font_prop = fm.FontProperties(fname=font_path)
|
||||
|
||||
protein = float(data["nutrition"].get("protein", 0))
|
||||
fat = float(data["nutrition"].get("fat", 0))
|
||||
carb = float(data["nutrition"].get("carbohydrate", 0))
|
||||
|
||||
protein_kcal = protein * 4
|
||||
fat_kcal = fat * 9
|
||||
carb_kcal = carb * 4
|
||||
|
||||
total = protein_kcal + fat_kcal + carb_kcal
|
||||
|
||||
values = [1,1,1] if total == 0 else [
|
||||
protein_kcal / total,
|
||||
fat_kcal / total,
|
||||
carb_kcal / total
|
||||
]
|
||||
|
||||
labels = [
|
||||
f"蛋白质 {protein}g",
|
||||
f"脂肪 {fat}g",
|
||||
f"碳水 {carb}g"
|
||||
]
|
||||
|
||||
plt.figure(figsize=(6,6))
|
||||
plt.gca().set_aspect('equal')
|
||||
|
||||
plt.pie(
|
||||
values,
|
||||
labels=labels,
|
||||
autopct="%1.1f%%",
|
||||
startangle=90,
|
||||
textprops={'fontproperties': font_prop, 'fontsize': 12}
|
||||
)
|
||||
|
||||
plt.title("三大营养素供能占比", fontproperties=font_prop)
|
||||
|
||||
plt.savefig(path, bbox_inches='tight')
|
||||
plt.close()
|
||||
|
||||
# ================= 饼图插入 =================
|
||||
def _get_pie(self, path):
|
||||
|
||||
if not os.path.exists(path):
|
||||
return ""
|
||||
|
||||
img = PILImage.open(path)
|
||||
w, h = img.size
|
||||
|
||||
target_w = 90 * mm
|
||||
target_h = target_w * (h / w)
|
||||
|
||||
return RLImage(path, width=target_w, height=target_h)
|
||||
Reference in new issue
Block a user