diff --git a/src/components/AIChat/ChatExplorer.vue b/src/components/AIChat/ChatExplorer.vue index f12e1826..66376f80 100644 --- a/src/components/AIChat/ChatExplorer.vue +++ b/src/components/AIChat/ChatExplorer.vue @@ -10,6 +10,7 @@ import AIChatInput from './input/AIChatInput.vue' import AIThinkingIndicator from './chat/AIThinkingIndicator.vue' import ChatStatusBar from './chat/ChatStatusBar.vue' import ChatHeaderActions from './chat/ChatHeaderActions.vue' +import LoadingState from '@/components/UI/LoadingState.vue' import { useAIChat } from '@/composables/useAIChat' import { useAIService, useLLMService } from '@/services' import AssistantInlineBar from './assistant/AssistantInlineBar.vue' @@ -46,12 +47,7 @@ const props = defineProps<{ chatType?: 'group' | 'private' }>() -const emit = defineEmits<{ - 'restore-loading-change': [loading: boolean] -}>() - const initialAIChatId = typeof route.query.aiChatId === 'string' ? route.query.aiChatId : null -emit('restore-loading-change', true) // 使用 AI 对话 Composable const { @@ -85,6 +81,19 @@ const { let isAIChatInitialized = false let isUnmounted = false +const showRestoreLoading = ref(false) +// 快速缓存恢复不显示遮罩,避免切换 Tab 时出现单帧闪屏;较慢恢复只覆盖 AI 内容区。 +let restoreLoadingTimer: ReturnType | null = setTimeout(() => { + showRestoreLoading.value = true +}, 120) + +function finishRestoreLoading(): void { + if (restoreLoadingTimer) { + clearTimeout(restoreLoadingTimer) + restoreLoadingTimer = null + } + showRestoreLoading.value = false +} function isCurrentSessionRoute(): boolean { const routeName = (props.chatType ?? 'group') === 'private' ? 'private-chat' : 'group-chat' @@ -106,16 +115,16 @@ async function syncAIChatIdToRoute(aiChatId: string | null): Promise { } void initialization.finally(() => { + finishRestoreLoading() if (!isCurrentSessionRoute()) return - emit('restore-loading-change', false) isAIChatInitialized = true void syncAIChatIdToRoute(currentAIChatId.value) }) onUnmounted(() => { isUnmounted = true - emit('restore-loading-change', false) + finishRestoreLoading() }) watch(currentAIChatId, (aiChatId) => { @@ -472,7 +481,9 @@ watch( -
+
-
+
{ + await t.mock.module('@/utils', { + namedExports: { + formatLocalizedDate: () => '', + }, + }) + const { useSessionAnalysisPageBase } = await import('./useSessionAnalysisPageBase') + + let analysisLoadCount = 0 + registerAdapter('data', { + getSession: async () => ({ id: 'session-one' }), + getMemberActivity: async () => [createMember('session-one')], + getHourlyActivity: async () => [], + getDailyActivity: async () => [], + getMessageTypeDistribution: async () => { + analysisLoadCount++ + return [] + }, + } as unknown as DataAdapter) + + t.mock.method(console, 'warn', () => undefined) + const scope = effectScope() + t.after(() => scope.stop()) + const route = reactive({ params: { id: 'session-one' }, query: {} }) as unknown as RouteLocationNormalizedLoaded + const router = { replace: async () => undefined } as unknown as Router + const currentSessionId = ref('session-one') + const page = scope.run(() => + useSessionAnalysisPageBase({ + route, + router, + currentSessionId, + selectSession: () => undefined, + defaultTab: 'ai-chat', + validTabIds: ['insights', 'ai-chat'], + }) + )! + + page.timeRangeValue.value = { + startTs: 1, + endTs: 2, + displayLabel: 'test', + isFullRange: false, + state: { mode: 'custom', customStart: '1970-01-01', customEnd: '1970-01-01' }, + } + await nextTick() + + page.activeTab.value = 'insights' + await flushPromises() + + assert.equal(analysisLoadCount, 1) + assert.equal(page.isLoading.value, false) + + page.activeTab.value = 'ai-chat' + await nextTick() + page.activeTab.value = 'insights' + await flushPromises() + + assert.equal(analysisLoadCount, 1) + assert.equal(page.isLoading.value, false) +}) diff --git a/src/composables/useSessionAnalysisPageBase.ts b/src/composables/useSessionAnalysisPageBase.ts index acb5cc73..dd7bc412 100644 --- a/src/composables/useSessionAnalysisPageBase.ts +++ b/src/composables/useSessionAnalysisPageBase.ts @@ -41,6 +41,14 @@ export function useSessionAnalysisPageBase(options: UseSessionAnalysisPageBaseOp let analysisLoadVersion = 0 let baseDataReady = false let analysisDataReady = false + let loadedAnalysisScopeKey: string | null = null + + function getAnalysisScopeKey(): string | null { + const sessionId = currentSessionId.value + if (!sessionId) return null + const filter = timeFilter.value + return filter ? `${sessionId}:${filter.startTs}:${filter.endTs}` : `${sessionId}:all` + } function finishSessionSwitchIfReady() { if (!isSessionSwitching.value || !baseDataReady) return @@ -96,6 +104,7 @@ export function useSessionAnalysisPageBase(options: UseSessionAnalysisPageBaseOp if (!sessionId) return const loadVersion = ++analysisLoadVersion + const analysisScopeKey = getAnalysisScopeKey() if (!activeTabUsesOverviewAnalytics.value) { isLoading.value = false analysisDataReady = true @@ -123,6 +132,7 @@ export function useSessionAnalysisPageBase(options: UseSessionAnalysisPageBaseOp hourlyActivity.value = hourly dailyActivity.value = daily messageTypes.value = types + loadedAnalysisScopeKey = analysisScopeKey } catch (error) { if (loadVersion === analysisLoadVersion) { console.error('加载分析数据失败:', error) @@ -178,6 +188,14 @@ export function useSessionAnalysisPageBase(options: UseSessionAnalysisPageBaseOp return } + const analysisScopeKey = getAnalysisScopeKey() + if (analysisScopeKey && analysisScopeKey === loadedAnalysisScopeKey) { + isLoading.value = false + analysisDataReady = true + finishSessionSwitchIfReady() + return + } + isLoading.value = true if (currentSessionId.value && timeRangeValue.value) void loadAnalysisData() }) @@ -201,6 +219,7 @@ export function useSessionAnalysisPageBase(options: UseSessionAnalysisPageBaseOp currentSessionId, () => { analysisLoadVersion++ + loadedAnalysisScopeKey = null resetTimeRange() baseDataReady = false analysisDataReady = !activeTabUsesTimeRange.value diff --git a/src/pages/group-chat/index.vue b/src/pages/group-chat/index.vue index 1835f5cc..9f1950ef 100644 --- a/src/pages/group-chat/index.vue +++ b/src/pages/group-chat/index.vue @@ -87,9 +87,6 @@ const { provide('session-switch-loading', isSessionSwitching) -const isAIChatRestoring = ref(activeTab.value === 'ai-chat' && typeof route.query.aiChatId === 'string') -const showSessionLoading = computed(() => isSessionSwitching.value || isAIChatRestoring.value) - // 当前筛选后的消息总数 const filteredMessageCount = computed(() => { return memberActivity.value.reduce((sum, m) => sum + m.messageCount, 0) @@ -104,7 +101,7 @@ const filteredMemberCount = computed(() => { diff --git a/src/pages/insight/index.vue b/src/pages/insight/index.vue index f4f36506..acce4d87 100644 --- a/src/pages/insight/index.vue +++ b/src/pages/insight/index.vue @@ -101,7 +101,7 @@ onBeforeUnmount(() => { > isSessionSwitching.value || isAIChatRestoring.value) - // 当前筛选后的消息总数 const filteredMessageCount = computed(() => { return memberActivity.value.reduce((sum, m) => sum + m.messageCount, 0) @@ -126,7 +123,7 @@ const otherMemberAvatar = computed(() => {