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>
432 lines
12 KiB
Python
432 lines
12 KiB
Python
#!/usr/bin/env python
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
Validators - 输入验证框架
|
||
|
||
提供统一的输入验证机制,用于 GUI 表单验证
|
||
"""
|
||
import os
|
||
import re
|
||
from datetime import datetime
|
||
from typing import Any, Callable, List, Optional
|
||
from tkinter import ttk
|
||
|
||
|
||
class ValidationResult:
|
||
"""
|
||
验证结果
|
||
|
||
表示验证操作的结果,包含是否成功和错误消息
|
||
"""
|
||
|
||
def __init__(self, is_valid: bool, error_message: str = ""):
|
||
"""
|
||
初始化验证结果
|
||
|
||
Args:
|
||
is_valid: 是否验证通过
|
||
error_message: 错误消息(验证失败时)
|
||
"""
|
||
self.is_valid = is_valid
|
||
self.error_message = error_message
|
||
|
||
def __bool__(self) -> bool:
|
||
"""允许直接在 if 语句中使用"""
|
||
return self.is_valid
|
||
|
||
def __str__(self) -> str:
|
||
"""返回错误消息或"验证通过" """
|
||
return self.error_message if not self.is_valid else "验证通过"
|
||
|
||
|
||
class Validator:
|
||
"""
|
||
输入验证器基类
|
||
|
||
提供常用的静态验证方法
|
||
"""
|
||
|
||
@staticmethod
|
||
def not_empty(value: Any, field_name: str = "字段") -> ValidationResult:
|
||
"""
|
||
非空验证
|
||
|
||
Args:
|
||
value: 要验证的值
|
||
field_name: 字段名称(用于错误消息)
|
||
|
||
Returns:
|
||
验证结果
|
||
"""
|
||
if value is None or value == "":
|
||
return ValidationResult(False, f"{field_name}不能为空")
|
||
if isinstance(value, str) and not value.strip():
|
||
return ValidationResult(False, f"{field_name}不能为空或仅包含空格")
|
||
return ValidationResult(True)
|
||
|
||
@staticmethod
|
||
def file_exists(path: str, field_name: str = "文件") -> ValidationResult:
|
||
"""
|
||
文件存在性验证
|
||
|
||
Args:
|
||
path: 文件路径
|
||
field_name: 字段名称
|
||
|
||
Returns:
|
||
验证结果
|
||
"""
|
||
if not path:
|
||
return ValidationResult(False, f"{field_name}路径不能为空")
|
||
if not os.path.exists(path):
|
||
return ValidationResult(False, f"{field_name}不存在: {path}")
|
||
return ValidationResult(True)
|
||
|
||
@staticmethod
|
||
def dir_exists(path: str, field_name: str = "目录") -> ValidationResult:
|
||
"""
|
||
目录存在性验证
|
||
|
||
Args:
|
||
path: 目录路径
|
||
field_name: 字段名称
|
||
|
||
Returns:
|
||
验证结果
|
||
"""
|
||
if not path:
|
||
return ValidationResult(False, f"{field_name}路径不能为空")
|
||
if not os.path.exists(path):
|
||
return ValidationResult(False, f"{field_name}不存在: {path}")
|
||
if not os.path.isdir(path):
|
||
return ValidationResult(False, f"{field_name}不是有效的目录: {path}")
|
||
return ValidationResult(True)
|
||
|
||
@staticmethod
|
||
def date_format(value: str, format_str: str = "%Y-%m-%d", field_name: str = "日期") -> ValidationResult:
|
||
"""
|
||
日期格式验证
|
||
|
||
Args:
|
||
value: 日期字符串
|
||
format_str: 期望的日期格式
|
||
field_name: 字段名称
|
||
|
||
Returns:
|
||
验证结果
|
||
"""
|
||
if not value:
|
||
return ValidationResult(False, f"{field_name}不能为空")
|
||
try:
|
||
datetime.strptime(value, format_str)
|
||
return ValidationResult(True)
|
||
except ValueError:
|
||
return ValidationResult(False, f"{field_name}格式错误,期望格式: {format_str}")
|
||
|
||
@staticmethod
|
||
def numeric(value: Any, field_name: str = "数值", min_value: Optional[float] = None,
|
||
max_value: Optional[float] = None) -> ValidationResult:
|
||
"""
|
||
数值验证
|
||
|
||
Args:
|
||
value: 要验证的值
|
||
field_name: 字段名称
|
||
min_value: 最小值(可选)
|
||
max_value: 最大值(可选)
|
||
|
||
Returns:
|
||
验证结果
|
||
"""
|
||
if value is None or value == "":
|
||
return ValidationResult(False, f"{field_name}不能为空")
|
||
|
||
try:
|
||
num = float(value)
|
||
except (ValueError, TypeError):
|
||
return ValidationResult(False, f"{field_name}必须是有效的数字")
|
||
|
||
if min_value is not None and num < min_value:
|
||
return ValidationResult(False, f"{field_name}不能小于 {min_value}")
|
||
|
||
if max_value is not None and num > max_value:
|
||
return ValidationResult(False, f"{field_name}不能大于 {max_value}")
|
||
|
||
return ValidationResult(True)
|
||
|
||
@staticmethod
|
||
def integer(value: Any, field_name: str = "整数") -> ValidationResult:
|
||
"""
|
||
整数验证
|
||
|
||
Args:
|
||
value: 要验证的值
|
||
field_name: 字段名称
|
||
|
||
Returns:
|
||
验证结果
|
||
"""
|
||
if value is None or value == "":
|
||
return ValidationResult(False, f"{field_name}不能为空")
|
||
|
||
try:
|
||
int(value)
|
||
return ValidationResult(True)
|
||
except (ValueError, TypeError):
|
||
return ValidationResult(False, f"{field_name}必须是有效的整数")
|
||
|
||
@staticmethod
|
||
def length(value: str, min_length: int = 0, max_length: Optional[int] = None,
|
||
field_name: str = "字段") -> ValidationResult:
|
||
"""
|
||
字符串长度验证
|
||
|
||
Args:
|
||
value: 要验证的字符串
|
||
min_length: 最小长度
|
||
max_length: 最大长度(可选)
|
||
field_name: 字段名称
|
||
|
||
Returns:
|
||
验证结果
|
||
"""
|
||
if not isinstance(value, str):
|
||
return ValidationResult(False, f"{field_name}必须是字符串")
|
||
|
||
length = len(value)
|
||
|
||
if length < min_length:
|
||
return ValidationResult(False, f"{field_name}长度不能少于 {min_length} 个字符")
|
||
|
||
if max_length is not None and length > max_length:
|
||
return ValidationResult(False, f"{field_name}长度不能超过 {max_length} 个字符")
|
||
|
||
return ValidationResult(True)
|
||
|
||
@staticmethod
|
||
def regex(value: str, pattern: str, field_name: str = "字段") -> ValidationResult:
|
||
"""
|
||
正则表达式验证
|
||
|
||
Args:
|
||
value: 要验证的字符串
|
||
pattern: 正则表达式模式
|
||
field_name: 字段名称
|
||
|
||
Returns:
|
||
验证结果
|
||
"""
|
||
if not isinstance(value, str):
|
||
return ValidationResult(False, f"{field_name}必须是字符串")
|
||
|
||
if not re.match(pattern, value):
|
||
return ValidationResult(False, f"{field_name}格式不正确")
|
||
return ValidationResult(True)
|
||
|
||
@staticmethod
|
||
def email(value: str, field_name: str = "邮箱") -> ValidationResult:
|
||
"""
|
||
邮箱格式验证
|
||
|
||
Args:
|
||
value: 邮箱地址
|
||
field_name: 字段名称
|
||
|
||
Returns:
|
||
验证结果
|
||
"""
|
||
if not value:
|
||
return ValidationResult(False, f"{field_name}不能为空")
|
||
|
||
# 简单的邮箱正则表达式
|
||
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
||
return Validator.regex(value, pattern, field_name)
|
||
|
||
@staticmethod
|
||
def phone(value: str, field_name: str = "手机号") -> ValidationResult:
|
||
"""
|
||
手机号验证(中国大陆)
|
||
|
||
Args:
|
||
value: 手机号
|
||
field_name: 字段名称
|
||
|
||
Returns:
|
||
验证结果
|
||
"""
|
||
if not value:
|
||
return ValidationResult(False, f"{field_name}不能为空")
|
||
|
||
# 中国大陆手机号正则表达式
|
||
pattern = r'^1[3-9]\d{9}$'
|
||
return Validator.regex(value, pattern, field_name)
|
||
|
||
@staticmethod
|
||
def in_range(value: Any, allowed_values: List[Any], field_name: str = "字段") -> ValidationResult:
|
||
"""
|
||
值范围验证
|
||
|
||
Args:
|
||
value: 要验证的值
|
||
allowed_values: 允许的值列表
|
||
field_name: 字段名称
|
||
|
||
Returns:
|
||
验证结果
|
||
"""
|
||
if value not in allowed_values:
|
||
return ValidationResult(
|
||
False,
|
||
f"{field_name}必须是以下值之一: {', '.join(str(v) for v in allowed_values)}"
|
||
)
|
||
return ValidationResult(True)
|
||
|
||
@staticmethod
|
||
def custom(value: Any, validator_func: Callable[[Any], bool],
|
||
error_message: str = "验证失败") -> ValidationResult:
|
||
"""
|
||
自定义验证函数
|
||
|
||
Args:
|
||
value: 要验证的值
|
||
validator_func: 验证函数,返回 True 表示验证通过
|
||
error_message: 错误消息
|
||
|
||
Returns:
|
||
验证结果
|
||
"""
|
||
try:
|
||
if validator_func(value):
|
||
return ValidationResult(True)
|
||
return ValidationResult(False, error_message)
|
||
except Exception as e:
|
||
return ValidationResult(False, f"验证过程出错: {str(e)}")
|
||
|
||
|
||
class ValidatedWidget:
|
||
"""
|
||
带验证功能的 Widget 基类
|
||
|
||
为任何支持 get_value() 方法的组件添加验证功能
|
||
"""
|
||
|
||
def __init__(self, widget: ttk.Widget, error_label: Optional[ttk.Label] = None):
|
||
"""
|
||
初始化验证组件
|
||
|
||
Args:
|
||
widget: 要验证的组件(必须支持 get_value() 方法)
|
||
error_label: 用于显示错误消息的 Label(可选)
|
||
"""
|
||
self.widget = widget
|
||
self.error_label = error_label
|
||
self.validators: List[Callable[[Any], ValidationResult]] = []
|
||
self._last_result: Optional[ValidationResult] = None
|
||
|
||
def add_validator(self, validator: Callable[[Any], ValidationResult]):
|
||
"""
|
||
添加验证器
|
||
|
||
Args:
|
||
validator: 验证函数,接收值并返回 ValidationResult
|
||
"""
|
||
self.validators.append(validator)
|
||
|
||
def add_not_empty_validator(self, field_name: str = "字段"):
|
||
"""
|
||
添加非空验证器
|
||
|
||
Args:
|
||
field_name: 字段名称
|
||
"""
|
||
self.add_validator(lambda v: Validator.not_empty(v, field_name))
|
||
|
||
def add_file_exists_validator(self, field_name: str = "文件"):
|
||
"""
|
||
添加文件存在性验证器
|
||
|
||
Args:
|
||
field_name: 字段名称
|
||
"""
|
||
self.add_validator(lambda v: Validator.file_exists(v, field_name))
|
||
|
||
def add_date_format_validator(self, format_str: str = "%Y-%m-%d", field_name: str = "日期"):
|
||
"""
|
||
添加日期格式验证器
|
||
|
||
Args:
|
||
format_str: 日期格式
|
||
field_name: 字段名称
|
||
"""
|
||
self.add_validator(lambda v: Validator.date_format(v, format_str, field_name))
|
||
|
||
def add_custom_validator(self, validator_func: Callable[[Any], bool], error_message: str = "验证失败"):
|
||
"""
|
||
添加自定义验证器
|
||
|
||
Args:
|
||
validator_func: 验证函数
|
||
error_message: 错误消息
|
||
"""
|
||
self.add_validator(lambda v: Validator.custom(v, validator_func, error_message))
|
||
|
||
def validate(self) -> bool:
|
||
"""
|
||
执行所有验证
|
||
|
||
Returns:
|
||
True 如果所有验证都通过,False 否则
|
||
"""
|
||
value = self._get_value()
|
||
|
||
for validator in self.validators:
|
||
result = validator(value)
|
||
self._last_result = result
|
||
|
||
if not result.is_valid:
|
||
self._show_error(result.error_message)
|
||
return False
|
||
|
||
self._clear_error()
|
||
return True
|
||
|
||
def _get_value(self) -> Any:
|
||
"""
|
||
获取组件的值
|
||
|
||
Returns:
|
||
组件的当前值
|
||
"""
|
||
if hasattr(self.widget, 'get'):
|
||
return self.widget.get()
|
||
elif hasattr(self.widget, 'cget'):
|
||
# 对于某些组件,尝试获取配置值
|
||
return self.widget.cget('text')
|
||
else:
|
||
raise AttributeError(f"Widget {type(self.widget).__name__} 不支持获取值")
|
||
|
||
def _show_error(self, message: str):
|
||
"""
|
||
显示错误消息
|
||
|
||
Args:
|
||
message: 错误消息
|
||
"""
|
||
if self.error_label:
|
||
self.error_label.config(text=message, foreground="red")
|
||
# 也可以添加其他错误显示方式,比如改变组件边框颜色
|
||
|
||
def _clear_error(self):
|
||
"""清除错误消息"""
|
||
if self.error_label:
|
||
self.error_label.config(text="")
|
||
|
||
def get_last_result(self) -> Optional[ValidationResult]:
|
||
"""
|
||
获取最后一次验证结果
|
||
|
||
Returns:
|
||
最后一次验证的 ValidationResult
|
||
"""
|
||
return self._last_result
|