会中聊天功能允许会议内的成员通过文字、表情等形式进行实时互动。该功能基于
tuikit-atomicx-vue3(Vue 3)或 tuikit-atomicx-react(React)的 IM 组件能力实现,为用户提供便捷的即时通信体验。适用场景
在线研讨会:主讲人进行视频演讲时,参会者可以通过文字提问或讨论。
远程协作:团队成员在沟通时,可发送文字、图片、文件等。
直播教学:学生可以在聊天区发送弹幕或问题,老师实时查看并解答,增强课堂互动性。
前提条件
在接入功能前,请确保满足以下条件:
环境依赖:项目已正确安装并引入
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。说明:
GROUP 为固定前缀,setActiveConversation 参数必须为 `GROUP${roomId}`,否则无法正常收发消息。GROUP 为 IM SDK 群组会话的固定前缀(参考 IM 会话类型)。房间聊天基于 IM 群组实现,因此会话 ID 必须为 `GROUP${roomId}` 格式。import { 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" + roomIdsetActiveConversation(`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" + roomIdsetActiveConversation(`GROUP${roomId}`);}, [currentRoom?.roomId, loginUserInfo?.userId, setActiveConversation]);}
步骤2: 使用组件
注意:
<template><UIKitProvider theme="light" language="zh-CN"><div class="room-chat-container"><!-- 消息列表:展示聊天记录,支持复制、撤回、删除操作 --><MessageListref="messageListRef"class="message-list":messageActionList="messageActionList"/><!-- 消息输入框:禁言时禁用;回车发送(不显示发送按钮) --><MessageInputclass="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' }}>{/* 消息列表:展示聊天记录,支持复制、撤回、删除操作 */}<MessageListref={messageListRef}style={{ flex: 1, overflow: 'hidden' }}messageActionList={messageActionList}/>{/* 消息输入框:禁言时禁用;回车发送(不显示发送按钮) */}<MessageInputstyle={{ flexShrink: 0, border: '1px solid #e0e0e0', borderRadius: '8px' }}placeholder={placeholder}disabled={isMessageDisabled}hideSendButton/></div></UIKitProvider>);}
步骤3:处理未读消息计数(可选)
如果聊天窗口平时是折叠或关闭状态,您需要通过监听新消息事件来统计未读消息数,并在 UI 上(如聊天图标角标)进行提示。Vue 3 和 React 均通过
useChatContext 提供的 messageListOnEvent 订阅 onReceiveNewMessage 事件实现,非自己发送且面板未打开时累加未读计数,面板打开时清零。import { 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:数据初始化
说明:
GROUP 为固定前缀,setActiveConversation 参数必须为 `GROUP${roomId}`,否则无法正常收发消息。在房间切换或退房时,建议重置或清空活跃会话,避免消息错发至错误会话。
import { 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" + roomIdsetActiveConversation(`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" + roomIdsetActiveConversation(`GROUP${roomId}`);}, [currentRoom?.roomId, loginUserInfo?.userId, setActiveConversation]);}
步骤2:获取消息列表
使用对应框架的 Hook 获取当前会话的消息列表状态与操作方法。
messageList 是响应式数据,会自动更新新收到的消息。<template><div class="message-list-wrapper"><divv-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 }) 传入。<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 (<inputvalue={inputText}onChange={(e) => setInputText(e.target.value)}onKeyDown={(e) => e.key === 'Enter' && handleSend()}/>);}
步骤4:加载历史消息
分页拉取历史消息,通常在滚动到列表顶部时触发。Vue 3 和 React 均使用
loadOlderMessages()(底层均对应 MessageListStore.loadOlderMessages())。注意:
在加载历史消息后,通常需要手动调整滚动条位置,以保持当前浏览视角不发生跳变。
import { 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)时触发。import { 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。import { 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) { ... } 覆盖。