当前位置: 首页 > article >正文

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

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

组件描述
该组件适用于论坛,发帖等地方。可以发布信息,表情包,图片。

样式展示

在这里插入图片描述

前置依赖

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

  1. react-icons(提供图标)
  2. emoji-mart(提供emoji表情包)
  3. 在上一个挑战中,制作的Img组件(展示照片)

安装 react-icons

# 使用 npm
npm install react-icons

# 或者使用 yarn
yarn add react-icons

使用的话,大家可以看这个网站。大家进去可以找需要的图标。具体使用里面有介绍,非常简单。
react-icons 图标

安装emoji-mart

# 使用npm
npm install --save emoji-mart @emoji-mart/data @emoji-mart/react

# 使用yarn
yarn add emoji-mart @emoji-mart/data @emoji-mart/react

具体文档,大家可以查看这个地址。emoji-mart 仓库

使用Img
Img的话,大家可以查看我的上一篇文章。
挑战用React封装100个组件【004】

好了,下面我们展示代码。(注意:如果实际使用的时候,大家需要按照实际需要修改,比如对接接口等等

代码展示

ChatBox.tsx
import './ChatBox.css';
import { useState, useEffect, useRef, useCallback } from 'react';
import { BiLaugh, BiImage } from "react-icons/bi";
import data from '@emoji-mart/data'
import Picker from '@emoji-mart/react'
import Img from '../../img/img01/Img';

// 全局常量定义
const MAX_LENGTH = 5000;  // 最大文本长度
const MAX_IMAGES = 9;     // 最大图片数量

// 类型定义
interface ImageItem {
    id: string;      // 图片唯一标识
    url: string;     // 图片URL或base64
    file: File;      // 图片文件对象
}

interface ChatContent {
    text: string;    // 聊天文本内容
    images: ImageItem[]; // 图片列表
}

const ChatBox = () => {
    // =============== 状态管理 ===============
    const [chatContent, setChatContent] = useState<ChatContent>({
        text: '',
        images: []
    });
    const [textLength, setTextLength] = useState(0);          // 当前文本长度
    const [showEmojiPicker, setShowEmojiPicker] = useState(false);  // 表情选择器显示状态

    // =============== DOM引用 ===============
    const textareaRef = useRef<HTMLTextAreaElement>(null);    // 文本框引用
    const emojiPickerRef = useRef<HTMLDivElement>(null);      // 表情选择器引用
    const fileInputRef = useRef<HTMLInputElement>(null);      // 文件输入框引用

    // =============== 文本处理 ===============
    // 调整文本框高度
    const adjustTextareaHeight = useCallback((textarea: HTMLTextAreaElement) => {
        textarea.style.height = 'auto';
        textarea.style.height = `${Math.min(textarea.scrollHeight, 200)}px`;
    }, []);

    // 处理文本输入
    const handleInput = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
        const newText = e.target.value;
        if (newText.length <= MAX_LENGTH) {
            setTextLength(newText.length);
            setChatContent(prev => ({
                ...prev,
                text: newText
            }));
            adjustTextareaHeight(e.target);
        }
    }, [adjustTextareaHeight]);

    // =============== 表情处理 ===============
    // 处理表情选择
    const handleEmojiSelect = useCallback((emoji: any) => {
        if (!textareaRef.current) return;

        const start = textareaRef.current.selectionStart;
        const end = textareaRef.current.selectionEnd;
        const currentText = textareaRef.current.value;
        const newText = currentText.slice(0, start) + emoji.native + currentText.slice(end);

        if (newText.length <= MAX_LENGTH) {
            setTextLength(newText.length);
            setChatContent(prev => ({
                ...prev,
                text: newText
            }));

            // 更新光标位置到表情后面
            setTimeout(() => {
                if (textareaRef.current) {
                    const newPosition = start + emoji.native.length;
                    textareaRef.current.selectionStart = newPosition;
                    textareaRef.current.selectionEnd = newPosition;
                    textareaRef.current.focus();
                    adjustTextareaHeight(textareaRef.current);
                }
            }, 0);
        }
        setShowEmojiPicker(false);
    }, [adjustTextareaHeight]);

    // 切换表情选择器显示状态
    const toggleEmojiPicker = useCallback(() => {
        setShowEmojiPicker(prev => !prev);
    }, []);

    // =============== 图片处理 ===============
    // 创建图片对象
    const createImageItem = useCallback((file: File, url: string): ImageItem => {
        return {
            id: Date.now().toString(),
            url,
            file
        };
    }, []);

    // 处理图片上传
    const handleImageUpload = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
        const files = e.target.files;
        if (!files) return;

        // 检查图片数量是否超出限制
        if (chatContent.images.length + files.length > MAX_IMAGES) {
            alert(`最多只能上传${MAX_IMAGES}张图片`);
            if (fileInputRef.current) {
                fileInputRef.current.value = '';
            }
            return;
        }

        // 处理每个选中的图片文件
        Array.from(files).forEach(file => {
            const reader = new FileReader();
            reader.onload = (e) => {
                if (e.target?.result) {
                    const newImage = createImageItem(file, e.target.result as string);
                    setChatContent(prev => ({
                        ...prev,
                        images: [...prev.images, newImage]
                    }));
                }
            };
            reader.readAsDataURL(file);
        });

        // 清空文件输入框,以便重复选择相同文件
        if (fileInputRef.current) {
            fileInputRef.current.value = '';
        }
    }, [chatContent.images.length, createImageItem]);

    // 处理图片选择按钮点击
    const handleImageClick = useCallback(() => {
        if (chatContent.images.length >= MAX_IMAGES) {
            alert(`最多只能上传${MAX_IMAGES}张图片`);
            return;
        }
        fileInputRef.current?.click();
    }, [chatContent.images.length]);

    // 处理图片删除
    const handleImageDelete = useCallback((id: string) => {
        setChatContent(prev => ({
            ...prev,
            images: prev.images.filter(img => img.id !== id)
        }));
    }, []);

    // =============== 副作用处理 ===============
    // 点击外部关闭表情选择器
    useEffect(() => {
        const handleClickOutside = (event: MouseEvent) => {
            if (emojiPickerRef.current && !emojiPickerRef.current.contains(event.target as Node)) {
                setShowEmojiPicker(false);
            }
        };

        document.addEventListener('mousedown', handleClickOutside);
        return () => document.removeEventListener('mousedown', handleClickOutside);
    }, []);

    // =============== 渲染 ===============
    return (
        <div className="chat-box">
            {/* 文本输入区域 */}
            <div className="chat-input">
                <textarea
                    ref={textareaRef}
                    placeholder="# 要不要发点什么话...."
                    onChange={handleInput}
                    value={chatContent.text}
                    rows={1}
                    maxLength={MAX_LENGTH}
                />
                <div>
                    <span>{textLength}/{MAX_LENGTH}</span>
                </div>
            </div>

            {/* 图片预览区域 */}
            {chatContent.images.length > 0 && (
                <div className='chat-imgs'>
                    {chatContent.images.map((img) => (
                        <Img
                            key={img.id}
                            src={img.url}
                            alt="已上传图片"
                            size="small"
                            onClose={() => handleImageDelete(img.id)}
                        />
                    ))}
                </div>
            )}

            {/* 功能按钮区域 */}
            <div className='chat-functions'>
                <div className='left'>
                    {/* 表情选择器 */}
                    <div>
                        <div onClick={toggleEmojiPicker} className="emoji-trigger">
                            <BiLaugh /> &nbsp;表情
                        </div>
                        {showEmojiPicker && (
                            <div className='emoji-picker' ref={emojiPickerRef}>
                                <Picker
                                    data={data}
                                    onEmojiSelect={handleEmojiSelect}
                                    theme="light"
                                    locale="zh"
                                />
                            </div>
                        )}
                    </div>
                    {/* 图片上传 */}
                    <div onClick={handleImageClick}>
                        <BiImage /> &nbsp;图片
                    </div>
                    <input
                        type="file"
                        ref={fileInputRef}
                        onChange={handleImageUpload}
                        accept="image/*"
                        multiple
                        style={{ display: 'none' }}
                    />
                </div>
                <div className='right'>
                    <button>发布</button>
                </div>
            </div>
        </div>
    );
};

