Bootstrap

挑战用React封装100个组件【007】

项目地址
https://github.com/hismeyy/react-component-100

组件描述
今天的组件是用来展示聊天列表,或者论坛内容列表的组件。配合挑战006的时候开发的组件,可以显示用户的具体信息。

样式展示

在这里插入图片描述
在这里插入图片描述

前置依赖

今天,我分享的组件,需要用到的依赖有:

  1. react-icons(提供图标)
  2. 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
    }
];
;