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

@@ -269,6 +269,34 @@ class ValidationConfig:
return errors 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 @dataclass
class AppConfig: class AppConfig:
"""应用总配置""" """应用总配置"""
@@ -278,6 +306,7 @@ class AppConfig:
paths: PathConfig paths: PathConfig
extraction: ExtractionConfig extraction: ExtractionConfig
validation: ValidationConfig validation: ValidationConfig
ui: UIConfig
@classmethod @classmethod
def from_env(cls) -> "AppConfig": def from_env(cls) -> "AppConfig":
@@ -288,6 +317,7 @@ class AppConfig:
paths=PathConfig.from_env(), paths=PathConfig.from_env(),
extraction=ExtractionConfig.from_env(), extraction=ExtractionConfig.from_env(),
validation=ValidationConfig.from_env(), validation=ValidationConfig.from_env(),
ui=UIConfig.from_env(),
) )
def validate(self) -> list[str]: def validate(self) -> list[str]:
@@ -298,6 +328,7 @@ class AppConfig:
errors.extend(self.paths.validate()) errors.extend(self.paths.validate())
errors.extend(self.extraction.validate()) errors.extend(self.extraction.validate())
errors.extend(self.validation.validate()) errors.extend(self.validation.validate())
errors.extend(self.ui.validate())
return errors return errors
def to_dict(self) -> dict: def to_dict(self) -> dict:
@@ -348,4 +379,9 @@ class AppConfig:
"default_manager": self.validation.default_manager, "default_manager": self.validation.default_manager,
"match_mode": self.validation.match_mode, "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,
},
} }

View File

