feat(WechatFriends): 实现联系人列表分页加载功能

添加react-window依赖并实现分组联系人列表的分页加载功能,优化大数据量下的性能表现
- 每组初始加载20条数据,滚动到底部可点击加载更多
- 添加加载状态提示和"没有更多了"的结束提示
- 优化样式添加加载更多按钮的容器样式
This commit is contained in:
超级老白兔
2025-08-28 18:24:05 +08:00
parent b4eb2919b1
commit d878a1fcaa
7 changed files with 212 additions and 50 deletions

View File

@@ -1,9 +1,9 @@
{
"_charts-M0qaf_ew.js": {
"file": "assets/charts-M0qaf_ew.js",
"_charts-DKSCc2_C.js": {
"file": "assets/charts-DKSCc2_C.js",
"name": "charts",
"imports": [
"_ui-D5qYGnLz.js",
"_ui-DhAz00L0.js",
"_vendor-2vc8h_ct.js"
]
},
@@ -11,8 +11,8 @@
"file": "assets/ui-D0C0OGrH.css",
"src": "_ui-D0C0OGrH.css"
},
"_ui-D5qYGnLz.js": {
"file": "assets/ui-D5qYGnLz.js",
"_ui-DhAz00L0.js": {
"file": "assets/ui-DhAz00L0.js",
"name": "ui",
"imports": [
"_vendor-2vc8h_ct.js"
@@ -33,18 +33,18 @@
"name": "vendor"
},
"index.html": {
"file": "assets/index-BQxyt58_.js",
"file": "assets/index-BdCPAYQ7.js",
"name": "index",
"src": "index.html",
"isEntry": true,
"imports": [
"_vendor-2vc8h_ct.js",
"_utils-6WF66_dS.js",
"_ui-D5qYGnLz.js",
"_charts-M0qaf_ew.js"
"_ui-DhAz00L0.js",
"_charts-DKSCc2_C.js"
],
"css": [
"assets/index-B6B8u-1D.css"
"assets/index-ChiFk16x.css"
]
}
}

View File

@@ -11,13 +11,13 @@
</style>
<!-- 引入 uni-app web-view SDK必须 -->
<script type="text/javascript" src="/websdk.js"></script>
<script type="module" crossorigin src="/assets/index-BQxyt58_.js"></script>
<script type="module" crossorigin src="/assets/index-BdCPAYQ7.js"></script>
<link rel="modulepreload" crossorigin href="/assets/vendor-2vc8h_ct.js">
<link rel="modulepreload" crossorigin href="/assets/utils-6WF66_dS.js">
<link rel="modulepreload" crossorigin href="/assets/ui-D5qYGnLz.js">
<link rel="modulepreload" crossorigin href="/assets/charts-M0qaf_ew.js">
<link rel="modulepreload" crossorigin href="/assets/ui-DhAz00L0.js">
<link rel="modulepreload" crossorigin href="/assets/charts-DKSCc2_C.js">
<link rel="stylesheet" crossorigin href="/assets/ui-D0C0OGrH.css">
<link rel="stylesheet" crossorigin href="/assets/index-B6B8u-1D.css">
<link rel="stylesheet" crossorigin href="/assets/index-ChiFk16x.css">
</head>
<body>
<div id="root"></div>

View File

@@ -15,6 +15,7 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.20.0",
"react-window": "^1.8.11",
"vconsole": "^3.15.1",
"zustand": "^5.0.6"
},

View File

@@ -41,6 +41,9 @@ importers:
react-router-dom:
specifier: ^6.20.0
version: 6.30.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-window:
specifier: ^1.8.11
version: 1.8.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
vconsole:
specifier: ^3.15.1
version: 3.15.1
@@ -1563,6 +1566,9 @@ packages:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
memoize-one@5.2.1:
resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==}
merge2@1.4.1:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
@@ -2001,6 +2007,13 @@ packages:
peerDependencies:
react: '>=16.8'
react-window@1.8.11:
resolution: {integrity: sha512-+SRbUVT2scadgFSWx+R1P754xHPEqvcfSfVX10QYg6POOz+WNgkN48pS+BtZNIMGiL1HYrSEiCkwsMS15QogEQ==}
engines: {node: '>8.0.0'}
peerDependencies:
react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react@18.3.1:
resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
engines: {node: '>=0.10.0'}
@@ -4025,6 +4038,8 @@ snapshots:
math-intrinsics@1.1.0: {}
memoize-one@5.2.1: {}
merge2@1.4.1: {}
micromatch@4.0.8:
@@ -4538,6 +4553,13 @@ snapshots:
'@remix-run/router': 1.23.0
react: 18.3.1
react-window@1.8.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
'@babel/runtime': 7.28.2
memoize-one: 5.2.1
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react@18.3.1:
dependencies:
loose-envify: 1.4.0

View File

@@ -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;

View File

@@ -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>
) : (
<>

View File

@@ -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;