From b44087f944db9cc0a068c548ecaf526820ab1969 Mon Sep 17 00:00:00 2001 From: taiyi Date: Sat, 23 May 2026 16:13:45 +0800 Subject: [PATCH] =?UTF-8?q?=E8=81=8A=E5=A4=A9=E8=AE=B0=E5=BD=95=E5=BC=B9?= =?UTF-8?q?=E7=AA=97=E5=81=9A=E4=BA=86=E6=89=8B=E6=9C=BA=E7=AB=AF=E9=80=82?= =?UTF-8?q?=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 弹窗在小屏下改成接近全屏宽度 筛选区域改成纵向排列,避免挤在一行 日期范围选择器在手机端强制撑满宽度,避免“结束时间”看不到 历史列表和分页在小屏下也做了可滚动与间距优化 --- frontend/src/views/ChatView.vue | 267 ++++++++++++++++++++++++++++++-- 1 file changed, 252 insertions(+), 15 deletions(-) diff --git a/frontend/src/views/ChatView.vue b/frontend/src/views/ChatView.vue index a3bb190..8e18868 100644 --- a/frontend/src/views/ChatView.vue +++ b/frontend/src/views/ChatView.vue @@ -337,7 +337,7 @@ viewportWidth.value < 768) + const LOCAL_HISTORY_KEY = 'pet_chat_history_backup' const petAvatarStyle = computed(() => { const x = Number(petPosition.value.x) || 50 @@ -978,9 +984,40 @@ export default { await switchToTextMode() } - const resetToDefaultMode = async () => { + const forceStopVoiceAndCall = async () => { + try { + if (isRecording.value) { + await chatStore.stopRecording({ sendEndSpeech: false }) + } + } catch (error) { + console.warn('[ChatView] stopRecording while switching to text failed:', error) + } + + try { + chatStore.stopAudioPlayback() + } catch (error) { + console.warn('[ChatView] stopAudioPlayback while switching to text failed:', error) + } + + try { + chatStore.exitVoiceChatMode() + } catch (error) { + console.warn('[ChatView] exitVoiceChatMode while switching to text failed:', error) + } + + try { + chatStore.exitRealtimeCallMode() + } catch (error) { + console.warn('[ChatView] exitRealtimeCallMode while switching to text failed:', error) + } + isInCall.value = false isRecording.value = false + stopCallTimer() + } + + const resetToDefaultMode = async () => { + await forceStopVoiceAndCall() showInputPanel.value = false chatMode.value = 'none' isAiTyping.value = false @@ -991,8 +1028,7 @@ export default { } const switchToTextMode = async () => { - isInCall.value = false - isRecording.value = false + await forceStopVoiceAndCall() showInputPanel.value = true chatMode.value = 'text' @@ -1205,6 +1241,78 @@ export default { } } + const getSavedHistoryBackup = () => { + try { + const saved = localStorage.getItem(LOCAL_HISTORY_KEY) + return saved ? JSON.parse(saved) : {} + } catch (err) { + console.error('Failed to load chat history backup:', err) + return {} + } + } + + const saveHistoryBackup = (message, mode = 'text') => { + try { + if (!message?.content) return + const petId = activePetInChat.value?.pet?.id || currentPet.value?.id || null + const backgroundId = currentBackground.value?.id || null + const backup = getSavedHistoryBackup() + const key = `${petId || 'unknown'}_${backgroundId || 'unknown'}` + const list = Array.isArray(backup[key]) ? backup[key] : [] + + const lastItem = list[list.length - 1] + const now = new Date().toISOString() + if (mode === 'voice' || mode === 'realtime') { + if (message.isUser) { + list.push({ + id: message.id || now, + user_msg: message.content, + ai_msg: '', + created_at: message.timestamp || now, + mode + }) + } else if (lastItem && !lastItem.ai_msg) { + lastItem.ai_msg = message.content + lastItem.updated_at = message.timestamp || now + } else { + list.push({ + id: message.id || now, + user_msg: '', + ai_msg: message.content, + created_at: message.timestamp || now, + mode + }) + } + } else { + if (message.isUser) { + list.push({ + id: message.id || now, + user_msg: message.content, + ai_msg: '', + created_at: message.timestamp || now, + mode + }) + } else if (lastItem && !lastItem.ai_msg) { + lastItem.ai_msg = message.content + lastItem.updated_at = message.timestamp || now + } else { + list.push({ + id: message.id || now, + user_msg: '', + ai_msg: message.content, + created_at: message.timestamp || now, + mode + }) + } + } + + backup[key] = list.slice(-200) + localStorage.setItem(LOCAL_HISTORY_KEY, JSON.stringify(backup)) + } catch (err) { + console.error('Failed to save chat history backup:', err) + } + } + const getSavedConfig = () => { try { const saved = localStorage.getItem(CONFIG_STORAGE_KEY) @@ -1383,7 +1491,15 @@ export default { const backgroundId = currentBackground.value?.id console.log('[ChatView] Sending message:', messageText, 'petId:', petId, 'backgroundId:', backgroundId) - await chatStore.sendMessage(messageText, { petId, backgroundId }) + const response = await chatStore.sendMessage(messageText, { petId, backgroundId }) + const userMsg = chatStore.messages.find(m => m.isUser && m.content === messageText && m.status === 'sent') + if (userMsg) { + saveHistoryBackup(userMsg, 'text') + } + const aiMsg = [...chatStore.messages].reverse().find(m => !m.isUser && m.status === 'completed') + if (aiMsg) { + saveHistoryBackup(aiMsg, 'text') + } console.log('[ChatView] Message sent successfully, messages:', chatStore.messages) console.log('[ChatView] latestAiMessage:', latestAiMessage.value) scrollToBottom() @@ -1398,17 +1514,13 @@ export default { } const toggleVoiceInput = async () => { - if (isRecording.value) { - await stopRecording() - } if (chatMode.value === 'voice') { - chatStore.exitVoiceChatMode() await resetToDefaultMode() } else { try { const hasPermission = await chatStore.requestMicrophonePermission() if (hasPermission) { - chatStore.exitVoiceChatMode() + await forceStopVoiceAndCall() showInputPanel.value = true chatMode.value = 'voice' } @@ -1422,6 +1534,7 @@ export default { const startVoiceRecording = async () => { if (isRecording.value) return try { + await forceStopVoiceAndCall() if (!chatStore.isVoiceConnected) { await chatStore.connectVoiceWebSocket() await new Promise(resolve => setTimeout(resolve, 300)) @@ -1486,8 +1599,7 @@ export default { const toggleCallMode = async () => { if (isInCall.value || chatMode.value === 'call') { await stopCallRecording() - chatStore.exitRealtimeCallMode() - stopCallTimer() + await forceStopVoiceAndCall() await resetToDefaultMode() return } @@ -1498,7 +1610,7 @@ export default { return } - chatStore.exitVoiceChatMode() + await forceStopVoiceAndCall() chatMode.value = 'call' showInputPanel.value = true @@ -1686,7 +1798,34 @@ export default { } const loadHistoryConversation = (item) => { - chatStore.clearMessages() + if (item?.user_msg || item?.ai_msg) { + const historyMessages = [ + { + id: `history-user-${item.id || Date.now()}`, + content: item.user_msg || '', + isUser: true, + timestamp: item.created_at, + status: 'completed', + source: 'history' + } + ] + + if (item.ai_msg) { + historyMessages.push({ + id: `history-ai-${item.id || Date.now()}`, + content: item.ai_msg, + isUser: false, + timestamp: item.created_at, + status: 'completed', + source: 'history' + }) + } + + chatStore.messages = historyMessages + } else { + chatStore.clearMessages() + } + showHistoryPanel.value = false resetToDefaultMode() } @@ -1747,9 +1886,19 @@ export default { console.log('[ChatView] New messages:', newMessages) console.log('[ChatView] Old messages length:', oldMessages?.length) - // 找出最新的AI消息 const latestAi = newMessages?.slice().reverse().find(m => !m.isUser) console.log('[ChatView] Latest AI message found:', latestAi) + + if (Array.isArray(newMessages) && newMessages.length) { + newMessages.forEach((message) => { + if (!message?.id || savedHistoryMessageKeys.has(message.id)) return + if (message.source === 'history') return + if (message.status === 'streaming') return + const mode = chatMode.value === 'call' ? 'realtime' : chatMode.value === 'voice' ? 'voice' : 'text' + saveHistoryBackup(message, mode) + savedHistoryMessageKeys.add(message.id) + }) + } syncStoreState() scrollToBottom() @@ -3174,6 +3323,16 @@ export default { overflow: hidden; } + .history-date-picker { + width: 100%; + min-width: 240px; + flex: 1 1 260px; + } + + .history-filters { + align-items: center; + } + .el-dialog__header { background: transparent !important; border-bottom: 1px solid rgba(255, 255, 255, 0.1); @@ -3216,6 +3375,84 @@ export default { .el-dialog__headerbtn:hover .el-dialog__close { color: #fff; } + + @media (max-width: 768px) { + .el-dialog { + width: calc(100vw - 16px) !important; + max-height: calc(100vh - 16px); + margin: 8px auto !important; + border-radius: 14px; + } + + .el-dialog__header { + padding: 12px 14px; + } + + .el-dialog__title { + font-size: 16px; + } + + .el-dialog__body { + padding: 12px 14px 14px; + } + + .history-filters { + gap: 8px; + flex-direction: column; + align-items: stretch; + } + + .history-list { + max-height: calc(100vh - 300px); + overflow-y: auto; + } + + .history-pagination { + margin-top: 12px; + flex-shrink: 0; + } + + .history-date-picker, + :deep(.history-dialog .el-select), + .history-context-tag { + width: 100%; + min-width: 0; + } + + .history-date-picker { + :deep(.el-range-editor), + :deep(.el-input__wrapper) { + width: 100%; + } + } + + :deep(.history-dialog .el-date-range-picker) { + width: calc(100vw - 16px) !important; + max-width: calc(100vw - 16px) !important; + } + + :deep(.history-dialog .el-picker-panel) { + width: 100% !important; + max-width: 100% !important; + } + + :deep(.history-dialog .el-date-range-picker__content) { + width: 100%; + float: none; + } + + :deep(.history-dialog .el-date-range-picker__header) { + padding: 8px 0; + } + + :deep(.history-dialog .el-date-table) { + width: 100%; + } + + .history-conversation-item .message-item .message-content { + max-width: 88%; + } + } } .feature-tooltip {