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

React + TipTap 富文本编辑器 实现消息列表展示,类似Slack,Deepseek等对话框功能

   经过几天折腾再折腾,弄出来了,弄出来了!!! 消息展示 + 在位编辑功能。

   两个tiptap实例1个用来展示 消息列表,一个用来在位编辑消息。

   tiptap灵活富文本编辑器,拓展性太好了!!! !!!

  关键点:实现只用了两个TipTap 实例。

每条消息创建一个tiptap实例简单AI可以给你直接生成,用两个tiptap实例完成就难了。出于对性能考虑,迭代几个版本更新,选用两个实例,完成所有工作,性能好了编码复杂度高了不少。

1.TipTap 展示AI聊天消息思路,自定拓展来显示结构内容

content: [{ type: 'text', text: '你好,我是 AI 🤖' },{ type: 'heading', level: 3, text: '功能介绍' },{type: 'bulletList',items: ['文字回复', '插入图片', '代码高亮'],},{ type: 'img', src: 'https://placekitten.com/200/200' },{type: 'codeBlock',language: 'js',code: 'console.log("你好 Tiptap!")',},],

 2.Tiptap拓展ChatMessage,消息展示+在位编辑

 renderContent把消息结构体渲染为reac标签

const renderContent = (content: any[]) => {return content.map((item, index) => {const key = `${item.type}-${index}` // 构造一个稳定的 keyswitch (item.type) {case 'text':return <p key={key}>{item.text}</p>case 'img':return (<imgkey={key}src={item.src}alt="chat image"style={{ maxWidth: '100%', margin: '0.5em 0' }}/>)case 'bulletList':return (<ul key={key} className="list-disc list-inside">{item.items.map((text: string, i: number) => (<li key={`bullet-${index}-${i}`}>{text}</li>))}</ul>)case 'heading':const HeadingTag = `h${item.level || 2}` as keyof JSX.IntrinsicElementsreturn <HeadingTag key={key}>{item.text}</HeadingTag>case 'codeBlock':return (<pre key={key}><code className={`language-${item.language || 'js'}`}>{item.code}</code></pre>)default:return ''}})
}

在位编辑html 传给shareEditor在位编辑。

 const startEdit = () => {if (!sharedEditor) returnconst html = ReactDOMServer.renderToStaticMarkup(<>{renderContent(content)}</>)sharedEditor.commands.setContent(html)setIsEditing(true)}

完整ChatMessageEx.tsx

import { Node, mergeAttributes } from '@tiptap/core'
import { ReactNodeViewRenderer } from '@tiptap/react'
import React, { useState } from 'react'
import { NodeViewWrapper } from '@tiptap/react'
import ReactDOMServer from 'react-dom/server'
import { EditorContent, Editor } from "@tiptap/react";export interface ChatMessageOptions {HTMLAttributes: Record<string, any>sharedEditor?: Editor | nullonEdit?: (node: any, updateAttributes: (attrs: any) => void) => void
}declare module '@tiptap/core' {interface Commands<ReturnType> {chatMessage: {insertChatMessage: (props: {author: stringcontent: any[] // structured array contentavatar?: stringtime?: string}) => ReturnType}}
}const renderContent = (content: any[]) => {return content.map((item, index) => {const key = `${item.type}-${index}` // 构造一个稳定的 keyswitch (item.type) {case 'text':return <p key={key}>{item.text}</p>case 'img':return (<imgkey={key}src={item.src}alt="chat image"style={{ maxWidth: '100%', margin: '0.5em 0' }}/>)case 'bulletList':return (<ul key={key} className="list-disc list-inside">{item.items.map((text: string, i: number) => (<li key={`bullet-${index}-${i}`}>{text}</li>))}</ul>)case 'heading':const HeadingTag = `h${item.level || 2}` as keyof JSX.IntrinsicElementsreturn <HeadingTag key={key}>{item.text}</HeadingTag>case 'codeBlock':return (<pre key={key}><code className={`language-${item.language || 'js'}`}>{item.code}</code></pre>)default:return ''}})
}const MessageView = ({ node, ...props }: any) => {const { author, content, avatar, time } = node.attrsconst [isEditing, setIsEditing] = useState(false)const sharedEditor = props.sharedEditor as Editorconst startEdit = () => {if (!sharedEditor) returnconst html = ReactDOMServer.renderToStaticMarkup(<>{renderContent(content)}</>)sharedEditor.commands.setContent(html)setIsEditing(true)}const saveEdit = () => {// 消息发送到服务器来更新setIsEditing(false)}return (<NodeViewWrapperas="div"data-type="chat-message"className="group relative flex items-start gap-2 pl-1 hover:bg-gray-100 dark:hover:bg-gray-900 pt-1 pb-1"><div className="flex items-start w-full"><div className="w-8 h-8 rounded-full overflow-hidden absolute top-2 left-3 z-10"><img src={avatar} className="w-full h-full object-cover" /></div><div className="pl-12 relative w-full"><div className="flex mb-1 text-xs text-gray-500 dark:text-gray-400"><span className="font-medium">{author}</span><span className="ml-1">{time}</span></div>{!isEditing ? (<div className="text-sm">{renderContent(content)}</div>) : (<div className="border p-2 rounded dark:bg-gray-800"><EditorContent editor={sharedEditor} /></div>)}<div className="absolute -top-1 right-0 hidden group-hover:flex gap-2 z-10 bg-white dark:bg-gray-800 dark:text-white shadow">{!isEditing ? (<buttononClick={startEdit}className="text-xs px-2 py-1 bg-gray-200 dark:bg-gray-700 rounded">编辑</button>) : (<buttononClick={saveEdit}className="text-xs px-2 py-1 bg-blue-500 text-white rounded">保存</button>)}<buttononClick={() => alert(`转发消息`)}className="text-xs px-2 py-0.5 hover:bg-gray-100 dark:hover:bg-gray-700 p-1">回复</button><buttononClick={() => alert(`你点赞了`)}className="text-xs px-2 py-0.5 hover:bg-gray-100 dark:hover:bg-gray-700 p-1">收到</button></div></div></div></NodeViewWrapper>)
}const ChatMessageEx = Node.create<ChatMessageOptions>({name: 'chatMessage',group: 'block',atom: true,selectable: true,addOptions() {return {HTMLAttributes: {},sharedEditor: null,onEdit: undefined,}},addAttributes() {return {author: { default: 'User' },content: { default: [] },avatar: { default: '' },time: { default: '' },side: { default: 'left' },}},parseHTML() {return [{ tag: 'div[data-type="chat-message"]' }]},renderHTML({ HTMLAttributes }) {return ['div', mergeAttributes(HTMLAttributes, { 'data-type': 'chat-message' })]},addNodeView() {return ReactNodeViewRenderer((props) => (<MessageView{...props}sharedEditor={this.options.sharedEditor}onEdit={this.options.onEdit}/>))},addCommands() {return {insertChatMessage:({ author, content, avatar, time }) =>({ chain, state }) => {const endPos = state.doc.content.sizereturn chain().insertContent([{type: 'chatMessage',attrs: {author,content: content,avatar,time,},},{ type: 'paragraph' },]).focus(endPos).run()},}},
})export default ChatMessageEx

3.使用ChatMessageEx拓展

为chatMessage传入一个 共享sharedEditor


const shardEditor = useEditor({extensions: [StarterKit,ChatMessageEx,Placeholder.configure({placeholder: "# 给发送消息",})],editable: true,})const editor = useEditor({extensions: [StarterKit,ChatMessageEx.configure({sharedEditor: shardEditor}),Placeholder.configure({placeholder: "# 给发送消息",})],editable: false})

完整channel.tsx

// Channel.tsx import React, { useState, createContext, useEffect, useRef } from "react";
import useChannelsStore from "@/Stores/useChannelListStore";
import { MessageSquare, Settings, Folder, Plus, Pencil, Check } from "lucide-react";
import InputMessage from "@/Components/Tiptap/InputMessage";
import { useMessageStore } from '@/Stores/UseChannelMessageStore' // 引入 Zustand store
import StarterKit from '@tiptap/starter-kit'
import { useEditor, EditorContent, Editor } from "@tiptap/react";
import TurndownService from 'turndown'
import ChatMessageEx from "@/Components/Tiptap/ChatMessageEx";
import Placeholder from '@tiptap/extension-placeholder'interface MessageItemProps {msg: {id: string;content: string;dateTime: string;};editor: Editor;updateMessage: (id: string, newContent: string) => void;
}const TabB = () => <div className="p-4">这是选项卡 B 的内容</div>;
const TabC = () => <div className="p-4">这是选项卡 C 的内容</div>;const ChatMessages = () => {const shardEditor = useEditor({extensions: [StarterKit,ChatMessageEx,Placeholder.configure({placeholder: "# 给发送消息",})],editable: true,})const editor = useEditor({extensions: [StarterKit,ChatMessageEx.configure({sharedEditor: shardEditor}),Placeholder.configure({placeholder: "# 给发送消息",})],editable: false})const onInputMessage = () => {editor?.commands.insertChatMessage({author: '小助手',time: '11:11 AM',avatar: 'https://i.pravatar.cc/32?img=5',content: [{ type: 'text', text: '你好,我是 AI 🤖' },{ type: 'heading', level: 3, text: '功能介绍' },{type: 'bulletList',items: ['文字回复', '插入图片', '代码高亮'],},{ type: 'img', src: 'https://placekitten.com/200/200' },{type: 'codeBlock',language: 'js',code: 'console.log("你好 Tiptap!")',},],})}const onOutMessage = () => {console.log("onOutMessage", editor?.getJSON());}return (// 1.显示高度<div className=" h-full flex flex-col "><button className=" cursor-pointer hover:bg-amber-400" onClick={() => onInputMessage()}>插入信息</button><button className=" cursor-pointer hover:bg-amber-400" onClick={() => onOutMessage()}>显示信息</button>{/* 滚动 显示内容 */}<div className=" p-3 pl-0 flex-1  overflow-y-scroll  custom-scrollbar  "><EditorContent editor={editor} /></div><div className="w-full  min-h-12 "><InputMessage></InputMessage></div></div>)
};const Channel: React.FC = () => {const { currentChannel } = useChannelsStore();const [activeTab, setActiveTab] = useState("chatMessage");// 选项卡列表,每个选项卡增加 `icon` 属性const [tabs, setTabs] = useState([{ id: "chatMessage", name: "消息", icon: <MessageSquare size={16} />, component: <ChatMessages /> },{ id: "tabB", name: "文件", icon: <Folder size={16} />, component: <TabB /> },{ id: "tabC", name: "设置", icon: <Settings size={16} />, component: <TabC /> },]);// 添加新选项卡const addTab = () => {const newTabId = `tab${tabs.length + 1}`;const newTab = {id: newTabId,name: `选项卡${tabs.length + 1}`,icon: <Folder size={16} />, // 默认使用 Folder 图标component: <div className="p-4">这是 {newTabId} 的内容</div>,};setTabs([...tabs, newTab]);};return (<div className="flex flex-col h-full w-full justify-center">{/* 顶部 */}<div className="h-20 justify-between border-b flex flex-col border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-200"><div className="p-2 text-[16px] font-bold cursor-pointer"># {currentChannel?.name}</div>{/* 选项卡导航 */}<div className="flex gap-2 ml-2">{tabs.map((tab) => (<divkey={tab.id}className={`pl-2 pr-2 pt-1 pb-1  flex items-center gap-1 cursor-pointer rounded-t-sm hover:bg-gray-200 dark:hover:bg-gray-700 ${activeTab === tab.id ? "border-b-2 bg-gray-200 dark:bg-gray-700 font-bold" : ""}`}onClick={() => setActiveTab(tab.id)}>{tab.icon} {/* 渲染图标 */}{tab.name}</div>))}<divclassName="ml-2 p-1  mb-1  mt-1 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-700 rounded-full"onClick={addTab}><Plus size={18} /></div></div></div>{/* 内容区 */}<div className="border-gray-300 dark:border-gray-600 h-full overflow-hidden">{tabs.find((tab) => tab.id === activeTab)?.component}</div></div>)
};export default Channel;

React + TipTap 富文本编辑器 实现消息列表展示,类似Slack,Deepseek等对话框功能

相关文章:

React + TipTap 富文本编辑器 实现消息列表展示,类似Slack,Deepseek等对话框功能

经过几天折腾再折腾&#xff0c;弄出来了&#xff0c;弄出来了&#xff01;&#xff01;&#xff01; 消息展示 在位编辑功能。 两个tiptap实例1个用来展示 消息列表&#xff0c;一个用来在位编辑消息。 tiptap灵活富文本编辑器&#xff0c;拓展性太好了!!! !!! 关键点&#x…...

博途 TIA Portal之1200做主站与汇川EASY的TCP通讯

前言,虽然已经做了几篇关于TCP通讯的文章,但是不同的PLC之间的配合可能不同,下面将演示这种差异。 关于汇川EASY做从站的配置请参见下方链接文章:汇川EASY系列之以太网通讯(套接字socket做从站)_汇川以太网tcp套接字fb块-CSDN博客 1、硬件准备: 1200PLC,汇川EASY320…...

蓝桥杯速成刷题清单(上)

一、1.排序 - 蓝桥云课 &#xff08;快速排序&#xff09;算法代码&#xff1a; #include <bits/stdc.h> using namespace std; const int N 5e5 10; int a[N];int main() {int n;cin >> n;for (int i 0; i < n; i) {cin >> a[i];}sort(a, a n);for …...

力扣第444场周赛

这次力扣周赛对我来说难度确实大, 只做出两题, 但还是想分享一下的做题经验和感受 1. 移除最小数对使数组有序 I 题目链接&#xff1a;力扣 给你一个数组 nums&#xff0c;你可以执行以下操作任意次数&#xff1a; 选择 相邻 元素对中 和最小 的一对。如果存在多个这样的对&a…...

Redis 持久化机制详解:RDB/AOF 过程、优缺点及配置。Redis持久化中的Fork与Copy-on-Write技术解析。

Redis 持久化机制详解&#xff1a;RDB/AOF 过程、优缺点及配置 一、RDB 持久化过程及特性 核心机制 生成快照&#xff1a;通过 fork 子进程生成内存数据的二进制快照文件&#xff08;.rdb&#xff09;&#xff0c;父进程继续处理请求。写时复制&#xff08;Copy-On-Write&…...

Go并发背后的双引擎:CSP通信模型与GMP调度|Go语言进阶(4)

为什么需要理解CSP与GMP&#xff1f; 当我们启动一个Go程序时&#xff0c;可能会创建成千上万个goroutine&#xff0c;它们是如何被调度到有限的CPU核心上的&#xff1f;为什么Go能够如此轻松地处理高并发场景&#xff1f;为什么有时候我们的并发程序会出现奇怪的性能瓶颈&…...

docker内安装达梦8数据库

1. 其他机器上实现挂载ISO # 1. 确保挂载点目录存在&#xff08;你已经创建了dm8目录&#xff09; ls -ld dm8# 2. 使用正确的mount命令挂载ISO sudo mount -o loop dm8_20250117_HWarm920_kylin10_sp1_64.iso dm8# 3. 验证是否挂载成功 mount | grep dm8 ls dm82. docker内运…...

UDP怎么样实现可靠传输?

如果需要在基于UDP的应用中实现可靠传输&#xff08;例如确保数据不丢失、按顺序到达等&#xff09;&#xff0c;通常需要在应用层实现相应的机制。 1. 确认应答机制 应用层可以使用确认应答机制来确保数据的可靠传输。当发送方发送一个数据包时&#xff0c;接收方收到数据包…...

代码随想录算法训练营Day25

一、力扣93.复原IP地址【medium】 题目链接&#xff1a;力扣93.复原IP地址 left x300 视频链接&#xff1a;代码随想录 1、思路 时间复杂度&#xff1a; O ( n ) O(n) O(n) 2、代码 class Solution:def restoreIpAddresses(self, s: str) -> List[str]:n len(s)ans []…...

Linux服务器——Samba服务器

简介 Samba 是一个开源的跨平台文件共享服务​​&#xff0c;允许 Linux/Unix 系统与 Windows 系统实现文件和打印机的共享与互操作。其核心协议为 ​​SMB/CIFS​​&#xff08;Server Message Block / Common Internet File System&#xff09;&#xff0c;是 Windows 网络中…...

华为网路设备学习-17

目录 一、加密算法 二、验证算法 三、IPsec协议 1.IKE协议&#xff08;密钥交换协议&#xff09; ①‌ISAKMP&#xff08;Internet Security Association and Key Management Protocol&#xff09;互联网安全关联和密钥管理协议 ②安全关联&#xff08;SA&#xff09; ③…...

各开源协议一览

在 GitHub 上&#xff0c;开源项目通常会使用一些常见的开源协议来定义项目的使用、修改和分发规则。以下是目前 GitHub 上最常见的几种开源协议及其差异和示例说明&#xff1a; TL;DR 协议宽松程度是否强制开源专利保护适用场景MIT最宽松否无希望代码被广泛使用Apache 2.0宽松…...

解决python manage.py shell ModuleNotFoundError: No module named xxx

报错如下&#xff1a; python manage.py shellTraceback (most recent call last):File "/Users/z/Documents/project/c/manage.py", line 10, in <module>execute_from_command_line(sys.argv)File "/Users/z/.virtualenvs/c/lib/python3.12/site-packa…...

机器学习12-集成学习-案例

参考 【数据挖掘】基于XGBoost的垃圾短信分类与预测 【分类】使用XGBoost算法对信用卡交易进行诈骗预测 银行卡电信诈骗危险预测(LightGBM版本) 【数据挖掘】基于XGBoost的垃圾短信分类与预测 基于XGBoost的垃圾短信分类与预测 我分享了一个项目给你《【数据挖掘】基于XG…...

使用Ubuntu18恢复群晖nas硬盘数据外接usb

使用Ubuntu18恢复群晖nas硬盘数据外接usb 1. 接入硬盘2.使用Ubuntu183.查看nas硬盘信息3. 挂载nas3.1 挂载损坏nas硬盘(USB)3.2 挂载当前运行的nas 4. 拷贝数据分批传输 5. 新旧数据对比 Synology NAS 出现故障&#xff0c;DS DiskStation损坏&#xff0c;则可以使用计算机和 U…...

微服务系统记录

记录下曾经工作涉及到微服务的相关知识。 1. 架构设计与服务划分 关键内容 领域驱动设计&#xff08;DDD&#xff09;&#xff1a; 利用领域模型和限界上下文&#xff08;Bounded Context&#xff09;拆分业务&#xff0c;明确服务边界。通过事件风暴&#xff08;Event Storm…...

【数据库原理及安全实验】实验二 数据库的语句操作

目录 指导书原文 实操备注 指导书原文 【实验目的】 1) 掌握使用SQL语言进行数据操纵的方法。 【实验原理】 1) 面对三个关系表student&#xff0c;course&#xff0c;sc。利用SQL语句向表中插入数据&#xff08;insert&#xff09;&#xff0c;然后对数据进行delete&…...

python 微信小程序支付、查询、退款使用wechatpy库

首先使用 wechatpy 库&#xff0c;执行以下命令进行安装 pip install wechatpy 1、 直连商户支付 import logging from django.http import JsonResponse from django.views.decorators.http import require_http_methods from wechatpy.pay import WeChatPay from wechatpy.…...

蓝桥杯备赛学习笔记:高频考点与真题预测(C++/Java/python版)

2025蓝桥杯备赛学习笔记 ——高频考点与真题预测 一、考察趋势分析 通过对第13-15届蓝桥杯真题的分析&#xff0c;可以发现题目主要围绕基础算法、数据结构、数学问题、字符串处理、编程语言基础展开&#xff0c;且近年逐渐增加动态规划、图论、贪心算法等较难题目。 1. 基…...

【BFT帝国】20250409更新PBFT总结

2411 2411 2411 Zhang G R, Pan F, Mao Y H, et al. Reaching Consensus in the Byzantine Empire: A Comprehensive Review of BFT Consensus Algorithms[J]. ACM COMPUTING SURVEYS, 2024,56(5).出版时间: MAY 2024 索引时间&#xff08;可被引用&#xff09;: 240412 被引:…...

Linux-CentOS-7—— 配置静态IP地址

文章目录 CentOS-7——配置静态IP地址VMware workstation的三种网络模式配置静态IP地址1. 编辑虚拟网络2. 确定网络接口名称3. 切换到网卡所在的目录4. 编辑网卡配置文件5. 查看网卡文件信息6. 重启网络服务7. 测试能否通网8. 远程虚拟主机&#xff08;可选&#xff09; 其他补…...

Jupyter Lab 无法启动 Kernel 问题排查与解决总结

&#x1f4c4; Jupyter Lab 无法启动 Kernel 问题排查与解决总结 一、问题概述 &#x1f6a8; 现象描述&#xff1a; 用户通过浏览器访问远程服务器的 Jupyter Lab 页面&#xff08;http://xx.xx.xx.xx:8891/lab&#xff09;后&#xff0c;.ipynb 文件可以打开&#xff0c;但无…...

算法训练之位运算

♥♥♥~~~~~~欢迎光临知星小度博客空间~~~~~~♥♥♥ ♥♥♥零星地变得优秀~也能拼凑出星河~♥♥♥ ♥♥♥我们一起努力成为更好的自己~♥♥♥ ♥♥♥如果这一篇博客对你有帮助~别忘了点赞分享哦~♥♥♥ ♥♥♥如果有什么问题可以评论区留言或者私信我哦~♥♥♥ ✨✨✨✨✨✨ 个…...

linux入门三:Linux 编辑器

一、轻量级编辑器&#xff1a;快速上手的首选 1.1 Leafpad&#xff1a;极简主义的轻量之选 核心特点 轻量快速&#xff1a;体积小、启动快&#xff0c;资源占用极低&#xff0c;适合低配设备或快速编辑简单文件。 无复杂功能&#xff1a;仅支持基础文本编辑&#xff0c;界面…...

C++设计模式+异常处理

#include <iostream> #include <cstring> #include <cstdlib> #include <unistd.h> #include <sstream> #include <vector> #include <memory> #include <stdexcept> // 包含异常类using namespace std;// 该作业要求各位写一…...

HttpServletRequest是什么

HttpServletRequest 是 Java Servlet API 中的一个接口&#xff0c;表示 HTTP 请求对象。它封装了客户端&#xff08;如浏览器&#xff09;发送到服务器的请求信息&#xff0c;并提供了访问这些信息的方法。 1. 基本概念 作用&#xff1a; HttpServletRequest 提供了一种机制&…...

checkra1n越狱出现的USB error -10问题解决

使用checkra1n进行越狱是出现&#xff1a; 解决办法(使用命令行进行越狱)&#xff1a; 1. cd /Applications/checkra1n.app/Contents/MacOS 2. ./checkra1n -cv 3. 先进入恢复模式 a .可使用爱思助手 b. 或者长按home,出现关机的滑条&#xff0c;同时按住home和电源键&#…...

golang-defer延迟机制

defer延迟机制 defer是什么 defer是go中一种延迟调用机制。 执行时机 defer后面的函数只有在当前函数执行完毕后才能执行。 执行顺序 将延迟的语句按defer的逆序进行执行&#xff0c;也就是说先被defer的语句最后被执行&#xff0c;最后被defer的语句&#xff0c;最先被执…...

【小沐学Web3D】three.js 加载三维模型(Angular)

文章目录 1、简介1.1 three.js1.2 angular.js 2、three.js Angular.js结语 1、简介 1.1 three.js Three.js 是一款 webGL&#xff08;3D绘图标准&#xff09;引擎&#xff0c;可以运行于所有支持 webGL 的浏览器。Three.js 封装了 webGL 底层的 API &#xff0c;为我们提供了…...

一种替代DOORS在WORD中进行需求管理的方法 (二)

一、前景 参考&#xff1a; 一种替代DOORS在WORD中进行需求管理的方法&#xff08;基于WORD插件的应用&#xff09;_doors aspice-CSDN博客 二、界面和资源 WORD2013/WORD2016 插件 【已使用该工具通过第三方功能安全产品认证】&#xff1a; 1、 核心功能 1、需求编号和跟…...