TaskBoard.vue
3.92 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
<!--
任务看板 —— goal/task 两级模型的独立页面(39 号文档 §二 P2)。
在此之前 goal/task 只能在 GraphConsole 左栏的窄侧栏里看,这里把同一份组件
(GoalTaskPane + goal-task-model.js)铺满整页,数据层原样不动,不重写。
点一条 task 不在本页画执行图(那是 GraphConsole 的活),而是跳过去 ——
`/:id/graph-console?goal=...&task=...` 是 GraphConsole 已经支持的选中态复原路径
(见该文件 onMounted 里 `route.query.goal` / `route.query.task` 那两行),不新开一条协议。
-->
<template>
<div class="board">
<header class="top">
<div class="who">
<router-link :to="`/${companyId}/overview`" class="back">←</router-link>
<b>{{ company?.name || companyId }}</b>
<span class="sep">任务看板</span>
</div>
<router-link :to="`/${companyId}/graph-console`" class="to-console">🕸️ 执行控制台</router-link>
</header>
<div class="body">
<GoalTaskPane
:company-id="companyId"
:goals="goals"
:loading="goalsLoading"
:selected-task-id="selectedTaskId"
:selected-goal-id="selectedGoalId"
:slots="slots"
@select="goToDetail"
@refresh="loadGoals"
/>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import GoalTaskPane from '@/components/graph-console/GoalTaskPane.vue';
const route = useRoute();
const router = useRouter();
const props = defineProps({ id: { type: String, default: '' } });
const companyId = computed(() => props.id || route.params.id);
const company = ref(null);
const goals = ref([]);
const goalsLoading = ref(false);
const slots = ref(null);
const selectedGoalId = ref('');
const selectedTaskId = ref('');
let treePoll = null;
onMounted(async () => {
selectedGoalId.value = String(route.query.goal || '');
selectedTaskId.value = String(route.query.task || '');
await Promise.all([loadCompany(), loadGoals()]);
// 与 GraphConsole 同款兜底:没有公司级 SSE 通道,靠轮询看到别处发生的状态变化
treePoll = setInterval(() => { loadGoals(); }, 20000);
});
onBeforeUnmount(() => clearInterval(treePoll));
async function loadCompany() {
try {
const r = await fetch(`/api/company/${companyId.value}`);
if (r.ok) company.value = await r.json();
} catch { /* 页头名字掉了不影响功能 */ }
}
async function loadGoals() {
goalsLoading.value = true;
try {
const r = await fetch(`/api/company/${companyId.value}/goals?withTasks=1`);
if (!r.ok) return;
const j = await r.json();
goals.value = j.goals || [];
slots.value = j.slots || null;
} catch { /* 轮询失败保留上一份树 */ } finally {
goalsLoading.value = false;
}
}
/** 点一条 task:跳到执行控制台看它的图,选中态通过 query 带过去 */
function goToDetail({ goalId, taskId }) {
selectedGoalId.value = goalId || '';
selectedTaskId.value = taskId || '';
if (!taskId) return;
router.push({ path: `/${companyId.value}/graph-console`, query: { goal: goalId || '', task: taskId } });
}
</script>
<style scoped>
.board { display: flex; flex-direction: column; height: 100vh; background: #fff; color: #1a1a1a; }
.top {
display: flex; align-items: center; justify-content: space-between;
padding: 10px 16px; border-bottom: 1px solid #eee; flex: 0 0 auto;
}
.who { display: flex; align-items: center; gap: 8px; font-size: 13px; }
.back { text-decoration: none; color: #666; font-size: 16px; }
.sep { color: #999; }
.to-console { text-decoration: none; color: #2b6cb0; font-size: 12px; font-weight: 600; }
.to-console:hover { text-decoration: underline; }
.body { flex: 1 1 auto; overflow-y: auto; padding: 16px 24px; max-width: 900px; width: 100%; margin: 0 auto; box-sizing: border-box; }
@media (max-width: 700px) {
.body { padding: 12px; }
}
</style>