523 lines
18 KiB
Python
523 lines
18 KiB
Python
"""
|
|
物联网图片采集管理系统 - 主应用程序
|
|
"""
|
|
import os
|
|
import sys
|
|
import customtkinter as ctk
|
|
from tkinter import filedialog, messagebox
|
|
import threading
|
|
import json
|
|
from datetime import datetime
|
|
|
|
# 导入配置
|
|
from config import (
|
|
APPEARANCE_MODE, COLOR_THEME,
|
|
WINDOW_WIDTH, WINDOW_HEIGHT,
|
|
LEFT_PANEL_WIDTH, RIGHT_PANEL_WIDTH,
|
|
ITEMS_PER_PAGE
|
|
)
|
|
|
|
# 导入数据库管理器
|
|
from database import MySQLManager, SQLiteManager
|
|
|
|
# 导入UI组件
|
|
from ui import FilterPanel, PreviewPanel, StatisticsPanel
|
|
|
|
# 导入工具
|
|
from utils import ImageDownloader
|
|
|
|
# 设置customtkinter外观
|
|
ctk.set_appearance_mode(APPEARANCE_MODE)
|
|
ctk.set_default_color_theme(COLOR_THEME)
|
|
|
|
|
|
class DataManagementApp:
|
|
"""物联网图片采集管理系统主应用"""
|
|
|
|
def __init__(self, root):
|
|
self.root = root
|
|
self.root.title("数字味道 - 物联网图片采集管理系统")
|
|
self.root.geometry(f"{WINDOW_WIDTH}x{WINDOW_HEIGHT}")
|
|
|
|
# 数据库管理器
|
|
try:
|
|
self.mysql_manager = MySQLManager()
|
|
self.sqlite_manager = SQLiteManager()
|
|
except Exception as e:
|
|
messagebox.showerror("错误", f"数据库连接失败: {e}")
|
|
sys.exit(1)
|
|
|
|
# 图片下载器
|
|
self.image_downloader = ImageDownloader(self.sqlite_manager)
|
|
|
|
# 当前筛选条件
|
|
self.current_filters = {}
|
|
|
|
# 创建UI
|
|
self._create_widgets()
|
|
|
|
# 窗口关闭事件
|
|
self.root.protocol("WM_DELETE_WINDOW", self._on_closing)
|
|
|
|
def _create_widgets(self):
|
|
"""创建UI组件"""
|
|
# 主容器
|
|
main_container = ctk.CTkFrame(self.root)
|
|
main_container.pack(fill="both", expand=True, padx=10, pady=10)
|
|
|
|
# ==================== 左侧筛选面板 ====================
|
|
self.filter_panel = FilterPanel(
|
|
main_container,
|
|
on_query_callback=self._on_query,
|
|
on_datasource_change_callback=self._on_datasource_changed,
|
|
width=LEFT_PANEL_WIDTH
|
|
)
|
|
self.filter_panel.pack(side="left", fill="y", padx=(0, 5))
|
|
|
|
# ==================== 中间预览面板 ====================
|
|
self.preview_panel = PreviewPanel(
|
|
main_container,
|
|
sqlite_manager=self.sqlite_manager,
|
|
on_download_callback=self._on_download
|
|
)
|
|
self.preview_panel.pack(side="left", fill="both", expand=True, padx=5)
|
|
|
|
# 绑定分页事件
|
|
self.preview_panel.prev_btn.configure(command=self._on_prev_page)
|
|
self.preview_panel.next_btn.configure(command=self._on_next_page)
|
|
|
|
# ==================== 右侧统计面板 ====================
|
|
self.statistics_panel = StatisticsPanel(
|
|
main_container,
|
|
on_refresh_callback=self._on_refresh,
|
|
on_download_history_callback=self._on_download_history,
|
|
on_export_callback=self._on_export,
|
|
on_settings_callback=self._on_settings,
|
|
width=RIGHT_PANEL_WIDTH
|
|
)
|
|
self.statistics_panel.pack(side="right", fill="y", padx=(5, 0))
|
|
|
|
def _on_query(self, filters: dict):
|
|
"""查询按钮回调"""
|
|
self.current_filters = filters
|
|
self._load_data(page=1)
|
|
|
|
def _on_datasource_changed(self, datasource: str):
|
|
"""数据源切换回调"""
|
|
self.statistics_panel.update_datasource(datasource)
|
|
|
|
def _load_data(self, page: int = 1):
|
|
"""加载数据"""
|
|
# 设置加载状态
|
|
self.filter_panel.set_loading_state(True)
|
|
|
|
# 在后台线程中查询数据
|
|
def query_thread():
|
|
try:
|
|
# 查询图片数据
|
|
data_list, total_count = self.mysql_manager.query_images(
|
|
datasource=self.current_filters.get('datasource', '菜品'),
|
|
start_date=self.current_filters.get('start_date'),
|
|
end_date=self.current_filters.get('end_date'),
|
|
goods_name=self.current_filters.get('goods_name'),
|
|
equipment_code=self.current_filters.get('equipment_code'),
|
|
page=page,
|
|
page_size=ITEMS_PER_PAGE
|
|
)
|
|
|
|
# 查询统计信息
|
|
stats = self.mysql_manager.get_statistics(
|
|
datasource=self.current_filters.get('datasource', '菜品'),
|
|
start_date=self.current_filters.get('start_date'),
|
|
end_date=self.current_filters.get('end_date')
|
|
)
|
|
|
|
# 在主线程中更新UI
|
|
self.root.after(0, lambda: self._update_ui(data_list, total_count, stats, page))
|
|
|
|
except Exception as e:
|
|
self.root.after(0, lambda: messagebox.showerror("错误", f"查询数据失败: {e}"))
|
|
finally:
|
|
self.root.after(0, lambda: self.filter_panel.set_loading_state(False))
|
|
|
|
threading.Thread(target=query_thread, daemon=True).start()
|
|
|
|
def _update_ui(self, data_list: list, total_count: int, stats: dict, page: int):
|
|
"""更新UI"""
|
|
# 更新预览面板
|
|
self.preview_panel.load_data(data_list, total_count, page)
|
|
|
|
# 更新统计面板
|
|
self.statistics_panel.update_statistics(stats)
|
|
|
|
def _on_prev_page(self):
|
|
"""上一页"""
|
|
current_page = self.preview_panel.get_current_page()
|
|
if current_page > 1:
|
|
self._load_data(page=current_page - 1)
|
|
|
|
def _on_next_page(self):
|
|
"""下一页"""
|
|
current_page = self.preview_panel.get_current_page()
|
|
total_pages = (self.preview_panel.total_count + ITEMS_PER_PAGE - 1) // ITEMS_PER_PAGE
|
|
if current_page < total_pages:
|
|
self._load_data(page=current_page + 1)
|
|
|
|
def _on_download(self, selected_data: list):
|
|
"""批量下载回调"""
|
|
# 选择保存目录
|
|
save_directory = filedialog.askdirectory(title="选择保存目录")
|
|
if not save_directory:
|
|
return
|
|
|
|
# 创建下载进度窗口
|
|
progress_window = ctk.CTkToplevel(self.root)
|
|
progress_window.title("下载进度")
|
|
progress_window.geometry("500x250")
|
|
progress_window.transient(self.root)
|
|
progress_window.grab_set()
|
|
|
|
# 居中显示
|
|
progress_window.update_idletasks()
|
|
x = (progress_window.winfo_screenwidth() // 2) - 250
|
|
y = (progress_window.winfo_screenheight() // 2) - 125
|
|
progress_window.geometry(f"500x250+{x}+{y}")
|
|
|
|
# 标题
|
|
title_label = ctk.CTkLabel(
|
|
progress_window,
|
|
text="正在下载图片...",
|
|
font=("Arial", 14, "bold")
|
|
)
|
|
title_label.pack(pady=(20, 10))
|
|
|
|
# 进度标签
|
|
progress_label = ctk.CTkLabel(
|
|
progress_window,
|
|
text="准备下载...",
|
|
font=("Arial", 12)
|
|
)
|
|
progress_label.pack(pady=10)
|
|
|
|
# 进度条
|
|
progress_bar = ctk.CTkProgressBar(progress_window, width=400)
|
|
progress_bar.pack(pady=10)
|
|
progress_bar.set(0)
|
|
|
|
# 当前文件名
|
|
current_file_label = ctk.CTkLabel(
|
|
progress_window,
|
|
text="",
|
|
font=("Arial", 10),
|
|
text_color="gray"
|
|
)
|
|
current_file_label.pack(pady=5)
|
|
|
|
# 取消按钮
|
|
cancel_btn = ctk.CTkButton(
|
|
progress_window,
|
|
text="取消",
|
|
command=lambda: self._cancel_download(progress_window),
|
|
fg_color="red",
|
|
hover_color="darkred"
|
|
)
|
|
cancel_btn.pack(pady=10)
|
|
|
|
# 进度回调
|
|
def progress_callback(current, total, filename):
|
|
progress = current / total
|
|
progress_bar.set(progress)
|
|
progress_label.configure(text=f"进度: {current}/{total} ({int(progress * 100)}%)")
|
|
current_file_label.configure(text=f"当前: {filename}")
|
|
|
|
# 完成回调
|
|
def complete_callback(success_count, failed_count, error_messages):
|
|
progress_window.destroy()
|
|
|
|
# 显示结果
|
|
if failed_count == 0:
|
|
messagebox.showinfo(
|
|
"下载完成",
|
|
f"成功下载 {success_count} 张图片!\n保存位置: {save_directory}"
|
|
)
|
|
else:
|
|
error_text = "\n".join(error_messages[:5]) # 只显示前5条错误
|
|
if len(error_messages) > 5:
|
|
error_text += f"\n... 还有 {len(error_messages) - 5} 条错误"
|
|
|
|
messagebox.showwarning(
|
|
"下载完成",
|
|
f"成功: {success_count} 张\n失败: {failed_count} 张\n\n错误信息:\n{error_text}"
|
|
)
|
|
|
|
# 刷新预览面板(更新下载状态)
|
|
self._on_refresh()
|
|
|
|
# 开始下载
|
|
def start_download():
|
|
self.image_downloader.download_images(
|
|
selected_data,
|
|
save_directory,
|
|
progress_callback=progress_callback,
|
|
complete_callback=complete_callback
|
|
)
|
|
|
|
threading.Thread(target=start_download, daemon=True).start()
|
|
|
|
def _cancel_download(self, progress_window):
|
|
"""取消下载"""
|
|
result = messagebox.askyesno("确认", "确定要取消下载吗?")
|
|
if result:
|
|
self.image_downloader.cancel()
|
|
progress_window.destroy()
|
|
|
|
def _on_refresh(self):
|
|
"""刷新数据"""
|
|
if self.current_filters:
|
|
current_page = self.preview_panel.get_current_page()
|
|
self._load_data(page=current_page)
|
|
else:
|
|
messagebox.showinfo("提示", "请先执行查询操作")
|
|
|
|
def _on_download_history(self):
|
|
"""查看下载历史"""
|
|
# 创建下载历史窗口
|
|
history_window = ctk.CTkToplevel(self.root)
|
|
history_window.title("下载历史")
|
|
history_window.geometry("900x600")
|
|
history_window.transient(self.root)
|
|
|
|
# 居中显示
|
|
history_window.update_idletasks()
|
|
x = (history_window.winfo_screenwidth() // 2) - 450
|
|
y = (history_window.winfo_screenheight() // 2) - 300
|
|
history_window.geometry(f"900x600+{x}+{y}")
|
|
|
|
# 标题
|
|
title_label = ctk.CTkLabel(
|
|
history_window,
|
|
text="下载历史记录",
|
|
font=("Arial", 16, "bold")
|
|
)
|
|
title_label.pack(pady=(20, 10))
|
|
|
|
# 统计信息
|
|
download_stats = self.sqlite_manager.get_download_statistics()
|
|
stats_text = f"总下载: {download_stats.get('total_count', 0)} 张 | 今日: {download_stats.get('today_count', 0)} 张"
|
|
|
|
stats_label = ctk.CTkLabel(
|
|
history_window,
|
|
text=stats_text,
|
|
font=("Arial", 12),
|
|
text_color="gray"
|
|
)
|
|
stats_label.pack(pady=5)
|
|
|
|
# 历史记录列表
|
|
history_scroll = ctk.CTkScrollableFrame(history_window)
|
|
history_scroll.pack(fill="both", expand=True, padx=20, pady=10)
|
|
|
|
# 加载历史记录
|
|
history_records = self.sqlite_manager.get_download_history(limit=100)
|
|
|
|
if not history_records:
|
|
no_data_label = ctk.CTkLabel(
|
|
history_scroll,
|
|
text="暂无下载历史",
|
|
font=("Arial", 12),
|
|
text_color="gray"
|
|
)
|
|
no_data_label.pack(pady=50)
|
|
else:
|
|
for record in history_records:
|
|
record_frame = ctk.CTkFrame(history_scroll)
|
|
record_frame.pack(fill="x", padx=10, pady=5)
|
|
|
|
info_text = f"""
|
|
数据源: {record['datasource']} | 物品: {record['goods_name']}
|
|
下载时间: {record['download_time']}
|
|
本地路径: {record['local_path']}
|
|
""".strip()
|
|
|
|
info_label = ctk.CTkLabel(
|
|
record_frame,
|
|
text=info_text,
|
|
font=("Arial", 10),
|
|
justify="left",
|
|
anchor="w"
|
|
)
|
|
info_label.pack(fill="x", padx=10, pady=5)
|
|
|
|
# 底部按钮
|
|
bottom_frame = ctk.CTkFrame(history_window)
|
|
bottom_frame.pack(fill="x", padx=20, pady=(0, 20))
|
|
|
|
# 清理无效记录按钮
|
|
clean_btn = ctk.CTkButton(
|
|
bottom_frame,
|
|
text="清理无效记录",
|
|
command=lambda: self._clean_invalid_records(history_window),
|
|
fg_color="orange",
|
|
hover_color="darkorange"
|
|
)
|
|
clean_btn.pack(side="left", padx=10, pady=10)
|
|
|
|
# 关闭按钮
|
|
close_btn = ctk.CTkButton(
|
|
bottom_frame,
|
|
text="关闭",
|
|
command=history_window.destroy,
|
|
fg_color="gray",
|
|
hover_color="darkgray"
|
|
)
|
|
close_btn.pack(side="right", padx=10, pady=10)
|
|
|
|
def _clean_invalid_records(self, window):
|
|
"""清理无效记录"""
|
|
result = messagebox.askyesno(
|
|
"确认",
|
|
"将清理本地文件已被删除的下载记录,确定继续吗?"
|
|
)
|
|
if result:
|
|
deleted_count = self.sqlite_manager.clear_invalid_records()
|
|
messagebox.showinfo("完成", f"已清理 {deleted_count} 条无效记录")
|
|
window.destroy()
|
|
self._on_download_history() # 重新打开窗口
|
|
|
|
def _on_export(self):
|
|
"""导出数据"""
|
|
if not self.current_filters:
|
|
messagebox.showinfo("提示", "请先执行查询操作")
|
|
return
|
|
|
|
# 选择保存文件
|
|
file_path = filedialog.asksaveasfilename(
|
|
title="导出数据",
|
|
defaultextension=".json",
|
|
filetypes=[
|
|
("JSON文件", "*.json"),
|
|
("CSV文件", "*.csv"),
|
|
("所有文件", "*.*")
|
|
]
|
|
)
|
|
|
|
if not file_path:
|
|
return
|
|
|
|
try:
|
|
# 查询所有数据(不分页)
|
|
all_data, total_count = self.mysql_manager.query_images(
|
|
datasource=self.current_filters.get('datasource', '菜品'),
|
|
start_date=self.current_filters.get('start_date'),
|
|
end_date=self.current_filters.get('end_date'),
|
|
goods_name=self.current_filters.get('goods_name'),
|
|
equipment_code=self.current_filters.get('equipment_code'),
|
|
page=1,
|
|
page_size=10000 # 大数量
|
|
)
|
|
|
|
if file_path.endswith('.json'):
|
|
# 导出为JSON
|
|
with open(file_path, 'w', encoding='utf-8') as f:
|
|
json.dump(all_data, f, ensure_ascii=False, indent=2)
|
|
elif file_path.endswith('.csv'):
|
|
# 导出为CSV
|
|
import csv
|
|
with open(file_path, 'w', newline='', encoding='utf-8-sig') as f:
|
|
if all_data:
|
|
writer = csv.DictWriter(f, fieldnames=all_data[0].keys())
|
|
writer.writeheader()
|
|
writer.writerows(all_data)
|
|
|
|
messagebox.showinfo("成功", f"已导出 {len(all_data)} 条数据到:\n{file_path}")
|
|
|
|
except Exception as e:
|
|
messagebox.showerror("错误", f"导出失败: {e}")
|
|
|
|
def _on_settings(self):
|
|
"""设置"""
|
|
# 创建设置窗口
|
|
settings_window = ctk.CTkToplevel(self.root)
|
|
settings_window.title("设置")
|
|
settings_window.geometry("500x400")
|
|
settings_window.transient(self.root)
|
|
settings_window.grab_set()
|
|
|
|
# 居中显示
|
|
settings_window.update_idletasks()
|
|
x = (settings_window.winfo_screenwidth() // 2) - 250
|
|
y = (settings_window.winfo_screenheight() // 2) - 200
|
|
settings_window.geometry(f"500x400+{x}+{y}")
|
|
|
|
# 标题
|
|
title_label = ctk.CTkLabel(
|
|
settings_window,
|
|
text="系统设置",
|
|
font=("Arial", 16, "bold")
|
|
)
|
|
title_label.pack(pady=(20, 10))
|
|
|
|
# 外观模式
|
|
appearance_frame = ctk.CTkFrame(settings_window)
|
|
appearance_frame.pack(fill="x", padx=20, pady=10)
|
|
|
|
appearance_label = ctk.CTkLabel(
|
|
appearance_frame,
|
|
text="外观模式:",
|
|
font=("Arial", 12)
|
|
)
|
|
appearance_label.pack(side="left", padx=10, pady=10)
|
|
|
|
appearance_menu = ctk.CTkOptionMenu(
|
|
appearance_frame,
|
|
values=["System", "Light", "Dark"],
|
|
command=lambda mode: ctk.set_appearance_mode(mode)
|
|
)
|
|
appearance_menu.pack(side="left", padx=10, pady=10)
|
|
appearance_menu.set(APPEARANCE_MODE)
|
|
|
|
# 说明文本
|
|
info_text = """
|
|
系统信息:
|
|
- 数据库: MySQL (生产环境,只读)
|
|
- 下载历史: SQLite (本地存储)
|
|
- 图片URL前缀: http://dev.yixiong-tech.com:8081
|
|
- 并发下载数: 5
|
|
""".strip()
|
|
|
|
info_label = ctk.CTkLabel(
|
|
settings_window,
|
|
text=info_text,
|
|
font=("Arial", 11),
|
|
justify="left",
|
|
anchor="w"
|
|
)
|
|
info_label.pack(fill="x", padx=30, pady=20)
|
|
|
|
# 关闭按钮
|
|
close_btn = ctk.CTkButton(
|
|
settings_window,
|
|
text="关闭",
|
|
command=settings_window.destroy
|
|
)
|
|
close_btn.pack(pady=20)
|
|
|
|
def _on_closing(self):
|
|
"""窗口关闭事件"""
|
|
result = messagebox.askyesno("确认", "确定要退出系统吗?")
|
|
if result:
|
|
# 关闭数据库连接
|
|
self.mysql_manager.close()
|
|
self.root.destroy()
|
|
|
|
|
|
def main():
|
|
"""主函数"""
|
|
root = ctk.CTk()
|
|
app = DataManagementApp(root)
|
|
root.mainloop()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|