聊天记录弹窗做了手机端适配

弹窗在小屏下改成接近全屏宽度
筛选区域改成纵向排列,避免挤在一行
日期范围选择器在手机端强制撑满宽度,避免“结束时间”看不到
历史列表和分页在小屏下也做了可滚动与间距优化
This commit is contained in:
taiyi 2026-05-23 16:13:45 +08:00
parent 62bdcc42c2
commit b44087f944

View File

@ -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 {