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
+ }
}
// ── 加载 ──