From 864a03d2e11b78eefa998a60b92469e1a46b9a01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BB=9D=E5=B0=98?= <237809796@qq.com> Date: Tue, 29 Sep 2026 14:58:35 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D:=20=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E5=8F=8D=E9=A6=88=E7=9C=9F=E5=AE=9E=E6=80=A7=E4=B8=8E=E7=AB=9E?= =?UTF-8?q?=E6=80=81=E5=AE=88=E5=8D=AB=E5=85=AB=E5=A4=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 知识创建失败不再误关弹窗弹成功提示(补返回值检查) - AuditLog 翻页补请求序号守卫防慢响应覆盖新页 - TaskDrawer 切任务慢响应校验 taskId 才写入 - GitChanges 竞态序号+moduleId 双校验,三处静默失败补错误提示 - 回收站恢复/彻底删除操作中禁用防双击重复提交 - 灵感状态切换失败补错误反馈(原 unhandled rejection) - FileExplorer 三处失败仅 console 补 Message 提示 - Dashboard 消费 store.error 故障不再伪装空数据 --- src/components/project/FileExplorer.vue | 4 ++ src/components/project/GitChanges.vue | 50 ++++++++++++++++++++----- src/components/task/TaskDrawer.vue | 9 +++-- src/views/AuditLog.vue | 11 +++++- src/views/Dashboard.vue | 9 +++-- src/views/Ideas.vue | 7 +++- src/views/Knowledge.vue | 3 +- src/views/Projects.vue | 23 ++++++++++-- 8 files changed, 92 insertions(+), 24 deletions(-) diff --git a/src/components/project/FileExplorer.vue b/src/components/project/FileExplorer.vue index bf4e9a6..71064d2 100644 --- a/src/components/project/FileExplorer.vue +++ b/src/components/project/FileExplorer.vue @@ -211,6 +211,7 @@ */ import { ref, computed, watch, reactive, onMounted, onUnmounted, onActivated, onDeactivated } from 'vue' import { useI18n } from 'vue-i18n' +import { Message } from '@arco-design/web-vue' import { listen, type UnlistenFn } from '@tauri-apps/api/event' import { moduleApi, type ProjectModuleRecord, type FileTreeEntry, type GitStatusResult } from '@/api/module' import FileTree from './FileTree.vue' @@ -461,6 +462,7 @@ async function onScanSubmodules() { console.log('[FileExplorer] 扫描到新工程:', n) } } catch (e) { + Message.error(t('common.unknownError')) console.error('[FileExplorer] 扫描子仓库失败:', e) } } @@ -526,6 +528,7 @@ async function onSaveModule() { showModuleModal.value = false await loadModules() } catch (e) { + Message.error(t('common.unknownError')) console.error('[FileExplorer] 保存工程失败:', e) } finally { savingModule.value = false @@ -542,6 +545,7 @@ async function onRemoveModule() { currentModuleId.value = '' await loadModules() } catch (e) { + Message.error(t('common.unknownError')) console.error('[FileExplorer] 删除工程失败:', e) } finally { removingModule.value = false diff --git a/src/components/project/GitChanges.vue b/src/components/project/GitChanges.vue index f08a256..ba88b18 100644 --- a/src/components/project/GitChanges.vue +++ b/src/components/project/GitChanges.vue @@ -34,10 +34,16 @@
{{ $t('gitChanges.branchSwitchHint') }}
+ +
⚠ {{ actionError }}
+
+ +
⚠ {{ loadError }}
+
{{ $t('gitChanges.notGitRepo') }} @@ -180,6 +186,12 @@ const showHistory = ref(false) const selectedFile = ref('') const diffContent = ref('') const diffLoading = ref(false) +// 行内错误提示:loadError 主区展示(loadStatus 失败),actionError 分支条下方展示(loadMore/selectFile 失败) +const loadError = ref('') +const actionError = ref('') +// 请求序号守卫:moduleId 切换与 refreshKey 刷新双触发源并发时,旧响应晚到不覆盖新模块数据 +// (对齐 ProjectDetail taskReqSeq 模式) +let statusReqSeq = 0 /** 单条提交记录形状(对齐 getModuleCommits / GitRecentCommit)。 */ interface CommitItem { @@ -346,13 +358,20 @@ function pathEqualsAnySide(parsed: string, targetPath: string): boolean { } async function loadStatus() { - if (!props.moduleId) return + const mid = props.moduleId + if (!mid) return + const seq = ++statusReqSeq loading.value = true + loadError.value = '' + actionError.value = '' try { - gitStatus.value = await moduleApi.getModuleGitStatus(props.moduleId) + const res = await moduleApi.getModuleGitStatus(mid) + // 序号 + moduleId 双校验:响应回来时仍是最新请求且模块未切换才写入 + if (seq !== statusReqSeq || mid !== props.moduleId) return + gitStatus.value = res // 复用 getModuleGitStatus 内部已返回的 recent_commits(避免再发一次 getModuleCommits // 造成首次 git log -50 重复请求 + recent_commits 被丢弃)。 - const recent = gitStatus.value?.recent_commits ?? [] + const recent = res?.recent_commits ?? [] commits.value = recent.map((c) => ({ hash: c.hash, subject: c.subject, @@ -360,7 +379,7 @@ async function loadStatus() { author: c.author, })) // 全量提交数读后端 total_commits(git rev-list --count HEAD),非 commits.length(后者受分页限制)。 - const backendTotal = gitStatus.value?.total_commits ?? 0 + const backendTotal = res?.total_commits ?? 0 totalCommits.value = backendTotal commitSkip.value = commits.value.length // has_more:后端无 total 时(commits 仍可能未全载)用页大小推断;有 total 时按已载 < total 判定。 @@ -368,11 +387,13 @@ async function loadStatus() { backendTotal > 0 ? commits.value.length < backendTotal : commits.value.length >= COMMIT_PAGE_SIZE - } catch { + } catch (e) { + if (seq !== statusReqSeq || mid !== props.moduleId) return // 旧请求的失败不污染当前视图 gitStatus.value = null commits.value = [] + loadError.value = String(e) } finally { - loading.value = false + if (seq === statusReqSeq) loading.value = false } } @@ -407,6 +428,7 @@ function closeBranchPicker() { async function loadMoreCommits() { if (loadingMore.value || !hasMoreCommits.value) return loadingMore.value = true + actionError.value = '' try { const res = await moduleApi.getModuleCommits(props.moduleId, commitSkip.value, COMMIT_PAGE_SIZE) // 按哈希去重(防止分页边界与 recent_commits 重叠) @@ -418,8 +440,8 @@ async function loadMoreCommits() { commitSkip.value += res.commits.length // 全量提交数读后端 total(git rev-list --count HEAD),非 commits.length(后者受分页限制)。 totalCommits.value = res.total - } catch { - // 静默失败 + } catch (e) { + actionError.value = String(e) } finally { loadingMore.value = false } @@ -435,12 +457,14 @@ async function selectFile(path: string) { // 先切预览到该文件(内容视图),diff 拉到后回填,点文件即展示变化 emit('select-file', path) diffContent.value = '' + actionError.value = '' diffLoading.value = true try { const res = await moduleApi.getModuleFileDiff(props.moduleId, path) diffContent.value = res.diff || '' - } catch { + } catch (e) { diffContent.value = '' + actionError.value = String(e) } finally { diffLoading.value = false } @@ -668,7 +692,7 @@ watch(() => props.refreshKey, () => { .gc-st-modified { color: var(--df-git-modified); } .gc-st-added { color: var(--df-git-added); } .gc-st-deleted { color: var(--df-git-deleted); } -.gc-st-untracked { color: #999; } +.gc-st-untracked { color: var(--df-git-untracked); } .gc-file-path { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -768,6 +792,12 @@ watch(() => props.refreshKey, () => { padding: 40px 0; color: var(--df-text-dim); font-size: 13px; } .gc-empty-inline { padding: 24px 0; } +.gc-error { color: var(--df-danger); } +.gc-error-strip { + padding: 6px 14px; font-size: 12px; color: var(--df-danger); + background: var(--df-danger-bg); border-bottom: 0.5px solid var(--df-border); + flex-shrink: 0; word-break: break-all; +} .spinner { width: 16px; height: 16px; border: 2px solid var(--df-border); border-top-color: var(--df-accent); diff --git a/src/components/task/TaskDrawer.vue b/src/components/task/TaskDrawer.vue index e749200..3a36055 100644 --- a/src/components/task/TaskDrawer.vue +++ b/src/components/task/TaskDrawer.vue @@ -244,24 +244,27 @@ const wfSnap = computed(() => { // 加载 async function load() { - if (!props.taskId) return + const tid = props.taskId // 捕获发起时 taskId,响应回来校验未变才写入(防慢响应覆盖新任务) + if (!tid) return loading.value = true errorMsg.value = '' try { const [t, ps, ideasList] = await Promise.all([ - taskApi.get(props.taskId), + taskApi.get(tid), projectApi.list(), ideaApi.list(), ]) + if (tid !== props.taskId) return // taskId 已切换,旧任务响应丢弃 task.value = t projects.value = ps ideas.value = ideasList await loadChildren() await loadParentTitle() } catch (e: any) { + if (tid !== props.taskId) return // 旧任务请求的失败不污染当前视图 errorMsg.value = e?.toString() ?? t('common.unknownError') } finally { - loading.value = false + if (tid === props.taskId) loading.value = false // 新一轮 load 已接管 loading 态 } } diff --git a/src/views/AuditLog.vue b/src/views/AuditLog.vue index 5d53cbe..79eea85 100644 --- a/src/views/AuditLog.vue +++ b/src/views/AuditLog.vue @@ -282,21 +282,28 @@ function buildQuery() { return q } +// 请求序号守卫(对齐 project/tasks store 的 _tasksSeq 模式):快速切筛选/翻页时 +// 旧响应晚到不覆盖新结果,旧请求的失败也不污染当前视图 +let _loadSeq = 0 + async function load() { + const seq = ++_loadSeq loading.value = true errorMsg.value = '' try { const result = await invoke('list_tool_executions', { query: buildQuery(), }) + if (seq !== _loadSeq) return // 旧响应丢弃,只取最新 records.value = result.items total.value = result.total hasMore.value = result.has_more // 后端 total 已正确,空结果即真无数据(不再像旧"满页推断"需回退) } catch (e) { + if (seq !== _loadSeq) return // 旧请求的失败不污染当前视图 errorMsg.value = String(e) } finally { - loading.value = false + if (seq === _loadSeq) loading.value = false } } @@ -378,12 +385,14 @@ function reloadAudit() { void load() } function prevPage() { + if (loading.value) return // 加载中禁止翻页,防并发请求互踩 if (offset.value === 0) return offset.value = Math.max(0, offset.value - PAGE_SIZE) expandedId.value = null void load() } function nextPage() { + if (loading.value) return // 加载中禁止翻页,防并发请求互踩 if (!hasMore.value) return offset.value += PAGE_SIZE expandedId.value = null diff --git a/src/views/Dashboard.vue b/src/views/Dashboard.vue index 4dc4f5f..dfa0241 100644 --- a/src/views/Dashboard.vue +++ b/src/views/Dashboard.vue @@ -14,10 +14,11 @@
- -
- {{ errorMsg }} - + +
+ {{ store.error || errorMsg }} +
diff --git a/src/views/Ideas.vue b/src/views/Ideas.vue index 739f248..e39e09d 100644 --- a/src/views/Ideas.vue +++ b/src/views/Ideas.vue @@ -449,7 +449,12 @@ async function onStatusChange(newStatus: IdeaStatus) { }) if (!await confirmDialog(msg, t('common.confirm'))) return } - await store.updateIdea(currentIdea.value.id, 'status', newStatus) + try { + await store.updateIdea(currentIdea.value.id, 'status', newStatus) + } catch (e: any) { + // 失败 toast(对齐 onUpdateDesc 先例),不吞成 unhandled rejection + Message.error(e?.toString() ?? t('common.unknownError')) + } } // 接收 IdeaDetail 子组件 emit 的 'update-desc'(描述编辑保存) diff --git a/src/views/Knowledge.vue b/src/views/Knowledge.vue index 841120c..eeae640 100644 --- a/src/views/Knowledge.vue +++ b/src/views/Knowledge.vue @@ -381,13 +381,14 @@ async function submitCreate() { submitting.value = true try { const tags = form.value.tagsInput.split(',').map(t => t.trim()).filter(Boolean) - await store.create({ + const r = await store.create({ kind: form.value.kind, title: form.value.title.trim(), content: form.value.content.trim(), tags: JSON.stringify(tags), confidence: form.value.confidence || undefined, }) + if (!r) return // 失败保持弹窗不关(错误走 error-banner) showCreateModal.value = false switchTopTab('inbox') showToast(t('knowledge.createdToast'), 'success') diff --git a/src/views/Projects.vue b/src/views/Projects.vue index 8bfa692..061da02 100644 --- a/src/views/Projects.vue +++ b/src/views/Projects.vue @@ -80,8 +80,8 @@ {{ formatDate(p.updated_at) }} {{ $t('projects.movedIn') }}
- - + +
@@ -313,6 +313,9 @@ async function submitCreate() { // ── 删除 / 回收站 ── const showTrashModal = ref(false) +// 回收站操作 pending 守卫:操作中禁用按钮,防双击重复提交/并发互踩 +const restoringId = ref(null) +const purgingId = ref(null) async function handleDelete(project: ProjectRecord) { if (!await confirmDialog(t('projects.confirmDelete', { name: project.name }))) return @@ -325,12 +328,24 @@ async function openTrash() { } async function handleRestore(id: string) { - await store.restoreProject(id) + if (restoringId.value || purgingId.value) return + restoringId.value = id + try { + await store.restoreProject(id) + } finally { + restoringId.value = null + } } async function handlePurge(project: ProjectRecord) { if (!await confirmDialog(t('projects.confirmPurge', { name: project.name }))) return - await store.purgeProject(project.id) + if (restoringId.value || purgingId.value) return + purgingId.value = project.id + try { + await store.purgeProject(project.id) + } finally { + purgingId.value = null + } } // ── 加载 ──