diff --git a/src/views/review/tablet/index.vue b/src/views/review/tablet/index.vue index c7c4e3913..9a09f5bea 100644 --- a/src/views/review/tablet/index.vue +++ b/src/views/review/tablet/index.vue @@ -144,7 +144,7 @@
- 当前项目尚未生成 AI 摘要,请上传资料后自动触发生成 + {{ aiSummary?.blockReason || '当前项目 AI 尚未就绪,请联系管理员先完成构建' }}
@@ -154,7 +154,7 @@
- 摘要生成失败:{{ aiSummary.errorMessage || '未知错误' }} + {{ aiSummary.blockReason || `摘要生成失败:${aiSummary.errorMessage || '未知错误'}` }}
@@ -268,7 +268,7 @@
- AI 摘要就绪后才能开始对话 + {{ aiSummary.blockReason || 'AI 摘要就绪后才能开始对话' }}
@@ -338,6 +338,12 @@ interface FileTreeNode { source: ReviewMeetingFileRespVO } +interface ProjectAiState { + summary: ReviewAiSummaryVO | null + conversationId: number | null + messages: ChatMessageVO[] +} + // ── 目录 & 预览 state ───────────────────────────────────────── const pageLoading = ref(false) const previewLoading = ref(false) @@ -393,8 +399,10 @@ const streamingLoading = ref(false) const aiMessagesRef = ref() let streamAbortCtrl: AbortController | null = null let summaryPollingTimer: ReturnType | null = null +let aiLoadToken = 0 let officeWarmupTimers: ReturnType[] = [] let previewFrameTimeoutTimer: ReturnType | null = null +const aiProjectCache = ref>({}) // ── 搜索过滤 ────────────────────────────────────────────────── watch(keyword, (val) => treeRef.value?.filter(val.trim())) @@ -529,14 +537,24 @@ const handleNodeCollapse = (data: TreeNode) => { const handleNodeClick = async (data: TreeNode) => { if (data.type === 'project') { + persistCurrentProjectAiState() const switched = activeProjectId.value !== data.projectId activeProjectId.value = data.projectId await loadProjectFiles(data) - if (switched) resetProjectAiState() - if (aiPanelOpen.value) await loadProjectAi(data.projectId) + if (switched && aiPanelOpen.value) { + restoreOrLoadProjectAi(data.projectId) + } + if (!switched && aiPanelOpen.value) { + restoreOrLoadProjectAi(data.projectId) + } return } + persistCurrentProjectAiState() + const switched = activeProjectId.value !== data.projectId await selectFileNode(data) + if (aiPanelOpen.value && (switched || !aiSummary.value)) { + restoreOrLoadProjectAi(data.projectId) + } } // ── 目录加载 ────────────────────────────────────────────────── @@ -571,7 +589,7 @@ const loadCatalog = async () => { const toggleAiPanel = async () => { aiPanelOpen.value = !aiPanelOpen.value if (aiPanelOpen.value && activeProjectId.value) { - await loadProjectAi(activeProjectId.value) + restoreOrLoadProjectAi(activeProjectId.value) } } @@ -585,11 +603,47 @@ const resetProjectAiState = () => { streamingText.value = '' } +const persistCurrentProjectAiState = () => { + const projectId = activeProjectId.value + if (!projectId) return + aiProjectCache.value[projectId] = { + summary: aiSummary.value, + conversationId: aiConversationId.value, + messages: [...aiMessages.value] + } +} + +const applyProjectAiState = (state?: ProjectAiState) => { + resetProjectAiState() + aiSummary.value = state?.summary ?? null + aiConversationId.value = state?.conversationId ?? null + aiMessages.value = state?.messages ? [...state.messages] : [] +} + +const restoreOrLoadProjectAi = (projectId: number) => { + const cached = aiProjectCache.value[projectId] + if (cached) { + applyProjectAiState(cached) + if (cached.summary?.status === AI_SUMMARY_STATUS.BUILDING) { + startSummaryPolling(projectId) + } else if (cached.summary?.status !== AI_SUMMARY_STATUS.SUCCESS) { + loadProjectAi(projectId) + } + scrollAiToBottom() + return + } + loadProjectAi(projectId) +} + const loadProjectAi = async (projectId: number) => { resetProjectAiState() aiSummaryLoading.value = true + const token = ++aiLoadToken try { - aiSummary.value = await getProjectAiSummary(projectId) + const summary = await getProjectAiSummary(projectId) + if (token !== aiLoadToken || activeProjectId.value !== projectId) return + aiSummary.value = summary + persistCurrentProjectAiState() // 如果生成中,开启轮询 if (aiSummary.value?.status === AI_SUMMARY_STATUS.BUILDING) { startSummaryPolling(projectId) @@ -608,11 +662,15 @@ const loadProjectAi = async (projectId: number) => { const openAndLoadConversation = async (projectId: number) => { aiChatLoading.value = true + const token = aiLoadToken try { const conv = await openProjectAiConversation(projectId) + if (token !== aiLoadToken || activeProjectId.value !== projectId) return aiConversationId.value = conv.conversationId const msgs = await ChatMessageApi.getChatMessageListByConversationId(conv.conversationId) + if (token !== aiLoadToken || activeProjectId.value !== projectId) return aiMessages.value = msgs || [] + persistCurrentProjectAiState() await scrollAiToBottom() } catch { // 会话打开失败不影响主流程 @@ -628,6 +686,7 @@ const startSummaryPolling = (projectId: number) => { if (activeProjectId.value !== projectId || !aiPanelOpen.value) return try { aiSummary.value = await getProjectAiSummary(projectId) + persistCurrentProjectAiState() } catch { /* silent */ } if (aiSummary.value?.status === AI_SUMMARY_STATUS.BUILDING) { startSummaryPolling(projectId) @@ -679,6 +738,7 @@ const handleSend = async () => { // 用真实 send 消息替换假消息 aiMessages.value.pop() aiMessages.value.push(data.send) + persistCurrentProjectAiState() } if (data?.receive?.content) { streamingText.value += data.receive.content @@ -699,6 +759,7 @@ const handleSend = async () => { createTime: new Date() } as any) streamingText.value = '' + persistCurrentProjectAiState() } streamingLoading.value = false await scrollAiToBottom() @@ -719,6 +780,7 @@ const stopStream = () => { createTime: new Date() } as any) streamingText.value = '' + persistCurrentProjectAiState() } streamingLoading.value = false } @@ -729,6 +791,7 @@ const handleClearMessages = async () => { stopStream() await clearProjectAiMessages(activeProjectId.value) aiMessages.value = [] + persistCurrentProjectAiState() ElMessage.success('问答记录已清空') }