Revert "Feat/virtualizarefactor: virtualization rework — unify Virtual components, fix memory leaks, migrate 15+ consumerstion rework (#472)"

This reverts commit 5953496d84.
This commit is contained in:
jxxghp
2026-05-15 21:42:43 +08:00
parent 5953496d84
commit 546af84dab
51 changed files with 2133 additions and 2401 deletions

View File

@@ -1,7 +1,6 @@
<script lang="ts" setup>
import type { Message } from '@/api/types'
import MessageCard from '@/components/cards/MessageCard.vue'
import VirtualList from '@/components/virtual/VirtualList.vue'
import api from '@/api'
import { useI18n } from 'vue-i18n'
import { useBackgroundOptimization } from '@/composables/useBackgroundOptimization'
@@ -10,28 +9,28 @@ import { useBackgroundOptimization } from '@/composables/useBackgroundOptimizati
const { t } = useI18n()
const { useSSE } = useBackgroundOptimization()
// 消息列表(按时间升序:旧 -> 新,最新在底部)
// 消息列表
const messages = ref<Message[]>([])
// 当前页数据
const currData = ref<Message[]>([])
// 已加载消息的签名集合,仅按秒级时间戳会误吞同一秒内的不同消息
// 已加载消息的签名集合
// 使用消息内容签名去重,避免仅按秒级时间戳判断时误吞同一秒内的不同消息。
const messageKeys = new Set<string>()
// 是否完成首屏加载(影响空态文案显示)
// 是否完成加载
const isLoaded = ref(false)
// 是否加载中(避免并发)
// 是否加载中
const loading = ref(false)
// 还有更早的消息可加载?拉到空页后置 false
const hasMoreOlder = ref(true)
// 当前页码:拉取「更早」消息时递增
// 当前页码
const page = ref(1)
// 存量消息最新时间
const lastTime = ref('')
// 反向加载阈值。首屏完成 + forceScrollToEnd 之后才打开,否则
// reverse sentinel 在 items 首次渲染时会和跳到底部竞速,多拉一页。
const reverseThreshold = ref(0)
// 虚拟列表实例引用:用于读取 scrollEl 做智能滚动
// 消息列表滚动容器
const messageListRef = ref<any>(null)
// 自动滚动状态
@@ -48,8 +47,7 @@ function getMessageTime(message: Message) {
return message.reg_time || message.date || ''
}
// 生成消息签名(多字段 fallback 链,单字段 keyField 表达不了,
// 所以走 VirtualList 的 getItemKey 函数 prop
// 生成消息签名
function getMessageKey(message: Message) {
return [
message.action ?? '',
@@ -64,11 +62,12 @@ function getMessageKey(message: Message) {
].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) {
@@ -77,8 +76,7 @@ function updateLastTime(message: Message) {
}
function getScrollContainer() {
const container =
messageListRef.value?.getScrollElement?.() ?? messageListRef.value?.$el ?? messageListRef.value
const container = messageListRef.value?.$el ?? messageListRef.value
return container instanceof HTMLElement ? container : null
}
@@ -102,6 +100,21 @@ function handleScroll() {
updateAutoScrollState()
}
function bindScrollListener() {
const container = getScrollContainer()
if (!container) {
return
}
container.removeEventListener('scroll', handleScroll)
container.addEventListener('scroll', handleScroll, { passive: true })
updateAutoScrollState()
}
function unbindScrollListener() {
getScrollContainer()?.removeEventListener('scroll', handleScroll)
}
function scrollContainerToEnd() {
const container = getScrollContainer()
if (!container) {
@@ -154,71 +167,101 @@ function forceScrollToEnd() {
requestScrollToEnd(true)
}
// 合并消息到当前列表
function mergeMessages(items: Message[]) {
let hasNewMessage = false
for (const item of sortMessages(items)) {
const messageKey = getMessageKey(item)
if (messageKeys.has(messageKey)) continue
if (messageKeys.has(messageKey)) {
continue
}
messageKeys.add(messageKey)
messages.value.push(item)
updateLastTime(item)
hasNewMessage = true
}
if (hasNewMessage) messages.value = sortMessages(messages.value)
if (hasNewMessage) {
messages.value = sortMessages(messages.value)
}
return hasNewMessage
}
// SSE 新消息到达 -> 智能跟随到底
// SSE消息处理函数
function handleSSEMessage(event: MessageEvent) {
const message = event.data
if (!message) return
const object = JSON.parse(message)
if (mergeMessages([object])) {
requestScrollToEnd()
if (message) {
const object = JSON.parse(message)
if (mergeMessages([object])) {
requestScrollToEnd() // 新消息到达时触发智能滚动
}
}
}
// 使用优化的 SSE 连接
const { manager } = useSSE(
// 使用优化的SSE连接
const { manager, isConnected } = useSSE(
`${import.meta.env.VITE_API_BASE_URL}system/message?role=user`,
handleSSEMessage,
'message-view',
{ backgroundCloseDelay: 5000, reconnectDelay: 3000, maxReconnectAttempts: 3 },
{
backgroundCloseDelay: 5000,
reconnectDelay: 3000,
maxReconnectAttempts: 3,
},
)
// 加载更早一页:首屏 + 用户向上滚动时由 @load-more-reverse 触发
async function loadOlderMessages() {
if (loading.value || !hasMoreOlder.value) return
// 调用API加载存量消息
async function loadMessages({ done }: { done: any }) {
// 如果正在加载中,直接返回
if (loading.value) {
done('ok')
return
}
try {
// 设置加载中
loading.value = true
const data = (await api.get('message/web', {
params: { page: page.value, size: 20 },
})) as Message[]
currData.value = await api.get('message/web', {
params: {
page: page.value,
size: 20,
},
})
// 已加载过
isLoaded.value = true
if (data.length > 0) {
const hasNewMessage = mergeMessages(data)
if (currData.value.length > 0) {
const hasNewMessage = mergeMessages(currData.value)
// 首次加载时滚动到底部
if (page.value === 1 && hasNewMessage) {
requestScrollToEnd(true)
}
// 页码+1
page.value++
// 完成
done('ok')
} else {
hasMoreOlder.value = false
// 没有新数据
done('empty')
}
} catch (error) {
console.error('加载消息失败:', error)
done('error')
} finally {
loading.value = false
}
}
// 主动刷新最新一页,作为 SSE 偶发丢流时的兜底
// 主动刷新最新一页消息作为SSE偶发丢流时的兜底
async function refreshLatestMessages() {
try {
const latestMessages = (await api.get('message/web', {
params: { page: 1, size: 20 },
params: {
page: 1,
size: 20,
},
})) as Message[]
if (mergeMessages(latestMessages)) {
@@ -229,41 +272,57 @@ async function refreshLatestMessages() {
}
}
// 比较时间
// 比较yyyy-MM-dd HH:mm:ss时间大小
function compareTime(time1: string, time2: string) {
if (!time1 && !time2) return 0
if (!time1) return -1
if (!time2) return 1
try {
// 统一时间格式处理,支持多种格式
const normalizeTime = (time: string) => {
if (time.includes('T')) return new Date(time).getTime()
// 如果是ISO格式直接使用
if (time.includes('T')) {
return new Date(time).getTime()
}
// 如果是yyyy-MM-dd HH:mm:ss格式替换-为/
return new Date(time.replaceAll(/-/g, '/')).getTime()
}
return normalizeTime(time1) - normalizeTime(time2)
const timestamp1 = normalizeTime(time1)
const timestamp2 = normalizeTime(time2)
return timestamp1 - timestamp2
} catch (error) {
console.error('时间比较错误:', error, 'time1:', time1, 'time2:', time2)
return 0
}
}
// 图片加载完成 -> 智能跟随(图片把行撑高,需要重判距底距离)
// 图片加载完成时触发智能滚动
function handleImageLoad() {
requestScrollToEnd()
}
// 暂停/恢复 SSE
// 暂停SSE连接
function pauseSSE() {
manager?.removeMessageListener('message-view')
if (manager) {
manager.removeMessageListener('message-view')
}
}
// 恢复SSE连接
function resumeSSE() {
if (manager) {
// 先移除再重建监听确保恢复时拿到一条新的SSE连接。
manager.removeMessageListener('message-view')
manager.addMessageListener('message-view', handleSSEMessage)
}
refreshLatestMessages()
}
// 暴露方法给父组件
defineExpose({
pauseSSE,
resumeSSE,
@@ -271,12 +330,10 @@ defineExpose({
forceScrollToEnd,
})
onMounted(async () => {
await loadOlderMessages()
await nextTick()
scrollContainerToEnd()
requestAnimationFrame(() => {
reverseThreshold.value = 1
onMounted(() => {
nextTick(() => {
bindScrollListener()
requestScrollToEnd(true)
})
})
@@ -288,43 +345,38 @@ onBeforeUnmount(() => {
if (scrollReleaseTimer) {
window.clearTimeout(scrollReleaseTimer)
}
unbindScrollListener()
})
</script>
<template>
<VirtualList
<VInfiniteScroll
ref="messageListRef"
:mode="!isLoaded ? 'intersect' : 'manual'"
side="start"
:items="messages"
:estimate-size="160"
:get-item-key="getMessageKey"
:load-more-reverse-threshold="reverseThreshold"
container-height="100%"
class="h-full overflow-auto"
@load-more-reverse="loadOlderMessages"
@scroll="handleScroll"
class="overflow-auto h-full"
@load="loadMessages"
:load-more-text="t('message.loadMore') + ' ...'"
>
<template #empty>
<div class="d-flex justify-center align-center h-full text-medium-emphasis">
{{ isLoaded ? t('message.noMoreData') : '' }}
</div>
</template>
<template #loading>
<LoadingBanner v-if="loading" />
<LoadingBanner />
</template>
<template #item="{ item }">
<div
class="chat-group d-flex mt-5 mb-8"
:class="item.action == 1 ? 'flex-row align-start' : 'flex-row-reverse align-end'"
>
<template #empty> {{ t('message.noMoreData') }} </template>
<VVirtualScroll renderless :items="messages" :item-height="160">
<template #default="{ item, index, itemRef }">
<div
class="d-inline-flex flex-column"
:class="item.action == 1 ? 'align-start' : 'align-end'"
:ref="itemRef"
:key="getMessageKey(item) || index"
class="chat-group d-flex mt-5 mb-8"
:class="item.action == 1 ? 'flex-row align-start' : 'flex-row-reverse align-end'"
>
<MessageCard :message="item" @imageload="handleImageLoad" />
<div class="d-inline-flex flex-column" :class="item.action == 1 ? 'align-start' : 'align-end'">
<MessageCard :message="item" @imageload="handleImageLoad" />
</div>
</div>
</div>
</template>
</VirtualList>
</template>
</VVirtualScroll>
</VInfiniteScroll>
</template>