You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

547 lines
21 KiB
Python

4 months ago
import sys
import os
from PyQt5.QtCore import Qt, QTimer, QAbstractTableModel, QModelIndex, QPoint, QSize, pyqtSignal, QFile,QTextStream
from PyQt5.QtGui import QBrush, QColor, QFont, QStandardItemModel, QStandardItem
from PyQt5.QtWidgets import (QApplication, QMainWindow, QTableView,
QPushButton, QVBoxLayout, QWidget, QHBoxLayout,
QLabel, QCheckBox, QSpinBox, QMenu, QFileDialog,
QTabWidget, QTabBar, QListWidget, QListWidgetItem, QDialog,
QLineEdit, QFormLayout, QDialogButtonBox, QMessageBox,
QHeaderView, QToolBar, QAction, QStatusBar, QComboBox, QSplitter, QAbstractItemView)
from docx import Document
from docx.shared import Pt, RGBColor
from docx.enum.text import WD_PARAGRAPH_ALIGNMENT
import qtawesome as qta
from datetime import datetime
# 导入其他模块
from model.ProcedureModel.ProcedureProcessor import ExcelParser, StepTableModel
from utils.DBModels.ProcedureModel import DatabaseManager
4 months ago
from UI.ProcedureManager.HistoryViewer import HistoryViewerWidget
from UI.ProcedureManager.StepExecutor import StepExecutor # 修改导入路径
4 months ago
class ExecutionDetailDialog(QDialog):
"""步骤详情对话框"""
def __init__(self, step_info, parent=None):
super().__init__(parent)
self.setWindowTitle("步骤详情")
self.setMinimumWidth(600)
layout = QVBoxLayout()
form_layout = QFormLayout()
form_layout.addRow("步骤ID", QLabel(step_info['step_id']))
form_layout.addRow("步骤描述", QLabel(step_info['step_description']))
form_layout.addRow("执行时间", QLabel(step_info['execution_time']))
form_layout.addRow("执行结果", QLabel("成功" if step_info['result'] else "失败"))
layout.addLayout(form_layout)
button_box = QDialogButtonBox(QDialogButtonBox.Ok)
button_box.accepted.connect(self.accept)
layout.addWidget(button_box)
self.setLayout(layout)
class AddCategoryDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("添加新分类")
self.setFixedSize(300, 150)
layout = QVBoxLayout()
form_layout = QFormLayout()
self.nameEdit = QLineEdit()
self.nameEdit.setPlaceholderText("输入分类名称")
form_layout.addRow("分类名称:", self.nameEdit)
layout.addLayout(form_layout)
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
button_box.accepted.connect(self.accept)
button_box.rejected.connect(self.reject)
layout.addWidget(button_box)
self.setLayout(layout)
def getCategoryName(self):
return self.nameEdit.text().strip()
class ProcedureManager(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("规程管理系统")
self.setGeometry(100, 100, 1200, 800)
# 创建工具栏
self.toolbar = QToolBar("主工具栏")
self.toolbar.setIconSize(QSize(32, 32))
# 新增:在工具栏级别统一设置文字显示位置
self.toolbar.setToolButtonStyle(Qt.ToolButtonTextUnderIcon)
self.addToolBar(self.toolbar)
self.createActions()
# 添加状态栏
self.statusBar = QStatusBar()
self.setStatusBar(self.statusBar)
def initUI(self):
# 加载样式表
self.loadStylesheet()
# 创建主控件 - 已经是QTabWidget
self.tabs = QTabWidget()
self.setCentralWidget(self.tabs)
# 启用标签页关闭按钮
self.tabs.setTabsClosable(True)
self.tabs.tabCloseRequested.connect(self.closeTab) # 新增关闭标签页信号连接
# 添加初始的规程管理标签页必须先创建UI组件
self.initProcedureManagementTab() # 先创建UI组件
# 初始化其他组件在UI创建后调用
self.loadCategories() # 现在categoryList已存在
# 新增:确保加载第一个分类的规程
if self.categoryList.count() > 0:
self.categorySelected(self.categoryList.item(0), None)
# 初始化标签锁定状态
self.activeExecutorIndex = -1
def initDB(self):
# 初始化数据库
self.db = DatabaseManager()
# 新增:关闭标签页的处理方法
def closeTab(self, index):
# 规程管理标签页(索引0)不能关闭
if index == 0:
return
widget = self.tabs.widget(index)
# 检查是否有正在运行的执行器
if isinstance(widget, StepExecutor) and widget.isRunning:
QMessageBox.warning(
self,
"操作被阻止",
"当前有规程正在执行中,请先停止执行后再关闭标签页。"
)
return
# 如果是当前激活的执行器标签页,解锁所有标签页
if index == self.activeExecutorIndex:
self.lockTabs(False, index)
# 移除标签页
self.tabs.removeTab(index)
widget.deleteLater()
# 修改:锁定/解锁标签页方法(增加关闭按钮控制)
def lockTabs(self, lock, currentIndex):
"""锁定或解锁标签页"""
self.activeExecutorIndex = currentIndex if lock else -1
# 遍历所有标签页
for index in range(self.tabs.count()):
# 当前执行器标签页保持可用,其他标签页根据锁定状态设置
enable = not lock or index == currentIndex
self.tabs.setTabEnabled(index, enable)
# 特殊处理:规程管理标签页(索引0)始终可用
if index == 0:
self.tabs.setTabEnabled(0, True)
# 设置关闭按钮状态:执行期间禁用关闭按钮
if index > 0: # 非规程管理标签页
self.tabs.tabBar().setTabButton(
index,
QTabBar.RightSide, # 关闭按钮位置
None if lock and index != currentIndex else self.tabs.tabBar().tabButton(index, QTabBar.RightSide)
)
# 修改:打开规程执行界面(显示规程全名)
def openProcedureInExecutor(self, item=None):
# 如果通过工具栏调用item为None使用当前选中项
currentItem = item or self.procedureList.currentItem()
if not currentItem:
return
# 检查是否有正在运行的执行器
if self.hasRunningExecutor():
QMessageBox.warning(
self,
"操作被阻止",
"当前有规程正在执行中,请先停止执行后再打开其他规程。"
)
return
procId = currentItem.data(Qt.UserRole) # 修改变量名
4 months ago
# print(procId, 22222222222222)
4 months ago
procedureData = self.db.getProcedureContent(procId) # 修改变量名
if not procedureData:
QMessageBox.warning(self, "打开失败", "无法获取规程内容")
return
# 创建新的执行界面传入规程ID和数据库管理器
executor = StepExecutor(procedureData, procId, self.db)
# 获取规程名称和编号
procName = procedureData["规程信息"]["规程名称"]
procNumber = procedureData["规程信息"]["规程编号"]
# 添加为新标签页,显示规程全名(名称+编号)
tab_index = self.tabs.addTab(executor, f"{procName} ({procNumber})")
# 记录当前执行器的标签索引
executor.tabLockRequired.connect(lambda lock: self.lockTabs(lock, tab_index))
# 切换到新添加的标签页
self.tabs.setCurrentWidget(executor)
def initProcedureManagementTab(self):
"""创建规程管理主标签页"""
mainWidget = QWidget()
mainLayout = QHBoxLayout()
self.categoryList = QListWidget()
self.categoryList.setFixedWidth(200)
self.categoryList.currentItemChanged.connect(self.categorySelected)
# 设置分类列表接受拖放
self.categoryList.setAcceptDrops(True)
self.categoryList.setDragDropMode(QAbstractItemView.DropOnly)
# 新增:设置分类列表右键菜单
self.categoryList.setContextMenuPolicy(Qt.CustomContextMenu)
self.categoryList.customContextMenuRequested.connect(self.showCategoryContextMenu) # 新增
self.procedureList = QListWidget()
# 设置规程列表支持拖拽
self.procedureList.setDragEnabled(True)
self.procedureList.setDragDropMode(QAbstractItemView.DragOnly)
self.procedureList.itemDoubleClicked.connect(
lambda item: self.openProcedureInExecutor(item)
)
# 新增:设置规程列表右键菜单
self.procedureList.setContextMenuPolicy(Qt.CustomContextMenu)
self.procedureList.customContextMenuRequested.connect(self.showProcedureContextMenu) # 新增
# 新增:拖拽事件处理
self.procedureList.dragEnterEvent = self.procedureDragEnterEvent
self.categoryList.dropEvent = self.categoryDropEvent
leftPanel = QWidget()
leftLayout = QVBoxLayout()
leftLayout.addWidget(QLabel("规程分类"))
leftLayout.addWidget(self.categoryList)
leftPanel.setLayout(leftLayout)
leftPanel.setFixedWidth(220)
rightPanel = QWidget()
rightLayout = QVBoxLayout()
rightLayout.addWidget(QLabel("规程列表"))
rightLayout.addWidget(self.procedureList)
rightPanel.setLayout(rightLayout)
mainLayout.addWidget(leftPanel)
mainLayout.addWidget(rightPanel)
mainWidget.setLayout(mainLayout)
self.tabs.addTab(mainWidget, "规程管理")
def createActions(self):
self.importAction = QAction(
qta.icon('fa5s.file-import', color='green'),
"导入规程",
self
)
self.importAction.setIconText("导入规程")
self.importAction.setShortcut("Ctrl+I")
self.importAction.setStatusTip("导入Excel规程文件")
self.importAction.triggered.connect(self.importProcedure)
self.toolbar.addAction(self.importAction) # 修改变量名
self.addCategoryAction = QAction(
qta.icon('fa5s.folder-plus', color='blue'),
"添加分类",
self
)
self.addCategoryAction.setIconText("添加分类")
self.addCategoryAction.setStatusTip("添加新的分类")
self.addCategoryAction.triggered.connect(self.addCategory)
self.toolbar.addAction(self.addCategoryAction) # 修改变量名
self.deleteCategoryAction = QAction(
qta.icon('fa5s.folder-minus', color='red'),
"删除分类",
self
)
self.deleteCategoryAction.setIconText("删除分类")
self.deleteCategoryAction.setStatusTip("删除当前分类")
self.deleteCategoryAction.triggered.connect(self.deleteCurrentCategory)
self.toolbar.addAction(self.deleteCategoryAction) # 修改变量名
self.deleteProcedureAction = QAction(
qta.icon('fa5s.trash', color='red'),
"删除规程",
self
)
self.deleteProcedureAction.setIconText("删除规程")
self.deleteProcedureAction.setStatusTip("删除选中的规程")
self.deleteProcedureAction.triggered.connect(self.deleteSelectedProcedure)
self.toolbar.addAction(self.deleteProcedureAction) # 修改变量名
self.openProcedureAction = QAction(
qta.icon('fa5s.folder-open', color='orange'),
"打开规程",
self
)
self.openProcedureAction.setIconText("打开规程")
self.openProcedureAction.setStatusTip("在步骤执行工具中打开选中的规程")
self.openProcedureAction.triggered.connect(self.openProcedureInExecutor)
self.toolbar.addAction(self.openProcedureAction) # 修改变量名
# 添加历史记录查看器动作
self.historyAction = QAction(
qta.icon('fa5s.history', color='purple'),
"历史记录",
self
)
self.historyAction.setIconText("历史记录")
self.historyAction.setStatusTip("查看历史执行记录")
self.historyAction.triggered.connect(self.openHistoryViewer)
self.toolbar.addAction(self.historyAction)
def loadCategories(self):
self.categoryList.clear()
categories = self.db.getCategories()
for catId, catName in categories:
item = QListWidgetItem(catName)
item.setData(Qt.UserRole, catId)
self.categoryList.addItem(item)
if self.categoryList.count() > 0:
self.categoryList.setCurrentRow(0)
# 新增:手动触发分类选择信号
self.categoryList.itemSelectionChanged.emit()
def loadProcedures(self, categoryId=None):
self.procedureList.clear()
procedures = self.db.getProcedures(categoryId)
for procId, name, number, type, createdAt in procedures:
item = QListWidgetItem(f"{name} ({number})")
item.setData(Qt.UserRole, procId)
item.setToolTip(f"类型: {type}\n创建时间: {createdAt}")
self.procedureList.addItem(item)
def categorySelected(self, currentItem, previousItem):
if currentItem:
categoryId = currentItem.data(Qt.UserRole)
self.loadProcedures(categoryId)
def importProcedure(self):
filePath, _ = QFileDialog.getOpenFileName(
self,
"选择规程文件",
"",
"Excel文件 (*.xlsx *.xls)"
)
if not filePath:
return
try:
parsedData = ExcelParser.parseProcedure(filePath)
currentItem = self.categoryList.currentItem()
categoryId = currentItem.data(Qt.UserRole) if currentItem else None
procInfo = parsedData["规程信息"]
# 添加空字符串作为report_path参数
procId = self.db.addProcedure(
categoryId,
procInfo["规程名称"],
procInfo["规程编号"],
procInfo["规程类型"],
parsedData,
"" # 显式传递空字符串作为report_path
)
if procId:
self.statusBar.showMessage(f"成功导入规程: {procInfo['规程名称']}", 5000)
self.loadProcedures(categoryId)
else:
QMessageBox.warning(self, "导入失败", "无法导入规程,请检查数据库连接")
except Exception as e:
QMessageBox.critical(self, "导入错误", f"导入规程时发生错误:\n{str(e)}")
def addCategory(self):
dialog = AddCategoryDialog(self)
if dialog.exec_() == QDialog.Accepted:
categoryName = dialog.getCategoryName()
if categoryName:
if self.db.addCategory(categoryName):
self.loadCategories()
else:
QMessageBox.warning(self, "添加失败", "分类名称已存在,请使用其他名称")
def deleteCurrentCategory(self):
currentItem = self.categoryList.currentItem()
if not currentItem:
return
categoryId = currentItem.data(Qt.UserRole)
categoryName = currentItem.text()
# 检查是否为默认分类
if categoryName == "默认分类":
QMessageBox.warning(
self,
"操作禁止",
"默认分类不可删除!"
)
return
reply = QMessageBox.question(
self,
"确认删除",
f"确定要删除分类 '{categoryName}' 吗?\n该分类下的规程将移动到默认分类。",
QMessageBox.Yes | QMessageBox.No
)
if reply == QMessageBox.Yes:
if self.db.deleteCategory(categoryId):
self.loadCategories()
self.statusBar.showMessage(f"已删除分类: {categoryName}", 5000)
def deleteSelectedProcedure(self):
currentItem = self.procedureList.currentItem()
if not currentItem:
return
procId = currentItem.data(Qt.UserRole)
procName = currentItem.text()
reply = QMessageBox.question(
self,
"确认删除",
f"确定要删除规程 '{procName}' 吗?",
QMessageBox.Yes | QMessageBox.No
)
if reply == QMessageBox.Yes:
if self.db.deleteProcedure(procId):
currentCategoryItem = self.categoryList.currentItem()
categoryId = currentCategoryItem.data(Qt.UserRole) if currentCategoryItem else None
self.loadProcedures(categoryId)
self.statusBar.showMessage(f"已删除规程: {procName}", 5000)
def hasRunningExecutor(self):
"""检查当前是否有正在运行的执行器 - 修正实现"""
for index in range(self.tabs.count()):
widget = self.tabs.widget(index)
# 修正: 只检查isRunning状态移除对isActive的检查
if isinstance(widget, StepExecutor) and widget.isRunning:
return True
return False
# 新增:打开历史记录查看器
def openHistoryViewer(self):
4 months ago
historyViewer = HistoryViewerWidget(self.db, self)
4 months ago
historyViewer.exec_()
def loadStylesheet(self):
qssPath = "Static/Procedure.qss"
try:
qssFile = QFile(qssPath) # 修改变量名
if qssFile.exists():
qssFile.open(QFile.ReadOnly | QFile.Text)
stream = QTextStream(qssFile)
stream.setCodec("UTF-8")
self.setStyleSheet(stream.readAll())
qssFile.close()
print(f"成功加载样式表: {qssPath}")
else:
print(f"警告:样式表文件不存在: {qssPath}")
except Exception as e:
print(f"加载样式表失败: {str(e)}")
def closeEvent(self, event):
self.db.close()
event.accept()
def procedureDragEnterEvent(self, event):
"""处理规程列表的拖拽进入事件"""
if event.mimeData().hasText():
event.acceptProposedAction()
def categoryDropEvent(self, event):
"""处理分类列表的拖放事件:将拖拽过来的规程移动到当前分类"""
# 获取拖拽源
source = event.source()
# 确保拖拽源是规程列表
if source != self.procedureList:
event.ignore()
return
# 获取选中的规程项(拖拽项)
items = self.procedureList.selectedItems()
if not items:
event.ignore()
return
# 获取目标分类项(鼠标释放位置对应的分类项)
pos = event.pos()
targetItem = self.categoryList.itemAt(pos)
if not targetItem:
event.ignore()
return
# 获取目标分类ID
targetCategoryId = targetItem.data(Qt.UserRole)
# 获取被拖拽的规程ID只处理第一个选中的项
procItem = items[0]
procId = procItem.data(Qt.UserRole)
procName = procItem.text()
# 更新数据库
if self.db.updateProcedureCategory(procId, targetCategoryId):
# 更新成功,重新加载目标分类的规程列表(因为规程已经移动到新分类)
self.loadProcedures(targetCategoryId)
# 新增:设置当前选中的分类为目标分类
self.categoryList.setCurrentItem(targetItem)
# 显示状态信息
self.statusBar.showMessage(f"已将规程 '{procName}' 移动到分类 '{targetItem.text()}'", 5000)
else:
QMessageBox.warning(self, "移动失败", "移动规程失败,请检查数据库连接")
event.accept()
# 新增:分类列表右键菜单
def showCategoryContextMenu(self, pos):
"""显示分类列表的右键菜单"""
item = self.categoryList.itemAt(pos)
if item:
menu = QMenu()
deleteAction = menu.addAction("删除分类")
deleteAction.triggered.connect(self.deleteCurrentCategory)
menu.exec_(self.categoryList.mapToGlobal(pos))
# 新增:规程列表右键菜单
def showProcedureContextMenu(self, pos):
"""显示规程列表的右键菜单"""
item = self.procedureList.itemAt(pos)
if item:
menu = QMenu()
deleteAction = menu.addAction("删除规程")
deleteAction.triggered.connect(self.deleteSelectedProcedure)
menu.exec_(self.procedureList.mapToGlobal(pos))