feat(WechatFriends): 实现联系人列表分页加载功能
添加react-window依赖并实现分组联系人列表的分页加载功能,优化大数据量下的性能表现 - 每组初始加载20条数据,滚动到底部可点击加载更多 - 添加加载状态提示和"没有更多了"的结束提示 - 优化样式添加加载更多按钮的容器样式
This commit is contained in:
@@ -44,8 +44,21 @@
|
||||
}
|
||||
|
||||
.groupPanel {
|
||||
background-color: transparent;
|
||||
}
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.loadMoreContainer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.noMoreText {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.list {
|
||||
flex: 1;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState } from "react";
|
||||
import { List, Avatar, Collapse } from "antd";
|
||||
import React, { useState, useCallback, useEffect } from "react";
|
||||
import { List, Avatar, Collapse, Button } from "antd";
|
||||
import { ContractData } from "@/pages/pc/ckbox/data";
|
||||
import styles from "./WechatFriends.module.scss";
|
||||
import { useCkChatStore } from "@/store/module/ckchat";
|
||||
@@ -20,6 +20,14 @@ const ContactListSimple: React.FC<WechatFriendsProps> = ({
|
||||
const newContractList = useCkChatStore(state => state.newContractList);
|
||||
const [activeKey, setActiveKey] = useState<string[]>(["0"]); // 默认展开第一个分组
|
||||
|
||||
// 分页加载相关状态
|
||||
const [visibleContacts, setVisibleContacts] = useState<{
|
||||
[key: string]: ContractData[];
|
||||
}>({});
|
||||
const [loading, setLoading] = useState<{ [key: string]: boolean }>({});
|
||||
const [hasMore, setHasMore] = useState<{ [key: string]: boolean }>({});
|
||||
const [page, setPage] = useState<{ [key: string]: number }>({});
|
||||
|
||||
// 渲染联系人项
|
||||
const renderContactItem = (contact: ContractData) => (
|
||||
<List.Item
|
||||
@@ -42,6 +50,86 @@ const ContactListSimple: React.FC<WechatFriendsProps> = ({
|
||||
</List.Item>
|
||||
);
|
||||
|
||||
// 初始化分页数据
|
||||
useEffect(() => {
|
||||
if (newContractList && newContractList.length > 0) {
|
||||
const initialVisibleContacts: { [key: string]: ContractData[] } = {};
|
||||
const initialLoading: { [key: string]: boolean } = {};
|
||||
const initialHasMore: { [key: string]: boolean } = {};
|
||||
const initialPage: { [key: string]: number } = {};
|
||||
|
||||
newContractList.forEach((group, index) => {
|
||||
const groupKey = index.toString();
|
||||
// 每个分组初始加载20条数据
|
||||
const pageSize = 20;
|
||||
initialVisibleContacts[groupKey] = group.contacts.slice(0, pageSize);
|
||||
initialLoading[groupKey] = false;
|
||||
initialHasMore[groupKey] = group.contacts.length > pageSize;
|
||||
initialPage[groupKey] = 1;
|
||||
});
|
||||
|
||||
setVisibleContacts(initialVisibleContacts);
|
||||
setLoading(initialLoading);
|
||||
setHasMore(initialHasMore);
|
||||
setPage(initialPage);
|
||||
}
|
||||
}, [newContractList]);
|
||||
|
||||
// 加载更多联系人
|
||||
const loadMoreContacts = useCallback(
|
||||
(groupKey: string) => {
|
||||
if (loading[groupKey] || !hasMore[groupKey] || !newContractList) return;
|
||||
|
||||
setLoading(prev => ({ ...prev, [groupKey]: true }));
|
||||
|
||||
// 模拟异步加载
|
||||
setTimeout(() => {
|
||||
const groupIndex = parseInt(groupKey);
|
||||
const group = newContractList[groupIndex];
|
||||
if (!group) return;
|
||||
|
||||
const pageSize = 20;
|
||||
const currentPage = page[groupKey] || 1;
|
||||
const nextPage = currentPage + 1;
|
||||
const startIndex = currentPage * pageSize;
|
||||
const endIndex = nextPage * pageSize;
|
||||
const newContacts = group.contacts.slice(startIndex, endIndex);
|
||||
|
||||
setVisibleContacts(prev => ({
|
||||
...prev,
|
||||
[groupKey]: [...(prev[groupKey] || []), ...newContacts],
|
||||
}));
|
||||
|
||||
setPage(prev => ({ ...prev, [groupKey]: nextPage }));
|
||||
setHasMore(prev => ({
|
||||
...prev,
|
||||
[groupKey]: endIndex < group.contacts.length,
|
||||
}));
|
||||
|
||||
setLoading(prev => ({ ...prev, [groupKey]: false }));
|
||||
}, 300);
|
||||
},
|
||||
[loading, hasMore, page, newContractList],
|
||||
);
|
||||
|
||||
// 渲染加载更多按钮
|
||||
const renderLoadMoreButton = (groupKey: string) => {
|
||||
if (!hasMore[groupKey])
|
||||
return <div className={styles.noMoreText}>没有更多了</div>;
|
||||
|
||||
return (
|
||||
<div className={styles.loadMoreContainer}>
|
||||
<Button
|
||||
size="small"
|
||||
loading={loading[groupKey]}
|
||||
onClick={() => loadMoreContacts(groupKey)}
|
||||
>
|
||||
{loading[groupKey] ? "加载中..." : "加载更多"}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.contractListSimple}>
|
||||
{newContractList && newContractList.length > 0 ? (
|
||||
@@ -50,26 +138,36 @@ const ContactListSimple: React.FC<WechatFriendsProps> = ({
|
||||
activeKey={activeKey}
|
||||
onChange={keys => setActiveKey(keys as string[])}
|
||||
>
|
||||
{newContractList.map((group, index) => (
|
||||
<Panel
|
||||
header={
|
||||
<div className={styles.groupHeader}>
|
||||
<span>{group.groupName}</span>
|
||||
<span className={styles.contactCount}>
|
||||
{group.contacts.length}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
key={index.toString()}
|
||||
className={styles.groupPanel}
|
||||
>
|
||||
<List
|
||||
className={styles.list}
|
||||
dataSource={group.contacts}
|
||||
renderItem={renderContactItem}
|
||||
/>
|
||||
</Panel>
|
||||
))}
|
||||
{newContractList.map((group, index) => {
|
||||
const groupKey = index.toString();
|
||||
const isActive = activeKey.includes(groupKey);
|
||||
|
||||
return (
|
||||
<Panel
|
||||
header={
|
||||
<div className={styles.groupHeader}>
|
||||
<span>{group.groupName}</span>
|
||||
<span className={styles.contactCount}>
|
||||
{group.contacts.length}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
key={groupKey}
|
||||
className={styles.groupPanel}
|
||||
>
|
||||
{isActive && (
|
||||
<>
|
||||
<List
|
||||
className={styles.list}
|
||||
dataSource={visibleContacts[groupKey] || []}
|
||||
renderItem={renderContactItem}
|
||||
/>
|
||||
{renderLoadMoreButton(groupKey)}
|
||||
</>
|
||||
)}
|
||||
</Panel>
|
||||
);
|
||||
})}
|
||||
</Collapse>
|
||||
) : (
|
||||
<>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Skeleton, Layout } from 'antd';
|
||||
import styles from './index.module.scss';
|
||||
import pageStyles from '../../index.module.scss';
|
||||
import React from "react";
|
||||
import { Skeleton, Layout } from "antd";
|
||||
import styles from "./index.module.scss";
|
||||
import pageStyles from "../../index.module.scss";
|
||||
|
||||
const { Header, Content, Sider } = Layout;
|
||||
|
||||
@@ -29,7 +29,10 @@ const PageSkeleton: React.FC<PageSkeletonProps> = ({ loading, children }) => {
|
||||
{Array(5)
|
||||
.fill(null)
|
||||
.map((_, index) => (
|
||||
<div key={`vertical-${index}`} className={styles.verticalUserItem}>
|
||||
<div
|
||||
key={`vertical-${index}`}
|
||||
className={styles.verticalUserItem}
|
||||
>
|
||||
<Skeleton.Avatar active size="large" shape="circle" />
|
||||
</div>
|
||||
))}
|
||||
@@ -42,18 +45,39 @@ const PageSkeleton: React.FC<PageSkeletonProps> = ({ loading, children }) => {
|
||||
<Skeleton.Input active size="small" block />
|
||||
</div>
|
||||
<div className={styles.tabsSkeleton}>
|
||||
<Skeleton.Button active size="small" shape="square" style={{ width: '30%' }} />
|
||||
<Skeleton.Button active size="small" shape="square" style={{ width: '30%' }} />
|
||||
<Skeleton.Button
|
||||
active
|
||||
size="small"
|
||||
shape="square"
|
||||
style={{ width: "30%" }}
|
||||
/>
|
||||
<Skeleton.Button
|
||||
active
|
||||
size="small"
|
||||
shape="square"
|
||||
style={{ width: "30%" }}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.contactListSkeleton}>
|
||||
{Array(8)
|
||||
.fill(null)
|
||||
.map((_, index) => (
|
||||
<div key={`contact-${index}`} className={styles.contactItemSkeleton}>
|
||||
<div
|
||||
key={`contact-${index}`}
|
||||
className={styles.contactItemSkeleton}
|
||||
>
|
||||
<Skeleton.Avatar active size="large" shape="circle" />
|
||||
<div className={styles.contactInfoSkeleton}>
|
||||
<Skeleton.Input active size="small" style={{ width: '60%' }} />
|
||||
<Skeleton.Input active size="small" style={{ width: '80%' }} />
|
||||
<Skeleton.Input
|
||||
active
|
||||
size="small"
|
||||
style={{ width: "60%" }}
|
||||
/>
|
||||
<Skeleton.Input
|
||||
active
|
||||
size="small"
|
||||
style={{ width: "80%" }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -64,7 +88,7 @@ const PageSkeleton: React.FC<PageSkeletonProps> = ({ loading, children }) => {
|
||||
<Content className={styles.skeletonMainContent}>
|
||||
<div className={styles.chatHeaderSkeleton}>
|
||||
<Skeleton.Avatar active size="large" shape="circle" />
|
||||
<Skeleton.Input active size="small" style={{ width: '30%' }} />
|
||||
<Skeleton.Input active size="small" style={{ width: "30%" }} />
|
||||
</div>
|
||||
<div className={styles.chatContentSkeleton}>
|
||||
{Array(5)
|
||||
@@ -75,7 +99,11 @@ const PageSkeleton: React.FC<PageSkeletonProps> = ({ loading, children }) => {
|
||||
className={`${styles.messageSkeleton} ${index % 2 === 0 ? styles.leftMessage : styles.rightMessage}`}
|
||||
>
|
||||
<Skeleton.Avatar active size="small" shape="circle" />
|
||||
<Skeleton.Input active size="small" style={{ width: index % 2 === 0 ? '60%' : '40%' }} />
|
||||
<Skeleton.Input
|
||||
active
|
||||
size="small"
|
||||
style={{ width: index % 2 === 0 ? "60%" : "40%" }}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -88,4 +116,4 @@ const PageSkeleton: React.FC<PageSkeletonProps> = ({ loading, children }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default PageSkeleton;
|
||||
export default PageSkeleton;
|
||||
|
||||
Reference in New Issue
Block a user