uni-app

最近更新时间:2026-06-11 10:48:17

我的收藏
TUIKit 默认实现了文本、图片、语音、视频、文件等基本消息类型的发送和展示,如果这些类型满足不了您的需求,您可以新增自定义消息类型(订单卡片、商品卡片、问卷、骰子、链接卡等)。

基本消息类型

消息类型
显示效果图
文本类消息



图片类消息



语音类消息



视频类消息



文件类消息




自定义消息实现步骤

步骤 1:定义业务数据

自定义消息需要传入哪些数据是用户自行决定的,假设我们要开发一个订单卡片(OrderCard)的自定义消息,示例代码如下所示:
{
businessID: 'order_card', // 业务类型标识,渲染分发依赖此字段
version: 1, // 协议版本号,后续字段升级时用于兼容判断
orderID: 'TX20260602001',
title: '腾讯云 IM 套餐 · 旗舰版',
price: 2999,
cover: 'https://web.sdk.qcloud.com/im/assets/images/tencent_rtc_logo.png' // 演示图片,业务方请替换为自己的图片
}

步骤 2:创建自定义消息组件

新建 OrderCardMessage.nvue,该组件接收 message 属性,解析 message.messageBody.customMessage.data 进行消息渲染。
<template>
<view v-if="data.businessID === 'order_card'" class="order-card">
<view class="order-card__cover">
<image class="order-card__cover-image" :src="data.cover" mode="aspectFit" resize="contain" />
</view>
<view class="order-card__body">
<text class="order-card__title">{{ data.title }}</text>
<text class="order-card__id">订单号:{{ data.orderID }}</text>
<text class="order-card__price">¥{{ priceText }}</text>
</view>
</view>
</template>

<script setup>
import { computed } from 'vue';

const props = defineProps({
message: { type: Object, required: true }
})

const data = computed(() => {
try {
return JSON.parse(props.message.messageBody?.customMessage?.data || '{}')
} catch (e) {
return {}
}
})
const priceText = computed(() => Number(data.value.price || 0).toFixed(2))
</script>