@@ -14,7 +14,7 @@ import tkinter as tk
from tkinter import ttk, filedialog, messagebox from tkinter import ttk, filedialog, messagebox
from pathlib import Path from pathlib import Path
from contextlib import redirect_stdout 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.config_manager import ConfigManager
from gui.progress import ProgressInfo, ProgressCalculator from gui.progress import ProgressInfo, ProgressCalculator
from gui.utils import RealtimeOutput from gui.utils import RealtimeOutput
@@ -23,16 +23,18 @@ from gui.utils import RealtimeOutput
class DataExtractionTab(ttk.Frame): class DataExtractionTab(ttk.Frame):
"""数据提取标签页""" """数据提取标签页"""
def __init__(self, parent, config: ConfigManager): def __init__(self, parent, config: ConfigManager, main_window=None):
""" """
初始化数据提取标签页 初始化数据提取标签页
Args: Args:
parent: 父容器 parent: 父容器
config: 配置管理器 config: 配置管理器
main_window: 主窗口引用,用于共享 Production ID 数据
""" """
super().__init__(parent) super().__init__(parent)
self.config = config self.config = config
self.main_window = main_window
self.extracting = False self.extracting = False
self.extractor = None self.extractor = None
self.extraction_thread = None self.extraction_thread = None
@@ -44,6 +46,9 @@ class DataExtractionTab(ttk.Frame):
self.create_widgets() self.create_widgets()
# 应用字体设置
self._apply_ui_config()
# 稍后显示就绪消息 # 稍后显示就绪消息
try: try:
self.log_text.info("数据提取标签页已就绪") self.log_text.info("数据提取标签页已就绪")
@@ -52,9 +57,50 @@ class DataExtractionTab(ttk.Frame):
def create_widgets(self): def create_widgets(self):
"""创建界面组件""" """创建界面组件"""
# 主容器 - 使用 PanedWindow 分割上下部分 # 主容器 - 使用水平 PanedWindow 分割左右部分
main_paned = ttk.PanedWindow(self, orient=tk.VERTICAL) horizontal_paned = ttk.PanedWindow(self, orient=tk.HORIZONTAL)
main_paned.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) 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) control_frame = ttk.Frame(main_paned)
@@ -69,24 +115,6 @@ class DataExtractionTab(ttk.Frame):
def _create_control_panel(self, parent): 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 = ttk.LabelFrame(parent, text="输出文件", padding=10)
output_group.pack(fill=tk.X, pady=5) 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 = LogText(parent, height=15, readonly=True)
self.log_text.pack(fill=tk.BOTH, expand=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): def start_extraction(self):
"""开始数据提取""" """开始数据提取"""
# 验证输入 # 获取 Production ID 列表
input_file = self.input_file_selector.get() production_ids = self.production_id_input.get()
if not production_ids:
messagebox.showerror("错误", "请输入至少一个 Production ID")
return
output_file = self.output_file_selector.get() 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: if not output_file:
messagebox.showerror("错误", "请指定输出文件路径") messagebox.showerror("错误", "请指定输出文件路径")
return return
@@ -177,11 +232,11 @@ class DataExtractionTab(ttk.Frame):
self.progress_bar["value"] = 0 self.progress_bar["value"] = 0
self.status_label.config(text="正在登录...") self.status_label.config(text="正在登录...")
self.log_text.clear() self.log_text.clear()
self.log_text.info("开始数据提取...") self.log_text.info(f"开始数据提取... ({len(production_ids)} 个 Production ID)")
# 在后台线程中执行提取 # 在后台线程中执行提取
self.extraction_thread = threading.Thread( 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() self.extraction_thread.start()
@@ -192,8 +247,16 @@ class DataExtractionTab(ttk.Frame):
self.log_text.warning("正在停止提取...") self.log_text.warning("正在停止提取...")
self.status_label.config(text="正在停止...") 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:
# 创建临时文件保存 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))
try: try:
# 导入提取器(延迟导入以避免启动时加载 Playwright # 导入提取器(延迟导入以避免启动时加载 Playwright
from utils.离散备料计划维护数据提取 import DiscreteMaterialPlanExtractor from utils.离散备料计划维护数据提取 import DiscreteMaterialPlanExtractor
@@ -224,7 +287,7 @@ class DataExtractionTab(ttk.Frame):
# 重定向 stdout 并执行提取(带进度回调) # 重定向 stdout 并执行提取(带进度回调)
with redirect_stdout(realtime_output): with redirect_stdout(realtime_output):
result = self.extractor.extract( result = self.extractor.extract(
production_id_file=input_file, production_id_file=temp_file,
output_file=output_file, output_file=output_file,
progress_callback=progress_callback, progress_callback=progress_callback,
) )
@@ -236,6 +299,13 @@ class DataExtractionTab(ttk.Frame):
else: else:
self._update_log("提取失败", "ERROR") self._update_log("提取失败", "ERROR")
finally:
# 删除临时文件
try:
os.unlink(temp_file)
except:
pass
except Exception as e: except Exception as e:
self._update_log(f"提取过程中发生错误:{str(e)}", "ERROR") self._update_log(f"提取过程中发生错误:{str(e)}", "ERROR")
finally: finally:
@@ -287,3 +357,15 @@ class DataExtractionTab(ttk.Frame):
self.log_text.error(message) self.log_text.error(message)
self.after(0, update) 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()

View File

@@ -28,6 +28,7 @@ class MainWindow:
self.root = root self.root = root
self.config = ConfigManager() self.config = ConfigManager()
self.session_manager = session_manager self.session_manager = session_manager
self.shared_production_ids = [] # 共享的 Production ID 列表
# 设置窗口属性(包含用户信息) # 设置窗口属性(包含用户信息)
user_type_display = "管理员" if session_manager.is_admin() else "用户" user_type_display = "管理员" if session_manager.is_admin() else "用户"
@@ -49,6 +50,18 @@ class MainWindow:
# 居中窗口 # 居中窗口
self._center_window() 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): def create_menu(self):
"""创建菜单栏""" """创建菜单栏"""
menubar = tk.Menu(self.root) menubar = tk.Menu(self.root)
@@ -70,18 +83,31 @@ class MainWindow:
self.notebook = ttk.Notebook(self.root) self.notebook = ttk.Notebook(self.root)
self.notebook.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) self.notebook.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
# 数据提取标签页 # 数据提取标签页(传递 self 以支持共享 Production ID
self.extraction_tab = DataExtractionTab(self.notebook, self.config) self.extraction_tab = DataExtractionTab(self.notebook, self.config, self)
self.notebook.add(self.extraction_tab, text="数据提取") self.notebook.add(self.extraction_tab, text="数据提取")
# 物料校验标签页(传入 session_manager # 物料校验标签页(传入 session_manager 和 main_window
self.validation_tab = MaterialValidationTab(self.notebook, self.config, self.session_manager) self.validation_tab = MaterialValidationTab(self.notebook, self.config, self.session_manager, self)
self.notebook.add(self.validation_tab, text="物料校验") self.notebook.add(self.validation_tab, text="物料校验")
# 设置标签页(传入 session_manager # 设置标签页(传入 session_manager
self.settings_tab = SettingsTab(self.notebook, self.config, self.session_manager) self.settings_tab = SettingsTab(self.notebook, self.config, self.session_manager)
self.notebook.add(self.settings_tab, text="设置") 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): def create_status_bar(self):
"""创建状态栏""" """创建状态栏"""
self.status_bar = ttk.Frame(self.root, relief=tk.SUNKEN) self.status_bar = ttk.Frame(self.root, relief=tk.SUNKEN)
@@ -144,3 +170,17 @@ class MainWindow:
"• 数据库持久化 - 将提取的数据自动保存到数据库\n\n" "• 数据库持久化 - 将提取的数据自动保存到数据库\n\n"
"基于 Playwright 和 Python 开发", "基于 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("配置已重新加载")

