Files
im-uniapp/pages/conversation/chating/components/MessageItem/index.vue
2025-07-14 13:39:08 +08:00

533 lines
13 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>
<view>
<view
v-if="!getNoticeContent"
:id="`auchor${source.clientMsgID}`"
class="message_item"
:class="{ message_item_self: isSender, message_item_active: isActive }"
>
<my-avatar
size="42"
:desc="source.senderNickname"
:src="source.senderFaceUrl"
@click="goToUserCard"
/>
<view class="message_container">
<view
class="message_sender"
:style="{ 'flex-direction': !isSender ? 'row-reverse' : 'row' }"
>
<text>{{ formattedMessageTime }}</text>
<text style="margin-left: 2rpx; margin-right: 2rpx">{{ "" }}</text>
<text v-if="!isSingle">{{ source.senderNickname }}</text>
</view>
<view class="message_send_state_box">
<view
style="
height: 100%;
display: flex;
justify-items: center;
align-items: center;
"
>
<view class="message_send_state">
<u-loading-icon v-if="showSending && !isPreview" />
<image
v-if="isFailedMessage && !isPreview"
src="@/static/images/chating_message_failed.png"
/>
</view>
</view>
<view class="message_content_wrap message_content_wrap_shadow" @longpress="handleLongPress">
<template v-if="source.contentType === 101">
<TextMessageRender :message="source" />
</template>
<template v-else-if="source.contentType === 102">
<MediaMessageRender :message="source" />
</template>
<template v-else-if="source.contentType === 103">
<VoiceMessageRender :source="source" :isSender="isSender" />
</template>
<template v-else-if="source.contentType === 104">
<view style="color:#999">[暂未实现] 视频消息</view>
</template>
<template v-else-if="source.contentType === 105">
<FileMessageRender :source="source" />
</template>
<template v-else-if="source.contentType === 106">
<view style="color:#999">[暂未实现] @消息</view>
</template>
<template v-else-if="source.contentType === 109">
<view style="color:#999">[暂未实现] 位置消息</view>
</template>
<template v-else-if="source.contentType === 110">
<view style="color:#999">[暂未实现] 自定义消息</view>
</template>
<template v-else-if="source.contentType === 1400">
<view style="color:#999">[暂未实现] 系统通知</view>
</template>
<template v-else>
<ErrorMessageRender :source="source" />
</template>
</view>
</view>
</view>
</view>
<view
v-else
class="notice_message_container"
:id="`auchor${source.clientMsgID}`"
>
<text>{{ getNoticeContent }}</text>
</view>
<!-- uni-popup 消息操作菜单 -->
<uni-popup ref="actionPopup" :show="showActionMenu" type="bottom" @change="onPopupChange">
<view class="popup-action-list">
<view
v-for="(action, index) in actionMenuItems"
:key="index"
class="popup-action-item"
@click="handleActionClick(action)"
>
<text class="popup-action-text">{{ action }}</text>
</view>
</view>
</uni-popup>
</view>
</template>
<script>
import { mapGetters } from "vuex";
import {
MessageStatus,
MessageType,
SessionType,
} from "openim-uniapp-polyfill";
import MyAvatar from "@/components/MyAvatar/index.vue";
import TextMessageRender from "./TextMessageRender.vue";
import MediaMessageRender from "./MediaMessageRender.vue";
import ErrorMessageRender from "./ErrorMessageRender.vue";
import VoiceMessageRender from './VoiceMessageRender.vue'
import FileMessageRender from './FileMessageRender.vue'
import uniPopup from '@/uni_modules/uni-popup/components/uni-popup/uni-popup.vue';
import { noticeMessageTypes } from "@/constant";
import { tipMessaggeFormat, formatMessageTime } from "@/util/imCommon";
import { revokeMessage, canRevokeMessage } from "@/util/revokeMessage";
const textRenderTypes = [MessageType.TextMessage];
const mediaRenderTypes = [MessageType.PictureMessage];
export default {
components: {
MyAvatar,
TextMessageRender,
MediaMessageRender,
ErrorMessageRender,
VoiceMessageRender,
FileMessageRender,
uniPopup,
},
props: {
source: Object,
isSender: {
type: Boolean,
default: false,
},
isPreview: Boolean,
isActive: Boolean,
},
data() {
return {
sendingDelay: true,
showActionMenu: false,
actionMenuItems: [],
};
},
computed: {
...mapGetters([
"storeCurrentConversation",
"storeSelfInfo",
"storeHistoryMessageList",
]),
isSingle() {
return (
this.storeCurrentConversation.conversationType === SessionType.Single
);
},
formattedMessageTime() {
return this.source && this.source.sendTime ? formatMessageTime(this.source.sendTime) : '';
},
showTextRender() {
return this.source && this.source.contentType ? textRenderTypes.includes(this.source.contentType) : false;
},
showMediaRender() {
return this.source && this.source.contentType ? mediaRenderTypes.includes(this.source.contentType) : false;
},
getNoticeContent() {
if (!this.source || !this.source.contentType) {
return "";
}
const isNoticeMessage = noticeMessageTypes.includes(
this.source.contentType
);
return !isNoticeMessage
? ""
: tipMessaggeFormat(
this.source,
this.$store.getters.storeCurrentUserID
);
},
isSuccessMessage() {
return this.source && this.source.status === MessageStatus.Succeed;
},
isFailedMessage() {
return this.source && this.source.status === MessageStatus.Failed;
},
showSending() {
return this.source && this.source.status === MessageStatus.Sending && !this.sendingDelay;
},
},
mounted() {
this.$emit("messageItemRender", this.source.clientMsgID);
this.setSendingDelay();
},
watch: {
source: {
handler(newVal, oldVal) {
console.log('MessageItem source changed:', { newVal, oldVal });
},
deep: true
}
},
methods: {
setSendingDelay() {
if (this.source && this.source.status === MessageStatus.Sending) {
setTimeout(() => {
this.sendingDelay = false;
}, 2000);
}
},
handleLongPress(e) {
this.actionMenuItems = this.getActionMenuItems();
this.showActionMenu = true;
this.$nextTick(() => {
this.$refs.actionPopup && this.$refs.actionPopup.open && this.$refs.actionPopup.open('bottom');
});
},
getActionMenuItems() {
const items = [];
// 复制消息(文本消息)
if (this.source.contentType === 101) {
items.push('复制');
}
// 撤回消息只有自己发送的2分钟内消息才能撤回
if (canRevokeMessage(this.source, this.storeSelfInfo.userID)) {
items.push('撤回');
}
return items;
},
handleActionClick(action) {
switch (action) {
case '复制':
this.copyMessage();
break;
case '撤回':
this.confirmRevokeMessage();
break;
case '转发':
this.forwardMessage();
break;
default:
console.log('未知操作:', action);
}
this.showActionMenu = false;
this.$refs.actionPopup && this.$refs.actionPopup.close && this.$refs.actionPopup.close();
},
closeActionMenu() {
this.showActionMenu = false;
this.$refs.actionPopup && this.$refs.actionPopup.close && this.$refs.actionPopup.close();
},
onPopupChange(e) {
if (!e.show) {
this.showActionMenu = false;
}
},
copyMessage() {
if (this.source.contentType === 101 && this.source.textElem && this.source.textElem.content) {
uni.setClipboardData({
data: this.source.textElem.content,
success: () => {
uni.showToast({
title: '已复制到剪贴板',
icon: 'success'
});
}
});
}
},
forwardMessage() {
// TODO: 实现转发消息功能
uni.showToast({
title: '转发功能开发中',
icon: 'none'
});
},
async confirmRevokeMessage() {
// 再次检查是否可以撤回(防止状态变化)
if (!canRevokeMessage(this.source, this.storeSelfInfo.userID)) {
uni.showToast({
title: '只能撤回自己发送的2分钟内的消息',
icon: 'none',
duration: 2000
});
return;
}
try {
uni.showLoading({
title: '撤回中...'
});
// 撤回消息
const result = await revokeMessage({
conversationID: this.storeCurrentConversation.conversationID,
clientMsgID: this.source.clientMsgID,
// revokerID: this.storeSelfInfo.userID,
// revokerNickname: this.storeSelfInfo.nickname
});
uni.hideLoading();
uni.showToast({
title: '撤回成功',
icon: 'success'
});
} catch (error) {
uni.hideLoading();
uni.showToast({
title: '撤回失败',
icon: 'none'
});
console.error('撤回消息失败:', error);
}
},
goToUserCard() {
// 跳转到用户信息页面,传递 userID
uni.navigateTo({
url: `/pages/common/userCard/index?sourceID=${this.source.sendID}&`
});
},
},
};
</script>
<style scoped lang="scss">
.message_item {
display: flex;
flex-direction: row;
padding: 16rpx 44rpx;
// padding-top: 48rpx;
position: relative;
.check_wrap {
@include centerBox();
box-sizing: border-box;
width: 40rpx;
min-width: 40rpx;
height: 40rpx;
min-height: 40rpx;
border: 2px solid #979797;
border-radius: 50%;
margin-top: 16rpx;
margin-right: 24rpx;
&_active {
background-color: #1d6bed;
border: none;
}
&_disabled {
background-color: #c8c9cc;
}
}
.message_container {
display: flex;
flex-direction: column;
align-items: flex-start;
margin-left: 20rpx;
// text-align: start;
max-width: 80%;
position: relative;
.message_sender {
@include nomalEllipsis();
display: flex;
max-width: 480rpx;
// font-size: 24rpx;
font-size: 0.85rem;
color: #666;
margin-bottom: 6rpx;
}
.message_content_wrap {
@include vCenterBox();
text-align: start;
// font-size: 14px;
color: $uni-text-color;
width: fit-content;
max-width: 100%;
.bg_container {
padding: 16rpx 24rpx;
border-radius: 0rpx 12rpx 12rpx 12rpx;
background-color: #f0f0f0;
}
}
}
.message_send_state_box {
display: flex;
flex-direction: row-reverse;
}
.message_send_state {
@include centerBox();
margin-left: 12rpx;
// margin-top: 48rpx;
width: 48rpx;
height: 48rpx;
.read_limit_count {
// font-size: 24rpx;
font-size: 0.85rem;
color: #999;
}
image {
width: 16px;
height: 16px;
}
}
/deep/.emoji_display {
width: 24px;
height: 18px;
vertical-align: sub;
}
&_self {
flex-direction: row-reverse;
.check_wrap {
margin-right: 0;
margin-left: 24rpx;
}
.message_container {
margin-left: 0;
margin-right: 20rpx;
// text-align: end;
align-items: flex-end;
.message_content_wrap {
flex-direction: row-reverse;
.bg_container {
border-radius: 12rpx 0 12rpx 12rpx;
background-color: #dcebfe !important;
}
}
}
.message_send_state_box {
flex-direction: row;
}
.message_send_state {
margin-left: 0rpx;
margin-right: 12rpx;
}
}
&_active {
background-color: #fdf5e9;
}
}
.notice_message_container {
@include ellipsisWithLine(2);
text-align: center;
margin: 24rpx 48rpx;
// font-size: 24rpx;
font-size: 0.85rem;
color: #999;
position: relative;
}
.fade-leave,
.fade-enter-to {
opacity: 1;
}
.fade-leave-active,
.fade-enter-active {
transition: all 0.5s;
}
.fade-leave-to,
.fade-enter {
opacity: 0;
}
.popup-action-list {
display: flex;
flex-direction: column;
padding: 24rpx 0;
background: #f8f8f8;
}
.popup-action-item {
margin: 0 32rpx 20rpx 32rpx;
padding: 24rpx 0;
background: #fff;
border-radius: 16rpx;
text-align: center;
font-size: 32rpx;
color: #333;
transition: background 0.2s;
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04);
&:last-child {
margin-bottom: 0;
}
&:active {
background: #e6f0ff;
color: #1677ff;
}
}
.popup-action-text {
display: block;
width: 100%;
height: 100%;
}
</style>