feat: add Production ID input widget and UI configuration
- Add ProductionIdInput widget with placeholder and multi-line support - Add UIConfig class for font family, font size, and input width settings - Refactor data extraction tab to use horizontal PanedWindow layout - Left panel: Production ID text input (draggable width) - Right panel: control panel and log output - Share Production IDs between data extraction and material validation tabs - Add UI settings group in settings page (font selection, size, input width) - For User users: automatically use shared Production IDs, simplified UI - Apply font settings to input and log widgets - Use sashpos() to set initial pane width correctly Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -14,7 +14,7 @@ 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.widgets import FileSelector, LogText, ProductionIdInput
|
||||
from gui.config_manager import ConfigManager
|
||||
from gui.progress import ProgressInfo, ProgressCalculator
|
||||
from gui.utils import RealtimeOutput
|
||||
@@ -23,16 +23,18 @@ from gui.utils import RealtimeOutput
|
||||
class DataExtractionTab(ttk.Frame):
|
||||
"""数据提取标签页"""
|
||||
|
||||
def __init__(self, parent, config: ConfigManager):
|
||||
def __init__(self, parent, config: ConfigManager, main_window=None):
|
||||
"""
|
||||
初始化数据提取标签页
|
||||
|
||||
Args:
|
||||
parent: 父容器
|
||||
config: 配置管理器
|
||||
main_window: 主窗口引用,用于共享 Production ID 数据
|
||||
"""
|
||||
super().__init__(parent)
|
||||
self.config = config
|
||||
self.main_window = main_window
|
||||
self.extracting = False
|
||||
self.extractor = None
|
||||
self.extraction_thread = None
|
||||
@@ -44,6 +46,9 @@ class DataExtractionTab(ttk.Frame):
|
||||
|
||||
self.create_widgets()
|
||||
|
||||
# 应用字体设置
|
||||
self._apply_ui_config()
|
||||
|
||||
# 稍后显示就绪消息
|
||||
try:
|
||||
self.log_text.info("数据提取标签页已就绪")
|
||||
@@ -52,9 +57,50 @@ class DataExtractionTab(ttk.Frame):
|
||||
|
||||
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)
|
||||
# 主容器 - 使用水平 PanedWindow 分割左右部分
|
||||
horizontal_paned = ttk.PanedWindow(self, orient=tk.HORIZONTAL)
|
||||
horizontal_paned.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||||
|
||||
# 左侧:Production ID 输入面板
|
||||
left_panel = ttk.Frame(horizontal_paned)
|
||||
horizontal_paned.add(left_panel, weight=0)
|
||||
|
||||
# 右侧:主面板(控制面板 + 日志)
|
||||
right_panel = ttk.Frame(horizontal_paned)
|
||||
horizontal_paned.add(right_panel, weight=1)
|
||||
|
||||
self._create_left_panel(left_panel)
|
||||
self._create_right_panel(right_panel)
|
||||
|
||||
# 保存 PanedWindow 引用,后续用于设置分隔条位置
|
||||
self.horizontal_paned = horizontal_paned
|
||||
|
||||
# 设置默认宽度(使用 after 确保在渲染后设置)
|
||||
input_width = self.config.get("ui.production_id_input_width", 20)
|
||||
# 字符宽度约 8 像素
|
||||
self.after(100, lambda: self._set_pane_width(input_width * 8))
|
||||
|
||||
def _create_left_panel(self, parent):
|
||||
"""创建左侧 Production ID 输入面板"""
|
||||
# 创建带标题的框架
|
||||
input_group = ttk.LabelFrame(parent, text="Production ID", padding=10)
|
||||
input_group.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# 创建 Production ID 输入控件
|
||||
self.production_id_input = ProductionIdInput(
|
||||
input_group,
|
||||
placeholder="每行输入一个 Production ID\n\n示例:\n26B848\n26B849"
|
||||
)
|
||||
self.production_id_input.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# 绑定变化事件:当文本框失去焦点时更新共享 Production ID
|
||||
self.production_id_input.text_widget.bind("<FocusOut>", self._on_production_ids_changed)
|
||||
|
||||
def _create_right_panel(self, parent):
|
||||
"""创建右侧主面板"""
|
||||
# 主容器 - 使用垂直 PanedWindow 分割上下部分
|
||||
main_paned = ttk.PanedWindow(parent, orient=tk.VERTICAL)
|
||||
main_paned.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# 上部:控制面板
|
||||
control_frame = ttk.Frame(main_paned)
|
||||
@@ -69,24 +115,6 @@ class DataExtractionTab(ttk.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)
|
||||
@@ -147,20 +175,47 @@ class DataExtractionTab(ttk.Frame):
|
||||
self.log_text = LogText(parent, height=15, readonly=True)
|
||||
self.log_text.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
def _apply_ui_config(self):
|
||||
"""应用 UI 配置(字体等)"""
|
||||
try:
|
||||
font_family = self.config.get("ui.font_family", "Microsoft YaHei UI")
|
||||
font_size = self.config.get("ui.font_size", 10)
|
||||
|
||||
# 应用到 Production ID 输入控件
|
||||
self.production_id_input.apply_font(font_family, font_size)
|
||||
|
||||
# 应用到日志控件(如果支持)
|
||||
if hasattr(self.log_text, 'apply_font'):
|
||||
self.log_text.apply_font(font_family, font_size)
|
||||
except Exception as e:
|
||||
# 如果应用字体失败,不影响主流程
|
||||
pass
|
||||
|
||||
def _set_pane_width(self, width: int):
|
||||
"""设置左侧 pane 的宽度
|
||||
|
||||
Args:
|
||||
width: 宽度(像素)
|
||||
"""
|
||||
try:
|
||||
# 使用 sashpos 方法设置分隔条位置
|
||||
# 参数 0 表示第一个分隔条(索引从 0 开始)
|
||||
self.horizontal_paned.sashpos(0, width)
|
||||
except Exception as e:
|
||||
# 如果设置失败,不影响主流程
|
||||
pass
|
||||
|
||||
def start_extraction(self):
|
||||
"""开始数据提取"""
|
||||
# 验证输入
|
||||
input_file = self.input_file_selector.get()
|
||||
# 获取 Production ID 列表
|
||||
production_ids = self.production_id_input.get()
|
||||
|
||||
if not production_ids:
|
||||
messagebox.showerror("错误", "请输入至少一个 Production ID")
|
||||
return
|
||||
|
||||
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
|
||||
@@ -177,11 +232,11 @@ class DataExtractionTab(ttk.Frame):
|
||||
self.progress_bar["value"] = 0
|
||||
self.status_label.config(text="正在登录...")
|
||||
self.log_text.clear()
|
||||
self.log_text.info("开始数据提取...")
|
||||
self.log_text.info(f"开始数据提取... ({len(production_ids)} 个 Production ID)")
|
||||
|
||||
# 在后台线程中执行提取
|
||||
self.extraction_thread = threading.Thread(
|
||||
target=self._extraction_worker, args=(input_file, output_file), daemon=True
|
||||
target=self._extraction_worker, args=(production_ids, output_file), daemon=True
|
||||
)
|
||||
self.extraction_thread.start()
|
||||
|
||||
@@ -192,49 +247,64 @@ class DataExtractionTab(ttk.Frame):
|
||||
self.log_text.warning("正在停止提取...")
|
||||
self.status_label.config(text="正在停止...")
|
||||
|
||||
def _extraction_worker(self, input_file: str, output_file: str):
|
||||
def _extraction_worker(self, production_ids: list[str], output_file: str):
|
||||
"""提取工作线程"""
|
||||
import tempfile
|
||||
|
||||
try:
|
||||
# 导入提取器(延迟导入以避免启动时加载 Playwright)
|
||||
from utils.离散备料计划维护数据提取 import DiscreteMaterialPlanExtractor
|
||||
# 创建临时文件保存 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))
|
||||
|
||||
# 创建提取器实例
|
||||
self.extractor = DiscreteMaterialPlanExtractor(
|
||||
username=self.config.get("erp.username"),
|
||||
password=self.config.get("erp.password"),
|
||||
headless=self.headless_var.get(),
|
||||
verbose=self.config.get("extraction.verbose", True),
|
||||
batch_size=self.config.get("extraction.batch_size", 100),
|
||||
enable_db_persistence=self.config.get("extraction.enable_db_persistence", False),
|
||||
)
|
||||
try:
|
||||
# 导入提取器(延迟导入以避免启动时加载 Playwright)
|
||||
from utils.离散备料计划维护数据提取 import DiscreteMaterialPlanExtractor
|
||||
|
||||
# 创建实时输出流,每次写入立即更新 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,
|
||||
# 创建提取器实例
|
||||
self.extractor = DiscreteMaterialPlanExtractor(
|
||||
username=self.config.get("erp.username"),
|
||||
password=self.config.get("erp.password"),
|
||||
headless=self.headless_var.get(),
|
||||
verbose=self.config.get("extraction.verbose", True),
|
||||
batch_size=self.config.get("extraction.batch_size", 100),
|
||||
enable_db_persistence=self.config.get("extraction.enable_db_persistence", False),
|
||||
)
|
||||
|
||||
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")
|
||||
# 创建实时输出流,每次写入立即更新 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=temp_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")
|
||||
|
||||
finally:
|
||||
# 删除临时文件
|
||||
try:
|
||||
os.unlink(temp_file)
|
||||
except:
|
||||
pass
|
||||
|
||||
except Exception as e:
|
||||
self._update_log(f"提取过程中发生错误:{str(e)}", "ERROR")
|
||||
@@ -287,3 +357,15 @@ class DataExtractionTab(ttk.Frame):
|
||||
self.log_text.error(message)
|
||||
|
||||
self.after(0, update)
|
||||
|
||||
def _on_production_ids_changed(self, event=None):
|
||||
"""Production ID 变化时的回调"""
|
||||
if self.main_window:
|
||||
production_ids = self.production_id_input.get()
|
||||
self.main_window.update_shared_production_ids(production_ids)
|
||||
|
||||
def reload_config(self):
|
||||
"""配置更新后重新应用 UI 设置"""
|
||||
self._apply_ui_config()
|
||||
# 通知主窗口当前的 Production ID
|
||||
self._on_production_ids_changed()
|
||||
|
||||
Reference in New Issue
Block a user