Files
dtm-py-all/UI/views/dut_selection_dialog.py

388 lines
16 KiB
Python
Raw Permalink 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 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)}"
}