<style>
.order-card { width: 480rpx; background-color: #FFFFFF; border-radius: 16rpx; overflow: hidden; }
.order-card__cover { width: 480rpx; height: 240rpx; justify-content: center; align-items: center; }
.order-card__cover-image { width: 480rpx; height: 240rpx; }
.order-card__body { padding: 20rpx; }
.order-card__title { font-size: 30rpx; color: #1A1A1A; lines: 2; }
.order-card__id { font-size: 24rpx; color: #999; margin-top: 8rpx; }
.order-card__price { font-size: 32rpx; color: #FF4D4F; font-weight: 600; margin-top: 16rpx; }
</style>

步骤 3:注册到 MessageList

MessageListcustomMessageRender 接受一个 (message, conversationID) => Component | null 工厂函数,根据 步骤 1:定义业务数据 中约定的 key(businessID)返回对应业务组件即可,返回 null 自动降级为内置渲染。
说明:
customMessageRender自己发出去的、对方发过来的、滚屏加载的历史消息全部生效。如果接收端没注册渲染器,会自动降级为 TUIKit 内置 CustomMessage 默认渲染。
<template>
<MessageList :conversationID="conversationID" :customMessageRender="renderCustomMessage" />
</template>

<script setup>
import { ref } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import MessageList from '@/uni_modules/tuikit-atomic-x/components/MessageList/MessageList.nvue';
import OrderCardMessage from './OrderCardMessage.nvue'

const renderCustomMessage = (message) => {
try {
const data = JSON.parse(message.messageBody?.customMessage?.data || '{}')
if (data.businessID === 'order_card') return OrderCardMessage
} catch (e) {}
return null
}

const conversationID = ref('');

// ==================== 生命周期 ====================
onLoad((options) => {
if (!options.conversationID) {
console.error('未接收到 conversationID 参数')
return
}
conversationID.value = options.conversationID;
})

</script>

步骤 4:触发发送自定义消息

MessageInputtoolList 中追加一个发送订单的工具按钮,在 callback 里调用 sendCustomMessage 发送订单卡片消息。
说明:
toolList 没传时用内置 DEFAULT_TOOLS(照片/视频/文件/语音通话/视频通话),传了就完全替换。
自定义 ToolItem 必须带 callback,否则不响应。
icon/cover 支持线上 URL 或工程内静态资源路径,业务上线请替换为自己的图标/图片。
<script setup>
import { ref, shallowRef } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import { DEFAULT_TOOLS } from '@/uni_modules/tuikit-atomic-x/components/MessageInput/MessageInput.nvue'
import { useMessageInputState } from '@/uni_modules/tuikit-atomic-x/state/MessageInputState'

const conversationID = ref('');
const inputState = shallowRef(null)

// 在内置工具基础上追加一个"订单"按钮
const toolList = [
...DEFAULT_TOOLS,
{
id: 'order',
name: '订单',
icon: '/uni_modules/tuikit-atomic-x/static/icon/custom.png', // 演示用工程内既有图标,业务方请替换为自己的图标
callback: () => {
// 实际业务一般跳订单选择页,回来后用拿到的订单数据调发送
inputState.value?.sendCustomMessage({
businessID: 'order_card',
version: 1,
orderID: 'TX20260602001',
title: '腾讯云 IM 套餐 · 旗舰版',
price: 2999,
cover: 'https://web.sdk.qcloud.com/im/assets/images/tencent_rtc_logo.png' // 演示图片,业务方请替换为自己的图片
}).catch(err => uni.showToast({ icon: 'none', title: err.message || '发送失败' }))
}
}
]

onLoad((options) => {
if (!options.conversationID) {
console.error('未接收到 conversationID 参数')
return
}
conversationID.value = options.conversationID;
inputState.value = useMessageInputState({ conversationID: conversationID.value });
})
</script>

<template>
<MessageInput :conversationID="conversationID" :toolList="toolList" />
</template>

完整示例代码

修改聊天页 pages/scenes/chat/chat/index.nvue,新增订单按钮 + 订单卡片消息渲染。下面是改完后的完整代码(复制覆盖即可),同目录下新增一个 OrderCardMessage.nvue 组件。
pages/scenes/chat/chat/index.nvue
pages/scenes/chat/chat/OrderCardMessage.nvue
<template>
<view class="page-container">
<CustomNavbar
position="fixed"
:title="navigationTitle"
:showBack="true"
:showMenu="true"
@back="onNavBack"
@menuSelect="onNavMenuSelect"
/>

<view
class="message-list-container"
:style="{ paddingBottom: inputToolbarHeight + 'px' }"
@tap="closeSoftKeyboard"
>
<MessageList
v-if="conversationID"
:conversationID="conversationID"
:locateMessage="locateMessage"
:inputToolbarHeight="inputToolbarHeight"
:inputPanelHeight="inputPanelHeight"
:customMessageRender="renderCustomMessage"
@onMessageListTap="closeSoftKeyboard"
/>
</view>

<MessageInput
v-if="conversationID"
ref="messageInputRef"
:conversationID="conversationID"
:toolList="toolList"
:setOfflinePushInfo="setOfflinePushInfo"
@height-change="onMessageInputHeightChange"
/>
</view>
</template>

<script setup lang="ts">
import { ref, shallowRef, computed } from 'vue'
import { onLoad, onShow } from '@dcloudio/uni-app'
import MessageList from '@/uni_modules/tuikit-atomic-x/components/MessageList/MessageList.nvue'
import MessageInput, { DEFAULT_TOOLS } from '@/uni_modules/tuikit-atomic-x/components/MessageInput/MessageInput.nvue'
import CustomNavbar from '@/uni_modules/tuikit-atomic-x/components/CustomNavbar/CustomNavbar.nvue'
import { useConversationListState } from '@/uni_modules/tuikit-atomic-x/state/ConversationListState'
import { useGroupState } from '@/uni_modules/tuikit-atomic-x/state/GroupState'
import { useMessageInputState } from '@/uni_modules/tuikit-atomic-x/state/MessageInputState'
import { MessageType } from '@/uni_modules/tuikit-atomic-x/types/message'
import type { OfflinePushContext, OfflinePushInfo } from '@/uni_modules/tuikit-atomic-x/types/message'
import OrderCardMessage from './OrderCardMessage.nvue'

const { joinedGroupList, fetchJoinedGroupList } = useGroupState()

const conversationID = ref('')
const locateMessage = ref(null)
const isInGroup = ref(false)
const messageInputRef = ref(null)
const inputToolbarHeight = ref(0)
const inputPanelHeight = ref(0)
const inputState = shallowRef<ReturnType<typeof useMessageInputState> | null>(null)

const { fetchConversationInfo, conversationList } = useConversationListState('chat')

const isC2CConversation = computed(() => conversationID.value.startsWith('c2c_'))
const currentConversation = computed(() =>
conversationList.value.find(conv => conv.conversationID === conversationID.value)
)
const navigationTitle = computed(() => currentConversation.value?.title || '聊天')

// ==================== 自定义消息:订单卡片 ====================
const toolList = [
...DEFAULT_TOOLS,
{
id: 'order',
name: '订单',
icon: '/uni_modules/tuikit-atomic-x/static/icon/custom.png', // 演示用工程内既有图标,业务方请替换为自己的图标
callback: () => {
// 实际业务一般跳订单选择页,回来后用拿到的订单数据调发送
inputState.value?.sendCustomMessage({
businessID: 'order_card',
version: 1,
orderID: 'TX20260602001',
title: '腾讯云 IM 套餐 · 旗舰版',
price: 2999,
cover: 'https://web.sdk.qcloud.com/im/assets/images/tencent_rtc_logo.png' // 演示图片,业务方请替换为自己的图片
}).catch(err => uni.showToast({ icon: 'none', title: err.message || '发送失败' }))
}
}
]

const renderCustomMessage = (message: any) => {
try {
const data = JSON.parse(message.messageBody?.customMessage?.data || '{}')
if (data.businessID === 'order_card') return OrderCardMessage
} catch (e) {}
return null
}

// ==================== 导航栏 ====================
const onNavBack = () => uni.navigateBack()

const onNavMenuSelect = () => {
if (!isC2CConversation.value) {
isInGroup.value = joinedGroupList.value.some(
group => group.groupID === conversationID.value?.replace('group_', '')
)
if (!isInGroup.value) {
uni.showToast({ title: '您已不在群内,无法进行此操作', icon: 'none' })
return
}
}
uni.navigateTo({
url: `/pages/scenes/chat/chatSetting/index?conversationID=${conversationID.value}&showType=0`
})
}

// ==================== 输入框 ====================
const onMessageInputHeightChange = ({ inputToolbarHeight: toolbar, inputPanelHeight: panel }) => {
inputToolbarHeight.value = toolbar
inputPanelHeight.value = panel
}

const closeSoftKeyboard = () => messageInputRef.value?.collapse()

// ==================== 离线推送 ====================
const setOfflinePushInfo = (ctx: OfflinePushContext): OfflinePushInfo | null => {
const title = currentConversation.value?.title || '新消息'
let description = '[新消息]'
switch (ctx.messageType) {
case MessageType.TEXT:
description = (ctx.messageBody as { text?: string })?.text || '[文本消息]'
break
case MessageType.IMAGE: description = '[图片]'; break
case MessageType.VIDEO: description = '[视频]'; break
case MessageType.SOUND: description = '[语音]'; break
case MessageType.FILE: description = '[文件]'; break
case MessageType.CUSTOM: return null
default: return null
}
return {
title,
description,
extensionInfo: {
ext: JSON.stringify({ conversationID: ctx.conversationID, messageType: ctx.messageType })
}
}
}

// ==================== 生命周期 ====================
onLoad((options) => {
if (!options.conversationID) {
console.error('未接收到 conversationID 参数')
return
}
conversationID.value = options.conversationID;
inputState.value = useMessageInputState({ conversationID: conversationID.value });
fetchConversationInfo(conversationID.value)
fetchJoinedGroupList()
if (uni.$locateMessage) {
locateMessage.value = uni.$locateMessage
uni.$locateMessage = null
}
})

onShow(() => fetchConversationInfo(conversationID.value))
</script>

<style>
.page-container { flex: 1; flex-direction: column; background-color: #F9FAFC; }
.message-list-container { flex: 1; flex-direction: column; background-color: #F9FAFC; }
</style>
<template>
<view v-if="data.businessID === 'order_card'" class="order-card">
<view class="order-card__cover">
<image class="order-card__cover-image" :src="data.cover" mode="aspectFit" resize="contain" />
</view>
<view class="order-card__body">
<text class="order-card__title">{{ data.title }}</text>
<text class="order-card__id">订单号:{{ data.orderID }}</text>
<text class="order-card__price">¥{{ priceText }}</text>
</view>
</view>
</template>

<script setup>
import { computed } from 'vue'

const props = defineProps({
message: { type: Object, required: true }
})

const data = computed(() => {
try {
return JSON.parse(props.message.messageBody?.customMessage?.data || '{}')
} catch (e) {
return {}
}
})
const priceText = computed(() => Number(data.value.price || 0).toFixed(2))
</script>

<style>
.order-card { width: 480rpx; background-color: #FFFFFF; border-radius: 16rpx; overflow: hidden; }
.order-card__cover { width: 480rpx; height: 240rpx; justify-content: center; align-items: center; }
.order-card__cover-image { width: 480rpx; height: 240rpx; }
.order-card__body { padding: 20rpx; }
.order-card__title { font-size: 30rpx; color: #1A1A1A; lines: 2; }
.order-card__id { font-size: 24rpx; color: #999; margin-top: 8rpx; }
.order-card__price { font-size: 32rpx; color: #FF4D4F; font-weight: 600; margin-top: 16rpx; }
</style>
效果如图所示: