|
|
|
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.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(True)
|
|
|
|
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)
|
|
|
|
|
|
|
|
# 执行结果
|
|
|
|
resultText = '成功' if step.get('result', False) else '失败'
|
|
|
|
resultItem = QStandardItem(resultText)
|
|
|
|
resultItem.setTextAlignment(Qt.AlignCenter)
|
|
|
|
if step.get('result', False):
|
|
|
|
resultItem.setBackground(QBrush(QColor(200, 255, 200)))
|
|
|
|
else:
|
|
|
|
resultItem.setBackground(QBrush(QColor(255, 200, 200)))
|
|
|
|
rowItems.append(resultItem)
|
|
|
|
|
|
|
|
self.stepModel.appendRow(rowItems)
|
|
|
|
|
|
|
|
self.adjustTableColumns(self.stepTable, self.stepModel)
|
|
|
|
|
|
|
|
def adjustTableColumns(self, table, model):
|
|
|
|
"""调整表格列宽"""
|
|
|
|
header = table.horizontalHeader()
|
|
|
|
if not header:
|
|
|
|
return
|
|
|
|
|
|
|
|
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: # 报告路径或结果列
|
|
|
|
if model == self.model:
|
|
|
|
header.setSectionResizeMode(col, QHeaderView.Stretch)
|
|
|
|
else:
|
|
|
|
header.setSectionResizeMode(col, QHeaderView.ResizeToContents)
|
|
|
|
|
|
|
|
table.resizeRowsToContents()
|
|
|
|
|
|
|
|
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()
|