|
|
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
|
|
|
from UI.ProcedureManager.HistoryViewer import HistoryViewerWidget
|
|
|
from UI.ProcedureManager.StepExecutor import StepExecutor # 修改导入路径
|
|
|
from UI.ProcedureManager.KeywordManager import KeywordManagerWidget # 新增导入
|
|
|
from UI.ProcedureManager.ProcedureEditor import ProcedureEditor # 新增导入
|
|
|
|
|
|
|
|
|
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 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)
|
|
|
|
|
|
# 批量执行相关变量
|
|
|
self.batchExecutionQueue = [] # 批量执行队列
|
|
|
self.currentBatchIndex = 0 # 当前执行的规程索引
|
|
|
self.isBatchExecuting = False # 是否正在批量执行
|
|
|
self.batchExecutionTimer = QTimer() # 批量执行检查定时器
|
|
|
self.batchExecutionTimer.timeout.connect(self.checkBatchExecutionStatus)
|
|
|
self.currentBatchTabIndex = -1 # 当前批量执行的标签页索引
|
|
|
|
|
|
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):
|
|
|
"""锁定或解锁标签页(只用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)
|
|
|
if index == 0:
|
|
|
self.tabs.setTabEnabled(0, True)
|
|
|
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):
|
|
|
# 如果通过工具栏调用,item为None,使用当前选中项
|
|
|
currentItem = item or self.procedureList.currentItem()
|
|
|
if not currentItem:
|
|
|
return
|
|
|
|
|
|
# 检查是否有正在运行的执行器
|
|
|
if self.hasRunningExecutor():
|
|
|
QMessageBox.warning(
|
|
|
self,
|
|
|
"操作被阻止",
|
|
|
"当前有规程正在执行中,请先停止执行后再打开其他规程。"
|
|
|
)
|
|
|
return
|
|
|
|
|
|
procId = currentItem.data(Qt.UserRole) # 修改变量名
|
|
|
# print(procId, 22222222222222)
|
|
|
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)
|
|
|
|
|
|
# 修改:只在多轮次执行完成时自动重置,单轮次执行完成时不重置
|
|
|
executor.executionFinished.connect(lambda executor_instance: self.handleExecutionFinished(executor_instance))
|
|
|
|
|
|
self.statusBar.showMessage(f"已打开规程执行界面: {procName}", 3000)
|
|
|
|
|
|
def refreshProcedureList(self):
|
|
|
"""刷新规程列表"""
|
|
|
currentCategoryItem = self.categoryList.currentItem()
|
|
|
categoryId = currentCategoryItem.data(Qt.UserRole) if currentCategoryItem else None
|
|
|
self.loadProcedures(categoryId)
|
|
|
|
|
|
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)
|
|
|
|
|
|
# 添加批量执行规程动作
|
|
|
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)
|
|
|
|
|
|
# 添加关键词管理动作
|
|
|
self.keywordManageAction = QAction(
|
|
|
qta.icon('fa5s.key', color='purple'),
|
|
|
"关键词管理",
|
|
|
self
|
|
|
)
|
|
|
self.keywordManageAction.setIconText("关键词管理")
|
|
|
self.keywordManageAction.setStatusTip("管理执行步骤关键词字段库")
|
|
|
self.keywordManageAction.triggered.connect(self.openKeywordManager)
|
|
|
self.toolbar.addAction(self.keywordManageAction)
|
|
|
|
|
|
# 添加导出规程动作
|
|
|
self.exportProcedureAction = QAction(
|
|
|
qta.icon('fa5s.file-export', color='green'),
|
|
|
"导出规程",
|
|
|
self
|
|
|
)
|
|
|
self.exportProcedureAction.setIconText("导出规程")
|
|
|
self.exportProcedureAction.setStatusTip("导出规程为Excel文件")
|
|
|
self.exportProcedureAction.triggered.connect(self.exportSelectedProcedure)
|
|
|
self.toolbar.addAction(self.exportProcedureAction)
|
|
|
|
|
|
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["规程信息"]
|
|
|
procedureName = procInfo["规程名称"]
|
|
|
procedureNumber = procInfo["规程编号"]
|
|
|
|
|
|
# 检查是否存在同名规程
|
|
|
existingProcedure = self.db.getProcedureByNameAndNumber(procedureName, procedureNumber)
|
|
|
|
|
|
if existingProcedure:
|
|
|
# 存在同名规程,询问是否覆盖
|
|
|
existingId, existingName, existingNumber, existingType, existingCategoryId = existingProcedure
|
|
|
|
|
|
reply = QMessageBox.question(
|
|
|
self,
|
|
|
"发现同名规程",
|
|
|
f"发现同名规程:\n"
|
|
|
f"规程名称:{existingName}\n"
|
|
|
f"规程编号:{existingNumber}\n"
|
|
|
f"规程类型:{existingType}\n\n"
|
|
|
f"是否要覆盖这个规程?\n"
|
|
|
f"覆盖后原规程内容将丢失。",
|
|
|
QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel
|
|
|
)
|
|
|
|
|
|
if reply == QMessageBox.Cancel:
|
|
|
return
|
|
|
elif reply == QMessageBox.Yes:
|
|
|
# 覆盖现有规程
|
|
|
success = self.db.updateProcedureById(
|
|
|
existingId,
|
|
|
categoryId,
|
|
|
procedureName,
|
|
|
procedureNumber,
|
|
|
procInfo["规程类型"],
|
|
|
parsedData,
|
|
|
""
|
|
|
)
|
|
|
|
|
|
if success:
|
|
|
self.statusBar.showMessage(f"成功覆盖规程: {procedureName}", 5000)
|
|
|
self.loadProcedures(categoryId)
|
|
|
else:
|
|
|
QMessageBox.warning(self, "覆盖失败", "无法覆盖规程,请检查数据库连接")
|
|
|
return
|
|
|
else:
|
|
|
# 用户选择不覆盖
|
|
|
return
|
|
|
|
|
|
# 不存在同名规程,正常添加
|
|
|
procId = self.db.addProcedure(
|
|
|
categoryId,
|
|
|
procedureName,
|
|
|
procedureNumber,
|
|
|
procInfo["规程类型"],
|
|
|
parsedData,
|
|
|
"" # 显式传递空字符串作为report_path
|
|
|
)
|
|
|
|
|
|
if procId:
|
|
|
self.statusBar.showMessage(f"成功导入规程: {procedureName}", 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 = HistoryViewerWidget(self.db, self)
|
|
|
|
|
|
# 创建新标签页
|
|
|
tabIndex = self.tabs.addTab(historyViewer, "历史记录")
|
|
|
self.tabs.setCurrentIndex(tabIndex)
|
|
|
|
|
|
self.statusBar.showMessage("已打开历史记录查看器", 3000)
|
|
|
|
|
|
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)
|
|
|
menu = QMenu()
|
|
|
|
|
|
if item:
|
|
|
# 编辑规程
|
|
|
editAction = menu.addAction(qta.icon('fa5s.edit', color='orange'), "编辑规程")
|
|
|
editAction.triggered.connect(self.editSelectedProcedure)
|
|
|
|
|
|
# 导出规程
|
|
|
exportAction = menu.addAction(qta.icon('fa5s.file-export', color='green'), "导出规程")
|
|
|
exportAction.triggered.connect(self.exportSelectedProcedure)
|
|
|
|
|
|
menu.addSeparator()
|
|
|
|
|
|
# 删除规程
|
|
|
deleteAction = menu.addAction(qta.icon('fa5s.trash', color='red'), "删除规程")
|
|
|
deleteAction.triggered.connect(self.deleteSelectedProcedure)
|
|
|
else:
|
|
|
# 在空白区域右键时显示新建规程
|
|
|
pass
|
|
|
|
|
|
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,
|
|
|
"批量执行完成",
|
|
|
"所有规程已按顺序执行完毕!"
|
|
|
)
|
|
|
|
|
|
def handleExecutionFinished(self, executor_instance):
|
|
|
"""处理执行完成事件"""
|
|
|
# 检查是否为多轮次执行
|
|
|
if executor_instance.infiniteCycles or executor_instance.remainingCycles > 1:
|
|
|
# 多轮次执行完成,自动重置
|
|
|
QTimer.singleShot(0, lambda: executor_instance.resetExecution(fromAuto=True))
|
|
|
else:
|
|
|
# 单轮次执行完成,不自动重置,让用户手动控制
|
|
|
# 可以在这里添加其他处理逻辑,比如显示完成提示等
|
|
|
pass
|
|
|
|
|
|
def openKeywordManager(self):
|
|
|
"""打开关键词管理界面"""
|
|
|
keywordManager = KeywordManagerWidget(self.db, self)
|
|
|
|
|
|
# 创建新标签页
|
|
|
tabIndex = self.tabs.addTab(keywordManager, "关键词管理")
|
|
|
self.tabs.setCurrentIndex(tabIndex)
|
|
|
|
|
|
self.statusBar.showMessage("已打开关键词管理界面", 3000)
|
|
|
|
|
|
def editSelectedProcedure(self):
|
|
|
"""编辑选中的规程"""
|
|
|
currentItem = self.procedureList.currentItem()
|
|
|
if not currentItem:
|
|
|
QMessageBox.warning(self, "未选择规程", "请先选择一个规程进行编辑")
|
|
|
return
|
|
|
|
|
|
# 获取规程信息
|
|
|
procedureId = currentItem.data(Qt.UserRole)
|
|
|
procedureName = currentItem.text()
|
|
|
|
|
|
# 获取规程数据
|
|
|
procedureData = self.db.getProcedureContent(procedureId)
|
|
|
if not procedureData:
|
|
|
QMessageBox.warning(self, "获取规程失败", f"无法获取规程 {procedureName} 的内容")
|
|
|
return
|
|
|
|
|
|
# 创建规程编辑器
|
|
|
editor = ProcedureEditor(procedureData, procedureId, self.db, self)
|
|
|
|
|
|
# 创建新标签页
|
|
|
tabIndex = self.tabs.addTab(editor, f"编辑规程-{procedureName}")
|
|
|
self.tabs.setCurrentIndex(tabIndex)
|
|
|
|
|
|
self.statusBar.showMessage(f"已打开规程编辑器: {procedureName}", 3000)
|
|
|
|
|
|
def exportSelectedProcedure(self):
|
|
|
"""导出选中的规程"""
|
|
|
currentItem = self.procedureList.currentItem()
|
|
|
if not currentItem:
|
|
|
QMessageBox.warning(self, "未选择规程", "请先选择一个要导出的规程")
|
|
|
return
|
|
|
|
|
|
procId = currentItem.data(Qt.UserRole)
|
|
|
procedureData = self.db.getProcedureContent(procId)
|
|
|
|
|
|
if not procedureData:
|
|
|
QMessageBox.warning(self, "获取失败", "无法获取规程内容")
|
|
|
return
|
|
|
|
|
|
try:
|
|
|
# 选择保存路径
|
|
|
procName = procedureData["规程信息"]["规程名称"]
|
|
|
procNumber = procedureData["规程信息"]["规程编号"]
|
|
|
defaultName = f"{procName}_{procNumber}.xlsx"
|
|
|
|
|
|
filePath, _ = QFileDialog.getSaveFileName(
|
|
|
self, "导出规程", defaultName, "Excel文件 (*.xlsx)"
|
|
|
)
|
|
|
|
|
|
if not filePath:
|
|
|
return
|
|
|
|
|
|
# 创建Excel文件
|
|
|
from openpyxl import Workbook
|
|
|
from openpyxl.styles import Font, Alignment, PatternFill
|
|
|
from openpyxl.utils import get_column_letter
|
|
|
|
|
|
wb = Workbook()
|
|
|
ws = wb.active
|
|
|
ws.title = "测试脚本"
|
|
|
|
|
|
# 设置标题样式
|
|
|
titleFont = Font(bold=True, size=12)
|
|
|
titleAlignment = Alignment(horizontal='center', vertical='center')
|
|
|
titleFill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid")
|
|
|
|
|
|
# 写入规程信息
|
|
|
procInfo = procedureData["规程信息"]
|
|
|
ws['A1'] = "规程名称"
|
|
|
ws['B1'] = procInfo["规程名称"]
|
|
|
ws['C1'] = "规程编号"
|
|
|
ws['D1'] = procInfo["规程编号"]
|
|
|
ws['E1'] = "规程类型"
|
|
|
ws['F1'] = procInfo["规程类型"]
|
|
|
|
|
|
# 写入测试用例信息
|
|
|
testCaseInfo = procedureData["测试用例信息"]
|
|
|
ws['A2'] = "测试用例"
|
|
|
ws['B2'] = testCaseInfo["测试用例"]
|
|
|
ws['C2'] = "用例编号"
|
|
|
ws['D2'] = testCaseInfo["用例编号"]
|
|
|
ws['E2'] = "工况描述"
|
|
|
ws['F2'] = testCaseInfo["工况描述"]
|
|
|
|
|
|
# 设置表头
|
|
|
headers = ['步骤', '实验步骤', '操作类型', '预期结果', '实际结果', '一致性', '测试时间', '备注']
|
|
|
for col, header in enumerate(headers, 1):
|
|
|
cell = ws.cell(row=4, column=col, value=header)
|
|
|
cell.font = titleFont
|
|
|
cell.alignment = titleAlignment
|
|
|
cell.fill = titleFill
|
|
|
|
|
|
# 写入测试步骤
|
|
|
currentRow = 5
|
|
|
for mainStep in procedureData["测试步骤"]:
|
|
|
# 写入主步骤
|
|
|
ws.cell(row=currentRow, column=1, value=mainStep['步骤ID'])
|
|
|
ws.cell(row=currentRow, column=2, value=mainStep['步骤描述'])
|
|
|
ws.cell(row=currentRow, column=3, value=mainStep['操作类型'])
|
|
|
ws.cell(row=currentRow, column=4, value=mainStep.get('预期结果', ''))
|
|
|
currentRow += 1
|
|
|
|
|
|
# 写入子步骤
|
|
|
for subStep in mainStep.get('子步骤', []):
|
|
|
ws.cell(row=currentRow, column=1, value=subStep['序号'])
|
|
|
ws.cell(row=currentRow, column=2, value=subStep['操作'])
|
|
|
ws.cell(row=currentRow, column=3, value=subStep['操作类型'])
|
|
|
ws.cell(row=currentRow, column=4, value=subStep.get('预期结果', ''))
|
|
|
ws.cell(row=currentRow, column=5, value='') # 实际结果导出时为空
|
|
|
ws.cell(row=currentRow, column=6, value='') # 一致性导出时为空
|
|
|
ws.cell(row=currentRow, column=7, value='') # 测试时间导出时为空
|
|
|
ws.cell(row=currentRow, column=8, value='') # 备注导出时为空
|
|
|
currentRow += 1
|
|
|
|
|
|
# 调整列宽
|
|
|
for col in range(1, len(headers) + 1):
|
|
|
ws.column_dimensions[get_column_letter(col)].width = 15
|
|
|
|
|
|
# 保存文件
|
|
|
wb.save(filePath)
|
|
|
|
|
|
QMessageBox.information(self, "导出成功", f"规程已成功导出到:\n{filePath}")
|
|
|
self.statusBar.showMessage(f"规程已导出: {procName}", 5000)
|
|
|
|
|
|
except Exception as e:
|
|
|
QMessageBox.critical(self, "导出错误", f"导出规程时发生错误:\n{str(e)}")
|