Files
playwrite/gui/settings_tab.py
Misaka_Company d18bedcd77 feat: implement user type-based UI access control
Restrict settings interface and material validation features based on user type:

- SettingsTab: User type only sees "Test ERP Connection" and "Test Database Connection" buttons; Admin sees full configuration interface
- MaterialValidationTab: Hide manager filter and data source options from non-admin users; default to database_filtered mode
- test_db_connection(): Read from config directly to support User type without UI variables
- Add backward compatibility: no session_manager defaults to full interface

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-10 17:30:26 +08:00

502 lines
22 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 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.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(
"<Configure>", 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)
# 按钮区域 - 根据用户类型显示不同按钮
button_frame = ttk.Frame(scrollable_frame)
button_frame.grid(row=6, column=0, columnspan=2, pady=20, sticky="ew")
if is_user_only:
# 普通用户只显示测试按钮
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)
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("<<ComboboxSelected>>", 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):
"""创建路径配置组"""
group = ttk.LabelFrame(parent, text="路径设置", padding=10)
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=3, column=0, columnspan=2, sticky="ew", pady=5
)
group.columnconfigure(0, weight=1)
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)
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=2, column=1, sticky="w", pady=5)
# 匹配模式
ttk.Label(group, text="匹配模式:").grid(row=3, 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=3, 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=4, column=0, columnspan=2, sticky="w", pady=5)
# 默认负责人
ttk.Label(group, text="默认负责人:").grid(row=5, 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=5, 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:
# 普通用户模式 - 只需要加载测试连接所需的配置
# 不需要加载设置到界面,因为界面没有配置输入框
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_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", ""))
def save_settings(self):
"""保存界面设置到配置"""
# 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("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())
# 保存到文件
if self.config.save():
messagebox.showinfo("成功", "设置已保存")
else:
messagebox.showerror("错误", "保存设置失败")
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("成功", "已恢复默认设置")