Add CSS properties to ensure table cells wrap long content properly: - table-layout: fixed for consistent column widths - word-wrap and overflow-wrap: break-word for content wrapping Co-Authored-By: Claude (glm-5) <noreply@anthropic.com>
451 lines
14 KiB
Python
451 lines
14 KiB
Python
#!/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
|
||
|
||
# 尝试导入 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:
|
||
"""删除进度窗口"""
|
||
|
||
def __init__(
|
||
self,
|
||
parent,
|
||
title: str = "执行删除",
|
||
managers: str = "",
|
||
dryrun: bool = False,
|
||
on_cancel: Optional[Callable] = None,
|
||
):
|
||
"""
|
||
初始化删除进度窗口
|
||
|
||
Args:
|
||
parent: 父窗口
|
||
title: 窗口标题
|
||
managers: 负责人列表字符串
|
||
dryrun: 是否为预览模式
|
||
on_cancel: 取消回调函数
|
||
"""
|
||
self.parent = parent
|
||
self.on_cancel = on_cancel
|
||
self.cancelled = False
|
||
self.managers = managers
|
||
self.dryrun = dryrun
|
||
|
||
# 创建窗口
|
||
self.window = tk.Toplevel(parent)
|
||
self.window.title(title)
|
||
self.window.resizable(True, True)
|
||
self.window.transient(parent)
|
||
|
||
# 设置窗口大小
|
||
self.window.geometry("700x600")
|
||
|
||
# 创建内容
|
||
self._create_widgets()
|
||
|
||
# 居中显示
|
||
self._center()
|
||
|
||
def _center(self):
|
||
"""将窗口居中显示"""
|
||
self.window.update_idletasks()
|
||
width = 700
|
||
height = 600
|
||
x = (self.window.winfo_screenwidth() // 2) - (width // 2)
|
||
y = (self.window.winfo_screenheight() // 2) - (height // 2)
|
||
self.window.geometry(f"{width}x{height}+{x}+{y}")
|
||
|
||
def _create_widgets(self):
|
||
"""创建窗口组件"""
|
||
# 主容器
|
||
self.main_frame = ttk.Frame(self.window, 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="black")
|
||
self.log_text.tag_configure("success", foreground="green")
|
||
self.log_text.tag_configure("warning", foreground="orange")
|
||
self.log_text.tag_configure("error", foreground="red")
|
||
|
||
# 报告区域(完成后显示)- 初始隐藏
|
||
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.window.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.window.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.window.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;
|
||
table-layout: fixed;
|
||
}}
|
||
th, td {{
|
||
border: 1px solid #bdc3c7;
|
||
padding: 8px;
|
||
text-align: left;
|
||
word-wrap: break-word;
|
||
overflow-wrap: break-word;
|
||
}}
|
||
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; table-layout: fixed; }",
|
||
"th, td { border: 1px solid #bdc3c7; padding: 8px; text-align: left; word-wrap: break-word; overflow-wrap: break-word; }",
|
||
"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.window.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)
|