Initial commit: 个性化饮食推荐助手 - 包含OCR识别、AI分析、现代化界面等功能
This commit is contained in:
1504
gui/main_window.py
Normal file
1504
gui/main_window.py
Normal file
File diff suppressed because it is too large
Load Diff
1359
gui/mobile_main_window.py
Normal file
1359
gui/mobile_main_window.py
Normal file
File diff suppressed because it is too large
Load Diff
625
gui/new_main_window.py
Normal file
625
gui/new_main_window.py
Normal file
@@ -0,0 +1,625 @@
|
||||
"""
|
||||
新的界面设计 - 信息录入/修改 + 随机转盘/扭蛋机
|
||||
基于用户需求重新设计的界面
|
||||
"""
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import messagebox
|
||||
import customtkinter as ctk
|
||||
from typing import Dict, List, Optional, Any
|
||||
import random
|
||||
import math
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime
|
||||
import json
|
||||
|
||||
# 设置CustomTkinter主题
|
||||
ctk.set_appearance_mode("dark")
|
||||
ctk.set_default_color_theme("blue")
|
||||
|
||||
|
||||
class SpinWheel(ctk.CTkCanvas):
|
||||
"""随机转盘/扭蛋机组件"""
|
||||
|
||||
def __init__(self, parent, width=300, height=300, **kwargs):
|
||||
super().__init__(parent, width=width, height=height, **kwargs)
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.center_x = width // 2
|
||||
self.center_y = height // 2
|
||||
self.radius = min(width, height) // 2 - 20
|
||||
|
||||
# 转盘状态
|
||||
self.is_spinning = False
|
||||
self.current_angle = 0
|
||||
self.spin_speed = 0
|
||||
self.target_angle = 0
|
||||
|
||||
# 转盘选项
|
||||
self.options = [
|
||||
{"text": "早餐推荐", "color": "#FF6B6B", "value": "breakfast"},
|
||||
{"text": "午餐推荐", "color": "#4ECDC4", "value": "lunch"},
|
||||
{"text": "晚餐推荐", "color": "#45B7D1", "value": "dinner"},
|
||||
{"text": "健康建议", "color": "#96CEB4", "value": "health"},
|
||||
{"text": "营养分析", "color": "#FFEAA7", "value": "nutrition"},
|
||||
{"text": "运动建议", "color": "#DDA0DD", "value": "exercise"}
|
||||
]
|
||||
|
||||
# 绑定点击事件
|
||||
self.bind("<Button-1>", self._on_click)
|
||||
|
||||
# 绘制转盘
|
||||
self._draw_wheel()
|
||||
|
||||
def _draw_wheel(self):
|
||||
"""绘制转盘"""
|
||||
self.delete("all")
|
||||
|
||||
# 绘制转盘背景
|
||||
self.create_oval(
|
||||
self.center_x - self.radius,
|
||||
self.center_y - self.radius,
|
||||
self.center_x + self.radius,
|
||||
self.center_y + self.radius,
|
||||
fill="#2B2B2B",
|
||||
outline="#FFFFFF",
|
||||
width=3
|
||||
)
|
||||
|
||||
# 绘制扇形区域
|
||||
angle_per_section = 360 / len(self.options)
|
||||
|
||||
for i, option in enumerate(self.options):
|
||||
start_angle = i * angle_per_section + self.current_angle
|
||||
end_angle = (i + 1) * angle_per_section + self.current_angle
|
||||
|
||||
# 绘制扇形
|
||||
self.create_arc(
|
||||
self.center_x - self.radius + 10,
|
||||
self.center_y - self.radius + 10,
|
||||
self.center_x + self.radius - 10,
|
||||
self.center_y + self.radius - 10,
|
||||
start=start_angle,
|
||||
extent=angle_per_section,
|
||||
fill=option["color"],
|
||||
outline="#FFFFFF",
|
||||
width=2
|
||||
)
|
||||
|
||||
# 绘制文字
|
||||
text_angle = start_angle + angle_per_section / 2
|
||||
text_radius = self.radius * 0.7
|
||||
|
||||
text_x = self.center_x + text_radius * math.cos(math.radians(text_angle))
|
||||
text_y = self.center_y + text_radius * math.sin(math.radians(text_angle))
|
||||
|
||||
self.create_text(
|
||||
text_x, text_y,
|
||||
text=option["text"],
|
||||
fill="#FFFFFF",
|
||||
font=("Arial", 10, "bold"),
|
||||
angle=text_angle
|
||||
)
|
||||
|
||||
# 绘制中心圆
|
||||
self.create_oval(
|
||||
self.center_x - 20,
|
||||
self.center_y - 20,
|
||||
self.center_x + 20,
|
||||
self.center_y + 20,
|
||||
fill="#FF6B6B",
|
||||
outline="#FFFFFF",
|
||||
width=2
|
||||
)
|
||||
|
||||
# 绘制指针
|
||||
pointer_length = self.radius - 30
|
||||
pointer_x = self.center_x + pointer_length * math.cos(math.radians(self.current_angle))
|
||||
pointer_y = self.center_y + pointer_length * math.sin(math.radians(self.current_angle))
|
||||
|
||||
self.create_line(
|
||||
self.center_x, self.center_y,
|
||||
pointer_x, pointer_y,
|
||||
fill="#FFFFFF",
|
||||
width=4
|
||||
)
|
||||
|
||||
def _on_click(self, event):
|
||||
"""点击转盘开始旋转"""
|
||||
if not self.is_spinning:
|
||||
self.spin()
|
||||
|
||||
def spin(self):
|
||||
"""开始旋转"""
|
||||
if self.is_spinning:
|
||||
return
|
||||
|
||||
self.is_spinning = True
|
||||
self.spin_speed = random.uniform(15, 25) # 初始速度
|
||||
self.target_angle = random.uniform(720, 1440) # 随机旋转角度
|
||||
|
||||
self._animate_spin()
|
||||
|
||||
def _animate_spin(self):
|
||||
"""动画旋转"""
|
||||
if not self.is_spinning:
|
||||
return
|
||||
|
||||
# 更新角度
|
||||
self.current_angle += self.spin_speed
|
||||
self.current_angle %= 360
|
||||
|
||||
# 减速
|
||||
self.spin_speed *= 0.95
|
||||
|
||||
# 重绘转盘
|
||||
self._draw_wheel()
|
||||
|
||||
# 检查是否停止
|
||||
if self.spin_speed < 0.1:
|
||||
self.is_spinning = False
|
||||
self._on_spin_complete()
|
||||
else:
|
||||
self.after(50, self._animate_spin)
|
||||
|
||||
def _on_spin_complete(self):
|
||||
"""旋转完成回调"""
|
||||
# 计算选中的选项
|
||||
angle_per_section = 360 / len(self.options)
|
||||
selected_index = int(self.current_angle // angle_per_section)
|
||||
selected_option = self.options[selected_index]
|
||||
|
||||
# 触发回调
|
||||
if hasattr(self, 'on_spin_complete'):
|
||||
self.on_spin_complete(selected_option)
|
||||
|
||||
|
||||
class UserInfoForm(ctk.CTkFrame):
|
||||
"""用户信息录入/修改表单"""
|
||||
|
||||
def __init__(self, parent, **kwargs):
|
||||
super().__init__(parent, **kwargs)
|
||||
self.user_data = {}
|
||||
self._create_widgets()
|
||||
|
||||
def _create_widgets(self):
|
||||
"""创建表单组件"""
|
||||
# 标题
|
||||
title_label = ctk.CTkLabel(
|
||||
self,
|
||||
text="📝 个人信息管理",
|
||||
font=ctk.CTkFont(size=24, weight="bold")
|
||||
)
|
||||
title_label.pack(pady=20)
|
||||
|
||||
# 表单框架
|
||||
form_frame = ctk.CTkScrollableFrame(self)
|
||||
form_frame.pack(fill="both", expand=True, padx=20, pady=10)
|
||||
|
||||
# 基本信息
|
||||
self._create_basic_info_section(form_frame)
|
||||
|
||||
# 健康信息
|
||||
self._create_health_info_section(form_frame)
|
||||
|
||||
# 饮食偏好
|
||||
self._create_diet_preferences_section(form_frame)
|
||||
|
||||
# 按钮区域
|
||||
self._create_buttons(form_frame)
|
||||
|
||||
def _create_basic_info_section(self, parent):
|
||||
"""创建基本信息区域"""
|
||||
section_frame = ctk.CTkFrame(parent)
|
||||
section_frame.pack(fill="x", padx=10, pady=10)
|
||||
|
||||
# 标题
|
||||
title = ctk.CTkLabel(
|
||||
section_frame,
|
||||
text="👤 基本信息",
|
||||
font=ctk.CTkFont(size=18, weight="bold")
|
||||
)
|
||||
title.pack(pady=10)
|
||||
|
||||
# 信息网格
|
||||
info_frame = ctk.CTkFrame(section_frame)
|
||||
info_frame.pack(fill="x", padx=20, pady=10)
|
||||
|
||||
# 姓名
|
||||
name_label = ctk.CTkLabel(info_frame, text="姓名:")
|
||||
name_label.grid(row=0, column=0, sticky="w", padx=10, pady=5)
|
||||
|
||||
self.name_var = tk.StringVar()
|
||||
name_entry = ctk.CTkEntry(info_frame, textvariable=self.name_var, width=200)
|
||||
name_entry.grid(row=0, column=1, sticky="w", padx=10, pady=5)
|
||||
|
||||
# 年龄
|
||||
age_label = ctk.CTkLabel(info_frame, text="年龄:")
|
||||
age_label.grid(row=0, column=2, sticky="w", padx=10, pady=5)
|
||||
|
||||
self.age_var = tk.StringVar(value="25")
|
||||
age_entry = ctk.CTkEntry(info_frame, textvariable=self.age_var, width=100)
|
||||
age_entry.grid(row=0, column=3, sticky="w", padx=10, pady=5)
|
||||
|
||||
# 性别
|
||||
gender_label = ctk.CTkLabel(info_frame, text="性别:")
|
||||
gender_label.grid(row=1, column=0, sticky="w", padx=10, pady=5)
|
||||
|
||||
self.gender_var = tk.StringVar(value="女")
|
||||
gender_menu = ctk.CTkOptionMenu(
|
||||
info_frame,
|
||||
variable=self.gender_var,
|
||||
values=["男", "女"]
|
||||
)
|
||||
gender_menu.grid(row=1, column=1, sticky="w", padx=10, pady=5)
|
||||
|
||||
# 身高体重
|
||||
height_label = ctk.CTkLabel(info_frame, text="身高(cm):")
|
||||
height_label.grid(row=1, column=2, sticky="w", padx=10, pady=5)
|
||||
|
||||
self.height_var = tk.StringVar(value="165")
|
||||
height_entry = ctk.CTkEntry(info_frame, textvariable=self.height_var, width=100)
|
||||
height_entry.grid(row=1, column=3, sticky="w", padx=10, pady=5)
|
||||
|
||||
weight_label = ctk.CTkLabel(info_frame, text="体重(kg):")
|
||||
weight_label.grid(row=2, column=0, sticky="w", padx=10, pady=5)
|
||||
|
||||
self.weight_var = tk.StringVar(value="55")
|
||||
weight_entry = ctk.CTkEntry(info_frame, textvariable=self.weight_var, width=100)
|
||||
weight_entry.grid(row=2, column=1, sticky="w", padx=10, pady=5)
|
||||
|
||||
def _create_health_info_section(self, parent):
|
||||
"""创建健康信息区域"""
|
||||
section_frame = ctk.CTkFrame(parent)
|
||||
section_frame.pack(fill="x", padx=10, pady=10)
|
||||
|
||||
# 标题
|
||||
title = ctk.CTkLabel(
|
||||
section_frame,
|
||||
text="🏥 健康信息",
|
||||
font=ctk.CTkFont(size=18, weight="bold")
|
||||
)
|
||||
title.pack(pady=10)
|
||||
|
||||
# 健康信息网格
|
||||
health_frame = ctk.CTkFrame(section_frame)
|
||||
health_frame.pack(fill="x", padx=20, pady=10)
|
||||
|
||||
# 活动水平
|
||||
activity_label = ctk.CTkLabel(health_frame, text="活动水平:")
|
||||
activity_label.grid(row=0, column=0, sticky="w", padx=10, pady=5)
|
||||
|
||||
self.activity_var = tk.StringVar(value="中等")
|
||||
activity_menu = ctk.CTkOptionMenu(
|
||||
health_frame,
|
||||
variable=self.activity_var,
|
||||
values=["久坐", "轻度活动", "中等", "高度活动", "极度活动"]
|
||||
)
|
||||
activity_menu.grid(row=0, column=1, sticky="w", padx=10, pady=5)
|
||||
|
||||
# 健康目标
|
||||
goal_label = ctk.CTkLabel(health_frame, text="健康目标:")
|
||||
goal_label.grid(row=0, column=2, sticky="w", padx=10, pady=5)
|
||||
|
||||
self.goal_var = tk.StringVar(value="保持健康")
|
||||
goal_menu = ctk.CTkOptionMenu(
|
||||
health_frame,
|
||||
variable=self.goal_var,
|
||||
values=["保持健康", "减重", "增重", "增肌", "改善消化", "提高免疫力"]
|
||||
)
|
||||
goal_menu.grid(row=0, column=3, sticky="w", padx=10, pady=5)
|
||||
|
||||
# 过敏信息
|
||||
allergy_label = ctk.CTkLabel(health_frame, text="过敏食物:")
|
||||
allergy_label.grid(row=1, column=0, sticky="w", padx=10, pady=5)
|
||||
|
||||
self.allergy_var = tk.StringVar(value="无")
|
||||
allergy_entry = ctk.CTkEntry(health_frame, textvariable=self.allergy_var, width=200)
|
||||
allergy_entry.grid(row=1, column=1, columnspan=2, sticky="w", padx=10, pady=5)
|
||||
|
||||
def _create_diet_preferences_section(self, parent):
|
||||
"""创建饮食偏好区域"""
|
||||
section_frame = ctk.CTkFrame(parent)
|
||||
section_frame.pack(fill="x", padx=10, pady=10)
|
||||
|
||||
# 标题
|
||||
title = ctk.CTkLabel(
|
||||
section_frame,
|
||||
text="🍽️ 饮食偏好",
|
||||
font=ctk.CTkFont(size=18, weight="bold")
|
||||
)
|
||||
title.pack(pady=10)
|
||||
|
||||
# 偏好网格
|
||||
pref_frame = ctk.CTkFrame(section_frame)
|
||||
pref_frame.pack(fill="x", padx=20, pady=10)
|
||||
|
||||
# 口味偏好
|
||||
taste_label = ctk.CTkLabel(pref_frame, text="主要口味:")
|
||||
taste_label.grid(row=0, column=0, sticky="w", padx=10, pady=5)
|
||||
|
||||
self.taste_var = tk.StringVar(value="均衡")
|
||||
taste_menu = ctk.CTkOptionMenu(
|
||||
pref_frame,
|
||||
variable=self.taste_var,
|
||||
values=["均衡", "偏甜", "偏咸", "偏辣", "偏酸", "偏清淡"]
|
||||
)
|
||||
taste_menu.grid(row=0, column=1, sticky="w", padx=10, pady=5)
|
||||
|
||||
# 饮食类型
|
||||
diet_label = ctk.CTkLabel(pref_frame, text="饮食类型:")
|
||||
diet_label.grid(row=0, column=2, sticky="w", padx=10, pady=5)
|
||||
|
||||
self.diet_var = tk.StringVar(value="普通饮食")
|
||||
diet_menu = ctk.CTkOptionMenu(
|
||||
pref_frame,
|
||||
variable=self.diet_var,
|
||||
values=["普通饮食", "素食", "低脂饮食", "低糖饮食", "高蛋白饮食"]
|
||||
)
|
||||
diet_menu.grid(row=0, column=3, sticky="w", padx=10, pady=5)
|
||||
|
||||
# 不喜欢食物
|
||||
dislike_label = ctk.CTkLabel(pref_frame, text="不喜欢食物:")
|
||||
dislike_label.grid(row=1, column=0, sticky="w", padx=10, pady=5)
|
||||
|
||||
self.dislike_var = tk.StringVar(value="无")
|
||||
dislike_entry = ctk.CTkEntry(pref_frame, textvariable=self.dislike_var, width=200)
|
||||
dislike_entry.grid(row=1, column=1, columnspan=2, sticky="w", padx=10, pady=5)
|
||||
|
||||
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_data,
|
||||
width=150,
|
||||
height=50,
|
||||
font=ctk.CTkFont(size=16, weight="bold")
|
||||
)
|
||||
save_button.pack(side="left", padx=20, pady=10)
|
||||
|
||||
# 加载按钮
|
||||
load_button = ctk.CTkButton(
|
||||
button_frame,
|
||||
text="📂 加载信息",
|
||||
command=self._load_data,
|
||||
width=150,
|
||||
height=50,
|
||||
font=ctk.CTkFont(size=16, weight="bold")
|
||||
)
|
||||
load_button.pack(side="left", padx=20, pady=10)
|
||||
|
||||
# 重置按钮
|
||||
reset_button = ctk.CTkButton(
|
||||
button_frame,
|
||||
text="🔄 重置表单",
|
||||
command=self._reset_form,
|
||||
width=150,
|
||||
height=50,
|
||||
font=ctk.CTkFont(size=16, weight="bold")
|
||||
)
|
||||
reset_button.pack(side="right", padx=20, pady=10)
|
||||
|
||||
def _save_data(self):
|
||||
"""保存数据"""
|
||||
try:
|
||||
self.user_data = {
|
||||
'basic_info': {
|
||||
'name': self.name_var.get(),
|
||||
'age': int(self.age_var.get()),
|
||||
'gender': self.gender_var.get(),
|
||||
'height': int(self.height_var.get()),
|
||||
'weight': int(self.weight_var.get())
|
||||
},
|
||||
'health_info': {
|
||||
'activity_level': self.activity_var.get(),
|
||||
'health_goal': self.goal_var.get(),
|
||||
'allergies': self.allergy_var.get()
|
||||
},
|
||||
'diet_preferences': {
|
||||
'taste': self.taste_var.get(),
|
||||
'diet_type': self.diet_var.get(),
|
||||
'dislikes': self.dislike_var.get()
|
||||
},
|
||||
'saved_at': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
# 保存到文件
|
||||
with open('data/user_info.json', 'w', encoding='utf-8') as f:
|
||||
json.dump(self.user_data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
messagebox.showinfo("成功", "信息保存成功!")
|
||||
|
||||
except Exception as e:
|
||||
messagebox.showerror("错误", f"保存失败: {str(e)}")
|
||||
|
||||
def _load_data(self):
|
||||
"""加载数据"""
|
||||
try:
|
||||
with open('data/user_info.json', 'r', encoding='utf-8') as f:
|
||||
self.user_data = json.load(f)
|
||||
|
||||
# 填充表单
|
||||
basic_info = self.user_data.get('basic_info', {})
|
||||
self.name_var.set(basic_info.get('name', ''))
|
||||
self.age_var.set(str(basic_info.get('age', 25)))
|
||||
self.gender_var.set(basic_info.get('gender', '女'))
|
||||
self.height_var.set(str(basic_info.get('height', 165)))
|
||||
self.weight_var.set(str(basic_info.get('weight', 55)))
|
||||
|
||||
health_info = self.user_data.get('health_info', {})
|
||||
self.activity_var.set(health_info.get('activity_level', '中等'))
|
||||
self.goal_var.set(health_info.get('health_goal', '保持健康'))
|
||||
self.allergy_var.set(health_info.get('allergies', '无'))
|
||||
|
||||
diet_prefs = self.user_data.get('diet_preferences', {})
|
||||
self.taste_var.set(diet_prefs.get('taste', '均衡'))
|
||||
self.diet_var.set(diet_prefs.get('diet_type', '普通饮食'))
|
||||
self.dislike_var.set(diet_prefs.get('dislikes', '无'))
|
||||
|
||||
messagebox.showinfo("成功", "信息加载成功!")
|
||||
|
||||
except FileNotFoundError:
|
||||
messagebox.showwarning("警告", "未找到保存的信息文件")
|
||||
except Exception as e:
|
||||
messagebox.showerror("错误", f"加载失败: {str(e)}")
|
||||
|
||||
def _reset_form(self):
|
||||
"""重置表单"""
|
||||
self.name_var.set("")
|
||||
self.age_var.set("25")
|
||||
self.gender_var.set("女")
|
||||
self.height_var.set("165")
|
||||
self.weight_var.set("55")
|
||||
self.activity_var.set("中等")
|
||||
self.goal_var.set("保持健康")
|
||||
self.allergy_var.set("无")
|
||||
self.taste_var.set("均衡")
|
||||
self.diet_var.set("普通饮食")
|
||||
self.dislike_var.set("无")
|
||||
|
||||
|
||||
class NewMainWindow(ctk.CTk):
|
||||
"""新的主窗口 - 信息录入/修改 + 随机转盘/扭蛋机"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
# 设置窗口
|
||||
self.title("🍎 智能饮食推荐助手")
|
||||
self.geometry("1200x800")
|
||||
self.minsize(1000, 700)
|
||||
|
||||
# 创建界面
|
||||
self._create_widgets()
|
||||
|
||||
# 设置转盘回调
|
||||
self.spin_wheel.on_spin_complete = self._on_spin_complete
|
||||
|
||||
def _create_widgets(self):
|
||||
"""创建界面组件"""
|
||||
# 主标题
|
||||
title_label = ctk.CTkLabel(
|
||||
self,
|
||||
text="🎯 智能饮食推荐助手",
|
||||
font=ctk.CTkFont(size=32, weight="bold")
|
||||
)
|
||||
title_label.pack(pady=20)
|
||||
|
||||
# 主内容区域
|
||||
main_frame = ctk.CTkFrame(self)
|
||||
main_frame.pack(fill="both", expand=True, padx=20, pady=10)
|
||||
|
||||
# 左侧 - 信息录入区域
|
||||
left_frame = ctk.CTkFrame(main_frame)
|
||||
left_frame.pack(side="left", fill="both", expand=True, padx=(0, 10))
|
||||
|
||||
self.user_info_form = UserInfoForm(left_frame)
|
||||
self.user_info_form.pack(fill="both", expand=True)
|
||||
|
||||
# 右侧 - 转盘区域
|
||||
right_frame = ctk.CTkFrame(main_frame)
|
||||
right_frame.pack(side="right", fill="both", padx=(10, 0))
|
||||
|
||||
# 转盘标题
|
||||
wheel_title = ctk.CTkLabel(
|
||||
right_frame,
|
||||
text="🎰 随机推荐转盘",
|
||||
font=ctk.CTkFont(size=20, weight="bold")
|
||||
)
|
||||
wheel_title.pack(pady=20)
|
||||
|
||||
# 转盘说明
|
||||
wheel_desc = ctk.CTkLabel(
|
||||
right_frame,
|
||||
text="点击转盘开始随机推荐!",
|
||||
font=ctk.CTkFont(size=14)
|
||||
)
|
||||
wheel_desc.pack(pady=10)
|
||||
|
||||
# 转盘组件
|
||||
self.spin_wheel = SpinWheel(right_frame, width=350, height=350)
|
||||
self.spin_wheel.pack(pady=20)
|
||||
|
||||
# 结果显示区域
|
||||
self.result_frame = ctk.CTkFrame(right_frame)
|
||||
self.result_frame.pack(fill="x", padx=20, pady=20)
|
||||
|
||||
self.result_label = ctk.CTkLabel(
|
||||
self.result_frame,
|
||||
text="等待转盘结果...",
|
||||
font=ctk.CTkFont(size=16),
|
||||
wraplength=300
|
||||
)
|
||||
self.result_label.pack(pady=20)
|
||||
|
||||
# 底部状态栏
|
||||
self.status_frame = ctk.CTkFrame(self)
|
||||
self.status_frame.pack(fill="x", padx=20, pady=10)
|
||||
|
||||
self.status_label = ctk.CTkLabel(
|
||||
self.status_frame,
|
||||
text="就绪",
|
||||
font=ctk.CTkFont(size=12)
|
||||
)
|
||||
self.status_label.pack(pady=5)
|
||||
|
||||
def _on_spin_complete(self, selected_option):
|
||||
"""转盘完成回调"""
|
||||
self._update_status(f"转盘选中: {selected_option['text']}")
|
||||
|
||||
# 显示结果
|
||||
result_text = f"🎯 推荐结果: {selected_option['text']}\n\n"
|
||||
|
||||
# 根据选中的选项生成具体建议
|
||||
if selected_option['value'] == 'breakfast':
|
||||
result_text += "🌅 早餐建议:\n"
|
||||
result_text += "• 燕麦粥 + 牛奶 + 香蕉\n"
|
||||
result_text += "• 全麦面包 + 鸡蛋 + 蔬菜\n"
|
||||
result_text += "• 小米粥 + 咸菜 + 煮蛋"
|
||||
elif selected_option['value'] == 'lunch':
|
||||
result_text += "🌞 午餐建议:\n"
|
||||
result_text += "• 米饭 + 鸡肉 + 青菜\n"
|
||||
result_text += "• 面条 + 牛肉 + 西红柿\n"
|
||||
result_text += "• 饺子 + 汤"
|
||||
elif selected_option['value'] == 'dinner':
|
||||
result_text += "🌙 晚餐建议:\n"
|
||||
result_text += "• 粥 + 咸菜 + 豆腐\n"
|
||||
result_text += "• 蒸蛋 + 青菜 + 汤\n"
|
||||
result_text += "• 面条 + 蔬菜"
|
||||
elif selected_option['value'] == 'health':
|
||||
result_text += "🏥 健康建议:\n"
|
||||
result_text += "• 多喝水,保持水分平衡\n"
|
||||
result_text += "• 适量运动,增强体质\n"
|
||||
result_text += "• 规律作息,保证睡眠"
|
||||
elif selected_option['value'] == 'nutrition':
|
||||
result_text += "🥗 营养建议:\n"
|
||||
result_text += "• 多吃蔬菜水果\n"
|
||||
result_text += "• 适量蛋白质摄入\n"
|
||||
result_text += "• 控制糖分和盐分"
|
||||
elif selected_option['value'] == 'exercise':
|
||||
result_text += "🏃 运动建议:\n"
|
||||
result_text += "• 每天30分钟有氧运动\n"
|
||||
result_text += "• 适量力量训练\n"
|
||||
result_text += "• 注意运动前后拉伸"
|
||||
|
||||
self.result_label.configure(text=result_text)
|
||||
|
||||
def _update_status(self, message):
|
||||
"""更新状态栏"""
|
||||
self.status_label.configure(text=f"{datetime.now().strftime('%H:%M:%S')} - {message}")
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
app = NewMainWindow()
|
||||
app.mainloop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
637
gui/ocr_calorie_gui.py
Normal file
637
gui/ocr_calorie_gui.py
Normal file
@@ -0,0 +1,637 @@
|
||||
"""
|
||||
OCR热量识别GUI界面
|
||||
提供图片上传、OCR识别、结果验证和修正功能
|
||||
"""
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, filedialog, messagebox, scrolledtext
|
||||
from PIL import Image, ImageTk
|
||||
import threading
|
||||
from typing import Dict, List, Optional, Any
|
||||
from pathlib import Path
|
||||
import json
|
||||
from datetime import datetime
|
||||
from core.base import UserData, CalorieInfo, FoodRecognitionResult
|
||||
|
||||
|
||||
class OCRCalorieGUI:
|
||||
"""OCR热量识别GUI界面"""
|
||||
|
||||
def __init__(self, parent_window, app_core):
|
||||
self.parent_window = parent_window
|
||||
self.app_core = app_core
|
||||
self.current_image_path = None
|
||||
self.current_recognition_result = None
|
||||
self.user_corrections = {}
|
||||
|
||||
# 创建主框架
|
||||
self.main_frame = ttk.Frame(parent_window)
|
||||
self.main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||||
|
||||
# 创建界面
|
||||
self._create_widgets()
|
||||
self._setup_layout()
|
||||
self._bind_events()
|
||||
|
||||
def _create_widgets(self):
|
||||
"""创建界面组件"""
|
||||
# 标题
|
||||
self.title_label = ttk.Label(
|
||||
self.main_frame,
|
||||
text="📷 图片OCR热量识别",
|
||||
font=("Arial", 18, "bold"),
|
||||
foreground="#2c3e50"
|
||||
)
|
||||
|
||||
# 图片上传区域
|
||||
self.image_frame = ttk.LabelFrame(
|
||||
self.main_frame,
|
||||
text="📸 图片上传",
|
||||
padding=15,
|
||||
relief="solid",
|
||||
borderwidth=1
|
||||
)
|
||||
|
||||
self.upload_button = ttk.Button(
|
||||
self.image_frame,
|
||||
text="📁 选择图片",
|
||||
command=self._select_image,
|
||||
style="Accent.TButton"
|
||||
)
|
||||
|
||||
self.image_label = ttk.Label(
|
||||
self.image_frame,
|
||||
text="请选择包含食物信息的图片",
|
||||
background="#f8f9fa",
|
||||
relief="solid",
|
||||
borderwidth=1,
|
||||
width=50,
|
||||
height=15,
|
||||
anchor="center"
|
||||
)
|
||||
|
||||
# 识别控制区域
|
||||
self.control_frame = ttk.LabelFrame(
|
||||
self.main_frame,
|
||||
text="⚙️ 识别控制",
|
||||
padding=15,
|
||||
relief="solid",
|
||||
borderwidth=1
|
||||
)
|
||||
|
||||
self.recognize_button = ttk.Button(
|
||||
self.control_frame,
|
||||
text="🚀 开始识别",
|
||||
command=self._start_recognition,
|
||||
state=tk.DISABLED,
|
||||
style="Accent.TButton"
|
||||
)
|
||||
|
||||
self.progress_bar = ttk.Progressbar(
|
||||
self.control_frame,
|
||||
mode='indeterminate',
|
||||
style="Accent.TProgressbar"
|
||||
)
|
||||
|
||||
self.status_label = ttk.Label(
|
||||
self.control_frame,
|
||||
text="✅ 准备就绪",
|
||||
foreground="#27ae60"
|
||||
)
|
||||
|
||||
# 识别结果区域
|
||||
self.result_frame = ttk.LabelFrame(self.main_frame, text="识别结果", padding=10)
|
||||
|
||||
# 创建结果表格
|
||||
self.result_tree = ttk.Treeview(
|
||||
self.result_frame,
|
||||
columns=('food_name', 'calories', 'confidence', 'source'),
|
||||
show='headings',
|
||||
height=8
|
||||
)
|
||||
|
||||
# 设置列标题
|
||||
self.result_tree.heading('food_name', text='食物名称')
|
||||
self.result_tree.heading('calories', text='热量(卡路里)')
|
||||
self.result_tree.heading('confidence', text='置信度')
|
||||
self.result_tree.heading('source', text='来源')
|
||||
|
||||
# 设置列宽
|
||||
self.result_tree.column('food_name', width=150)
|
||||
self.result_tree.column('calories', width=100)
|
||||
self.result_tree.column('confidence', width=80)
|
||||
self.result_tree.column('source', width=100)
|
||||
|
||||
# 结果操作按钮
|
||||
self.result_button_frame = ttk.Frame(self.result_frame)
|
||||
|
||||
self.edit_button = ttk.Button(
|
||||
self.result_button_frame,
|
||||
text="编辑结果",
|
||||
command=self._edit_result,
|
||||
state=tk.DISABLED
|
||||
)
|
||||
|
||||
self.confirm_button = ttk.Button(
|
||||
self.result_button_frame,
|
||||
text="确认结果",
|
||||
command=self._confirm_result,
|
||||
state=tk.DISABLED
|
||||
)
|
||||
|
||||
self.clear_button = ttk.Button(
|
||||
self.result_button_frame,
|
||||
text="清空结果",
|
||||
command=self._clear_results
|
||||
)
|
||||
|
||||
# 详细信息区域
|
||||
self.detail_frame = ttk.LabelFrame(self.main_frame, text="详细信息", padding=10)
|
||||
|
||||
self.detail_text = scrolledtext.ScrolledText(
|
||||
self.detail_frame,
|
||||
height=8,
|
||||
width=60
|
||||
)
|
||||
|
||||
# 建议区域
|
||||
self.suggestion_frame = ttk.LabelFrame(self.main_frame, text="建议", padding=10)
|
||||
|
||||
self.suggestion_text = scrolledtext.ScrolledText(
|
||||
self.suggestion_frame,
|
||||
height=4,
|
||||
width=60,
|
||||
state=tk.DISABLED
|
||||
)
|
||||
|
||||
def _setup_layout(self):
|
||||
"""设置布局"""
|
||||
# 标题
|
||||
self.title_label.pack(pady=(0, 10))
|
||||
|
||||
# 图片上传区域
|
||||
self.image_frame.pack(fill=tk.X, pady=(0, 10))
|
||||
self.upload_button.pack(side=tk.LEFT, padx=(0, 10))
|
||||
self.image_label.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# 识别控制区域
|
||||
self.control_frame.pack(fill=tk.X, pady=(0, 10))
|
||||
self.recognize_button.pack(side=tk.LEFT, padx=(0, 10))
|
||||
self.progress_bar.pack(side=tk.LEFT, padx=(0, 10), fill=tk.X, expand=True)
|
||||
self.status_label.pack(side=tk.LEFT)
|
||||
|
||||
# 识别结果区域
|
||||
self.result_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
|
||||
|
||||
# 结果表格
|
||||
result_scrollbar = ttk.Scrollbar(self.result_frame, orient=tk.VERTICAL, command=self.result_tree.yview)
|
||||
self.result_tree.configure(yscrollcommand=result_scrollbar.set)
|
||||
|
||||
self.result_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||
result_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||||
|
||||
# 结果操作按钮
|
||||
self.result_button_frame.pack(fill=tk.X, pady=(10, 0))
|
||||
self.edit_button.pack(side=tk.LEFT, padx=(0, 10))
|
||||
self.confirm_button.pack(side=tk.LEFT, padx=(0, 10))
|
||||
self.clear_button.pack(side=tk.LEFT)
|
||||
|
||||
# 详细信息和建议区域
|
||||
detail_suggestion_frame = ttk.Frame(self.main_frame)
|
||||
detail_suggestion_frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
self.detail_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 5))
|
||||
self.detail_text.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
self.suggestion_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(5, 0))
|
||||
self.suggestion_text.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
def _bind_events(self):
|
||||
"""绑定事件"""
|
||||
self.result_tree.bind('<<TreeviewSelect>>', self._on_result_select)
|
||||
self.result_tree.bind('<Double-1>', self._on_result_double_click)
|
||||
|
||||
def _select_image(self):
|
||||
"""选择图片文件"""
|
||||
file_types = [
|
||||
("图片文件", "*.jpg *.jpeg *.png *.bmp *.gif"),
|
||||
("JPEG文件", "*.jpg *.jpeg"),
|
||||
("PNG文件", "*.png"),
|
||||
("所有文件", "*.*")
|
||||
]
|
||||
|
||||
file_path = filedialog.askopenfilename(
|
||||
title="选择包含食物信息的图片",
|
||||
filetypes=file_types
|
||||
)
|
||||
|
||||
if file_path:
|
||||
self.current_image_path = file_path
|
||||
self._display_image(file_path)
|
||||
self.recognize_button.config(state=tk.NORMAL)
|
||||
self.status_label.config(text=f"已选择图片: {Path(file_path).name}")
|
||||
|
||||
def _display_image(self, image_path: str):
|
||||
"""显示图片"""
|
||||
try:
|
||||
# 加载图片
|
||||
image = Image.open(image_path)
|
||||
|
||||
# 调整图片大小以适应显示区域
|
||||
display_size = (400, 300)
|
||||
image.thumbnail(display_size, Image.Resampling.LANCZOS)
|
||||
|
||||
# 转换为Tkinter可显示的格式
|
||||
photo = ImageTk.PhotoImage(image)
|
||||
|
||||
# 更新标签
|
||||
self.image_label.config(image=photo, text="")
|
||||
self.image_label.image = photo # 保持引用
|
||||
|
||||
except Exception as e:
|
||||
messagebox.showerror("错误", f"无法显示图片: {str(e)}")
|
||||
self.image_label.config(image="", text="图片显示失败")
|
||||
|
||||
def _start_recognition(self):
|
||||
"""开始OCR识别"""
|
||||
if not self.current_image_path:
|
||||
messagebox.showwarning("警告", "请先选择图片")
|
||||
return
|
||||
|
||||
# 禁用按钮,显示进度条
|
||||
self.recognize_button.config(state=tk.DISABLED)
|
||||
self.progress_bar.start()
|
||||
self.status_label.config(text="正在识别...")
|
||||
|
||||
# 在新线程中执行识别
|
||||
thread = threading.Thread(target=self._perform_recognition)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
def _perform_recognition(self):
|
||||
"""执行OCR识别"""
|
||||
try:
|
||||
# 准备请求数据
|
||||
request_data = {
|
||||
'type': 'recognize_image',
|
||||
'image_path': self.current_image_path
|
||||
}
|
||||
|
||||
# 获取当前用户数据(这里需要根据实际情况调整)
|
||||
user_data = UserData(
|
||||
user_id="current_user",
|
||||
profile={},
|
||||
meals=[],
|
||||
feedback=[],
|
||||
preferences={}
|
||||
)
|
||||
|
||||
# 调用OCR模块
|
||||
from modules.ocr_calorie_recognition import OCRCalorieRecognitionModule
|
||||
ocr_module = OCRCalorieRecognitionModule(self.app_core.config)
|
||||
|
||||
if not ocr_module.initialize():
|
||||
raise Exception("OCR模块初始化失败")
|
||||
|
||||
result = ocr_module.process(request_data, user_data)
|
||||
|
||||
# 在主线程中更新UI
|
||||
self.parent_window.after(0, self._on_recognition_complete, result)
|
||||
|
||||
except Exception as e:
|
||||
self.parent_window.after(0, self._on_recognition_error, str(e))
|
||||
|
||||
def _on_recognition_complete(self, result):
|
||||
"""识别完成回调"""
|
||||
try:
|
||||
# 停止进度条
|
||||
self.progress_bar.stop()
|
||||
self.recognize_button.config(state=tk.NORMAL)
|
||||
|
||||
if result.result.get('success', False):
|
||||
self.current_recognition_result = result.result['result']
|
||||
self._display_recognition_results()
|
||||
self.status_label.config(text="识别完成")
|
||||
else:
|
||||
error_msg = result.result.get('error', '识别失败')
|
||||
messagebox.showerror("识别失败", error_msg)
|
||||
self.status_label.config(text="识别失败")
|
||||
|
||||
except Exception as e:
|
||||
self._on_recognition_error(str(e))
|
||||
|
||||
def _on_recognition_error(self, error_msg: str):
|
||||
"""识别错误回调"""
|
||||
self.progress_bar.stop()
|
||||
self.recognize_button.config(state=tk.NORMAL)
|
||||
self.status_label.config(text="识别失败")
|
||||
messagebox.showerror("识别错误", f"OCR识别过程中出现错误: {error_msg}")
|
||||
|
||||
def _display_recognition_results(self):
|
||||
"""显示识别结果"""
|
||||
if not self.current_recognition_result:
|
||||
return
|
||||
|
||||
try:
|
||||
# 清空现有结果
|
||||
self._clear_results()
|
||||
|
||||
# 显示热量信息
|
||||
calorie_infos = self.current_recognition_result.calorie_infos
|
||||
|
||||
for info in calorie_infos:
|
||||
self.result_tree.insert('', tk.END, values=(
|
||||
info.food_name,
|
||||
f"{info.calories:.1f}" if info.calories else "未知",
|
||||
f"{info.confidence:.2f}",
|
||||
info.source
|
||||
))
|
||||
|
||||
# 显示详细信息
|
||||
self._update_detail_text()
|
||||
|
||||
# 显示建议
|
||||
self._update_suggestion_text()
|
||||
|
||||
# 启用操作按钮
|
||||
self.edit_button.config(state=tk.NORMAL)
|
||||
self.confirm_button.config(state=tk.NORMAL)
|
||||
|
||||
except Exception as e:
|
||||
messagebox.showerror("错误", f"显示识别结果失败: {str(e)}")
|
||||
|
||||
def _update_detail_text(self):
|
||||
"""更新详细信息文本"""
|
||||
if not self.current_recognition_result:
|
||||
return
|
||||
|
||||
try:
|
||||
detail_text = "=== OCR识别详细信息 ===\n\n"
|
||||
|
||||
# OCR结果
|
||||
detail_text += "OCR识别结果:\n"
|
||||
for ocr_result in self.current_recognition_result.ocr_results:
|
||||
detail_text += f"- 方法: {ocr_result.method}\n"
|
||||
detail_text += f" 置信度: {ocr_result.confidence:.2f}\n"
|
||||
detail_text += f" 识别文本: {ocr_result.text[:100]}...\n\n"
|
||||
|
||||
# 处理时间
|
||||
detail_text += f"处理时间: {self.current_recognition_result.processing_time:.2f}秒\n"
|
||||
detail_text += f"整体置信度: {self.current_recognition_result.overall_confidence:.2f}\n"
|
||||
|
||||
# 热量信息
|
||||
detail_text += "\n=== 热量信息 ===\n"
|
||||
for info in self.current_recognition_result.calorie_infos:
|
||||
detail_text += f"食物: {info.food_name}\n"
|
||||
detail_text += f"热量: {info.calories} 卡路里\n"
|
||||
detail_text += f"置信度: {info.confidence:.2f}\n"
|
||||
detail_text += f"来源: {info.source}\n"
|
||||
detail_text += f"原始文本: {info.raw_text}\n\n"
|
||||
|
||||
self.detail_text.delete(1.0, tk.END)
|
||||
self.detail_text.insert(1.0, detail_text)
|
||||
|
||||
except Exception as e:
|
||||
self.detail_text.delete(1.0, tk.END)
|
||||
self.detail_text.insert(1.0, f"详细信息加载失败: {str(e)}")
|
||||
|
||||
def _update_suggestion_text(self):
|
||||
"""更新建议文本"""
|
||||
if not self.current_recognition_result:
|
||||
return
|
||||
|
||||
try:
|
||||
suggestions = self.current_recognition_result.suggestions
|
||||
|
||||
suggestion_text = "=== 建议 ===\n\n"
|
||||
for i, suggestion in enumerate(suggestions, 1):
|
||||
suggestion_text += f"{i}. {suggestion}\n"
|
||||
|
||||
self.suggestion_text.config(state=tk.NORMAL)
|
||||
self.suggestion_text.delete(1.0, tk.END)
|
||||
self.suggestion_text.insert(1.0, suggestion_text)
|
||||
self.suggestion_text.config(state=tk.DISABLED)
|
||||
|
||||
except Exception as e:
|
||||
self.suggestion_text.config(state=tk.NORMAL)
|
||||
self.suggestion_text.delete(1.0, tk.END)
|
||||
self.suggestion_text.insert(1.0, f"建议加载失败: {str(e)}")
|
||||
self.suggestion_text.config(state=tk.DISABLED)
|
||||
|
||||
def _on_result_select(self, event):
|
||||
"""结果选择事件"""
|
||||
selection = self.result_tree.selection()
|
||||
if selection:
|
||||
self.edit_button.config(state=tk.NORMAL)
|
||||
self.confirm_button.config(state=tk.NORMAL)
|
||||
else:
|
||||
self.edit_button.config(state=tk.DISABLED)
|
||||
self.confirm_button.config(state=tk.DISABLED)
|
||||
|
||||
def _on_result_double_click(self, event):
|
||||
"""结果双击事件"""
|
||||
self._edit_result()
|
||||
|
||||
def _edit_result(self):
|
||||
"""编辑识别结果"""
|
||||
selection = self.result_tree.selection()
|
||||
if not selection:
|
||||
messagebox.showwarning("警告", "请先选择要编辑的结果")
|
||||
return
|
||||
|
||||
try:
|
||||
item = selection[0]
|
||||
values = self.result_tree.item(item, 'values')
|
||||
|
||||
food_name = values[0]
|
||||
calories = values[1]
|
||||
|
||||
# 创建编辑对话框
|
||||
self._create_edit_dialog(food_name, calories, item)
|
||||
|
||||
except Exception as e:
|
||||
messagebox.showerror("错误", f"编辑结果失败: {str(e)}")
|
||||
|
||||
def _create_edit_dialog(self, food_name: str, calories: str, item_id: str):
|
||||
"""创建编辑对话框"""
|
||||
dialog = tk.Toplevel(self.parent_window)
|
||||
dialog.title("编辑识别结果")
|
||||
dialog.geometry("400x300")
|
||||
dialog.resizable(False, False)
|
||||
|
||||
# 居中显示
|
||||
dialog.transient(self.parent_window)
|
||||
dialog.grab_set()
|
||||
|
||||
# 创建表单
|
||||
form_frame = ttk.Frame(dialog, padding=20)
|
||||
form_frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# 食物名称
|
||||
ttk.Label(form_frame, text="食物名称:").pack(anchor=tk.W, pady=(0, 5))
|
||||
food_name_var = tk.StringVar(value=food_name)
|
||||
food_name_entry = ttk.Entry(form_frame, textvariable=food_name_var, width=40)
|
||||
food_name_entry.pack(fill=tk.X, pady=(0, 15))
|
||||
|
||||
# 热量
|
||||
ttk.Label(form_frame, text="热量(卡路里):").pack(anchor=tk.W, pady=(0, 5))
|
||||
calories_var = tk.StringVar(value=calories)
|
||||
calories_entry = ttk.Entry(form_frame, textvariable=calories_var, width=40)
|
||||
calories_entry.pack(fill=tk.X, pady=(0, 15))
|
||||
|
||||
# 置信度
|
||||
ttk.Label(form_frame, text="置信度:").pack(anchor=tk.W, pady=(0, 5))
|
||||
confidence_var = tk.StringVar(value="0.95")
|
||||
confidence_entry = ttk.Entry(form_frame, textvariable=confidence_var, width=40)
|
||||
confidence_entry.pack(fill=tk.X, pady=(0, 20))
|
||||
|
||||
# 按钮
|
||||
button_frame = ttk.Frame(form_frame)
|
||||
button_frame.pack(fill=tk.X)
|
||||
|
||||
def save_changes():
|
||||
try:
|
||||
new_food_name = food_name_var.get().strip()
|
||||
new_calories = calories_var.get().strip()
|
||||
new_confidence = float(confidence_var.get())
|
||||
|
||||
if not new_food_name:
|
||||
messagebox.showwarning("警告", "食物名称不能为空")
|
||||
return
|
||||
|
||||
if new_calories and new_calories != "未知":
|
||||
try:
|
||||
float(new_calories)
|
||||
except ValueError:
|
||||
messagebox.showwarning("警告", "热量必须是数字")
|
||||
return
|
||||
|
||||
# 更新表格
|
||||
self.result_tree.item(item_id, values=(
|
||||
new_food_name,
|
||||
new_calories,
|
||||
f"{new_confidence:.2f}",
|
||||
"user_corrected"
|
||||
))
|
||||
|
||||
# 保存用户修正
|
||||
self.user_corrections[new_food_name] = {
|
||||
'calories': float(new_calories) if new_calories and new_calories != "未知" else None,
|
||||
'confidence': new_confidence,
|
||||
'timestamp': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
dialog.destroy()
|
||||
messagebox.showinfo("成功", "结果已更新")
|
||||
|
||||
except Exception as e:
|
||||
messagebox.showerror("错误", f"保存失败: {str(e)}")
|
||||
|
||||
def cancel_changes():
|
||||
dialog.destroy()
|
||||
|
||||
ttk.Button(button_frame, text="保存", command=save_changes).pack(side=tk.LEFT, padx=(0, 10))
|
||||
ttk.Button(button_frame, text="取消", command=cancel_changes).pack(side=tk.LEFT)
|
||||
|
||||
def _confirm_result(self):
|
||||
"""确认识别结果"""
|
||||
if not self.current_recognition_result:
|
||||
messagebox.showwarning("警告", "没有可确认的结果")
|
||||
return
|
||||
|
||||
try:
|
||||
# 获取所有结果
|
||||
results = []
|
||||
for item in self.result_tree.get_children():
|
||||
values = self.result_tree.item(item, 'values')
|
||||
results.append({
|
||||
'food_name': values[0],
|
||||
'calories': float(values[1]) if values[1] != "未知" else None,
|
||||
'confidence': float(values[2]),
|
||||
'source': values[3]
|
||||
})
|
||||
|
||||
if not results:
|
||||
messagebox.showwarning("警告", "没有可确认的结果")
|
||||
return
|
||||
|
||||
# 确认对话框
|
||||
confirm_msg = "确认以下识别结果:\n\n"
|
||||
for i, result in enumerate(results, 1):
|
||||
confirm_msg += f"{i}. {result['food_name']}: {result['calories']} 卡路里\n"
|
||||
|
||||
confirm_msg += "\n是否确认这些结果?"
|
||||
|
||||
if messagebox.askyesno("确认结果", confirm_msg):
|
||||
# 保存到餐食记录
|
||||
self._save_to_meal_record(results)
|
||||
messagebox.showinfo("成功", "结果已保存到餐食记录")
|
||||
|
||||
# 清空结果
|
||||
self._clear_results()
|
||||
|
||||
except Exception as e:
|
||||
messagebox.showerror("错误", f"确认结果失败: {str(e)}")
|
||||
|
||||
def _save_to_meal_record(self, results: List[Dict[str, Any]]):
|
||||
"""保存到餐食记录"""
|
||||
try:
|
||||
# 这里需要调用应用核心的餐食记录功能
|
||||
# 暂时保存到本地文件
|
||||
meal_record = {
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'source': 'ocr_recognition',
|
||||
'foods': results,
|
||||
'total_calories': sum(r['calories'] for r in results if r['calories'])
|
||||
}
|
||||
|
||||
# 保存到文件
|
||||
record_file = Path("data/ocr_meal_records.json")
|
||||
record_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
records = []
|
||||
if record_file.exists():
|
||||
with open(record_file, 'r', encoding='utf-8') as f:
|
||||
records = json.load(f)
|
||||
|
||||
records.append(meal_record)
|
||||
|
||||
with open(record_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(records, f, ensure_ascii=False, indent=2)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"保存餐食记录失败: {e}")
|
||||
raise
|
||||
|
||||
def _clear_results(self):
|
||||
"""清空识别结果"""
|
||||
self.result_tree.delete(*self.result_tree.get_children())
|
||||
self.detail_text.delete(1.0, tk.END)
|
||||
|
||||
self.suggestion_text.config(state=tk.NORMAL)
|
||||
self.suggestion_text.delete(1.0, tk.END)
|
||||
self.suggestion_text.config(state=tk.DISABLED)
|
||||
|
||||
self.edit_button.config(state=tk.DISABLED)
|
||||
self.confirm_button.config(state=tk.DISABLED)
|
||||
|
||||
self.current_recognition_result = None
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 测试GUI
|
||||
root = tk.Tk()
|
||||
root.title("OCR热量识别测试")
|
||||
root.geometry("800x600")
|
||||
|
||||
# 模拟应用核心
|
||||
class MockAppCore:
|
||||
def __init__(self):
|
||||
self.config = type('Config', (), {})()
|
||||
|
||||
app_core = MockAppCore()
|
||||
|
||||
# 创建GUI
|
||||
ocr_gui = OCRCalorieGUI(root, app_core)
|
||||
|
||||
root.mainloop()
|
||||
526
gui/quick_user_input.py
Normal file
526
gui/quick_user_input.py
Normal file
@@ -0,0 +1,526 @@
|
||||
"""
|
||||
快速用户需求录入界面
|
||||
优化用户录入流程,提高效率
|
||||
"""
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, messagebox
|
||||
import customtkinter as ctk
|
||||
from typing import Dict, List, Optional
|
||||
from datetime import datetime
|
||||
import json
|
||||
|
||||
|
||||
class QuickUserInputDialog:
|
||||
"""快速用户需求录入对话框"""
|
||||
|
||||
def __init__(self, parent, user_id: str):
|
||||
self.parent = parent
|
||||
self.user_id = user_id
|
||||
self.input_data = {}
|
||||
|
||||
# 创建对话框
|
||||
self.dialog = ctk.CTkToplevel(parent)
|
||||
self.dialog.title("快速需求录入")
|
||||
self.dialog.geometry("800x600")
|
||||
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 _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="🚀 快速需求录入",
|
||||
font=ctk.CTkFont(size=24, weight="bold")
|
||||
)
|
||||
title_label.pack(pady=20)
|
||||
|
||||
# 步骤1:基础信息
|
||||
self._create_basic_info_section(main_frame)
|
||||
|
||||
# 步骤2:饮食偏好
|
||||
self._create_preferences_section(main_frame)
|
||||
|
||||
# 步骤3:健康目标
|
||||
self._create_health_goals_section(main_frame)
|
||||
|
||||
# 步骤4:快速餐食记录
|
||||
self._create_quick_meal_section(main_frame)
|
||||
|
||||
# 按钮区域
|
||||
self._create_buttons(main_frame)
|
||||
|
||||
def _create_basic_info_section(self, parent):
|
||||
"""创建基础信息区域"""
|
||||
section_frame = ctk.CTkFrame(parent)
|
||||
section_frame.pack(fill="x", padx=10, pady=10)
|
||||
|
||||
# 标题
|
||||
title = ctk.CTkLabel(
|
||||
section_frame,
|
||||
text="1️⃣ 基础信息",
|
||||
font=ctk.CTkFont(size=18, weight="bold")
|
||||
)
|
||||
title.pack(pady=10)
|
||||
|
||||
# 信息网格
|
||||
info_frame = ctk.CTkFrame(section_frame)
|
||||
info_frame.pack(fill="x", padx=20, pady=10)
|
||||
|
||||
# 姓名
|
||||
name_label = ctk.CTkLabel(info_frame, text="姓名:")
|
||||
name_label.grid(row=0, column=0, sticky="w", padx=10, pady=5)
|
||||
|
||||
self.name_var = tk.StringVar()
|
||||
name_entry = ctk.CTkEntry(info_frame, textvariable=self.name_var, width=200)
|
||||
name_entry.grid(row=0, column=1, sticky="w", padx=10, pady=5)
|
||||
|
||||
# 年龄范围
|
||||
age_label = ctk.CTkLabel(info_frame, text="年龄范围:")
|
||||
age_label.grid(row=0, column=2, sticky="w", padx=10, pady=5)
|
||||
|
||||
self.age_var = tk.StringVar(value="25-30岁")
|
||||
age_menu = ctk.CTkOptionMenu(
|
||||
info_frame,
|
||||
variable=self.age_var,
|
||||
values=["18-24岁", "25-30岁", "31-35岁", "36-40岁", "41-45岁", "46-50岁", "51-55岁", "56-60岁", "60岁以上"]
|
||||
)
|
||||
age_menu.grid(row=0, column=3, sticky="w", padx=10, pady=5)
|
||||
|
||||
# 性别
|
||||
gender_label = ctk.CTkLabel(info_frame, text="性别:")
|
||||
gender_label.grid(row=1, column=0, sticky="w", padx=10, pady=5)
|
||||
|
||||
self.gender_var = tk.StringVar(value="女")
|
||||
gender_menu = ctk.CTkOptionMenu(
|
||||
info_frame,
|
||||
variable=self.gender_var,
|
||||
values=["男", "女"]
|
||||
)
|
||||
gender_menu.grid(row=1, column=1, sticky="w", padx=10, pady=5)
|
||||
|
||||
# 身高体重范围
|
||||
height_label = ctk.CTkLabel(info_frame, text="身高范围:")
|
||||
height_label.grid(row=1, column=2, sticky="w", padx=10, pady=5)
|
||||
|
||||
self.height_var = tk.StringVar(value="160-165cm")
|
||||
height_menu = ctk.CTkOptionMenu(
|
||||
info_frame,
|
||||
variable=self.height_var,
|
||||
values=["150cm以下", "150-155cm", "155-160cm", "160-165cm", "165-170cm", "170-175cm", "175-180cm", "180cm以上"]
|
||||
)
|
||||
height_menu.grid(row=1, column=3, sticky="w", padx=10, pady=5)
|
||||
|
||||
weight_label = ctk.CTkLabel(info_frame, text="体重范围:")
|
||||
weight_label.grid(row=2, column=0, sticky="w", padx=10, pady=5)
|
||||
|
||||
self.weight_var = tk.StringVar(value="50-55kg")
|
||||
weight_menu = ctk.CTkOptionMenu(
|
||||
info_frame,
|
||||
variable=self.weight_var,
|
||||
values=["40kg以下", "40-45kg", "45-50kg", "50-55kg", "55-60kg", "60-65kg", "65-70kg", "70-75kg", "75-80kg", "80kg以上"]
|
||||
)
|
||||
weight_menu.grid(row=2, column=1, sticky="w", padx=10, pady=5)
|
||||
|
||||
# 活动水平
|
||||
activity_label = ctk.CTkLabel(info_frame, text="活动水平:")
|
||||
activity_label.grid(row=2, column=2, sticky="w", padx=10, pady=5)
|
||||
|
||||
self.activity_var = tk.StringVar(value="中等")
|
||||
activity_menu = ctk.CTkOptionMenu(
|
||||
info_frame,
|
||||
variable=self.activity_var,
|
||||
values=["久坐", "轻度活动", "中等", "高度活动", "极度活动"]
|
||||
)
|
||||
activity_menu.grid(row=2, column=3, sticky="w", padx=10, pady=5)
|
||||
|
||||
def _create_preferences_section(self, parent):
|
||||
"""创建饮食偏好区域"""
|
||||
section_frame = ctk.CTkFrame(parent)
|
||||
section_frame.pack(fill="x", padx=10, pady=10)
|
||||
|
||||
# 标题
|
||||
title = ctk.CTkLabel(
|
||||
section_frame,
|
||||
text="2️⃣ 饮食偏好",
|
||||
font=ctk.CTkFont(size=18, weight="bold")
|
||||
)
|
||||
title.pack(pady=10)
|
||||
|
||||
# 偏好网格
|
||||
pref_frame = ctk.CTkFrame(section_frame)
|
||||
pref_frame.pack(fill="x", padx=20, pady=10)
|
||||
|
||||
# 口味偏好
|
||||
taste_label = ctk.CTkLabel(pref_frame, text="主要口味偏好:")
|
||||
taste_label.grid(row=0, column=0, sticky="w", padx=10, pady=5)
|
||||
|
||||
self.taste_var = tk.StringVar(value="均衡")
|
||||
taste_menu = ctk.CTkOptionMenu(
|
||||
pref_frame,
|
||||
variable=self.taste_var,
|
||||
values=["均衡", "偏甜", "偏咸", "偏辣", "偏酸", "偏清淡"]
|
||||
)
|
||||
taste_menu.grid(row=0, column=1, sticky="w", padx=10, pady=5)
|
||||
|
||||
# 饮食类型
|
||||
diet_label = ctk.CTkLabel(pref_frame, text="饮食类型:")
|
||||
diet_label.grid(row=0, column=2, sticky="w", padx=10, pady=5)
|
||||
|
||||
self.diet_var = tk.StringVar(value="普通饮食")
|
||||
diet_menu = ctk.CTkOptionMenu(
|
||||
pref_frame,
|
||||
variable=self.diet_var,
|
||||
values=["普通饮食", "素食", "低脂饮食", "低糖饮食", "高蛋白饮食", "地中海饮食"]
|
||||
)
|
||||
diet_menu.grid(row=0, column=3, sticky="w", padx=10, pady=5)
|
||||
|
||||
# 过敏食物
|
||||
allergy_label = ctk.CTkLabel(pref_frame, text="过敏食物:")
|
||||
allergy_label.grid(row=1, column=0, sticky="w", padx=10, pady=5)
|
||||
|
||||
self.allergy_var = tk.StringVar(value="无")
|
||||
allergy_menu = ctk.CTkOptionMenu(
|
||||
pref_frame,
|
||||
variable=self.allergy_var,
|
||||
values=["无", "花生", "海鲜", "牛奶", "鸡蛋", "坚果", "大豆", "小麦", "其他"]
|
||||
)
|
||||
allergy_menu.grid(row=1, column=1, sticky="w", padx=10, pady=5)
|
||||
|
||||
# 不喜欢食物
|
||||
dislike_label = ctk.CTkLabel(pref_frame, text="不喜欢食物:")
|
||||
dislike_label.grid(row=1, column=2, sticky="w", padx=10, pady=5)
|
||||
|
||||
self.dislike_var = tk.StringVar(value="无")
|
||||
dislike_menu = ctk.CTkOptionMenu(
|
||||
pref_frame,
|
||||
variable=self.dislike_var,
|
||||
values=["无", "内脏", "辛辣", "油腻", "甜食", "酸味", "苦味", "其他"]
|
||||
)
|
||||
dislike_menu.grid(row=1, column=3, sticky="w", padx=10, pady=5)
|
||||
|
||||
def _create_health_goals_section(self, parent):
|
||||
"""创建健康目标区域"""
|
||||
section_frame = ctk.CTkFrame(parent)
|
||||
section_frame.pack(fill="x", padx=10, pady=10)
|
||||
|
||||
# 标题
|
||||
title = ctk.CTkLabel(
|
||||
section_frame,
|
||||
text="3️⃣ 健康目标",
|
||||
font=ctk.CTkFont(size=18, weight="bold")
|
||||
)
|
||||
title.pack(pady=10)
|
||||
|
||||
# 目标选择
|
||||
goals_frame = ctk.CTkFrame(section_frame)
|
||||
goals_frame.pack(fill="x", padx=20, pady=10)
|
||||
|
||||
# 主要目标
|
||||
main_goal_label = ctk.CTkLabel(goals_frame, text="主要目标:")
|
||||
main_goal_label.grid(row=0, column=0, sticky="w", padx=10, pady=5)
|
||||
|
||||
self.main_goal_var = tk.StringVar(value="保持健康")
|
||||
main_goal_menu = ctk.CTkOptionMenu(
|
||||
goals_frame,
|
||||
variable=self.main_goal_var,
|
||||
values=["保持健康", "减重", "增重", "增肌", "改善消化", "提高免疫力", "控制血糖", "降低血压"]
|
||||
)
|
||||
main_goal_menu.grid(row=0, column=1, sticky="w", padx=10, pady=5)
|
||||
|
||||
# 次要目标
|
||||
sub_goal_label = ctk.CTkLabel(goals_frame, text="次要目标:")
|
||||
sub_goal_label.grid(row=0, column=2, sticky="w", padx=10, pady=5)
|
||||
|
||||
self.sub_goal_var = tk.StringVar(value="无")
|
||||
sub_goal_menu = ctk.CTkOptionMenu(
|
||||
goals_frame,
|
||||
variable=self.sub_goal_var,
|
||||
values=["无", "改善睡眠", "提高精力", "美容养颜", "延缓衰老", "改善皮肤", "增强记忆", "缓解压力"]
|
||||
)
|
||||
sub_goal_menu.grid(row=0, column=3, sticky="w", padx=10, pady=5)
|
||||
|
||||
def _create_quick_meal_section(self, parent):
|
||||
"""创建快速餐食记录区域"""
|
||||
section_frame = ctk.CTkFrame(parent)
|
||||
section_frame.pack(fill="x", padx=10, pady=10)
|
||||
|
||||
# 标题
|
||||
title = ctk.CTkLabel(
|
||||
section_frame,
|
||||
text="4️⃣ 快速餐食记录",
|
||||
font=ctk.CTkFont(size=18, weight="bold")
|
||||
)
|
||||
title.pack(pady=10)
|
||||
|
||||
# 餐食选择
|
||||
meal_frame = ctk.CTkFrame(section_frame)
|
||||
meal_frame.pack(fill="x", padx=20, pady=10)
|
||||
|
||||
# 餐次选择
|
||||
meal_type_label = ctk.CTkLabel(meal_frame, text="餐次:")
|
||||
meal_type_label.grid(row=0, column=0, sticky="w", padx=10, pady=5)
|
||||
|
||||
self.meal_type_var = tk.StringVar(value="午餐")
|
||||
meal_type_menu = ctk.CTkOptionMenu(
|
||||
meal_frame,
|
||||
variable=self.meal_type_var,
|
||||
values=["早餐", "午餐", "晚餐", "加餐"]
|
||||
)
|
||||
meal_type_menu.grid(row=0, column=1, sticky="w", padx=10, pady=5)
|
||||
|
||||
# 快速食物选择
|
||||
food_label = ctk.CTkLabel(meal_frame, text="快速选择食物:")
|
||||
food_label.grid(row=1, column=0, sticky="w", padx=10, pady=5)
|
||||
|
||||
self.quick_food_var = tk.StringVar(value="米饭+鸡肉+蔬菜")
|
||||
quick_food_menu = ctk.CTkOptionMenu(
|
||||
meal_frame,
|
||||
variable=self.quick_food_var,
|
||||
values=[
|
||||
"米饭+鸡肉+蔬菜",
|
||||
"面条+鸡蛋+青菜",
|
||||
"馒头+豆腐+白菜",
|
||||
"粥+咸菜+鸡蛋",
|
||||
"面包+牛奶+水果",
|
||||
"饺子+汤",
|
||||
"包子+豆浆",
|
||||
"其他"
|
||||
]
|
||||
)
|
||||
quick_food_menu.grid(row=1, column=1, sticky="w", padx=10, pady=5)
|
||||
|
||||
# 分量选择
|
||||
portion_label = ctk.CTkLabel(meal_frame, text="分量:")
|
||||
portion_label.grid(row=1, column=2, sticky="w", padx=10, pady=5)
|
||||
|
||||
self.portion_var = tk.StringVar(value="正常")
|
||||
portion_menu = ctk.CTkOptionMenu(
|
||||
meal_frame,
|
||||
variable=self.portion_var,
|
||||
values=["少量", "正常", "较多", "很多"]
|
||||
)
|
||||
portion_menu.grid(row=1, column=3, sticky="w", padx=10, pady=5)
|
||||
|
||||
# 满意度
|
||||
satisfaction_label = ctk.CTkLabel(meal_frame, text="满意度:")
|
||||
satisfaction_label.grid(row=2, column=0, sticky="w", padx=10, pady=5)
|
||||
|
||||
self.satisfaction_var = tk.IntVar(value=3)
|
||||
satisfaction_slider = ctk.CTkSlider(
|
||||
meal_frame,
|
||||
from_=1,
|
||||
to=5,
|
||||
number_of_steps=4,
|
||||
variable=self.satisfaction_var
|
||||
)
|
||||
satisfaction_slider.grid(row=2, column=1, columnspan=2, sticky="ew", padx=10, pady=5)
|
||||
|
||||
# 满意度标签
|
||||
self.satisfaction_display = ctk.CTkLabel(meal_frame, text="3分 - 一般")
|
||||
self.satisfaction_display.grid(row=2, column=3, sticky="w", padx=10, pady=5)
|
||||
|
||||
# 绑定滑块事件
|
||||
satisfaction_slider.configure(command=self._on_satisfaction_changed)
|
||||
|
||||
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_all_data,
|
||||
width=200,
|
||||
height=50,
|
||||
font=ctk.CTkFont(size=16, weight="bold")
|
||||
)
|
||||
save_button.pack(side="left", padx=20, pady=10)
|
||||
|
||||
# 取消按钮
|
||||
cancel_button = ctk.CTkButton(
|
||||
button_frame,
|
||||
text="❌ 取消",
|
||||
command=self._cancel,
|
||||
width=150,
|
||||
height=50
|
||||
)
|
||||
cancel_button.pack(side="right", padx=20, pady=10)
|
||||
|
||||
def _on_satisfaction_changed(self, value):
|
||||
"""满意度改变事件"""
|
||||
score = int(float(value))
|
||||
score_texts = {
|
||||
1: "1分 - 很不满意",
|
||||
2: "2分 - 不满意",
|
||||
3: "3分 - 一般",
|
||||
4: "4分 - 满意",
|
||||
5: "5分 - 很满意"
|
||||
}
|
||||
self.satisfaction_display.configure(text=score_texts.get(score, "3分 - 一般"))
|
||||
|
||||
def _save_all_data(self):
|
||||
"""保存所有数据"""
|
||||
try:
|
||||
# 收集所有数据
|
||||
self.input_data = {
|
||||
'basic_info': {
|
||||
'name': self.name_var.get(),
|
||||
'age_range': self.age_var.get(),
|
||||
'gender': self.gender_var.get(),
|
||||
'height_range': self.height_var.get(),
|
||||
'weight_range': self.weight_var.get(),
|
||||
'activity_level': self.activity_var.get()
|
||||
},
|
||||
'preferences': {
|
||||
'taste': self.taste_var.get(),
|
||||
'diet_type': self.diet_var.get(),
|
||||
'allergies': self.allergy_var.get(),
|
||||
'dislikes': self.dislike_var.get()
|
||||
},
|
||||
'health_goals': {
|
||||
'main_goal': self.main_goal_var.get(),
|
||||
'sub_goal': self.sub_goal_var.get()
|
||||
},
|
||||
'quick_meal': {
|
||||
'meal_type': self.meal_type_var.get(),
|
||||
'food_combo': self.quick_food_var.get(),
|
||||
'portion': self.portion_var.get(),
|
||||
'satisfaction': self.satisfaction_var.get()
|
||||
}
|
||||
}
|
||||
|
||||
# 保存到数据库
|
||||
if self._save_to_database():
|
||||
messagebox.showinfo("成功", "所有信息保存成功!")
|
||||
self.dialog.destroy()
|
||||
else:
|
||||
messagebox.showerror("错误", "保存失败,请重试")
|
||||
|
||||
except Exception as e:
|
||||
messagebox.showerror("错误", f"保存失败: {str(e)}")
|
||||
|
||||
def _save_to_database(self) -> bool:
|
||||
"""保存到数据库"""
|
||||
try:
|
||||
from modules.data_collection import collect_questionnaire_data, record_meal
|
||||
|
||||
# 保存基础信息
|
||||
basic_data = self.input_data['basic_info']
|
||||
age_mapping = {
|
||||
"18-24岁": 21, "25-30岁": 27, "31-35岁": 33, "36-40岁": 38,
|
||||
"41-45岁": 43, "46-50岁": 48, "51-55岁": 53, "56-60岁": 58, "60岁以上": 65
|
||||
}
|
||||
height_mapping = {
|
||||
"150cm以下": 150, "150-155cm": 152, "155-160cm": 157, "160-165cm": 162,
|
||||
"165-170cm": 167, "170-175cm": 172, "175-180cm": 177, "180cm以上": 180
|
||||
}
|
||||
weight_mapping = {
|
||||
"40kg以下": 40, "40-45kg": 42, "45-50kg": 47, "50-55kg": 52,
|
||||
"55-60kg": 57, "60-65kg": 62, "65-70kg": 67, "70-75kg": 72,
|
||||
"75-80kg": 77, "80kg以上": 80
|
||||
}
|
||||
activity_mapping = {
|
||||
"久坐": "sedentary", "轻度活动": "light", "中等": "moderate",
|
||||
"高度活动": "high", "极度活动": "very_high"
|
||||
}
|
||||
|
||||
basic_answers = {
|
||||
'name': basic_data['name'],
|
||||
'age': age_mapping.get(basic_data['age_range'], 25),
|
||||
'gender': basic_data['gender'],
|
||||
'height': height_mapping.get(basic_data['height_range'], 165),
|
||||
'weight': weight_mapping.get(basic_data['weight_range'], 55),
|
||||
'activity_level': activity_mapping.get(basic_data['activity_level'], 'moderate')
|
||||
}
|
||||
|
||||
collect_questionnaire_data(self.user_id, 'basic', basic_answers)
|
||||
|
||||
# 保存口味偏好
|
||||
preferences_data = self.input_data['preferences']
|
||||
taste_answers = {
|
||||
'taste_preference': preferences_data['taste'],
|
||||
'diet_type': preferences_data['diet_type'],
|
||||
'allergies': [preferences_data['allergies']] if preferences_data['allergies'] != "无" else [],
|
||||
'dislikes': [preferences_data['dislikes']] if preferences_data['dislikes'] != "无" else []
|
||||
}
|
||||
|
||||
collect_questionnaire_data(self.user_id, 'taste', taste_answers)
|
||||
|
||||
# 保存健康目标
|
||||
health_data = self.input_data['health_goals']
|
||||
health_answers = {
|
||||
'main_goal': health_data['main_goal'],
|
||||
'sub_goal': health_data['sub_goal']
|
||||
}
|
||||
|
||||
collect_questionnaire_data(self.user_id, 'health', health_answers)
|
||||
|
||||
# 保存快速餐食记录
|
||||
meal_data = self.input_data['quick_meal']
|
||||
meal_type_mapping = {
|
||||
"早餐": "breakfast", "午餐": "lunch", "晚餐": "dinner", "加餐": "snack"
|
||||
}
|
||||
|
||||
# 解析食物组合
|
||||
food_combo = meal_data['food_combo']
|
||||
if "+" in food_combo:
|
||||
foods = [food.strip() for food in food_combo.split("+")]
|
||||
else:
|
||||
foods = [food_combo]
|
||||
|
||||
# 估算分量
|
||||
portion_mapping = {
|
||||
"少量": "1小份", "正常": "1份", "较多": "1大份", "很多": "2份"
|
||||
}
|
||||
quantities = [portion_mapping.get(meal_data['portion'], "1份")] * len(foods)
|
||||
|
||||
meal_record = {
|
||||
'date': datetime.now().strftime('%Y-%m-%d'),
|
||||
'meal_type': meal_type_mapping.get(meal_data['meal_type'], 'lunch'),
|
||||
'foods': foods,
|
||||
'quantities': quantities,
|
||||
'satisfaction_score': meal_data['satisfaction']
|
||||
}
|
||||
|
||||
record_meal(self.user_id, meal_record)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"保存到数据库失败: {e}")
|
||||
return False
|
||||
|
||||
def _cancel(self):
|
||||
"""取消录入"""
|
||||
self.dialog.destroy()
|
||||
|
||||
|
||||
# 便捷函数
|
||||
def show_quick_user_input_dialog(parent, user_id: str):
|
||||
"""显示快速用户需求录入对话框"""
|
||||
dialog = QuickUserInputDialog(parent, user_id)
|
||||
parent.wait_window(dialog.dialog)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 测试快速录入对话框
|
||||
root = tk.Tk()
|
||||
root.title("测试快速录入")
|
||||
def test_dialog():
|
||||
show_quick_user_input_dialog(root, "test_user")
|
||||
test_button = tk.Button(root, text="测试快速录入", command=test_dialog)
|
||||
test_button.pack(pady=20)
|
||||
root.mainloop()
|
||||
579
gui/smart_meal_record.py
Normal file
579
gui/smart_meal_record.py
Normal 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()
|
||||
326
gui/styles.py
Normal file
326
gui/styles.py
Normal file
@@ -0,0 +1,326 @@
|
||||
"""
|
||||
界面美化样式配置
|
||||
提供统一的圆角设计和颜色主题
|
||||
"""
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
import customtkinter as ctk
|
||||
|
||||
|
||||
class StyleConfig:
|
||||
"""样式配置类"""
|
||||
|
||||
# 颜色主题
|
||||
COLORS = {
|
||||
'primary': '#3498db',
|
||||
'primary_hover': '#2980b9',
|
||||
'secondary': '#2ecc71',
|
||||
'secondary_hover': '#27ae60',
|
||||
'accent': '#e74c3c',
|
||||
'accent_hover': '#c0392b',
|
||||
'warning': '#f39c12',
|
||||
'warning_hover': '#e67e22',
|
||||
'info': '#9b59b6',
|
||||
'info_hover': '#8e44ad',
|
||||
'success': '#27ae60',
|
||||
'success_hover': '#229954',
|
||||
'danger': '#e74c3c',
|
||||
'danger_hover': '#c0392b',
|
||||
|
||||
# 背景色
|
||||
'bg_light': '#ffffff',
|
||||
'bg_dark': '#2b2b2b',
|
||||
'bg_card': '#f8f9fa',
|
||||
'bg_card_dark': '#3b3b3b',
|
||||
'bg_container': '#f0f0f0',
|
||||
'bg_container_dark': '#1e1e1e',
|
||||
|
||||
# 文字色
|
||||
'text_primary': '#2c3e50',
|
||||
'text_primary_dark': '#ecf0f1',
|
||||
'text_secondary': '#34495e',
|
||||
'text_secondary_dark': '#bdc3c7',
|
||||
'text_muted': '#7f8c8d',
|
||||
'text_muted_dark': '#95a5a6',
|
||||
|
||||
# 边框色
|
||||
'border': '#e0e0e0',
|
||||
'border_dark': '#404040',
|
||||
'border_light': '#f0f0f0',
|
||||
'border_light_dark': '#555555',
|
||||
}
|
||||
|
||||
# 圆角半径
|
||||
CORNER_RADIUS = {
|
||||
'small': 8,
|
||||
'medium': 12,
|
||||
'large': 15,
|
||||
'xlarge': 20,
|
||||
'xxlarge': 25,
|
||||
}
|
||||
|
||||
# 字体配置
|
||||
FONTS = {
|
||||
'title': ('Arial', 22, 'bold'),
|
||||
'subtitle': ('Arial', 18, 'bold'),
|
||||
'heading': ('Arial', 16, 'bold'),
|
||||
'body': ('Arial', 14),
|
||||
'small': ('Arial', 12),
|
||||
'tiny': ('Arial', 10),
|
||||
}
|
||||
|
||||
# 间距配置
|
||||
SPACING = {
|
||||
'xs': 5,
|
||||
'sm': 10,
|
||||
'md': 15,
|
||||
'lg': 20,
|
||||
'xl': 25,
|
||||
'xxl': 30,
|
||||
}
|
||||
|
||||
|
||||
def apply_rounded_theme():
|
||||
"""应用圆角主题到CustomTkinter"""
|
||||
# 设置全局主题
|
||||
ctk.set_appearance_mode("light")
|
||||
ctk.set_default_color_theme("blue")
|
||||
|
||||
# 注意:CustomTkinter不支持类级别的configure方法
|
||||
# 样式需要在创建组件时单独设置
|
||||
print("✅ 圆角主题已应用")
|
||||
|
||||
|
||||
def create_rounded_frame(parent, **kwargs):
|
||||
"""创建圆角框架"""
|
||||
colors = StyleConfig.COLORS
|
||||
radius = StyleConfig.CORNER_RADIUS
|
||||
|
||||
default_kwargs = {
|
||||
'corner_radius': radius['medium'],
|
||||
'fg_color': (colors['bg_light'], colors['bg_dark']),
|
||||
'border_width': 1,
|
||||
'border_color': (colors['border'], colors['border_dark']),
|
||||
}
|
||||
|
||||
default_kwargs.update(kwargs)
|
||||
return ctk.CTkFrame(parent, **default_kwargs)
|
||||
|
||||
|
||||
def create_rounded_button(parent, text, **kwargs):
|
||||
"""创建圆角按钮"""
|
||||
colors = StyleConfig.COLORS
|
||||
radius = StyleConfig.CORNER_RADIUS
|
||||
|
||||
default_kwargs = {
|
||||
'corner_radius': radius['medium'],
|
||||
'fg_color': (colors['primary'], colors['primary_hover']),
|
||||
'hover_color': (colors['primary_hover'], colors['primary']),
|
||||
'text_color': ('#ffffff', '#ffffff'),
|
||||
'font': StyleConfig.FONTS['body'],
|
||||
}
|
||||
|
||||
default_kwargs.update(kwargs)
|
||||
return ctk.CTkButton(parent, text=text, **default_kwargs)
|
||||
|
||||
|
||||
def create_rounded_entry(parent, **kwargs):
|
||||
"""创建圆角输入框"""
|
||||
colors = StyleConfig.COLORS
|
||||
radius = StyleConfig.CORNER_RADIUS
|
||||
|
||||
default_kwargs = {
|
||||
'corner_radius': radius['medium'],
|
||||
'fg_color': (colors['bg_card'], colors['bg_card_dark']),
|
||||
'border_width': 1,
|
||||
'border_color': (colors['border_light'], colors['border_light_dark']),
|
||||
'font': StyleConfig.FONTS['body'],
|
||||
'text_color': (colors['text_primary'], colors['text_primary_dark']),
|
||||
}
|
||||
|
||||
default_kwargs.update(kwargs)
|
||||
return ctk.CTkEntry(parent, **default_kwargs)
|
||||
|
||||
|
||||
def create_rounded_label(parent, text, **kwargs):
|
||||
"""创建圆角标签"""
|
||||
colors = StyleConfig.COLORS
|
||||
|
||||
default_kwargs = {
|
||||
'text_color': (colors['text_primary'], colors['text_primary_dark']),
|
||||
'font': StyleConfig.FONTS['body'],
|
||||
}
|
||||
|
||||
default_kwargs.update(kwargs)
|
||||
return ctk.CTkLabel(parent, text=text, **default_kwargs)
|
||||
|
||||
|
||||
def create_card_frame(parent, **kwargs):
|
||||
"""创建卡片式框架"""
|
||||
colors = StyleConfig.COLORS
|
||||
radius = StyleConfig.CORNER_RADIUS
|
||||
|
||||
default_kwargs = {
|
||||
'corner_radius': radius['large'],
|
||||
'fg_color': (colors['bg_card'], colors['bg_card_dark']),
|
||||
'border_width': 1,
|
||||
'border_color': (colors['border'], colors['border_dark']),
|
||||
}
|
||||
|
||||
default_kwargs.update(kwargs)
|
||||
return ctk.CTkFrame(parent, **default_kwargs)
|
||||
|
||||
|
||||
def create_accent_button(parent, text, color_type='primary', **kwargs):
|
||||
"""创建强调色按钮"""
|
||||
colors = StyleConfig.COLORS
|
||||
radius = StyleConfig.CORNER_RADIUS
|
||||
|
||||
color_map = {
|
||||
'primary': (colors['primary'], colors['primary_hover']),
|
||||
'secondary': (colors['secondary'], colors['secondary_hover']),
|
||||
'accent': (colors['accent'], colors['accent_hover']),
|
||||
'warning': (colors['warning'], colors['warning_hover']),
|
||||
'info': (colors['info'], colors['info_hover']),
|
||||
'success': (colors['success'], colors['success_hover']),
|
||||
'danger': (colors['danger'], colors['danger_hover']),
|
||||
}
|
||||
|
||||
fg_color, hover_color = color_map.get(color_type, color_map['primary'])
|
||||
|
||||
default_kwargs = {
|
||||
'corner_radius': radius['medium'],
|
||||
'fg_color': fg_color,
|
||||
'hover_color': hover_color,
|
||||
'text_color': ('#ffffff', '#ffffff'),
|
||||
'font': StyleConfig.FONTS['body'],
|
||||
}
|
||||
|
||||
default_kwargs.update(kwargs)
|
||||
return ctk.CTkButton(parent, text=text, **default_kwargs)
|
||||
|
||||
|
||||
def apply_ttk_styles():
|
||||
"""应用TTK样式"""
|
||||
style = ttk.Style()
|
||||
|
||||
# 配置样式
|
||||
style.configure('Rounded.TFrame',
|
||||
relief='solid',
|
||||
borderwidth=1,
|
||||
background='#f8f9fa')
|
||||
|
||||
style.configure('Rounded.TLabelFrame',
|
||||
relief='solid',
|
||||
borderwidth=1,
|
||||
background='#f8f9fa')
|
||||
|
||||
style.configure('Accent.TButton',
|
||||
relief='flat',
|
||||
borderwidth=0,
|
||||
background='#3498db',
|
||||
foreground='white',
|
||||
font=('Arial', 12, 'bold'))
|
||||
|
||||
style.map('Accent.TButton',
|
||||
background=[('active', '#2980b9'),
|
||||
('pressed', '#1f618d')])
|
||||
|
||||
style.configure('Accent.TProgressbar',
|
||||
background='#3498db',
|
||||
troughcolor='#ecf0f1',
|
||||
borderwidth=0,
|
||||
lightcolor='#3498db',
|
||||
darkcolor='#3498db')
|
||||
|
||||
|
||||
def get_spacing(size='md'):
|
||||
"""获取间距值"""
|
||||
return StyleConfig.SPACING.get(size, StyleConfig.SPACING['md'])
|
||||
|
||||
|
||||
def get_font(font_type='body'):
|
||||
"""获取字体配置"""
|
||||
return StyleConfig.FONTS.get(font_type, StyleConfig.FONTS['body'])
|
||||
|
||||
|
||||
def get_color(color_name):
|
||||
"""获取颜色值"""
|
||||
return StyleConfig.COLORS.get(color_name, StyleConfig.COLORS['text_primary'])
|
||||
|
||||
|
||||
def get_radius(size='medium'):
|
||||
"""获取圆角半径"""
|
||||
return StyleConfig.CORNER_RADIUS.get(size, StyleConfig.CORNER_RADIUS['medium'])
|
||||
|
||||
|
||||
# 预定义的样式组合
|
||||
STYLE_PRESETS = {
|
||||
'card': {
|
||||
'corner_radius': 20,
|
||||
'fg_color': ('#ffffff', '#3b3b3b'),
|
||||
'border_width': 1,
|
||||
'border_color': ('#e0e0e0', '#404040'),
|
||||
},
|
||||
'button_primary': {
|
||||
'corner_radius': 15,
|
||||
'fg_color': ('#3498db', '#2980b9'),
|
||||
'hover_color': ('#2980b9', '#1f618d'),
|
||||
'text_color': ('#ffffff', '#ffffff'),
|
||||
},
|
||||
'button_secondary': {
|
||||
'corner_radius': 15,
|
||||
'fg_color': ('#2ecc71', '#27ae60'),
|
||||
'hover_color': ('#27ae60', '#229954'),
|
||||
'text_color': ('#ffffff', '#ffffff'),
|
||||
},
|
||||
'button_accent': {
|
||||
'corner_radius': 15,
|
||||
'fg_color': ('#e74c3c', '#c0392b'),
|
||||
'hover_color': ('#c0392b', '#a93226'),
|
||||
'text_color': ('#ffffff', '#ffffff'),
|
||||
},
|
||||
'input_field': {
|
||||
'corner_radius': 12,
|
||||
'fg_color': ('#f8f9fa', '#404040'),
|
||||
'border_width': 1,
|
||||
'border_color': ('#e0e0e0', '#555555'),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def apply_preset_style(widget, preset_name):
|
||||
"""应用预设样式"""
|
||||
if preset_name in STYLE_PRESETS:
|
||||
style_config = STYLE_PRESETS[preset_name]
|
||||
for key, value in style_config.items():
|
||||
if hasattr(widget, key):
|
||||
setattr(widget, key, value)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 测试样式配置
|
||||
root = tk.Tk()
|
||||
root.title("样式测试")
|
||||
root.geometry("400x300")
|
||||
|
||||
# 应用TTK样式
|
||||
apply_ttk_styles()
|
||||
|
||||
# 创建测试框架
|
||||
main_frame = ttk.Frame(root, style='Rounded.TFrame')
|
||||
main_frame.pack(fill='both', expand=True, padx=20, pady=20)
|
||||
|
||||
# 创建测试按钮
|
||||
test_button = ttk.Button(main_frame, text="测试按钮", style='Accent.TButton')
|
||||
test_button.pack(pady=10)
|
||||
|
||||
# 创建测试标签框架
|
||||
test_frame = ttk.LabelFrame(main_frame, text="测试框架", style='Rounded.TLabelFrame')
|
||||
test_frame.pack(fill='both', expand=True, pady=10)
|
||||
|
||||
test_label = ttk.Label(test_frame, text="这是一个测试标签")
|
||||
test_label.pack(pady=20)
|
||||
|
||||
root.mainloop()
|
||||
Reference in New Issue
Block a user