修复: 审批浮窗 3 bug(打开面板权限/审批后空转自关/关闭顺序)

This commit is contained in:
lxy
2026-08-01 16:05:44 +08:00
parent 7193901bc4
commit 2b08e32c26
5 changed files with 39 additions and 19 deletions
+1
View File
@@ -9,6 +9,7 @@
"core:event:allow-emit",
"core:window:allow-create",
"core:window:allow-close",
"core:window:allow-show",
"core:window:allow-set-always-on-top",
"core:window:allow-set-focus",
"core:window:allow-set-position",
+6 -2
View File
@@ -122,12 +122,16 @@ let _stopApprovalPopup: (() => void) | null = null
let _unlistenPopupReady: (() => void) | null = null
async function activateForPopup() {
// 浮窗「打开审批面板」按钮 → 聚焦主窗口 + 打开 AI 面板/激活分离窗口
// 修复 BUG1:setFocus 先行(已授权,失焦场景有效),show 兜底(需 core:window:allow-show,
// capabilities/default.json 已补)。原顺序 show→setFocus,show reject 直接吞掉 setFocus,
// 主窗口最小化/隐藏时 show 失败 → 用户看到「点了没反应」。
try {
const { WebviewWindow } = await import('@tauri-apps/api/webviewWindow')
const mainWin = await WebviewWindow.getByLabel('main')
if (mainWin) {
await mainWin.show()
await mainWin.setFocus()
// setFocus 先行(对未聚焦但可见的窗口即可恢复焦点);show 兜底取消最小化/隐藏
try { await mainWin.setFocus() } catch { /* setFocus 失败不阻塞 show */ }
try { await mainWin.show() } catch { /* show 未授权或不可用,忽略(setFocus 已尽力) */ }
}
} catch { /* 忽略 */ }
if (!aiStore.state.detached && !aiStore.state.panelOpen) {
+2 -2
View File
@@ -57,8 +57,8 @@ export default {
popupTagPath: 'Path auth',
popupTagRisk: 'Risk action',
popupOpenPanel: 'Open approval panel',
// 根因1:首屏空态友好文案(数据未到达时,替代裸 spinner)
popupEmptyHint: 'Waiting for approval data…',
// 空态文案:正常空态会被 maybeAutoClose 即时关窗,此 hint 仅 close 失败时兜底
popupEmptyHint: 'Closing…',
// ── Provider status ──
clickToSwitchProvider: 'Click to switch provider',
+2 -2
View File
@@ -57,8 +57,8 @@ export default {
popupTagPath: '路径授权',
popupTagRisk: '风险操作',
popupOpenPanel: '打开审批面板',
// 根因1:首屏空态友好文案(数据未到达时,替代裸 spinner)
popupEmptyHint: '等待审批数据…',
// 空态文案:正常空态会被 maybeAutoClose 即时关窗,此 hint 仅 close 失败时兜底
popupEmptyHint: '正在关闭…',
// ── Provider 状态 ──
clickToSwitchProvider: '点击切换 Provider',
+28 -13
View File
@@ -30,7 +30,8 @@
<!-- 待审批列表 -->
<div class="popup-body">
<div v-if="approvals.length === 0" class="popup-empty">
<span class="popup-empty-spinner"></span>
<!-- BUG2 修复:空态不再永久转圈( popup-empty-spinner 处理完仍转圈误判卡死)
空态正常会被 maybeAutoClose 即时关窗; hint 仅作防御性兜底(close 失败时给可见反馈) -->
<span class="popup-empty-hint">{{ $t('aiChat.popupEmptyHint') }}</span>
</div>
<div
@@ -144,6 +145,8 @@ onMounted(async () => {
ApprovalPopupEvents.EVT_UPDATE,
(e) => {
approvals.value = e.payload?.approvals || []
// BUG2 修复:主窗口推空快照(所有 pending 已清)→ 立即关浮窗,避免停留在空态转圈
void maybeAutoClose()
},
)
} catch (e) {
@@ -175,6 +178,8 @@ async function onApprove(tc: AiToolCallInfo) {
}
// 乐观移除(主窗口 listener 收后端 emit 后会推新快照覆盖,此乐观仅缩短视觉延迟)
approvals.value = approvals.value.filter(a => a.id !== tc.id)
// BUG2 修复:乐观移除后若列表空,立即关浮窗(避免空列表停留在 popup-empty 永久转圈态)
await maybeAutoClose()
} catch (e) {
console.error('[ApprovalPopup] 批准失败:', e)
} finally {
@@ -193,6 +198,7 @@ async function onReject(tc: AiToolCallInfo) {
await aiApi.approve(tc.id, false)
}
approvals.value = approvals.value.filter(a => a.id !== tc.id)
await maybeAutoClose()
} catch (e) {
console.error('[ApprovalPopup] 拒绝失败:', e)
} finally {
@@ -200,6 +206,20 @@ async function onReject(tc: AiToolCallInfo) {
}
}
/** 列表清空时自动关浮窗。
* 场景:用户处理完最后一条审批 / 主窗口推空快照(EVT_UPDATE approvals:[])。
* 修复 BUG2:原实现空列表渲染 popup-empty + 永久 popup-empty-spinner,用户看到「列表空 + 一直转」
* 误以为卡死。空 = 完成,应即时关浮窗(不等待主窗口 watch closePopup 的跨窗口往返)。 */
async function maybeAutoClose() {
if (approvals.value.length > 0) return
try {
const { emit } = await import('@tauri-apps/api/event')
// 先关窗(确保用户立刻看到反馈),再 emit 清主窗口引用(顺序与 onClose 一致)
await getCurrentWebviewWindow().close()
await emit(ApprovalPopupEvents.EVT_CLOSED, {})
} catch { /* ignore */ }
}
/** 点击「打开审批面板」:emit 主窗口聚焦 + togglePanel */
async function onActivate() {
try {
@@ -208,12 +228,17 @@ async function onActivate() {
} catch { /* ignore */ }
}
/** 关闭浮窗(用户主动关 → emit closed 让主窗口清引用,允许下次有新审批再弹) */
/** 关闭浮窗(用户主动关 → emit closed 让主窗口清引用,允许下次有新审批再弹)
* 修复 BUG3:原顺序 emit→close,若 emit 跨窗口分发阻塞/异常(close 永不执行)→ 关闭按钮「无效」。
* 改为先 close(即时反馈,core:window:allow-close 已授权),再 emit 清主窗口引用;
* 即便 emit 失败,主窗口 win.once('tauri://destroyed') 也会清 _popupWin(双保险)。 */
async function onClose() {
try {
await getCurrentWebviewWindow().close()
} catch { /* ignore */ }
try {
const { emit } = await import('@tauri-apps/api/event')
await emit(ApprovalPopupEvents.EVT_CLOSED, {})
await getCurrentWebviewWindow().close()
} catch { /* ignore */ }
}
@@ -323,16 +348,6 @@ function onDragEnd() { isDragging.value = false }
min-height: 80px;
padding: 16px;
}
.popup-empty-spinner {
width: 14px;
height: 14px;
border: 2px solid var(--df-border, #444);
border-top-color: var(--df-accent);
border-radius: 50%;
animation: df-spin 0.8s linear infinite;
display: inline-block;
flex-shrink: 0;
}
.popup-empty-hint {
font-size: 11px;
color: var(--df-text-dim);