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,46 @@
<template>
<view class="bubble" :class="{ 'bubble-me': message?.flow === 'out' }">
<div v-if="isCallSignaling(message)" @click="rePlayCall(handleCallKitSignaling(message).callType)"
style="display: flex; align-items: center;">
<image class="callMessage-icon"
:src="handleCallKitSignaling(message).callType === 1 ? CallVoiceIcon : CallVideoIcon" />
<text class="text">{{ handleCallKitSignaling(message).callTip }}</text>
</div>
<div v-else>
<text class="text">{{ message?.payload }}</text>
</div>
</view>
</template>
<script lang="ts" setup>
import { useMessageListState } from '../../../index';
import CallVoiceIcon from '../../../assets/chat/voice-call-message.svg';
import CallVideoIcon from '../../../assets/chat/call-video.svg';
import { removeC2C } from '../../../utils';
import { handleCallKitSignaling, isCallSignaling } from '../../../utils/processCallSignaling';
import { TUIBridge } from '../../../TUIBridge';
import { EVENT } from '../../../constants/event';
const { activeConversationID } = useMessageListState();
const props = defineProps({
message: {
type: Object,
},
})
async function rePlayCall(callType) {
const userID = removeC2C(activeConversationID.value)
TUIBridge.notifyEvent({
eventName: EVENT.ON_CALLS,
params: {
userIDList: [userID],
type: callType,
},
});
}
</script>
<style lang="scss" scoped>
@import './Message.module.scss';
</style>
@@ -0,0 +1,131 @@
<template>
<view class="group-tip-message">
<text class="tip-text">{{ renderText() }}</text>
</view>
</template>
<script lang="ts" setup>
import { handleCallKitSignaling, isCallSignaling } from '../../../utils/processCallSignaling';
interface Props {
message: any;
}
const props = defineProps<Props>();
const renderText = () => {
const messageContent = props.message.getMessageContent()
if (messageContent.businessID === 'group_create') {
return `${messageContent.showName || ''} 创建群组`;
}
if (isCallSignaling(props.message) && props.message.conversationType === 'GROUP') {
return handleCallKitSignaling(props.message);
}
return resolveGroupTipMessage(props.message).text;
};
const resolveGroupTipMessage = (message: any) => {
const ret: {
text: string;
} = {
text: '',
};
let showName: string = message?.nick || message?.payload?.userIDList?.join(',');
if (message?.payload?.memberList?.length > 0) {
showName = '';
message?.payload?.memberList?.map((user: any) => {
const _showName = user?.nick || user?.userID;
showName += `${_showName},`;
return user;
});
showName = showName?.slice(0, -1);
}
switch (message.payload.operationType) {
case 1: // GRP_TIP_MBR_JOIN
ret.text = `${showName} 加入群组`;
break;
case 2: // GRP_TIP_MBR_QUIT
ret.text = `${showName} 退出群组`;
break;
case 3: // GRP_TIP_MBR_KICKED_OUT
ret.text = `${showName} 被踢出群组`;
break;
case 4: // GRP_TIP_MBR_SET_ADMIN
ret.text = `${showName} 成为管理员`;
break;
case 5: // GRP_TIP_MBR_CANCELED_ADMIN
ret.text = `${showName} 被撤销管理员`;
break;
case 6: // GRP_TIP_GRP_PROFILE_UPDATED
ret.text = handleGroupProfileUpdated(message);
break;
case 7: // GRP_TIP_MBR_PROFILE_UPDATED
message.payload.memberList.forEach((member: any) => {
if (member.muteTime > 0) {
ret.text = `${showName} 被禁言`;
} else {
ret.text = `${showName} 被取消禁言`;
}
});
break;
default:
ret.text = `[群提示消息]`;
break;
}
return ret;
}
const handleGroupProfileUpdated = (message: any) => {
const { nick, payload } = message;
const { newGroupProfile, memberList, operatorID } = payload;
let text = '';
const showName: string = nick || operatorID;
const key: string = Object.keys(newGroupProfile)[0];
switch (key) {
case 'muteAllMembers':
if (newGroupProfile[key]) {
text = `管理员 ${showName} 开启全员禁言`;
} else {
text = `管理员 ${showName} 取消全员禁言`;
}
break;
case 'ownerID':
if (memberList && memberList.length > 0) {
text = `${memberList[0].nick || memberList[0].userID} 成为新的群主`;
}
break;
case 'groupName':
text = `${showName} 修改群名为 ${newGroupProfile[key]}`;
break;
case 'notification':
text = `${showName} 发布新公告`;
break;
default:
break;
}
return text;
}
</script>
<style scoped>
.group-tip-message {
display: flex;
justify-content: center;
align-items: center;
padding: 16rpx 32rpx;
margin: 8rpx 0;
}
.tip-text {
font-size: 24rpx;
color: #999;
text-align: center;
line-height: 1.4;
max-width: 80%;
word-break: break-all;
}
</style>
@@ -0,0 +1,91 @@
<template>
<div class="image-message">
<image :src="message?.payload.imageInfoArray[0]?.url" :style="imgStyle" @click="previewImage" />
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
const props = defineProps({
message: {
type: Object,
},
})
const imgStyle = ref({});
const previewImage = (e: Event) => {
const currentUrl = props.message?.payload.imageInfoArray[0]?.url;
if (!currentUrl) return;
uni.previewImage({
current: currentUrl,
urls: [currentUrl],
indicator: 'default',
loop: false,
});
}
const calculateImageSize = (width: number, height: number) => {
const maxWidth = 200;
const maxHeight = 200;
if (width > maxWidth) {
height = (maxWidth / width) * height;
width = maxWidth;
}
if (height > maxHeight) {
width = (maxHeight / height) * width;
height = maxHeight;
}
return { width, height };
}
const setImageStyle = (width: number, height: number) => {
imgStyle.value = {
width: `${width}px`,
height: `${height}px`
};
}
const getImageSizeFromProps = () => {
const imageInfo = props.message?.payload.imageInfoArray[0];
if (imageInfo?.width && imageInfo?.height) {
return { width: imageInfo.width, height: imageInfo.height };
}
return null;
}
const detectImageSize = () => {
uni.getImageInfo({
src: props.message?.payload.imageInfoArray[0]?.url,
success: (res) => {
const { width, height } = calculateImageSize(res.width, res.height);
setImageStyle(width, height);
},
fail: () => {
setImageStyle(200, 200);
}
});
}
onMounted(() => {
const sizeFromProps = getImageSizeFromProps();
if (sizeFromProps) {
const { width, height } = calculateImageSize(sizeFromProps.width, sizeFromProps.height);
setImageStyle(width, height);
} else {
detectImageSize();
}
});
</script>
<style lang="scss" scoped>
.image-message {
image {
border-radius: 8px;
object-fit: contain;
}
}
</style>
@@ -0,0 +1,24 @@
.bubble {
background-color: #F0F2F7;
border-radius: 0px 10px 10px 10px;
padding: 15rpx;
position: relative;
&.bubble-me {
border-radius: 10px 0 10px 10px;
background-color: #CCE2FF;
}
.callMessage-icon {
width: 20px;
height: 20px;
margin-right: 6px;
}
.text {
font-size: 14px;
font-weight: 400;
color: #333;
}
}
@@ -0,0 +1,26 @@
<template>
<view class="bubble" :class="{ 'bubble-me': message?.flow === 'out' }">
<text class="text" :style="{ wordBreak: shouldBreakWord ? 'break-all' : 'normal' }">
{{ message.payload.text }}
</text>
</view>
</template>
<script lang="ts" setup>
import { computed } from 'vue'
const props = defineProps({
message: {
type: Object,
},
})
const shouldBreakWord = computed(() => {
return props.message?.payload?.text?.length > 50
})
</script>
<style lang="scss" scoped>
@import './Message.module.scss';
</style>
@@ -0,0 +1,190 @@
<template>
<view class="video-message">
<!-- 视频预览图 -->
<view class="video-preview" @click="handleVideoClick">
<image class="video-poster" :src="message.payload.snapshotUrl" mode="aspectFill" />
<!-- 播放按钮图标 -->
<view class="play-icon">
<image :src="playControlIcon" mode="aspectFit" />
</view>
<!-- 视频时长 -->
<view class="video-duration">
{{ formatDuration(message.payload.videoSecond) }}
</view>
</view>
<!-- 全屏播放器 -->
<view class="fullscreen-container" v-if="isPlaying">
<video id="videoPlayer" :src="message.payload.remoteVideoUrl" :autoplay="true" :controls="true"
:show-fullscreen-btn="false" :show-center-play-btn="false" @ended="handleVideoEnd"
@pause="handleVideoPause" @fullscreenchange="handleFullscreenChange" class="fullscreen-video"
:enable-progress-gesture="true" objectFit="contain" />
<!-- 关闭按钮 -->
<view class="video-controls">
<view class="close-btn" @click="handleCloseVideo">
<view class="close-icon"></view>
</view>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import playControlIcon from '../../../assets/chat/play-control.svg';
const props = defineProps({
message: {
type: Object,
required: true
},
});
const isPlaying = ref(false);
const formatDuration = (seconds: number) => {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs < 10 ? '0' : ''}${secs}`;
};
const handleVideoClick = () => {
isPlaying.value = true;
setTimeout(() => {
const videoContext = uni.createVideoContext('videoPlayer', this);
videoContext.requestFullScreen({
direction: 0 // 0表示正常竖屏
});
}, 100);
};
const handleVideoPause = () => {
};
const handleVideoEnd = () => {
isPlaying.value = false;
};
const handleFullscreenChange = (e: any) => {
if (!e.detail.fullScreen) {
isPlaying.value = false;
}
};
const handleCloseVideo = () => {
const videoContext = uni.createVideoContext('videoPlayer', this);
videoContext.pause();
videoContext.exitFullScreen();
isPlaying.value = false;
};
</script>
<style lang="scss" scoped>
.video-message {
position: relative;
width: 200px;
height: 200px;
border-radius: 8px;
overflow: hidden;
background-color: black;
.video-preview {
position: relative;
width: 100%;
height: 100%;
}
.video-poster {
width: 100%;
height: 100%;
}
.play-icon {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 48px;
height: 48px;
image {
width: 100%;
height: 100%;
}
}
.video-duration {
position: absolute;
right: 8px;
bottom: 8px;
padding: 2px 6px;
background-color: rgba(0, 0, 0, 0.5);
color: white;
font-size: 12px;
border-radius: 4px;
}
.fullscreen-container {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 999;
background-color: black;
}
.fullscreen-video {
width: 100%;
height: 100%;
}
.video-controls {
position: absolute;
top: 0;
left: 0;
width: 100%;
padding: 15px;
box-sizing: border-box;
z-index: 1000;
/* 确保关闭按钮在控制条上方 */
}
.close-btn {
width: 24px;
height: 24px;
background-color: rgba(0, 0, 0, 0.5);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.close-icon {
width: 16px;
height: 16px;
position: relative;
}
.close-icon::before,
.close-icon::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 100%;
height: 2px;
background-color: white;
border-radius: 1px;
}
.close-icon::before {
transform: translate(-50%, -50%) rotate(45deg);
}
.close-icon::after {
transform: translate(-50%, -50%) rotate(-45deg);
}
}
</style>