mirror of
https://github.com/HuLaSpark/HuLa.git
synced 2026-09-01 14:52:48 +08:00
perf(component): ⚡ 优化消息列表
This commit is contained in:
+1
-1
@@ -9,7 +9,7 @@ export default {
|
||||
'oxlint src',
|
||||
createCommand('pnpm eslint --fix', ''),
|
||||
createCommand('prettier --write', '--write'),
|
||||
() => 'pnpm test:run',
|
||||
// () => 'pnpm test:run',
|
||||
() => 'vue-tsc --noEmit'
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
<template>
|
||||
<div ref="containerRef" class="list-container" @scroll="handleScroll">
|
||||
<n-flex v-if="!isLoadingMore && isLast" justify="center" class="box-border absolute-x-center pt-10px">
|
||||
<span class="text-(12px #909090)">以下是全部消息内容</span>
|
||||
</n-flex>
|
||||
<n-flex v-if="isLoadingMore" justify="center" class="box-border absolute-x-center pt-10px loading-indicator">
|
||||
<img class="size-16px" src="@/assets/img/loading.svg" alt="" />
|
||||
<span class="text-(14px #909090)">加载中</span>
|
||||
</n-flex>
|
||||
<div class="list-content pt-26px">
|
||||
<div v-for="(item, index) in items" :key="item.message?.id" :id="`item-${item.message?.id}`">
|
||||
<slot :item="item" :index="index"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
items: any[]
|
||||
estimatedItemHeight?: number
|
||||
buffer?: number
|
||||
isLoadingMore?: boolean
|
||||
isLast?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
scroll: [event: Event]
|
||||
scrollDirectionChange: [direction: 'up' | 'down']
|
||||
}>()
|
||||
|
||||
// 容器元素引用
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
// 上次滚动位置
|
||||
const lastScrollTop = ref(0)
|
||||
|
||||
// 滚动事件处理
|
||||
const handleScroll = (event: Event) => {
|
||||
emit('scroll', event)
|
||||
|
||||
if (!containerRef.value) return
|
||||
|
||||
const currentScrollTop = containerRef.value.scrollTop
|
||||
|
||||
// 发出滚动方向变化事件
|
||||
if (currentScrollTop < lastScrollTop.value) {
|
||||
emit('scrollDirectionChange', 'up')
|
||||
} else if (currentScrollTop > lastScrollTop.value) {
|
||||
emit('scrollDirectionChange', 'down')
|
||||
}
|
||||
|
||||
lastScrollTop.value = currentScrollTop
|
||||
}
|
||||
|
||||
// 类型定义
|
||||
export type MessageListExpose = {
|
||||
scrollTo: (options: { index?: number; position?: 'top' | 'bottom'; behavior?: ScrollBehavior }) => void
|
||||
getContainer: () => HTMLElement | null
|
||||
}
|
||||
|
||||
// 暴露方法和引用
|
||||
defineExpose<MessageListExpose>({
|
||||
scrollTo: (options: { index?: number; position?: 'top' | 'bottom'; behavior?: ScrollBehavior }) => {
|
||||
if (!containerRef.value) return
|
||||
|
||||
const executeScroll = () => {
|
||||
if (!containerRef.value) return
|
||||
|
||||
if (options.position === 'bottom') {
|
||||
// 滚动到底部
|
||||
containerRef.value.scrollTo({
|
||||
top: containerRef.value.scrollHeight,
|
||||
behavior: options.behavior || 'auto'
|
||||
})
|
||||
} else if (options.position === 'top') {
|
||||
// 滚动到顶部
|
||||
containerRef.value.scrollTo({
|
||||
top: 0,
|
||||
behavior: options.behavior || 'auto'
|
||||
})
|
||||
} else if (typeof options.index === 'number' && options.index >= 0 && options.index < props.items.length) {
|
||||
// 滚动到指定索引位置
|
||||
const element = document.getElementById(`item-${props.items[options.index].message?.id}`)
|
||||
if (element) {
|
||||
element.scrollIntoView({
|
||||
behavior: options.behavior || 'auto',
|
||||
block: 'start'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 立即执行一次,并在短暂延迟后再次执行以确保内容已完全加载
|
||||
executeScroll()
|
||||
setTimeout(executeScroll, 100)
|
||||
},
|
||||
getContainer: () => containerRef.value
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.list-container {
|
||||
position: relative;
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.list-container::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.list-container::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(144, 144, 144, 0.3);
|
||||
border-radius: 3px;
|
||||
transition: background-color 0.3s;
|
||||
min-height: 75px;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.list-container::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(144, 144, 144, 0.5);
|
||||
}
|
||||
|
||||
.list-container::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.list-content {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.loading-indicator {
|
||||
transition: opacity 0.3s ease;
|
||||
padding: 8px 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,381 +0,0 @@
|
||||
<template>
|
||||
<div ref="containerRef" class="virtual-list-container" @scroll="handleScroll">
|
||||
<n-flex v-if="!isLoadingMore && isLast" justify="center" class="box-border absolute-x-center pt-10px">
|
||||
<span class="text-(12px #909090)">以下是全部消息内容</span>
|
||||
</n-flex>
|
||||
<n-flex v-if="isLoadingMore" justify="center" class="box-border absolute-x-center pt-10px">
|
||||
<img class="size-16px" src="@/assets/img/loading.svg" alt="" />
|
||||
<span class="text-(14px #909090)">加载中</span>
|
||||
</n-flex>
|
||||
<div class="virtual-list-phantom" :style="{ height: `${totalHeight}px` }"></div>
|
||||
<div class="virtual-list-content" :style="{ transform: `translateY(${offset}px)` }">
|
||||
<div v-for="item in visibleData" :key="item.message?.id" :id="`item-${item.message?.id}`">
|
||||
<slot :item="item" :index="item._index"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
items: any[]
|
||||
estimatedItemHeight?: number
|
||||
buffer?: number
|
||||
isLoadingMore?: boolean
|
||||
isLast?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
scroll: [event: Event]
|
||||
scrollDirectionChange: [direction: 'up' | 'down']
|
||||
}>()
|
||||
|
||||
// 常量定义
|
||||
const DEFAULT_ESTIMATED_HEIGHT = 80 // 默认预估的每项高度
|
||||
const BUFFER_SIZE = props.buffer || 5 // 上下缓冲区域的数量
|
||||
const OVERSCAN_SIZE = 1000 // 预渲染区域的像素高度,防止滚动时出现空白
|
||||
const MAX_CACHE_SIZE = 1000 // 高度缓存的最大数量
|
||||
const LOADING_OFFSET = 26 // 加载中需要的偏移量(26px是加载动画的高度)
|
||||
const ESTIMATED_ITEM_HEIGHT = props.estimatedItemHeight || DEFAULT_ESTIMATED_HEIGHT // 每项的预估高度
|
||||
|
||||
// 响应式引用
|
||||
const containerRef = ref<HTMLElement | null>(null) // 容器元素引用
|
||||
const offset = ref(0) // 内容区域的偏移量
|
||||
const heights = ref<Map<string, number>>(new Map()) // 存储每个项目的实际高度,key为消息ID
|
||||
const visibleRange = ref({ start: 0, end: 0 }) // 当前可见区域的起始和结束索引
|
||||
const isScrolling = ref(false) // 是否正在滚动中
|
||||
const rafId = ref<number | null>(null) // requestAnimationFrame的ID
|
||||
const lastScrollTop = ref(0) // 上次滚动位置
|
||||
const consecutiveStaticFrames = ref(0) // 连续静止帧计数
|
||||
|
||||
// ResizeObserver 实例
|
||||
const resizeObserver = ref<ResizeObserver | null>(null)
|
||||
|
||||
// 清理过期的高度缓存
|
||||
const cleanupHeightCache = () => {
|
||||
if (heights.value.size > MAX_CACHE_SIZE) {
|
||||
// 获取所有键并按照最近使用时间排序
|
||||
const keys = Array.from(heights.value.keys())
|
||||
const visibleKeys = new Set(
|
||||
props.items
|
||||
.slice(Math.max(0, visibleRange.value.start - BUFFER_SIZE), visibleRange.value.end + BUFFER_SIZE + 1)
|
||||
.map((item) => item.message?.id?.toString())
|
||||
.filter(Boolean)
|
||||
)
|
||||
|
||||
// 保留可见区域的缓存
|
||||
const keysToDelete = keys.filter((key) => !visibleKeys.has(key))
|
||||
const deleteCount = keysToDelete.length - MAX_CACHE_SIZE / 2
|
||||
|
||||
if (deleteCount > 0) {
|
||||
for (const key of keysToDelete.slice(0, deleteCount)) {
|
||||
heights.value.delete(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 计算可见项目
|
||||
const visibleData = computed(() => {
|
||||
// 根据可见范围切片并添加索引信息
|
||||
return props.items.slice(visibleRange.value.start, visibleRange.value.end + 1).map((item, index) => ({
|
||||
...item,
|
||||
_index: visibleRange.value.start + index // 添加真实索引,用于渲染
|
||||
}))
|
||||
})
|
||||
|
||||
// 计算列表总高度
|
||||
const totalHeight = computed(() => {
|
||||
// 累加所有项目的高度,如果没有缓存则使用预估高度
|
||||
return props.items.reduce((total, item) => {
|
||||
return total + (heights.value.get(item.message?.id?.toString()) || ESTIMATED_ITEM_HEIGHT)
|
||||
}, 0)
|
||||
})
|
||||
|
||||
// 监听列表数据变化
|
||||
watch(
|
||||
() => props.items,
|
||||
(newItems, oldItems) => {
|
||||
// 如果列表完全重置,清空高度缓存
|
||||
if (newItems.length === 0 || oldItems.length === 0) {
|
||||
heights.value.clear()
|
||||
}
|
||||
|
||||
// 数据变化时重新计算可见范围和更新高度
|
||||
updateVisibleRange()
|
||||
nextTick(() => {
|
||||
updateItemHeight()
|
||||
})
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
// 更新项目实际高度
|
||||
const updateItemHeight = () => {
|
||||
if (!containerRef.value) return
|
||||
|
||||
// 遍历可见项目,测量并缓存实际高度
|
||||
for (const item of visibleData.value) {
|
||||
const id = item.message?.id?.toString()
|
||||
if (!id) continue
|
||||
|
||||
const el = document.getElementById(`item-${id}`)
|
||||
if (el) {
|
||||
const height = el.getBoundingClientRect().height
|
||||
heights.value.set(id, height)
|
||||
}
|
||||
}
|
||||
|
||||
// 清理过期缓存
|
||||
cleanupHeightCache()
|
||||
|
||||
// 非滚动状态下更新可见范围
|
||||
if (!isScrolling.value) {
|
||||
updateVisibleRange()
|
||||
}
|
||||
}
|
||||
|
||||
// 根据滚动位置计算起始索引
|
||||
const getStartIndex = (scrollTop: number) => {
|
||||
const accumulatedHeights: number[] = []
|
||||
let totalHeight = 0
|
||||
// 预计算累积高度 O(n),但只需要在列表数据变化时更新
|
||||
props.items.forEach((item, index) => {
|
||||
totalHeight += heights.value.get(item.message?.id?.toString()) || ESTIMATED_ITEM_HEIGHT
|
||||
accumulatedHeights[index] = totalHeight
|
||||
})
|
||||
|
||||
// 二分查找 O(log n)
|
||||
let left = 0
|
||||
let right = accumulatedHeights.length - 1
|
||||
const target = scrollTop - OVERSCAN_SIZE
|
||||
|
||||
while (left <= right) {
|
||||
const mid = Math.floor((left + right) / 2)
|
||||
if (accumulatedHeights[mid] < target) {
|
||||
left = mid + 1
|
||||
} else {
|
||||
right = mid - 1
|
||||
}
|
||||
}
|
||||
|
||||
return Math.max(0, left - BUFFER_SIZE)
|
||||
}
|
||||
|
||||
// 计算指定索引的偏移量
|
||||
const getOffsetForIndex = (index: number) => {
|
||||
let total = 0
|
||||
// 累加到目标索引前的所有项目高度
|
||||
for (let i = 0; i < index; i++) {
|
||||
const itemHeight = heights.value.get(props.items[i].message?.id?.toString()) || ESTIMATED_ITEM_HEIGHT
|
||||
total += itemHeight
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
// 更新可见范围
|
||||
const updateVisibleRange = () => {
|
||||
if (!containerRef.value) return
|
||||
|
||||
const scrollTop = containerRef.value.scrollTop
|
||||
const clientHeight = containerRef.value.clientHeight
|
||||
|
||||
// 计算起始索引
|
||||
const start = getStartIndex(scrollTop)
|
||||
let total = 0
|
||||
let end = start
|
||||
|
||||
// 累加高度直到超过可视区域加上预渲染区域
|
||||
while (total < clientHeight + OVERSCAN_SIZE * 2 && end < props.items.length) {
|
||||
const itemHeight = heights.value.get(props.items[end].message?.id?.toString()) || ESTIMATED_ITEM_HEIGHT
|
||||
total += itemHeight
|
||||
end++
|
||||
}
|
||||
|
||||
// 确保结束索引不超出范围,并添加缓冲区
|
||||
end = Math.min(props.items.length - 1, end + BUFFER_SIZE)
|
||||
|
||||
// 更新可见范围和偏移量
|
||||
visibleRange.value = { start, end }
|
||||
// 加上加载中需要的偏移量
|
||||
offset.value = getOffsetForIndex(start) + LOADING_OFFSET
|
||||
}
|
||||
|
||||
// 更新可见范围的帧动画处理
|
||||
const updateFrame = () => {
|
||||
if (!containerRef.value) return
|
||||
|
||||
const currentScrollTop = containerRef.value.scrollTop
|
||||
|
||||
// 检查滚动位置是否变化
|
||||
if (currentScrollTop !== lastScrollTop.value) {
|
||||
// 发生滚动,重置静止帧计数
|
||||
consecutiveStaticFrames.value = 0
|
||||
// 发出滚动方向变化事件
|
||||
if (currentScrollTop < lastScrollTop.value) {
|
||||
emit('scrollDirectionChange', 'up')
|
||||
} else if (currentScrollTop > lastScrollTop.value) {
|
||||
emit('scrollDirectionChange', 'down')
|
||||
}
|
||||
updateVisibleRange()
|
||||
lastScrollTop.value = currentScrollTop
|
||||
} else {
|
||||
// 滚动位置未变化,增加静止帧计数
|
||||
consecutiveStaticFrames.value++
|
||||
|
||||
// 如果连续3帧未发生滚动,认为滚动已结束
|
||||
if (consecutiveStaticFrames.value >= 3) {
|
||||
// 滚动结束,更新高度并停止动画
|
||||
isScrolling.value = false
|
||||
updateItemHeight()
|
||||
if (rafId.value !== null) {
|
||||
cancelAnimationFrame(rafId.value)
|
||||
rafId.value = null
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 继续下一帧
|
||||
rafId.value = requestAnimationFrame(updateFrame)
|
||||
}
|
||||
|
||||
// 滚动事件处理
|
||||
const handleScroll = (event: Event) => {
|
||||
emit('scroll', event)
|
||||
|
||||
// 标记滚动状态并开始帧动画
|
||||
if (!isScrolling.value) {
|
||||
isScrolling.value = true
|
||||
consecutiveStaticFrames.value = 0
|
||||
if (rafId.value === null) {
|
||||
rafId.value = requestAnimationFrame(updateFrame)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 初始化可见范围
|
||||
updateVisibleRange()
|
||||
|
||||
// 使用 ResizeObserver 监听容器大小变化
|
||||
if (containerRef.value) {
|
||||
resizeObserver.value = new ResizeObserver(() => {
|
||||
updateVisibleRange()
|
||||
nextTick(() => {
|
||||
updateItemHeight()
|
||||
})
|
||||
})
|
||||
resizeObserver.value.observe(containerRef.value)
|
||||
}
|
||||
|
||||
// 初始化高度计算
|
||||
nextTick(() => {
|
||||
updateItemHeight()
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
// 清理动画
|
||||
if (rafId.value !== null) {
|
||||
cancelAnimationFrame(rafId.value)
|
||||
rafId.value = null
|
||||
}
|
||||
|
||||
// 清理 ResizeObserver
|
||||
if (resizeObserver.value) {
|
||||
resizeObserver.value.disconnect()
|
||||
resizeObserver.value = null
|
||||
}
|
||||
|
||||
// 清理缓存
|
||||
heights.value.clear()
|
||||
})
|
||||
|
||||
// 类型定义
|
||||
export type VirtualListExpose = {
|
||||
scrollTo: (options: { index?: number; position?: 'top' | 'bottom'; behavior?: ScrollBehavior }) => void
|
||||
getContainer: () => HTMLElement | null
|
||||
}
|
||||
|
||||
// 暴露方法和引用
|
||||
defineExpose<VirtualListExpose>({
|
||||
scrollTo: (options: { index?: number; position?: 'top' | 'bottom'; behavior?: ScrollBehavior }) => {
|
||||
if (!containerRef.value) return
|
||||
|
||||
const executeScroll = () => {
|
||||
if (!containerRef.value) return
|
||||
|
||||
if (options.position === 'bottom') {
|
||||
// 滚动到底部前确保高度已更新
|
||||
nextTick(() => {
|
||||
updateItemHeight()
|
||||
nextTick(() => {
|
||||
if (containerRef.value) {
|
||||
containerRef.value.scrollTop = totalHeight.value
|
||||
}
|
||||
})
|
||||
})
|
||||
} else if (options.position === 'top') {
|
||||
// 滚动到顶部
|
||||
containerRef.value.scrollTop = 0
|
||||
} else if (typeof options.index === 'number') {
|
||||
// 滚动到指定索引位置
|
||||
const offset = getOffsetForIndex(options.index)
|
||||
containerRef.value.scrollTo({
|
||||
top: offset,
|
||||
behavior: options.behavior || 'auto'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 立即执行一次,并在短暂延迟后再次执行以确保内容已完全加载
|
||||
executeScroll()
|
||||
setTimeout(executeScroll, 100)
|
||||
},
|
||||
getContainer: () => containerRef.value
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.virtual-list-container {
|
||||
position: relative;
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.virtual-list-container::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.virtual-list-container::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(144, 144, 144, 0.3);
|
||||
border-radius: 3px;
|
||||
transition: background-color 0.3s;
|
||||
min-height: 75px;
|
||||
}
|
||||
|
||||
.virtual-list-container::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(144, 144, 144, 0.5);
|
||||
}
|
||||
|
||||
.virtual-list-container::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.virtual-list-phantom {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.virtual-list-content {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
will-change: transform;
|
||||
}
|
||||
</style>
|
||||
@@ -39,10 +39,10 @@
|
||||
</n-flex>
|
||||
|
||||
<!-- 中间聊天内容(使用虚拟列表) -->
|
||||
<VirtualList
|
||||
<MessageList
|
||||
v-else
|
||||
id="image-chat-main"
|
||||
ref="virtualListInst"
|
||||
ref="messageListInst"
|
||||
:items="chatMessageList"
|
||||
:estimatedItemHeight="itemSize"
|
||||
:buffer="5"
|
||||
@@ -50,6 +50,7 @@
|
||||
:isLast="messageOptions?.isLast"
|
||||
@scroll="handleScroll"
|
||||
@scroll-direction-change="handleScrollDirectionChange"
|
||||
@loadMore="handleLoadMore"
|
||||
style="max-height: calc(100vh - 260px)">
|
||||
<template #default="{ item, index }">
|
||||
<n-flex
|
||||
@@ -235,6 +236,7 @@
|
||||
@mouseenter="handleMouseEnter(item.message.id)"
|
||||
@mouseleave="handleMouseLeave"
|
||||
class="w-fit relative flex flex-col"
|
||||
:data-key="item.fromUser.uid === userUid ? `U${item.message.id}` : `Q${item.message.id}`"
|
||||
:class="item.fromUser.uid === userUid ? 'items-end' : 'items-start'"
|
||||
:style="{ '--bubble-max-width': chatStore.isGroup ? '32vw' : '50vw' }"
|
||||
@select="$event.click(item)"
|
||||
@@ -350,7 +352,7 @@
|
||||
</div>
|
||||
</n-flex>
|
||||
</template>
|
||||
</VirtualList>
|
||||
</MessageList>
|
||||
</Transition>
|
||||
|
||||
<!-- 弹出框 -->
|
||||
@@ -379,19 +381,7 @@
|
||||
</div>
|
||||
</n-modal>
|
||||
|
||||
<!-- 悬浮按钮提示(头部悬浮) // TODO 要结合已读未读功能来判断之前的信息有多少没有读,当现在的距离没有到最底部并且又有新消息来未读的时候显示下标的更多信息 (nyh -> 2024-03-07 01:27:22)-->
|
||||
<!-- <header class="float-header" :class="chatStore.isGroup ? 'right-220px' : 'right-50px'">
|
||||
<div class="float-box">
|
||||
<n-flex justify="space-between" align="center">
|
||||
<n-icon :color="'#13987f'">
|
||||
<svg><use href="#double-up"></use></svg>
|
||||
</n-icon>
|
||||
<span class="text-12px">xx条新信息</span>
|
||||
</n-flex>
|
||||
</div>
|
||||
</header> -->
|
||||
|
||||
<!-- 悬浮按钮提示(底部悬浮) -->
|
||||
<!-- 悬浮按钮提示(底部悬浮) -->
|
||||
<footer
|
||||
class="float-footer"
|
||||
v-if="floatFooter && currentNewMsgCount?.count && currentNewMsgCount.count > 0"
|
||||
@@ -423,7 +413,7 @@ import { type } from '@tauri-apps/plugin-os'
|
||||
import { useUserStore } from '@/stores/user.ts'
|
||||
import { useNetwork } from '@vueuse/core'
|
||||
import { AvatarUtils } from '@/utils/AvatarUtils'
|
||||
import VirtualList, { type VirtualListExpose } from '@/components/common/VirtualList.vue'
|
||||
import MessageList, { type MessageListExpose } from '@/components/common/MessageList.vue'
|
||||
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
|
||||
import { useTauriListener } from '@/hooks/useTauriListener'
|
||||
import { useGroupStore } from '@/stores/group.ts'
|
||||
@@ -462,7 +452,7 @@ const activeReply = ref('')
|
||||
/** item最小高度,用于计算滚动大小和位置 */
|
||||
const itemSize = computed(() => (chatStore.isGroup ? 90 : 76))
|
||||
/** 虚拟列表 */
|
||||
const virtualListInst = useTemplateRef<VirtualListExpose>('virtualListInst')
|
||||
const messageListInst = useTemplateRef<MessageListExpose>('messageListInst')
|
||||
/** 手动触发Popover显示 */
|
||||
const infoPopover = ref(false)
|
||||
// 记录 requestAnimationFrame 的返回值
|
||||
@@ -499,8 +489,8 @@ provide('popoverControls', { enableScroll })
|
||||
|
||||
// 添加防抖处理
|
||||
const debouncedScrollToBottom = useDebounceFn(() => {
|
||||
if (!virtualListInst.value) return
|
||||
virtualListInst.value?.scrollTo({ position: 'bottom', behavior: 'instant' })
|
||||
if (!messageListInst.value) return
|
||||
messageListInst.value?.scrollTo({ position: 'bottom', behavior: 'instant' })
|
||||
}, 100)
|
||||
|
||||
// 监听会话切换
|
||||
@@ -531,7 +521,7 @@ watch(
|
||||
if (isLoadingMore.value) {
|
||||
if (scrollTop.value < 26) {
|
||||
requestAnimationFrame(() => {
|
||||
virtualListInst.value?.scrollTo({ index: value.length - oldValue.length })
|
||||
messageListInst.value?.scrollTo({ index: value.length - oldValue.length })
|
||||
})
|
||||
}
|
||||
return
|
||||
@@ -539,16 +529,16 @@ watch(
|
||||
|
||||
// 优先级1:用户发送的消息,始终滚动到底部
|
||||
if (latestMessage?.fromUser?.uid === userUid.value) {
|
||||
virtualListInst.value?.scrollTo({ position: 'bottom', behavior: 'instant' })
|
||||
messageListInst.value?.scrollTo({ position: 'bottom', behavior: 'instant' })
|
||||
return
|
||||
}
|
||||
|
||||
// 优先级2:已经在底部时的新消息
|
||||
const container = virtualListInst.value?.getContainer()
|
||||
const container = messageListInst.value?.getContainer()
|
||||
if (container) {
|
||||
const distanceFromBottom = container.scrollHeight - container.scrollTop - container.clientHeight
|
||||
if (distanceFromBottom <= 300) {
|
||||
virtualListInst.value?.scrollTo({ position: 'bottom', behavior: 'smooth' })
|
||||
messageListInst.value?.scrollTo({ position: 'bottom', behavior: 'instant' })
|
||||
return
|
||||
}
|
||||
|
||||
@@ -578,7 +568,7 @@ const handleScrollDirectionChange = (direction: 'up' | 'down') => {
|
||||
/** 处理滚动事件(用于页脚显示功能) */
|
||||
const handleScroll = () => {
|
||||
if (isAutoScrolling.value) return // 如果是自动滚动,不处理
|
||||
const container = virtualListInst.value?.getContainer()
|
||||
const container = messageListInst.value?.getContainer()
|
||||
if (!container) return
|
||||
|
||||
// 获取已滚动的距离
|
||||
@@ -634,7 +624,7 @@ const handleScroll = () => {
|
||||
const handleTransitionComplete = () => {
|
||||
if (!messageOptions.value?.isLoading) {
|
||||
nextTick(() => {
|
||||
virtualListInst.value?.scrollTo({ position: 'bottom', behavior: 'instant' })
|
||||
messageListInst.value?.scrollTo({ position: 'bottom', behavior: 'instant' })
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -699,7 +689,7 @@ const jumpToReplyMsg = (key: string) => {
|
||||
// 找到对应消息的索引
|
||||
const messageIndex = chatMessageList.value.findIndex((msg) => msg.message.id === key)
|
||||
if (messageIndex !== -1) {
|
||||
virtualListInst.value?.scrollTo({ index: messageIndex, behavior: 'instant' })
|
||||
messageListInst.value?.scrollTo({ index: messageIndex, behavior: 'instant' })
|
||||
activeReply.value = key
|
||||
}
|
||||
})
|
||||
@@ -710,35 +700,31 @@ const jumpToReplyMsg = (key: string) => {
|
||||
* @param index 下标
|
||||
* @param id 用户ID
|
||||
*/
|
||||
// const addToDomUpdateQueue = (index: number, id: number) => {
|
||||
// // 使用 nextTick 确保虚拟列表渲染完最新的项目后进行滚动
|
||||
// nextTick(() => {
|
||||
// if (!floatFooter.value || id === userUid.value) {
|
||||
// virtualListInst.value?.scrollTo({ position: 'bottom', behavior: 'auto' })
|
||||
// }
|
||||
// /** data-key标识的气泡,添加前缀用于区分用户消息,不然气泡动画会被覆盖 */
|
||||
// const dataKey = id === userUid.value ? `U${index}` : `Q${index}`
|
||||
// const lastMessageElement = document.querySelector(`[data-key="${dataKey}"]`) as HTMLElement
|
||||
// if (lastMessageElement) {
|
||||
// // 添加动画类
|
||||
// lastMessageElement.classList.add('bubble-animation')
|
||||
// // 监听动画结束事件
|
||||
// const handleAnimationEnd = () => {
|
||||
// lastMessageElement.classList.remove('bubble-animation')
|
||||
// lastMessageElement.removeEventListener('animationend', handleAnimationEnd)
|
||||
// }
|
||||
// lastMessageElement.addEventListener('animationend', handleAnimationEnd)
|
||||
// }
|
||||
// })
|
||||
// chatStore.clearNewMsgCount()
|
||||
// }
|
||||
const addToDomUpdateQueue = (index: string, id: string) => {
|
||||
// 使用 nextTick 确保虚拟列表渲染完最新的项目后进行滚动
|
||||
nextTick(() => {
|
||||
/** data-key标识的气泡,添加前缀用于区分用户消息,不然气泡动画会被覆盖 */
|
||||
const dataKey = id === userUid.value ? `U${index}` : `Q${index}`
|
||||
const lastMessageElement = document.querySelector(`[data-key="${dataKey}"]`) as HTMLElement
|
||||
if (lastMessageElement) {
|
||||
// 添加动画类
|
||||
lastMessageElement.classList.add('bubble-animation')
|
||||
// 监听动画结束事件
|
||||
const handleAnimationEnd = () => {
|
||||
lastMessageElement.classList.remove('bubble-animation')
|
||||
lastMessageElement.removeEventListener('animationend', handleAnimationEnd)
|
||||
}
|
||||
lastMessageElement.addEventListener('animationend', handleAnimationEnd)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** 点击后滚动到底部 */
|
||||
const scrollBottom = () => {
|
||||
if (!virtualListInst.value) return
|
||||
if (!messageListInst.value) return
|
||||
|
||||
nextTick(() => {
|
||||
virtualListInst.value?.scrollTo({ position: 'bottom', behavior: 'instant' })
|
||||
messageListInst.value?.scrollTo({ position: 'bottom', behavior: 'instant' })
|
||||
})
|
||||
}
|
||||
|
||||
@@ -799,15 +785,43 @@ const handleReEdit = (msgId: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
requestAnimationFrame(() => {
|
||||
virtualListInst.value?.scrollTo({ position: 'bottom', behavior: 'instant' })
|
||||
// 处理加载更多
|
||||
const handleLoadMore = async () => {
|
||||
// 如果正在加载或已经触发了加载,则不重复触发
|
||||
if (messageOptions.value?.isLoading || isLoadingMore.value) return
|
||||
|
||||
// 记录当前的内容高度
|
||||
const container = messageListInst.value?.getContainer()
|
||||
if (!container) return
|
||||
const oldScrollHeight = container.scrollHeight
|
||||
|
||||
isLoadingMore.value = true
|
||||
// 禁用滚动交互但保持滚动条显示
|
||||
container.style.pointerEvents = 'none'
|
||||
await new Promise((resolve) => setTimeout(resolve, 300))
|
||||
await chatStore.loadMore()
|
||||
|
||||
// 加载完成后,计算新增内容的高度差,并设置滚动位置
|
||||
nextTick(() => {
|
||||
const newScrollHeight = container.scrollHeight
|
||||
const heightDiff = newScrollHeight - oldScrollHeight
|
||||
if (heightDiff > 0) {
|
||||
container.scrollTop = heightDiff
|
||||
}
|
||||
// 恢复滚动交互
|
||||
nextTick(() => {
|
||||
container.style.pointerEvents = 'auto'
|
||||
isLoadingMore.value = false
|
||||
})
|
||||
})
|
||||
useMitt.on(MittEnum.SEND_MESSAGE, async (messageType: MessageType) => {
|
||||
await chatStore.pushMsg(messageType)
|
||||
// nextTick(() => {
|
||||
// addToDomUpdateQueue(event.message.id, event.fromUser.uid)
|
||||
// })
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
nextTick(() => {
|
||||
messageListInst.value?.scrollTo({ position: 'bottom', behavior: 'instant' })
|
||||
})
|
||||
useMitt.on(MittEnum.MESSAGE_ANIMATION, async (messageType: MessageType) => {
|
||||
addToDomUpdateQueue(messageType.message.id, messageType.fromUser.uid)
|
||||
})
|
||||
useMitt.on(`${MittEnum.INFO_POPOVER}-Main`, (event: any) => {
|
||||
selectKey.value = event.uid
|
||||
|
||||
+3
-3
@@ -58,8 +58,6 @@ export enum MittEnum {
|
||||
UPDATE_MSG_TOTAL = 'updateMsgTotal',
|
||||
/** 显示消息框 */
|
||||
MSG_BOX_SHOW = 'msgBoxShow',
|
||||
/** 发送消息 */
|
||||
SEND_MESSAGE = 'sendMessage',
|
||||
/** 跳到发送信息 */
|
||||
TO_SEND_MSG = 'toSendMsg',
|
||||
/** 缩小窗口 */
|
||||
@@ -89,7 +87,9 @@ export enum MittEnum {
|
||||
/** 隐藏会话 */
|
||||
HIDE_SESSION = 'hideSession',
|
||||
/** 定位会话 */
|
||||
LOCATE_SESSION = 'locateSession'
|
||||
LOCATE_SESSION = 'locateSession',
|
||||
/** 消息动画 */
|
||||
MESSAGE_ANIMATION = 'messageAnimation'
|
||||
}
|
||||
|
||||
/** 主题类型 */
|
||||
|
||||
@@ -301,6 +301,7 @@ export const useMsgInput = (messageInputDom: Ref) => {
|
||||
|
||||
// 先添加到消息列表 - 此时会显示本地预览
|
||||
chatStore.pushMsg(tempMsg)
|
||||
useMitt.emit(MittEnum.MESSAGE_ANIMATION, tempMsg)
|
||||
console.log('👾临时消息:', tempMsg)
|
||||
|
||||
// 设置发送状态的定时器
|
||||
@@ -354,22 +355,6 @@ export const useMsgInput = (messageInputDom: Ref) => {
|
||||
// 更新会话最后活动时间
|
||||
chatStore.updateSessionLastActiveTime(globalStore.currentSession.roomId)
|
||||
|
||||
// // 保存到数据库
|
||||
// await db.value?.execute(
|
||||
// 'INSERT INTO message (room_id, from_uid, content, reply_msg_id, status, gap_count, type, create_time, update_time) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)',
|
||||
// [
|
||||
// globalStore.currentSession.roomId,
|
||||
// userUid.value,
|
||||
// msg.content,
|
||||
// msg.reply,
|
||||
// 0,
|
||||
// 0,
|
||||
// msg.type,
|
||||
// new Date().getTime(),
|
||||
// new Date().getTime()
|
||||
// ]
|
||||
// )
|
||||
|
||||
// 消息发送成功后释放预览URL
|
||||
if (msg.type === MsgEnum.IMAGE && msg.url.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(msg.url)
|
||||
|
||||
@@ -181,6 +181,9 @@ useMitt.on(WsResponseMessageType.MSG_RECALL, (data: RevokedMsgType) => {
|
||||
})
|
||||
useMitt.on(WsResponseMessageType.RECEIVE_MESSAGE, async (data: MessageType) => {
|
||||
chatStore.pushMsg(data)
|
||||
if (data.fromUser.uid !== userStore.userInfo.uid) {
|
||||
useMitt.emit(MittEnum.MESSAGE_ANIMATION, data)
|
||||
}
|
||||
// 接收到通知就设置图标闪烁
|
||||
const username = useUserInfo(data.fromUser.uid).value.name!
|
||||
// 不是自己发的消息才通知
|
||||
|
||||
@@ -1,192 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { nextTick } from 'vue'
|
||||
import VirtualList from '../components/common/VirtualList.vue'
|
||||
|
||||
// 模拟数据生成函数
|
||||
const generateMockItems = (count: number) => {
|
||||
return Array.from({ length: count }, (_, index) => ({
|
||||
message: {
|
||||
id: index + 1,
|
||||
body: { content: `Message ${index + 1}` }
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
// 性能测试辅助函数
|
||||
const measurePerformance = async (fn: () => Promise<void> | void) => {
|
||||
const start = performance.now()
|
||||
await fn()
|
||||
return performance.now() - start
|
||||
}
|
||||
|
||||
// 内存使用测试辅助函数
|
||||
interface MemoryInfo {
|
||||
jsHeapSizeLimit: number
|
||||
totalJSHeapSize: number
|
||||
usedJSHeapSize: number
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Performance {
|
||||
memory?: MemoryInfo
|
||||
}
|
||||
}
|
||||
|
||||
describe('VirtualList Component', () => {
|
||||
// 测试基本渲染
|
||||
it('should render with default props', () => {
|
||||
const wrapper = mount(VirtualList, {
|
||||
props: {
|
||||
items: [],
|
||||
estimatedItemHeight: 80,
|
||||
buffer: 5
|
||||
}
|
||||
})
|
||||
expect(wrapper.exists()).toBe(true)
|
||||
expect(wrapper.find('.virtual-list-container').exists()).toBe(true)
|
||||
})
|
||||
|
||||
// 测试高度计算
|
||||
it('should calculate total height correctly', async () => {
|
||||
const items = generateMockItems(10)
|
||||
const estimatedItemHeight = 80
|
||||
const wrapper = mount(VirtualList, {
|
||||
props: {
|
||||
items,
|
||||
estimatedItemHeight,
|
||||
buffer: 5
|
||||
}
|
||||
})
|
||||
|
||||
await nextTick()
|
||||
const phantom = wrapper.find('.virtual-list-phantom')
|
||||
expect(phantom.attributes('style')).toContain(`height: ${items.length * estimatedItemHeight}px`)
|
||||
})
|
||||
|
||||
// 测试可见项目计算
|
||||
it('should calculate visible items correctly', async () => {
|
||||
const items = generateMockItems(100)
|
||||
const wrapper = mount(VirtualList, {
|
||||
props: {
|
||||
items,
|
||||
estimatedItemHeight: 80,
|
||||
buffer: 5
|
||||
}
|
||||
})
|
||||
|
||||
await nextTick()
|
||||
// 检查初始渲染的项目数量是否合理(考虑到缓冲区)
|
||||
const renderedItems = wrapper.findAll('[id^="item-"]')
|
||||
expect(renderedItems.length).toBeGreaterThan(0)
|
||||
expect(renderedItems.length).toBeLessThan(items.length)
|
||||
})
|
||||
|
||||
// 测试滚动事件
|
||||
it('should emit scroll events', async () => {
|
||||
const wrapper = mount(VirtualList, {
|
||||
props: {
|
||||
items: generateMockItems(100),
|
||||
estimatedItemHeight: 80,
|
||||
buffer: 5
|
||||
}
|
||||
})
|
||||
|
||||
const container = wrapper.find('.virtual-list-container')
|
||||
await container.trigger('scroll')
|
||||
expect(wrapper.emitted('scroll')).toBeTruthy()
|
||||
})
|
||||
|
||||
// 测试高度缓存清理
|
||||
it('should cleanup height cache when exceeding limit', async () => {
|
||||
const items = generateMockItems(2000) // 创建足够多的项目以触发清理
|
||||
const wrapper = mount(VirtualList, {
|
||||
props: {
|
||||
items,
|
||||
estimatedItemHeight: 80,
|
||||
buffer: 5
|
||||
}
|
||||
})
|
||||
|
||||
await nextTick()
|
||||
// 模拟滚动以触发高度缓存
|
||||
const container = wrapper.get('.virtual-list-container')
|
||||
Object.defineProperty(container.element, 'scrollTop', { value: 1000 })
|
||||
await container.trigger('scroll')
|
||||
|
||||
// 等待清理完成
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
// 验证缓存大小是否在合理范围内
|
||||
const vm = wrapper.vm as any
|
||||
expect(vm.heights.size).toBeLessThanOrEqual(1000)
|
||||
})
|
||||
|
||||
// 测试 scrollTo 方法
|
||||
it('should scroll to specified position', async () => {
|
||||
const wrapper = mount(VirtualList, {
|
||||
props: {
|
||||
items: generateMockItems(100),
|
||||
estimatedItemHeight: 80,
|
||||
buffer: 5
|
||||
}
|
||||
})
|
||||
|
||||
const vm = wrapper.vm as any
|
||||
|
||||
// 测试滚动到底部
|
||||
await vm.scrollTo({ position: 'bottom' })
|
||||
await nextTick()
|
||||
|
||||
// 测试滚动到顶部
|
||||
await vm.scrollTo({ position: 'top' })
|
||||
await nextTick()
|
||||
|
||||
// 测试滚动到指定索引
|
||||
await vm.scrollTo({ index: 50 })
|
||||
await nextTick()
|
||||
})
|
||||
|
||||
describe('Performance Benchmarks', () => {
|
||||
// 测试大数据量渲染性能
|
||||
it('should render large datasets efficiently', async () => {
|
||||
const itemCount = 10000
|
||||
const items = generateMockItems(itemCount)
|
||||
|
||||
const renderTime = await measurePerformance(async () => {
|
||||
mount(VirtualList, {
|
||||
props: {
|
||||
items,
|
||||
estimatedItemHeight: 80,
|
||||
buffer: 5
|
||||
}
|
||||
})
|
||||
await nextTick()
|
||||
})
|
||||
|
||||
// 验证渲染时间是否在可接受范围内(例如小于1秒)
|
||||
expect(renderTime).toBeLessThan(1000)
|
||||
})
|
||||
|
||||
// 测试大量数据更新性能
|
||||
it('should handle large data updates efficiently', async () => {
|
||||
const wrapper = mount(VirtualList, {
|
||||
props: {
|
||||
items: generateMockItems(1000),
|
||||
estimatedItemHeight: 80,
|
||||
buffer: 5
|
||||
}
|
||||
})
|
||||
|
||||
const updateTime = await measurePerformance(async () => {
|
||||
await wrapper.setProps({
|
||||
items: generateMockItems(2000)
|
||||
})
|
||||
await nextTick()
|
||||
})
|
||||
|
||||
// 验证更新时间是否在可接受范围内(例如小于500ms)
|
||||
expect(updateTime).toBeLessThan(500)
|
||||
})
|
||||
})
|
||||
})
|
||||
Vendored
+1
@@ -22,6 +22,7 @@ declare module 'vue' {
|
||||
Image: typeof import('./../components/rightBox/renderMessage/Image.vue')['default']
|
||||
InfoPopover: typeof import('./../components/common/InfoPopover.vue')['default']
|
||||
LoadingSpinner: typeof import('./../components/common/LoadingSpinner.vue')['default']
|
||||
MessageList: typeof import('./../components/common/MessageList.vue')['default']
|
||||
MsgInput: typeof import('./../components/rightBox/MsgInput.vue')['default']
|
||||
NaiveProvider: typeof import('./../components/common/NaiveProvider.vue')['default']
|
||||
NAlert: typeof import('naive-ui')['NAlert']
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
:loading="loading"
|
||||
:disabled="loginDisabled"
|
||||
class="w-full mt-8px mb-50px"
|
||||
@click="normalLogin()"
|
||||
@click="debouncedLogin()"
|
||||
color="#13987f">
|
||||
<span>{{ loginText }}</span>
|
||||
</n-button>
|
||||
@@ -120,7 +120,7 @@
|
||||
:loading="loading"
|
||||
:disabled="loginDisabled"
|
||||
class="w-200px mt-12px mb-40px"
|
||||
@click="normalLogin(true)"
|
||||
@click="debouncedLogin(true)"
|
||||
color="#13987f">
|
||||
{{ loginText }}
|
||||
</n-button>
|
||||
@@ -168,7 +168,7 @@ import { useSettingStore } from '@/stores/setting.ts'
|
||||
import { AvatarUtils } from '@/utils/AvatarUtils'
|
||||
import { useMitt } from '@/hooks/useMitt'
|
||||
import { WsResponseMessageType } from '@/services/wsType'
|
||||
import { useNetwork } from '@vueuse/core'
|
||||
import { useNetwork, useDebounceFn } from '@vueuse/core'
|
||||
import { useUserStatusStore } from '@/stores/userStatus'
|
||||
import { clearListener } from '@/utils/ReadCountQueue'
|
||||
import { useGlobalStore } from '@/stores/global'
|
||||
@@ -196,22 +196,27 @@ const info = ref({
|
||||
})
|
||||
/** 协议 */
|
||||
const protocol = ref(true)
|
||||
const loginDisabled = ref(false)
|
||||
const loginDisabled = ref(!isOnline.value)
|
||||
const loading = ref(false)
|
||||
const arrowStatus = ref(false)
|
||||
const moreShow = ref(false)
|
||||
const isAutoLogin = ref(false)
|
||||
const isAutoLogin = ref(login.value.autoLogin && TOKEN.value && REFRESH_TOKEN.value)
|
||||
const { setLoginState } = useLogin()
|
||||
const accountPH = ref('输入HuLa账号')
|
||||
const passwordPH = ref('输入HuLa密码')
|
||||
/** 登录按钮的文本内容 */
|
||||
const loginText = ref('登录')
|
||||
const loginText = ref(isOnline.value ? (isAutoLogin.value ? '登录' : '登录') : '网络异常')
|
||||
/** 是否直接跳转 */
|
||||
const isJumpDirectly = ref(false)
|
||||
const { createWebviewWindow } = useWindow()
|
||||
|
||||
watchEffect(() => {
|
||||
loginDisabled.value = !(info.value.account && info.value.password && protocol.value)
|
||||
loginDisabled.value = !(info.value.account && info.value.password && protocol.value && isOnline.value)
|
||||
})
|
||||
|
||||
watch(isOnline, (v) => {
|
||||
loginDisabled.value = !v
|
||||
loginText.value = v ? (isAutoLogin.value ? '登录' : '登录') : '网络异常'
|
||||
})
|
||||
|
||||
// 监听账号输入
|
||||
@@ -233,13 +238,6 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
watch(isOnline, (v) => {
|
||||
if (v) {
|
||||
loginDisabled.value = false
|
||||
loginText.value = '登录'
|
||||
}
|
||||
})
|
||||
|
||||
/** 删除账号列表内容 */
|
||||
const delAccount = (item: UserInfoType) => {
|
||||
// 获取删除前账户列表的长度
|
||||
@@ -271,14 +269,14 @@ const giveAccount = (item: UserInfoType) => {
|
||||
/**登录后创建主页窗口*/
|
||||
const normalLogin = async (auto = false) => {
|
||||
loading.value = true
|
||||
loginText.value = '登录中...'
|
||||
loginDisabled.value = true
|
||||
// 根据auto参数决定从哪里获取登录信息
|
||||
const loginInfo = auto ? (userStore.userInfo as UserInfoType) : info.value
|
||||
const { account } = loginInfo
|
||||
|
||||
// 自动登录
|
||||
if (auto) {
|
||||
isAutoLogin.value = true
|
||||
loginText.value = '登录中...'
|
||||
// 添加2秒延迟
|
||||
await new Promise((resolve) => setTimeout(resolve, 1200))
|
||||
|
||||
@@ -298,10 +296,20 @@ const normalLogin = async (auto = false) => {
|
||||
await openHomeWindow()
|
||||
loading.value = false
|
||||
} catch (error) {
|
||||
console.error('自动登录失败')
|
||||
localStorage.removeItem('TOKEN')
|
||||
isAutoLogin.value = false
|
||||
loginDisabled.value = true
|
||||
console.error('自动登录失败', error)
|
||||
// 如果是网络异常,不删除token
|
||||
if (!isOnline.value) {
|
||||
loginDisabled.value = true
|
||||
loginText.value = '网络异常'
|
||||
loading.value = false
|
||||
} else {
|
||||
// 其他错误才清除token并重置状态
|
||||
localStorage.removeItem('TOKEN')
|
||||
isAutoLogin.value = false
|
||||
loginDisabled.value = true
|
||||
loginText.value = '登录'
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -349,6 +357,8 @@ const normalLogin = async (auto = false) => {
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false
|
||||
loginDisabled.value = false
|
||||
loginText.value = '登录'
|
||||
// 如果是自动登录失败,重置按钮状态允许手动登录
|
||||
if (auto) {
|
||||
loginDisabled.value = false
|
||||
@@ -357,6 +367,8 @@ const normalLogin = async (auto = false) => {
|
||||
})
|
||||
}
|
||||
|
||||
const debouncedLogin = useDebounceFn(normalLogin, 500)
|
||||
|
||||
const openHomeWindow = async () => {
|
||||
await createWebviewWindow('HuLa', 'home', 960, 720, 'login', true)
|
||||
}
|
||||
@@ -380,7 +392,7 @@ const closeMenu = (event: MouseEvent) => {
|
||||
|
||||
const enterKey = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && !loginDisabled.value) {
|
||||
normalLogin()
|
||||
debouncedLogin()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -424,9 +436,9 @@ onMounted(async () => {
|
||||
loginText.value = '服务异常断开'
|
||||
})
|
||||
|
||||
// 自动登录
|
||||
if (login.value.autoLogin && TOKEN.value && REFRESH_TOKEN.value) {
|
||||
normalLogin(true)
|
||||
// 自动登录时直接触发登录
|
||||
if (isAutoLogin.value) {
|
||||
debouncedLogin(true)
|
||||
} else {
|
||||
loginHistories.length > 0 && giveAccount(loginHistories[0])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user