Dish_Recognition/ui/theme.py

564 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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