You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1227 lines
50 KiB
Python

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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))