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)