Files
playwrite/gui/utils/validators.py
Misaka Server f715cb97e3 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>
2026-02-26 18:40:26 +08:00

432 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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