修复: 设置页和 AI 独立窗口渲染崩溃(循环依赖 TDZ)

- ProviderPanel 改静态 import useAiStore 为动态 import,打破循环依赖链
- ErrorBoundary 增强显示具体错误信息和可折叠堆栈详情
This commit is contained in:
2026-06-30 21:44:56 +08:00
parent b19b0f3679
commit f5f101d88a
2 changed files with 60 additions and 7 deletions

View File

@@ -7,7 +7,11 @@
<template v-if="error">
<div class="error-boundary-icon"></div>
<div class="error-boundary-title">{{ $t('error.title') }}</div>
<div class="error-boundary-desc">{{ $t('error.desc') }}</div>
<div class="error-boundary-desc">{{ errorMessage }}</div>
<details class="error-boundary-details">
<summary>堆栈详情</summary>
<pre class="error-boundary-stack">{{ errorStack }}</pre>
</details>
<button class="error-boundary-retry" @click="reset">{{ $t('error.retry') }}</button>
</template>
<slot v-else />
@@ -15,11 +19,26 @@
</template>
<script setup lang="ts">
import { ref, onErrorCaptured } from 'vue'
import { ref, computed, onErrorCaptured } from 'vue'
// errorCaptured 捕获子组件树抛出的错误(渲染/生命周期),返回 false 阻止继续向上冒泡
const error = ref<unknown>(null)
const errorMessage = computed(() => {
if (!error.value) return ''
if (error.value instanceof Error) return error.value.message
return String(error.value)
})
const errorStack = computed(() => {
if (!error.value) return ''
if (error.value instanceof Error) return error.value.stack || error.value.message
try {
return JSON.stringify(error.value, null, 2)
} catch {
return String(error.value)
}
})
onErrorCaptured((err) => {
error.value = err
console.error('[ErrorBoundary] 子树渲染错误:', err)
@@ -61,7 +80,32 @@ function reset() {
.error-boundary-desc {
font-size: 12px;
line-height: 1.5;
max-width: 360px;
max-width: 480px;
word-break: break-all;
}
.error-boundary-details {
max-width: 600px;
width: 100%;
margin-top: 8px;
font-size: 11px;
color: var(--df-text-dim);
}
.error-boundary-details summary {
cursor: pointer;
user-select: none;
}
.error-boundary-stack {
margin-top: 6px;
padding: 8px;
background: rgba(0,0,0,0.3);
border-radius: 4px;
font-family: var(--df-font-mono, monospace);
font-size: 10px;
line-height: 1.4;
white-space: pre-wrap;
word-break: break-all;
max-height: 240px;
overflow: auto;
}
.error-boundary-retry {
margin-top: 8px;

View File

@@ -130,14 +130,22 @@
import { reactive, ref, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { aiApi } from '@/api'
import { useAiStore } from '@/stores/ai'
import type { AiProviderConfig, ModelConfig } from '@/api/types'
// ============================================================
// AI 提供商域 — 真实后端 CRUD + F-04c 负载均衡池(enabled / weight)
// ============================================================
const { t } = useI18n()
// 懒加载 useAiStore 避免静态 import 形成循环依赖:
// ProviderPanel → stores/ai → useAiEvents → aiShared → useAppSettingsStore
// 在 Settings chunk 首次加载时,stores/ai 模块顶层代码(__bindMessages)可能触发 TDZ。
// 改为运行时动态 import,模块加载顺序不再耦合。
async function syncAiStoreProviders() {
const { useAiStore } = await import('@/stores/ai')
const aiStore = useAiStore()
await aiStore.loadProviders()
}
const emit = defineEmits<{
(e: 'toast', msg: string, type: 'error' | 'warning' | 'info'): void
@@ -170,7 +178,8 @@ async function loadProviders() {
try {
aiProviders.value = await aiApi.listProviders()
// 同步刷新全局 store(AiChat 等 AI 面板消费方共享同一 state,避免改完仍提示「未配置」)
await aiStore.loadProviders()
// 懒加载避免循环依赖(见文件顶部 syncAiStoreProviders 注释)
await syncAiStoreProviders()
emit('providers-changed')
} catch (e) {
console.error(t('settings.toastLoadProviderFail'), e)