feat: add delete execution feature with progress tracking and dryrun mode

- Add ExecutionConfig for dryrun settings in config schema
- Create DeleteProgressWindow widget for real-time progress display
- Integrate delete execution flow in MaterialValidationTab with threading
- Add dryrun checkbox for admin users in settings
- Add progress callback support to DiscreteMaterialPlanCleaner
- Add markdown report generation with statistics
- Include tkinterweb and markdown2 dependencies for report rendering

Co-Authored-By: Claude (glm-5) <noreply@anthropic.com>
This commit is contained in:
Misaka_Company
2026-02-26 13:02:48 +08:00
parent 63601a994f
commit 8d51c5b368
8 changed files with 957 additions and 18 deletions

View File

@@ -134,6 +134,11 @@ class ConfigManager:
"""获取提取配置"""
return self.config.extraction
@property
def execution(self):
"""获取执行配置"""
return self.config.execution
# 为了向后兼容,保留旧版本的导入
DEFAULT_SETTINGS = DEFAULT_SETTINGS_DICT

View File

@@ -18,12 +18,13 @@ from pathlib import Path
from io import StringIO
from contextlib import redirect_stdout
from typing import List, Dict
from gui.widgets import FileSelector, LogText, GuiTextHandler
from gui.widgets import FileSelector, LogText, GuiTextHandler, DeleteProgressWindow
from gui.config_manager import ConfigManager
from gui.log_config import setup_gui_logging, get_logger
from gui.material_type_management_dialog import MaterialTypeManagementDialog
from db.materials_to_be_deleted_dao import MaterialsTypeToBeDeletedDAO
import pandas as pd
import tempfile
class CheckboxTreeview(ttk.Treeview):
@@ -437,6 +438,25 @@ class MaterialValidationTab(ttk.Frame):
)
self.confirm_delete_button.pack(side=tk.LEFT, padx=5)
# 执行删除按钮
self.execute_delete_button = ttk.Button(
button_frame,
text="执行删除",
command=self.start_delete_execution,
state=tk.DISABLED
)
self.execute_delete_button.pack(side=tk.LEFT, padx=5)
# Admin 用户:显示 dryrun 复选框
if self.session_manager.is_admin():
self.dryrun_var = tk.BooleanVar(value=False)
self.dryrun_checkbox = ttk.Checkbutton(
button_frame,
text="预览模式 (不保存)",
variable=self.dryrun_var
)
self.dryrun_checkbox.pack(side=tk.LEFT, padx=5)
self.export_button = ttk.Button(
button_frame,
text="导出结果",
@@ -556,6 +576,9 @@ class MaterialValidationTab(ttk.Frame):
# User 模式:静默更新,不显示任何提示
if is_user_only:
# 启用执行删除按钮(如果有 Production ID
if production_ids and hasattr(self, 'execute_delete_button'):
self.execute_delete_button.config(state=tk.NORMAL)
return
# Admin 模式:显示提示信息
@@ -568,12 +591,18 @@ class MaterialValidationTab(ttk.Frame):
# 显示提示标签
if hasattr(self, 'shared_ids_label') and not self.shared_ids_label.winfo_ismapped():
self.shared_ids_label.pack(anchor="w", pady=(0, 5))
# 启用执行删除按钮
if hasattr(self, 'execute_delete_button'):
self.execute_delete_button.config(state=tk.NORMAL)
else:
self.shared_ids_info_label.config(text="")
# 隐藏提示标签
if hasattr(self, 'shared_ids_label'):
if self.shared_ids_label.winfo_ismapped():
self.shared_ids_label.pack_forget()
# 禁用执行删除按钮
if hasattr(self, 'execute_delete_button'):
self.execute_delete_button.config(state=tk.DISABLED)
def reload_config(self):
"""配置更新后重新加载"""
@@ -1062,6 +1091,14 @@ class MaterialValidationTab(ttk.Frame):
self.deselect_all_button.config(state=tk.NORMAL)
self.confirm_delete_button.config(state=tk.NORMAL)
# 启用执行删除按钮(需要 Production ID
if self.shared_production_ids or (
self.session_manager.is_admin() and
hasattr(self, 'db_filtered_production_id_selector') and
self.db_filtered_production_id_selector.get()
):
self.execute_delete_button.config(state=tk.NORMAL)
# 恢复筛选状态
if hasattr(self, 'previously_selected_managers') and self.previously_selected_managers:
self._restore_manager_filter_state(self.previously_selected_managers)
@@ -1380,3 +1417,175 @@ class MaterialValidationTab(ttk.Frame):
def open_type_management(self):
"""打开类型管理窗口"""
dialog = MaterialTypeManagementDialog(self, self.session_manager, title="物料类型管理")
def start_delete_execution(self):
"""开始执行删除"""
# 1. 获取 Production ID
production_ids = None
is_admin = self.session_manager.is_admin()
if is_admin:
# Admin 用户:检查数据源选择
if hasattr(self, 'production_id_source_var'):
source = self.production_id_source_var.get()
if source == "shared":
production_ids = self.shared_production_ids
else:
production_id_file = self.db_filtered_production_id_selector.get()
if production_id_file and os.path.exists(production_id_file):
from db.production_order_query import read_production_ids
production_ids = read_production_ids(production_id_file)
else:
# 普通用户:使用共享的 Production ID
production_ids = self.shared_production_ids
# 验证 Production ID
if not production_ids:
messagebox.showerror("错误", "没有可用的 Production ID\n请先在校验页面获取数据")
return
# 2. 获取负责人
if is_admin:
manager_names = self._get_selected_managers()
if not manager_names:
messagebox.showwarning("警告", "请至少选择一个负责人")
return
else:
manager_names = [self.session_manager.get_username()]
# 3. 获取 dryrun 设置
dryrun = False
if is_admin and hasattr(self, 'dryrun_var'):
# Admin 用户:使用界面上的 dryrun 复选框
dryrun = self.dryrun_var.get()
else:
# User 用户:从配置读取 dryrun 设置
dryrun = self.config.get("execution.dryrun", False)
# 4. 确认执行
mode_text = "预览模式(不会保存更改)" if dryrun else "正式执行(将保存更改)"
manager_text = "".join(manager_names)
confirm_msg = (
f"确认执行删除操作?\n\n"
f"模式: {mode_text}\n"
f"负责人: {manager_text}\n"
f"Production ID 数量: {len(production_ids)}\n\n"
f"此操作可能需要较长时间,是否继续?"
)
if not messagebox.askyesno("确认执行", confirm_msg):
return
# 5. 创建进度窗口
self.progress_window = DeleteProgressWindow(
self,
title="执行删除",
managers=manager_text,
dryrun=dryrun,
on_cancel=self._cancel_delete_execution
)
self.log_text.info(f"开始执行删除(模式: {'预览' if dryrun else '正式'}...")
# 6. 启动后台线程执行
delete_thread = threading.Thread(
target=self._delete_worker,
args=(production_ids, manager_names, dryrun),
daemon=True
)
delete_thread.start()
def _cancel_delete_execution(self):
"""取消删除执行"""
self.log_text.info("用户取消了执行操作")
if hasattr(self, 'progress_window') and self.progress_window:
self.progress_window.append_log("正在取消...", "warning")
def _delete_worker(self, production_ids: list, manager_names: list, dryrun: bool):
"""
后台线程执行删除
Args:
production_ids: Production ID 列表
manager_names: 负责人列表
dryrun: 是否为预览模式
"""
progress_window = self.progress_window
cancelled = False
try:
from utils.discrete_material_plan_cleaner import DiscreteMaterialPlanCleaner
from db.production_order_query import query_production_order_numbers
# 创建临时文件保存 Production ID
with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False, encoding='utf-8') as f:
temp_file = f.name
f.write('\n'.join(production_ids))
# 进度回调函数
def progress_callback(current: int, total: int, message: str):
if progress_window and progress_window.is_cancelled():
return
progress_window.update_progress(current, total, message)
progress_window.append_log(message, "info")
# 创建 Cleaner 实例
cleaner = DiscreteMaterialPlanCleaner(
username=self.config.get("erp.username"),
password=self.config.get("erp.password"),
manager_names=manager_names,
headless=self.config.get("erp.headless", True),
verbose=True,
dryrun=dryrun,
progress_callback=progress_callback
)
# 执行清理
progress_window.append_log("正在连接 ERP 系统...", "info")
cleaner.clean(temp_file)
# 检查是否被取消
if progress_window.is_cancelled():
cancelled = True
self.after(0, lambda: self._update_log("执行已取消", "WARNING"))
else:
# 生成报告
report = cleaner.generate_report()
# 在主线程中更新 UI
self.after(0, lambda: self._delete_complete(report, cleaner.stats))
except Exception as e:
error_msg = f"执行过程中发生错误: {str(e)}"
self.after(0, lambda: self._update_log(error_msg, "ERROR"))
if progress_window:
progress_window.append_log(error_msg, "error")
progress_window.set_completed()
import traceback
self.after(0, lambda: self._update_log(traceback.format_exc(), "ERROR"))
finally:
# 清理临时文件
try:
if 'temp_file' in locals() and os.path.exists(temp_file):
os.unlink(temp_file)
except:
pass
def _delete_complete(self, report: str, stats: dict):
"""
删除完成回调
Args:
report: Markdown 格式的报告
stats: 统计信息字典
"""
# 更新日志
self.log_text.info("执行完成!")
self.log_text.info(f"处理订单: {stats.get('processed_orders', 0)}/{stats.get('total_orders', 0)}")
self.log_text.info(f"删除物料: {len(stats.get('deleted_materials', []))}")
self.log_text.info(f"跳过物料: {len(stats.get('skipped_materials', []))}")
self.log_text.info(f"错误数量: {len(stats.get('errors', []))}")
# 显示报告
if hasattr(self, 'progress_window') and self.progress_window:
self.progress_window.show_report(report)

