Files
im-uniapp/pages/conversation/chating/components/MessageItem/index.vue

531 lines
13 KiB
Vue
Raw Normal View History

2025-07-04 16:18:58 +08:00
<template>
2025-07-14 11:04:49 +08:00
<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"
2025-07-14 11:39:26 +08:00
@click="goToUserCard"
2025-07-14 11:04:49 +08:00
/>
<view class="message_container">
2025-07-04 16:18:58 +08:00
<view
2025-07-14 11:04:49 +08:00
class="message_sender"
:style="{ 'flex-direction': !isSender ? 'row-reverse' : 'row' }"
2025-07-04 16:18:58 +08:00
>
2025-07-14 11:04:49 +08:00
<text>{{ formattedMessageTime }}</text>
<text style="margin-left: 2rpx; margin-right: 2rpx">{{ "" }}</text>
<text v-if="!isSingle">{{ source.senderNickname }}</text>
2025-07-04 16:18:58 +08:00
</view>
2025-07-14 11:04:49 +08:00
<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>
2025-07-04 16:18:58 +08:00
</view>
</view>
</view>
2025-07-14 11:04:49 +08:00
<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>
2025-07-04 16:18:58 +08:00
</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";
2025-07-04 22:34:07 +08:00
import VoiceMessageRender from './VoiceMessageRender.vue'
import FileMessageRender from './FileMessageRender.vue'
2025-07-14 11:04:49 +08:00
import uniPopup from '@/uni_modules/uni-popup/components/uni-popup/uni-popup.vue';
2025-07-04 16:18:58 +08:00
import { noticeMessageTypes } from "@/constant";
import { tipMessaggeFormat, formatMessageTime } from "@/util/imCommon";
2025-07-14 11:04:49 +08:00
import { revokeMessage, canRevokeMessage } from "@/util/revokeMessage";
2025-07-04 16:18:58 +08:00
const textRenderTypes = [MessageType.TextMessage];
const mediaRenderTypes = [MessageType.PictureMessage];
export default {
components: {
MyAvatar,
TextMessageRender,
MediaMessageRender,
ErrorMessageRender,
VoiceMessageRender,
2025-07-14 11:04:49 +08:00
FileMessageRender,
uniPopup,
2025-07-04 16:18:58 +08:00
},
props: {
source: Object,
isSender: {
type: Boolean,
default: false,
},
isPreview: Boolean,
isActive: Boolean,
},
data() {
2025-07-14 11:04:49 +08:00
return {
sendingDelay: true,
showActionMenu: false,
actionMenuItems: [],
};
2025-07-04 16:18:58 +08:00
},
computed: {
...mapGetters([
"storeCurrentConversation",
"storeSelfInfo",
2025-07-14 11:04:49 +08:00
"storeHistoryMessageList",
2025-07-04 16:18:58 +08:00
]),
isSingle() {
return (
this.storeCurrentConversation.conversationType === SessionType.Single
);
},
formattedMessageTime() {
return this.source && this.source.sendTime ? formatMessageTime(this.source.sendTime) : '';
2025-07-04 16:18:58 +08:00
},
showTextRender() {
return this.source && this.source.contentType ? textRenderTypes.includes(this.source.contentType) : false;
2025-07-04 16:18:58 +08:00
},
showMediaRender() {
return this.source && this.source.contentType ? mediaRenderTypes.includes(this.source.contentType) : false;
2025-07-04 16:18:58 +08:00
},
getNoticeContent() {
if (!this.source || !this.source.contentType) {
return "";
}
2025-07-04 16:18:58 +08:00
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;
2025-07-04 16:18:58 +08:00
},
isFailedMessage() {
return this.source && this.source.status === MessageStatus.Failed;
2025-07-04 16:18:58 +08:00
},
showSending() {
return this.source && this.source.status === MessageStatus.Sending && !this.sendingDelay;
2025-07-04 16:18:58 +08:00
},
},
mounted() {
this.$emit("messageItemRender", this.source.clientMsgID);
this.setSendingDelay();
},
2025-07-14 11:04:49 +08:00
watch: {
source: {
handler(newVal, oldVal) {
console.log('MessageItem source changed:', { newVal, oldVal });
},
deep: true
}
},
2025-07-04 16:18:58 +08:00
methods: {
setSendingDelay() {
if (this.source && this.source.status === MessageStatus.Sending) {
2025-07-04 16:18:58 +08:00
setTimeout(() => {
this.sendingDelay = false;
}, 2000);
}
},
2025-07-14 11:04:49 +08:00
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(
this.storeCurrentConversation.conversationID,
this.source.clientMsgID
);
uni.hideLoading();
uni.showToast({
title: '撤回成功',
icon: 'success'
});
} catch (error) {
uni.hideLoading();
uni.showToast({
title: '撤回失败',
icon: 'none'
});
console.error('撤回消息失败:', error);
}
},
2025-07-14 11:39:26 +08:00
goToUserCard() {
// 跳转到用户信息页面,传递 userID
uni.navigateTo({
url: `/pages/common/userCard/index?sourceID=${this.source.sendID}&`
});
},
2025-07-04 16:18:58 +08:00
},
};
</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;
}
2025-07-14 11:04:49 +08:00
.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%;
}
2025-07-04 16:18:58 +08:00
</style>