261 lines
12 KiB
Python
261 lines
12 KiB
Python
import json
|
||
import re
|
||
from PyQt6 import *
|
||
from PyQt6.QtCore import *
|
||
from PyQt6.QtGui import *
|
||
from PyQt6.QtWidgets import *
|
||
from ui.Ui_taskListForm import Ui_taskListForm
|
||
from taskModel.taskManager import taskManager
|
||
from instructionModel.instructionManager import instructionManager
|
||
from taskGroupModel.taskGroupManager import taskGroupManager
|
||
from taskInstructionModel.taskInstructionManager import taskInstructionManager
|
||
from taskModel.taskActuatorManager import taskActuatorManager
|
||
from logs import log
|
||
from common import NoWheelComboBox
|
||
from common import TaskProgressBar
|
||
from common import common
|
||
|
||
|
||
class ProgressBarDelegate(QStyledItemDelegate):
|
||
def paint(self, painter, option, index):
|
||
if index.column() == 1: # 仅对第二列添加进度条
|
||
data = index.data(Qt.ItemDataRole.UserRole) # 获取进度条数据
|
||
if data is not None and isinstance(data, dict):
|
||
value = data.get('value', -9999)
|
||
maximum = data.get('maximum', -9999)
|
||
# 没有有效进度值时不绘制
|
||
if value < 0 or maximum < 1:
|
||
return
|
||
rect = option.rect
|
||
# 缩小一圈(与任务列表一致)
|
||
rect.adjust(4, 4, -4, -4)
|
||
# 绘制进度条背景(与列表交替色一致)
|
||
if index.row() % 2 == 0:
|
||
painter.fillRect(rect, QColor("#ffffff"))
|
||
else:
|
||
painter.fillRect(rect, QColor("#F8F9FA"))
|
||
# 绘制边框
|
||
pen = painter.pen()
|
||
pen.setColor(QColor("#D0D5DD"))
|
||
pen.setWidth(1)
|
||
painter.setPen(pen)
|
||
painter.drawRect(rect.adjusted(0, 0, -1, -1))
|
||
# 计算进度条宽度
|
||
inner_rect = QRect(rect).adjusted(1, 1, -1, -1)
|
||
progress_width = int((value / maximum) * inner_rect.width())
|
||
progress_rect = QRect(inner_rect)
|
||
progress_rect.setWidth(progress_width)
|
||
# 进度颜色:完成绿色,进行中蓝色
|
||
if value >= maximum:
|
||
bar_color = QColor("#4CAF50")
|
||
else:
|
||
bar_color = QColor("#007EFF")
|
||
painter.fillRect(progress_rect, bar_color)
|
||
# 文本(不加粗,与任务列表字体一致)
|
||
text = f"{value}/{maximum}"
|
||
pen = painter.pen()
|
||
text_rect = QRect(rect)
|
||
text_rect.adjust(6, 0, -6, 0)
|
||
# 黑色画一次(进度条外)
|
||
pen.setColor(Qt.GlobalColor.black)
|
||
painter.setPen(pen)
|
||
painter.drawText(text_rect, Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter, text)
|
||
# 裁剪到进度条内,白色覆盖
|
||
painter.save()
|
||
painter.setClipRect(progress_rect)
|
||
pen.setColor(Qt.GlobalColor.white)
|
||
painter.setPen(pen)
|
||
painter.drawText(text_rect, Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter, text)
|
||
painter.restore()
|
||
else:
|
||
super().paint(painter, option, index)
|
||
class QxStandardItem(QStandardItem):
|
||
def __init__(self, text=''):
|
||
super().__init__(text)
|
||
def updateProgress(self, taskId, value, maxValue):
|
||
obj = self.data(Qt.ItemDataRole.UserRole)
|
||
if obj is not None and isinstance(obj, dict):
|
||
if str(taskId) == str(obj.get('task_instruction_id')):
|
||
self.setData({'value': value, 'maximum': maxValue, 'task_instruction_id': obj.get('task_instruction_id'), 'name':obj.get('name')}, Qt.ItemDataRole.UserRole)
|
||
|
||
class TaskListForm(QWidget):
|
||
|
||
def __init__(self, parent=None):
|
||
super().__init__(parent)
|
||
self.ui = Ui_taskListForm()
|
||
self.ui.setupUi(self)
|
||
self.model = QStandardItemModel()
|
||
self.model.setHorizontalHeaderLabels(["任务","进度"])
|
||
self.rootItem = self.model.invisibleRootItem()
|
||
self.ui.treeView.setModel(self.model)
|
||
self.ui.treeView.setColumnWidth(0, 400)
|
||
taskActuatorManager.taskStart.connect(self.appendTask)
|
||
taskActuatorManager.taskStop.connect(self.clearTaskData)
|
||
taskActuatorManager.updateDetails.connect(self._onUpdateDetails)
|
||
delegate = ProgressBarDelegate()
|
||
self.ui.treeView.setItemDelegateForColumn(1, delegate)
|
||
self.ui.treeView.setAlternatingRowColors(True)
|
||
self.ui.treeView.setStyleSheet("""
|
||
QTreeView {
|
||
background-color: #ffffff;
|
||
alternate-background-color: #F8F9FA;
|
||
selection-color: #262626;
|
||
}
|
||
QTreeView::item {
|
||
height: 32px;
|
||
}
|
||
QTreeView::item:selected {
|
||
background-color: #D9D9D9;
|
||
}
|
||
QHeaderView::section {
|
||
background-color: #F7F7F7;
|
||
color: #8C8C8C;
|
||
border: none;
|
||
height: 32px;
|
||
padding-left: 4px;
|
||
}
|
||
""")
|
||
self.ui.treeView.expandAll()
|
||
# 设置列表最小高度(由 mainWindow 动态调整实际高度)
|
||
self.ui.treeView.setMinimumHeight(400)
|
||
# 进度缓存:key=task_instruction_id, value={'value': x, 'maximum': y}
|
||
self.progressCache = {}
|
||
# progressItem 索引:key=task_instruction_id, value=QxStandardItem 引用
|
||
self.progressItemMap = {}
|
||
|
||
def clearTaskData(self, taskId):
|
||
taskId = str(taskId)
|
||
for row in range(self.model.rowCount()):
|
||
index = self.model.index(row, 1)
|
||
data = index.data(Qt.ItemDataRole.UserRole)
|
||
if data is not None and isinstance(data, dict):
|
||
if str(data.get('task_instruction_id')) == taskId:
|
||
# 递归清理该任务及其子任务的缓存和索引
|
||
self._cleanupItemCache(self.model.item(row, 0), taskId)
|
||
# 清理自身
|
||
self.progressCache.pop(taskId, None)
|
||
self.progressItemMap.pop(taskId, None)
|
||
self.model.removeRow(row)
|
||
# 强制刷新UI
|
||
self.ui.treeView.viewport().update()
|
||
break
|
||
|
||
def _cleanupItemCache(self, parentItem, parentId):
|
||
"""递归清理子任务的缓存和索引"""
|
||
if parentItem is None:
|
||
return
|
||
for row in range(parentItem.rowCount()):
|
||
childProgress = parentItem.child(row, 1)
|
||
if childProgress is not None:
|
||
obj = childProgress.data(Qt.ItemDataRole.UserRole)
|
||
if obj and isinstance(obj, dict):
|
||
child_id = str(obj.get('task_instruction_id', ''))
|
||
self.progressCache.pop(child_id, None)
|
||
self.progressItemMap.pop(child_id, None)
|
||
childItem = parentItem.child(row, 0)
|
||
if childItem is not None and childItem.hasChildren():
|
||
self._cleanupItemCache(childItem, parentId)
|
||
|
||
def appendTask(self, taskId):
|
||
taskId = str(taskId)
|
||
for row in range(self.model.rowCount()):
|
||
index = self.model.index(row, 1)
|
||
data = index.data(Qt.ItemDataRole.UserRole)
|
||
if data is not None and isinstance(data, dict):
|
||
if str(data.get('task_instruction_id')) == taskId:
|
||
log.debug(f"TaskId {taskId} already exists, removing existing item...")
|
||
self._cleanupItemCache(self.model.item(row, 0), taskId)
|
||
self.progressCache.pop(taskId, None)
|
||
self.progressItemMap.pop(taskId, None)
|
||
self.model.removeRow(row)
|
||
break
|
||
task = taskManager.getInfo(taskId)
|
||
if task:
|
||
name = task["name"]
|
||
item = QStandardItem(str(name))
|
||
progressItem = QxStandardItem()
|
||
progressItem.setData({'task_instruction_id': task["id"], 'name': name}, Qt.ItemDataRole.UserRole)
|
||
# 注册到索引
|
||
self.progressItemMap[taskId] = progressItem
|
||
self.appendChildTask(item, taskId, taskId)
|
||
self.rootItem.appendRow([item, progressItem])
|
||
self.ui.treeView.expandAll()
|
||
|
||
def appendChildTask(self, parent, taskId, parentId, depth=0):
|
||
MAX_DEPTH = 20 # 防止循环引用导致无限递归
|
||
if depth >= MAX_DEPTH:
|
||
log.warning(f"任务嵌套深度超过 {MAX_DEPTH},停止递归: taskId={taskId}")
|
||
return
|
||
taskInstructions = taskInstructionManager.getInfo(taskId)
|
||
if taskInstructions:
|
||
for taskInstruction in taskInstructions:
|
||
target_type = taskInstruction["target_type"]
|
||
task_instruction_id = str(taskInstruction["id"])
|
||
composite_id = parentId + task_instruction_id
|
||
level = taskInstruction["level"]
|
||
if target_type == "task":
|
||
task = taskManager.getInfo(str(taskInstruction["target_id"]))
|
||
if task:
|
||
name = task["name"]
|
||
childItem = QStandardItem(str(name))
|
||
childProgressItem = QxStandardItem()
|
||
childProgressItem.setData({'task_instruction_id': composite_id, 'name': name}, Qt.ItemDataRole.UserRole)
|
||
# 注册到索引
|
||
self.progressItemMap[composite_id] = childProgressItem
|
||
self.appendChildTask(childItem, str(taskInstruction["target_id"]), composite_id, depth + 1)
|
||
parent.appendRow([childItem, childProgressItem])
|
||
|
||
elif target_type == "instruction":
|
||
instruction = instructionManager.getInfo(str(taskInstruction["target_id"]))
|
||
if instruction:
|
||
name = instruction["name"]
|
||
childItem = QStandardItem(str(name))
|
||
childProgressItem = QxStandardItem()
|
||
childProgressItem.setData({'task_instruction_id': composite_id, 'name': name}, Qt.ItemDataRole.UserRole)
|
||
# 注册到索引
|
||
self.progressItemMap[composite_id] = childProgressItem
|
||
parent.appendRow([childItem, childProgressItem])
|
||
|
||
def _onUpdateDetails(self, taskId, value, maxValue):
|
||
taskId = str(taskId)
|
||
# 更新缓存
|
||
self.progressCache[taskId] = {'value': value, 'maximum': maxValue}
|
||
# 完成时清理缓存
|
||
if value >= maxValue and maxValue > 0:
|
||
self.progressCache.pop(taskId, None)
|
||
# 优先从索引查找(O(1))
|
||
progressItem = self.progressItemMap.get(taskId)
|
||
if progressItem is not None:
|
||
obj = progressItem.data(Qt.ItemDataRole.UserRole)
|
||
if obj is not None and isinstance(obj, dict):
|
||
progressItem.setData({
|
||
'value': value,
|
||
'maximum': maxValue,
|
||
'task_instruction_id': obj.get('task_instruction_id'),
|
||
'name': obj.get('name')
|
||
}, Qt.ItemDataRole.UserRole)
|
||
return
|
||
# 索引未命中时回退到递归遍历(兜底)
|
||
self._updateTreeProgress(self.model.invisibleRootItem(), taskId, value, maxValue)
|
||
|
||
def _updateTreeProgress(self, parentItem, taskId, value, maxValue):
|
||
for row in range(parentItem.rowCount()):
|
||
progressItem = parentItem.child(row, 1)
|
||
if progressItem is not None:
|
||
obj = progressItem.data(Qt.ItemDataRole.UserRole)
|
||
if obj is not None and isinstance(obj, dict):
|
||
if str(taskId) == str(obj.get('task_instruction_id')):
|
||
progressItem.setData({
|
||
'value': value,
|
||
'maximum': maxValue,
|
||
'task_instruction_id': obj.get('task_instruction_id'),
|
||
'name': obj.get('name')
|
||
}, Qt.ItemDataRole.UserRole)
|
||
# 补注册到索引,下次就能 O(1) 命中
|
||
self.progressItemMap[str(taskId)] = progressItem
|
||
return
|
||
childItem = parentItem.child(row, 0)
|
||
if childItem is not None and childItem.hasChildren():
|
||
self._updateTreeProgress(childItem, taskId, value, maxValue)
|
||
|