2026-03-02 14:29:58 +08:00
|
|
|
|
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
|
|
|
|
|
|
|
2026-04-17 15:31:41 +08:00
|
|
|
|
|
2026-03-02 14:29:58 +08:00
|
|
|
|
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)
|
2026-04-17 15:31:41 +08:00
|
|
|
|
# 没有有效进度值时不绘制
|
|
|
|
|
|
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"))
|
|
|
|
|
|
# 绘制边框
|
2026-03-02 14:29:58 +08:00
|
|
|
|
pen = painter.pen()
|
2026-04-17 15:31:41 +08:00
|
|
|
|
pen.setColor(QColor("#D0D5DD"))
|
|
|
|
|
|
pen.setWidth(1)
|
2026-03-02 14:29:58 +08:00
|
|
|
|
painter.setPen(pen)
|
2026-04-17 15:31:41 +08:00
|
|
|
|
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)
|
2026-03-02 14:29:58 +08:00
|
|
|
|
progress_rect.setWidth(progress_width)
|
2026-04-17 15:31:41 +08:00
|
|
|
|
# 进度颜色:完成绿色,进行中蓝色
|
|
|
|
|
|
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()
|
2026-03-02 14:29:58 +08:00
|
|
|
|
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)
|
2026-04-16 14:23:37 +08:00
|
|
|
|
taskActuatorManager.updateDetails.connect(self._onUpdateDetails)
|
2026-03-02 14:29:58 +08:00
|
|
|
|
delegate = ProgressBarDelegate()
|
2026-04-17 15:31:41 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
""")
|
2026-03-02 14:29:58 +08:00
|
|
|
|
self.ui.treeView.expandAll()
|
2026-04-17 15:31:41 +08:00
|
|
|
|
# 设置列表最小高度(由 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 = {}
|
2026-03-02 14:29:58 +08:00
|
|
|
|
|
|
|
|
|
|
def clearTaskData(self, taskId):
|
2026-04-17 15:31:41 +08:00
|
|
|
|
taskId = str(taskId)
|
2026-03-02 14:29:58 +08:00
|
|
|
|
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):
|
2026-04-17 15:31:41 +08:00
|
|
|
|
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)
|
2026-03-02 14:29:58 +08:00
|
|
|
|
self.model.removeRow(row)
|
2026-04-17 15:31:41 +08:00
|
|
|
|
# 强制刷新UI
|
|
|
|
|
|
self.ui.treeView.viewport().update()
|
2026-03-02 14:29:58 +08:00
|
|
|
|
break
|
|
|
|
|
|
|
2026-04-17 15:31:41 +08:00
|
|
|
|
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)
|
|
|
|
|
|
|
2026-03-02 14:29:58 +08:00
|
|
|
|
def appendTask(self, taskId):
|
2026-04-17 15:31:41 +08:00
|
|
|
|
taskId = str(taskId)
|
2026-03-02 14:29:58 +08:00
|
|
|
|
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):
|
2026-04-17 15:31:41 +08:00
|
|
|
|
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)
|
2026-03-02 14:29:58 +08:00
|
|
|
|
self.model.removeRow(row)
|
|
|
|
|
|
break
|
2026-04-17 15:31:41 +08:00
|
|
|
|
task = taskManager.getInfo(taskId)
|
2026-03-02 14:29:58 +08:00
|
|
|
|
if task:
|
|
|
|
|
|
name = task["name"]
|
|
|
|
|
|
item = QStandardItem(str(name))
|
|
|
|
|
|
progressItem = QxStandardItem()
|
2026-04-17 15:31:41 +08:00
|
|
|
|
progressItem.setData({'task_instruction_id': task["id"], 'name': name}, Qt.ItemDataRole.UserRole)
|
|
|
|
|
|
# 注册到索引
|
|
|
|
|
|
self.progressItemMap[taskId] = progressItem
|
|
|
|
|
|
self.appendChildTask(item, taskId, taskId)
|
2026-03-02 14:29:58 +08:00
|
|
|
|
self.rootItem.appendRow([item, progressItem])
|
|
|
|
|
|
self.ui.treeView.expandAll()
|
2026-04-16 14:23:37 +08:00
|
|
|
|
|
2026-04-17 15:31:41 +08:00
|
|
|
|
def appendChildTask(self, parent, taskId, parentId, depth=0):
|
|
|
|
|
|
MAX_DEPTH = 20 # 防止循环引用导致无限递归
|
|
|
|
|
|
if depth >= MAX_DEPTH:
|
|
|
|
|
|
log.warning(f"任务嵌套深度超过 {MAX_DEPTH},停止递归: taskId={taskId}")
|
|
|
|
|
|
return
|
2026-03-02 14:29:58 +08:00
|
|
|
|
taskInstructions = taskInstructionManager.getInfo(taskId)
|
|
|
|
|
|
if taskInstructions:
|
|
|
|
|
|
for taskInstruction in taskInstructions:
|
|
|
|
|
|
target_type = taskInstruction["target_type"]
|
|
|
|
|
|
task_instruction_id = str(taskInstruction["id"])
|
2026-04-17 15:31:41 +08:00
|
|
|
|
composite_id = parentId + task_instruction_id
|
2026-03-02 14:29:58 +08:00
|
|
|
|
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()
|
2026-04-17 15:31:41 +08:00
|
|
|
|
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)
|
2026-03-02 14:29:58 +08:00
|
|
|
|
parent.appendRow([childItem, childProgressItem])
|
2026-04-16 14:23:37 +08:00
|
|
|
|
|
2026-03-02 14:29:58 +08:00
|
|
|
|
elif target_type == "instruction":
|
|
|
|
|
|
instruction = instructionManager.getInfo(str(taskInstruction["target_id"]))
|
|
|
|
|
|
if instruction:
|
|
|
|
|
|
name = instruction["name"]
|
|
|
|
|
|
childItem = QStandardItem(str(name))
|
|
|
|
|
|
childProgressItem = QxStandardItem()
|
2026-04-17 15:31:41 +08:00
|
|
|
|
childProgressItem.setData({'task_instruction_id': composite_id, 'name': name}, Qt.ItemDataRole.UserRole)
|
|
|
|
|
|
# 注册到索引
|
|
|
|
|
|
self.progressItemMap[composite_id] = childProgressItem
|
2026-03-02 14:29:58 +08:00
|
|
|
|
parent.appendRow([childItem, childProgressItem])
|
|
|
|
|
|
|
2026-04-16 14:23:37 +08:00
|
|
|
|
def _onUpdateDetails(self, taskId, value, maxValue):
|
2026-04-17 15:31:41 +08:00
|
|
|
|
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
|
|
|
|
|
|
# 索引未命中时回退到递归遍历(兜底)
|
2026-04-16 14:23:37 +08:00
|
|
|
|
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')):
|
2026-04-17 15:31:41 +08:00
|
|
|
|
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
|
2026-04-16 14:23:37 +08:00
|
|
|
|
return
|
|
|
|
|
|
childItem = parentItem.child(row, 0)
|
|
|
|
|
|
if childItem is not None and childItem.hasChildren():
|
|
|
|
|
|
self._updateTreeProgress(childItem, taskId, value, maxValue)
|
|
|
|
|
|
|