增加了查看入库秤上传图片的GUI界面。
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
"""
|
||||
UI模块
|
||||
"""
|
||||
from .filter_panel import FilterPanel
|
||||
from .preview_panel import PreviewPanel
|
||||
from .statistics_panel import StatisticsPanel
|
||||
|
||||
__all__ = ['FilterPanel', 'PreviewPanel', 'StatisticsPanel']
|
||||
@@ -0,0 +1,278 @@
|
||||
"""
|
||||
左侧筛选面板
|
||||
"""
|
||||
import customtkinter as ctk
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Callable, Optional
|
||||
import sys
|
||||
import os
|
||||
|
||||
# 添加父目录到路径
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
from config import TIME_SHORTCUTS
|
||||
|
||||
|
||||
class FilterPanel(ctk.CTkFrame):
|
||||
"""左侧筛选面板"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent,
|
||||
on_query_callback: Callable,
|
||||
on_datasource_change_callback: Optional[Callable] = None,
|
||||
**kwargs
|
||||
):
|
||||
"""
|
||||
初始化筛选面板
|
||||
|
||||
Args:
|
||||
parent: 父窗口
|
||||
on_query_callback: 查询回调函数
|
||||
on_datasource_change_callback: 数据源切换回调函数
|
||||
"""
|
||||
super().__init__(parent, **kwargs)
|
||||
|
||||
self.on_query_callback = on_query_callback
|
||||
self.on_datasource_change_callback = on_datasource_change_callback
|
||||
|
||||
# 创建UI组件
|
||||
self._create_widgets()
|
||||
|
||||
def _create_widgets(self):
|
||||
"""创建UI组件"""
|
||||
# 标题
|
||||
title_label = ctk.CTkLabel(
|
||||
self,
|
||||
text="筛选条件",
|
||||
font=("Arial", 16, "bold")
|
||||
)
|
||||
title_label.pack(pady=(15, 10), padx=15)
|
||||
|
||||
# ==================== 数据源选择 ====================
|
||||
datasource_frame = ctk.CTkFrame(self)
|
||||
datasource_frame.pack(fill="x", padx=15, pady=(0, 10))
|
||||
|
||||
datasource_label = ctk.CTkLabel(
|
||||
datasource_frame,
|
||||
text="数据源:",
|
||||
font=("Arial", 12, "bold")
|
||||
)
|
||||
datasource_label.pack(pady=(10, 5), padx=10, anchor="w")
|
||||
|
||||
# 数据源单选按钮
|
||||
self.datasource_var = ctk.StringVar(value="菜品")
|
||||
|
||||
datasource_options = ["菜品", "食材", "半加工"]
|
||||
for option in datasource_options:
|
||||
radio_btn = ctk.CTkRadioButton(
|
||||
datasource_frame,
|
||||
text=option,
|
||||
variable=self.datasource_var,
|
||||
value=option,
|
||||
command=self._on_datasource_changed
|
||||
)
|
||||
radio_btn.pack(pady=3, padx=20, anchor="w")
|
||||
|
||||
# ==================== 时间范围 ====================
|
||||
time_frame = ctk.CTkFrame(self)
|
||||
time_frame.pack(fill="x", padx=15, pady=(0, 10))
|
||||
|
||||
time_label = ctk.CTkLabel(
|
||||
time_frame,
|
||||
text="时间范围:",
|
||||
font=("Arial", 12, "bold")
|
||||
)
|
||||
time_label.pack(pady=(10, 5), padx=10, anchor="w")
|
||||
|
||||
# 快捷时间按钮
|
||||
shortcuts_frame = ctk.CTkFrame(time_frame)
|
||||
shortcuts_frame.pack(fill="x", padx=10, pady=(0, 5))
|
||||
|
||||
for text, days in TIME_SHORTCUTS.items():
|
||||
btn = ctk.CTkButton(
|
||||
shortcuts_frame,
|
||||
text=text,
|
||||
command=lambda d=days: self._set_time_shortcut(d),
|
||||
width=60,
|
||||
height=25,
|
||||
font=("Arial", 10)
|
||||
)
|
||||
btn.pack(side="left", padx=2, pady=5)
|
||||
|
||||
# 开始日期
|
||||
start_date_label = ctk.CTkLabel(
|
||||
time_frame,
|
||||
text="开始日期:",
|
||||
font=("Arial", 11)
|
||||
)
|
||||
start_date_label.pack(pady=(5, 2), padx=10, anchor="w")
|
||||
|
||||
self.start_date_entry = ctk.CTkEntry(
|
||||
time_frame,
|
||||
placeholder_text="YYYY-MM-DD",
|
||||
width=200
|
||||
)
|
||||
self.start_date_entry.pack(pady=(0, 5), padx=10)
|
||||
|
||||
# 结束日期
|
||||
end_date_label = ctk.CTkLabel(
|
||||
time_frame,
|
||||
text="结束日期:",
|
||||
font=("Arial", 11)
|
||||
)
|
||||
end_date_label.pack(pady=(5, 2), padx=10, anchor="w")
|
||||
|
||||
self.end_date_entry = ctk.CTkEntry(
|
||||
time_frame,
|
||||
placeholder_text="YYYY-MM-DD",
|
||||
width=200
|
||||
)
|
||||
self.end_date_entry.pack(pady=(0, 10), padx=10)
|
||||
|
||||
# ==================== 物品筛选 ====================
|
||||
goods_frame = ctk.CTkFrame(self)
|
||||
goods_frame.pack(fill="x", padx=15, pady=(0, 10))
|
||||
|
||||
goods_label = ctk.CTkLabel(
|
||||
goods_frame,
|
||||
text="物品筛选:",
|
||||
font=("Arial", 12, "bold")
|
||||
)
|
||||
goods_label.pack(pady=(10, 5), padx=10, anchor="w")
|
||||
|
||||
self.goods_search_entry = ctk.CTkEntry(
|
||||
goods_frame,
|
||||
placeholder_text="输入物品名称搜索",
|
||||
width=200
|
||||
)
|
||||
self.goods_search_entry.pack(pady=(0, 10), padx=10)
|
||||
|
||||
# ==================== 设备编码 ====================
|
||||
equipment_frame = ctk.CTkFrame(self)
|
||||
equipment_frame.pack(fill="x", padx=15, pady=(0, 10))
|
||||
|
||||
equipment_label = ctk.CTkLabel(
|
||||
equipment_frame,
|
||||
text="设备编码:",
|
||||
font=("Arial", 12, "bold")
|
||||
)
|
||||
equipment_label.pack(pady=(10, 5), padx=10, anchor="w")
|
||||
|
||||
self.equipment_entry = ctk.CTkEntry(
|
||||
equipment_frame,
|
||||
placeholder_text="可选,精确匹配",
|
||||
width=200
|
||||
)
|
||||
self.equipment_entry.pack(pady=(0, 10), padx=10)
|
||||
|
||||
# ==================== 按钮区域 ====================
|
||||
button_frame = ctk.CTkFrame(self)
|
||||
button_frame.pack(fill="x", padx=15, pady=(10, 15))
|
||||
|
||||
# 查询按钮
|
||||
self.query_button = ctk.CTkButton(
|
||||
button_frame,
|
||||
text="查询",
|
||||
command=self._on_query,
|
||||
height=35,
|
||||
fg_color="green",
|
||||
hover_color="darkgreen"
|
||||
)
|
||||
self.query_button.pack(fill="x", padx=10, pady=(10, 5))
|
||||
|
||||
# 重置按钮
|
||||
self.reset_button = ctk.CTkButton(
|
||||
button_frame,
|
||||
text="重置",
|
||||
command=self._on_reset,
|
||||
height=35,
|
||||
fg_color="gray",
|
||||
hover_color="darkgray"
|
||||
)
|
||||
self.reset_button.pack(fill="x", padx=10, pady=(5, 10))
|
||||
|
||||
def _set_time_shortcut(self, days: int):
|
||||
"""设置时间快捷选项"""
|
||||
end_date = datetime.now()
|
||||
if days == 0:
|
||||
# 今日
|
||||
start_date = end_date.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
else:
|
||||
# 最近N天
|
||||
start_date = end_date - timedelta(days=days)
|
||||
|
||||
self.start_date_entry.delete(0, 'end')
|
||||
self.start_date_entry.insert(0, start_date.strftime('%Y-%m-%d'))
|
||||
|
||||
self.end_date_entry.delete(0, 'end')
|
||||
self.end_date_entry.insert(0, end_date.strftime('%Y-%m-%d'))
|
||||
|
||||
def _on_datasource_changed(self):
|
||||
"""数据源改变时的回调"""
|
||||
if self.on_datasource_change_callback:
|
||||
self.on_datasource_change_callback(self.datasource_var.get())
|
||||
|
||||
def _on_query(self):
|
||||
"""查询按钮点击"""
|
||||
# 获取筛选条件
|
||||
filters = self.get_filters()
|
||||
|
||||
# 调用回调函数
|
||||
if self.on_query_callback:
|
||||
self.on_query_callback(filters)
|
||||
|
||||
def _on_reset(self):
|
||||
"""重置按钮点击"""
|
||||
# 清空所有输入
|
||||
self.start_date_entry.delete(0, 'end')
|
||||
self.end_date_entry.delete(0, 'end')
|
||||
self.goods_search_entry.delete(0, 'end')
|
||||
self.equipment_entry.delete(0, 'end')
|
||||
|
||||
# 重置数据源为菜品
|
||||
self.datasource_var.set("菜品")
|
||||
|
||||
def get_filters(self) -> dict:
|
||||
"""获取当前筛选条件"""
|
||||
filters = {
|
||||
'datasource': self.datasource_var.get(),
|
||||
'start_date': None,
|
||||
'end_date': None,
|
||||
'goods_name': None,
|
||||
'equipment_code': None
|
||||
}
|
||||
|
||||
# 解析开始日期
|
||||
start_date_str = self.start_date_entry.get().strip()
|
||||
if start_date_str:
|
||||
try:
|
||||
filters['start_date'] = datetime.strptime(start_date_str, '%Y-%m-%d')
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# 解析结束日期
|
||||
end_date_str = self.end_date_entry.get().strip()
|
||||
if end_date_str:
|
||||
try:
|
||||
filters['end_date'] = datetime.strptime(end_date_str, '%Y-%m-%d')
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# 物品名称
|
||||
goods_name = self.goods_search_entry.get().strip()
|
||||
if goods_name:
|
||||
filters['goods_name'] = goods_name
|
||||
|
||||
# 设备编码
|
||||
equipment_code = self.equipment_entry.get().strip()
|
||||
if equipment_code:
|
||||
filters['equipment_code'] = equipment_code
|
||||
|
||||
return filters
|
||||
|
||||
def set_loading_state(self, loading: bool):
|
||||
"""设置加载状态"""
|
||||
if loading:
|
||||
self.query_button.configure(state="disabled", text="查询中...")
|
||||
else:
|
||||
self.query_button.configure(state="normal", text="查询")
|
||||
@@ -0,0 +1,469 @@
|
||||
"""
|
||||
中间图片预览面板
|
||||
"""
|
||||
import customtkinter as ctk
|
||||
from tkinter import messagebox
|
||||
from typing import List, Dict, Callable, Optional
|
||||
import threading
|
||||
import sys
|
||||
import os
|
||||
|
||||
# 添加父目录到路径
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
from config import GRID_COLUMNS, THUMBNAIL_SIZE, CARD_PADDING, ITEMS_PER_PAGE
|
||||
from utils.image_loader import load_image_from_url, create_placeholder_image
|
||||
|
||||
|
||||
class ImageCard(ctk.CTkFrame):
|
||||
"""图片卡片组件"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent,
|
||||
data: Dict,
|
||||
is_downloaded: bool,
|
||||
on_check_changed: Callable,
|
||||
on_preview: Callable,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(parent, **kwargs)
|
||||
|
||||
self.data = data
|
||||
self.is_downloaded = is_downloaded
|
||||
self.on_check_changed = on_check_changed
|
||||
self.on_preview = on_preview
|
||||
|
||||
# 勾选状态
|
||||
self.check_var = ctk.BooleanVar(value=False)
|
||||
|
||||
self._create_widgets()
|
||||
self._load_thumbnail()
|
||||
|
||||
def _create_widgets(self):
|
||||
"""创建卡片组件"""
|
||||
# 勾选框
|
||||
self.checkbox = ctk.CTkCheckBox(
|
||||
self,
|
||||
text="",
|
||||
variable=self.check_var,
|
||||
command=self._on_check,
|
||||
width=20
|
||||
)
|
||||
self.checkbox.pack(pady=(5, 0), padx=5, anchor="w")
|
||||
|
||||
# 图片容器
|
||||
self.image_label = ctk.CTkLabel(
|
||||
self,
|
||||
text="加载中...",
|
||||
width=THUMBNAIL_SIZE,
|
||||
height=THUMBNAIL_SIZE
|
||||
)
|
||||
self.image_label.pack(pady=5, padx=5)
|
||||
self.image_label.bind("<Button-1>", lambda e: self.on_preview(self.data))
|
||||
|
||||
# 物品名称(加粗)
|
||||
name_label = ctk.CTkLabel(
|
||||
self,
|
||||
text=self.data['goods_name'],
|
||||
font=("Arial", 11, "bold"),
|
||||
wraplength=THUMBNAIL_SIZE - 10
|
||||
)
|
||||
name_label.pack(pady=(0, 2), padx=5)
|
||||
|
||||
# 采集时间
|
||||
time_label = ctk.CTkLabel(
|
||||
self,
|
||||
text=self.data['create_time'],
|
||||
font=("Arial", 9),
|
||||
text_color="gray"
|
||||
)
|
||||
time_label.pack(pady=(0, 2), padx=5)
|
||||
|
||||
# 设备编码
|
||||
if self.data['equipment_code']:
|
||||
equipment_label = ctk.CTkLabel(
|
||||
self,
|
||||
text=f"设备: {self.data['equipment_code']}",
|
||||
font=("Arial", 9),
|
||||
text_color="gray"
|
||||
)
|
||||
equipment_label.pack(pady=(0, 2), padx=5)
|
||||
|
||||
# 已下载标记
|
||||
if self.is_downloaded:
|
||||
downloaded_label = ctk.CTkLabel(
|
||||
self,
|
||||
text="✅ 已下载",
|
||||
font=("Arial", 10),
|
||||
text_color="green"
|
||||
)
|
||||
downloaded_label.pack(pady=(0, 5), padx=5)
|
||||
|
||||
# 查看大图按钮
|
||||
preview_btn = ctk.CTkButton(
|
||||
self,
|
||||
text="查看大图",
|
||||
command=lambda: self.on_preview(self.data),
|
||||
width=100,
|
||||
height=25,
|
||||
font=("Arial", 10)
|
||||
)
|
||||
preview_btn.pack(pady=(0, 5), padx=5)
|
||||
|
||||
def _load_thumbnail(self):
|
||||
"""异步加载缩略图"""
|
||||
def load():
|
||||
image = load_image_from_url(
|
||||
self.data['image_url'],
|
||||
size=(THUMBNAIL_SIZE, THUMBNAIL_SIZE)
|
||||
)
|
||||
|
||||
if image:
|
||||
self.image_label.configure(image=image, text="")
|
||||
self.image_label.image = image
|
||||
else:
|
||||
placeholder = create_placeholder_image(
|
||||
size=(THUMBNAIL_SIZE, THUMBNAIL_SIZE)
|
||||
)
|
||||
self.image_label.configure(image=placeholder, text="")
|
||||
self.image_label.image = placeholder
|
||||
|
||||
threading.Thread(target=load, daemon=True).start()
|
||||
|
||||
def _on_check(self):
|
||||
"""勾选框改变"""
|
||||
self.on_check_changed(self.data['id'], self.check_var.get())
|
||||
|
||||
def is_checked(self) -> bool:
|
||||
"""是否被勾选"""
|
||||
return self.check_var.get()
|
||||
|
||||
def set_checked(self, checked: bool):
|
||||
"""设置勾选状态"""
|
||||
self.check_var.set(checked)
|
||||
|
||||
|
||||
class PreviewPanel(ctk.CTkFrame):
|
||||
"""中间图片预览面板"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent,
|
||||
sqlite_manager,
|
||||
on_download_callback: Callable,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(parent, **kwargs)
|
||||
|
||||
self.sqlite_manager = sqlite_manager
|
||||
self.on_download_callback = on_download_callback
|
||||
|
||||
# 数据
|
||||
self.current_data = []
|
||||
self.total_count = 0
|
||||
self.current_page = 1
|
||||
self.selected_ids = set()
|
||||
|
||||
self._create_widgets()
|
||||
|
||||
def _create_widgets(self):
|
||||
"""创建UI组件"""
|
||||
# 标题栏
|
||||
header_frame = ctk.CTkFrame(self)
|
||||
header_frame.pack(fill="x", padx=15, pady=(15, 10))
|
||||
|
||||
title_label = ctk.CTkLabel(
|
||||
header_frame,
|
||||
text="图片预览",
|
||||
font=("Arial", 16, "bold")
|
||||
)
|
||||
title_label.pack(side="left", padx=10)
|
||||
|
||||
# 统计信息
|
||||
self.count_label = ctk.CTkLabel(
|
||||
header_frame,
|
||||
text="共 0 张 | 已选 0 张",
|
||||
font=("Arial", 12),
|
||||
text_color="gray"
|
||||
)
|
||||
self.count_label.pack(side="left", padx=20)
|
||||
|
||||
# 图片网格(可滚动)
|
||||
self.scroll_frame = ctk.CTkScrollableFrame(self)
|
||||
self.scroll_frame.pack(fill="both", expand=True, padx=15, pady=(0, 10))
|
||||
|
||||
# 网格容器
|
||||
self.grid_frame = ctk.CTkFrame(self.scroll_frame)
|
||||
self.grid_frame.pack(fill="both", expand=True)
|
||||
|
||||
# 底部操作栏
|
||||
bottom_frame = ctk.CTkFrame(self)
|
||||
bottom_frame.pack(fill="x", padx=15, pady=(0, 15))
|
||||
|
||||
# 左侧按钮
|
||||
left_buttons = ctk.CTkFrame(bottom_frame)
|
||||
left_buttons.pack(side="left", padx=10, pady=10)
|
||||
|
||||
self.select_all_btn = ctk.CTkButton(
|
||||
left_buttons,
|
||||
text="全选",
|
||||
command=self._select_all,
|
||||
width=80,
|
||||
height=30
|
||||
)
|
||||
self.select_all_btn.pack(side="left", padx=5)
|
||||
|
||||
self.deselect_all_btn = ctk.CTkButton(
|
||||
left_buttons,
|
||||
text="取消全选",
|
||||
command=self._deselect_all,
|
||||
width=80,
|
||||
height=30,
|
||||
fg_color="gray",
|
||||
hover_color="darkgray"
|
||||
)
|
||||
self.deselect_all_btn.pack(side="left", padx=5)
|
||||
|
||||
self.download_btn = ctk.CTkButton(
|
||||
left_buttons,
|
||||
text="批量下载",
|
||||
command=self._on_download,
|
||||
width=100,
|
||||
height=30,
|
||||
fg_color="green",
|
||||
hover_color="darkgreen"
|
||||
)
|
||||
self.download_btn.pack(side="left", padx=5)
|
||||
|
||||
# 分页控件
|
||||
page_frame = ctk.CTkFrame(bottom_frame)
|
||||
page_frame.pack(side="right", padx=10, pady=10)
|
||||
|
||||
self.prev_btn = ctk.CTkButton(
|
||||
page_frame,
|
||||
text="上一页",
|
||||
command=self._prev_page,
|
||||
width=80,
|
||||
height=30
|
||||
)
|
||||
self.prev_btn.pack(side="left", padx=5)
|
||||
|
||||
self.page_label = ctk.CTkLabel(
|
||||
page_frame,
|
||||
text="第 1 页",
|
||||
font=("Arial", 11)
|
||||
)
|
||||
self.page_label.pack(side="left", padx=10)
|
||||
|
||||
self.next_btn = ctk.CTkButton(
|
||||
page_frame,
|
||||
text="下一页",
|
||||
command=self._next_page,
|
||||
width=80,
|
||||
height=30
|
||||
)
|
||||
self.next_btn.pack(side="left", padx=5)
|
||||
|
||||
def load_data(self, data_list: List[Dict], total_count: int, page: int = 1):
|
||||
"""加载图片数据"""
|
||||
self.current_data = data_list
|
||||
self.total_count = total_count
|
||||
self.current_page = page
|
||||
self.selected_ids.clear()
|
||||
|
||||
self._update_grid()
|
||||
self._update_stats()
|
||||
self._update_pagination()
|
||||
|
||||
def _update_grid(self):
|
||||
"""更新图片网格"""
|
||||
# 清空现有卡片
|
||||
for widget in self.grid_frame.winfo_children():
|
||||
widget.destroy()
|
||||
|
||||
if not self.current_data:
|
||||
no_data_label = ctk.CTkLabel(
|
||||
self.grid_frame,
|
||||
text="暂无数据",
|
||||
font=("Arial", 14),
|
||||
text_color="gray"
|
||||
)
|
||||
no_data_label.grid(row=0, column=0, pady=50, padx=50)
|
||||
return
|
||||
|
||||
# 创建网格卡片
|
||||
for i, data in enumerate(self.current_data):
|
||||
row = i // GRID_COLUMNS
|
||||
col = i % GRID_COLUMNS
|
||||
|
||||
# 检查是否已下载
|
||||
is_downloaded = self.sqlite_manager.is_downloaded(
|
||||
data['datasource'],
|
||||
data['image_url']
|
||||
)
|
||||
|
||||
card = ImageCard(
|
||||
self.grid_frame,
|
||||
data=data,
|
||||
is_downloaded=is_downloaded,
|
||||
on_check_changed=self._on_check_changed,
|
||||
on_preview=self._on_preview
|
||||
)
|
||||
card.grid(
|
||||
row=row,
|
||||
column=col,
|
||||
padx=CARD_PADDING,
|
||||
pady=CARD_PADDING,
|
||||
sticky="nsew"
|
||||
)
|
||||
|
||||
# 配置网格列权重
|
||||
for col in range(GRID_COLUMNS):
|
||||
self.grid_frame.grid_columnconfigure(col, weight=1)
|
||||
|
||||
def _update_stats(self):
|
||||
"""更新统计信息"""
|
||||
selected_count = len(self.selected_ids)
|
||||
self.count_label.configure(
|
||||
text=f"共 {self.total_count} 张 | 已选 {selected_count} 张"
|
||||
)
|
||||
|
||||
def _update_pagination(self):
|
||||
"""更新分页控件"""
|
||||
total_pages = (self.total_count + ITEMS_PER_PAGE - 1) // ITEMS_PER_PAGE
|
||||
if total_pages == 0:
|
||||
total_pages = 1
|
||||
|
||||
self.page_label.configure(text=f"第 {self.current_page}/{total_pages} 页")
|
||||
|
||||
# 更新按钮状态
|
||||
self.prev_btn.configure(state="normal" if self.current_page > 1 else "disabled")
|
||||
self.next_btn.configure(state="normal" if self.current_page < total_pages else "disabled")
|
||||
|
||||
def _on_check_changed(self, item_id: int, checked: bool):
|
||||
"""勾选框改变"""
|
||||
if checked:
|
||||
self.selected_ids.add(item_id)
|
||||
else:
|
||||
self.selected_ids.discard(item_id)
|
||||
|
||||
self._update_stats()
|
||||
|
||||
def _select_all(self):
|
||||
"""全选"""
|
||||
for widget in self.grid_frame.winfo_children():
|
||||
if isinstance(widget, ImageCard):
|
||||
widget.set_checked(True)
|
||||
self.selected_ids.add(widget.data['id'])
|
||||
|
||||
self._update_stats()
|
||||
|
||||
def _deselect_all(self):
|
||||
"""取消全选"""
|
||||
for widget in self.grid_frame.winfo_children():
|
||||
if isinstance(widget, ImageCard):
|
||||
widget.set_checked(False)
|
||||
|
||||
self.selected_ids.clear()
|
||||
self._update_stats()
|
||||
|
||||
def _on_download(self):
|
||||
"""批量下载"""
|
||||
if not self.selected_ids:
|
||||
messagebox.showwarning("提示", "请先选择要下载的图片")
|
||||
return
|
||||
|
||||
# 获取选中的图片数据
|
||||
selected_data = [
|
||||
data for data in self.current_data
|
||||
if data['id'] in self.selected_ids
|
||||
]
|
||||
|
||||
# 调用下载回调
|
||||
if self.on_download_callback:
|
||||
self.on_download_callback(selected_data)
|
||||
|
||||
def _on_preview(self, data: Dict):
|
||||
"""查看大图"""
|
||||
# 创建预览窗口
|
||||
preview_window = ctk.CTkToplevel(self)
|
||||
preview_window.title(f"预览 - {data['goods_name']}")
|
||||
preview_window.geometry("800x700")
|
||||
|
||||
# 窗口属性
|
||||
preview_window.transient(self)
|
||||
preview_window.grab_set()
|
||||
preview_window.lift()
|
||||
preview_window.focus_set()
|
||||
|
||||
# 居中显示
|
||||
preview_window.update_idletasks()
|
||||
x = (preview_window.winfo_screenwidth() // 2) - 400
|
||||
y = (preview_window.winfo_screenheight() // 2) - 350
|
||||
preview_window.geometry(f"800x700+{x}+{y}")
|
||||
|
||||
# 信息框架
|
||||
info_frame = ctk.CTkFrame(preview_window)
|
||||
info_frame.pack(fill="x", padx=20, pady=(20, 10))
|
||||
|
||||
info_text = f"""
|
||||
物品名称: {data['goods_name']}
|
||||
采集时间: {data['create_time']}
|
||||
设备编码: {data['equipment_code']}
|
||||
数据源: {data['datasource']}
|
||||
""".strip()
|
||||
|
||||
info_label = ctk.CTkLabel(
|
||||
info_frame,
|
||||
text=info_text,
|
||||
font=("Arial", 12),
|
||||
justify="left"
|
||||
)
|
||||
info_label.pack(pady=10, padx=10)
|
||||
|
||||
# 图片容器
|
||||
image_container = ctk.CTkLabel(
|
||||
preview_window,
|
||||
text="加载中...",
|
||||
width=760,
|
||||
height=550
|
||||
)
|
||||
image_container.pack(pady=10, padx=20)
|
||||
|
||||
# 异步加载大图
|
||||
def load_large_image():
|
||||
image = load_image_from_url(data['image_url'], size=(760, 550))
|
||||
if image:
|
||||
image_container.configure(image=image, text="")
|
||||
image_container.image = image
|
||||
else:
|
||||
image_container.configure(text="图片加载失败")
|
||||
|
||||
threading.Thread(target=load_large_image, daemon=True).start()
|
||||
|
||||
def _prev_page(self):
|
||||
"""上一页"""
|
||||
if self.current_page > 1:
|
||||
# 需要重新查询数据(在主应用中处理)
|
||||
pass
|
||||
|
||||
def _next_page(self):
|
||||
"""下一页"""
|
||||
total_pages = (self.total_count + ITEMS_PER_PAGE - 1) // ITEMS_PER_PAGE
|
||||
if self.current_page < total_pages:
|
||||
# 需要重新查询数据(在主应用中处理)
|
||||
pass
|
||||
|
||||
def get_current_page(self) -> int:
|
||||
"""获取当前页码"""
|
||||
return self.current_page
|
||||
|
||||
def clear(self):
|
||||
"""清空数据"""
|
||||
self.current_data = []
|
||||
self.total_count = 0
|
||||
self.current_page = 1
|
||||
self.selected_ids.clear()
|
||||
self._update_grid()
|
||||
self._update_stats()
|
||||
self._update_pagination()
|
||||
@@ -0,0 +1,296 @@
|
||||
"""
|
||||
右侧统计面板
|
||||
"""
|
||||
import customtkinter as ctk
|
||||
from typing import Dict, Callable, Optional
|
||||
|
||||
|
||||
class StatisticsPanel(ctk.CTkFrame):
|
||||
"""右侧统计面板"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent,
|
||||
on_refresh_callback: Callable,
|
||||
on_download_history_callback: Callable,
|
||||
on_export_callback: Callable,
|
||||
on_settings_callback: Callable,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(parent, **kwargs)
|
||||
|
||||
self.on_refresh_callback = on_refresh_callback
|
||||
self.on_download_history_callback = on_download_history_callback
|
||||
self.on_export_callback = on_export_callback
|
||||
self.on_settings_callback = on_settings_callback
|
||||
|
||||
self._create_widgets()
|
||||
|
||||
def _create_widgets(self):
|
||||
"""创建UI组件"""
|
||||
# 标题
|
||||
title_label = ctk.CTkLabel(
|
||||
self,
|
||||
text="统计信息",
|
||||
font=("Arial", 16, "bold")
|
||||
)
|
||||
title_label.pack(pady=(15, 10), padx=15)
|
||||
|
||||
# ==================== 当前数据源信息 ====================
|
||||
datasource_frame = ctk.CTkFrame(self)
|
||||
datasource_frame.pack(fill="x", padx=15, pady=(0, 10))
|
||||
|
||||
datasource_title = ctk.CTkLabel(
|
||||
datasource_frame,
|
||||
text="当前数据源",
|
||||
font=("Arial", 12, "bold")
|
||||
)
|
||||
datasource_title.pack(pady=(10, 5), padx=10)
|
||||
|
||||
self.datasource_label = ctk.CTkLabel(
|
||||
datasource_frame,
|
||||
text="菜品",
|
||||
font=("Arial", 14, "bold"),
|
||||
text_color="blue"
|
||||
)
|
||||
self.datasource_label.pack(pady=(0, 10), padx=10)
|
||||
|
||||
# ==================== 数据统计 ====================
|
||||
stats_frame = ctk.CTkFrame(self)
|
||||
stats_frame.pack(fill="x", padx=15, pady=(0, 10))
|
||||
|
||||
stats_title = ctk.CTkLabel(
|
||||
stats_frame,
|
||||
text="数据统计",
|
||||
font=("Arial", 12, "bold")
|
||||
)
|
||||
stats_title.pack(pady=(10, 5), padx=10)
|
||||
|
||||
# 查询结果
|
||||
self.query_result_label = ctk.CTkLabel(
|
||||
stats_frame,
|
||||
text="查询结果: 0 张",
|
||||
font=("Arial", 11),
|
||||
anchor="w"
|
||||
)
|
||||
self.query_result_label.pack(fill="x", pady=2, padx=15)
|
||||
|
||||
# 已选择
|
||||
self.selected_label = ctk.CTkLabel(
|
||||
stats_frame,
|
||||
text="已选择: 0 张",
|
||||
font=("Arial", 11),
|
||||
anchor="w"
|
||||
)
|
||||
self.selected_label.pack(fill="x", pady=2, padx=15)
|
||||
|
||||
# 分隔线
|
||||
separator1 = ctk.CTkFrame(stats_frame, height=2)
|
||||
separator1.pack(fill="x", pady=10, padx=15)
|
||||
|
||||
# ==================== 时间分布 ====================
|
||||
time_frame = ctk.CTkFrame(self)
|
||||
time_frame.pack(fill="x", padx=15, pady=(0, 10))
|
||||
|
||||
time_title = ctk.CTkLabel(
|
||||
time_frame,
|
||||
text="时间分布",
|
||||
font=("Arial", 12, "bold")
|
||||
)
|
||||
time_title.pack(pady=(10, 5), padx=10)
|
||||
|
||||
# 今日
|
||||
self.today_label = ctk.CTkLabel(
|
||||
time_frame,
|
||||
text="今日: 0 张",
|
||||
font=("Arial", 11),
|
||||
anchor="w"
|
||||
)
|
||||
self.today_label.pack(fill="x", pady=2, padx=15)
|
||||
|
||||
# 本周
|
||||
self.week_label = ctk.CTkLabel(
|
||||
time_frame,
|
||||
text="本周: 0 张",
|
||||
font=("Arial", 11),
|
||||
anchor="w"
|
||||
)
|
||||
self.week_label.pack(fill="x", pady=2, padx=15)
|
||||
|
||||
# 本月
|
||||
self.month_label = ctk.CTkLabel(
|
||||
time_frame,
|
||||
text="本月: 0 张",
|
||||
font=("Arial", 11),
|
||||
anchor="w"
|
||||
)
|
||||
self.month_label.pack(fill="x", pady=2, padx=15)
|
||||
|
||||
# 分隔线
|
||||
separator2 = ctk.CTkFrame(time_frame, height=2)
|
||||
separator2.pack(fill="x", pady=10, padx=15)
|
||||
|
||||
# ==================== TOP 10物品排行 ====================
|
||||
top_frame = ctk.CTkFrame(self)
|
||||
top_frame.pack(fill="both", expand=True, padx=15, pady=(0, 10))
|
||||
|
||||
top_title = ctk.CTkLabel(
|
||||
top_frame,
|
||||
text="TOP 10 物品排行",
|
||||
font=("Arial", 12, "bold")
|
||||
)
|
||||
top_title.pack(pady=(10, 5), padx=10)
|
||||
|
||||
# 可滚动的排行榜
|
||||
self.top_scroll = ctk.CTkScrollableFrame(top_frame, height=200)
|
||||
self.top_scroll.pack(fill="both", expand=True, padx=10, pady=(0, 10))
|
||||
|
||||
# ==================== 快捷操作 ====================
|
||||
actions_frame = ctk.CTkFrame(self)
|
||||
actions_frame.pack(fill="x", padx=15, pady=(0, 15))
|
||||
|
||||
actions_title = ctk.CTkLabel(
|
||||
actions_frame,
|
||||
text="快捷操作",
|
||||
font=("Arial", 12, "bold")
|
||||
)
|
||||
actions_title.pack(pady=(10, 5), padx=10)
|
||||
|
||||
# 刷新按钮
|
||||
refresh_btn = ctk.CTkButton(
|
||||
actions_frame,
|
||||
text="🔄 刷新",
|
||||
command=self.on_refresh_callback,
|
||||
height=30
|
||||
)
|
||||
refresh_btn.pack(fill="x", padx=10, pady=2)
|
||||
|
||||
# 下载历史按钮
|
||||
history_btn = ctk.CTkButton(
|
||||
actions_frame,
|
||||
text="📜 下载历史",
|
||||
command=self.on_download_history_callback,
|
||||
height=30,
|
||||
fg_color="purple",
|
||||
hover_color="darkviolet"
|
||||
)
|
||||
history_btn.pack(fill="x", padx=10, pady=2)
|
||||
|
||||
# 导出按钮
|
||||
export_btn = ctk.CTkButton(
|
||||
actions_frame,
|
||||
text="📊 导出数据",
|
||||
command=self.on_export_callback,
|
||||
height=30,
|
||||
fg_color="orange",
|
||||
hover_color="darkorange"
|
||||
)
|
||||
export_btn.pack(fill="x", padx=10, pady=2)
|
||||
|
||||
# 设置按钮
|
||||
settings_btn = ctk.CTkButton(
|
||||
actions_frame,
|
||||
text="⚙️ 设置",
|
||||
command=self.on_settings_callback,
|
||||
height=30,
|
||||
fg_color="gray",
|
||||
hover_color="darkgray"
|
||||
)
|
||||
settings_btn.pack(fill="x", padx=10, pady=(2, 10))
|
||||
|
||||
def update_datasource(self, datasource: str):
|
||||
"""更新当前数据源显示"""
|
||||
self.datasource_label.configure(text=datasource)
|
||||
|
||||
def update_statistics(self, stats: Dict):
|
||||
"""
|
||||
更新统计信息
|
||||
|
||||
Args:
|
||||
stats: 统计数据字典,包含:
|
||||
- total_count: 总数量
|
||||
- today_count: 今日数量
|
||||
- week_count: 本周数量
|
||||
- month_count: 本月数量
|
||||
- top_items: TOP 10列表 [{'name': str, 'count': int}, ...]
|
||||
"""
|
||||
# 查询结果
|
||||
total_count = stats.get('total_count', 0)
|
||||
self.query_result_label.configure(text=f"查询结果: {total_count} 张")
|
||||
|
||||
# 时间分布
|
||||
today_count = stats.get('today_count', 0)
|
||||
self.today_label.configure(text=f"今日: {today_count} 张")
|
||||
|
||||
week_count = stats.get('week_count', 0)
|
||||
self.week_label.configure(text=f"本周: {week_count} 张")
|
||||
|
||||
month_count = stats.get('month_count', 0)
|
||||
self.month_label.configure(text=f"本月: {month_count} 张")
|
||||
|
||||
# TOP 10排行
|
||||
self._update_top_items(stats.get('top_items', []))
|
||||
|
||||
def _update_top_items(self, top_items: list):
|
||||
"""更新TOP 10排行榜"""
|
||||
# 清空现有内容
|
||||
for widget in self.top_scroll.winfo_children():
|
||||
widget.destroy()
|
||||
|
||||
if not top_items:
|
||||
no_data_label = ctk.CTkLabel(
|
||||
self.top_scroll,
|
||||
text="暂无数据",
|
||||
font=("Arial", 11),
|
||||
text_color="gray"
|
||||
)
|
||||
no_data_label.pack(pady=10)
|
||||
return
|
||||
|
||||
# 显示TOP 10
|
||||
for i, item in enumerate(top_items, 1):
|
||||
item_frame = ctk.CTkFrame(self.top_scroll)
|
||||
item_frame.pack(fill="x", pady=2, padx=5)
|
||||
|
||||
# 排名
|
||||
rank_label = ctk.CTkLabel(
|
||||
item_frame,
|
||||
text=f"{i}.",
|
||||
font=("Arial", 11, "bold"),
|
||||
width=30
|
||||
)
|
||||
rank_label.pack(side="left", padx=(5, 2))
|
||||
|
||||
# 物品名称
|
||||
name_label = ctk.CTkLabel(
|
||||
item_frame,
|
||||
text=item['name'],
|
||||
font=("Arial", 10),
|
||||
anchor="w"
|
||||
)
|
||||
name_label.pack(side="left", fill="x", expand=True, padx=5)
|
||||
|
||||
# 数量
|
||||
count_label = ctk.CTkLabel(
|
||||
item_frame,
|
||||
text=f"{item['count']}张",
|
||||
font=("Arial", 10),
|
||||
text_color="blue"
|
||||
)
|
||||
count_label.pack(side="right", padx=5)
|
||||
|
||||
def update_selected_count(self, count: int):
|
||||
"""更新已选择数量"""
|
||||
self.selected_label.configure(text=f"已选择: {count} 张")
|
||||
|
||||
def clear_statistics(self):
|
||||
"""清空统计信息"""
|
||||
self.query_result_label.configure(text="查询结果: 0 张")
|
||||
self.selected_label.configure(text="已选择: 0 张")
|
||||
self.today_label.configure(text="今日: 0 张")
|
||||
self.week_label.configure(text="本周: 0 张")
|
||||
self.month_label.configure(text="本月: 0 张")
|
||||
|
||||
# 清空TOP 10
|
||||
for widget in self.top_scroll.winfo_children():
|
||||
widget.destroy()
|
||||
Reference in New Issue
Block a user