挑战用React封装100个组件【007】
项目地址
https://github.com/hismeyy/react-component-100
组件描述
今天的组件是用来展示聊天列表,或者论坛内容列表的组件。配合挑战006的时候开发的组件,可以显示用户的具体信息。
样式展示
前置依赖
今天,我分享的组件,需要用到的依赖有:
- react-icons(提供图标)
- InfoCard(提供查看用户详细信息)
安装 react-icons
# 使用 npm
npm install react-icons
# 或者使用 yarn
yarn add react-icons
使用的话,大家可以看这个网站。大家进去可以找需要的图标。具体使用里面有介绍,非常简单。
react-icons 图标
InfoCard
这个组件的话,大家可以看 挑战用React封装100个组件【006】 文章。
代码展示
InfoCard.tsx
import './ChatList.css';
import { useState, useRef, useCallback } from 'react';
import { AiOutlineLike, AiOutlineMessage, AiOutlineStar } from "react-icons/ai";
import { chatData, ChatItem } from './data';
import InfoCard from '../card/infoCard04/InfoCard';
const ChatList = () => {
// 用于管理悬停交互和信息卡片显示的状态
const [hoveredUser, setHoveredUser] = useState<ChatItem | null>(null);
const [infoCardPosition, setInfoCardPosition] = useState({ x: 0, y: 0 });
const [isCardFading, setIsCardFading] = useState(false);
// 用于管理动画超时的引用
const timeoutRef = useRef<number | null>(null);
// 用于跟踪鼠标是否在卡片区域内
const isMouseInCardRef = useRef(false);
/**
* 处理头像的鼠标进入事件
* 显示信息卡片并计算其正确位置
*/
const handleAvatarMouseEnter = (e: React.MouseEvent<HTMLDivElement>, item: ChatItem) => {
if (timeoutRef.current) {
window.clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
setIsCardFading(false);
// 根据头像位置计算信息卡片位置
const rect = e.currentTarget.getBoundingClientRect();
setInfoCardPosition({
x: rect.left + window.scrollX,
y: rect.bottom + window.scrollY
});
setHoveredUser(item);
};
/**
* 检查鼠标是否移动到了InfoCard上
* 通过检查鼠标当前位置和InfoCard的位置关系来判断
*/
const checkIfMouseMovingToCard = useCallback((e: MouseEvent) => {
const cardElement = document.querySelector('.info-card-container');
if (!cardElement) return false;
const cardRect = cardElement.getBoundingClientRect();
const mouseX = e.clientX;
const mouseY = e.clientY;
// 扩大判定区域,给予用户更大的移动空间
const expandedRect = {
left: cardRect.left - 20,
right: cardRect.right + 20,
top: cardRect.top - 20,
bottom: cardRect.bottom + 20
};
return mouseX >= expandedRect.left &&
mouseX <= expandedRect.right &&
mouseY >= expandedRect.top &&
mouseY <= expandedRect.bottom;
}, []);
/**
* 处理头像和信息卡片的鼠标离开事件
*/
const handleMouseLeave = (e: React.MouseEvent) => {
// 如果鼠标正在往InfoCard方向移动,不触发隐藏
if (checkIfMouseMovingToCard(e.nativeEvent)) {
return;
}
// 如果鼠标已经在卡片内,不触发隐藏
if (isMouseInCardRef.current) {
return;
}
setIsCardFading(true);
timeoutRef.current = window.setTimeout(() => {
if (!isMouseInCardRef.current) {
setHoveredUser(null);
setIsCardFading(false);
}
}, 300);
};
/**
* 处理信息卡片的鼠标进入事件
*/
const handleInfoCardMouseEnter = () => {
isMouseInCardRef.current = true;
if (timeoutRef.current) {
window.clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
setIsCardFading(false);
};
/**
* 处理信息卡片的鼠标离开事件
*/
const handleInfoCardMouseLeave = () => {
isMouseInCardRef.current = false;
setIsCardFading(true);
timeoutRef.current = window.setTimeout(() => {
if (!isMouseInCardRef.current) {
setHoveredUser(null);
setIsCardFading(false);
}
}, 300);
};
/**
* 处理关注按钮点击事件
*/
const handleFollow = () => {
console.log('关注用户:', hoveredUser?.userName);
};
/**
* 处理发送消息按钮点击事件
*/
const handleMessage = () => {
console.log('发送消息给:', hoveredUser?.userName);
};
return (
<div className="chat-list">
{/* 聊天项目列表 */}
<ul className="chat-items">
{chatData.map((item: ChatItem) => (
<li key={item.id} className="chat-item">
{/* 用户信息区域 */}
<div className='chat-info'>
<div
className='user-avatar'
onMouseEnter={(e) => handleAvatarMouseEnter(e, item)}
onMouseLeave={handleMouseLeave}
>
<img src={item.userAvatar} alt={item.avatarAlt} />
</div>
<div className='user-info'>
<h6 className='user-name'>{item.userName}</h6>
<p className='send-time'>{item.sendTime}</p>
</div>
</div>
{/* 消息内容 */}
<div className='chat-content'>
<p>{item.content}</p>
</div>
{/* 互动按钮 */}
<div className='chat-functions'>
<div className='like'>
<AiOutlineLike />{item.likes}
</div>
<div className='comment'>
<AiOutlineMessage />{item.comments}
</div>
<div className='collect'>
<AiOutlineStar />{item.collections}
</div>
</div>
</li>
))}
</ul>
{/* 悬停信息卡片 */}
{hoveredUser && (
<div
className={`info-card-container ${isCardFading ? 'fade-out' : ''}`}
style={{
position: 'absolute',
left: `${infoCardPosition.x}px`,
top: `${infoCardPosition.y + 10}px`,
zIndex: 1000
}}
onMouseEnter={handleInfoCardMouseEnter}
onMouseLeave={handleInfoCardMouseLeave}
>
<InfoCard
avatarUrl={hoveredUser.userAvatar}
avatarAlt={hoveredUser.avatarAlt}
name={hoveredUser.userName}
description={hoveredUser.description}
labels={hoveredUser.labels}
isVerified={hoveredUser.isVerified}
onFollow={handleFollow}
onMessage={handleMessage}
/>
</div>
)}
</div>
);
};
export default ChatList;
InfoCard.css
/* 聊天列表容器 */
.chat-list {
width: 100%;
background-color: #FFFFFF;
border-radius: 10px;
padding: 10px 30px;
box-sizing: border-box;
}
/* 聊天项目列表 */
.chat-list .chat-items {
all: unset;
}
/* 单个聊天项目 */
.chat-list .chat-items .chat-item {
display: flex;
flex-direction: column;
justify-content: left;
margin: 20px 0;
}
/* 聊天项目分隔线 */
.chat-list .chat-items .chat-item::after {
content: '';
display: block;
width: 100%;
height: 1px;
background-color: #e6e6e6;
margin-top: 20px;
}
/* 用户信息区域 */
.chat-list .chat-items .chat-item .chat-info {
height: 50px;
display: flex;
justify-content: left;
}
/* 用户头像 */
.chat-list .chat-items .chat-item .chat-info .user-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
overflow: hidden;
cursor: pointer;
transition: transform 0.2s ease;
}
/* 头像悬停效果 */
.chat-list .chat-items .chat-item .chat-info .user-avatar:hover {
transform: scale(1.05);
}
/* 头像图片 */
.chat-list .chat-items .chat-item .chat-info .user-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
/* 用户信息容器 */
.chat-list .chat-items .chat-item .chat-info .user-info {
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
margin-left: 20px;
gap: 5px;
}
/* 用户名称 */
.chat-list .chat-items .chat-item .chat-info .user-info .user-name {
all: unset;
display: block;
font-size: 16px;
font-weight: bold;
}
/* 发送时间 */
.chat-list .chat-items .chat-item .chat-info .user-info .send-time {
all: unset;
display: block;
font-size: 12px;
color: #B3B3B3
}
/* 聊天内容区域 */
.chat-list .chat-items .chat-item .chat-content {
margin-top: 15px;
}
/* 聊天内容文本 */
.chat-list .chat-items .chat-item .chat-content p {
all: unset;
display: block;
font-size: 14px;
line-height: 1.5;
word-break: break-all;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
line-clamp: 2;
box-orient: vertical;
cursor: pointer;
}
/* 互动功能区域 */
.chat-list .chat-items .chat-item .chat-functions {
display: flex;
gap: 20px;
margin-top: 20px;
justify-content: left;
font-size: 14px;
color: #B3B3B3;
}
/* 互动按钮 */
.chat-list .chat-items .chat-item .chat-functions div {
display: flex;
gap: 2px;
align-items: center;
cursor: pointer;
color: #292929;
transition: all 0.3s ease
}
/* 互动按钮悬停效果 */
.chat-list .chat-items .chat-item .chat-functions div:hover {
color: #f08a5d;
}
/* 信息卡片淡入动画 */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 信息卡片淡出动画 */
@keyframes fadeOut {
from {
opacity: 1;
transform: translateY(0);
}
to {
opacity: 0;
transform: translateY(-10px);
}
}
/* 信息卡片容器 */
.info-card-container {
pointer-events: auto;
animation: fadeIn 0.2s ease-out forwards;
}
.info-card-container.fade-out {
animation: fadeOut 0.2s ease-out forwards;
}
使用
App.tsx
import './App.css'
import ChatList from './components/chatList/ChatList';
function App() {
return (
<>
<div className="App">
<ChatList />
</div>
</>
);
}
export default App
数据
除了代码之外,我们还需要数据!我放在了data.ts
中
export interface ChatItem {
id: number;
userAvatar: string;
avatarAlt: string;
userName: string;
sendTime: string;
content: string;
likes: number;
comments: number;
collections: number;
description: string;
labels: string[];
isVerified: boolean;
}
export const chatData: ChatItem[] = [
{
id: 1,
userAvatar: "https://randomuser.me/api/portraits/men/1.jpg",
avatarAlt: "张明的头像",
userName: "张明",
sendTime: "今天 08:30",
content: "刚刚参加完一场很棒的技术分享会,讲的是React 18的新特性。Concurrent Mode和Server Components真的让人印象深刻,感觉未来的前端开发会更加有趣!",
likes: 156,
comments: 32,
collections: 18,
description: "资深前端工程师 / React 技术专家",
labels: ["React", "TypeScript", "前端架构", "性能优化", "开源贡献者"],
isVerified: true
},
{
id: 2,
userAvatar: "https://randomuser.me/api/portraits/women/2.jpg",
avatarAlt: "李小云的头像",
userName: "李小云",
sendTime: "今天 09:15",
content: "分享一个我最近在项目中遇到的性能优化问题:大量数据渲染导致页面卡顿。通过使用虚拟列表和React.memo()成功解决,页面加载速度提升了80%。欢迎交流讨论~",
likes: 234,
comments: 45,
collections: 28,
description: "高级前端开发 / 性能优化专家",
labels: ["性能优化", "React", "虚拟列表", "前端架构"],
isVerified: true
},
{
id: 3,
userAvatar: "https://randomuser.me/api/portraits/men/3.jpg",
avatarAlt: "王大力的头像",
userName: "王大力",
sendTime: "今天 10:42",
content: "推荐一本超棒的技术书籍《深入浅出React和Redux》,对于想深入学习React的同学来说是一本不可多得的好书。书中的案例都很实用,概念讲解也非常清晰。",
likes: 89,
comments: 15,
collections: 42,
description: "技术作家 / React 培训讲师",
labels: ["技术写作", "React", "Redux", "技术分享"],
isVerified: false
},
{
id: 4,
userAvatar: "https://randomuser.me/api/portraits/women/4.jpg",
avatarAlt: "陈佳慧的头像",
userName: "陈佳慧",
sendTime: "今天 11:20",
content: "今天终于解决了困扰团队一周的Bug!原来是在处理异步请求时没有正确处理竞态条件,导致数据更新错乱。分享一下解决方案:使用AbortController和useEffect的cleanup函数完美解决了这个问题。",
likes: 312,
comments: 56,
collections: 33,
description: "全栈工程师 / React Native 专家",
labels: ["React", "React Native", "移动开发", "全栈开发"],
isVerified: true
},
{
id: 5,
userAvatar: "https://randomuser.me/api/portraits/men/5.jpg",
avatarAlt: "刘技术的头像",
userName: "刘技术",
sendTime: "今天 12:05",
content: "最近在研究微前端架构,感觉qiankun框架真的很强大。已经成功将我们的老项目逐步迁移到微前端架构,既保证了系统的稳定性,又提高了团队的开发效率。有同样经历的同学吗?",
likes: 178,
comments: 43,
collections: 25,
description: "架构师 / 微前端专家",
labels: ["微前端", "架构设计", "qiankun", "模块联邦"],
isVerified: true
},
{
id: 6,
userAvatar: "https://randomuser.me/api/portraits/women/6.jpg",
avatarAlt: "赵晓晓的头像",
userName: "赵晓晓",
sendTime: "今天 13:30",
content: "发现一个超实用的VS Code插件:GitHub Copilot!AI辅助编程真的太强大了,特别是在写一些重复性的代码时效率提升明显。推荐给大家!",
likes: 267,
comments: 89,
collections: 54,
description: "开发工具专家 / 效率工程师",
labels: ["开发工具", "VS Code", "AI编程", "效率提升"],
isVerified: false
},
{
id: 7,
userAvatar: "https://randomuser.me/api/portraits/men/7.jpg",
avatarAlt: "孙小明的头像",
userName: "孙小明",
sendTime: "今天 14:15",
content: "今天做了一个有趣的小实验:用React + Three.js开发了一个3D数据可视化组件。效果出乎意料的好,准备开源出来。感兴趣的同学请留言,我会把仓库地址分享出来。",
likes: 423,
comments: 98,
collections: 76,
description: "3D可视化专家 / React 开发者",
labels: ["Three.js", "WebGL", "数据可视化", "React"],
isVerified: true
},
{
id: 8,
userAvatar: "https://randomuser.me/api/portraits/women/8.jpg",
avatarAlt: "周雪的头像",
userName: "周雪",
sendTime: "今天 15:00",
content: "作为一名前端开发,最近开始学习TypeScript,真的改变了我的编程习惯。类型系统不仅让代码更安全,重构时也更有信心。强烈建议还没入门的同学抓紧学起来!",
likes: 345,
comments: 67,
collections: 45,
description: "前端开发工程师 / TypeScript 布道者",
labels: ["TypeScript", "前端开发", "代码质量", "最佳实践"],
isVerified: false
}
];