Files
playwrite/gui/data_extraction_tab.py
Misaka_Company 2ca53f6a55 style: normalize code formatting across the codebase
- Standardize quote style (single to double quotes)
- Improve code formatting consistency
- Apply formatting to utilities, GUI components, and tools
- Update imports and docstrings for consistency

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-05 14:56:35 +08:00

296 lines
10 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 -*-
"""
数据提取标签页
从 ERP 系统提取备料计划数据。
"""
import os
import sys
import threading
import queue
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
from pathlib import Path
from contextlib import redirect_stdout
from gui.widgets import FileSelector, LogText
from gui.config_manager import ConfigManager
from gui.progress import ProgressInfo, ProgressCalculator
from gui.utils import RealtimeOutput
class DataExtractionTab(ttk.Frame):
"""数据提取标签页"""
def __init__(self, parent, config: ConfigManager):
"""
初始化数据提取标签页
Args:
parent: 父容器
config: 配置管理器
"""
super().__init__(parent)
self.config = config
self.extracting = False
self.extractor = None
self.extraction_thread = None
self.progress_calculator = ProgressCalculator()
self.progress_queue = queue.Queue() # 进度更新队列
# 启动进度更新轮询
self._poll_progress_queue()
self.create_widgets()
# 稍后显示就绪消息
try:
self.log_text.info("数据提取标签页已就绪")
except:
pass # 如果窗口还未完全就绪,忽略错误
def create_widgets(self):
"""创建界面组件"""
# 主容器 - 使用 PanedWindow 分割上下部分
main_paned = ttk.PanedWindow(self, orient=tk.VERTICAL)
main_paned.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# 上部:控制面板
control_frame = ttk.Frame(main_paned)
main_paned.add(control_frame, weight=0)
# 下部:日志输出
log_frame = ttk.LabelFrame(main_paned, text="日志输出", padding=5)
main_paned.add(log_frame, weight=1)
self._create_control_panel(control_frame)
self._create_log_panel(log_frame)
def _create_control_panel(self, parent):
"""创建控制面板"""
# 输入文件选择
input_group = ttk.LabelFrame(parent, text="输入文件", padding=10)
input_group.pack(fill=tk.X, pady=5)
self.input_file_selector = FileSelector(
input_group,
label_text="ProductionID 文件:",
file_type="file",
file_types=[("文本文件", "*.txt"), ("所有文件", "*.*")],
initial_dir="D:/python/playwrite/",
)
self.input_file_selector.pack(fill=tk.X)
# 设置默认文件
default_input = self.config.get("paths.production_id_file", "ProductionID.txt")
if os.path.exists(default_input):
self.input_file_selector.set(default_input)
# 输出文件选择
output_group = ttk.LabelFrame(parent, text="输出文件", padding=10)
output_group.pack(fill=tk.X, pady=5)
self.output_file_selector = FileSelector(
output_group,
label_text="保存为:",
file_type="file",
file_types=[("Excel 文件", "*.xlsx"), ("所有文件", "*.*")],
initial_dir=self.config.get("paths.data_dir", "data/"),
)
self.output_file_selector.pack(fill=tk.X)
# 设置默认输出文件
default_output = os.path.join(
self.config.get("paths.data_dir", "data/"),
self.config.get("paths.default_output", "离散备料计划维护_合并.xlsx"),
)
self.output_file_selector.set(default_output)
# 选项
options_group = ttk.LabelFrame(parent, text="提取选项", padding=10)
options_group.pack(fill=tk.X, pady=5)
self.verbose_var = tk.BooleanVar(
value=self.config.get("extraction.verbose", True)
)
ttk.Checkbutton(options_group, text="详细日志", variable=self.verbose_var).grid(
row=0, column=0, sticky="w", padx=5
)
self.headless_var = tk.BooleanVar(value=self.config.get("erp.headless", True))
ttk.Checkbutton(
options_group, text="无头模式 (不显示浏览器)", variable=self.headless_var
).grid(row=0, column=1, sticky="w", padx=5)
# 进度显示
progress_group = ttk.LabelFrame(parent, text="进度", padding=10)
progress_group.pack(fill=tk.X, pady=5)
self.progress_bar = ttk.Progressbar(progress_group, mode="determinate")
self.progress_bar.pack(fill=tk.X, pady=5)
self.status_label = ttk.Label(
progress_group, text="就绪", relief=tk.SUNKEN, anchor=tk.W
)
self.status_label.pack(fill=tk.X)
# 控制按钮
button_frame = ttk.Frame(parent)
button_frame.pack(fill=tk.X, pady=10)
self.start_button = ttk.Button(
button_frame, text="开始提取", command=self.start_extraction
)
self.start_button.pack(side=tk.LEFT, padx=5)
self.stop_button = ttk.Button(
button_frame, text="停止", command=self.stop_extraction, state=tk.DISABLED
)
self.stop_button.pack(side=tk.LEFT, padx=5)
def _create_log_panel(self, parent):
"""创建日志面板"""
self.log_text = LogText(parent, height=15, readonly=True)
self.log_text.pack(fill=tk.BOTH, expand=True)
def start_extraction(self):
"""开始数据提取"""
# 验证输入
input_file = self.input_file_selector.get()
output_file = self.output_file_selector.get()
if not input_file:
messagebox.showerror("错误", "请选择 ProductionID 输入文件")
return
if not os.path.exists(input_file):
messagebox.showerror("错误", f"输入文件不存在:{input_file}")
return
if not output_file:
messagebox.showerror("错误", "请指定输出文件路径")
return
# 确保输出目录存在
output_dir = os.path.dirname(output_file)
if output_dir and not os.path.exists(output_dir):
os.makedirs(output_dir, exist_ok=True)
# 更新 UI 状态
self.extracting = True
self.start_button.config(state=tk.DISABLED)
self.stop_button.config(state=tk.NORMAL)
self.progress_bar["value"] = 0
self.status_label.config(text="正在登录...")
self.log_text.clear()
self.log_text.info("开始数据提取...")
# 在后台线程中执行提取
self.extraction_thread = threading.Thread(
target=self._extraction_worker, args=(input_file, output_file), daemon=True
)
self.extraction_thread.start()
def stop_extraction(self):
"""停止数据提取"""
if self.extracting:
self.extracting = False
self.log_text.warning("正在停止提取...")
self.status_label.config(text="正在停止...")
def _extraction_worker(self, input_file: str, output_file: str):
"""提取工作线程"""
try:
# 导入提取器(延迟导入以避免启动时加载 Playwright
from utils.离散备料计划维护数据提取 import DiscreteMaterialPlanExtractor
# 创建提取器实例
self.extractor = DiscreteMaterialPlanExtractor(
username=self.config.get("erp.username"),
password=self.config.get("erp.password"),
headless=self.headless_var.get(),
verbose=self.verbose_var.get(),
batch_size=self.config.get("extraction.batch_size", 100),
)
# 创建实时输出流,每次写入立即更新 GUI
realtime_output = RealtimeOutput(
lambda line: self._update_log(line, "INFO")
)
# 创建进度回调函数
def progress_callback(progress_info: ProgressInfo):
# 计算总体进度百分比
overall_percent = self.progress_calculator.calculate_overall_percent(
progress_info
)
self._update_progress(overall_percent, progress_info.message)
# 重定向 stdout 并执行提取(带进度回调)
with redirect_stdout(realtime_output):
result = self.extractor.extract(
production_id_file=input_file,
output_file=output_file,
progress_callback=progress_callback,
)
if result and self.extracting:
self._update_log(f"数据已保存到:{output_file}", "SUCCESS")
elif not self.extracting:
self._update_log("提取已取消", "WARNING")
else:
self._update_log("提取失败", "ERROR")
except Exception as e:
self._update_log(f"提取过程中发生错误:{str(e)}", "ERROR")
finally:
# 更新 UI 状态
self.after(0, self._extraction_complete)
def _extraction_complete(self):
"""提取完成后的 UI 更新"""
self.extracting = False
self.start_button.config(state=tk.NORMAL)
self.stop_button.config(state=tk.DISABLED)
self.extractor = None
def _poll_progress_queue(self):
"""轮询进度队列,处理进度更新"""
try:
while True:
# 非阻塞地获取队列中的消息
try:
progress_data = self.progress_queue.get_nowait()
value, message = progress_data
self.progress_bar["value"] = value
self.status_label.config(text=message)
except queue.Empty:
break
finally:
# 继续轮询(每 50ms 检查一次)
self.after(50, self._poll_progress_queue)
def _update_progress(self, value: int, message: str):
"""线程安全的进度更新(通过队列)"""
try:
self.progress_queue.put_nowait((value, message))
except:
pass # 队列满时忽略
def _update_log(self, message: str, level: str = "INFO"):
"""线程安全的日志更新"""
def update():
if self.extracting or level in ["ERROR", "WARNING", "SUCCESS"]:
if level == "INFO":
self.log_text.info(message)
elif level == "SUCCESS":
self.log_text.success(message)
elif level == "WARNING":
self.log_text.warning(message)
elif level == "ERROR":
self.log_text.error(message)
self.after(0, update)