From 55eb458d5b53432e5c760a1bfd5bf7101a67df2c Mon Sep 17 00:00:00 2001 From: zcwBit Date: Mon, 16 Jun 2025 14:12:06 +0800 Subject: [PATCH] =?UTF-8?q?0616=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Static/Procedure.qss | 652 +++++++++++ UI/Main/Main.py | 26 +- UI/Main/MainLeft.py | 14 +- UI/ProcedureManager/ProcedureManager.py | 1226 ++++++++++++++++++++ UI/ProfibusWidgets/AreaTabWidget.py | 2 +- model/ProcedureModel/ProcedureProcessor.py | 204 ++++ utils/DBModels/ProcedureModel.py | 302 +++++ 7 files changed, 2411 insertions(+), 15 deletions(-) create mode 100644 Static/Procedure.qss create mode 100644 UI/ProcedureManager/ProcedureManager.py create mode 100644 model/ProcedureModel/ProcedureProcessor.py create mode 100644 utils/DBModels/ProcedureModel.py diff --git a/Static/Procedure.qss b/Static/Procedure.qss new file mode 100644 index 0000000..93d3be1 --- /dev/null +++ b/Static/Procedure.qss @@ -0,0 +1,652 @@ +/* 规程管理系统 - 白色浅蓝色QSS样式 */ + +/* ==================== 全局样式 ==================== */ +QWidget { + background-color: #ffffff; + color: #2c3e50; + font-family: "Microsoft YaHei", "Segoe UI", Arial, sans-serif; + font-size: 9pt; + selection-background-color: #3498db; + selection-color: #ffffff; +} + +/* ==================== 主窗口 ==================== */ +QMainWindow { + background-color: #f8f9fa; + border: none; +} + +QMainWindow::separator { + background-color: #bdc3c7; + width: 2px; + height: 2px; +} + +/* ==================== 工具栏 ==================== */ +QToolBar { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #ecf0f1, stop: 1 #d5dbdb); + border: none; + spacing: 3px; + padding: 5px; + border-bottom: 2px solid #3498db; +} + +QToolBar::handle { + background: #bdc3c7; + width: 8px; + margin: 2px; +} + +QToolBar QToolButton { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #ffffff, stop: 1 #ecf0f1); + border: 1px solid #bdc3c7; + border-radius: 6px; + padding: 8px 12px; + margin: 2px; + color: #2c3e50; + font-weight: bold; +} + +QToolBar QToolButton:hover { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #e8f4fd, stop: 1 #d6eaff); + border: 1px solid #3498db; + box-shadow: 0 2px 4px rgba(52, 152, 219, 0.3); +} + +QToolBar QToolButton:pressed { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #d6eaff, stop: 1 #c3e2ff); + border: 1px solid #2980b9; +} + +QToolBar QToolButton:disabled { + background: #f5f6fa; + color: #95a5a6; + border: 1px solid #d5dbdb; +} + +/* ==================== 状态栏 ==================== */ +QStatusBar { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #ecf0f1, stop: 1 #d5dbdb); + border-top: 1px solid #bdc3c7; + color: #34495e; + padding: 3px; +} + +QStatusBar::item { + border: none; +} + +/* ==================== 按钮 ==================== */ +QPushButton { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #ffffff, stop: 1 #ecf0f1); + border: 1px solid #bdc3c7; + border-radius: 8px; + padding: 10px 16px; + font-weight: bold; + color: #2c3e50; + min-width: 80px; + min-height: 30px; +} + +QPushButton:hover { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #e8f4fd, stop: 1 #d6eaff); + border: 1px solid #3498db; + box-shadow: 0 2px 8px rgba(52, 152, 219, 0.4); +} + +QPushButton:pressed { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #d6eaff, stop: 1 #c3e2ff); + border: 1px solid #2980b9; +} + +QPushButton:disabled { + background: #f5f6fa; + color: #95a5a6; + border: 1px solid #d5dbdb; +} + +/* 特殊按钮颜色 */ +QPushButton[text*="开始"], QPushButton[text*="执行"] { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #58d68d, stop: 1 #27ae60); + border: 1px solid #2ecc71; + color: #ffffff; +} + +QPushButton[text*="开始"]:hover, QPushButton[text*="执行"]:hover { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #7dcea0, stop: 1 #58d68d); + box-shadow: 0 2px 8px rgba(46, 204, 113, 0.4); +} + +QPushButton[text*="停止"], QPushButton[text*="删除"] { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #ec7063, stop: 1 #e74c3c); + border: 1px solid #e67e22; + color: #ffffff; +} + +QPushButton[text*="停止"]:hover, QPushButton[text*="删除"]:hover { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #f1948a, stop: 1 #ec7063); + box-shadow: 0 2px 8px rgba(231, 76, 60, 0.4); +} + +QPushButton[text*="重置"] { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #f7dc6f, stop: 1 #f39c12); + border: 1px solid #f1c40f; + color: #ffffff; +} + +QPushButton[text*="重置"]:hover { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #f8e6a0, stop: 1 #f7dc6f); + box-shadow: 0 2px 8px rgba(243, 156, 18, 0.4); +} + +QPushButton[text*="报告"], QPushButton[text*="导入"] { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #bb8fce, stop: 1 #9b59b6); + border: 1px solid #af7ac5; + color: #ffffff; +} + +QPushButton[text*="报告"]:hover, QPushButton[text*="导入"]:hover { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #d2b4de, stop: 1 #bb8fce); + box-shadow: 0 2px 8px rgba(155, 89, 182, 0.4); +} + +/* ==================== 标签 ==================== */ +QLabel { + color: #2c3e50; + background: transparent; + font-weight: normal; +} + +QLabel[text="规程分类"], QLabel[text="规程列表"], QLabel[text="测试步骤:"] { + font-size: 11pt; + font-weight: bold; + color: #3498db; + padding: 5px 0px; +} + +/* ==================== 表格视图 ==================== */ +QTableView { + background-color: #ffffff; + alternate-background-color: #f8f9fa; + gridline-color: #d5dbdb; + border: 1px solid #bdc3c7; + border-radius: 8px; + selection-background-color: #3498db; + selection-color: #ffffff; +} + + + +QHeaderView::section { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #ffffff, stop: 1 #ecf0f1); + color: #2c3e50; + padding: 10px 8px; + border: none; + border-right: 1px solid #bdc3c7; + border-bottom: 2px solid #3498db; + font-weight: bold; +} + +QHeaderView::section:first { + border-top-left-radius: 8px; +} + +QHeaderView::section:last { + border-top-right-radius: 8px; + border-right: none; +} + +QHeaderView::section:hover { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #e8f4fd, stop: 1 #d6eaff); +} + +/* ==================== 列表控件 ==================== */ +QListWidget { + background-color: #ffffff; + border: 1px solid #bdc3c7; + border-radius: 8px; + padding: 5px; + outline: none; +} + +QListWidget::item { + background-color: transparent; + border: none; + border-radius: 4px; + padding: 10px 8px; + margin: 2px; + color: #2c3e50; +} + +QListWidget::item:hover { + background-color: #e8f4fd; + border: 1px solid #3498db; +} + +QListWidget::item:selected { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #3498db, stop: 1 #2980b9); + color: #ffffff; + border: 1px solid #5dade2; +} + +/* ==================== 选项卡 ==================== */ +QTabWidget::pane { + border: 1px solid #bdc3c7; + border-radius: 8px; + background-color: #ffffff; + top: -1px; +} + +QTabBar::tab { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #ffffff, stop: 1 #ecf0f1); + border: 1px solid #bdc3c7; + padding: 10px 20px; + margin-right: 2px; + border-top-left-radius: 8px; + border-top-right-radius: 8px; + color: #34495e; +} + +QTabBar::tab:selected { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #3498db, stop: 1 #2980b9); + color: #ffffff; + border-bottom-color: #ffffff; +} + +QTabBar::tab:hover:!selected { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #e8f4fd, stop: 1 #d6eaff); +} + +/* ==================== 输入控件 ==================== */ +QLineEdit { + background-color: #ffffff; + border: 2px solid #bdc3c7; + border-radius: 6px; + padding: 8px 12px; + color: #2c3e50; + font-size: 9pt; +} + +QLineEdit:focus { + border: 2px solid #3498db; + background-color: #fdfdfe; + box-shadow: 0 0 8px rgba(52, 152, 219, 0.3); +} + +QLineEdit:hover { + border: 2px solid #3498db; +} + +QSpinBox { + background-color: #ffffff; + border: 2px solid #bdc3c7; + border-radius: 6px; + padding: 8px; + color: #2c3e50; + min-width: 60px; +} + +QSpinBox:focus { + border: 2px solid #3498db; + background-color: #fdfdfe; +} + +QSpinBox::up-button, QSpinBox::down-button { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #ffffff, stop: 1 #ecf0f1); + border: 1px solid #bdc3c7; + width: 20px; +} + +QSpinBox::up-button:hover, QSpinBox::down-button:hover { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #e8f4fd, stop: 1 #d6eaff); +} + +QSpinBox::up-arrow { + image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAGCAYAAAD37n+BAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAAdgAAAHYBTnsmCAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAABHSURBVBiVY/z//z8DJYCJgUIwqmFUw6gGcjWQ5QdqaKBYA7ma/lOggWwN5PqBGhoo1kCuJnI1/KdAA9ka/lOggVwN5PoBAJZzBTcQ48y8AAAAAElFTkSuQmCC); + width: 8px; + height: 4px; +} + +QSpinBox::down-arrow { + image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAGCAYAAAD37n+BAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAAdgAAAHYBTnsmCAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAABHSURBVBiVY2D4/58BFWBioBCMahgYDf8p0EC2hv8UaCBbA7ma/lOggWwN5Gqipwayff+fAg1ka/hPgQayNZCriboGAJd7BTc3gZfyAAAAAElFTkSuQmCC); + width: 8px; + height: 4px; +} + +/* ==================== 复选框 ==================== */ +QCheckBox { + color: #2c3e50; + spacing: 8px; +} + +QCheckBox::indicator { + width: 18px; + height: 18px; + border: 2px solid #bdc3c7; + border-radius: 3px; + background-color: #ffffff; +} + +QCheckBox::indicator:hover { + border: 2px solid #3498db; + background-color: #e8f4fd; +} + +QCheckBox::indicator:checked { + background-color: #3498db; + border: 2px solid #2980b9; + image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAJCAYAAAAGuM1UAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAAdgAAAHYBTnsmCAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAABmSURBVBiVjZExDsIwEAT3JFpKSoo8gCfwCF7ACyipa3oCb+ANvIEn8AaegJYqLWnSRMmy1/YkFhftzNw9jUajUQMwBTaStpJOkmZm9jSzB/A0sy2wljS193O6nZmdgR/wBhYRcfsDGEwlvgBP2TAAAAAASUVORK5CYII=); +} + +QCheckBox::indicator:checked:hover { + background-color: #5dade2; +} + +/* ==================== 组合框 ==================== */ +QComboBox { + background-color: #ffffff; + border: 2px solid #bdc3c7; + border-radius: 6px; + padding: 8px 12px; + color: #2c3e50; + min-width: 100px; +} + +QComboBox:focus { + border: 2px solid #3498db; + background-color: #fdfdfe; +} + +QComboBox::drop-down { + border: none; + width: 30px; +} + +QComboBox::down-arrow { + image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAGCAYAAAD37n+BAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAAdgAAAHYBTnsmCAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAABHSURBVBiVY2D4/58BFWBioBCMahgYDf8p0EC2hv8UaCBbA7ma/lOggWwN5Gqipwayff+fAg1ka/hPgQayNZCriboGAJd7BTc3gZfyAAAAAElFTkSuQmCC); + width: 12px; + height: 8px; +} + +QComboBox QAbstractItemView { + background-color: #ffffff; + border: 1px solid #bdc3c7; + border-radius: 6px; + color: #2c3e50; + selection-background-color: #3498db; + padding: 4px; +} + +QComboBox QAbstractItemView::item { + height: 30px; + padding: 6px 12px; + border: none; + border-radius: 4px; +} + +QComboBox QAbstractItemView::item:hover { + background-color: #e8f4fd; +} + +QComboBox QAbstractItemView::item:selected { + background-color: #3498db; + color: #ffffff; +} + +/* ==================== 对话框 ==================== */ +QDialog { + background-color: #ffffff; + border: 1px solid #bdc3c7; + border-radius: 12px; +} + +QDialogButtonBox QPushButton { + min-width: 100px; + padding: 10px 20px; +} + +/* ==================== 消息框 ==================== */ +QMessageBox { + background-color: #ffffff; + color: #2c3e50; + border-radius: 8px; +} + +QMessageBox QLabel { + color: #2c3e50; + font-size: 10pt; +} + +QMessageBox QPushButton { + min-width: 80px; + padding: 8px 16px; +} + +/* ==================== 表单布局 ==================== */ +QFormLayout QLabel { + font-weight: bold; + color: #3498db; + min-width: 100px; +} + +/* ==================== 滚动条 ==================== */ +QScrollBar:vertical { + background-color: #f8f9fa; + width: 12px; + border-radius: 6px; + margin: 0px; +} + +QScrollBar::handle:vertical { + background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, + stop: 0 #d5dbdb, stop: 1 #bdc3c7); + border-radius: 6px; + min-height: 30px; +} + +QScrollBar::handle:vertical:hover { + background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, + stop: 0 #3498db, stop: 1 #2980b9); +} + +QScrollBar::handle:vertical:pressed { + background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, + stop: 0 #2980b9, stop: 1 #21618c); +} + +QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { + height: 0px; + background: none; +} + +QScrollBar:horizontal { + background-color: #f8f9fa; + height: 12px; + border-radius: 6px; + margin: 0px; +} + +QScrollBar::handle:horizontal { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #d5dbdb, stop: 1 #bdc3c7); + border-radius: 6px; + min-width: 30px; +} + +QScrollBar::handle:horizontal:hover { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #3498db, stop: 1 #2980b9); +} + +QScrollBar::handle:horizontal:pressed { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #2980b9, stop: 1 #21618c); +} + +QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal { + width: 0px; + background: none; +} + +/* ==================== 菜单 ==================== */ +QMenu { + background-color: #ffffff; + border: 1px solid #bdc3c7; + border-radius: 8px; + padding: 4px; + color: #2c3e50; +} + +QMenu::item { + background-color: transparent; + padding: 8px 16px; + border-radius: 4px; + margin: 1px; +} + +QMenu::item:selected { + background-color: #3498db; + color: #ffffff; +} + +QMenu::item:disabled { + color: #95a5a6; +} + +QMenu::separator { + height: 1px; + background-color: #d5dbdb; + margin: 4px 8px; +} + +/* ==================== 进度条 ==================== */ +QProgressBar { + background-color: #ffffff; + border: 1px solid #bdc3c7; + border-radius: 8px; + text-align: center; + color: #2c3e50; + font-weight: bold; +} + +QProgressBar::chunk { + background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, + stop: 0 #3498db, stop: 1 #2980b9); + border-radius: 7px; + margin: 1px; +} + +/* ==================== 工具提示 ==================== */ +QToolTip { + background-color: #ffffff; + color: #2c3e50; + border: 1px solid #3498db; + border-radius: 6px; + padding: 8px; + font-size: 9pt; + opacity: 240; +} + +/* ==================== 分割器 ==================== */ +QSplitter::handle { + background-color: #d5dbdb; +} + +QSplitter::handle:horizontal { + width: 3px; + border-radius: 1px; +} + +QSplitter::handle:vertical { + height: 3px; + border-radius: 1px; +} + +QSplitter::handle:hover { + background-color: #3498db; +} + +/* ==================== 特殊样式 ==================== */ +/* 成功状态的行 */ +QTableView::item[success="true"] { + background-color: rgba(46, 204, 113, 0.1); + color: #27ae60; +} + +/* 失败状态的行 */ +QTableView::item[success="false"] { + background-color: rgba(231, 76, 60, 0.1); + color: #e74c3c; +} + +/* 主步骤样式 */ +QTableView::item[main_step="true"] { + background-color: rgba(52, 152, 219, 0.1); + font-weight: bold; +} + +/* 状态指示器 */ +.status-success { + color: #27ae60; + font-weight: bold; +} + +.status-error { + color: #e74c3c; + font-weight: bold; +} + +.status-warning { + color: #f39c12; + font-weight: bold; +} + +.status-info { + color: #3498db; + font-weight: bold; +} + +/* ==================== 动画效果 ==================== */ +QPushButton { + transition: all 0.3s ease; +} + +QToolButton { + transition: all 0.3s ease; +} + +QLineEdit { + transition: border-color 0.3s ease, background-color 0.3s ease; +} + +QListWidget::item { + transition: background-color 0.2s ease, border-color 0.2s ease; +} + +QTableView::item { + transition: background-color 0.2s ease; +} \ No newline at end of file diff --git a/UI/Main/Main.py b/UI/Main/Main.py index f222032..52c7979 100644 --- a/UI/Main/Main.py +++ b/UI/Main/Main.py @@ -19,6 +19,7 @@ from model.ClientModel.Client import Client from utils import Globals from utils.DBModels.InitParameterDB import InitParameterDB from UI.ProfibusWidgets.ProfibusWindow import ProfibusWidgets +from UI.ProcedureManager.ProcedureManager import ProcedureManager class CommonHelper: def __init__(self): @@ -76,6 +77,7 @@ class MainWindow(QMainWindow): self.projectWidget = ProjectWidgets() self.userWidget = UserWidgets() + self.procedureManagerWidget = ProcedureManager() self.ModbusTcpMasterWidget = VarWidgets('ModbusTcpMaster') self.ModbusTcpSlaveWidget = VarWidgets('ModbusTcpSlave') @@ -99,6 +101,7 @@ class MainWindow(QMainWindow): # self.ModBusWidget.setObjectName('varWidget') self.analogWidget.setObjectName('analogWidget') self.hartsimulateWidget.setObjectName('hartsimulateWidget') + self.varManageTabWidget = QTabWidget() self.varManageTabWidget.setObjectName("varManageTabWidget") @@ -121,7 +124,7 @@ class MainWindow(QMainWindow): self.rightWidget.addWidget(self.trendWidget) self.rightWidget.addWidget(self.userWidget) self.rightWidget.addWidget(self.SettingWidget) - + self.rightWidget.addWidget(self.procedureManagerWidget) self.rightWidget.widget(1) self.leftWidget.createProject.clicked.connect(lambda: self.exButtonClicked(0)) @@ -129,6 +132,7 @@ class MainWindow(QMainWindow): self.leftWidget.trendMag.clicked.connect(self.reFreshTrendWidget) self.leftWidget.userMag.clicked.connect(lambda: self.exButtonClicked(3)) self.leftWidget.protocolMag.clicked.connect(self.showSetting) + self.leftWidget.procedureMag.clicked.connect(lambda: self.initProcedureDB()) self.setCentralWidget(self.centralwidget) self.setWindowOpacity(0.995) # 设置窗口透明度 @@ -158,34 +162,36 @@ class MainWindow(QMainWindow): self.setWindowFlag(Qt.FramelessWindowHint) def exButtonClicked(self, index): - if not Globals.getValue('currentPro') and index != 0: + if Globals.getValue('currentPro') == -1 and index not in [0, 3]: return -1 + print(index, Globals.getValue('currentPro')) self.rightWidget.setCurrentIndex(index) def showSetting(self): - proType = Globals.getValue('currentProType') - - if proType == -1: + if Globals.getValue('currentPro') == -1: return self.SettingWidget.setupUI() self.rightWidget.setCurrentIndex(4) def varShow(self): - proType = Globals.getValue('currentProType') - if proType == -1: - return self.exButtonClicked(1) Globals.setValue('SearchWidget', 1) def reFreshTrendWidget(self): - proType = Globals.getValue('currentProType') - if proType == -1: + if Globals.getValue('currentPro') == -1: return self.trendWidget.getMems() self.exButtonClicked(2) + + def initProcedureDB(self): + if Globals.getValue('currentPro') == -1: + return + self.procedureManagerWidget.initDB() + self.procedureManagerWidget.initUI() + self.exButtonClicked(5) def enum(self,**enums): return type('Enum', (), enums) diff --git a/UI/Main/MainLeft.py b/UI/Main/MainLeft.py index 75cf686..fb821eb 100644 --- a/UI/Main/MainLeft.py +++ b/UI/Main/MainLeft.py @@ -29,6 +29,8 @@ class MainLeft(QWidget): self.protocolMag = QtWidgets.QPushButton(self) self.protocolMag.setObjectName("protocolMag") + self.procedureMag = QtWidgets.QPushButton(self) + self.procedureMag.setObjectName("protocolMag") self.verticalLayout = QtWidgets.QVBoxLayout(self) self.verticalLayout.setContentsMargins(0, 0, 0, 0) @@ -48,6 +50,8 @@ class MainLeft(QWidget): self.verticalLayout.addWidget(QtWidgets.QSplitter()) self.verticalLayout.addWidget(self.protocolMag) self.verticalLayout.addWidget(QtWidgets.QSplitter()) + self.verticalLayout.addWidget(self.procedureMag) + self.verticalLayout.addWidget(QtWidgets.QSplitter()) self.verticalLayout.setStretch(0, 1) self.verticalLayout.setStretch(1, 4) @@ -59,9 +63,9 @@ class MainLeft(QWidget): self.verticalLayout.setStretch(7, 4) self.verticalLayout.setStretch(8, 2) self.verticalLayout.setStretch(9, 4) - # self.verticalLayout.setStretch(10, 2) - # self.verticalLayout.setStretch(11, 4) - self.verticalLayout.setStretch(10, 40) + self.verticalLayout.setStretch(10, 2) + self.verticalLayout.setStretch(11, 4) + self.verticalLayout.setStretch(12, 40) QtCore.QMetaObject.connectSlotsByName(self) @@ -71,6 +75,7 @@ class MainLeft(QWidget): self.trendMag.setText("历史趋势") self.userMag.setText("用户管理") self.protocolMag.setText("通讯配置") + self.procedureMag.setText("规程管理") self.createProject.setIcon(QIcon(':/static/newH.png')) # self.openProject.setIcon(QIcon(':/static/open.png')) @@ -78,8 +83,9 @@ class MainLeft(QWidget): self.trendMag.setIcon(QIcon(':/static/trend.png')) self.userMag.setIcon(QIcon(':/static/userMag.png')) self.protocolMag.setIcon(QIcon(':/static/setting.png')) + self.procedureMag.setIcon(QIcon(':/static/procedure.png')) - for btn in [self.createProject, self.varMag, self.trendMag, self.userMag, self.protocolMag]: + for btn in [self.createProject, self.varMag, self.trendMag, self.userMag, self.protocolMag, self.procedureMag]: self.setBtn(btn) # self.openProject.clicked.connect(lambda:self.openProject.setIcon(QIcon(':/static/openH.png'))) diff --git a/UI/ProcedureManager/ProcedureManager.py b/UI/ProcedureManager/ProcedureManager.py new file mode 100644 index 0000000..15ce017 --- /dev/null +++ b/UI/ProcedureManager/ProcedureManager.py @@ -0,0 +1,1226 @@ +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)) diff --git a/UI/ProfibusWidgets/AreaTabWidget.py b/UI/ProfibusWidgets/AreaTabWidget.py index 258dac4..4bb0a81 100644 --- a/UI/ProfibusWidgets/AreaTabWidget.py +++ b/UI/ProfibusWidgets/AreaTabWidget.py @@ -1,7 +1,7 @@ import re import sys import json -from anyio import value +# from anyio import value import qtawesome from PyQt5.QtWidgets import QApplication, QTabWidget, QWidget, QLabel, QPushButton, \ diff --git a/model/ProcedureModel/ProcedureProcessor.py b/model/ProcedureModel/ProcedureProcessor.py new file mode 100644 index 0000000..a85ecf7 --- /dev/null +++ b/model/ProcedureModel/ProcedureProcessor.py @@ -0,0 +1,204 @@ +import json +import re +from datetime import datetime +from PyQt5.QtCore import Qt, QTimer, QAbstractTableModel, QModelIndex, QPoint, QSize +from PyQt5.QtGui import QFont, QBrush, QColor +from openpyxl import load_workbook + +class ExcelParser: + @staticmethod + def parseProcedure(filePath): + wb = load_workbook(filename=filePath) + + if '测试脚本' in wb.sheetnames: + sheet = wb['测试脚本'] + else: + sheet = wb.active + + specInfo = { + "规程名称": sheet['B1'].value, + "规程编号": sheet['D1'].value, + "规程类型": sheet['F1'].value + } + + testCaseInfo = { + "测试用例": sheet['B2'].value, + "用例编号": sheet['D2'].value, + "工况描述": sheet['H2'].value + } + + testSteps = [] + currentStep = None + stepCounter = 0 + + for rowIdx in range(5, sheet.max_row + 1): + cellA = sheet[f'A{rowIdx}'].value + cellB = sheet[f'B{rowIdx}'].value + cellC = sheet[f'C{rowIdx}'].value + + if cellB is None and cellA is None: + continue + + if cellA and re.match(r'STEP\d+[::]', str(cellA)): + if currentStep: + testSteps.append(currentStep) + stepCounter += 1 + + stepName = str(cellA).replace(':', ':').strip() + stepDesc = str(cellB).replace('\n', ' ').strip() if cellB else "" + + currentStep = { + "步骤ID": stepName, + "步骤描述": stepDesc, + "操作类型": cellC if cellC else "", + "预期结果": sheet[f'D{rowIdx}'].value, + "子步骤": [] + } + elif currentStep and cellA and str(cellA).isdigit(): + subStep = { + "序号": int(cellA), + "操作": str(cellB).replace('\n', ' ').strip() if cellB else "", + "操作类型": cellC if cellC else "", + "预期结果": sheet[f'D{rowIdx}'].value, + "实际结果": sheet[f'E{rowIdx}'].value, + "一致性": sheet[f'F{rowIdx}'].value if sheet[f'F{rowIdx}'].value == "是" else "否", + "测试时间": sheet[f'H{rowIdx}'].value, + "备注": sheet[f'I{rowIdx}'].value + } + currentStep["子步骤"].append(subStep) + + if currentStep: + testSteps.append(currentStep) + + return { + "文件路径": filePath, + "规程信息": specInfo, + "测试用例信息": testCaseInfo, + "测试步骤": testSteps + } + +class StepTableModel(QAbstractTableModel): + columns = ['序号', '实验步骤', '执行时间', '是否与预期一致', '实际结果', '备注'] + + def __init__(self, testSteps): + super().__init__() + self.stepData = [] + self.stepIndex = 0 + + for mainStep in testSteps: + self.stepData.append({ + 'id': self.stepIndex, + 'isMain': True, + 'stepId': mainStep['步骤ID'], + 'description': mainStep['步骤描述'], + 'executed': False, + 'time': None, + 'result': None, + 'note': "" # 新增备注字段,主步骤默认为空 + }) + self.stepIndex += 1 + + for subStep in mainStep['子步骤']: + self.stepData.append({ + 'id': self.stepIndex, + 'isMain': False, + 'stepId': f"{mainStep['步骤ID']}{subStep['序号']}", + 'description': subStep['操作'], + 'executed': False, + 'time': None, + 'result': None, + 'note': subStep['备注'] # 新增:从Excel解析的备注字段 + }) + self.stepIndex += 1 + + def rowCount(self, parent=QModelIndex()): + return len(self.stepData) + + def columnCount(self, parent=QModelIndex()): + return len(self.columns) + + def data(self, index, role=Qt.DisplayRole): + if not index.isValid(): + return None + + row = index.row() + col = index.column() + step = self.stepData[row] + + if role == Qt.DisplayRole: + if col == 0: + return step['stepId'] + elif col == 1: + return step['description'] + elif col == 2: + return step['time'].strftime("%Y-%m-%d %H:%M:%S") if step['time'] else '' + elif col == 3: + return {True: '是', False: '否', None: ''}[step['result']] + elif col == 4: + return {True: '成功', False: '失败', None: ''}[step['result']] + elif col == 5: + return step['note'] if step['note'] else '' + + elif role == Qt.BackgroundRole: + if step['executed']: + if step['result']: + return QBrush(QColor(144, 238, 144)) + else: + return QBrush(QColor(255, 182, 193)) + elif step['isMain']: + return QBrush(QColor(220, 220, 220)) + + elif role == Qt.FontRole and step['isMain']: + font = QFont() + font.setBold(True) + return font + + return None + + def headerData(self, section, orientation, role): + if role == Qt.DisplayRole and orientation == Qt.Horizontal: + return self.columns[section] + return None + + def getStepInfo(self, row): + if 0 <= row < len(self.stepData): + return self.stepData[row] + return None + + def getFullStepInfo(self, row): + """获取完整的步骤信息""" + if 0 <= row < len(self.stepData): + return self.stepData[row] + return None + + def updateStepResult(self, row, result, time): + if 0 <= row < len(self.stepData): + self.stepData[row]['executed'] = True + self.stepData[row]['time'] = time + self.stepData[row]['result'] = result + self.dataChanged.emit(self.index(row, 0), + self.index(row, self.columnCount()-1), + [Qt.DisplayRole, Qt.BackgroundRole]) + return True + return False + + def resetExecutionState(self): + """只重置执行状态(颜色),不清除时间和结果""" + for step in self.stepData: + step['executed'] = False + step['result'] = None + self.dataChanged.emit(self.index(0, 0), + self.index(self.rowCount()-1, self.columnCount()-1), + [Qt.BackgroundRole]) + + def resetAll(self): + """完全重置所有状态(包括时间和结果)""" + for step in self.stepData: + step.update({ + 'executed': False, + 'time': None, + 'result': None + }) + self.dataChanged.emit(self.index(0, 0), + self.index(self.rowCount()-1, self.columnCount()-1), + [Qt.DisplayRole, Qt.BackgroundRole]) diff --git a/utils/DBModels/ProcedureModel.py b/utils/DBModels/ProcedureModel.py new file mode 100644 index 0000000..d495a6c --- /dev/null +++ b/utils/DBModels/ProcedureModel.py @@ -0,0 +1,302 @@ +import sqlite3 +import json +import os # 添加os模块导入 +from datetime import datetime +from utils import Globals + + + +class DatabaseManager: + def __init__(self, dbPath="procedures.db"): + # 修改:使用跨平台的路径构造方式 + project_dir = os.path.join(os.getcwd(), 'project', str(Globals.getValue('currentPro'))) + db_dir = os.path.join(project_dir, 'db') + + # 确保目录存在 + os.makedirs(db_dir, exist_ok=True) + + self.dbPath = os.path.join(db_dir, 'procedures.db') + print(f"数据库路径: {self.dbPath}") + self.conn = sqlite3.connect(self.dbPath) + self.cursor = self.conn.cursor() + self.createTables() + + def createTables(self): + # 创建分类表 + self.cursor.execute(""" + CREATE TABLE IF NOT EXISTS categories ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT UNIQUE NOT NULL + ) + """) + + # 创建规程表 + self.cursor.execute(""" + CREATE TABLE IF NOT EXISTS procedures ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + category_id INTEGER, + name TEXT NOT NULL, + number TEXT, + type TEXT, + content TEXT, -- 存储解析后的JSON数据 + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY(category_id) REFERENCES categories(id) + ) + """) + + # 创建规程执行历史表(每次执行记录一条) + self.cursor.execute(""" + CREATE TABLE IF NOT EXISTS procedure_execution_history ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + procedure_id INTEGER NOT NULL, + start_time TEXT NOT NULL, + end_time TEXT, + result BOOLEAN, -- 总体结果 + report_path TEXT, -- 报告路径 + step_results TEXT -- 新增:存储所有步骤执行结果的JSON + ) + """) + + # 插入默认分类 + self.cursor.execute(""" + INSERT OR IGNORE INTO categories (name) VALUES + ('默认分类'), + ('紧急规程'), + ('测试规程'), + ('维护规程') + """) + + # 确保procedures表有report_path列 + try: + self.cursor.execute("ALTER TABLE procedures ADD COLUMN report_path TEXT DEFAULT ''") + except sqlite3.OperationalError: + pass # 列已存在则忽略错误 + + # 创建操作历史表 + self.cursor.execute(""" + CREATE TABLE IF NOT EXISTS operation_history ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + execution_id INTEGER NOT NULL, + operation_type TEXT NOT NULL, + operation_detail TEXT, + operation_time TEXT NOT NULL, + FOREIGN KEY(execution_id) REFERENCES procedure_execution_history(id) + ) + """) + self.conn.commit() + + def getCategories(self): + self.cursor.execute("SELECT id, name FROM categories ORDER BY name") + return self.cursor.fetchall() + + def addCategory(self, name): + try: + self.cursor.execute("INSERT INTO categories (name) VALUES (?)", (name,)) + self.conn.commit() + return True + except sqlite3.IntegrityError: + return False + + def deleteCategory(self, categoryId): + # 检查是否为默认分类 + self.cursor.execute("SELECT name FROM categories WHERE id = ?", (categoryId,)) + categoryName = self.cursor.fetchone()[0] + if categoryName == "默认分类": + return False # 默认分类不可删除 + + self.cursor.execute("SELECT id FROM categories WHERE name = '默认分类'") + defaultCatId = self.cursor.fetchone()[0] + + self.cursor.execute(""" + UPDATE procedures + SET category_id = ? + WHERE category_id = ? + """, (defaultCatId, categoryId)) + + self.cursor.execute("DELETE FROM categories WHERE id = ?", (categoryId,)) + self.conn.commit() + return True + + def addProcedure(self, categoryId, name, number, type, content, reportPath=""): + """添加新规程到数据库""" + contentJson = json.dumps(content, ensure_ascii=False) + self.cursor.execute(""" + INSERT INTO procedures (category_id, name, number, type, content, report_path) + VALUES (?, ?, ?, ?, ?, ?) + """, (categoryId, name, number, type, contentJson, reportPath)) + self.conn.commit() + return self.cursor.lastrowid + + def getProcedures(self, categoryId=None): + if categoryId: + self.cursor.execute(""" + SELECT id, name, number, type, created_at + FROM procedures + WHERE category_id = ? + ORDER BY name + """, (categoryId,)) + else: + self.cursor.execute(""" + SELECT id, name, number, type, created_at + FROM procedures + ORDER BY name + """) + return self.cursor.fetchall() + + def getProcedureContent(self, procedureId): + self.cursor.execute("SELECT content FROM procedures WHERE id = ?", (procedureId,)) + result = self.cursor.fetchone() + if result: + return json.loads(result[0]) + return None + + def deleteProcedure(self, procedureId): + self.cursor.execute("DELETE FROM procedures WHERE id = ?", (procedureId,)) + self.conn.commit() + return self.cursor.rowcount > 0 + + def insertProcedureExecution(self, procedureId, startTime, result=None, reportPath="", stepResults="[]"): + self.cursor.execute(""" + INSERT INTO procedure_execution_history + (procedure_id, start_time, result, report_path, step_results) + VALUES (?, ?, ?, ?, ?) + """, (procedureId, startTime, result, reportPath, stepResults)) + self.conn.commit() + return self.cursor.lastrowid + + def updateStepResults(self, executionId, stepResults): + self.cursor.execute(""" + UPDATE procedure_execution_history + SET step_results = ? + WHERE id = ? + """, (json.dumps(stepResults), executionId)) + self.conn.commit() + return self.cursor.rowcount > 0 + + def getStepResults(self, executionId): + """根据执行ID获取步骤结果 - 修正SQL语句""" + # 修正:使用正确的列名 execution_id -> id + self.cursor.execute("SELECT step_results FROM procedure_execution_history WHERE id=?", (executionId,)) + row = self.cursor.fetchone() + if row: + return json.loads(row[0]) + return None + + def getExecutionHistory(self, filterText=None): + query = """ + SELECT + peh.id, + p.name || '(' || p.number || ')' AS full_name, -- 拼接规程全名 + p.type, + peh.start_time, + peh.report_path + FROM procedure_execution_history peh + JOIN procedures p ON peh.procedure_id = p.id + WHERE peh.report_path IS NOT NULL + """ + params = [] + + if filterText: + query += " AND (p.name LIKE ? OR p.number LIKE ? OR p.type LIKE ?)" + searchTerm = f"%{filterText}%" + params = [searchTerm, searchTerm, searchTerm] + + self.cursor.execute(query, params) + return self.cursor.fetchall() + + def updateExecutionReportPath(self, executionId, reportPath): + self.cursor.execute(""" + UPDATE procedure_execution_history + SET report_path = ? + WHERE id = ? + """, (reportPath, executionId)) + self.conn.commit() + return self.cursor.rowcount > 0 + + def deleteExecutionHistory(self, executionIds): + """删除指定的执行历史记录及其相关操作历史 - 方法名已修正""" + if not executionIds: + return False + + try: + # 转换为整数列表 + ids = [int(id) for id in executionIds] + + # 删除操作历史记录 + self.cursor.executemany( + "DELETE FROM operation_history WHERE execution_id = ?", + [(id,) for id in ids] + ) + + # 删除执行历史记录 + self.cursor.executemany( + "DELETE FROM procedure_execution_history WHERE id = ?", + [(id,) for id in ids] + ) + + self.conn.commit() + return True + except Exception as e: + print(f"删除执行历史失败: {str(e)}") + return False + + def getExecutionDetails(self, executionId): + """获取执行记录的完整详情""" + try: + # 获取执行记录基本信息 + self.cursor.execute(""" + SELECT + peh.id, + p.id AS procedure_id, + p.name AS procedure_name, + p.content AS procedure_content, + peh.step_results + FROM procedure_execution_history peh + JOIN procedures p ON peh.procedure_id = p.id + WHERE peh.id = ? + """, (executionId,)) + row = self.cursor.fetchone() + + if not row: + return None + + # 解析数据 + details = { + "execution_id": row[0], + "procedure_id": row[1], + "procedure_name": row[2], + "procedure_content": json.loads(row[3]), + "step_results": json.loads(row[4]) + } + + return details + except Exception as e: + print(f"获取执行详情失败: {str(e)}") + return None + + def updateProcedureCategory(self, procedureId, newCategoryId): + """更新规程的分类""" + try: + self.cursor.execute(""" + UPDATE procedures + SET category_id = ? + WHERE id = ? + """, (newCategoryId, procedureId)) + self.conn.commit() + return self.cursor.rowcount > 0 + except Exception as e: + print(f"更新规程分类失败: {str(e)}") + return False + + def saveOperationHistory(self, executionId, operationType, operationDetail, operationTime): + """保存操作历史记录""" + self.cursor.execute(""" + INSERT INTO operation_history + (execution_id, operation_type, operation_detail, operation_time) + VALUES (?, ?, ?, ?) + """, (executionId, operationType, operationDetail, operationTime)) + self.conn.commit() + return self.cursor.lastrowid + + def close(self): + self.conn.close() \ No newline at end of file