refactor: extract common tab functionality to BaseTab and add decorators
- Add BaseTab base class for common tab initialization and logging - Add constants module for shared GUI constants - Move CheckboxTreeview to separate widget file - Add admin_only and require_session decorators to utils.py - Refactor DataExtractionTab and MaterialValidationTab to inherit BaseTab - Remove duplicate _update_log method from tab classes Co-Authored-By: Claude (glm-5) <noreply@anthropic.com>
This commit is contained in:
@@ -5,3 +5,27 @@ ERP 自动化工具 - GUI 模块
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
__version__ = "1.0.0"
|
__version__ = "1.0.0"
|
||||||
|
|
||||||
|
# 导出常用模块
|
||||||
|
from .base_tab import BaseTab
|
||||||
|
from .constants import (
|
||||||
|
WINDOW_SIZE,
|
||||||
|
MIN_WINDOW_SIZE,
|
||||||
|
POLL_INTERVAL_MS,
|
||||||
|
LOG_COLORS,
|
||||||
|
DEFAULT_FONT_FAMILY,
|
||||||
|
DEFAULT_FONT_SIZE,
|
||||||
|
)
|
||||||
|
from .utils import admin_only, require_session
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'BaseTab',
|
||||||
|
'WINDOW_SIZE',
|
||||||
|
'MIN_WINDOW_SIZE',
|
||||||
|
'POLL_INTERVAL_MS',
|
||||||
|
'LOG_COLORS',
|
||||||
|
'DEFAULT_FONT_FAMILY',
|
||||||
|
'DEFAULT_FONT_SIZE',
|
||||||
|
'admin_only',
|
||||||
|
'require_session',
|
||||||
|
]
|
||||||
|
|||||||
132
gui/base_tab.py
Normal file
132
gui/base_tab.py
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
BaseTab - 标签页基类
|
||||||
|
|
||||||
|
提供所有标签页共享的通用功能,包括:
|
||||||
|
- 统一的日志更新方法
|
||||||
|
- 线程安全的 GUI 操作
|
||||||
|
- 通用工具方法
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import ttk
|
||||||
|
|
||||||
|
|
||||||
|
class BaseTab(ttk.Frame):
|
||||||
|
"""标签页基类
|
||||||
|
|
||||||
|
提供所有标签页共享的通用功能。
|
||||||
|
子类应继承此类并实现 create_widgets 方法。
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, parent, config=None, main_window=None):
|
||||||
|
"""初始化基类
|
||||||
|
|
||||||
|
Args:
|
||||||
|
parent: 父容器
|
||||||
|
config: 配置管理器(可选)
|
||||||
|
main_window: 主窗口引用(可选)
|
||||||
|
"""
|
||||||
|
super().__init__(parent)
|
||||||
|
self.config = config
|
||||||
|
self.main_window = main_window
|
||||||
|
|
||||||
|
def _update_log(self, message: str, level: str = "INFO"):
|
||||||
|
"""
|
||||||
|
线程安全的日志更新方法
|
||||||
|
|
||||||
|
通过统一的 logging 系统输出日志,自动同时输出到控制台和 GUI。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message: 日志消息
|
||||||
|
level: 日志级别 (INFO, SUCCESS, WARNING, ERROR, DEBUG)
|
||||||
|
|
||||||
|
Note:
|
||||||
|
子类需要设置 self.logger 和 self._gui_handler 才能使用此方法。
|
||||||
|
如果 self.logger 未设置,将使用 logging.getLogger(__name__) 作为后备。
|
||||||
|
"""
|
||||||
|
# 获取 logger(优先使用实例的 logger,否则使用模块 logger)
|
||||||
|
logger = getattr(self, 'logger', None) or logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# 将自定义级别映射到 logging 级别
|
||||||
|
level_upper = level.upper()
|
||||||
|
if level_upper == "SUCCESS":
|
||||||
|
# SUCCESS 映射到 INFO,但在 UI 中仍显示为 SUCCESS
|
||||||
|
logger.info(message)
|
||||||
|
else:
|
||||||
|
# 其他级别直接映射
|
||||||
|
log_level = getattr(logging, level_upper, logging.INFO)
|
||||||
|
logger.log(log_level, message)
|
||||||
|
|
||||||
|
def _run_on_main_thread(self, callback, *args, **kwargs):
|
||||||
|
"""在主线程中执行回调函数(线程安全)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
callback: 要执行的回调函数
|
||||||
|
*args: 位置参数
|
||||||
|
**kwargs: 关键字参数
|
||||||
|
|
||||||
|
Note:
|
||||||
|
使用 after(0, ...) 确保在主线程中执行。
|
||||||
|
"""
|
||||||
|
self.after(0, lambda: callback(*args, **kwargs))
|
||||||
|
|
||||||
|
def _is_admin(self) -> bool:
|
||||||
|
"""检查当前用户是否为管理员
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 如果是管理员返回 True,否则返回 False
|
||||||
|
|
||||||
|
Note:
|
||||||
|
需要 main_window 或 session_manager 支持。
|
||||||
|
"""
|
||||||
|
# 尝试从 session_manager 获取
|
||||||
|
if hasattr(self, 'session_manager') and self.session_manager:
|
||||||
|
return self.session_manager.is_admin()
|
||||||
|
|
||||||
|
# 尝试从 main_window 获取
|
||||||
|
if self.main_window and hasattr(self.main_window, 'session_manager'):
|
||||||
|
return self.main_window.session_manager.is_admin()
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _get_username(self) -> str:
|
||||||
|
"""获取当前用户名
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: 当前用户名,如果无法获取则返回空字符串
|
||||||
|
|
||||||
|
Note:
|
||||||
|
需要 session_manager 支持。
|
||||||
|
"""
|
||||||
|
if hasattr(self, 'session_manager') and self.session_manager:
|
||||||
|
return self.session_manager.get_username() or ""
|
||||||
|
|
||||||
|
if self.main_window and hasattr(self.main_window, 'session_manager'):
|
||||||
|
return self.main_window.session_manager.get_username() or ""
|
||||||
|
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def set_busy(self, busy: bool):
|
||||||
|
"""设置窗口忙碌状态(显示等待光标)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
busy: True 显示等待光标,False 恢复正常光标
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
cursor = "watch" if busy else "arrow"
|
||||||
|
# 获取顶层窗口
|
||||||
|
toplevel = self.winfo_toplevel()
|
||||||
|
if toplevel:
|
||||||
|
toplevel.config(cursor=cursor)
|
||||||
|
toplevel.update()
|
||||||
|
except tk.TclError:
|
||||||
|
# 窗口可能已被销毁
|
||||||
|
pass
|
||||||
|
|
||||||
|
def reload_config(self):
|
||||||
|
"""重新加载配置(子类可覆盖此方法)"""
|
||||||
|
if self.config and hasattr(self.config, 'reload'):
|
||||||
|
self.config.reload()
|
||||||
112
gui/constants.py
Normal file
112
gui/constants.py
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
GUI 常量模块
|
||||||
|
|
||||||
|
集中管理 GUI 相关的常量配置,包括:
|
||||||
|
- 窗口尺寸
|
||||||
|
- 进度条轮询间隔
|
||||||
|
- 日志颜色
|
||||||
|
- 默认字体配置
|
||||||
|
"""
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 窗口尺寸
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# 主窗口默认尺寸
|
||||||
|
WINDOW_SIZE = (1000, 700)
|
||||||
|
|
||||||
|
# 主窗口最小尺寸
|
||||||
|
MIN_WINDOW_SIZE = (800, 600)
|
||||||
|
|
||||||
|
# 日志面板默认高度(行数)
|
||||||
|
LOG_PANEL_HEIGHT = 15
|
||||||
|
|
||||||
|
# 结果表格默认高度(行数)
|
||||||
|
RESULT_TABLE_HEIGHT = 10
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 时间间隔(毫秒)
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# 进度队列轮询间隔
|
||||||
|
POLL_INTERVAL_MS = 50
|
||||||
|
|
||||||
|
# UI 更新延迟
|
||||||
|
UI_UPDATE_DELAY_MS = 100
|
||||||
|
|
||||||
|
# 标题存储延迟
|
||||||
|
HEADING_STORE_DELAY_MS = 100
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 日志颜色
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# 日志级别对应的颜色
|
||||||
|
LOG_COLORS = {
|
||||||
|
"INFO": "#000000", # 黑色
|
||||||
|
"SUCCESS": "#008000", # 绿色
|
||||||
|
"WARNING": "#FF8C00", # 橙色
|
||||||
|
"ERROR": "#FF0000", # 红色
|
||||||
|
"DEBUG": "#808080", # 灰色
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 字体配置
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# 默认字体
|
||||||
|
DEFAULT_FONT_FAMILY = "Microsoft YaHei UI"
|
||||||
|
|
||||||
|
# 默认字号
|
||||||
|
DEFAULT_FONT_SIZE = 10
|
||||||
|
|
||||||
|
# 可用字体列表
|
||||||
|
AVAILABLE_FONTS = [
|
||||||
|
"Microsoft YaHei UI",
|
||||||
|
"SimSun",
|
||||||
|
"KaiTi",
|
||||||
|
"FangSong",
|
||||||
|
"Arial",
|
||||||
|
"Segoe UI",
|
||||||
|
]
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 文件类型过滤器
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Excel 文件过滤器
|
||||||
|
EXCEL_FILE_TYPES = [("Excel 文件", "*.xlsx"), ("所有文件", "*.*")]
|
||||||
|
|
||||||
|
# 文本文件过滤器
|
||||||
|
TEXT_FILE_TYPES = [("文本文件", "*.txt"), ("所有文件", "*.*")]
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 默认值
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# 默认数据目录
|
||||||
|
DEFAULT_DATA_DIR = "data/"
|
||||||
|
|
||||||
|
# 默认输出文件名
|
||||||
|
DEFAULT_OUTPUT_FILE = "离散备料计划维护_合并.xlsx"
|
||||||
|
|
||||||
|
# 默认校验输出文件名
|
||||||
|
DEFAULT_VALIDATION_OUTPUT = "物料状态校验结果.xlsx"
|
||||||
|
|
||||||
|
# 默认批次大小
|
||||||
|
DEFAULT_BATCH_SIZE = 100
|
||||||
|
|
||||||
|
# 默认数据库批次大小
|
||||||
|
DEFAULT_DB_BATCH_SIZE = 2000
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 复选框字符
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# 选中状态
|
||||||
|
CHECKBOX_CHECKED = "☑"
|
||||||
|
|
||||||
|
# 未选中状态
|
||||||
|
CHECKBOX_UNCHECKED = "☐"
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
数据提取标签页 - 稳定性修复版
|
数据提取标签页
|
||||||
|
|
||||||
修复了 LogText.info 不支持 add_timestamp 参数导致的 TypeError。
|
从 ERP 系统提取生产订单数据的标签页。
|
||||||
|
继承自 BaseTab,使用统一的日志系统。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
@@ -14,6 +15,7 @@ import queue
|
|||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk, filedialog, messagebox
|
from tkinter import ttk, filedialog, messagebox
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from gui.base_tab import BaseTab
|
||||||
from gui.widgets import FileSelector, LogText, ProductionIdInput, GuiTextHandler
|
from gui.widgets import FileSelector, LogText, ProductionIdInput, GuiTextHandler
|
||||||
from gui.config_manager import ConfigManager
|
from gui.config_manager import ConfigManager
|
||||||
from gui.log_config import setup_gui_logging, get_logger
|
from gui.log_config import setup_gui_logging, get_logger
|
||||||
@@ -21,13 +23,11 @@ from gui.progress import ProgressInfo, ProgressCalculator
|
|||||||
from gui.utils import RealtimeOutput
|
from gui.utils import RealtimeOutput
|
||||||
|
|
||||||
|
|
||||||
class DataExtractionTab(ttk.Frame):
|
class DataExtractionTab(BaseTab):
|
||||||
"""数据提取标签页"""
|
"""数据提取标签页"""
|
||||||
|
|
||||||
def __init__(self, parent, config: ConfigManager, main_window=None):
|
def __init__(self, parent, config: ConfigManager, main_window=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent, config, main_window)
|
||||||
self.config = config
|
|
||||||
self.main_window = main_window
|
|
||||||
self.extracting = False
|
self.extracting = False
|
||||||
self.extractor = None
|
self.extractor = None
|
||||||
self.extraction_thread = None
|
self.extraction_thread = None
|
||||||
@@ -42,10 +42,11 @@ class DataExtractionTab(ttk.Frame):
|
|||||||
self.create_widgets()
|
self.create_widgets()
|
||||||
self._apply_ui_config()
|
self._apply_ui_config()
|
||||||
|
|
||||||
|
# 初始化日志消息
|
||||||
try:
|
try:
|
||||||
self.log_text.info("数据提取标签页已就绪")
|
self.log_text.info("数据提取标签页已就绪")
|
||||||
except:
|
except Exception as e:
|
||||||
pass
|
self.logger.debug(f"初始化日志消息失败: {e}")
|
||||||
|
|
||||||
def create_widgets(self):
|
def create_widgets(self):
|
||||||
horizontal_paned = ttk.PanedWindow(self, orient=tk.HORIZONTAL)
|
horizontal_paned = ttk.PanedWindow(self, orient=tk.HORIZONTAL)
|
||||||
@@ -137,11 +138,14 @@ class DataExtractionTab(ttk.Frame):
|
|||||||
self.production_id_input.apply_font(font_family, font_size)
|
self.production_id_input.apply_font(font_family, font_size)
|
||||||
if hasattr(self.log_text, 'apply_font'):
|
if hasattr(self.log_text, 'apply_font'):
|
||||||
self.log_text.apply_font(font_family, font_size)
|
self.log_text.apply_font(font_family, font_size)
|
||||||
except: pass
|
except Exception as e:
|
||||||
|
self.logger.debug(f"应用 UI 配置失败: {e}")
|
||||||
|
|
||||||
def _set_pane_width(self, width: int):
|
def _set_pane_width(self, width: int):
|
||||||
try: self.horizontal_paned.sashpos(0, width)
|
try:
|
||||||
except: pass
|
self.horizontal_paned.sashpos(0, width)
|
||||||
|
except tk.TclError as e:
|
||||||
|
self.logger.debug(f"设置窗格宽度失败: {e}")
|
||||||
|
|
||||||
def start_extraction(self):
|
def start_extraction(self):
|
||||||
production_ids = self.production_id_input.get()
|
production_ids = self.production_id_input.get()
|
||||||
@@ -208,8 +212,10 @@ class DataExtractionTab(ttk.Frame):
|
|||||||
self._update_log(f"运行时错误: {str(e)}", "ERROR")
|
self._update_log(f"运行时错误: {str(e)}", "ERROR")
|
||||||
finally:
|
finally:
|
||||||
if temp_file and os.path.exists(temp_file):
|
if temp_file and os.path.exists(temp_file):
|
||||||
try: os.unlink(temp_file)
|
try:
|
||||||
except: pass
|
os.unlink(temp_file)
|
||||||
|
except OSError as e:
|
||||||
|
self.logger.debug(f"清理临时文件失败: {e}")
|
||||||
self.after(0, self._extraction_complete)
|
self.after(0, self._extraction_complete)
|
||||||
|
|
||||||
def _extraction_complete(self):
|
def _extraction_complete(self):
|
||||||
@@ -225,32 +231,18 @@ class DataExtractionTab(ttk.Frame):
|
|||||||
value, message = self.progress_queue.get_nowait()
|
value, message = self.progress_queue.get_nowait()
|
||||||
self.progress_bar["value"] = value
|
self.progress_bar["value"] = value
|
||||||
self.status_label.config(text=message)
|
self.status_label.config(text=message)
|
||||||
except queue.Empty: break
|
except queue.Empty:
|
||||||
finally: self.after(50, self._poll_progress_queue)
|
break
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.debug(f"轮询进度队列失败: {e}")
|
||||||
|
finally:
|
||||||
|
self.after(50, self._poll_progress_queue)
|
||||||
|
|
||||||
def _update_progress(self, value: int, message: str):
|
def _update_progress(self, value: int, message: str):
|
||||||
try: self.progress_queue.put_nowait((value, message))
|
try:
|
||||||
except: pass
|
self.progress_queue.put_nowait((value, message))
|
||||||
|
except queue.Full as e:
|
||||||
def _update_log(self, message: str, level: str = "INFO"):
|
self.logger.debug(f"进度队列已满: {e}")
|
||||||
"""
|
|
||||||
标准的日志更新方法(兼容接口)
|
|
||||||
|
|
||||||
通过统一的 logging 系统输出日志,自动同时输出到控制台和 GUI。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
message: 日志消息
|
|
||||||
level: 日志级别 (INFO, SUCCESS, WARNING, ERROR, DEBUG)
|
|
||||||
"""
|
|
||||||
# 将自定义级别映射到 logging 级别
|
|
||||||
level_upper = level.upper()
|
|
||||||
if level_upper == "SUCCESS":
|
|
||||||
# SUCCESS 映射到 INFO,但在 UI 中仍显示为 SUCCESS
|
|
||||||
self.logger.info(message)
|
|
||||||
else:
|
|
||||||
# 其他级别直接映射
|
|
||||||
log_level = getattr(logging, level_upper, logging.INFO)
|
|
||||||
self.logger.log(log_level, message)
|
|
||||||
|
|
||||||
def _on_production_ids_changed(self, event=None):
|
def _on_production_ids_changed(self, event=None):
|
||||||
if self.main_window:
|
if self.main_window:
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ from pathlib import Path
|
|||||||
from io import StringIO
|
from io import StringIO
|
||||||
from contextlib import redirect_stdout
|
from contextlib import redirect_stdout
|
||||||
from typing import List, Dict
|
from typing import List, Dict
|
||||||
from gui.widgets import FileSelector, LogText, GuiTextHandler, DeleteProgressWindow
|
from gui.base_tab import BaseTab
|
||||||
|
from gui.widgets import FileSelector, LogText, GuiTextHandler, DeleteProgressWindow, CheckboxTreeview
|
||||||
from gui.config_manager import ConfigManager
|
from gui.config_manager import ConfigManager
|
||||||
from gui.log_config import setup_gui_logging, get_logger
|
from gui.log_config import setup_gui_logging, get_logger
|
||||||
from gui.material_type_management_dialog import MaterialTypeManagementDialog
|
from gui.material_type_management_dialog import MaterialTypeManagementDialog
|
||||||
@@ -27,232 +28,7 @@ import pandas as pd
|
|||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
class CheckboxTreeview(ttk.Treeview):
|
class MaterialValidationTab(BaseTab):
|
||||||
"""支持 checkbox 的 Treeview 组件
|
|
||||||
|
|
||||||
使用 Unicode 字符模拟 checkbox:
|
|
||||||
- ☐ 未选中
|
|
||||||
- ☑ 选中
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, parent, on_checkbox_change=None, **kwargs):
|
|
||||||
"""初始化 CheckboxTreeview
|
|
||||||
|
|
||||||
Args:
|
|
||||||
parent: 父容器
|
|
||||||
on_checkbox_change: checkbox 状态改变时的回调函数
|
|
||||||
**kwargs: 传递给 Treeview 的参数
|
|
||||||
"""
|
|
||||||
super().__init__(parent, **kwargs)
|
|
||||||
self.checkboxes = {} # item_id -> bool
|
|
||||||
self.checkbox_column = "选择"
|
|
||||||
self.on_checkbox_change = on_checkbox_change # checkbox 状态改变回调
|
|
||||||
|
|
||||||
# 排序状态
|
|
||||||
self.sort_column = None # 当前排序列的列标识符
|
|
||||||
self.sort_direction = None # 'asc', 'desc', 或 None
|
|
||||||
self.sortable_columns = ["选择", "材料名称"] # 可排序的列白名单
|
|
||||||
self.original_headings = {} # 存储原始列标题文本(不含箭头)
|
|
||||||
|
|
||||||
# 存储原始列标题(延迟执行以确保标题已设置)
|
|
||||||
self.after(100, self._store_original_headings)
|
|
||||||
|
|
||||||
# 绑定点击事件
|
|
||||||
self.bind("<Button-1>", self._on_click)
|
|
||||||
# 绑定表头点击事件
|
|
||||||
self.bind("<ButtonRelease-1>", self._on_heading_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)
|
|
||||||
new_state = not current_state
|
|
||||||
self.set_checked(item, new_state)
|
|
||||||
|
|
||||||
# 通知父组件 checkbox 状态已改变
|
|
||||||
if self.on_checkbox_change:
|
|
||||||
self.on_checkbox_change(item, new_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)
|
|
||||||
|
|
||||||
def _store_original_headings(self):
|
|
||||||
"""存储原始列标题文本(不含箭头)"""
|
|
||||||
for col in self['columns']:
|
|
||||||
self.original_headings[col] = self.heading(col, 'text')
|
|
||||||
|
|
||||||
def _get_column_id_from_column_index(self, column_index):
|
|
||||||
"""将列索引 ('#1', '#2') 转换为列标识符
|
|
||||||
|
|
||||||
Args:
|
|
||||||
column_index: 列索引字符串,如 '#1', '#2'
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
列标识符,如 '选择', '材料名称'
|
|
||||||
"""
|
|
||||||
index = int(column_index[1:]) - 1
|
|
||||||
columns = self['columns']
|
|
||||||
if 0 <= index < len(columns):
|
|
||||||
return columns[index]
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _on_heading_click(self, event):
|
|
||||||
"""处理表头点击事件,触发排序"""
|
|
||||||
region = self.identify_region(event.x, event.y)
|
|
||||||
|
|
||||||
if region == "heading":
|
|
||||||
column = self.identify_column(event.x)
|
|
||||||
column_id = self._get_column_id_from_column_index(column)
|
|
||||||
|
|
||||||
# 仅对可排序列进行排序
|
|
||||||
if column_id in self.sortable_columns:
|
|
||||||
self._toggle_sort(column_id)
|
|
||||||
|
|
||||||
def _toggle_sort(self, column_id):
|
|
||||||
"""切换指定列的排序状态
|
|
||||||
|
|
||||||
Args:
|
|
||||||
column_id: 列标识符(如 '选择', '材料名称')
|
|
||||||
"""
|
|
||||||
# 确定新的排序方向
|
|
||||||
if self.sort_column == column_id:
|
|
||||||
# 同一列:asc -> desc -> None
|
|
||||||
if self.sort_direction == 'asc':
|
|
||||||
new_direction = 'desc'
|
|
||||||
elif self.sort_direction == 'desc':
|
|
||||||
new_direction = None
|
|
||||||
else:
|
|
||||||
new_direction = 'asc'
|
|
||||||
else:
|
|
||||||
# 不同列:从升序开始
|
|
||||||
new_direction = 'asc'
|
|
||||||
|
|
||||||
# 应用排序
|
|
||||||
if new_direction:
|
|
||||||
self._sort_by_column(column_id, new_direction)
|
|
||||||
self.sort_column = column_id
|
|
||||||
self.sort_direction = new_direction
|
|
||||||
else:
|
|
||||||
# 清除排序状态
|
|
||||||
self.sort_column = None
|
|
||||||
self.sort_direction = None
|
|
||||||
|
|
||||||
# 更新表头显示
|
|
||||||
self._update_heading_display()
|
|
||||||
|
|
||||||
def _sort_by_column(self, column_id, direction):
|
|
||||||
"""按指定列和方向排序
|
|
||||||
|
|
||||||
Args:
|
|
||||||
column_id: 列标识符
|
|
||||||
direction: 'asc' 或 'desc'
|
|
||||||
"""
|
|
||||||
# 收集所有项目及其数据和复选框状态
|
|
||||||
items_data = []
|
|
||||||
for item in self.get_children():
|
|
||||||
values = self.item(item, "values")
|
|
||||||
checkbox_state = self.checkboxes.get(item, False)
|
|
||||||
items_data.append({
|
|
||||||
'item_id': item,
|
|
||||||
'values': values,
|
|
||||||
'checked': checkbox_state
|
|
||||||
})
|
|
||||||
|
|
||||||
# 根据列和方向排序
|
|
||||||
if column_id == "选择":
|
|
||||||
# 按复选框状态排序(选中在前,未选中在后)
|
|
||||||
items_data.sort(key=lambda x: x['checked'], reverse=(direction == 'desc'))
|
|
||||||
elif column_id == "材料名称":
|
|
||||||
# 按材料名称排序
|
|
||||||
items_data.sort(
|
|
||||||
key=lambda x: str(x['values'][1]) if len(x['values']) > 1 else "",
|
|
||||||
reverse=(direction == 'desc')
|
|
||||||
)
|
|
||||||
|
|
||||||
# 重新排列项目顺序(使用 detach 和 move 保留项目ID和状态)
|
|
||||||
for item_data in items_data:
|
|
||||||
self.move(item_data['item_id'], '', 'end')
|
|
||||||
|
|
||||||
def _update_heading_display(self):
|
|
||||||
"""更新列标题显示(添加/移除排序箭头)"""
|
|
||||||
for col in self['columns']:
|
|
||||||
original = self.original_headings.get(col, col)
|
|
||||||
if col == self.sort_column:
|
|
||||||
# 添加排序箭头
|
|
||||||
arrow = " ↑" if self.sort_direction == 'asc' else " ↓"
|
|
||||||
self.heading(col, text=original + arrow)
|
|
||||||
else:
|
|
||||||
# 移除箭头,显示原始标题
|
|
||||||
self.heading(col, text=original)
|
|
||||||
|
|
||||||
|
|
||||||
class MaterialValidationTab(ttk.Frame):
|
|
||||||
"""物料校验标签页"""
|
"""物料校验标签页"""
|
||||||
|
|
||||||
def __init__(self, parent, config: ConfigManager, session_manager, main_window=None):
|
def __init__(self, parent, config: ConfigManager, session_manager, main_window=None):
|
||||||
@@ -265,10 +41,8 @@ class MaterialValidationTab(ttk.Frame):
|
|||||||
session_manager: SessionManager 实例,用于权限控制
|
session_manager: SessionManager 实例,用于权限控制
|
||||||
main_window: 主窗口引用,用于获取共享的 Production ID
|
main_window: 主窗口引用,用于获取共享的 Production ID
|
||||||
"""
|
"""
|
||||||
super().__init__(parent)
|
super().__init__(parent, config, main_window)
|
||||||
self.config = config
|
|
||||||
self.session_manager = session_manager
|
self.session_manager = session_manager
|
||||||
self.main_window = main_window
|
|
||||||
self.validating = False
|
self.validating = False
|
||||||
self.validation_results = None
|
self.validation_results = None
|
||||||
self.material_records_cache = None # 缓存完整的物料记录
|
self.material_records_cache = None # 缓存完整的物料记录
|
||||||
@@ -290,8 +64,8 @@ class MaterialValidationTab(ttk.Frame):
|
|||||||
# 稍后显示就绪消息
|
# 稍后显示就绪消息
|
||||||
try:
|
try:
|
||||||
self.log_text.info("物料校验标签页已就绪")
|
self.log_text.info("物料校验标签页已就绪")
|
||||||
except:
|
except Exception as e:
|
||||||
pass # 如果窗口还未完全就绪,忽略错误
|
self.logger.debug(f"初始化日志消息失败: {e}")
|
||||||
|
|
||||||
def create_widgets(self):
|
def create_widgets(self):
|
||||||
"""创建界面组件"""
|
"""创建界面组件"""
|
||||||
@@ -1179,8 +953,8 @@ class MaterialValidationTab(ttk.Frame):
|
|||||||
if temp_production_id_file and os.path.exists(temp_production_id_file):
|
if temp_production_id_file and os.path.exists(temp_production_id_file):
|
||||||
try:
|
try:
|
||||||
os.unlink(temp_production_id_file)
|
os.unlink(temp_production_id_file)
|
||||||
except:
|
except OSError as e:
|
||||||
pass
|
self.logger.debug(f"清理临时文件失败: {e}")
|
||||||
# 更新 UI 状态
|
# 更新 UI 状态
|
||||||
self.after(0, self._validation_complete)
|
self.after(0, self._validation_complete)
|
||||||
|
|
||||||
@@ -1441,26 +1215,6 @@ class MaterialValidationTab(ttk.Frame):
|
|||||||
# 刷新筛选器和结果(可能添加了新的负责人)
|
# 刷新筛选器和结果(可能添加了新的负责人)
|
||||||
self._initialize_manager_filter()
|
self._initialize_manager_filter()
|
||||||
|
|
||||||
def _update_log(self, message: str, level: str = "INFO"):
|
|
||||||
"""
|
|
||||||
线程安全的日志更新(兼容接口)
|
|
||||||
|
|
||||||
通过统一的 logging 系统输出日志,自动同时输出到控制台和 GUI。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
message: 日志消息
|
|
||||||
level: 日志级别 (INFO, SUCCESS, WARNING, ERROR, DEBUG)
|
|
||||||
"""
|
|
||||||
# 将自定义级别映射到 logging 级别
|
|
||||||
level_upper = level.upper()
|
|
||||||
if level_upper == "SUCCESS":
|
|
||||||
# SUCCESS 映射到 INFO,但在 UI 中仍显示为 SUCCESS
|
|
||||||
self.logger.info(message)
|
|
||||||
else:
|
|
||||||
# 其他级别直接映射
|
|
||||||
log_level = getattr(logging, level_upper, logging.INFO)
|
|
||||||
self.logger.log(log_level, message)
|
|
||||||
|
|
||||||
def export_results(self):
|
def export_results(self):
|
||||||
"""导出结果到 Excel"""
|
"""导出结果到 Excel"""
|
||||||
# 从配置获取输出文件路径
|
# 从配置获取输出文件路径
|
||||||
@@ -1676,8 +1430,8 @@ class MaterialValidationTab(ttk.Frame):
|
|||||||
try:
|
try:
|
||||||
if 'temp_file' in locals() and os.path.exists(temp_file):
|
if 'temp_file' in locals() and os.path.exists(temp_file):
|
||||||
os.unlink(temp_file)
|
os.unlink(temp_file)
|
||||||
except:
|
except OSError as e:
|
||||||
pass
|
self.logger.debug(f"清理临时文件失败: {e}")
|
||||||
|
|
||||||
def _delete_complete(self, report: str, stats: dict):
|
def _delete_complete(self, report: str, stats: dict):
|
||||||
"""
|
"""
|
||||||
|
|||||||
90
gui/utils.py
90
gui/utils.py
@@ -6,6 +6,8 @@ GUI 工具模块
|
|||||||
提供 GUI 相关的工具类和函数。
|
提供 GUI 相关的工具类和函数。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import functools
|
||||||
|
|
||||||
|
|
||||||
class RealtimeOutput:
|
class RealtimeOutput:
|
||||||
"""实时输出流,每次写入立即回调通知"""
|
"""实时输出流,每次写入立即回调通知"""
|
||||||
@@ -37,3 +39,91 @@ class RealtimeOutput:
|
|||||||
def isatty(self):
|
def isatty(self):
|
||||||
"""返回 False,表示不是终端"""
|
"""返回 False,表示不是终端"""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def admin_only(func):
|
||||||
|
"""
|
||||||
|
管理员权限装饰器
|
||||||
|
|
||||||
|
用于标记需要管理员权限的方法。如果当前用户不是管理员,
|
||||||
|
方法将不执行任何操作并返回 None。
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
@admin_only
|
||||||
|
def some_admin_function(self):
|
||||||
|
# 只有管理员才能执行的代码
|
||||||
|
pass
|
||||||
|
|
||||||
|
Note:
|
||||||
|
- 被装饰的方法必须属于一个有 session_manager 属性的对象
|
||||||
|
- session_manager 必须有 is_admin() 方法
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
如果是管理员,返回原函数的结果;否则返回 None
|
||||||
|
"""
|
||||||
|
@functools.wraps(func)
|
||||||
|
def wrapper(self, *args, **kwargs):
|
||||||
|
# 尝试从 self 获取 session_manager
|
||||||
|
session_manager = getattr(self, 'session_manager', None)
|
||||||
|
|
||||||
|
# 如果没有 session_manager,尝试从 main_window 获取
|
||||||
|
if session_manager is None:
|
||||||
|
main_window = getattr(self, 'main_window', None)
|
||||||
|
if main_window:
|
||||||
|
session_manager = getattr(main_window, 'session_manager', None)
|
||||||
|
|
||||||
|
# 检查是否为管理员
|
||||||
|
if session_manager and hasattr(session_manager, 'is_admin'):
|
||||||
|
if session_manager.is_admin():
|
||||||
|
return func(self, *args, **kwargs)
|
||||||
|
|
||||||
|
# 非管理员,记录日志并返回 None
|
||||||
|
logger = getattr(self, 'logger', None)
|
||||||
|
if logger:
|
||||||
|
logger.debug(f"权限拒绝: {func.__name__} 需要管理员权限")
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def require_session(func):
|
||||||
|
"""
|
||||||
|
会话验证装饰器
|
||||||
|
|
||||||
|
确保方法执行时有有效的会话。如果会话无效,
|
||||||
|
方法将不执行任何操作并返回 None。
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
@require_session
|
||||||
|
def some_function(self):
|
||||||
|
# 需要有效会话才能执行的代码
|
||||||
|
pass
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
如果有有效会话,返回原函数的结果;否则返回 None
|
||||||
|
"""
|
||||||
|
@functools.wraps(func)
|
||||||
|
def wrapper(self, *args, **kwargs):
|
||||||
|
# 尝试从 self 获取 session_manager
|
||||||
|
session_manager = getattr(self, 'session_manager', None)
|
||||||
|
|
||||||
|
# 如果没有 session_manager,尝试从 main_window 获取
|
||||||
|
if session_manager is None:
|
||||||
|
main_window = getattr(self, 'main_window', None)
|
||||||
|
if main_window:
|
||||||
|
session_manager = getattr(main_window, 'session_manager', None)
|
||||||
|
|
||||||
|
# 检查会话是否有效
|
||||||
|
if session_manager and hasattr(session_manager, 'is_authenticated'):
|
||||||
|
if session_manager.is_authenticated():
|
||||||
|
return func(self, *args, **kwargs)
|
||||||
|
|
||||||
|
# 会话无效,记录日志并返回 None
|
||||||
|
logger = getattr(self, 'logger', None)
|
||||||
|
if logger:
|
||||||
|
logger.warning(f"会话无效: {func.__name__} 需要有效会话")
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|||||||
@@ -9,5 +9,13 @@ from .log_text import LogText
|
|||||||
from .production_id_input import ProductionIdInput
|
from .production_id_input import ProductionIdInput
|
||||||
from .log_handler import GuiTextHandler
|
from .log_handler import GuiTextHandler
|
||||||
from .delete_progress_window import DeleteProgressWindow
|
from .delete_progress_window import DeleteProgressWindow
|
||||||
|
from .checkbox_treeview import CheckboxTreeview
|
||||||
|
|
||||||
__all__ = ['FileSelector', 'LogText', 'ProductionIdInput', 'GuiTextHandler', 'DeleteProgressWindow']
|
__all__ = [
|
||||||
|
'FileSelector',
|
||||||
|
'LogText',
|
||||||
|
'ProductionIdInput',
|
||||||
|
'GuiTextHandler',
|
||||||
|
'DeleteProgressWindow',
|
||||||
|
'CheckboxTreeview'
|
||||||
|
]
|
||||||
|
|||||||
242
gui/widgets/checkbox_treeview.py
Normal file
242
gui/widgets/checkbox_treeview.py
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
CheckboxTreeview 组件
|
||||||
|
|
||||||
|
支持 checkbox 的 Treeview 组件,使用 Unicode 字符模拟 checkbox:
|
||||||
|
- ☐ 未选中
|
||||||
|
- ☑ 选中
|
||||||
|
"""
|
||||||
|
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import ttk
|
||||||
|
|
||||||
|
|
||||||
|
class CheckboxTreeview(ttk.Treeview):
|
||||||
|
"""支持 checkbox 的 Treeview 组件
|
||||||
|
|
||||||
|
使用 Unicode 字符模拟 checkbox:
|
||||||
|
- ☐ 未选中
|
||||||
|
- ☑ 选中
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- Checkbox 点击切换
|
||||||
|
- 排序功能(支持按选择状态和材料名称排序)
|
||||||
|
- 全选/取消全选
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, parent, on_checkbox_change=None, **kwargs):
|
||||||
|
"""初始化 CheckboxTreeview
|
||||||
|
|
||||||
|
Args:
|
||||||
|
parent: 父容器
|
||||||
|
on_checkbox_change: checkbox 状态改变时的回调函数
|
||||||
|
**kwargs: 传递给 Treeview 的参数
|
||||||
|
"""
|
||||||
|
super().__init__(parent, **kwargs)
|
||||||
|
self.checkboxes = {} # item_id -> bool
|
||||||
|
self.checkbox_column = "选择"
|
||||||
|
self.on_checkbox_change = on_checkbox_change # checkbox 状态改变回调
|
||||||
|
|
||||||
|
# 排序状态
|
||||||
|
self.sort_column = None # 当前排序列的列标识符
|
||||||
|
self.sort_direction = None # 'asc', 'desc', 或 None
|
||||||
|
self.sortable_columns = ["选择", "材料名称"] # 可排序的列白名单
|
||||||
|
self.original_headings = {} # 存储原始列标题文本(不含箭头)
|
||||||
|
|
||||||
|
# 存储原始列标题(延迟执行以确保标题已设置)
|
||||||
|
self.after(100, self._store_original_headings)
|
||||||
|
|
||||||
|
# 绑定点击事件
|
||||||
|
self.bind("<Button-1>", self._on_click)
|
||||||
|
# 绑定表头点击事件
|
||||||
|
self.bind("<ButtonRelease-1>", self._on_heading_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)
|
||||||
|
new_state = not current_state
|
||||||
|
self.set_checked(item, new_state)
|
||||||
|
|
||||||
|
# 通知父组件 checkbox 状态已改变
|
||||||
|
if self.on_checkbox_change:
|
||||||
|
self.on_checkbox_change(item, new_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)
|
||||||
|
|
||||||
|
def _store_original_headings(self):
|
||||||
|
"""存储原始列标题文本(不含箭头)"""
|
||||||
|
for col in self['columns']:
|
||||||
|
self.original_headings[col] = self.heading(col, 'text')
|
||||||
|
|
||||||
|
def _get_column_id_from_column_index(self, column_index):
|
||||||
|
"""将列索引 ('#1', '#2') 转换为列标识符
|
||||||
|
|
||||||
|
Args:
|
||||||
|
column_index: 列索引字符串,如 '#1', '#2'
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
列标识符,如 '选择', '材料名称'
|
||||||
|
"""
|
||||||
|
index = int(column_index[1:]) - 1
|
||||||
|
columns = self['columns']
|
||||||
|
if 0 <= index < len(columns):
|
||||||
|
return columns[index]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _on_heading_click(self, event):
|
||||||
|
"""处理表头点击事件,触发排序"""
|
||||||
|
region = self.identify_region(event.x, event.y)
|
||||||
|
|
||||||
|
if region == "heading":
|
||||||
|
column = self.identify_column(event.x)
|
||||||
|
column_id = self._get_column_id_from_column_index(column)
|
||||||
|
|
||||||
|
# 仅对可排序列进行排序
|
||||||
|
if column_id in self.sortable_columns:
|
||||||
|
self._toggle_sort(column_id)
|
||||||
|
|
||||||
|
def _toggle_sort(self, column_id):
|
||||||
|
"""切换指定列的排序状态
|
||||||
|
|
||||||
|
Args:
|
||||||
|
column_id: 列标识符(如 '选择', '材料名称')
|
||||||
|
"""
|
||||||
|
# 确定新的排序方向
|
||||||
|
if self.sort_column == column_id:
|
||||||
|
# 同一列:asc -> desc -> None
|
||||||
|
if self.sort_direction == 'asc':
|
||||||
|
new_direction = 'desc'
|
||||||
|
elif self.sort_direction == 'desc':
|
||||||
|
new_direction = None
|
||||||
|
else:
|
||||||
|
new_direction = 'asc'
|
||||||
|
else:
|
||||||
|
# 不同列:从升序开始
|
||||||
|
new_direction = 'asc'
|
||||||
|
|
||||||
|
# 应用排序
|
||||||
|
if new_direction:
|
||||||
|
self._sort_by_column(column_id, new_direction)
|
||||||
|
self.sort_column = column_id
|
||||||
|
self.sort_direction = new_direction
|
||||||
|
else:
|
||||||
|
# 清除排序状态
|
||||||
|
self.sort_column = None
|
||||||
|
self.sort_direction = None
|
||||||
|
|
||||||
|
# 更新表头显示
|
||||||
|
self._update_heading_display()
|
||||||
|
|
||||||
|
def _sort_by_column(self, column_id, direction):
|
||||||
|
"""按指定列和方向排序
|
||||||
|
|
||||||
|
Args:
|
||||||
|
column_id: 列标识符
|
||||||
|
direction: 'asc' 或 'desc'
|
||||||
|
"""
|
||||||
|
# 收集所有项目及其数据和复选框状态
|
||||||
|
items_data = []
|
||||||
|
for item in self.get_children():
|
||||||
|
values = self.item(item, "values")
|
||||||
|
checkbox_state = self.checkboxes.get(item, False)
|
||||||
|
items_data.append({
|
||||||
|
'item_id': item,
|
||||||
|
'values': values,
|
||||||
|
'checked': checkbox_state
|
||||||
|
})
|
||||||
|
|
||||||
|
# 根据列和方向排序
|
||||||
|
if column_id == "选择":
|
||||||
|
# 按复选框状态排序(选中在前,未选中在后)
|
||||||
|
items_data.sort(key=lambda x: x['checked'], reverse=(direction == 'desc'))
|
||||||
|
elif column_id == "材料名称":
|
||||||
|
# 按材料名称排序
|
||||||
|
items_data.sort(
|
||||||
|
key=lambda x: str(x['values'][1]) if len(x['values']) > 1 else "",
|
||||||
|
reverse=(direction == 'desc')
|
||||||
|
)
|
||||||
|
|
||||||
|
# 重新排列项目顺序(使用 detach 和 move 保留项目ID和状态)
|
||||||
|
for item_data in items_data:
|
||||||
|
self.move(item_data['item_id'], '', 'end')
|
||||||
|
|
||||||
|
def _update_heading_display(self):
|
||||||
|
"""更新列标题显示(添加/移除排序箭头)"""
|
||||||
|
for col in self['columns']:
|
||||||
|
original = self.original_headings.get(col, col)
|
||||||
|
if col == self.sort_column:
|
||||||
|
# 添加排序箭头
|
||||||
|
arrow = " ↑" if self.sort_direction == 'asc' else " ↓"
|
||||||
|
self.heading(col, text=original + arrow)
|
||||||
|
else:
|
||||||
|
# 移除箭头,显示原始标题
|
||||||
|
self.heading(col, text=original)
|
||||||
Reference in New Issue
Block a user