# coding=utf-8 """菜品识别客户端视觉主题(工业工具风)。 设计令牌分四层: COLORS —— 颜色(含语义色与状态色,全部为浅色底 + 深色字,适配亮色主题) FONTS —— 字体阶梯 RADIUS —— 圆角 SPACING —— 间距 约定: - 旧版 key 一律保留(`core/common_classifier.py` 的 BaseClassifierUI 仍在使用), 新增 key 只做增量扩展,不做重命名。 - 所有颜色为十六进制字符串,可直接用于 tk / ttk / PIL。 """ from tkinter import ttk # --------------------------------------------------------------------------- # 颜色 # --------------------------------------------------------------------------- COLORS = { # 底色层次:页面 < 侧栏 < 卡片 "bg": "#F6F7F9", "surface": "#FFFFFF", "surface_alt": "#FAFBFC", "sidebar": "#F2F4F7", "header_bg": "#FFFFFF", "threshold_bg": "#F7F8FA", "overlay": "#F2F4F7", # 品牌主色(结算机绿) "primary": "#2EAB6F", "primary_dark": "#24965F", "primary_soft": "#E8F8EF", "primary_border": "#A8E0C0", # 语义色(成功 / 警告 / 危险 / 中性) "success": "#2EAB6F", "success_soft": "#E8F8EF", "success_border": "#A8E0C0", "warn": "#FF7D00", "warn_soft": "#FFF7E8", "warn_border": "#FFCF8B", "danger": "#E53935", "danger_dark": "#C62828", "danger_soft": "#FFEBEE", "danger_border": "#FFCDD2", "neutral": "#86909C", "neutral_soft": "#F2F3F5", "sidebar_muted": "#F2F4F7", # 文字层次 "text": "#1D2129", "text_body": "#4E5969", "text_sub": "#6B7280", "muted": "#86909C", "disabled_text": "#A9AEB8", "text_inv": "#FFFFFF", # 描边 / 分隔 "border": "#E5E6EB", "border_strong": "#D8DEE6", "divider": "#EEF0F3", "focus_ring": "#8FD6B0", "shadow": "#D8DEE6", # 交互态 "row_hover": "#E8F8EF", "row_selected": "#2EAB6F", "disabled_bg": "#C9CDD4", # 预览区(结算机浅色托盘区) "preview_bg": "#F0F2F5", "preview_mid": "#ECEFF3", "preview_hi": "#E2E6EB", "preview_fg": "#6B7785", "preview_line": "#C9CDD4", # 结算机专用 "pos_tip_bar": "#2EAB6F", "pos_tip_fg": "#FFFFFF", "pos_total_bg": "#E53935", "pos_total_fg": "#FFFFFF", "pos_row_alt": "#F7F8FA", "pos_bottom_bar": "#FFFFFF", "pos_btn": "#3CB878", "pos_btn_dark": "#2EAB6F", # 滚动条 "scroll_track": "#F2F4F7", "scroll_thumb": "#C9CED6", "scroll_thumb_hover": "#A9AEB8", # 浮层 "tooltip_bg": "#2A3342", "tooltip_fg": "#F2F3F5", # 徽章实心底 "chip_ok_bg": "#2EAB6F", "chip_bad_bg": "#E53935", "chip_warn_bg": "#FF7D00", "chip_info_bg": "#2EAB6F", # 别名(历史代码兼容) "accent": "#2EAB6F", "accent_dark": "#24965F", "accent_soft": "#E8F8EF", "info": "#2EAB6F", "info_soft": "#E8F8EF", } # --------------------------------------------------------------------------- # 字体 # --------------------------------------------------------------------------- _FAMILY = "Microsoft YaHei UI" _FAMILY_MONO = "Consolas" FONTS = { # 历史 key(勿删) "ui": (_FAMILY, 10), "ui_bold": (_FAMILY, 10, "bold"), "title": (_FAMILY, 20, "bold"), "subtitle": (_FAMILY, 10), "section": (_FAMILY, 14, "bold"), "card_title": (_FAMILY, 14, "bold"), "button": (_FAMILY, 10), "button_lg": (_FAMILY, 10, "bold"), "small": (_FAMILY, 9), "result": (_FAMILY, 10), "preview_hint": (_FAMILY, 11), # 新增阶梯 "display": (_FAMILY, 22, "bold"), "h1": (_FAMILY, 16, "bold"), "h2": (_FAMILY, 13, "bold"), "h3": (_FAMILY, 11, "bold"), "body": (_FAMILY, 10), "caption": (_FAMILY, 9), "metric": (_FAMILY, 20, "bold"), "mono": (_FAMILY_MONO, 9), "badge": (_FAMILY, 9), } # --------------------------------------------------------------------------- # 圆角 / 间距 # --------------------------------------------------------------------------- RADIUS = { "sm": 6, "md": 8, "lg": 12, "xl": 16, "pill": 999, } SPACING = { "xs": 4, "sm": 8, "md": 12, "lg": 16, "xl": 20, "xxl": 24, } # 常用控件高度,统一视觉节奏 SIZES = { "header_h": 52, "sidebar_w": 268, "cart_w": 320, "button_h": 44, "button_sm_h": 28, "notice_h": 36, "badge_h": 28, "preview_min_h": 480, "wrap": 980, "pos_bottom_h": 64, } # 菜品缩略图渐变色板(按菜名关键词 / hash) DISH_GRADIENTS = [ ("#FFE08A", "#F7BA1E"), # 土豆黄 ("#FF9A8B", "#F53F3F"), # 鸡肉红 ("#9BE7A3", "#00B42A"), # 青菜绿 ("#8FD3FF", "#2EAB6F"), # 海鲜青绿 ("#E8C4FF", "#8D4EDA"), # 蒸蛋紫 ("#FFD4A3", "#FF7D00"), # 炒肉橙 ("#B7F0E5", "#0FC6C2"), # 清汤青 ("#FFC0D9", "#F5319D"), # 虾仁粉 ] def dish_gradient_for(name: str): n = name or "" if any(k in n for k in ("土豆", "薯", "丝")): return DISH_GRADIENTS[0] if any(k in n for k in ("鸡", "肉", "鸭", "排骨")): return DISH_GRADIENTS[1] if any(k in n for k in ("青菜", "菜", "芹", "笋", "菇", "瓜")): return DISH_GRADIENTS[2] if any(k in n for k in ("虾", "鱼", "蟹", "海")): return DISH_GRADIENTS[3] if any(k in n for k in ("蛋", "蒸")): return DISH_GRADIENTS[4] return DISH_GRADIENTS[sum(ord(ch) for ch in n) % len(DISH_GRADIENTS)] # --------------------------------------------------------------------------- # 颜色工具 # --------------------------------------------------------------------------- def hex_to_rgb(value): v = (value or "#000000").lstrip("#") if len(v) == 3: v = "".join(ch * 2 for ch in v) return tuple(int(v[i:i + 2], 16) for i in (0, 2, 4)) def rgb_to_hex(rgb): return "#{:02X}{:02X}{:02X}".format(*(max(0, min(255, int(c))) for c in rgb)) def mix(color_a, color_b, t=0.5): """在两个颜色之间线性插值,t=0 取 a,t=1 取 b。""" t = max(0.0, min(1.0, t)) ra, rb = hex_to_rgb(color_a), hex_to_rgb(color_b) return rgb_to_hex(tuple(ra[i] * (1 - t) + rb[i] * t for i in range(3))) def tone_pair(tone): """tone -> (浅底, 主色) 二元组,供徽章 / 提示条统一取色。""" c = COLORS return { "ok": (c["success_soft"], c["success"]), "success": (c["success_soft"], c["success"]), "warn": (c["warn_soft"], c["warn"]), "err": (c["danger_soft"], c["danger"]), "danger": (c["danger_soft"], c["danger"]), "busy": (c["primary_soft"], c["primary"]), "info": (c["primary_soft"], c["primary"]), "mute": (c["neutral_soft"], c["neutral"]), "neutral": (c["neutral_soft"], c["neutral"]), }.get(tone or "info", (c["primary_soft"], c["primary"])) def tone_text(tone): """tone -> 深色文字,保证浅底上的对比度。""" c = COLORS return { "ok": c["success"], "success": c["success"], "warn": c["warn"], "err": c["danger"], "danger": c["danger"], "busy": c["primary"], "info": c["primary"], "mute": c["text_body"], "neutral": c["text_body"], }.get(tone or "info", c["primary"]) # --------------------------------------------------------------------------- # ttk 主题 # --------------------------------------------------------------------------- def apply_theme(root=None): style = ttk.Style() try: style.theme_use("clam") except Exception: pass c = COLORS f = FONTS style.configure(".", font=f["ui"], background=c["bg"], foreground=c["text_body"]) style.configure("TFrame", background=c["bg"]) style.configure("Surface.TFrame", background=c["surface"]) style.configure("Sidebar.TFrame", background=c["sidebar"]) style.configure("Raised.TFrame", background=c["surface"], relief="flat") # ---- 标签 ---- style.configure("TLabel", background=c["bg"], foreground=c["text_body"], font=f["ui"]) style.configure("Surface.TLabel", background=c["surface"], foreground=c["text_body"], font=f["ui"]) style.configure("Title.TLabel", background=c["bg"], foreground=c["text"], font=f["section"]) style.configure("SurfaceTitle.TLabel", background=c["surface"], foreground=c["text"], font=f["card_title"]) style.configure("Medium.TLabel", background=c["bg"], foreground=c["text_body"], font=f["subtitle"]) style.configure("Small.TLabel", background=c["bg"], foreground=c["muted"], font=f["small"]) style.configure("SurfaceSmall.TLabel", background=c["surface"], foreground=c["muted"], font=f["small"]) style.configure("Muted.TLabel", background=c["surface"], foreground=c["muted"], font=f["small"]) style.configure("CardTitle.TLabel", background=c["surface"], foreground=c["text"], font=f["card_title"]) style.configure("CardMeta.TLabel", background=c["surface"], foreground=c["muted"], font=f["small"]) style.configure("SidebarTitle.TLabel", background=c["sidebar"], foreground=c["text"], font=f["section"]) style.configure("SidebarHint.TLabel", background=c["sidebar"], foreground=c["muted"], font=f["small"]) style.configure("Metric.TLabel", background=c["surface"], foreground=c["primary"], font=f["metric"]) style.configure("Danger.TLabel", background=c["surface"], foreground=c["danger"], font=f["small"]) # ---- 兜底按钮(主路径走 Canvas PillButton)---- style.configure( "Primary.TButton", font=f["button_lg"], background=c["primary"], foreground="#FFFFFF", bordercolor=c["primary"], lightcolor=c["primary"], darkcolor=c["primary"], focusthickness=0, padding=(18, 8), ) style.map( "Primary.TButton", background=[("active", c["primary_dark"]), ("pressed", c["primary_dark"]), ("disabled", c["disabled_bg"])], foreground=[("disabled", "#FFFFFF")], ) style.configure( "Secondary.TButton", font=f["button"], background=c["surface"], foreground=c["primary"], bordercolor=c["primary_border"], lightcolor=c["primary"], darkcolor=c["primary"], relief="solid", borderwidth=1, focusthickness=0, padding=(16, 8), ) style.map( "Secondary.TButton", background=[("active", c["primary_soft"]), ("pressed", c["primary_soft"]), ("disabled", c["neutral_soft"])], foreground=[("active", c["primary_dark"]), ("disabled", c["disabled_text"])], ) style.configure( "Ghost.TButton", font=f["button"], background=c["neutral_soft"], foreground=c["text_body"], bordercolor=c["border"], focusthickness=0, padding=(14, 8), ) style.map( "Ghost.TButton", background=[("active", c["border"]), ("disabled", c["neutral_soft"])], foreground=[("disabled", c["disabled_text"])], ) style.configure( "Sidebar.TButton", font=f["button"], background=c["surface"], foreground=c["text_body"], bordercolor=c["border"], focusthickness=0, padding=(10, 8), ) style.map( "Sidebar.TButton", background=[("active", c["primary_soft"])], foreground=[("active", c["primary"])], ) style.configure( "Danger.TButton", font=f["button"], background=c["surface"], foreground=c["danger"], bordercolor=c["danger_border"], focusthickness=0, padding=(10, 8), ) style.map( "Danger.TButton", background=[("active", c["danger"]), ("pressed", c["danger_dark"])], foreground=[("active", "#FFFFFF"), ("pressed", "#FFFFFF")], ) style.configure( "GhostDanger.TButton", font=f["small"], background=c["surface"], foreground=c["danger"], bordercolor=c["surface"], focusthickness=0, padding=(6, 2), ) style.map( "GhostDanger.TButton", background=[("active", c["danger_soft"])], foreground=[("active", c["danger"])], ) style.configure("Large.TButton", font=f["button"], padding=(12, 8)) # ---- 单选 / 复选 ---- style.configure( "TRadiobutton", background=c["bg"], foreground=c["text_body"], font=f["ui"], focuscolor=c["bg"], indicatorcolor=c["surface"], bordercolor=c["border"], ) style.map( "TRadiobutton", background=[("active", c["bg"])], foreground=[("active", c["primary"]), ("disabled", c["disabled_text"])], indicatorcolor=[("selected", c["primary"]), ("pressed", c["primary_dark"])], ) style.configure( "TCheckbutton", background=c["bg"], foreground=c["text_body"], font=f["ui"], focuscolor=c["bg"], indicatorcolor=c["surface"], bordercolor=c["border"], ) style.map( "TCheckbutton", background=[("active", c["bg"])], foreground=[("active", c["primary"]), ("disabled", c["disabled_text"])], indicatorcolor=[("selected", c["primary"])], ) # ---- 输入控件 ---- style.configure( "Large.TEntry", font=f["ui"], fieldbackground="#FFFFFF", background="#FFFFFF", foreground=c["text"], bordercolor=c["border"], lightcolor=c["focus_ring"], darkcolor=c["border"], insertcolor=c["text"], padding=6, ) style.map( "Large.TEntry", bordercolor=[("focus", c["primary"])], lightcolor=[("focus", c["primary"])], ) style.configure( "Large.TSpinbox", font=f["ui"], fieldbackground="#FFFFFF", background="#FFFFFF", foreground=c["text"], bordercolor=c["border"], arrowcolor=c["text_body"], arrowsize=13, padding=4, ) style.map("Large.TSpinbox", bordercolor=[("focus", c["primary"])]) style.configure( "TCombobox", font=f["ui"], fieldbackground="#FFFFFF", background="#FFFFFF", foreground=c["text"], bordercolor=c["border"], arrowcolor=c["text_body"], padding=4, ) style.map( "TCombobox", fieldbackground=[("readonly", "#FFFFFF"), ("disabled", c["neutral_soft"])], foreground=[("disabled", c["disabled_text"])], bordercolor=[("focus", c["primary"])], ) try: root_option_db = root.option_get if root is not None else None if root_option_db is not None: root.option_add("*TCombobox*Listbox.background", "#FFFFFF") root.option_add("*TCombobox*Listbox.foreground", c["text"]) root.option_add("*TCombobox*Listbox.selectBackground", c["primary_soft"]) root.option_add("*TCombobox*Listbox.selectForeground", c["primary"]) root.option_add("*TCombobox*Listbox.font", f["ui"]) except Exception: pass # ---- 滑块 ---- style.configure( "Threshold.Horizontal.TScale", background=c["threshold_bg"], troughcolor=c["border"], bordercolor=c["threshold_bg"], lightcolor=c["primary"], darkcolor=c["primary"], sliderthickness=14, ) style.configure( "Horizontal.TScale", background=c["surface"], troughcolor=c["border"], bordercolor=c["surface"], lightcolor=c["primary"], darkcolor=c["primary"], sliderthickness=14, ) # ---- 表格 ---- style.configure( "Treeview", background=c["surface"], fieldbackground=c["surface"], foreground=c["text_body"], rowheight=30, font=f["ui"], borderwidth=0, relief="flat", ) style.configure( "Treeview.Heading", background=c["bg"], foreground=c["text"], font=f["ui_bold"], relief="flat", padding=(8, 6), ) style.map( "Treeview", background=[("selected", c["primary_soft"])], foreground=[("selected", c["text"])], ) style.map("Treeview.Heading", background=[("active", c["border"])]) # ---- 滚动条 ---- for orient in ("Vertical", "Horizontal"): style.configure( f"{orient}.TScrollbar", background=c["scroll_thumb"], troughcolor=c["scroll_track"], bordercolor=c["scroll_track"], arrowcolor=c["muted"], lightcolor=c["scroll_thumb"], darkcolor=c["scroll_thumb"], relief="flat", borderwidth=0, arrowsize=12, width=10, ) style.map( f"{orient}.TScrollbar", background=[("active", c["scroll_thumb_hover"]), ("pressed", c["scroll_thumb_hover"])], ) # ---- 容器类 ---- style.configure("TLabelframe", background=c["surface"], bordercolor=c["border"], borderwidth=1, relief="solid") style.configure( "TLabelframe.Label", background=c["surface"], foreground=c["text"], font=f["ui_bold"], ) style.configure("TNotebook", background=c["bg"], borderwidth=0, tabmargins=(0, 4, 0, 0)) style.configure( "TNotebook.Tab", background=c["neutral_soft"], foreground=c["text_body"], font=f["ui"], padding=(16, 8), borderwidth=0, ) style.map( "TNotebook.Tab", background=[("selected", c["surface"])], foreground=[("selected", c["primary"])], ) style.configure("TSeparator", background=c["divider"]) style.configure("TProgressbar", background=c["primary"], troughcolor=c["border"], borderwidth=0) if root is not None: try: root.configure(bg=c["bg"]) except Exception: pass return style