View File

@@ -131,7 +131,7 @@ class CheckboxTreeview(ttk.Treeview):
class MaterialValidationTab(ttk.Frame): 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: 父容器 parent: 父容器
config: 配置管理器 config: 配置管理器
session_manager: SessionManager 实例,用于权限控制 session_manager: SessionManager 实例,用于权限控制
main_window: 主窗口引用,用于获取共享的 Production ID
""" """
super().__init__(parent) super().__init__(parent)
self.config = config self.config = config
self.session_manager = session_manager self.session_manager = session_manager
self.main_window = main_window
self.validating = False self.validating = False
self.validation_results = None self.validation_results = None
self.material_records_cache = None # 缓存完整的物料记录 self.material_records_cache = None # 缓存完整的物料记录
self.shared_production_ids = [] # 共享的 Production ID 列表
# 负责人筛选相关 # 负责人筛选相关
self.managers: List[str] = [] # 可用负责人列表 self.managers: List[str] = [] # 可用负责人列表
@@ -244,15 +247,73 @@ class MaterialValidationTab(ttk.Frame):
# 数据库过滤模式 - 需要 ProductionID 文件 # 数据库过滤模式 - 需要 ProductionID 文件
self.db_filtered_frame = ttk.Frame(file_group) self.db_filtered_frame = ttk.Frame(file_group)
# 判断用户类型
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_production_id_selector = FileSelector(
self.db_filtered_frame, self.db_filtered_frame,
label_text="ProductionID 文件:", label_text="",
file_type="file", file_type="file",
file_types=[("文本文件", "*.txt"), ("所有文件", "*.*")], file_types=[("文本文件", "*.txt"), ("所有文件", "*.*")],
initial_dir="D:/python/playwrite/", initial_dir="D:/python/playwrite/",
) )
self.db_filtered_production_id_selector.pack(fill=tk.X) 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( self.output_file_selector = FileSelector(
file_group, file_group,
@@ -396,6 +457,56 @@ class MaterialValidationTab(ttk.Frame):
elif mode == "database_filtered": elif mode == "database_filtered":
self.db_filtered_frame.pack(fill=tk.X, pady=(0, 5)) 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): def _create_manager_filter_area(self, parent):
"""创建负责人筛选区域""" """创建负责人筛选区域"""
# PERMISSION CHECK: 非管理员用户不显示筛选区域 # PERMISSION CHECK: 非管理员用户不显示筛选区域
@@ -716,12 +827,34 @@ class MaterialValidationTab(ttk.Frame):
mode = self.source_mode.get() mode = self.source_mode.get()
input_file = None input_file = None
production_id_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": if mode == "database_full":
# 无需输入文件 # 无需输入文件
pass pass
elif mode == "database_filtered": elif mode == "database_filtered":
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() production_id_file = self.db_filtered_production_id_selector.get()
if not production_id_file: if not production_id_file:
messagebox.showerror("错误", "请选择 ProductionID 文件") messagebox.showerror("错误", "请选择 ProductionID 文件")
@@ -758,16 +891,27 @@ class MaterialValidationTab(ttk.Frame):
# 在后台线程中执行校验 # 在后台线程中执行校验
validation_thread = threading.Thread( validation_thread = threading.Thread(
target=self._validation_worker_enhanced, 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, daemon=True,
) )
validation_thread.start() validation_thread.start()
def _validation_worker_enhanced( 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: 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 from utils.material_status_validator import MaterialStatusValidator
@@ -820,6 +964,12 @@ class MaterialValidationTab(ttk.Frame):
import traceback import traceback
self._update_log(traceback.format_exc(), "ERROR") self._update_log(traceback.format_exc(), "ERROR")
finally: finally:
# 清理临时文件
if temp_production_id_file and os.path.exists(temp_production_id_file):
try:
os.unlink(temp_production_id_file)
except:
pass
# 更新 UI 状态 # 更新 UI 状态
self.after(0, self._validation_complete) self.after(0, self._validation_complete)

View File

@@ -57,10 +57,11 @@ class SettingsTab(ttk.Frame):
self._create_paths_group(scrollable_frame) self._create_paths_group(scrollable_frame)
self._create_extraction_group(scrollable_frame) self._create_extraction_group(scrollable_frame)
self._create_validation_group(scrollable_frame) self._create_validation_group(scrollable_frame)
self._create_ui_group(scrollable_frame)
# 按钮区域 - 根据用户类型显示不同按钮 # 按钮区域 - 根据用户类型显示不同按钮
button_frame = ttk.Frame(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: if is_user_only:
# 普通用户只显示测试按钮 # 普通用户只显示测试按钮
@@ -331,6 +332,39 @@ class SettingsTab(ttk.Frame):
group.columnconfigure(1, weight=1) 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): 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_enable_crud_var.set(self.config.get("validation.enable_crud_operations", False))
self.validation_default_manager_var.set(self.config.get("validation.default_manager", "")) 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): def save_settings(self):
"""保存界面设置到配置""" """保存界面设置到配置"""
# ERP 设置 # 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.enable_crud_operations", self.validation_enable_crud_var.get())
self.config.set("validation.default_manager", self.validation_default_manager_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(): if self.config.save():
messagebox.showinfo("成功", "设置已保存") messagebox.showinfo("成功", "设置已保存")
# 通知其他标签页重新加载配置
self._notify_config_reload()
else: else:
messagebox.showerror("错误", "保存设置失败") 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): def test_db_connection(self):
"""测试数据库连接""" """测试数据库连接"""
# 从配置读取而不是从 UI 变量(支持 User 类型用户) # 从配置读取而不是从 UI 变量(支持 User 类型用户)

View File

@@ -6,5 +6,6 @@ GUI 自定义组件模块
from .file_selector import FileSelector from .file_selector import FileSelector
from .log_text import LogText from .log_text import LogText
from .production_id_input import ProductionIdInput
__all__ = ['FileSelector', 'LogText'] __all__ = ['FileSelector', 'LogText', 'ProductionIdInput']

View File

@@ -178,3 +178,9 @@ class LogText(tk.Frame):
def grid(self, **kwargs): def grid(self, **kwargs):
"""Grid 布局""" """Grid 布局"""
super().grid(**kwargs) 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)

View File

@@ -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("<FocusIn>", self._on_focus_in)
self.text_widget.bind("<FocusOut>", 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)