feat: add material type management dialog with checkbox filter

- Add MaterialTypeManagementDialog for managing MaterialsTypeToBeDeleted table
- Implement checkbox-based filtering by manager name with dynamic manager list
- Support CRUD operations: create, read, update, delete with local caching
- Add "类型管理" button in material validation tab to open the dialog
- Include select all/deselect all functionality for manager filters
- Track pending changes and show unsaved changes warning

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Misaka_Company
2026-02-09 13:17:25 +08:00
parent 13d6c20383
commit 2fb44a2a9b
2 changed files with 717 additions and 0 deletions

View File

@@ -0,0 +1,704 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
物料类型管理对话框
提供对 MaterialsTypeToBeDeleted 表的管理功能:
- 查看所有记录
- 按负责人筛选
- 新增/删除/编辑记录
- 保存更改到数据库
"""
import tkinter as tk
from tkinter import ttk, messagebox, simpledialog
from typing import List, Dict, Any, Optional, Tuple
class MaterialTypeManagementDialog(tk.Toplevel):
"""物料类型管理对话框
管理 MaterialsTypeToBeDeleted 表的 CRUD 操作。
"""
def __init__(self, parent, title: str = "类型管理"):
"""初始化对话框
Args:
parent: 父窗口
title: 窗口标题
"""
super().__init__(parent)
self.title(title)
self.geometry("900x600")
# 使窗口模态
self.transient(parent)
self.grab_set()
# 数据缓存
self.original_data: List[Dict[str, Any]] = [] # 原始数据(从数据库加载)
self.current_data: List[Dict[str, Any]] = [] # 当前显示的数据(可能已筛选)
self.pending_changes: Dict[str, List[Dict[str, Any]]] = {
'insert': [], # 待插入的记录
'delete': [], # 待删除的记录
'update': [] # 待更新的记录 (old, new)
}
# 筛选相关
self.managers: List[str] = [] # 所有的负责人列表
self.manager_checkboxes: Dict[str, tk.BooleanVar] = {} # 负责人 -> Checkbox变量
self.select_all_var: tk.BooleanVar = tk.BooleanVar(value=True) # 全选复选框变量
# 创建界面
self._create_widgets()
# 加载数据
self._load_data()
# 等待窗口关闭
self.wait_window()
def _create_widgets(self):
"""创建界面组件"""
# 主容器
main_container = ttk.Frame(self, padding=10)
main_container.pack(fill=tk.BOTH, expand=True)
# 顶部:筛选区域
filter_frame = ttk.LabelFrame(main_container, text="筛选(按负责人)", padding=10)
filter_frame.pack(fill=tk.X, pady=(0, 10))
# 创建复选框容器使用Canvas实现滚动
canvas_container = ttk.Frame(filter_frame)
canvas_container.pack(fill=tk.BOTH, expand=True)
# Canvas和滚动条
self.filter_canvas = tk.Canvas(canvas_container, height=80)
scrollbar = ttk.Scrollbar(canvas_container, orient="vertical", command=self.filter_canvas.yview)
self.filter_frame = ttk.Frame(self.filter_canvas)
self.filter_frame.bind(
"<Configure>",
lambda e: self.filter_canvas.configure(scrollregion=self.filter_canvas.bbox("all"))
)
self.filter_canvas.create_window((0, 0), window=self.filter_frame, anchor="nw")
self.filter_canvas.configure(yscrollcommand=scrollbar.set)
# 布局
self.filter_canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
# 鼠标滚轮支持
def _on_mousewheel(event):
self.filter_canvas.yview_scroll(int(-1*(event.delta/120)), "units")
self.filter_canvas.bind("<MouseWheel>", _on_mousewheel) # Windows
# 复选框将在加载数据后动态创建
self._create_filter_checkboxes()
# 快捷按钮
button_frame = ttk.Frame(filter_frame)
button_frame.pack(fill=tk.X, pady=(5, 0))
ttk.Button(
button_frame,
text="全选",
command=self._select_all_managers
).pack(side=tk.LEFT, padx=5)
ttk.Button(
button_frame,
text="取消全选",
command=self._deselect_all_managers
).pack(side=tk.LEFT, padx=5)
# 中部:数据表格
table_frame = ttk.LabelFrame(main_container, text="数据列表", padding=10)
table_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
# 创建 Treeview
columns = ("material_name", "manager_name")
self.tree = ttk.Treeview(
table_frame,
columns=columns,
show="headings",
selectmode="extended"
)
# 设置列标题和宽度
self.tree.heading("material_name", text="物料名称")
self.tree.heading("manager_name", text="负责人")
self.tree.column("material_name", width=400)
self.tree.column("manager_name", width=200)
# 添加滚动条
scrollbar_y = ttk.Scrollbar(table_frame, orient=tk.VERTICAL, command=self.tree.yview)
scrollbar_x = ttk.Scrollbar(table_frame, 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")
table_frame.rowconfigure(0, weight=1)
table_frame.columnconfigure(0, weight=1)
# 绑定双击事件用于编辑
self.tree.bind("<Double-1>", self._on_item_double_click)
# 底部:操作按钮
button_frame = ttk.Frame(main_container)
button_frame.pack(fill=tk.X)
ttk.Button(
button_frame,
text="新增",
command=self._add_record
).pack(side=tk.LEFT, padx=5)
ttk.Button(
button_frame,
text="删除",
command=self._delete_records
).pack(side=tk.LEFT, padx=5)
ttk.Button(
button_frame,
text="编辑",
command=self._edit_selected_record
).pack(side=tk.LEFT, padx=5)
ttk.Button(
button_frame,
text="刷新",
command=self._load_data
).pack(side=tk.LEFT, padx=5)
# 待保存提示
self.status_label = ttk.Label(button_frame, text="")
self.status_label.pack(side=tk.LEFT, padx=20)
# 右侧按钮
right_frame = ttk.Frame(button_frame)
right_frame.pack(side=tk.RIGHT)
ttk.Button(
right_frame,
text="保存",
command=self._save_changes
).pack(side=tk.LEFT, padx=5)
ttk.Button(
right_frame,
text="关闭",
command=self._close_dialog
).pack(side=tk.LEFT, padx=5)
def _create_filter_checkboxes(self):
"""创建筛选复选框(在数据加载后调用)"""
# 清空现有复选框
for widget in self.filter_frame.winfo_children():
widget.destroy()
self.manager_checkboxes.clear()
# 获取所有负责人并排序
from db.materials_to_be_deleted_dao import MaterialsTypeToBeDeletedDAO
dao = MaterialsTypeToBeDeletedDAO()
self.managers = dao.get_managers()
# 如果没有数据,稍后会在 _load_data 中重新创建
if not self.managers:
ttk.Label(self.filter_frame, text="正在加载数据...").pack(anchor="w")
return
# 计算列数每列最多显示4个负责人
items_per_column = 6
num_columns = max(1, (len(self.managers) + items_per_column - 1) // items_per_column)
# 创建全选复选框
select_all_cb = ttk.Checkbutton(
self.filter_frame,
text="全选",
variable=self.select_all_var,
command=self._on_select_all_toggle
)
select_all_cb.grid(row=0, column=0, sticky="w", padx=5, pady=2)
# 为每个负责人创建复选框
for idx, manager in enumerate(self.managers):
var = tk.BooleanVar(value=True)
self.manager_checkboxes[manager] = var
row = (idx // items_per_column) + 1
col = idx % items_per_column
cb = ttk.Checkbutton(
self.filter_frame,
text=manager,
variable=var,
command=self._on_manager_checkbox_change
)
cb.grid(row=row, column=col, sticky="w", padx=5, pady=2)
def _on_select_all_toggle(self):
"""全选复选框状态改变"""
select_all = self.select_all_var.get()
for var in self.manager_checkboxes.values():
var.set(select_all)
self._apply_filter()
def _on_manager_checkbox_change(self):
"""单个负责人复选框状态改变"""
# 检查是否所有负责人都被选中
all_selected = all(var.get() for var in self.manager_checkboxes.values())
self.select_all_var.set(all_selected)
self._apply_filter()
def _select_all_managers(self):
"""全选所有负责人"""
self.select_all_var.set(True)
for var in self.manager_checkboxes.values():
var.set(True)
self._apply_filter()
def _deselect_all_managers(self):
"""取消全选"""
self.select_all_var.set(False)
for var in self.manager_checkboxes.values():
var.set(False)
self._apply_filter()
def _get_selected_managers(self) -> List[str]:
"""获取选中的负责人列表"""
return [
manager for manager, var in self.manager_checkboxes.items()
if var.get()
]
def _load_data(self):
"""从数据库加载数据"""
try:
from db.materials_to_be_deleted_dao import MaterialsTypeToBeDeletedDAO
dao = MaterialsTypeToBeDeletedDAO()
self.original_data = dao.get_all_materials()
self.current_data = list(self.original_data)
# 清空待保存的更改
self.pending_changes = {'insert': [], 'delete': [], 'update': []}
# 重新创建筛选复选框(负责人列表可能变化)
self._create_filter_checkboxes()
# 刷新显示
self._apply_filter() # 使用 _apply_filter 而不是 _refresh_tree
self._update_status()
except Exception as e:
messagebox.showerror("错误", f"加载数据失败:{str(e)}")
def _refresh_tree(self):
"""刷新表格显示"""
# 清空表格
for item in self.tree.get_children():
self.tree.delete(item)
# 填充数据
for record in self.current_data:
self.tree.insert(
"",
tk.END,
values=(
record.get('MaterialName', ''),
record.get('ManagerName', '')
)
)
def _apply_filter(self):
"""应用筛选条件(基于选中的负责人复选框)"""
selected_managers = self._get_selected_managers()
if not selected_managers:
# 没有选中任何负责人,显示空表格
self.current_data = []
else:
# 获取当前有效数据(原始数据减去已删除的,加上新插入的)
deleted_set = {
(r['MaterialName'], r['ManagerName'])
for r in self.pending_changes['delete']
}
inserted = [
r for r in self.pending_changes['insert']
if (r['MaterialName'], r['ManagerName']) not in deleted_set
]
base_data = [
r for r in self.original_data
if (r['MaterialName'], r['ManagerName']) not in deleted_set
] + inserted
# 按选中的负责人筛选
self.current_data = [
r for r in base_data
if r.get('ManagerName') in selected_managers
]
self._refresh_tree()
def _add_record(self):
"""添加新记录"""
# 弹出输入对话框
dialog = tk.Toplevel(self)
dialog.title("新增记录")
dialog.geometry("400x200")
dialog.transient(self)
dialog.grab_set()
# 物料名称
ttk.Label(dialog, text="物料名称:").pack(pady=5)
material_entry = ttk.Entry(dialog, width=40)
material_entry.pack(pady=5)
# 负责人
ttk.Label(dialog, text="负责人:").pack(pady=5)
manager_entry = ttk.Entry(dialog, width=40)
manager_entry.pack(pady=5)
# 按钮框架
button_frame = ttk.Frame(dialog)
button_frame.pack(pady=20)
result = {'material': '', 'manager': '', 'confirmed': False}
def on_confirm():
material = material_entry.get().strip()
manager = manager_entry.get().strip()
if not material:
messagebox.showwarning("警告", "请输入物料名称")
return
if not manager:
messagebox.showwarning("警告", "请输入负责人")
return
result['material'] = material
result['manager'] = manager
result['confirmed'] = True
dialog.destroy()
def on_cancel():
dialog.destroy()
ttk.Button(button_frame, text="确定", command=on_confirm).pack(side=tk.LEFT, padx=5)
ttk.Button(button_frame, text="取消", command=on_cancel).pack(side=tk.LEFT, padx=5)
# 设置焦点到第一个输入框
material_entry.focus_set()
# 等待对话框关闭
dialog.wait_window()
if result['confirmed']:
# 检查是否已存在
for record in self.current_data:
if (record['MaterialName'] == result['material'] and
record['ManagerName'] == result['manager']):
messagebox.showinfo("提示", "该记录已存在")
return
# 添加到待插入列表
new_record = {
'MaterialName': result['material'],
'ManagerName': result['manager']
}
self.pending_changes['insert'].append(new_record)
# 立即显示在表格中
self.current_data.append(new_record)
self._refresh_tree()
self._update_status()
def _delete_records(self):
"""删除选中的记录"""
selection = self.tree.selection()
if not selection:
messagebox.showwarning("警告", "请先选择要删除的记录")
return
if not messagebox.askyesno("确认", f"确定要删除选中的 {len(selection)} 条记录吗?"):
return
indices_to_delete = []
for item in selection:
values = self.tree.item(item, 'values')
material_name = values[0]
manager_name = values[1]
# 查找在 current_data 中的索引
for idx, record in enumerate(self.current_data):
if (record['MaterialName'] == material_name and
record['ManagerName'] == manager_name):
indices_to_delete.append(idx)
# 检查是否是新插入的记录
is_new_insert = any(
r['MaterialName'] == material_name and r['ManagerName'] == manager_name
for r in self.pending_changes['insert']
)
if is_new_insert:
# 从待插入列表中移除
self.pending_changes['insert'] = [
r for r in self.pending_changes['insert']
if not (r['MaterialName'] == material_name and
r['ManagerName'] == manager_name)
]
else:
# 添加到待删除列表
self.pending_changes['delete'].append({
'MaterialName': material_name,
'ManagerName': manager_name
})
break
# 从 current_data 中删除(倒序删除,避免索引问题)
for idx in sorted(indices_to_delete, reverse=True):
del self.current_data[idx]
self._refresh_tree()
self._update_status()
def _edit_selected_record(self):
"""编辑选中的记录"""
selection = self.tree.selection()
if not selection:
messagebox.showwarning("警告", "请先选择要编辑的记录")
return
if len(selection) > 1:
messagebox.showwarning("警告", "请只选择一条记录进行编辑")
return
item = selection[0]
values = self.tree.item(item, 'values')
old_material = values[0]
old_manager = values[1]
# 弹出编辑对话框
dialog = tk.Toplevel(self)
dialog.title("编辑记录")
dialog.geometry("400x200")
dialog.transient(self)
dialog.grab_set()
# 物料名称
ttk.Label(dialog, text="物料名称:").pack(pady=5)
material_entry = ttk.Entry(dialog, width=40)
material_entry.insert(0, old_material)
material_entry.pack(pady=5)
# 负责人
ttk.Label(dialog, text="负责人:").pack(pady=5)
manager_entry = ttk.Entry(dialog, width=40)
manager_entry.insert(0, old_manager)
manager_entry.pack(pady=5)
# 按钮框架
button_frame = ttk.Frame(dialog)
button_frame.pack(pady=20)
result = {'material': old_material, 'manager': old_manager, 'confirmed': False}
def on_confirm():
material = material_entry.get().strip()
manager = manager_entry.get().strip()
if not material:
messagebox.showwarning("警告", "物料名称不能为空")
return
if not manager:
messagebox.showwarning("警告", "负责人不能为空")
return
result['material'] = material
result['manager'] = manager
result['confirmed'] = True
dialog.destroy()
def on_cancel():
dialog.destroy()
ttk.Button(button_frame, text="确定", command=on_confirm).pack(side=tk.LEFT, padx=5)
ttk.Button(button_frame, text="取消", command=on_cancel).pack(side=tk.LEFT, padx=5)
# 等待对话框关闭
dialog.wait_window()
if result['confirmed']:
new_material = result['material']
new_manager = result['manager']
# 如果值没有变化,不处理
if new_material == old_material and new_manager == old_manager:
return
# 更新 current_data
for record in self.current_data:
if (record['MaterialName'] == old_material and
record['ManagerName'] == old_manager):
record['MaterialName'] = new_material
record['ManagerName'] = new_manager
break
# 添加到待更新列表
self.pending_changes['update'].append({
'old': {'MaterialName': old_material, 'ManagerName': old_manager},
'new': {'MaterialName': new_material, 'ManagerName': new_manager}
})
self._refresh_tree()
self._update_status()
def _on_item_double_click(self, event):
"""处理双击事件,编辑记录"""
self._edit_selected_record()
def _save_changes(self):
"""保存更改到数据库"""
total_changes = (
len(self.pending_changes['insert']) +
len(self.pending_changes['delete']) +
len(self.pending_changes['update'])
)
if total_changes == 0:
messagebox.showinfo("提示", "没有需要保存的更改")
return
# 确认保存
msg_parts = []
if self.pending_changes['insert']:
msg_parts.append(f"新增 {len(self.pending_changes['insert'])}")
if self.pending_changes['delete']:
msg_parts.append(f"删除 {len(self.pending_changes['delete'])}")
if self.pending_changes['update']:
msg_parts.append(f"更新 {len(self.pending_changes['update'])}")
if not messagebox.askyesno(
"确认保存",
"确定要将以下更改保存到数据库吗?\n\n" + "\n".join(msg_parts)
):
return
try:
from db.materials_to_be_deleted_dao import MaterialsTypeToBeDeletedDAO
dao = MaterialsTypeToBeDeletedDAO()
stats = {
'insert_success': 0,
'insert_failed': 0,
'delete_success': 0,
'delete_failed': 0,
'update_success': 0,
'update_failed': 0
}
# 执行插入
for record in self.pending_changes['insert']:
if dao.insert_material(record['MaterialName'], record['ManagerName']):
stats['insert_success'] += 1
else:
stats['insert_failed'] += 1
# 执行删除
for record in self.pending_changes['delete']:
if dao.delete_material(record['MaterialName'], record['ManagerName']):
stats['delete_success'] += 1
else:
stats['delete_failed'] += 1
# 执行更新
for update in self.pending_changes['update']:
old = update['old']
new = update['new']
# 先删除旧记录,再插入新记录
if dao.delete_material(old['MaterialName'], old['ManagerName']):
if dao.insert_material(new['MaterialName'], new['ManagerName']):
stats['update_success'] += 1
else:
# 删除成功但插入失败,尝试回滚
dao.insert_material(old['MaterialName'], old['ManagerName'])
stats['update_failed'] += 1
else:
stats['update_failed'] += 1
# 显示结果
result_parts = []
if stats['insert_success'] > 0:
result_parts.append(f"新增成功:{stats['insert_success']}")
if stats['insert_failed'] > 0:
result_parts.append(f"新增失败:{stats['insert_failed']}")
if stats['delete_success'] > 0:
result_parts.append(f"删除成功:{stats['delete_success']}")
if stats['delete_failed'] > 0:
result_parts.append(f"删除失败:{stats['delete_failed']}")
if stats['update_success'] > 0:
result_parts.append(f"更新成功:{stats['update_success']}")
if stats['update_failed'] > 0:
result_parts.append(f"更新失败:{stats['update_failed']}")
result_msg = "保存完成!\n\n" + "\n".join(result_parts)
if (stats['insert_failed'] + stats['delete_failed'] + stats['update_failed']) > 0:
messagebox.showwarning("完成(部分失败)", result_msg)
else:
messagebox.showinfo("成功", result_msg)
# 重新加载数据
self._load_data()
except Exception as e:
messagebox.showerror("错误", f"保存失败:{str(e)}")
def _update_status(self):
"""更新状态标签"""
total_changes = (
len(self.pending_changes['insert']) +
len(self.pending_changes['delete']) +
len(self.pending_changes['update'])
)
if total_changes > 0:
self.status_label.config(
text=f"{total_changes} 项待保存的更改",
foreground="red"
)
else:
self.status_label.config(text="")
def _close_dialog(self):
"""关闭对话框"""
# 检查是否有未保存的更改
total_changes = (
len(self.pending_changes['insert']) +
len(self.pending_changes['delete']) +
len(self.pending_changes['update'])
)
if total_changes > 0:
if not messagebox.askyesno(
"警告",
f"{total_changes} 项未保存的更改,确定要关闭吗?"
):
return
self.destroy()

View File

@@ -18,6 +18,7 @@ from io import StringIO
from contextlib import redirect_stdout
from gui.widgets import FileSelector, LogText
from gui.config_manager import ConfigManager
from gui.material_type_management_dialog import MaterialTypeManagementDialog
import pandas as pd
@@ -287,6 +288,14 @@ class MaterialValidationTab(ttk.Frame):
)
self.export_button.pack(side=tk.LEFT, padx=5)
# 类型管理按钮
self.type_management_button = ttk.Button(
button_frame,
text="类型管理",
command=self.open_type_management
)
self.type_management_button.pack(side=tk.LEFT, padx=5)
def _create_result_table(self, parent):
"""创建结果表格"""
# 新列定义:选择、材料名称、材料代码、规格、型号、负责人
@@ -802,3 +811,7 @@ class MaterialValidationTab(ttk.Frame):
except Exception as e:
messagebox.showerror("错误", f"导出失败:{str(e)}")
self.log_text.error(f"导出失败:{str(e)}")
def open_type_management(self):
"""打开类型管理窗口"""
dialog = MaterialTypeManagementDialog(self, title="物料类型管理")