Files
zjsh_mixing_system_ui/dispatch_task.py

530 lines
19 KiB
Python
Raw Normal View History

2025-11-16 11:55:28 +08:00
#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''
# @Time : 2025/11/10 11:29
# @Author : reenrr
# @Description : 派单任务的详情按钮点击之后弹出, 显示派单任务的详情
'''
from PySide6.QtWidgets import (
QApplication,
QDialog,
QVBoxLayout,
QHBoxLayout,
QGridLayout,
QLabel,
QWidget,
QPushButton,
)
from PySide6.QtGui import QPixmap, QFont, QPainter, QIcon
from PySide6.QtCore import Qt, QEvent, QSize, QTimer, Slot
from PySide6.QtNetwork import QTcpSocket, QAbstractSocket
import sys
import json
from datetime import datetime
# -----------
# 参数配置
# -----------
tcp_server_host = "127.0.0.1"
tcp_server_port = 8888
RECONNECT_INTERVAL = 2000 # 重连间隔(毫秒)
class DispatchDetailsDialog(QDialog):
"""
派单任务的界面
"""
def __init__(self, parent=None):
super().__init__(parent)
self.setAttribute(Qt.WA_TranslucentBackground)
self.is_running = False # 系统运行状态标记
self.latest_json_data = {"erp_id": None, "artifact_id": None} # 缓存服务端发送的最新JSON数据
self.statusWidgets = [] # 存储api_field + 对应的value标签
# ---------------
# TCP客户端核心配置
# ---------------
self.tcp_socket = QTcpSocket(self) # TCP socket实例
self.tcp_server_host = tcp_server_host
self.tcp_server_port = tcp_server_port
self.is_tcp_connected = False # TCP连接状态标记
self.has_connected_once = False # 连接至服务器至少一次标记(区别首次连接和断开后重连)
self.reconnect_count = 0 # 重连次数计数器
# 重连定时器每隔RECONNECT_INTERVAL毫秒重连一次
self.reconnect_timer = QTimer(self)
self.reconnect_timer.setInterval(RECONNECT_INTERVAL)
self.reconnect_timer.timeout.connect(self._reconnect_to_server) # 绑定重连函数
# 绑定TCP信号与槽事件驱动
self._bind_tcp_signals()
# 初始化存储需要修改的控件
self.id_value_label = None # 对应管片ID值标签
self.rows = [] # 所有行的单元格列表包含label、value
self._init_ui()
# ----------------
# 客户端自动后自动连接服务端
# ----------------
print(f"客户端启动,自动连接服务端{self.tcp_server_host}:{self.tcp_server_port}...")
self._connect_to_server()
# ---------------------
# TCP连接相关函数
# ---------------------
def _connect_to_server(self):
"""主动发起连接(仅在未连接状态下有效"""
if not self.is_tcp_connected and self.tcp_socket.state() != QAbstractSocket.ConnectingState:
self.tcp_socket.abort() # 终止现有连接
self.tcp_socket.connectToHost(self.tcp_server_host, self.tcp_server_port)
print(f"发起连接请求:{self.tcp_server_host}:{self.tcp_server_port}")
def _reconnect_to_server(self):
"""重连执行函数:无线重连,知道连接成功"""
if not self.is_tcp_connected:
self.reconnect_count += 1
# 日志优化每10次重连提示一次避免日志刷屏
if self.reconnect_count % 10 == 0:
print(f"已连续重连{self.reconnect_count}次,仍未连接服务端,请检查服务端状态...")
else:
print(f"{self.reconnect_count}次重连请求:{self.tcp_server_host}:{self.tcp_server_port}")
self._connect_to_server()
def _bind_tcp_signals(self):
"""绑定TCP socket的核心信号连接、断开、接收数据、错误"""
# 连接成功信号
self.tcp_socket.connected.connect(self._on_tcp_connected)
# 断开连接信号
self.tcp_socket.disconnected.connect(self._on_tcp_disconnected)
# 接收数据信号(有新数据时触发)
self.tcp_socket.readyRead.connect(self._on_tcp_data_received)
# 错误信号(连接/通信出错时触发)
self.tcp_socket.errorOccurred.connect(self._on_tcp_error)
# -----------------------
# 界面初始化函数
# -----------------------
def _init_ui(self):
self.setWindowFlags(Qt.FramelessWindowHint)
self._load_background()
main_layout = QVBoxLayout(self)
main_layout.setContentsMargins(32, 20, 32, 50)
main_layout.setSpacing(0)
# 1. 顶部区域(标题 + 关闭按钮)
self._add_top_area(main_layout)
# 2. 对应管片ID区域
self._add_segment_id_area(main_layout)
# 3. 网格信息区域单列6行
self._add_grid_info_area(main_layout)
main_layout.addSpacing(30)
# 4. 操作按钮区域
self._init_operation_buttons(main_layout)
def _init_operation_buttons(self, parent_layout):
"""初始化操作按钮(生产异常/生产取消)"""
buttonContainer = QWidget()
buttonLayout = QHBoxLayout(buttonContainer)
buttonLayout.setSpacing(100)
self.errorButton = QPushButton("生产异常")
self.cancelButton = QPushButton("生产取消")
self.errorButton.setStyleSheet("""
QPushButton {
color: #1E1E1E;
font-size: 18px;
font-weight: bold;
padding: 15px 50px;
border: 1px solid;
background-color: #FF0000;
border-color: transparent;
}
QPushButton:hover {
opacity: 0.9;
background-color: #B71C1C;
}
QPushButton:pressed {
opacity: 0.8;
}
""")
self.cancelButton.setStyleSheet("""
QPushButton {
color: #1E1E1E;
font-size: 18px;
font-weight: bold;
padding: 15px 50px;
border: 1px solid;
background-color: #FFD700;
border-color: transparent;
}
QPushButton:hover {
opacity: 0.9;
background-color: #FFB300
}
QPushButton:pressed {
opacity: 0.8;
}
""")
self.errorButton.clicked.connect(self.on_error_clicked)
self.cancelButton.clicked.connect(self.on_cancel_clicked)
# 初始禁用“停止”和“异常”按钮(未连接时不可用)
self.errorButton.setDisabled(True)
self.cancelButton.setDisabled(True)
buttonLayout.addStretch(1)
buttonLayout.addWidget(self.errorButton)
buttonLayout.addWidget(self.cancelButton)
buttonLayout.addStretch(1)
parent_layout.addWidget(buttonContainer)
def _load_background(self):
self.bg_pixmap = QPixmap("详情弹出背景.png") # 修改为派单任务背景图
if self.bg_pixmap.isNull():
print("错误:派单任务背景.png 加载失败,请检查路径!")
self.setFixedSize(800, 500)
else:
self.setFixedSize(self.bg_pixmap.size())
def _add_top_area(self, parent_layout):
top_layout = QHBoxLayout()
top_layout.setContentsMargins(0, 0, 0, 36)
top_layout.addStretch()
# 标题改为“任务派单”
title_label = QLabel("任务派单")
font = QFont()
font.setPixelSize(24)
font.setLetterSpacing(QFont.AbsoluteSpacing, 2)
font.setBold(True)
title_label.setFont(font)
title_label.setStyleSheet("color: #13fffc; font-weight: Bold;")
title_label.setAlignment(Qt.AlignCenter)
top_layout.addWidget(title_label, alignment=Qt.AlignTop)
# 关闭按钮
top_layout.addStretch()
parent_layout.addLayout(top_layout)
def _add_segment_id_area(self, parent_layout):
id_layout = QHBoxLayout()
id_label = QLabel("对应管片ID") # 标签文字修改
id_label.setFixedSize(318, 32)
id_font = QFont()
id_font.setPixelSize(18)
id_font.setLetterSpacing(QFont.AbsoluteSpacing, 2)
id_font.setBold(True)
id_label.setFont(id_font)
id_label.setStyleSheet(
"""
background-image: url("详情标题.png");
background-repeat: no-repeat;
background-position: center;
color: #13ffff;
"""
)
id_label.setContentsMargins(16, 0, 0, 0)
id_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
self.id_value_label = QLabel("") # 初始管片ID值
value_font = QFont()
value_font.setPixelSize(18)
value_font.setBold(True)
value_font.setLetterSpacing(QFont.AbsoluteSpacing, 2)
self.id_value_label.setFont(value_font)
self.id_value_label.setStyleSheet("color: #13ffff;")
self.id_value_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
id_layout.addWidget(id_label)
id_layout.addStretch()
id_layout.addWidget(self.id_value_label)
id_layout.setContentsMargins(0, 0, 0, 16)
parent_layout.addLayout(id_layout)
parent_layout.setAlignment(id_layout, Qt.AlignTop)
def _add_grid_info_area(self, parent_layout):
grid_layout = QGridLayout()
grid_layout.setSpacing(12)
# 初始化信息条目6行
info_items = [
{"name":"任务单号", "value": "", "api_field": "task_id"},
{"name": "工程名称", "value": "", "api_field": "project_name"},
{"name": "配比编号", "value": "", "api_field": "produce_mix_id"},
{"name": "要料状态", "value": "", "api_field": "flag"},
{"name": "砼强度", "value": "", "api_field": "beton_grade"},
{"name": "要料方量", "value": "", "api_field": "adjusted_volume"},
]
self.rows.clear()
self.statusWidgets.clear() # 清空缓存
for row, item in enumerate(info_items):
cell_widget = self._create_info_cell(item["name"], item["value"])
self.rows.append(cell_widget)
grid_layout.addWidget(cell_widget, row, 0)
self.statusWidgets.append({
'api_field': item["api_field"],
'valueLabel': cell_widget.value # 绑定实际UI控件
}
)
parent_layout.addLayout(grid_layout)
def _create_info_cell(self, label_text, value_text):
cell_widget = QWidget()
cell_bg = QPixmap("派单任务信息栏1.png") # 正常背景图
cell_widget.setObjectName("infoCell")
if not cell_bg.isNull():
cell_widget.setFixedSize(cell_bg.size())
cell_widget.setStyleSheet(
"""
QWidget {
background-image: url(派单任务信息栏1.png);
background-repeat: no-repeat;
background-position: Center;
}
QWidget:hover {
background-image: url(派单任务信息栏2.png);
}
QWidget QLabel#valueLabel {
color: #9fbfd4;
background: none;
}
"""
)
cell_layout = QHBoxLayout(cell_widget)
cell_layout.setContentsMargins(0, 0, 0, 0)
# 左侧标签
label = QLabel(label_text)
label.setFixedSize(136, 60)
label_font = QFont()
label_font.setPixelSize(16)
label_font.setLetterSpacing(QFont.AbsoluteSpacing, 2)
label.setFont(label_font)
label.setStyleSheet("background: none;color: #fffffd; font-weight:Bold;")
label.setAlignment(Qt.AlignCenter)
cell_widget.label = label
# 右侧值标签设置objectName以便样式选择
value = QLabel(value_text)
value.setObjectName("valueLabel")
value_font = QFont()
value_font.setPixelSize(20)
value.setFont(value_font)
value.setAlignment(Qt.AlignCenter)
cell_widget.value = value
cell_layout.addWidget(label) # 左侧的标题标签
cell_layout.addSpacing(60)
cell_layout.addWidget(value) # 右侧的值标签
cell_widget.installEventFilter(self)
return cell_widget
def eventFilter(self, obj, event):
"""
实现事件过滤器动态修改右侧值颜色
"""
# 只处理父控件infoCell的事件
if obj.objectName() == "infoCell":
# 鼠标进入父控件 → 改#13f0f3
if event.type() == QEvent.Enter:
if hasattr(obj, "value"): # 确保存在value控件
obj.value.setStyleSheet("background: none; color: #13f0f3;")
# 鼠标离开父控件 → 恢复默认色
elif event.type() == QEvent.Leave:
if hasattr(obj, "value"):
obj.value.setStyleSheet("background: none; color: #9fbfd4;")
return super().eventFilter(obj, event)
def paintEvent(self, event):
if not self.bg_pixmap.isNull():
painter = QPainter(self)
painter.drawPixmap(self.rect(), self.bg_pixmap)
super().paintEvent(event)
def _update_ui_from_data(self, data):
"""根据TCP获取的数据更新界面状态"""
# 1、更新顶部独立的“对应管片ID”
if "artifact_id" in data:
self.id_value_label.setText(str(data["artifact_id"]))
# 2、更新网格中的所有字段
for widget in self.statusWidgets:
api_field = widget['api_field']
value_label = widget['valueLabel']
if api_field in data:
new_value = str(data[api_field])
value_label.setText(new_value)
# ------------------- 对外修改接口 -------------------
# row 对应行号(0-6)从0开始
# --------------------------------------------------
def set_segment_id(self, new_id):
"""修改上方的 对应的管片ID的值"""
if self.id_value_label:
self.id_value_label.setText(str(new_id))
def set_row_label(self, row, new_label_text: str):
"""修改左侧的显示的标签的文本,如: 创建时间、派单时间等"""
if 0 <= row < len(self.rows):
self.rows[row].label.setText(new_label_text)
def set_row_value(self, row, new_value_text: str):
"""修改右侧的显示的值, 如: 2025年9月9日 9:9:9"""
if 0 <= row < len(self.rows):
self.rows[row].value.setText(new_value_text)
# ------------------
# 按钮点击事件
# ------------------
def on_error_clicked(self):
"""点击“生产异常”:向服务端发送异常指令"""
print("🔘 点击「生产异常」按钮")
self._clear_ui_info()
self._send_tcp_request("production_error")
def on_cancel_clicked(self):
"""点击“生产取消”:向服务端发送取消指令"""
print("🔘 点击「生产取消」按钮")
self._clear_ui_info()
self._send_tcp_request("cancel_feed")
# --------------------
# 清空界面信息的通用方法
# --------------------
def _clear_ui_info(self):
"""清空管片ID和网格信息"""
if self.id_value_label:
self.id_value_label.setText("")
for widget in self.statusWidgets:
widget['valueLabel'].setText("")
print(" 界面信息已清空")
# ------------------
# TCP客户端核心功能
# ------------------
@Slot()
def _on_tcp_connected(self):
"""TCP连接成功回调"""
self.is_tcp_connected = True
self.has_connected_once = True
self.reconnect_timer.stop() # 停止重连定时器
self.reconnect_count = 0 # 重连计数器清零
self.is_running = True
print(f"TCP连接成功{self.tcp_server_host}:{self.tcp_server_port}")
# 连接成功后,向服务器发送“请求初始数据”指令
self._send_tcp_request("get_initial_data")
# 更新按钮状态:启用“生产异常”“生产取消”
self.errorButton.setDisabled(False)
self.cancelButton.setDisabled(False)
@Slot()
def _on_tcp_disconnected(self):
"""TCP连接断开回调"""
self.is_tcp_connected = False
self.is_running = False
print(f"TCP连接断开{self.tcp_server_host}:{self.tcp_server_port}")
# 启用/禁用按钮
self.errorButton.setDisabled(True)
self.cancelButton.setDisabled(True)
# 清空界面
self._clear_ui_info()
if not self.reconnect_timer.isActive():
print(f"连接断开,将在{RECONNECT_INTERVAL / 1000}秒后启动重连...")
self.reconnect_timer.start()
@Slot()
def _on_tcp_data_received(self):
"""TCP数据接收回调服务器发送数据时触发"""
tcp_data = self.tcp_socket.readAll().data().decode("utf-8").strip()
print(f"TCP数据接收{tcp_data}")
# 解析数据
try:
status_data = json.loads(tcp_data)
self.latest_json_data = status_data
self._update_ui_from_data(status_data)
except json.JSONDecodeError as e:
print(f"TCP数据解析失败非JSON格式{e}, 原始数据:{tcp_data}")
except Exception as e:
print(f"TCP数据处理异常{e}")
@Slot(QAbstractSocket.SocketError)
def _on_tcp_error(self, error):
"""TCP错误回调"""
if not self.is_tcp_connected:
error_str = self.tcp_socket.errorString()
print(f"TCP错误{error_str}")
self.is_tcp_connected = False
self.is_running = False
# 启用/禁用按钮
self.errorButton.setDisabled(True)
self.cancelButton.setDisabled(True)
# 首次连接失败时,启动重连定时器
if not self.reconnect_timer.isActive():
print(f"将在{RECONNECT_INTERVAL / 1000}秒后启动重连")
self.reconnect_timer.start()
def _send_tcp_request(self, request_cmd="get_status"):
"""向TCP服务器发送请求指令"""
if not self.is_tcp_connected:
print("TCP连接未建立无法发送请求")
return
# 构造请求数据
request_data = json.dumps({
"cmd": request_cmd,
"erp_id": self.latest_json_data["erp_id"],
"artifact_id": self.latest_json_data["artifact_id"],
"timestamp": self.get_current_time(),
"client_info": "布料系统客户端"
}) + "\n" # 增加换行符作为数据结束标识
# 发送请求数据
self.tcp_socket.write(request_data.encode("utf-8"))
print(f"TCP请求发送{request_data.strip()}")
# ------------------
# 时间相关的通用方法
# ------------------
def get_current_time(self):
"""获取格式化的当前时间"""
return datetime.now().strftime('%Y-%m-%d %H:%M:%S')
# 测试代码
if __name__ == "__main__":
app = QApplication(sys.argv)
dialog = DispatchDetailsDialog()
dialog.show()
sys.exit(app.exec())