TUIKit 是基于腾讯云 Chat SDK 的一款 React 组件库,它提供了一些通用的 UI 组件,包含会话、聊天、群组等功能。本文介绍如何快速集成 TUIKit 并实现核心功能。
AI 助手使用说明:
IM 知识咨询
AI 集成
关键概念
chat-uikit-react 主要分为 ConversationListH5、MessageListH5、ChatHeaderH5、MessageInputH5、ChatSettingH5、SearchH5、ContactH5 等核心 UI 组件,每个 UI 组件负责展示不同的内容。
1. ConversationListH5 提供会话列表组件。
2. Chat 提供会话的容器组件。
3. MessageListH5 提供会话的消息列表组件。
4. ChatHeaderH5 提供会话的头部信息组件。
5. MessageInputH5 提供输入框组件。
6. ChatSettingH5 提供单聊和群聊的管理组件。
7. SearchH5 提供云端搜索组件。
8. ContactH5 提供联系人组件。
前提条件
Node.js v18 版本及以上,建议使用当前的 LTS 版本
React^18.2 || React^19.0.0 版本
TypeScript@^5.0.0
创建项目
使用 Vite 创建一个新的名为 chat-integration-react-h5 的 React 项目,并按照脚手架的步骤提示完成项目的初始化。
初始项目启动成功后,可以删掉脚手架自带的示例文件(src/App.css、src/index.css 里的默认样式、src/assets、public 里的图标等),保留 src/main.tsx 与 index.html 即可。
说明:
建议使用 npm 进行安装,npm 会主动下载所需的 peerDependencies 依赖。
npm create vite@latest 会使用最新的 vite 版本,最新版本的 vite 可能会与低版本的 node.js 冲突,请检查您电脑上的相关环境配置。npm create vite@latest◇ Project name:│ chat-integration-react-h5│◇ Select a framework:│ React│◇ Select a variant:│ TypeScript + React Compiler│◇ Use ESLint instead of Oxlint?│ Yes (ESLint)│◇ Install with npm and start now?│ Yes
下载并导入组件
步骤1:安装依赖
npm install @tencentcloud/chat-uikit-react @tencentcloud/uikit-base-component-react
步骤2:引入组件
1. 配置移动端 viewport。
良好的移动端体验的第一步:在项目根目录里把 index.html 中默认 viewport 设置为:覆盖全屏、锁定缩放,让页面更像原生 App。
<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8" /><!-- H5 viewport: 覆盖刘海屏区域,锁定缩放,营造类原生应用的体验 --><metaname="viewport"content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover"/><title>Chat UIKit H5 Quickstart (React)</title></head><body><div id="root"></div><script type="module" src="/src/main.tsx"></script></body></html>
2. 配置全局基础样式。
新建 src/styles.css,设置全局基础样式。
/* ------- 移动端基础布局 ------- */* {box-sizing: border-box;}html,body,#root {width: 100%;height: 100%;margin: 0;}body {/* 使用 dvh 来处理 iOS Safari 地址栏. */min-height: 100vh;min-height: 100dvh;font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;color: #1f2329;background: #f5f6f8;/* 移除移动版 WebKit 上的点击亮块。 */-webkit-tap-highlight-color: transparent;}button {font: inherit;cursor: pointer;}/* 16px 可防止 iOS 在输入框获得焦点时自动放大屏幕。 */input {font-size: 16px;}
3. 配置应用入口。
把以下内容写入 src/main.tsx,在应用入口引入样式文件。
import { createRoot } from 'react-dom/client';import App from './App';import './styles.css';createRoot(document.getElementById('root')!).render(<App />,);
4. 配置首页。
把以下内容写入 src/App.tsx,用于路由登录页和聊天页面。
import { useState } from 'react';import { LoginStatus, UIKitProvider, useLoginState } from '@tencentcloud/chat-uikit-react';import { LoginView } from './LoginView';import { ChatLayout } from './ChatLayout';import { languageResources } from './i18n';function App() {const { status } = useLoginState();const isLogined = status === LoginStatus.SUCCESS;// 语言在此处进行维护,并通过 UIKitProvider 的 `language` 属性进行配置。// 这是控制语言的推荐方式// 更新这个单一数据源,它驱动整个 UIKit。const [language, setLanguage] = useState('zh-CN');return (<UIKitProvider language={language} languageResources={languageResources}>{isLogined? <ChatLayout onChangeLanguage={setLanguage} />: <LoginView language={language} onChangeLanguage={setLanguage} />}</UIKitProvider>);}export default App;
5. 配置登录页和聊天主页面。
新建 src/LoginView.tsx 和 src/LoginView.css 并写入以下代码,用于构造登录页。
import { useState, type SubmitEvent } from 'react';import { useLoginState, useUIKit } from '@tencentcloud/chat-uikit-react';import './LoginView.css';// 语言切换器中显示的语言。标签使用本来的语言,确保用户// 不受当前 UI 语言影响,始终能识别自己的选项。const LANGUAGES = [{ value: 'zh-CN', label: '中文' },{ value: 'en-US', label: 'English' },];interface LoginViewProps {language: string;onChangeLanguage: (language: string) => void;}export function LoginView({ language, onChangeLanguage }: LoginViewProps) {const { login } = useLoginState();const { t } = useUIKit();const [sdkAppId, setSdkAppId] = useState<number>();const [userId, setUserId] = useState('');const [userSig, setUserSig] = useState('');const [loading, setLoading] = useState(false);const [error, setError] = useState('');const handleSubmit = async (e: SubmitEvent) => {e.preventDefault();setError('');if (!sdkAppId || !userId || !userSig) {setError(t('demo.error.required'));return;}setLoading(true);try {await login({ SDKAppID: Number(sdkAppId), userID: userId, userSig });// On success the login gate in App.tsx switches to ChatLayout automatically.} catch (err) {setError(err instanceof Error ? err.message : t('demo.error.loginFailed'));} finally {setLoading(false);}};return (<div className="login"><div className="login__lang" role="group" aria-label="Language">{LANGUAGES.map(item => (<buttonkey={item.value}type="button"className={item.value === language ? 'login__lang-btn login__lang-btn--active' : 'login__lang-btn'}onClick={() => onChangeLanguage(item.value)}>{item.label}</button>))}</div><form className="login__card" onSubmit={handleSubmit}><h1 className="login__title">{t('demo.appTitle')}</h1><label className="login__field"><span>{t('demo.field.sdkAppId')}</span><inputtype="number"placeholder={t('demo.placeholder.sdkAppId')}value={sdkAppId || ''}onChange={e => setSdkAppId(Number(e.target.value))}/></label><label className="login__field"><span>{t('demo.field.userId')}</span><inputtype="text"placeholder={t('demo.placeholder.userId')}value={userId}onChange={e => setUserId(e.target.value)}/></label><label className="login__field"><span>{t('demo.field.userSig')}</span><inputtype="text"placeholder={t('demo.placeholder.userSig')}value={userSig}onChange={e => setUserSig(e.target.value)}/></label>{error && <p className="login__error">{error}</p>}<button className="login__submit" type="submit" disabled={loading}>{loading ? t('demo.loggingIn') : t('demo.login')}</button></form></div>);}
.login {position: relative;display: flex;width: 100%;height: 100dvh;align-items: center;justify-content: center;padding: max(20px, env(safe-area-inset-top)) 20px max(20px, env(safe-area-inset-bottom));/* Soft natural-white backdrop with a subtle top-down gradient for depth. */background: linear-gradient(180deg, #ffffff 0%, #f3f5f8 100%);}/* Segmented language switcher, pinned to the top-right. */.login__lang {position: absolute;top: calc(16px + env(safe-area-inset-top));right: 16px;display: inline-flex;padding: 3px;border-radius: 999px;background: #eef0f3;}.login__lang-btn {min-width: 56px;padding: 6px 12px;border: 0;border-radius: 999px;background: transparent;color: #6b7280;font-size: 13px;font-weight: 500;transition: background 0.2s, color 0.2s;}.login__lang-btn--active {background: #fff;color: #1f2329;box-shadow: 0 1px 2px rgb(16 24 40 / 10%);}.login__card {display: flex;width: 100%;max-width: 360px;flex-direction: column;gap: 16px;padding: 28px 24px;border-radius: 16px;background: #fff;/* Layered, low-opacity shadow for a soft, natural lift off the white bg. */border: 1px solid rgb(16 24 40 / 4%);box-shadow:0 1px 2px rgb(16 24 40 / 4%),0 12px 32px rgb(16 24 40 / 8%);}.login__title {margin: 0 0 4px;font-size: 20px;font-weight: 600;text-align: center;}.login__field {display: flex;flex-direction: column;gap: 6px;font-size: 13px;color: #6b7280;}.login__field input {padding: 12px 14px;border: 1px solid #e8eaed;border-radius: 10px;outline: none;appearance: none;}.login__field input:focus {border-color: #1464ff;}.login__error {margin: 0;color: #e34d59;font-size: 13px;}.login__submit {padding: 13px;border: 0;border-radius: 10px;background: #1464ff;color: #fff;font-size: 16px;font-weight: 600;}.login__submit:disabled {opacity: 0.6;}
新建 src/ChatLayout.tsx 和 src/ChatLayout.css 并写入以下代码:
import { useEffect, useRef, useState, type ReactNode } from 'react';import {Chat,ChatHeaderH5,ChatSettingH5,ContactInfoH5,ContactListH5,ConversationListH5,MessageInputH5,MessageListH5,SearchH5,VariantType,useChatContext,useLoginState,useUIKit,} from '@tencentcloud/chat-uikit-react';import { IconBulletpoint, IconSearch } from '@tencentcloud/uikit-base-component-react';import './ChatLayout.css';type MobileView = 'conversations' | 'chat' | 'contacts' | 'contactInfo' | 'search' | 'setting';interface ChatLayoutProps {onChangeLanguage: (language: string) => void;}/** // 全屏视图;仅当前激活的视图可见(保持挂载以保留状态)。 */function Screen({ active, children }: { active: boolean; children: ReactNode }) {return (<section className="screen" style={{ zIndex: active ? 1 : 0, visibility: active ? 'visible' : 'hidden' }}>{children}</section>);}export function ChatLayout({ onChangeLanguage }: ChatLayoutProps) {const { activeConversationID, setActiveConversation } = useChatContext();const { loginUserInfo, logout } = useLoginState();const { t, language } = useUIKit();const [activeView, setActiveView] = useState<MobileView>('conversations');// 记住搜索是从哪里打开的,使其返回按钮能回到原处。const [previousView, setPreviousView] = useState<MobileView>('chat');const activeViewRef = useRef(activeView);const prevConversationIDRef = useRef(activeConversationID);useEffect(() => {activeViewRef.current = activeView;}, [activeView]);useEffect(() => {const changed = prevConversationIDRef.current !== activeConversationID;prevConversationIDRef.current = activeConversationID;if (changed && activeConversationID && activeViewRef.current === 'conversations') {setActiveView('chat');}}, [activeConversationID]);const openFromChat = (view: 'search' | 'setting') => {setPreviousView(activeView);setActiveView(view);};const handleChatBack = () => {setActiveConversation(undefined);setActiveView('conversations');};return (<div className="app-shell"><Screen active={activeView === 'conversations'}><header className="topbar"><span className="topbar__title">{t('demo.chats')}{loginUserInfo?.userId ? ` · ${loginUserInfo.userId}` : ''}</span><div className="chat-pane__actions"><buttonclassName="topbar__text-btn"type="button"onClick={() => onChangeLanguage(language === 'zh-CN' ? 'en-US' : 'zh-CN')}>{language === 'zh-CN' ? 'EN' : '中'}</button><button className="topbar__text-btn" type="button" onClick={logout}>{t('demo.logout')}</button></div></header><div className="screen__body"><ConversationListH5style={{ height: '100%' }}onSelectConversation={() => setActiveView('chat')}/></div><nav className="tabbar"><button className="tabbar__item tabbar__item--active" type="button">{t('demo.chats')}</button><button className="tabbar__item" type="button" onClick={() => setActiveView('contacts')}>{t('demo.contacts')}</button></nav></Screen><Screen active={activeView === 'contacts'}><header className="topbar"><span className="topbar__title">{t('demo.contacts')}</span></header><div className="screen__body"><ContactListH5 onContactItemClick={() => setActiveView('contactInfo')} /></div><nav className="tabbar"><button className="tabbar__item" type="button" onClick={() => setActiveView('conversations')}>{t('demo.chats')}</button><button className="tabbar__item tabbar__item--active" type="button">{t('demo.contacts')}</button></nav></Screen><Screen active={activeView === 'contactInfo'}><div className="screen__body screen__body--surface"><ContactInfoH5onClose={() => setActiveView('contacts')}onSendMessage={() => setActiveView('chat')}onEnterGroup={() => setActiveView('chat')}/></div></Screen><Screen active={activeView === 'chat'}><Chat className="chat-pane"><ChatHeaderH5className="chat-pane__header"enableUserStatusonBack={handleChatBack}ChatHeaderRight={(<div className="chat-pane__actions"><button className="icon-btn" type="button" aria-label={t('demo.search')} onClick={() => openFromChat('search')}><IconSearch size="20px" /></button><button className="icon-btn" type="button" aria-label={t('demo.setting')} onClick={() => openFromChat('setting')}><IconBulletpoint size="20px" /></button></div>)}/><MessageListH5 /><MessageInputH5 /></Chat></Screen><Screen active={activeView === 'search'}><div className="screen__body"><SearchH5variant={VariantType.EMBEDDED}onBack={() => setActiveView(previousView)}onResultItemClick={(_item, type) => {if (type === 'chat_message') {setActiveView('chat');}}}/></div></Screen><Screen active={activeView === 'setting'}><div className="screen__body screen__body--surface"><ChatSettingH5 onBack={() => setActiveView('chat')} /></div></Screen></div>);}
/* ------- Shell + stacked screens ------- */.app-shell {position: relative;width: 100vw;height: 100vh;height: 100dvh;overflow: hidden;background: #fff;}/* Each full-screen view is stacked; only the active one is visible. */.screen {position: absolute;inset: 0;display: flex;flex-direction: column;background: #fff;}.screen__body {flex: 1;min-height: 0;overflow: hidden;}.screen__body--surface {overflow: auto;background: #f5f6f8;}/* ------- Top bar ------- */.topbar {display: flex;flex: 0 0 auto;min-height: 52px;align-items: center;justify-content: space-between;padding: calc(8px + env(safe-area-inset-top)) 16px 8px;border-bottom: 1px solid #e8eaed;}.topbar__title {overflow: hidden;font-size: 17px;font-weight: 600;text-overflow: ellipsis;white-space: nowrap;}.topbar__text-btn {border: 0;background: transparent;color: #1464ff;font-size: 14px;font-weight: 500;}/* ------- Tab bar ------- */.tabbar {display: flex;flex: 0 0 auto;gap: 8px;padding: 6px 12px calc(6px + env(safe-area-inset-bottom));border-top: 1px solid #e8eaed;}.tabbar__item {flex: 1;min-height: 40px;border: 0;border-radius: 12px;background: transparent;color: #6b7280;font-size: 14px;font-weight: 600;}.tabbar__item--active {background: #eef4ff;color: #1464ff;}/* ------- Chat pane ------- ** The library's <Chat> root is already `display:flex; flex-direction:column;* height:100%`, so it fills the screen on its own. The demo only arranges the* three slotted children: header (fixed) / message list (grow + scroll) /* input (fixed). `min-height:0` on the list breaks the flexbox min-height* chain so it scrolls instead of stretching the pane.*/.chat-pane__header {flex: 0 0 auto;padding-top: env(safe-area-inset-top);border-bottom: 1px solid #e8eaed;}.chat-pane > :nth-child(2) {flex: 1;min-height: 0;}.chat-pane > :last-child {flex: 0 0 auto;padding-bottom: env(safe-area-inset-bottom);border-top: 1px solid #e8eaed;}.chat-pane__actions {display: flex;align-items: center;gap: 4px;}.icon-btn {display: inline-flex;width: 36px;height: 36px;align-items: center;justify-content: center;border: 0;border-radius: 10px;background: transparent;color: #1f2329;}.icon-btn:active {background: #eef0f3;}
6. 配置国际化内容。
新建 src/i18n.ts 并写入以下内容:
// 本 demo 自身字符串的翻译(聊天组件的翻译由 UIKit 自身// 通过 `language` prop 处理)。键名以 `demo.` 作为命名空间,// 确保不会与库的 i18next 资源发生冲突。//// `languageResources` 传递给 <UIKitProvider>;可在任意位置// 通过 `const { t } = useUIKit()` 和 `t('demo.chats')` 读取字符串。type TranslationTree = { [key: string]: string | TranslationTree };interface LanguageResource {lng: string;translation: TranslationTree;}const en = {demo: {appTitle: 'Chat UIKit H5',field: { sdkAppId: 'SDKAppID', userId: 'UserID', userSig: 'UserSig' },placeholder: {sdkAppId: 'Your SDKAppID',userId: 'e.g. user_001',userSig: 'Test UserSig from IM console',},error: {required: 'Please fill in SDKAppID, UserID and UserSig.',loginFailed: 'Login failed, please check your credentials.',},login: 'Login',loggingIn: 'Logging in…',chats: 'Chats',contacts: 'Contacts',logout: 'Logout',search: 'Search',setting: 'Setting',},};const zh = {demo: {appTitle: 'Chat UIKit H5',field: { sdkAppId: 'SDKAppID', userId: '用户 ID', userSig: '用户签名' },placeholder: {sdkAppId: '请输入 SDKAppID',userId: '例如 user_001',userSig: '控制台获取的测试 UserSig',},error: {required: '请填写 SDKAppID、用户 ID 和用户签名。',loginFailed: '登录失败,请检查登录凭据。',},login: '登录',loggingIn: '登录中…',chats: '会话',contacts: '通讯录',logout: '退出登录',search: '搜索',setting: '设置',},};export const languageResources: LanguageResource[] = [{ lng: 'en-US', translation: en },{ lng: 'zh-CN', translation: zh },];
步骤3:获取 SDKAppID、userID 和 userSig
参数 | 类型 | 说明 |
SDKAppID | Number | 说明: SDKAppID 是腾讯云 IM 客户应用的唯一标识。我们建议每一个独立的 App 都申请一个新的 SDKAppID。不同 SDKAppID 之间的消息是天然隔离的,不能互通。 |
userID | String | 用户的唯一标识符,由您定义,只能包含大小写字母(a-z,A-Z)、数字(0-9)、下划线和连字符。 |
userSig | String | 用户登录即时通信 IM 的密码,其本质是对 UserID 等信息加密后得到的密文。 说明: 开发环境:快速跑通 Demo,可以通过 即时通信 IM 控制台 获取 UserSig。 生产环境:将 UserSig 的计算代码集成到您的服务端,并提供面向项目的接口, 正确的 UserSig 签发方式请参见 服务端生成 UserSig。 |
注意:
SDKAppID:在 即时通信 IM 控制台 > 应用管理 单击创建新应用,获取 SDKAppID。

userID:单击 即时通信 IM 控制台 > 消息服务 Chat > 账号管理,切换至目标应用所在账号,您可以创建 2~3 个账号用于体验单聊、群聊的功能。

userSig:单击 即时通信 IM 控制台 > 开发工具 > UserSig 生成校验,切换至目标应用所在账号,填写创建的 userID,即可生成 userSig。

运行和测试
填入登录鉴权信息后即可运行项目开始体验。
npm run dev
集成更多高级特性
音视频通话
1. 安装
@trtc/calls-uikit-react 依赖npm install @trtc/calls-uikit-react
2. 从
@trtc/calls-uikit-react 导出 TUICallKit,并挂载到 DOM 节点上。在
src/App.tsx 文件中继续补充下面的代码:import { useState } from 'react';import { LoginStatus, UIKitProvider, useLoginState } from '@tencentcloud/chat-uikit-react';import { TUICallKit } from '@trtc/calls-uikit-react';import { LoginView } from './LoginView';import { ChatLayout } from './ChatLayout';import { languageResources } from './i18n';function App() {const { status } = useLoginState();const isLogined = status === LoginStatus.SUCCESS;// `language` drives both the UIKit's own strings and our demo strings.const [language, setLanguage] = useState('en-US');return (<UIKitProvider theme="light" language={language} languageResources={languageResources}>{isLogined? (<><ChatLayout onChangeLanguage={setLanguage} />{/* Audio/video calls. Mount once at the app root; it shares the chatlogin session, so no extra login is needed. The call buttons arealready built into the chat header. */}<div className="call-kit"><TUICallKit /></div></>): <LoginView language={language} onChangeLanguage={setLanguage} />}</UIKitProvider>);}export default App;
云端搜索
说明:
搜索,在客服、社交、在线教育、在线医疗、OA 等场景下是刚需功能,可帮助用户快速查找群组、用户、消息,提升产品使用体验和用户粘性。
由于 Web 平台本地存储特殊性等原因,无法实现本地搜索,为了更好地满足对于搜索能力的需求,推出了云端搜索能力。云端搜索功能支持全局搜索和会话内搜索,同时支持搜索群组、用户和消息。
云端搜索在“步骤 2”中已经默认集成,如果需要关闭云端搜索功能,请参考以下代码:
1. 注释
ChatHeader 中 ChatHeaderRight 属性,和会话内搜索侧边栏相关的代码<ChatHeaderH5className="chat-pane__header"enableUserStatusonBack={handleChatBack}ChatHeaderRight={(<div className="chat-pane__actions">{/* <button className="icon-btn" type="button" aria-label={t('demo.search')} onClick={() => openFromChat('search')}><IconSearch size="20px" /></button> */}<button className="icon-btn" type="button" aria-label={t('demo.setting')} onClick={() => openFromChat('setting')}><IconBulletpoint size="20px" /></button></div>)}/>
2. 在
ConversationList 组件上关闭云端搜索<ConversationListH5 enableSearch={false} enableCreate={false} />
常见问题
什么是 UserSig?如何生成 UserSig?
UserSig 是用户登录即时通信 IM 的密码,其本质是对 UserID 等信息加密后得到的密文。UserSig 签发方式是将 UserSig 的计算代码集成到您的服务端,并提供面向项目的接口,在需要 UserSig 时由您的项目向业务服务器发起请求获取动态 UserSig。更多详情请参见 服务端生成 UserSig。
注意:
是否支持 React 17 版本?
目前不支持 React v17.x,仅支持 React v18.2+ 以上的版本。
我能不能使用第三方组件库,例如 Ant-Design?
核心组件之间的粘连代码可以使用其他组件库,这一点在示例代码中也可以看到,例如您可以将
<ChatSettingH5 /> 封装成全屏抽屉组件。但是核心组件内部已经存在的组件暂时不支持修改。import { Drawer } from 'antd';const [isChatSettingShow, setIsChatSettingShow] = useState(false);function onChatSettingClose() {setIsChatSettingShow(false);}<Drawertitle="设置"open={isChatSettingShow}onClose={onChatSettingClose}><ChatSettingH5 /></Drawer>
表情包的使用
为了尊重版权,IM Demo/TUIKit 工程中默认不包含大表情元素切图。正式上线商用前请您替换为自己设计或拥有版权的其他表情包。下图所示默认的小黄脸表情包版权归腾讯云所有,可以有偿授权使用,如需获得授权您可以通过升级至 IM 企业版套餐 免费使用该表情包。
