diff --git a/web/hwlab-cloud-web/src/utils/tasktree-route-loader.test.ts b/web/hwlab-cloud-web/src/utils/tasktree-route-loader.test.ts new file mode 100644 index 00000000..ea347536 --- /dev/null +++ b/web/hwlab-cloud-web/src/utils/tasktree-route-loader.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it, vi } from "vitest"; + +import { loadTaskTreeRoute } from "./tasktree-route-loader"; + +describe("loadTaskTreeRoute", () => { + it("loads only the global overview for the TaskTree root route", async () => { + const overview = vi.fn().mockResolvedValue({ ok: true }); + const timeline = vi.fn(); + + const result = await loadTaskTreeRoute({ overview, timeline }, ""); + + expect(result.view).toBe("global"); + expect(overview).toHaveBeenCalledOnce(); + expect(timeline).not.toHaveBeenCalled(); + }); + + it("loads a group timeline without depending on the global overview", async () => { + const overview = vi.fn(); + const timeline = vi.fn().mockResolvedValue({ ok: true }); + + const result = await loadTaskTreeRoute({ overview, timeline }, "tg_example"); + + expect(result.view).toBe("group"); + expect(timeline).toHaveBeenCalledOnce(); + expect(timeline).toHaveBeenCalledWith("tg_example"); + expect(overview).not.toHaveBeenCalled(); + }); +}); diff --git a/web/hwlab-cloud-web/src/utils/tasktree-route-loader.ts b/web/hwlab-cloud-web/src/utils/tasktree-route-loader.ts new file mode 100644 index 00000000..57cebe88 --- /dev/null +++ b/web/hwlab-cloud-web/src/utils/tasktree-route-loader.ts @@ -0,0 +1,10 @@ +import type { tasktreeAPI } from "@/api/tasktree"; + +type TaskTreeReadApi = Pick; + +export async function loadTaskTreeRoute(api: TaskTreeReadApi, groupId: string) { + if (groupId) { + return { view: "group" as const, response: await api.timeline(groupId) }; + } + return { view: "global" as const, response: await api.overview() }; +} diff --git a/web/hwlab-cloud-web/src/views/projects/TaskTreeView.vue b/web/hwlab-cloud-web/src/views/projects/TaskTreeView.vue index 1959f63c..e10a8ed0 100644 --- a/web/hwlab-cloud-web/src/views/projects/TaskTreeView.vue +++ b/web/hwlab-cloud-web/src/views/projects/TaskTreeView.vue @@ -9,6 +9,7 @@ import TaskTreeDetailPanel from "@/components/tasktree/TaskTreeDetailPanel.vue"; import MessageMarkdown from "@/components/workbench/MessageMarkdown.vue"; import { displayTimeConfig } from "@/config/runtime"; import { decideNestedWheelBoundary, type NestedWheelBoundaryState } from "@/utils/nested-wheel-boundary"; +import { loadTaskTreeRoute } from "@/utils/tasktree-route-loader"; import { deriveEffectiveTaskTimeRanges } from "@/utils/tasktree-time-range"; const route = useRoute(); @@ -108,32 +109,37 @@ watch(requestedDetailMode, (mode) => { detailMode.value = mode; }); async function loadActiveView() { loading.value = true; error.value = null; - const overviewResponse = await tasktreeAPI.overview(); - if (!overviewResponse.ok || overviewResponse.data?.ok === false) { - loading.value = false; - error.value = overviewResponse.data?.error?.message || overviewResponse.error || "TaskTree 全局视图加载失败"; - return; - } - overview.value = overviewResponse.data?.data?.groups ?? []; - groups.value = overview.value.map((item) => item.group); - if (!groupId.value) { + const activeGroupId = groupId.value; + const result = await loadTaskTreeRoute(tasktreeAPI, activeGroupId); + if (result.view === "global") { + const overviewResponse = result.response; + if (!overviewResponse.ok || overviewResponse.data?.ok === false) { + loading.value = false; + error.value = overviewResponse.data?.error?.message || overviewResponse.error || "TaskTree 全局视图加载失败"; + return; + } + overview.value = overviewResponse.data?.data?.groups ?? []; + groups.value = overview.value.map((item) => item.group); timeline.value = null; loading.value = false; await nextTick(); positionTimelineAtNow("global"); return; } - const timelineResponse = await tasktreeAPI.timeline(groupId.value); + const timelineResponse = result.response; loading.value = false; if (!timelineResponse.ok || timelineResponse.data?.ok === false || !timelineResponse.data?.data) { error.value = timelineResponse.data?.error?.message || timelineResponse.error || "TaskTree timeline 加载失败"; return; } timeline.value = timelineResponse.data.data; + if (!groups.value.some((group) => group.id === timeline.value?.group.id)) { + groups.value = [...groups.value, timeline.value.group]; + } initializedChildGroups.clear(); syncSelectedTaskFromRoute(); await nextTick(); - positionTimelineAtNow(groupId.value); + positionTimelineAtNow(activeGroupId); } function selectGroup(event: Event) { const value = (event.target as HTMLSelectElement).value;