feat(朋友圈组件): 实现朋友圈列表分页加载和样式优化

添加分页加载功能,优化样式结构,使用折叠面板管理不同朋友圈类型
重构组件结构,添加加载状态和空状态显示
This commit is contained in:
超级老白兔
2025-09-16 16:31:56 +08:00
parent 21396f5a1d
commit 1d94368579
3 changed files with 483 additions and 292 deletions

View File

@@ -0,0 +1,6 @@
import request2 from "@/api/request2";
import request from "@/api/request";
// 静音聊天会话
// export const muteChatSession = (chatId: string): Promise<void> => {
// return request2(`/v1/chats/${chatId}/mute`, {}, "PUT");
// };

View File

@@ -1,230 +1,248 @@
/* ===== 组件根容器 ===== */
.friendsCircle {
height: 100%;
overflow-y: auto;
padding: 0;
background-color: #f5f5f5;
}
.itemWrapper {
margin-bottom: 1px;
}
// 可折叠组件样式
.collapseContainer {
margin-bottom: 1px;
:global(.ant-collapse-item) {
border-bottom: 1px solid #e8e8e8;
&:last-child {
border-bottom: 1px solid #e8e8e8;
}
/* 滚动条样式 */
&::-webkit-scrollbar {
width: 6px;
}
:global(.ant-collapse-header) {
padding: 12px 16px !important;
background-color: #ffffff;
&::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
&::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
&:hover {
background-color: #f8f8f8;
background: #a8a8a8;
}
}
:global(.ant-collapse-content-box) {
padding: 16px;
background-color: #ffffff;
}
}
.collapseHeader {
display: flex;
align-items: center;
gap: 12px;
}
.specialAvatar {
background-color: #1890ff;
}
.groupAvatars {
display: flex;
position: relative;
width: 32px;
height: 32px;
.groupAvatar {
position: absolute;
border: 1px solid #fff;
background-color: #52c41a;
&:nth-child(1) {
top: 0;
left: 0;
z-index: 4;
}
&:nth-child(2) {
top: 0;
right: 0;
z-index: 3;
}
&:nth-child(3) {
bottom: 0;
left: 0;
z-index: 2;
}
&:nth-child(4) {
bottom: 0;
right: 0;
z-index: 1;
}
}
}
.specialText {
font-size: 16px;
color: #333;
font-weight: 400;
}
.myCircleContent,
.squareContent {
padding: 0;
.itemWrapper {
/* ===== 折叠面板样式 ===== */
.collapseContainer {
margin-bottom: 1px;
&:last-child {
margin-bottom: 0;
:global(.ant-collapse-item) {
border-bottom: 1px solid #e8e8e8;
&:last-child {
border-bottom: 1px solid #e8e8e8;
}
}
:global(.ant-collapse-header) {
padding: 12px 16px !important;
background-color: #ffffff;
&:hover {
background-color: #f8f8f8;
}
}
:global(.ant-collapse-content-box) {
padding: 16px;
background-color: #ffffff;
}
/* 折叠面板头部 */
.collapseHeader {
display: flex;
align-items: center;
gap: 6px;
/* 特殊头像样式 */
.specialAvatar {
background-color: #1890ff;
}
/* 群组头像样式 */
.groupAvatars {
display: flex;
position: relative;
width: 32px;
height: 32px;
.groupAvatar {
position: absolute;
border: 1px solid #fff;
background-color: #52c41a;
&:nth-child(1) {
top: 0;
left: 0;
z-index: 4;
}
&:nth-child(2) {
top: 0;
right: 0;
z-index: 3;
}
&:nth-child(3) {
bottom: 0;
left: 0;
z-index: 2;
}
&:nth-child(4) {
bottom: 0;
right: 0;
z-index: 1;
}
}
}
/* 特殊文本样式 */
.specialText {
font-size: 16px;
color: #333;
font-weight: 400;
}
}
}
// 当内容为空时的样式
.emptyText {
padding: 20px;
text-align: center;
color: #999;
font-size: 14px;
margin: 0;
/* ===== 内容区域样式 ===== */
.myCircleContent,
.squareContent {
padding: 0;
/* 项目包装器 */
.itemWrapper {
margin-bottom: 1px;
&:last-child {
margin-bottom: 0;
}
/* ===== 朋友圈项目样式 ===== */
.circleItem {
background-color: #ffffff;
margin-bottom: 20px;
display: flex;
/* 头像样式 */
.avatar {
margin-right: 10px;
}
/* 项目头部 */
.itemHeader {
display: flex;
align-items: flex-start;
gap: 12px;
margin-bottom: 12px;
/* 用户信息 */
.userInfo {
flex: 1;
.username {
font-size: 14px;
font-weight: 500;
color: #333;
line-height: 1.4;
}
}
}
/* 项目内容 */
.itemContent {
margin-bottom: 12px;
font-size: 12px;
.contentText {
color: #333;
line-height: 1.6;
margin-bottom: 8px;
word-wrap: break-word;
}
/* 图片容器 */
.imageContainer {
margin: 8px 0;
.contentImage {
width: 60px;
height: 60px;
object-fit: cover;
border-radius: 4px;
margin-right: 8px;
margin-bottom: 8px;
}
}
/* 蓝色链接 */
.blueLink {
color: #1890ff;
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
}
/* 项目底部 */
.itemFooter {
display: flex;
align-items: center;
justify-content: space-between;
/* 时间信息 */
.timeInfo {
font-size: 12px;
color: #999;
}
/* 操作按钮区域 */
.actions {
display: flex;
align-items: center;
gap: 8px;
.actionButton {
padding: 4px 8px;
color: #666;
&:hover {
color: #1890ff;
background-color: #f0f8ff;
}
.anticon {
font-size: 14px;
}
}
}
}
}
}
/* 空状态样式 */
.emptyText {
padding: 20px;
text-align: center;
color: #999;
font-size: 14px;
margin: 0;
}
/* 加载更多样式 */
.loadingMore {
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
color: #666;
font-size: 14px;
gap: 8px;
.anticon {
margin-right: 4px;
}
}
}
}
// 普通朋友圈项目样式
.circleItem {
background-color: #ffffff;
padding: 16px;
border-bottom: 1px solid #e8e8e8;
}
.itemHeader {
display: flex;
align-items: flex-start;
gap: 12px;
margin-bottom: 12px;
}
.avatar {
flex-shrink: 0;
}
.userInfo {
flex: 1;
}
.username {
font-size: 16px;
font-weight: 500;
color: #333;
line-height: 1.4;
}
.itemContent {
margin-left: 52px;
margin-bottom: 12px;
}
.contentText {
font-size: 14px;
color: #333;
line-height: 1.6;
margin-bottom: 8px;
word-wrap: break-word;
}
.imageContainer {
margin: 8px 0;
}
.contentImage {
width: 60px;
height: 60px;
object-fit: cover;
border-radius: 4px;
margin-right: 8px;
margin-bottom: 8px;
}
.blueLink {
color: #1890ff;
font-size: 14px;
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
.itemFooter {
display: flex;
align-items: center;
justify-content: space-between;
margin-left: 52px;
}
.timeInfo {
font-size: 12px;
color: #999;
}
.actions {
display: flex;
align-items: center;
gap: 8px;
}
.actionButton {
padding: 4px 8px;
color: #666;
&:hover {
color: #1890ff;
background-color: #f0f8ff;
}
.anticon {
font-size: 14px;
}
}
// 滚动条样式
.friendsCircle::-webkit-scrollbar {
width: 6px;
}
.friendsCircle::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
.friendsCircle::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
&:hover {
background: #a8a8a8;
}
}

View File

@@ -1,6 +1,13 @@
import React from "react";
import { Avatar, Button, Collapse } from "antd";
import { HeartOutlined, MessageOutlined } from "@ant-design/icons";
import React, { useState, useEffect } from "react";
import { Avatar, Button, Collapse, Spin } from "antd";
import {
HeartOutlined,
ChromeOutlined,
MessageOutlined,
LoadingOutlined,
AppstoreOutlined,
} from "@ant-design/icons";
import { InfiniteScroll } from "antd-mobile";
import styles from "./index.module.scss";
// 朋友圈数据类型定义
@@ -15,6 +22,20 @@ interface FriendsCircleItem {
comments?: number;
}
// API响应类型
interface ApiResponse {
list: FriendsCircleItem[];
total: number;
hasMore: boolean;
}
// 分页参数类型
interface PaginationParams {
pageNum: number;
pageSize: number;
type: "my" | "square";
}
// 模拟朋友圈数据
const mockFriendsCircleData: FriendsCircleItem[] = [
{
@@ -76,6 +97,123 @@ const mockFriendsCircleData: FriendsCircleItem[] = [
];
const FriendsCircle: React.FC = () => {
// 状态管理
const [myCircleData, setMyCircleData] = useState<FriendsCircleItem[]>([]);
const [squareData, setSquareData] = useState<FriendsCircleItem[]>([]);
const [myCircleLoading, setMyCircleLoading] = useState(false);
const [squareLoading, setSquareLoading] = useState(false);
const [myCircleHasMore, setMyCircleHasMore] = useState(true);
const [squareHasMore, setSquareHasMore] = useState(true);
const [myCirclePage, setMyCirclePage] = useState(1);
const [squarePage, setSquarePage] = useState(1);
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
// 模拟API调用函数
const fetchFriendsCircleData = async (
params: PaginationParams,
): Promise<ApiResponse> => {
// 模拟网络延迟
await new Promise(resolve => setTimeout(resolve, 1000));
const { pageNum, pageSize, type } = params;
const startIndex = (pageNum - 1) * pageSize;
// 使用原有的模拟数据
const allData = mockFriendsCircleData.slice(2); // 排除前两个特殊项
const typeData = type === "my" ? allData.slice(0, 2) : allData.slice(2);
const paginatedData = typeData.slice(startIndex, startIndex + pageSize);
return {
list: paginatedData,
total: typeData.length,
hasMore: startIndex + pageSize < typeData.length,
};
};
// 加载我的朋友圈数据
const loadMyCircleData = async (
pageNum: number = 1,
reset: boolean = false,
) => {
setMyCircleLoading(true);
try {
const response = await fetchFriendsCircleData({
pageNum,
pageSize: 10,
type: "my",
});
if (reset) {
setMyCircleData(response.list);
} else {
setMyCircleData(prev => [...prev, ...response.list]);
}
setMyCircleHasMore(response.hasMore);
setMyCirclePage(pageNum);
} catch (error) {
console.error("加载我的朋友圈失败:", error);
} finally {
setMyCircleLoading(false);
}
};
// 加载朋友圈广场数据
const loadSquareData = async (
pageNum: number = 1,
reset: boolean = false,
) => {
setSquareLoading(true);
try {
const response = await fetchFriendsCircleData({
pageNum,
pageSize: 10,
type: "square",
});
if (reset) {
setSquareData(response.list);
} else {
setSquareData(prev => [...prev, ...response.list]);
}
setSquareHasMore(response.hasMore);
setSquarePage(pageNum);
} catch (error) {
console.error("加载朋友圈广场失败:", error);
} finally {
setSquareLoading(false);
}
};
// 加载更多我的朋友圈
const loadMoreMyCircle = async () => {
if (!myCircleHasMore || myCircleLoading) return;
await loadMyCircleData(myCirclePage + 1);
};
// 加载更多朋友圈广场
const loadMoreSquare = async () => {
if (!squareHasMore || squareLoading) return;
await loadSquareData(squarePage + 1);
};
// 处理折叠面板展开/收起
const handleCollapseChange = (keys: string | string[]) => {
const keyArray = Array.isArray(keys) ? keys : [keys];
setExpandedKeys(keyArray);
// 当展开时加载数据
keyArray.forEach(key => {
if (key === "1" && myCircleData.length === 0) {
loadMyCircleData(1, true);
}
if (key === "2" && squareData.length === 0) {
loadSquareData(1, true);
}
});
};
const handleLike = (id: string) => {
console.log("点赞:", id);
};
@@ -84,18 +222,88 @@ const FriendsCircle: React.FC = () => {
console.log("评论:", id);
};
const renderMyFriendsCircle = () => {
// 显示部分朋友圈数据作为"我的朋友圈"
const myCircleData = mockFriendsCircleData.slice(2, 4);
const renderNormalItem = (item: FriendsCircleItem, isNotMy?: boolean) => {
return (
<div className={styles.circleItem}>
{isNotMy && (
<div className={styles.avatar}>
<Avatar size={36} shape="square" src={item.avatar} />
</div>
)}
<div className={styles.itemWrap}>
<div className={styles.itemHeader}>
<div className={styles.userInfo}>
<div className={styles.username}>{item.username}</div>
</div>
</div>
<div className={styles.itemContent}>
<div className={styles.contentText}>{item.content}</div>
{item.images && item.images.length > 0 && (
<div className={styles.imageContainer}>
{item.images.map((image, index) => (
<img
key={index}
src={image}
className={styles.contentImage}
/>
))}
</div>
)}
<div className={styles.blueLink}></div>
</div>
<div className={styles.itemFooter}>
<div className={styles.timeInfo}>{item.time}</div>
<div className={styles.actions}>
<Button
type="text"
size="small"
icon={<HeartOutlined />}
onClick={() => handleLike(item.id)}
className={styles.actionButton}
/>
<Button
type="text"
size="small"
icon={<MessageOutlined />}
onClick={() => handleComment(item.id)}
className={styles.actionButton}
/>
</div>
</div>
</div>
</div>
);
};
const renderMyFriendsCircle = () => {
return (
<div className={styles.myCircleContent}>
{myCircleData.length > 0 ? (
myCircleData.map(item => (
<div key={item.id} className={styles.itemWrapper}>
{renderNormalItem(item)}
</div>
))
<>
{myCircleData.map(item => (
<div key={item.id} className={styles.itemWrapper}>
{renderNormalItem(item, false)}
</div>
))}
<InfiniteScroll
loadMore={loadMoreMyCircle}
hasMore={myCircleHasMore}
threshold={10}
>
{myCircleLoading && (
<div className={styles.loadingMore}>
<Spin indicator={<LoadingOutlined spin />} /> ...
</div>
)}
</InfiniteScroll>
</>
) : myCircleLoading ? (
<div className={styles.loadingMore}>
<Spin indicator={<LoadingOutlined spin />} /> ...
</div>
) : (
<p className={styles.emptyText}></p>
)}
@@ -104,17 +312,31 @@ const FriendsCircle: React.FC = () => {
};
const renderFriendsSquare = () => {
// 显示剩余的朋友圈数据作为"朋友圈广场"
const squareData = mockFriendsCircleData.slice(4);
return (
<div className={styles.squareContent}>
{squareData.length > 0 ? (
squareData.map(item => (
<div key={item.id} className={styles.itemWrapper}>
{renderNormalItem(item)}
</div>
))
<>
{squareData.map(item => (
<div key={item.id} className={styles.itemWrapper}>
{renderNormalItem(item, true)}
</div>
))}
<InfiniteScroll
loadMore={loadMoreSquare}
hasMore={squareHasMore}
threshold={10}
>
{squareLoading && (
<div className={styles.loadingMore}>
<Spin indicator={<LoadingOutlined spin />} /> ...
</div>
)}
</InfiniteScroll>
</>
) : squareLoading ? (
<div className={styles.loadingMore}>
<Spin indicator={<LoadingOutlined spin />} /> ...
</div>
) : (
<p className={styles.emptyText}>广</p>
)}
@@ -127,7 +349,7 @@ const FriendsCircle: React.FC = () => {
key: "1",
label: (
<div className={styles.collapseHeader}>
<Avatar size={32} className={styles.specialAvatar} />
<ChromeOutlined style={{ fontSize: 20 }} />
<span className={styles.specialText}></span>
</div>
),
@@ -137,12 +359,7 @@ const FriendsCircle: React.FC = () => {
key: "2",
label: (
<div className={styles.collapseHeader}>
<div className={styles.groupAvatars}>
<Avatar size={16} className={styles.groupAvatar} />
<Avatar size={16} className={styles.groupAvatar} />
<Avatar size={16} className={styles.groupAvatar} />
<Avatar size={16} className={styles.groupAvatar} />
</div>
<AppstoreOutlined style={{ fontSize: 20 }} />
<span className={styles.specialText}>广</span>
</div>
),
@@ -150,58 +367,6 @@ const FriendsCircle: React.FC = () => {
},
];
const renderNormalItem = (item: FriendsCircleItem) => {
return (
<div className={styles.circleItem}>
<div className={styles.itemHeader}>
<Avatar size={40} src={item.avatar} className={styles.avatar} />
<div className={styles.userInfo}>
<div className={styles.username}>{item.username}</div>
</div>
</div>
<div className={styles.itemContent}>
<div className={styles.contentText}>{item.content}</div>
{item.images && item.images.length > 0 && (
<div className={styles.imageContainer}>
{item.images.map((image, index) => (
<img
key={index}
src={image}
alt="朋友圈图片"
className={styles.contentImage}
/>
))}
</div>
)}
<div className={styles.blueLink}></div>
</div>
<div className={styles.itemFooter}>
<div className={styles.timeInfo}>{item.time}</div>
<div className={styles.actions}>
<Button
type="text"
size="small"
icon={<HeartOutlined />}
onClick={() => handleLike(item.id)}
className={styles.actionButton}
/>
<Button
type="text"
size="small"
icon={<MessageOutlined />}
onClick={() => handleComment(item.id)}
className={styles.actionButton}
/>
</div>
</div>
</div>
);
};
return (
<div className={styles.friendsCircle}>
{/* 可折叠的特殊模块,包含所有朋友圈数据 */}
@@ -209,6 +374,8 @@ const FriendsCircle: React.FC = () => {
items={collapseItems}
className={styles.collapseContainer}
ghost
activeKey={expandedKeys}
onChange={handleCollapseChange}
/>
</div>
);