已点菜品栏更新

This commit is contained in:
LiXin01 2026-10-09 16:09:27 +08:00
parent 93fb61e140
commit 1bef9631eb
2 changed files with 243 additions and 64 deletions

View File

@ -144,6 +144,24 @@ class MealScheduleStore:
return False return False
return name in self.today_dish_names() return name in self.today_dish_names()
def lookup_price(self, dish_name):
"""按菜名查今日排餐价格;未命中返回 None。"""
name = (dish_name or '').strip()
if not name:
return None
today = datetime.now().strftime('%Y-%m-%d')
with self._lock:
raw = self._raw
date = self._date
if not raw or date != today:
return None
for base in raw.get('bases') or []:
for meal in base.get('meals') or []:
for dish in meal.get('dishes') or []:
if (dish.get('name') or '').strip() == name:
return dish.get('price')
return None
def current_period(self, now=None): def current_period(self, now=None):
""" """
根据当前时间匹配餐段。 根据当前时间匹配餐段。

View File

@ -134,6 +134,7 @@ class BaseDishApp(PendingLearningUIMixin):
self._order_locked = False # 报过一单后锁定,清场后才允许下一单 self._order_locked = False # 报过一单后锁定,清场后才允许下一单
self._order_clear_since = None # 无盘连续计时起点 self._order_clear_since = None # 无盘连续计时起点
self._order_clear_hold = 1.5 # 无盘持续多久才解锁(秒) self._order_clear_hold = 1.5 # 无盘持续多久才解锁(秒)
self._cart_hold = False # 本盘清单已展示:清场前不刷新
self._last_detect_fp = None # 上一帧指纹(静止画面跳过检测) self._last_detect_fp = None # 上一帧指纹(静止画面跳过检测)
self._last_live_dishes = None # 上一轮识别结果(跳过检测时推进稳定计时) self._last_live_dishes = None # 上一轮识别结果(跳过检测时推进稳定计时)
self._frame_sim_threshold = 0.992 # 帧相似度阈值:非常相似则跳过 YOLO/分类 self._frame_sim_threshold = 0.992 # 帧相似度阈值:非常相似则跳过 YOLO/分类
@ -304,11 +305,15 @@ class BaseDishApp(PendingLearningUIMixin):
def _on_wheel_all(self, event): def _on_wheel_all(self, event):
alive = [] alive = []
menu_cv = getattr(self, "_menu_canvas", None)
for canvas in list(getattr(self, "_scroll_canvases", [])): for canvas in list(getattr(self, "_scroll_canvases", [])):
try: try:
if not canvas.winfo_exists(): if not canvas.winfo_exists():
continue continue
alive.append(canvas) alive.append(canvas)
# 今日菜单为空时禁止滚动
if menu_cv is not None and canvas is menu_cv and not getattr(self, "_menu_scroll_enabled", True):
continue
x, y = canvas.winfo_rootx(), canvas.winfo_rooty() x, y = canvas.winfo_rootx(), canvas.winfo_rooty()
if x <= event.x_root <= x + canvas.winfo_width() and \ if x <= event.x_root <= x + canvas.winfo_width() and \
y <= event.y_root <= y + canvas.winfo_height(): y <= event.y_root <= y + canvas.winfo_height():
@ -1297,38 +1302,120 @@ class BaseDishApp(PendingLearningUIMixin):
self._sync_dialog = None self._sync_dialog = None
tk.Frame(self.root, bg=c["border"], height=1).pack(fill=tk.X) tk.Frame(self.root, bg=c["border"], height=1).pack(fill=tk.X)
def _build_today_menu(self, parent, c): def _build_left_panel(self, parent, c):
"""左侧:第三方接口拉取的今日排餐菜单。""" """左侧大框:已点菜品 / 今日菜单 同级页签切换。"""
panel = tk.Frame( shell = tk.Frame(
parent, bg=c["surface"], parent, bg=c["surface"],
highlightbackground=c["border"], highlightthickness=1, highlightbackground=c["border"], highlightthickness=1,
) )
panel.pack(fill=tk.X, pady=(0, 8)) shell.pack(fill=tk.BOTH, expand=True)
head = tk.Frame(panel, bg=c["surface"]) tab_row = tk.Frame(shell, bg=c["surface"])
head.pack(fill=tk.X, padx=12, pady=(10, 4)) tab_row.pack(fill=tk.X, padx=10, pady=(10, 6))
tk.Label(
head, text="今日菜单", bg=c["surface"], fg=c["text"], font=FONTS["h1"], tabs = tk.Frame(tab_row, bg=c["surface"])
).pack(side=tk.LEFT) tabs.pack(side=tk.LEFT)
PillButton( self._left_tab = "cart"
head, text="刷新", variant="ghost", size="sm", self._left_tab_btns = {}
self._left_tab_cart_btn = PillButton(
tabs, text="已点菜品", variant="primary", size="sm",
command=lambda: self._switch_left_tab("cart"),
)
self._left_tab_cart_btn.pack(side=tk.LEFT, padx=(0, 6))
self._left_tab_menu_btn = PillButton(
tabs, text="今日菜单", variant="ghost", size="sm",
command=lambda: self._switch_left_tab("menu"),
)
self._left_tab_menu_btn.pack(side=tk.LEFT)
self._left_tab_btns = {
"cart": self._left_tab_cart_btn,
"menu": self._left_tab_menu_btn,
}
# 右侧:随页签切换的操作区
self._left_tab_actions = tk.Frame(tab_row, bg=c["surface"])
self._left_tab_actions.pack(side=tk.RIGHT)
self.cart_count_label = tk.Label(
self._left_tab_actions, text="0 项", bg=c["surface"], fg=c["muted"],
font=FONTS["small"],
)
self._menu_refresh_btn = PillButton(
self._left_tab_actions, text="刷新", variant="ghost", size="sm",
command=self.refresh_today_menu_async, command=self.refresh_today_menu_async,
tooltip="重新从管理系统拉取今日排餐", tooltip="重新从管理系统拉取今日排餐",
).pack(side=tk.RIGHT) )
body = tk.Frame(shell, bg=c["surface"])
body.pack(fill=tk.BOTH, expand=True)
self._left_body = body
self._cart_page = tk.Frame(body, bg=c["surface"])
self._menu_page = tk.Frame(body, bg=c["surface"])
self._build_pos_cart(self._cart_page, c)
self._build_today_menu(self._menu_page, c)
self._switch_left_tab("cart")
return shell
def _switch_left_tab(self, which):
"""切换左侧页签:cart=已点菜品,menu=今日菜单。"""
which = "menu" if which == "menu" else "cart"
self._left_tab = which
cart_page = getattr(self, "_cart_page", None)
menu_page = getattr(self, "_menu_page", None)
if cart_page is None or menu_page is None:
return
if which == "cart":
menu_page.pack_forget()
cart_page.pack(fill=tk.BOTH, expand=True)
else:
cart_page.pack_forget()
menu_page.pack(fill=tk.BOTH, expand=True)
for key, btn in (getattr(self, "_left_tab_btns", {}) or {}).items():
try:
btn.set_variant("primary" if key == which else "ghost")
except Exception:
try:
btn.configure(variant="primary" if key == which else "ghost")
except Exception:
pass
# 操作区:已点显示数量,菜单显示刷新
try:
self.cart_count_label.pack_forget()
except Exception:
pass
try:
self._menu_refresh_btn.pack_forget()
except Exception:
pass
if which == "cart":
try:
self.cart_count_label.pack(side=tk.RIGHT)
except Exception:
pass
else:
try:
self._menu_refresh_btn.pack(side=tk.RIGHT)
except Exception:
pass
def _build_today_menu(self, parent, c):
"""今日排餐内容(嵌入左侧页签页,不再单独外框)。"""
panel = parent
self.menu_meta_label = tk.Label( self.menu_meta_label = tk.Label(
panel, text="正在加载今日菜单…", bg=c["surface"], fg=c["muted"], panel, text="正在加载今日菜单…", bg=c["surface"], fg=c["muted"],
font=FONTS["small"], anchor="w", font=FONTS["small"], anchor="w",
) )
self.menu_meta_label.pack(fill=tk.X, padx=12, pady=(0, 4)) self.menu_meta_label.pack(fill=tk.X, padx=12, pady=(4, 4))
# 餐段切换 # 餐段切换
self.menu_period_bar = tk.Frame(panel, bg=c["surface"]) self.menu_period_bar = tk.Frame(panel, bg=c["surface"])
self.menu_period_bar.pack(fill=tk.X, padx=8, pady=(0, 4)) self.menu_period_bar.pack(fill=tk.X, padx=8, pady=(0, 4))
list_host = tk.Frame(panel, bg=c["surface"], height=148) list_host = tk.Frame(panel, bg=c["surface"])
list_host.pack(fill=tk.X, padx=4, pady=(0, 8)) list_host.pack(fill=tk.BOTH, expand=True, padx=4, pady=(0, 8))
list_host.pack_propagate(False)
wrap, self.menu_dish_frame, self._menu_canvas = self._make_scroll_area( wrap, self.menu_dish_frame, self._menu_canvas = self._make_scroll_area(
list_host, c["surface"], list_host, c["surface"],
) )
@ -1338,6 +1425,7 @@ class BaseDishApp(PendingLearningUIMixin):
self._menu_selected_period = None # None=自动当前餐段 self._menu_selected_period = None # None=自动当前餐段
self._menu_period_btns = [] self._menu_period_btns = []
self._menu_refresh_job = None self._menu_refresh_job = None
self._menu_scroll_enabled = False
return panel return panel
def refresh_today_menu_async(self): def refresh_today_menu_async(self):
@ -1442,7 +1530,7 @@ class BaseDishApp(PendingLearningUIMixin):
if not periods: if not periods:
EmptyState( EmptyState(
self.menu_dish_frame, text="暂无今日菜单", self.menu_dish_frame, text="暂无今日菜单",
hint="启动后会自动从管理系统拉取;也可点右上角「刷新」", hint="启动后会自动从管理系统拉取;也可点页签旁「刷新」",
glyph="◎", bg=c["surface"], pady=16, glyph="◎", bg=c["surface"], pady=16,
).pack(fill=tk.X) ).pack(fill=tk.X)
elif not dishes: elif not dishes:
@ -1478,16 +1566,24 @@ class BaseDishApp(PendingLearningUIMixin):
row, text=name, bg=bg, fg=c["text"], font=FONTS["ui"], row, text=name, bg=bg, fg=c["text"], font=FONTS["ui"],
anchor="w", anchor="w",
).pack(side=tk.LEFT, padx=(8, 4), pady=4) ).pack(side=tk.LEFT, padx=(8, 4), pady=4)
if price_txt:
tk.Label( tk.Label(
row, text=price_txt, bg=bg, fg=c["muted"], font=FONTS["small"], row, text=price_txt or "—", bg=bg,
fg=c["text"] if price_txt else c["muted"],
font=FONTS["small"],
).pack(side=tk.RIGHT, padx=8) ).pack(side=tk.RIGHT, padx=8)
# 有菜才允许滚动;空菜单锁死滚动区域,避免空转
try: try:
self._menu_canvas.update_idletasks() self._menu_canvas.update_idletasks()
if dishes:
self._menu_canvas.configure(scrollregion=self._menu_canvas.bbox("all")) self._menu_canvas.configure(scrollregion=self._menu_canvas.bbox("all"))
self._menu_scroll_enabled = True
else:
self._menu_canvas.yview_moveto(0)
self._menu_canvas.configure(scrollregion=(0, 0, 0, 0))
self._menu_scroll_enabled = False
except Exception: except Exception:
pass self._menu_scroll_enabled = bool(dishes)
# 每分钟刷新一次(餐段切换 / 元信息) # 每分钟刷新一次(餐段切换 / 元信息)
job = getattr(self, "_menu_refresh_job", None) job = getattr(self, "_menu_refresh_job", None)
@ -1506,25 +1602,25 @@ class BaseDishApp(PendingLearningUIMixin):
self.refresh_today_menu() self.refresh_today_menu()
def _build_pos_cart(self, parent, c): def _build_pos_cart(self, parent, c):
panel = tk.Frame(parent, bg=c["surface"], highlightbackground=c["border"], highlightthickness=1) """已点菜品内容(嵌入左侧页签页,标题由页签承担)。"""
panel.pack(fill=tk.BOTH, expand=True) panel = parent
head = tk.Frame(panel, bg=c["surface"])
head.pack(fill=tk.X, padx=12, pady=(12, 8))
tk.Label(head, text="已点菜品", bg=c["surface"], fg=c["text"],
font=FONTS["h1"]).pack(side=tk.LEFT)
self.cart_count_label = tk.Label(head, text="0 项", bg=c["surface"], fg=c["muted"],
font=FONTS["small"])
self.cart_count_label.pack(side=tk.RIGHT)
cols = tk.Frame(panel, bg=c["pos_row_alt"]) cols = tk.Frame(panel, bg=c["pos_row_alt"])
cols.pack(fill=tk.X) cols.pack(fill=tk.X)
for text, w in (("菜品", 22), ("数量", 10), ("", 4)): for text, w in (("菜品", 14), ("金额", 8), ("数量", 8), ("", 4)):
tk.Label(cols, text=text, bg=c["pos_row_alt"], fg=c["muted"], tk.Label(cols, text=text, bg=c["pos_row_alt"], fg=c["muted"],
font=FONTS["small"], width=w, anchor="w").pack(side=tk.LEFT, padx=4, pady=6) font=FONTS["small"], width=w, anchor="w").pack(side=tk.LEFT, padx=4, pady=6)
wrap, self.cart_frame, self._cart_canvas = self._make_scroll_area(panel, c["surface"]) wrap, self.cart_frame, self._cart_canvas = self._make_scroll_area(panel, c["surface"])
wrap.pack(fill=tk.BOTH, expand=True, padx=4, pady=4) wrap.pack(fill=tk.BOTH, expand=True, padx=4, pady=4)
foot = tk.Frame(panel, bg=c["surface"])
foot.pack(fill=tk.X, padx=12, pady=(0, 10))
self.cart_total_label = tk.Label(
foot, text="合计 ¥0", bg=c["surface"], fg=c["text"],
font=FONTS["ui_bold"], anchor="e",
)
self.cart_total_label.pack(side=tk.RIGHT)
return panel return panel
def _build_pos_preview(self, parent, c): def _build_pos_preview(self, parent, c):
@ -1696,8 +1792,7 @@ class BaseDishApp(PendingLearningUIMixin):
left = tk.Frame(body, bg=c["bg"], width=SIZES.get("cart_w", 320)) left = tk.Frame(body, bg=c["bg"], width=SIZES.get("cart_w", 320))
left.grid(row=0, column=0, sticky="nsew", padx=(0, 8)) left.grid(row=0, column=0, sticky="nsew", padx=(0, 8))
left.grid_propagate(False) left.grid_propagate(False)
self._build_today_menu(left, c) self._build_left_panel(left, c)
self._build_pos_cart(left, c)
right = tk.Frame(body, bg=c["bg"]) right = tk.Frame(body, bg=c["bg"])
right.grid(row=0, column=1, sticky="nsew") right.grid(row=0, column=1, sticky="nsew")
@ -1975,16 +2070,15 @@ class BaseDishApp(PendingLearningUIMixin):
self._order_locked = False self._order_locked = False
self._order_clear_since = None self._order_clear_since = None
self._last_reported_sig = None self._last_reported_sig = None
self._cart_hold = False
def _note_order_scene_busy(self): def _note_order_scene_busy(self):
"""画面上仍有托盘/已识别菜:清场计时清零。""" """画面上仍有托盘/已识别菜:清场计时清零。"""
self._order_clear_since = None self._order_clear_since = None
def _note_order_scene_clear(self): def _note_order_scene_clear(self):
"""无餐盘:累计清场时间,满 hold 后解除订单锁定。""" """无餐盘:累计清场时间,满 hold 后解除订单锁定,并允许下一盘刷新清单。"""
if not self._order_locked: # 无上报锁定时也要能「松开清单」:无盘持续后允许下一盘覆盖已点菜品
self._order_clear_since = None
return False
now = time.time() now = time.time()
if self._order_clear_since is None: if self._order_clear_since is None:
self._order_clear_since = now self._order_clear_since = now
@ -1992,10 +2086,15 @@ class BaseDishApp(PendingLearningUIMixin):
hold = float(getattr(self, "_order_clear_hold", 1.5) or 1.5) hold = float(getattr(self, "_order_clear_hold", 1.5) or 1.5)
if now - self._order_clear_since < hold: if now - self._order_clear_since < hold:
return False return False
was_locked = bool(self._order_locked)
self._order_locked = False self._order_locked = False
self._order_clear_since = None self._order_clear_since = None
self._last_reported_sig = None self._last_reported_sig = None
self._cart_hold = False # 下一盘识别到目标时可更换清单
if was_locked:
print(f"[order] 清场解锁(无盘持续 ≥ {hold:g}s,允许下一单)") print(f"[order] 清场解锁(无盘持续 ≥ {hold:g}s,允许下一单)")
else:
print(f"[order] 清场,已点清单可被下一盘覆盖(无盘 ≥ {hold:g}s)")
return True return True
def _try_live_report(self, save_path, known_dishes, preview): def _try_live_report(self, save_path, known_dishes, preview):
@ -2451,15 +2550,15 @@ class BaseDishApp(PendingLearningUIMixin):
def _no_plate(did_unlock=unlocked): def _no_plate(did_unlock=unlocked):
self._live_overlay_dishes = [] self._live_overlay_dishes = []
self.render_result_dock([], None) # 无盘不刷空清单:保留上一盘已点菜品,直到下一盘识别到目标
tip = "扫描中:未检测到餐盘" tip = "扫描中:未检测到餐盘|左侧保留上一盘"
if did_unlock: if did_unlock:
tip += "|已清场,可报下一单" tip += "|已清场,可报下一单"
self._set_result_text(tip, "warn") self._set_result_text(tip, "warn")
if hasattr(self, "scan_tip_label"): if hasattr(self, "scan_tip_label"):
try: try:
self.scan_tip_label.config( self.scan_tip_label.config(
text="扫描中 · 请将餐盘放入画面中央" text="扫描中 · 请放入下一盘(已点清单暂留)"
+ (" · 已解锁下一单" if did_unlock else "") + (" · 已解锁下一单" if did_unlock else "")
) )
except Exception: except Exception:
@ -2518,9 +2617,9 @@ class BaseDishApp(PendingLearningUIMixin):
written=pending_written, elapsed=stable_elapsed, written=pending_written, elapsed=stable_elapsed,
committed=already_committed, hold_s=hold, committed=already_committed, hold_s=hold,
): ):
# 只更新叠加框 + 左侧清单;预览继续刷实时帧 # 只更新叠加框;清单:本盘展示后冻结,清场后下一盘再换
self._live_overlay_dishes = list(ds or []) self._live_overlay_dishes = list(ds or [])
self.render_result_dock(ds, path) self.render_result_dock(ds, path, live_hold=True)
try: try:
self.update_pending_btn() self.update_pending_btn()
except Exception: except Exception:
@ -2529,13 +2628,14 @@ class BaseDishApp(PendingLearningUIMixin):
known = [d for d in ds if not d.get("is_unknown")] known = [d for d in ds if not d.get("is_unknown")]
unknown = [d for d in ds if d.get("is_unknown")] unknown = [d for d in ds if d.get("is_unknown")]
if known or unknown:
msg = f"扫描中|清单 {len(ds)} 项"
if known: if known:
top = max(known, key=lambda d: d.get("score") or 0) top = max(known, key=lambda d: d.get("score") or 0)
name = top.get("label") or "菜品" msg += f"|{top.get('label') or '菜品'}"
msg = f"扫描中|已识别 {len(known)}|{name}"
if unknown: if unknown:
msg += f"|未知 {len(unknown)}(不入清单)" msg += f"|未知 {len(unknown)}"
self._set_result_text(msg, "ok" if not unknown else "warn") self._set_result_text(msg, "ok" if known else "warn")
else: else:
self._set_result_text( self._set_result_text(
txt or f"扫描中|检出 {len(ds)} 盘,尚无达到阈值的菜品", txt or f"扫描中|检出 {len(ds)} 盘,尚无达到阈值的菜品",
@ -2571,7 +2671,7 @@ class BaseDishApp(PendingLearningUIMixin):
) )
else: else:
self.scan_tip_label.config( self.scan_tip_label.config(
text="实时扫描中 · 仅已识别菜计入左侧清单" text="实时扫描中 · 下一盘识别后刷新已点清单"
) )
except Exception: except Exception:
pass pass
@ -2590,6 +2690,7 @@ class BaseDishApp(PendingLearningUIMixin):
self._cart = [] self._cart = []
self._last_dishes = [] self._last_dishes = []
self._live_overlay_dishes = [] self._live_overlay_dishes = []
self._cart_hold = False
self.render_cart() self.render_cart()
if not self._preview_enabled and not self._live_enabled and hasattr(self, "img_label"): if not self._preview_enabled and not self._live_enabled and hasattr(self, "img_label"):
self.img_label.config( self.img_label.config(
@ -2599,23 +2700,40 @@ class BaseDishApp(PendingLearningUIMixin):
self.img_label.image = None self.img_label.image = None
self._set_result_text("已清空本次已点菜品", "ok") self._set_result_text("已清空本次已点菜品", "ok")
def _format_price_text(self, price):
if isinstance(price, (int, float)):
return f"¥{price:g}"
if price not in (None, ""):
return f"¥{price}"
return "—"
def fill_cart_from_dishes(self, dishes): def fill_cart_from_dishes(self, dishes):
"""左侧清单只放已识别菜;未知仅留在预览标注 / 待学习流程。""" """写入左侧清单:已识别 + 未知;金额按今日菜单价查询。"""
cart = [] cart = []
for d in dishes or []: for d in dishes or []:
if d.get("is_unknown"): is_unk = bool(d.get("is_unknown"))
continue if is_unk:
sug = (d.get("suggested_category") or "").strip()
name = f"未知·{sug}" if sug else "未知"
price = None
else:
name = d.get("label") or d.get("name") or "菜品" name = d.get("label") or d.get("name") or "菜品"
try:
price = self.meal_schedule.lookup_price(name)
except Exception:
price = None
cart.append({ cart.append({
"index": d.get("index"), "index": d.get("index"),
"name": name, "name": name,
"label": d.get("label"), "label": d.get("label"),
"score": d.get("score"), "score": d.get("score"),
"is_unknown": False, "is_unknown": is_unk,
"qty": 1, "qty": 1,
"price": price,
"crop_image": d.get("crop_image"), "crop_image": d.get("crop_image"),
"pending_id": d.get("pending_id"), "pending_id": d.get("pending_id"),
"suggested_category": d.get("suggested_category"), "suggested_category": d.get("suggested_category"),
"in_today_menu": bool(d.get("in_today_menu")),
}) })
self._cart = cart self._cart = cart
self._last_dishes = dishes or [] self._last_dishes = dishes or []
@ -2641,10 +2759,9 @@ class BaseDishApp(PendingLearningUIMixin):
self._cart_thumbs = [] self._cart_thumbs = []
if not self._cart: if not self._cart:
hold_s = float(getattr(self, "_live_stable_seconds", DISH_LIVE_PENDING_STABLE_SECONDS) or 1.0)
EmptyState( EmptyState(
self.cart_frame, text="暂无已识别菜品", self.cart_frame, text="暂无已点菜品",
hint=f"未知菜不入清单;画面稳定约 {hold_s:g} 秒后记入待学习", hint="下一盘出现并识别到目标后刷新;未知菜也会显示",
glyph="◍", bg=c["surface"], pady=28, glyph="◍", bg=c["surface"], pady=28,
).pack(fill=tk.X) ).pack(fill=tk.X)
else: else:
@ -2669,7 +2786,8 @@ class BaseDishApp(PendingLearningUIMixin):
info = tk.Frame(pad, bg=bg) info = tk.Frame(pad, bg=bg)
info.pack(side=tk.LEFT, fill=tk.X, expand=True) info.pack(side=tk.LEFT, fill=tk.X, expand=True)
tk.Label(info, text=item.get("name") or "菜品", bg=bg, fg=c["text"], name_fg = c["warn"] if item.get("is_unknown") else c["text"]
tk.Label(info, text=item.get("name") or "菜品", bg=bg, fg=name_fg,
font=FONTS["ui_bold"], anchor="w").pack(fill=tk.X) font=FONTS["ui_bold"], anchor="w").pack(fill=tk.X)
score = item.get("score") score = item.get("score")
meta = f"#{item.get('index') or i + 1}" meta = f"#{item.get('index') or i + 1}"
@ -2677,9 +2795,18 @@ class BaseDishApp(PendingLearningUIMixin):
meta += f" · {score:.2f}" meta += f" · {score:.2f}"
if item.get("is_unknown"): if item.get("is_unknown"):
meta += " · 待确认" meta += " · 待确认"
elif item.get("in_today_menu"):
meta += " · 今日菜单"
tk.Label(info, text=meta, bg=bg, fg=c["muted"], tk.Label(info, text=meta, bg=bg, fg=c["muted"],
font=FONTS["small"], anchor="w").pack(fill=tk.X) font=FONTS["small"], anchor="w").pack(fill=tk.X)
price_txt = self._format_price_text(item.get("price"))
tk.Label(
pad, text=price_txt, bg=bg,
fg=c["text"] if price_txt != "—" else c["muted"],
font=FONTS["ui"], width=6, anchor="e",
).pack(side=tk.LEFT, padx=(4, 2))
qty_box = tk.Frame(pad, bg=bg) qty_box = tk.Frame(pad, bg=bg)
qty_box.pack(side=tk.LEFT, padx=6) qty_box.pack(side=tk.LEFT, padx=6)
PillButton(qty_box, text="−", variant="ghost", size="sm", PillButton(qty_box, text="−", variant="ghost", size="sm",
@ -2693,11 +2820,45 @@ class BaseDishApp(PendingLearningUIMixin):
if hasattr(self, "cart_count_label"): if hasattr(self, "cart_count_label"):
self.cart_count_label.config(text=f"{len(self._cart)} 项") self.cart_count_label.config(text=f"{len(self._cart)} 项")
if hasattr(self, "cart_total_label"):
total = 0.0
has_price = False
for item in self._cart:
p = item.get("price")
q = int(item.get("qty") or 1)
if isinstance(p, (int, float)):
total += float(p) * q
has_price = True
self.cart_total_label.config(
text=f"合计 ¥{total:g}" if has_price else "合计 —"
)
def render_result_dock(self, dishes, labeled_path=None): def render_result_dock(self, dishes, labeled_path=None, *, live_hold=False):
"""兼容旧调用:识别结果写入购物车。""" """识别结果写入购物车。
live_hold=True(实时扫描):
- 无目标:不改清单
- 本盘已展示过(_cart_hold):不因标签抖动刷新
- 清场后下一盘识别到目标:才更换清单
"""
if live_hold:
if not dishes:
return
if getattr(self, "_cart_hold", False):
return
self.fill_cart_from_dishes(dishes) self.fill_cart_from_dishes(dishes)
self.render_cart() self.render_cart()
self._cart_hold = True
# 新一盘写入清单时自动切到「已点菜品」
try:
if getattr(self, "_left_tab", None) != "cart":
self._switch_left_tab("cart")
except Exception:
pass
return
self.fill_cart_from_dishes(dishes)
self.render_cart()
self._cart_hold = False
# --------------------------------------------------------------- # ---------------------------------------------------------------
# 模板详情 / 删除 # 模板详情 / 删除