挑战用React封装100个组件【005】
项目地址
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或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 /> 表情
</div>
{showEmojiPicker && (
<div className='emoji-picker' ref={emojiPickerRef}>
<Picker
data={data}
onEmojiSelect={handleEmojiSelect}
theme="light"
locale="zh"
/>
</div>
)}
</div>
{/* 图片上传 */}
<div onClick={handleImageClick}>
<BiImage /> 图片
</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