会中聊天(Web)

最近更新时间:2026-06-25 18:00:32

我的收藏
会中聊天功能允许会议内的成员通过文字、表情等形式进行实时互动。该功能基于 tuikit-atomicx-vue3(Vue 3)或 tuikit-atomicx-react(React)的 IM 组件能力实现,为用户提供便捷的即时通信体验。

适用场景

在线研讨会:主讲人进行视频演讲时,参会者可以通过文字提问或讨论。
远程协作:团队成员在沟通时,可发送文字、图片、文件等。
直播教学:学生可以在聊天区发送弹幕或问题,老师实时查看并解答,增强课堂互动性。

前提条件

在接入功能前,请确保满足以下条件:
用户状态:用户已经通过 useLoginState 完成登录鉴权,参见 接入概览,并已作为一个房间的房主或成员在房间内,参考 房间管理
环境依赖:项目已正确安装并引入 tuikit-atomicx-vue3(Vue 3)或 tuikit-atomicx-react(React)。

实现会中聊天功能

本功能提供两种集成方案,您可以根据业务需求选择最适合的一种:
方案一(推荐):使用 UI 组件快速集成。直接引入 tuikit-atomicx-vue3tuikit-atomicx-react 提供的消息列表组件(MessageList)和消息输入组件(MessageInput),开发成本最低。
方案二(高级):使用底层 Store API 自定义集成。基于 atomicx-core SDK 的 MessageListStoreMessageInputStore 自行实现 UI 和交互逻辑,灵活度最高。
会中聊天
会中聊天


方案一:使用 UI 组件快速集成

tuikit-atomicx-vue3tuikit-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}` 格式。
Vue3
React
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" + 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 内部,否则将无法获取上下文依赖,导致样式异常。
Vue3
React
<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 事件实现,非自己发送且面板未打开时累加未读计数,面板打开时清零。
Vue3
React
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:数据初始化

与方案一步骤1相同,同样需要先调用 setActiveConversation 接口激活对应的 IM 群组会话。在退房时建议清空活跃会话,避免消息错发。
说明:
GROUP 为固定前缀,setActiveConversation 参数必须为 `GROUP${roomId}`,否则无法正常收发消息。
在房间切换或退房时,建议重置或清空活跃会话,避免消息错发至错误会话。
Vue3
React
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" + 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 是响应式数据,会自动更新新收到的消息。
Vue3
React
<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" + roomId
const 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 }) 传入。
Vue3
React
<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" + roomId
const 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())。
注意:
在加载历史消息后,通常需要手动调整滚动条位置,以保持当前浏览视角不发生跳变。
Vue3
React
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)时触发。
Vue3
React
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
Vue3
React
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) { ... } 覆盖。

API 文档

State
功能描述
API 文档
useRoomState
房间状态管理(创建、加入、预约等)
useRoomParticipantState
房间参与者管理(成员列表、权限控制)