聊天记录弹窗做了手机端适配
弹窗在小屏下改成接近全屏宽度 筛选区域改成纵向排列,避免挤在一行 日期范围选择器在手机端强制撑满宽度,避免“结束时间”看不到 历史列表和分页在小屏下也做了可滚动与间距优化
This commit is contained in:
parent
62bdcc42c2
commit
b44087f944
@ -337,7 +337,7 @@
|
||||
<el-dialog
|
||||
v-model="showHistoryPanel"
|
||||
title="聊天历史"
|
||||
width="80%"
|
||||
:width="isMobileViewport ? 'calc(100vw - 16px)' : '80%'"
|
||||
class="history-dialog"
|
||||
:modal-append-to-body="false"
|
||||
destroy-on-close
|
||||
@ -351,8 +351,11 @@
|
||||
end-placeholder="结束日期"
|
||||
popper-class="history-date-range-popper"
|
||||
:cell-class-name="getHistoryDateCellClass"
|
||||
:teleported="false"
|
||||
placement="bottom-start"
|
||||
@change="loadChatHistory"
|
||||
size="small"
|
||||
class="history-date-picker"
|
||||
/>
|
||||
<el-select
|
||||
v-model="selectedHistoryPetId"
|
||||
@ -496,6 +499,7 @@ export default {
|
||||
let conversationPanelTimer = null
|
||||
let inputPanelObserver = null
|
||||
let conversationBubbleTimer = null
|
||||
const savedHistoryMessageKeys = new Set()
|
||||
|
||||
const viewportWidth = ref(window.innerWidth)
|
||||
const inputPanelOffset = ref(140)
|
||||
@ -758,6 +762,8 @@ export default {
|
||||
const selectedHistoryPetId = ref(null)
|
||||
const historyMarkedDates = ref(new Set())
|
||||
const callDuration = ref('')
|
||||
const isMobileViewport = computed(() => 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 {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user