TUIKit 是基于 IM SDK 的一款 UI 组件库,可通过 UI 组件快速实现聊天、会话、搜索、关系链、群组等功能。本文介绍如何快速集成 TUIKit 并实现核心功能。
标准版界面效果如下图所示:
会话列表  | 聊天页面  | 视频通话  | 
![]()   | ![]()   |  ![]()   | 
关键概念
针对用户不同场景的诉求和体积要求,我们推出了多个版本的 UI 组件 ,您可以根据实际业务需求选择集成最适合的版本。
前提条件
HBuilderX 需要升级到最新版本
TypeScript / JavaScript (TUIKit 使用 ts 语言开发,支持在 js 或者 ts 项目中集成)
Vue2/Vue3
sass(sass-loader 版本 ≤ 10.1.1)
node(12.13.0 ≤ node,推荐使用 Node.js 官方 LTS 版本 16.17.0)
npm(版本请与 node 版本匹配)
创建项目
1. 打开 HBuilderX,在菜单栏中选择 “文件 > 创建 > 项目”,创建一个名为 chat-example 的 uni-app 项目。

2. 在终端输入
npm init -y,创建package.json文件。npm init -y
下载并导入组件
步骤1:安装依赖
1. 下载组件。
npm i tuikit-atomicx-uniapp-wx-standard
npm i tuikit-atomicx-uniapp-wx-standard@vue2
2. 拷贝源码。
mkdir -p ./TUIKit && cp -r node_modules/tuikit-atomicx-uniapp-wx-standard/ ./TUIKit && cp node_modules/@trtc/call-engine-lite-wx/RTCCallEngine.wasm.br ./static
xcopy node_modules\\tuikit-atomicx-uniapp-wx-standard .\\TUIKit /i /excopy node_modules\\@trtc\\call-engine-lite-wx\\RTCCallEngine.wasm.br .\\static
步骤2:引入组件
注意:
请检查以下页面文件是否已存在。如果不存在,请按照指定的路径结构新建对应文件。
Vue3 使用组合式 API (Composition API) 实现。
请将以下内容复制到 pages/login/login.vue 文件。
<template><view class="container"><view class="login-section"><text class="title">用户登录</text><inputclass="input-box"v-model="userID"placeholder="请输入用户ID"placeholder-style="color:#BBBBBB;"/><button class="login-btn" @click="handleLogin">登录</button></view></view></template><script lang="ts" setup>import { ref } from 'vue'import { useLoginState } from '../../TUIKit'const userID = ref('')const handleLogin = async () => {// 必填信息// 测试TUIKit时可以从腾讯云IM控制台获取userSig// 生产环境部署请从您的服务器获取// 查看文档:https://cloud.tencent.com/document/product/269/32688await useLoginState().login({userId: userID.value,userSig: '',sdkAppId: 0,})wx.$globalCallPagePath = 'TUIKit/components/CallView/CallView'; // 配置全局监听页面路径uni.switchTab({ url: '/pages/conversation/conversation' })}</script><style scoped>.container {padding: 40px;height: 100vh;}.login-section {display: flex;flex-direction: column;align-items: center;margin-top: 100px;}.title {font-size: 24px;font-weight: bold;margin-bottom: 40px;}.input-box {width: 80%;height: 50px;border: 1px solid #DDD;border-radius: 8px;padding: 0 15px;margin-bottom: 20px;}.login-btn {width: 80%;height: 50px;background: #006EFF;color: white;border-radius: 8px;}</style>
请将以下内容复制到 pages/conversation/conversation.vue 文件。
<template><view><Conversation :onConversationSelect="handleConversationSelect"></Conversation><view v-if="!conversationList?.length" class="empty-guide"><text class="guide-text">暂无会话,请输入用户ID创建聊天</text><view class="input-container"><input v-model="inputUserID" class="userid-input" placeholder="请输入对方用户ID"placeholder-class="placeholder-style" /><button class="guide-btn" @click="handleCreateConversation">创建会话</button></view></view></view></template><script lang="ts" setup>import { onShow } from '@dcloudio/uni-app'import { watch, ref } from 'vue';import Conversation from '../../TUIKit/components/ConversationList/ConversationList.vue'import { useConversationListState } from '../../TUIKit';const { createC2CConversation, conversationList, setActiveConversation, totalUnRead } = useConversationListState();const inputUserID = ref('');const handleCreateConversation = async () => {if (!inputUserID.value.trim()) {return;}await createC2CConversation(inputUserID.value);};const updateTabBarBadge = () => {const tabBarList = ['pages/profile/profile', 'pages/conversation/conversation']const pages = getCurrentPages()const currentPage = pages[pages.length - 1]?.routeconst isTabBarPage = currentPage && tabBarList.includes(currentPage)if (!isTabBarPage) {return}if (totalUnRead.value > 0) {uni.setTabBarBadge({index: 0,text: totalUnRead.value > 99 ? '99+' : totalUnRead.value.toString()});}};watch(totalUnRead, updateTabBarBadge, { immediate: true });const handleConversationSelect = (conversation) => {const { conversationID } = conversationsetActiveConversation(conversationID);uni.navigateTo({url: '/pages/chat/chat',});}onShow(() => {if (totalUnRead.value > 0) {uni.setTabBarBadge({index: 0,text: totalUnRead.value > 99 ? '99+' : totalUnRead.value.toString()});} else {uni.removeTabBarBadge({ index: 0 });}});</script><style scoped>.empty-guide {display: flex;flex-direction: column;align-items: center;justify-content: center;padding: 40px 20px;}.guide-text {font-size: 16px;color: red;margin-bottom: 20px;}.input-container {width: 80%;display: flex;flex-direction: column;align-items: center;}.userid-input {width: 100%;height: 40px;padding: 0 10px;margin-bottom: 15px;border: 1px solid #eee;border-radius: 6px;font-size: 14px;}.placeholder-style {color: #ccc;}.guide-btn {width: 50%;line-height: 40px;height: 40px;background-color: #07c160;color: white;border-radius: 6px;font-size: 16px;}</style>
请将以下内容复制到 pages/chat/chat.vue 文件。
<template><div class="TUIChat"><MessageList /><MessageInput /></div></template><script lang="ts" setup>import MessageInput from '../../TUIKit/components/MessageInput/MessageInput.vue';import MessageList from '../../TUIKit/components/MessageList/MessageList.vue';</script><style>.TUIChat {display: flex;flex-direction: column;width: 100vw;height: 100vh;overflow: hidden;}</style>
请将以下内容复制到 pages/profile/profile.vue 文件。
<template><view class="container"><view class="profile"><image class="avatar" :src="userInfo.avatarUrl || ''" /><text class="name">{{ userInfo.userName || '未设置昵称' }}</text><text class="id">userID: {{ userInfo.userId }}</text></view><input v-model="nick" placeholder="新昵称" /><input v-model="avatar" placeholder="新头像URL" /><button @click="save">保存</button><button @click="logoutHandle" class="logout">退出</button></view></template><script lang="ts" setup>import { ref } from 'vue'import { useLoginState } from '../../TUIKit'const { loginUserInfo: userInfo, setSelfInfo, logout } = useLoginState()const nick = ref('')const avatar = ref('')const save = async () => {if (!nick.value && !avatar.value) return uni.showToast({ title: '请输入内容', icon: 'none' })try {await setSelfInfo({...(nick.value && { userName: nick.value }),...(avatar.value && { avatarUrl: avatar.value })})uni.showToast({ title: '保存成功' })} catch {uni.showToast({ title: '保存失败', icon: 'none' })}}const logoutHandle = async () => {await logout()uni.reLaunch({ url: '/pages/login/login' })}</script><style scoped>.container {padding: 20px;}.profile {display: flex;flex-direction: column;align-items: center;margin-bottom: 20px;}.avatar {width: 80px;height: 80px;border-radius: 50%;background: #f0f0f0;}.name {font-size: 18px;margin: 10px 0;}.id {color: #888;font-size: 14px;}input {width: 100%;height: 40px;margin-bottom: 15px;padding: 0 10px;border: 1px solid #ddd;border-radius: 6px;}button {width: 100%;height: 40px;margin-bottom: 10px;background: #07c160;color: white;border-radius: 6px;}.logout {background: white;color: #f56c6c;border: 1px solid #f56c6c;}</style>
请将以下内容复制到 pages.json 文件。
{"pages": [{"path": "pages/login/login","style": {"navigationBarTitleText": "uni-app"}},{"path": "pages/conversation/conversation","style": {"navigationBarTitleText": "uni-app"}},{"path": "pages/profile/profile","style": {"navigationBarTitleText": "uni-app"}},{"path": "pages/chat/chat","style": {"navigationBarTitleText": "uni-app"}}],"tabBar": {"list": [{"pagePath": "pages/conversation/conversation","text": "消息","badge": "{{totalUnRead > 99 ? '99+' : totalUnRead.toString()}}"},{"pagePath": "pages/profile/profile","text": "个人中心"}]},"globalStyle": {"navigationBarTextStyle": "black","navigationBarTitleText": "uni-app","navigationBarBackgroundColor": "#F8F8F8","backgroundColor": "#F8F8F8"},"uniIdRouter": {}}
Vue2 使用选项式 API (Options API) 实现。
请将以下内容复制到 pages/login/login.vue 文件。
<template><view class="container"><view class="login-section"><text class="title">用户登录</text><input class="input-box" v-model="userID" placeholder="请输入用户ID" placeholder-style="color:#BBBBBB;" /><button class="login-btn" @click="handleLogin">登录</button></view></view></template><script lang="ts">// @ts-nocheckimport { useLoginState } from '../../TUIKit'export default {data() {return {userID: ''}},methods: {async handleLogin() {// 必填信息// 测试TUIKit时可以从腾讯云IM控制台获取userSig// 生产环境部署请从您的服务器获取// 查看文档:https://cloud.tencent.com/document/product/269/32688await useLoginState().login({userId: this.userID,userSig: '',sdkAppId: 0,})wx.$globalCallPagePath = 'TUIKit/components/CallView/CallView'; // 配置全局监听页面路径uni.switchTab({ url: '/pages/conversation/conversation' })}}}</script><style scoped>.container {padding: 40px;height: 100vh;}.login-section {display: flex;flex-direction: column;align-items: center;margin-top: 100px;}.title {font-size: 24px;font-weight: bold;margin-bottom: 40px;}.input-box {width: 80%;height: 50px;border: 1px solid #DDD;border-radius: 8px;padding: 0 15px;margin-bottom: 20px;}.login-btn {width: 80%;height: 50px;background: #006EFF;color: white;border-radius: 8px;}</style>
请将以下内容复制到 pages/conversation/conversation.vue 文件。
<template><view><Conversation :onConversationSelect="handleConversationSelect"></Conversation><view v-if="!conversationList || !conversationList.length" class="empty-guide"><text class="guide-text">暂无会话,请输入用户ID创建聊天</text><view class="input-container"><input v-model="inputUserID" class="userid-input" placeholder="请输入对方用户ID"placeholder-class="placeholder-style" /><button class="guide-btn" @click="handleCreateConversation">创建会话</button></view></view></view></template><script lang="ts">// @ts-nocheckimport Conversation from '../../TUIKit/components/ConversationList/ConversationList.vue'import { useConversationListState } from '../../TUIKit';const conversationState = useConversationListState();export default {components: {Conversation},data() {return {inputUserID: '',conversationList: conversationState.conversationList || [],totalUnRead: conversationState.totalUnRead || 0}},watch: {totalUnRead(newVal) {this.updateTabBarBadge();}},mounted() {this.$watch(() => conversationState.conversationList,(newVal) => {this.conversationList = newVal || [];},{ immediate: true, deep: true });this.$watch(() => conversationState.totalUnRead,(newVal) => {this.totalUnRead = newVal || 0;},{ immediate: true });},onShow() {this.updateTabBarBadge();},methods: {async handleCreateConversation() {if (!this.inputUserID.trim()) {uni.showToast({title: '请输入用户ID',icon: 'none'});return;}try {await conversationState.createC2CConversation(this.inputUserID);this.inputUserID = '';} catch (error) {uni.showToast({title: '创建会话失败,请检查用户ID是否注册',icon: 'none'});}},updateTabBarBadge() {const tabBarList = ['pages/profile/profile', 'pages/conversation/conversation'];const pages = getCurrentPages();const currentPage = pages[pages.length - 1] ? pages[pages.length - 1].route : '';const isTabBarPage = currentPage && tabBarList.includes(currentPage);if (!isTabBarPage) {return;}if (this.totalUnRead > 0) {uni.setTabBarBadge({index: 0,text: this.totalUnRead > 99 ? '99+' : this.totalUnRead.toString()});} else {uni.removeTabBarBadge({ index: 0 });}},handleConversationSelect(conversation) {const { conversationID } = conversation;conversationState.setActiveConversation(conversationID);uni.navigateTo({url: '/pages/chat/chat',});}}}</script><style scoped>.empty-guide {display: flex;flex-direction: column;align-items: center;justify-content: center;padding: 40px 20px;}.guide-text {font-size: 16px;color: red;margin-bottom: 20px;}.input-container {width: 80%;display: flex;flex-direction: column;align-items: center;}.userid-input {width: 100%;height: 40px;padding: 0 10px;margin-bottom: 15px;border: 1px solid #eee;border-radius: 6px;font-size: 14px;}.placeholder-style {color: #ccc;}.guide-btn {width: 50%;line-height: 40px;height: 40px;background-color: #07c160;color: white;border-radius: 6px;font-size: 16px;}</style>
请将以下内容复制到 pages/chat/chat.vue 文件。
<template><div class="TUIChat"><MessageList class="message-list" /><MessageInput /></div></template><script lang="ts">// @ts-nocheckimport MessageInput from '../../TUIKit/components/MessageInput/MessageInput.vue';import MessageList from '../../TUIKit/components/MessageList/MessageList.vue';export default {components: {MessageInput,MessageList}}</script><style lang="scss">.TUIChat {display: flex;flex-direction: column;width: 100vw;height: 100vh;.message-list {display: flex;flex: 1;min-height: 0;width: 100%;height: 100%;}}</style>
请将以下内容复制到 pages/profile/profile.vue 文件。
<template><view class="container"><view class="profile"><image class="avatar" :src="userInfo.avatarUrl || ''" /><text class="name">{{ userInfo.userName || '未设置昵称' }}</text><text class="id">userID: {{ userInfo.userId }}</text></view><input v-model="nick" placeholder="新昵称" /><input v-model="avatar" placeholder="新头像URL" /><button @click="save">保存</button><button @click="logoutHandle" class="logout">退出</button></view></template><script lang="ts">// @ts-nocheckimport { useLoginState } from '../../TUIKit'export default {data() {return {nick: '',avatar: '',userInfo: {}}},created() {// 初始化用户信息const loginState = useLoginState();this.setSelfInfo = loginState.setSelfInfo;this.logout = loginState.logout;// 监听用户信息变化this.$watch(() => loginState.loginUserInfo, (newVal) => {this.userInfo = newVal;}, { immediate: true, deep: true });},methods: {async save() {if (!this.nick && !this.avatar) {uni.showToast({ title: '请输入内容', icon: 'none' });return;}try {const updateData = {};if (this.nick) {updateData.userName = this.nick;}if (this.avatar) {updateData.avatarUrl = this.avatar;}await this.setSelfInfo(updateData);uni.showToast({ title: '保存成功' });this.nick = '';this.avatar = '';} catch (error) {uni.showToast({ title: '保存失败', icon: 'none' });}},async logoutHandle() {await this.logout();uni.reLaunch({ url: '/pages/login/login' });}}}</script><style scoped>.container {padding: 20px;}.profile {display: flex;flex-direction: column;align-items: center;margin-bottom: 20px;}.avatar {width: 80px;height: 80px;border-radius: 50%;background: #f0f0f0;}.name {font-size: 18px;margin: 10px 0;}.id {color: #888;font-size: 14px;}input {width: 100%;height: 40px;margin-bottom: 15px;padding: 0 10px;border: 1px solid #ddd;border-radius: 6px;}button {width: 100%;height: 40px;margin-bottom: 10px;background: #07c160;color: white;border-radius: 6px;}.logout {background: white;color: #f56c6c;border: 1px solid #f56c6c;}</style>
请将以下内容复制到 pages.json 文件。
{"pages": [{"path": "pages/login/login","style": {"navigationBarTitleText": "uni-app"}},{"path": "pages/conversation/conversation","style": {"navigationBarTitleText": "uni-app"}},{"path": "pages/profile/profile","style": {"navigationBarTitleText": "uni-app"}},{"path": "pages/chat/chat","style": {"navigationBarTitleText": "uni-app"}}],"tabBar": {"list": [{"pagePath": "pages/conversation/conversation","text": "消息","badge": "{{totalUnRead > 99 ? '99+' : totalUnRead.toString()}}"},{"pagePath": "pages/profile/profile","text": "个人中心"}]},"globalStyle": {"navigationBarTextStyle": "black","navigationBarTitleText": "uni-app","navigationBarBackgroundColor": "#F8F8F8","backgroundColor": "#F8F8F8"},"uniIdRouter": {}}
步骤3:获取 SDKAppID、userID 和 userSig
注意:
SDKAppID:在 即时通信 IM 控制台 > 应用管理 单击创建新应用,获取 SDKAppID。

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

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

运行和测试
步骤1:运行

步骤2:发送第一条消息

注意:
如果集成音视频通话功能,将增加 400KB 的体积增量。
步骤3:增加音视频通话(可选)
功能  | 音视频通话组件  | 
1v1 视频、音频通话  | ✓  | 
全局监听来电  | ✓  | 
呼叫/接听/拒绝/挂断  | ✓  | 
1. 开通服务
2. 配置微信开放平台
开通企业类小程序

在小程序控制台开启实时音视频接口
小程序推拉流标签使用权限暂时只开放给有限类目,具体支持类目参见该地址。
符合类目要求的小程序,需要在 微信公众平台 > 开发 > 开发管理 > 接口设置中自助开通该组件权限。

3. 在小程序控制台配置域名

将以下域名添加到 socket 合法域名:
域名  | 说明  | 是否必须  | 
wss://${SDKAppID}w4c.my-imcloud.com | v3.4.6起,SDK 支持独立域名,可更好地保障服务稳定性。 例如您的 SDKAppID 是 1400xxxxxx,则独立域名为:  wss://1400xxxxxxw4c.my-imcloud.com | 必须  | 
wss://wss.im.qcloud.com | Web IM 业务域名  | 必须  | 
wss://wss.tim.qq.com | Web IM 业务域名  | 必须  | 
wss://wssv6.im.qcloud.com | Web IM 业务域名  | 必须  | 
将以下域名添加到 request 合法域名:
域名  | 说明  | 是否必须  | 
https://web.sdk.qcloud.com | Web IM 业务域名  | 必须  | 
https://boce-cdn.my-imcloud.com | Web IM 业务域名  | 必须  | 
https://api.im.qcloud.com | Web IM 业务域名  | 必须  | 
https://events.im.qcloud.com | Web IM 业务域名  | 必须  | 
https://webim.tim.qq.com | Web IM 业务域名  | 必须  | 
https://wss.im.qcloud.com | Web IM 业务域名  | 必须  | 
https://wss.tim.qq.com | Web IM 业务域名  | 必须  | 
将以下域名添加到 uploadFile 合法域名:
域名  | 说明  | 是否必须  | 
https://${SDKAppID}-cn.rich.my-imcloud.com | 从 2024年9月10日起,新增应用默认分配 COS 独立域名。 例如您的 SDKAppID 是 1400xxxxxx,则 COS 独立域名为: https://1400xxxxxx-cn.rich.my-imcloud.com | 必须  | 
https://cn.rich.my-imcloud.com | 文件上传域名  | 必须  | 
https://cn.imrich.qcloud.com | 文件上传域名  | 必须  | 
https://cos.ap-shanghai.myqcloud.com | 文件上传域名  | 必须  | 
https://cos.ap-shanghai.tencentcos.cn | 文件上传域名  | 必须  | 
https://cos.ap-guangzhou.myqcloud.com | 文件上传域名  | 必须  | 
将以下域名添加到 downloadFile 合法域名:
域名  | 说明  | 是否必须  | 
https://${SDKAppID}-cn.rich.my-imcloud.com | 从 2024年9月10日起,新增应用默认分配 COS 独立域名。 例如您的 SDKAppID 是 1400xxxxxx,则 COS 独立域名为: https://1400xxxxxx-cn.rich.my-imcloud.com | 必须  | 
https://cn.rich.my-imcloud.com | 文件下载域名  | 必须  | 
https://cn.imrich.qcloud.com | 文件下载域名  | 必须  | 
https://cos.ap-shanghai.myqcloud.com | 文件下载域名  | 必须  | 
https://cos.ap-shanghai.tencentcos.cn | 文件下载域名  | 必须  | 
https://cos.ap-guangzhou.myqcloud.com | 文件下载域名  | 必须  | 
4. 配置页面路由
在 pages.json 文件注册全局监听页面。
{"path": "TUIKit/components/CallView/CallView","style": {"navigationBarTitleText": "uni-app"}}
删除 Debug 脚本
出于体积和安全双重因素考虑,请在发布前删除项目目录下 
/TUIKit/debug 文件夹。在开发阶段为了方便开发,项目提供生成本地 UserSig 的脚本文件存放于TUIKit/debug文件夹中,但这并不安全,该方法中 SECRETKEY 很容易被反编译逆向破解,一旦您的密钥泄露,攻击者就可以盗用您的腾讯云流量,因此该方法仅适合本地跑通 Demo 和功能调试。因此,请在项目发布前删除 Debug 脚本,使用后台生成 UserSig,具体原因和操作步骤请参考文档:生成 UserSig。常见问题
移除音视频通话功能
移除 
static/RTCCallEngine.wasm.br 文件。移除 TUIKit/index.ts 中的模块导出。

移除 TUIKit/components/MessageInput/MessageInput.vue 中的通话按钮。

移除在 pages.json 中为音视频通话添加的全局页面监听配置。
// {// "path": "TUIKit/components/CallView/CallView",// "style": {// "navigationBarTitleText": "uni-app"// }// }
Debug 脚本的作用
出于体积和安全双重因素考虑,请在发布前删除项目目录下 
/TUIKit/debug 文件夹。在开发阶段为了方便开发,项目提供生成本地 UserSig 的脚本文件存放于TUIKit/debug文件夹中,但这并不安全,该方法中 SECRETKEY 很容易被反编译逆向破解,一旦您的密钥泄露,攻击者就可以盗用您的腾讯云流量,因此该方法仅适合本地跑通 Demo 和功能调试。因此,请在项目发布前删除 Debug 脚本,使用后台生成 UserSig,具体原因和操作步骤请参考文档:生成 UserSig。

