diff --git a/Touchkebao/src/components/VirtualContactList/index.tsx b/Touchkebao/src/components/VirtualContactList/index.tsx index 9a7d60e1..ce320406 100644 --- a/Touchkebao/src/components/VirtualContactList/index.tsx +++ b/Touchkebao/src/components/VirtualContactList/index.tsx @@ -9,32 +9,27 @@ * - 支持分组内分页加载 */ -import React, { useMemo, useCallback, useRef, useState, useEffect } from "react"; +import React, { + useMemo, + useCallback, + useRef, + useState, + useEffect, +} from "react"; import { VariableSizeList, ListChildComponentProps } from "react-window"; import { Spin, Button } from "antd"; import { Contact } from "@/utils/db"; -import { ContactGroup, GroupContactData } from "@/store/module/weChat/contacts.data"; +import { + ContactGroup, + GroupContactData, +} from "@/store/module/weChat/contacts.data"; import styles from "./index.module.scss"; /** - * 分组头部高度(固定) + * 统一的列表行高(分组 / 好友 / 群聊 / 加载中 / 加载更多 都使用同一高度) + * 由视觉统一规范,避免高度不一致导致的视觉错位和虚拟滚动跳动。 */ -const GROUP_HEADER_HEIGHT = 40; - -/** - * 联系人项高度(固定),由设计统一控制,避免高度动态变化造成折叠/抖动 - */ -const CONTACT_ITEM_HEIGHT = 60; - -/** - * Loading项高度(固定) - */ -const LOADING_ITEM_HEIGHT = 60; - -/** - * 加载更多按钮高度(固定) - */ -const LOAD_MORE_ITEM_HEIGHT = 50; +const ROW_HEIGHT = 60; /** * 可见区域缓冲项数 @@ -48,7 +43,13 @@ type VirtualItem = | { type: "group"; data: ContactGroup; index: number } | { type: "loading"; groupIndex: number; groupKey: string } | { type: "contact"; data: Contact; groupIndex: number; contactIndex: number } - | { type: "loadMore"; groupIndex: number; groupId: number; groupType: 1 | 2; groupKey: string }; + | { + type: "loadMore"; + groupIndex: number; + groupId: number; + groupType: 1 | 2; + groupKey: string; + }; /** * 虚拟滚动联系人列表Props @@ -69,9 +70,16 @@ export interface VirtualContactListProps { /** 当前选中的联系人ID */ selectedContactId?: number; /** 渲染分组头部 */ - renderGroupHeader: (group: ContactGroup, isExpanded: boolean) => React.ReactNode; + renderGroupHeader: ( + group: ContactGroup, + isExpanded: boolean, + ) => React.ReactNode; /** 渲染联系人项 */ - renderContact: (contact: Contact, groupIndex: number, contactIndex: number) => React.ReactNode; + renderContact: ( + contact: Contact, + groupIndex: number, + contactIndex: number, + ) => React.ReactNode; /** 点击分组头部(展开/折叠) */ onGroupToggle?: (groupId: number, groupType: 1 | 2) => void; /** 点击联系人项 */ @@ -129,18 +137,28 @@ export const VirtualContactList: React.FC = ({ }); // 如果分组展开,添加联系人项或loading项 - const groupKey = getGroupKey(group.id, group.groupType, selectedAccountId); + const groupKey = getGroupKey( + group.id, + group.groupType, + selectedAccountId, + ); if (expandedGroups.has(groupKey)) { const groupDataItem = groupData.get(groupKey); if (groupDataItem) { // 如果正在加载,显示loading项 - if (groupDataItem.loading && (!groupDataItem.loaded || groupDataItem.contacts.length === 0)) { + if ( + groupDataItem.loading && + (!groupDataItem.loaded || groupDataItem.contacts.length === 0) + ) { items.push({ type: "loading", groupIndex, groupKey, }); - } else if (groupDataItem.loaded && groupDataItem.contacts.length > 0) { + } else if ( + groupDataItem.loaded && + groupDataItem.contacts.length > 0 + ) { // 如果已加载,显示联系人项 groupDataItem.contacts.forEach((contact, contactIndex) => { items.push({ @@ -179,18 +197,10 @@ export const VirtualContactList: React.FC = ({ const getItemSize = useCallback( (index: number): number => { const item = virtualItems[index]; - if (!item) return CONTACT_ITEM_HEIGHT; + if (!item) return ROW_HEIGHT; - if (item.type === "group") { - return GROUP_HEADER_HEIGHT; - } else if (item.type === "loading") { - return LOADING_ITEM_HEIGHT; - } else if (item.type === "loadMore") { - return LOAD_MORE_ITEM_HEIGHT; - } else { - // 联系人项使用固定高度,避免动态计算造成的样式折叠 - return CONTACT_ITEM_HEIGHT; - } + // 所有类型统一使用固定高度,避免高度差异导致的布局偏移 + return ROW_HEIGHT; }, [virtualItems], ); @@ -224,14 +234,19 @@ export const VirtualContactList: React.FC = ({ // 检查是否需要加载更多 if (onGroupLoadMore && listRef.current) { const currentTotalHeight = totalHeight; - const distanceToBottom = currentTotalHeight - scrollTop - containerHeight; + const distanceToBottom = + currentTotalHeight - scrollTop - containerHeight; if (distanceToBottom < loadMoreThreshold) { // 找到最后一个可见的分组,触发加载更多 // 简化处理:找到最后一个展开的分组 for (let i = groups.length - 1; i >= 0; i--) { const group = groups[i]; - const groupKey = getGroupKey(group.id, group.groupType, selectedAccountId); + const groupKey = getGroupKey( + group.id, + group.groupType, + selectedAccountId, + ); if (expandedGroups.has(groupKey)) { onGroupLoadMore(group.id, group.groupType); break; @@ -261,7 +276,11 @@ export const VirtualContactList: React.FC = ({ if (item.type === "group") { const group = item.data; - const groupKey = getGroupKey(group.id, group.groupType, selectedAccountId); + const groupKey = getGroupKey( + group.id, + group.groupType, + selectedAccountId, + ); const isExpanded = expandedGroups.has(groupKey); return ( @@ -369,7 +388,11 @@ export const VirtualContactList: React.FC = ({ const prevItemsLength = prevItemsLengthForScrollRef.current; // 只在添加新项时恢复滚动位置(加载更多场景) - if (listRef.current && scrollOffsetRef.current > 0 && currentItemsLength > prevItemsLength) { + if ( + listRef.current && + scrollOffsetRef.current > 0 && + currentItemsLength > prevItemsLength + ) { // 使用 requestAnimationFrame 确保在渲染后恢复滚动位置 requestAnimationFrame(() => { if (listRef.current && scrollOffsetRef.current > 0) {