diff --git a/track-uniapp/src/pages/scan/components/TreeCanvas.vue b/track-uniapp/src/pages/scan/components/TreeCanvas.vue index c0cff2a..c48dd3c 100644 --- a/track-uniapp/src/pages/scan/components/TreeCanvas.vue +++ b/track-uniapp/src/pages/scan/components/TreeCanvas.vue @@ -60,79 +60,68 @@ export default { treeNodes() { if (!this.product || !this.product.task_tree) return []; - // 1. 拍平所有任务(无视后端的错误嵌套) - const allTasks = []; - const flatten = (tasks) => { - if (!tasks) return; - for (const t of tasks) { allTasks.push(t); flatten(t.child_tasks); } - }; - flatten(this.product.task_tree); - - // 2. 绝对时间轴排序(强制自上而下流转) - allTasks.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime()); - - // 3. 智能分层(判定谁是同行裂变,谁是上下游) - const levels = []; - let currentLevel = []; - let lastTime = null; - - allTasks.forEach(t => { - const tTime = new Date(t.created_at).getTime(); - if (lastTime === null) { - currentLevel.push(t); lastTime = tTime; - } else { - if (Math.abs(tTime - lastTime) < 2000 || - (t.parent_task_id && currentLevel.some(c => c.parent_task_id === t.parent_task_id))) { - currentLevel.push(t); - } else { - levels.push(currentLevel); - currentLevel = [t]; lastTime = tTime; - } - } - }); - if (currentLevel.length > 0) levels.push(currentLevel); - - // 4. 计算 X, Y 坐标 const CARD_W = 320, CARD_H = 460, GAP_X = 80, GAP_Y = 120; const nodes = []; nodes._lines = []; - let maxCards = Math.max(...levels.map(l => l.length)); - const CENTER_X = (maxCards * CARD_W + (maxCards - 1) * GAP_X) / 2 + 60; + // 按行组织:每行是一个 { y, tasks: [...] } + const rows = []; - levels.forEach((lvlTasks, depth) => { - const y = 40 + depth * (CARD_H + GAP_Y); - const lvlWidth = lvlTasks.length * CARD_W + (lvlTasks.length - 1) * GAP_X; - const startX = CENTER_X - lvlWidth / 2; - lvlTasks.forEach((t, i) => { - nodes.push({ ...t, x: startX + i * (CARD_W + GAP_X), y, _depth: depth }); + const layout = (tasks, parentRowIdx, parentX) => { + if (!tasks || !tasks.length) return; + tasks.forEach(t => { + // 判定:parent COMPLETED → 串行 Y+1;parent WIP/PENDING → 并行同Y偏右 + const isSerial = !t.parent_task_id || (this.taskStatusMap[t.parent_task_id] === 'COMPLETED'); + const rowIdx = isSerial ? (parentRowIdx >= 0 ? parentRowIdx + 1 : rows.length) : parentRowIdx; + while (rows.length <= rowIdx) rows.push({ y: 0, tasks: [] }); + + 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) { + const parent = nodes.find(n => n.id === t.parent_task_id); + if (parent) { + nodes._lines.push({ + key: parent.id + '-' + t.id, + x1: parent.x + CARD_W / 2, y1: parent.y + CARD_H, + x2: node.x + CARD_W / 2, y2: node.y, + }); + } + } }); + + // 递归子任务 + 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); + } + }); + }; + + 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; }); - // 5. 智能连线生成 - for (let d = 1; d < levels.length; d++) { - const curLvl = levels[d]; - const prevLvl = levels[d - 1]; - curLvl.forEach(curr => { - const currNode = nodes.find(n => n.id === curr.id); - let parents = []; - if (curr.parent_task_id) { - parents = nodes.filter(n => n.id === curr.parent_task_id); - } - if (parents.length === 0) { - parents = prevLvl.map(p => nodes.find(n => n.id === p.id)); - } - parents.forEach(p => { - nodes._lines.push({ - key: p.id + '-' + curr.id, - x1: p.x + CARD_W / 2, y1: p.y + CARD_H, - x2: currNode.x + CARD_W / 2, y2: currNode.y - }); - }); - }); - } return nodes; }, + taskStatusMap() { + const map = {}; + const walk = (tasks) => { if (!tasks) return; tasks.forEach(t => { map[t.id] = t.status; walk(t.child_tasks); }); }; + if (this.product) walk(this.product.task_tree); + return map; + }, treeLines() { return this.treeNodes._lines || []; }, canvasWidth() { if (!this.treeNodes.length) return 400; return Math.max(800, Math.max(...this.treeNodes.map(n => n.x)) + 300); }, canvasHeight() { if (!this.treeNodes.length) return 400; return Math.max(800, Math.max(...this.treeNodes.map(n => n.y)) + 300); } diff --git a/track-uniapp/src/pages/scan/components/WorkspaceArea.vue b/track-uniapp/src/pages/scan/components/WorkspaceArea.vue index da90fac..57c7c28 100644 --- a/track-uniapp/src/pages/scan/components/WorkspaceArea.vue +++ b/track-uniapp/src/pages/scan/components/WorkspaceArea.vue @@ -23,7 +23,9 @@ + {{ branchLabelsMap[t.id] || '' }} {{ t.task_name }} + → {{ t.assignee_id || '未分配' }} {{ getTaskRemark(t) }} @@ -44,6 +46,7 @@ + {{ branchLabelsMap[lockedTask.id] || '' }} {{ statusLabel(lockedTask.status) }} ⚠返工 { if (!tasks) return; for (const t of tasks) { - const isMine = t.status === 'WIP' || t.status === 'PENDING'; - if (isMine && (t.assignee_id == this.currentUserId || t.assignee_id == this.currentUsername)) result.push(t); + if (t.status === 'WIP' || t.status === 'PENDING') result.push(t); walk(t.child_tasks); } }; @@ -126,6 +128,21 @@ export default { return result; }, lockedTask() { return this.focusTasks.find(t => t.id === this.lockedTaskId) || null; }, + branchLabelsMap() { + const map = {}; + const walk = (tasks, prefix, idx) => { + if (!tasks) return; + tasks.forEach((t, i) => { + const label = prefix === '' ? '主分支' : `${prefix}${i + 1}`; + map[t.id] = label; + if (t.child_tasks && t.child_tasks.length) { + walk(t.child_tasks, prefix === '' ? '' : `${label}.`, i); + } + }); + }; + if (this.product) walk(this.product.task_tree, '', 0); + return map; + }, }, methods: { statusLabel(s) { return STATUS_MAP[s] || s; }, @@ -150,7 +167,9 @@ export default { .task-list-item.s-yellow { border-left-color: #f59e0b; } .task-list-item.s-blue { border-left-color: #3b82f6; } .tli-left { flex: 1; min-width: 0; } +.tag-branch { font-size: 10px; padding: 2px 8px; border-radius: 6px; background: #ede9fe; color: #7c3aed; font-weight: 700; display: inline-block; width: max-content; margin-bottom: 4px; } .tli-name { font-size: 15px; font-weight: 700; color: #1f2937; display: block; } +.tli-assignee { font-size: 11px; color: #9ca3af; display: block; margin-top: 2px; } .tli-remark { font-size: 12px; color: #a16207; font-weight: bold; margin-top: 4px; display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .tli-right { display: flex; flex-direction: column; align-items: flex-end; gap: 4px; flex-shrink: 0; margin-left: 8px; } .tli-badge { font-size: 11px; padding: 2px 10px; border-radius: 12px; font-weight: 600; }