import sys
import os
from PyQt5.QtCore import Qt, QTimer, QModelIndex, QSize, pyqtSignal, QFile, QTextStream
from PyQt5.QtGui import QBrush, QColor, QStandardItemModel, QStandardItem
from PyQt5.QtWidgets import (QTableView, QPushButton, QVBoxLayout, QWidget, QHBoxLayout,
QLabel, QMenu, QFileDialog, QDialog, QLineEdit,
QDialogButtonBox, QMessageBox, QHeaderView, QSplitter)
import qtawesome as qta
from datetime import datetime
from utils.DBModels.ProcedureModel import DatabaseManager
from UI.ProcedureManager.StepExecutor import StepExecutor
class HistoryViewerWidget(QWidget):
def __init__(self, dbManager, parent=None):
super().__init__(parent)
self.dbManager = dbManager
self.initUi()
self.loadHistory()
def initUi(self):
layout = QVBoxLayout()
# 搜索栏
searchLayout = QHBoxLayout()
self.searchEdit = QLineEdit()
self.searchEdit.setPlaceholderText("搜索规程...")
self.searchEdit.textChanged.connect(self.loadHistory)
searchLayout.addWidget(QLabel("搜索:"))
searchLayout.addWidget(self.searchEdit)
layout.addLayout(searchLayout)
# 历史记录表格
self.table = QTableView()
self.table.setEditTriggers(QTableView.NoEditTriggers)
self.model = QStandardItemModel()
self.model.setHorizontalHeaderLabels(["ID", "规程全名", "类型", "执行时间", "报告路径"])
self.table.setModel(self.model)
self.table.doubleClicked.connect(self.openReportOrDetails)
self.table.setSelectionBehavior(QTableView.SelectRows)
self.table.setContextMenuPolicy(Qt.CustomContextMenu)
self.table.customContextMenuRequested.connect(self.showHistoryContextMenu)
self.setupTableDisplay(self.table)
# 步骤详情表格
self.stepTable = QTableView()
self.stepTable.setEditTriggers(QTableView.NoEditTriggers)
self.stepModel = QStandardItemModel()
self.stepModel.setHorizontalHeaderLabels(["步骤ID", "步骤描述", "执行时间", "执行状态", "详细结果", "实际/预期对比"])
self.stepTable.setModel(self.stepModel)
self.stepTable.doubleClicked.connect(self.showStepDetailDialog)
self.setupTableDisplay(self.stepTable)
# 分割窗口
splitter = QSplitter(Qt.Vertical)
splitter.addWidget(self.table)
splitter.addWidget(self.stepTable)
splitter.setSizes([400, 400])
layout.addWidget(splitter)
# 操作按钮
buttonLayout = QHBoxLayout()
separator = QWidget()
separator.setFixedWidth(20)
buttonLayout.addWidget(separator)
self.deleteButton = QPushButton("删除历史记录")
self.deleteButton.setIcon(qta.icon('fa5s.trash', color='red'))
self.deleteButton.clicked.connect(self.deleteSelectedHistory)
buttonLayout.addWidget(self.deleteButton)
self.exportButton = QPushButton("导出报告")
self.exportButton.setIcon(qta.icon('fa5s.file-export', color='green'))
self.exportButton.clicked.connect(self.exportReport)
buttonLayout.addWidget(self.exportButton)
buttonLayout.addStretch()
layout.addLayout(buttonLayout)
self.setLayout(layout)
def setupTableDisplay(self, table):
"""设置表格显示属性"""
table.setWordWrap(True)
table.setAlternatingRowColors(False)
table.setSortingEnabled(True)
header = table.horizontalHeader()
if header:
header.setStretchLastSection(True)
header.setSectionResizeMode(QHeaderView.Interactive)
header.setDefaultAlignment(Qt.AlignLeft)
verticalHeader = table.verticalHeader()
if verticalHeader:
verticalHeader.setSectionResizeMode(QHeaderView.ResizeToContents)
verticalHeader.setDefaultAlignment(Qt.AlignCenter)
if table == self.table:
table.setSelectionMode(QTableView.ExtendedSelection)
else:
table.setSelectionMode(QTableView.SingleSelection)
table.setSelectionBehavior(QTableView.SelectRows)
table.setStyleSheet("""
QTableView {
gridline-color: #d0d0d0;
background-color: white;
alternate-background-color: #f5f5f5;
selection-background-color: #0078d4;
selection-color: white;
}
QTableView::item {
padding: 5px;
border: none;
}
QTableView::item:selected {
background-color: #0078d4;
color: white;
}
QHeaderView::section {
background-color: #f0f0f0;
padding: 5px;
border: 1px solid #d0d0d0;
font-weight: bold;
}
""")
def loadHistory(self):
"""加载历史记录"""
self.model.removeRows(0, self.model.rowCount())
filterText = self.searchEdit.text().strip()
history = self.dbManager.getExecutionHistory(filterText)
for record in history:
rowItems = []
for i, value in enumerate(record):
item = QStandardItem(str(value) if value is not None else "")
item.setTextAlignment(Qt.AlignTop | Qt.AlignLeft)
if i in [1, 4]: # 规程全名和报告路径列
item.setToolTip(str(value) if value is not None else "")
rowItems.append(item)
self.model.appendRow(rowItems)
self.adjustTableColumns(self.table, self.model)
def openReportOrDetails(self, index):
"""打开报告或详情"""
executionId = self.model.item(index.row(), 0).text()
stepResults = self.dbManager.getStepResults(executionId)
self.showStepDetails(stepResults)
self.saveOperationHistory(executionId, "查看步骤详情", "")
def saveOperationHistory(self, executionId, operationType, operationDetail):
"""保存操作历史"""
self.dbManager.saveOperationHistory(
executionId,
operationType,
operationDetail,
datetime.now().strftime("%Y-%m-%d %H:%M:%S")
)
def showStepDetails(self, stepResults):
"""显示步骤详情"""
self.stepModel.removeRows(0, self.stepModel.rowCount())
if not stepResults:
return
for step in stepResults:
rowItems = []
# 步骤ID
stepIdItem = QStandardItem(step.get('step_id', ''))
stepIdItem.setTextAlignment(Qt.AlignCenter)
rowItems.append(stepIdItem)
# 步骤描述
descItem = QStandardItem(step.get('step_description', ''))
descItem.setTextAlignment(Qt.AlignTop | Qt.AlignLeft)
descItem.setToolTip(step.get('step_description', ''))
rowItems.append(descItem)
# 执行时间
timeItem = QStandardItem(step.get('execution_time', ''))
timeItem.setTextAlignment(Qt.AlignCenter)
rowItems.append(timeItem)
# 解析执行结果
resultInfo = self.parseStepResult(step.get('result', ''))
# 执行状态
statusItem = QStandardItem(resultInfo['statusText'])
statusItem.setTextAlignment(Qt.AlignCenter)
statusItem.setBackground(QBrush(resultInfo['statusColor']))
if resultInfo['status'] == 'success':
statusItem.setText("✓ 成功")
elif resultInfo['status'] == 'failed':
statusItem.setText("✗ 失败")
elif resultInfo['status'] == 'partial':
statusItem.setText("⚠ 部分成功")
else:
statusItem.setText("? 未知")
rowItems.append(statusItem)
# 详细结果
detailItem = QStandardItem(resultInfo['details'])
detailItem.setTextAlignment(Qt.AlignTop | Qt.AlignLeft)
detailItem.setToolTip(resultInfo['details'])
rowItems.append(detailItem)
# 实际/预期对比
comparisonItem = QStandardItem(resultInfo['comparison'])
comparisonItem.setTextAlignment(Qt.AlignCenter)
comparisonItem.setBackground(QBrush(resultInfo['typeColor']))
comparisonItem.setToolTip(resultInfo['comparisonDetail'])
rowItems.append(comparisonItem)
self.stepModel.appendRow(rowItems)
self.adjustTableColumns(self.stepTable, self.stepModel)
def parseStepResult(self, result):
"""解析步骤执行结果"""
if not result or result == 'None':
return {
'status': 'failed',
'statusText': '失败',
'statusColor': QColor(255, 200, 200),
'details': '无执行结果',
'comparison': '无对比',
'comparisonDetail': '步骤未执行或执行失败',
'typeColor': QColor(220, 220, 220)
}
result_str = str(result)
# 设置操作结果解析
if "=" in result_str:
# 提取设置的变量和值
import re
matches = re.findall(r'([^\s=]+)\s*=\s*([0-9]+(?:\.[0-9]+)?)', result_str)
failedMatches = re.findall(r'([^\s=]+)强制失败', result_str)
if matches or failedMatches:
successCount = len(matches)
failCount = len(failedMatches)
totalCount = successCount + failCount
if failCount > 0:
status = 'partial' if successCount > 0 else 'failed'
statusColor = QColor(255, 255, 200) if status == 'partial' else QColor(255, 200, 200)
statusText = '部分成功' if status == 'partial' else '失败'
comparison = f"设置 {successCount}/{totalCount} 成功"
comparisonDetail = f"成功设置: {', '.join([f'{var}={val}' for var, val in matches])}"
if failedMatches:
comparisonDetail += f"; 失败变量: {', '.join(failedMatches)}"
else:
status = 'success'
statusColor = QColor(200, 255, 200)
statusText = '成功'
comparison = f"设置 {successCount} 个变量"
comparisonDetail = f"成功设置: {', '.join([f'{var}={val}' for var, val in matches])}"
return {
'status': status,
'statusText': statusText,
'statusColor': statusColor,
'details': result_str,
'comparison': comparison,
'comparisonDetail': comparisonDetail,
'typeColor': QColor(173, 216, 230)
}
elif "成功" in result_str or "✓" in result_str:
# 包含等号但没有匹配到具体变量,但有成功标识
return {
'status': 'success',
'statusText': '成功',
'statusColor': QColor(200, 255, 200),
'details': result_str,
'comparison': "设置成功",
'comparisonDetail': "变量设置操作执行成功",
'typeColor': QColor(173, 216, 230)
}
# 检查操作结果解析
if any(op in result_str for op in ['>', '<', '>=', '<=', '==', '!=']):
# 分析检查结果
successCount = result_str.count('✓')
failCount = result_str.count('✗')
totalChecks = successCount + failCount
if failCount > 0:
status = 'partial' if successCount > 0 else 'failed'
statusColor = QColor(255, 255, 200) if status == 'partial' else QColor(255, 200, 200)
statusText = '部分成功' if status == 'partial' else '失败'
comparison = f"{successCount}/{totalChecks} 通过"
comparisonDetail = f"检查结果: {successCount}个成功, {failCount}个失败"
else:
status = 'success'
statusColor = QColor(200, 255, 200)
statusText = '成功'
comparison = f"{totalChecks}/{totalChecks} 通过"
comparisonDetail = f"所有 {totalChecks} 个检查项均通过"
return {
'status': status,
'statusText': statusText,
'statusColor': statusColor,
'details': result_str,
'comparison': comparison,
'comparisonDetail': comparisonDetail,
'typeColor': QColor(255, 218, 185)
}
# 等待操作结果解析
if "等待" in result_str or "wait" in result_str.lower():
# 提取等待时间
import re
timeMatch = re.search(r'([0-9]+(?:\.[0-9]+)?)', result_str)
if timeMatch:
waitTime = timeMatch.group(1)
comparison = f"等待 {waitTime}s"
comparisonDetail = f"按预期等待了 {waitTime} 秒"
else:
comparison = "等待完成"
comparisonDetail = "等待操作按预期完成"
return {
'status': 'success',
'statusText': '成功',
'statusColor': QColor(200, 255, 200),
'details': result_str,
'comparison': comparison,
'comparisonDetail': comparisonDetail,
'typeColor': QColor(221, 160, 221)
}
# 接收操作结果解析
if "t1=" in result_str or "deltaT" in result_str:
# 计算接收到的数据点数量
import re
dataPoints = re.findall(r't\d+=[\d.-]+', result_str)
comparison = f"接收 {len(dataPoints)} 个数据"
comparisonDetail = f"成功接收数据: {result_str}"
return {
'status': 'success',
'statusText': '成功',
'statusColor': QColor(200, 255, 200),
'details': result_str,
'comparison': comparison,
'comparisonDetail': comparisonDetail,
'typeColor': QColor(144, 238, 144)
}
# 错误情况
if "失败" in result_str or "error" in result_str.lower() or "错误" in result_str:
return {
'status': 'failed',
'statusText': '失败',
'statusColor': QColor(255, 200, 200),
'details': result_str,
'comparison': '执行失败',
'comparisonDetail': f"执行过程中出现错误: {result_str}",
'typeColor': QColor(255, 182, 193)
}
# 成功情况
if "成功" in result_str or "执行成功" in result_str:
return {
'status': 'success',
'statusText': '成功',
'statusColor': QColor(200, 255, 200),
'details': result_str,
'comparison': '执行成功',
'comparisonDetail': '操作按预期成功执行',
'typeColor': QColor(220, 220, 220)
}
# 默认情况
return {
'status': 'unknown',
'statusText': '未知',
'statusColor': QColor(240, 240, 240),
'details': result_str,
'comparison': '结果未知',
'comparisonDetail': f'无法解析的执行结果: {result_str}',
'typeColor': QColor(220, 220, 220)
}
def adjustTableColumns(self, table, model):
"""调整表格列宽"""
header = table.horizontalHeader()
if not header:
return
if model == self.model: # 历史记录表格
for col in range(model.columnCount()):
if col == 0: # ID列
header.setSectionResizeMode(col, QHeaderView.ResizeToContents)
elif col == 1: # 规程全名列
header.setSectionResizeMode(col, QHeaderView.Stretch)
elif col == 2: # 类型列
header.setSectionResizeMode(col, QHeaderView.ResizeToContents)
elif col == 3: # 执行时间列
header.setSectionResizeMode(col, QHeaderView.ResizeToContents)
elif col == 4: # 报告路径列
header.setSectionResizeMode(col, QHeaderView.Stretch)
else: # 步骤详情表格
for col in range(model.columnCount()):
if col == 0: # 步骤ID列
header.setSectionResizeMode(col, QHeaderView.ResizeToContents)
elif col == 1: # 步骤描述列
header.setSectionResizeMode(col, QHeaderView.Stretch)
elif col == 2: # 执行时间列
header.setSectionResizeMode(col, QHeaderView.ResizeToContents)
elif col == 3: # 执行状态列
header.setSectionResizeMode(col, QHeaderView.ResizeToContents)
elif col == 4: # 详细结果列
header.setSectionResizeMode(col, QHeaderView.Stretch)
elif col == 5: # 操作类型列
header.setSectionResizeMode(col, QHeaderView.ResizeToContents)
table.resizeRowsToContents()
def showStepDetailDialog(self, index):
"""显示步骤详情对话框"""
if not index.isValid():
return
row = index.row()
stepId = self.stepModel.item(row, 0).text()
stepDesc = self.stepModel.item(row, 1).text()
execTime = self.stepModel.item(row, 2).text()
execStatus = self.stepModel.item(row, 3).text()
detailResult = self.stepModel.item(row, 4).text()
operationType = self.stepModel.item(row, 5).text()
# 创建详情对话框
dialog = QDialog(self)
dialog.setWindowTitle(f"步骤详情 - {stepId}")
dialog.setMinimumSize(600, 400)
dialog.setModal(True)
layout = QVBoxLayout()
# 基本信息区域
infoLayout = QVBoxLayout()
infoGroup = QWidget()
infoGroup.setStyleSheet("""
QWidget {
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 5px;
padding: 10px;
margin: 5px;
}
""")
infoGroupLayout = QVBoxLayout()
infoGroupLayout.addWidget(QLabel(f"步骤ID: {stepId}"))
infoGroupLayout.addWidget(QLabel(f"实际/预期对比: {operationType}"))
infoGroupLayout.addWidget(QLabel(f"执行时间: {execTime}"))
infoGroupLayout.addWidget(QLabel(f"执行状态: {execStatus}"))
infoGroup.setLayout(infoGroupLayout)
layout.addWidget(QLabel("基本信息"))
layout.addWidget(infoGroup)
# 步骤描述区域
layout.addWidget(QLabel("步骤描述"))
descTextEdit = QLineEdit()
descTextEdit.setText(stepDesc)
descTextEdit.setReadOnly(True)
descTextEdit.setStyleSheet("""
QLineEdit {
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 3px;
padding: 8px;
font-family: 'Consolas', monospace;
}
""")
layout.addWidget(descTextEdit)
# 详细结果区域
layout.addWidget(QLabel("执行结果详情"))
resultTextEdit = QLineEdit()
resultTextEdit.setText(detailResult)
resultTextEdit.setReadOnly(True)
# 根据执行状态设置结果显示样式
if "成功" in execStatus:
resultTextEdit.setStyleSheet("""
QLineEdit {
background-color: #d4edda;
border: 1px solid #c3e6cb;
border-radius: 3px;
padding: 8px;
color: #155724;
font-family: 'Consolas', monospace;
}
""")
elif "失败" in execStatus:
resultTextEdit.setStyleSheet("""
QLineEdit {
background-color: #f8d7da;
border: 1px solid #f5c6cb;
border-radius: 3px;
padding: 8px;
color: #721c24;
font-family: 'Consolas', monospace;
}
""")
else:
resultTextEdit.setStyleSheet("""
QLineEdit {
background-color: #fff3cd;
border: 1px solid #ffeaa7;
border-radius: 3px;
padding: 8px;
color: #856404;
font-family: 'Consolas', monospace;
}
""")
layout.addWidget(resultTextEdit)
# 按钮区域
buttonBox = QDialogButtonBox(QDialogButtonBox.Ok)
buttonBox.accepted.connect(dialog.accept)
layout.addWidget(buttonBox)
dialog.setLayout(layout)
dialog.exec_()
def deleteSelectedHistory(self):
"""删除选中的历史记录"""
selectedIndexes = self.table.selectionModel().selectedRows()
if not selectedIndexes:
QMessageBox.warning(self, "未选择", "请先选择要删除的历史记录")
return
executionIds = []
for index in selectedIndexes:
executionId = self.model.item(index.row(), 0).text()
executionIds.append(executionId)
reply = QMessageBox.question(
self,
"确认删除",
f"确定要删除选中的 {len(executionIds)} 条历史记录吗?\n此操作不可恢复!",
QMessageBox.Yes | QMessageBox.No
)
if reply == QMessageBox.Yes:
success = self.dbManager.deleteExecutionHistory(executionIds)
if success:
QMessageBox.information(self, "删除成功", "已成功删除选中的历史记录")
self.loadHistory()
else:
QMessageBox.warning(self, "删除失败", "删除历史记录时发生错误")
def exportReport(self):
"""导出报告"""
selectedIndexes = self.table.selectionModel().selectedRows()
if not selectedIndexes:
QMessageBox.warning(self, "未选择", "请先选择要导出的历史记录")
return
if len(selectedIndexes) > 1:
QMessageBox.warning(self, "选择过多", "一次只能导出一个历史记录的报告")
return
index = selectedIndexes[0]
executionId = self.model.item(index.row(), 0).text()
executionData = self.dbManager.getExecutionDetails(executionId)
if not executionData:
QMessageBox.warning(self, "数据错误", "无法获取执行详情数据")
return
try:
executor = StepExecutor(executionData['procedure_content'],
executionData['procedure_id'],
self.dbManager)
executor.stepResults = executionData['step_results']
for step in executor.tableModel.stepData:
stepResult = next((s for s in executionData['step_results']
if s['step_id'] == step['stepId']), None)
if stepResult:
step['executed'] = True
step['result'] = stepResult['result']
step['time'] = datetime.strptime(stepResult['execution_time'], "%Y-%m-%d %H:%M:%S")
resultPath = executor.generateReport()
if resultPath:
return
else:
QMessageBox.information(self, "导出取消", "用户取消了报告导出")
except Exception as e:
QMessageBox.critical(self, "导出错误", f"生成报告时出错:\n{str(e)}")
import traceback
print(f"导出报告详细错误: {traceback.format_exc()}")
def showHistoryContextMenu(self, pos):
"""显示右键菜单"""
index = self.table.indexAt(pos)
menu = QMenu()
if index.isValid():
deleteAction = menu.addAction(qta.icon('fa5s.trash', color='red'), "删除历史记录")
deleteAction.triggered.connect(self.deleteSelectedHistory)
menu.addSeparator()
exportAction = menu.addAction(qta.icon('fa5s.file-export', color='green'), "导出报告")
exportAction.triggered.connect(lambda: self.exportReportFromContextMenu(index))
menu.addSeparator()
selectAllAction = menu.addAction("全选")
selectAllAction.triggered.connect(self.selectAllHistory)
deselectAllAction = menu.addAction("取消全选")
deselectAllAction.triggered.connect(self.deselectAllHistory)
if index.isValid():
menu.exec_(self.table.viewport().mapToGlobal(pos))
else:
menu.exec_(self.table.mapToGlobal(pos))
def showEvent(self, event):
"""窗口显示事件"""
super().showEvent(event)
QTimer.singleShot(100, self.adjustTablesAfterShow)
def adjustTablesAfterShow(self):
"""窗口显示后调整表格"""
self.adjustTableColumns(self.table, self.model)
self.adjustTableColumns(self.stepTable, self.stepModel)
def selectAllHistory(self):
"""全选历史记录"""
self.table.selectAll()
def deselectAllHistory(self):
"""取消全选历史记录"""
self.table.clearSelection()
def exportReportFromContextMenu(self, index):
"""从右键菜单导出报告"""
self.table.selectRow(index.row())
self.exportReport()