470 lines
14 KiB
Python
470 lines
14 KiB
Python
"""
|
|
中间图片预览面板
|
|
"""
|
|
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()
|