uniapp-app/pages_chat/chat/message/message-bubble.vue
2025-09-16 16:19:29 +08:00

884 lines
22 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<Tooltip
v-if="!props.msg.isSelf"
:placement="placement"
ref="tooltipRef"
color="white"
>
<template #content>
<div class="msg-action-groups" v-if="!isUnknownMsg">
<div
class="msg-action-btn"
v-if="
props.msg.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT
"
@tap="handleCopy"
>
<Icon
:size="18"
color="#656A72"
class="msg-action-btn-icon"
type="icon-fuzhi1"
></Icon>
<text class="msg-action-btn-text">{{ t('copyText') }}</text>
</div>
<div
v-if="
props.msg.messageType !==
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CALL
"
class="msg-action-btn"
@tap="handleReplyMsg"
>
<Icon
:size="18"
color="#656A72"
class="msg-action-btn-icon"
type="icon-huifu"
></Icon>
<text class="msg-action-btn-text">{{ t('replyText') }}</text>
</div>
<div
v-if="
props.msg.messageType !==
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_AUDIO &&
props.msg.messageType !==
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CALL
"
class="msg-action-btn"
@tap="handleForwardMsg"
>
<Icon
:size="18"
color="#656A72"
class="msg-action-btn-icon"
type="icon-zhuanfa"
></Icon>
<text class="msg-action-btn-text">{{ t('forwardText') }}</text>
</div>
<div
class="msg-action-btn"
v-if="
props.msg.messageType !==
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CALL
"
@tap="handlePinMsg"
>
<Icon
:size="18"
color="#656A72"
class="msg-action-btn-icon"
type="icon-pin"
></Icon>
<!-- pinState 为 0 或者 undefined显示“标记”其他显示“取消标记” -->
<text class="msg-action-btn-text">{{
props.msg.pinState ? t('unpinText') : t('pinText')
}}</text>
</div>
<div class="msg-action-btn" @tap="handleDeleteMsg">
<Icon
:size="18"
color="#656A72"
class="msg-action-btn-icon"
type="icon-shanchu"
></Icon>
<text class="msg-action-btn-text">{{ t('deleteText') }}</text>
</div>
<div
class="msg-action-btn"
v-if="
props.msg.messageType !==
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CALL
"
@tap="handleCollectionMsg"
>
<Icon
:size="18"
color="#656A72"
class="msg-action-btn-icon"
type="icon-collection"
></Icon>
<text class="msg-action-btn-text">{{ t('collectionText') }}</text>
</div>
</div>
<!-- 未知消息体 -->
<div class="msg-action-groups-unknown" v-else>
<div class="msg-action-btn" @tap="handleDeleteMsg">
<Icon
:size="18"
color="#656A72"
class="msg-action-btn-icon"
type="icon-shanchu"
></Icon>
<text class="msg-action-btn-text">{{ t('deleteText') }}</text>
</div>
</div>
</template>
<div v-if="bgVisible" class="msg-bg msg-bg-in">
<slot></slot>
</div>
<slot v-else></slot>
</Tooltip>
<div
v-else-if="
props.msg.sendingState ===
V2NIMConst.V2NIMMessageSendingState.V2NIM_MESSAGE_SENDING_STATE_SENDING
"
class="msg-status-wrapper"
>
<Icon
:size="21"
color="#337EFF"
class="msg-status-icon icon-loading"
type="icon-a-Frame8"
></Icon>
<Tooltip
:placement="placement"
ref="tooltipRef"
color="white"
:align="props.msg.isSelf"
>
<template #content>
<div class="msg-action-groups">
<div
class="msg-action-btn"
v-if="
props.msg.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT
"
@tap="handleCopy"
>
<Icon
:size="18"
color="#656A72"
class="msg-action-btn-icon"
type="icon-fuzhi1"
></Icon>
<text class="msg-action-btn-text">{{ t('copyText') }}</text>
</div>
<div class="msg-action-btn" @tap="handleDeleteMsg">
<Icon
:size="18"
color="#656A72"
class="msg-action-btn-icon"
type="icon-shanchu"
></Icon>
<text class="msg-action-btn-text">{{ t('deleteText') }}</text>
</div>
</div>
</template>
<div v-if="bgVisible" class="msg-bg msg-bg-out">
<slot></slot>
</div>
<slot v-else></slot>
</Tooltip>
</div>
<div
v-else-if="
props.msg.sendingState ===
V2NIMConst.V2NIMMessageSendingState
.V2NIM_MESSAGE_SENDING_STATE_FAILED ||
props.msg.messageStatus.errorCode === 102426 ||
props.msg.messageStatus.errorCode === 104404
"
class="msg-failed-wrapper"
>
<div class="msg-failed">
<div class="msg-status-wrapper" @tap="handleResendMsg">
<div class="icon-fail">!</div>
</div>
<Tooltip
:placement="placement"
ref="tooltipRef"
color="white"
:align="props.msg.isSelf"
>
<template #content>
<div
class="msg-action-groups"
:style="{
width:
props.msg.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT
? '112px'
: '56px',
}"
>
<div
class="msg-action-btn"
v-if="
props.msg.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT
"
@tap="handleCopy"
>
<Icon
:size="18"
color="#656A72"
class="msg-action-btn-icon"
type="icon-fuzhi1"
></Icon>
<text class="msg-action-btn-text">{{ t('copyText') }}</text>
</div>
<div class="msg-action-btn" @tap="handleDeleteMsg">
<Icon
:size="18"
color="#656A72"
class="msg-action-btn-icon"
type="icon-shanchu"
></Icon>
<text class="msg-action-btn-text">{{ t('deleteText') }}</text>
</div>
</div>
</template>
<div v-if="bgVisible" class="msg-bg msg-bg-out">
<slot></slot>
</div>
<slot v-else></slot>
</Tooltip>
</div>
<div
class="in-blacklist"
v-if="props.msg.messageStatus.errorCode === 102426"
>
{{ t('sendFailWithInBlackText') }}
</div>
<div
class="friend-delete"
v-else-if="props.msg.messageStatus.errorCode === 104404"
>
{{ t('sendFailWithDeleteText') }}
<span @tap="addFriend" class="friend-verification">{{
t('friendVerificationText')
}}</span>
</div>
</div>
<Tooltip
v-else-if="tooltipVisible"
:placement="placement"
ref="tooltipRef"
color="white"
:align="props.msg.isSelf"
>
<template #content>
<div class="msg-action-groups" v-if="!isUnknownMsg">
<div
class="msg-action-btn"
v-if="
props.msg.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT
"
@tap="handleCopy"
>
<Icon
:size="18"
color="#656A72"
class="msg-action-btn-icon"
type="icon-fuzhi1"
></Icon>
<text class="msg-action-btn-text">{{ t('copyText') }}</text>
</div>
<div
v-if="
props.msg.messageType !==
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CALL
"
class="msg-action-btn"
@tap="handleReplyMsg"
>
<Icon
:size="18"
color="#656A72"
class="msg-action-btn-icon"
type="icon-huifu"
></Icon>
<text class="msg-action-btn-text">{{ t('replyText') }}</text>
</div>
<div
v-if="
props.msg.messageType !==
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_AUDIO &&
props.msg.messageType !==
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CALL
"
class="msg-action-btn"
@tap="handleForwardMsg"
>
<Icon
:size="18"
color="#656A72"
class="msg-action-btn-icon"
type="icon-zhuanfa"
></Icon>
<text class="msg-action-btn-text">{{ t('forwardText') }}</text>
</div>
<div
class="msg-action-btn"
v-if="
props.msg.messageType !==
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CALL
"
@tap="handlePinMsg"
>
<Icon
:size="18"
color="#656A72"
class="msg-action-btn-icon"
type="icon-pin"
></Icon>
<!-- pinState 为 0 或者 undefined显示“标记”其他显示“取消标记” -->
<text class="msg-action-btn-text">{{
props.msg.pinState ? t('unpinText') : t('pinText')
}}</text>
</div>
<div class="msg-action-btn" @tap="handleDeleteMsg">
<Icon
:size="18"
color="#656A72"
class="msg-action-btn-icon"
type="icon-shanchu"
></Icon>
<text class="msg-action-btn-text">{{ t('deleteText') }}</text>
</div>
<div
v-if="
props.msg.messageType !==
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CALL
"
class="msg-action-btn"
@tap="handleRecallMsg"
>
<Icon
:size="18"
color="#656A72"
class="msg-action-btn-icon"
type="icon-chehui"
></Icon>
<text class="msg-action-btn-text">{{ t('recallText') }}</text>
</div>
<div
class="msg-action-btn"
v-if="
props.msg.messageType !==
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CALL
"
@tap="handleCollectionMsg"
>
<Icon
:size="18"
color="#656A72"
class="msg-action-btn-icon"
type="icon-collection"
></Icon>
<text class="msg-action-btn-text">{{ t('collectionText') }}</text>
</div>
</div>
<!-- 未知消息体 -->
<div class="msg-action-groups-unknown" v-else>
<div class="msg-action-btn" @tap="handleDeleteMsg">
<Icon
:size="18"
color="#656A72"
class="msg-action-btn-icon"
type="icon-shanchu"
></Icon>
<text class="msg-action-btn-text">{{ t('deleteText') }}</text>
</div>
</div>
</template>
<div v-if="bgVisible" class="msg-bg msg-bg-out">
<slot></slot>
</div>
<slot v-else></slot>
</Tooltip>
<div v-else-if="bgVisible" class="msg-bg msg-bg-out">
<slot></slot>
</div>
<div v-else>
<slot></slot>
</div>
</template>
<script lang="ts" setup>
/** 消息操作组件 */
import { onMounted, onUnmounted, ref } from 'vue'
// @ts-ignore
import Tooltip from '@/components/Tooltip.vue'
import Icon from '@/components/Icon.vue'
import { customNavigateTo } from '@/utils/im/customNavigate'
import { events } from '@/utils/im/constants'
import { autorun } from 'mobx'
import { V2NIMMessageForUI } from '@xkit-yx/im-store-v2/dist/types/types'
//@ts-ignore
import { V2NIMConst } from '@/utils/im/nim'
import { msgRecallTime } from '@/utils/im/constants'
import { t } from '@/utils/im/i18n'
import { V2NIMMessage } from 'nim-web-sdk-ng/dist/esm/nim/src/V2NIMMessageService'
const tooltipRef = ref(null)
const props = withDefaults(
defineProps<{
msg: V2NIMMessageForUI
tooltipVisible?: boolean
bgVisible?: boolean
placement?: string
}>(),
{}
)
/**会话类型 */
const conversationType =
uni.$UIKitNIM.V2NIMConversationIdUtil.parseConversationType(
props.msg.conversationId
) as unknown as V2NIMConst.V2NIMConversationType
onMounted(() => {
/** 当前版本仅支持文本、图片、文件、语音、视频 话单消息,其他消息类型统一为未知消息 */
isUnknownMsg.value = !(
props.msg.messageType ==
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT ||
props.msg.messageType ==
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_IMAGE ||
props.msg.messageType ==
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_FILE ||
props.msg.messageType ==
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_AUDIO ||
props.msg.messageType ==
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_VIDEO ||
props.msg.messageType == V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CALL
)
})
/** 是否是好友 */
const isFriend = ref(true)
/** 未知消息 */
const isUnknownMsg = ref(false)
const closeTooltip = () => {
// @ts-ignore
tooltipRef.value.close()
}
/** 复制消息 */
const handleCopy = () => {
uni.setClipboardData({
data: props.msg.text || '',
showToast: false,
success: () => {
uni.showToast({
title: t('copySuccessText'),
icon: 'none',
})
},
fail: () => {
uni.showToast({
title: t('copyFailText'),
icon: 'none',
})
},
complete() {
closeTooltip()
},
})
}
/** 页面滚动到底部 */
const scrollBottom = () => {
setTimeout(() => {
uni.$emit(events.ON_SCROLL_BOTTOM)
}, 100)
}
/** 重发消息 */
const handleResendMsg = async () => {
const _msg = props.msg as V2NIMMessageForUI
uni.$UIKitStore.msgStore.removeMsg(_msg.conversationId, [
_msg.messageClientId,
])
try {
if (_msg.threadReply) {
const beReplyMsg =
await uni.$UIKitNIM.V2NIMMessageService.getMessageListByRefers([
//@ts-ignore
_msg.threadReply,
])
if (beReplyMsg.length > 0) {
//@ts-ignore
uni.$UIKitStore.msgStore.replyMsgActive(beReplyMsg[0])
}
}
uni.$UIKitStore.msgStore.sendMessageActive({
msg: _msg,
conversationId: _msg.conversationId,
progress: () => true,
sendBefore: () => {
scrollBottom()
},
})
scrollBottom()
} catch (error) {
console.log(error)
}
}
/** 转发消息 */
const handleForwardMsg = () => {
uni.showActionSheet({
itemList: [t('forwardToTeamText'), t('forwardToFriendText')],
success(data) {
if (data.tapIndex === 0) {
customNavigateTo({
url: `/pages/Chat/forward?forwardConversationType=${V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM}&msgIdClient=${props.msg.messageClientId}`,
})
} else if (data.tapIndex === 1) {
customNavigateTo({
url: `/pages/Chat/forward?forwardConversationType=${V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_P2P}&msgIdClient=${props.msg.messageClientId}`,
})
}
},
complete() {
closeTooltip()
},
})
}
/** pin消息 */
const handlePinMsg = () => {
const _msg = props.msg
if (_msg.pinState) {
// 取消标记
uni.$UIKitStore.msgStore.unpinMessageActive(_msg).catch((err: any) => {
if (err?.code && typeof t(`${err.code}`) !== 'undefined') {
uni.showToast({
title: t(`${err.code}`),
icon: 'error',
duration: 1000,
})
} else {
uni.showToast({
title: t('unpinFailedText'),
icon: 'error',
duration: 1000,
})
}
})
} else {
/** 显示标记 */
uni.$UIKitStore.msgStore.pinMessageActive(_msg).catch((err: any) => {
if (err?.code && typeof t(`${err.code}`) !== 'undefined') {
uni.showToast({
title: t(`${err.code}`),
icon: 'error',
duration: 1000,
})
} else {
uni.showToast({
title: t('pinFailedText'),
icon: 'error',
duration: 1000,
})
}
})
}
closeTooltip()
}
/**是否是云端会话 */
const enableV2CloudConversation =
uni.$UIKitStore?.sdkOptions?.enableV2CloudConversation
/** 收藏消息 */
const handleCollectionMsg = () => {
const _msg = props.msg
const conversation = enableV2CloudConversation
? uni.$UIKitStore.conversationStore?.conversations.get(_msg.conversationId)
: uni.$UIKitStore.localConversationStore?.conversations.get(
_msg.conversationId
)
const collectionDataObj = {
//@ts-expect-error
message: uni.$UIKitNIM.V2NIMMessageConverter.messageSerialization(_msg), // 序列化
avatar: uni.$UIKitStore.userStore.users.get(_msg.senderId)?.avatar,
conversationName: conversation?.name,
senderName: uni.$UIKitStore.uiStore.getAppellation({
account: _msg.senderId,
teamId:
conversationType ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM
? _msg.receiverId
: '',
}),
}
const addCollectionParams = {
collectionType: 1000 + _msg.messageType, // 和移动端对齐
collectionData: JSON.stringify(collectionDataObj),
uniqueId: _msg.messageServerId,
}
uni.$UIKitStore.msgStore
.addCollectionActive(addCollectionParams)
.then(() => {
uni.showToast({
title: t('addCollectionSuccessText'),
icon: 'none',
})
})
.catch((err: any) => {
if (err?.code && typeof t(`${err.code}`) !== 'undefined') {
uni.showToast({
title: t(`${err.code}`),
icon: 'error',
duration: 1000,
})
} else {
uni.showToast({
title: t('addCollectionFailedText'),
icon: 'error',
duration: 1000,
})
}
})
closeTooltip()
}
/** 回复消息 */
const handleReplyMsg = async () => {
const _msg = props.msg
uni.$UIKitStore.msgStore.replyMsgActive(_msg)
closeTooltip()
uni.$emit(events.REPLY_MSG, props.msg)
// 在群里回复其他人的消息,也是@被回复人
if (
conversationType ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM &&
!props.msg.isSelf
) {
uni.$emit(events.AIT_TEAM_MEMBER, {
accountId: props.msg.senderId,
appellation: uni.$UIKitStore.uiStore.getAppellation({
account: props.msg.senderId,
teamId: props.msg.receiverId,
ignoreAlias: true,
}),
})
}
}
/** 撤回消息 */
const handleRecallMsg = () => {
const diff = Date.now() - props.msg.createTime
if (diff > msgRecallTime) {
uni.showToast({
title: t('msgRecallTimeErrorText'),
icon: 'none',
})
closeTooltip()
return
}
uni.showModal({
title: t('recallText'),
content: t('recall3'),
showCancel: true,
confirmText: t('recallText'),
confirmColor: '#1861df',
success(data) {
if (data.confirm) {
const _msg = props.msg
uni.$UIKitStore.msgStore.reCallMsgActive(_msg).catch(() => {
uni.showToast({
title: t('recallMsgFailText'),
icon: 'error',
})
})
}
},
complete() {
closeTooltip()
},
})
}
/** 删除消息 */
const handleDeleteMsg = () => {
const _msg = props.msg
uni.showModal({
title: t('deleteText'),
content: t('delete'),
showCancel: true,
confirmText: t('deleteText'),
confirmColor: '#1861df',
success(data) {
if (data.confirm) {
uni.$UIKitStore.msgStore
.deleteMsgActive([_msg])
.then(() => {
uni.showToast({
title: t('deleteMsgSuccessText'),
icon: 'none',
})
})
.catch((error: any) => {
uni.showToast({
title: t('deleteMsgFailText'),
icon: 'error',
})
})
}
},
complete() {
closeTooltip()
},
})
}
/** 添加好友 */
const addFriend = () => {
customNavigateTo({
url: `/pages/User/friend/index?account=${props.msg.receiverId}`,
})
}
/** 监听好友列表 */
const friendsWatch = autorun(() => {
const _isFriend = uni.$UIKitStore.uiStore.friends
.filter(
(item) =>
!uni.$UIKitStore.relationStore.blacklist.includes(item.accountId)
)
.map((item) => item.accountId)
.some((item: any) => item.account === props.msg.receiverId)
isFriend.value = _isFriend
})
/** 卸载监听 */
onUnmounted(() => {
friendsWatch()
})
</script>
<style scoped lang="scss">
@import '@/styles/common.scss';
.msg-bg {
max-width: 360rpx;
overflow: hidden;
padding: 12px 16px;
&-in {
border-radius: 0 8px 8px 8px;
background-color: #e8eaed;
margin-left: 8px;
}
&-out {
border-radius: 8px 0 8px 8px;
background-color: #d6e5f6;
margin-right: 8px;
}
}
.msg-action-groups {
display: flex;
flex-direction: row;
flex-wrap: wrap;
max-width: 224px;
width: max-content;
}
.msg-action-btn {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 10px;
width: 56px;
&-icon {
color: #656a72;
font-size: 18px;
}
&-text {
color: #333;
font-size: 14px;
word-break: keep-all;
}
}
.msg-failed-wrapper {
display: flex;
flex-direction: column;
align-items: flex-end;
width: 100%;
.in-blacklist {
color: #b3b7bc;
font-size: 14px;
position: relative;
right: 20%;
margin: 10px 0;
}
.friend-delete {
color: #b3b7bc;
font-size: 14px;
margin: 10px 0;
.friend-verification {
color: #337eff;
font-size: 14px;
}
}
}
.msg-status-wrapper {
// max-width: 450rpx;
position: relative;
display: flex;
flex-direction: row;
align-items: center;
margin-right: 8px;
box-sizing: border-box;
.msg-bg-out {
margin-right: 0;
flex: 1;
}
}
.msg-status-icon {
margin-right: 8px;
font-size: 21px;
position: absolute;
bottom: 2px;
left: -30px;
&.icon-loading {
color: #337eff;
animation: loadingCircle 1s infinite linear;
}
}
.icon-fail {
background: #fc596a;
color: white;
border-radius: 50%;
width: 20px;
height: 20px;
text-align: center;
line-height: 20px;
margin-right: 5px;
font-size: 15px;
}
.msg-failed {
display: flex;
flex-direction: row;
align-items: center;
}
</style>