diff --git a/gui/material_type_management_dialog.py b/gui/material_type_management_dialog.py index 3b17d83..5488339 100644 --- a/gui/material_type_management_dialog.py +++ b/gui/material_type_management_dialog.py @@ -5,22 +5,221 @@ 提供对 MaterialsTypeToBeDeleted 表的管理功能: - 查看所有记录 -- 按负责人筛选 -- 新增/删除/编辑记录 +- 按负责人筛选(复选框) +- 直接在表格中编辑(双击或按F2) +- 新增/删除记录(直接在表格中操作) - 保存更改到数据库 """ import tkinter as tk -from tkinter import ttk, messagebox, simpledialog -from typing import List, Dict, Any, Optional, Tuple +from tkinter import ttk, messagebox +from typing import List, Dict, Any, Optional + + +class EditableTreeview(ttk.Treeview): + """可编辑的 Treeview 组件 + + 支持双击单元格进行编辑,使用 Entry 组件覆盖在单元格上。 + """ + + def __init__(self, parent, on_edit_complete=None, on_edit_cancelled=None, **kwargs): + """初始化可编辑的 Treeview + + Args: + parent: 父容器 + on_edit_complete: 编辑完成回调 (item_id, column, old_value, new_value) + on_edit_cancelled: 编辑取消回调 (item_id, column, old_value) + **kwargs: 传递给 Treeview 的参数 + """ + super().__init__(parent, **kwargs) + self.on_edit_complete = on_edit_complete + self.on_edit_cancelled = on_edit_cancelled + + # 编辑状态 + self.editing_item = None + self.editing_column = None + self.edit_entry = None + + # 绑定事件 + self.bind("", self._on_double_click) + self.bind("", self._on_enter_key) + self.bind("", self._on_f2_key) + self.bind("", self._on_escape_key) + self.bind("", self._on_delete_key) + + def _on_double_click(self, event): + """处理双击事件,开始编辑""" + region = self.identify_region(event.x, event.y) + if region == "cell": + column = self.identify_column(event.x) + item = self.identify_row(event.y) + self.start_edit(item, column) + + def _on_f2_key(self, event): + """处理 F2 键,开始编辑选中单元格""" + selection = self.selection() + if selection: + item = selection[0] + # 编辑第二列(物料名称列) + self.start_edit(item, "#1") + + def _on_enter_key(self, event): + """处理 Enter 键,完成编辑并移动到下一行""" + if self.editing_item: + self._finish_edit() + # 尝试移动到下一行 + selection = self.selection() + if selection: + item = selection[0] + next_item = self.next(item) + if next_item: + self.selection_set(next_item) + self.focus(next_item) + + def _on_escape_key(self, event): + """处理 Escape 键,取消编辑""" + if self.editing_item: + self._cancel_edit() + + def _on_delete_key(self, event): + """处理 Delete 键,触发删除(由父组件处理)""" + pass # 父组件会处理删除逻辑 + + def start_edit(self, item: str, column: str): + """开始编辑单元格 + + Args: + item: Treeview item ID + column: 列标识(如 "#1", "#2") + """ + if not item or not column: + return + + # 检查 item 是否存在 + if not self.exists(item): + return + + # 如果正在编辑,先完成 + if self.editing_item: + self._finish_edit() + + # 获取单元格位置和当前值 + bbox_result = self.bbox(item, column) + if not bbox_result: + # 单元格不可见(在滚动区域外),先滚动到可见区域 + self.see(item) + bbox_result = self.bbox(item, column) + if not bbox_result: + # 仍然无法获取位置,放弃编辑 + return + + x, y, width, height = bbox_result + values = self.item(item, "values") + + # 根据列索引获取值 + column_index = int(column[1:]) - 1 + if column_index < 0 or column_index >= len(values): + return + + current_value = values[column_index] + + # 创建 Entry 组件 + self.edit_entry = ttk.Entry(self) + self.edit_entry.insert(0, current_value) + self.edit_entry.select_range(0, tk.END) + self.edit_entry.focus_set() + + # 定位 Entry 组件 + self.edit_entry.place(x=x, y=y, width=width, height=height) + + # 记录编辑状态 + self.editing_item = item + self.editing_column = column_index + + # 绑定事件 + self.edit_entry.bind("", self._on_entry_focus_out) + self.edit_entry.bind("", lambda e: self._finish_edit()) + self.edit_entry.bind("", lambda e: self._cancel_edit()) + + def _finish_edit(self): + """完成编辑""" + if not self.edit_entry or not self.editing_item: + return + + new_value = self.edit_entry.get() + values = self.item(self.editing_item, "values") + old_value = values[self.editing_column] if self.editing_column < len(values) else "" + + # 销毁 Entry(先销毁,防止重复触发) + entry = self.edit_entry + editing_item = self.editing_item + editing_column = self.editing_column + + self.edit_entry = None + self.editing_item = None + self.editing_column = None + + entry.destroy() + + # 调用回调 + try: + if self.on_edit_complete: + self.on_edit_complete(editing_item, editing_column, old_value, new_value) + except Exception as e: + # 回调出错,清除状态 + print(f"Error in on_edit_complete: {e}") + + def _cancel_edit(self): + """取消编辑""" + if not self.edit_entry or not self.editing_item: + return + + values = self.item(self.editing_item, "values") + old_value = values[self.editing_column] if self.editing_column < len(values) else "" + + # 销毁 Entry(先保存状态引用) + entry = self.edit_entry + editing_item = self.editing_item + editing_column = self.editing_column + + self.edit_entry = None + self.editing_item = None + self.editing_column = None + + entry.destroy() + + # 调用回调 + try: + if self.on_edit_cancelled: + self.on_edit_cancelled(editing_item, editing_column, old_value) + except Exception as e: + # 回调出错,状态已清除 + print(f"Error in on_edit_cancelled: {e}") + + def _on_entry_focus_out(self, event): + """Entry 失去焦点时完成编辑""" + # 延迟执行,避免在点击其他控件时触发 + self.after(100, self._finish_edit_if_focus_lost) + + def _finish_edit_if_focus_lost(self): + """如果焦点不在 Entry 上,完成编辑""" + if self.edit_entry and self.focus_get() != self.edit_entry: + self._finish_edit() class MaterialTypeManagementDialog(tk.Toplevel): """物料类型管理对话框 管理 MaterialsTypeToBeDeleted 表的 CRUD 操作。 + 支持直接在表格中编辑。 """ + # 行状态标记 + ROW_STATUS_UNCHANGED = "unchanged" + ROW_STATUS_NEW = "new" + ROW_STATUS_MODIFIED = "modified" + ROW_STATUS_DELETED = "deleted" + def __init__(self, parent, title: str = "类型管理"): """初始化对话框 @@ -30,7 +229,7 @@ class MaterialTypeManagementDialog(tk.Toplevel): """ super().__init__(parent) self.title(title) - self.geometry("900x600") + self.geometry("900x650") # 使窗口模态 self.transient(parent) @@ -38,17 +237,14 @@ class MaterialTypeManagementDialog(tk.Toplevel): # 数据缓存 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.current_data: List[Dict[str, Any]] = [] # 当前显示的数据 + self.row_status: Dict[str, str] = {} # key -> 行状态 + self.original_values: Dict[str, Tuple[str, str]] = {} # key -> (original_material, original_manager) 用于修改 # 筛选相关 - self.managers: List[str] = [] # 所有的负责人列表 - self.manager_checkboxes: Dict[str, tk.BooleanVar] = {} # 负责人 -> Checkbox变量 - self.select_all_var: tk.BooleanVar = tk.BooleanVar(value=True) # 全选复选框变量 + self.managers: List[str] = [] + self.manager_checkboxes: Dict[str, tk.BooleanVar] = {} + self.select_all_var: tk.BooleanVar = tk.BooleanVar(value=True) # 创建界面 self._create_widgets() @@ -69,8 +265,24 @@ class MaterialTypeManagementDialog(tk.Toplevel): 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) + self._create_filter_area(filter_frame) + + # 中部:数据表格 + table_frame = ttk.LabelFrame(main_container, text="数据列表(双击编辑,Delete删除,Insert新增)", padding=10) + table_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10)) + + self._create_table(table_frame) + + # 底部:操作按钮 + button_frame = ttk.Frame(main_container) + button_frame.pack(fill=tk.X) + + self._create_buttons(button_frame) + + def _create_filter_area(self, parent): + """创建筛选区域""" + # 创建复选框容器 + canvas_container = ttk.Frame(parent) canvas_container.pack(fill=tk.BOTH, expand=True) # Canvas和滚动条 @@ -80,7 +292,7 @@ class MaterialTypeManagementDialog(tk.Toplevel): self.filter_frame.bind( "", - lambda e: self.filter_canvas.configure(scrollregion=self.filter_canvas.bbox("all")) + lambda e: self.filter_canvas.configure(scrollregion=self.filter_frame.bbox("all")) ) self.filter_canvas.create_window((0, 0), window=self.filter_frame, anchor="nw") @@ -93,38 +305,25 @@ class MaterialTypeManagementDialog(tk.Toplevel): # 鼠标滚轮支持 def _on_mousewheel(event): self.filter_canvas.yview_scroll(int(-1*(event.delta/120)), "units") - self.filter_canvas.bind("", _on_mousewheel) # Windows - - # 复选框将在加载数据后动态创建 - self._create_filter_checkboxes() + self.filter_canvas.bind("", _on_mousewheel) # 快捷按钮 - button_frame = ttk.Frame(filter_frame) + button_frame = ttk.Frame(parent) 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._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) - 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 + def _create_table(self, parent): + """创建数据表格""" columns = ("material_name", "manager_name") - self.tree = ttk.Treeview( - table_frame, + self.tree = EditableTreeview( + parent, columns=columns, show="headings", - selectmode="extended" + selectmode="extended", + on_edit_complete=self._on_edit_complete, + on_edit_cancelled=self._on_edit_cancelled ) # 设置列标题和宽度 @@ -135,8 +334,8 @@ class MaterialTypeManagementDialog(tk.Toplevel): 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) + 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, @@ -148,91 +347,71 @@ class MaterialTypeManagementDialog(tk.Toplevel): 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) + parent.rowconfigure(0, weight=1) + parent.columnconfigure(0, weight=1) - # 绑定双击事件用于编辑 - self.tree.bind("", self._on_item_double_click) + # 绑定键盘快捷键 + self.tree.bind("", lambda e: self._add_new_row()) + self.tree.bind("", lambda e: self._delete_selected_rows()) - # 底部:操作按钮 - button_frame = ttk.Frame(main_container) - button_frame.pack(fill=tk.X) + # 右键菜单 + self.context_menu = tk.Menu(self.tree, tearoff=0) + self.context_menu.add_command(label="新增记录 (Insert)", command=self._add_new_row) + self.context_menu.add_command(label="编辑记录 (F2)", command=self._edit_selected_cell) + self.context_menu.add_separator() + self.context_menu.add_command(label="删除记录 (Delete)", command=self._delete_selected_rows) - ttk.Button( - button_frame, - text="新增", - command=self._add_record - ).pack(side=tk.LEFT, padx=5) + self.tree.bind("", self._show_context_menu) - 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) + def _create_buttons(self, parent): + """创建底部按钮""" + ttk.Button(parent, text="新增 (Insert)", command=self._add_new_row).pack(side=tk.LEFT, padx=5) + ttk.Button(parent, text="删除 (Delete)", command=self._delete_selected_rows).pack(side=tk.LEFT, padx=5) + ttk.Button(parent, text="刷新", command=self._load_data).pack(side=tk.LEFT, padx=5) # 待保存提示 - self.status_label = ttk.Label(button_frame, text="") + self.status_label = ttk.Label(parent, text="") self.status_label.pack(side=tk.LEFT, padx=20) # 右侧按钮 - right_frame = ttk.Frame(button_frame) + right_frame = ttk.Frame(parent) 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._save_changes).pack(side=tk.LEFT, padx=5) + ttk.Button(right_frame, text="关闭", command=self._close_dialog).pack(side=tk.LEFT, padx=5) - ttk.Button( - right_frame, - text="关闭", - command=self._close_dialog - ).pack(side=tk.LEFT, padx=5) + def _show_context_menu(self, event): + """显示右键菜单""" + item = self.tree.identify_row(event.y) + if item: + self.tree.selection_set(item) + self.context_menu.post(event.x_root, event.y_root) 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( + # 全选复选框 + 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) + ).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 @@ -240,13 +419,12 @@ class MaterialTypeManagementDialog(tk.Toplevel): row = (idx // items_per_column) + 1 col = idx % items_per_column - cb = ttk.Checkbutton( + 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) + ).grid(row=row, column=col, sticky="w", padx=5, pady=2) def _on_select_all_toggle(self): """全选复选框状态改变""" @@ -257,7 +435,6 @@ class MaterialTypeManagementDialog(tk.Toplevel): 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() @@ -291,29 +468,92 @@ class MaterialTypeManagementDialog(tk.Toplevel): dao = MaterialsTypeToBeDeletedDAO() self.original_data = dao.get_all_materials() self.current_data = list(self.original_data) + self.row_status = {} + self.original_values = {} - # 清空待保存的更改 - self.pending_changes = {'insert': [], 'delete': [], 'update': []} - - # 重新创建筛选复选框(负责人列表可能变化) + # 重新创建筛选复选框 self._create_filter_checkboxes() # 刷新显示 - self._apply_filter() # 使用 _apply_filter 而不是 _refresh_tree + self._apply_filter() self._update_status() except Exception as e: messagebox.showerror("错误", f"加载数据失败:{str(e)}") + def _apply_filter(self): + """应用筛选条件""" + selected_managers = self._get_selected_managers() + + if not selected_managers: + self.current_data = [] + else: + # 获取未删除的数据 + base_data = [ + r for r in self.original_data + if self.row_status.get(self._get_record_key(r), self.ROW_STATUS_UNCHANGED) != self.ROW_STATUS_DELETED + ] + + # 添加新增的记录 + new_records = [ + r for r, status in self.row_status.items() + if status == self.ROW_STATUS_NEW + ] + + # 从行状态中恢复新增记录的数据 + for item_id in list(self.row_status.keys()): + if self.row_status[item_id] == self.ROW_STATUS_NEW: + # 尝试从 tree 中获取数据 + try: + if self.tree.exists(item_id): + values = self.tree.item(item_id, "values") + new_records.append({ + 'MaterialName': values[0], + 'ManagerName': values[1] + }) + except: + pass + + # 合并数据并筛选 + all_data = base_data + new_records + self.current_data = [ + r for r in all_data + if r.get('ManagerName') in selected_managers + ] + + self._refresh_tree() + + def _get_record_key(self, record: Dict[str, Any]) -> str: + """获取记录的唯一键""" + return f"{record.get('MaterialName', '')}|{record.get('ManagerName', '')}" + def _refresh_tree(self): """刷新表格显示""" + # 保存当前正在编辑的行数据 + editing_data = {} + for key, status in self.row_status.items(): + if status == self.ROW_STATUS_NEW and key.startswith("temp:"): + item_id = key.split(":", 1)[1] + if self.tree.exists(item_id): + values = self.tree.item(item_id, "values") + if values[0] or values[1]: # 保存有数据的新增行 + editing_data[key] = values + + # 保存当前选中的行和滚动位置 + selected_items = self.tree.selection() + selected_data = [] + for item in selected_items: + if self.tree.exists(item): + values = self.tree.item(item, "values") + selected_data.append((values[0], values[1])) + # 清空表格 for item in self.tree.get_children(): self.tree.delete(item) # 填充数据 for record in self.current_data: - self.tree.insert( + item_id = self.tree.insert( "", tk.END, values=( @@ -322,262 +562,256 @@ class MaterialTypeManagementDialog(tk.Toplevel): ) ) - def _apply_filter(self): - """应用筛选条件(基于选中的负责人复选框)""" - selected_managers = self._get_selected_managers() + # 恢复行状态 + key = self._get_record_key(record) + if key in self.row_status: + status = self.row_status[key] + if status == self.ROW_STATUS_NEW: + self.tree.item(item_id, tags=('new',)) + elif status == self.ROW_STATUS_MODIFIED: + self.tree.item(item_id, tags=('modified',)) - 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 - ] + # 恢复正在编辑的新增行 + for temp_key, values in editing_data.items(): + item_id = temp_key.split(":", 1)[1] + new_item_id = self.tree.insert("", tk.END, values=values) + # 更新 key 映射 + if values[0] and values[1]: + new_key = self._get_record_key({'MaterialName': values[0], 'ManagerName': values[1]}) + self.row_status[new_key] = self.ROW_STATUS_NEW + self.tree.item(new_item_id, tags=('new',)) + else: + # 空行,保持临时 key + self.row_status[temp_key] = self.ROW_STATUS_NEW + self.row_status[new_key] = self.ROW_STATUS_NEW - base_data = [ - r for r in self.original_data - if (r['MaterialName'], r['ManagerName']) not in deleted_set - ] + inserted + # 配置标签样式 + self.tree.tag_configure('new', background='#e6f7e6') # 浅绿色 + self.tree.tag_configure('modified', background='#fff4e6') # 浅黄色 - # 按选中的负责人筛选 - self.current_data = [ - r for r in base_data - if r.get('ManagerName') in selected_managers - ] + # 恢复选中状态 + for material, manager in selected_data: + for item in self.tree.get_children(): + values = self.tree.item(item, "values") + if values[0] == material and values[1] == manager: + self.tree.selection_add(item) + break - self._refresh_tree() + def _add_new_row(self): + """添加新行""" + # 插入空行 + item_id = self.tree.insert("", tk.END, values=("", "")) - def _add_record(self): - """添加新记录""" - # 弹出输入对话框 - dialog = tk.Toplevel(self) - dialog.title("新增记录") - dialog.geometry("400x200") - dialog.transient(self) - dialog.grab_set() + # 使用临时 key 标记为新行(编辑后会更新为正确的 key) + temp_key = f"temp:{item_id}" + self.row_status[temp_key] = self.ROW_STATUS_NEW - # 物料名称 - ttk.Label(dialog, text="物料名称:").pack(pady=5) - material_entry = ttk.Entry(dialog, width=40) - material_entry.pack(pady=5) + # 设置标签 + self.tree.item(item_id, tags=('new',)) - # 负责人 - ttk.Label(dialog, text="负责人:").pack(pady=5) - manager_entry = ttk.Entry(dialog, width=40) - manager_entry.pack(pady=5) + # 选中并开始编辑第一个单元格 + self.tree.selection_set(item_id) + self.tree.focus(item_id) + self.tree.start_edit(item_id, "#1") - # 按钮框架 - button_frame = ttk.Frame(dialog) - button_frame.pack(pady=20) + self._update_status() - result = {'material': '', 'manager': '', 'confirmed': False} + def _edit_selected_cell(self): + """编辑选中的单元格""" + selection = self.tree.selection() + if selection: + item = selection[0] + self.tree.start_edit(item, "#1") - 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): - """删除选中的记录""" + def _delete_selected_rows(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') + 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) + key = self._get_record_key({'MaterialName': material_name, 'ManagerName': manager_name}) + temp_key = f"temp:{item}" - # 检查是否是新插入的记录 - is_new_insert = any( - r['MaterialName'] == material_name and r['ManagerName'] == manager_name - for r in self.pending_changes['insert'] - ) + # 如果是新增的行,直接移除 + if temp_key in self.row_status and self.row_status[temp_key] == self.ROW_STATUS_NEW: + del self.row_status[temp_key] + else: + # 标记为删除 + self.row_status[key] = self.ROW_STATUS_DELETED - 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 + self.tree.delete(item) - # 从 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 + def _on_edit_complete(self, item_id: str, column: int, old_value: str, new_value: str): + """编辑完成回调""" + new_value = new_value.strip() - if len(selection) > 1: - messagebox.showwarning("警告", "请只选择一条记录进行编辑") - return + # 获取编辑前的完整值 + old_values = list(self.tree.item(item_id, "values")) - item = selection[0] - values = self.tree.item(item, 'values') - old_material = values[0] - old_manager = values[1] + # 计算新的完整值 + new_values = list(old_values) + new_values[column] = new_value - # 弹出编辑对话框 - dialog = tk.Toplevel(self) - dialog.title("编辑记录") - dialog.geometry("400x200") - dialog.transient(self) - dialog.grab_set() + new_material = new_values[0] + new_manager = new_values[1] - # 物料名称 - 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) + # 检查是否是新增的行 + temp_key = f"temp:{item_id}" + is_new_row = temp_key in self.row_status and self.row_status[temp_key] == self.ROW_STATUS_NEW - # 负责人 - 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() + # 对于新增行,如果只输入了部分字段,允许继续 + if is_new_row: + # 更新表格显示 + self.tree.item(item_id, values=new_values) + # 检查是否已输入完整数据 + if new_material and new_manager: + # 输入完整,更新 key + new_key = self._get_record_key({'MaterialName': new_material, 'ManagerName': new_manager}) + # 检查重复 + for other_item in self.tree.get_children(): + if other_item == item_id: + continue + other_values = self.tree.item(other_item, "values") + if other_values[0] == new_material and other_values[1] == new_manager: + messagebox.showwarning("警告", "该记录已存在") + self.tree.item(item_id, values=("", "")) + self._update_status() + return + # 从临时 key 更新为实际 key + del self.row_status[temp_key] + self.row_status[new_key] = self.ROW_STATUS_NEW + # 更新状态提示(无论是否输入完整都更新) self._update_status() + return - def _on_item_double_click(self, event): - """处理双击事件,编辑记录""" - self._edit_selected_record() + # 对于现有行的编辑,验证输入 + if not new_value: + messagebox.showwarning("警告", "物料名称和负责人不能为空") + # 恢复原值 + self.tree.item(item_id, values=old_values) + return + + # 检查数据完整性 + if not new_material or not new_manager: + self.tree.item(item_id, values=new_values) + return + + new_key = self._get_record_key({'MaterialName': new_material, 'ManagerName': new_manager}) + + # 检查是否重复(除了自己) + for other_item in self.tree.get_children(): + if other_item == item_id: + continue + other_values = self.tree.item(other_item, "values") + if other_values[0] == new_material and other_values[1] == new_manager: + messagebox.showwarning("警告", "该记录已存在") + # 恢复原值 + self.tree.item(item_id, values=old_values) + return + + # 更新表格显示 + self.tree.item(item_id, values=new_values) + + # 更新行状态 + # 检查是否是新增的行(查找临时 key) + temp_key = f"temp:{item_id}" + is_new_row = temp_key in self.row_status and self.row_status[temp_key] == self.ROW_STATUS_NEW + + if is_new_row: + # 新增行:从临时 key 更新为实际 key + del self.row_status[temp_key] + self.row_status[new_key] = self.ROW_STATUS_NEW + else: + # 现有行的编辑,标记为修改 + # 获取原始值 + if new_key in self.original_values: + # 已经修改过,使用已保存的原始值 + old_material, old_manager = self.original_values[new_key] + else: + # 第一次修改,从 old_values 获取原始值 + old_material = old_values[0] if column != 0 else old_value + old_manager = old_values[1] if column != 1 else old_value + self.original_values[new_key] = (old_material, old_manager) + + self.row_status[new_key] = self.ROW_STATUS_MODIFIED + + # 设置标签 + self.tree.item(item_id, tags=('modified',)) + + self._update_status() + + def _on_edit_cancelled(self, item_id: str, column: int, old_value: str): + """编辑取消回调""" + # 恢复原值 + values = list(self.tree.item(item_id, "values")) + values[column] = old_value + self.tree.item(item_id, values=values) def _save_changes(self): """保存更改到数据库""" - total_changes = ( - len(self.pending_changes['insert']) + - len(self.pending_changes['delete']) + - len(self.pending_changes['update']) - ) + # 收集所有更改 + to_insert = [] + to_delete = [] + to_update = [] + + # 遍历所有行状态 + for key, status in self.row_status.items(): + if status == self.ROW_STATUS_DELETED: + # 解析 key + parts = key.split('|') + if len(parts) == 2: + to_delete.append({'MaterialName': parts[0], 'ManagerName': parts[1]}) + + elif status == self.ROW_STATUS_NEW: + # 从表格中获取数据 + # 处理临时 key 或正常 key + item_to_find = None + if key.startswith("temp:"): + # 临时 key,从 key 中提取 item_id + item_id = key.split(":", 1)[1] + if self.tree.exists(item_id): + item_to_find = item_id + else: + # 正常 key,在表格中查找匹配的行 + for item in self.tree.get_children(): + values = self.tree.item(item, "values") + item_key = self._get_record_key({'MaterialName': values[0], 'ManagerName': values[1]}) + if item_key == key: + item_to_find = item + break + + if item_to_find: + values = self.tree.item(item_to_find, "values") + if values[0] and values[1]: # 只保存非空行 + to_insert.append({'MaterialName': values[0], 'ManagerName': values[1]}) + + elif status == self.ROW_STATUS_MODIFIED: + # 从表格中获取新数据,从 original_values 获取旧数据 + if key in self.original_values: + old_material, old_manager = self.original_values[key] + + # 从表格中找到对应的新数据 + for item in self.tree.get_children(): + values = self.tree.item(item, "values") + item_key = self._get_record_key({'MaterialName': values[0], 'ManagerName': values[1]}) + if item_key == key: + to_update.append({ + 'old': {'MaterialName': old_material, 'ManagerName': old_manager}, + 'new': {'MaterialName': values[0], 'ManagerName': values[1]} + }) + break + + total_changes = len(to_insert) + len(to_delete) + len(to_update) if total_changes == 0: messagebox.showinfo("提示", "没有需要保存的更改") @@ -585,12 +819,12 @@ class MaterialTypeManagementDialog(tk.Toplevel): # 确认保存 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 to_insert: + msg_parts.append(f"新增 {len(to_insert)} 条") + if to_delete: + msg_parts.append(f"删除 {len(to_delete)} 条") + if to_update: + msg_parts.append(f"更新 {len(to_update)} 条") if not messagebox.askyesno( "确认保存", @@ -613,29 +847,27 @@ class MaterialTypeManagementDialog(tk.Toplevel): } # 执行插入 - for record in self.pending_changes['insert']: + for record in to_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']: + for record in to_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']: + for update in to_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: @@ -671,11 +903,24 @@ class MaterialTypeManagementDialog(tk.Toplevel): def _update_status(self): """更新状态标签""" - total_changes = ( - len(self.pending_changes['insert']) + - len(self.pending_changes['delete']) + - len(self.pending_changes['update']) - ) + total_changes = 0 + + for key, status in self.row_status.items(): + if status == self.ROW_STATUS_DELETED: + total_changes += 1 + elif status == self.ROW_STATUS_NEW: + # 对于新增行,检查是否有实际数据 + if key.startswith("temp:"): + # 临时 key,检查表格中的数据 + item_id = key.split(":", 1)[1] + if self.tree.exists(item_id): + values = self.tree.item(item_id, "values") + if values[0] and values[1]: + total_changes += 1 + else: + total_changes += 1 + elif status == self.ROW_STATUS_MODIFIED: + total_changes += 1 if total_changes > 0: self.status_label.config( @@ -687,12 +932,24 @@ class MaterialTypeManagementDialog(tk.Toplevel): def _close_dialog(self): """关闭对话框""" - # 检查是否有未保存的更改 - total_changes = ( - len(self.pending_changes['insert']) + - len(self.pending_changes['delete']) + - len(self.pending_changes['update']) - ) + total_changes = 0 + + for key, status in self.row_status.items(): + if status == self.ROW_STATUS_DELETED: + total_changes += 1 + elif status == self.ROW_STATUS_NEW: + # 对于新增行,检查是否有实际数据 + if key.startswith("temp:"): + # 临时 key,检查表格中的数据 + item_id = key.split(":", 1)[1] + if self.tree.exists(item_id): + values = self.tree.item(item_id, "values") + if values[0] and values[1]: + total_changes += 1 + else: + total_changes += 1 + elif status == self.ROW_STATUS_MODIFIED: + total_changes += 1 if total_changes > 0: if not messagebox.askyesno(