View File

@@ -63,12 +63,18 @@ class SettingsTab(ttk.Frame):
self._create_validation_group(scrollable_frame)
self._create_ui_group(scrollable_frame)
else:
# User 用户:显示路径配置
# User 用户:显示路径配置和执行设置
self._create_paths_group(scrollable_frame)
self._create_user_execution_group(scrollable_frame)
# 按钮区域 - 根据用户类型显示不同按钮
button_frame = ttk.Frame(scrollable_frame)
button_frame.grid(row=7, column=0, columnspan=2, pady=20, sticky="ew")
if is_user_only:
# User 用户显示测试按钮和保存设置按钮row=4 因为有执行设置组)
button_frame.grid(row=4, column=0, columnspan=2, pady=20, sticky="ew")
else:
# 管理员显示所有按钮
button_frame.grid(row=7, column=0, columnspan=2, pady=20, sticky="ew")
if is_user_only:
# User 用户:显示测试按钮和保存设置按钮
@@ -225,8 +231,14 @@ class SettingsTab(ttk.Frame):
def _create_paths_group(self, parent):
"""创建路径配置组"""
# 根据用户类型调整 grid 位置
is_user_only = self.session_manager and self.session_manager.get_user_type() == 'User'
group = ttk.LabelFrame(parent, text="路径设置", padding=10)
group.grid(row=2, column=1, pady=10, padx=10, sticky="nsew")
if is_user_only:
group.grid(row=2, column=1, pady=10, padx=10, sticky="nsew")
else:
group.grid(row=2, column=1, pady=10, padx=10, sticky="nsew")
from gui.widgets import FileSelector
@@ -255,6 +267,27 @@ class SettingsTab(ttk.Frame):
group.columnconfigure(0, weight=1)
def _create_user_execution_group(self, parent):
"""创建 User 用户的执行设置组"""
group = ttk.LabelFrame(parent, text="执行设置", padding=10)
group.grid(row=3, column=0, columnspan=2, pady=10, padx=10, sticky="ew")
# dryrun 模式设置
self.user_dryrun_var = tk.BooleanVar(value=False)
ttk.Checkbutton(
group,
text="预览模式 (执行删除时不保存更改)",
variable=self.user_dryrun_var
).grid(row=0, column=0, sticky="w", pady=5)
# 说明文字
hint_label = ttk.Label(
group,
text="提示:勾选后,执行删除操作时将只预览不实际保存,用于测试流程。",
foreground="gray"
)
hint_label.grid(row=1, column=0, sticky="w", pady=(0, 5))
def _create_extraction_group(self, parent):
"""创建处理配置组"""
group = ttk.LabelFrame(parent, text="数据提取设置", padding=10)
@@ -398,6 +431,8 @@ class SettingsTab(ttk.Frame):
self.data_dir_selector.set(self.config.get("paths.data_dir", ""))
self.default_output_var.set(self.config.get("paths.default_output", ""))
self.validation_output_filename_var.set(self.config.get("paths.validation_output", ""))
# 执行设置
self.user_dryrun_var.set(self.config.get("execution.dryrun", False))
return
# 管理员模式 - 加载所有配置
@@ -462,10 +497,12 @@ class SettingsTab(ttk.Frame):
is_user_only = self.session_manager and self.session_manager.get_user_type() == 'User'
if is_user_only:
# User 用户模式 - 只保存路径设置
# User 用户模式 - 只保存路径设置和执行设置
self.config.set("paths.data_dir", self.data_dir_selector.get())
self.config.set("paths.default_output", self.default_output_var.get())
self.config.set("paths.validation_output", self.validation_output_filename_var.get())
# 保存执行设置
self.config.set("execution.dryrun", self.user_dryrun_var.get())
# 保存到文件
if self.config.save():

View File

@@ -8,5 +8,6 @@ from .file_selector import FileSelector
from .log_text import LogText
from .production_id_input import ProductionIdInput
from .log_handler import GuiTextHandler
from .delete_progress_window import DeleteProgressWindow
__all__ = ['FileSelector', 'LogText', 'ProductionIdInput', 'GuiTextHandler']
__all__ = ['FileSelector', 'LogText', 'ProductionIdInput', 'GuiTextHandler', 'DeleteProgressWindow']

View File

@@ -0,0 +1,447 @@
#!/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;
}}
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.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)