一、适用场景
·在线研讨会:主讲人进行视频演讲时,参会者可以通过文字提问或讨论。
·远程协作:团队成员在沟通时,可发送文字、图片、文件等。
·直播教学:学生可以在聊天区发送弹幕或问题,老师实时查看并解答,增强课堂互动性。
二、前提条件
在接入功能前,请确保满足以下条件:
·用户状态:用户已经通过 useLoginState 完成登录鉴权,参见 接入概览,并已作为一个房间的房主或成员在房间内,参考 房间管理。
·环境依赖:项目已正确安装并引入 tuikit-atomicx-vue3(Vue 3)或 tuikit-atomicx-react(React)。
三、实现会中聊天功能
本功能提供两种集成方案,你可以根据业务需求选择最适合的一种:
·方案一(推荐):使用 UI 组件快速集成。直接引入 tuikit-atomicx-vue3 和 tuikit-atomicx-react 提供的消息列表组件(MessageList)和消息输入组件(MessageInput),开发成本最低。
·方案二(高级):使用底层 Store API 自定义集成。基于 atomicx-core SDK 的 MessageListStore 和 MessageInputStore 自行实现 UI 和交互逻辑,灵活度最高。
会中聊天
四、方案一:使用 UI 组件快速集成
tuikit-atomicx-vue3 及 tuikit-atomicx-react 提供了两个核心组件:
·MessageList:消息列表组件,展示聊天记录,支持消息的复制、撤回、删除等操作。可通过 ref 调用 scrollToBottom 等方法。
·MessageInput:消息输入组件,支持文本、表情、图片、文件发送,并可根据禁言状态自动禁用。
五、步骤1:数据初始化
在用户加入房间成功后,需要调用 setActiveConversation 接口初始化聊天数据。Vue 3 中建议使用 watch 监听 currentRoom 的变化;React 中使用 useEffect。
注意:说明:
Vue3Reactimport { watch } from 'vue';import { useChatContext } from 'tuikit-atomicx-vue3/chat';import { useLoginState, useRoomState } from 'tuikit-atomicx-vue3/room';function useChatConversationSync() { const { setActiveConversation } = useChatContext(); const { currentRoom } = useRoomState(); const { loginUserInfo } = useLoginState(); // roomId 或登录用户变化时绑定活跃会话 watch( [() => currentRoom.value?.roomId, () => loginUserInfo.value?.userId], ([roomId, userId]) => { if (!userId) return; if (!roomId) { // 退房时清空活跃会话 setActiveConversation(''); return; } // 关键步骤:设置活跃会话 ID,格式为 "GROUP" + roomId setActiveConversation(`GROUP${roomId}`); }, { immediate: true }, );}import { useEffect } from 'react';import { useChatContext } from 'tuikit-atomicx-react/chat';import { useRoomState, useLoginState } from 'tuikit-atomicx-react/room';function useChatConversationSync() { const { setActiveConversation } = useChatContext(); const { currentRoom } = useRoomState(); const { loginUserInfo } = useLoginState(); // roomId 或登录用户变化时绑定活跃会话 useEffect(() => { const roomId = currentRoom?.roomId; if (!loginUserInfo?.userId) return; if (!roomId) { // 退房时清空活跃会话 setActiveConversation(''); return; } // 关键步骤:设置活跃会话 ID,格式为 "GROUP" + roomId setActiveConversation(`GROUP${roomId}`); }, [currentRoom?.roomId, loginUserInfo?.userId, setActiveConversation]);}
六、步骤2: 使用组件
在你的页面中,可以直接组合使用这两个组件。同时结合 useRoomParticipantState 获取当前用户的禁言状态,控制输入框的可用性。
注意:所有 TUIKit 组件必须包裹在 UIKitProvider 内部,否则将无法获取上下文依赖,导致样式异常。
Vue3React<template> <UIKitProvider theme="light" language="zh-CN"> <div class="room-chat-container"> <!– 消息列表:展示聊天记录,支持复制、撤回、删除操作 –> <MessageList ref="messageListRef" class="message-list" :messageActionList="messageActionList" /> <!– 消息输入框:禁言时禁用;回车发送(不显示发送按钮) –> <MessageInput class="message-input" :placeholder="placeholder" :disabled="localParticipant?.isMessageDisabled" hideSendButton /> </div> </UIKitProvider></template><script setup lang="ts">import { computed, ref } from 'vue';import { UIKitProvider } from '@tencentcloud/uikit-base-component-vue3';import { MessageInput, MessageList, useMessageActions } from 'tuikit-atomicx-vue3/chat';import { useRoomParticipantState } from 'tuikit-atomicx-vue3/room';import type { MessageListHandle } from 'tuikit-atomicx-vue3/chat';const { localParticipant } = useRoomParticipantState();// 用于命令式控制,如面板打开时滚动到底部const messageListRef = ref<MessageListHandle | null>(null);// isMessageDisabled 由房间后台实时同步,无需前端主动轮询const placeholder = computed(() => localParticipant.value?.isMessageDisabled ? '你已被禁言' : '请输入消息…');// 配置消息长按/右键操作菜单const messageActionList = useMessageActions(['copy', 'recall', 'delete']);</script><style scoped>.room-chat-container{display:flex;flex-direction:column;height:100%;padding:8px;gap:8px}.message-list{flex:1;overflow:hidden}.message-input{flex-shrink:0;border:1px solid #e0e0e0;border-radius:8px}</style>import { useRef } from 'react';import { UIKitProvider } from '@tencentcloud/uikit-base-component-react';import { MessageList, MessageInput, useMessageActions } from 'tuikit-atomicx-react/chat';import { useRoomParticipantState } from 'tuikit-atomicx-react/room';import type { MessageListHandle } from 'tuikit-atomicx-react/chat';function RoomChatPanel() { const { localParticipant } = useRoomParticipantState(); // isMessageDisabled 由房间后台实时同步,无需轮询 const isMessageDisabled = localParticipant?.isMessageDisabled; const placeholder = isMessageDisabled ? '你已被禁言' : '请输入消息…'; // 配置消息长按/右键操作菜单 const messageActionList = useMessageActions(['copy', 'recall', 'delete']); // 用于命令式控制,如面板打开时滚动到底部 const messageListRef = useRef<MessageListHandle>(null); return ( <UIKitProvider theme="light" language="zh-CN"> <div style={{ display: 'flex', flexDirection: 'column', height: '100%', padding: '8px', gap: '8px' }}> {/* 消息列表:展示聊天记录,支持复制、撤回、删除操作 */} <MessageList ref={messageListRef} style={{ flex: 1, overflow: 'hidden' }} messageActionList={messageActionList} /> {/* 消息输入框:禁言时禁用;回车发送(不显示发送按钮) */} <MessageInput style={{ flexShrink: 0, border: '1px solid #e0e0e0', borderRadius: '8px' }} placeholder={placeholder} disabled={isMessageDisabled} hideSendButton /> </div> </UIKitProvider> );}
七、步骤3:处理未读消息计数(可选)
如果聊天窗口平时是折叠或关闭状态,你需要通过监听新消息事件来统计未读消息数,并在 UI 上(如聊天图标角标)进行提示。Vue 3 和 React 均通过 useChatContext 提供的 messageListOnEvent 订阅 onReceiveNewMessage 事件实现,非自己发送且面板未打开时累加未读计数,面板打开时清零。
Vue3Reactimport { ref, watch, onUnmounted } from 'vue';import { useChatContext } from 'tuikit-atomicx-vue3/chat';const { activeConversationID, messageListOnEvent } = useChatContext();const unreadCount = ref(0);const isActive = ref(false); // 根据业务逻辑控制聊天面板是否打开// 订阅新消息事件;活跃会话变化时重新订阅watch(activeConversationID, () => { const unsub = messageListOnEvent((event) => { if (event.type !== 'onReceiveNewMessage') return; // 跳过自己发送的消息,或面板已打开时不计数 if (event.message.isSentBySelf || isActive.value) return; unreadCount.value += 1; }); onUnmounted(unsub);}, { immediate: true });// 面板打开时清空未读计数watch(isActive, (open) => { if (open) unreadCount.value = 0;});import { useState, useEffect, useRef } from 'react';import { useChatContext } from 'tuikit-atomicx-react/chat';function useChatUnreadCount(isChatPanelOpen: boolean) { const [unreadCount, setUnreadCount] = useState(0); const { activeConversationID, messageListOnEvent } = useChatContext(); // 用 ref 保持最新值,避免因依赖变化重新订阅 const isPanelOpenRef = useRef(isChatPanelOpen); useEffect(() => { isPanelOpenRef.current = isChatPanelOpen; }, [isChatPanelOpen]); // 订阅新消息事件;活跃会话变化时重新订阅 useEffect(() => { if (!activeConversationID || !messageListOnEvent) return; const unsub = messageListOnEvent((event) => { if (event.type !== 'onReceiveNewMessage') return; const { message } = event; // 跳过自己发送的消息,或面板已打开时不计数 if (message.isSentBySelf || isPanelOpenRef.current) return; setUnreadCount(prev => prev + 1); }); return unsub; }, [activeConversationID, messageListOnEvent]); // 面板打开时清空未读计数 useEffect(() => { if (isChatPanelOpen) { setUnreadCount(0); } }, [isChatPanelOpen]); return { unreadCount };}
八、方案二:使用底层 API 自定义集成
本节主要介绍如何通过 MessageListStore(消息列表管理)和 MessageInputStore(消息发送)来实现完全自定义的聊天界面。Vue 3 和 React 均通过 useMessageListStore() / useMessageInputStore() 使用。
九、步骤1:数据初始化
与方案一步骤1相同,同样需要先调用 setActiveConversation 接口激活对应的 IM 群组会话。在退房时建议清空活跃会话,避免消息错发。
注意:说明:
Vue3Reactimport { watch, onUnmounted } from 'vue';import { useChatContext } from 'tuikit-atomicx-vue3/chat';import { useLoginState, useRoomState } from 'tuikit-atomicx-vue3/room';function useChatConversationSync() { const { setActiveConversation } = useChatContext(); const { currentRoom } = useRoomState(); const { loginUserInfo } = useLoginState(); // roomId 或登录用户变化时绑定活跃会话 watch( [() => currentRoom.value?.roomId, () => loginUserInfo.value?.userId], ([roomId, userId]) => { if (!userId) return; if (!roomId) { // 退房时清空活跃会话 setActiveConversation(''); return; } // 关键步骤:设置活跃会话 ID,格式为 "GROUP" + roomId setActiveConversation(`GROUP${roomId}`); }, { immediate: true }, ); onUnmounted(() => { setActiveConversation(''); });}import { useEffect } from 'react';import { useChatContext } from 'tuikit-atomicx-react/chat';import { useRoomState, useLoginState } from 'tuikit-atomicx-react/room';function useChatConversationSync() { const { setActiveConversation } = useChatContext(); const { currentRoom } = useRoomState(); const { loginUserInfo } = useLoginState(); useEffect(() => { const roomId = currentRoom?.roomId; if (!loginUserInfo?.userId) return; if (!roomId) { setActiveConversation(''); return; } // 关键步骤:设置活跃会话 ID,格式为 "GROUP" + roomId setActiveConversation(`GROUP${roomId}`); }, [currentRoom?.roomId, loginUserInfo?.userId, setActiveConversation]);}
十、步骤2:获取消息列表
使用对应框架的 Hook 获取当前会话的消息列表状态与操作方法。messageList 是响应式数据,会自动更新新收到的消息。
Vue3React<template> <div class="message-list-wrapper"> <div v-for="msg in messageList" :key="msg.msgID" class="message-item" > {{ msg.messagePayload?.text }} </div> </div></template><script setup lang="ts">import { onUnmounted } from 'vue';import { useMessageListStore } from 'tuikit-atomicx-vue3/chat';// conversationID = "GROUP" + roomIdconst conversationID = `GROUP${roomId}`;const { messageList, hasOlderMessages, hasNewerMessages, pinnedMessageList, loadMessages, loadOlderMessages, loadNewerMessages, onEvent,} = useMessageListStore(conversationID);loadMessages();const unsub = onEvent((event) => { if (event.type === 'onReceiveNewMessage') { // 触发滚动到底部或未读角标更新 }});onUnmounted(unsub);</script>import { useEffect } from 'react';import { useMessageListStore } from 'tuikit-atomicx-react/chat';function MessageListPanel({ roomId }: { roomId: string }) { const conversationID = `GROUP${roomId}`; const { messageList, hasOlderMessages, hasNewerMessages, pinnedMessageList, loadMessages, loadOlderMessages, loadNewerMessages, onEvent, } = useMessageListStore(conversationID); useEffect(() => { loadMessages(); }, [conversationID]); useEffect(() => { return onEvent((event) => { if (event.type === 'onReceiveNewMessage') { // 触发滚动到底部或未读角标更新 } }); }, [conversationID]); return ( <div style={{ display: 'flex', flexDirection: 'column', gap: '8px', padding: '8px' }}> {messageList.map(msg => ( <div key={msg.msgID} style={{ padding: '4px 8px', borderRadius: '4px' }}> {msg.messagePayload?.text} </div> ))} </div> );}
十一、步骤3:发送消息
使用对应框架的 Hook 管理输入框内容并发送消息。两者底层均调用相同的 MessageInputStore.sendMessage(payload)。
注意:MessageInputStore 是纯命令式 Store,不持有输入框内容状态。需由业务层自行管理输入内容,发送时将内容通过 sendMessage({ type: 'textMessage', text }) 传入。
Vue3React<template> <input :value="inputText" @input="(e) => inputText = (e.target as HTMLInputElement).value" @keyup.enter="handleSend" /></template><script setup lang="ts">import { ref } from 'vue';import { useMessageInputStore } from 'tuikit-atomicx-vue3/chat';// conversationID = "GROUP" + roomIdconst conversationID = `GROUP${roomId}`;const { sendMessage } = useMessageInputStore(conversationID);// 输入框内容由业务层自行管理,Store 不持有输入状态const inputText = ref('');const handleSend = async () => { if (!inputText.value.trim()) return; try { await sendMessage({ type: 'textMessage', text: inputText.value }); inputText.value = ''; } catch (error) { console.error('发送失败', error); }};</script>import { useState } from 'react';import { useMessageInputStore } from 'tuikit-atomicx-react/chat';function MessageInputPanel({ roomId }: { roomId: string }) { const conversationID = `GROUP${roomId}`; const { sendMessage } = useMessageInputStore(conversationID); // 输入框内容由 React state 管理,Store 不持有输入状态 const [inputText, setInputText] = useState(''); const handleSend = async () => { if (!inputText.trim()) return; try { await sendMessage({ type: 'textMessage', text: inputText }); setInputText(''); } catch (error) { console.error('发送失败', error); } }; return ( <input value={inputText} onChange={(e) => setInputText(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleSend()} /> );}
十二、步骤4:加载历史消息
分页拉取历史消息,通常在滚动到列表顶部时触发。Vue 3 和 React 均使用 loadOlderMessages()(底层均对应 MessageListStore.loadOlderMessages())。
注意:在加载历史消息后,通常需要手动调整滚动条位置,以保持当前浏览视角不发生跳变。
Vue3Reactimport { nextTick, ref } from 'vue';import { useMessageListStore } from 'tuikit-atomicx-vue3/chat';const conversationID = `GROUP${roomId}`;const { hasOlderMessages, loadOlderMessages } = useMessageListStore(conversationID);const scrollContainerRef = ref<HTMLElement | null>(null);const loadHistory = async () => { if (!hasOlderMessages.value) return; const container = scrollContainerRef.value; // 记录加载前的滚动高度 const previousHeight = container?.scrollHeight ?? 0; await loadOlderMessages(); // DOM 更新后恢复滚动位置,防止视图跳动 nextTick(() => { if (container) { container.scrollTop += container.scrollHeight – previousHeight; } });};import { useRef } from 'react';import { useMessageListStore } from 'tuikit-atomicx-react/chat';const { hasOlderMessages, loadOlderMessages } = useMessageListStore(conversationID);const scrollContainerRef = useRef<HTMLDivElement>(null);const loadHistory = async () => { if (!hasOlderMessages) return; const container = scrollContainerRef.current; // 记录加载前的滚动高度 const previousHeight = container?.scrollHeight ?? 0; await loadOlderMessages(); // React 重新渲染后,恢复滚动位置防止视图跳动 requestAnimationFrame(() => { if (container) { container.scrollTop += container.scrollHeight – previousHeight; } });};
十三、步骤5:高级功能(已读回执)
如果你需要已读回执功能,可以在消息可见时调用 sendMessageReadReceipts 上报已读状态(底层对应 MessageListStore.sendMessageReadReceipts())。通常在消息进入视口(Intersection Observer)时触发。
Vue3Reactimport { useMessageListStore } from 'tuikit-atomicx-vue3/chat';import type { MessageInfo } from 'tuikit-atomicx-vue3/chat';const conversationID = `GROUP${roomId}`;const { sendMessageReadReceipts } = useMessageListStore(conversationID);// 示例:消息进入视口时上报已读const markAsRead = async (visibleMessages: MessageInfo[]) => { const unread = visibleMessages.filter( (msg) => !msg.isSentBySelf && msg.needReadReceipt, ); if (unread.length > 0) { await sendMessageReadReceipts(unread); }};import { useMessageListStore } from 'tuikit-atomicx-react/chat';import type { MessageInfo } from 'tuikit-atomicx-react/chat';const { messageList, sendMessageReadReceipts } = useMessageListStore(conversationID);// 示例:消息进入视口时标记为已读const markAsRead = async (visibleMessages: MessageInfo[]) => { const unread = visibleMessages.filter( (msg) => !msg.isSentBySelf && msg.needReadReceipt, ); if (unread.length > 0) { await sendMessageReadReceipts(unread); }};
十四、开发注意事项
·禁言状态同步
localParticipant.isMessageDisabled 是由房间管理员控制的。当管理员开启“全员禁言”或单独禁言某用户时,该属性会自动更新。请务必将此属性绑定到输入框的 disabled 状态上,以在前端层面限制用户发言。
·会话切换时机
确保 setActiveConversation 在进房成功后且 roomId 有值时调用。如果在进房前调用,可能会因为 SDK 尚未初始化完毕或未登录而失败。
·容器布局要求(重要)
消息列表组件内部处理了滚动逻辑,因此父容器必须有固定的高度(例如 height: 100% 或具体像素值)并设置 overflow: hidden,否则会导致列表无限拉长、无法滚动。
·消息类型处理
messageList 中包含多种类型的消息(文本、图片、文件等),自定义渲染时需根据 msg.type 进行区分处理。
·输入状态管理
Vue 3 和 React 均使用 sendMessage({ type: 'textMessage', text }) 发送消息,Store 均不持有输入框内容状态。Vue 3 用 ref 维护输入内容,React 用 useState。发送成功后手动清空对应的状态变量即可。
·历史消息滚动维持
在自定义开发中,调用 loadOlderMessages() 后 DOM 高度会增加。为了防止视图跳动,需要在加载前记录 scrollHeight,加载后(Vue 3 使用 nextTick,React 使用 requestAnimationFrame)计算高度差并调整 scrollTop。
·新消息滚动策略
建议实现“智能滚动”逻辑:当用户位于列表底部时,收到新消息自动滚动到底部;当用户正在浏览历史消息时,保持当前位置不动,并显示“有新消息”的提示气泡。
·会话 ID 格式要求
调用 setActiveConversation 时,必须使用 `GROUP${roomId}` 格式(GROUP 为固定前缀)。这是 IM SDK 群组会话的标准格式,不可自定义或省略前缀,否则无法正常收发消息。
十五、常见问题
切到群组会话失败或消息列表无内容?
确保已登录并进房成功后再调用 setActiveConversation('GROUP' + roomId);未登录或 roomId 为空会导致失败。
消息收不到或列表空白?
检查当前活跃会话是否正确(GROUP 前缀 + roomId),以及网络/鉴权是否正常;必要时重新设置活跃会话 setActiveConversation 刷新。
未读计数不清零?
需要手动维护未读计数逻辑。打开聊天窗口时,或收到新消息但窗口关闭时进行计数更新。
被禁言仍能点击发送?
请确保前端 UI 根据 isMessageDisabled 状态禁用了发送按钮和输入框。虽然服务端也会拦截,但前端禁用体验更好。
自定义头像/昵称展示?
可通过给 MessageList 传入自定义 Message prop(React 组件),优先显示房间内的 nameCard,并自定义头像样式等。
自定义方案如何发送图片或文件?
底层 sendMessage 接受 SendMessagePayload 可辨识联合类型,Vue 3 和 React 均适用。根据 type 字段区分消息类型,图片对应 imageMessage,文件对应 fileMessage,视频对应 videoMessage。
Vue3Reactimport { useMessageInputStore } from 'tuikit-atomicx-vue3/chat';const { sendMessage } = useMessageInputStore(`GROUP${roomId}`);// 示例:发送图片// const file = …; // 从 input type="file" 获取的 File 对象await sendMessage({ type: 'imageMessage', file });// 示例:发送文件await sendMessage({ type: 'fileMessage', file });// 示例:发送文本await sendMessage({ type: 'textMessage', text: 'Hello World' });import { useMessageInputStore } from 'tuikit-atomicx-react/chat';const { sendMessage } = useMessageInputStore(conversationID);// 示例:发送图片// const file = …; // 从 <input type="file" /> 获取的 File 对象await sendMessage({ type: 'imageMessage', file });// 示例:发送文件await sendMessage({ type: 'fileMessage', file });// 示例:发送文本消息await sendMessage({ type: 'textMessage', text: 'Hello World' });
如何修改 UI 组件的默认样式?
TUIKit 组件的类名通常保持稳定。Vue 3:使用 :deep() 深度选择器穿透 scoped 样式,例如 .message-list :deep(.tui-message-bubble) { background-color: #f0f0f0; }。React:通过给组件传入 className 并结合后代选择器,或在全局样式文件中用 :global(.tui-message-bubble) { … } 覆盖。
十六、API 文档
| State | 功能描述 | API 文档 |
| useRoomState | 房间状态管理(创建、加入、预约等) | API 文档 |
| useRoomParticipantState | 房间参与者管理(成员列表、权限控制) | API 文档 |