Compare commits

..

No commits in common. "c8794b8d472a850b2819d5024d604bb4b1371e3f" and "63d36548aa0bbf2e27bedd1beac4a6ac3a10186b" have entirely different histories.

9 changed files with 393 additions and 522 deletions

@ -41,6 +41,64 @@ def is_alertjson_file_empty():
except Exception as e: except Exception as e:
return None return None
# def parse_log_line(log_line):
# """
# 解析特定格式的日志行,提取关键信息
# 参数:
# log_line (str): 要解析的日志行字符串
# 返回:
# dict: 包含提取的信息的字典若解析失败则返回None
# """
# try:
# # 处理开头的 <162> 部分,先移除这部分内容
# # 找到第一个空格,跳过 <162> 部分
# first_space_index = log_line.find(' ')
# if first_space_index == -1:
# return None
# content_after_prefix = log_line[first_space_index:].strip()
# # 从剩余内容中提取第一个时间戳(到下一个空格)
# timestamp_end = content_after_prefix.find(' ')
# if timestamp_end == -1:
# return None
# timestamp = content_after_prefix[:timestamp_end].strip()
# # 查找包含|分隔符的部分
# pipe_start = log_line.find('|')
# if pipe_start == -1:
# return None
# pipe_content = log_line[pipe_start:]
# # 按|分割内容
# parts = [part.strip() for part in pipe_content.split('|') if part.strip()]
# # 检查是否有足够的部分
# if len(parts) < 5:
# return None
# # 提取各部分信息
# component_type = parts[1] # 部件类型
# event_type = parts[2] # 事件类型
# event_level = parts[3] # 事件等级
# event_code = parts[4] # 事件代码
# # 事件描述是剩余部分的组合
# event_description = '|'.join(parts[5:]) if len(parts) > 5 else ""
# return {
# 'timestamp': timestamp,
# 'component_type': component_type,
# 'event_type': event_type,
# 'event_level': event_level,
# 'event_code': event_code,
# 'description': event_description
# }
# except Exception as e:
# print(f"解析日志行时出错: {str(e)}")
# return None
def process_log_with_keywords(file_path, keywords, json_output_path=cache_sys_error): def process_log_with_keywords(file_path, keywords, json_output_path=cache_sys_error):
""" """
结合关键字匹配日志解析并将结果保存为JSON文件的完整处理函数 结合关键字匹配日志解析并将结果保存为JSON文件的完整处理函数
@ -62,7 +120,7 @@ def process_log_with_keywords(file_path, keywords, json_output_path=cache_sys_er
for line_number, line in enumerate(file, 1): for line_number, line in enumerate(file, 1):
line_lower = line.lower() line_lower = line.lower()
if any(keyword in line_lower for keyword in lower_keywords): if any(keyword in line_lower for keyword in lower_keywords):
# print(f"\n第{line_number}行: {line.strip()}") print(f"\n{line_number}行: {line.strip()}")
match_count += 1 match_count += 1
# 解析日志行 # 解析日志行
@ -71,9 +129,9 @@ def process_log_with_keywords(file_path, keywords, json_output_path=cache_sys_er
# 添加行号信息,方便追溯 # 添加行号信息,方便追溯
parsed['line_number'] = line_number parsed['line_number'] = line_number
parsed_results.append(parsed) parsed_results.append(parsed)
# print("提取的信息:") print("提取的信息:")
# for key, value in parsed.items(): for key, value in parsed.items():
# print(f" {key}: {value}") print(f" {key}: {value}")
else: else:
print(" 无法解析此行的格式") print(" 无法解析此行的格式")

@ -1,88 +0,0 @@
import os
from PyQt5.QtWidgets import QFileDialog
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QDragEnterEvent, QDropEvent
class EventHandler:
def __init__(self, main_window):
self.main_window = main_window # 持有主窗口引用
self.file_processor = main_window.file_processor # 关联文件处理模块
self.view_renderer = main_window.view_renderer # 关联视图渲染模块
def bind_events(self):
"""绑定所有界面事件"""
# 菜单项事件
self.main_window.actionUpload_log.triggered.connect(self.upload_file)
# 下拉框变化事件
self.main_window.comboBox.currentTextChanged.connect(self.on_combo_changed)
# 图形视图滚轮缩放事件
self.main_window.graphicsView.wheelEvent = self.zoom_with_mouse_wheel
# 窗口 resize 事件
self.main_window.resizeEvent = self.resize_event
def upload_file(self):
"""打开文件选择对话框"""
file_path, _ = QFileDialog.getOpenFileName(
self.main_window,
"选择文件",
os.getcwd(),
"所有文件 (*);;日志压缩文件 (*.gz)"
)
if file_path:
self.file_processor.process_uploaded_file(file_path)
def on_combo_changed(self, selected_text):
"""下拉框选项变化:切换图片"""
# 获取选中项对应的路径关键词
key = self.main_window.combo_box_text_dict.get(selected_text)
if key:
self.view_renderer.display_pic(self.main_window.file_processor.get_sensorhistory_path(key))
def dragEnterEvent(self, event: QDragEnterEvent):
"""拖入事件:判断是否为文件"""
if event.mimeData().hasUrls():
event.acceptProposedAction()
else:
event.ignore()
def dropEvent(self, event: QDropEvent):
"""放下事件:处理拖放的文件"""
file_paths = [url.toLocalFile() for url in event.mimeData().urls()]
for file_path in file_paths:
self.file_processor.process_uploaded_file(file_path)
def zoom_with_mouse_wheel(self, event):
"""鼠标滚轮缩放图片"""
if self.main_window.pixmap_item:
# 缩放中心:鼠标当前位置
mouse_pos = event.pos()
scene_pos = self.main_window.graphicsView.mapToScene(mouse_pos)
# 缩放因子
scale_factor = 1.1 if event.angleDelta().y() > 0 else 0.9
# 执行缩放
self.main_window.graphicsView.scale(scale_factor, scale_factor)
# 保持缩放后鼠标位置不变
new_mouse_pos = self.main_window.graphicsView.mapFromScene(scene_pos)
delta = new_mouse_pos - mouse_pos
self.main_window.graphicsView.horizontalScrollBar().setValue(
self.main_window.graphicsView.horizontalScrollBar().value() - delta.x()
)
self.main_window.graphicsView.verticalScrollBar().setValue(
self.main_window.graphicsView.verticalScrollBar().value() - delta.y()
)
else:
# 无图片时调用父类默认行为
super(type(self.main_window.graphicsView), self.main_window.graphicsView).wheelEvent(event)
def resize_event(self, event):
"""窗口 resize 时自适应图片"""
if self.main_window.pixmap_item and not self.main_window.graphicsView.transform().isIdentity():
self.main_window.graphicsView.fitInView(self.main_window.scene.sceneRect(), Qt.KeepAspectRatio)
# 调用父类默认 resize 行为
super(type(self.main_window), self.main_window).resizeEvent(event)
# 绑定拖放事件到主窗口
def patch_drag_events(self):
"""将拖放事件绑定到主窗口(需在主窗口初始化时调用)"""
self.main_window.dragEnterEvent = self.dragEnterEvent
self.main_window.dropEvent = self.dropEvent

@ -1,74 +0,0 @@
import os
import service
from utils import show_error_message, show_critical_message
class FileProcessor:
def __init__(self, main_window):
self.main_window = main_window # 持有主窗口引用
self.parse_status = main_window.parse_status # 服务状态
self.view_renderer = main_window.view_renderer # 关联视图渲染模块
def is_valid_file_format(self, file_path):
"""校验文件是否为 .tar.gz 格式"""
_, ext = os.path.splitext(file_path)
if ext.lower() == '.gz':
base, ext2 = os.path.splitext(os.path.splitext(file_path)[0])
return ext2.lower() == '.tar'
return False
def get_sensorhistory_path(self, key):
"""调用服务层获取传感器历史图片路径"""
return service.get_sensorhistory_path(key)
def process_uploaded_file(self, file_path):
"""核心:处理上传的文件(校验→解析→传递结果)"""
# 1. 格式校验
if not self.is_valid_file_format(file_path):
show_error_message(self.main_window, file_path)
return
try:
# 2. 提取文件信息并显示到控制台
file_name = os.path.basename(file_path)
file_size = os.path.getsize(file_path) / 1024 # 转换为 KB
info = (
f"已上传文件:\n"
f"文件名:{file_name}\n"
f"路径:{file_path}\n"
f"大小:{file_size:.2f} KB\n"
f"------------------------------------------------------"+ "\n"
)
self.main_window.textBrowser_console.insertPlainText(info)
# 3. 调用服务层解析文件
service.send_log_to_cache(file_path)
service.start_diagnose(self.parse_status)
# 4. 根据解析状态更新视图
self._update_view_after_parse()
self.main_window.textBrowser_console.insertPlainText("完成文件解析\n")
except Exception as e:
show_critical_message(self.main_window, "处理失败", f"文件处理出错:{str(e)}")
def _update_view_after_parse(self):
"""解析完成后更新各视图组件"""
# 更新基础信息文本框
if self.parse_status.baseinfo_status:
baseinfo_str = service.get_baseinfo_str()
self.main_window.textBrowser_info.insertPlainText(baseinfo_str)
# 更新传感器图片
if self.parse_status.sensorhistory_status:
self.view_renderer.display_pic(self.get_sensorhistory_path("all"))
# 更新告警表格
if self.parse_status.parseidl_status and not service.is_idl_alert_empty():
alert_json = service.get_idl_alert_json()
self.view_renderer.fill_tableView_alert(alert_json)
# 更新时间线
if self.parse_status.eventline_status:
event_json = service.get_timeline_event_json()
self.main_window.timeline_content.load_events_from_json(event_json)

@ -1,14 +1,324 @@
import sys import sys
import os
from PyQt5.QtWidgets import (QApplication, QMainWindow, QFileDialog, QTextBrowser, QVBoxLayout,
QMessageBox, QTextEdit, QGraphicsScene, QGraphicsPixmapItem)
from PyQt5.QtCore import Qt, QMimeData
from PyQt5.QtGui import QPixmap
from PyQt5.QtGui import QPainter # 单独导入QPainter用于抗锯齿设置
from PyQt5.QtGui import QStandardItemModel, QStandardItem, QDragEnterEvent, QDropEvent
from MainWindow_ui import Ui_MainWindow # 导入转换后的UI类
import service import service
from PyQt5.QtWidgets import QApplication import timelineEvent
from main_window import MainWindow # 导入重构后的主窗口类
class MainWindow(QMainWindow, Ui_MainWindow):
def __init__(self):
super().__init__() # 初始化父类
self.setupUi(self) # 调用UI类的方法初始化界面
self.init_events() # 绑定事件(自定义方法)
self.parseStatus = service.ServiceStatus()
self.init_graphic_views()
self.init_textbrowser_style()
self.init_tableView_alert_model()
self.initUI()
self.reInitTimeLineTab()
self.comboBoxTextDict = {
"ALL_Temp": "all",
"CPU_Temp": "cpu",
"FPGA_Temp": "fpga",
"DIMM_Temp": "dimm",
"Inlet_CPU_Temp": "inlet_cpu",
"Inlet_FPGA_Temp": "inlet_fpga",
"M2_Temp": "m2",
"Outlet_CPU_Temp": "outlet_cpu",
"Outlet_FPGA_Temp": "outlet_fpga",
"VR_CPU_Temp": "vr_cpu",
"VR_FPGA_Temp": "vr_fpga"
}
def initUI(self):
# 允许窗口接收拖放事件
self.setAcceptDrops(True)
def reInitTimeLineTab(self):
# 1. 清除目标标签页的现有布局
if hasattr(self.tab_timeline_event, 'layout'):
# 移除现有布局中的所有组件
layout = self.tab_timeline_event.layout()
if layout:
while layout.count():
item = layout.takeAt(0)
widget = item.widget()
if widget:
widget.deleteLater()
# 2. 创建时间线内容组件实例
self.timeline_content = timelineEvent.TimelineTabContent()
# 3. 为标签页创建新布局并添加时间线组件
layout = QVBoxLayout(self.tab_timeline_event)
layout.setContentsMargins(0, 0, 0, 0) # 可选:去除边距
layout.addWidget(self.timeline_content)
def dragEnterEvent(self, event: QDragEnterEvent):
"""拖入事件:判断拖入的是否是文件"""
if event.mimeData().hasUrls():
# 只接受文件拖入
event.acceptProposedAction()
else:
event.ignore()
def dropEvent(self, event: QDropEvent):
"""放下事件:处理拖放的文件"""
# 获取拖放的文件路径列表
file_paths = [url.toLocalFile() for url in event.mimeData().urls()]
# 调用你的文件上传函数
for file_path in file_paths:
self.process_uploaded_file(file_path) # 这是你已经写好的上传函数
def init_textbrowser_style(self):
# 设置样式表(全局文本样式)
self.textBrowser_info.setStyleSheet("""
QTextBrowser {
font-family: 'SimHei';
font-size: 24px;
color: #2c3e50;
background-color: #f8f9fa;
border: 1px solid #ddd;
border-radius: 4px;
padding: 8px;
}
""")
def init_events(self):
"""绑定界面元素的事件(如按钮点击、输入框变化等)"""
# 示例:给按钮添加点击事件
# self.pushButton.clicked.connect(self.on_button_click) # pushButton是UI中定义的按钮对象名
# 绑定上传菜单项事件
self.actionUpload_log.triggered.connect(self.upload_file)
self.comboBox.currentTextChanged.connect(self.on_combo_changed)
def init_graphic_views(self):
# 初始化图形场景和视图
self.scene = QGraphicsScene(self) # 创建场景
self.graphicsView.setScene(self.scene) # 将场景绑定到视图
self.graphicsView.setRenderHint(QPainter.Antialiasing) # 抗锯齿
self.graphicsView.setRenderHint(QPainter.SmoothPixmapTransform) # 平滑缩放
# 存储当前显示的图片项
self.pixmap_item = None
self.current_image_path = None
def init_tableView_alert_model(self):
# 创建一个4行3列的模型
self.model_alert = QStandardItemModel(4, 5)
# 设置表头
self.model_alert.setHorizontalHeaderLabels(["时间", "部件", "等级", "方向", "描述"])
# 将模型绑定到QTableView
self.tableView_alert.setModel(self.model_alert)
# 可选:调整列宽自适应内容
self.tableView_alert.horizontalHeader().setSectionResizeMode(
self.tableView_alert.horizontalHeader().ResizeToContents
)
def show_error_message(self, file_path):
"""显示格式错误提示弹窗"""
QMessageBox.critical(
self,
"文件格式错误",
f"不支持的文件格式:\n{os.path.basename(file_path)}\n\n请上传.tar.gz格式的压缩包。"
)
def is_valid_file_format(self, file_path):
"""检查文件是否为.tar.gz格式"""
# 获取文件后缀
_, ext = os.path.splitext(file_path)
# 先检查是否有.gz后缀
if ext.lower() == '.gz':
# 再检查去掉.gz后的后缀是否为.tar
base, ext2 = os.path.splitext(os.path.splitext(file_path)[0])
return ext2.lower() == '.tar'
return False
def upload_file(self):
"""打开文件选择对话框并处理选中的文件"""
# 打开文件选择对话框
# 参数说明:
# - self父窗口
# - "选择文件":对话框标题
# - os.getcwd():默认打开路径(当前工作目录)
# - "所有文件 (*);;文本文件 (*.txt);;图片文件 (*.png *.jpg)":文件筛选器
file_path, file_type = QFileDialog.getOpenFileName(
self,
"选择文件",
os.getcwd(),
"所有文件 (*);;日志压缩文件 (*.gz)"
)
# 判断用户是否选择了文件取消选择时file_path为空
if file_path:
self.process_uploaded_file(file_path)
# else:
# self.text_edit.append("已取消文件选择")
def process_uploaded_file(self, file_path):
"""处理上传的文件"""
if not self.is_valid_file_format(file_path):
self.show_error_message(file_path)
return
try:
# 获取文件信息
file_name = os.path.basename(file_path)
file_size = os.path.getsize(file_path)
file_size_kb = file_size / 1024 # 转换为KB
# 显示文件信息
info = f"已上传文件:\n"
info += f"文件名:{file_name}\n"
info += f"路径:{file_path}\n"
info += f"大小:{file_size_kb:.2f} KB\n"
info += "-" * 50 + "\n"
self.textBrowser_console.insertPlainText(info)
# 这里可以添加实际的文件处理逻辑:
service.send_log_to_cache(file_path)
service.start_diagnose(self.parseStatus)
if self.parseStatus.baseinfo_status:
baseinfo_str = service.get_baseinfo_str()
self.textBrowser_info.insertPlainText(baseinfo_str)
if self.parseStatus.sensorhistory_status:
self.display_pic(service.get_sensorhistory_path("all"))
if self.parseStatus.parseidl_status:
if not service.is_idl_alert_empty():
alert_json = service.get_idl_alert_json()
self.fill_tableView_alert(alert_json)
if self.parseStatus.eventline_status:
event_json = service.get_timeline_event_json()
self.timeline_content.load_events_from_json(event_json)
self.textBrowser_console.insertPlainText("完成文件解析\n")
except Exception as e:
QMessageBox.critical(self, "处理失败", f"文件处理出错:{str(e)}")
def fill_tableView_alert(self, json_data):
"""从JSON数据更新表格内容"""
# 清空现有数据
self.model_alert.clear()
# 根据JSON数据结构设置表头和内容
if isinstance(json_data, list):
# 处理列表类型的JSON如多条记录
if json_data and isinstance(json_data[0], dict):
# 使用第一条记录的键作为表头
headers = json_data[0].keys()
self.model_alert.setHorizontalHeaderLabels(headers)
# 添加所有行数据
for item in json_data:
row_items = []
for key in headers:
# 将值转换为字符串显示
value = str(item.get(key, ""))
row_items.append(QStandardItem(value))
self.model_alert.appendRow(row_items)
elif isinstance(json_data, dict):
# 处理字典类型的JSON键值对
self.model_alert.setHorizontalHeaderLabels(["", ""])
for key, value in json_data.items():
key_item = QStandardItem(str(key))
value_item = QStandardItem(str(value))
# 设置单元格不可编辑
key_item.setEditable(False)
value_item.setEditable(False)
self.model_alert.appendRow([key_item, value_item])
else:
# 处理简单类型的JSON
self.model_alert.setHorizontalHeaderLabels(["数据"])
self.model_alert.appendRow([QStandardItem(str(json_data))])
# 调整列宽以适应内容
self.tableView_alert.horizontalHeader().setSectionResizeMode(
self.tableView_alert.horizontalHeader().ResizeToContents
)
def on_combo_changed(self, selected_text):
"""下拉列表选项变化时切换图片"""
self.display_pic(service.get_sensorhistory_path(self.comboBoxTextDict[selected_text]))
def display_pic(self, image_path):
"""在QGraphicsView中显示图片"""
try:
# 清空场景中已有的内容
self.scene.clear()
# 加载图片并创建图形项
pixmap = QPixmap(image_path)
if pixmap.isNull():
raise Exception("无法加载图片文件")
# 创建图片项并添加到场景
self.pixmap_item = QGraphicsPixmapItem(pixmap)
self.scene.addItem(self.pixmap_item)
# 初始显示时让图片居中
self.graphicsView.fitInView(self.scene.sceneRect(), Qt.KeepAspectRatio)
except Exception as e:
QMessageBox.critical(self, "错误", f"显示图片失败:{str(e)}")
def zoom_with_mouse_wheel(self, event):
"""鼠标滚轮缩放图片"""
if self.pixmap_item: # 只有存在图片时才允许缩放
# 获取当前鼠标位置作为缩放中心
mouse_pos = event.pos()
scene_pos = self.graphicsView.mapToScene(mouse_pos)
# 缩放因子(滚轮向前放大,向后缩小)
scale_factor = 1.1 if event.angleDelta().y() > 0 else 0.9
# 缩放视图
self.graphicsView.scale(scale_factor, scale_factor)
# 缩放后将鼠标位置保持在原场景位置(避免缩放时画面跳动)
new_mouse_pos = self.graphicsView.mapFromScene(scene_pos)
delta = new_mouse_pos - mouse_pos
self.graphicsView.horizontalScrollBar().setValue(
self.graphicsView.horizontalScrollBar().value() - delta.x()
)
self.graphicsView.verticalScrollBar().setValue(
self.graphicsView.verticalScrollBar().value() - delta.y()
)
else:
# 若无图片,忽略滚轮事件
super(type(self.graphicsView), self.graphicsView).wheelEvent(event)
def resizeEvent(self, event):
"""窗口大小改变时,自适应调整图片显示"""
if self.pixmap_item and not self.graphicsView.transform().isIdentity():
# 仅在未手动缩放时自动适应窗口
self.graphicsView.fitInView(self.scene.sceneRect(), Qt.KeepAspectRatio)
super().resizeEvent(event)
if __name__ == "__main__": if __name__ == "__main__":
# 初始化服务缓存
service.app_cache_init() service.app_cache_init()
# 创建PyQt应用实例 # 创建应用实例
app = QApplication(sys.argv) app = QApplication(sys.argv)
# 初始化并显示主窗口 # 创建主窗口并显示
window = MainWindow() window = MainWindow()
window.show() window.show()
# 进入应用主循环 # 进入应用主循环

@ -1,48 +0,0 @@
from PyQt5.QtWidgets import QMainWindow
from MainWindow_ui import Ui_MainWindow # 原UI类
from ui_initializer import UIInitializer # UI初始化模块
from event_handler import EventHandler # 事件处理模块
from file_processor import FileProcessor # 文件处理模块
from view_renderer import ViewRenderer # 视图渲染模块
import service
class MainWindow(QMainWindow, Ui_MainWindow):
def __init__(self):
super().__init__()
# 1. 初始化UI界面
self.setupUi(self)
# 2. 初始化服务状态
self.parse_status = service.ServiceStatus()
# 3. 初始化各功能模块(注入主窗口实例)
self.ui_init = UIInitializer(self)
self.view_renderer = ViewRenderer(self)
self.file_processor = FileProcessor(self)
self.event_handler = EventHandler(self)
# 4. 执行各模块初始化逻辑
self._init_all_modules()
def _init_all_modules(self):
"""统一执行所有模块的初始化"""
# UI初始化样式、图形视图、表格模型、时间线
self.ui_init.init_ui()
self.ui_init.init_graphic_views()
self.ui_init.init_textbrowser_style()
self.ui_init.init_tableView_alert_model()
self.ui_init.re_init_time_line_tab()
# 事件绑定(按钮、拖放、下拉框)
self.event_handler.bind_events()
self.event_handler.patch_drag_events() # 将拖放事件绑定到主窗口
# 初始化下拉框映射字典(跨模块使用,放在主窗口)
self.combo_box_text_dict = {
"ALL_Temp": "all",
"CPU_Temp": "cpu",
"FPGA_Temp": "fpga",
"DIMM_Temp": "dimm",
"Inlet_CPU_Temp": "inlet_cpu",
"Inlet_FPGA_Temp": "inlet_fpga",
"M2_Temp": "m2",
"Outlet_CPU_Temp": "outlet_cpu",
"Outlet_FPGA_Temp": "outlet_fpga",
"VR_CPU_Temp": "vr_cpu",
"VR_FPGA_Temp": "vr_fpga"
}

@ -4,7 +4,7 @@ import json
from PyQt5.QtWidgets import (QApplication, QWidget, QGraphicsView, QGraphicsScene, from PyQt5.QtWidgets import (QApplication, QWidget, QGraphicsView, QGraphicsScene,
QGraphicsTextItem, QToolTip, QVBoxLayout, QGraphicsTextItem, QToolTip, QVBoxLayout,
QTabWidget, QLabel) QTabWidget, QLabel)
from PyQt5.QtCore import Qt, QEvent, QTimer, QPoint from PyQt5.QtCore import Qt, QEvent
from PyQt5.QtGui import QPen, QBrush, QColor, QFont, QPainter from PyQt5.QtGui import QPen, QBrush, QColor, QFont, QPainter
class TimelineEvent: class TimelineEvent:
@ -15,41 +15,6 @@ class TimelineEvent:
self.title = title self.title = title
self.details = details # 详细信息字典 self.details = details # 详细信息字典
class CustomTooltip(QWidget):
"""自定义提示框,支持设置显示时长"""
def __init__(self, parent=None):
super().__init__(parent, Qt.ToolTip | Qt.FramelessWindowHint)
self.setWindowOpacity(0.95) # 半透明
# 设置样式
self.setStyleSheet("""
background-color: #f5f5f5;
border: 1px solid #ccc;
border-radius: 4px;
padding: 8px;
""")
self.layout = QVBoxLayout(self)
self.label = QLabel()
self.label.setFont(QFont("SimHei", 10))
self.label.setTextInteractionFlags(Qt.TextSelectableByMouse) # 允许选中文本
self.layout.addWidget(self.label)
# 定时器控制隐藏
self.hide_timer = QTimer(self)
self.hide_timer.setSingleShot(True)
self.hide_timer.timeout.connect(self.hide)
def setText(self, text):
self.label.setText(text)
self.adjustSize() # 自动调整大小以适应内容
def showAt(self, pos, duration=10000):
"""在指定位置显示duration为显示时长毫秒"""
self.move(pos)
self.show()
self.hide_timer.start(duration) # 10秒后自动隐藏
class TimelineGraphicsView(QGraphicsView): class TimelineGraphicsView(QGraphicsView):
"""时间线视图实现3个事件一组的高度调整和间隔时间戳显示""" """时间线视图实现3个事件一组的高度调整和间隔时间戳显示"""
def __init__(self, parent=None): def __init__(self, parent=None):
@ -89,10 +54,6 @@ class TimelineGraphicsView(QGraphicsView):
(0, 0, 150) # 组6 :浅蓝 (0, 0, 150) # 组6 :浅蓝
] ]
# 初始化自定义提示框
self.tooltip = CustomTooltip(self)
self.hovered_event = None # 当前悬停的事件
def wheelEvent(self, event): def wheelEvent(self, event):
"""鼠标滚轮缩放""" """鼠标滚轮缩放"""
delta = event.angleDelta().y() delta = event.angleDelta().y()
@ -108,45 +69,23 @@ class TimelineGraphicsView(QGraphicsView):
self.current_scale = new_scale self.current_scale = new_scale
def eventFilter(self, obj, event): def eventFilter(self, obj, event):
"""事件过滤器,处理鼠标移动和离开""" """事件悬停提示"""
if obj == self.viewport(): if obj == self.viewport() and event.type() == QEvent.MouseMove:
if event.type() == QEvent.MouseMove:
self.handle_mouse_move(event)
elif event.type() == QEvent.Leave:
# 鼠标离开视图时隐藏提示
self.tooltip.hide()
self.hovered_event = None
return super().eventFilter(obj, event)
def handle_mouse_move(self, event):
"""处理鼠标移动,显示/隐藏提示"""
scene_pos = self.mapToScene(event.pos()) scene_pos = self.mapToScene(event.pos())
current_hover = None
# 检查是否悬停在事件上
for item, event_data in self.event_items: for item, event_data in self.event_items:
if item.contains(item.mapFromScene(scene_pos)): if item.contains(item.mapFromScene(scene_pos)):
current_hover = event_data details_text = f"<b>{event_data.title}</b><br>"
break details_text += f"时间: {event_data.timestamp.strftime('%Y-%m-%d %H:%M:%S')}<br><br>"
for key, value in event_data.details.items():
if current_hover:
if current_hover != self.hovered_event:
# 显示新的提示10秒后自动隐藏
self.hovered_event = current_hover
details_text = f"<b>{current_hover.title}</b><br>"
details_text += f"时间: {current_hover.timestamp.strftime('%Y-%m-%d %H:%M:%S')}<br><br>"
for key, value in current_hover.details.items():
details_text += f"{key}: {value}<br>" details_text += f"{key}: {value}<br>"
# 在鼠标位置附近显示提示 QToolTip.showText(event.globalPos(), details_text)
global_pos = event.globalPos() return True
self.tooltip.setText(details_text)
self.tooltip.showAt(QPoint(global_pos.x() + 10, global_pos.y() + 10), 10000) QToolTip.hideText()
else:
# 没有悬停在事件上,隐藏提示 return super().eventFilter(obj, event)
self.tooltip.hide()
self.hovered_event = None
def add_events(self, events): def add_events(self, events):
"""添加事件并按组布局""" """添加事件并按组布局"""

