#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ DUT(试验样品)选择对话框 用于试验监控界面选择样品分配到指定机台和通道 """ from PyQt5.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QTableWidget, QTableWidgetItem, QHeaderView, QCheckBox, QMessageBox, QWidget ) from PyQt5.QtCore import Qt from models.dut_model import DUTModel import requests import json # 尝试导入配置 try: from config import USE_HTTP_API, HTTP_API_BASE_URL except ImportError: USE_HTTP_API = False HTTP_API_BASE_URL = "http://127.0.0.1:5050" class DUTSelectionDialog(QDialog): """DUT 选择对话框""" def __init__(self, machine_sn, station_num, machine_label="", user_id="", max_dut_count=1, parent=None): super().__init__(parent) self.machine_sn = machine_sn self.station_num = station_num self.machine_label = machine_label self.user_id = user_id # 安排试验的用户ID self.max_dut_count = max_dut_count # 最多可选择的样品数量(由connectState决定) # HTTP API 配置 self.use_http_api = USE_HTTP_API self.api_base_url = HTTP_API_BASE_URL self.dut_direction_mode = 2 # 跌落方向模式(固定为2) self.dut_model = DUTModel() self.selected_duts = [] # 选中的样品列表(支持多选) self.setWindowTitle(f"选择试验样品-{machine_sn}:通道{station_num}") self.setModal(True) self.resize(900, 500) # 调整为更合理的宽度 self._build_ui() self._load_duts() def _build_ui(self): """构建界面""" layout = QVBoxLayout(self) layout.setContentsMargins(20, 20, 20, 20) layout.setSpacing(15) # 标题栏 title_layout = QHBoxLayout() title = QLabel(f"选择试验样品-{self.machine_sn}:通道{self.station_num} (最多选择{self.max_dut_count}个)") title.setObjectName("dialog-title") title.setStyleSheet("font-size: 16px; font-weight: bold; color: #333;") title_layout.addWidget(title) title_layout.addStretch() # 复选框:包含未分配机台的样品 self.include_unassigned_checkbox = QCheckBox("包含未分配机台样品") self.include_unassigned_checkbox.setChecked(False) self.include_unassigned_checkbox.stateChanged.connect(self._on_filter_changed) title_layout.addWidget(self.include_unassigned_checkbox) # 复选框:包含已完成的样品 self.include_finished_checkbox = QCheckBox("包含已完成样品") self.include_finished_checkbox.setChecked(False) self.include_finished_checkbox.stateChanged.connect(self._on_filter_changed) title_layout.addWidget(self.include_finished_checkbox) layout.addLayout(title_layout) # 样品列表表格 self.table = QTableWidget() self.table.setObjectName("dut-selection-table") self.table.setColumnCount(7) self.table.setHorizontalHeaderLabels([ "选择", "产品序列号", "项目代码", "项目阶段", "测试方案", "测试周次","状态" ]) # 表格样式 - 禁用行选中高亮,避免与复选框冲突 self.table.setSelectionMode(QTableWidget.NoSelection) self.table.setFocusPolicy(Qt.NoFocus) self.table.setAlternatingRowColors(True) self.table.verticalHeader().setVisible(False) self.table.setStyleSheet( "QTableWidget { gridline-color: #e0e0e0; }" "QTableWidget::item { padding: 5px; }" "QCheckBox::indicator { width: 27px; height: 27px; }" "QCheckBox::indicator:unchecked { border: 2px solid #999; background: white; border-radius: 4px; }" "QCheckBox::indicator:checked { border: 2px solid #3A84FF; background: #3A84FF; border-radius: 4px; }" "QCheckBox::indicator:checked::after { content: ''; width: 9px; height: 15px; }" ) # 列宽设置 - 优化宽度分配 header = self.table.horizontalHeader() header.setSectionResizeMode(0, QHeaderView.Fixed) # 选择列 header.setSectionResizeMode(1, QHeaderView.Interactive) # 产品序列号 header.setSectionResizeMode(2, QHeaderView.ResizeToContents) # 项目代码 header.setSectionResizeMode(3, QHeaderView.ResizeToContents) # 项目阶段 header.setSectionResizeMode(4, QHeaderView.ResizeToContents) # 测试方案 header.setSectionResizeMode(5, QHeaderView.ResizeToContents) # 测试周次 header.setSectionResizeMode(6, QHeaderView.ResizeToContents) # 状态 self.table.setColumnWidth(0, 50) # 选择列 self.table.setColumnWidth(1, 180) # 产品序列号固定宽度 # 表格行高 self.table.verticalHeader().setDefaultSectionSize(35) # 移除单击行选中功能(因为禁用了行选中) # self.table.cellClicked.connect(self._on_cell_clicked) layout.addWidget(self.table) # 底部按钮 btn_layout = QHBoxLayout() btn_layout.addStretch() self.btn_confirm = QPushButton("选择") self.btn_confirm.setObjectName("primary-btn") self.btn_confirm.setMinimumSize(100, 35) self.btn_confirm.clicked.connect(self.accept) btn_layout.addWidget(self.btn_confirm) self.btn_cancel = QPushButton("取消") self.btn_cancel.setMinimumSize(100, 35) self.btn_cancel.clicked.connect(self.reject) btn_layout.addWidget(self.btn_cancel) layout.addLayout(btn_layout) def _load_duts(self): """加载 DUT 列表""" try: # 获取所有 DUT all_duts = self.dut_model.getAllDUTs() # 过滤逻辑 include_unassigned = self.include_unassigned_checkbox.isChecked() include_finished = self.include_finished_checkbox.isChecked() # 统一通道号格式(确保两位数字格式,如 "01") station_num_normalized = str(self.station_num).zfill(2) if self.station_num else "" filtered_duts = [] print(f"{self.machine_sn} {self.station_num} DUTs:", len(all_duts)) for dut in all_duts: # 安全获取 dtMachine 和 station,处理 None、空字符串等情况 dtm = (dut.get("dtMachine") or "").strip() station = (dut.get("station") or "").strip() # print(f"stationAssigned:{dut.get('stationAssigned')} {dtm} {station}") # 额外检查:如果 dtMachine 或 station 为空,也检查 stationAssigned 字段 if not dtm and not station: station_assigned = dut.get("stationAssigned") # 判断 stationAssigned 是否为有效值(排除 None、""、"{}"、空JSON等) if station_assigned: station_assigned = str(station_assigned).strip() # 排除空字符串、"{}"、"null" 等无效值 if station_assigned and station_assigned not in ["","{}", "null", "NULL"]: # 尝试解析 JSON try: import json obj = json.loads(station_assigned) dtm = (obj.get("dtMachine") or "").strip() station = (obj.get("station") or "").strip() except Exception: pass # 统一通道号格式 station_normalized = station.zfill(2) if station else "" # 匹配当前机台+通道 is_assigned_here = ( dtm == self.machine_sn and station_normalized == station_num_normalized ) # 未分配机台(dtMachine 和 station 都为空,或 stationAssigned 无有效值) is_unassigned = (not dtm and not station) # 检查样品状态是否为已完成 dut_status = (dut.get("status") or "").strip() is_not_finished = dut_status not in ['完成', '已完成', 'finished', 'Finished', 'FINISHED'] # 判断是否显示 if (include_unassigned or is_assigned_here) and (include_finished or is_not_finished): # 分配给当前机台+通道的样品,始终显示 filtered_duts.append(dut) elif not is_unassigned and not is_assigned_here: # 分配给其他机台的样品,不显示 pass self._fill_table(filtered_duts) except Exception as e: import traceback traceback.print_exc() QMessageBox.warning(self, "加载失败", f"加载样品列表失败:{e}") def _fill_table(self, duts): """填充表格数据""" self.table.setRowCount(0) self.table.setRowCount(len(duts)) for row, dut in enumerate(duts): # 选择列(复选框,单选逻辑) select_checkbox = QCheckBox() select_checkbox.setProperty("row", row) select_checkbox.setProperty("dut", dut) select_checkbox.toggled.connect(self._on_checkbox_toggled) checkbox_widget = QWidget() checkbox_layout = QHBoxLayout(checkbox_widget) checkbox_layout.addWidget(select_checkbox) checkbox_layout.setAlignment(Qt.AlignCenter) checkbox_layout.setContentsMargins(0, 0, 0, 0) self.table.setCellWidget(row, 0, checkbox_widget) # 产品序列号 sn_item = QTableWidgetItem(str(dut.get("SN", ""))) sn_item.setFlags(sn_item.flags() & ~Qt.ItemIsEditable) self.table.setItem(row, 1, sn_item) # 项目代码 project_item = QTableWidgetItem(str(dut.get("project", ""))) project_item.setFlags(project_item.flags() & ~Qt.ItemIsEditable) self.table.setItem(row, 2, project_item) # 项目阶段 phase_item = QTableWidgetItem(str(dut.get("phase", ""))) phase_item.setFlags(phase_item.flags() & ~Qt.ItemIsEditable) self.table.setItem(row, 3, phase_item) # 测试方案 test_req_item = QTableWidgetItem(str(dut.get("testReq", ""))) test_req_item.setFlags(test_req_item.flags() & ~Qt.ItemIsEditable) self.table.setItem(row, 4, test_req_item) # 测试周次 weeks_item = QTableWidgetItem(str(dut.get("weeks", ""))) weeks_item.setFlags(weeks_item.flags() & ~Qt.ItemIsEditable) self.table.setItem(row, 5, weeks_item) # 状态 status_item = QTableWidgetItem(str(dut.get("status", ""))) status_item.setFlags(status_item.flags() & ~Qt.ItemIsEditable) self.table.setItem(row, 6, status_item) def _on_filter_changed(self): """过滤条件变化,重新加载列表""" # 清空当前选中的样品 self.selected_duts = [] # 重新加载 self._load_duts() def _on_checkbox_toggled(self, checked): """复选框切换(多选逻辑,限制最大选择数量)""" sender = self.sender() sender_dut = sender.property("dut") if checked: # 勾选:检查是否超过最大数量 if sender_dut not in self.selected_duts: if len(self.selected_duts) >= self.max_dut_count: # 超过限制,取消勾选 sender.setChecked(False) QMessageBox.warning( self, "超过限制", f"最多只能选择 {self.max_dut_count} 个样品" ) return self.selected_duts.append(sender_dut) else: # 取消勾选:从列表中移除 if sender_dut in self.selected_duts: self.selected_duts.remove(sender_dut) def get_selected_duts(self): """获取选中的 DUT 列表""" return self.selected_duts def accept(self): """确认选择""" if not self.selected_duts: QMessageBox.warning(self, "未选择", "请先选择至少一个试验样品") return # 调用后台 API 将样品分配到机台+通道 try: result = self._attach_dut_to_station() if result.get("status") == "success": dut_sns = ", ".join([dut.get('SN', '') for dut in self.selected_duts]) QMessageBox.information( self, "选择成功", f"样品 {dut_sns} 已分配到 {self.machine_sn}:通道{self.station_num}" ) super().accept() else: error_msg = result.get("message", "未知错误") QMessageBox.warning( self, "分配失败", f"样品分配失败:{error_msg}" ) except Exception as e: import traceback traceback.print_exc() QMessageBox.critical( self, "错误", f"调用 API 失败:{str(e)}" ) def _attach_dut_to_station(self): """将选中的 DUT 分配到机台通道 Returns: dict: API 响应结果 """ # 根据 dutDirectionMode 决定 action if self.dut_direction_mode == 2: action = "select_and_transfer" # 选择并传送参数到 PLC else: action = "select" # 仅选择 # 构造请求参数:将多个选中的DUT的SN组成数组 dut_sns = [dut.get("SN") for dut in self.selected_duts] control_reqs = { "attachDut": { "action": action, "machine": self.machine_sn, "station": self.station_num, "dut": dut_sns, # 使用数组传递多个DUT的SN "updateTableFlag": True, # 更新数据表 "userid": "123" # 用户ID } } print(f"\n[调试] 分配 DUT 到机台:") print(f" 机台: {self.machine_sn}") print(f" 通道: {self.station_num}") print(f" 样品: {dut_sns}") print(f" Action: {action}") print(f" 请求参数: {json.dumps(control_reqs, ensure_ascii=False, indent=2)}") # 调用 HTTP API try: url = f"{self.api_base_url}/machine_control" response = requests.post( url, json=control_reqs, timeout=10 ) response.raise_for_status() result = response.json() print(f" API 响应: {json.dumps(result, ensure_ascii=False, indent=2)}") return result except requests.exceptions.RequestException as e: print(f" HTTP 请求失败: {e}") return { "status": "error", "message": f"HTTP 请求失败: {str(e)}" } except Exception as e: print(f" 未知错误: {e}") import traceback traceback.print_exc() return { "status": "error", "message": f"未知错误: {str(e)}" }