修复: 页面反馈真实性与竞态守卫八处

- 知识创建失败不再误关弹窗弹成功提示(补返回值检查)
- AuditLog 翻页补请求序号守卫防慢响应覆盖新页
- TaskDrawer 切任务慢响应校验 taskId 才写入
- GitChanges 竞态序号+moduleId 双校验,三处静默失败补错误提示
- 回收站恢复/彻底删除操作中禁用防双击重复提交
- 灵感状态切换失败补错误反馈(原 unhandled rejection)
- FileExplorer 三处失败仅 console 补 Message 提示
- Dashboard 消费 store.error 故障不再伪装空数据
This commit is contained in:
lxy
2026-09-29 14:58:35 +08:00
parent 1d1111ebe7
commit 864a03d2e1
8 changed files with 92 additions and 24 deletions
+10 -1
View File
@@ -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<ToolExecutionPage>('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
+5 -4
View File
@@ -14,10 +14,11 @@
</div>
</header>
<!-- ═══ 错误条:refresh/onMounted 加载失败对用户可见(样式全局 .error-banner) ═══ -->
<div v-if="errorMsg" class="error-banner" style="margin-bottom: 14px">
<span class="error-text">{{ errorMsg }}</span>
<button class="error-dismiss" @click="errorMsg = ''">✕</button>
<!-- ═══ 错误条:消费 store.error(loadProjects/loadIdeas 吞错写 state.error,不再伪装空态)
+ 本地加载异常 errorMsg(样式全局 .error-banner) ═══ -->
<div v-if="store.error || errorMsg" class="error-banner" style="margin-bottom: 14px">
<span class="error-text">{{ store.error || errorMsg }}</span>
<button class="error-dismiss" @click="store.clearError(); errorMsg = ''">✕</button>
</div>
<!-- ═══ Main Grid ═══ -->
+6 -1
View File
@@ -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'(描述编辑保存)
+2 -1
View File
@@ -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')
+19 -4
View File
@@ -80,8 +80,8 @@
<span class="trash-date">{{ formatDate(p.updated_at) }} {{ $t('projects.movedIn') }}</span>
</div>
<div class="trash-actions">
<button class="btn btn-ghost btn-sm" @click="handleRestore(p.id)">{{ $t('projects.restore') }}</button>
<button class="btn btn-danger btn-sm" @click="handlePurge(p)">{{ $t('projects.purge') }}</button>
<button class="btn btn-ghost btn-sm" :disabled="!!restoringId || !!purgingId" @click="handleRestore(p.id)">{{ $t('projects.restore') }}</button>
<button class="btn btn-danger btn-sm" :disabled="!!restoringId || !!purgingId" @click="handlePurge(p)">{{ $t('projects.purge') }}</button>
</div>
</div>
</div>
@@ -313,6 +313,9 @@ async function submitCreate() {
// ── 删除 / 回收站 ──
const showTrashModal = ref(false)
// 回收站操作 pending 守卫:操作中禁用按钮,防双击重复提交/并发互踩
const restoringId = ref<string | null>(null)
const purgingId = ref<string | null>(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
}
}
// ── 加载 ──