This commit is contained in:
Your Name
2026-03-11 09:49:47 +08:00
parent 02ae537b4c
commit 38ad60f4bb
290 changed files with 36917 additions and 123 deletions
@@ -0,0 +1,325 @@
<template>
<div class="message-container">
<!-- 消息列表滚动区域 -->
<scroll-view ref="scrollViewRef" scroll-y class="message-list" :scroll-into-view="currentMessage"
scroll-with-animation @scrolltolower="handleScrollToBottom" @scrolltoupper="handleScrollToTop">
<!-- 下拉加载提示 -->
<view v-if="isLoadingOlderMessage || showNoMoreTip" class="loading-tip">
<text>{{ isLoadingOlderMessage ? '加载中...' : '没有更多消息了' }}</text>
</view>
<!-- 消息列表 -->
<view v-for="(message, index) in messageList">
<!-- tips 消息 -->
<GroupTipMessage v-if="shouldRenderAsGroupTip(message)" :message="message" />
<!-- 消息时间分割线 -->
<MessageTimestamp v-if="!shouldRenderAsGroupTip(message)" :currTime="message.time"
:prevTime="index > 0 ? messageList[index - 1].time : 0" />
<!-- 非群 tips 消息 -->
<view v-if="isSupportedMessageType(message)" :id="`msg-${message.ID}`" class="message-item"
:class="{ 'message-item-me': message?.flow === 'out' }">
<!-- 消息头像 -->
<Avatar :avatarStyle="avatarStyle" :src="message?.avatar || DefaultAvatarIcon" />
<!-- 消息内容 -->
<view class="message-content">
<!-- 群聊用户名显示 -->
<view v-if="activeConversation?.type === 'GROUP' && message?.flow === 'in'" class="sender-name">
{{ getDisplayName(message) }}
</view>
<!-- 文本消息 -->
<TextMessage v-if="message?.type === MessageType.MSG_TEXT" :message="message" />
<!-- 通话消息 -->
<CustomMessage v-if="message?.type === MessageType.MSG_CUSTOM" :message="message" />
<!-- 图片消息 -->
<ImageMessage v-if="message?.type === MessageType.MSG_IMAGE" :message="message" />
<!-- 视频消息 -->
<VideoMessage v-if="message?.type === MessageType.MSG_VIDEO" :message="message" />
</view>
<!-- 消息状态 -->
<MessageStatus class="message-status" :message="message" />
</view>
</view>
</scroll-view>
<!-- 新消息提示条当有新消息且不在底部时显示 -->
<view v-if="showNewMessageTip" class="new-message-tip" @click="handleNewMessageTipClick">
<image :src="NewMessageTipIcon"></image>
{{ newMessageTipText }}
</view>
</div>
</template>
<script lang="ts">
export default {
options: {
virtualHost: true,
}
}
</script>
<script lang="ts" setup>
import { ref, watch, computed, onMounted, onUnmounted } from 'vue'
import Avatar from '../Avatar/Avatar.vue'
import TextMessage from './Message/TextMessage.vue';
import CustomMessage from './Message/CustomMessage.vue';
import ImageMessage from './Message/ImageMessage.vue';
import VideoMessage from './Message/VideoMessage.vue';
import GroupTipMessage from './Message/GroupTipMessage.vue';
import MessageStatus from './MessageStatus/MessageStatus.vue';
import MessageTimestamp from './MessageTimeDivider/MessageTimeDivider.vue';
import DefaultAvatarIcon from '../../assets/base/default-avatar.png';
import NewMessageTipIcon from '../../assets/chat/new-message.svg';
import { useMessageListState, useConversationListState } from '../../chat';
import { isCallSignaling } from '../../utils/processCallSignaling';
import { MessageType } from '../../constants/chat'
const { setActiveConversation, activeConversation } = useConversationListState()
const { messageList, loadMoreOlderMessage, hasMoreOlderMessage } = useMessageListState();
const scrollViewRef = ref();
const showNewMessageTip = ref(false);
const newMessageCount = ref(0);
const autoScroll = ref(true);
const isLoadingOlderMessage = ref(false);
const showNoMoreTip = ref(false);
const historyFirstMessageID = ref<string>('');
const currentMessage = ref('');
const avatarStyle = ref({
width: '40px',
height: '40px',
borderRadius: '5px'
})
// 获取显示的用户名
const getDisplayName = (message: any) => {
const nameCard = message?.nameCard || '';
const senderNick = message?.nick || '';
const senderUserID = message?.from || '';
// 如果是群聊,按优先级显示:nameCard > nick > userID
if (activeConversation.value?.type === 'GROUP') {
return nameCard || senderNick || senderUserID;
}
// 如果不是群聊,只显示昵称
return senderNick || senderUserID;
};
const isSupportedMessageType = (message) => {
const type = message?.type
return [
MessageType.MSG_TEXT,
MessageType.MSG_CUSTOM,
MessageType.MSG_IMAGE,
MessageType.MSG_VIDEO
].includes(type) && !shouldRenderAsGroupTip(message)
}
const newMessageTipText = computed(() => {
return `${newMessageCount.value}条新消息`
})
const scrollToBottom = () => {
if (messageList.value?.length) {
const lastMessage = messageList.value[messageList.value.length - 1]
const targetId = `msg-${lastMessage.ID}`
// Clear and re-set to ensure scroll triggers even when value is the same
if (currentMessage.value === targetId) {
currentMessage.value = ''
setTimeout(() => {
currentMessage.value = targetId
}, 50)
} else {
currentMessage.value = targetId
}
}
}
const handleNewMessageTipClick = () => {
showNewMessageTip.value = false
newMessageCount.value = 0
autoScroll.value = true
scrollToBottom()
}
const handleScrollToTop = async (e) => {
if (isLoadingOlderMessage.value) return;
if (!hasMoreOlderMessage.value) {
showNoMoreTip.value = true;
setTimeout(() => {
showNoMoreTip.value = false;
}, 1000);
return;
}
isLoadingOlderMessage.value = true;
const currentFirstMessageID = messageList.value?.[0]?.ID || '';
await loadMoreOlderMessage();
historyFirstMessageID.value = currentFirstMessageID;
isLoadingOlderMessage.value = false;
}
const handleScrollToBottom = () => {
showNewMessageTip.value = false
}
onMounted(() => {
scrollToBottom()
// Listen for keyboard pop-up to auto scroll to bottom
uni.$on('TUIChat:keyboardHeightChange', () => {
scrollToBottom()
})
})
onUnmounted(() => {
setActiveConversation('')
uni.$off('TUIChat:keyboardHeightChange')
})
watch(messageList, (newVal, oldVal) => {
if (!newVal?.length) return
const lastMessage = newVal[newVal.length - 1]
const isMyMessage = lastMessage?.flow === 'out'
if (isMyMessage) {
scrollToBottom()
} else {
if (autoScroll.value) {
// 新消息如果在底部一个屏幕范围内,自动滚动
scrollToBottom()
showNewMessageTip.value = false
newMessageCount.value = 0
} else {
// 否则显示新消息提示
newMessageCount.value += 1
showNewMessageTip.value = true
}
}
}, { deep: true });
const shouldRenderAsGroupTip = (message: any) => {
// 创建群消息
if (message.type === MessageType.MSG_CUSTOM && message.getMessageContent().businessID === 'group_create') {
return true;
}
// 群通话消息
if (
message.type === MessageType.MSG_CUSTOM
&& isCallSignaling(message)
&& message.conversationType === "GROUP"
) {
return true;
}
// 群提示消息
if ( message.type === MessageType.MSG_GRP_TIP ) {
return true
}
return false;
};
</script>
<style lang="scss" scoped>
.message-container {
background-color: #F9FAFC;
display: flex;
flex: 1;
min-height: 0;
width: 100%;
height: 100%;
}
.loading-tip {
display: flex;
justify-content: center;
align-items: center;
padding: 12px 0;
color: #999;
font-size: 14px;
animation: fadeIn 0.3s;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes rotating {
from {
transform: rotate(0deg)
}
to {
transform: rotate(360deg)
}
}
.new-message-tip {
position: absolute;
bottom: 50px;
right: 10px;
background-color: #FFFFFF;
color: #1C66E5;
padding: 8px 16px;
border-radius: 3px;
font-size: 12px;
z-index: 100;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
white-space: nowrap;
image {
width: 12px;
height: 11px;
margin-right: 5px;
}
}
.message-list {
flex: 1;
overflow: auto;
margin: 8px;
overscroll-behavior: contain;
::-webkit-scrollbar {
display: none;
width: 0;
height: 0;
color: transparent;
}
-webkit-overflow-scrolling: touch;
}
.message-item {
display: flex;
margin: 7px 0;
&.message-item-me {
flex-direction: row-reverse;
}
}
.message-status {
position: relative;
}
.message-content {
margin: 0 20rpx;
max-width: 70%;
}
.sender-name {
font-size: 12px;
color: #999;
margin-bottom: 4px;
line-height: 1.2;
}
</style>