388 lines
16 KiB
Python
388 lines
16 KiB
Python
#!/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)}"
|
||
}
|