项目地址
https://github.com/hismeyy/react-component-100
组件描述
该组件适用于论坛,发帖等地方。可以发布信息,表情包,图片。
样式展示
前置依赖
今天,我分享的组件,需要用到的依赖有:
- react-icons(提供图标)
- emoji-mart(提供emoji表情包)
- 在上一个挑战中,制作的
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或base64file: 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"><textarearef={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) => (<Imgkey={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 /> 表情</div>{showEmojiPicker && (<div className='emoji-picker' ref={emojiPickerRef}><Pickerdata={data}onEmojiSelect={handleEmojiSelect}theme="light"locale="zh"/></div>)}</div>{/* 图片上传 */}<div onClick={handleImageClick}><BiImage /> 图片</div><inputtype="file"ref={fileInputRef}onChange={handleImageUpload}accept="image/*"multiplestyle={{ 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