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等對話框功能

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/76527.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/76527.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/76527.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Ubuntu搭建Pytorch環境

Ubuntu搭建Pytorch環境 例如&#xff1a;第一章 Python 機器學習入門之pandas的使用 提示&#xff1a;寫完文章后&#xff0c;目錄可以自動生成&#xff0c;如何生成可參考右邊的幫助文檔 文章目錄 Ubuntu搭建Pytorch環境前言一、Anaconda二、Cuda1.安裝流程2、環境變量&#…

Sping Cloud配置和注冊中心

1.Nacos實現原理了解嗎&#xff1f; Nacos是注冊中心&#xff0c;主要是幫助我們管理服務列表。Nacos的實現原理大概可以從下面三個方面來講&#xff1a; 服務注冊與發現&#xff1a;當一個服務實例啟動時&#xff0c;它會向Nacos Server發送注冊請求&#xff0c;將自己的信息…

C++筆記之父類引用是否可以訪問到子類特有的屬性?

C++筆記之父類引用是否可以訪問到子類特有的屬性? code review! 參考筆記 1.C++筆記之在基類和派生類之間進行類型轉換的所有方法 文章目錄 C++筆記之父類引用是否可以訪問到子類特有的屬性?1.主要原因2.示例代碼3.說明4.如何訪問子類特有的屬性5.注意事項6.總結在 C++ 中,…

JavaScript逆向工程:如何判斷對稱加密與非對稱加密

在現代Web應用安全分析中&#xff0c;加密算法的識別是JavaScript逆向工程的關鍵環節。本文將詳細介紹如何在逆向工程中判斷JavaScript代碼使用的是對稱加密還是非對稱加密。 一、加密算法基礎概念 1. 對稱加密 (Symmetric Encryption) 特點&#xff1a;加密和解密使用相同的…

物理備份工具 BRM vs gs_probackup

什么是BRM 上一篇文章講了openGauss的物理備份工具gs_probackup&#xff0c;今天來說說BRM備份工具。 BRM備份恢復工具全稱為&#xff1a;Backup and Recovery Manager&#xff0c;是MogDB基于opengauss的備份工具 gs_probackup 做了一些封裝和優化,面向MogDB數據庫實現備份和…

問問lua怎么寫DeepSeek,,,,,

很坦白說&#xff0c;這十年&#xff0c;我幾乎沒辦法從互聯網找到這個這樣的代碼&#xff0c;互聯網引擎找不到&#xff0c;我也沒有很大的“追求”要傳承&#xff0c;或者要宣傳什么&#xff1b;直到DeepSeek的出現 兄弟&#xff0c;Deepseek現在已經比你更了解你樓下的超市…

react+Tesseract.js實現前端拍照獲取/選擇文件等文字識別OCR

需求背景 在開發過程中可能會存在用戶上傳一張圖片后下方需要自己識別出來文字數字等信息&#xff0c;有的時候會通過后端來識別后返回&#xff0c;但是也會存在純前端去識別的情況&#xff0c;這個時候就需要使用到Tesseract.js這個庫了 附Tesseract.js官方&#xff08;htt…

藍橋杯考前復盤

明天就是考試了&#xff0c;適當的停下刷題的步伐。 靜靜回望、思考、總結一下&#xff0c;我走過的步伐。 考試不是結束&#xff0c;他只是檢測這一段時間學習成果的工具。 該繼續走的路&#xff0c;還是要繼續走的。 只是最近&#xff0c;我偶爾會感到迷惘&#xff0c;看…

前端-Vue3

1. Vue3簡介 2020年9月18日&#xff0c;Vue.js發布版3.0版本&#xff0c;代號&#xff1a;One Piece&#xff08;n 經歷了&#xff1a;4800次提交、40個RFC、600次PR、300貢獻者 官方發版地址&#xff1a;Release v3.0.0 One Piece vuejs/core 截止2023年10月&#xff0c;最…

[ctfshow web入門] web39

信息收集 題目發生了微妙的變化&#xff0c;只過濾flag&#xff0c;include后固定跟上了.php。且沒有了echo $flag;&#xff0c;雖說本來就沒什么用 if(isset($_GET[c])){$c $_GET[c];if(!preg_match("/flag/i", $c)){include($c.".php");} }else{…

