|
|
import sys
|
|
|
import os
|
|
|
from PyQt5.QtCore import Qt, QTimer, QAbstractTableModel, QModelIndex, QPoint, QSize, pyqtSignal, QFile,QTextStream
|
|
|
from PyQt5.QtGui import QBrush, QColor, QFont, QStandardItemModel, QStandardItem
|
|
|
from PyQt5.QtWidgets import (QApplication, QMainWindow, QTableView,
|
|
|
QPushButton, QVBoxLayout, QWidget, QHBoxLayout,
|
|
|
QLabel, QCheckBox, QSpinBox, QMenu, QFileDialog,
|
|
|
QTabWidget, QTabBar, QListWidget, QListWidgetItem, QDialog,
|
|
|
QLineEdit, QFormLayout, QDialogButtonBox, QMessageBox,
|
|
|
QHeaderView, QToolBar, QAction, QStatusBar, QComboBox, QSplitter, QAbstractItemView)
|
|
|
from docx import Document
|
|
|
from docx.shared import Pt, RGBColor
|
|
|
from docx.enum.text import WD_PARAGRAPH_ALIGNMENT
|
|
|
import qtawesome as qta
|
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
|
# 导入其他模块
|
|
|
from model.ProcedureModel.ProcedureProcessor import ExcelParser, StepTableModel
|
|
|
from utils.DBModels.ProcedureModel import DatabaseManager
|
|
|
|
|
|
class HistoryViewer(QDialog):
|
|
|
def __init__(self, dbManager, parent=None):
|
|
|
super().__init__(parent)
|
|
|
self.dbManager = dbManager
|
|
|
self.setWindowTitle("历史记录查看器")
|
|
|
self.setGeometry(200, 200, 1000, 800)
|
|
|
|
|
|
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.table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
|
|
|
|
|
|
# 步骤详情表格
|
|
|
self.stepTable = QTableView()
|
|
|
self.stepTable.setEditTriggers(QTableView.NoEditTriggers) # 新增:禁用编辑
|
|
|
self.stepModel = QStandardItemModel()
|
|
|
self.stepModel.setHorizontalHeaderLabels(["步骤ID", "步骤描述", "执行时间", "执行结果"])
|
|
|
self.stepTable.setModel(self.stepModel)
|
|
|
|
|
|
# 新增:设置表格列宽自适应内容
|
|
|
self.stepTable.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
|
|
|
|
|
|
# 分割窗口
|
|
|
splitter = QSplitter(Qt.Vertical)
|
|
|
splitter.addWidget(self.table)
|
|
|
splitter.addWidget(self.stepTable)
|
|
|
splitter.setSizes([400, 400]) # 初始分配高度
|
|
|
|
|
|
layout.addWidget(splitter)
|
|
|
# 新增:操作按钮布局
|
|
|
button_layout = QHBoxLayout()
|
|
|
self.deleteButton = QPushButton("删除历史记录")
|
|
|
self.deleteButton.setIcon(qta.icon('fa5s.trash', color='red'))
|
|
|
self.deleteButton.clicked.connect(self.deleteSelectedHistory)
|
|
|
button_layout.addWidget(self.deleteButton)
|
|
|
|
|
|
# 新增:导出报告按钮
|
|
|
self.exportButton = QPushButton("导出报告")
|
|
|
self.exportButton.setIcon(qta.icon('fa5s.file-export', color='green'))
|
|
|
self.exportButton.clicked.connect(self.exportReport)
|
|
|
button_layout.addWidget(self.exportButton)
|
|
|
|
|
|
button_layout.addStretch()
|
|
|
layout.addLayout(button_layout)
|
|
|
|
|
|
# 按钮
|
|
|
buttonBox = QDialogButtonBox(QDialogButtonBox.Ok)
|
|
|
buttonBox.accepted.connect(self.accept)
|
|
|
layout.addWidget(buttonBox)
|
|
|
|
|
|
self.setLayout(layout)
|
|
|
|
|
|
def loadHistory(self):
|
|
|
self.model.removeRows(0, self.model.rowCount())
|
|
|
filterText = self.searchEdit.text().strip()
|
|
|
history = self.dbManager.getExecutionHistory(filterText)
|
|
|
|
|
|
for record in history:
|
|
|
# 修改:使用正确的列索引(0-4)
|
|
|
row = [
|
|
|
QStandardItem(str(record[0])), # ID
|
|
|
QStandardItem(record[1]), # 规程全名(名称+编号)
|
|
|
QStandardItem(record[2]), # 类型
|
|
|
QStandardItem(record[3]), # 执行时间
|
|
|
QStandardItem(record[4]) # 报告路径
|
|
|
]
|
|
|
self.model.appendRow(row)
|
|
|
|
|
|
self.table.resizeColumnsToContents()
|
|
|
|
|
|
def openReportOrDetails(self, index):
|
|
|
# 修改:将 get_step_results 改为 getStepResults
|
|
|
execution_id = self.model.item(index.row(), 0).text()
|
|
|
step_results = self.dbManager.getStepResults(execution_id) # 修正方法名
|
|
|
self.showStepDetails(step_results)
|
|
|
# 保存操作历史
|
|
|
self.saveOperationHistory(execution_id, "查看步骤详情", "")
|
|
|
|
|
|
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:
|
|
|
row = [
|
|
|
QStandardItem(step['step_id']),
|
|
|
QStandardItem(step['step_description']),
|
|
|
QStandardItem(step['execution_time']),
|
|
|
QStandardItem('成功' if step['result'] else '失败')
|
|
|
]
|
|
|
self.stepModel.appendRow(row)
|
|
|
|
|
|
# 新增删除历史记录的方法
|
|
|
def deleteSelectedHistory(self):
|
|
|
"""删除选中的历史记录"""
|
|
|
selected_indexes = self.table.selectionModel().selectedRows()
|
|
|
if not selected_indexes:
|
|
|
QMessageBox.warning(self, "未选择", "请先选择要删除的历史记录")
|
|
|
return
|
|
|
|
|
|
# 获取选中的执行ID
|
|
|
execution_ids = []
|
|
|
for index in selected_indexes:
|
|
|
execution_id = self.model.item(index.row(), 0).text()
|
|
|
execution_ids.append(execution_id)
|
|
|
|
|
|
# 确认删除
|
|
|
reply = QMessageBox.question(
|
|
|
self,
|
|
|
"确认删除",
|
|
|
f"确定要删除选中的 {len(execution_ids)} 条历史记录吗?\n此操作不可恢复!",
|
|
|
QMessageBox.Yes | QMessageBox.No
|
|
|
)
|
|
|
|
|
|
if reply == QMessageBox.Yes:
|
|
|
# 修改:将 delete_execution_history 改为 deleteExecutionHistory
|
|
|
success = self.dbManager.deleteExecutionHistory(execution_ids) # 修正方法名
|
|
|
if success:
|
|
|
QMessageBox.information(self, "删除成功", "已成功删除选中的历史记录")
|
|
|
self.loadHistory() # 重新加载历史记录
|
|
|
else:
|
|
|
QMessageBox.warning(self, "删除失败", "删除历史记录时发生错误")
|
|
|
|
|
|
def exportReport(self):
|
|
|
"""导出选中历史记录的报告"""
|
|
|
selected_indexes = self.table.selectionModel().selectedRows()
|
|
|
if not selected_indexes:
|
|
|
QMessageBox.warning(self, "未选择", "请先选择要导出的历史记录")
|
|
|
return
|
|
|
|
|
|
if len(selected_indexes) > 1:
|
|
|
QMessageBox.warning(self, "选择过多", "一次只能导出一个历史记录的报告")
|
|
|
return
|
|
|
|
|
|
index = selected_indexes[0]
|
|
|
execution_id = self.model.item(index.row(), 0).text()
|
|
|
# 修改:报告路径现在是第5列(索引4)
|
|
|
reportPath = self.model.item(index.row(), 4).text()
|
|
|
|
|
|
# 获取执行详情数据
|
|
|
# 修改:将 get_execution_details 改为 getExecutionDetails
|
|
|
execution_data = self.dbManager.getExecutionDetails(execution_id) # 修正方法名
|
|
|
if not execution_data:
|
|
|
QMessageBox.warning(self, "数据错误", "无法获取执行详情数据")
|
|
|
return
|
|
|
|
|
|
# 生成报告
|
|
|
try:
|
|
|
# 创建临时StepExecutor实例用于生成报告
|
|
|
executor = StepExecutor(execution_data['procedure_content'],
|
|
|
execution_data['procedure_id'],
|
|
|
self.dbManager)
|
|
|
executor.stepResults = execution_data['step_results']
|
|
|
|
|
|
# 设置步骤数据
|
|
|
for step in executor.tableModel.stepData:
|
|
|
step_result = next((s for s in execution_data['step_results']
|
|
|
if s['step_id'] == step['stepId']), None)
|
|
|
if step_result:
|
|
|
step['executed'] = True
|
|
|
step['result'] = step_result['result']
|
|
|
step['time'] = datetime.strptime(step_result['execution_time'], "%Y-%m-%d %H:%M:%S")
|
|
|
|
|
|
# 生成报告文件
|
|
|
default_name = f"{execution_data['procedure_name']}_历史报告_{execution_id}.docx"
|
|
|
file_path, _ = QFileDialog.getSaveFileName(
|
|
|
self, "保存报告", default_name, "Word文档 (*.docx)"
|
|
|
)
|
|
|
|
|
|
if file_path:
|
|
|
executor.generateReport(file_path)
|
|
|
QMessageBox.information(self, "导出成功", f"报告已保存到:\n{file_path}")
|
|
|
|
|
|
except Exception as e:
|
|
|
QMessageBox.critical(self, "导出错误", f"生成报告时出错:\n{str(e)}")
|
|
|
|
|
|
# 新增:历史记录表格的右键菜单
|
|
|
def showHistoryContextMenu(self, pos):
|
|
|
"""显示历史记录的右键菜单"""
|
|
|
index = self.table.indexAt(pos)
|
|
|
if index.isValid():
|
|
|
menu = QMenu()
|
|
|
deleteAction = menu.addAction("删除历史记录")
|
|
|
deleteAction.triggered.connect(self.deleteSelectedHistory)
|
|
|
menu.exec_(self.table.viewport().mapToGlobal(pos))
|
|
|
|
|
|
class ExecutionDetailDialog(QDialog):
|
|
|
"""步骤详情对话框"""
|
|
|
def __init__(self, step_info, parent=None):
|
|
|
super().__init__(parent)
|
|
|
self.setWindowTitle("步骤详情")
|
|
|
self.setMinimumWidth(600)
|
|
|
|
|
|
layout = QVBoxLayout()
|
|
|
|
|
|
form_layout = QFormLayout()
|
|
|
form_layout.addRow("步骤ID", QLabel(step_info['step_id']))
|
|
|
form_layout.addRow("步骤描述", QLabel(step_info['step_description']))
|
|
|
form_layout.addRow("执行时间", QLabel(step_info['execution_time']))
|
|
|
form_layout.addRow("执行结果", QLabel("成功" if step_info['result'] else "失败"))
|
|
|
|
|
|
layout.addLayout(form_layout)
|
|
|
|
|
|
button_box = QDialogButtonBox(QDialogButtonBox.Ok)
|
|
|
button_box.accepted.connect(self.accept)
|
|
|
layout.addWidget(button_box)
|
|
|
|
|
|
self.setLayout(layout)
|
|
|
|
|
|
class AddCategoryDialog(QDialog):
|
|
|
def __init__(self, parent=None):
|
|
|
super().__init__(parent)
|
|
|
self.setWindowTitle("添加新分类")
|
|
|
self.setFixedSize(300, 150)
|
|
|
|
|
|
layout = QVBoxLayout()
|
|
|
|
|
|
form_layout = QFormLayout()
|
|
|
self.nameEdit = QLineEdit()
|
|
|
self.nameEdit.setPlaceholderText("输入分类名称")
|
|
|
form_layout.addRow("分类名称:", self.nameEdit)
|
|
|
|
|
|
layout.addLayout(form_layout)
|
|
|
|
|
|
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
|
|
button_box.accepted.connect(self.accept)
|
|
|
button_box.rejected.connect(self.reject)
|
|
|
|
|
|
layout.addWidget(button_box)
|
|
|
self.setLayout(layout)
|
|
|
|
|
|
def getCategoryName(self):
|
|
|
return self.nameEdit.text().strip()
|
|
|
|
|
|
class StepExecutor(QWidget):
|
|
|
# 添加标签锁定信号
|
|
|
tabLockRequired = pyqtSignal(bool)
|
|
|
|
|
|
def __init__(self, procedureData, procedureId, dbManager):
|
|
|
super().__init__()
|
|
|
self.procedureData = procedureData
|
|
|
self.procedureId = procedureId # 新增规程ID
|
|
|
self.dbManager = dbManager # 新增数据库管理器
|
|
|
self.isRunning = False
|
|
|
self.isActive = False
|
|
|
|
|
|
testSteps = procedureData["测试步骤"]
|
|
|
|
|
|
self.initUi(testSteps)
|
|
|
self.currentIndex = 0
|
|
|
self.timer = QTimer()
|
|
|
self.timer.timeout.connect(self.autoExecuteStep)
|
|
|
self.remainingCycles = 1
|
|
|
self.infiniteCycles = False
|
|
|
self.isFirstRun = True # 新增标志位,用于区分首次执行
|
|
|
self.stepResults = [] # 新增:存储所有步骤执行结果的列表
|
|
|
|
|
|
def initUi(self, testSteps):
|
|
|
layout = QVBoxLayout()
|
|
|
|
|
|
info_layout = QFormLayout()
|
|
|
info_layout.setLabelAlignment(Qt.AlignRight)
|
|
|
info_layout.addRow("规程名称:", QLabel(self.procedureData["规程信息"]["规程名称"]))
|
|
|
info_layout.addRow("规程编号:", QLabel(self.procedureData["规程信息"]["规程编号"]))
|
|
|
info_layout.addRow("规程类型:", QLabel(self.procedureData["规程信息"]["规程类型"]))
|
|
|
info_layout.addRow("测试用例:", QLabel(self.procedureData["测试用例信息"]["测试用例"]))
|
|
|
info_layout.addRow("用例编号:", QLabel(self.procedureData["测试用例信息"]["用例编号"]))
|
|
|
info_layout.addRow("工况描述:", QLabel(self.procedureData["测试用例信息"]["工况描述"]))
|
|
|
|
|
|
layout.addLayout(info_layout)
|
|
|
layout.addSpacing(20)
|
|
|
|
|
|
self.tableModel = StepTableModel(testSteps)
|
|
|
self.tableView = QTableView()
|
|
|
self.tableView.setModel(self.tableModel)
|
|
|
self.tableView.setContextMenuPolicy(Qt.CustomContextMenu)
|
|
|
self.tableView.customContextMenuRequested.connect(self.showContextMenu)
|
|
|
|
|
|
self.tableView.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeToContents)
|
|
|
self.tableView.horizontalHeader().setSectionResizeMode(1, QHeaderView.Stretch)
|
|
|
self.tableView.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeToContents)
|
|
|
self.tableView.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeToContents)
|
|
|
self.tableView.horizontalHeader().setSectionResizeMode(5, QHeaderView.Stretch) # 新增:备注列自适应宽度
|
|
|
|
|
|
layout.addWidget(QLabel("测试步骤:"))
|
|
|
layout.addWidget(self.tableView)
|
|
|
|
|
|
# 创建控制按钮布局
|
|
|
control_layout = QHBoxLayout()
|
|
|
|
|
|
self.autoButton = QPushButton(" 开始自动执行")
|
|
|
self.autoButton.clicked.connect(self.startAutoExecute)
|
|
|
self.autoButton.setIcon(qta.icon('fa5s.play', color='green'))
|
|
|
|
|
|
self.stopButton = QPushButton(" 停止自动执行")
|
|
|
self.stopButton.clicked.connect(self.stopAutoExecute)
|
|
|
self.stopButton.setEnabled(False)
|
|
|
self.stopButton.setIcon(qta.icon('fa5s.stop', color='red'))
|
|
|
|
|
|
self.nextButton = QPushButton(" 执行下一步")
|
|
|
self.nextButton.clicked.connect(self.executeNextStep)
|
|
|
self.nextButton.setIcon(qta.icon('fa5s.step-forward', color='blue'))
|
|
|
|
|
|
self.resetButton = QPushButton(" 完全重置")
|
|
|
self.resetButton.clicked.connect(self.resetExecution)
|
|
|
self.resetButton.setIcon(qta.icon('fa5s.redo', color='orange'))
|
|
|
|
|
|
self.exportButton = QPushButton(" 生成报告")
|
|
|
self.exportButton.clicked.connect(self.generateReport)
|
|
|
self.exportButton.setIcon(qta.icon('fa5s.file-alt', color='purple'))
|
|
|
|
|
|
control_layout.addWidget(self.autoButton)
|
|
|
control_layout.addWidget(self.stopButton)
|
|
|
control_layout.addWidget(self.nextButton)
|
|
|
control_layout.addWidget(self.resetButton)
|
|
|
control_layout.addWidget(self.exportButton)
|
|
|
|
|
|
# 添加循环设置
|
|
|
cycle_layout = QHBoxLayout()
|
|
|
cycle_layout.addWidget(QLabel("执行轮次:"))
|
|
|
|
|
|
self.cycleSpin = QSpinBox()
|
|
|
self.cycleSpin.setRange(1, 999)
|
|
|
self.cycleSpin.setValue(1)
|
|
|
cycle_layout.addWidget(self.cycleSpin)
|
|
|
|
|
|
self.infiniteCheckbox = QCheckBox("无限循环")
|
|
|
cycle_layout.addWidget(self.infiniteCheckbox)
|
|
|
cycle_layout.addStretch()
|
|
|
|
|
|
# 将所有布局添加到主布局
|
|
|
layout.addLayout(control_layout)
|
|
|
layout.addLayout(cycle_layout)
|
|
|
|
|
|
# 设置主布局
|
|
|
self.setLayout(layout)
|
|
|
|
|
|
# 初始化工具栏
|
|
|
self.toolbar = QToolBar("执行工具栏")
|
|
|
self.toolbar.setIconSize(QSize(24, 24))
|
|
|
|
|
|
# 工具栏操作
|
|
|
self.toolbar.addAction(qta.icon('fa5s.play', color='green'), "开始执行", self.startAutoExecute)
|
|
|
self.toolbar.addAction(qta.icon('fa5s.stop', color='red'), "停止执行", self.stopAutoExecute)
|
|
|
self.toolbar.addAction(qta.icon('fa5s.step-forward', color='blue'), "下一步", self.executeNextStep)
|
|
|
self.toolbar.addSeparator()
|
|
|
self.toolbar.addAction(qta.icon('fa5s.redo', color='orange'), "重置", self.resetExecution)
|
|
|
self.toolbar.addSeparator()
|
|
|
self.toolbar.addAction(qta.icon('fa5s.file-alt', color='purple'), "生成报告", self.generateReport)
|
|
|
|
|
|
def showContextMenu(self, pos):
|
|
|
index = self.tableView.indexAt(pos)
|
|
|
if index.isValid():
|
|
|
menu = QMenu()
|
|
|
jumpAction = menu.addAction("从该步骤开始执行")
|
|
|
jumpAction.triggered.connect(lambda: self.jumpExecute(index.row()))
|
|
|
detailAction = menu.addAction("查看步骤详情")
|
|
|
detailAction.triggered.connect(lambda: self.showStepDetail(index.row()))
|
|
|
menu.exec_(self.tableView.viewport().mapToGlobal(pos))
|
|
|
|
|
|
def jumpExecute(self, rowIndex):
|
|
|
if 0 <= rowIndex < self.tableModel.rowCount():
|
|
|
stepInfo = self.tableModel.getStepInfo(rowIndex)
|
|
|
if stepInfo and not stepInfo['isMain']:
|
|
|
self.executeStep(rowIndex)
|
|
|
self.currentIndex = rowIndex + 1
|
|
|
|
|
|
def showStepDetail(self, row):
|
|
|
step_info = self.tableModel.getStepInfo(row)
|
|
|
if not step_info:
|
|
|
return
|
|
|
|
|
|
detail_dialog = QDialog(self)
|
|
|
detail_dialog.setWindowTitle("步骤详情")
|
|
|
detail_dialog.setMinimumWidth(500)
|
|
|
|
|
|
layout = QVBoxLayout()
|
|
|
|
|
|
form_layout = QFormLayout()
|
|
|
form_layout.addRow("步骤ID", QLabel(step_info['stepId']))
|
|
|
form_layout.addRow("步骤类型", QLabel("主步骤" if step_info['isMain'] else "子步骤"))
|
|
|
form_layout.addRow("步骤描述", QLabel(step_info['description']))
|
|
|
|
|
|
if step_info['time']:
|
|
|
form_layout.addRow("执行时间"); QLabel(step_info['time'].strftime("%Y-%m-%d %H:%M:%S"))
|
|
|
|
|
|
if step_info['result'] is not None:
|
|
|
status = "成功" if step_info['result'] else "失败"
|
|
|
form_layout.addRow("执行结果"); QLabel(status)
|
|
|
|
|
|
layout.addLayout(form_layout)
|
|
|
|
|
|
button_box = QDialogButtonBox(QDialogButtonBox.Ok)
|
|
|
button_box.accepted.connect(detail_dialog.accept)
|
|
|
layout.addWidget(button_box)
|
|
|
|
|
|
detail_dialog.setLayout(layout)
|
|
|
detail_dialog.exec_()
|
|
|
|
|
|
def startAutoExecute(self):
|
|
|
self.isRunning = True
|
|
|
self.isActive = True
|
|
|
# 发送标签锁定信号
|
|
|
self.tabLockRequired.emit(True)
|
|
|
self.autoButton.setEnabled(False)
|
|
|
self.stopButton.setEnabled(True)
|
|
|
self.nextButton.setEnabled(False)
|
|
|
self.resetButton.setEnabled(False)
|
|
|
self.exportButton.setEnabled(False)
|
|
|
|
|
|
# 如果是首次执行,则初始化步骤结果集合
|
|
|
if self.isFirstRun:
|
|
|
self.stepResults = [] # 重置步骤结果集合
|
|
|
self.tableModel.resetExecutionState()
|
|
|
self.isFirstRun = False # 标记已执行过
|
|
|
|
|
|
self.remainingCycles = self.cycleSpin.value()
|
|
|
self.infiniteCycles = self.infiniteCheckbox.isChecked()
|
|
|
self.timer.start(1000)
|
|
|
|
|
|
def stopAutoExecute(self):
|
|
|
self.isRunning = False # 清除自动执行状态
|
|
|
self.timer.stop()
|
|
|
self.autoButton.setEnabled(True)
|
|
|
self.stopButton.setEnabled(False)
|
|
|
self.nextButton.setEnabled(True)
|
|
|
self.resetButton.setEnabled(True)
|
|
|
self.exportButton.setEnabled(True)
|
|
|
# 注意: 这里不重置isActive,因为执行器仍处于激活状态
|
|
|
# 执行结束时更新完整步骤结果到数据库
|
|
|
if hasattr(self, 'current_execution_id'):
|
|
|
self.dbManager.updateStepResults(self.current_execution_id, self.stepResults)
|
|
|
|
|
|
def autoExecuteStep(self):
|
|
|
if self.currentIndex < self.tableModel.rowCount():
|
|
|
step_info = self.tableModel.getStepInfo(self.currentIndex)
|
|
|
if step_info and not step_info['isMain']:
|
|
|
self.executeStep(self.currentIndex)
|
|
|
self.currentIndex += 1
|
|
|
else:
|
|
|
if self.infiniteCycles or self.remainingCycles > 0:
|
|
|
if not self.infiniteCycles:
|
|
|
self.remainingCycles -= 1
|
|
|
self.timer.stop()
|
|
|
self.currentIndex = 0
|
|
|
|
|
|
should_continue = self.infiniteCycles or self.remainingCycles > 0
|
|
|
if should_continue:
|
|
|
self.timer.start()
|
|
|
else:
|
|
|
self.stopAutoExecute()
|
|
|
else:
|
|
|
self.stopAutoExecute()
|
|
|
|
|
|
def executeNextStep(self):
|
|
|
self.isActive = True
|
|
|
# 发送标签锁定信号
|
|
|
self.tabLockRequired.emit(True)
|
|
|
if self.currentIndex < self.tableModel.rowCount():
|
|
|
step_info = self.tableModel.getStepInfo(self.currentIndex)
|
|
|
if step_info and not step_info['isMain']:
|
|
|
self.executeStep(self.currentIndex)
|
|
|
self.currentIndex += 1
|
|
|
# 手动执行不需要修改isRunning状态
|
|
|
|
|
|
def executeStep(self, row):
|
|
|
stepInfo = self.tableModel.getStepInfo(row)
|
|
|
if not stepInfo:
|
|
|
return False
|
|
|
|
|
|
print(f"开始执行步骤 {stepInfo['stepId']}: {stepInfo['description']}")
|
|
|
|
|
|
result = self.handleStep(row, stepInfo)
|
|
|
|
|
|
# 确保result总是布尔值(避免None导致数据库错误)
|
|
|
if result is None:
|
|
|
print(f"警告:步骤 {stepInfo['stepId']} 返回了None结果,设置为False")
|
|
|
result = False
|
|
|
|
|
|
# 存储执行结果到数据库
|
|
|
execution_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
|
|
|
|
# 新增:获取当前执行的ID(每次执行一个唯一的execution_id)
|
|
|
if not hasattr(self, 'current_execution_id'):
|
|
|
self.current_execution_id = self.dbManager.insertProcedureExecution(
|
|
|
self.procedureId,
|
|
|
execution_time
|
|
|
)
|
|
|
|
|
|
# 存储执行结果到内存集合
|
|
|
step_result = {
|
|
|
'step_id': stepInfo['stepId'],
|
|
|
'step_description': stepInfo['description'],
|
|
|
'execution_time': execution_time,
|
|
|
'result': result
|
|
|
}
|
|
|
self.stepResults.append(step_result)
|
|
|
|
|
|
# 更新数据库中的步骤结果集合
|
|
|
self.dbManager.updateStepResults(self.current_execution_id, self.stepResults)
|
|
|
|
|
|
success = self.tableModel.updateStepResult(row, result, datetime.now())
|
|
|
|
|
|
return result
|
|
|
|
|
|
def handleStep(self, rowIndex, stepInfo): # 修改参数名
|
|
|
description = stepInfo['description']
|
|
|
stepId = stepInfo['stepId']
|
|
|
|
|
|
print(f"处理步骤 {stepId}: {description}")
|
|
|
|
|
|
if "设置" in description:
|
|
|
return self.performLogin(stepId, description) # 修改方法名
|
|
|
elif "数据导入" in description:
|
|
|
return self.performDataImport(stepId, description) # 修改方法名
|
|
|
elif "验证" in description:
|
|
|
return self.performValidation(stepId, description) # 修改方法名
|
|
|
elif "导出" in description:
|
|
|
return self.performExport(stepId, description) # 修改方法名
|
|
|
elif "备份" in description:
|
|
|
return self.performBackup(stepId, description) # 修改方法名
|
|
|
elif "发送" in description:
|
|
|
return self.performNotification(stepId, description) # 修改方法名
|
|
|
|
|
|
def simulateExecution(self, stepId, description): # 修改方法名
|
|
|
import random
|
|
|
return random.random() < 0.9
|
|
|
|
|
|
def performLogin(self, stepId, description): # 修改方法名
|
|
|
print(f"执行登录操作 (步骤 {stepId}): {description}")
|
|
|
return True
|
|
|
|
|
|
def performDataImport(self, stepId, description): # 修改方法名
|
|
|
import random
|
|
|
return random.random() < 0.95
|
|
|
|
|
|
def performValidation(self, stepId, description): # 修改方法名
|
|
|
import random
|
|
|
return random.random() < 0.85
|
|
|
|
|
|
def performExport(self, stepId, description): # 修改方法名
|
|
|
import random
|
|
|
return random.random() < 0.98
|
|
|
|
|
|
def performBackup(self, stepId, description): # 修改方法名
|
|
|
return True
|
|
|
|
|
|
def performNotification(self, stepId, description): # 修改方法名
|
|
|
import random
|
|
|
return random.random() < 0.99
|
|
|
|
|
|
def performWait(self, stepId, description): # 修改方法名
|
|
|
import time
|
|
|
waitTime = 1 # 修改变量名
|
|
|
if "s" in description:
|
|
|
try:
|
|
|
waitTime = int(description.split("等待")[1].split("s")[0].strip())
|
|
|
except:
|
|
|
pass
|
|
|
time.sleep(waitTime)
|
|
|
return True
|
|
|
|
|
|
def performSetting(self, stepId, description): # 修改方法名
|
|
|
return True
|
|
|
|
|
|
def resetExecution(self):
|
|
|
self.tableModel.resetAll()
|
|
|
self.currentIndex = 0
|
|
|
self.stopAutoExecute()
|
|
|
self.cycleSpin.setValue(1)
|
|
|
self.infiniteCheckbox.setChecked(False)
|
|
|
self.isRunning = False
|
|
|
self.isActive = False
|
|
|
self.isFirstRun = True # 重置标志位
|
|
|
self.stepResults = [] # 重置步骤结果集合
|
|
|
|
|
|
# 新增:重置当前执行ID
|
|
|
if hasattr(self, 'current_execution_id'):
|
|
|
del self.current_execution_id
|
|
|
|
|
|
# 发送标签解锁信号
|
|
|
self.tabLockRequired.emit(False)
|
|
|
|
|
|
# 修改方法签名,添加file_path参数
|
|
|
def generateReport(self):
|
|
|
# 生成规程全名(名称+编号)
|
|
|
proc_full_name = f"{self.procedureData['规程信息']['规程名称']}({self.procedureData['规程信息']['规程编号']})"
|
|
|
|
|
|
# 弹出文件选择对话框
|
|
|
default_name = f"{proc_full_name}_报告_{datetime.now().strftime('%Y%m%d_%H%M%S')}.docx"
|
|
|
file_path, _ = QFileDialog.getSaveFileName(
|
|
|
self,
|
|
|
"保存报告",
|
|
|
default_name,
|
|
|
"Word文档 (*.docx)"
|
|
|
)
|
|
|
|
|
|
# 如果用户取消选择,则直接返回
|
|
|
if not file_path:
|
|
|
return
|
|
|
|
|
|
doc = Document()
|
|
|
|
|
|
# 生成规程全名(名称+编号)
|
|
|
proc_full_name = f"{self.procedureData['规程信息']['规程名称']}({self.procedureData['规程信息']['规程编号']})"
|
|
|
title = doc.add_paragraph(f"{proc_full_name} - 测试报告")
|
|
|
title.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
|
|
|
title.runs[0].font.size = Pt(18)
|
|
|
title.runs[0].bold = True
|
|
|
|
|
|
info = doc.add_paragraph()
|
|
|
info.add_run("规程信息:\n").bold = True
|
|
|
info.add_run(f"规程编号: {self.procedureData['规程信息']['规程编号']}\n")
|
|
|
info.add_run(f"规程类型: {self.procedureData['规程信息']['规程类型']}\n")
|
|
|
|
|
|
case_info = doc.add_paragraph()
|
|
|
case_info.add_run("测试用例信息:\n").bold = True
|
|
|
case_info.add_run(f"测试用例: {self.procedureData['测试用例信息']['测试用例']}\n")
|
|
|
case_info.add_run(f"用例编号: {self.procedureData['测试用例信息']['用例编号']}\n")
|
|
|
case_info.add_run(f"工况描述: {self.procedureData['测试用例信息']['工况描述']}\n")
|
|
|
|
|
|
stats = doc.add_paragraph()
|
|
|
stats.add_run("执行统计:\n").bold = True
|
|
|
total = self.tableModel.rowCount()
|
|
|
success = sum(1 for s in self.tableModel.stepData if s['result'])
|
|
|
failure = total - success
|
|
|
stats.add_run(f"总步骤数: {total}\n成功数: {success}\n失败数: {failure}\n")
|
|
|
|
|
|
# 优化步骤表格展示
|
|
|
steps_table = doc.add_table(rows=1, cols=6)
|
|
|
steps_table.style = 'Table Grid' # 添加表格边框样式
|
|
|
hdr_cells = steps_table.rows[0].cells
|
|
|
|
|
|
# 设置表头
|
|
|
headers = ['步骤ID', '步骤类型', '步骤描述', '执行时间', '执行结果', '状态']
|
|
|
for i, header in enumerate(headers):
|
|
|
hdr_cells[i].text = header
|
|
|
# 设置表头样式(加粗居中)
|
|
|
for paragraph in hdr_cells[i].paragraphs:
|
|
|
paragraph.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
|
|
|
run = paragraph.runs[0]
|
|
|
run.font.bold = True
|
|
|
|
|
|
# 填充表格数据
|
|
|
for step in self.tableModel.stepData:
|
|
|
row_cells = steps_table.add_row().cells
|
|
|
|
|
|
# 步骤ID
|
|
|
row_cells[0].text = step['stepId']
|
|
|
|
|
|
# 步骤类型
|
|
|
step_type = "主步骤" if step['isMain'] else "子步骤"
|
|
|
row_cells[1].text = step_type
|
|
|
|
|
|
# 步骤描述
|
|
|
row_cells[2].text = step['description']
|
|
|
|
|
|
# 执行时间
|
|
|
time_text = step['time'].strftime("%Y-%m-%d %H:%M:%S") if step['time'] else 'N/A'
|
|
|
row_cells[3].text = time_text
|
|
|
|
|
|
# 执行结果(带颜色标记)
|
|
|
result_cell = row_cells[4]
|
|
|
if step['result'] is True:
|
|
|
result_text = '成功'
|
|
|
# 确保单元格有run对象
|
|
|
if not result_cell.paragraphs[0].runs:
|
|
|
result_cell.paragraphs[0].add_run(result_text)
|
|
|
result_cell.paragraphs[0].runs[0].font.color.rgb = RGBColor(0, 128, 0) # 绿色
|
|
|
elif step['result'] is False:
|
|
|
result_text = '失败'
|
|
|
# 确保单元格有run对象
|
|
|
if not result_cell.paragraphs[0].runs:
|
|
|
result_cell.paragraphs[0].add_run(result_text)
|
|
|
result_cell.paragraphs[0].runs[0].font.color.rgb = RGBColor(255, 0, 0) # 红色
|
|
|
else:
|
|
|
result_text = '未执行'
|
|
|
result_cell.text = result_text
|
|
|
|
|
|
# 状态
|
|
|
status = '已执行' if step['executed'] else '未执行'
|
|
|
row_cells[5].text = status
|
|
|
|
|
|
# 设置表格自动适应宽度
|
|
|
for col in steps_table.columns:
|
|
|
col.width = doc.sections[0].page_width // len(headers)
|
|
|
|
|
|
# 设置默认文件名(包含规程全名)
|
|
|
if not file_path:
|
|
|
filename = f"{proc_full_name}_报告_{datetime.now().strftime('%Y%m%d_%H%M%S')}.docx"
|
|
|
else:
|
|
|
filename = file_path
|
|
|
|
|
|
doc.save(filename)
|
|
|
|
|
|
# 确保导出成功时显示提示信息
|
|
|
QMessageBox.information(self, "报告生成", f"报告已生成: {filename}")
|
|
|
|
|
|
class ProcedureManager(QMainWindow):
|
|
|
def __init__(self):
|
|
|
super().__init__()
|
|
|
|
|
|
self.setWindowTitle("规程管理系统")
|
|
|
self.setGeometry(100, 100, 1200, 800)
|
|
|
# 创建工具栏
|
|
|
self.toolbar = QToolBar("主工具栏")
|
|
|
self.toolbar.setIconSize(QSize(32, 32))
|
|
|
# 新增:在工具栏级别统一设置文字显示位置
|
|
|
self.toolbar.setToolButtonStyle(Qt.ToolButtonTextUnderIcon)
|
|
|
self.addToolBar(self.toolbar)
|
|
|
self.createActions()
|
|
|
|
|
|
# 添加状态栏
|
|
|
self.statusBar = QStatusBar()
|
|
|
self.setStatusBar(self.statusBar)
|
|
|
|
|
|
def initUI(self):
|
|
|
# 加载样式表
|
|
|
self.loadStylesheet()
|
|
|
|
|
|
|
|
|
# 创建主控件 - 已经是QTabWidget
|
|
|
self.tabs = QTabWidget()
|
|
|
self.setCentralWidget(self.tabs)
|
|
|
|
|
|
# 启用标签页关闭按钮
|
|
|
self.tabs.setTabsClosable(True)
|
|
|
self.tabs.tabCloseRequested.connect(self.closeTab) # 新增关闭标签页信号连接
|
|
|
|
|
|
|
|
|
# 添加初始的规程管理标签页(必须先创建UI组件)
|
|
|
self.initProcedureManagementTab() # 先创建UI组件
|
|
|
|
|
|
# 初始化其他组件(在UI创建后调用)
|
|
|
|
|
|
self.loadCategories() # 现在categoryList已存在
|
|
|
# 新增:确保加载第一个分类的规程
|
|
|
if self.categoryList.count() > 0:
|
|
|
self.categorySelected(self.categoryList.item(0), None)
|
|
|
|
|
|
# 初始化标签锁定状态
|
|
|
self.activeExecutorIndex = -1
|
|
|
|
|
|
def initDB(self):
|
|
|
# 初始化数据库
|
|
|
self.db = DatabaseManager()
|
|
|
|
|
|
# 新增:关闭标签页的处理方法
|
|
|
def closeTab(self, index):
|
|
|
# 规程管理标签页(索引0)不能关闭
|
|
|
if index == 0:
|
|
|
return
|
|
|
|
|
|
widget = self.tabs.widget(index)
|
|
|
|
|
|
# 检查是否有正在运行的执行器
|
|
|
if isinstance(widget, StepExecutor) and widget.isRunning:
|
|
|
QMessageBox.warning(
|
|
|
self,
|
|
|
"操作被阻止",
|
|
|
"当前有规程正在执行中,请先停止执行后再关闭标签页。"
|
|
|
)
|
|
|
return
|
|
|
|
|
|
# 如果是当前激活的执行器标签页,解锁所有标签页
|
|
|
if index == self.activeExecutorIndex:
|
|
|
self.lockTabs(False, index)
|
|
|
|
|
|
# 移除标签页
|
|
|
self.tabs.removeTab(index)
|
|
|
widget.deleteLater()
|
|
|
|
|
|
# 修改:锁定/解锁标签页方法(增加关闭按钮控制)
|
|
|
def lockTabs(self, lock, currentIndex):
|
|
|
"""锁定或解锁标签页"""
|
|
|
self.activeExecutorIndex = currentIndex if lock else -1
|
|
|
|
|
|
# 遍历所有标签页
|
|
|
for index in range(self.tabs.count()):
|
|
|
# 当前执行器标签页保持可用,其他标签页根据锁定状态设置
|
|
|
enable = not lock or index == currentIndex
|
|
|
self.tabs.setTabEnabled(index, enable)
|
|
|
|
|
|
# 特殊处理:规程管理标签页(索引0)始终可用
|
|
|
if index == 0:
|
|
|
self.tabs.setTabEnabled(0, True)
|
|
|
|
|
|
# 设置关闭按钮状态:执行期间禁用关闭按钮
|
|
|
if index > 0: # 非规程管理标签页
|
|
|
self.tabs.tabBar().setTabButton(
|
|
|
index,
|
|
|
QTabBar.RightSide, # 关闭按钮位置
|
|
|
None if lock and index != currentIndex else self.tabs.tabBar().tabButton(index, QTabBar.RightSide)
|
|
|
)
|
|
|
|
|
|
# 修改:打开规程执行界面(显示规程全名)
|
|
|
def openProcedureInExecutor(self, item=None):
|
|
|
# 如果通过工具栏调用,item为None,使用当前选中项
|
|
|
currentItem = item or self.procedureList.currentItem()
|
|
|
if not currentItem:
|
|
|
return
|
|
|
|
|
|
# 检查是否有正在运行的执行器
|
|
|
if self.hasRunningExecutor():
|
|
|
QMessageBox.warning(
|
|
|
self,
|
|
|
"操作被阻止",
|
|
|
"当前有规程正在执行中,请先停止执行后再打开其他规程。"
|
|
|
)
|
|
|
return
|
|
|
|
|
|
procId = currentItem.data(Qt.UserRole) # 修改变量名
|
|
|
procedureData = self.db.getProcedureContent(procId) # 修改变量名
|
|
|
|
|
|
if not procedureData:
|
|
|
QMessageBox.warning(self, "打开失败", "无法获取规程内容")
|
|
|
return
|
|
|
|
|
|
# 创建新的执行界面,传入规程ID和数据库管理器
|
|
|
executor = StepExecutor(procedureData, procId, self.db)
|
|
|
# 获取规程名称和编号
|
|
|
procName = procedureData["规程信息"]["规程名称"]
|
|
|
procNumber = procedureData["规程信息"]["规程编号"]
|
|
|
|
|
|
# 添加为新标签页,显示规程全名(名称+编号)
|
|
|
tab_index = self.tabs.addTab(executor, f"{procName} ({procNumber})")
|
|
|
# 记录当前执行器的标签索引
|
|
|
executor.tabLockRequired.connect(lambda lock: self.lockTabs(lock, tab_index))
|
|
|
# 切换到新添加的标签页
|
|
|
self.tabs.setCurrentWidget(executor)
|
|
|
|
|
|
def initProcedureManagementTab(self):
|
|
|
"""创建规程管理主标签页"""
|
|
|
mainWidget = QWidget()
|
|
|
mainLayout = QHBoxLayout()
|
|
|
|
|
|
self.categoryList = QListWidget()
|
|
|
self.categoryList.setFixedWidth(200)
|
|
|
self.categoryList.currentItemChanged.connect(self.categorySelected)
|
|
|
# 设置分类列表接受拖放
|
|
|
self.categoryList.setAcceptDrops(True)
|
|
|
self.categoryList.setDragDropMode(QAbstractItemView.DropOnly)
|
|
|
# 新增:设置分类列表右键菜单
|
|
|
self.categoryList.setContextMenuPolicy(Qt.CustomContextMenu)
|
|
|
self.categoryList.customContextMenuRequested.connect(self.showCategoryContextMenu) # 新增
|
|
|
|
|
|
self.procedureList = QListWidget()
|
|
|
# 设置规程列表支持拖拽
|
|
|
self.procedureList.setDragEnabled(True)
|
|
|
self.procedureList.setDragDropMode(QAbstractItemView.DragOnly)
|
|
|
self.procedureList.itemDoubleClicked.connect(
|
|
|
lambda item: self.openProcedureInExecutor(item)
|
|
|
)
|
|
|
# 新增:设置规程列表右键菜单
|
|
|
self.procedureList.setContextMenuPolicy(Qt.CustomContextMenu)
|
|
|
self.procedureList.customContextMenuRequested.connect(self.showProcedureContextMenu) # 新增
|
|
|
|
|
|
# 新增:拖拽事件处理
|
|
|
self.procedureList.dragEnterEvent = self.procedureDragEnterEvent
|
|
|
self.categoryList.dropEvent = self.categoryDropEvent
|
|
|
|
|
|
leftPanel = QWidget()
|
|
|
leftLayout = QVBoxLayout()
|
|
|
leftLayout.addWidget(QLabel("规程分类"))
|
|
|
leftLayout.addWidget(self.categoryList)
|
|
|
leftPanel.setLayout(leftLayout)
|
|
|
leftPanel.setFixedWidth(220)
|
|
|
|
|
|
rightPanel = QWidget()
|
|
|
rightLayout = QVBoxLayout()
|
|
|
rightLayout.addWidget(QLabel("规程列表"))
|
|
|
rightLayout.addWidget(self.procedureList)
|
|
|
rightPanel.setLayout(rightLayout)
|
|
|
|
|
|
mainLayout.addWidget(leftPanel)
|
|
|
mainLayout.addWidget(rightPanel)
|
|
|
|
|
|
mainWidget.setLayout(mainLayout)
|
|
|
self.tabs.addTab(mainWidget, "规程管理")
|
|
|
|
|
|
def createActions(self):
|
|
|
self.importAction = QAction(
|
|
|
qta.icon('fa5s.file-import', color='green'),
|
|
|
"导入规程",
|
|
|
self
|
|
|
)
|
|
|
self.importAction.setIconText("导入规程")
|
|
|
self.importAction.setShortcut("Ctrl+I")
|
|
|
self.importAction.setStatusTip("导入Excel规程文件")
|
|
|
self.importAction.triggered.connect(self.importProcedure)
|
|
|
self.toolbar.addAction(self.importAction) # 修改变量名
|
|
|
|
|
|
self.addCategoryAction = QAction(
|
|
|
qta.icon('fa5s.folder-plus', color='blue'),
|
|
|
"添加分类",
|
|
|
self
|
|
|
)
|
|
|
self.addCategoryAction.setIconText("添加分类")
|
|
|
self.addCategoryAction.setStatusTip("添加新的分类")
|
|
|
self.addCategoryAction.triggered.connect(self.addCategory)
|
|
|
self.toolbar.addAction(self.addCategoryAction) # 修改变量名
|
|
|
|
|
|
self.deleteCategoryAction = QAction(
|
|
|
qta.icon('fa5s.folder-minus', color='red'),
|
|
|
"删除分类",
|
|
|
self
|
|
|
)
|
|
|
self.deleteCategoryAction.setIconText("删除分类")
|
|
|
self.deleteCategoryAction.setStatusTip("删除当前分类")
|
|
|
self.deleteCategoryAction.triggered.connect(self.deleteCurrentCategory)
|
|
|
self.toolbar.addAction(self.deleteCategoryAction) # 修改变量名
|
|
|
|
|
|
self.deleteProcedureAction = QAction(
|
|
|
qta.icon('fa5s.trash', color='red'),
|
|
|
"删除规程",
|
|
|
self
|
|
|
)
|
|
|
self.deleteProcedureAction.setIconText("删除规程")
|
|
|
self.deleteProcedureAction.setStatusTip("删除选中的规程")
|
|
|
self.deleteProcedureAction.triggered.connect(self.deleteSelectedProcedure)
|
|
|
self.toolbar.addAction(self.deleteProcedureAction) # 修改变量名
|
|
|
|
|
|
self.openProcedureAction = QAction(
|
|
|
qta.icon('fa5s.folder-open', color='orange'),
|
|
|
"打开规程",
|
|
|
self
|
|
|
)
|
|
|
self.openProcedureAction.setIconText("打开规程")
|
|
|
self.openProcedureAction.setStatusTip("在步骤执行工具中打开选中的规程")
|
|
|
self.openProcedureAction.triggered.connect(self.openProcedureInExecutor)
|
|
|
self.toolbar.addAction(self.openProcedureAction) # 修改变量名
|
|
|
|
|
|
# 添加历史记录查看器动作
|
|
|
self.historyAction = QAction(
|
|
|
qta.icon('fa5s.history', color='purple'),
|
|
|
"历史记录",
|
|
|
self
|
|
|
)
|
|
|
self.historyAction.setIconText("历史记录")
|
|
|
self.historyAction.setStatusTip("查看历史执行记录")
|
|
|
self.historyAction.triggered.connect(self.openHistoryViewer)
|
|
|
self.toolbar.addAction(self.historyAction)
|
|
|
|
|
|
def loadCategories(self):
|
|
|
self.categoryList.clear()
|
|
|
categories = self.db.getCategories()
|
|
|
for catId, catName in categories:
|
|
|
item = QListWidgetItem(catName)
|
|
|
item.setData(Qt.UserRole, catId)
|
|
|
self.categoryList.addItem(item)
|
|
|
|
|
|
if self.categoryList.count() > 0:
|
|
|
self.categoryList.setCurrentRow(0)
|
|
|
# 新增:手动触发分类选择信号
|
|
|
self.categoryList.itemSelectionChanged.emit()
|
|
|
|
|
|
def loadProcedures(self, categoryId=None):
|
|
|
self.procedureList.clear()
|
|
|
procedures = self.db.getProcedures(categoryId)
|
|
|
|
|
|
for procId, name, number, type, createdAt in procedures:
|
|
|
item = QListWidgetItem(f"{name} ({number})")
|
|
|
item.setData(Qt.UserRole, procId)
|
|
|
item.setToolTip(f"类型: {type}\n创建时间: {createdAt}")
|
|
|
self.procedureList.addItem(item)
|
|
|
|
|
|
def categorySelected(self, currentItem, previousItem):
|
|
|
if currentItem:
|
|
|
categoryId = currentItem.data(Qt.UserRole)
|
|
|
self.loadProcedures(categoryId)
|
|
|
|
|
|
def importProcedure(self):
|
|
|
filePath, _ = QFileDialog.getOpenFileName(
|
|
|
self,
|
|
|
"选择规程文件",
|
|
|
"",
|
|
|
"Excel文件 (*.xlsx *.xls)"
|
|
|
)
|
|
|
|
|
|
if not filePath:
|
|
|
return
|
|
|
|
|
|
try:
|
|
|
parsedData = ExcelParser.parseProcedure(filePath)
|
|
|
|
|
|
currentItem = self.categoryList.currentItem()
|
|
|
categoryId = currentItem.data(Qt.UserRole) if currentItem else None
|
|
|
|
|
|
procInfo = parsedData["规程信息"]
|
|
|
# 添加空字符串作为report_path参数
|
|
|
procId = self.db.addProcedure(
|
|
|
categoryId,
|
|
|
procInfo["规程名称"],
|
|
|
procInfo["规程编号"],
|
|
|
procInfo["规程类型"],
|
|
|
parsedData,
|
|
|
"" # 显式传递空字符串作为report_path
|
|
|
)
|
|
|
|
|
|
if procId:
|
|
|
self.statusBar.showMessage(f"成功导入规程: {procInfo['规程名称']}", 5000)
|
|
|
self.loadProcedures(categoryId)
|
|
|
else:
|
|
|
QMessageBox.warning(self, "导入失败", "无法导入规程,请检查数据库连接")
|
|
|
|
|
|
except Exception as e:
|
|
|
QMessageBox.critical(self, "导入错误", f"导入规程时发生错误:\n{str(e)}")
|
|
|
|
|
|
def addCategory(self):
|
|
|
dialog = AddCategoryDialog(self)
|
|
|
if dialog.exec_() == QDialog.Accepted:
|
|
|
categoryName = dialog.getCategoryName()
|
|
|
if categoryName:
|
|
|
if self.db.addCategory(categoryName):
|
|
|
self.loadCategories()
|
|
|
else:
|
|
|
QMessageBox.warning(self, "添加失败", "分类名称已存在,请使用其他名称")
|
|
|
|
|
|
def deleteCurrentCategory(self):
|
|
|
currentItem = self.categoryList.currentItem()
|
|
|
if not currentItem:
|
|
|
return
|
|
|
|
|
|
categoryId = currentItem.data(Qt.UserRole)
|
|
|
categoryName = currentItem.text()
|
|
|
|
|
|
# 检查是否为默认分类
|
|
|
if categoryName == "默认分类":
|
|
|
QMessageBox.warning(
|
|
|
self,
|
|
|
"操作禁止",
|
|
|
"默认分类不可删除!"
|
|
|
)
|
|
|
return
|
|
|
|
|
|
reply = QMessageBox.question(
|
|
|
self,
|
|
|
"确认删除",
|
|
|
f"确定要删除分类 '{categoryName}' 吗?\n该分类下的规程将移动到默认分类。",
|
|
|
QMessageBox.Yes | QMessageBox.No
|
|
|
)
|
|
|
|
|
|
if reply == QMessageBox.Yes:
|
|
|
if self.db.deleteCategory(categoryId):
|
|
|
self.loadCategories()
|
|
|
self.statusBar.showMessage(f"已删除分类: {categoryName}", 5000)
|
|
|
|
|
|
def deleteSelectedProcedure(self):
|
|
|
currentItem = self.procedureList.currentItem()
|
|
|
if not currentItem:
|
|
|
return
|
|
|
|
|
|
procId = currentItem.data(Qt.UserRole)
|
|
|
procName = currentItem.text()
|
|
|
|
|
|
reply = QMessageBox.question(
|
|
|
self,
|
|
|
"确认删除",
|
|
|
f"确定要删除规程 '{procName}' 吗?",
|
|
|
QMessageBox.Yes | QMessageBox.No
|
|
|
)
|
|
|
|
|
|
if reply == QMessageBox.Yes:
|
|
|
if self.db.deleteProcedure(procId):
|
|
|
currentCategoryItem = self.categoryList.currentItem()
|
|
|
categoryId = currentCategoryItem.data(Qt.UserRole) if currentCategoryItem else None
|
|
|
self.loadProcedures(categoryId)
|
|
|
self.statusBar.showMessage(f"已删除规程: {procName}", 5000)
|
|
|
|
|
|
def hasRunningExecutor(self):
|
|
|
"""检查当前是否有正在运行的执行器 - 修正实现"""
|
|
|
for index in range(self.tabs.count()):
|
|
|
widget = self.tabs.widget(index)
|
|
|
# 修正: 只检查isRunning状态,移除对isActive的检查
|
|
|
if isinstance(widget, StepExecutor) and widget.isRunning:
|
|
|
return True
|
|
|
return False
|
|
|
|
|
|
# 新增:打开历史记录查看器
|
|
|
def openHistoryViewer(self):
|
|
|
historyViewer = HistoryViewer(self.db, self)
|
|
|
historyViewer.exec_()
|
|
|
|
|
|
def loadStylesheet(self):
|
|
|
qssPath = "Static/Procedure.qss"
|
|
|
try:
|
|
|
qssFile = QFile(qssPath) # 修改变量名
|
|
|
if qssFile.exists():
|
|
|
qssFile.open(QFile.ReadOnly | QFile.Text)
|
|
|
stream = QTextStream(qssFile)
|
|
|
stream.setCodec("UTF-8")
|
|
|
self.setStyleSheet(stream.readAll())
|
|
|
qssFile.close()
|
|
|
print(f"成功加载样式表: {qssPath}")
|
|
|
else:
|
|
|
print(f"警告:样式表文件不存在: {qssPath}")
|
|
|
except Exception as e:
|
|
|
print(f"加载样式表失败: {str(e)}")
|
|
|
|
|
|
def closeEvent(self, event):
|
|
|
self.db.close()
|
|
|
event.accept()
|
|
|
|
|
|
def procedureDragEnterEvent(self, event):
|
|
|
"""处理规程列表的拖拽进入事件"""
|
|
|
if event.mimeData().hasText():
|
|
|
event.acceptProposedAction()
|
|
|
|
|
|
def categoryDropEvent(self, event):
|
|
|
"""处理分类列表的拖放事件:将拖拽过来的规程移动到当前分类"""
|
|
|
# 获取拖拽源
|
|
|
source = event.source()
|
|
|
# 确保拖拽源是规程列表
|
|
|
if source != self.procedureList:
|
|
|
event.ignore()
|
|
|
return
|
|
|
|
|
|
# 获取选中的规程项(拖拽项)
|
|
|
items = self.procedureList.selectedItems()
|
|
|
if not items:
|
|
|
event.ignore()
|
|
|
return
|
|
|
|
|
|
# 获取目标分类项(鼠标释放位置对应的分类项)
|
|
|
pos = event.pos()
|
|
|
targetItem = self.categoryList.itemAt(pos)
|
|
|
if not targetItem:
|
|
|
event.ignore()
|
|
|
return
|
|
|
|
|
|
# 获取目标分类ID
|
|
|
targetCategoryId = targetItem.data(Qt.UserRole)
|
|
|
# 获取被拖拽的规程ID(只处理第一个选中的项)
|
|
|
procItem = items[0]
|
|
|
procId = procItem.data(Qt.UserRole)
|
|
|
procName = procItem.text()
|
|
|
|
|
|
# 更新数据库
|
|
|
if self.db.updateProcedureCategory(procId, targetCategoryId):
|
|
|
# 更新成功,重新加载目标分类的规程列表(因为规程已经移动到新分类)
|
|
|
self.loadProcedures(targetCategoryId)
|
|
|
# 新增:设置当前选中的分类为目标分类
|
|
|
self.categoryList.setCurrentItem(targetItem)
|
|
|
# 显示状态信息
|
|
|
self.statusBar.showMessage(f"已将规程 '{procName}' 移动到分类 '{targetItem.text()}'", 5000)
|
|
|
else:
|
|
|
QMessageBox.warning(self, "移动失败", "移动规程失败,请检查数据库连接")
|
|
|
|
|
|
event.accept()
|
|
|
|
|
|
# 新增:分类列表右键菜单
|
|
|
def showCategoryContextMenu(self, pos):
|
|
|
"""显示分类列表的右键菜单"""
|
|
|
item = self.categoryList.itemAt(pos)
|
|
|
if item:
|
|
|
menu = QMenu()
|
|
|
deleteAction = menu.addAction("删除分类")
|
|
|
deleteAction.triggered.connect(self.deleteCurrentCategory)
|
|
|
menu.exec_(self.categoryList.mapToGlobal(pos))
|
|
|
|
|
|
# 新增:规程列表右键菜单
|
|
|
def showProcedureContextMenu(self, pos):
|
|
|
"""显示规程列表的右键菜单"""
|
|
|
item = self.procedureList.itemAt(pos)
|
|
|
if item:
|
|
|
menu = QMenu()
|
|
|
deleteAction = menu.addAction("删除规程")
|
|
|
deleteAction.triggered.connect(self.deleteSelectedProcedure)
|
|
|
menu.exec_(self.procedureList.mapToGlobal(pos))
|