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:
Misaka
2026-02-11 21:47:17 +08:00
parent 62f323d420
commit 6f8df2f2e6
8 changed files with 561 additions and 96 deletions

View File

@@ -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()