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

View File

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

View File

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

View File

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

View File

@@ -44,8 +44,21 @@
} }
.groupPanel { .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 { .list {
flex: 1; flex: 1;

View File

@@ -1,5 +1,5 @@
import React, { useState } from "react"; import React, { useState, useCallback, useEffect } from "react";
import { List, Avatar, Collapse } from "antd"; import { List, Avatar, Collapse, Button } from "antd";
import { ContractData } from "@/pages/pc/ckbox/data"; import { ContractData } from "@/pages/pc/ckbox/data";
import styles from "./WechatFriends.module.scss"; import styles from "./WechatFriends.module.scss";
import { useCkChatStore } from "@/store/module/ckchat"; import { useCkChatStore } from "@/store/module/ckchat";
@@ -20,6 +20,14 @@ const ContactListSimple: React.FC<WechatFriendsProps> = ({
const newContractList = useCkChatStore(state => state.newContractList); const newContractList = useCkChatStore(state => state.newContractList);
const [activeKey, setActiveKey] = useState<string[]>(["0"]); // 默认展开第一个分组 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) => ( const renderContactItem = (contact: ContractData) => (
<List.Item <List.Item
@@ -42,6 +50,86 @@ const ContactListSimple: React.FC<WechatFriendsProps> = ({
</List.Item> </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 ( return (
<div className={styles.contractListSimple}> <div className={styles.contractListSimple}>
{newContractList && newContractList.length > 0 ? ( {newContractList && newContractList.length > 0 ? (
@@ -50,26 +138,36 @@ const ContactListSimple: React.FC<WechatFriendsProps> = ({
activeKey={activeKey} activeKey={activeKey}
onChange={keys => setActiveKey(keys as string[])} onChange={keys => setActiveKey(keys as string[])}
> >
{newContractList.map((group, index) => ( {newContractList.map((group, index) => {
<Panel const groupKey = index.toString();
header={ const isActive = activeKey.includes(groupKey);
<div className={styles.groupHeader}>
<span>{group.groupName}</span> return (
<span className={styles.contactCount}> <Panel
{group.contacts.length} header={
</span> <div className={styles.groupHeader}>
</div> <span>{group.groupName}</span>
} <span className={styles.contactCount}>
key={index.toString()} {group.contacts.length}
className={styles.groupPanel} </span>
> </div>
<List }
className={styles.list} key={groupKey}
dataSource={group.contacts} className={styles.groupPanel}
renderItem={renderContactItem} >
/> {isActive && (
</Panel> <>
))} <List
className={styles.list}
dataSource={visibleContacts[groupKey] || []}
renderItem={renderContactItem}
/>
{renderLoadMoreButton(groupKey)}
</>
)}
</Panel>
);
})}
</Collapse> </Collapse>
) : ( ) : (
<> <>

View File

@@ -1,7 +1,7 @@
import React from 'react'; import React from "react";
import { Skeleton, Layout } from 'antd'; import { Skeleton, Layout } from "antd";
import styles from './index.module.scss'; import styles from "./index.module.scss";
import pageStyles from '../../index.module.scss'; import pageStyles from "../../index.module.scss";
const { Header, Content, Sider } = Layout; const { Header, Content, Sider } = Layout;
@@ -29,7 +29,10 @@ const PageSkeleton: React.FC<PageSkeletonProps> = ({ loading, children }) => {
{Array(5) {Array(5)
.fill(null) .fill(null)
.map((_, index) => ( .map((_, index) => (
<div key={`vertical-${index}`} className={styles.verticalUserItem}> <div
key={`vertical-${index}`}
className={styles.verticalUserItem}
>
<Skeleton.Avatar active size="large" shape="circle" /> <Skeleton.Avatar active size="large" shape="circle" />
</div> </div>
))} ))}
@@ -42,18 +45,39 @@ const PageSkeleton: React.FC<PageSkeletonProps> = ({ loading, children }) => {
<Skeleton.Input active size="small" block /> <Skeleton.Input active size="small" block />
</div> </div>
<div className={styles.tabsSkeleton}> <div className={styles.tabsSkeleton}>
<Skeleton.Button active size="small" shape="square" style={{ width: '30%' }} /> <Skeleton.Button
<Skeleton.Button active size="small" shape="square" style={{ width: '30%' }} /> active
size="small"
shape="square"
style={{ width: "30%" }}
/>
<Skeleton.Button
active
size="small"
shape="square"
style={{ width: "30%" }}
/>
</div> </div>
<div className={styles.contactListSkeleton}> <div className={styles.contactListSkeleton}>
{Array(8) {Array(8)
.fill(null) .fill(null)
.map((_, index) => ( .map((_, index) => (
<div key={`contact-${index}`} className={styles.contactItemSkeleton}> <div
key={`contact-${index}`}
className={styles.contactItemSkeleton}
>
<Skeleton.Avatar active size="large" shape="circle" /> <Skeleton.Avatar active size="large" shape="circle" />
<div className={styles.contactInfoSkeleton}> <div className={styles.contactInfoSkeleton}>
<Skeleton.Input active size="small" style={{ width: '60%' }} /> <Skeleton.Input
<Skeleton.Input active size="small" style={{ width: '80%' }} /> active
size="small"
style={{ width: "60%" }}
/>
<Skeleton.Input
active
size="small"
style={{ width: "80%" }}
/>
</div> </div>
</div> </div>
))} ))}
@@ -64,7 +88,7 @@ const PageSkeleton: React.FC<PageSkeletonProps> = ({ loading, children }) => {
<Content className={styles.skeletonMainContent}> <Content className={styles.skeletonMainContent}>
<div className={styles.chatHeaderSkeleton}> <div className={styles.chatHeaderSkeleton}>
<Skeleton.Avatar active size="large" shape="circle" /> <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>
<div className={styles.chatContentSkeleton}> <div className={styles.chatContentSkeleton}>
{Array(5) {Array(5)
@@ -75,7 +99,11 @@ const PageSkeleton: React.FC<PageSkeletonProps> = ({ loading, children }) => {
className={`${styles.messageSkeleton} ${index % 2 === 0 ? styles.leftMessage : styles.rightMessage}`} className={`${styles.messageSkeleton} ${index % 2 === 0 ? styles.leftMessage : styles.rightMessage}`}
> >
<Skeleton.Avatar active size="small" shape="circle" /> <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>
))} ))}
</div> </div>
@@ -88,4 +116,4 @@ const PageSkeleton: React.FC<PageSkeletonProps> = ({ loading, children }) => {
); );
}; };
export default PageSkeleton; export default PageSkeleton;