重构VirtualContactList组件:统一项目高度以实现一致的布局,并通过重构代码格式来提高可读性。此更改通过确保所有项目类型共享相同的高度来增强虚拟滚动期间的视觉稳定性。

This commit is contained in:
乘风
2025-12-17 16:39:47 +08:00
parent 65167be7f8
commit 72cf3fd81c

View File

@@ -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<VirtualContactListProps> = ({
});
// 如果分组展开添加联系人项或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<VirtualContactListProps> = ({
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<VirtualContactListProps> = ({
// 检查是否需要加载更多
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<VirtualContactListProps> = ({
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<VirtualContactListProps> = ({
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) {