fix(review-tablet): 修复AI助手项目切换状态

pull/874/head
Codewoc 2026-03-27 14:25:13 +08:00
parent 91b550fb1e
commit ee816f95c7
1 changed files with 70 additions and 7 deletions

View File

@ -144,7 +144,7 @@
<!-- 未生成 --> <!-- 未生成 -->
<div v-if="!aiSummary || aiSummary.status === 0" class="ai-summary-empty"> <div v-if="!aiSummary || aiSummary.status === 0" class="ai-summary-empty">
<el-icon><InfoFilled /></el-icon> <el-icon><InfoFilled /></el-icon>
<span>当前项目尚未生成 AI 摘要请上传资料后自动触发生成</span> <span>{{ aiSummary?.blockReason || '当前项目 AI 尚未就绪,请联系管理员先完成构建' }}</span>
</div> </div>
<!-- 生成中 --> <!-- 生成中 -->
<div v-else-if="aiSummary.status === 1" class="ai-summary-building"> <div v-else-if="aiSummary.status === 1" class="ai-summary-building">
@ -154,7 +154,7 @@
<!-- 失败 --> <!-- 失败 -->
<div v-else-if="aiSummary.status === 3" class="ai-summary-failed"> <div v-else-if="aiSummary.status === 3" class="ai-summary-failed">
<el-icon><WarningFilled /></el-icon> <el-icon><WarningFilled /></el-icon>
<span>摘要生成失败{{ aiSummary.errorMessage || '未知错误' }}</span> <span>{{ aiSummary.blockReason || `摘要生成失败:${aiSummary.errorMessage || '未知错误'}` }}</span>
</div> </div>
<!-- 已完成 --> <!-- 已完成 -->
<div v-else-if="aiSummary.status === 2 && aiSummary.summary" class="ai-summary-card"> <div v-else-if="aiSummary.status === 2 && aiSummary.summary" class="ai-summary-card">
@ -268,7 +268,7 @@
<!-- 摘要未就绪时提示 --> <!-- 摘要未就绪时提示 -->
<div v-else-if="aiSummary && aiSummary.status !== 2" class="ai-chat-unavail"> <div v-else-if="aiSummary && aiSummary.status !== 2" class="ai-chat-unavail">
AI 摘要就绪后才能开始对话 {{ aiSummary.blockReason || 'AI 摘要就绪后才能开始对话' }}
</div> </div>
</aside> </aside>
</div> </div>
@ -338,6 +338,12 @@ interface FileTreeNode {
source: ReviewMeetingFileRespVO source: ReviewMeetingFileRespVO
} }
interface ProjectAiState {
summary: ReviewAiSummaryVO | null
conversationId: number | null
messages: ChatMessageVO[]
}
// & state // & state
const pageLoading = ref(false) const pageLoading = ref(false)
const previewLoading = ref(false) const previewLoading = ref(false)
@ -393,8 +399,10 @@ const streamingLoading = ref(false)
const aiMessagesRef = ref<HTMLElement>() const aiMessagesRef = ref<HTMLElement>()
let streamAbortCtrl: AbortController | null = null let streamAbortCtrl: AbortController | null = null
let summaryPollingTimer: ReturnType<typeof setTimeout> | null = null let summaryPollingTimer: ReturnType<typeof setTimeout> | null = null
let aiLoadToken = 0
let officeWarmupTimers: ReturnType<typeof setTimeout>[] = [] let officeWarmupTimers: ReturnType<typeof setTimeout>[] = []
let previewFrameTimeoutTimer: ReturnType<typeof setTimeout> | null = null let previewFrameTimeoutTimer: ReturnType<typeof setTimeout> | null = null
const aiProjectCache = ref<Record<number, ProjectAiState>>({})
// //
watch(keyword, (val) => treeRef.value?.filter(val.trim())) watch(keyword, (val) => treeRef.value?.filter(val.trim()))
@ -529,14 +537,24 @@ const handleNodeCollapse = (data: TreeNode) => {
const handleNodeClick = async (data: TreeNode) => { const handleNodeClick = async (data: TreeNode) => {
if (data.type === 'project') { if (data.type === 'project') {
persistCurrentProjectAiState()
const switched = activeProjectId.value !== data.projectId const switched = activeProjectId.value !== data.projectId
activeProjectId.value = data.projectId activeProjectId.value = data.projectId
await loadProjectFiles(data) await loadProjectFiles(data)
if (switched) resetProjectAiState() if (switched && aiPanelOpen.value) {
if (aiPanelOpen.value) await loadProjectAi(data.projectId) restoreOrLoadProjectAi(data.projectId)
}
if (!switched && aiPanelOpen.value) {
restoreOrLoadProjectAi(data.projectId)
}
return return
} }
persistCurrentProjectAiState()
const switched = activeProjectId.value !== data.projectId
await selectFileNode(data) await selectFileNode(data)
if (aiPanelOpen.value && (switched || !aiSummary.value)) {
restoreOrLoadProjectAi(data.projectId)
}
} }
// //
@ -571,7 +589,7 @@ const loadCatalog = async () => {
const toggleAiPanel = async () => { const toggleAiPanel = async () => {
aiPanelOpen.value = !aiPanelOpen.value aiPanelOpen.value = !aiPanelOpen.value
if (aiPanelOpen.value && activeProjectId.value) { if (aiPanelOpen.value && activeProjectId.value) {
await loadProjectAi(activeProjectId.value) restoreOrLoadProjectAi(activeProjectId.value)
} }
} }
@ -585,11 +603,47 @@ const resetProjectAiState = () => {
streamingText.value = '' 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) => { const loadProjectAi = async (projectId: number) => {
resetProjectAiState() resetProjectAiState()
aiSummaryLoading.value = true aiSummaryLoading.value = true
const token = ++aiLoadToken
try { 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) { if (aiSummary.value?.status === AI_SUMMARY_STATUS.BUILDING) {
startSummaryPolling(projectId) startSummaryPolling(projectId)
@ -608,11 +662,15 @@ const loadProjectAi = async (projectId: number) => {
const openAndLoadConversation = async (projectId: number) => { const openAndLoadConversation = async (projectId: number) => {
aiChatLoading.value = true aiChatLoading.value = true
const token = aiLoadToken
try { try {
const conv = await openProjectAiConversation(projectId) const conv = await openProjectAiConversation(projectId)
if (token !== aiLoadToken || activeProjectId.value !== projectId) return
aiConversationId.value = conv.conversationId aiConversationId.value = conv.conversationId
const msgs = await ChatMessageApi.getChatMessageListByConversationId(conv.conversationId) const msgs = await ChatMessageApi.getChatMessageListByConversationId(conv.conversationId)
if (token !== aiLoadToken || activeProjectId.value !== projectId) return
aiMessages.value = msgs || [] aiMessages.value = msgs || []
persistCurrentProjectAiState()
await scrollAiToBottom() await scrollAiToBottom()
} catch { } catch {
// //
@ -628,6 +686,7 @@ const startSummaryPolling = (projectId: number) => {
if (activeProjectId.value !== projectId || !aiPanelOpen.value) return if (activeProjectId.value !== projectId || !aiPanelOpen.value) return
try { try {
aiSummary.value = await getProjectAiSummary(projectId) aiSummary.value = await getProjectAiSummary(projectId)
persistCurrentProjectAiState()
} catch { /* silent */ } } catch { /* silent */ }
if (aiSummary.value?.status === AI_SUMMARY_STATUS.BUILDING) { if (aiSummary.value?.status === AI_SUMMARY_STATUS.BUILDING) {
startSummaryPolling(projectId) startSummaryPolling(projectId)
@ -679,6 +738,7 @@ const handleSend = async () => {
// send // send
aiMessages.value.pop() aiMessages.value.pop()
aiMessages.value.push(data.send) aiMessages.value.push(data.send)
persistCurrentProjectAiState()
} }
if (data?.receive?.content) { if (data?.receive?.content) {
streamingText.value += data.receive.content streamingText.value += data.receive.content
@ -699,6 +759,7 @@ const handleSend = async () => {
createTime: new Date() createTime: new Date()
} as any) } as any)
streamingText.value = '' streamingText.value = ''
persistCurrentProjectAiState()
} }
streamingLoading.value = false streamingLoading.value = false
await scrollAiToBottom() await scrollAiToBottom()
@ -719,6 +780,7 @@ const stopStream = () => {
createTime: new Date() createTime: new Date()
} as any) } as any)
streamingText.value = '' streamingText.value = ''
persistCurrentProjectAiState()
} }
streamingLoading.value = false streamingLoading.value = false
} }
@ -729,6 +791,7 @@ const handleClearMessages = async () => {
stopStream() stopStream()
await clearProjectAiMessages(activeProjectId.value) await clearProjectAiMessages(activeProjectId.value)
aiMessages.value = [] aiMessages.value = []
persistCurrentProjectAiState()
ElMessage.success('问答记录已清空') ElMessage.success('问答记录已清空')
} }