血缘锚定法: task_type字段+迁移+三大接口写入+TreeCanvas基于task_type布局
This commit is contained in:
@ -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;
|
||||
|
||||
Reference in New Issue
Block a user