From 72cf3fd81cf2332d0af5cb7f0fb051214f1b7ee0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B9=98=E9=A3=8E?= Date: Wed, 17 Dec 2025 16:39:47 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84VirtualContactList=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=EF=BC=9A=E7=BB=9F=E4=B8=80=E9=A1=B9=E7=9B=AE=E9=AB=98?= =?UTF-8?q?=E5=BA=A6=E4=BB=A5=E5=AE=9E=E7=8E=B0=E4=B8=80=E8=87=B4=E7=9A=84?= =?UTF-8?q?=E5=B8=83=E5=B1=80=EF=BC=8C=E5=B9=B6=E9=80=9A=E8=BF=87=E9=87=8D?= =?UTF-8?q?=E6=9E=84=E4=BB=A3=E7=A0=81=E6=A0=BC=E5=BC=8F=E6=9D=A5=E6=8F=90?= =?UTF-8?q?=E9=AB=98=E5=8F=AF=E8=AF=BB=E6=80=A7=E3=80=82=E6=AD=A4=E6=9B=B4?= =?UTF-8?q?=E6=94=B9=E9=80=9A=E8=BF=87=E7=A1=AE=E4=BF=9D=E6=89=80=E6=9C=89?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E7=B1=BB=E5=9E=8B=E5=85=B1=E4=BA=AB=E7=9B=B8?= =?UTF-8?q?=E5=90=8C=E7=9A=84=E9=AB=98=E5=BA=A6=E6=9D=A5=E5=A2=9E=E5=BC=BA?= =?UTF-8?q?=E8=99=9A=E6=8B=9F=E6=BB=9A=E5=8A=A8=E6=9C=9F=E9=97=B4=E7=9A=84?= =?UTF-8?q?=E8=A7=86=E8=A7=89=E7=A8=B3=E5=AE=9A=E6=80=A7=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/VirtualContactList/index.tsx | 103 +++++++++++------- 1 file changed, 63 insertions(+), 40 deletions(-) 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) {