diff --git a/Static/Main.qss b/Static/Main.qss index decf7c5..f02b8f0 100644 --- a/Static/Main.qss +++ b/Static/Main.qss @@ -2,6 +2,8 @@ QWidget#centralwidget{ background-color: #F5F5F5; + border-radius: 8px; + } QWidget#titlewidget{ @@ -908,50 +910,37 @@ QListView#trendListView::item{ } QMessageBox { - background-color: #f0f0f0; - - color: black; - - font-size: 14px; - - border: 1px solid #cccccc; - - border-radius: 5px; - - padding: 5px; - } - - QMessageBox QLabel { - - color: black; - - font-size: 14px; - } - - QMessageBox QPushButton { - background-color: #e0e0e0; - - color: black; - - font-size: 14px; - - border: 1px solid #cccccc; - - border-radius: 5px; - - padding: 5px; - - min-width: 5em; - } - - QMessageBox QPushButton:hover { + background-color: #ffffff; + border-radius: 16px; + border: 2px solid #3498db; + min-width: 400px; + min-height: 180px; + font-size: 18px; + padding: 24px; +} - background-color: #f5f5f5; +QMessageBox QLabel { + color: #222; + font-size: 20px; + font-weight: bold; + padding: 16px 0 8px 0; +} - } +QMessageBox QPushButton { + min-width: 120px; + min-height: 40px; + font-size: 18px; + border-radius: 8px; + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #3498db, stop:1 #2980b9); + color: #fff; + font-weight: bold; + margin: 0 12px; +} - QMessageBox QPushButton:pressed { +QMessageBox QPushButton:hover { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #5dade2, stop:1 #3498db); +} - background-color: #e5e5e5; - - } +QMessageBox QPushButton:pressed { + background: #2980b9; +} diff --git a/Static/Procedure.qss b/Static/Procedure.qss index 93d3be1..da72041 100644 --- a/Static/Procedure.qss +++ b/Static/Procedure.qss @@ -53,7 +53,6 @@ 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 { @@ -98,7 +97,6 @@ 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 { @@ -124,7 +122,6 @@ QPushButton[text*="开始"], QPushButton[text*="执行"] { 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*="删除"] { @@ -137,7 +134,6 @@ QPushButton[text*="停止"], QPushButton[text*="删除"] { 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*="重置"] { @@ -150,7 +146,6 @@ QPushButton[text*="重置"] { 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*="导入"] { @@ -163,7 +158,167 @@ QPushButton[text*="报告"], QPushButton[text*="导入"] { 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); +} + +/* ==================== 规程界面按钮样式 ==================== */ +/* 开始自动执行按钮 - 绿色主题 */ +QPushButton[text*="开始自动执行"] { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #28a745, stop: 1 #218838); + border: 2px solid #28a745; + border-radius: 6px; + color: #ffffff; + font-size: 13px; + font-weight: bold; + min-width: 140px; + padding: 10px 20px; + icon-size: 16px 16px; +} + +QPushButton[text*="开始自动执行"]:hover { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #218838, stop: 1 #1e7e34); + border-color: #1e7e34; +} + +QPushButton[text*="开始自动执行"]:pressed { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #1e7e34, stop: 1 #1c7430); + border-color: #1c7430; +} + +QPushButton[text*="开始自动执行"]:disabled { + background: #6c757d; + border-color: #6c757d; + color: #adb5bd; +} + +/* 停止自动执行按钮 - 红色主题 */ +QPushButton[text*="停止自动执行"] { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #dc3545, stop: 1 #c82333); + border: 2px solid #dc3545; + border-radius: 6px; + color: #ffffff; + font-size: 13px; + font-weight: bold; + min-width: 140px; + padding: 10px 20px; + icon-size: 16px 16px; +} + +QPushButton[text*="停止自动执行"]:hover { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #c82333, stop: 1 #bd2130); + border-color: #bd2130; +} + +QPushButton[text*="停止自动执行"]:pressed { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #bd2130, stop: 1 #b21f2d); + border-color: #b21f2d; +} + +QPushButton[text*="停止自动执行"]:disabled { + background: #6c757d; + border-color: #6c757d; + color: #adb5bd; +} + +/* 执行下一步按钮 - 蓝色主题 */ +QPushButton[text*="执行下一步"] { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #007bff, stop: 1 #0056b3); + border: 2px solid #007bff; + border-radius: 6px; + color: #ffffff; + font-size: 13px; + font-weight: bold; + min-width: 140px; + padding: 10px 20px; + icon-size: 16px 16px; +} + +QPushButton[text*="执行下一步"]:hover { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #0056b3, stop: 1 #004085); + border-color: #004085; +} + +QPushButton[text*="执行下一步"]:pressed { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #004085, stop: 1 #003d7a); + border-color: #003d7a; +} + +QPushButton[text*="执行下一步"]:disabled { + background: #6c757d; + border-color: #6c757d; + color: #adb5bd; +} + +/* 完全重置按钮 - 橙色主题 */ +QPushButton[text*="完全重置"] { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #fd7e14, stop: 1 #e8690b); + border: 2px solid #fd7e14; + border-radius: 6px; + color: #ffffff; + font-size: 13px; + font-weight: bold; + min-width: 140px; + padding: 10px 20px; + icon-size: 16px 16px; +} + +QPushButton[text*="完全重置"]:hover { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #e8690b, stop: 1 #d6620a); + border-color: #d6620a; +} + +QPushButton[text*="完全重置"]:pressed { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #d6620a, stop: 1 #c55a09); + border-color: #c55a09; +} + +QPushButton[text*="完全重置"]:disabled { + background: #6c757d; + border-color: #6c757d; + color: #adb5bd; +} + +/* 生成报告按钮 - 紫色主题 */ +QPushButton[text*="生成报告"] { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #6f42c1, stop: 1 #5a32a3); + border: 2px solid #6f42c1; + border-radius: 6px; + color: #ffffff; + font-size: 13px; + font-weight: bold; + min-width: 140px; + padding: 10px 20px; + icon-size: 16px 16px; +} + +QPushButton[text*="生成报告"]:hover { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #5a32a3, stop: 1 #4c2a85); + border-color: #4c2a85; +} + +QPushButton[text*="生成报告"]:pressed { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #4c2a85, stop: 1 #3d2175); + border-color: #3d2175; +} + +QPushButton[text*="生成报告"]:disabled { + background: #6c757d; + border-color: #6c757d; + color: #adb5bd; } /* ==================== 标签 ==================== */ @@ -191,17 +346,44 @@ QTableView { selection-color: #ffffff; } +QTableWidget { + background-color: #ffffff; + alternate-background-color: #f8f9fa; + gridline-color: #d5dbdb; + border: 1px solid #bdc3c7; + border-radius: 8px; + selection-background-color: #3498db; + selection-color: #ffffff; + outline: none; +} + +QTableWidget::item { + padding: 8px; + border: none; + background-color: transparent; +} + +QTableWidget::item:selected { + background-color: #3498db; + color: #ffffff; +} +QTableWidget::item:hover { + background-color: #e8f4fd; +} QHeaderView::section { background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #ffffff, stop: 1 #ecf0f1); color: #2c3e50; - padding: 10px 8px; + padding: 12px 8px; border: none; border-right: 1px solid #bdc3c7; border-bottom: 2px solid #3498db; font-weight: bold; + font-size: 10pt; + min-height: 45px; + text-align: center; } QHeaderView::section:first { @@ -218,6 +400,54 @@ QHeaderView::section:hover { stop: 0 #e8f4fd, stop: 1 #d6eaff); } +/* 规程编辑器表格特殊样式 */ +QTableWidget#stepsTable { + background-color: #ffffff; + border: 2px solid #bdc3c7; + border-radius: 8px; + gridline-color: #d5dbdb; +} + +QTableWidget#stepsTable::item { + padding: 10px 8px; + border: none; + background-color: transparent; + font-size: 9pt; +} + +QTableWidget#stepsTable::item:selected { + background-color: #3498db; + color: #ffffff; +} + +QTableWidget#stepsTable::item:hover { + background-color: #e8f4fd; +} + +/* 主步骤样式 */ +QTableWidget#stepsTable::item[main_step="true"] { + background-color: #dcdcdc; + font-weight: bold; + color: #2c3e50; +} + +/* 子步骤样式 */ +QTableWidget#stepsTable::item[sub_step="true"] { + background-color: #f8f9fa; + color: #2c3e50; +} + +/* 表格行高设置 */ +QTableWidget#stepsTable { + gridline-color: #d5dbdb; + alternate-background-color: #f8f9fa; +} + +QTableWidget#stepsTable::item { + min-height: 35px; + padding: 8px 12px; +} + /* ==================== 列表控件 ==================== */ QListWidget { background-color: #ffffff; @@ -292,7 +522,6 @@ QLineEdit { QLineEdit:focus { border: 2px solid #3498db; background-color: #fdfdfe; - box-shadow: 0 0 8px rgba(52, 152, 219, 0.3); } QLineEdit:hover { @@ -332,7 +561,7 @@ QSpinBox::up-arrow { } QSpinBox::down-arrow { - image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAGCAYAAAD37n+BAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAAdgAAAHYBTnsmCAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAABHSURBVBiVY2D4/58BFWBioBCMahgYDf8p0EC2hv8UaCBbA7ma/lOggWwN5Gqipwayff+fAg1ka/hPgQayNZCriboGAJd7BTc3gZfyAAAAAElFTkSuQmCC); + image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAGCAYAAAD37n+BAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAAdgAAAHYBTnsmCAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXblLm9yZ5vuPBoAAABHSURBVBiVY2D4/58BFWBioBCMahgYDf8p0EC2hv8UaCBbA7ma/lOggWwN5Gqipwayff+fAg1ka/hPgQayNZCriboGAJd7BTc3gZfyAAAAAElFTkSuQmCC); width: 8px; height: 4px; } @@ -359,7 +588,7 @@ QCheckBox::indicator:hover { QCheckBox::indicator:checked { background-color: #3498db; border: 2px solid #2980b9; - image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAJCAYAAAAGuM1UAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAAdgAAAHYBTnsmCAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAABmSURBVBiVjZExDsIwEAT3JFpKSoo8gCfwCF7ACyipa3oCb+ANvIEn8AaegJYqLWnSRMmy1/YkFhftzNw9jUajUQMwBTaStpJOkmZm9jSzB/A0sy2wljS193O6nZmdgR/wBhYRcfsDGEwlvgBP2TAAAAAASUVORK5CYII=); + image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAJCAYAAAAGuM1UAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAAdgAAAHYBTnsmCAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXblLm9yZ5vuPBoAAABmSURBVBiVjZExDsIwEAT3JFpKSoo8gCfwCF7ACyipa3oCb+ANvIEn8AaegJYqLWnSRMmy1/YkFhftzNw9jUajUQMwBTaStpJOkmZm9jSzB/A0sy2wljS193O6nZmdgR/wBhYRcfsDGEwlvgBP2TAAAAAASUVORK5CYII=); } QCheckBox::indicator:checked:hover { @@ -387,7 +616,7 @@ QComboBox::drop-down { } QComboBox::down-arrow { - image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAGCAYAAAD37n+BAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAAdgAAAHYBTnsmCAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAABHSURBVBiVY2D4/58BFWBioBCMahgYDf8p0EC2hv8UaCBbA7ma/lOggWwN5Gqipwayff+fAg1ka/hPgQayNZCriboGAJd7BTc3gZfyAAAAAElFTkSuQmCC); + image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAGCAYAAAD37n+BAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAAdgAAAHYBTnsmCAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXblLm9yZ5vuPBoAAABHSURBVBiVY2D4/58BFWBioBCMahgYDf8p0EC2hv8UaCBbA7ma/lOggWwN5Gqipwayff+fAg1ka/hPgQayNZCriboGAJd7BTc3gZfyAAAAAElFTkSuQmCC); width: 12px; height: 8px; } @@ -632,21 +861,254 @@ QTableView::item[main_step="true"] { /* ==================== 动画效果 ==================== */ 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; +} + +/* ==================== 规程编辑器样式 ==================== */ +/* 标题样式 */ +QLabel[text="规程编辑器"] { + font-size: 16px; + font-weight: bold; + color: #2c3e50; + padding: 10px; + background-color: #ecf0f1; + border-radius: 5px; + margin: 5px; +} + +/* 分组容器样式 */ +QWidget[class="info_group"] { + background-color: #f8f9fa; + border: 1px solid #dee2e6; + border-radius: 5px; + padding: 10px; + margin: 5px; +} + +QWidget[class="steps_group"] { + background-color: #f8f9fa; + border: 1px solid #dee2e6; + border-radius: 5px; + padding: 10px; + margin: 5px; +} + +/* 分组标题样式 */ +QLabel[text="基本信息"], QLabel[text="步骤列表"] { + font-weight: bold; + color: #3498db; + margin-bottom: 5px; +} + +/* 添加步骤按钮样式 */ +QPushButton[text="添加步骤"] { + background-color: #28a745; + color: white; + border: none; + padding: 5px 10px; + border-radius: 3px; + font-size: 12px; +} + +QPushButton[text="添加步骤"]:hover { + background-color: #218838; +} + +/* 删除步骤按钮样式 */ +QPushButton[text="删除步骤"] { + background-color: #dc3545; + color: white; + border: none; + padding: 5px 10px; + border-radius: 3px; + font-size: 12px; +} + +QPushButton[text="删除步骤"]:hover { + background-color: #c82333; +} + +/* 保存规程按钮样式 */ +QPushButton[text="保存规程"] { + background-color: #007bff; + color: white; + border: none; + padding: 10px 20px; + border-radius: 5px; + font-size: 14px; + font-weight: bold; +} + +QPushButton[text="保存规程"]:hover { + background-color: #0056b3; +} + +/* 取消按钮样式 */ +QPushButton[text="❌ 取消"] { + background-color: #6c757d; + color: white; + border: none; + padding: 10px 20px; + border-radius: 5px; + font-size: 14px; + font-weight: bold; +} + +QPushButton[text="❌ 取消"]:hover { + background-color: #5a6268; +} + +/* ==================== 规程执行界面样式 ==================== */ +/* 规程信息容器 */ +QWidget#procedureInfoContainer { + background-color: #ffffff; + border: 2px solid #dee2e6; + border-radius: 10px; + padding: 20px; + margin: 10px; +} + +/* 信息分组组件 */ +QWidget#infoGroupWidget { + background-color: #f8f9fa; + border: 1px solid #dee2e6; + border-radius: 8px; + padding: 15px; + margin: 5px; + min-width: 200px; + min-height: 80px; +} + +/* 规程名称分组 */ +QWidget#procedureNameGroup { + border-color: #343a40; +} + +/* 规程编号分组 */ +QWidget#procedureNumberGroup { + border-color: #343a40; +} + +/* 规程类型分组 */ +QWidget#procedureTypeGroup { + border-color: #343a40; +} + +/* 测试用例分组 */ +QWidget#testCaseGroup { + border-color: #343a40; +} + +/* 用例编号分组 */ +QWidget#caseNumberGroup { + border-color: #343a40; +} + +/* 工况描述分组 */ +QWidget#conditionDescriptionGroup { + border-color: #343a40; +} + +/* 信息标签样式 */ +QLabel#infoGroupLabel { + font-weight: bold; + font-size: 14px; + color: #212529; + margin-bottom: 8px; + padding: 5px 0px; +} + +/* 规程名称标签 */ +QLabel#procedureNameLabel { + color: #212529; +} + +/* 规程编号标签 */ +QLabel#procedureNumberLabel { + color: #212529; +} + +/* 规程类型标签 */ +QLabel#procedureTypeLabel { + color: #212529; +} + +/* 测试用例标签 */ +QLabel#testCaseLabel { + color: #212529; +} + +/* 用例编号标签 */ +QLabel#caseNumberLabel { + color: #212529; +} + +/* 工况描述标签 */ +QLabel#conditionDescriptionLabel { + color: #212529; +} + +/* 信息值样式 */ +QLabel#infoGroupValue { + color: #212529; + font-weight: bold; + font-size: 16px; + padding: 8px; + background-color: #ffffff; + border-radius: 6px; + border: 1px solid #dee2e6; + min-height: 20px; +} + +/* 工况描述值样式 */ +QLabel#descriptionValue { + color: #495057; + font-size: 14px; + padding: 10px; + background-color: #ffffff; + border-radius: 6px; + border: 1px solid #dee2e6; + line-height: 1.4; +} + +/* 状态标签样式 */ +QLabel#statusLabel { + color: #212529; + font-weight: bold; + font-size: 16px; +} + +/* 倒计时标签样式 */ +QLabel#countdownLabel { + color: #212529; + font-weight: bold; + font-size: 16px; +} + +/* 倒计时标签状态样式 */ +QLabel#countdownLabel[timeRemaining="low"] { + color: #dc3545; +} + +QLabel#countdownLabel[timeRemaining="medium"] { + color: #fd7e14; +} + +QLabel#countdownLabel[timeRemaining="high"] { + color: #212529; +} + +QLabel#countdownLabel[timeRemaining="completed"] { + color: #28a745; } \ No newline at end of file diff --git a/Static/QSS.py b/Static/QSS.py index 4d2d910..5a9715c 100644 --- a/Static/QSS.py +++ b/Static/QSS.py @@ -166,3 +166,5 @@ def qCleanupResources(): QtCore.qUnregisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data) qInitResources() + + diff --git a/UI/Main/Main.py b/UI/Main/Main.py index e092cf5..d2e26dc 100644 --- a/UI/Main/Main.py +++ b/UI/Main/Main.py @@ -4,9 +4,11 @@ import pandas as pd from PyQt5 import QtWidgets from PyQt5.QtCore import Qt from PyQt5.Qt import * -from PyQt5.QtWidgets import QApplication, QMainWindow, QStackedWidget, QMessageBox, QStackedWidget, QWidget, QTabWidget, QFileDialog, QPushButton +from PyQt5.QtWidgets import QApplication, QMainWindow, QStackedWidget, QMessageBox, QStackedWidget, QWidget, QTabWidget, QFileDialog, QPushButton, QGraphicsDropShadowEffect from PyQt5.QtGui import QIcon from PyQt5.QtCore import QSize +from PyQt5.QtGui import QPainter, QBrush, QColor +from PyQt5.QtCore import QEvent from ctypes import POINTER, cast from ctypes.wintypes import MSG from win32 import win32api, win32gui @@ -64,10 +66,10 @@ class MainWindow(QMainWindow): self.topWidget.setMouseTracking(True) #初始化接口 - self.windowEffect = WindowEffect() - # 添加DWM阴影效果 - self.windowEffect.addWindowAnimation(self.winId()) - self.windowEffect.addShadowEffect(self.winId()) + # self.windowEffect = WindowEffect() + # # 添加DWM阴影效果 + # self.windowEffect.addWindowAnimation(self.winId()) + # self.windowEffect.addShadowEffect(self.winId()) # 解决报错 self.windowHandle().screenChanged.connect(self.__onScreenChanged) @@ -179,6 +181,11 @@ class MainWindow(QMainWindow): # self.setAttribute(Qt.WA_TranslucentBackground) # 设置窗口背景透明 self.setWindowFlag(Qt.FramelessWindowHint) + # 添加阴影效果(接近Windows原生) + + # 添加圆角(QSS) + # self.centralwidget.setStyleSheet("") + def exButtonClicked(self, index): if Globals.getValue('currentPro') == -1 and index not in [0, 3]: return -1 @@ -360,6 +367,7 @@ class MainWindow(QMainWindow): self.move(event.globalPos() - self.dragPosition) event.accept() + def nativeEvent(self, eventType, message): """ 接受windows发送的信息 """ msg = MSG.from_address(message.__int__()) @@ -475,7 +483,6 @@ class MainWindow(QMainWindow): event.ignore() - if __name__ == '__main__': app = QApplication(sys.argv) app.setStyle(QtWidgets.QStyleFactory.create('Fusion')) diff --git a/UI/ProcedureManager/ProcedureEditor.py b/UI/ProcedureManager/ProcedureEditor.py new file mode 100644 index 0000000..3a03afb --- /dev/null +++ b/UI/ProcedureManager/ProcedureEditor.py @@ -0,0 +1,686 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +规程编辑器 +支持编辑规程内容并保存到数据库 +""" + +import json +from datetime import datetime +from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel, + QLineEdit, QTextEdit, QPushButton, QTableWidget, + QTableWidgetItem, QHeaderView, QMessageBox, + QDialog, QFormLayout, QSpinBox, QComboBox, + QDialogButtonBox, QAbstractItemView, QMenu) +from PyQt5.QtCore import Qt, pyqtSignal +from PyQt5.QtGui import QFont, QBrush, QColor +import qtawesome as qta + +class NoClearDelegate(QAbstractItemView): + """自定义委托,防止双击时清除内容""" + def createEditor(self, parent, option, index): + editor = super().createEditor(parent, option, index) + if hasattr(editor, 'setText'): + # 保持原有内容 + current_text = index.data() + if current_text: + editor.setText(current_text) + return editor + +class StepEditDialog(QDialog): + """步骤编辑对话框""" + + def __init__(self, step_data=None, parent=None): + super().__init__(parent) + self.setWindowTitle("编辑步骤") + self.setModal(True) + self.setMinimumWidth(500) + + self.step_data = step_data or {} + self.initUI() + + def initUI(self): + layout = QVBoxLayout() + self.setLayout(layout) + + # 表单布局 + form_layout = QFormLayout() + + # 步骤ID + self.stepIdEdit = QLineEdit() + self.stepIdEdit.setText(self.step_data.get('stepId', '')) + form_layout.addRow("步骤ID:", self.stepIdEdit) + + # 操作(合并关键词和步骤描述) + self.operationEdit = QTextEdit() + self.operationEdit.setMaximumHeight(80) + self.operationEdit.setPlainText(self.step_data.get('operation', '')) + form_layout.addRow("操作:", self.operationEdit) + + # 操作类型(直接输入) + self.typeEdit = QLineEdit() + self.typeEdit.setText(self.step_data.get('type', 'SET')) + form_layout.addRow("操作类型:", self.typeEdit) + + # 预期值 + self.expectedValueEdit = QLineEdit() + self.expectedValueEdit.setText(str(self.step_data.get('expectedValue', ''))) + form_layout.addRow("预期值:", self.expectedValueEdit) + + # 备注 + self.remarkEdit = QTextEdit() + self.remarkEdit.setMaximumHeight(60) + self.remarkEdit.setPlainText(self.step_data.get('remark', '')) + form_layout.addRow("备注:", self.remarkEdit) + + # 序号 + self.orderSpin = QSpinBox() + self.orderSpin.setRange(1, 999) + self.orderSpin.setValue(self.step_data.get('order', 1)) + form_layout.addRow("序号:", self.orderSpin) + + 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) + + def getStepData(self): + """获取步骤数据""" + return { + 'stepId': self.stepIdEdit.text(), + 'operation': self.operationEdit.toPlainText(), + 'type': self.typeEdit.text(), + 'expectedValue': self.expectedValueEdit.text(), + 'remark': self.remarkEdit.toPlainText(), + 'order': self.orderSpin.value() + } + +class ProcedureEditor(QWidget): + """规程编辑器""" + + procedureSaved = pyqtSignal(int) # 规程保存信号 + + def __init__(self, procedureData, procedureId, dbManager, parent=None): + super().__init__(parent) + self.procedureData = procedureData + self.procedureId = procedureId + self.dbManager = dbManager + self.parent = parent + + self.initUI() + self.loadProcedureData() + + def initUI(self): + """初始化界面""" + layout = QVBoxLayout() + self.setLayout(layout) + + # 标题 + title_label = QLabel("规程编辑器") + title_label.setObjectName("procedureEditorTitle") # 设置对象名称以便QSS选择器使用 + layout.addWidget(title_label) + + # 基本信息区域 + self.createBasicInfoSection(layout) + + # 步骤表格区域 + self.createStepsSection(layout) + + # 按钮区域 + self.createButtonSection(layout) + + def createBasicInfoSection(self, layout): + """创建基本信息区域""" + info_group = QWidget() + info_group.setObjectName("basicInfoGroup") # 设置对象名称以便QSS选择器使用 + layout.addWidget(info_group) + + # 标题 + info_title = QLabel("基本信息") + info_layout = QVBoxLayout() + info_group.setLayout(info_layout) + info_layout.addWidget(info_title) + + # 表单布局 + form_layout = QFormLayout() + + self.nameEdit = QLineEdit() + self.nameEdit.setPlaceholderText("输入规程名称") + self.nameEdit.setObjectName("procedureNameEdit") # 设置对象名称以便QSS选择器使用 + form_layout.addRow("规程名称:", self.nameEdit) + + self.numberEdit = QLineEdit() + self.numberEdit.setPlaceholderText("输入规程编号") + self.numberEdit.setObjectName("procedureNumberEdit") # 设置对象名称以便QSS选择器使用 + form_layout.addRow("规程编号:", self.numberEdit) + + self.typeEdit = QLineEdit() + self.typeEdit.setPlaceholderText("输入规程类型") + self.typeEdit.setObjectName("procedureTypeEdit") # 设置对象名称以便QSS选择器使用 + form_layout.addRow("规程类型:", self.typeEdit) + + self.descriptionEdit = QTextEdit() + self.descriptionEdit.setMaximumHeight(80) + self.descriptionEdit.setPlaceholderText("输入规程描述") + self.descriptionEdit.setObjectName("procedureDescriptionEdit") # 设置对象名称以便QSS选择器使用 + form_layout.addRow("规程描述:", self.descriptionEdit) + + info_layout.addLayout(form_layout) + + def createStepsSection(self, layout): + """创建步骤表格区域""" + steps_group = QWidget() + steps_group.setObjectName("stepsGroup") # 设置对象名称以便QSS选择器使用 + layout.addWidget(steps_group) + + # 标题和按钮 + header_layout = QHBoxLayout() + steps_title = QLabel("步骤列表") + header_layout.addWidget(steps_title) + + # 添加步骤按钮 + add_step_btn = QPushButton("添加步骤") + add_step_btn.setIcon(qta.icon('fa5s.plus', color='white')) + add_step_btn.clicked.connect(self.addStep) + header_layout.addWidget(add_step_btn) + + # 删除步骤按钮 + delete_step_btn = QPushButton("删除步骤") + delete_step_btn.setIcon(qta.icon('fa5s.trash', color='white')) + delete_step_btn.clicked.connect(self.deleteStep) + header_layout.addWidget(delete_step_btn) + + header_layout.addStretch() + steps_layout = QVBoxLayout() + steps_group.setLayout(steps_layout) + steps_layout.addLayout(header_layout) + + # 步骤表格 + self.stepsTable = QTableWidget() + self.stepsTable.setObjectName("stepsTable") # 设置对象名称以便QSS选择器使用 + self.stepsTable.setColumnCount(6) + self.stepsTable.setHorizontalHeaderLabels(['序号', '步骤ID', '操作', '类型', '预期值', '备注']) + + # 设置表格属性 + self.stepsTable.setSelectionBehavior(QAbstractItemView.SelectRows) + self.stepsTable.setEditTriggers(QAbstractItemView.AllEditTriggers) # 允许所有方式编辑 + self.stepsTable.setAlternatingRowColors(True) + + # 设置列宽 + header = self.stepsTable.horizontalHeader() + if header: + header.setSectionResizeMode(0, QHeaderView.ResizeToContents) # 序号 + header.setSectionResizeMode(1, QHeaderView.ResizeToContents) # 步骤ID + header.setSectionResizeMode(2, QHeaderView.Stretch) # 操作 + header.setSectionResizeMode(3, QHeaderView.ResizeToContents) # 类型 + header.setSectionResizeMode(4, QHeaderView.ResizeToContents) # 预期值 + header.setSectionResizeMode(5, QHeaderView.ResizeToContents) # 备注 + + # 设置最小列宽,确保表头显示完整 + self.stepsTable.setColumnWidth(0, 60) # 序号 + self.stepsTable.setColumnWidth(1, 120) # 步骤ID + self.stepsTable.setColumnWidth(3, 80) # 类型 + self.stepsTable.setColumnWidth(4, 100) # 预期值 + self.stepsTable.setColumnWidth(5, 120) # 备注 + + # 移除双击弹窗 + # self.stepsTable.cellDoubleClicked.connect(self.editStep) + + # 添加右键菜单 + self.stepsTable.setContextMenuPolicy(Qt.CustomContextMenu) + self.stepsTable.customContextMenuRequested.connect(self.showContextMenu) + + steps_layout.addWidget(self.stepsTable) + + def createButtonSection(self, layout): + """创建按钮区域""" + button_layout = QHBoxLayout() + + # 保存按钮 + save_btn = QPushButton("保存规程") + save_btn.setIcon(qta.icon('fa5s.save', color='white')) + save_btn.setObjectName("saveProcedureButton") # 设置对象名称以便QSS选择器使用 + save_btn.clicked.connect(self.saveProcedure) + button_layout.addWidget(save_btn) + + # 取消按钮 + cancel_btn = QPushButton("取消") + cancel_btn.setIcon(qta.icon('fa5s.times', color='red')) + cancel_btn.setObjectName("cancelEditButton") # 设置对象名称以便QSS选择器使用 + cancel_btn.clicked.connect(self.cancelEdit) + button_layout.addWidget(cancel_btn) + + button_layout.addStretch() + layout.addLayout(button_layout) + + def loadProcedureData(self): + """加载规程数据""" + if not self.procedureData: + print("警告:规程数据为空") + return + + print(f"加载规程数据: {self.procedureData.keys()}") + + # 加载基本信息 + procedure_info = self.procedureData.get('规程信息', {}) + self.nameEdit.setText(procedure_info.get('规程名称', '')) + self.numberEdit.setText(procedure_info.get('规程编号', '')) + self.typeEdit.setText(procedure_info.get('规程类型', '')) + + # 加载描述(可能在不同位置) + description = self.procedureData.get('description', '') + if not description: + # 尝试从测试用例信息中获取描述 + test_case_info = self.procedureData.get('测试用例信息', {}) + description = test_case_info.get('工况描述', '') + self.descriptionEdit.setPlainText(description) + + # 加载步骤数据 + self.loadStepsData() + + def loadStepsData(self): + """加载步骤数据到表格(主子步骤都显示,字段驼峰)""" + steps = [] + # 兼容老数据 + if 'testSteps' in self.procedureData: + steps = self.procedureData['testSteps'] + elif '测试步骤' in self.procedureData: + steps = self.procedureData['测试步骤'] + if not steps: + self.stepsTable.setRowCount(0) + return + # 计算总行数 + totalRows = 0 + for step in steps: + totalRows += 1 + totalRows += len(step.get('subSteps', []) or step.get('子步骤', [])) + self.stepsTable.setRowCount(totalRows) + currentRow = 0 + for mainStepIndex, mainStep in enumerate(steps): + # 主步骤 + orderItem = QTableWidgetItem(str(mainStepIndex + 1)) + orderItem.setFlags(orderItem.flags() & ~Qt.ItemIsEditable) + self.stepsTable.setItem(currentRow, 0, orderItem) + stepId = mainStep.get('stepId') or mainStep.get('步骤ID', '') or '' + self.stepsTable.setItem(currentRow, 1, QTableWidgetItem(stepId)) + operation = mainStep.get('operation') or mainStep.get('步骤描述', '') or '' + self.stepsTable.setItem(currentRow, 2, QTableWidgetItem(operation)) + stepType = mainStep.get('stepType') or mainStep.get('操作类型', '') or '' + self.stepsTable.setItem(currentRow, 3, QTableWidgetItem(stepType)) + expectedValue = mainStep.get('expectedValue') or mainStep.get('预期结果', '') or '' + self.stepsTable.setItem(currentRow, 4, QTableWidgetItem(str(expectedValue))) + remark = mainStep.get('remark') or mainStep.get('备注', '') or '' + self.stepsTable.setItem(currentRow, 5, QTableWidgetItem(remark)) + # 主步骤样式 + for col in range(6): + item = self.stepsTable.item(currentRow, col) + if item: + item.setBackground(QBrush(QColor(220, 220, 220))) + font = item.font() + font.setBold(True) + item.setFont(font) + currentRow += 1 + # 子步骤 + subSteps = mainStep.get('subSteps') or mainStep.get('子步骤', []) + for subIndex, subStep in enumerate(subSteps): + orderItem = QTableWidgetItem(str(subStep.get('order', subStep.get('序号', subIndex + 1)))) + orderItem.setFlags(orderItem.flags() & ~Qt.ItemIsEditable) + self.stepsTable.setItem(currentRow, 0, orderItem) + subStepId = subStep.get('stepId') or f"{stepId}{subStep.get('order', subStep.get('序号', subIndex + 1))}" or '' + self.stepsTable.setItem(currentRow, 1, QTableWidgetItem(subStepId)) + subOperation = subStep.get('operation') or subStep.get('操作', '') or '' + self.stepsTable.setItem(currentRow, 2, QTableWidgetItem(subOperation)) + subType = subStep.get('stepType') or subStep.get('操作类型', '') or '' + self.stepsTable.setItem(currentRow, 3, QTableWidgetItem(subType)) + subExpected = subStep.get('expectedValue') or subStep.get('预期结果', '') or '' + self.stepsTable.setItem(currentRow, 4, QTableWidgetItem(str(subExpected))) + subRemark = subStep.get('remark') or subStep.get('备注', '') or '' + self.stepsTable.setItem(currentRow, 5, QTableWidgetItem(subRemark)) + currentRow += 1 + self.stepsTable.resizeRowsToContents() + + def addStep(self): + """添加步骤(默认为子步骤)""" + dialog = StepEditDialog(parent=self) + if dialog.exec_() == QDialog.Accepted: + step_data = dialog.getStepData() + + # 添加到表格 + row = self.stepsTable.rowCount() + self.stepsTable.insertRow(row) + + # 设置序号 + order_item = QTableWidgetItem(str(row + 1)) + order_item.setFlags(order_item.flags() & ~Qt.ItemIsEditable) + self.stepsTable.setItem(row, 0, order_item) + + # 设置其他字段 + self.stepsTable.setItem(row, 1, QTableWidgetItem(step_data['stepId'])) + self.stepsTable.setItem(row, 2, QTableWidgetItem(step_data['operation'])) + self.stepsTable.setItem(row, 3, QTableWidgetItem(step_data['type'])) + self.stepsTable.setItem(row, 4, QTableWidgetItem(step_data['expectedValue'])) + self.stepsTable.setItem(row, 5, QTableWidgetItem(step_data['remark'])) + + # 不设置主步骤样式,默认为子步骤 + + def deleteStep(self): + """删除选中的步骤""" + current_row = self.stepsTable.currentRow() + if current_row < 0: + QMessageBox.warning(self, "未选择步骤", "请先选择要删除的步骤") + return + + # 检查是否是最后一个步骤 + total_rows = self.stepsTable.rowCount() + is_last_step = (total_rows == 1) + + if is_last_step: + reply = QMessageBox.question( + self, "确认删除", + "这是最后一个步骤,删除后规程将为空。确定要删除吗?", + QMessageBox.Yes | QMessageBox.No + ) + else: + reply = QMessageBox.question( + self, "确认删除", + f"确定要删除第 {current_row + 1} 个步骤吗?", + QMessageBox.Yes | QMessageBox.No + ) + + if reply == QMessageBox.Yes: + self.stepsTable.removeRow(current_row) + # 重新编号 + self.renumberSteps() + + # 如果删除后没有步骤了,给出提示 + if self.stepsTable.rowCount() == 0: + QMessageBox.information( + self, "步骤已清空", + "所有步骤已删除,规程现在为空。您可以添加新步骤或保存空规程。" + ) + + def editStep(self, row, column): + """编辑步骤""" + # 获取当前步骤数据 + stepIdItem = self.stepsTable.item(row, 1) + operationItem = self.stepsTable.item(row, 2) + typeItem = self.stepsTable.item(row, 3) + expectedValueItem = self.stepsTable.item(row, 4) + remarkItem = self.stepsTable.item(row, 5) + + step_data = { + 'stepId': stepIdItem.text() if stepIdItem else '', + 'operation': operationItem.text() if operationItem else '', + 'type': typeItem.text() if typeItem else 'SET', + 'expectedValue': expectedValueItem.text() if expectedValueItem else '', + 'remark': remarkItem.text() if remarkItem else '', + 'order': row + 1 + } + + dialog = StepEditDialog(step_data, self) + if dialog.exec_() == QDialog.Accepted: + updated_data = dialog.getStepData() + + # 更新表格 + self.stepsTable.setItem(row, 1, QTableWidgetItem(updated_data['stepId'])) + self.stepsTable.setItem(row, 2, QTableWidgetItem(updated_data['operation'])) + self.stepsTable.setItem(row, 3, QTableWidgetItem(updated_data['type'])) + self.stepsTable.setItem(row, 4, QTableWidgetItem(updated_data['expectedValue'])) + self.stepsTable.setItem(row, 5, QTableWidgetItem(updated_data['remark'])) + + def renumberSteps(self): + """重新编号步骤""" + for row in range(self.stepsTable.rowCount()): + order_item = QTableWidgetItem(str(row + 1)) + order_item.setFlags(order_item.flags() & ~Qt.ItemIsEditable) + + # 检查这一行是否原本是主步骤(通过检查其他列的背景色) + is_main_step = False + for col in range(1, 6): # 检查步骤ID、操作、类型、预期值、备注列 + item = self.stepsTable.item(row, col) + if item and hasattr(item, 'background') and item.background().color() == QColor(220, 220, 220): + is_main_step = True + break + + # 如果是主步骤,设置背景色和字体 + if is_main_step: + order_item.setBackground(QBrush(QColor(220, 220, 220))) + font = order_item.font() + font.setBold(True) + order_item.setFont(font) + + self.stepsTable.setItem(row, 0, order_item) + + def getStepsData(self): + """获取表格中的步骤数据""" + steps = [] + for row in range(self.stepsTable.rowCount()): + stepIdItem = self.stepsTable.item(row, 1) + operationItem = self.stepsTable.item(row, 2) + typeItem = self.stepsTable.item(row, 3) + expectedValueItem = self.stepsTable.item(row, 4) + remarkItem = self.stepsTable.item(row, 5) + + step = { + 'order': row + 1, + 'stepId': stepIdItem.text() if stepIdItem else '', + 'operation': operationItem.text() if operationItem else '', + 'type': typeItem.text() if typeItem else 'SET', + 'expectedValue': expectedValueItem.text() if expectedValueItem else '', + 'remark': remarkItem.text() if remarkItem else '' + } + steps.append(step) + return steps + + def saveProcedure(self): + """保存规程到数据库(字段驼峰,主子步骤分组)""" + try: + procedureInfo = { + 'procedureName': self.nameEdit.text().strip(), + 'procedureNumber': self.numberEdit.text().strip(), + 'procedureType': self.typeEdit.text().strip() + } + + # 数据验证和默认值处理 + if not procedureInfo['procedureName']: + QMessageBox.warning(self, "验证失败", "规程名称不能为空") + return + if not procedureInfo['procedureNumber']: + QMessageBox.warning(self, "验证失败", "规程编号不能为空") + return + + # 如果规程类型为空,设置默认值 + if not procedureInfo['procedureType']: + procedureInfo['procedureType'] = '标准规程' + + testSteps = [] + currentMainStep = None + print(f"开始处理步骤,总行数: {self.stepsTable.rowCount()}") + for row in range(self.stepsTable.rowCount()): + try: + stepIdItem = self.stepsTable.item(row, 1) + operationItem = self.stepsTable.item(row, 2) + stepTypeItem = self.stepsTable.item(row, 3) + expectedValueItem = self.stepsTable.item(row, 4) + remarkItem = self.stepsTable.item(row, 5) + + stepId = stepIdItem.text() if stepIdItem else '' + operation = operationItem.text() if operationItem else '' + stepType = stepTypeItem.text() if stepTypeItem else '' + expectedValue = expectedValueItem.text() if expectedValueItem else '' + remark = remarkItem.text() if remarkItem else '' + + print(f"处理第{row+1}行: stepId='{stepId}', operation='{operation}', stepType='{stepType}', expectedValue='{expectedValue}', remark='{remark}'") + + # 简化验证:只要步骤ID或操作描述不为空就认为是有效步骤 + if not (stepId.strip() or operation.strip()): + print(f" 跳过第{row+1}行:步骤ID和操作描述都为空") + continue + + # 判断是否为主步骤(通过背景色判断) + isMainStep = False + item = self.stepsTable.item(row, 0) + if item and hasattr(item, 'background') and item.background().color() == QColor(220, 220, 220): + isMainStep = True + + print(f" 第{row+1}行是主步骤: {isMainStep}") + + if isMainStep: + # 如果有前一个主步骤,先添加到结果中 + if currentMainStep is not None: + testSteps.append(currentMainStep) + print(f" 添加主步骤到testSteps,当前testSteps长度: {len(testSteps)}") + # 创建新的主步骤 + currentMainStep = { + '步骤ID': stepId, + '步骤描述': operation, + '操作类型': stepType, + '预期结果': expectedValue, + '备注': remark, + '子步骤': [] + } + print(f" 创建新主步骤: {stepId}") + else: + # 子步骤 + if currentMainStep is not None: + orderItem = self.stepsTable.item(row, 0) + orderText = orderItem.text() if orderItem else '1' + try: + orderNum = int(orderText) + except ValueError: + orderNum = 1 + subStep = { + '序号': orderNum, + '操作': operation, + '操作类型': stepType, + '预期结果': expectedValue, + '实际结果': '', + '一致性': '是', + '测试时间': '', + '备注': remark + } + currentMainStep['子步骤'].append(subStep) + print(f" 添加子步骤到当前主步骤,子步骤数量: {len(currentMainStep['子步骤'])}") + except Exception as rowError: + print(f"处理第 {row + 1} 行时出错: {str(rowError)}") + continue + + # 添加最后一个主步骤 + if currentMainStep is not None: + testSteps.append(currentMainStep) + print(f"添加最后一个主步骤,最终testSteps长度: {len(testSteps)}") + else: + print("没有找到任何主步骤") + + # 修改验证逻辑:允许保存空规程,但给出提示 + if not testSteps: + reply = QMessageBox.question( + self, "确认保存", + "当前规程没有步骤,确定要保存空规程吗?", + QMessageBox.Yes | QMessageBox.No + ) + if reply == QMessageBox.No: + return + updatedProcedure = { + '规程信息': { + '规程名称': procedureInfo['procedureName'], + '规程编号': procedureInfo['procedureNumber'], + '规程类型': procedureInfo['procedureType'] + }, + '测试用例信息': { + '测试用例': procedureInfo['procedureName'], + '用例编号': procedureInfo['procedureNumber'], + '工况描述': self.descriptionEdit.toPlainText() + }, + '测试步骤': testSteps, + 'updatedAt': datetime.now().isoformat() + } + print(f"准备保存规程: {procedureInfo['procedureName']}") + print(f"步骤数量: {len(testSteps)}") + success = self.dbManager.updateProcedure(self.procedureId, updatedProcedure) + if success: + QMessageBox.information(self, "保存成功", "规程已成功保存到数据库") + self.procedureSaved.emit(self.procedureId) + if self.parent: + currentIndex = self.parent.tabs.currentIndex() + if currentIndex > 0: + self.parent.tabs.removeTab(currentIndex) + else: + QMessageBox.critical(self, "保存失败", "保存规程到数据库时发生错误") + except Exception as e: + print(f"保存规程时发生错误: {str(e)}") + import traceback + traceback.print_exc() + QMessageBox.critical(self, "保存失败", f"保存规程时发生错误:{str(e)}") + + def cancelEdit(self): + """取消编辑""" + reply = QMessageBox.question( + self, "确认取消", + "确定要取消编辑吗?未保存的更改将丢失。", + QMessageBox.Yes | QMessageBox.No + ) + + if reply == QMessageBox.Yes: + # 关闭编辑器标签页 + if self.parent: + current_index = self.parent.tabs.currentIndex() + if current_index > 0: # 不是规程管理标签页 + self.parent.tabs.removeTab(current_index) + + def showContextMenu(self, pos): + """显示右键菜单""" + menu = QMenu(self) + + # 获取当前行 + current_row = self.stepsTable.rowAt(pos.y()) + + # 添加步骤 + add_action = menu.addAction("添加步骤") + add_action.triggered.connect(self.addStep) + + # 插入步骤(在选中行之前) + if current_row >= 0: + insert_action = menu.addAction(f"在此行前插入步骤") + insert_action.triggered.connect(lambda: self.insertStep(current_row)) + + # 编辑步骤 + if current_row >= 0: + edit_action = menu.addAction("编辑步骤") + edit_action.triggered.connect(lambda: self.editStep(current_row, 0)) + + # 删除步骤 + if current_row >= 0: + delete_action = menu.addAction("删除步骤") + delete_action.triggered.connect(self.deleteStep) + + # 显示菜单 + menu.exec_(self.stepsTable.mapToGlobal(pos)) + + def insertStep(self, position): + """在指定位置插入新步骤(默认为子步骤)""" + dialog = StepEditDialog(parent=self) + if dialog.exec_() == QDialog.Accepted: + step_data = dialog.getStepData() + + # 在指定位置插入行 + self.stepsTable.insertRow(position) + + # 设置序号 + order_item = QTableWidgetItem(str(position + 1)) + order_item.setFlags(order_item.flags() & ~Qt.ItemIsEditable) + self.stepsTable.setItem(position, 0, order_item) + + # 设置其他字段 + self.stepsTable.setItem(position, 1, QTableWidgetItem(step_data['stepId'])) + self.stepsTable.setItem(position, 2, QTableWidgetItem(step_data['operation'])) + self.stepsTable.setItem(position, 3, QTableWidgetItem(step_data['type'])) + self.stepsTable.setItem(position, 4, QTableWidgetItem(step_data['expectedValue'])) + self.stepsTable.setItem(position, 5, QTableWidgetItem(step_data['remark'])) + + # 不设置主步骤样式,默认为子步骤 + # 重新编号所有步骤 + self.renumberSteps() \ No newline at end of file diff --git a/UI/ProcedureManager/ProcedureManager.py b/UI/ProcedureManager/ProcedureManager.py index 039d53e..9a6193d 100644 --- a/UI/ProcedureManager/ProcedureManager.py +++ b/UI/ProcedureManager/ProcedureManager.py @@ -21,6 +21,7 @@ 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): @@ -212,6 +213,14 @@ class ProcedureManager(QMainWindow): # 修改:只在多轮次执行完成时自动重置,单轮次执行完成时不重置 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): """创建规程管理主标签页""" @@ -347,6 +356,17 @@ class ProcedureManager(QMainWindow): 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() @@ -393,18 +413,64 @@ class ProcedureManager(QMainWindow): categoryId = currentItem.data(Qt.UserRole) if currentItem else None procInfo = parsedData["规程信息"] - # 添加空字符串作为report_path参数 + 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, - procInfo["规程名称"], - procInfo["规程编号"], + procedureName, + procedureNumber, procInfo["规程类型"], parsedData, "" # 显式传递空字符串作为report_path ) if procId: - self.statusBar.showMessage(f"成功导入规程: {procInfo['规程名称']}", 5000) + self.statusBar.showMessage(f"成功导入规程: {procedureName}", 5000) self.loadProcedures(categoryId) else: QMessageBox.warning(self, "导入失败", "无法导入规程,请检查数据库连接") @@ -574,14 +640,30 @@ class ProcedureManager(QMainWindow): def showProcedureContextMenu(self, pos): """显示规程列表的右键菜单""" item = self.procedureList.itemAt(pos) + menu = QMenu() + if item: - menu = QMenu() - deleteAction = menu.addAction("删除规程") + # 编辑规程 + 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) - menu.exec_(self.procedureList.mapToGlobal(pos)) + else: + # 在空白区域右键时显示新建规程 + pass + + menu.exec_(self.procedureList.mapToGlobal(pos)) # 批量执行相关方法 - def batchExecuteProcedures(self): + def batchExecuteProcedures(self): """开始批量执行当前分类中的所有规程""" # 检查是否有正在运行的执行器 if self.hasRunningExecutor(): @@ -753,3 +835,131 @@ class ProcedureManager(QMainWindow): 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)}") diff --git a/UI/ProcedureManager/StepExecutor.py b/UI/ProcedureManager/StepExecutor.py index cecdee2..d7cb20f 100644 --- a/UI/ProcedureManager/StepExecutor.py +++ b/UI/ProcedureManager/StepExecutor.py @@ -17,6 +17,9 @@ from datetime import datetime import random import json import time +from openpyxl import Workbook +from openpyxl.styles import Font, Alignment, PatternFill +from openpyxl.utils import get_column_letter from model.ProcedureModel.ProcedureProcessor import StepTableModel from utils.DBModels.ProcedureModel import DatabaseManager @@ -35,7 +38,10 @@ class StepExecutor(QWidget): self.isRunning = False self.isActive = False - testSteps = procedureData["测试步骤"] + # 修复:添加数据健壮性检查 + testSteps = procedureData.get("测试步骤", []) + if not testSteps: + testSteps = [] # 确保不为None self.protocolManager = Globals.getValue("protocolManage") @@ -81,81 +87,195 @@ class StepExecutor(QWidget): def createInfoSection(self, layout): """创建信息显示区域""" - infoLayout = QFormLayout() - infoLayout.setLabelAlignment(Qt.AlignRight) - - infoItems = [ - ("规程名称:", self.procedureData["规程信息"]["规程名称"]), - ("规程编号:", self.procedureData["规程信息"]["规程编号"]), - ("规程类型:", self.procedureData["规程信息"]["规程类型"]), - ("测试用例:", self.procedureData["测试用例信息"]["测试用例"]), - ("用例编号:", self.procedureData["测试用例信息"]["用例编号"]), - ("工况描述:", self.procedureData["测试用例信息"]["工况描述"]) - ] - - for label, value in infoItems: - infoLayout.addRow(label, QLabel(value)) - - layout.addLayout(infoLayout) + # 创建主信息容器 + infoContainer = QWidget() + infoContainer.setObjectName("procedureInfoContainer") + + infoLayout = QVBoxLayout() + infoContainer.setLayout(infoLayout) + + # 兼容新旧数据结构 + procedure_info = self.procedureData.get("规程信息", {}) + test_case_info = self.procedureData.get("测试用例信息", {}) + + # 获取规程信息,支持新旧字段名 + procedure_name = procedure_info.get("规程名称", "") or procedure_info.get("procedureName", "") + procedure_number = procedure_info.get("规程编号", "") or procedure_info.get("procedureNumber", "") + procedure_type = procedure_info.get("规程类型", "") or procedure_info.get("procedureType", "") + + # 获取测试用例信息,支持新旧字段名 + test_case = test_case_info.get("测试用例", "") or test_case_info.get("testCase", "") + case_number = test_case_info.get("用例编号", "") or test_case_info.get("caseNumber", "") + condition_description = test_case_info.get("工况描述", "") or test_case_info.get("conditionDescription", "") + + # 第一行:规程基本信息(三个并排) + procedureRow = QHBoxLayout() + procedureRow.setSpacing(15) + + # 规程名称 + nameGroup = self.createInfoGroup("规程名称", procedure_name, "procedureNameGroup", "procedureNameLabel") + procedureRow.addWidget(nameGroup) + + # 规程编号 + numberGroup = self.createInfoGroup("规程编号", procedure_number, "procedureNumberGroup", "procedureNumberLabel") + procedureRow.addWidget(numberGroup) + + # 规程类型 + typeGroup = self.createInfoGroup("规程类型", procedure_type, "procedureTypeGroup", "procedureTypeLabel") + procedureRow.addWidget(typeGroup) + + procedureRow.addStretch() + infoLayout.addLayout(procedureRow) + + # 第二行:测试用例信息(两个并排) + testCaseRow = QHBoxLayout() + testCaseRow.setSpacing(15) + + # 测试用例 + testCaseGroup = self.createInfoGroup("测试用例", test_case, "testCaseGroup", "testCaseLabel") + testCaseRow.addWidget(testCaseGroup) + + # 用例编号 + caseNumberGroup = self.createInfoGroup("用例编号", case_number, "caseNumberGroup", "caseNumberLabel") + testCaseRow.addWidget(caseNumberGroup) + + testCaseRow.addStretch() + infoLayout.addLayout(testCaseRow) + + # 第三行:工况描述(独占一行,因为可能较长) + if condition_description: + descriptionRow = QHBoxLayout() + descriptionGroup = self.createInfoGroup("工况描述", condition_description, "conditionDescriptionGroup", "conditionDescriptionLabel", isDescription=True) + descriptionRow.addWidget(descriptionGroup) + descriptionRow.addStretch() + infoLayout.addLayout(descriptionRow) + + layout.addWidget(infoContainer) layout.addSpacing(20) + + def createInfoGroup(self, label, value, groupObjectName, labelObjectName, isDescription=False): + """创建信息分组组件""" + groupWidget = QWidget() + groupWidget.setObjectName(groupObjectName) + + groupLayout = QVBoxLayout() + groupLayout.setSpacing(8) + groupWidget.setLayout(groupLayout) + + # 标签 + labelWidget = QLabel(label) + labelWidget.setObjectName(labelObjectName) + groupLayout.addWidget(labelWidget) + + # 值 + valueWidget = QLabel(value if value else "未设置") + if isDescription: + valueWidget.setObjectName("descriptionValue") + valueWidget.setWordWrap(True) + valueWidget.setMaximumHeight(80) + else: + valueWidget.setObjectName("infoGroupValue") + + groupLayout.addWidget(valueWidget) + return groupWidget + def createTableSection(self, layout, testSteps): """创建表格区域""" - self.tableModel = StepTableModel(testSteps) - self.tableView = QTableView() - self.tableView.setModel(self.tableModel) - self.tableView.setContextMenuPolicy(Qt.CustomContextMenu) - self.tableView.customContextMenuRequested.connect(self.showContextMenu) - - self.setupTableHeaders() - - layout.addWidget(QLabel("测试步骤:")) - layout.addWidget(self.tableView) - + try: + 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.setEditTriggers(QTableView.DoubleClicked) + self.tableView.setSelectionBehavior(QTableView.SelectRows) + + self.setupTableHeaders() + + layout.addWidget(QLabel("测试步骤:")) + layout.addWidget(self.tableView) + + except Exception as e: + print(f"创建表格区域时出错: {e}") + import traceback + traceback.print_exc() + # 如果出错,至少创建一个空的表格 + self.tableView = QTableView() + layout.addWidget(QLabel("测试步骤:")) + layout.addWidget(self.tableView) + def setupTableHeaders(self): - """设置表格表头""" - header = self.tableView.horizontalHeader() - if header: - header.setSectionResizeMode(0, QHeaderView.ResizeToContents) - header.setSectionResizeMode(1, QHeaderView.Stretch) - header.setSectionResizeMode(2, QHeaderView.ResizeToContents) - header.setSectionResizeMode(3, QHeaderView.ResizeToContents) - header.setSectionResizeMode(5, QHeaderView.Stretch) - - verticalHeader = self.tableView.verticalHeader() - if verticalHeader: - self.tableView.setWordWrap(True) - verticalHeader.setSectionResizeMode(QHeaderView.ResizeToContents) - + """设置表格头部""" + try: + header = self.tableView.horizontalHeader() + if header: + header.setStretchLastSection(True) + header.setSectionResizeMode(0, QHeaderView.ResizeToContents) + header.setSectionResizeMode(1, QHeaderView.Stretch) + header.setSectionResizeMode(2, QHeaderView.ResizeToContents) + header.setSectionResizeMode(3, QHeaderView.ResizeToContents) + header.setSectionResizeMode(4, QHeaderView.ResizeToContents) + header.setSectionResizeMode(5, QHeaderView.ResizeToContents) + header.setSectionResizeMode(6, QHeaderView.ResizeToContents) + except Exception as e: + print(f"设置表格头部时出错: {e}") + def createControlSection(self, layout): """创建控制按钮区域""" controlLayout = QHBoxLayout() - self.autoButton = QPushButton(" 开始自动执行") + # 开始自动执行按钮 - 绿色主题 + self.autoButton = QPushButton("开始自动执行") self.autoButton.clicked.connect(self.startAutoExecute) - self.autoButton.setIcon(qta.icon('fa5s.play', color='green')) + self.autoButton.setIcon(qta.icon('fa5s.play', color='white')) + self.autoButton.setToolTip("开始自动执行整个规程流程\n将按照设定的轮次和间隔时间自动执行所有步骤") - self.stopButton = QPushButton(" 停止自动执行") + # 停止自动执行按钮 - 红色主题 + self.stopButton = QPushButton("停止自动执行") self.stopButton.clicked.connect(self.stopAutoExecute) self.stopButton.setEnabled(False) - self.stopButton.setIcon(qta.icon('fa5s.stop', color='red')) + self.stopButton.setIcon(qta.icon('fa5s.stop', color='white')) + self.stopButton.setToolTip("停止当前正在执行的自动流程\n可以随时中断执行过程") - self.nextButton = QPushButton(" 执行下一步") + # 执行下一步按钮 - 蓝色主题 + self.nextButton = QPushButton("执行下一步") self.nextButton.clicked.connect(self.executeNextStep) - self.nextButton.setIcon(qta.icon('fa5s.step-forward', color='blue')) + self.nextButton.setIcon(qta.icon('fa5s.step-forward', color='white')) + self.nextButton.setToolTip("手动执行下一个步骤\n用于单步调试和手动控制执行过程") - self.resetButton = QPushButton(" 完全重置") + # 完全重置按钮 - 橙色主题 + self.resetButton = QPushButton("完全重置") self.resetButton.clicked.connect(self.resetExecution) - self.resetButton.setIcon(qta.icon('fa5s.redo', color='orange')) + self.resetButton.setIcon(qta.icon('fa5s.redo', color='white')) + self.resetButton.setToolTip("重置所有执行状态\n清除所有步骤的执行结果和进度") - self.exportButton = QPushButton(" 生成报告") + # 生成报告按钮 - 紫色主题 + self.exportButton = QPushButton("生成报告") self.exportButton.clicked.connect(self.onExportReportClicked) - self.exportButton.setIcon(qta.icon('fa5s.file-alt', color='purple')) - - buttons = [self.autoButton, self.stopButton, self.nextButton, - self.resetButton, self.exportButton] - for button in buttons: - controlLayout.addWidget(button) + self.exportButton.setIcon(qta.icon('fa5s.file-alt', color='white')) + self.exportButton.setToolTip("生成执行报告\n导出详细的执行结果和统计数据") + + # 创建按钮分组布局 + # 第一组:执行控制按钮 + executionGroup = QHBoxLayout() + executionGroup.addWidget(self.autoButton) + executionGroup.addWidget(self.stopButton) + executionGroup.addWidget(self.nextButton) + # executionGroup.addStretch() # 添加弹性空间 + + # 第二组:管理按钮 + # managementGroup = QHBoxLayout() + # managementGroup.addStretch() # 添加弹性空间 + executionGroup.addWidget(self.resetButton) + executionGroup.addWidget(self.exportButton) + controlLayout.setSpacing(15) + + # 将两组按钮添加到主布局 + controlLayout.addLayout(executionGroup) + # controlLayout.addLayout(managementGroup) layout.addLayout(controlLayout) @@ -175,12 +295,12 @@ class StepExecutor(QWidget): # 状态显示 self.statusLabel = QLabel("就绪") - self.statusLabel.setStyleSheet("color: blue; font-weight: bold;") + self.statusLabel.setObjectName("statusLabel") cycleLayout.addWidget(self.statusLabel) # 倒计时显示 self.countdownLabel = QLabel("") - self.countdownLabel.setStyleSheet("color: red; font-weight: bold; font-size: 14px;") + self.countdownLabel.setObjectName("countdownLabel") cycleLayout.addWidget(self.countdownLabel) # 步骤间隔设置 @@ -1032,19 +1152,19 @@ class StepExecutor(QWidget): else: countdownText = f"当前轮次剩余: {seconds}秒 (进度: {progressPercent:.1f}%)" - # 根据剩余时间设置颜色 + # 根据剩余时间设置颜色状态 if self.remainingTime <= 10: - self.countdownLabel.setStyleSheet("color: red; font-weight: bold; font-size: 14px;") + self.countdownLabel.setProperty("timeRemaining", "low") elif self.remainingTime <= 30: - self.countdownLabel.setStyleSheet("color: orange; font-weight: bold; font-size: 14px;") + self.countdownLabel.setProperty("timeRemaining", "medium") else: - self.countdownLabel.setStyleSheet("color: blue; font-weight: bold; font-size: 14px;") + self.countdownLabel.setProperty("timeRemaining", "high") self.countdownLabel.setText(countdownText) self.remainingTime -= 1 else: + self.countdownLabel.setProperty("timeRemaining", "completed") self.countdownLabel.setText("当前轮次完成") - self.countdownLabel.setStyleSheet("color: green; font-weight: bold; font-size: 14px;") self.countdownTimer.stop() def resetCountdown(self): diff --git a/bin.py b/bin.py index 7cf5676..5d92ef9 100644 --- a/bin.py +++ b/bin.py @@ -11,7 +11,7 @@ import sys if __name__ == '__main__': app = QApplication(sys.argv) app.setStyle(QStyleFactory.create('Fusion')) - app.setStyleSheet(CommonHelper.readQss('static/main.qss') + app.setStyleSheet(CommonHelper.readQss('static/Main.qss') + CommonHelper.readQss('static/profibus.qss') + CommonHelper.readQss('static/Area.qss')) reg = Register() diff --git a/model/ProcedureModel/ProcedureProcessor.py b/model/ProcedureModel/ProcedureProcessor.py index b214b1d..53f90a0 100644 --- a/model/ProcedureModel/ProcedureProcessor.py +++ b/model/ProcedureModel/ProcedureProcessor.py @@ -16,15 +16,15 @@ class ExcelParser: sheet = wb.active specInfo = { - "规程名称": sheet['B1'].value, - "规程编号": sheet['D1'].value, - "规程类型": sheet['F1'].value + "规程名称": sheet['B1'].value or "", + "规程编号": sheet['D1'].value or "", + "规程类型": sheet['F1'].value or "" } testCaseInfo = { - "测试用例": sheet['B2'].value, - "用例编号": sheet['D2'].value, - "工况描述": sheet['H2'].value + "测试用例": sheet['B2'].value or "", + "用例编号": sheet['D2'].value or "", + "工况描述": sheet['H2'].value or "" } testSteps = [] @@ -72,8 +72,16 @@ class ExcelParser: return { "文件路径": filePath, - "规程信息": specInfo, - "测试用例信息": testCaseInfo, + "规程信息": { + "规程名称": specInfo["规程名称"], + "规程编号": specInfo["规程编号"], + "规程类型": specInfo["规程类型"] + }, + "测试用例信息": { + "测试用例": testCaseInfo["测试用例"], + "用例编号": testCaseInfo["用例编号"], + "工况描述": testCaseInfo["工况描述"] + }, "测试步骤": testSteps } @@ -95,7 +103,7 @@ class StepTableModel(QAbstractTableModel): 'stepType': mainStep['操作类型'], 'time': None, 'result': None, - 'note': "" # 新增备注字段,主步骤默认为空 + 'note': mainStep.get('备注', '') # 修复:从主步骤的备注字段读取 }) self.stepIndex += 1 @@ -129,11 +137,11 @@ class StepTableModel(QAbstractTableModel): if role == Qt.DisplayRole: if col == 0: - return step['stepId'] + return step['stepId'] or '' elif col == 1: - return step['description'] + return step['description'] or '' elif col == 2: - return step['stepType'] + return step['stepType'] or '' elif col == 3: return step['time'].strftime("%Y-%m-%d %H:%M:%S") if step['time'] else '' elif col == 4: @@ -145,7 +153,7 @@ class StepTableModel(QAbstractTableModel): return '✓' elif col == 5: # print(step['result']) - return step['result'] + return step['result'] or '' elif col == 6: return step['note'] if step['note'] else '' @@ -173,6 +181,11 @@ class StepTableModel(QAbstractTableModel): return None + def flags(self, index): + """返回单元格标志""" + flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable + return flags + def headerData(self, section, orientation, role): if role == Qt.DisplayRole and orientation == Qt.Horizontal: return self.columns[section] @@ -221,3 +234,50 @@ class StepTableModel(QAbstractTableModel): self.dataChanged.emit(self.index(0, 0), self.index(self.rowCount()-1, self.columnCount()-1), [Qt.DisplayRole, Qt.BackgroundRole]) + + def getTestSteps(self): + """获取测试步骤数据,用于保存""" + testSteps = [] + currentMainStep = None + + for step in self.stepData: + if step['isMain']: + # 如果有前一个主步骤,先添加到结果中 + if currentMainStep: + testSteps.append(currentMainStep) + + # 创建新的主步骤 + currentMainStep = { + '步骤ID': step['stepId'], + '步骤描述': step['description'], + '操作类型': step['stepType'], + '预期结果': '', + '子步骤': [] + } + else: + # 子步骤 + if currentMainStep: + # 从步骤ID中提取序号 + stepId = step['stepId'] + if currentMainStep['步骤ID'] in stepId: + subStepId = stepId.replace(currentMainStep['步骤ID'], '') + else: + subStepId = stepId + + subStep = { + '序号': subStepId, + '操作': step['description'], + '操作类型': step['stepType'], + '预期结果': '', + '实际结果': step.get('result', ''), + '一致性': '是' if step.get('result') and '失败' not in step.get('result', '') else '否', + '测试时间': step.get('time', ''), + '备注': step.get('note', '') + } + currentMainStep['子步骤'].append(subStep) + + # 添加最后一个主步骤 + if currentMainStep: + testSteps.append(currentMainStep) + + return testSteps diff --git a/protocol/ProtocolManage.py b/protocol/ProtocolManage.py index b650ab6..102b39d 100644 --- a/protocol/ProtocolManage.py +++ b/protocol/ProtocolManage.py @@ -96,6 +96,13 @@ class ProtocolManage(object): """ varInfo = self.lookupVariable(variableName) if not varInfo: + if self.RpcServer: + existsVar, clientNames = self.RpcServer.existsVar(variableName) + if existsVar: + value = self.RpcServer.writeVar(variableName, value) + return True + else: + return False return False modelType = varInfo['model_type'] @@ -165,14 +172,7 @@ class ProtocolManage(object): elif modelType == 'HartSimulateVar': # 仅设置值,不保存到数据库 pass - else: - if self.RpcServer: - existsVar, clientNames = self.RpcServer.existsVar(variableName) - if existsVar: - value = self.RpcServer.writeVar(variableName, value) - return True - else: - return True + if self.RpcClient: self.RpcClient.setVarContent(variableName, value, info['min'], info['max'], info['varType']) @@ -189,8 +189,18 @@ class ProtocolManage(object): :param variableName: 变量名 :return: 读取的值或None(失败时) """ + # varInfo = self.lookupVariable(variableName) if not varInfo: + if self.RpcServer: + print(variableName, 1111111111111111111) + existsVar, clientNames = self.RpcServer.existsVar(variableName) + if existsVar: + # print(clientNames, 1111111111111111) + value =float(self.RpcServer.getVarValue(clientNames[0], variableName)['value']) + return value + else: + return None return None modelType = varInfo['model_type'] @@ -202,13 +212,13 @@ class ProtocolManage(object): # 读取操作(留空) pass - if modelType == 'ModbusTcpSlaveVar': + elif modelType == 'ModbusTcpSlaveVar': pass - if modelType == 'ModbusRtuMasterVar': + elif modelType == 'ModbusRtuMasterVar': pass - if modelType == 'ModbusRtuSlaveVar': + elif modelType == 'ModbusRtuSlaveVar': pass # HART协议变量处理 @@ -258,14 +268,7 @@ class ProtocolManage(object): elif modelType == 'HartSimulateVar': pass # print(1111111111111111) - if self.RpcServer: - existsVar, clientNames = self.RpcServer.existsVar(variableName) - if existsVar: - # print(clientNames, 1111111111111111) - value = self.RpcServer.getVarValue(clientNames[0], variableName) - return value - else: - return None + return None # 暂时返回None except Exception as e: diff --git a/utils/DBModels/ProcedureModel.py b/utils/DBModels/ProcedureModel.py index f61b0e6..683eb50 100644 --- a/utils/DBModels/ProcedureModel.py +++ b/utils/DBModels/ProcedureModel.py @@ -180,6 +180,27 @@ class DatabaseManager: self.conn.commit() return self.cursor.rowcount > 0 + def updateProcedure(self, procedureId, content): + """更新规程信息""" + try: + # 从content中提取基本信息 + procedure_info = content.get('规程信息', {}) + name = procedure_info.get('规程名称', '') + number = procedure_info.get('规程编号', '') + type_info = procedure_info.get('规程类型', '') + + contentJson = json.dumps(content, ensure_ascii=False) + self.cursor.execute(""" + UPDATE procedures + SET name = ?, number = ?, type = ?, content = ? + WHERE id = ? + """, (name, number, type_info, contentJson, procedureId)) + self.conn.commit() + return self.cursor.rowcount > 0 + except Exception as e: + print(f"更新规程失败: {str(e)}") + return False + def insertProcedureExecution(self, procedureId, startTime, result=None, reportPath="", stepResults="[]"): self.cursor.execute(""" INSERT INTO procedure_execution_history @@ -392,4 +413,28 @@ class DatabaseManager: FROM step_keywords ORDER BY operation_type """) - return [row[0] for row in self.cursor.fetchall()] \ No newline at end of file + return [row[0] for row in self.cursor.fetchall()] + + def getProcedureByNameAndNumber(self, name, number): + """根据规程名称和编号查找规程""" + self.cursor.execute(""" + SELECT id, name, number, type, category_id + FROM procedures + WHERE name = ? AND number = ? + """, (name, number)) + return self.cursor.fetchone() + + def updateProcedureById(self, procedureId, categoryId, name, number, type, content, reportPath=""): + """根据ID更新规程""" + try: + contentJson = json.dumps(content, ensure_ascii=False) + self.cursor.execute(""" + UPDATE procedures + SET category_id = ?, name = ?, number = ?, type = ?, content = ?, report_path = ? + WHERE id = ? + """, (categoryId, name, number, type, contentJson, reportPath, procedureId)) + self.conn.commit() + return self.cursor.rowcount > 0 + except Exception as e: + print(f"更新规程失败: {str(e)}") + return False \ No newline at end of file diff --git a/utils/Globals.py b/utils/Globals.py index c5c9154..1069cd9 100644 --- a/utils/Globals.py +++ b/utils/Globals.py @@ -1,3 +1,6 @@ +# 初始化全局字典 +_globalDict = {} + def _init():#初始化 global _globalDict _globalDict = {} @@ -30,6 +33,9 @@ def _init():#初始化 _globalDict['protocolManage'] = None +# 确保初始化 +_init() + def setValue(key,value): """ 定义一个全局变量 """ _globalDict[key] = value diff --git a/windoweffect/__init__.py b/windoweffect/__init__.py deleted file mode 100644 index a3fb4ed..0000000 --- a/windoweffect/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .window_effect import WindowEffect -from .c_structures import * \ No newline at end of file diff --git a/windoweffect/c_structures.py b/windoweffect/c_structures.py deleted file mode 100644 index 1553383..0000000 --- a/windoweffect/c_structures.py +++ /dev/null @@ -1,135 +0,0 @@ -# coding:utf-8 - -from ctypes import POINTER, Structure, c_int -from ctypes.wintypes import DWORD, HWND, ULONG, POINT, RECT, UINT -from enum import Enum - - -class WINDOWCOMPOSITIONATTRIB(Enum): - WCA_UNDEFINED = 0 - WCA_NCRENDERING_ENABLED = 1 - WCA_NCRENDERING_POLICY = 2 - WCA_TRANSITIONS_FORCEDISABLED = 3 - WCA_ALLOW_NCPAINT = 4 - WCA_CAPTION_BUTTON_BOUNDS = 5 - WCA_NONCLIENT_RTL_LAYOUT = 6 - WCA_FORCE_ICONIC_REPRESENTATION = 7 - WCA_EXTENDED_FRAME_BOUNDS = 8 - WCA_HAS_ICONIC_BITMAP = 9 - WCA_THEME_ATTRIBUTES = 10 - WCA_NCRENDERING_EXILED = 11 - WCA_NCADORNMENTINFO = 12 - WCA_EXCLUDED_FROM_LIVEPREVIEW = 13 - WCA_VIDEO_OVERLAY_ACTIVE = 14 - WCA_FORCE_ACTIVEWINDOW_APPEARANCE = 15 - WCA_DISALLOW_PEEK = 16 - WCA_CLOAK = 17 - WCA_CLOAKED = 18 - WCA_ACCENT_POLICY = 19 - WCA_FREEZE_REPRESENTATION = 20 - WCA_EVER_UNCLOAKED = 21 - WCA_VISUAL_OWNER = 22 - WCA_LAST = 23 - - -class ACCENT_STATE(Enum): - """ Client area status enumeration class """ - ACCENT_DISABLED = 0 - ACCENT_ENABLE_GRADIENT = 1 - ACCENT_ENABLE_TRANSPARENTGRADIENT = 2 - ACCENT_ENABLE_BLURBEHIND = 3 # Aero effect - ACCENT_ENABLE_ACRYLICBLURBEHIND = 4 # Acrylic effect - ACCENT_ENABLE_HOSTBACKDROP = 5 # Mica effect - ACCENT_INVALID_STATE = 6 - - -class ACCENT_POLICY(Structure): - """ Specific attributes of client area """ - - _fields_ = [ - ("AccentState", DWORD), - ("AccentFlags", DWORD), - ("GradientColor", DWORD), - ("AnimationId", DWORD), - ] - - -class WINDOWCOMPOSITIONATTRIBDATA(Structure): - _fields_ = [ - ("Attribute", DWORD), - # Pointer() receives any ctypes type and returns a pointer type - ("Data", POINTER(ACCENT_POLICY)), - ("SizeOfData", ULONG), - ] - - -class DWMNCRENDERINGPOLICY(Enum): - DWMNCRP_USEWINDOWSTYLE = 0 - DWMNCRP_DISABLED = 1 - DWMNCRP_ENABLED = 2 - DWMNCRP_LAS = 3 - - -class DWMWINDOWATTRIBUTE(Enum): - DWMWA_NCRENDERING_ENABLED = 1 - DWMWA_NCRENDERING_POLICY = 2 - DWMWA_TRANSITIONS_FORCEDISABLED = 3 - DWMWA_ALLOW_NCPAINT = 4 - DWMWA_CAPTION_BUTTON_BOUNDS = 5 - DWMWA_NONCLIENT_RTL_LAYOUT = 6 - DWMWA_FORCE_ICONIC_REPRESENTATION = 7 - DWMWA_FLIP3D_POLICY = 8 - DWMWA_EXTENDED_FRAME_BOUNDS = 9 - DWMWA_HAS_ICONIC_BITMAP = 10 - DWMWA_DISALLOW_PEEK = 11 - DWMWA_EXCLUDED_FROM_PEEK = 12 - DWMWA_CLOAK = 13 - DWMWA_CLOAKED = 14 - DWMWA_FREEZE_REPRESENTATION = 15 - DWMWA_PASSIVE_UPDATE_MODE = 16 - DWMWA_USE_HOSTBACKDROPBRUSH = 17 - DWMWA_USE_IMMERSIVE_DARK_MODE = 18 - DWMWA_WINDOW_CORNER_PREFERENCE = 19 - DWMWA_BORDER_COLOR = 20 - DWMWA_CAPTION_COLOR = 21 - DWMWA_TEXT_COLOR = 22 - DWMWA_VISIBLE_FRAME_BORDER_THICKNESS = 23 - DWMWA_LAST = 24 - - -class MARGINS(Structure): - _fields_ = [ - ("cxLeftWidth", c_int), - ("cxRightWidth", c_int), - ("cyTopHeight", c_int), - ("cyBottomHeight", c_int), - ] - - -class MINMAXINFO(Structure): - _fields_ = [ - ("ptReserved", POINT), - ("ptMaxSize", POINT), - ("ptMaxPosition", POINT), - ("ptMinTrackSize", POINT), - ("ptMaxTrackSize", POINT), - ] - - -class PWINDOWPOS(Structure): - _fields_ = [ - ('hWnd', HWND), - ('hwndInsertAfter', HWND), - ('x', c_int), - ('y', c_int), - ('cx', c_int), - ('cy', c_int), - ('flags', UINT) - ] - - -class NCCALCSIZE_PARAMS(Structure): - _fields_ = [ - ('rgrc', RECT*3), - ('lppos', POINTER(PWINDOWPOS)) - ] diff --git a/windoweffect/window_effect.py b/windoweffect/window_effect.py deleted file mode 100644 index aa8537c..0000000 --- a/windoweffect/window_effect.py +++ /dev/null @@ -1,222 +0,0 @@ -# coding:utf-8 -import sys - -from ctypes import POINTER, c_bool, c_int, pointer, sizeof, WinDLL, byref -from ctypes.wintypes import DWORD, LONG, LPCVOID - -from win32 import win32api, win32gui -from win32.lib import win32con - -from .c_structures import ( - ACCENT_POLICY, - ACCENT_STATE, - MARGINS, - DWMNCRENDERINGPOLICY, - DWMWINDOWATTRIBUTE, - WINDOWCOMPOSITIONATTRIB, - WINDOWCOMPOSITIONATTRIBDATA, -) - - -class WindowEffect: - """ A class that calls Windows API to realize window effect """ - - def __init__(self): - # Declare the function signature of the API - self.user32 = WinDLL("user32") - self.dwmapi = WinDLL("dwmapi") - self.SetWindowCompositionAttribute = self.user32.SetWindowCompositionAttribute - self.DwmExtendFrameIntoClientArea = self.dwmapi.DwmExtendFrameIntoClientArea - self.DwmSetWindowAttribute = self.dwmapi.DwmSetWindowAttribute - self.SetWindowCompositionAttribute.restype = c_bool - self.DwmExtendFrameIntoClientArea.restype = LONG - self.DwmSetWindowAttribute.restype = LONG - self.SetWindowCompositionAttribute.argtypes = [ - c_int, - POINTER(WINDOWCOMPOSITIONATTRIBDATA), - ] - self.DwmSetWindowAttribute.argtypes = [c_int, DWORD, LPCVOID, DWORD] - self.DwmExtendFrameIntoClientArea.argtypes = [c_int, POINTER(MARGINS)] - - # Initialize structure - self.accentPolicy = ACCENT_POLICY() - self.winCompAttrData = WINDOWCOMPOSITIONATTRIBDATA() - self.winCompAttrData.Attribute = WINDOWCOMPOSITIONATTRIB.WCA_ACCENT_POLICY.value - self.winCompAttrData.SizeOfData = sizeof(self.accentPolicy) - self.winCompAttrData.Data = pointer(self.accentPolicy) - - def setAcrylicEffect(self, hWnd, gradientColor: str = "F2F2F299", isEnableShadow: bool = True, animationId: int = 0): - """ Add the acrylic effect to the window - - Parameters - ---------- - hWnd: int or `sip.voidptr` - Window handle - - gradientColor: str - Hexadecimal acrylic mixed color, corresponding to four RGBA channels - - isEnableShadow: bool - Enable window shadows - - animationId: int - Turn on matte animation - """ - hWnd = int(hWnd) - # Acrylic mixed color - gradientColor = ( - gradientColor[6:] - + gradientColor[4:6] - + gradientColor[2:4] - + gradientColor[:2] - ) - gradientColor = DWORD(int(gradientColor, base=16)) - # matte animation - animationId = DWORD(animationId) - # window shadow - accentFlags = DWORD(0x20 | 0x40 | 0x80 | - 0x100) if isEnableShadow else DWORD(0) - self.accentPolicy.AccentState = ACCENT_STATE.ACCENT_ENABLE_ACRYLICBLURBEHIND.value - self.accentPolicy.GradientColor = gradientColor - self.accentPolicy.AccentFlags = accentFlags - self.accentPolicy.AnimationId = animationId - # enable acrylic effect - self.SetWindowCompositionAttribute(hWnd, pointer(self.winCompAttrData)) - - def setMicaEffect(self, hWnd): - """ Add the mica effect to the window (Win11 only) - - Parameters - ---------- - hWnd: int or `sip.voidptr` - Window handle - """ - if sys.getwindowsversion().build < 22000: - raise Exception("The mica effect is only available on Win11") - - hWnd = int(hWnd) - margins = MARGINS(-1, -1, -1, -1) - self.DwmExtendFrameIntoClientArea(hWnd, byref(margins)) - self.DwmSetWindowAttribute(hWnd, 1029, byref(c_int(1)), 4) - self.accentPolicy.AccentState = ACCENT_STATE.ACCENT_ENABLE_HOSTBACKDROP.value - self.SetWindowCompositionAttribute(hWnd, pointer(self.winCompAttrData)) - - def setAeroEffect(self, hWnd): - """ Add the aero effect to the window - - Parameters - ---------- - hWnd: int or `sip.voidptr` - Window handle - """ - hWnd = int(hWnd) - self.accentPolicy.AccentState = ACCENT_STATE.ACCENT_ENABLE_BLURBEHIND.value - self.SetWindowCompositionAttribute(hWnd, pointer(self.winCompAttrData)) - - def removeBackgroundEffect(self, hWnd): - """ Remove background effect - - Parameters - ---------- - hWnd: int or `sip.voidptr` - Window handle - """ - hWnd = int(hWnd) - self.accentPolicy.AccentState = ACCENT_STATE.ACCENT_DISABLED.value - self.SetWindowCompositionAttribute(hWnd, pointer(self.winCompAttrData)) - - @staticmethod - def moveWindow(hWnd): - """ Move the window - - Parameters - ---------- - hWnd: int or `sip.voidptr` - Window handle - """ - hWnd = int(hWnd) - win32gui.ReleaseCapture() - win32api.SendMessage( - hWnd, win32con.WM_SYSCOMMAND, win32con.SC_MOVE + win32con.HTCAPTION, 0 - ) - - def addShadowEffect(self, hWnd): - """ Add DWM shadow to window - - Parameters - ---------- - hWnd: int or `sip.voidptr` - Window handle - """ - hWnd = int(hWnd) - margins = MARGINS(-1, -1, -1, -1) - self.DwmExtendFrameIntoClientArea(hWnd, byref(margins)) - - def addMenuShadowEffect(self, hWnd): - """ Add DWM shadow to menu - - Parameters - ---------- - hWnd: int or `sip.voidptr` - Window handle - """ - hWnd = int(hWnd) - self.DwmSetWindowAttribute( - hWnd, - DWMWINDOWATTRIBUTE.DWMWA_NCRENDERING_POLICY.value, - byref(c_int(DWMNCRENDERINGPOLICY.DWMNCRP_ENABLED.value)), - 4, - ) - margins = MARGINS(-1, -1, -1, -1) - self.DwmExtendFrameIntoClientArea(hWnd, byref(margins)) - - def removeShadowEffect(self, hWnd): - """ Remove DWM shadow from the window - - Parameters - ---------- - hWnd: int or `sip.voidptr` - Window handle - """ - hWnd = int(hWnd) - self.DwmSetWindowAttribute( - hWnd, - DWMWINDOWATTRIBUTE.DWMWA_NCRENDERING_POLICY.value, - byref(c_int(DWMNCRENDERINGPOLICY.DWMNCRP_DISABLED.value)), - 4, - ) - - @staticmethod - def removeMenuShadowEffect(hWnd): - """ Remove shadow from pop-up menu - - Parameters - ---------- - hWnd: int or `sip.voidptr` - Window handle - """ - hWnd = int(hWnd) - style = win32gui.GetClassLong(hWnd, win32con.GCL_STYLE) - style &= ~0x00020000 # CS_DROPSHADOW - win32api.SetClassLong(hWnd, win32con.GCL_STYLE, style) - - @staticmethod - def addWindowAnimation(hWnd): - """ Enables the maximize and minimize animation of the window - - Parameters - ---------- - hWnd : int or `sip.voidptr` - Window handle - """ - hWnd = int(hWnd) - style = win32gui.GetWindowLong(hWnd, win32con.GWL_STYLE) - win32gui.SetWindowLong( - hWnd, - win32con.GWL_STYLE, - style - | win32con.WS_MAXIMIZEBOX - | win32con.WS_CAPTION - | win32con.CS_DBLCLKS - | win32con.WS_THICKFRAME, - )