- Add MaterialsToBeDeletedDAO for managing material deletion records by MaterialCode - Add MaterialValidationResult dataclass for enhanced validation results - Add CheckboxTreeview component with selectable checkbox functionality - Refactor material validation UI with new columns: select, material name, code, spec, model, manager - Add double-click to edit manager name functionality - Add select all/deselect all buttons - Add confirm deletion button to write selected records to database - Prioritize MaterialsToBeDeleted.ManagerName over type-based matching when displaying - Export results to Excel with selection state Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
710 lines
25 KiB
Python
710 lines
25 KiB
Python
#!/usr/bin/env python
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
物料校验标签页
|
||
|
||
校验物料状态并匹配待删除物料。
|
||
支持 2 种校验模式:
|
||
1. database_full - 数据库全表校验
|
||
2. database_filtered - 数据库 ProductionID 过滤校验
|
||
"""
|
||
|
||
import os
|
||
import threading
|
||
import tkinter as tk
|
||
from tkinter import ttk, messagebox, filedialog, simpledialog
|
||
from pathlib import Path
|
||
from io import StringIO
|
||
from contextlib import redirect_stdout
|
||
from gui.widgets import FileSelector, LogText
|
||
from gui.config_manager import ConfigManager
|
||
import pandas as pd
|
||
|
||
|
||
class CheckboxTreeview(ttk.Treeview):
|
||
"""支持 checkbox 的 Treeview 组件
|
||
|
||
使用 Unicode 字符模拟 checkbox:
|
||
- ☐ 未选中
|
||
- ☑ 选中
|
||
"""
|
||
|
||
def __init__(self, parent, **kwargs):
|
||
"""初始化 CheckboxTreeview
|
||
|
||
Args:
|
||
parent: 父容器
|
||
**kwargs: 传递给 Treeview 的参数
|
||
"""
|
||
super().__init__(parent, **kwargs)
|
||
self.checkboxes = {} # item_id -> bool
|
||
self.checkbox_column = "选择"
|
||
|
||
# 绑定点击事件
|
||
self.bind("<Button-1>", self._on_click)
|
||
|
||
def _on_click(self, event):
|
||
"""处理点击事件,切换 checkbox 状态"""
|
||
# 获取点击位置对应的 item 和 column
|
||
region = self.identify_region(event.x, event.y)
|
||
|
||
if region == "cell":
|
||
column = self.identify_column(event.x)
|
||
item = self.identify_row(event.y)
|
||
|
||
# 检查是否点击了 checkbox 列(第一列)
|
||
if column == "#1" and item:
|
||
# 切换 checkbox 状态
|
||
current_state = self.checkboxes.get(item, False)
|
||
self.set_checked(item, not current_state)
|
||
return "break" # 阻止默认行为
|
||
|
||
def set_checked(self, item, checked: bool):
|
||
"""设置指定 item 的 checkbox 状态
|
||
|
||
Args:
|
||
item: Treeview item ID
|
||
checked: 是否选中
|
||
"""
|
||
self.checkboxes[item] = checked
|
||
|
||
# 更新显示
|
||
checkbox_char = "☑" if checked else "☐"
|
||
values = list(self.item(item, "values"))
|
||
if values:
|
||
values[0] = checkbox_char
|
||
self.item(item, values=values)
|
||
|
||
def get_checked_items(self) -> list:
|
||
"""获取所有选中的 item
|
||
|
||
Returns:
|
||
List of item IDs
|
||
"""
|
||
return [item for item, checked in self.checkboxes.items() if checked]
|
||
|
||
def check_all(self, checked: bool = True):
|
||
"""全选或取消全选
|
||
|
||
Args:
|
||
checked: True 为全选,False 为取消全选
|
||
"""
|
||
for item in self.get_children():
|
||
self.set_checked(item, checked)
|
||
|
||
def insert(self, parent, index, values=None, **kwargs):
|
||
"""重写 insert 方法,初始化 checkbox 状态"""
|
||
if values is None:
|
||
values = []
|
||
|
||
# 确保第一个值是 checkbox
|
||
if not values or values[0] not in ["☐", "☑"]:
|
||
values = ["☐"] + list(values)
|
||
|
||
item = super().insert(parent, index, values=values, **kwargs)
|
||
|
||
# 初始化 checkbox 状态为未选中
|
||
checkbox_char = values[0] if values else "☐"
|
||
self.checkboxes[item] = (checkbox_char == "☑")
|
||
|
||
return item
|
||
|
||
def delete(self, *items):
|
||
"""重写 delete 方法,清理 checkbox 状态"""
|
||
for item in items:
|
||
if item in self.checkboxes:
|
||
del self.checkboxes[item]
|
||
super().delete(*items)
|
||
|
||
|
||
class MaterialValidationTab(ttk.Frame):
|
||
"""物料校验标签页"""
|
||
|
||
def __init__(self, parent, config: ConfigManager):
|
||
"""
|
||
初始化物料校验标签页
|
||
|
||
Args:
|
||
parent: 父容器
|
||
config: 配置管理器
|
||
"""
|
||
super().__init__(parent)
|
||
self.config = config
|
||
self.validating = False
|
||
self.validation_results = None
|
||
self.material_records_cache = None # 缓存完整的物料记录
|
||
|
||
self.create_widgets()
|
||
|
||
# 稍后显示就绪消息
|
||
try:
|
||
self.log_text.info("物料校验标签页已就绪")
|
||
except:
|
||
pass # 如果窗口还未完全就绪,忽略错误
|
||
|
||
def create_widgets(self):
|
||
"""创建界面组件"""
|
||
# 主容器
|
||
main_container = ttk.Frame(self)
|
||
main_container.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||
|
||
# 上部:控制面板
|
||
control_frame = ttk.Frame(main_container)
|
||
control_frame.pack(fill=tk.X, pady=(0, 10))
|
||
|
||
# 中部:结果表格
|
||
result_frame = ttk.LabelFrame(main_container, text="校验结果", padding=5)
|
||
result_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
|
||
|
||
# 下部:日志输出
|
||
log_frame = ttk.LabelFrame(main_container, text="日志", padding=5)
|
||
log_frame.pack(fill=tk.BOTH, expand=True)
|
||
|
||
self._create_control_panel(control_frame)
|
||
self._create_result_table(result_frame)
|
||
self._create_log_panel(log_frame)
|
||
|
||
# 初始化文件选择器显示状态
|
||
self._on_source_mode_change()
|
||
|
||
def _create_control_panel(self, parent):
|
||
"""创建控制面板"""
|
||
# 数据来源选择
|
||
source_group = ttk.LabelFrame(parent, text="数据来源", padding=10)
|
||
source_group.pack(fill=tk.X, pady=5)
|
||
|
||
self.source_mode = tk.StringVar(value="database_full")
|
||
|
||
# 数据库模式
|
||
ttk.Radiobutton(
|
||
source_group,
|
||
text="数据库 - 全表校验",
|
||
variable=self.source_mode,
|
||
value="database_full",
|
||
command=self._on_source_mode_change,
|
||
).grid(row=0, column=0, sticky="w", padx=5)
|
||
|
||
ttk.Radiobutton(
|
||
source_group,
|
||
text="数据库 - ProductionID 过滤",
|
||
variable=self.source_mode,
|
||
value="database_filtered",
|
||
command=self._on_source_mode_change,
|
||
).grid(row=0, column=1, sticky="w", padx=5)
|
||
|
||
# 文件选择
|
||
file_group = ttk.LabelFrame(parent, text="文件选择", padding=10)
|
||
file_group.pack(fill=tk.X, pady=5)
|
||
|
||
# 数据库全表模式 - 无需输入文件
|
||
self.db_full_frame = ttk.Frame(file_group)
|
||
ttk.Label(
|
||
self.db_full_frame,
|
||
text="数据库全表模式:将查询 DiscreteMaterialPlanData 表中的所有材料",
|
||
foreground="gray"
|
||
).pack(anchor="w")
|
||
|
||
# 数据库过滤模式 - 需要 ProductionID 文件
|
||
self.db_filtered_frame = ttk.Frame(file_group)
|
||
self.db_filtered_production_id_selector = FileSelector(
|
||
self.db_filtered_frame,
|
||
label_text="ProductionID 文件:",
|
||
file_type="file",
|
||
file_types=[("文本文件", "*.txt"), ("所有文件", "*.*")],
|
||
initial_dir="D:/python/playwrite/",
|
||
)
|
||
self.db_filtered_production_id_selector.pack(fill=tk.X)
|
||
|
||
# 输出文件
|
||
output_frame = ttk.Frame(file_group)
|
||
output_frame.grid(row=1, column=0, columnspan=2, sticky="ew", pady=(10, 0))
|
||
|
||
self.output_file_selector = FileSelector(
|
||
output_frame,
|
||
label_text="输出文件:",
|
||
file_type="file",
|
||
file_types=[("Excel 文件", "*.xlsx"), ("所有文件", "*.*")],
|
||
initial_dir=self.config.get("paths.data_dir", "data/"),
|
||
)
|
||
self.output_file_selector.pack(fill=tk.X)
|
||
|
||
# 设置默认输出
|
||
default_output = os.path.join(
|
||
self.config.get("paths.data_dir", "data/"),
|
||
self.config.get("paths.validation_output", "物料状态校验结果.xlsx"),
|
||
)
|
||
self.output_file_selector.set(default_output)
|
||
|
||
# 控制按钮
|
||
button_frame = ttk.Frame(parent)
|
||
button_frame.pack(fill=tk.X, pady=10)
|
||
|
||
self.start_button = ttk.Button(
|
||
button_frame, text="开始校验", command=self.start_validation
|
||
)
|
||
self.start_button.pack(side=tk.LEFT, padx=5)
|
||
|
||
# 全选按钮
|
||
self.select_all_button = ttk.Button(
|
||
button_frame,
|
||
text="全选",
|
||
command=lambda: self._select_all(check=True),
|
||
state=tk.DISABLED
|
||
)
|
||
self.select_all_button.pack(side=tk.LEFT, padx=5)
|
||
|
||
# 取消全选按钮
|
||
self.deselect_all_button = ttk.Button(
|
||
button_frame,
|
||
text="取消全选",
|
||
command=lambda: self._select_all(check=False),
|
||
state=tk.DISABLED
|
||
)
|
||
self.deselect_all_button.pack(side=tk.LEFT, padx=5)
|
||
|
||
# 确认删除按钮
|
||
self.confirm_delete_button = ttk.Button(
|
||
button_frame,
|
||
text="确认删除",
|
||
command=self.confirm_deletion,
|
||
state=tk.DISABLED
|
||
)
|
||
self.confirm_delete_button.pack(side=tk.LEFT, padx=5)
|
||
|
||
self.export_button = ttk.Button(
|
||
button_frame,
|
||
text="导出结果",
|
||
command=self.export_results,
|
||
state=tk.DISABLED,
|
||
)
|
||
self.export_button.pack(side=tk.LEFT, padx=5)
|
||
|
||
def _create_result_table(self, parent):
|
||
"""创建结果表格"""
|
||
# 新列定义:选择、材料名称、材料代码、规格、型号、负责人
|
||
columns = ("选择", "材料名称", "材料代码", "规格", "型号", "负责人")
|
||
self.tree = CheckboxTreeview(parent, columns=columns, show="headings", height=10)
|
||
|
||
# 设置列标题和宽度
|
||
self.tree.heading("选择", text="选择")
|
||
self.tree.heading("材料名称", text="材料名称")
|
||
self.tree.heading("材料代码", text="材料代码")
|
||
self.tree.heading("规格", text="规格")
|
||
self.tree.heading("型号", text="型号")
|
||
self.tree.heading("负责人", text="负责人(双击编辑)")
|
||
|
||
self.tree.column("选择", width=50, anchor="center")
|
||
self.tree.column("材料名称", width=250)
|
||
self.tree.column("材料代码", width=150)
|
||
self.tree.column("规格", width=150)
|
||
self.tree.column("型号", width=150)
|
||
self.tree.column("负责人", width=150)
|
||
|
||
# 添加滚动条
|
||
scrollbar_y = ttk.Scrollbar(parent, orient=tk.VERTICAL, command=self.tree.yview)
|
||
scrollbar_x = ttk.Scrollbar(
|
||
parent, orient=tk.HORIZONTAL, command=self.tree.xview
|
||
)
|
||
|
||
self.tree.configure(
|
||
yscrollcommand=scrollbar_y.set, xscrollcommand=scrollbar_x.set
|
||
)
|
||
|
||
# 布局
|
||
self.tree.grid(row=0, column=0, sticky="nsew")
|
||
scrollbar_y.grid(row=0, column=1, sticky="ns")
|
||
scrollbar_x.grid(row=1, column=0, sticky="ew")
|
||
|
||
parent.rowconfigure(0, weight=1)
|
||
parent.columnconfigure(0, weight=1)
|
||
|
||
# 绑定双击事件用于编辑负责人
|
||
self.tree.bind("<Double-1>", self._on_cell_double_click)
|
||
|
||
def _create_log_panel(self, parent):
|
||
"""创建日志面板"""
|
||
self.log_text = LogText(parent, height=8, readonly=True)
|
||
self.log_text.pack(fill=tk.BOTH, expand=True)
|
||
|
||
def _on_source_mode_change(self):
|
||
"""数据源模式切换"""
|
||
mode = self.source_mode.get()
|
||
|
||
# 隐藏所有文件选择框架
|
||
self.db_full_frame.grid_remove()
|
||
self.db_filtered_frame.grid_remove()
|
||
|
||
# 根据模式显示对应的文件选择器
|
||
if mode == "database_full":
|
||
self.db_full_frame.grid(row=0, column=0, columnspan=2, sticky="ew")
|
||
elif mode == "database_filtered":
|
||
self.db_filtered_frame.grid(row=0, column=0, columnspan=2, sticky="ew")
|
||
|
||
def _on_cell_double_click(self, event):
|
||
"""处理单元格双击事件,编辑负责人"""
|
||
# 获取点击位置
|
||
region = self.tree.identify_region(event.x, event.y)
|
||
|
||
if region == "cell":
|
||
column = self.tree.identify_column(event.x)
|
||
item = self.tree.identify_row(event.y)
|
||
|
||
# 检查是否点击了"负责人"列(第6列)
|
||
if column == "#6" and item:
|
||
values = self.tree.item(item, "values")
|
||
current_value = values[5] if len(values) > 5 else ""
|
||
|
||
# 弹出编辑对话框
|
||
new_value = simpledialog.askstring(
|
||
"编辑负责人",
|
||
f"请输入负责人姓名:",
|
||
initialvalue=current_value
|
||
)
|
||
|
||
if new_value is not None: # 用户没有取消
|
||
# 更新单元格值
|
||
new_values = list(values)
|
||
new_values[5] = new_value
|
||
self.tree.item(item, values=new_values)
|
||
|
||
self.log_text.info(f"已更新负责人: {current_value} -> {new_value}")
|
||
|
||
def _select_all(self, check: bool = True):
|
||
"""全选或取消全选
|
||
|
||
Args:
|
||
check: True 为全选,False 为取消全选
|
||
"""
|
||
self.tree.check_all(check)
|
||
action = "全选" if check else "取消全选"
|
||
self.log_text.info(f"{action}完成")
|
||
|
||
def start_validation(self):
|
||
"""开始校验"""
|
||
# 验证输入
|
||
output_file = self.output_file_selector.get()
|
||
if not output_file:
|
||
messagebox.showerror("错误", "请指定输出文件路径")
|
||
return
|
||
|
||
mode = self.source_mode.get()
|
||
input_file = None
|
||
production_id_file = None
|
||
|
||
# 根据模式验证输入文件
|
||
if mode == "database_full":
|
||
# 无需输入文件
|
||
pass
|
||
elif mode == "database_filtered":
|
||
production_id_file = self.db_filtered_production_id_selector.get()
|
||
if not production_id_file:
|
||
messagebox.showerror("错误", "请选择 ProductionID 文件")
|
||
return
|
||
if not os.path.exists(production_id_file):
|
||
messagebox.showerror("错误", f"文件不存在:{production_id_file}")
|
||
return
|
||
else:
|
||
messagebox.showerror("错误", f"未知的校验模式: {mode}")
|
||
return
|
||
|
||
# 确保输出目录存在
|
||
output_dir = os.path.dirname(output_file)
|
||
if output_dir and not os.path.exists(output_dir):
|
||
os.makedirs(output_dir, exist_ok=True)
|
||
|
||
# 更新 UI 状态
|
||
self.validating = True
|
||
self.start_button.config(state=tk.DISABLED)
|
||
self.confirm_delete_button.config(state=tk.DISABLED)
|
||
self.select_all_button.config(state=tk.DISABLED)
|
||
self.deselect_all_button.config(state=tk.DISABLED)
|
||
self.log_text.clear()
|
||
self.log_text.info(f"开始物料校验(模式: {mode})...")
|
||
|
||
# 清空结果表格
|
||
for item in self.tree.get_children():
|
||
self.tree.delete(item)
|
||
|
||
# 在后台线程中执行校验
|
||
validation_thread = threading.Thread(
|
||
target=self._validation_worker_enhanced,
|
||
args=(mode, input_file, production_id_file, output_file),
|
||
daemon=True,
|
||
)
|
||
validation_thread.start()
|
||
|
||
def _validation_worker_enhanced(
|
||
self, mode: str, input_file: str, production_id_file: str, output_file: str
|
||
):
|
||
"""增强的校验工作线程(使用完整记录模式)"""
|
||
try:
|
||
# 导入校验器
|
||
from utils.material_status_validator import MaterialStatusValidator
|
||
|
||
# 创建校验器实例
|
||
validator = MaterialStatusValidator(
|
||
username=self.config.get("erp.username"),
|
||
password=self.config.get("erp.password"),
|
||
headless=self.config.get("erp.headless", True),
|
||
verbose=True,
|
||
)
|
||
|
||
# 捕获 stdout 输出
|
||
captured_output = StringIO()
|
||
|
||
# 根据模式执行增强校验
|
||
with redirect_stdout(captured_output):
|
||
if mode == "database_full":
|
||
result_file, results = validator.validate_from_database_enhanced(
|
||
full_table=True,
|
||
output_file=output_file
|
||
)
|
||
elif mode == "database_filtered":
|
||
result_file, results = validator.validate_from_database_enhanced(
|
||
production_id_file=production_id_file,
|
||
full_table=False,
|
||
output_file=output_file
|
||
)
|
||
else:
|
||
raise ValueError(f"未知的校验模式: {mode}")
|
||
|
||
# 缓存结果记录
|
||
self.material_records_cache = results
|
||
|
||
# 获取捕获的输出并显示到日志
|
||
output_text = captured_output.getvalue()
|
||
if output_text:
|
||
for line in output_text.split("\n"):
|
||
if line.strip():
|
||
self._update_log(line, "INFO")
|
||
|
||
if result_file and results:
|
||
self._update_log(f"校验完成,结果已保存到:{output_file}", "SUCCESS")
|
||
# 加载结果显示(带删除状态)
|
||
self._load_results_with_deletion_status(result_file)
|
||
else:
|
||
self._update_log("校验失败", "ERROR")
|
||
|
||
except Exception as e:
|
||
self._update_log(f"校验过程中发生错误:{str(e)}", "ERROR")
|
||
import traceback
|
||
self._update_log(traceback.format_exc(), "ERROR")
|
||
finally:
|
||
# 更新 UI 状态
|
||
self.after(0, self._validation_complete)
|
||
|
||
def _validation_complete(self):
|
||
"""校验完成后的 UI 更新"""
|
||
self.validating = False
|
||
self.start_button.config(state=tk.NORMAL)
|
||
self.select_all_button.config(state=tk.NORMAL)
|
||
self.deselect_all_button.config(state=tk.NORMAL)
|
||
self.confirm_delete_button.config(state=tk.NORMAL)
|
||
|
||
def _load_results_with_deletion_status(self, file_path: str):
|
||
"""加载结果并设置 checkbox 选中状态"""
|
||
try:
|
||
df = pd.read_excel(file_path)
|
||
|
||
# 在主线程中更新表格
|
||
def update_table():
|
||
# 从数据库获取已标记删除的 MaterialCode -> ManagerName 映射
|
||
from db.materials_to_be_deleted_records_dao import MaterialsToBeDeletedDAO
|
||
record_dao = MaterialsToBeDeletedDAO()
|
||
marked_records = record_dao.get_all_records()
|
||
|
||
# Build dictionary: MaterialCode -> ManagerName
|
||
marked_codes_dict = {
|
||
r['MaterialCode']: r['ManagerName']
|
||
for r in marked_records
|
||
if r.get('MaterialCode') and r.get('ManagerName')
|
||
}
|
||
|
||
for _, row in df.iterrows():
|
||
material_code = str(row.get("材料代码", ""))
|
||
|
||
# 优先使用 MaterialsToBeDeleted 中的 ManagerName
|
||
manager_name = marked_codes_dict.get(material_code)
|
||
is_marked = manager_name is not None
|
||
|
||
# 如果没有在 MaterialsToBeDeleted 中找到,使用 Excel 中的负责人
|
||
if not manager_name:
|
||
manager_name = str(row.get("负责人", ""))
|
||
|
||
# 准备显示值
|
||
checkbox = "☑" if is_marked else "☐"
|
||
values = (
|
||
checkbox,
|
||
str(row.get("材料名称", "")),
|
||
material_code,
|
||
str(row.get("规格", "")),
|
||
str(row.get("型号", "")),
|
||
manager_name,
|
||
)
|
||
|
||
item = self.tree.insert("", tk.END, values=values)
|
||
|
||
# 设置 checkbox 状态
|
||
if is_marked:
|
||
self.tree.set_checked(item, True)
|
||
|
||
if len(df) > 0:
|
||
self.export_button.config(state=tk.NORMAL)
|
||
self._update_log(f"已加载 {len(df)} 条结果(其中 {len(marked_codes_dict)} 条已标记删除)", "INFO")
|
||
|
||
self.after(0, update_table)
|
||
|
||
except Exception as e:
|
||
self._update_log(f"加载结果失败:{str(e)}", "ERROR")
|
||
|
||
def confirm_deletion(self):
|
||
"""确认删除主流程"""
|
||
# 获取选中的 item
|
||
checked_items = self.tree.get_checked_items()
|
||
|
||
if not checked_items:
|
||
messagebox.showwarning("警告", "请先选择要删除的记录")
|
||
return
|
||
|
||
# 收集数据
|
||
materials = []
|
||
missing_manager = []
|
||
|
||
for item in checked_items:
|
||
values = self.tree.item(item, "values")
|
||
material_code = values[2] if len(values) > 2 else ""
|
||
manager_name = values[5] if len(values) > 5 else ""
|
||
|
||
if not material_code or not material_code.strip():
|
||
continue
|
||
|
||
if not manager_name or not manager_name.strip():
|
||
missing_manager.append(material_code)
|
||
else:
|
||
materials.append({
|
||
"material_code": material_code.strip(),
|
||
"manager_name": manager_name.strip()
|
||
})
|
||
|
||
# 验证负责人
|
||
if missing_manager:
|
||
msg = f"以下选中的记录缺少负责人信息,无法删除:\n\n" + \
|
||
"\n".join(missing_manager[:10]) # 最多显示10个
|
||
if len(missing_manager) > 10:
|
||
msg += f"\n... 共 {len(missing_manager)} 条"
|
||
messagebox.showwarning("警告", msg)
|
||
return
|
||
|
||
if not materials:
|
||
messagebox.showwarning("警告", "没有有效的记录可删除")
|
||
return
|
||
|
||
# 显示确认对话框
|
||
confirm_msg = f"确认要将 {len(materials)} 条记录写入删除表吗?\n\n"
|
||
confirm_msg += "这些记录将被标记为待删除状态。"
|
||
|
||
if not messagebox.askyesno("确认删除", confirm_msg):
|
||
return
|
||
|
||
# 执行删除操作(后台线程)
|
||
self._update_log(f"开始处理 {len(materials)} 条记录...", "INFO")
|
||
|
||
deletion_thread = threading.Thread(
|
||
target=self._execute_deletion_in_background,
|
||
args=(materials,),
|
||
daemon=True,
|
||
)
|
||
deletion_thread.start()
|
||
|
||
def _execute_deletion_in_background(self, materials: list):
|
||
"""后台线程执行删除操作"""
|
||
try:
|
||
from db.materials_to_be_deleted_records_dao import MaterialsToBeDeletedDAO
|
||
|
||
dao = MaterialsToBeDeletedDAO()
|
||
stats = dao.upsert_batch(materials)
|
||
|
||
self.after(0, lambda: self._deletion_complete(stats))
|
||
|
||
except Exception as e:
|
||
self.after(0, lambda: self._update_log(f"删除操作失败:{str(e)}", "ERROR"))
|
||
|
||
def _deletion_complete(self, stats: dict):
|
||
"""删除完成回调,更新 UI"""
|
||
total = stats.get('total', 0)
|
||
success = stats.get('success', 0)
|
||
failed = stats.get('failed', 0)
|
||
|
||
msg = f"删除操作完成!\n\n总计:{total} 条\n成功:{success} 条\n失败:{failed} 条"
|
||
|
||
if failed > 0:
|
||
messagebox.showwarning("完成", msg)
|
||
self._update_log(msg, "WARNING")
|
||
else:
|
||
messagebox.showinfo("成功", msg)
|
||
self._update_log(msg, "SUCCESS")
|
||
|
||
def _update_log(self, message: str, level: str = "INFO"):
|
||
"""线程安全的日志更新"""
|
||
|
||
def update():
|
||
if level == "INFO":
|
||
self.log_text.info(message)
|
||
elif level == "SUCCESS":
|
||
self.log_text.success(message)
|
||
elif level == "WARNING":
|
||
self.log_text.warning(message)
|
||
elif level == "ERROR":
|
||
self.log_text.error(message)
|
||
|
||
self.after(0, update)
|
||
|
||
def export_results(self):
|
||
"""导出结果到 Excel"""
|
||
output_file = self.output_file_selector.get()
|
||
|
||
if not output_file:
|
||
output_file = filedialog.asksaveasfilename(
|
||
title="保存结果",
|
||
defaultextension=".xlsx",
|
||
filetypes=[("Excel 文件", "*.xlsx"), ("所有文件", "*.*")],
|
||
)
|
||
|
||
if not output_file:
|
||
return
|
||
|
||
try:
|
||
# 收集表格数据
|
||
data = []
|
||
for item in self.tree.get_children():
|
||
values = self.tree.item(item)["values"]
|
||
# 转换 checkbox 字符为布尔值
|
||
checkbox = values[0] if values else "☐"
|
||
is_checked = checkbox == "☑"
|
||
|
||
data.append([
|
||
"是" if is_checked else "否", # 选择状态
|
||
values[1] if len(values) > 1 else "", # 材料名称
|
||
values[2] if len(values) > 2 else "", # 材料代码
|
||
values[3] if len(values) > 3 else "", # 规格
|
||
values[4] if len(values) > 4 else "", # 型号
|
||
values[5] if len(values) > 5 else "", # 负责人
|
||
])
|
||
|
||
if not data:
|
||
messagebox.showwarning("警告", "没有数据可导出")
|
||
return
|
||
|
||
# 创建 DataFrame 并保存
|
||
df = pd.DataFrame(
|
||
data, columns=["选择", "材料名称", "材料代码", "规格", "型号", "负责人"]
|
||
)
|
||
df.to_excel(output_file, index=False)
|
||
|
||
messagebox.showinfo("成功", f"结果已导出到:{output_file}")
|
||
self.log_text.success(f"结果已导出到:{output_file}")
|
||
|
||
except Exception as e:
|
||
messagebox.showerror("错误", f"导出失败:{str(e)}")
|
||
self.log_text.error(f"导出失败:{str(e)}")
|