#!/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 MAX_RECONNECT = 3 # 最大重连次数 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: self.tcp_socket.abort() # 终止现有连接 self.tcp_socket.connectToHost(self.tcp_server_host, self.tcp_server_port) def _reconnect_to_server(self): """重连执行函数:仅在未连接且未达最大次数时触发""" if not self.is_tcp_connected and self.reconnect_count < MAX_RECONNECT: self.reconnect_count += 1 print(f"第{self.reconnect_count}次重连(共{MAX_RECONNECT}次尝试)...") self._connect_to_server() elif self.reconnect_count >= MAX_RECONNECT: self.reconnect_timer.stop() # 停止重连定时器 print(f"已达最大重连次数({MAX_RECONNECT}次),停止重连,请检查服务端状态") 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) # 重置状态指示灯为“未连接”状态 for widget in self.statusWidgets: widget['indicator'].setStyleSheet(""" QLabel { background-color: #9E9E9E; border-radius: 10px; border: 2px solid #555555; } """) @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.has_connected_once and self.reconnect_count == 0: print(f"将在{RECONNECT_INTERVAL / 1000}秒后启动重连(最多{MAX_RECONNECT}次)") self.reconnect_timer.start() def _send_tcp_request(self, request_cmd="get_status"): """向TCP服务器发送请求指令""" if not self.is_tcp_connected: print("TCP连接未建立,无法发送请求") return print(self.latest_json_data) # 构造请求数据 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())