0710更新

main
zcwBit 3 months ago
parent 1eb5e3f2d3
commit 1ff43e4347

@ -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;
}

@ -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;
}

@ -166,3 +166,5 @@ def qCleanupResources():
QtCore.qUnregisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data)
qInitResources()

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

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

@ -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)}")

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

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

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

@ -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:

@ -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()]
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

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

@ -1,2 +0,0 @@
from .window_effect import WindowEffect
from .c_structures import *

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

@ -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,
)
Loading…
Cancel
Save