diff --git a/UI/ProcedureManager/ProcedureManager.py b/UI/ProcedureManager/ProcedureManager.py index 53ac778..c1202d9 100644 --- a/UI/ProcedureManager/ProcedureManager.py +++ b/UI/ProcedureManager/ProcedureManager.py @@ -89,6 +89,14 @@ class ProcedureManager(QMainWindow): # 添加状态栏 self.statusBar = QStatusBar() self.setStatusBar(self.statusBar) + + # 批量执行相关变量 + self.batchExecutionQueue = [] # 批量执行队列 + self.currentBatchIndex = 0 # 当前执行的规程索引 + self.isBatchExecuting = False # 是否正在批量执行 + self.batchExecutionTimer = QTimer() # 批量执行检查定时器 + self.batchExecutionTimer.timeout.connect(self.checkBatchExecutionStatus) + self.currentBatchTabIndex = -1 # 当前批量执行的标签页索引 def initUI(self): # 加载样式表 @@ -148,26 +156,21 @@ class ProcedureManager(QMainWindow): # 修改:锁定/解锁标签页方法(增加关闭按钮控制) def lockTabs(self, lock, currentIndex): - """锁定或解锁标签页""" + """锁定或解锁标签页(只用Qt原生关闭按钮方案)""" self.activeExecutorIndex = currentIndex if lock else -1 - - # 遍历所有标签页 + self.tabs.setTabsClosable(True) 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) - ) + if index > 0: + if lock and index != currentIndex: + self.tabs.tabBar().setTabButton(index, QTabBar.RightSide, None) + # 解锁时强制恢复所有关闭按钮 + if not lock: + self.tabs.setTabsClosable(False) + self.tabs.setTabsClosable(True) # 修改:打开规程执行界面(显示规程全名) def openProcedureInExecutor(self, item=None): @@ -206,6 +209,8 @@ class ProcedureManager(QMainWindow): # 切换到新添加的标签页 self.tabs.setCurrentWidget(executor) + executor.executionFinished.connect(lambda _: QTimer.singleShot(0, lambda: executor.resetExecution(fromAuto=True))) + def initProcedureManagementTab(self): """创建规程管理主标签页""" mainWidget = QWidget() @@ -318,6 +323,17 @@ class ProcedureManager(QMainWindow): self.historyAction.triggered.connect(self.openHistoryViewer) self.toolbar.addAction(self.historyAction) + # 添加批量执行规程动作 + self.batchExecuteAction = QAction( + qta.icon('fa5s.play-circle', color='green'), + "批量执行", + self + ) + self.batchExecuteAction.setIconText("批量执行") + self.batchExecuteAction.setStatusTip("按顺序批量执行当前分类中的所有规程") + self.batchExecuteAction.triggered.connect(self.batchExecuteProcedures) + self.toolbar.addAction(self.batchExecuteAction) + def loadCategories(self): self.categoryList.clear() categories = self.db.getCategories() @@ -544,3 +560,156 @@ class ProcedureManager(QMainWindow): deleteAction = menu.addAction("删除规程") deleteAction.triggered.connect(self.deleteSelectedProcedure) menu.exec_(self.procedureList.mapToGlobal(pos)) + + # 批量执行相关方法 + def batchExecuteProcedures(self): + """开始批量执行当前分类中的所有规程""" + # 检查是否有正在运行的执行器 + if self.hasRunningExecutor(): + QMessageBox.warning( + self, + "操作被阻止", + "当前有规程正在执行中,请先停止执行后再开始批量执行。" + ) + return + + # 获取当前分类的所有规程 + currentItem = self.categoryList.currentItem() + if not currentItem: + QMessageBox.warning(self, "未选择分类", "请先选择一个分类") + return + + categoryId = currentItem.data(Qt.UserRole) + procedures = self.db.getProcedures(categoryId) + + if not procedures: + QMessageBox.information(self, "无规程", "当前分类中没有规程") + return + + # 确认批量执行 + reply = QMessageBox.question( + self, + "确认批量执行", + f"确定要批量执行当前分类中的 {len(procedures)} 个规程吗?\n" + f"规程将按顺序逐个执行,每个规程执行完毕后自动开始下一个。", + QMessageBox.Yes | QMessageBox.No + ) + + if reply == QMessageBox.Yes: + # 初始化批量执行队列 + self.batchExecutionQueue = [(procId, name, number) for procId, name, number, type, createdAt in procedures] + self.currentBatchIndex = 0 + self.isBatchExecuting = True + + # 更新状态 + self.statusBar.showMessage(f"开始批量执行 {len(procedures)} 个规程", 3000) + + # 开始执行第一个规程 + self.executeNextBatchProcedure() + + def executeNextBatchProcedure(self): + """执行下一个规程""" + print(f"executeNextBatchProcedure: 批量执行={self.isBatchExecuting}, 当前索引={self.currentBatchIndex}, 队列长度={len(self.batchExecutionQueue)}") + + if not self.isBatchExecuting or self.currentBatchIndex >= len(self.batchExecutionQueue): + print("批量执行结束条件满足,调用finishBatchExecution") + self.finishBatchExecution() + return + + # 获取当前要执行的规程 + procId, name, number = self.batchExecutionQueue[self.currentBatchIndex] + print(f"准备执行规程: {name} ({number}) - {self.currentBatchIndex + 1}/{len(self.batchExecutionQueue)}") + + # 获取规程数据 + procedureData = self.db.getProcedureContent(procId) + if not procedureData: + QMessageBox.warning(self, "获取规程失败", f"无法获取规程 {name} 的内容") + self.currentBatchIndex += 1 + self.executeNextBatchProcedure() + return + + # 创建执行器 + executor = StepExecutor(procedureData, procId, self.db) + + # 设置规程全名 + procName = procedureData["规程信息"]["规程名称"] + procNumber = procedureData["规程信息"]["规程编号"] + tab_title = f"批量执行-{procName}({procNumber})" + + # 添加为新标签页 + tab_index = self.tabs.addTab(executor, tab_title) + self.currentBatchTabIndex = tab_index # 记录当前标签页索引 + print(f"创建标签页: {tab_title}, 索引={tab_index}") + + # 连接标签锁定信号 + executor.tabLockRequired.connect(lambda lock: self.lockTabs(lock, tab_index)) + + # 切换到新添加的标签页 + self.tabs.setCurrentWidget(executor) + + # 开始自动执行 + executor.startAutoExecute() + print(f"开始自动执行规程: {procName}") + + # 启动批量执行状态检查定时器(如果还没启动) + if not self.batchExecutionTimer.isActive(): + self.batchExecutionTimer.start(1000) # 每秒检查一次 + print("启动批量执行状态检查定时器") + else: + print("批量执行状态检查定时器已在运行") + + # 更新状态 + self.statusBar.showMessage(f"正在执行: {name} ({number}) - {self.currentBatchIndex + 1}/{len(self.batchExecutionQueue)}", 0) + + def checkBatchExecutionStatus(self): + """检查批量执行状态""" + if not self.isBatchExecuting: + return + + # 检查当前是否有正在运行的执行器 + has_running = self.hasRunningExecutor() + print(f"批量执行状态检查: 当前索引={self.currentBatchIndex}, 队列长度={len(self.batchExecutionQueue)}, 有运行执行器={has_running}") + + if not has_running: + # 如果没有正在运行的执行器,说明当前规程执行完成 + print(f"规程执行完成,准备执行下一个规程") + + # 确保当前标签页存在且有效 + if self.currentBatchTabIndex > 0 and self.currentBatchTabIndex < self.tabs.count(): + # 关闭当前执行器标签页 + self.tabs.removeTab(self.currentBatchTabIndex) + self.currentBatchTabIndex = -1 # 重置标签页索引 + print(f"已关闭标签页,索引={self.currentBatchTabIndex}") + + # 移动到下一个规程 + self.currentBatchIndex += 1 + print(f"移动到下一个规程,新索引={self.currentBatchIndex}") + + if self.currentBatchIndex >= len(self.batchExecutionQueue): + # 所有规程执行完成 + print("所有规程执行完成") + self.finishBatchExecution() + else: + # 延迟一下再执行下一个规程,让用户看到执行结果 + print(f"延迟2秒后执行下一个规程: {self.currentBatchIndex + 1}/{len(self.batchExecutionQueue)}") + # 停止当前定时器,避免重复检查 + self.batchExecutionTimer.stop() + QTimer.singleShot(2000, self.executeNextBatchProcedure) + + def finishBatchExecution(self): + """完成批量执行""" + self.isBatchExecuting = False + self.batchExecutionQueue = [] + self.currentBatchIndex = 0 + self.currentBatchTabIndex = -1 # 重置标签页索引 + self.batchExecutionTimer.stop() + + # 更新状态 + self.statusBar.showMessage("批量执行完成", 5000) + + # 显示完成消息 + QMessageBox.information( + self, + "批量执行完成", + "所有规程已按顺序执行完毕!" + ) diff --git a/UI/ProcedureManager/StepExecutor.py b/UI/ProcedureManager/StepExecutor.py index 421371a..5924dd5 100644 --- a/UI/ProcedureManager/StepExecutor.py +++ b/UI/ProcedureManager/StepExecutor.py @@ -7,7 +7,7 @@ from PyQt5.QtWidgets import (QApplication, QMainWindow, QTableView, QLabel, QCheckBox, QSpinBox, QMenu, QFileDialog, QTabWidget, QTabBar, QListWidget, QListWidgetItem, QDialog, QLineEdit, QFormLayout, QDialogButtonBox, QMessageBox, - QHeaderView, QToolBar, QAction, QStatusBar, QComboBox, QSplitter, QAbstractItemView) + QHeaderView, QToolBar, QAction, QStatusBar, QComboBox, QSplitter, QAbstractItemView, QDoubleSpinBox) from docx import Document from docx.shared import Pt, RGBColor from docx.enum.text import WD_PARAGRAPH_ALIGNMENT @@ -18,12 +18,15 @@ from datetime import datetime # 导入其他模块 from model.ProcedureModel.ProcedureProcessor import ExcelParser, StepTableModel from utils.DBModels.ProcedureModel import DatabaseManager +from utils import Globals # 修改导入路径 class StepExecutor(QWidget): # 添加标签锁定信号 tabLockRequired = pyqtSignal(bool) + # 添加执行完成信号 + executionFinished = pyqtSignal(object) # 发送执行器实例 def __init__(self, procedureData, procedureId, dbManager): super().__init__() @@ -44,6 +47,14 @@ class StepExecutor(QWidget): self.isFirstRun = True # 新增标志位,用于区分首次执行 self.stepResults = [] # 新增:存储所有步骤执行结果的列表 + # 新增:倒计时相关变量 + self.countdownTimer = QTimer() + self.countdownTimer.timeout.connect(self.updateCountdown) + self.remainingTime = 0 # 当前轮次剩余时间(秒) + self.totalSteps = 0 # 当前轮次总步骤数 + + self.protocolManage = Globals.getValue("protocolManage") + def initUi(self, testSteps): layout = QVBoxLayout() @@ -65,11 +76,18 @@ class StepExecutor(QWidget): 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) # 新增:备注列自适应宽度 + # QHeaderView保护 + hh = self.tableView.horizontalHeader() + if hh: + hh.setSectionResizeMode(0, QHeaderView.ResizeToContents) + hh.setSectionResizeMode(1, QHeaderView.Stretch) + hh.setSectionResizeMode(2, QHeaderView.ResizeToContents) + hh.setSectionResizeMode(3, QHeaderView.ResizeToContents) + hh.setSectionResizeMode(5, QHeaderView.Stretch) + vh = self.tableView.verticalHeader() + if vh: + self.tableView.setWordWrap(True) + vh.setSectionResizeMode(QHeaderView.ResizeToContents) layout.addWidget(QLabel("测试步骤:")) layout.addWidget(self.tableView) @@ -95,7 +113,7 @@ class StepExecutor(QWidget): self.resetButton.setIcon(qta.icon('fa5s.redo', color='orange')) self.exportButton = QPushButton(" 生成报告") - self.exportButton.clicked.connect(self.generateReport) + self.exportButton.clicked.connect(self.onExportReportClicked) self.exportButton.setIcon(qta.icon('fa5s.file-alt', color='purple')) control_layout.addWidget(self.autoButton) @@ -115,6 +133,27 @@ class StepExecutor(QWidget): self.infiniteCheckbox = QCheckBox("无限循环") cycle_layout.addWidget(self.infiniteCheckbox) + + # 新增:状态显示标签 + self.statusLabel = QLabel("就绪") + self.statusLabel.setStyleSheet("color: blue; font-weight: bold;") + cycle_layout.addWidget(self.statusLabel) + + # 新增:倒计时显示标签 + self.countdownLabel = QLabel("") + self.countdownLabel.setStyleSheet("color: red; font-weight: bold; font-size: 14px;") + cycle_layout.addWidget(self.countdownLabel) + + # 新增:步骤间隔时间设置 + cycle_layout.addWidget(QLabel("步骤间隔(秒):")) + self.stepIntervalSpin = QDoubleSpinBox() + self.stepIntervalSpin.setRange(0, 60.0) + self.stepIntervalSpin.setValue(1) + self.stepIntervalSpin.setDecimals(2) # 支持一位小数 + self.stepIntervalSpin.setSingleStep(0.1) # 每次调整0.1秒 + self.stepIntervalSpin.setToolTip("设置步骤执行计时器的间隔时间(秒)") + cycle_layout.addWidget(self.stepIntervalSpin) + cycle_layout.addStretch() # 将所有布局添加到主布局 @@ -135,17 +174,20 @@ class StepExecutor(QWidget): 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) + self.toolbar.addAction(qta.icon('fa5s.file-alt', color='purple'), "生成报告", self.onExportReportClicked) 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())) + if jumpAction: + 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)) + if detailAction: + detailAction.triggered.connect(lambda: self.showStepDetail(index.row())) + if hasattr(self.tableView, 'viewport') and hasattr(self.tableView.viewport(), 'mapToGlobal'): + menu.exec_(self.tableView.viewport().mapToGlobal(pos)) def jumpExecute(self, rowIndex): if 0 <= rowIndex < self.tableModel.rowCount(): @@ -171,11 +213,11 @@ class StepExecutor(QWidget): 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")) + 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) + form_layout.addRow("执行结果", QLabel(status)) layout.addLayout(form_layout) @@ -186,6 +228,11 @@ class StepExecutor(QWidget): detail_dialog.setLayout(layout) detail_dialog.exec_() + def updateStatusDisplay(self, message, color="blue"): + """更新状态显示""" + self.statusLabel.setText(message) + self.statusLabel.setStyleSheet(f"color: {color}; font-weight: bold;") + def startAutoExecute(self): self.isRunning = True self.isActive = True @@ -202,10 +249,21 @@ class StepExecutor(QWidget): self.stepResults = [] # 重置步骤结果集合 self.tableModel.resetExecutionState() self.isFirstRun = False # 标记已执行过 + # 创建第一个执行记录 + self.createNewExecutionRecord() self.remainingCycles = self.cycleSpin.value() self.infiniteCycles = self.infiniteCheckbox.isChecked() - self.timer.start(1000) + + # 使用用户设置的步骤间隔时间启动计时器 + stepInterval = int(self.stepIntervalSpin.value() * 1000) # 转换为毫秒 + self.timer.start(stepInterval) + + # 更新状态显示 + self.updateStatusDisplay(f"开始执行 - 第1轮", "green") + + # 开始倒计时(在第一个步骤执行前) + self.startCountdown() def stopAutoExecute(self): self.isRunning = False # 清除自动执行状态 @@ -216,9 +274,28 @@ class StepExecutor(QWidget): self.resetButton.setEnabled(True) self.exportButton.setEnabled(True) # 注意: 这里不重置isActive,因为执行器仍处于激活状态 - # 执行结束时更新完整步骤结果到数据库 - if hasattr(self, 'current_execution_id'): + # 执行结束时保存当前轮次的步骤结果到数据库 + if hasattr(self, 'current_execution_id') and self.stepResults: + import json + try: + json.dumps(self.stepResults) + except Exception as e: + print("stepResults不能序列化", self.stepResults) + raise self.dbManager.updateStepResults(self.current_execution_id, self.stepResults) + print(f"执行停止,当前轮次结果已保存") + + # 更新状态显示 + self.updateStatusDisplay("执行已停止", "orange") + + # 停止倒计时 + self.stopCountdown() + + # 发送执行完成信号 + self.executionFinished.emit(self) + # 执行完毕后自动解锁标签页 + self.tabLockRequired.emit(False) + # self.resetExecution() # 自动执行完毕后自动重置 def autoExecuteStep(self): if self.currentIndex < self.tableModel.rowCount(): @@ -226,7 +303,21 @@ class StepExecutor(QWidget): if step_info and not step_info['isMain']: self.executeStep(self.currentIndex) self.currentIndex += 1 + + # 步骤执行完成后,更新倒计时 + self.startCountdown() else: + # 当前轮次执行完成,立即存储执行结果 + if hasattr(self, 'current_execution_id') and self.stepResults: + import json + try: + json.dumps(self.stepResults) + except Exception as e: + print("stepResults不能序列化", self.stepResults) + raise + self.dbManager.updateStepResults(self.current_execution_id, self.stepResults) + print(f"第 {self.getCurrentCycleNumber()} 轮执行完成,结果已存储") + if self.infiniteCycles or self.remainingCycles > 0: if not self.infiniteCycles: self.remainingCycles -= 1 @@ -235,14 +326,49 @@ class StepExecutor(QWidget): should_continue = self.infiniteCycles or self.remainingCycles > 0 if should_continue: - self.tableModel.resetAll() - if hasattr(self, 'current_execution_id'): - self.dbManager.updateStepResults(self.current_execution_id, self.stepResults) - self.timer.start() + # 开始新一轮执行前,创建新的执行记录 + self.createNewExecutionRecord() + self.tableModel.resetAll() # 确保每轮自动清除颜色 + self.stepResults = [] # 清空步骤结果,准备新一轮 + + # 更新状态显示 + cycle_num = self.getCurrentCycleNumber() + self.updateStatusDisplay(f"执行中 - 第{cycle_num}轮", "green") + + # 重新开始倒计时 + self.startCountdown() + + # 使用用户设置的步骤间隔时间重新启动计时器 + stepInterval = int(self.stepIntervalSpin.value() * 1000) # 转换为毫秒 + self.timer.start(stepInterval) else: self.stopAutoExecute() + + # 发送执行完成信号 + self.executionFinished.emit(self) else: self.stopAutoExecute() + + # 发送执行完成信号 + self.executionFinished.emit(self) + + def createNewExecutionRecord(self): + """创建新的执行记录""" + execution_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + self.current_execution_id = self.dbManager.insertProcedureExecution( + self.procedureId, + execution_time + ) + print(f"创建新的执行记录,ID: {self.current_execution_id}") + + def getCurrentCycleNumber(self): + """获取当前执行的轮次数""" + if self.infiniteCycles: + return "无限" + else: + total_cycles = self.cycleSpin.value() + completed_cycles = total_cycles - self.remainingCycles + return f"{completed_cycles + 1}/{total_cycles}" def executeNextStep(self): self.isActive = True @@ -259,40 +385,23 @@ class StepExecutor(QWidget): 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'], + 'step_id': str(stepInfo.get('stepId', '')), + 'step_description': str(stepInfo.get('description', '')), 'execution_time': execution_time, - 'result': result + 'result': bool(result) } self.stepResults.append(step_result) - - # 更新数据库中的步骤结果集合 - self.dbManager.updateStepResults(self.current_execution_id, self.stepResults) - + if hasattr(self, 'current_execution_id'): + self.dbManager.updateStepResults(self.current_execution_id, self.stepResults) success = self.tableModel.updateStepResult(row, result, datetime.now()) - return result def handleStep(self, rowIndex, stepInfo): # 修改参数名 @@ -302,10 +411,10 @@ class StepExecutor(QWidget): print(f"处理步骤 {stepId}: {description}") if "设置" in description: - return self.performLogin(stepId, description) # 修改方法名 - elif "数据导入" in description: + return self.performWrite(stepId, description) # 修改方法名 + elif "检查" in description: return self.performDataImport(stepId, description) # 修改方法名 - elif "验证" in description: + elif "接收" in description: return self.performValidation(stepId, description) # 修改方法名 elif "导出" in description: return self.performExport(stepId, description) # 修改方法名 @@ -318,7 +427,7 @@ class StepExecutor(QWidget): import random return random.random() < 0.9 - def performLogin(self, stepId, description): # 修改方法名 + def performWrite(self, stepId, description): # 修改方法名 print(f"执行登录操作 (步骤 {stepId}): {description}") return True @@ -355,136 +464,104 @@ class StepExecutor(QWidget): def performSetting(self, stepId, description): # 修改方法名 return True - def resetExecution(self): + def resetExecution(self, fromAuto=False): self.tableModel.resetAll() self.currentIndex = 0 - self.stopAutoExecute() + if not fromAuto: + self.stopAutoExecute() self.cycleSpin.setValue(1) self.infiniteCheckbox.setChecked(False) self.isRunning = False self.isActive = False self.isFirstRun = True # 重置标志位 - self.stepResults = [] # 重置步骤结果集合 - - # 新增:重置当前执行ID + self.stepResults = [] # 重置步骤结果集合,禁止赋值为tableModel.stepData if hasattr(self, 'current_execution_id'): del self.current_execution_id - - # 发送标签解锁信号 self.tabLockRequired.emit(False) + self.updateStatusDisplay("已重置", "blue") + self.resetCountdown() - # 修改方法签名,添加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 + def onExportReportClicked(self): + self.generateReport() + + def startCountdown(self): + """开始倒计时""" + # 计算当前轮次的总步骤数(只计算子步骤) + self.totalSteps = sum(1 for step in self.tableModel.stepData if not step['isMain']) + + # 计算剩余步骤数(从当前索引开始到结束) + remainingSteps = 0 + for i in range(self.currentIndex, len(self.tableModel.stepData)): + if not self.tableModel.stepData[i]['isMain']: + remainingSteps += 1 + + # 获取步骤间隔时间 + stepInterval = self.stepIntervalSpin.value() + + # 计算当前轮次的剩余时间(剩余步骤数 * 步骤间隔时间) + if remainingSteps > 0: + self.remainingTime = int(remainingSteps * stepInterval) # 转换为整数秒 + self.countdownTimer.start(1000) # 每秒更新一次 + self.updateCountdown() + else: + # 如果没有剩余步骤,清空显示 + self.countdownLabel.setText("") + + def stopCountdown(self): + """停止倒计时""" + self.countdownTimer.stop() + self.countdownLabel.setText("") + + def updateCountdown(self): + """更新倒计时显示""" + if self.remainingTime > 0: + minutes = self.remainingTime // 60 + seconds = self.remainingTime % 60 - # 执行结果(带颜色标记) - 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) # 红色 + if minutes > 0: + countdown_text = f"当前轮次剩余: {minutes:02d}:{seconds:02d}" else: - result_text = '未执行' - result_cell.text = result_text + countdown_text = f"当前轮次剩余: {seconds}秒" - # 状态 - 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" + # 根据剩余时间调整颜色 + if self.remainingTime <= 10: + self.countdownLabel.setStyleSheet("color: red; font-weight: bold; font-size: 14px;") + elif self.remainingTime <= 30: + self.countdownLabel.setStyleSheet("color: orange; font-weight: bold; font-size: 14px;") + else: + self.countdownLabel.setStyleSheet("color: blue; font-weight: bold; font-size: 14px;") + + self.countdownLabel.setText(countdown_text) + self.remainingTime -= 1 else: - filename = file_path - - doc.save(filename) - - # 确保导出成功时显示提示信息 - QMessageBox.information(self, "报告生成", f"报告已生成: {filename}") - return filename \ No newline at end of file + self.countdownLabel.setText("当前轮次完成") + self.countdownLabel.setStyleSheet("color: green; font-weight: bold; font-size: 14px;") + self.countdownTimer.stop() + + def updateCycleCountdown(self): + """更新轮次剩余时间倒计时 - 已废弃,保留兼容性""" + self.updateCountdown() + + def resetCountdown(self): + """重置倒计时""" + self.stopCountdown() + self.remainingTime = 0 + self.totalSteps = 0 + + def isExecutionCompleted(self): + """检查规程是否执行完成""" + # 检查是否所有步骤都执行完成 + return self.currentIndex >= self.tableModel.rowCount() + + def getExecutionProgress(self): + """获取执行进度""" + total_steps = self.tableModel.rowCount() + completed_steps = self.currentIndex + return completed_steps, total_steps + + def showEvent(self, event): + super().showEvent(event) + # 只在第一次显示时调整行高 + if not hasattr(self, '_hasResizedRows'): + self.tableView.resizeRowsToContents() + self._hasResizedRows = True \ No newline at end of file diff --git a/model/ProcedureModel/ProcedureProcessor.py b/model/ProcedureModel/ProcedureProcessor.py index a85ecf7..fdc3d86 100644 --- a/model/ProcedureModel/ProcedureProcessor.py +++ b/model/ProcedureModel/ProcedureProcessor.py @@ -45,7 +45,7 @@ class ExcelParser: stepCounter += 1 stepName = str(cellA).replace(':', ':').strip() - stepDesc = str(cellB).replace('\n', ' ').strip() if cellB else "" + stepDesc = str(cellB) if cellB else "" currentStep = { "步骤ID": stepName, @@ -57,7 +57,7 @@ class ExcelParser: elif currentStep and cellA and str(cellA).isdigit(): subStep = { "序号": int(cellA), - "操作": str(cellB).replace('\n', ' ').strip() if cellB else "", + "操作": str(cellB) if cellB else "", "操作类型": cellC if cellC else "", "预期结果": sheet[f'D{rowIdx}'].value, "实际结果": sheet[f'E{rowIdx}'].value, @@ -153,6 +153,14 @@ class StepTableModel(QAbstractTableModel): font.setBold(True) return font + # 新增:支持自动换行 + elif role == Qt.TextAlignmentRole: + if col in [1, 5]: # 描述和备注列 + return Qt.AlignLeft | Qt.AlignVCenter + elif role == Qt.TextWordWrap: + if col in [1, 5]: + return True + return None def headerData(self, section, orientation, role): diff --git a/protocol/ProtocolManage.py b/protocol/ProtocolManage.py index 1f640c1..4923eb0 100644 --- a/protocol/ProtocolManage.py +++ b/protocol/ProtocolManage.py @@ -3,7 +3,7 @@ from utils.DBModels.ProtocolModel import ( HartVar, TcRtdVar, AnalogVar, HartSimulateVar ) -from protocol.TCP.TCPVarManage import TCPVarManager +from protocol.TCP.TCPVarManage import * from protocol.TCP.TemToMv import temToMv class ProtocolManage(object): @@ -20,6 +20,7 @@ class ProtocolManage(object): self.tcpVarManager = TCPVarManager('127.0.0.1', 8000) self.writeTC = [0] * 8 self.writeRTD = [0] * 8 + @classmethod def lookupVariable(cls, variableName): @@ -91,22 +92,29 @@ class ProtocolManage(object): channel = int(info['channelNumber']) - 1 varType = info['varType'] compensationVar = float(info['compensationVar']) + varModel = info['varModel'] + model = self.getModelType(varModel) # print(value + compensationVar) - if varType in ['R', 'S', 'B', 'J', 'T', 'E', 'K', 'N', 'C', 'A', 'PT100']: - mvΩvalue = temToMv(varType, value + compensationVar) # 直接补偿温度 补偿mv调整到括号外 - self.tcpVarManager.writeValue(varType, channel, mvΩvalue) - if varType == 'PT100': - self.wrtieRTD[channel] = value - else: - self.writeTC[channel] = value + if model == localModel: + if varType == 'PT100': + self.writeRTD[channel] = value + else: + self.writeTC[channel] = value + if varType in ['R', 'S', 'B', 'J', 'T', 'E', 'K', 'N', 'C', 'A', 'PT100'] and model != SimModel: + value = temToMv(varType, value + compensationVar) # 直接补偿温度 补偿mv调整到括号外 + self.tcpVarManager.writeValue(varType, channel, value, model=model) + # 模拟量变量处理 elif modelType == 'AnalogVar': channel = int(info['channelNumber']) - 1 varType = info['varType'] - if info['varType'] == 'AO': + varModel = info['varModel'] + model = self.getModelType(varModel) + if info['varType'] in ['AI','AO']: value = self.getRealAO(value, info['max'], info['min']) - self.tcpVarManager.writeValue(varType, channel, value) + self.tcpVarManager.writeValue(varType, channel, value, model=model) + # print(1) # HART模拟变量处理 @@ -157,18 +165,29 @@ class ProtocolManage(object): elif modelType == 'TcRtdVar': channel = int(info['channelNumber']) - 1 varType = info['varType'] - if varType == 'PT100': - value = self.writeRTD[channel] - elif varType in ['R', 'S', 'B', 'J', 'T', 'E', 'K', 'N', 'C', 'A']: - value = self.writeTC[channel] - return value + varModel = info['varModel'] + model = self.getModelType(varModel) + if model == SimModel: + if varType == 'PT100': + value = self.tcpVarManager.simRTDData[channel] + elif varType in ['R', 'S', 'B', 'J', 'T', 'E', 'K', 'N', 'C', 'A']: + value = self.tcpVarManager.simTCData[channel] + return value + else: + if varType == 'PT100': + value = self.writeRTD[channel] + elif varType in ['R', 'S', 'B', 'J', 'T', 'E', 'K', 'N', 'C', 'A']: + value = self.writeTC[channel] + return value # 模拟量变量处理 elif modelType == 'AnalogVar': channel = int(info['channelNumber']) - 1 varType = info['varType'] # print(varType, channel) - value = self.tcpVarManager.readValue(varType, channel) + varModel = info['varModel'] + model = self.getModelType(varModel) + value = self.tcpVarManager.readValue(varType, channel, model=model) if varType in ['AI','AO']: value = self.getRealAI(value, info['max'], info['min']) return value @@ -215,3 +234,11 @@ class ProtocolManage(object): except Exception as e: print(f"工程值转换失败: {str(e)}") return 0.0 # 默认返回0避免中断流程 + + def getModelType(self, varModel): + if varModel == '本地值': + return localModel + elif varModel == '远程值': + return NetModel + elif varModel == '模拟值': + return SimModel diff --git a/protocol/TCP/TCPVarManage.py b/protocol/TCP/TCPVarManage.py index aea9fd6..9a99003 100644 --- a/protocol/TCP/TCPVarManage.py +++ b/protocol/TCP/TCPVarManage.py @@ -7,6 +7,10 @@ FPGATrigger = 1 TCTrigger = 2 RTDTrigger = 3 +localModel = 1 +NetModel = 2 +SimModel = 3 + class TCPVarManager: def __init__(self, host, port): @@ -24,6 +28,13 @@ class TCPVarManager: self.AIDATA = [0.0] * 8 self.DIDATA = [0] * 16 self.DELTATDATA = [0] * 16 + + self.simAOData = [0.004] * 16 + self.simDOData = [0] * 16 + self.simTCData = [0.0] * 8 # mv + self.simRTDData = [0.0] * 8 # Ω + self.simAIata = [0.0] * 8 + self.simDIata = [0] * 16 self.startPeriodicRead() def startTimeTest(self, triggerType, time = 2000): @@ -122,12 +133,12 @@ class TCPVarManager: try: # 将DO状态转换为单个浮点数(协议要求) - do_value = 0 + doValue = 0 for i, state in enumerate(self.DODATA): - do_value |= (state << i) + doValue |= (state << i) # 替换data中第20个元素为DO状态值 - data[19] = float(do_value) + data[19] = float(doValue) success = self.communicator.writeAo(data) if not success: @@ -152,15 +163,39 @@ class TCPVarManager: return None - def writeValue(self, variableType, channel, value, trigger=None): + def writeValue(self, variableType, channel, value, trigger=None, model = localModel): if variableType == "AO": - self.AODATA[channel] = float(value) + if model == SimModel: + self.simAOData[channel] = float(value) + return + else: + self.AODATA[channel] = float(value) elif variableType == "DO": - self.DODATA[channel] = int(value) + if model == SimModel: + self.simDOData[channel] = int(value) + return + else: + self.DODATA[channel] = int(value) elif variableType in ['R', 'S', 'B', 'J', 'T', 'E', 'K', 'N', 'C', 'A']: - self.TCDATA[channel] = float(value) + if model == SimModel: + self.simTCData[channel] = float(value) + return + else: + self.TCDATA[channel] = float(value) elif variableType == "PT100": - self.RTDDATA[channel] = float(value) + if model == SimModel: + self.simRTDData[channel] = float(value) + return + else: + self.RTDDATA[channel] = float(value) + elif variableType == "AI": + if model == SimModel: + self.simAIata[channel] = float(value) + return + elif variableType == "DI": + if model == SimModel: + self.simDIata[channel] = int(value) + return if not trigger: data = [ @@ -188,17 +223,29 @@ class TCPVarManager: # print(data) self.writeData(data) - def readValue(self, variableType, channel): + def readValue(self, variableType, channel, model = localModel): # channel = channel if variableType == "AI": # print(self.AIDATA) - return self.AIDATA[channel] + if model == SimModel: + return self.simAIata[channel] + else: + return self.AIDATA[channel] elif variableType == "DI": - return self.DIDATA[channel] + if model == SimModel: + return self.simDIata[channel] + else: + return self.DIDATA[channel] elif variableType == "AO": - return self.AODATA[channel] + if model == SimModel: + return self.simAOData[channel] + else: + return self.AODATA[channel] elif variableType == "DO": - return self.DODATA[channel] + if model == SimModel: + return self.simDOData[channel] + else: + return self.DODATA[channel]