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

898 lines
41 KiB
Python
Raw 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 -*-
"""
试验监控视图,实时监控试验设备和工位状态(响应式架构)
"""
from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QTabWidget, QGridLayout, QGroupBox,
QFrame, QScrollArea, QMessageBox, QSizePolicy, QDialog,
QTableWidget, QTableWidgetItem, QHeaderView, QStackedWidget)
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QFont, QColor
from viewmodels.test_monitor_viewmodel import TestMonitorViewModel
from ui_utils.websocket_client import WebSocketClient
from views.dut_selection_dialog import DUTSelectionDialog
from views.dut_form_dialog import DUTFormDialog
from models.dut_model import DUTModel
from models.system_model import SystemModel
import requests
import json
class TestMonitorView(QWidget):
"""试验监控主视图 - 响应式架构"""
def __init__(self):
super().__init__()
# 创建 ViewModel
self.viewModel = TestMonitorViewModel()
# 创建 Model用于新建样品
self.dut_model = DUTModel()
self.system_model = SystemModel()
# HTTP API 配置
try:
from config import USE_HTTP_API, HTTP_API_BASE_URL
self.use_http_api = USE_HTTP_API
self.api_base_url = HTTP_API_BASE_URL
except ImportError:
self.use_http_api = False
self.api_base_url = "http://127.0.0.1:5050"
# 创建 WebSocket 客户端
self.ws_client = WebSocketClient(url="ws://localhost:5080")
# 记录各测试类型下当前选中的机台标签文本,刷新时保持不变
self._selected_machine_per_type = {}
self._machine_channel_containers = []
# 存储站点控件引用,用于响应式更新
self._station_widgets = {} # {(machine_sn, station_sn): widget_components}
# 初始化界面
self.init_ui()
# 记录结构签名,避免不必要的重建
self._last_structure_signature = None
# 绑定响应式信号
self._bind_signals()
# 启动 WebSocket 连接
self.ws_client.start()
def init_ui(self):
"""初始化界面"""
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
# 标签页容器按测试类型Phone/Pad/Mac分组 + 右侧状态
self.tab_widget = QTabWidget()
self.tab_widget.setObjectName("monitor-tabs")
self.tab_widget.setTabsClosable(False)
self.tab_widget.setMovable(True)
# 标签样式:选中为蓝底白字,未选中黑字
self.tab_widget.setStyleSheet(
"QTabBar::tab{min-width:100px; min-height:32px; padding:6px 12px;}"
"QTabBar::tab:selected{background:#3A84FF; color:white; border-radius:4px;}"
"QTabBar::tab:!selected{background:transparent; color:black; border:1px solid #dcdcdc; border-radius:4px;}"
"QTabWidget::pane{border:0;}"
)
# 右侧状态区域(机台数量紧挨连接状态)
corner = QWidget()
corner_layout = QHBoxLayout(corner)
corner_layout.setContentsMargins(0, 0, 10, 0)
corner_layout.setSpacing(10)
self.machine_count_label = QLabel("机台数量: 0")
self.connection_label = QLabel("● 未连接")
self.connection_label.setObjectName("connection-status")
self.connection_label.setStyleSheet("color: gray; font-weight: bold;")
corner_layout.addWidget(self.machine_count_label)
corner_layout.addWidget(self.connection_label)
# 放置到 Tab 右上角
self.tab_widget.setCornerWidget(corner, Qt.TopRightCorner|Qt.AlignVCenter)
layout.addWidget(self.tab_widget)
def _bind_signals(self):
"""绑定响应式信号"""
# ViewModel 数据变化 -> 刷新界面
self.viewModel.machinesChanged.connect(self._refresh_machines)
self.viewModel.stationsChanged.connect(self._refresh_stations)
self.viewModel.connectionStatusChanged.connect(self._refresh_connection_status)
# WebSocket 消息 -> 更新 ViewModel按命令类型分发避免覆盖完整站点数据
self.ws_client.machine_data_changed.connect(self.viewModel.updateMachineData)
def _on_station_msg(message):
try:
cmd = message.get('command', '')
payload = message.get('data', message)
if cmd == 'station_drop_result_change':
self.viewModel.updateStationDropResults(payload)
else:
self.viewModel.updateStationData(payload)
except Exception:
pass
self.ws_client.station_data_changed.connect(_on_station_msg)
self.ws_client.connection_status_changed.connect(self.viewModel.setConnectionStatus)
self.ws_client.error_occurred.connect(self._on_ws_error)
# ===================== 响应式刷新方法 =====================
def _refresh_connection_status(self):
"""刷新 WebSocket 连接状态"""
connected = self.viewModel.connected
if connected:
self.connection_label.setText("● 已连接")
self.connection_label.setStyleSheet("color: green; font-weight: bold;")
else:
self.connection_label.setText("● 未连接")
self.connection_label.setStyleSheet("color: gray; font-weight: bold;")
def _refresh_machines(self):
"""刷新机台列表(响应式,避免不必要重建)"""
machines = self.viewModel.machines
machine_count = len(machines)
# 更新统计信息
self.machine_count_label.setText(f"机台数量: {machine_count}")
# 计算结构签名(测试类型 + 机台SN集合
sig_parts = []
for t in self.viewModel.getAllTestTypes():
sns = [m.get('SN', '') for m in self.viewModel.getMachinesByTestType(t)]
sig_parts.append(f"{t}:{','.join(sorted(sns))}")
signature = '|'.join(sig_parts)
# 结构未变化时,仅增量刷新通道,不重建标签页
if hasattr(self, '_last_structure_signature') and self._last_structure_signature == signature:
self._refresh_stations()
return
# 结构变化,重建标签页并记录签名
self._last_structure_signature = signature
self._rebuild_tabs()
def _refresh_stations(self):
"""刷新工位数据(响应式更新)"""
# 遍历所有存储的站点控件并更新它们
for (machine_sn, station_sn), widget_components in self._station_widgets.items():
# 获取最新的站点数据
machine = self.viewModel.getMachineBySN(machine_sn)
if machine:
stations = machine.get('stations', [])
station_data = None
for station in stations:
if station.get('SN') == station_sn:
station_data = station
break
if station_data:
# 更新站点显示
self._update_station_widget(widget_components, machine, station_data)
def _rebuild_tabs(self):
"""重新构建标签页(按测试类型分组),保留当前选中分类"""
# 清空站点控件引用
self._station_widgets.clear()
# 记录当前选中的标签文本(分类),避免复位
current_index = self.tab_widget.currentIndex()
current_text = self.tab_widget.tabText(current_index) if current_index >= 0 else None
# 清空现有标签页
self.tab_widget.clear()
# 获取所有测试类型
test_types = self.viewModel.getAllTestTypes()
if not test_types:
# 无数据时显示空白页
empty_widget = QWidget()
empty_layout = QVBoxLayout(empty_widget)
empty_label = QLabel("等待机台数据...")
empty_label.setAlignment(Qt.AlignCenter)
empty_label.setStyleSheet("color: gray; font-size: 18px;")
empty_layout.addWidget(empty_label)
self.tab_widget.addTab(empty_widget, "监控")
return
# 为每个测试类型创建标签页
for test_type in test_types:
machines = self.viewModel.getMachinesByTestType(test_type)
tab = self._create_test_type_tab(test_type, machines)
self.tab_widget.addTab(tab, test_type)
# 恢复到原先选中的标签页(如果仍存在)
if current_text:
for i in range(self.tab_widget.count()):
if self.tab_widget.tabText(i) == current_text:
self.tab_widget.setCurrentIndex(i)
break
def _create_test_type_tab(self, test_type, machines):
"""创建测试类型标签页(同类机台使用二级 Tab 切换)"""
tab = QWidget()
tab_layout = QVBoxLayout(tab)
tab_layout.setContentsMargins(10, 10, 10, 10)
tab_layout.setSpacing(10)
# 二级机台 Tab
machine_tab = QTabWidget()
machine_tab.setObjectName(f"machine-tab-{test_type}")
machine_tab.setTabsClosable(False)
machine_tab.setMovable(True)
machine_tab.setStyleSheet(
"QTabBar::tab{min-width:160px; min-height:28px; padding:4px 10px;}"
"QTabBar::tab:selected{background:#3A84FF; color:white; border-radius:4px;}"
"QTabBar::tab:!selected{background:transparent; color:black; border:1px solid #dcdcdc; border-radius:4px;}"
"QTabWidget::pane{border:0;}"
)
# 为每个机台创建一个 Tab
for machine in machines:
machine_card = self._create_machine_card(machine)
machine_label_text = machine.get('label', '未知机台')
machine_tab.addTab(machine_card, machine_label_text)
tab_layout.addWidget(machine_tab)
# 恢复该测试类型下之前选中的机台 Tab
prev_text = self._selected_machine_per_type.get(test_type)
if prev_text:
for i in range(machine_tab.count()):
if machine_tab.tabText(i) == prev_text:
machine_tab.setCurrentIndex(i)
break
# 监听机台 Tab 切换,记录当前选择
def on_machine_tab_changed(idx):
text = machine_tab.tabText(idx) if idx >= 0 else None
if text:
self._selected_machine_per_type[test_type] = text
machine_tab.currentChanged.connect(on_machine_tab_changed)
return tab
def _create_machine_card(self, machine):
"""创建机台监控卡片"""
card = QGroupBox()
card.setObjectName("machine-card")
card_layout = QVBoxLayout(card)
card_layout.setSpacing(10)
# 机台标题
machine_label = machine.get('label', '未知机台')
machine_sn = machine.get('SN', '')
# 机台标题已在二级Tab上显示这里不再占用垂直空间
# 工位(通道)监控区域(固定双通道显示)
stations = machine.get('stations', [])
# 准备两个通道数据
st1 = stations[0] if len(stations) >= 1 else None
st2 = stations[1] if len(stations) >= 2 else None
monitor_widget = QWidget()
grid = QGridLayout(monitor_widget)
grid.setSpacing(8)
# 最多4个通道按2x2网格布局缺省位置保留空白传入None
channels = []
for i in range(4):
st = stations[i] if len(stations) > i else None
ch = self._create_station_widget(machine, st, i + 1)
row, col = divmod(i, 2)
grid.addWidget(ch, row, col)
# 限制通道区域最大高度为窗口高度的 40%
monitor_widget.setMaximumHeight(int(max(800, self.height() * 0.4)))
self._machine_channel_containers.append(monitor_widget)
card_layout.addWidget(monitor_widget)
return card
def _create_station_widget(self, machine, station, channel_index):
"""创建工位监控组件(一次性构建,后续复用控件)"""
widget = QGroupBox()
widget.setObjectName("station-widget")
widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
layout = QVBoxLayout(widget)
layout.setSpacing(10)
machine_sn = machine.get('SN', '')
offline = not machine.get('connectState', 0) # 默认为 0离线
# 未配置通道:留空白占位,不显示任何文字
if station is None:
widget.setStyleSheet("QGroupBox{border:1px dashed #e0e0e0; background:#fafafa}")
layout.addStretch()
return widget
# 工位标题
station_label = station.get('label', '未知工位')
station_sn = station.get('SN', '')
# 通道标题行(标题在中间,状态在右边)
title_widget = QWidget()
title_widget.setStyleSheet("background-color: lightgray;")
title_layout = QHBoxLayout(title_widget)
title_layout.setContentsMargins(0, 5, 0, 5) # 增加上下边距
title_layout.setSpacing(5)
machine_label = QLabel(f"{machine_sn}")
machine_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
machine_label.setStyleSheet("font-size: 14px; padding-left: 8px;")
title_layout.addWidget(machine_label)
# 左侧占位
title_layout.addStretch()
# 中间:通道标题
title = QLabel(f"{station_label}")
title.setAlignment(Qt.AlignCenter)
title_font = QFont(); title_font.setPointSize(12); title_font.setBold(True)
title.setFont(title_font)
title_layout.addWidget(title)
# 右侧占位
title_layout.addStretch()
# 最右边:状态标签(根据实际状态动态设置)
station_status_label = QLabel() # 局部变量,不是实例变量
station_status_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
station_status_label.setStyleSheet("font-size: 16px; padding-right: 8px;") # 增大字体大小
title_layout.addWidget(station_status_label)
layout.addWidget(title_widget)
# 存储控件引用以便后续更新
widget_components = {
'stationGroup': widget,
'station_status_label': station_status_label,
'title_widget': title_widget
}
# 保存控件引用
if machine_sn and station_sn:
self._station_widgets[(machine_sn, station_sn)] = widget_components
# 内容栈offline/idle/dut
content_stack = QStackedWidget()
layout.addWidget(content_stack)
widget_components['stack'] = content_stack
# 离线页
offline_page = QWidget(); offline_layout = QVBoxLayout(offline_page)
offline_label = QLabel("设备离线")
offline_label.setAlignment(Qt.AlignCenter)
offline_label.setStyleSheet("color: red; font-size: 18px; padding: 20px;")
offline_layout.addWidget(offline_label); offline_layout.addStretch()
# 离线页底部status bar
offline_status_bar = QLabel("")
offline_status_bar.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
offline_status_bar.setStyleSheet("color: blue; font-size: 12px; padding: 5px; background: #f0f0f0; border-top: 1px solid #ccc;")
offline_status_bar.setWordWrap(True)
offline_status_bar.setMinimumHeight(30)
offline_layout.addWidget(offline_status_bar)
content_stack.addWidget(offline_page)
widget_components['offline_label'] = offline_label
widget_components['offline_status_bar'] = offline_status_bar
# 空闲页
idle_page = QWidget(); idle_layout = QVBoxLayout(idle_page)
idle_tip = QLabel("设备在线,请安排试验")
idle_tip.setAlignment(Qt.AlignCenter)
idle_tip.setStyleSheet("color: gray; font-size: 16px; padding: 20px;")
idle_layout.addWidget(idle_tip)
idle_layout.addStretch()
btn_layout = QHBoxLayout(); btn_layout.setSpacing(5)
select_dut_btn = QPushButton("选择样品")
select_dut_btn.setObjectName("station-action-btn")
select_dut_btn.setMinimumHeight(35)
select_dut_btn.clicked.connect(lambda: self._on_select_dut(machine_sn, station_sn))
btn_layout.addWidget(select_dut_btn)
new_test_btn = QPushButton("新建试验")
new_test_btn.setObjectName("station-action-btn")
new_test_btn.setMinimumHeight(35)
new_test_btn.clicked.connect(lambda: self._on_new_test(machine_sn, station_sn))
btn_layout.addWidget(new_test_btn)
idle_layout.addLayout(btn_layout)
# 空闲页底部status bar
idle_status_bar = QLabel("")
idle_status_bar.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
idle_status_bar.setStyleSheet("color: blue; font-size: 12px; padding: 5px; background: #f0f0f0; border-top: 1px solid #ccc;")
idle_status_bar.setWordWrap(True)
idle_status_bar.setMinimumHeight(30)
idle_layout.addWidget(idle_status_bar)
content_stack.addWidget(idle_page)
widget_components['idle_tip'] = idle_tip
widget_components['idle_status_bar'] = idle_status_bar
# DUT页信息 + 跌落方向表格 + status bar
dut_page = QWidget(); dut_page_layout = QVBoxLayout(dut_page); dut_page_layout.setSpacing(8)
info_widget = QWidget()
info_widget.setStyleSheet("QWidget { background-color: #FFF4E6; border: 1px solid #FFB84D; border-radius: 4px; padding: 8px; }")
info_layout = QGridLayout(info_widget); info_layout.setContentsMargins(8, 8, 8, 8); info_layout.setSpacing(5)
sample_title = QLabel("样品信息")
sample_title.setStyleSheet("background-color: #FF8C00; color: white; font-weight: bold; padding: 4px 12px; border-radius: 3px; font-size: 12px;")
sample_title.setAlignment(Qt.AlignCenter)
info_layout.addWidget(sample_title, 0, 0, 1, 5)
# 表头
sn_label_header = QLabel("产品序列号"); sn_label_header.setStyleSheet("border:none"); info_layout.addWidget(sn_label_header, 1, 0, Qt.AlignHCenter)
project_label_header = QLabel("项目代码"); project_label_header.setStyleSheet("border:none"); info_layout.addWidget(project_label_header, 1, 1, Qt.AlignHCenter)
phase_label_header = QLabel("项目阶段"); phase_label_header.setStyleSheet("border:none"); info_layout.addWidget(phase_label_header, 1, 2, Qt.AlignHCenter)
work_order_label_header = QLabel("工单名称"); work_order_label_header.setStyleSheet("border:none"); info_layout.addWidget(work_order_label_header, 1, 3, Qt.AlignHCenter)
weeks_label_header = QLabel("测试周次"); weeks_label_header.setStyleSheet("border:none"); info_layout.addWidget(weeks_label_header, 1, 4, Qt.AlignHCenter)
# 数据标签支持最多4个DUT初始创建4行
dut_data_labels = []
for i in range(4): # 最多4个DUT
row_idx = 2 + i
sn_label = QLabel(""); sn_label.setStyleSheet("font-weight: bold; color: #FF8C00; font-size: 13px; border:none"); sn_label.setAlignment(Qt.AlignCenter); info_layout.addWidget(sn_label, row_idx, 0)
project_code_label = QLabel(""); project_code_label.setStyleSheet("color: #0066CC; font-weight: bold; border:none"); project_code_label.setAlignment(Qt.AlignCenter); info_layout.addWidget(project_code_label, row_idx, 1)
phase_label = QLabel(""); phase_label.setStyleSheet("color: #0066CC; border:none"); phase_label.setAlignment(Qt.AlignCenter); info_layout.addWidget(phase_label, row_idx, 2)
work_order_label = QLabel(""); work_order_label.setStyleSheet("border:none"); work_order_label.setAlignment(Qt.AlignCenter); info_layout.addWidget(work_order_label, row_idx, 3)
weeks_value_label = QLabel(""); weeks_value_label.setStyleSheet("border:none"); weeks_value_label.setAlignment(Qt.AlignCenter); info_layout.addWidget(weeks_value_label, row_idx, 4)
# 默认隐藏
sn_label.hide()
project_code_label.hide()
phase_label.hide()
work_order_label.hide()
weeks_value_label.hide()
dut_data_labels.append({
'sn': sn_label,
'project': project_code_label,
'phase': phase_label,
'work_order': work_order_label,
'weeks': weeks_value_label,
})
dut_page_layout.addWidget(info_widget)
# 跌落方向表格
drop_table = QTableWidget(); drop_table.setObjectName("drop-direction-table")
dut_page_layout.addWidget(drop_table)
dut_page_layout.addStretch()
# DUT页底部status bar
dut_status_bar = QLabel("")
dut_status_bar.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
dut_status_bar.setStyleSheet("color: blue; font-size: 16px; padding: 5px; background: #f0f0f0; border-top: 1px solid #ccc;")
dut_status_bar.setWordWrap(True)
dut_status_bar.setMinimumHeight(30)
dut_page_layout.addWidget(dut_status_bar)
content_stack.addWidget(dut_page)
widget_components['dut_data_labels'] = dut_data_labels # 存储多行DUT标签
widget_components['drop_table'] = drop_table
widget_components['dut_status_bar'] = dut_status_bar
# 调用更新方法来初始化显示
self._update_station_widget(widget_components, machine, station)
return widget
def _update_station_widget(self, widget_components, machine, station):
"""更新工位监控组件(复用控件,不重建)"""
station_group = widget_components['stationGroup']
station_status_label = widget_components['station_status_label']
stack = widget_components.get('stack')
machine_sn = machine.get('SN', '')
offline = not machine.get('connectState', 0) # 默认为 0离线
# 获取status bar控件
offline_status_bar = widget_components.get('offline_status_bar')
idle_status_bar = widget_components.get('idle_status_bar')
dut_status_bar = widget_components.get('dut_status_bar')
# 处理离线状态
if offline:
station_status_label.setText("设备离线")
station_status_label.setStyleSheet(self._get_status_style('offline') + " padding-right: 8px;")
if offline_status_bar:
offline_status_bar.setText(station.get('lastDropResult', ''))
if stack: stack.setCurrentIndex(0)
return
# 获取通道状态不依赖是否有DUT
station_status = station.get('status', '')
# 根据 station_status 判断显示哪个页面
if station_status == 'finished':
# finished 状态显示DUT页面左上角显示"完成",底部显示"试验已经完成"
station_status_label.setText("完成")
station_status_label.setStyleSheet(self._get_status_style('finished') + " padding-right: 8px;")
if dut_status_bar:
dut_status_bar.setText("试验已经完成")
dut_status_bar.setAlignment(Qt.AlignCenter)
dut_status_bar.setStyleSheet("color: green; font-size: 20px; font-weight: bold; padding: 5px; background: #f0f0f0; border-top: 1px solid #ccc;")
# 更新DUT信息和跌落方向表
self._update_dut_info(widget_components, station)
if stack: stack.setCurrentIndex(2)
elif station_status in ['running', 'paused', 'error'] or station.get('dutSN'):
# 有DUT分配或试验运行中显示DUT页面
station_status_label.setText(self._get_status_text(station_status))
station_status_label.setStyleSheet(self._get_status_style(station_status))
if dut_status_bar:
dut_status_bar.setText(station.get('lastDropResult', ''))
dut_status_bar.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
dut_status_bar.setStyleSheet("color: blue; font-size: 16px; padding: 5px; background: #f0f0f0; border-top: 1px solid #ccc;")
# 更新DUT信息和跌落方向表
self._update_dut_info(widget_components, station)
if stack: stack.setCurrentIndex(2)
elif station_status in ['idle']:
# 空闲状态:显示空闲页面
station_status_label.setText("空闲")
station_status_label.setStyleSheet(self._get_status_style('idle') + " padding-right: 8px;")
idle_tip = widget_components.get('idle_tip')
if idle_tip:
idle_tip.setText("设备在线,请安排试验")
if idle_status_bar:
idle_status_bar.setText(station.get('lastDropResult', ''))
if stack: stack.setCurrentIndex(1)
else:
pass
def _update_dut_info(self, widget_components, station):
"""更新DUT信息和跌落方向表提取公共逻辑"""
# 获取DUT列表从 station['dut']['duts'] 获取)
duts = []
dut_obj = station.get('dut', {})
if isinstance(dut_obj, dict):
# 新的数据结构dut 对象包含 duts 数组
duts = dut_obj.get('duts', [])
if not duts:
# 如果 dut.duts 为空,但有 dutSN则兼容旧的单DUT结构
dut_sn = station.get('dutSN', '')
if dut_sn:
duts = [{
'SN': dut_sn,
'name': station.get('dutName', ''),
'project': station.get('dutProject', ''),
'projectName': station.get('projectName', ''),
'phase': station.get('dutPhase', ''),
'projectPhase': station.get('projectPhase', ''),
'workOrder': dut_obj.get('workOrder', ''),
'weeks': dut_obj.get('weeks', 0)
}]
# 更新DUT数据标签
dut_data_labels = widget_components.get('dut_data_labels', [])
if dut_data_labels:
# 隐藏所有行
for labels in dut_data_labels:
labels['sn'].hide()
labels['project'].hide()
labels['phase'].hide()
labels['work_order'].hide()
labels['weeks'].hide()
# 填充有数据的行
for idx, dut_info in enumerate(duts[:4]): # 最多显示4个
if idx < len(dut_data_labels):
labels = dut_data_labels[idx]
labels['sn'].setText(dut_info.get('SN', ''))
labels['project'].setText(dut_info.get('project', ''))
labels['phase'].setText(dut_info.get('phase', ''))
labels['work_order'].setText(dut_info.get('workOrder', ''))
labels['weeks'].setText(str(dut_info.get('weeks', 0)))
# 显示该行
labels['sn'].show()
labels['project'].show()
labels['phase'].show()
labels['work_order'].show()
labels['weeks'].show()
# 更新跌落方向表
table = widget_components.get('drop_table')
if table:
drop_dirs = station.get('dropDirections', [])
self._populate_drop_table(table, drop_dirs)
def _populate_drop_table(self, table, drop_directions):
"""增量填充跌落方向表格(复用表实例)"""
if drop_directions is None:
drop_directions = []
# 如果没有数据,保持空表
table.setColumnCount(4)
table.setHorizontalHeaderLabels(["跌落方向", "跌落高度(mm)", "设定跌落次数", "当前跌落次数"])
table.setRowCount(len(drop_directions))
# 样式与属性(只需设置一次,但多次设置也安全)
table.setStyleSheet(
"QTableWidget { gridline-color: #d0d0d0; border: 1px solid #d0d0d0; background-color: white; font-size: 11px; } "
"QHeaderView::section { background-color: #E8F4FD; padding: 4px; border: 1px solid #d0d0d0; font-weight: bold; font-size: 11px; } "
"QTableWidget::item { padding: 3px; } "
)
table.setEditTriggers(QTableWidget.NoEditTriggers)
table.setSelectionBehavior(QTableWidget.SelectRows)
table.verticalHeader().setVisible(True)
table.verticalHeader().setDefaultSectionSize(28)
# 填充数据
for row, direction in enumerate(drop_directions):
direction_name = direction.get('方向') or direction.get('name', '')
height = direction.get('高度', 0)
total_cycles = direction.get('设定次数', 0)
current_cycles = direction.get('当前次数', 0)
item0 = QTableWidgetItem(str(direction_name)); item0.setTextAlignment(Qt.AlignCenter)
item1 = QTableWidgetItem(str(height)); item1.setTextAlignment(Qt.AlignCenter)
item2 = QTableWidgetItem(str(total_cycles)); item2.setTextAlignment(Qt.AlignCenter)
item3 = QTableWidgetItem(str(current_cycles)); item3.setTextAlignment(Qt.AlignCenter)
# 将第1、2、3列字体大小设为其他列的1.1倍
font = item0.font()
font.setPointSize(int(9 * 1.1)) # 基础字号11px * 1.1
item0.setFont(font)
item1.setFont(font)
item2.setFont(font)
# 设置item3字体大小为其他列的1.4倍
font = item3.font()
font.setPointSize(int(11 * 1.4)) # 基础字号11px * 1.4
item3.setFont(font)
table.setItem(row, 0, item0)
table.setItem(row, 1, item1)
table.setItem(row, 2, item2)
table.setItem(row, 3, item3)
if current_cycles > 0:
for item in [item0, item1, item2, item3]:
item.setBackground(QColor("#ADD8E6"))
item3.setForeground(QColor("#FF0000"))
# 自动调整列宽
header = table.horizontalHeader()
header.setSectionResizeMode(0, QHeaderView.Fixed)
header.setSectionResizeMode(1, QHeaderView.ResizeToContents)
header.setSectionResizeMode(2, QHeaderView.ResizeToContents)
header.setSectionResizeMode(3, QHeaderView.Stretch)
table.setColumnWidth(0, 100) # 跌落方向
table.setColumnWidth(1, 100) # 设定高度
table.setColumnWidth(2, 110) # 设定次数
# 限制高度
table_height = table.verticalHeader().length() + table.horizontalHeader().height() + 10
table.setMaximumHeight(table_height); table.setMinimumHeight(table_height)
def _get_status_text(self, status):
"""获取状态文本"""
status_map = {
'idle': '空闲',
'running': '运行中',
'pause': '暂停',
'paused': '暂停',
'error': '故障',
'offline': '离线',
'finished': '完成'
}
return status_map.get(status, '未知')
def _get_status_style(self, status):
"""获取状态样式"""
style_map = {
'idle': 'color: blue; font-size: 14px; font-weight: bold;',
'running': 'color: red; font-size: 14px; font-weight: bold;',
'paused': 'color: orange; font-size: 14px; font-weight: bold;',
'pause': 'color: orange; font-size: 14px; font-weight: bold;',
'error': 'color: red; font-size: 14px; font-weight: bold;',
'offline': 'color: gray; font-size: 14px; font-weight: bold;',
'finished': 'color: green; font-size: 14px; font-weight: bold;'
}
return style_map.get(status, 'color: black; font-size: 14px;')
def _get_dut_status_style(self, dut_status):
"""获取 DUT 状态样式"""
style_map = {
'待启动': 'color: orange; font-size: 14px; font-weight: bold; padding: 5px;',
'运行中': 'color: blue; font-size: 14px; font-weight: bold; padding: 5px;',
'暂停': 'color: orange; font-size: 14px; font-weight: bold; padding: 5px;',
'已完成': 'color: green; font-size: 14px; font-weight: bold; padding: 5px;',
'异常': 'color: red; font-size: 14px; font-weight: bold; padding: 5px;'
}
return style_map.get(dut_status, 'color: black; font-size: 14px; padding: 5px;')
def _get_dut_status_style_inline(self, dut_status):
"""获取 DUT 状态内联样式(用于标题栏右侧小字体显示)"""
style_map = {
'待启动': 'color: orange; font-size: 16px; font-weight: bold; padding-right: 8px;',
'运行中': 'color: blue; font-size: 16px; font-weight: bold; padding-right: 8px;',
'暂停': 'color: orange; font-size: 16px; font-weight: bold; padding-right: 8px;',
'已完成': 'color: green; font-size: 16px; font-weight: bold; padding-right: 8px;',
'异常': 'color: red; font-size: 16px; font-weight: bold; padding-right: 8px;'
}
return style_map.get(dut_status, 'color: black; font-size: 16px; padding-right: 8px;')
# ===================== 事件处理 =====================
def _on_select_dut(self, machine_sn, station_sn):
"""选择 DUT样品"""
# 获取机台信息
machine = self.viewModel.getMachineBySN(machine_sn)
machine_label = machine.get('label', '') if machine else ''
# 获取connectState作为最大样品数量1-4
max_dut_count = machine.get('connectState', 1) if machine else 1
# 确保在有效范围内
if not isinstance(max_dut_count, int) or max_dut_count < 1:
max_dut_count = 1
if max_dut_count > 4:
max_dut_count = 4
# TODO: 获取当前登录用户ID从系统中获取
user_id = "" # 默认为空,后续集成用户管理后填充
# 打开 DUT 选择对话框
dialog = DUTSelectionDialog(
machine_sn=machine_sn,
station_num=station_sn,
machine_label=machine_label,
user_id=user_id,
max_dut_count=max_dut_count, # 传递最大样品数量
parent=self
)
# 对话框关闭后自动刷新界面
if dialog.exec_() == QDialog.Accepted:
# API 已在对话框中调用,这里可以选择刷新界面
print(f"样品分配完成 - 机台: {machine_sn}, 通道: {station_sn}")
# TODO: 刷新机台状态显示(如果需要)
def _on_new_test(self, machine_sn, station_sn):
"""新建试验 - 打开新建样品对话框并自动分配到当前工位"""
# 获取机台信息
machine = self.viewModel.getMachineBySN(machine_sn)
if not machine:
QMessageBox.warning(self, "错误", "无法获取机台信息")
return
# 预填充机台和通道信息
initial_form = {
"dtMachine": machine_sn,
"station": station_sn
}
# 打开新建样品对话框
dialog = DUTFormDialog(
mode="create",
dut_form=initial_form,
system_model=self.system_model,
parent=self
)
if dialog.exec_() == QDialog.Accepted:
# 获取表单数据
form = dialog.get_form_dict()
# 验证必填字段
if not form.get("SN"):
QMessageBox.warning(self, "错误", "产品序列号不能为空")
return
# 补充非表单字段的默认值
form.update({
'name': '',
'projectType': '',
'itemOnGoing': '',
'itemsFinished': 0,
'status': '待安排', # 新建默认为待安排状态
'deadLine': '',
'createdate': '',
'direction_codes': ''
})
try:
# 保存样品到数据库
self.dut_model.addDUT(form)
print(f"样品 {form.get('SN')} 已保存到数据库")
# 刷新 DUT Model 数据(确保能获取到刚创建的样品)
self.dut_model._load_duts()
# 自动选择并分配样品到当前机台和通道
# 使用与 DUTSelectionDialog 相同的分配逻辑
dut_sn = form.get("SN")
success = self._attach_dut_to_station(machine_sn, station_sn, [dut_sn])
if success:
QMessageBox.information(
self,
"成功",
f"样品 {dut_sn} 已创建并分配到\n机台: {machine_sn}\n通道: {station_sn}"
)
else:
QMessageBox.warning(
self,
"警告",
f"样品 {dut_sn} 已创建,但分配到工位失败\n请手动选择样品进行分配"
)
except Exception as e:
import traceback
traceback.print_exc()
QMessageBox.critical(self, "错误", f"创建样品失败:{str(e)}")
def _attach_dut_to_station(self, machine_sn, station_sn, dut_sns):
"""将样品分配到指定机台和通道(复用 DUTSelectionDialog 的逻辑)
Args:
machine_sn: 机台SN
station_sn: 通道编号(如 "01"
dut_sns: 样品SN列表
Returns:
bool: 是否成功
"""
try:
# 根据 dut_direction_mode 决定 action默认为2表示从PLC读取跌落参数
dut_direction_mode = 2
if dut_direction_mode == 2:
action = "select_and_transfer" # 选择并传送参数到 PLC
else:
action = "select" # 仅选择
# 构造请求参数(与 DUTSelectionDialog._attach_dut_to_station 完全一致)
control_reqs = {
"attachDut": {
"action": action,
"machine": machine_sn,
"station": station_sn,
"dut": dut_sns, # 使用数组传递DUT的SN
"updateTableFlag": True, # 更新数据表
"userid": "" # 用户ID可选
}
}
print(f"\n[调试] 分配 DUT 到机台:")
print(f" 机台: {machine_sn}")
print(f" 通道: {station_sn}")
print(f" 样品: {dut_sns}")
print(f" Action: {action}")
print(f" 请求参数: {json.dumps(control_reqs, ensure_ascii=False, indent=2)}")
# 调用 HTTP API
if self.use_http_api:
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)}")
if result.get("status") == "success":
return True
else:
print(f"样品分配失败: {result.get('message', '未知错误')}")
return False
else:
print("HTTP API未启用无法分配样品")
return False
except Exception as e:
print(f"分配样品到工位失败: {e}")
import traceback
traceback.print_exc()
return False
def _on_ws_error(self, error_message):
"""WebSocket 错误处理"""
try:
print(f"WebSocket 错误: {error_message}")
except Exception:
pass
def resizeEvent(self, event):
"""调整通道容器高度为窗口高度的约 40%"""
super().resizeEvent(event)
h = int(max(800, self.height() * 0.4))
for w in getattr(self, '_machine_channel_containers', []):
try:
w.setMaximumHeight(h)
except Exception:
pass