feat(WechatFriends): 实现联系人列表分页加载功能
添加react-window依赖并实现分组联系人列表的分页加载功能,优化大数据量下的性能表现 - 每组初始加载20条数据,滚动到底部可点击加载更多 - 添加加载状态提示和"没有更多了"的结束提示 - 优化样式添加加载更多按钮的容器样式
This commit is contained in:
18
Cunkebao/dist/.vite/manifest.json
vendored
18
Cunkebao/dist/.vite/manifest.json
vendored
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
8
Cunkebao/dist/index.html
vendored
8
Cunkebao/dist/index.html
vendored
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
22
Cunkebao/pnpm-lock.yaml
generated
22
Cunkebao/pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
@@ -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