#!/usr/bin/env python # -*- coding: utf-8 -*- """ 设置标签页 管理系统配置(ERP、数据库、浏览器、路径等)。 """ import tkinter as tk from tkinter import ttk, messagebox import pyodbc from gui.config_manager import ConfigManager from config.schema import DatabaseType class SettingsTab(ttk.Frame): """设置标签页""" def __init__(self, parent, config: ConfigManager, session_manager=None): """ 初始化设置标签页 Args: parent: 父容器 config: 配置管理器 session_manager: 会话管理器,用于获取用户类型 """ super().__init__(parent) self.config = config self.session_manager = session_manager # 初始化物料校验输出文件名变量 self.validation_output_filename_var = tk.StringVar() self.create_widgets() self.load_settings() def create_widgets(self): """创建界面组件""" # 判断用户类型 is_user_only = self.session_manager and self.session_manager.get_user_type() == 'User' # 创建主容器,带滚动条 canvas = tk.Canvas(self) scrollbar = ttk.Scrollbar(self, orient="vertical", command=canvas.yview) scrollable_frame = ttk.Frame(canvas) scrollable_frame.bind( "", lambda e: canvas.configure(scrollregion=canvas.bbox("all")) ) canvas.create_window((0, 0), window=scrollable_frame, anchor="nw") canvas.configure(yscrollcommand=scrollbar.set) # 根据用户类型创建配置组 if not is_user_only: # 管理员显示所有配置组 self._create_erp_group(scrollable_frame) self._create_database_group(scrollable_frame) self._create_browser_group(scrollable_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) else: # User 用户:显示路径配置和执行设置 self._create_paths_group(scrollable_frame) self._create_user_execution_group(scrollable_frame) # 按钮区域 - 根据用户类型显示不同按钮 button_frame = ttk.Frame(scrollable_frame) 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 用户:显示测试按钮和保存设置按钮 ttk.Button( button_frame, text="测试 ERP 连接", command=self.test_erp_connection ).pack(side="left", padx=5) ttk.Button( button_frame, text="测试数据库连接", command=self.test_db_connection ).pack(side="left", padx=5) ttk.Button(button_frame, text="保存设置", command=self.save_settings).pack( side="left", padx=5 ) else: # 管理员显示所有按钮 ttk.Button( button_frame, text="测试 ERP 连接", command=self.test_erp_connection ).pack(side="left", padx=5) ttk.Button( button_frame, text="测试数据库连接", command=self.test_db_connection ).pack(side="left", padx=5) ttk.Button(button_frame, text="保存设置", command=self.save_settings).pack( side="left", padx=5 ) ttk.Button(button_frame, text="恢复默认", command=self.reset_defaults).pack( side="left", padx=5 ) # 布局 canvas.grid(row=0, column=0, sticky="nsew") scrollbar.grid(row=0, column=1, sticky="ns") self.rowconfigure(0, weight=1) self.columnconfigure(0, weight=1) def _create_erp_group(self, parent): """创建 ERP 配置组""" group = ttk.LabelFrame(parent, text="ERP 系统配置", padding=10) group.grid(row=0, column=0, columnspan=2, pady=10, padx=10, sticky="ew") # URL ttk.Label(group, text="ERP URL:").grid(row=0, column=0, sticky="w", pady=5) self.erp_url_var = tk.StringVar() ttk.Entry(group, textvariable=self.erp_url_var, width=50).grid( row=0, column=1, pady=5, sticky="ew" ) # 用户名 ttk.Label(group, text="用户名:").grid(row=1, column=0, sticky="w", pady=5) self.erp_username_var = tk.StringVar() ttk.Entry(group, textvariable=self.erp_username_var, width=50).grid( row=1, column=1, pady=5, sticky="ew" ) # 密码 ttk.Label(group, text="密码:").grid(row=2, column=0, sticky="w", pady=5) self.erp_password_var = tk.StringVar() entry = ttk.Entry(group, textvariable=self.erp_password_var, width=50, show="*") entry.grid(row=2, column=1, pady=5, sticky="ew") group.columnconfigure(1, weight=1) def _create_database_group(self, parent): """创建数据库配置组""" group = ttk.LabelFrame(parent, text="数据库配置", padding=10) group.grid(row=1, column=0, columnspan=2, pady=10, padx=10, sticky="ew") # 数据库类型选择 ttk.Label(group, text="数据库类型:").grid(row=0, column=0, sticky="w", pady=5) self.db_type_var = tk.StringVar() db_type_combo = ttk.Combobox( group, textvariable=self.db_type_var, values=["sqlserver", "mysql"], state="readonly", width=30, ) db_type_combo.grid(row=0, column=1, sticky="w", pady=5) db_type_combo.bind("<>", self._on_db_type_changed) # SQL Server 配置 self.sqlserver_frame = ttk.Frame(group) self.sqlserver_frame.grid(row=1, column=0, columnspan=2, sticky="ew", pady=5) ttk.Label(self.sqlserver_frame, text="服务器:").grid(row=0, column=0, sticky="w", pady=5) self.db_server_var = tk.StringVar() ttk.Entry(self.sqlserver_frame, textvariable=self.db_server_var, width=50).grid( row=0, column=1, pady=5, sticky="ew" ) # MySQL 配置 self.mysql_frame = ttk.Frame(group) ttk.Label(self.mysql_frame, text="主机:").grid(row=0, column=0, sticky="w", pady=5) self.mysql_host_var = tk.StringVar() ttk.Entry(self.mysql_frame, textvariable=self.mysql_host_var, width=50).grid( row=0, column=1, pady=5, sticky="ew" ) ttk.Label(self.mysql_frame, text="端口:").grid(row=1, column=0, sticky="w", pady=5) self.mysql_port_var = tk.IntVar(value=3306) ttk.Spinbox( self.mysql_frame, from_=1, to=65535, textvariable=self.mysql_port_var, width=10 ).grid(row=1, column=1, sticky="w", pady=5) # 通用配置(两种数据库都需要) ttk.Label(group, text="数据库:").grid(row=2, column=0, sticky="w", pady=5) self.db_name_var = tk.StringVar() ttk.Entry(group, textvariable=self.db_name_var, width=50).grid( row=2, column=1, pady=5, sticky="ew" ) ttk.Label(group, text="用户名:").grid(row=3, column=0, sticky="w", pady=5) self.db_username_var = tk.StringVar() ttk.Entry(group, textvariable=self.db_username_var, width=50).grid( row=3, column=1, pady=5, sticky="ew" ) ttk.Label(group, text="密码:").grid(row=4, column=0, sticky="w", pady=5) self.db_password_var = tk.StringVar() entry = ttk.Entry(group, textvariable=self.db_password_var, width=50, show="*") entry.grid(row=4, column=1, pady=5, sticky="ew") group.columnconfigure(1, weight=1) def _on_db_type_changed(self, event=None): """数据库类型改变时的回调""" db_type = self.db_type_var.get() if db_type == "mysql": self.sqlserver_frame.grid_forget() self.mysql_frame.grid(row=1, column=0, columnspan=2, sticky="ew", pady=5) else: self.mysql_frame.grid_forget() self.sqlserver_frame.grid(row=1, column=0, columnspan=2, sticky="ew", pady=5) def _create_browser_group(self, parent): """创建浏览器配置组""" group = ttk.LabelFrame(parent, text="浏览器设置", padding=10) group.grid(row=2, column=0, pady=10, padx=10, sticky="ew") self.browser_headless_var = tk.BooleanVar() ttk.Checkbutton( group, text="无头模式 (不显示浏览器)", variable=self.browser_headless_var ).grid(row=0, column=0, sticky="w", pady=5) self.browser_ignore_https_var = tk.BooleanVar() ttk.Checkbutton( group, text="忽略 HTTPS 错误", variable=self.browser_ignore_https_var ).grid(row=1, column=0, sticky="w", pady=5) self.browser_auto_close_var = tk.BooleanVar() ttk.Checkbutton( group, text="操作完成后自动关闭浏览器", variable=self.browser_auto_close_var ).grid(row=2, column=0, sticky="w", pady=5) 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) 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 # 数据目录 ttk.Label(group, text="数据目录:").grid(row=0, column=0, sticky="w", pady=5) self.data_dir_selector = FileSelector( group, label_text="", file_type="directory", initial_dir="D:/python/playwrite/data/", ) self.data_dir_selector.grid(row=1, column=0, columnspan=2, sticky="ew", pady=5) # 默认输出文件 ttk.Label(group, text="默认输出文件:").grid(row=2, column=0, sticky="w", pady=5) self.default_output_var = tk.StringVar() ttk.Entry(group, textvariable=self.default_output_var, width=40).grid( row=2, column=1, columnspan=2, sticky="ew", pady=5 ) # 校验输出文件 ttk.Label(group, text="校验输出文件:").grid(row=3, column=0, sticky="w", pady=5) ttk.Entry(group, textvariable=self.validation_output_filename_var, width=40).grid( row=3, column=1, columnspan=2, sticky="ew", pady=5 ) 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) group.grid(row=3, column=0, columnspan=2, pady=10, padx=10, sticky="ew") # 批次大小 ttk.Label(group, text="批次大小:").grid(row=0, column=0, sticky="w", pady=5) self.batch_size_var = tk.IntVar(value=100) ttk.Spinbox( group, from_=10, to=500, textvariable=self.batch_size_var, width=10 ).grid(row=0, column=1, sticky="w", pady=5) # 详细日志 self.verbose_var = tk.BooleanVar() ttk.Checkbutton(group, text="启用详细日志", variable=self.verbose_var).grid( row=1, column=0, columnspan=2, sticky="w", pady=5 ) # 自动转换 self.auto_convert_var = tk.BooleanVar() ttk.Checkbutton( group, text="自动转换 Excel 格式", variable=self.auto_convert_var ).grid(row=2, column=0, columnspan=2, sticky="w", pady=5) # 合并批次 self.merge_batches_var = tk.BooleanVar() ttk.Checkbutton( group, text="自动合并批次数据", variable=self.merge_batches_var ).grid(row=3, column=0, columnspan=2, sticky="w", pady=5) # 数据库持久化 self.enable_db_persistence_var = tk.BooleanVar() ttk.Checkbutton( group, text="保存到数据库", variable=self.enable_db_persistence_var ).grid(row=4, column=0, columnspan=2, sticky="w", pady=5) def _create_validation_group(self, parent): """创建物料校验配置组""" group = ttk.LabelFrame(parent, text="物料校验设置", padding=10) group.grid(row=4, column=0, columnspan=2, pady=10, padx=10, sticky="ew") # 数据源选择 ttk.Label(group, text="默认数据源:").grid(row=0, column=0, sticky="w", pady=5) self.validation_data_source_var = tk.StringVar() data_source_combo = ttk.Combobox( group, textvariable=self.validation_data_source_var, values=["database_full", "database_filtered", "excel_existing", "excel_full"], state="readonly", width=30, ) data_source_combo.grid(row=0, column=1, sticky="w", pady=5) # 使用数据库 self.validation_use_database_var = tk.BooleanVar() ttk.Checkbutton( group, text="使用数据库作为数据源", variable=self.validation_use_database_var ).grid(row=1, column=0, columnspan=2, sticky="w", pady=5) # 输出文件名 ttk.Label(group, text="输出文件名:").grid(row=2, column=0, sticky="w", pady=5) ttk.Entry(group, textvariable=self.validation_output_filename_var, width=30).grid( row=2, column=1, sticky="w", pady=5 ) # 批次大小 ttk.Label(group, text="数据库批次大小:").grid(row=3, column=0, sticky="w", pady=5) self.validation_batch_size_var = tk.IntVar(value=2000) ttk.Spinbox( group, from_=100, to=2000, textvariable=self.validation_batch_size_var, width=10 ).grid(row=3, column=1, sticky="w", pady=5) # 匹配模式 ttk.Label(group, text="匹配模式:").grid(row=4, column=0, sticky="w", pady=5) self.validation_match_mode_var = tk.StringVar() match_mode_combo = ttk.Combobox( group, textvariable=self.validation_match_mode_var, values=["substring", "exact"], state="readonly", width=30, ) match_mode_combo.grid(row=4, column=1, sticky="w", pady=5) # CRUD 操作 self.validation_enable_crud_var = tk.BooleanVar() ttk.Checkbutton( group, text="启用 CRUD 操作(管理待删除物料)", variable=self.validation_enable_crud_var ).grid(row=5, column=0, columnspan=2, sticky="w", pady=5) # 默认负责人 ttk.Label(group, text="默认负责人:").grid(row=6, column=0, sticky="w", pady=5) self.validation_default_manager_var = tk.StringVar() ttk.Entry(group, textvariable=self.validation_default_manager_var, width=30).grid( row=6, column=1, sticky="w", pady=5 ) 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): """从配置加载设置到界面""" # 判断是否为仅测试用户模式 is_user_only = self.session_manager and self.session_manager.get_user_type() == 'User' if is_user_only: # User 用户模式 - 只需要加载路径设置到界面 # 路径设置 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 # 管理员模式 - 加载所有配置 # ERP 设置 self.erp_url_var.set(self.config.get("erp.url", "")) self.erp_username_var.set(self.config.get("erp.username", "")) self.erp_password_var.set(self.config.get("erp.password", "")) # 数据库设置 db_type = self.config.get("database.db_type", "sqlserver") self.db_type_var.set(db_type) if db_type == "mysql": self.db_server_var.set(self.config.get("database.server", "")) self.mysql_host_var.set(self.config.get("database.mysql.host", "")) self.mysql_port_var.set(self.config.get("database.mysql.port", 3306)) else: self.db_server_var.set(self.config.get("database.server", "")) self.db_name_var.set(self.config.get("database.database", "")) self.db_username_var.set(self.config.get("database.username", "")) self.db_password_var.set(self.config.get("database.password", "")) # 更新界面显示 self._on_db_type_changed() # 浏览器设置(已合并到 ERP 配置中) self.browser_headless_var.set(self.config.get("erp.headless", True)) self.browser_ignore_https_var.set( self.config.get("erp.ignore_https_errors", True) ) self.browser_auto_close_var.set(self.config.get("erp.auto_close_browser", True)) # 路径设置 self.data_dir_selector.set(self.config.get("paths.data_dir", "")) self.default_output_var.set(self.config.get("paths.default_output", "")) # 处理设置 self.batch_size_var.set(self.config.get("extraction.batch_size", 100)) self.verbose_var.set(self.config.get("extraction.verbose", True)) self.auto_convert_var.set(self.config.get("extraction.auto_convert", True)) self.merge_batches_var.set(self.config.get("extraction.merge_batches", True)) self.enable_db_persistence_var.set(self.config.get("extraction.enable_db_persistence", False)) # 校验设置 self.validation_data_source_var.set(self.config.get("validation.data_source", "database_full")) self.validation_use_database_var.set(self.config.get("validation.use_database", True)) self.validation_output_filename_var.set(self.config.get("paths.validation_output", "物料状态校验结果.xlsx")) self.validation_batch_size_var.set(self.config.get("validation.batch_size", 2000)) self.validation_match_mode_var.set(self.config.get("validation.match_mode", "substring")) 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): """保存界面设置到配置""" # 判断是否为仅测试用户模式 is_user_only = self.session_manager and self.session_manager.get_user_type() == 'User' if is_user_only: # 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(): messagebox.showinfo("成功", "设置已保存") # 通知其他标签页重新加载配置 self._notify_config_reload() else: messagebox.showerror("错误", "保存设置失败") return # 管理员模式 - 加载所有配置 # ERP 设置 self.config.set("erp.url", self.erp_url_var.get()) self.config.set("erp.username", self.erp_username_var.get()) self.config.set("erp.password", self.erp_password_var.get()) # 数据库设置 db_type = self.db_type_var.get() self.config.set("database.db_type", db_type) if db_type == "mysql": # MySQL: 使用 host 字段 self.config.set("database.server", self.mysql_host_var.get()) self.config.set("database.mysql.host", self.mysql_host_var.get()) self.config.set("database.mysql.port", self.mysql_port_var.get()) else: # SQL Server: 使用 server 字段 self.config.set("database.server", self.db_server_var.get()) self.config.set("database.database", self.db_name_var.get()) self.config.set("database.username", self.db_username_var.get()) self.config.set("database.password", self.db_password_var.get()) # 浏览器设置(已合并到 ERP 配置中) self.config.set("erp.headless", self.browser_headless_var.get()) self.config.set("erp.ignore_https_errors", self.browser_ignore_https_var.get()) self.config.set("erp.auto_close_browser", self.browser_auto_close_var.get()) # 路径设置 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("extraction.batch_size", self.batch_size_var.get()) self.config.set("extraction.verbose", self.verbose_var.get()) self.config.set("extraction.auto_convert", self.auto_convert_var.get()) self.config.set("extraction.merge_batches", self.merge_batches_var.get()) self.config.set("extraction.enable_db_persistence", self.enable_db_persistence_var.get()) # 校验设置 self.config.set("validation.data_source", self.validation_data_source_var.get()) self.config.set("validation.use_database", self.validation_use_database_var.get()) self.config.set("paths.validation_output", self.validation_output_filename_var.get()) self.config.set("validation.batch_size", self.validation_batch_size_var.get()) self.config.set("validation.match_mode", self.validation_match_mode_var.get()) 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 类型用户) db_type = self.config.get("database.db_type", "sqlserver") try: if db_type == "mysql": import mysql.connector from mysql.connector import Error conn = mysql.connector.connect( host=self.config.get("database.mysql.host", ""), port=self.config.get("database.mysql.port", 3306), database=self.config.get("database.database", ""), user=self.config.get("database.username", ""), password=self.config.get("database.password", ""), connection_timeout=5 ) conn.close() messagebox.showinfo("成功", "MySQL 数据库连接测试成功!") else: conn_str = ( f"DRIVER={{ODBC Driver 18 for SQL Server}};" f"SERVER={self.config.get('database.server', '')};" f"DATABASE={self.config.get('database.database', '')};" f"UID={self.config.get('database.username', '')};" f"PWD={self.config.get('database.password', '')};" f"TrustServerCertificate=yes;" ) conn = pyodbc.connect(conn_str, timeout=5) conn.close() messagebox.showinfo("成功", "SQL Server 数据库连接测试成功!") except ImportError: if db_type == "mysql": messagebox.showerror("错误", "未安装 mysql-connector-python,请运行:\npip install mysql-connector-python") else: messagebox.showerror("错误", "未安装 pyodbc,请运行:\npip install pyodbc") except Exception as e: messagebox.showerror("错误", f"数据库连接失败:\n{str(e)}") def test_erp_connection(self): """测试 ERP 连接""" # ERP 连接测试需要实际启动浏览器 messagebox.showinfo("提示", "ERP 连接测试将在数据提取功能中自动验证") def reset_defaults(self): """恢复默认设置""" if messagebox.askyesno("确认", "确定要恢复默认设置吗?这将覆盖 .env 文件中的所有配置。"): from config.schema import AppConfig self.config.config = AppConfig.from_env() # 重新加载默认配置 self.config.save() self.load_settings() messagebox.showinfo("成功", "已恢复默认设置")