diff --git a/web/hwlab-cloud-web/src/components/common/RefreshBoundary.vue b/web/hwlab-cloud-web/src/components/common/RefreshBoundary.vue new file mode 100644 index 00000000..f6e1a4ef --- /dev/null +++ b/web/hwlab-cloud-web/src/components/common/RefreshBoundary.vue @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + diff --git a/web/hwlab-cloud-web/src/components/mdtodo/MdtodoPageShell.vue b/web/hwlab-cloud-web/src/components/mdtodo/MdtodoPageShell.vue index 8bf46173..73f1a561 100644 --- a/web/hwlab-cloud-web/src/components/mdtodo/MdtodoPageShell.vue +++ b/web/hwlab-cloud-web/src/components/mdtodo/MdtodoPageShell.vue @@ -47,6 +47,14 @@ const workbench = useWorkbenchStore(); const selectedSourceId = ref(null); const selectedFileRef = ref(null); const selectedTaskRef = ref(null); +type RefreshMode = "persistent" | "blocking"; +interface LoadFilesAndTasksOptions { + fileRef?: string | null; + taskId?: string | null; + reportLinkId?: string | null; + updateRoute?: boolean; + refreshMode?: RefreshMode; +} // Tree / pane view state (local, not URL-authoritative) const taskSearch = ref(""); @@ -56,8 +64,10 @@ const taskPaneCollapsed = ref(false); const taskPaneWidth = ref(26); const reportPaneCollapsed = ref(false); const reportPaneWidth = ref(50); +const taskTreeRefreshMode = ref("blocking"); const providerOptionsReady = ref(false); const providerOptionsError = ref(null); +let taskWindowRefreshSeq = 0; // Dialog visibility const showInfo = ref(false); @@ -172,7 +182,7 @@ async function refreshMdtodoProviderOptions(): Promise { watch(selectedSourceId, async (sourceId) => { if (!sourceId || loading.value || selection.applyingRouteSelection.value) return; - await loadFilesAndTasks(sourceId, { updateRoute: true }); + await loadFilesAndTasks(sourceId, { updateRoute: true, refreshMode: "blocking" }); }); watch(selectedTaskRef, async (taskRef) => { @@ -190,6 +200,21 @@ watch(taskData.taskDetail, (detail) => { if (detail?.taskRef === selectedTaskRef.value && !editingBody.value) editBody.value = detail.body || ""; }); +function beginTaskWindowRefresh(mode: RefreshMode): number { + const refreshSeq = ++taskWindowRefreshSeq; + taskTreeRefreshMode.value = mode; + taskData.taskLoading.value = true; + return refreshSeq; +} + +function isCurrentTaskWindowRefresh(refreshSeq: number): boolean { + return refreshSeq === taskWindowRefreshSeq; +} + +function finishTaskWindowRefresh(refreshSeq: number): void { + if (isCurrentTaskWindowRefresh(refreshSeq)) taskData.taskLoading.value = false; +} + async function loadPage(): Promise { loading.value = true; error.value = null; @@ -203,7 +228,7 @@ async function loadPage(): Promise { : selectedSourceId.value && sources.some((s) => s.sourceId === selectedSourceId.value) ? selectedSourceId.value : sources[0]?.sourceId ?? null; - if (selectedSourceId.value) await loadFilesAndTasks(selectedSourceId.value, selection.routeSelectionOptions(true)); + if (selectedSourceId.value) await loadFilesAndTasks(selectedSourceId.value, { ...selection.routeSelectionOptions(true), refreshMode: "blocking" }); else await taskData.loadLinks(null); } catch (err) { setError(err); @@ -212,30 +237,34 @@ async function loadPage(): Promise { } } -async function loadFilesAndTasks(sourceId: string, options: { fileRef?: string | null; taskId?: string | null; reportLinkId?: string | null; updateRoute?: boolean } = {}): Promise { - taskData.taskLoading.value = true; +async function loadFilesAndTasks(sourceId: string, options: LoadFilesAndTasksOptions = {}): Promise { + const refreshMode = options.refreshMode ?? "blocking"; + const refreshSeq = beginTaskWindowRefresh(refreshMode); try { await taskData.loadFiles(sourceId); + if (!isCurrentTaskWindowRefresh(refreshSeq)) return; selectedFileRef.value = options.fileRef && taskData.files.value.some((f) => f.fileRef === options.fileRef) ? options.fileRef : selectedFileRef.value && taskData.files.value.some((f) => f.fileRef === selectedFileRef.value) ? selectedFileRef.value : taskData.files.value[0]?.fileRef ?? null; await taskData.loadTaskWindow(sourceId, selectedFileRef.value, options.taskId ?? selectedTaskRef.value); + if (!isCurrentTaskWindowRefresh(refreshSeq)) return; const preferred = taskData.resolvePreferredTask(options.taskId ?? selectedTaskRef.value); selectedTaskRef.value = preferred?.taskRef ?? taskData.tasks.value[0]?.taskRef ?? null; - collapsedTaskRefs.value = new Set(); + if (refreshMode === "blocking") collapsedTaskRefs.value = new Set(); if (!selectedTaskRef.value) await Promise.all([taskData.loadLinks(null), taskData.loadTaskDetail(null)]); if (options.reportLinkId && selectedTaskRef.value) { await Promise.all([taskData.loadLinks(selectedTaskRef.value), taskData.loadTaskDetail(selectedTaskRef.value)]); + if (!isCurrentTaskWindowRefresh(refreshSeq)) return; await report.openReportPreviewById(selectedTaskRef.value, selectedTaskLinks.value, options.reportLinkId); } else if (options.updateRoute !== false) { void syncRoute({ replace: true }); } } catch (err) { - setError(err); + if (isCurrentTaskWindowRefresh(refreshSeq)) setError(err); } finally { - taskData.taskLoading.value = false; + finishTaskWindowRefresh(refreshSeq); } } @@ -243,9 +272,10 @@ async function selectFile(fileRef: string): Promise { selectedFileRef.value = fileRef || null; report.closeReportPreview(); if (!selectedSourceId.value) return; - taskData.taskLoading.value = true; + const refreshSeq = beginTaskWindowRefresh("blocking"); try { await taskData.loadTaskWindow(selectedSourceId.value, selectedFileRef.value, null); + if (!isCurrentTaskWindowRefresh(refreshSeq)) return; selectedTaskRef.value = taskData.tasks.value[0]?.taskRef ?? null; collapsedTaskRefs.value = new Set(); if (!selectedTaskRef.value) await Promise.all([taskData.loadLinks(null), taskData.loadTaskDetail(null)]); @@ -253,7 +283,7 @@ async function selectFile(fileRef: string): Promise { } catch (err) { setError(err); } finally { - taskData.taskLoading.value = false; + finishTaskWindowRefresh(refreshSeq); } } @@ -279,8 +309,25 @@ async function applyRouteSelection(): Promise { if (!nextSourceId) return; selection.applyingRouteSelection.value = true; try { + const sourceChanged = nextSourceId !== selectedSourceId.value; + const routeFileChanged = Boolean(sel.fileRef && sel.fileRef !== selectedFileRef.value); + const missingTaskWindow = !taskData.files.value.length || !taskData.tasks.value.length; selectedSourceId.value = nextSourceId; - await loadFilesAndTasks(nextSourceId, selection.routeSelectionOptions(false)); + if (sourceChanged || routeFileChanged || missingTaskWindow) { + await loadFilesAndTasks(nextSourceId, { ...selection.routeSelectionOptions(false), refreshMode: "blocking" }); + return; + } + + const preferred = sel.taskId ? taskData.resolvePreferredTask(sel.taskId) : selectedTask.value; + if (sel.taskId && !preferred) { + await loadFilesAndTasks(nextSourceId, { ...selection.routeSelectionOptions(false), refreshMode: taskData.tasks.value.length ? "persistent" : "blocking" }); + return; + } + selectedTaskRef.value = preferred?.taskRef ?? selectedTaskRef.value ?? taskData.tasks.value[0]?.taskRef ?? null; + if (sel.linkId && selectedTaskRef.value) { + await Promise.all([taskData.loadLinks(selectedTaskRef.value), taskData.loadTaskDetail(selectedTaskRef.value)]); + await report.openReportPreviewById(selectedTaskRef.value, selectedTaskLinks.value, sel.linkId); + } } finally { selection.applyingRouteSelection.value = false; } @@ -302,8 +349,14 @@ function taskFingerprint(): string | undefined { async function reloadAfterMutation(): Promise { const sourceId = selectedSourceId.value; if (!sourceId) return; - await taskData.loadFiles(sourceId); - await taskData.loadTaskWindow(sourceId, selectedFileRef.value, selectedTaskRef.value); + const refreshSeq = beginTaskWindowRefresh("persistent"); + try { + await taskData.loadFiles(sourceId); + if (!isCurrentTaskWindowRefresh(refreshSeq)) return; + await taskData.loadTaskWindow(sourceId, selectedFileRef.value, selectedTaskRef.value); + } finally { + finishTaskWindowRefresh(refreshSeq); + } } async function saveTaskBasics(): Promise { @@ -414,7 +467,14 @@ async function reindexSource(): Promise { const sourceId = selectedSourceId.value; if (!sourceId) return; const result = await source.reindexSource(sourceId); - if (result) await loadFilesAndTasks(sourceId); + if (result) { + await loadFilesAndTasks(sourceId, { + fileRef: selectedFileRef.value, + taskId: selectedTaskRef.value, + updateRoute: true, + refreshMode: taskData.tasks.value.length ? "persistent" : "blocking" + }); + } } function setError(err: unknown): void { @@ -444,7 +504,7 @@ function setError(err: unknown): void { :selected-source-id="selectedSourceId" :files="taskData.files.value" :selected-file-ref="selectedFileRef" - :file-loading="taskData.taskLoading.value" + :file-loading="taskData.taskLoading.value && taskTreeRefreshMode === 'blocking'" :reindex-loading="source.sourceReindexLoading.value" @update:selected-source-id="selectedSourceId = $event" @update:selected-file-ref="selectFile($event)" @@ -484,6 +544,7 @@ function setError(err: unknown): void { :collapsed-task-refs="collapsedTaskRefs" :collapsed="taskPaneCollapsed" :loading="taskData.taskLoading.value" + :refresh-mode="taskTreeRefreshMode" :search="taskSearch" :status-filter="taskStatusFilter" @update:search="taskSearch = $event" diff --git a/web/hwlab-cloud-web/src/components/mdtodo/MdtodoTaskPanel.vue b/web/hwlab-cloud-web/src/components/mdtodo/MdtodoTaskPanel.vue index 4a01c22b..6aa3c834 100644 --- a/web/hwlab-cloud-web/src/components/mdtodo/MdtodoTaskPanel.vue +++ b/web/hwlab-cloud-web/src/components/mdtodo/MdtodoTaskPanel.vue @@ -7,9 +7,9 @@ import type { MdtodoTaskDetailRecord, MdtodoTaskLinkRecord, MdtodoTaskRecord, Pr import type { ProviderProfile } from "@/types"; import type { ApiError, ErrorDiagnostic } from "@/types"; import type { ProviderProfileOption } from "@/stores/workbench-session"; -import LoadingState from "@/components/common/LoadingState.vue"; import EmptyState from "@/components/common/EmptyState.vue"; import ApiErrorDiagnostic from "@/components/common/ApiErrorDiagnostic.vue"; +import RefreshBoundary from "@/components/common/RefreshBoundary.vue"; const props = defineProps<{ task: MdtodoTaskRecord | null; @@ -112,11 +112,8 @@ function normalizeTaskText(value?: string | null): string { {{ launchBlocker }} - - - - {{ detailError }} - + + {{ detailError }} @@ -131,7 +128,7 @@ function normalizeTaskText(value?: string | null): string { 暂无补充正文 - +
{{ launchBlocker }}
{{ detailError }}
暂无补充正文