Initial commit: 个性化饮食推荐助手 - 包含OCR识别、AI分析、现代化界面等功能

This commit is contained in:
赵杰
2025-09-25 14:20:11 +01:00
commit aea5f6bf74
27 changed files with 14015 additions and 0 deletions

579
gui/smart_meal_record.py Normal file
View File

@@ -0,0 +1,579 @@
"""
简化的餐食记录界面
使用选择式输入,减少用户负担
"""
import tkinter as tk
from tkinter import ttk, messagebox
import customtkinter as ctk
from typing import Dict, List, Optional
from smart_food.smart_database import (
search_foods, get_food_categories, get_foods_by_category,
get_portion_options, estimate_calories, record_meal_smart
)
class SmartMealRecordDialog:
"""智能餐食记录对话框"""
def __init__(self, parent, user_id: str, meal_type: str = "lunch"):
self.parent = parent
self.user_id = user_id
self.meal_type = meal_type
self.selected_foods = []
# 创建对话框
self.dialog = ctk.CTkToplevel(parent)
self.dialog.title(f"记录{self._get_meal_name(meal_type)}")
self.dialog.geometry("600x700")
self.dialog.transient(parent)
self.dialog.grab_set()
# 居中显示
self.dialog.geometry("+%d+%d" % (parent.winfo_rootx() + 50, parent.winfo_rooty() + 50))
self._create_widgets()
def _get_meal_name(self, meal_type: str) -> str:
"""获取餐次中文名称"""
meal_names = {
"breakfast": "早餐",
"lunch": "午餐",
"dinner": "晚餐"
}
return meal_names.get(meal_type, "餐食")
def _create_widgets(self):
"""创建界面组件"""
# 主框架
main_frame = ctk.CTkScrollableFrame(self.dialog)
main_frame.pack(fill="both", expand=True, padx=20, pady=20)
# 标题
title_label = ctk.CTkLabel(
main_frame,
text=f"🍽️ 记录{self._get_meal_name(self.meal_type)}",
font=ctk.CTkFont(size=20, weight="bold")
)
title_label.pack(pady=10)
# 食物选择区域
self._create_food_selection(main_frame)
# 已选食物列表
self._create_selected_foods(main_frame)
# 满意度评分
self._create_satisfaction_rating(main_frame)
# 备注
self._create_notes_section(main_frame)
# 按钮区域
self._create_buttons(main_frame)
def _create_food_selection(self, parent):
"""创建食物选择区域"""
# 食物选择框架
food_frame = ctk.CTkFrame(parent)
food_frame.pack(fill="x", padx=10, pady=10)
# 标题
food_title = ctk.CTkLabel(
food_frame,
text="选择食物",
font=ctk.CTkFont(size=16, weight="bold")
)
food_title.pack(pady=10)
# 食物搜索
search_frame = ctk.CTkFrame(food_frame)
search_frame.pack(fill="x", padx=10, pady=5)
search_label = ctk.CTkLabel(search_frame, text="搜索食物:")
search_label.pack(anchor="w", padx=5, pady=2)
self.search_var = tk.StringVar()
self.search_entry = ctk.CTkEntry(search_frame, textvariable=self.search_var, placeholder_text="输入食物名称搜索...")
self.search_entry.pack(fill="x", padx=5, pady=2)
self.search_entry.bind("<KeyRelease>", self._on_search_changed)
# 搜索结果
self.search_results_frame = ctk.CTkFrame(food_frame)
self.search_results_frame.pack(fill="x", padx=10, pady=5)
self.search_results_label = ctk.CTkLabel(self.search_results_frame, text="搜索结果:")
self.search_results_label.pack(anchor="w", padx=5, pady=2)
self.search_results_menu = ctk.CTkOptionMenu(
self.search_results_frame,
values=[],
command=self._on_search_result_selected
)
self.search_results_menu.pack(fill="x", padx=5, pady=2)
# 分隔线
separator = ctk.CTkFrame(food_frame, height=2)
separator.pack(fill="x", padx=10, pady=5)
# 分类选择
category_frame = ctk.CTkFrame(food_frame)
category_frame.pack(fill="x", padx=10, pady=5)
category_label = ctk.CTkLabel(category_frame, text="食物分类:")
category_label.pack(side="left", padx=5)
self.category_var = tk.StringVar(value="主食")
self.category_menu = ctk.CTkOptionMenu(
category_frame,
variable=self.category_var,
values=get_food_categories(),
command=self._on_category_changed
)
self.category_menu.pack(side="left", padx=5)
# 食物选择
food_select_frame = ctk.CTkFrame(food_frame)
food_select_frame.pack(fill="x", padx=10, pady=5)
food_label = ctk.CTkLabel(food_select_frame, text="选择食物:")
food_label.pack(anchor="w", padx=5, pady=2)
self.food_var = tk.StringVar()
self.food_menu = ctk.CTkOptionMenu(
food_select_frame,
variable=self.food_var,
values=[]
)
self.food_menu.pack(fill="x", padx=5, pady=2)
# 分量选择
portion_frame = ctk.CTkFrame(food_frame)
portion_frame.pack(fill="x", padx=10, pady=5)
portion_label = ctk.CTkLabel(portion_frame, text="分量:")
portion_label.pack(anchor="w", padx=5, pady=2)
self.portion_var = tk.StringVar(value="适量")
self.portion_menu = ctk.CTkOptionMenu(
portion_frame,
variable=self.portion_var,
values=["适量"]
)
self.portion_menu.pack(fill="x", padx=5, pady=2)
# 添加按钮
add_button = ctk.CTkButton(
food_frame,
text="添加到餐食",
command=self._add_food,
width=150
)
add_button.pack(pady=10)
# AI分析按钮
ai_analyze_button = ctk.CTkButton(
food_frame,
text="AI分析食物",
command=self._analyze_food_with_ai,
width=150,
fg_color="green"
)
ai_analyze_button.pack(pady=5)
# 初始化食物列表
self._on_category_changed("主食")
def _create_selected_foods(self, parent):
"""创建已选食物列表"""
# 已选食物框架
selected_frame = ctk.CTkFrame(parent)
selected_frame.pack(fill="x", padx=10, pady=10)
# 标题
selected_title = ctk.CTkLabel(
selected_frame,
text="已选食物",
font=ctk.CTkFont(size=16, weight="bold")
)
selected_title.pack(pady=10)
# 食物列表
self.foods_listbox = tk.Listbox(selected_frame, height=6)
self.foods_listbox.pack(fill="x", padx=10, pady=5)
# 删除按钮
delete_button = ctk.CTkButton(
selected_frame,
text="删除选中",
command=self._remove_food,
width=150
)
delete_button.pack(pady=5)
def _create_satisfaction_rating(self, parent):
"""创建满意度评分"""
# 满意度框架
satisfaction_frame = ctk.CTkFrame(parent)
satisfaction_frame.pack(fill="x", padx=10, pady=10)
# 标题
satisfaction_title = ctk.CTkLabel(
satisfaction_frame,
text="满意度评分",
font=ctk.CTkFont(size=16, weight="bold")
)
satisfaction_title.pack(pady=10)
# 评分滑块
self.satisfaction_var = tk.IntVar(value=3)
satisfaction_slider = ctk.CTkSlider(
satisfaction_frame,
from_=1,
to=5,
number_of_steps=4,
variable=self.satisfaction_var
)
satisfaction_slider.pack(fill="x", padx=20, pady=10)
# 评分标签
self.satisfaction_label = ctk.CTkLabel(
satisfaction_frame,
text="3分 - 一般",
font=ctk.CTkFont(size=14)
)
self.satisfaction_label.pack(pady=5)
# 绑定滑块事件
satisfaction_slider.configure(command=self._on_satisfaction_changed)
def _create_notes_section(self, parent):
"""创建备注区域"""
# 备注框架
notes_frame = ctk.CTkFrame(parent)
notes_frame.pack(fill="x", padx=10, pady=10)
# 标题
notes_title = ctk.CTkLabel(
notes_frame,
text="备注 (可选)",
font=ctk.CTkFont(size=16, weight="bold")
)
notes_title.pack(pady=10)
# 备注输入
self.notes_text = ctk.CTkTextbox(notes_frame, height=60)
self.notes_text.pack(fill="x", padx=10, pady=5)
self.notes_text.insert("1.0", "可以记录一些感受或特殊说明...")
def _create_buttons(self, parent):
"""创建按钮区域"""
# 按钮框架
button_frame = ctk.CTkFrame(parent)
button_frame.pack(fill="x", padx=10, pady=20)
# 保存按钮
save_button = ctk.CTkButton(
button_frame,
text="保存餐食记录",
command=self._save_meal,
width=150,
height=40
)
save_button.pack(side="left", padx=20, pady=10)
# 取消按钮
cancel_button = ctk.CTkButton(
button_frame,
text="取消",
command=self._cancel,
width=150,
height=40
)
cancel_button.pack(side="right", padx=20, pady=10)
def _on_search_changed(self, event=None):
"""搜索输入改变事件"""
query = self.search_var.get().strip()
if not query:
self.search_results_menu.configure(values=[])
return
try:
from smart_food.smart_database import search_foods
results = search_foods(query)
if results:
food_names = [result["name"] for result in results]
self.search_results_menu.configure(values=food_names)
self.search_results_label.configure(text=f"搜索结果 ({len(food_names)}个):")
else:
self.search_results_menu.configure(values=[])
self.search_results_label.configure(text="未找到匹配的食物")
except Exception as e:
self.search_results_menu.configure(values=[])
self.search_results_label.configure(text="搜索失败")
def _on_search_result_selected(self, food_name):
"""搜索结果选择事件"""
self.food_var.set(food_name)
self._on_food_changed(food_name)
# 自动选择对应的分类
try:
from smart_food.smart_database import get_food_categories, get_foods_by_category
categories = get_food_categories()
for category in categories:
foods_in_category = get_foods_by_category(category)
if food_name in foods_in_category:
self.category_var.set(category)
self._on_category_changed(category)
break
except Exception:
pass
def _analyze_food_with_ai(self):
"""使用AI分析食物"""
food_name = self.food_var.get()
portion = self.portion_var.get()
if not food_name:
messagebox.showwarning("警告", "请选择食物")
return
try:
from smart_food.smart_database import analyze_food_with_ai
# 显示分析进度
self._show_ai_analysis_dialog(food_name, portion)
except Exception as e:
messagebox.showerror("错误", f"AI分析失败: {str(e)}")
def _show_ai_analysis_dialog(self, food_name: str, portion: str):
"""显示AI分析对话框"""
# 创建分析对话框
analysis_dialog = ctk.CTkToplevel(self.dialog)
analysis_dialog.title(f"AI分析 - {food_name}")
analysis_dialog.geometry("500x600")
analysis_dialog.transient(self.dialog)
analysis_dialog.grab_set()
# 居中显示
analysis_dialog.geometry("+%d+%d" % (self.dialog.winfo_rootx() + 50, self.dialog.winfo_rooty() + 50))
# 创建滚动框架
scroll_frame = ctk.CTkScrollableFrame(analysis_dialog)
scroll_frame.pack(fill="both", expand=True, padx=20, pady=20)
# 标题
title_label = ctk.CTkLabel(
scroll_frame,
text=f"🤖 AI分析: {food_name}",
font=ctk.CTkFont(size=18, weight="bold")
)
title_label.pack(pady=10)
# 分析进度
progress_label = ctk.CTkLabel(scroll_frame, text="正在分析中...")
progress_label.pack(pady=10)
# 分析结果区域
result_text = ctk.CTkTextbox(scroll_frame, height=400, width=450)
result_text.pack(fill="both", expand=True, pady=10)
# 关闭按钮
close_button = ctk.CTkButton(
scroll_frame,
text="关闭",
command=analysis_dialog.destroy,
width=100
)
close_button.pack(pady=10)
# 在后台线程中执行AI分析
import threading
def analyze_thread():
try:
from smart_food.smart_database import analyze_food_with_ai
# 执行AI分析
result = analyze_food_with_ai(food_name, portion)
# 更新UI
analysis_dialog.after(0, lambda: self._update_ai_analysis_result(
analysis_dialog, result_text, progress_label, result
))
except Exception as e:
analysis_dialog.after(0, lambda: self._update_ai_analysis_error(
analysis_dialog, result_text, progress_label, str(e)
))
threading.Thread(target=analyze_thread, daemon=True).start()
def _update_ai_analysis_result(self, dialog, result_text, progress_label, result):
"""更新AI分析结果"""
progress_label.configure(text="分析完成")
if result.get('success'):
content = f"""
🍎 食物分析结果: {result.get('reasoning', 'AI分析')}
📊 营养成分:
- 热量: {result.get('calories', 0)} 卡路里
- 蛋白质: {result.get('protein', 0):.1f}
- 碳水化合物: {result.get('carbs', 0):.1f}
- 脂肪: {result.get('fat', 0):.1f}
- 纤维: {result.get('fiber', 0):.1f}
🏷️ 分类: {result.get('category', '其他')}
💡 健康建议:
"""
for tip in result.get('health_tips', []):
content += f"{tip}\n"
content += "\n👨‍🍳 制作建议:\n"
for suggestion in result.get('cooking_suggestions', []):
content += f"{suggestion}\n"
content += f"\n🎯 置信度: {result.get('confidence', 0.5):.1%}"
else:
content = "AI分析失败请稍后重试。"
result_text.delete("1.0", "end")
result_text.insert("1.0", content)
def _update_ai_analysis_error(self, dialog, result_text, progress_label, error_msg):
"""更新AI分析错误"""
progress_label.configure(text="分析失败")
result_text.delete("1.0", "end")
result_text.insert("1.0", f"AI分析失败: {error_msg}")
def _on_category_changed(self, category):
"""分类改变事件"""
foods = get_foods_by_category(category)
self.food_menu.configure(values=foods)
if foods:
self.food_var.set(foods[0])
self._on_food_changed(foods[0])
def _on_food_changed(self, food_name):
"""食物改变事件"""
portions = get_portion_options(food_name)
self.portion_menu.configure(values=portions)
if portions:
self.portion_var.set(portions[0])
def _add_food(self):
"""添加食物"""
food_name = self.food_var.get()
portion = self.portion_var.get()
if not food_name:
messagebox.showwarning("警告", "请选择食物")
return
# 估算热量
calories = estimate_calories(food_name, portion)
# 添加到列表
food_item = {
"name": food_name,
"portion": portion,
"calories": calories
}
self.selected_foods.append(food_item)
self._update_foods_list()
def _update_foods_list(self):
"""更新食物列表显示"""
self.foods_listbox.delete(0, tk.END)
total_calories = 0
for i, food_item in enumerate(self.selected_foods):
display_text = f"{food_item['name']} - {food_item['portion']} ({food_item['calories']}卡)"
self.foods_listbox.insert(tk.END, display_text)
total_calories += food_item['calories']
# 显示总热量
if self.selected_foods:
total_text = f"总热量: {total_calories}卡路里"
self.foods_listbox.insert(tk.END, "")
self.foods_listbox.insert(tk.END, total_text)
def _remove_food(self):
"""删除选中的食物"""
selection = self.foods_listbox.curselection()
if not selection:
messagebox.showwarning("警告", "请选择要删除的食物")
return
index = selection[0]
if index < len(self.selected_foods):
self.selected_foods.pop(index)
self._update_foods_list()
def _on_satisfaction_changed(self, value):
"""满意度改变事件"""
score = int(float(value))
score_texts = {
1: "1分 - 很不满意",
2: "2分 - 不满意",
3: "3分 - 一般",
4: "4分 - 满意",
5: "5分 - 很满意"
}
self.satisfaction_label.configure(text=score_texts.get(score, "3分 - 一般"))
def _save_meal(self):
"""保存餐食记录"""
if not self.selected_foods:
messagebox.showwarning("警告", "请至少添加一种食物")
return
try:
# 构建餐食数据
meal_data = {
"meal_type": self.meal_type,
"foods": self.selected_foods,
"satisfaction_score": self.satisfaction_var.get(),
"notes": self.notes_text.get("1.0", "end-1c").strip()
}
# 智能记录餐食
if record_meal_smart(self.user_id, meal_data):
messagebox.showinfo("成功", "餐食记录保存成功!")
self.dialog.destroy()
else:
messagebox.showerror("错误", "餐食记录保存失败")
except Exception as e:
messagebox.showerror("错误", f"保存失败: {str(e)}")
def _cancel(self):
"""取消记录"""
self.dialog.destroy()
# 便捷函数
def show_smart_meal_record_dialog(parent, user_id: str, meal_type: str = "lunch"):
"""显示智能餐食记录对话框"""
dialog = SmartMealRecordDialog(parent, user_id, meal_type)
parent.wait_window(dialog.dialog)
if __name__ == "__main__":
# 测试智能餐食记录对话框
root = tk.Tk()
root.title("测试智能餐食记录")
def test_dialog():
show_smart_meal_record_dialog(root, "test_user", "lunch")
test_button = tk.Button(root, text="测试餐食记录", command=test_dialog)
test_button.pack(pady=20)
root.mainloop()