修复: 设置页和 AI 独立窗口渲染崩溃(循环依赖 TDZ)
- ProviderPanel 改静态 import useAiStore 为动态 import,打破循环依赖链 - ErrorBoundary 增强显示具体错误信息和可折叠堆栈详情
This commit is contained in:
@@ -7,7 +7,11 @@
|
|||||||
<template v-if="error">
|
<template v-if="error">
|
||||||
<div class="error-boundary-icon">⚠</div>
|
<div class="error-boundary-icon">⚠</div>
|
||||||
<div class="error-boundary-title">{{ $t('error.title') }}</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>
|
<button class="error-boundary-retry" @click="reset">{{ $t('error.retry') }}</button>
|
||||||
</template>
|
</template>
|
||||||
<slot v-else />
|
<slot v-else />
|
||||||
@@ -15,11 +19,26 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onErrorCaptured } from 'vue'
|
import { ref, computed, onErrorCaptured } from 'vue'
|
||||||
|
|
||||||
// errorCaptured 捕获子组件树抛出的错误(渲染/生命周期),返回 false 阻止继续向上冒泡
|
|
||||||
const error = ref<unknown>(null)
|
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) => {
|
onErrorCaptured((err) => {
|
||||||
error.value = err
|
error.value = err
|
||||||
console.error('[ErrorBoundary] 子树渲染错误:', err)
|
console.error('[ErrorBoundary] 子树渲染错误:', err)
|
||||||
@@ -61,7 +80,32 @@ function reset() {
|
|||||||
.error-boundary-desc {
|
.error-boundary-desc {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 1.5;
|
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 {
|
.error-boundary-retry {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
|
|||||||
@@ -130,14 +130,22 @@
|
|||||||
import { reactive, ref, onMounted } from 'vue'
|
import { reactive, ref, onMounted } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { aiApi } from '@/api'
|
import { aiApi } from '@/api'
|
||||||
import { useAiStore } from '@/stores/ai'
|
|
||||||
import type { AiProviderConfig, ModelConfig } from '@/api/types'
|
import type { AiProviderConfig, ModelConfig } from '@/api/types'
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// AI 提供商域 — 真实后端 CRUD + F-04c 负载均衡池(enabled / weight)
|
// AI 提供商域 — 真实后端 CRUD + F-04c 负载均衡池(enabled / weight)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const aiStore = useAiStore()
|
|
||||||
|
// 懒加载 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<{
|
const emit = defineEmits<{
|
||||||
(e: 'toast', msg: string, type: 'error' | 'warning' | 'info'): void
|
(e: 'toast', msg: string, type: 'error' | 'warning' | 'info'): void
|
||||||
@@ -170,7 +178,8 @@ async function loadProviders() {
|
|||||||
try {
|
try {
|
||||||
aiProviders.value = await aiApi.listProviders()
|
aiProviders.value = await aiApi.listProviders()
|
||||||
// 同步刷新全局 store(AiChat 等 AI 面板消费方共享同一 state,避免改完仍提示「未配置」)
|
// 同步刷新全局 store(AiChat 等 AI 面板消费方共享同一 state,避免改完仍提示「未配置」)
|
||||||
await aiStore.loadProviders()
|
// 懒加载避免循环依赖(见文件顶部 syncAiStoreProviders 注释)
|
||||||
|
await syncAiStoreProviders()
|
||||||
emit('providers-changed')
|
emit('providers-changed')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(t('settings.toastLoadProviderFail'), e)
|
console.error(t('settings.toastLoadProviderFail'), e)
|
||||||
|
|||||||
Reference in New Issue
Block a user