export default ChatBox;
ChatBox.css
/* 聊天框主容器 */
.chat-box {
    width: 100%;
    background-color: #FFFFFF;
    border-radius: 10px;
    padding: 15px;
    box-sizing: border-box;
}

/* 输入区域样式 */
.chat-box .chat-input {
    width: 100%;
    background-color: #F3F4F6;
    box-sizing: border-box;
    border-radius: 10px;
    padding: 20px 20px 10px;
}

/* 文本输入框样式 */
.chat-box .chat-input textarea {
    width: 100%;
    min-height: 45px;
    border: none;
    outline: none;
    background-color: transparent;
    font-size: 14px;
    line-height: 1.5;
    resize: none;
    overflow-y: auto;
    word-break: break-all;
}

/* 滚动条样式 */
.chat-box .chat-input textarea::-webkit-scrollbar {
    width: 4px;
}

.chat-box .chat-input textarea::-webkit-scrollbar-thumb {
    background-color: #d4d4d4;
    border-radius: 2px;
}

.chat-box .chat-input textarea::-webkit-scrollbar-track {
    background: transparent;
}

/* 字数统计容器 */
.chat-box .chat-input div {
    display: flex;
    justify-content: flex-end;
    margin-top: 5px;
}

/* 字数统计文本 */
.chat-box .chat-input span {
    font-size: 14px;
    color: #bbbbbb;
}

