v2 4.9 该版本集成了Excel导出功能

This commit is contained in:
赖力诚 committed 2026-04-09 15:19:48 +08:00
1 parent e5030abf39
commit 86567eece5
16 files changed
+2516 -30

No files matched your search

View File
Whitespace-only changes.
+226
View File
@@ -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
+250
View File
@@ -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)