This commit is contained in:
cansnow
2025-12-11 22:33:31 +08:00
parent 375917f06c
commit 5a086fa1fa
44 changed files with 1978 additions and 359 deletions
@@ -0,0 +1,24 @@
<template>
<view class="at_text_message_container bg_container">
消息
</view>
</template>
<script>
export default {
name: "AtTextMessageRender",
components: {},
props: {
message: Object,
conversationID:String,
},
data() {
return {};
},
methods: {
},
};
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,24 @@
<template>
<view class="card_message_container bg_container">
消息
</view>
</template>
<script>
export default {
name: "CardMessageRender",
components: {},
props: {
message: Object,
conversationID:String,
},
data() {
return {};
},
methods: {
},
};
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,24 @@
<template>
<view class="custom_message_container bg_container">
消息
</view>
</template>
<script>
export default {
name: "CustomMessageRender",
components: {},
props: {
message: Object,
conversationID:String,
},
data() {
return {};
},
methods: {
},
};
</script>
<style lang="scss" scoped></style>
@@ -1,17 +1,17 @@
<template>
<view class="text_message_container bg_container">
<view> [暂未支持的消息类型] </view>
</view>
<view class="text_message_container bg_container">
<view> [暂未支持的消息类型] </view>
</view>
</template>
<script>
export default {
name: "ErrorMessagegRender",
components: {},
data() {
return {};
},
};
export default {
name: "ErrorMessagegRender",
components: {},
data() {
return {};
},
};
</script>
<style lang="scss" scoped></style>
<style lang="scss" scoped></style>
@@ -0,0 +1,103 @@
<template>
<view class="face_message_container bg_container">
<u--image
:showLoading="true"
width="120"
:height="maxHeight"
mode="widthFix"
v-if="src"
:src="src"
@load="onLoaded"
@click="clickMediaItem">
<template v-slot:loading>
<u-loading-icon color="red"></u-loading-icon>
</template>
</u--image>
</view>
</template>
<script>
import util from "@/util"
import md5 from "md5";
export default {
name: "FaceMessageRender",
components: {},
props: {
message: Object,
conversationID:String,
},
data() {
//console.log(this.message);
return {
loadingWidth: "120px",
maxHeight:'120px',
src:"",
coverCachePath:"",
coverDownloading:false,
coverExists:false,
coverDownloadProgress:"",
};
},
computed: {
getImgUrl() {
return (
this.message.FaceElem?.data ??
this.message.FaceElem.index
);
}
},
created() {
this.init();
},
methods: {
async init(){
const self = this;
let url = "";
// 如果有远程 snapshotUrl,则下载到 coverCachePath
const snapshotUrl = this.message.FaceElem.data;
const key = md5(snapshotUrl || '');
this.coverCachePath = `_doc/${this.conversationID}/face_${key}.jpg`;
if (typeof plus === 'undefined' || !this.coverCachePath) return;
try {
// 检查封面是否存在
const coverExists = await util.fileExsit(self.coverCachePath);
this.coverExists = !!coverExists;
if (this.coverExists) {
this.src = this.coverCachePath;
return;
}
if (!snapshotUrl) {
this.src="/static/images/sync_error.png";
return;
}
this.coverDownloading = true;
await new Promise((resolve, reject) => {
util.downloadFile(snapshotUrl, self.coverCachePath, function(localPath) {
self.coverDownloading = false;
self.coverExists = true;
resolve(localPath);
}, function(err) {
self.coverDownloading = false;
reject(err);
}, function(progress) {
self.coverDownloadProgress = progress;
});
});
} catch (e) {
this.coverDownloading = false;
}
},
onLoaded() {
this.loadingWidth = "auto";
}
},
};
</script>
<style lang="scss" scoped>
.face_message_container {
position: relative;
border-radius: 16rpx;
overflow: hidden;
}
</style>
@@ -0,0 +1,24 @@
<template>
<view class="file_message_container bg_container">
文件
</view>
</template>
<script>
export default {
name: "FileMessageRender",
components: {},
props: {
message: Object,
conversationID:String,
},
data() {
return {};
},
methods: {
},
};
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,24 @@
<template>
<view class="location_message_container bg_container">
消息
</view>
</template>
<script>
export default {
name: "LocationMessageRender",
components: {},
props: {
message: Object,
conversationID:String,
},
data() {
return {};
},
methods: {
},
};
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,24 @@
<template>
<view class="markdown_message_container bg_container">
消息
</view>
</template>
<script>
export default {
name: "MarkdownMessageRender",
components: {},
props: {
message: Object,
conversationID:String,
},
data() {
return {};
},
methods: {
},
};
</script>
<style lang="scss" scoped></style>
@@ -1,81 +0,0 @@
<template>
<view class="media_message_container" @click="clickMediaItem">
<u--image
@load="onLoaded"
:showLoading="true"
width="120"
:height="maxHeight"
mode="widthFix"
:src="getImgUrl"
@click="clickMediaItem"
>
<template v-slot:loading>
<u-loading-icon color="red"></u-loading-icon>
</template>
</u--image>
</view>
</template>
<script>
export default {
name: "",
props: {
message: Object,
},
data() {
return {
loadingWidth: "120px",
};
},
computed: {
getImgUrl() {
return (
this.message.pictureElem.snapshotPicture?.url ??
this.message.pictureElem.sourcePath
);
},
maxHeight() {
const imageHeight = this.message.pictureElem.sourcePicture.height;
const imageWidth = this.message.pictureElem.sourcePicture.width;
const aspectRatio = imageHeight / imageWidth;
return 120 * aspectRatio;
},
},
methods: {
clickMediaItem() {
uni.previewImage({
current: 0,
urls: [this.message.pictureElem.sourcePicture.url],
indicator: "none",
});
},
onLoaded() {
this.loadingWidth = "auto";
},
},
};
</script>
<style lang="scss" scoped>
.media_message_container {
position: relative;
border-radius: 16rpx;
overflow: hidden;
.play_icon {
width: 48px;
height: 48px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.video_duration {
position: absolute;
bottom: 12rpx;
right: 24rpx;
color: #fff;
}
}
</style>
@@ -0,0 +1,24 @@
<template>
<view class="merge_message_container bg_container">
消息
</view>
</template>
<script>
export default {
name: "MergeMessageRender",
components: {},
props: {
message: Object,
conversationID:String,
},
data() {
return {};
},
methods: {
},
};
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,24 @@
<template>
<view class="oanotification_message_container bg_container">
消息
</view>
</template>
<script>
export default {
name: "OANotificationMessageRender",
components: {},
props: {
message: Object,
conversationID:String,
},
data() {
return {};
},
methods: {
},
};
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,124 @@
<template>
<view class="picture_message_container" @click="clickMediaItem">
<u--image
:showLoading="true"
width="120"
:height="maxHeight"
mode="widthFix"
v-if="src"
:src="src"
@load="onLoaded"
@click="clickMediaItem">
<template v-slot:loading>
<u-loading-icon color="red"></u-loading-icon>
</template>
</u--image>
</view>
</template>
<script>
import util from "@/util"
import md5 from "md5";
export default {
name: "PictureMessageContainer",
props: {
message: Object,
conversationID:String,
},
data() {
//console.log(this.message);
return {
loadingWidth: "120px",
src:"",
coverCachePath:"",
coverDownloading:false,
coverExists:false,
coverDownloadProgress:"",
};
},
computed: {
maxHeight() {
const imageHeight = this.message.pictureElem.sourcePicture.height;
const imageWidth = this.message.pictureElem.sourcePicture.width;
const aspectRatio = imageHeight / imageWidth;
return 120 * aspectRatio;
},
},
created() {
this.init();
},
methods: {
async init(){
const self = this;
let url = "";
// 如果有远程 snapshotUrl,则下载到 coverCachePath
const snapshotUrl = (this.message.pictureElem.snapshotPicture?.url ?? this.message.pictureElem.sourcePath );
const key = md5(snapshotUrl || '');
this.coverCachePath = `_doc/${this.conversationID}/img_${key}.jpg`;
if (typeof plus === 'undefined' || !this.coverCachePath) return;
try {
// 检查封面是否存在
const coverExists = await util.fileExsit(self.coverCachePath);
this.coverExists = !!coverExists;
if (this.coverExists) {
this.src = this.coverCachePath;
return;
}
if (!snapshotUrl) {
this.src="/static/images/sync_error.png";
return;
}
this.coverDownloading = true;
await new Promise((resolve, reject) => {
util.downloadFile(snapshotUrl, self.coverCachePath, function(localPath) {
self.coverDownloading = false;
self.coverExists = true;
resolve(localPath);
}, function(err) {
self.coverDownloading = false;
reject(err);
}, function(progress) {
self.coverDownloadProgress = progress;
});
});
} catch (e) {
this.coverDownloading = false;
}
},
clickMediaItem() {
uni.previewImage({
current: 0,
urls: [this.message.pictureElem.sourcePicture.url],
indicator: "none",
});
},
onLoaded() {
this.loadingWidth = "auto";
},
},
};
</script>
<style lang="scss" scoped>
.picture_message_container {
position: relative;
border-radius: 16rpx;
overflow: hidden;
.play_icon {
width: 48px;
height: 48px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.video_duration {
position: absolute;
bottom: 12rpx;
right: 24rpx;
color: #fff;
}
}
</style>
@@ -0,0 +1,24 @@
<template>
<view class="quote_message_container bg_container">
消息
</view>
</template>
<script>
export default {
name: "QuoteMessageRender",
components: {},
props: {
message: Object,
conversationID:String,
},
data() {
return {};
},
methods: {
},
};
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,24 @@
<template>
<view class="stream_message_container bg_container">
消息
</view>
</template>
<script>
export default {
name: "StreamMessageRender",
components: {},
props: {
message: Object,
conversationID:String,
},
data() {
return {};
},
methods: {
},
};
</script>
<style lang="scss" scoped></style>
@@ -1,36 +1,33 @@
<template>
<view class="text_message_container bg_container">
<mp-html
:previewImg="false"
:showImgMenu="false"
:lazyLoad="false"
selectable
:content="getContent"
/>
</view>
<view class="text_message_container bg_container">
<mp-html :previewImg="false" :showImgMenu="false" :lazyLoad="false" selectable :content="getContent" />
</view>
</template>
<script>
import { parseBr } from "@/util/common";
import {
parseBr
} from "@/util/common";
export default {
name: "TextMessageRender",
components: {},
props: {
message: Object,
},
computed: {
getContent() {
return parseBr(this.message.textElem?.content);
},
},
data() {
return {};
},
methods: {
},
};
export default {
name: "TextMessageRender",
components: {},
props: {
message: Object,
conversationID:String,
},
computed: {
getContent() {
return parseBr(this.message.textElem?.content);
},
},
data() {
return {};
},
methods: {
},
};
</script>
<style lang="scss" scoped></style>
<style lang="scss" scoped></style>
@@ -0,0 +1,24 @@
<template>
<view class="typing_message_container bg_container">
消息
</view>
</template>
<script>
export default {
name: "TypingMessageRender",
components: {},
props: {
message: Object,
conversationID:String,
},
data() {
return {};
},
methods: {
},
};
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,264 @@
<template>
<view>
<view class="video_message_container" @click="clickMediaItem">
<u--image
:showLoading="true"
:src="src"
:width="imgWidth"
:height="maxHeight"
mode="widthFix"
@load="onLoaded"
@click="clickMediaItem">
<template v-slot:loading>
<u-loading-icon color="red"></u-loading-icon>
</template>
</u--image>
<!-- 覆盖层显示封面下载状态/视频下载入口/播放按钮 -->
<view class="download-overlay" @click.stop="onOverlayClick">
<view v-if="videoDownloading" class="progress-box">
<view class="progress-bar">
<view class="progress-fill" :style="{ width: videoDownloadProgress + '%' }"></view>
</view>
<text class="progress-text">{{ videoDownloadProgress }}%</text>
</view>
<u-icon v-else-if="videoExists" class="play_icon" name="play-circle" size="48" color="#fff"></u-icon>
<uni-icons v-else class="play_icon" type="cloud-download" size="48" color="#fff"></uni-icons>
</view>
</view>
<!-- 视频播放器组件 -->
<videoPlayer :previewVideoFlag="previewVideoFlag" :previewVideoSrc="previewVideoSrc" @quitPlay="previewVideoFlag=false"></videoPlayer>
</view>
</template>
<script>
/*
{
"clientMsgID": "d9a596f3e665e84559821a8aa5fb9f16",
"serverMsgID": "76a56965a5fba40df213da2acb331517",
"createTime": 1765413732786,
"sendTime": 1765413733732,
"sessionType": 1,
"sendID": "100003",
"recvID": "100004",
"msgFrom": 100,
"contentType": 104,
"senderPlatformID": 2,
"senderNickname": "131****2222",
"senderFaceUrl": "/static/img/avatar.png",
"seq": 22,
"isRead": false,
"status": 2,
"videoElem": {
"videoPath": "/storage/emulated/0/Movies/顺网云电脑 2025-10-21 04-26-04-converted.mp4",
"videoUrl": "http://www.axzc.xyz/object/100003/msg_video_d9a596f3e665e84559821a8aa5fb9f16.mp4",
"videoType": "mp4",
"videoSize": 3751005,
"duration": 44,
"snapshotPath": "/storage/emulated/0/Android/data/hk.huisheng.im/apps/__UNI__CA458BA/doc/video_cover_2ea1c8ec.jpg",
"snapshotSize": 36974,
"snapshotUrl": "http://www.axzc.xyz/object/100003/msg_videoSnapshot_d9a596f3e665e84559821a8aa5fb9f16.jpg",
"snapshotWidth": 1087,
"snapshotHeight": 726
},
"attachedInfoElem": {
"groupHasReadInfo": {
"hasReadCount": 0,
"groupMemberCount": 0
},
"isPrivateChat": false,
"burnDuration": 0,
"hasReadTime": 0,
"isEncryption": false,
"inEncryptStatus": false
}
}
*/
const imgWidth = 240;
import md5 from "md5";
import videoPlayer from "@/components/videoPlayer.vue";
import util from "@/util"
export default {
name: "VideoMessageContainer",
components: {
videoPlayer,
},
props: {
message: Object,
conversationID:String,
},
data() {
return {
loadingWidth: imgWidth+"px",
imgWidth:imgWidth,
isDownloading: false,
downloadProgress: 0,
coverDownloading: false,
coverDownloadProgress: 0,
videoDownloading: false,
videoDownloadProgress: 0,
coverExists: false,
videoExists: false,
coverCachePath:"",
videoCachePath:"",
previewVideoFlag: false,
previewVideoSrc: "",
src:"",
};
},
computed: {
maxHeight() {
const imageHeight = this.message.videoElem.snapshotHeight;
const imageWidth = this.message.videoElem.snapshotWidth;
const aspectRatio = imageHeight / imageWidth;
return imgWidth * aspectRatio;
}
},
async created(){
this.init();
},
methods: {
async init(){
// 如果有远程 snapshotUrl,则下载到 coverCachePath
const snapshotUrl = this.message?.videoElem?.snapshotUrl;
const key = md5(this.message?.videoElem?.videoUrl || '');
this.coverCachePath = `_doc/${this.conversationID}/cover_${key}.jpg`;
this.videoCachePath = `_doc/${this.conversationID}/${key}.mp4`;
const coverExists = await util.fileExsit(this.coverCachePath);
// 自动缓存封面到 this.coverCachePath
const self = this;
if (typeof plus === 'undefined' || !this.coverCachePath) return;
try {
// 检查封面是否存在
const coverExists = await util.fileExsit(self.coverCachePath);
this.coverExists = !!coverExists;
// 同时检查视频缓存是否存在
const vidExists = await util.fileExsit(self.videoCachePath);
this.videoExists = !!vidExists;
if (this.coverExists) {
this.src = this.coverCachePath;
return;
}
if (!snapshotUrl) {
this.src="/static/images/sync_error.png";
return;
}
this.coverDownloading = true;
await new Promise((resolve, reject) => {
util.downloadFile(snapshotUrl, self.coverCachePath, function(localPath) {
self.coverDownloading = false;
self.coverExists = true;
resolve(localPath);
}, function(err) {
self.coverDownloading = false;
reject(err);
}, function(progress) {
self.coverDownloadProgress = progress;
});
});
} catch (e) {
this.coverDownloading = false;
}
},
clickMediaItem() {
uni.previewImage({
current: 0,
urls: [this.message.videoElem?.snapshotUrl],
indicator: "none",
});
},
async onLoaded() {
this.loadingWidth = "auto";
},
onOverlayClick() {
// 点击覆盖层:如果视频已缓存则直接播放,否则开始下载
if (this.videoExists) {
this.playVideo(this.videoCachePath);
return;
}
const url = this.message?.videoElem?.videoUrl || this.message?.videoElem?.videoPath;
if (!url) {
uni.showToast({ title: '无可下载的视频' });
return;
}
this.videoDownloading = true;
this.videoDownloadProgress = 0;
util.downloadFile(url, this.videoCachePath, (localPath) => {
this.videoDownloading = false;
this.videoExists = true;
this.playVideo(localPath);
}, (err) => {
this.videoDownloading = false;
uni.showToast({ title: '下载失败' });
}, (prog) => {
this.videoDownloadProgress = prog;
});
},
playVideo(localPath) {
if (!localPath) return;
this.previewVideoSrc = localPath;
this.previewVideoFlag = true;
},
},
};
</script>
<style lang="scss" scoped>
.video_message_container {
position: relative;
border-radius: 16rpx;
overflow: hidden;
.play_icon {
width: 48px;
height: 48px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.video_duration {
position: absolute;
bottom: 12rpx;
right: 24rpx;
color: #fff;
}
}
.download-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0,0,0,0.35);
}
.progress-box {
width: 70%;
max-width: 240px;
text-align: center;
}
.progress-bar {
width: 100%;
height: 8px;
background: rgba(255,255,255,0.25);
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: #4caf50;
width: 0%;
}
.progress-text {
display: block;
color: #fff;
margin-top: 8px;
font-size: 12px;
}
</style>
@@ -0,0 +1,24 @@
<template>
<view class="voice_message_container bg_container">
消息
</view>
</template>v
<script>
export default {
name: "VoiceMessageRender",
components: {},
props: {
message: Object,
conversationID:String,
},
data() {
return {};
},
methods: {
},
};
</script>
<style lang="scss" scoped></style>
@@ -16,9 +16,7 @@
</view>
</view>
<view class="message_content_wrap message_content_wrap_shadow" :id="`message_content_wrap_${source.clientMsgID}`" @longtap.stop.prevent="longtapEvent($event)">
<text-message-render v-if="showTextRender" :message="source" />
<media-message-render v-else-if="showMediaRender" :message="source" />
<error-message-render v-else />
<component :is="component" :message="source" :conversationID="conversationID"></component>
</view>
</view>
</view>
@@ -35,20 +33,46 @@
import {MessageStatus,MessageType,SessionType,} from "openim-uniapp-polyfill";
import MyAvatar from "@/components/MyAvatar/index.vue";
import TextMessageRender from "./TextMessageRender";
import MediaMessageRender from "./MediaMessageRender";
import PictureMessageRender from "./PictureMessageRender";
import VoiceMessageRender from "./VoiceMessageRender";
import VideoMessageRender from "./VideoMessageRender";
import FileMessageRender from "./FileMessageRender";
import AtTextMessageRender from "./AtTextMessageRender";
import MergeMessageRender from "./MergeMessageRender";
import CardMessageRender from "./CardMessageRender";
import LocationMessageRender from "./LocationMessageRender";
import CustomMessageRender from "./CustomMessageRender";
import TypingMessageRender from "./TypingMessageRender";
import QuoteMessageRender from "./QuoteMessageRender";
import FaceMessageRender from "./FaceMessageRender";
import MarkdownMessageRender from "./MarkdownMessageRender";
import StreamMessageRender from "./StreamMessageRender";
import OANotificationRender from "./OANotificationRender";
import ErrorMessageRender from "./ErrorMessageRender";
import {noticeMessageTypes} from "@/constant";
import {tipMessaggeFormat,formatMessageTime} from "@/util/imCommon";
const textRenderTypes = [MessageType.TextMessage];
const mediaRenderTypes = [MessageType.PictureMessage];
import { ca } from "date-fns/locale";
export default {
components: {
MyAvatar,
TextMessageRender,
MediaMessageRender,
PictureMessageRender,
VoiceMessageRender,
VideoMessageRender,
FileMessageRender,
AtTextMessageRender,
MergeMessageRender,
CardMessageRender,
LocationMessageRender,
CustomMessageRender,
TypingMessageRender,
QuoteMessageRender,
FaceMessageRender,
MarkdownMessageRender,
StreamMessageRender,
OANotificationRender,
ErrorMessageRender
},
props: {
@@ -57,6 +81,7 @@
type: Boolean,
default: false,
},
conversationID:String,
isPreview: Boolean,
isActive: Boolean,
},
@@ -67,6 +92,7 @@
toolTipFlag: false,
popPostion:"default",
toolTipData: [],
component:"ErrorMessageRender",
};
},
computed: {
@@ -82,12 +108,6 @@
formattedMessageTime() {
return formatMessageTime(this.source.sendTime);
},
showTextRender() {
return textRenderTypes.includes(this.source.contentType);
},
showMediaRender() {
return mediaRenderTypes.includes(this.source.contentType);
},
getNoticeContent() {
const isNoticeMessage = noticeMessageTypes.includes(
this.source.contentType
@@ -110,7 +130,26 @@
},
},
mounted() {
this.$emit("messageItemRender", this.source.clientMsgID);
const MsgType2Components = {
['type_'+MessageType.TextMessage] : "TextMessageRender",
['type_'+MessageType.PictureMessage] : "PictureMessageRender",
['type_'+MessageType.VoiceMessage] : "VoiceMessageRender",
['type_'+MessageType.VideoMessage] : "VideoMessageRender",
['type_'+MessageType.FileMessage] : "FileMessageRender",
['type_'+MessageType.AtTextMessage] : "AtTextMessageRender",
['type_'+MessageType.MergeMessage] : "MergeMessageRender",
['type_'+MessageType.CardMessage] : "CardMessageRender",
['type_'+MessageType.LocationMessage] : "LocationMessageRender",
['type_'+MessageType.CustomMessage] : "CustomMessageRender",
['type_'+MessageType.TypingMessage] : "TypingMessageRender",
['type_'+MessageType.QuoteMessage] : "QuoteMessageRender",
['type_'+MessageType.FaceMessage] : "FaceMessageRender",
['type_'+MessageType.MarkdownMessage] : "MarkdownMessageRender",
['type_'+MessageType.StreamMessage] : "StreamMessageRender",
['type_'+MessageType.OANotification] : "OANotificationRender"
};
this.component = MsgType2Components['type_'+this.source.contentType] || "ErrorMessageRender";
this.$emit('userEvent',{type:"messageItemRender"},this.source.clientMsgID);
this.setSendingDelay();
},
methods: {