From 6f8df2f2e69000f1d18677a9faf3662be159233a Mon Sep 17 00:00:00 2001 From: Misaka Date: Wed, 11 Feb 2026 21:47:17 +0800 Subject: [PATCH] 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 --- config/schema.py | 36 +++++ gui/data_extraction_tab.py | 226 ++++++++++++++++++++--------- gui/main_window.py | 48 +++++- gui/material_validation_tab.py | 186 +++++++++++++++++++++--- gui/settings_tab.py | 60 +++++++- gui/widgets/__init__.py | 3 +- gui/widgets/log_text.py | 6 + gui/widgets/production_id_input.py | 92 ++++++++++++ 8 files changed, 561 insertions(+), 96 deletions(-) create mode 100644 gui/widgets/production_id_input.py diff --git a/config/schema.py b/config/schema.py index b66a9d2..0c172a7 100644 --- a/config/schema.py +++ b/config/schema.py @@ -269,6 +269,34 @@ class ValidationConfig: return errors +@dataclass +class UIConfig: + """用户界面配置""" + + font_family: str = "Microsoft YaHei UI" + font_size: int = 10 + production_id_input_width: int = 20 + + @classmethod + def from_env(cls) -> "UIConfig": + """从环境变量创建配置""" + from config.env_loader import get_env, get_env_int + return cls( + font_family=get_env("UI_FONT_FAMILY", "Microsoft YaHei UI"), + font_size=get_env_int("UI_FONT_SIZE", 10), + production_id_input_width=get_env_int("UI_PRODUCTION_ID_INPUT_WIDTH", 20), + ) + + def validate(self) -> list[str]: + """验证配置,返回错误列表""" + errors = [] + if self.font_size < 8 or self.font_size > 24: + errors.append("字号必须在 8-24 之间") + if self.production_id_input_width < 10 or self.production_id_input_width > 100: + errors.append("输入框宽度必须在 10-100 之间") + return errors + + @dataclass class AppConfig: """应用总配置""" @@ -278,6 +306,7 @@ class AppConfig: paths: PathConfig extraction: ExtractionConfig validation: ValidationConfig + ui: UIConfig @classmethod def from_env(cls) -> "AppConfig": @@ -288,6 +317,7 @@ class AppConfig: paths=PathConfig.from_env(), extraction=ExtractionConfig.from_env(), validation=ValidationConfig.from_env(), + ui=UIConfig.from_env(), ) def validate(self) -> list[str]: @@ -298,6 +328,7 @@ class AppConfig: errors.extend(self.paths.validate()) errors.extend(self.extraction.validate()) errors.extend(self.validation.validate()) + errors.extend(self.ui.validate()) return errors def to_dict(self) -> dict: @@ -348,4 +379,9 @@ class AppConfig: "default_manager": self.validation.default_manager, "match_mode": self.validation.match_mode, }, + "ui": { + "font_family": self.ui.font_family, + "font_size": self.ui.font_size, + "production_id_input_width": self.ui.production_id_input_width, + }, } diff --git a/gui/data_extraction_tab.py b/gui/data_extraction_tab.py index ff0cde8..1d40fc6 100644 --- a/gui/data_extraction_tab.py +++ b/gui/data_extraction_tab.py @@ -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("", 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() diff --git a/gui/main_window.py b/gui/main_window.py index 1e0a602..dd1b1c3 100644 --- a/gui/main_window.py +++ b/gui/main_window.py @@ -28,6 +28,7 @@ class MainWindow: self.root = root self.config = ConfigManager() self.session_manager = session_manager + self.shared_production_ids = [] # 共享的 Production ID 列表 # 设置窗口属性(包含用户信息) user_type_display = "管理员" if session_manager.is_admin() else "用户" @@ -49,6 +50,18 @@ class MainWindow: # 居中窗口 self._center_window() + def get_shared_production_ids(self) -> list: + """获取共享的 Production ID 列表""" + return self.shared_production_ids.copy() + + def update_shared_production_ids(self, production_ids: list): + """更新共享的 Production ID 列表""" + self.shared_production_ids = production_ids + # 通知物料校验标签页 Production ID 已更新 + if hasattr(self, 'validation_tab'): + if hasattr(self.validation_tab, 'on_production_ids_updated'): + self.validation_tab.on_production_ids_updated(production_ids) + def create_menu(self): """创建菜单栏""" menubar = tk.Menu(self.root) @@ -70,18 +83,31 @@ class MainWindow: self.notebook = ttk.Notebook(self.root) self.notebook.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) - # 数据提取标签页 - self.extraction_tab = DataExtractionTab(self.notebook, self.config) + # 数据提取标签页(传递 self 以支持共享 Production ID) + self.extraction_tab = DataExtractionTab(self.notebook, self.config, self) self.notebook.add(self.extraction_tab, text="数据提取") - # 物料校验标签页(传入 session_manager) - self.validation_tab = MaterialValidationTab(self.notebook, self.config, self.session_manager) + # 物料校验标签页(传入 session_manager 和 main_window) + self.validation_tab = MaterialValidationTab(self.notebook, self.config, self.session_manager, self) self.notebook.add(self.validation_tab, text="物料校验") # 设置标签页(传入 session_manager) self.settings_tab = SettingsTab(self.notebook, self.config, self.session_manager) self.notebook.add(self.settings_tab, text="设置") + # 初始化:如果数据提取页面已有 Production ID,通知物料校验页面 + self._initialize_shared_production_ids() + + def _initialize_shared_production_ids(self): + """初始化共享的 Production ID(从数据提取页面获取)""" + try: + if hasattr(self.extraction_tab, 'production_id_input'): + production_ids = self.extraction_tab.production_id_input.get() + if production_ids: + self.update_shared_production_ids(production_ids) + except Exception: + pass # 如果获取失败,忽略错误 + def create_status_bar(self): """创建状态栏""" self.status_bar = ttk.Frame(self.root, relief=tk.SUNKEN) @@ -144,3 +170,17 @@ class MainWindow: "• 数据库持久化 - 将提取的数据自动保存到数据库\n\n" "基于 Playwright 和 Python 开发", ) + + def reload_config(self): + """配置更新后重新加载配置到各个标签页""" + # 重新加载配置 + self.config.reload() + + # 通知各个标签页重新加载配置 + if hasattr(self.extraction_tab, 'reload_config'): + self.extraction_tab.reload_config() + if hasattr(self.validation_tab, 'reload_config'): + self.validation_tab.reload_config() + + # 更新状态栏 + self.config_status.set("配置已重新加载") diff --git a/gui/material_validation_tab.py b/gui/material_validation_tab.py index 6d0652b..c92acfc 100644 --- a/gui/material_validation_tab.py +++ b/gui/material_validation_tab.py @@ -131,7 +131,7 @@ class CheckboxTreeview(ttk.Treeview): class MaterialValidationTab(ttk.Frame): """物料校验标签页""" - def __init__(self, parent, config: ConfigManager, session_manager): + def __init__(self, parent, config: ConfigManager, session_manager, main_window=None): """ 初始化物料校验标签页 @@ -139,13 +139,16 @@ class MaterialValidationTab(ttk.Frame): parent: 父容器 config: 配置管理器 session_manager: SessionManager 实例,用于权限控制 + main_window: 主窗口引用,用于获取共享的 Production ID """ super().__init__(parent) self.config = config self.session_manager = session_manager + self.main_window = main_window self.validating = False self.validation_results = None self.material_records_cache = None # 缓存完整的物料记录 + self.shared_production_ids = [] # 共享的 Production ID 列表 # 负责人筛选相关 self.managers: List[str] = [] # 可用负责人列表 @@ -244,14 +247,72 @@ class MaterialValidationTab(ttk.Frame): # 数据库过滤模式 - 需要 ProductionID 文件 self.db_filtered_frame = ttk.Frame(file_group) - self.db_filtered_production_id_selector = FileSelector( - self.db_filtered_frame, - label_text="ProductionID 文件:", - file_type="file", - file_types=[("文本文件", "*.txt"), ("所有文件", "*.*")], - initial_dir="D:/python/playwrite/", - ) - self.db_filtered_production_id_selector.pack(fill=tk.X) + + # 判断用户类型 + is_user_only = not self.session_manager.is_admin() + + if is_user_only: + # ========== User 模式:直接使用共享的 Production ID ========== + # 自动设置为使用共享数据源 + self.production_id_source_var = tk.StringVar(value="shared") + + # 创建共享 Production ID 显示标签(但不显示任何内容,只是用于更新) + self.shared_ids_info_label = ttk.Label( + self.db_filtered_frame, + text="", + foreground="gray" + ) + # 不 pack,保持隐藏状态 + + else: + # ========== Admin 模式:可选择文件或共享数据 ========== + # 共享 Production ID 提示标签 + self.shared_ids_label = ttk.Label( + self.db_filtered_frame, + text="💡 数据提取页面输入了 Production ID,可直接使用", + foreground="green", + font=("", 9) + ) + # 初始时不显示,等待 Production ID 更新 + + # Production ID 数据源选择 + self.production_id_source_var = tk.StringVar(value="file") + source_frame = ttk.Frame(self.db_filtered_frame) + source_frame.pack(fill=tk.X, pady=5) + + ttk.Radiobutton( + source_frame, + text="使用文件:", + variable=self.production_id_source_var, + value="file", + command=self._on_production_id_source_changed + ).pack(side=tk.LEFT) + + ttk.Radiobutton( + source_frame, + text="使用数据提取页面的 Production ID:", + variable=self.production_id_source_var, + value="shared", + command=self._on_production_id_source_changed + ).pack(side=tk.LEFT, padx=10) + + # 文件选择器 + self.db_filtered_production_id_selector = FileSelector( + self.db_filtered_frame, + label_text="", + file_type="file", + file_types=[("文本文件", "*.txt"), ("所有文件", "*.*")], + initial_dir="D:/python/playwrite/", + ) + self.db_filtered_production_id_selector.pack(fill=tk.X) + + # 共享 Production ID 显示标签 + self.shared_ids_info_label = ttk.Label( + self.db_filtered_frame, + text="", + foreground="blue" + ) + # 初始时不显示 # 输出文件(对于所有用户都可见) self.output_file_selector = FileSelector( @@ -396,6 +457,56 @@ class MaterialValidationTab(ttk.Frame): elif mode == "database_filtered": self.db_filtered_frame.pack(fill=tk.X, pady=(0, 5)) + def _on_production_id_source_changed(self): + """Production ID 数据源切换回调(仅 Admin 模式)""" + # User 模式下没有文件选择器,直接返回 + if not self.session_manager.is_admin(): + return + + source = self.production_id_source_var.get() + if source == "shared": + # 使用共享 Production ID + self.db_filtered_production_id_selector.pack_forget() + self.shared_ids_info_label.pack(anchor="w", pady=(0, 5)) + else: + # 使用文件 + self.shared_ids_info_label.pack_forget() + self.db_filtered_production_id_selector.pack(fill=tk.X) + + def on_production_ids_updated(self, production_ids: list): + """当数据提取页面的 Production ID 更新时调用""" + self.shared_production_ids = production_ids + + is_user_only = not self.session_manager.is_admin() + + # User 模式:静默更新,不显示任何提示 + if is_user_only: + return + + # Admin 模式:显示提示信息 + if production_ids: + count = len(production_ids) + preview = ", ".join(production_ids[:3]) + if count > 3: + preview += f" ... (共 {count} 个)" + self.shared_ids_info_label.config(text=f"📋 {preview}") + # 显示提示标签 + if hasattr(self, 'shared_ids_label') and not self.shared_ids_label.winfo_ismapped(): + self.shared_ids_label.pack(anchor="w", pady=(0, 5)) + 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() + + def reload_config(self): + """配置更新后重新加载""" + # 从主窗口获取最新的 Production ID + if self.main_window: + production_ids = self.main_window.get_shared_production_ids() + self.on_production_ids_updated(production_ids) + def _create_manager_filter_area(self, parent): """创建负责人筛选区域""" # PERMISSION CHECK: 非管理员用户不显示筛选区域 @@ -716,19 +827,41 @@ class MaterialValidationTab(ttk.Frame): mode = self.source_mode.get() input_file = None production_id_file = None + production_ids_list = None # 新增:用于传递共享的 Production ID 列表 + + is_user_only = not self.session_manager.is_admin() # 根据模式验证输入文件 if mode == "database_full": # 无需输入文件 pass elif mode == "database_filtered": - production_id_file = self.db_filtered_production_id_selector.get() - if not production_id_file: - messagebox.showerror("错误", "请选择 ProductionID 文件") - return - if not os.path.exists(production_id_file): - messagebox.showerror("错误", f"文件不存在:{production_id_file}") - return + if is_user_only: + # User 模式:自动使用共享的 Production ID + production_ids_list = self.shared_production_ids + if not production_ids_list: + messagebox.showerror("错误", "没有可用的共享 Production ID\n请在数据提取页面输入 Production ID 后再试") + return + self.log_text.info(f"使用数据提取页面的 Production ID({len(production_ids_list)} 个)") + else: + # Admin 模式:检查数据源选择 + source = self.production_id_source_var.get() + if source == "shared": + # 使用共享的 Production ID + production_ids_list = self.shared_production_ids + if not production_ids_list: + messagebox.showerror("错误", "没有可用的共享 Production ID\n请在数据提取页面输入 Production ID 后再试") + return + self.log_text.info(f"使用共享的 Production ID({len(production_ids_list)} 个)") + else: + # 使用文件 + production_id_file = self.db_filtered_production_id_selector.get() + if not production_id_file: + messagebox.showerror("错误", "请选择 ProductionID 文件") + return + if not os.path.exists(production_id_file): + messagebox.showerror("错误", f"文件不存在:{production_id_file}") + return else: messagebox.showerror("错误", f"未知的校验模式: {mode}") return @@ -758,16 +891,27 @@ class MaterialValidationTab(ttk.Frame): # 在后台线程中执行校验 validation_thread = threading.Thread( target=self._validation_worker_enhanced, - args=(mode, input_file, production_id_file, output_file), + args=(mode, input_file, production_id_file, output_file, production_ids_list), daemon=True, ) validation_thread.start() def _validation_worker_enhanced( - self, mode: str, input_file: str, production_id_file: str, output_file: str + self, mode: str, input_file: str, production_id_file: str, output_file: str, production_ids_list: list = None ): """增强的校验工作线程(使用完整记录模式)""" + import tempfile + temp_production_id_file = None + try: + # 如果提供了共享的 Production ID 列表,创建临时文件 + if production_ids_list: + with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False, encoding='utf-8') as f: + temp_production_id_file = f.name + f.write('\n'.join(production_ids_list)) + production_id_file = temp_production_id_file + self._update_log(f"使用共享的 Production ID 列表({len(production_ids_list)} 个)", "INFO") + # 导入校验器 from utils.material_status_validator import MaterialStatusValidator @@ -820,6 +964,12 @@ class MaterialValidationTab(ttk.Frame): import traceback self._update_log(traceback.format_exc(), "ERROR") finally: + # 清理临时文件 + if temp_production_id_file and os.path.exists(temp_production_id_file): + try: + os.unlink(temp_production_id_file) + except: + pass # 更新 UI 状态 self.after(0, self._validation_complete) diff --git a/gui/settings_tab.py b/gui/settings_tab.py index e91a55b..5531232 100644 --- a/gui/settings_tab.py +++ b/gui/settings_tab.py @@ -57,10 +57,11 @@ class SettingsTab(ttk.Frame): self._create_paths_group(scrollable_frame) self._create_extraction_group(scrollable_frame) self._create_validation_group(scrollable_frame) + self._create_ui_group(scrollable_frame) # 按钮区域 - 根据用户类型显示不同按钮 button_frame = ttk.Frame(scrollable_frame) - button_frame.grid(row=6, column=0, columnspan=2, pady=20, sticky="ew") + button_frame.grid(row=7, column=0, columnspan=2, pady=20, sticky="ew") if is_user_only: # 普通用户只显示测试按钮 @@ -331,6 +332,39 @@ class SettingsTab(ttk.Frame): group.columnconfigure(1, weight=1) + def _create_ui_group(self, parent): + """创建 UI 配置组""" + group = ttk.LabelFrame(parent, text="界面设置", padding=10) + group.grid(row=5, column=0, columnspan=2, pady=10, padx=10, sticky="ew") + + # 字体选择 + ttk.Label(group, text="字体:").grid(row=0, column=0, sticky="w", pady=5) + self.ui_font_family_var = tk.StringVar() + font_combo = ttk.Combobox( + group, + textvariable=self.ui_font_family_var, + values=["Microsoft YaHei UI", "SimSun", "KaiTi", "FangSong", "Arial", "Segoe UI"], + state="readonly", + width=30, + ) + font_combo.grid(row=0, column=1, sticky="w", pady=5) + + # 字号选择 + ttk.Label(group, text="字号:").grid(row=1, column=0, sticky="w", pady=5) + self.ui_font_size_var = tk.IntVar(value=10) + ttk.Spinbox( + group, from_=8, to=24, textvariable=self.ui_font_size_var, width=10 + ).grid(row=1, column=1, sticky="w", pady=5) + + # Production ID 输入框宽度 + ttk.Label(group, text="输入框宽度(字符):").grid(row=2, column=0, sticky="w", pady=5) + self.ui_input_width_var = tk.IntVar(value=20) + ttk.Spinbox( + group, from_=10, to=100, textvariable=self.ui_input_width_var, width=10 + ).grid(row=2, column=1, sticky="w", pady=5) + + group.columnconfigure(1, weight=1) + def load_settings(self): """从配置加载设置到界面""" # 判断是否为仅测试用户模式 @@ -391,6 +425,11 @@ class SettingsTab(ttk.Frame): self.validation_enable_crud_var.set(self.config.get("validation.enable_crud_operations", False)) self.validation_default_manager_var.set(self.config.get("validation.default_manager", "")) + # UI 设置 + self.ui_font_family_var.set(self.config.get("ui.font_family", "Microsoft YaHei UI")) + self.ui_font_size_var.set(self.config.get("ui.font_size", 10)) + self.ui_input_width_var.set(self.config.get("ui.production_id_input_width", 20)) + def save_settings(self): """保存界面设置到配置""" # ERP 设置 @@ -439,12 +478,31 @@ class SettingsTab(ttk.Frame): self.config.set("validation.enable_crud_operations", self.validation_enable_crud_var.get()) self.config.set("validation.default_manager", self.validation_default_manager_var.get()) + # UI 设置 + self.config.set("ui.font_family", self.ui_font_family_var.get()) + self.config.set("ui.font_size", self.ui_font_size_var.get()) + self.config.set("ui.production_id_input_width", self.ui_input_width_var.get()) + # 保存到文件 if self.config.save(): messagebox.showinfo("成功", "设置已保存") + # 通知其他标签页重新加载配置 + self._notify_config_reload() else: messagebox.showerror("错误", "保存设置失败") + def _notify_config_reload(self): + """通知其他标签页配置已更新""" + # 尝试通知主窗口重新加载配置 + try: + # 获取主窗口 + main_window = self.winfo_toplevel() + # 调用主窗口的 reload_config 方法(如果存在) + if hasattr(main_window, 'reload_config'): + main_window.reload_config() + except Exception: + pass + def test_db_connection(self): """测试数据库连接""" # 从配置读取而不是从 UI 变量(支持 User 类型用户) diff --git a/gui/widgets/__init__.py b/gui/widgets/__init__.py index d2451b9..48992c0 100644 --- a/gui/widgets/__init__.py +++ b/gui/widgets/__init__.py @@ -6,5 +6,6 @@ GUI 自定义组件模块 from .file_selector import FileSelector from .log_text import LogText +from .production_id_input import ProductionIdInput -__all__ = ['FileSelector', 'LogText'] +__all__ = ['FileSelector', 'LogText', 'ProductionIdInput'] diff --git a/gui/widgets/log_text.py b/gui/widgets/log_text.py index 8c9a6a0..f9c3d28 100644 --- a/gui/widgets/log_text.py +++ b/gui/widgets/log_text.py @@ -178,3 +178,9 @@ class LogText(tk.Frame): def grid(self, **kwargs): """Grid 布局""" super().grid(**kwargs) + + def apply_font(self, font_family: str, font_size: int): + """应用字体设置""" + from tkinter import font as tk_font + font_spec = tk_font.Font(family=font_family, size=font_size) + self.text.configure(font=font_spec) diff --git a/gui/widgets/production_id_input.py b/gui/widgets/production_id_input.py new file mode 100644 index 0000000..9fc6200 --- /dev/null +++ b/gui/widgets/production_id_input.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Production ID 输入控件 + +多行文本输入框,用于输入 Production ID 列表。 +""" + +import tkinter as tk +from tkinter import ttk, font + + +class ProductionIdInput(ttk.Frame): + """Production ID 输入控件""" + + def __init__(self, parent, placeholder="每行输入一个 Production ID", **kwargs): + super().__init__(parent, **kwargs) + self.placeholder = placeholder + + # 创建文本框和滚动条 + self.text_widget = tk.Text(self, wrap=tk.WORD, padx=5, pady=5) + self.scrollbar = ttk.Scrollbar(self, orient=tk.VERTICAL, command=self.text_widget.yview) + self.text_widget.configure(yscrollcommand=self.scrollbar.set) + + # 布局 + self.text_widget.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + self.scrollbar.pack(side=tk.RIGHT, fill=tk.Y) + + # 绑定事件 + self.text_widget.bind("", self._on_focus_in) + self.text_widget.bind("", self._on_focus_out) + + # 显示占位符 + self._show_placeholder() + + def _on_focus_in(self, event): + """获得焦点时隐藏占位符""" + if self.text_widget.get("1.0", "end-1c") == self.placeholder: + self.text_widget.delete("1.0", tk.END) + + def _on_focus_out(self, event): + """失去焦点时显示占位符""" + content = self.text_widget.get("1.0", "end-1c") + if not content: + self._show_placeholder() + + def _show_placeholder(self): + """显示占位符""" + self.text_widget.delete("1.0", tk.END) + self.text_widget.insert("1.0", self.placeholder) + self.text_widget.configure(foreground="gray") + + def _hide_placeholder(self): + """隐藏占位符""" + if self.text_widget.get("1.0", "end-1c") == self.placeholder: + self.text_widget.delete("1.0", tk.END) + self.text_widget.configure(foreground="black") + + def get(self) -> list[str]: + """获取 Production ID 列表""" + self._hide_placeholder() + content = self.text_widget.get("1.0", "end-1c").strip() + return [line.strip() for line in content.split("\n") if line.strip()] + + def set(self, production_ids: list[str]): + """设置 Production ID 列表""" + self.text_widget.delete("1.0", tk.END) + if production_ids: + self.text_widget.insert("1.0", "\n".join(production_ids)) + self.text_widget.configure(foreground="black") + else: + self._show_placeholder() + + def clear(self): + """清空内容""" + self.text_widget.delete("1.0", tk.END) + self._show_placeholder() + + def append(self, production_ids: list[str]): + """追加 Production ID 列表""" + self._hide_placeholder() + if production_ids: + current_content = self.text_widget.get("1.0", "end-1c") + if current_content.strip(): + self.text_widget.insert(tk.END, "\n" + "\n".join(production_ids)) + else: + self.text_widget.insert("1.0", "\n".join(production_ids)) + + def apply_font(self, font_family: str, font_size: int): + """应用字体设置""" + font_spec = font.Font(family=font_family, size=font_size) + self.text_widget.configure(font=font_spec)