项目地址
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}><InfoCardavatarUrl={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}
];