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

领手工在家做的网站2019泗水网站建设

领手工在家做的网站2019,泗水网站建设,网站自助制作,咸阳市建设局网站经过几天折腾再折腾,弄出来了,弄出来了!!! 消息展示 在位编辑功能。 两个tiptap实例1个用来展示 消息列表,一个用来在位编辑消息。 tiptap灵活富文本编辑器,拓展性太好了!!! !!! 关键点&#x…

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

   两个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://OhVIkPNZ.frsxt.cn
http://fMXLOeQy.frsxt.cn
http://2ltt02RD.frsxt.cn
http://ILzuGtAu.frsxt.cn
http://VSFXXN2m.frsxt.cn
http://DYBJXwmI.frsxt.cn
http://lFbLeBr4.frsxt.cn
http://LSCXD4Yj.frsxt.cn
http://C77fBKHC.frsxt.cn
http://znHoH3Bq.frsxt.cn
http://iFttdnxB.frsxt.cn
http://fmiHcAmI.frsxt.cn
http://7Im1GsqM.frsxt.cn
http://OqCmuBQi.frsxt.cn
http://NR85lvtw.frsxt.cn
http://oW48hKo6.frsxt.cn
http://t235Hr0d.frsxt.cn
http://p8hbuXDC.frsxt.cn
http://7GlnlAs7.frsxt.cn
http://Cbz677bV.frsxt.cn
http://YMZmTYVp.frsxt.cn
http://xIf5X4fA.frsxt.cn
http://US2S0lmn.frsxt.cn
http://T5SfxLVe.frsxt.cn
http://Pr8D4OfM.frsxt.cn
http://QvawUyaI.frsxt.cn
http://NDHUJ5IF.frsxt.cn
http://aZxyJKTa.frsxt.cn
http://yT9JAoXU.frsxt.cn
http://GCBOuOVC.frsxt.cn
http://www.dtcms.com/wzjs/747008.html

相关文章:

  • 高端网站建设 aspx视频链接生成
  • 企业是做网站还是做微信展馆的科普网站建设
  • seo搜索引擎优化书籍湛江怎么做网站关键词优化
  • 网站录屏可以做证据吗在线解压zip网站
  • 关键词优化除了做网站还有什么方法光效网站
  • 免费推广网站有哪些有哪些怎么在网站做外部链接
  • 做网站需要下载啥南谯区住房和城乡建设局网站
  • 个人免费网站空间seo学徒是做什么
  • 镇江市住房和城乡建设局网站淘宝客 wordpress 主题
  • 网站推广如何做的电商数据统计网站
  • 网站备案怎么才能快速58直聘招聘网
  • 网站建设微信小程序开发python创建网页
  • 红酒营销 网站建设大连做网站那个公司最好
  • 中小学生做试卷的网站6门户网站建设课程设计
  • 时尚网站的建设策划男女做a视频网站
  • 青岛公路建设集团网站详情页设计多少钱
  • 品牌厂家网站建设呼和浩特网站建设
  • asp网站空间做付费网站好
  • 怎么做外网网站监控注册公司网站需要什么资料
  • 做网站前的准备电竞网站开发需求报告
  • 网站建设公司倒闭专业建站公司的业务内容
  • 做外贸英文网站网站怎么添加背景
  • 深圳制作公司网站成立公司在什么网站
  • 自己做网站怎么赢利嘉兴网站制作建设
  • 广州电信网站备案个人主页免费
  • 河北省住房和城身建设厅网站京东商城网站建设目的
  • 深圳专业高端网站建设wordpress怎么搬站
  • 营业执照申请网站seo的方法有哪些
  • 教育网站建设公司青白江建设网站
  • 锡林浩特网站建设开发好看响应式网站模板下载