Files
playwrite/gui/widgets/delete_progress_window.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

461 lines
15 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 -*-
"""
删除进度窗口组件
显示删除操作的进度和日志,完成后显示 Markdown 格式的报告。
"""
import tkinter as tk
from tkinter import ttk, scrolledtext
from typing import Optional, Callable
from datetime import datetime
from gui.log_config import LogTheme
from gui.widgets.base_dialog import BaseDialog
# 尝试导入 tkinterweb 和 markdown2
try:
from tkinterweb import HtmlFrame
HAS_TKINTERWEB = True
except ImportError:
HAS_TKINTERWEB = False
try:
import markdown2
HAS_MARKDOWN2 = True
except ImportError:
HAS_MARKDOWN2 = False
class DeleteProgressWindow(BaseDialog):
"""删除进度窗口"""
def __init__(
self,
parent,
title: str = "执行删除",
managers: str = "",
dryrun: bool = False,
on_cancel: Optional[Callable] = None
):
"""
初始化删除进度窗口
Args:
parent: 父窗口
title: 窗口标题
managers: 负责人列表字符串
dryrun: 是否为预览模式
on_cancel: 取消回调函数
"""
self.on_cancel = on_cancel
self.cancelled = False
self.managers = managers
self.dryrun = dryrun
self.main_frame = None
self.progress_frame = None
self.progress_var = None
self.progress_label = None
self.progress_bar = None
self.log_frame = None
self.log_text = None
self.report_frame = None
self.report_html = None
self.report_text = None
self.cancel_button = None
self.close_button = None
# 调用父类初始化(会自动设置为模态并居中)
super().__init__(parent, title)
# 设置可调整大小
self.resizable(True, True)
# 创建内容
self._create_widgets()
# 设置固定大小并重新居中
self._set_fixed_size(700, 600)
def _set_fixed_size(self, width: int, height: int):
"""
设置固定大小并重新居中
Args:
width: 宽度
height: 高度
"""
self.update_idletasks()
self.geometry(f"{width}x{height}")
self._center_window()
def _create_widgets(self):
"""创建窗口组件"""
# 主容器
self.main_frame = ttk.Frame(self, padding=10)
self.main_frame.pack(fill=tk.BOTH, expand=True)
# 信息区域
info_frame = ttk.Frame(self.main_frame)
info_frame.pack(fill=tk.X, pady=(0, 10))
# 负责人信息
if self.managers:
ttk.Label(info_frame, text=f"负责人: {self.managers}").pack(anchor="w")
# 模式信息
mode_text = "预览模式 (不保存)" if self.dryrun else "正常执行"
mode_label = ttk.Label(info_frame, text=f"模式: {mode_text}")
mode_label.pack(anchor="w")
# 进度区域
self.progress_frame = ttk.LabelFrame(self.main_frame, text="进度", padding=5)
self.progress_frame.pack(fill=tk.X, pady=(0, 10))
self.progress_var = tk.StringVar(value="准备中...")
self.progress_label = ttk.Label(self.progress_frame, textvariable=self.progress_var)
self.progress_label.pack(anchor="w")
self.progress_bar = ttk.Progressbar(
self.progress_frame,
mode='determinate',
length=660,
maximum=100
)
self.progress_bar.pack(fill=tk.X, pady=5)
# 日志区域(执行过程中显示)
self.log_frame = ttk.LabelFrame(self.main_frame, text="日志", padding=5)
self.log_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
self.log_text = scrolledtext.ScrolledText(
self.log_frame,
height=10,
wrap=tk.WORD,
state=tk.DISABLED,
font=('Consolas', 9)
)
self.log_text.pack(fill=tk.BOTH, expand=True)
# 配置日志标签颜色(使用统一主题)
self.log_text.tag_configure('info', foreground=LogTheme.get_color('INFO'))
self.log_text.tag_configure('success', foreground=LogTheme.get_color('SUCCESS'))
self.log_text.tag_configure('warning', foreground=LogTheme.get_color('WARNING'))
self.log_text.tag_configure('error', foreground=LogTheme.get_color('ERROR'))
# 报告区域(完成后显示)- 初始隐藏
self.report_frame = ttk.LabelFrame(self.main_frame, text="执行报告", padding=5)
# 根据 tkinterweb 可用性选择渲染方式
if HAS_TKINTERWEB:
# 使用 HtmlFrame 渲染 HTML
self.report_html = HtmlFrame(self.report_frame)
self.report_html.pack(fill=tk.BOTH, expand=True)
else:
# 降级为文本显示
self.report_text = scrolledtext.ScrolledText(
self.report_frame,
height=20,
wrap=tk.WORD,
state=tk.DISABLED,
font=('Consolas', 9)
)
self.report_text.pack(fill=tk.BOTH, expand=True)
# 按钮区域
button_frame = ttk.Frame(self.main_frame)
button_frame.pack(fill=tk.X)
self.cancel_button = ttk.Button(
button_frame,
text="取消执行",
command=self._on_cancel
)
self.cancel_button.pack(side=tk.RIGHT)
# 关闭按钮(初始隐藏)
self.close_button = ttk.Button(
button_frame,
text="关闭",
command=self.close
)
def _on_cancel(self):
"""处理取消操作"""
self.cancelled = True
self.cancel_button.config(state=tk.DISABLED, text="正在取消...")
if self.on_cancel:
self.on_cancel()
else:
self.append_log("用户取消了操作", "warning")
def update_progress(self, current: int, total: int, message: str):
"""
更新进度
Args:
current: 当前进度值
total: 总数
message: 进度消息
"""
if total > 0:
percentage = int((current / total) * 100)
self.progress_bar['value'] = percentage
self.progress_var.set(message)
else:
self.progress_var.set(message)
self.update_idletasks()
def append_log(self, message: str, level: str = "info"):
"""
追加日志
Args:
message: 日志消息
level: 日志级别 (info, success, warning, error)
"""
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
log_entry = f"[{timestamp}] {message}\n"
self.log_text.config(state=tk.NORMAL)
self.log_text.insert(tk.END, log_entry, level)
self.log_text.see(tk.END)
self.log_text.config(state=tk.DISABLED)
self.update_idletasks()
def show_report(self, markdown_content: str):
"""
显示报告
Args:
markdown_content: Markdown 格式的报告内容
"""
# 隐藏进度区域和日志区域
self.progress_frame.pack_forget()
self.log_frame.pack_forget()
# 显示报告区域
self.report_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
# 根据可用库选择渲染方式
if HAS_TKINTERWEB and HAS_MARKDOWN2:
# 使用 tkinterweb 渲染 HTML
html_content = self._markdown_to_html(markdown_content)
self.report_html.load_html(html_content)
elif HAS_TKINTERWEB:
# 只有 tkinterweb使用简单 HTML
html_content = self._markdown_to_simple_html(markdown_content)
self.report_html.load_html(html_content)
else:
# 降级为文本显示
text_content = self._markdown_to_text(markdown_content)
self.report_text.config(state=tk.NORMAL)
self.report_text.delete(1.0, tk.END)
self.report_text.insert(tk.END, text_content)
self.report_text.config(state=tk.DISABLED)
# 更新标题
self.title("执行报告")
# 隐藏取消按钮,显示关闭按钮
self.cancel_button.pack_forget()
self.close_button.pack(side=tk.RIGHT)
# 更新进度标签
self.progress_var.set("执行完成")
def _markdown_to_html(self, markdown_content: str) -> str:
"""
将 Markdown 转换为 HTML
Args:
markdown_content: Markdown 内容
Returns:
HTML 内容
"""
# 使用 markdown2 转换
html_body = markdown2.markdown(
markdown_content,
extras=['tables', 'fenced-code-blocks']
)
# 添加样式
html_content = f"""
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body {{
font-family: "Microsoft YaHei", "Segoe UI", Arial, sans-serif;
font-size: 12px;
padding: 10px;
line-height: 1.6;
background-color: #ffffff;
}}
h1 {{
color: #2c3e50;
border-bottom: 2px solid #3498db;
padding-bottom: 10px;
font-size: 18px;
}}
h2 {{
color: #34495e;
border-bottom: 1px solid #bdc3c7;
padding-bottom: 5px;
margin-top: 20px;
font-size: 14px;
}}
table {{
border-collapse: collapse;
width: 100%;
margin: 10px 0;
}}
th, td {{
border: 1px solid #bdc3c7;
padding: 8px;
text-align: left;
}}
th {{
background-color: #3498db;
color: white;
}}
tr:nth-child(even) {{
background-color: #f2f2f2;
}}
ul {{
list-style-type: disc;
padding-left: 20px;
}}
li {{
margin: 5px 0;
}}
.success {{ color: #27ae60; }}
.warning {{ color: #f39c12; }}
.error {{ color: #e74c3c; }}
</style>
</head>
<body>
{html_body}
</body>
</html>
"""
return html_content
def _markdown_to_simple_html(self, markdown_content: str) -> str:
"""
将 Markdown 转换为简单 HTML不依赖 markdown2
Args:
markdown_content: Markdown 内容
Returns:
HTML 内容
"""
lines = markdown_content.split('\n')
html_parts = ['<!DOCTYPE html><html><head><meta charset="UTF-8">',
'<style>',
'body { font-family: "Microsoft YaHei", Arial, sans-serif; font-size: 12px; padding: 10px; }',
'h1 { color: #2c3e50; border-bottom: 2px solid #3498db; padding-bottom: 10px; }',
'h2 { color: #34495e; border-bottom: 1px solid #bdc3c7; margin-top: 20px; }',
'table { border-collapse: collapse; width: 100%; margin: 10px 0; }',
'th, td { border: 1px solid #bdc3c7; padding: 8px; text-align: left; }',
'th { background-color: #3498db; color: white; }',
'</style></head><body>']
in_table = False
for line in lines:
if line.startswith('# '):
html_parts.append(f'<h1>{line[2:]}</h1>')
elif line.startswith('## '):
html_parts.append(f'<h2>{line[3:]}</h2>')
elif line.startswith('| '):
if not in_table:
html_parts.append('<table>')
in_table = True
# 检查是否是表头分隔行
if '|--' in line or '|-' in line:
continue
cells = [cell.strip() for cell in line.split('|')[1:-1]]
if cells:
# 第一行作为表头
if html_parts[-1] == '<table>':
html_parts.append('<tr>' + ''.join(f'<th>{c}</th>' for c in cells) + '</tr>')
else:
html_parts.append('<tr>' + ''.join(f'<td>{c}</td>' for c in cells) + '</tr>')
elif line.startswith('- '):
if in_table:
html_parts.append('</table>')
in_table = False
html_parts.append(f'<li>{line[2:]}</li>')
elif line.strip() == '':
if in_table:
html_parts.append('</table>')
in_table = False
html_parts.append('<br>')
else:
if in_table:
html_parts.append('</table>')
in_table = False
if line.strip():
html_parts.append(f'<p>{line}</p>')
if in_table:
html_parts.append('</table>')
html_parts.append('</body></html>')
return '\n'.join(html_parts)
def _markdown_to_text(self, markdown_content: str) -> str:
"""
将 Markdown 转换为简单的文本格式
Args:
markdown_content: Markdown 内容
Returns:
格式化后的文本
"""
lines = markdown_content.split('\n')
result = []
for line in lines:
# 标题
if line.startswith('# '):
result.append('=' * 60)
result.append(line[2:])
result.append('=' * 60)
elif line.startswith('## '):
result.append('')
result.append(line[3:])
result.append('-' * 40)
elif line.startswith('| '):
# 表格行 - 保持原样
result.append(line)
elif line.startswith('|--') or line.startswith('|-'):
# 表格分隔线 - 跳过
continue
elif line.startswith('- '):
# 列表项
result.append(' ' + line)
elif line.strip() == '':
result.append('')
else:
result.append(line)
return '\n'.join(result)
def close(self):
"""关闭窗口"""
self.destroy()
def is_cancelled(self) -> bool:
"""检查是否已取消"""
return self.cancelled
def set_completed(self):
"""设置为完成状态"""
self.cancel_button.pack_forget()
self.close_button.pack(side=tk.RIGHT)