From 3d860b22fd6a4aee3b48dccc71d0b92f3bb571bd Mon Sep 17 00:00:00 2001 From: yanganjie Date: Thu, 13 Nov 2025 09:37:41 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=BA=86=20=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E5=88=97=E8=A1=A8=E5=BC=B9=E7=AA=97(=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E7=8A=B6=E6=80=81=E6=B6=88=E6=81=AF=E3=80=81=E9=A2=84?= =?UTF-8?q?=E8=AD=A6=E6=B6=88=E6=81=AF)=EF=BC=8C=E4=BB=A5=E5=8F=8A?= =?UTF-8?q?=E5=AD=98=E5=82=A8=E6=B6=88=E6=81=AF=E7=9A=84=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- common/msg_db_helper.py | 122 ++++++++++ controller/bottom_control_controller.py | 109 +++++++-- controller/main_controller.py | 21 +- db/three.db | Bin 53248 -> 53248 bytes images/消息列表背景.png | Bin 0 -> 6260 bytes service/msg_query_thread.py | 52 ++++ service/msg_recorder.py | 44 ++++ utils/image_paths.py | 7 +- view/main_window.py | 4 + view/widgets/message_popup_widget.py | 277 ++++++++++++++++++++++ view/widgets/system_center_dialog.py | 1 + view/widgets/system_diagnostics_dialog.py | 1 + view/widgets/task_widget.py | 6 +- 13 files changed, 615 insertions(+), 29 deletions(-) create mode 100644 common/msg_db_helper.py create mode 100644 images/消息列表背景.png create mode 100644 service/msg_query_thread.py create mode 100644 service/msg_recorder.py create mode 100644 view/widgets/message_popup_widget.py diff --git a/common/msg_db_helper.py b/common/msg_db_helper.py new file mode 100644 index 0000000..9da22ea --- /dev/null +++ b/common/msg_db_helper.py @@ -0,0 +1,122 @@ +import sqlite3 +from datetime import datetime +from PySide6.QtCore import QMutex + +""" + sqlite消息数据库管理: db/messages.db +""" + +class DBHelper: + _instance = None # 单例模式 + _mutex = QMutex() + + def __new__(cls, db_name="db/messages.db"): + """消息数据库连接管理器""" + cls._mutex.lock() + try: + if not cls._instance: + cls._instance = super().__new__(cls) + cls._instance.db_name = db_name + cls._instance._init_db() + finally: + cls._mutex.unlock() + return cls._instance + + def _init_db(self): + """创建消息表""" + conn = sqlite3.connect(self.db_name) + cursor = conn.cursor() + # 创建消息表:id(主键)、内容、类型(1=系统消息,2=预警)、是否处理、创建时间 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + content TEXT NOT NULL, + message_type INTEGER NOT NULL, -- 1: normal, 2: warning + is_processed INTEGER NOT NULL DEFAULT 0, -- 为1表示已经解决/已经处理 + create_time TEXT NOT NULL, -- 创建时间 + last_modified TEXT NOT NULL -- 最后修改时间(新增/更新时都会变) + ) + ''') + conn.commit() + conn.close() + + def insert_message(self, content, message_type, is_processed=0): + """插入消息: create_time和last_modified一开始都设为当前时间""" + current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + conn = sqlite3.connect(self.db_name) + cursor = conn.cursor() + cursor.execute(''' + INSERT INTO messages (content, message_type, is_processed, create_time, last_modified) + VALUES (?, ?, ?, ?, ?) + ''', (content, message_type, is_processed, current_time, current_time)) + conn.commit() + conn.close() + + def update_processed_status(self, content, message_type, is_processed=1): + """更新is_processed状态, 同时更新last_modified为当前时间""" + current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + conn = sqlite3.connect(self.db_name) + cursor = conn.cursor() + cursor.execute(''' + UPDATE messages + SET is_processed = ?, last_modified = ? + WHERE content = ? AND message_type = ? + ''', (is_processed, current_time, content, message_type)) # 更新last_modified + conn.commit() + conn.close() + + def get_latest_messages(self, message_type, limit=16): + """查询最新limit条消息, 包含last_modified字段""" + conn = sqlite3.connect(self.db_name) + cursor = conn.cursor() + # 按创建时间倒序(确保取最新的消息),返回时包含last_modified + cursor.execute(''' + SELECT content, is_processed, create_time, last_modified + FROM messages + WHERE message_type = ? + ORDER BY create_time DESC + LIMIT ? + ''', (message_type, limit)) + messages = cursor.fetchall() # 结果:[(content, is_processed, create_time, last_modified), ...] + conn.close() + return messages[::-1] # 反转后按时间正序排列 + + +if __name__ == "__main__": + """测试用例:向数据库插入测试消息""" + import time + + # 创建DBHelper实例(会自动创建数据库和表) + db = DBHelper() + + # # 插入系统状态消息(message_type=1) + # db.insert_message( + # content="开始自动智能浇筑系统", + # message_type=1, # 系统消息 + # is_processed=False + # ) + # time.sleep(1) + # db.insert_message( + # content="智能浇筑系统启动成功", + # message_type=1, + # is_processed=False + # ) + # time.sleep(1) + # db.insert_message( + # content="上料斗载荷达到80%", + # message_type=1, # 系统消息 + # is_processed=True # 已处理 + # ) + + # 插入预警消息(message_type=2) + db.insert_message( + content="振动频率异常", + message_type=2, # 预警消息 + is_processed=False # 未处理 + ) + time.sleep(1) + db.insert_message( + content="料斗重量超出阈值", + message_type=2, # 预警消息 + is_processed=True # 已处理 + ) \ No newline at end of file diff --git a/controller/bottom_control_controller.py b/controller/bottom_control_controller.py index bd1c000..cc6d632 100644 --- a/controller/bottom_control_controller.py +++ b/controller/bottom_control_controller.py @@ -1,8 +1,12 @@ # controller/bottom_control_controller.py -from PySide6.QtCore import Qt, QPropertyAnimation, QRect, QParallelAnimationGroup, QEasingCurve +from PySide6.QtCore import Qt, QPropertyAnimation, QRect, QParallelAnimationGroup, QEasingCurve, QPoint, Slot from view.widgets.system_center_dialog import SystemCenterDialog from view.widgets.bottom_control_widget import BottomControlWidget from view.widgets.system_diagnostics_dialog import SystemDiagnosticsDialog +from view.widgets.message_popup_widget import MessagePopupWidget + +from service.msg_recorder import MessageRecorder +from service.msg_query_thread import MsgQueryThread """ 控制主界面底部的所有按钮, 包括系统诊断、系统中心等的行为。 @@ -13,29 +17,71 @@ class BottomControlController: def __init__(self, bottom_control_widget:BottomControlWidget, main_window): self.bottom_control_widget = bottom_control_widget self.main_window = main_window - self._bind_buttons() - - # 系统中心弹窗 - self.system_center_dialog = SystemCenterDialog(self.main_window) - self.system_center_dialog.hide() # 初始隐藏 (必须) - self._init_system_center_dialog_hide_animations() # 系统诊断弹窗 self.system_diagnostics_dialog = SystemDiagnosticsDialog(self.main_window) - self.system_diagnostics_dialog.hide() self._init_system_diagnostics_dialog_hide_animations() + + # 系统中心弹窗 + self.system_center_dialog = SystemCenterDialog(self.main_window) + self._init_system_center_dialog_hide_animations() + # ===================== 消息列表相关 ==================================== + # 系统状态消息列表控件 + self.status_msg_widget = MessagePopupWidget("系统状态消息", self.main_window) + + # 预警消息列表控件 + self.warning_msg_widget = MessagePopupWidget("预警消息", self.main_window) + + # 消息数据库查询线程 + self.msg_query_thread = MsgQueryThread() # 默认1秒查询一次,每次16条消息 + # 连接线程信号到UI更新函数 + self.msg_query_thread.new_messages.connect(self.update_message_ui, Qt.QueuedConnection) + self.msg_query_thread.start() # 启动线程 + # ======================================================================= + + # 绑定主界面底部按钮的信号(如:系统诊断按钮等,点击触发弹窗) + self._bind_buttons() + # 绑定弹窗中按钮的信号 self._bind_dialog_signals() + @Slot(int, list) + def update_message_ui(self, message_type, messages): + """根据消息类型更新对应UI, message_type为1表示系统状态消息, 为2表示预警消息""" + # 清空当前消息列表(避免重复) + target_widget = self.status_msg_widget if message_type == 1 else self.warning_msg_widget + target_widget.list_widget.clear() + # target_widget.messages.clear() + + # 添加新消息 (第三个为 create_time, 第四个为 last_modified) + for content, is_processed, create_time, _ in messages: + # 从create_time中提取时分秒 + # time_part = create_time.split()[1] # 空格分隔之后的[1]为时分秒 + # 需要添加到消息列表的消息格式为 时分秒 + 消息原始内容 + # msg_list_content = f"{create_time} {content}" + target_widget.add_message(content, is_processed = bool(is_processed), msg_time = create_time) + + def stop_threads(self): + """停止当前控制器中的所有线程""" + if hasattr(self, 'msg_query_thread') and self.msg_query_thread.isRunning(): + self.msg_query_thread.stop() + def _bind_buttons(self): # 底部系统中心按钮 → 触发弹窗显示/隐藏 self.bottom_control_widget.center_btn.clicked.connect(self.toggle_system_center_dialog) # 底部系统诊断按钮 → 触发弹窗显示/隐藏 self.bottom_control_widget.diagnosis_btn.clicked.connect(self.toggle_system_diagnostics_dialog) + + # 底部系统状态消息按钮 → 触发系统状态消息列表显示/隐藏 + self.bottom_control_widget.status_msg_btn.clicked.connect(self.toggle_system_status_msg_list) + + # 底部预警消息列表按钮 → 触发预警消息列表显示/隐藏 + self.bottom_control_widget.warning_list_btn.clicked.connect(self.toggle_system_warning_msg_list) def _bind_dialog_signals(self): """绑定弹窗按钮的信号""" + # 系统中心弹窗的按钮信号 self.system_center_dialog.sys_setting_clicked.connect(self.handle_sys_setting) self.system_center_dialog.data_center_clicked.connect(self.handle_data_center) self.system_center_dialog.user_center_clicked.connect(self.handle_user_center) @@ -155,25 +201,12 @@ class BottomControlController: self.system_diagnostics_dialog.show() def _calc_system_diagnostics_dialog_position(self): - """计算系统诊断弹窗位置(显示在诊断按钮上方,与中心弹窗布局一致)""" + """计算系统诊断弹窗位置(显示在系统诊断按钮上方)""" btn = self.bottom_control_widget.diagnosis_btn # 诊断按钮 - bottom_widget = self.bottom_control_widget - # 计算按钮在主窗口中的绝对位置 - bottom_pos_rel_main = bottom_widget.pos() # 底部控件相对于主窗口的位置 - btn_pos_rel_bottom = btn.pos() # 诊断按钮相对于底部控件的位置 - btn_pos_rel_main = bottom_pos_rel_main + btn_pos_rel_bottom # 诊断按钮在主窗口中的绝对位置 - - # 计算弹窗坐标(显示在按钮上方,水平居中对齐) - btn_width = btn.width() - dialog_size = self.system_diagnostics_dialog.size() - # 水平方向:与按钮居中对齐 - # dialog_x = btn_pos_rel_main.x() + (btn_width - dialog_size.width()) // 2 - # dialog_x = btn_pos_rel_main.x() + (btn_width - dialog_size.width()) - dialog_x = btn_pos_rel_main.x() # 与系统诊断按钮的左边平齐 - # 垂直方向:在按钮上方(与按钮保持10px间距) - # dialog_y = btn_pos_rel_main.y() - dialog_size.height() - 10 - dialog_y = btn_pos_rel_main.y() - dialog_size.height() + btn_pos = btn.mapToGlobal(QPoint(0, 0)) + dialog_x = btn_pos.x() + dialog_y = btn_pos.y() - self.system_diagnostics_dialog.height() # 设置弹窗位置(动画会基于此位置执行滑入效果) self.system_diagnostics_dialog.move(dialog_x, dialog_y) @@ -191,4 +224,28 @@ class BottomControlController: # 设置动画参数并启动 self.dia_hide_pos_anim.setStartValue(current_geo) self.dia_hide_pos_anim.setEndValue(end_rect) - self.dia_hide_anim_group.start() \ No newline at end of file + self.dia_hide_anim_group.start() + + # ================== 系统状态消息列表: 显示系统消息 =================== + def toggle_system_status_msg_list(self): + """系统状态消息按钮点击之后 显示系统消息""" + if not self.status_msg_widget.isVisible(): + btn_pos = self.bottom_control_widget.status_msg_btn.mapToGlobal(QPoint(0, 0)) + popup_x = btn_pos.x() + popup_y = btn_pos.y() - self.status_msg_widget.height() + self.status_msg_widget.move(popup_x, popup_y) + self.status_msg_widget.show() + else: + self.status_msg_widget.close() + + # ================== 预警消息列表: 显示预警消息 =================== + def toggle_system_warning_msg_list(self): + """预警消息列表按钮点击之后 显示预警消息""" + if not self.warning_msg_widget.isVisible(): + btn_pos = self.bottom_control_widget.warning_list_btn.mapToGlobal(QPoint(0, 0)) + popup_x = btn_pos.x() + popup_y = btn_pos.y() - self.warning_msg_widget.height() + self.warning_msg_widget.move(popup_x, popup_y) + self.warning_msg_widget.show() + else: + self.warning_msg_widget.close() \ No newline at end of file diff --git a/controller/main_controller.py b/controller/main_controller.py index 0b771fa..c74e8aa 100644 --- a/controller/main_controller.py +++ b/controller/main_controller.py @@ -7,15 +7,23 @@ from .camera_controller import CameraController from .bottom_control_controller import BottomControlController from .hopper_controller import HopperController +from service.msg_recorder import MessageRecorder + class MainController: def __init__(self): # 主界面 self.main_window = MainWindow() + + self.msg_recorder = MessageRecorder() + self.msg_recorder.normal_record("开始自动智能浇筑系统") # 初始化子界面和控制器 self._initSubViews() self._initSubControllers() + # 连接信号 + self.__connectSignals() + def showMainWindow(self): self.main_window.showFullScreen() # self.main_window.show() @@ -44,4 +52,15 @@ class MainController: def _initSubViews(self): - pass \ No newline at end of file + pass + + + def __connectSignals(self): + self.main_window.about_to_close.connect(self.handleMainWindowClose) # 处理主界面关闭 + + def handleMainWindowClose(self): + """主界面关闭""" + self.msg_recorder.normal_record("关闭自动智能浇筑系统") + # 停止系统底部控制器中的线程 + if hasattr(self, 'bottom_control_controller'): + self.bottom_control_controller.stop_threads() \ No newline at end of file diff --git a/db/three.db b/db/three.db index 79d639ee09739771356cd9e1f8a069d8ff490d8a..668148240201c0f07f1c48ba95aebe887257168b 100644 GIT binary patch delta 382 zcmZozz}&Ead4jYcI|Bm)FA%c;F$WM!Ow=(JXJ^o}{>aOIkb#AziGhC|&rNPMmL`^_ zjg34kn-9*hX9g-}PTkm;$h_INZ!_a&-;S$HlYjRHZ5A|m&p&a3pad_!IRg{FI0L^b zzc@17*!Z7svba94IEw%~vnr!wQAuW6Vsc4HVsSPr(C(8H8_&ps9K-~~AR6Q#P9Ww2 zVs0Sj0orhff02X2W(9)>{5Xk;6AlRQ@_I0^@boe8^zkw9M)T)wY^>wi+;_%_k&$`w z#j`J&7`9Dp+?3SF&nn5F&L+zwtu81pDafwO!NJH0wAENxSlB&E!O76rz{1eT(!`m8 zX|A1!UxTZ=fq{{^xpNewXs}0=lcBB&NR!L#ifpU&@g aGMv2ktb%}vm5HI1q2aR`>z__o21Ed;+G5uL delta 1784 zcmZozz}&Ead4jYc8v_FaFA&3k&_o?$VKxRm>yNBJAx8e+APFx1-caApg6zs144j-mHItW}iJz= zq{JH;7?~Ox7#qZ!8Y!^iS7BnLz=B_ev5^8ZvnykKEN*Lzm=u^mN^q+%WRhiMcI3dV zVo`v=A_oO7QiF|T4Pqc0*ch)c@c!mj;_&73WtC>U!gyw5<37gvHWzkwaaUKy9`2IF zq?}aTj*U0MC&%s__f`XE#i;{xp=8G&l8J%>%0>LGT zC8fncLB9}Jch?}OV18*)aw?jjn^S5@eqLC9PH8Ss3TU&>)Z6%z<%jYB;*%E zJPtHuBHAy8wDL>Icn&fx*w{Fcsh$Iv zXc^j)xFE^Xosfr3@kFU953!z(H-&k7^BER7CP=c1;Dorrn2;Mx@VLQ*n`k$fz}x`K zBHtM$Ah`io!uR8VxKN&u3ytx((3pd07aF6x@CKtUsrmIh69dQQIu>VUlFKuaHSqEB odN45a^fB=C@iFj51FM$?p3Qw{oERCICSN@Jl8Mn{W1|Zr08+)|vj6}9 diff --git a/images/消息列表背景.png b/images/消息列表背景.png new file mode 100644 index 0000000000000000000000000000000000000000..958bf7800ee22132f54e4d4c438fb5fa695bfae2 GIT binary patch literal 6260 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 literal 0 HcmV?d00001 diff --git a/service/msg_query_thread.py b/service/msg_query_thread.py new file mode 100644 index 0000000..a3b3270 --- /dev/null +++ b/service/msg_query_thread.py @@ -0,0 +1,52 @@ +from PySide6.QtCore import QThread, Signal +from .msg_recorder import MessageRecorder + +""" + 定时查询消息数据库, 并发送最新的消息数据 +""" + +class MsgQueryThread(QThread): + # 定义信号:发送消息类型(1=系统,2=预警)和 消息信息列表 + new_messages = Signal(int, list) # (message_type, [(content, is_processed, create_time, last_modified), ...]) + + def __init__(self, interval=1000, query_limit = 16): + super().__init__() + self.interval = interval # 查询间隔(毫秒) + self.query_limit = query_limit # 单次查询的项目条数,如每次查询16条消息 + self.running = True # 线程运行标志 + self.recorder = MessageRecorder() # 消息管理器 + + # 记录上次查询到的“最新last_modified时间”(按类型区分) + self.last_normal_modified = "" + self.last_warning_modified = "" + + def run(self): + """线程主逻辑: 每隔interval查询一次消息""" + while self.running: + # 1. 处理系统消息(类型1) + normal_msgs = self.recorder.get_latest_normal_messages(self.query_limit) + if normal_msgs: + # 找到这16条消息中最新的last_modified时间(即最大的) + latest_modified = max([msg[3] for msg in normal_msgs]) # msg[3]是last_modified + # 比较是否有更新(新增或状态变化) + # 注意:是根据last_modified来决定是否进行界面更新的 + if latest_modified != self.last_normal_modified: + self.last_normal_modified = latest_modified + self.new_messages.emit(1, normal_msgs) # 发送信号更新UI,1表示系统消息 + + # 2. 处理预警消息(类型2) + warning_msgs = self.recorder.get_latest_warning_messages(self.query_limit) + if warning_msgs: + latest_modified = max([msg[3] for msg in warning_msgs]) + if latest_modified != self.last_warning_modified: + self.last_warning_modified = latest_modified + self.new_messages.emit(2, warning_msgs) + + self.msleep(self.interval) + + def stop(self): + """停止消息查询线程""" + self.running = False + self.wait(500) + if self.isRunning(): + self.terminate() \ No newline at end of file diff --git a/service/msg_recorder.py b/service/msg_recorder.py new file mode 100644 index 0000000..f113209 --- /dev/null +++ b/service/msg_recorder.py @@ -0,0 +1,44 @@ +from common.msg_db_helper import DBHelper +from PySide6.QtCore import QMutex + +""" + 消息管理器: 系统状态消息 和 预警消息 +""" + +class MessageRecorder: + _instance = None # 单例模式 + _mutex = QMutex() + + def __new__(cls): + cls._mutex.lock() + try: + if not cls._instance: + cls._instance = super().__new__(cls) + cls._instance.db = DBHelper() + finally: + cls._mutex.unlock() + return cls._instance + + def normal_record(self, content, is_processed=0): + """记录系统状态消息""" + self.db.insert_message(content, message_type=1, is_processed=is_processed) + + def warning_record(self, content, is_processed=0): + """记录预警消息""" + self.db.insert_message(content, message_type=2, is_processed=is_processed) + + def update_warning_status(self, content, is_processed=1): + """更新预警消息的处理状态: is_processed为1表示已经处理, 字体变为灰色""" + self.db.update_processed_status(content, message_type=2, is_processed=is_processed) + + def update_normal_status(self, content, is_processed=1): + """更新系统消息的处理状态: is_processed为1表示已经处理, 字体变为灰色""" + self.db.update_processed_status(content, message_type=1, is_processed=is_processed) + + def get_latest_normal_messages(self, limit=16): + """获取最新的系统状态消息(message_type=1), limit为获取的消息的条数, 默认16条""" + return self.db.get_latest_messages(message_type=1, limit=limit) + + def get_latest_warning_messages(self, limit=16): + """获取最新的预警消息(message_type=2), limit为获取的消息的条数, 默认16条""" + return self.db.get_latest_messages(message_type=2, limit=limit) \ No newline at end of file diff --git a/utils/image_paths.py b/utils/image_paths.py index 32fc454..7f94f64 100644 --- a/utils/image_paths.py +++ b/utils/image_paths.py @@ -114,4 +114,9 @@ class ImagePaths: DESPATCH_DETAILS_TITLE_BG = "images/详情标题.png" DESPATCH_DETAILS_INFO_BAR_NORMAL = "images/派单任务信息栏1.png" DESPATCH_DETAILS_INFO_BAR_HOVER = "images/派单任务信息栏2.png" - DESPATCH_DETAILS_CLOSE_ICON = "images/关闭图标.png" \ No newline at end of file + DESPATCH_DETAILS_CLOSE_ICON = "images/关闭图标.png" + + # 功能:消息列表(系统状态消息、预警消息) + MESSAGE_LIST_POPUP_BG = "images/消息列表背景.png" + SYSTEM_MSG_HORN_ICON = "images/系统消息喇叭.png" + SYSTEM_MSG_ITEM_BG = "images/系统消息背景.png" \ No newline at end of file diff --git a/view/main_window.py b/view/main_window.py index 2e6934b..2bf54d9 100644 --- a/view/main_window.py +++ b/view/main_window.py @@ -26,6 +26,9 @@ from .widgets.dispatch_details_dialog import DispatchDetailsDialog class MainWindow(QWidget): + # 定义“即将关闭”的信号 + about_to_close = Signal() + def __init__(self): super().__init__() self.initWindow() @@ -329,6 +332,7 @@ class MainWindow(QWidget): def closeEvent(self, e): """窗口关闭时的回调""" + self.about_to_close.emit() super().closeEvent(e) def keyPressEvent(self, event): diff --git a/view/widgets/message_popup_widget.py b/view/widgets/message_popup_widget.py new file mode 100644 index 0000000..f195721 --- /dev/null +++ b/view/widgets/message_popup_widget.py @@ -0,0 +1,277 @@ +from PySide6.QtWidgets import ( + QWidget, + QVBoxLayout, + QListWidget, + QListWidgetItem, + QLabel, + QHBoxLayout, + QDialog +) +from PySide6.QtCore import Qt, QRect, QSize +from PySide6.QtGui import QFont, QColor, QPixmap, QPainter +from utils.image_paths import ImagePaths + +""" + 系统状态消息按钮 和 预警消息按钮点击之后出现的消息列表, 显示系统状态消息 和 预警消息 +""" + +# 自定义消息项组件 +class MessageItemWidget(QWidget): + def __init__(self, content, is_processed, msg_time:str = "", parent=None): + super().__init__(parent) + self.content = content + self.is_processed = is_processed + self.msg_time = msg_time # 对应产生该消息的时间,如: 2025-11-9 19:09:09 + self.bg_pixmap = QPixmap(ImagePaths.SYSTEM_MSG_ITEM_BG) # 消息项背景图 + self.icon_pixmap = QPixmap(ImagePaths.SYSTEM_MSG_HORN_ICON) # 左侧喇叭图标 + self.setStyleSheet("background: none; border: none;") + self.setFixedWidth(310) # 内部显示的消息条目的宽度,控制每条消息的宽度 + self.init_ui() + + def init_ui(self): + # 布局:仅显示消息文本 + layout = QHBoxLayout(self) + + layout.setContentsMargins(6, 6, 6, 6) + layout.setSpacing(5) # 调整图标与文本的间距 + + # 1. 左侧喇叭图标 + self.icon_label = QLabel() + if not self.icon_pixmap.isNull(): + # 设置图标大小(例如20x20,可根据需求调整) + self.icon_label.setFixedSize(13, 21) + # 图片自适应label大小,保持比例 + self.icon_label.setScaledContents(True) + self.icon_label.setPixmap(self.icon_pixmap) + # 图标背景透明 + self.icon_label.setStyleSheet("background: transparent;padding-top:3px;") + layout.addWidget(self.icon_label, alignment=Qt.AlignTop) + + # 2. 右侧消息时间 + # if self.msg_time: + # self.time_label = QLabel(self.msg_time) + # self.time_label.setFont(QFont("微软雅黑", 8)) # 消息时间的字体 + # self.time_label.setStyleSheet("color: #03f5ff;") + # self.time_label.setFixedWidth(106) # 显示时间的标签的宽度,需要根据上面的消息时间字体来调整 + # self.time_label.setFixedHeight(19) + # layout.addWidget(self.time_label, alignment=Qt.AlignTop) + + # # 3. 右侧消息内容文本 + # self.label = QLabel(self.content) + # self.label.setFont(QFont("微软雅黑", 12)) # 消息内容中的字体 + # self.label.setWordWrap(True) + + # # 已处理消息文字变灰 + # if self.is_processed: + # self.label.setStyleSheet("color: gray;") + # else: + # self.label.setStyleSheet("color: white;") + # layout.addWidget(self.label) + + # 2. 右侧:时间 + 内容的垂直布局 + right_layout = QVBoxLayout() + right_layout.setContentsMargins(0, 0, 0, 0) + right_layout.setSpacing(0) + + # 时间标签(可选:若有时间则显示) + if self.msg_time: + self.time_label = QLabel(self.msg_time) + self.time_label.setFont(QFont("微软雅黑", 8)) + self.time_label.setStyleSheet("color: #03f5ff; margin: 0px; padding: 0px;") + right_layout.addWidget(self.time_label, alignment=Qt.AlignTop) + + # 内容标签 + self.label = QLabel(self.content) + # font = QFont("微软雅黑", 11) + # font.setStyleStrategy(QFont.PreferAntialias | QFont.PreferDefault) + self.label.setFont(QFont("微软雅黑", 11)) + self.label.setWordWrap(True) + if self.is_processed: + self.label.setStyleSheet("color: gray; margin: 0px; padding: 0px;") + else: + self.label.setStyleSheet("color: white; margin: 0px; padding: 0px;") + right_layout.addWidget(self.label, alignment=Qt.AlignTop) + + # 将右侧垂直布局加入主水平布局 + layout.addLayout(right_layout) + + def paintEvent(self, event): + # 绘制消息项背景图(自适应组件大小) + if not self.bg_pixmap.isNull(): + painter = QPainter(self) + scaled_pixmap = self.bg_pixmap.scaled( + self.size(), + Qt.KeepAspectRatioByExpanding, # Qt.KeepAspectRatioByExpanding按比例缩放填满 + Qt.SmoothTransformation, # 平滑缩放 + ) + painter.drawPixmap(self.rect(), scaled_pixmap) + super().paintEvent(event) + + +# 消息列表弹窗 (整个显示消息的区域) +class MessagePopupWidget(QWidget): + def __init__(self, message_type, parent=None): + super().__init__(parent) + self.message_type = message_type + self.bg_pixmap = QPixmap(ImagePaths.MESSAGE_LIST_POPUP_BG) # 列表背景图 + # 弹窗样式:无框 + 置顶,宽度固定为339(与按钮一致) + self.setWindowFlags(Qt.FramelessWindowHint) + self.hide() # 初始状态为隐藏 (必须) + self.setFixedWidth(339) # 宽度与下方的按钮一致 + # self.setMinimumHeight(181) # 整个消息列表的高度,需要根据实际情况调整 + # self.setMinimumHeight(211) # 整个消息列表的高度,需要根据实际情况调整 + # self.setFixedHeight(260) # 整个消息列表的高度,需要根据实际情况调整 + # self.setFixedHeight(290) # 整个消息列表的高度,需要根据实际情况调整 + self.setFixedHeight(299) # 整个消息列表弹窗的高度,需要根据实际情况调整 (一页能够有六行消息[单行]) + + # 布局:标题 + 消息列表 + layout = QVBoxLayout(self) + layout.setContentsMargins(10, 10, 10, 20) + layout.setSpacing(3) # 消息项间距 + + # 标题 + self.title_label = QLabel(self.message_type) + self.title_label.setFont(QFont("微软雅黑", 14, QFont.Bold)) + self.title_label.setStyleSheet( + "background: none; color: #03f5ff; text-align: center;" + ) + self.title_label.setAlignment(Qt.AlignCenter) + layout.addWidget(self.title_label) + + # 消息列表(支持选中) + self.list_widget = QListWidget() + # 禁用列表选择 + self.list_widget.setSelectionMode(QListWidget.SelectionMode.NoSelection) + # 禁用水平滚动条 + self.list_widget.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + self.list_widget.setStyleSheet( + """ + QListWidget { background-color: transparent; border: none; } + QListWidget::item { height: 30px; } + + QListWidget QScrollBar:vertical { + background: transparent; /* 轨道背景色 */ + width: 8px; + margin: 0; + border-radius: 4px; + } + + /* 滚动条滑块 */ + QListWidget QScrollBar::handle:vertical { + background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #039ec3, stop:1 #03f5ff); + border-radius: 4px; + min-height: 20px; + } + + /* 滑块悬停效果 */ + QListWidget QScrollBar::handle:vertical:hover { + background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #16ffff, stop:1 #00347e); + } + + /* 隐藏上下箭头按钮 */ + QListWidget QScrollBar::add-line:vertical, QListWidget QScrollBar::sub-line:vertical { + height: 0; + } + + /* 滑块上下的空白区域(透明) */ + QListWidget QScrollBar::add-page:vertical, QListWidget QScrollBar::sub-page:vertical { + background: transparent; + } + """ + ) + layout.addWidget(self.list_widget) + + def paintEvent(self, event): + # 这里需要根据不变的图片部分来决定 + self.top = 32 # 上分割线距离顶部 + self.right = 32 # 右分割线距离右侧 + self.bottom = 32 # 下分割线距离底部 + self.left = 32 # 左分割线距离左侧 + + # 按九宫格来加载图片 + painter = QPainter(self) + w, h = self.width(), self.height() # 控件当前大小 + img_w, img_h = self.bg_pixmap.width(), self.bg_pixmap.height() # 原图大小 + + # 1. 绘制四个角(不拉伸) + # 左上 + painter.drawPixmap( + 0, 0, self.left, self.top, + self.bg_pixmap, 0, 0, self.left, self.top + ) + # 右上 + painter.drawPixmap( + w - self.right, 0, self.right, self.top, + self.bg_pixmap, img_w - self.right, 0, self.right, self.top + ) + # 左下 + painter.drawPixmap( + 0, h - self.bottom, self.left, self.bottom, + self.bg_pixmap, 0, img_h - self.bottom, self.left, self.bottom + ) + # 右下 + painter.drawPixmap( + w - self.right, h - self.bottom, self.right, self.bottom, + self.bg_pixmap, img_w - self.right, img_h - self.bottom, self.right, self.bottom + ) + + # 2. 绘制四条边(拉伸中间部分) + # 上边(水平拉伸) + painter.drawPixmap( + self.left, 0, w - self.left - self.right, self.top, + self.bg_pixmap, self.left, 0, img_w - self.left - self.right, self.top + ) + # 下边(水平拉伸) + painter.drawPixmap( + self.left, h - self.bottom, w - self.left - self.right, self.bottom, + self.bg_pixmap, self.left, img_h - self.bottom, img_w - self.left - self.right, self.bottom + ) + # 左边(垂直拉伸) + painter.drawPixmap( + 0, self.top, self.left, h - self.top - self.bottom, + self.bg_pixmap, 0, self.top, self.left, img_h - self.top - self.bottom + ) + # 右边(垂直拉伸) + painter.drawPixmap( + w - self.right, self.top, self.right, h - self.top - self.bottom, + self.bg_pixmap, img_w - self.right, self.top, self.right, img_h - self.top - self.bottom + ) + + # 3. 绘制中心区域(自由拉伸) + painter.drawPixmap( + self.left, self.top, w - self.left - self.right, h - self.top - self.bottom, + self.bg_pixmap, self.left, self.top, img_w - self.left - self.right, img_h - self.top - self.bottom + ) + + super().paintEvent(event) + + # 显示设置 + def showEvent(self, event): + super().showEvent(event) + # 弹窗显示时,强制滚动到底部,以便显示最新的消息 + self.list_widget.scrollToBottom() + + """ + 对外接口: 添加消息到消息列表 + is_processed状态为True表示 已经处理/已经解决, 此时显示的字体为灰色 + """ + def add_message(self, content, is_processed:bool = False, msg_time:str = ""): + """添加带背景的消息项 + Args: + content: 消息的内容 + is_processed: 消息的状态 (是否已经解决, 已经解决,字体变为灰色) + msg_time: 该消息对应的时间 (如: 产生消息的时间等), 会显示在消息内容之前,如: 2025-11-12 消息内容 + """ + # 1、创建自定义消息项组件 + item_widget = MessageItemWidget(content, is_processed, msg_time) + # 2、创建列表项并关联组件 + item = QListWidgetItem() + # item.setSizeHint(item_widget.sizeHint()) # 自适应组件大小 + # item.setSizeHint(QSize(item_widget.sizeHint().width(), 30)) # 固定行高,设置为30px (不行,必须根据item_widget的高度决定) + + # 行高减去6px, 刚好可以显示出4行, 对应了消息的字体大小为11px的情况 + item.setSizeHint(QSize(item_widget.sizeHint().width(), item_widget.sizeHint().height() - 6)) + self.list_widget.addItem(item) + self.list_widget.setItemWidget(item, item_widget) + # 3、添加新消息后,滚动到列表底部,以便显示最新的消息 + self.list_widget.scrollToBottom() diff --git a/view/widgets/system_center_dialog.py b/view/widgets/system_center_dialog.py index be444c9..1edee1f 100644 --- a/view/widgets/system_center_dialog.py +++ b/view/widgets/system_center_dialog.py @@ -68,6 +68,7 @@ class SystemCenterDialog(QDialog): self.setWindowTitle("系统中心") self.setWindowFlags(Qt.FramelessWindowHint) # 隐藏默认边框 self.setWindowOpacity(0.0) # 初始状态为完全透明,实现动画效果 + self.hide() # 初始状态为隐藏 (必须) # 加载背景图 self.background = QPixmap(ImagePaths.SYSTEM_CENTER_POPUP_BG) diff --git a/view/widgets/system_diagnostics_dialog.py b/view/widgets/system_diagnostics_dialog.py index 7295384..a210f27 100644 --- a/view/widgets/system_diagnostics_dialog.py +++ b/view/widgets/system_diagnostics_dialog.py @@ -186,6 +186,7 @@ class SystemDiagnosticsDialog(QDialog): def _init_ui(self): # 无边框模式 self.setWindowFlags(Qt.FramelessWindowHint) + self.hide() # 初始状态为隐藏 (必须) # 加载系统诊断弹窗的背景图片 self.bg_pixmap = QPixmap(ImagePaths.SYSTEM_DIAGNOSTICS_POPUP_BG) diff --git a/view/widgets/task_widget.py b/view/widgets/task_widget.py index d0ba1d0..161ca04 100644 --- a/view/widgets/task_widget.py +++ b/view/widgets/task_widget.py @@ -171,9 +171,13 @@ class TaskWidget(QWidget): # -------------------------- def set_task_volume(self, task_name:str, volume: float): """修改指定任务的方量, 传入具体的方量值,如: 200.0""" + # 褚工说 管片任务 和 派单任务的方量只有一位小数。 2025-11-8 + # 所以这里限制为 保留一位小数的浮点数 + current_volume = round(float(volume), 1) + if task_name in self.task_controls: volume_label = self.task_controls[task_name]["volume_label"] - volume_label.setText(f"方量 {volume}") + volume_label.setText(f"方量 {current_volume}") def set_task_time(self, task_name:str, time_str: str): """修改指定任务的时间, 传入对应格式的时间,如: 03:22PM"""