diff --git a/package.json b/package.json index 6036091c..a9bf89dc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "moviepilot", - "version": "2.10.5", + "version": "2.10.7", "private": true, "type": "module", "bin": "dist/service.js", diff --git a/src/layouts/components/ShortcutBar.vue b/src/layouts/components/ShortcutBar.vue index 4e4a3bd7..ec5b75f6 100644 --- a/src/layouts/components/ShortcutBar.vue +++ b/src/layouts/components/ShortcutBar.vue @@ -14,6 +14,12 @@ import { getQueryValue } from '@/@core/utils' import { useI18n } from 'vue-i18n' import { clearAppBadge } from '@/utils/badge' +type MessageViewExpose = { + pauseSSE?: () => void + resumeSSE?: () => void + refreshLatestMessages?: () => Promise | void +} + // 国际化 const { t } = useI18n() @@ -63,7 +69,7 @@ const sendButtonDisabled = ref(false) const messageDialogRef = ref(null) // 消息视图引用 -const messageViewRef = ref(null) +const messageViewRef = ref(null) // 滚动容器引用 const messageContentRef = ref() @@ -153,9 +159,7 @@ async function openMessageDialog() { }, 600) // 等待对话框打开后恢复SSE连接 nextTick(() => { - if (messageViewRef.value && typeof messageViewRef.value.resumeSSE === 'function') { - messageViewRef.value.resumeSSE() - } + messageViewRef.value?.resumeSSE?.() }) } @@ -203,16 +207,23 @@ function allLoggingUrl() { // 发送消息 async function sendMessage() { - if (user_message.value) { - try { - sendButtonDisabled.value = true - await api.post(`message/web?text=${user_message.value}`) - user_message.value = '' - sendButtonDisabled.value = false - forceScrollToEnd() // 发送消息后强制滚动到底部 - } catch (error) { - console.error(error) - } + const messageText = user_message.value.trim() + if (!messageText) { + return + } + + try { + sendButtonDisabled.value = true + await api.post(`message/web?text=${encodeURIComponent(messageText)}`) + user_message.value = '' + + // 发送成功后主动同步最新一页消息,避免SSE短暂断流时界面停留在旧状态。 + await messageViewRef.value?.refreshLatestMessages?.() + forceScrollToEnd() // 发送消息后强制滚动到底部 + } catch (error) { + console.error(error) + } finally { + sendButtonDisabled.value = false } } @@ -228,7 +239,7 @@ defineExpose({ // 监听消息对话框状态变化 watch(messageDialog, newValue => { - if (!newValue && messageViewRef.value && typeof messageViewRef.value.pauseSSE === 'function') { + if (!newValue && messageViewRef.value?.pauseSSE) { // 对话框关闭时暂停SSE连接 messageViewRef.value.pauseSSE() } diff --git a/src/views/system/MessageView.vue b/src/views/system/MessageView.vue index 3de43464..63289601 100644 --- a/src/views/system/MessageView.vue +++ b/src/views/system/MessageView.vue @@ -17,6 +17,10 @@ const messages = ref([]) // 当前页数据 const currData = ref([]) +// 已加载消息的签名集合 +// 使用消息内容签名去重,避免仅按秒级时间戳判断时误吞同一秒内的不同消息。 +const messageKeys = new Set() + // 是否完成加载 const isLoaded = ref(false) @@ -29,18 +33,72 @@ const page = ref(1) // 存量消息最新时间 const lastTime = ref('') +// 获取消息时间 +function getMessageTime(message: Message) { + return message.reg_time || message.date || '' +} + +// 生成消息签名 +function getMessageKey(message: Message) { + return [ + message.action ?? '', + message.userid ?? '', + message.reg_time ?? '', + message.date ?? '', + message.title ?? '', + message.text ?? '', + message.image ?? '', + message.link ?? '', + message.note ?? '', + ].join('::') +} + +// 排序消息列表,确保最新消息始终位于底部 +function sortMessages(items: Message[]) { + return [...items].sort((a, b) => compareTime(getMessageTime(a), getMessageTime(b))) +} + +// 记录最新消息时间 +function updateLastTime(message: Message) { + const messageTime = getMessageTime(message) + if (messageTime && compareTime(messageTime, lastTime.value) > 0) { + lastTime.value = messageTime + } +} + +// 合并消息到当前列表 +function mergeMessages(items: Message[]) { + let hasNewMessage = false + + for (const item of sortMessages(items)) { + const messageKey = getMessageKey(item) + if (messageKeys.has(messageKey)) { + continue + } + + messageKeys.add(messageKey) + messages.value.push(item) + updateLastTime(item) + hasNewMessage = true + } + + if (hasNewMessage) { + messages.value = sortMessages(messages.value) + } + + return hasNewMessage +} + // SSE消息处理函数 function handleSSEMessage(event: MessageEvent) { const message = event.data if (message) { const object = JSON.parse(message) - // 使用reg_time或date字段进行比较 - const messageTime = object.reg_time || object.date - if (compareTime(messageTime, lastTime.value) <= 0) return - messages.value.push(object) - nextTick(() => { - emit('scroll') // 新消息到达时触发智能滚动 - }) + if (mergeMessages([object])) { + nextTick(() => { + emit('scroll') // 新消息到达时触发智能滚动 + }) + } } } @@ -75,28 +133,10 @@ async function loadMessages({ done }: { done: any }) { // 已加载过 isLoaded.value = true if (currData.value.length > 0) { - // 按时间排序,确保最新的消息在最后 - currData.value.sort((a, b) => { - const timeA = a.reg_time || a.date || '' - const timeB = b.reg_time || b.date || '' - return compareTime(timeA, timeB) - }) - - // 取最后一条时间为存量消息最新时间 - const lastMessage = currData.value[currData.value.length - 1] - lastTime.value = lastMessage.reg_time || lastMessage.date || '' - - // 合并数据并重新排序 - const allMessages = [...currData.value, ...messages.value] - allMessages.sort((a, b) => { - const timeA = a.reg_time || a.date || '' - const timeB = b.reg_time || b.date || '' - return compareTime(timeA, timeB) - }) - messages.value = allMessages + const hasNewMessage = mergeMessages(currData.value) // 首次加载时滚动到底部 - if (page.value === 1) { + if (page.value === 1 && hasNewMessage) { nextTick(() => { emit('scroll') }) @@ -118,6 +158,26 @@ async function loadMessages({ done }: { done: any }) { } } +// 主动刷新最新一页消息,作为SSE偶发丢流时的兜底 +async function refreshLatestMessages() { + try { + const latestMessages = (await api.get('message/web', { + params: { + page: 1, + size: 20, + }, + })) as Message[] + + if (mergeMessages(latestMessages)) { + nextTick(() => { + emit('scroll') + }) + } + } catch (error) { + console.error('刷新最新消息失败:', error) + } +} + // 比较yyyy-MM-dd HH:mm:ss时间大小 function compareTime(time1: string, time2: string) { if (!time1 && !time2) return 0 @@ -160,14 +220,19 @@ function pauseSSE() { // 恢复SSE连接 function resumeSSE() { if (manager) { + // 先移除再重建监听,确保恢复时拿到一条新的SSE连接。 + manager.removeMessageListener('message-view') manager.addMessageListener('message-view', handleSSEMessage) } + + refreshLatestMessages() } // 暴露方法给父组件 defineExpose({ pauseSSE, resumeSSE, + refreshLatestMessages, }) onMounted(() => { @@ -194,7 +259,7 @@ onMounted(() => {