|
|
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.loadStylesheet() # 加载样式表
|
|
|
self.loadHistory()
|
|
|
|
|
|
def loadStylesheet(self):
|
|
|
"""加载历史查看器样式表"""
|
|
|
try:
|
|
|
qssPath = "Static/HistoryViewer.qss"
|
|
|
qssFile = QFile(qssPath)
|
|
|
if qssFile.exists():
|
|
|
qssFile.open(QFile.ReadOnly | QFile.Text)
|
|
|
stream = QTextStream(qssFile)
|
|
|
stream.setCodec("UTF-8")
|
|
|
qss_content = stream.readAll()
|
|
|
qssFile.close()
|
|
|
|
|
|
# 应用样式表到当前widget
|
|
|
self.setStyleSheet(qss_content)
|
|
|
|
|
|
# 确保按钮样式生效 - 直接设置按钮样式
|
|
|
self.applyButtonStyles()
|
|
|
|
|
|
print(f"✅ HistoryViewer成功加载样式表: {qssPath}")
|
|
|
else:
|
|
|
print(f"⚠️ HistoryViewer样式表文件不存在: {qssPath}")
|
|
|
except Exception as e:
|
|
|
print(f"❌ HistoryViewer加载样式表失败: {str(e)}")
|
|
|
|
|
|
def applyButtonStyles(self):
|
|
|
"""直接应用按钮样式"""
|
|
|
# 删除按钮样式
|
|
|
delete_style = """
|
|
|
QPushButton#deleteHistoryButton {
|
|
|
color: white;
|
|
|
background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
|
|
|
stop: 0 #F87171, stop: 1 #DC2626);
|
|
|
border: 2px solid #DC2626;
|
|
|
font-weight: bold;
|
|
|
border-radius: 8px;
|
|
|
font-size: 14px;
|
|
|
padding: 10px 20px;
|
|
|
min-height: 40px;
|
|
|
min-width: 120px;
|
|
|
}
|
|
|
QPushButton#deleteHistoryButton:hover {
|
|
|
background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
|
|
|
stop: 0 #FCA5A5, stop: 1 #EF4444);
|
|
|
border: 2px solid #F87171;
|
|
|
}
|
|
|
"""
|
|
|
|
|
|
# 导出按钮样式
|
|
|
export_style = """
|
|
|
QPushButton#exportReportButton {
|
|
|
color: white;
|
|
|
background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
|
|
|
stop: 0 #34D399, stop: 1 #059669);
|
|
|
border: 2px solid #10B981;
|
|
|
font-weight: bold;
|
|
|
border-radius: 8px;
|
|
|
font-size: 14px;
|
|
|
padding: 10px 20px;
|
|
|
min-height: 40px;
|
|
|
min-width: 120px;
|
|
|
}
|
|
|
QPushButton#exportReportButton:hover {
|
|
|
background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
|
|
|
stop: 0 #6EE7B7, stop: 1 #10B981);
|
|
|
border: 2px solid #34D399;
|
|
|
}
|
|
|
"""
|
|
|
|
|
|
# 应用样式到按钮
|
|
|
if hasattr(self, 'deleteButton'):
|
|
|
self.deleteButton.setStyleSheet(delete_style)
|
|
|
if hasattr(self, 'exportButton'):
|
|
|
self.exportButton.setStyleSheet(export_style)
|
|
|
|
|
|
def initUi(self):
|
|
|
layout = QVBoxLayout()
|
|
|
layout.setContentsMargins(1, 1, 1, 1) # 极小边距
|
|
|
layout.setSpacing(1) # 极小间距
|
|
|
|
|
|
# 搜索栏 - 极度紧凑布局,固定高度
|
|
|
searchLayout = QHBoxLayout()
|
|
|
searchLayout.setContentsMargins(0, 0, 0, 0)
|
|
|
searchLayout.setSpacing(2)
|
|
|
self.searchEdit = QLineEdit()
|
|
|
self.searchEdit.setPlaceholderText("搜索规程...")
|
|
|
self.searchEdit.setMaximumHeight(20) # 限制搜索框高度
|
|
|
self.searchEdit.textChanged.connect(self.loadHistory)
|
|
|
searchLabel = QLabel("搜索:")
|
|
|
searchLabel.setMaximumHeight(20) # 限制标签高度
|
|
|
searchLayout.addWidget(searchLabel)
|
|
|
searchLayout.addWidget(self.searchEdit)
|
|
|
layout.addLayout(searchLayout, 0) # 拉伸因子为0,不占用额外空间
|
|
|
|
|
|
# 历史记录表格 - 设置对象名称用于样式识别
|
|
|
self.table = QTableView()
|
|
|
self.table.setObjectName("historyTable") # 重要:设置对象名称
|
|
|
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.table.verticalHeader().hide()
|
|
|
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.stepTable.verticalHeader().hide()
|
|
|
self.setupTableDisplay(self.stepTable)
|
|
|
|
|
|
# 使用分割器 - 表格占据绝大部分空间
|
|
|
splitter = QSplitter(Qt.Vertical)
|
|
|
splitter.setContentsMargins(0, 0, 0, 0)
|
|
|
splitter.addWidget(self.table)
|
|
|
splitter.addWidget(self.stepTable)
|
|
|
# 设置比例:上方历史记录表格占40%,下方步骤详情表格占60% (2:3比例)
|
|
|
splitter.setSizes([2, 3]) # 直接使用比例值
|
|
|
splitter.setStretchFactor(0, 2) # 历史记录表格拉伸因子为2
|
|
|
splitter.setStretchFactor(1, 3) # 步骤详情表格拉伸因子为3
|
|
|
splitter.setChildrenCollapsible(False) # 防止子窗口被完全折叠
|
|
|
layout.addWidget(splitter, 1) # 拉伸因子为1,占据主要空间
|
|
|
|
|
|
# 操作按钮 - 极度紧凑布局,固定高度
|
|
|
buttonLayout = QHBoxLayout()
|
|
|
buttonLayout.setContentsMargins(0, 0, 0, 0)
|
|
|
buttonLayout.setSpacing(2)
|
|
|
|
|
|
self.deleteButton = QPushButton("删除历史记录")
|
|
|
self.deleteButton.setObjectName("deleteHistoryButton") # 设置对象名称
|
|
|
self.deleteButton.setIcon(qta.icon('fa5s.trash', color='#FFFFFF')) # 白色图标
|
|
|
self.deleteButton.clicked.connect(self.deleteSelectedHistory)
|
|
|
buttonLayout.addWidget(self.deleteButton)
|
|
|
|
|
|
self.exportButton = QPushButton("导出报告")
|
|
|
self.exportButton.setObjectName("exportReportButton") # 设置对象名称
|
|
|
self.exportButton.setIcon(qta.icon('fa5s.file-export', color='#FFFFFF')) # 白色图标
|
|
|
self.exportButton.clicked.connect(self.exportReport)
|
|
|
buttonLayout.addWidget(self.exportButton)
|
|
|
|
|
|
buttonLayout.addStretch()
|
|
|
layout.addLayout(buttonLayout, 0) # 拉伸因子为0,不占用额外空间
|
|
|
|
|
|
self.setLayout(layout)
|
|
|
|
|
|
# 在UI创建完成后应用按钮样式
|
|
|
QTimer.singleShot(0, self.applyButtonStyles)
|
|
|
|
|
|
def setupTableDisplay(self, table):
|
|
|
"""设置表格显示属性 - 极度紧凑版"""
|
|
|
table.setWordWrap(False) # 禁用自动换行以节省空间
|
|
|
table.setAlternatingRowColors(True) # 启用交替行颜色
|
|
|
table.setSortingEnabled(True)
|
|
|
|
|
|
# 水平表头设置
|
|
|
header = table.horizontalHeader()
|
|
|
if header:
|
|
|
header.setStretchLastSection(True)
|
|
|
header.setSectionResizeMode(QHeaderView.Interactive)
|
|
|
header.setDefaultAlignment(Qt.AlignLeft)
|
|
|
header.setMinimumSectionSize(50) # 设置最小列宽
|
|
|
header.setDefaultSectionSize(80) # 设置默认列宽
|
|
|
|
|
|
# 垂直表头设置 - 极度紧凑
|
|
|
verticalHeader = table.verticalHeader()
|
|
|
if verticalHeader:
|
|
|
verticalHeader.setVisible(False) # 隐藏行号以节省空间
|
|
|
verticalHeader.setSectionResizeMode(QHeaderView.Fixed)
|
|
|
if table.objectName() == "historyTable":
|
|
|
verticalHeader.setDefaultSectionSize(20) # 历史表格适中行高,适应2:3比例
|
|
|
else:
|
|
|
verticalHeader.setDefaultSectionSize(18) # 步骤表格稍大行高
|
|
|
|
|
|
if table == self.table:
|
|
|
table.setSelectionMode(QTableView.ExtendedSelection)
|
|
|
else:
|
|
|
table.setSelectionMode(QTableView.SingleSelection)
|
|
|
|
|
|
table.setSelectionBehavior(QTableView.SelectRows)
|
|
|
|
|
|
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.setObjectName("stepDetailInfoGroup")
|
|
|
|
|
|
infoGroupLayout = QVBoxLayout()
|
|
|
infoGroupLayout.addWidget(QLabel(f"<b>步骤ID:</b> {stepId}"))
|
|
|
infoGroupLayout.addWidget(QLabel(f"<b>实际/预期对比:</b> {operationType}"))
|
|
|
infoGroupLayout.addWidget(QLabel(f"<b>执行时间:</b> {execTime}"))
|
|
|
infoGroupLayout.addWidget(QLabel(f"<b>执行状态:</b> {execStatus}"))
|
|
|
infoGroup.setLayout(infoGroupLayout)
|
|
|
|
|
|
layout.addWidget(QLabel("<b>基本信息</b>"))
|
|
|
layout.addWidget(infoGroup)
|
|
|
|
|
|
# 步骤描述区域
|
|
|
layout.addWidget(QLabel("<b>步骤描述</b>"))
|
|
|
descTextEdit = QLineEdit()
|
|
|
descTextEdit.setText(stepDesc)
|
|
|
descTextEdit.setReadOnly(True)
|
|
|
descTextEdit.setObjectName("stepDescriptionText")
|
|
|
layout.addWidget(descTextEdit)
|
|
|
|
|
|
# 详细结果区域
|
|
|
layout.addWidget(QLabel("<b>执行结果详情</b>"))
|
|
|
resultTextEdit = QLineEdit()
|
|
|
resultTextEdit.setText(detailResult)
|
|
|
resultTextEdit.setReadOnly(True)
|
|
|
|
|
|
# 根据执行状态设置结果显示样式
|
|
|
if "成功" in execStatus:
|
|
|
resultTextEdit.setObjectName("stepResultText")
|
|
|
resultTextEdit.setProperty("status", "success")
|
|
|
elif "失败" in execStatus:
|
|
|
resultTextEdit.setObjectName("stepResultText")
|
|
|
resultTextEdit.setProperty("status", "error")
|
|
|
else:
|
|
|
resultTextEdit.setObjectName("stepResultText")
|
|
|
resultTextEdit.setProperty("status", "default")
|
|
|
|
|
|
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)
|
|
|
QTimer.singleShot(500, self.forceSplitterRatio) # 延迟强制设置比例
|
|
|
|
|
|
def adjustTablesAfterShow(self):
|
|
|
"""窗口显示后调整表格"""
|
|
|
self.adjustTableColumns(self.table, self.model)
|
|
|
self.adjustTableColumns(self.stepTable, self.stepModel)
|
|
|
|
|
|
def forceSplitterRatio(self):
|
|
|
"""强制设置分割器比例为2:3"""
|
|
|
splitter = self.findChild(QSplitter)
|
|
|
if splitter:
|
|
|
total_height = splitter.height()
|
|
|
|
|
|
if total_height > 0:
|
|
|
# 计算2:3比例的实际像素值
|
|
|
history_height = int(total_height * 0.4) # 40%
|
|
|
step_height = int(total_height * 0.6) # 60%
|
|
|
|
|
|
# 设置表格的固定高度以确保比例
|
|
|
self.table.setFixedHeight(history_height)
|
|
|
self.stepTable.setMinimumHeight(step_height)
|
|
|
|
|
|
# 设置分割器尺寸
|
|
|
splitter.setSizes([history_height, step_height])
|
|
|
|
|
|
|
|
|
|
|
|
def selectAllHistory(self):
|
|
|
"""全选历史记录"""
|
|
|
self.table.selectAll()
|
|
|
|
|
|
def deselectAllHistory(self):
|
|
|
"""取消全选历史记录"""
|
|
|
self.table.clearSelection()
|
|
|
|
|
|
def exportReportFromContextMenu(self, index):
|
|
|
"""从右键菜单导出报告"""
|
|
|
self.table.selectRow(index.row())
|
|
|
self.exportReport() |