@ -1,67 +0,0 @@
from PyQt5.QtWidgets import QVBoxLayout, QGraphicsScene
from PyQt5.QtGui import QPainter, QStandardItemModel
from PyQt5.QtCore import Qt
import timelineEvent
class UIInitializer:
def __init__(self, main_window):
self.main_window = main_window # 持有主窗口引用
def init_ui(self):
"""初始化窗口基础设置(如拖放允许)"""
self.main_window.setAcceptDrops(True)
def re_init_time_line_tab(self):
"""重新初始化时间线标签页布局"""
# 清除现有布局
if hasattr(self.main_window.tab_timeline_event, 'layout'):
layout = self.main_window.tab_timeline_event.layout()
if layout:
while layout.count():
item = layout.takeAt(0)
widget = item.widget()
if widget:
widget.deleteLater()
# 创建新布局和时间线组件
self.main_window.timeline_content = timelineEvent.TimelineTabContent()
layout = QVBoxLayout(self.main_window.tab_timeline_event)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(self.main_window.timeline_content)
def init_textbrowser_style(self):
"""设置文本浏览器样式"""
self.main_window.textBrowser_info.setStyleSheet("""
QTextBrowser {
font-family: 'SimHei';
font-size: 24px;
color: #2c3e50;
background-color: #f8f9fa;
border: 1px solid #ddd;
border-radius: 4px;
padding: 8px;
}
""")
def init_graphic_views(self):
"""初始化图形视图(抗锯齿、场景绑定)"""
# 创建图形场景
self.main_window.scene = QGraphicsScene(self.main_window)
self.main_window.graphicsView.setScene(self.main_window.scene)
# 设置渲染优化
self.main_window.graphicsView.setRenderHint(QPainter.Antialiasing)
self.main_window.graphicsView.setRenderHint(QPainter.SmoothPixmapTransform)
# 初始化图片相关变量
self.main_window.pixmap_item = None
self.main_window.current_image_path = None
def init_tableView_alert_model(self):
"""初始化告警表格模型"""
# 创建4行5列模型
self.main_window.model_alert = QStandardItemModel(4, 5)
self.main_window.model_alert.setHorizontalHeaderLabels(["时间", "部件", "等级", "方向", "描述"])
# 绑定模型到表格
self.main_window.tableView_alert.setModel(self.main_window.model_alert)
# 列宽自适应
self.main_window.tableView_alert.horizontalHeader().setSectionResizeMode(
self.main_window.tableView_alert.horizontalHeader().ResizeToContents
)

