import os import sys import logging import json from datetime import datetime from pathlib import Path from utils.modbus_utils import ModbusUtils # 导入PySide6 from PySide6.QtWidgets import ( QWidget, QMessageBox, QTableWidgetItem, QStackedWidget, QLabel, QMainWindow, QTableWidget, QMenu, QComboBox, QFormLayout, QDialog, QVBoxLayout, QHBoxLayout, QPushButton ) from PySide6.QtCore import Qt, QTimer from PySide6.QtGui import QBrush, QColor # 导入UI from ui.main_window_ui import MainWindowUI # 导入相机显示组件和设置组件 from widgets.camera_display_widget import CameraDisplayWidget from widgets.camera_settings_widget import CameraSettingsWidget # 导入检验配置管理器 from utils.inspection_config_manager import InspectionConfigManager # 导入托盘类型管理器 from utils.pallet_type_manager import PalletTypeManager class MainWindow(MainWindowUI): """主窗口""" def __init__(self, user_id=None, user_name=None, corp_name=None, corp_id=None, position_id=None): super().__init__() self.user_id = user_id self.user_name = user_name self.corp_name = corp_name self.corp_id = corp_id self.position_id = position_id # 设置窗口标题 if user_name and corp_name: self.setWindowTitle(f"腾智微丝产线包装系统 - {user_name} ({corp_name})") # 加载配置文件 self.config = self.load_config() self.camera_enabled = self.config.get('camera', {}).get('enabled', False) # 初始化检验配置管理器 self.inspection_manager = InspectionConfigManager.get_instance() # 初始化托盘类型管理器 self.pallet_type_manager = PalletTypeManager.get_instance() # 创建表单布局,用于添加托盘类型选择控件 self.material_form_layout = QFormLayout() self.material_content_layout.addLayout(self.material_form_layout) self.output_form_layout = QFormLayout() self.output_content_layout.addLayout(self.output_form_layout) # 只有在相机启用时创建相机显示组件 if self.camera_enabled: # 创建相机显示组件并添加到上料区 self.camera_display = CameraDisplayWidget() self.material_content_layout.addWidget(self.camera_display) else: # 在上料区添加占位标签 self.material_placeholder = QLabel("相机功能已禁用") self.material_placeholder.setAlignment(Qt.AlignCenter) self.material_placeholder.setStyleSheet("color: #888888; background-color: #f0f0f0;") self.material_content_layout.addWidget(self.material_placeholder) # 为下料区添加占位标签,确保它保持为空 self.output_placeholder = QLabel("下料区 - 暂无内容") self.output_placeholder.setAlignment(Qt.AlignCenter) self.output_placeholder.setStyleSheet("color: #888888; background-color: #f0f0f0;") self.output_content_layout.addWidget(self.output_placeholder) # 创建堆叠部件 self.stacked_widget = QStackedWidget() self.stacked_widget.addWidget(self.central_widget) # 主页面 # 不在这里直接初始化相机设置组件 # 延迟创建,保证创建的时候SettingsUI的所有控件都已经准备好 self.camera_settings = None # 设置中央部件为堆叠部件 self.setCentralWidget(self.stacked_widget) # 添加托盘类型选择下拉框 self.add_pallet_type_selectors() # 连接信号和槽 self.connect_signals() # 默认显示主页面 self.stacked_widget.setCurrentIndex(0) # 配置检验列 - 使用检验配置管理器获取启用的列数和标题 self.update_inspection_columns() # 设置表格上下文菜单 self.process_table.setContextMenuPolicy(Qt.CustomContextMenu) # 加载未完成的检验数据 self.load_finished_inspection_data() # 加载已完成检验数据 self.show_pack_item() logging.info(f"主窗口已创建,用户: {user_name}") def add_pallet_type_selectors(self): """添加托盘类型选择下拉框""" # 创建上料托盘类型选择下拉框 self.input_pallet_type_label = QLabel("上料托盘类型:") self.input_pallet_type_label.setFont(self.normal_font) self.input_pallet_type_label.setVisible(False) self.material_form_layout.addRow(self.input_pallet_type_label) self.input_pallet_type_combo = QComboBox() self.input_pallet_type_combo.setFont(self.normal_font) self.input_pallet_type_combo.setVisible(False) self.material_form_layout.addRow("", self.input_pallet_type_combo) # 创建下料托盘类型选择下拉框 self.output_pallet_type_label = QLabel("下料托盘类型:") self.output_pallet_type_label.setFont(self.normal_font) self.output_pallet_type_label.setVisible(False) self.output_form_layout.addRow(self.output_pallet_type_label) self.output_pallet_type_combo = QComboBox() self.output_pallet_type_combo.setFont(self.normal_font) self.output_pallet_type_combo.setVisible(False) self.output_form_layout.addRow("", self.output_pallet_type_combo) # 加载托盘类型数据 self.update_pallet_types() def update_pallet_types(self): """更新托盘类型下拉框""" # 重新加载托盘类型数据 self.pallet_type_manager.reload_pallet_types() # 更新上料托盘类型 input_types = self.pallet_type_manager.get_pallet_types_by_operation("input") self.input_pallet_type_combo.clear() for pallet_type in input_types: self.input_pallet_type_combo.addItem(pallet_type['type_name'], pallet_type['id']) # 更新下料托盘类型 output_types = self.pallet_type_manager.get_pallet_types_by_operation("output") self.output_pallet_type_combo.clear() for pallet_type in output_types: self.output_pallet_type_combo.addItem(pallet_type['type_name'], pallet_type['id']) def load_config(self): """加载配置文件""" config_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "config", "app_config.json") try: with open(config_path, 'r', encoding='utf-8') as f: config = json.load(f) logging.info(f"已加载配置文件: {config_path}") return config except Exception as e: logging.error(f"加载配置文件失败: {e}") return {} def connect_signals(self): # 连接菜单动作 self.main_action.triggered.connect(self.show_main_page) self.settings_action.triggered.connect(self.show_settings_page) # 工程号输入框回车事件 self.order_edit.returnPressed.connect(self.handle_order_enter) # 托盘号输入框回车和切换事件,触发未加载数据查询 # QComboBox没有returnPressed信号,只有currentTextChanged和activated信号 self.tray_edit.currentTextChanged.connect(self.load_finished_inspection_data) self.tray_edit.activated.connect(self.load_finished_inspection_data) # 当用户选择一项时触发 # 连接按钮事件 self.input_button.clicked.connect(self.handle_input) self.output_button.clicked.connect(self.handle_output) self.start_button.clicked.connect(self.handle_start) self.stop_button.clicked.connect(self.handle_stop) # 设置表格上下文菜单 self.process_table.setContextMenuPolicy(Qt.CustomContextMenu) self.process_table.customContextMenuRequested.connect(self.show_table_context_menu) # 只有在相机启用时连接相机信号 if self.camera_enabled and hasattr(self, 'camera_display'): self.camera_display.signal_camera_status.connect(self.handle_camera_status) # 初始化后检查是否有已完成的记录 QTimer.singleShot(1000, self.check_and_process_finished_records) def update_inspection_columns(self): """更新检验列配置 - 使用检验配置管理器获取启用的列数和标题""" try: # 获取已启用的检验配置 enabled_configs = self.inspection_manager.get_enabled_configs() # 获取启用的列数 column_count = len(enabled_configs) if column_count == 0: # 如果没有启用的列,至少显示一列 column_count = 1 headers = ["检验项"] else: # 如果有启用的列,使用配置的标题 headers = [config['display_name'] for config in enabled_configs] # 设置检验列 self.set_inspection_columns(column_count, headers) logging.info(f"已更新检验列配置:{column_count}列, 标题: {headers}") except Exception as e: logging.error(f"更新检验列配置失败: {str(e)}") # 如果更新失败,使用默认配置 self.set_inspection_columns(1, ["检验项"]) def show_main_page(self): self.stacked_widget.setCurrentWidget(self.central_widget) # 更新检验列配置 self.update_inspection_columns() # 加载未完成的检验数据 self.load_finished_inspection_data() # 只有在相机启用时处理相机显示 if self.camera_enabled and hasattr(self, 'camera_display'): # 如果相机已连接,直接开始显示相机画面 if self.camera_display.camera_manager.isOpen: if not self.camera_display.camera_manager.isGrabbing: self.camera_display.start_display() logging.info("显示主页面") def show_settings_page(self): """显示设置页面""" # 延迟创建设置组件 if not hasattr(self, 'settings_widget'): from widgets.settings_widget import SettingsWidget self.settings_widget = SettingsWidget(self) self.stacked_widget.addWidget(self.settings_widget) # 切换到设置页面 self.stacked_widget.setCurrentWidget(self.settings_widget) logging.info("显示设置页面") def handle_input(self): """处理上料按钮点击事件""" # 创建对话框 dialog = QDialog(self) dialog.setWindowTitle("上料操作") dialog.setFixedSize(300, 200) # 对话框布局 layout = QVBoxLayout(dialog) # 添加提示信息 info_label = QLabel("请选择上料托盘类型:") info_label.setFont(self.normal_font) layout.addWidget(info_label) # 添加托盘类型选择 pallet_combo = QComboBox() pallet_combo.setFont(self.normal_font) # 复制当前托盘类型选择器的内容 for i in range(self.input_pallet_type_combo.count()): pallet_combo.addItem(self.input_pallet_type_combo.itemText(i)) layout.addWidget(pallet_combo) # 添加按钮 button_layout = QHBoxLayout() confirm_button = QPushButton("确认") confirm_button.setFont(self.normal_font) confirm_button.setStyleSheet("background-color: #e3f2fd; border: 1px solid #2196f3; padding: 8px 16px; font-weight: bold; border-radius: 4px;") cancel_button = QPushButton("取消") cancel_button.setFont(self.normal_font) cancel_button.setStyleSheet("padding: 8px 16px; font-weight: bold; border-radius: 4px;") button_layout.addStretch() button_layout.addWidget(confirm_button) button_layout.addWidget(cancel_button) layout.addLayout(button_layout) # 连接按钮信号 confirm_button.clicked.connect(dialog.accept) cancel_button.clicked.connect(dialog.reject) # 显示对话框 result = dialog.exec() # 如果用户确认,则执行上料操作 if result == QDialog.Accepted: selected_type = pallet_combo.currentText() # 执行Modbus操作 modbus = ModbusUtils() client = modbus.get_client() try: # 上料 D2 寄存器写入 1 ,D0 寄存器写入托盘类型 if modbus.write_register_until_success(client, 2, 1) and modbus.write_register_until_success(client, 0, selected_type): # 创建状态标签并显示在右上角 self.show_operation_status("上料托盘", "input", selected_type) else: QMessageBox.information(self, "操作提示", "上料失败") except Exception as e: logging.error(f"上料操作失败: {str(e)}") QMessageBox.critical(self, "错误", f"上料操作失败: {str(e)}") finally: modbus.close_client(client) def handle_output(self): """处理下料按钮点击事件""" # 创建对话框 dialog = QDialog(self) dialog.setWindowTitle("下料操作") dialog.setFixedSize(300, 200) # 对话框布局 layout = QVBoxLayout(dialog) # 添加提示信息 info_label = QLabel("请选择下料托盘类型:") info_label.setFont(self.normal_font) layout.addWidget(info_label) # 添加托盘类型选择 pallet_combo = QComboBox() pallet_combo.setFont(self.normal_font) # 复制当前托盘类型选择器的内容 for i in range(self.output_pallet_type_combo.count()): pallet_combo.addItem(self.output_pallet_type_combo.itemText(i)) layout.addWidget(pallet_combo) # 添加按钮 button_layout = QHBoxLayout() confirm_button = QPushButton("确认") confirm_button.setFont(self.normal_font) confirm_button.setStyleSheet("background-color: #fff8e1; border: 1px solid #ffc107; padding: 8px 16px; font-weight: bold; border-radius: 4px;") cancel_button = QPushButton("取消") cancel_button.setFont(self.normal_font) cancel_button.setStyleSheet("padding: 8px 16px; font-weight: bold; border-radius: 4px;") button_layout.addStretch() button_layout.addWidget(confirm_button) button_layout.addWidget(cancel_button) layout.addLayout(button_layout) # 连接按钮信号 confirm_button.clicked.connect(dialog.accept) cancel_button.clicked.connect(dialog.reject) # 显示对话框 result = dialog.exec() # 如果用户确认,则执行下料操作 if result == QDialog.Accepted: selected_type = pallet_combo.currentText() # 获取托盘的排序,该顺序影响着下料寄存器的写入值 切记,需要和 PLC 确认沟通完成后才能修改排序值 pallets_dict = self.pallet_type_manager.get_pallet_type_by_type(selected_type) # 执行Modbus操作 modbus = ModbusUtils() client = modbus.get_client() try: #TODO: 下料 D3 寄存器写入 1 D1 寄存器写入托盘类型 if modbus.write_register_until_success(client, 3, 1) and modbus.write_register_until_success(client, 1, pallets_dict.get(selected_type)): # 创建状态标签并显示在右上角 self.show_operation_status("下料托盘", "output", selected_type) else: QMessageBox.information(self, "操作提示", "下料失败") except Exception as e: logging.error(f"下料操作失败: {str(e)}") QMessageBox.critical(self, "错误", f"下料操作失败: {str(e)}") finally: modbus.close_client(client) def handle_start(self): """处理启动按钮点击事件""" modbus = ModbusUtils() client = modbus.get_client() try: # 启动 D1 寄存器写入 1 if not modbus.write_register_until_success(client, 1, 1): QMessageBox.information(self, "操作提示", "启动失败") except Exception as e: logging.error(f"启动操作失败: {str(e)}") QMessageBox.critical(self, "错误", f"启动操作失败: {str(e)}") finally: modbus.close_client(client) def handle_stop(self): """处理停止按钮点击事件""" modbus = ModbusUtils() client = modbus.get_client() try: # 停止 D1 寄存器写入 0 if not modbus.write_register_until_success(client, 1, 0): QMessageBox.information(self, "操作提示", "停止失败") except Exception as e: logging.error(f"停止操作失败: {str(e)}") QMessageBox.critical(self, "错误", f"停止操作失败: {str(e)}") finally: modbus.close_client(client) def handle_camera_status(self, is_connected, message): """处理相机状态变化""" if is_connected: logging.info("相机已连接并显示") else: logging.warning(f"相机显示问题: {message}") def handle_camera_connection(self, is_connected, message): """处理相机连接状态变化""" if is_connected: logging.info("相机已连接") # 如果当前在主页面,直接开始显示相机画面 if self.stacked_widget.currentWidget() == self.central_widget: self.camera_display.start_display() else: if message: logging.warning(f"相机连接失败: {message}") else: logging.info("相机已断开") # 如果相机断开,确保停止显示 self.camera_display.stop_display() def handle_camera_params_changed(self, exposure_time, gain, frame_rate): """处理相机参数变化""" logging.info(f"相机参数已更新: 曝光={exposure_time:.1f}μs, 增益={gain:.1f}dB, 帧率={frame_rate:.1f}fps") # 这里可以添加对相机参数变化的处理逻辑 def handle_camera_error(self, error_msg): """处理相机错误""" logging.error(f"相机错误: {error_msg}") QMessageBox.warning(self, "相机错误", error_msg) def closeEvent(self, event): """窗口关闭事件""" # 只有在相机启用时处理相机关闭 if self.camera_enabled and hasattr(self, 'camera_display'): # 停止相机显示 self.camera_display.stop_display() # 接受关闭事件 event.accept() def handle_order_enter(self): """处理工程号输入框按下回车事件""" logging.info("工程号输入框按下回车事件") # 获取当前输入的工程号 order_text = self.order_edit.text().strip() if order_text: logging.info(f"输入的工程号: {order_text}") # 在微丝产线表格中添加一条新记录 self.add_new_inspection_row(order_text) else: logging.warning("工程号为空") QMessageBox.warning(self, "输入提示", "请输入有效的工程号") # 处理完后可以清除焦点,让输入框失去焦点 self.central_widget.setFocus() def add_new_inspection_row(self, order_id): """在微丝产线表格中添加一条新记录 Args: order_id: 工程号 """ try: # 获取启用的检验配置 enabled_configs = self.inspection_manager.get_enabled_configs() # 固定的数据起始行索引 data_start_row = 2 # 数据从第3行开始 # 断开单元格变更信号,避免加载过程中触发保存 try: self.process_table.cellChanged.disconnect(self.handle_inspection_cell_changed) except: pass # 在指定行索引插入新行 - 总是插入到第一个数据行 self.process_table.insertRow(data_start_row) # 更新序号 - 所有现有行序号+1 for row in range(data_start_row + 1, self.process_table.rowCount()): seq_item = self.process_table.item(row, 0) if seq_item: try: current_seq = int(seq_item.text()) seq_item.setText(str(current_seq + 1)) except ValueError: pass # 添加工程号到表格的第二列 item = QTableWidgetItem(order_id) item.setTextAlignment(Qt.AlignCenter) self.process_table.setItem(data_start_row, 1, item) # 添加序号到表格的第一列 - 新行始终是第1条 item = QTableWidgetItem("1") item.setTextAlignment(Qt.AlignCenter) self.process_table.setItem(data_start_row, 0, item) # 检验列设置为可编辑状态 for i, config in enumerate(enabled_configs): col_index = 2 + i # 检验列从第3列开始 # 创建空的可编辑单元格 item = QTableWidgetItem("") item.setTextAlignment(Qt.AlignCenter) # 设置单元格属性以标识其关联的检验项 item.setData(Qt.UserRole, config.get('id')) self.process_table.setItem(data_start_row, col_index, item) # 包装列设置为可编辑状态 packaging_start_col = 2 + len(enabled_configs) for i in range(2): # 贴标和称重 col_index = packaging_start_col + i item = QTableWidgetItem("") item.setTextAlignment(Qt.AlignCenter) self.process_table.setItem(data_start_row, col_index, item) # 设置表格为可编辑状态 self.process_table.setEditTriggers(QTableWidget.DoubleClicked | QTableWidget.EditKeyPressed) # 重新连接单元格内容变更信号 self.process_table.cellChanged.connect(self.handle_inspection_cell_changed) # 选中新添加的行 self.process_table.selectRow(data_start_row) # 限制最大行数 self.limit_table_rows(10) # 最多保留10行数据 # 将工程号和托盘号保存到数据库,确保能够正确关联 from dao.inspection_dao import InspectionDAO inspection_dao = InspectionDAO() tray_id = self.tray_edit.currentText() # 为每个检验位置创建一个空记录,确保工程号在数据库中存在 for config in enabled_configs: data = [{ 'position': config.get('position'), 'config_id': config.get('id'), 'value': '', 'status': '', # 默认设置为通过状态 'remark': '', 'tray_id': tray_id }] inspection_dao.save_inspection_data(order_id, data) # 为贴标和称重也创建空记录 for position in [11, 12]: # 11是贴标,12是称重 data = [{ 'position': position, 'config_id': position, 'value': '', 'status': 'pass', # 默认设置为通过状态 'remark': '', 'tray_id': tray_id }] inspection_dao.save_inspection_data(order_id, data) logging.info(f"已添加工程号 {order_id} 的新记录,显示在第1条") except Exception as e: logging.error(f"添加新记录失败: {str(e)}") QMessageBox.warning(self, "添加失败", f"添加新记录失败: {str(e)}") finally: # 重新加载数据,确保UI显示正确 self.load_finished_inspection_data() def limit_table_rows(self, max_rows): """限制表格最大行数 Args: max_rows: 最大行数(不包括表头行) """ try: # 计算数据总行数 data_rows = self.process_table.rowCount() - 2 # 减去表头行 # 如果超过最大行数,删除多余的行 if data_rows > max_rows: # 要删除的行数 rows_to_remove = data_rows - max_rows # 从最后一行开始删除 for i in range(rows_to_remove): self.process_table.removeRow(self.process_table.rowCount() - 1) logging.info(f"已限制表格最大行数为 {max_rows} 行数据,删除了 {rows_to_remove} 行") except Exception as e: logging.error(f"限制表格行数失败: {str(e)}") def handle_inspection_cell_changed(self, row, column): """处理检验单元格内容变更 Args: row: 行索引 column: 列索引 """ try: # 只处理数据行的检验列变更 if row < 2: # 忽略表头行 return # 忽略首尾两列(序号和工程号) if column < 2: return # 获取工程号 order_id_item = self.process_table.item(row, 1) if not order_id_item: return order_id = order_id_item.text().strip() if not order_id: return # 获取托盘号 tray_id = self.tray_edit.currentText() # 获取启用的检验配置 enabled_configs = self.inspection_manager.get_enabled_configs() # 判断是否是检验列(非包装列) packaging_start_col = 2 + len(enabled_configs) # 获取单元格内容 cell_item = self.process_table.item(row, column) if not cell_item: return value = cell_item.text().strip() # 默认设置为通过状态 status = 'pass' # 记录当前正在处理的数据类型,用于日志输出 data_type = "检验" if column >= 2 and column < packaging_start_col: # 是检验列 config_index = column - 2 if config_index < len(enabled_configs): config = enabled_configs[config_index] data_type = config['display_name'] # 显示临时状态消息 self.statusBar().showMessage(f"正在保存检验数据: {data_type}={value}", 1000) # 验证数据有效性 if self.validate_inspection_value(config, value): # 设置单元格颜色为通过 cell_item.setBackground(QBrush(QColor("#c8e6c9"))) # 浅绿色 status = 'pass' else: # 设置单元格颜色为警告 cell_item.setBackground(QBrush(QColor("#fff9c4"))) # 浅黄色 status = 'warning' # 保存到数据库 self.save_inspection_data(order_id, tray_id, config['position'], config['id'], value, status) # 判断是否是包装列 elif column == packaging_start_col: # 贴标列 data_type = "贴标" self.statusBar().showMessage(f"正在保存贴标数据: {value}", 1000) # 设置单元格颜色为通过 cell_item.setBackground(QBrush(QColor("#c8e6c9"))) # 浅绿色 # 保存贴标数据,position和config_id都是11 self.save_inspection_data(order_id, tray_id, 11, 11, value, status) elif column == packaging_start_col + 1: # 称重列 data_type = "称重" self.statusBar().showMessage(f"正在保存称重数据: {value}", 1000) # 设置单元格颜色为通过 cell_item.setBackground(QBrush(QColor("#c8e6c9"))) # 浅绿色 # 保存称重数据,position和config_id都是12 self.save_inspection_data(order_id, tray_id, 12, 12, value, status) # 记录详细日志 logging.info(f"处理单元格变更: 行={row}, 列={column}, 类型={data_type}, 工程号={order_id}, 值={value}, 状态={status}") except Exception as e: logging.error(f"处理检验单元格变更失败: {str(e)}") self.statusBar().showMessage(f"处理检验数据失败: {str(e)[:50]}...", 3000) finally: # 延迟一段时间后再触发查询,避免频繁刷新UI QTimer.singleShot(1000, self.load_finished_inspection_data) def validate_inspection_value(self, config, value): """验证检验值是否有效 Args: config: 检验配置 value: 检验值 Returns: bool: 是否有效 """ try: # 特殊处理贴标和称重数据 - 这些数据默认都是有效的 if config.get('position') in [11, 12]: # 11是贴标,12是称重 return True # 检查值是否为空 if not value and config.get('required', False): return False # 根据数据类型验证 data_type = config.get('data_type') if data_type == 'number': # 数值类型验证 try: # 如果值为空且不是必填,则视为有效 if not value and not config.get('required', False): return True num_value = float(value) min_value = config.get('min_value') max_value = config.get('max_value') if min_value is not None and num_value < min_value: return False if max_value is not None and num_value > max_value: return False return True except ValueError: return False elif data_type == 'enum': # 枚举类型验证 enum_values = config.get('enum_values') if enum_values and isinstance(enum_values, list): # 如果值为空且不是必填,则视为有效 if not value and not config.get('required', False): return True return value in enum_values return False # 文本类型不做特殊验证 return True except Exception as e: logging.error(f"验证检验值失败: {str(e)}") return False def save_inspection_data(self, order_id, tray_id, position, config_id, value, status): """保存检验数据到数据库 Args: order_id: 工程号 position: 位置序号 config_id: 配置ID value: 检验值 status: 状态 """ try: from dao.inspection_dao import InspectionDAO inspection_dao = InspectionDAO() # 记录保存前的详细日志 logging.info(f"正在保存检验数据: 工程号={order_id}, 托盘号={tray_id}, 位置={position}, 配置ID={config_id}, 值={value}, 状态={status}") # 构建数据 data = [{ 'position': position, 'config_id': config_id, 'value': value, 'status': status, 'remark': '', 'tray_id': tray_id }] # 保存到数据库 result = inspection_dao.save_inspection_data(order_id, data) if result: logging.info(f"已成功保存工程号 {order_id} 的检验数据,位置: {position}, 值: {value}") # 显示临时状态消息 self.statusBar().showMessage(f"已保存检验数据:{value}", 3000) # 如果是贴标字段且有值,直接写入包装记录 if position == 11 and value: # 直接调用加载到包装记录的方法 self.load_finished_record_to_package_record(order_id, tray_id) logging.info(f"检测到贴标字段有值,已自动写入包装记录") else: # 使用延迟调用,避免频繁刷新UI QTimer.singleShot(500, self.check_and_process_finished_records) else: logging.warning(f"保存工程号 {order_id} 的检验数据失败") # 显示错误消息 self.statusBar().showMessage(f"保存检验数据失败", 3000) except Exception as e: logging.error(f"保存检验数据失败: {str(e)}") # 显示错误消息 self.statusBar().showMessage(f"保存检验数据错误: {str(e)[:50]}...", 3000) def load_finished_inspection_data(self): """加载未完成的检验数据并显示在表格中""" try: # 使用InspectionDAO获取未完成的检验数据 from dao.inspection_dao import InspectionDAO inspection_dao = InspectionDAO() # 获取托盘号 tray_id = self.tray_edit.currentText() # 使用get_inspection_data_unfinished获取未完成的数据 unfinished_data = inspection_dao.get_inspection_data_unfinished(tray_id) if not unfinished_data: logging.info("没有未完成的检验数据") # 清空表格现有数据行,但保留表头 while self.process_table.rowCount() > 2: self.process_table.removeRow(2) return logging.info(f"已加载未完成的检验数据,共 {len(unfinished_data)} 条记录") # 获取启用的检验配置 enabled_configs = self.inspection_manager.get_enabled_configs() # 按工程号分组 orders_data = {} for data in unfinished_data: order_id = data['order_id'] if order_id not in orders_data: orders_data[order_id] = [] orders_data[order_id].append(data) # 断开单元格变更信号,避免加载过程中触发保存 try: self.process_table.cellChanged.disconnect(self.handle_inspection_cell_changed) except: pass # 清空表格现有数据行,但保留表头 while self.process_table.rowCount() > 2: self.process_table.removeRow(2) # 添加数据到表格 - 从第3行开始添加数据 row_idx = 2 # 确保按工程号倒序排列,最新的工程号在最前面 sorted_order_ids = sorted(orders_data.keys(), reverse=True) for order_id in sorted_order_ids: items = orders_data[order_id] # 添加新行 self.process_table.insertRow(row_idx) # 添加序号到第一列 seq_item = QTableWidgetItem(str(row_idx - 1)) seq_item.setTextAlignment(Qt.AlignCenter) self.process_table.setItem(row_idx, 0, seq_item) # 添加工程号到第二列 order_item = QTableWidgetItem(order_id) order_item.setTextAlignment(Qt.AlignCenter) self.process_table.setItem(row_idx, 1, order_item) # 添加检验数据 for item in items: position = item['position'] value = item['value'] if item['value'] else "" status = item['status'] config_id = item['config_id'] # 找到对应的列索引 col_index = None for i, config in enumerate(enabled_configs): if config.get('position') == position: col_index = 2 + i # 检验列从第3列开始 break if col_index is not None: # 创建单元格并设置值 cell_item = QTableWidgetItem(str(value)) cell_item.setTextAlignment(Qt.AlignCenter) # 存储配置ID,用于保存时确定是哪个检验项 cell_item.setData(Qt.UserRole, config_id) # 根据状态设置单元格颜色 if status == 'fail': cell_item.setBackground(QBrush(QColor("#ffcdd2"))) # 浅红色 elif status == 'warning': cell_item.setBackground(QBrush(QColor("#fff9c4"))) # 浅黄色 elif status == 'pass': cell_item.setBackground(QBrush(QColor("#c8e6c9"))) # 浅绿色 # 设置单元格 self.process_table.setItem(row_idx, col_index, cell_item) # 添加贴标(11)和称重数据(12) if position == 11: # 贴标 # 贴标列索引 = 2(序号和工程号) + 检验列数 label_col = 2 + len(enabled_configs) self.process_table.setItem(row_idx, label_col, QTableWidgetItem(str(value))) elif position == 12: # 称重 # 称重列索引 = 2(序号和工程号) + 检验列数 + 1(贴标) weight_col = 2 + len(enabled_configs) + 1 self.process_table.setItem(row_idx, weight_col, QTableWidgetItem(str(value))) row_idx += 1 # 设置表格为可编辑状态 self.process_table.setEditTriggers(QTableWidget.DoubleClicked | QTableWidget.EditKeyPressed) # 重新连接单元格变更信号 self.process_table.cellChanged.connect(self.handle_inspection_cell_changed) except Exception as e: logging.error(f"加载未完成的检验数据失败: {str(e)}") QMessageBox.warning(self, "加载失败", f"加载未完成的检验数据失败: {str(e)}") finally: # 加载包装记录 self.show_pack_item() def load_finished_record_to_package_record(self, order_id, tray_id): """加载已完成检验数据到包装记录 Args: order_id: 工程号 tray_id: 托盘号 """ try: from dao.inspection_dao import InspectionDAO inspection_dao = InspectionDAO() # 获取该工程号的所有检验数据 inspection_data = inspection_dao.get_inspection_data_by_order(order_id, tray_id) if not inspection_data: logging.warning(f"未找到工程号 {order_id} 托盘号 {tray_id} 的检验数据") return # 从检验数据中获取贴标和称重数据 label_value = "" weight_value = "" for item in inspection_data: if item['position'] == 11: # 贴标 label_value = item['value'] elif item['position'] == 12: # 称重 weight_value = item['value'] # 只要贴标字段有值,就可以写入包装记录 if not label_value: logging.warning(f"工程号 {order_id} 托盘号 {tray_id} 的贴标字段为空,不添加到包装记录") return # 获取当前包装记录,检查是否已经存在相同的记录 existing_records = inspection_dao.get_package_record(tray_id) for record in existing_records: if record[0] == order_id and record[4] == label_value: logging.info(f"工程号 {order_id} 托盘号 {tray_id} 贴标值 {label_value} 的包装记录已存在,不重复添加") return # 获取当前时间作为完成时间 finish_time = datetime.now() # 将数据写入到数据库表 inspection_pack_data inspection_dao.save_package_record(order_id, tray_id, label_value, weight_value, finish_time) # 回显数据 self.show_pack_item() logging.info(f"已将工程号 {order_id} 托盘号 {tray_id} 的检验数据添加到包装记录并回显") except Exception as e: logging.error(f"加载已完成检验数据到包装记录失败: {str(e)}") QMessageBox.warning(self, "加载失败", f"加载已完成检验数据到包装记录失败: {str(e)}") def show_pack_item(self): from dao.inspection_dao import InspectionDAO inspection_dao = InspectionDAO() # 获取托盘号 tray_id = self.tray_edit.currentText() # 读取已包装的记录信息,然后回显到 UI package_record = inspection_dao.get_package_record(tray_id) # 清空包装记录表格,只保留表头 while self.record_table.rowCount() > 1: self.record_table.removeRow(1) # 断开包装记录表的信号连接(如果有) try: self.record_table.cellChanged.disconnect() except: pass # 添加所有包装记录到表格 for index, item in enumerate(package_record): # 在包装记录表中添加新行 row_index = index + 1 # 从第2行开始(索引为1) self.record_table.insertRow(row_index) # 设置包装记录数据 # 序号 - 第1列 seq_item = QTableWidgetItem(str(row_index)) seq_item.setTextAlignment(Qt.AlignCenter) self.record_table.setItem(row_index, 0, seq_item) # 工程号 - 第2列 order_item = QTableWidgetItem(item[0]) order_item.setTextAlignment(Qt.AlignCenter) self.record_table.setItem(row_index, 1, order_item) # 材质 - 第3列 material_item = QTableWidgetItem(item[1]) material_item.setTextAlignment(Qt.AlignCenter) self.record_table.setItem(row_index, 2, material_item) # 规格 - 第4列 spec_item = QTableWidgetItem(item[2]) spec_item.setTextAlignment(Qt.AlignCenter) self.record_table.setItem(row_index, 3, spec_item) # 托盘号 - 第5列 tray_item = QTableWidgetItem(item[3]) tray_item.setTextAlignment(Qt.AlignCenter) self.record_table.setItem(row_index, 4, tray_item) # 轴包装号(贴标)- 第6列 label_item = QTableWidgetItem(item[4]) label_item.setTextAlignment(Qt.AlignCenter) self.record_table.setItem(row_index, 5, label_item) # 重量 - 第7列 weight_item = QTableWidgetItem(str(item[5])) weight_item.setTextAlignment(Qt.AlignCenter) self.record_table.setItem(row_index, 6, weight_item) # 包装时间 pack_time = QTableWidgetItem(str(item[6])) weight_item.setTextAlignment(Qt.AlignCenter) self.record_table.setItem(row_index, 7, pack_time) # 更新包装记录统计数据 self.update_package_statistics() def update_package_statistics(self): """更新包装记录统计数据""" try: # 获取包装记录表的行数(不包括表头) package_count = self.record_table.rowCount() - 1 # 更新任务表格中的已完成数量 completed_item = QTableWidgetItem(str(package_count)) completed_item.setTextAlignment(Qt.AlignCenter) self.task_table.setItem(2, 2, completed_item) # 计算已完成公斤数(如果称重列有数值) completed_kg = 0 for row in range(1, self.record_table.rowCount()): weight_item = self.record_table.item(row, 6) # 称重列 if weight_item and weight_item.text(): try: completed_kg += float(weight_item.text()) except ValueError: pass # 更新任务表格中的已完成公斤 completed_kg_item = QTableWidgetItem(str(completed_kg)) completed_kg_item.setTextAlignment(Qt.AlignCenter) self.task_table.setItem(2, 3, completed_kg_item) logging.info(f"已更新包装记录统计数据: 完成数量={package_count}, 完成公斤={completed_kg}") except Exception as e: logging.error(f"更新包装记录统计数据失败: {str(e)}") def check_and_process_finished_records(self): """检查并处理所有已完成的检验记录""" try: # 获取当前托盘号 tray_id = self.tray_edit.currentText() # 使用InspectionDAO获取未完成的检验数据 from dao.inspection_dao import InspectionDAO inspection_dao = InspectionDAO() # 获取所有检验数据 all_data = inspection_dao.get_inspection_data_unfinished(tray_id) # 按工程号分组 orders_data = {} for data in all_data: order_id = data['order_id'] if order_id not in orders_data: orders_data[order_id] = [] orders_data[order_id].append(data) # 检查每个工程号是否有贴标数据 for order_id, items in orders_data.items(): # 查找贴标数据 has_label = False label_value = "" for item in items: if item['position'] == 11: # 贴标 label_value = item['value'] if label_value: has_label = True break if has_label: # 将已有贴标的记录添加到包装记录 self.load_finished_record_to_package_record(order_id, tray_id) logging.info(f"已将工程号 {order_id} 的贴标记录添加到包装记录") # 重新加载检验数据 self.load_finished_inspection_data() except Exception as e: logging.error(f"检查并处理已完成的检验记录失败: {str(e)}") QMessageBox.warning(self, "处理失败", f"检查并处理已完成的检验记录失败: {str(e)}") def show_table_context_menu(self, pos): """显示表格上下文菜单 Args: pos: 鼠标位置 """ try: # 获取当前单元格 cell_index = self.process_table.indexAt(pos) if not cell_index.isValid(): return row = cell_index.row() column = cell_index.column() # 只对数据行和检验列显示上下文菜单 if row < 2: # 忽略表头行 return # 获取工程号 order_id_item = self.process_table.item(row, 1) if not order_id_item: return order_id = order_id_item.text().strip() if not order_id: return # 获取托盘号 tray_id = self.tray_edit.currentText() # 创建上下文菜单 menu = QMenu(self) # 获取启用的检验配置 enabled_configs = self.inspection_manager.get_enabled_configs() # 判断是否是检验列(非包装列) packaging_start_col = 2 + len(enabled_configs) if column >= 2 and column < packaging_start_col: # 是检验列 config_index = column - 2 if config_index < len(enabled_configs): config = enabled_configs[config_index] position = config.get('position') # 添加查询数据库菜单项 check_action = menu.addAction("检查数据库记录") check_action.triggered.connect(lambda: self.check_database_record(order_id, position, tray_id)) # 显示菜单 menu.exec_(self.process_table.viewport().mapToGlobal(pos)) except Exception as e: logging.error(f"显示表格上下文菜单失败: {str(e)}") def check_database_record(self, order_id, position, tray_id): """检查数据库记录 Args: order_id: 工程号 position: 位置序号 tray_id: 托盘号 """ try: from dao.inspection_dao import InspectionDAO inspection_dao = InspectionDAO() # 获取检验数据 inspection_data = inspection_dao.get_inspection_data_by_order(order_id, tray_id) # 查找对应位置的数据 matching_data = None for data in inspection_data: if data.get('position') == position: matching_data = data break # 显示结果 if matching_data: value = matching_data.get('value') status = matching_data.get('status') message = f"数据库记录:\n\n" message += f"工程号: {order_id}\n" message += f"位置: {position}\n" message += f"值: {value}\n" message += f"状态: {status}\n" QMessageBox.information(self, "数据库记录", message) else: QMessageBox.warning(self, "数据库记录", f"未找到工程号 {order_id} 位置 {position} 的数据") except Exception as e: logging.error(f"检查数据库记录失败: {str(e)}") QMessageBox.warning(self, "查询失败", f"检查数据库记录失败: {str(e)}") def show_operation_status(self, status, operation_type, pallet_type): """在右上角显示操作状态 Args: status: 状态文本 operation_type: 操作类型 (input/output) pallet_type: 托盘类型 """ # 确定要添加标签的容器 if operation_type == "input": container = self.material_content else: container = self.output_content # 如果已存在状态标签,则移除它 status_label_name = f"{operation_type}_status_label" if hasattr(self, status_label_name): old_label = getattr(self, status_label_name) old_label.deleteLater() # 创建新的状态标签 status_label = QLabel(f"{status}: {pallet_type}", container) status_label.setFont(self.normal_font) status_label.setStyleSheet("color: red; background-color: transparent;") status_label.setAlignment(Qt.AlignRight | Qt.AlignTop) # 使用绝对定位,放置在右上角 status_label.setGeometry(container.width() - 200, 5, 190, 30) # 确保标签始终保持在顶层显示 status_label.raise_() status_label.show() # 保存标签引用 setattr(self, status_label_name, status_label) # 保存原始的resize事件处理函数 if not hasattr(container, "_original_resize_event"): container._original_resize_event = container.resizeEvent # 添加窗口大小变化事件处理,确保标签位置随窗口调整 container.resizeEvent = lambda event: self.adjust_status_label_position(event, container, status_label) def adjust_status_label_position(self, event, container, label): """调整状态标签位置,确保始终在右上角 Args: event: 窗口大小变化事件 container: 标签所在的容器 label: 状态标签 """ # 更新标签位置,保持在右上角 label.setGeometry(container.width() - 200, 5, 190, 30) # 调用原始的resizeEvent(如果有的话) original_resize = getattr(container, "_original_resize_event", None) if original_resize: original_resize(event)