refactor: implement UI code optimization and refactoring

This commit implements the comprehensive UI code refactoring plan to improve
code quality, reduce duplication, and enhance maintainability.

Phase 1: High Priority Improvements
- Create BaseDialog class to eliminate window centering code duplication
  across ProgressDialog, DeleteProgressWindow, LoginDialog, and
  UserSelectionDialog (~30 lines of duplicate code removed)
- Unify log color configuration with LogTheme class in gui/log_config.py
- Create permission check decorators (@require_admin, @require_permission,
  @require_user_type, @handle_errors)
- Update all dialog classes to use unified patterns

Phase 2: Architecture Improvements
- Create input validation framework (ValidationResult, Validator,
  ValidatedWidget classes)
- Implement StateManager with observer pattern for component state sharing
- Create unified ErrorHandler for consistent error handling
- Extract CheckboxTreeview into reusable component

New Modules:
- gui/widgets/base_dialog.py - Base dialog class with modal setup and centering
- gui/utils/decorators.py - Permission and error handling decorators
- gui/utils/error_handler.py - Unified error handling with user-friendly messages
- gui/utils/state_manager.py - State management with observer pattern
- gui/utils/validators.py - Input validation framework
- gui/material_validation/checkbox_treeview.py - Reusable checkbox table

Modified Files:
- gui/log_config.py - Added LogTheme class for centralized styling
- gui/widgets/log_text.py - Use LogTheme.COLORS
- gui/widgets/progress_dialog.py - Inherit from BaseDialog
- gui/widgets/delete_progress_window.py - Inherit from BaseDialog, use LogTheme
- gui/widgets/__init__.py - Add new exports, optional imports
- gui/login_dialog.py - Use unified centering pattern
- gui/user_selection_dialog.py - Use unified centering pattern
- gui/material_validation_tab.py - Import CheckboxTreeview from new module

Benefits:
- Reduced code duplication by ~200 lines
- Improved maintainability through centralized configuration
- Better abstraction with 5 new reusable base classes
- Enhanced type safety with type hints
- Future-proof theming support via LogTheme

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Misaka Server
2026-02-26 18:40:26 +08:00
parent 3a9c6f0978
commit f715cb97e3
16 changed files with 1735 additions and 313 deletions

View File

@@ -0,0 +1,319 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
CheckboxTreeview - 支持复选框和排序的高级表格组件
使用 Unicode 字符模拟 checkbox
- ☐ 未选中
- ☑ 选中
支持点击列标题进行排序
"""
import tkinter as tk
from tkinter import ttk
from typing import Callable, Optional, List
class CheckboxTreeview(ttk.Treeview):
"""
支持 checkbox 的 Treeview 组件
Features:
- Checkbox 列支持(第一列)
- 点击列标题排序(选择列、材料名称列)
- Checkbox 状态变化回调
- 全选/取消全选功能
Usage:
def on_checkbox_change(item_id, is_checked):
print(f"Item {item_id} checked: {is_checked}")
tree = CheckboxTreeview(
parent,
columns=('选择', '材料名称', '状态'),
on_checkbox_change=on_checkbox_change
)
tree.pack()
"""
def __init__(
self,
parent,
on_checkbox_change: Optional[Callable[[str, bool], None]] = None,
**kwargs
):
"""
初始化 CheckboxTreeview
Args:
parent: 父容器
on_checkbox_change: checkbox 状态改变时的回调函数 (item_id, is_checked) -> None
**kwargs: 传递给 Treeview 的参数
"""
super().__init__(parent, **kwargs)
# Checkbox 状态管理
self.checkboxes: dict = {} # item_id -> bool
self.checkbox_column = "选择"
self.on_checkbox_change = on_checkbox_change
# 排序状态管理
self.sort_column: Optional[str] = None # 当前排序列的列标识符
self.sort_direction: Optional[str] = None # 'asc', 'desc', 或 None
self.sortable_columns: List[str] = ["选择", "材料名称"] # 可排序的列白名单
self.original_headings: dict = {} # 存储原始列标题文本(不含箭头)
# 存储原始列标题(延迟执行以确保标题已设置)
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 状态
Args:
event: 鼠标点击事件
"""
# 获取点击位置对应的 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: str, 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[str]:
"""
获取所有选中的 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 状态
Args:
parent: 父节点 ID
index: 插入位置索引
values: 值列表
**kwargs: 其他参数
Returns:
新创建的 item ID
"""
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 状态
Args:
*items: 要删除的 item ID 列表
"""
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: str) -> Optional[str]:
"""
将列索引 ('#1', '#2') 转换为列标识符
Args:
column_index: 列索引字符串,如 '#1', '#2'
Returns:
列标识符,如 '选择', '材料名称',或 None
"""
try:
index = int(column_index[1:]) - 1
columns = self['columns']
if 0 <= index < len(columns):
return columns[index]
except (ValueError, IndexError):
pass
return None
def _on_heading_click(self, event):
"""
处理表头点击事件,触发排序
Args:
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: str):
"""
切换指定列的排序状态
排序状态循环asc -> desc -> None -> asc
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: str, direction: str):
"""
按指定列和方向排序
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')
)
# 重新排列项目顺序(使用 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)
def get_checkbox_state(self, item: str) -> bool:
"""
获取指定 item 的 checkbox 状态
Args:
item: Treeview item ID
Returns:
是否选中
"""
return self.checkboxes.get(item, False)
def set_sortable_columns(self, columns: List[str]):
"""
设置可排序的列
Args:
columns: 列标识符列表
"""
self.sortable_columns = columns.copy()