@ -4,41 +4,6 @@ import shutil
import stat import stat
import re import re
from datetime import datetime from datetime import datetime
from PyQt5.QtWidgets import QMessageBox
def show_error_message(parent, file_path):
"""显示文件格式错误提示弹窗"""
QMessageBox.critical(
parent,
"文件格式错误",
f"不支持的文件格式:\n{file_path}\n\n请上传.tar.gz格式的压缩包。"
)
def show_critical_message(parent, title, message):
"""显示通用错误提示弹窗"""
QMessageBox.critical(
parent,
title,
message
)
def show_info_message(parent, title, message):
"""显示信息提示弹窗"""
QMessageBox.information(
parent,
title,
message
)
def show_question_message(parent, title, message):
"""显示询问提示弹窗返回用户选择Yes/No"""
return QMessageBox.question(
parent,
title,
message,
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No
)
def get_project_root(): def get_project_root():
current_file = os.path.abspath(__file__) current_file = os.path.abspath(__file__)

@ -1,124 +0,0 @@
from PyQt5.QtWidgets import QGraphicsPixmapItem
from PyQt5.QtGui import QPixmap, QStandardItem
from PyQt5.QtCore import Qt
from utils import show_critical_message # 导入工具类的错误弹窗函数
class ViewRenderer:
"""视图渲染模块负责图片显示、表格数据填充等UI渲染逻辑"""
def __init__(self, main_window):
# 持有主窗口引用用于访问UI组件如graphicsView、model_alert
self.main_window = main_window
def display_pic(self, image_path):
"""
QGraphicsView 中显示图片支持抗锯齿居中适配
:param image_path: 图片文件的绝对路径
"""
try:
# 1. 清空场景中已有的图片(避免叠加显示)
self.main_window.scene.clear()
# 2. 加载图片文件QPixmap 是PyQt5中处理图片的核心类
pixmap = QPixmap(image_path)
# 检查图片是否成功加载(路径错误或格式不支持会返回空 pixmap
if pixmap.isNull():
raise ValueError(f"图片文件加载失败,路径:{image_path}(可能是路径错误或格式不支持)")
# 3. 创建图片项并添加到场景
self.main_window.pixmap_item = QGraphicsPixmapItem(pixmap)
self.main_window.scene.addItem(self.main_window.pixmap_item)
# 4. 适配视图大小(保持图片宽高比,避免拉伸变形)
self.main_window.graphicsView.fitInView(
self.main_window.scene.sceneRect(), # 场景范围(即图片大小)
Qt.KeepAspectRatio # 保持宽高比
)
except Exception as e:
# 调用工具类弹窗显示错误信息,提升用户体验
show_critical_message(
parent=self.main_window,
title="图片显示错误",
message=f"无法在图形视图中显示图片:\n{str(e)}"
)
def fill_tableView_alert(self, json_data):
"""
从JSON数据动态填充告警表格支持列表/字典两种JSON格式
:param json_data: 告警数据JSON格式可能是列表[多条记录]或字典[键值对]
"""
try:
# 1. 清空表格现有数据(避免新旧数据叠加)
self.main_window.model_alert.clear()
# 2. 根据JSON数据类型动态处理表格结构
if isinstance(json_data, list):
# 情况1JSON是列表多条告警记录每条记录是字典
self._fill_table_from_list(json_data)
elif isinstance(json_data, dict):
# 情况2JSON是字典单条记录或键值对数据
self._fill_table_from_dict(json_data)
else:
# 情况3JSON是简单类型如字符串、数字直接显示
self._fill_table_from_simple_type(json_data)
# 3. 调整列宽(自适应内容,避免文字被截断)
self.main_window.tableView_alert.horizontalHeader().setSectionResizeMode(
self.main_window.tableView_alert.horizontalHeader().ResizeToContents
)
except Exception as e:
show_critical_message(
parent=self.main_window,
title="表格填充错误",
message=f"无法从JSON数据更新告警表格\n{str(e)}"
)
def _fill_table_from_list(self, json_list):
"""私有方法处理列表类型的JSON数据多条告警记录"""
# 检查列表是否为空,且第一条记录是否为字典(确保数据格式正确)
if not json_list or not isinstance(json_list[0], dict):
raise ValueError("列表类型的JSON数据格式错误需为[{...}, {...}]结构")
# 以第一条记录的键作为表格表头(如"时间"、"部件"、"等级"
headers = json_list[0].keys()
self.main_window.model_alert.setHorizontalHeaderLabels(headers)
# 遍历列表,为每条记录创建一行表格数据
for record in json_list:
row_items = []
for key in headers:
# 获取当前字段的值(无值时显示空字符串,避免报错)
field_value = str(record.get(key, ""))
# 创建表格项QStandardItem 是表格模型的基础单元)
item = QStandardItem(field_value)
# 设置单元格不可编辑(防止用户误修改告警数据)
item.setEditable(False)
row_items.append(item)
# 将行数据添加到表格模型
self.main_window.model_alert.appendRow(row_items)
def _fill_table_from_dict(self, json_dict):
"""私有方法处理字典类型的JSON数据键值对"""
# 设置表头为"键"和"值"(适配键值对结构)
self.main_window.model_alert.setHorizontalHeaderLabels(["字段名", "字段值"])
# 遍历字典,为每个键值对创建一行表格数据
for key, value in json_dict.items():
# 键和值都转换为字符串(确保显示格式统一)
key_item = QStandardItem(str(key))
value_item = QStandardItem(str(value))
# 设置单元格不可编辑
key_item.setEditable(False)
value_item.setEditable(False)
# 添加到表格模型
self.main_window.model_alert.appendRow([key_item, value_item])
def _fill_table_from_simple_type(self, data):
"""私有方法处理简单类型的JSON数据如字符串、数字"""
# 设置表头为"数据内容"(适配单一数据)
self.main_window.model_alert.setHorizontalHeaderLabels(["告警信息"])
# 创建表格项并添加到模型
data_item = QStandardItem(str(data))
data_item.setEditable(False)
self.main_window.model_alert.appendRow([data_item])