血缘锚定法: task_type字段+迁移+三大接口写入+TreeCanvas基于task_type布局

This commit is contained in:
2026-08-06 13:03:25 +08:00
parent e5e2a395e7
commit 0d323e8181
5 changed files with 78 additions and 40 deletions

View File

@ -59,55 +59,60 @@ export default {
const CARD_W = 320, CARD_H = 460, GAP_X = 80, GAP_Y = 120;
const nodes = [];
const rowMaxX = {};
let currentMaxYIdx = -1;
// 按行组织:每行是一个 { y, tasks: [...] }
const rows = [];
const flatMap = {};
const flatten = (tasks) => {
if (!tasks) return;
tasks.forEach(t => { flatMap[t.id] = t; flatten(t.child_tasks); });
};
flatten(this.product.task_tree);
const layout = (tasks, parentRowIdx, parentX) => {
if (!tasks || !tasks.length) return;
tasks.forEach(t => {
// 工业拓扑父WIP→并行(同Y)父COMPLETED→串行(换行)
// 后端保证 COMPLETED 后不可 spawn故 COMPLETED 的子任务必为转交产物
const parent = this.taskDataMap[t.parent_task_id];
const isSerial = !t.parent_task_id || (parent && (parent.status === 'COMPLETED' || parent.status === 'CANCELED'));
const rowIdx = isSerial ? (parentRowIdx >= 0 ? parentRowIdx + 1 : rows.length) : parentRowIdx;
while (rows.length <= rowIdx) rows.push({ y: 0, tasks: [] });
const layoutNode = (t) => {
if (t._visited) return;
t._visited = true;
const row = rows[rowIdx];
const x = row.tasks.length === 0 ? 40 : row.tasks[row.tasks.length - 1]._xEnd + GAP_X;
const node = { ...t, x, y: 0, _rowIdx: rowIdx, _xEnd: x + CARD_W };
row.tasks.push(node);
nodes.push(node);
});
if (!t.parent_task_id) {
// 规则A根节点 → 新行 X=0
currentMaxYIdx++;
t._yIdx = currentMaxYIdx;
t._xIdx = 0;
} else {
const parent = flatMap[t.parent_task_id];
if (parent && !parent._visited) layoutNode(parent);
// 递归子任务
tasks.forEach(t => {
if (t.child_tasks && t.child_tasks.length) {
const parentNode = nodes.find(n => n.id === t.id);
const parentRow = parentNode ? parentNode._rowIdx : rows.length - 1;
layout(t.child_tasks, parentRow, parentNode ? parentNode.x : 40);
const type = t.task_type || (parent && parent.status === 'CANCELED' ? 'RECOVERY' : 'SPAWN');
if (type === 'SPAWN') {
t._yIdx = parent._yIdx;
t._xIdx = (rowMaxX[t._yIdx] !== undefined ? rowMaxX[t._yIdx] : 0) + 1;
} else if (type === 'TRANSFER' || type === 'MAIN') {
currentMaxYIdx++;
t._yIdx = currentMaxYIdx;
t._xIdx = 0;
} else if (type === 'RECOVERY') {
currentMaxYIdx++;
t._yIdx = currentMaxYIdx;
t._xIdx = parent._xIdx;
}
});
}
rowMaxX[t._yIdx] = Math.max(rowMaxX[t._yIdx] || 0, t._xIdx);
t.x = 40 + t._xIdx * (CARD_W + GAP_X);
t.y = 20 + t._yIdx * (CARD_H + GAP_Y);
nodes.push(t);
if (t.child_tasks && t.child_tasks.length) {
t.child_tasks
.sort((a, b) => (a.task_type === 'TRANSFER' ? -1 : 1))
.forEach(child => layoutNode(child));
}
};
layout(this.product.task_tree, -1, 40);
// 计算 Y 坐标
let curY = 20;
rows.forEach(row => {
row.y = curY;
row.tasks.forEach(n => { n.y = curY; });
curY += CARD_H + GAP_Y;
});
this.product.task_tree.forEach(root => layoutNode(root));
return nodes;
},
taskDataMap() {
const map = {};
const walk = (tasks) => { if (!tasks) return; tasks.forEach(t => { map[t.id] = t; walk(t.child_tasks); }); };
if (this.product) walk(this.product.task_tree);
return map;
},
branchLabelMap() {
const map = {};
if (!this.product || !this.product.task_tree) return map;