【動手學深度學習】LeNet:卷積神經網絡的開山之作

【動手學深度學習】LeNet&#xff1a;卷積神經網絡的開山之作 1&#xff0c;LeNet卷積神經網絡簡介2&#xff0c;Fashion-MNIST圖像分類數據集3&#xff0c;LeNet總體架構4&#xff0c;LeNet代碼實現4.1&#xff0c;定義LeNet模型4.2&#xff0c;定義模型評估函數4.3&#xff0…

代碼隨想錄第15天:(二叉樹)

一、二叉搜索樹的最小絕對差&#xff08;Leetcode 530&#xff09; 思路1 &#xff1a;中序遍歷將二叉樹轉化為有序數組&#xff0c;然后暴力求解。 class Solution:def __init__(self):# 初始化一個空的列表&#xff0c;用于保存樹的節點值self.vec []def traversal(self, r…

計算機操作系統-【死鎖】

文章目錄 一、什么是死鎖&#xff1f;死鎖產生的原因&#xff1f;死鎖產生的必要條件&#xff1f;互斥條件請求并保持不可剝奪環路等待 二、處理死鎖的基本方法死鎖的預防摒棄請求和保持條件摒棄不可剝奪條件摒棄環路等待條件 死鎖的避免銀行家算法案例 提示&#xff1a;以下是…

vue拓撲圖組件

vue拓撲圖組件 介紹技術棧功能特性快速開始安裝依賴開發調試構建部署 使用示例演示截圖組件源碼 介紹 一個基于 Vue3 的拓撲圖組件&#xff0c;具有以下特點&#xff1a; 1.基于 vue-flow 實現&#xff0c;提供流暢的拓撲圖展示體驗 2.支持傳入 JSON 對象自動生成拓撲結構 3.自…

go 通過匯編分析函數傳參與返回值機制

文章目錄 概要一、前置知識二、匯編分析2.1、示例2.2、匯編2.2.1、 寄存器傳值的匯編2.2.2、 棧內存傳值的匯編 三、拓展3.1 了解go中的Duff’s Device3.2 go tool compile3.2 call 0x46dc70 & call 0x46dfda 概要 在上一篇文章中&#xff0c;我們研究了go函數調用時的棧布…

python-1. 找單獨的數

問題描述 在一個班級中&#xff0c;每位同學都拿到了一張卡片&#xff0c;上面有一個整數。有趣的是&#xff0c;除了一個數字之外&#xff0c;所有的數字都恰好出現了兩次。現在需要你幫助班長小C快速找到那個拿了獨特數字卡片的同學手上的數字是什么。 要求&#xff1a; 設…

算法學習C++需注意的基本知識

文章目錄 01_算法中C需注意的基本知識cmath頭文件一些計算符ASCII碼表數據類型長度運算符cout固定輸出格式浮點數的比較max排序自定義類型字符的大小寫轉換與判斷判斷字符是數字還是字母 02_數據結構需要注意的內容1.stringgetline函數的使用string::findsubstr截取字符串strin…

從零開始寫android 的智能指針

Android中定義了兩種智能指針類型&#xff0c;一種是強指針sp&#xff08;strong pointer&#xff09;&#xff0c;源碼中的位置在system/core/include/utils/StrongPointer.h。另外一種是弱指針&#xff08;weak pointer&#xff09;。其實稱之為強引用和弱引用更合適一些。強…

【leetcode hot 100 152】乘積最大子數組

錯誤解法&#xff1a;db[i]表示以i結尾的最大的非空連續&#xff0c;動態規劃&#xff1a;dp[i] Math.max(nums[i], nums[i] * dp[i - 1]); class Solution {public int maxProduct(int[] nums) {int n nums.length;int[] dp new int[n]; // db[i]表示以i結尾的最大的非空連…

圖論整理復習

回溯&#xff1a; 模板&#xff1a; void backtracking(參數) {if (終止條件) {存放結果;return;}for (選擇&#xff1a;本層集合中元素&#xff08;樹中節點孩子的數量就是集合的大小&#xff09;) {處理節點;backtracking(路徑&#xff0c;選擇列表); // 遞歸回溯&#xff…