/* 功能区域样式 */
.chat-functions {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-top: 15px;
    font-size: 14px;
}

/* 左侧功能区 */
.chat-functions .left {
    display: flex;
    gap: 10px;
}

.chat-functions .left div {
    display: flex;
    align-items: center;
    cursor: pointer;
    position: relative;
}

/* 表情选择器触发器 */
.chat-functions .left .emoji-trigger {
    display: flex;
    align-items: center;
    transition: color 0.3s ease;
}

/* 表情选择器定位 */
.chat-functions .left .emoji-picker {
    position: absolute;
    top: 30px;
    left: -10px;
    z-index: 5;
}

/* 功能按钮悬浮效果 */
.chat-functions .left div:hover {
    color: #f08a5d;
}

/* 发布按钮样式 */
.chat-functions .right button {
    all: unset;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 60px;
    padding: 5px;
    border-radius: 15px;
    background-color: #f08a5d;
    color: #FFFFFF;
    cursor: pointer;
    transition: background-color 0.3s ease;
}

.chat-functions .right button:hover {
    background-color: #f1946c;
}

/* 图片展示区域 */
.chat-box .chat-imgs {
    width: 100%;
    margin-top: 15px;
    display: flex;
    flex-wrap: wrap;
    justify-content: flex-start;
    gap: 15px;
}

使用

App.tsx
import './App.css'
import ChatBox from './components/chatBox/chatBox01/ChatBox';

function App() {
  return (
    <>
      <div className="App">
        <ChatBox />
      </div>
    </>
  );
}

export default App

http://www.kler.cn/a/418882.html

相关文章:

  • STM32的CAN波特率计算
  • 软考高项经验分享:我的备考之路与实战心得
  • 【论文笔记】A Token-level Contrastive Framework for Sign Language Translation
  • registry 删除私有仓库镜像
  • Web开发基础学习——HTML中id 和 class 标识和选择元素的属性的理解
  • 基于Java Springboot在线点餐系统
  • 【linux】(23)对象存储服务-MinIo
  • Linux 僵尸进程和孤儿进程, 进程优先级
  • Android 12.0新增自定义HIDL问题记录
  • 内网穿透步骤
  • Spring Data JPA(二) 高级进阶
  • linux——进程间通信及管道的应用场景
  • 蓝桥杯经验分享
  • 医院分诊管理系统|Java|SSM|VUE| 前后端分离
  • 2. STM32_中断
  • 深入理解 PyTorch .pth 文件和 Python pickle 模块:功能、应用及实际示例
  • 前端学习week8——vue.js
  • 支持向量机算法:原理、实现与应用
  • LeetCode题解:34.在排序数组中查找元素的第一个和最后一个位置【Python题解超详细,二分查找法、index法】,知识拓展:index方法详解
  • [MySQL]流程控制语句
  • SpringCloud书单推荐
  • 深度学习常见数据集处理方法
  • 爬虫专栏第一篇:深入探索爬虫世界:基础原理、类型特点与规范要点全解析
  • npm : 无法加载文件 D:\nodejs\npm.ps1,因为在此系统上禁止运行脚本
  • 云技术基础(泷羽sec)
  • ubuntu配置网络