TaskBoard.vue
4.61 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
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
<!--
任务看板 —— goal/task 两级模型的独立页面(39 号文档 §二 P2)。
表格视图(TaskTable + TaskDetailModal),数据层沿用原来那份(GET /goals?withTasks=1,
20s 轮询),只是不再走 GoalTaskPane 那套折叠列表 —— GoalTaskPane 还在 GraphConsole
左栏服役,两边分开维护,改这里不会动到那边。
-->
<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">
<TaskTable
:company-id="companyId"
:goals="goals"
:agents="agents"
:loading="goalsLoading"
@select="openDetail"
@create="openCreate"
/>
</div>
<TaskDetailModal
v-if="showModal"
:company-id="companyId"
:task="modalTask"
:goals="goals"
:agents="agents"
@close="closeModal"
@saved="onSaved"
/>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
import { useRoute } from 'vue-router';
import TaskTable from '@/components/task-board/TaskTable.vue';
import TaskDetailModal from '@/components/task-board/TaskDetailModal.vue';
const route = useRoute();
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 agents = ref([]);
const selectedTaskId = ref('');
const showCreate = ref(false);
const showModal = computed(() => showCreate.value || !!selectedTaskId.value);
/** 从当前 goals 树里现查,而不是缓存点击那一刻的快照——刷新之后弹窗跟着显示最新状态 */
const modalTask = computed(() => {
if (!selectedTaskId.value) return null;
for (const g of goals.value) {
const t = (g.tasks || []).find((x) => x.taskId === selectedTaskId.value);
if (t) return t;
}
return null;
});
let treePoll = null;
onMounted(async () => {
await Promise.all([loadCompany(), loadGoals(), loadAgents()]);
// 与 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 || [];
} catch { /* 轮询失败保留上一份树 */ } finally {
goalsLoading.value = false;
}
}
/** agent 花名册不常变,拉一次即可,不用跟 goals 一起轮询 */
async function loadAgents() {
try {
const r = await fetch(`/api/company/${companyId.value}/agents`);
if (r.ok) agents.value = await r.json();
} catch { /* 拉不到就退回显示 assignee 原始 id,不影响其余功能 */ }
}
function openDetail(t) {
showCreate.value = false;
selectedTaskId.value = t.taskId;
}
function openCreate() {
selectedTaskId.value = '';
showCreate.value = true;
}
function closeModal() {
showCreate.value = false;
selectedTaskId.value = '';
}
/** 弹窗里任一增删改查动作成功后:重新拉 goals;新建 / 归档导致弹窗没有对象可显示时关掉它 */
async function onSaved() {
await loadGoals();
if (showCreate.value) { closeModal(); return; }
if (selectedTaskId.value && !modalTask.value) closeModal();
}
</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; box-sizing: border-box; }
@media (max-width: 700px) {
.body { padding: 12px; }
}
</style>