Commit ece66d8a by Sixong.Zhu

消息结构调整

parent 15e55bff
<template>
<div
class="msg-detail voice-message d-flex align-items-center"
:class="{ playing: playing, 'can-play': messageRealUrl }"
v-if="messageType === 'voice'"
@click.stop="play"
:style="{ width: getVoiceMessageWidth + 'px' }"
>
<div class="d-flex align-items-center" v-if="messageRealUrl">
<voice-icon :loading="playing"></voice-icon>
<audio ref="audio" @play="onPlay" @pause="onPause">
<source type="audio/aac" :src="messageRealUrl" />
</audio>
<span v-if="duration" class="duration text-nowrap text-hint"
>{{ durationInSecond }}s</span
>
</div>
<i
class="el-icon-warning-outline"
v-else-if="fileFailed2Load"
title="[语音加载失败]"
></i>
</div>
</template>
<script lang="ts">
import { Component, Ref } from "vue-property-decorator";
import BaseMessage from "./index";
import VoiceIcon from "./voice.vue";
@Component({ components: { VoiceIcon } })
export default class Index extends BaseMessage {
@Ref("audio")
private readonly audioRef!: HTMLAudioElement;
private playing = false;
private get duration() {
const v = this.messageBody.msg.duration as number;
return v || 0;
}
private get durationInSecond() {
return Math.round(this.duration / 1000);
}
private get getVoiceMessageWidth() {
if (this.fileFailed2Load) {
return 35;
}
const d = this.duration / 1000;
if (d <= 3) {
return 60;
}
if (d >= 60) {
return 200;
}
return 60 + d;
}
private play() {
if (this.audioRef?.paused) {
this.audioRef?.load();
this.audioRef?.play();
} else {
this.audioRef?.pause();
}
}
private onPlay() {
this.playing = true;
}
private onPause() {
this.playing = false;
}
mounted() {
this.buildMessageUrl();
}
}
</script>
<style lang="less" scoped></style>
<template>
<span class="file-icon" :title="value" v-html="html"></span>
<span class="file-icon" :title="value" v-html="html"></span>
</template>
<script lang="ts">
......@@ -9,70 +9,70 @@ import { FileType, getSvg } from "./file-controller";
@Component({ components: {} })
export default class FileIcon extends Vue {
@Model("update")
private value!: FileType;
@Model("update")
private value!: FileType;
private get audio() {
return this.value === FileType.Audio;
}
private get audio() {
return this.value === FileType.Audio;
}
private get excel() {
return this.value === FileType.Excel;
}
private get excel() {
return this.value === FileType.Excel;
}
private get image() {
return this.value === FileType.Image;
}
private get image() {
return this.value === FileType.Image;
}
private get others() {
return this.value === FileType.Others;
}
private get others() {
return this.value === FileType.Others;
}
private get pdf() {
return this.value === FileType.Pdf;
}
private get pdf() {
return this.value === FileType.Pdf;
}
private get ppt() {
return this.value === FileType.Ppt;
}
private get ppt() {
return this.value === FileType.Ppt;
}
private get rp() {
return this.value === FileType.Rp;
}
private get rp() {
return this.value === FileType.Rp;
}
private get txt() {
return this.value === FileType.Txt;
}
private get txt() {
return this.value === FileType.Txt;
}
private get video() {
return this.value === FileType.Video;
}
private get video() {
return this.value === FileType.Video;
}
private get word() {
return this.value === FileType.Word;
}
private get word() {
return this.value === FileType.Word;
}
private get xmid() {
return this.value === FileType.Xmind;
}
private get xmid() {
return this.value === FileType.Xmind;
}
private get zip() {
return this.value === FileType.Zip;
}
private get zip() {
return this.value === FileType.Zip;
}
private get html() {
return getSvg(this.value);
}
private get html() {
return getSvg(this.value);
}
}
</script>
<style lang="less" scoped>
.file-icon {
margin-left: 10px;
margin-left: 10px;
svg {
max-width: 36px;
max-height: 36px;
}
/deep/ svg {
max-width: 36px;
max-height: 36px;
}
}
</style>
<template>
<div class="msg-detail file-message d-flex" @dblclick="openFile">
<div class="file-message-info">
<div
class="text-nowrap text-truncate file-message-name"
:title="messageBody.msg.name"
>
{{ messageBody.msg.name }}
</div>
<div class="text-hint">
{{ format(messageBody.msg.size) }}
</div>
</div>
<file-icon :value="fileIcon"></file-icon>
<a
class="
d-flex
align-items-center
justify-content-center
download-icon
"
:href="messageRealUrl"
:download="getAttachment"
title="下载文件"
>
<img src="~@/customer-service/imgs/download.png" alt="Download" />
</a>
</div>
</template>
<script lang="ts">
import { Component } from "vue-property-decorator";
import BaseMessage from "./index";
import FileIcon from "./file-icon.vue";
import { FileType, getFileType } from "./file-controller";
const k = 1024,
m = 1024 * k,
g = 1024 * m,
t = 1024 * g;
function formatSize(size: number) {
if (size === undefined || size === null) {
return "";
}
if (size < k) {
return size + " B";
}
if (size < m) {
return Number((size / k).toFixed(2)) + " KB";
}
if (size < g) {
return Number((size / m).toFixed(2)) + " MB";
}
if (size < t) {
return Number((size / g).toFixed(2)) + " GB";
}
return Number((size / t).toFixed(2)) + " TB";
}
@Component({ components: { FileIcon } })
export default class Index extends BaseMessage {
private get getAttachment() {
if (this.messageBody) {
return this.messageBody.msg.name;
}
return "文件下载";
}
private get fileIcon() {
if (this.value) {
return getFileType(this.messageBody.msg.name);
}
return FileType.Others;
}
private format(v: number) {
return formatSize(v);
}
mounted() {
this.buildMessageUrl();
}
}
</script>
<style lang="less" scoped></style>
<template>
<div
class="msg-detail image-message"
:class="{ 'image-404': fileFailed2Load }"
@dblclick="openFile"
>
<img
v-if="messageRealUrl"
:src="messageRealUrl"
:title="messageBody.msg.name"
:alt="messageBody.msg.name"
@error="onImageError"
/>
<file-icon v-else-if="fileFailed2Load" :value="image404"></file-icon>
</div>
</template>
<script lang="ts">
import { Component } from "vue-property-decorator";
import { FileType } from "./file-controller";
import BaseMessage from "./index";
import FileIcon from "./file-icon.vue";
@Component({ components: { FileIcon } })
export default class Index extends BaseMessage {
private readonly image404 = FileType.Image_404;
private onImageError() {
this.fileFailed2Load = true;
this.messageRealUrl = "";
}
mounted() {
this.buildMessageUrl();
}
}
</script>
<style lang="less" scoped></style>
import { Component, Vue, Model, Prop } from "vue-property-decorator";
import { Message } from "@/customer-service/model";
import { isAccessibleUrl } from "@/customer-service/service/tools";
@Component({ components: {} })
export default class BaseMessage extends Vue {
@Model()
protected readonly value!: Message;
@Prop()
protected readonly userName!: string;
protected messageRealUrl = "";
protected fileFailed2Load = false;
protected loadingRealUrl = false;
protected get messageBody(): { eid?: string; oid?: string; msg: any } {
if (this.value) {
try {
return { ...this.value, msg: JSON.parse(this.value.msg) };
} catch {
return {
...this.value,
msg: JSON.parse(this.value.msg.replace(/\n/g, "\\n")),
};
}
}
return { msg: { text: "" } };
}
protected openFile() {
this.$emit("open", this.messageRealUrl);
}
protected buildMessageUrl() {
if (this.messageRealUrl || this.loadingRealUrl) {
return;
}
const url = this.messageBody.msg.url as string;
if (url) {
if (isAccessibleUrl(url)) {
return (this.messageRealUrl = url);
}
this.loadingRealUrl = true;
} else {
this.fileFailed2Load = true;
}
}
}
<template>
<div
class="msg-detail inline-text"
v-html="format2Link(messageBody.msg.text || emptyText)"
></div>
</template>
<script lang="ts">
import { replaceText2Link } from "@/utils/isUrl";
import { Component } from "vue-property-decorator";
import BaseMessage from "./index";
@Component({ components: {} })
export default class Index extends BaseMessage {
private readonly emptyText = " ";
private format2Link(text: string) {
return replaceText2Link(text);
}
}
</script>
<style lang="less" scoped></style>
<template>
<div
class="
msg-detail
video-message
d-flex
align-items-center
justify-content-center
"
>
<video-player-icon @click.native="openFile"></video-player-icon>
</div>
</template>
<script lang="ts">
import { Component } from "vue-property-decorator";
import BaseMessage from "./index";
import VideoPlayerIcon from "./video-player-icon.vue";
@Component({ components: { VideoPlayerIcon } })
export default class Index extends BaseMessage {
mounted() {
this.buildMessageUrl();
}
}
</script>
<style lang="less" scoped></style>
......@@ -32,20 +32,18 @@
</template>
<script lang="ts">
import { Component, Prop, Vue, Watch } from "vue-property-decorator"
;
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
@Component({ components: {} })
export default class VoiceIcon extends Vue {
@Prop({ default: 25 })
private size!: number
private size!: number;
@Prop()
private loading!: boolean
private loading!: boolean;
private status = 0
private interval = 0
private status = 0;
private interval = 0;
@Watch("loading")
private onLoadingChanged() {
......
<template>
<div class="msg-detail withdraw-message">{{ userName }}撤回了一条消息</div>
</template>
<script lang="ts">
import { Component } from "vue-property-decorator";
import BaseMessage from "./index";
@Component({ components: {} })
export default class Index extends BaseMessage {}
</script>
<style lang="less" scoped></style>
......@@ -197,8 +197,7 @@ export default class MessageList extends Vue {
this.scollWrapper.addEventListener("scroll", this.handleScroll);
this.saveScrollToBottomFunc(this.scrollToNewMsg);
this.scrollToNewMsg();
// force scroll end
setTimeout(() => this.scrollToNewMsg(), 100);
this.scroll2End(200);
}
public beforeDestroy() {
......@@ -216,12 +215,9 @@ export default class MessageList extends Vue {
) as HTMLElement;
if (wrap) {
if (delay) {
setTimeout(() => {
wrap.scrollTop = 100000;
}, delay);
return;
return setTimeout(() => (wrap.scrollTop = wrap.scrollHeight), delay);
}
wrap.scrollTop = 100000;
wrap.scrollTop = wrap.scrollHeight;
}
});
}
......
......@@ -14,110 +14,16 @@
>
{{ userName }}
</div>
<!-- Image -->
<div
class="msg-detail image-message"
:class="{ 'image-404': fileFailed2Load }"
v-if="messageType === 'image'"
@dblclick="openFile"
>
<img
v-if="messageRealUrl"
:src="messageRealUrl"
:title="messageBody.msg.name"
:alt="messageBody.msg.name"
@error="onImageError"
/>
<file-icon
v-else-if="fileFailed2Load"
:value="image404"
></file-icon>
</div>
<!-- File -->
<div
class="msg-detail file-message d-flex"
v-else-if="messageType === 'file'"
@dblclick="openFile"
>
<div class="file-message-info">
<div
class="text-nowrap text-truncate file-message-name"
:title="messageBody.msg.name"
>
{{ messageBody.msg.name }}
</div>
<div class="text-hint">
{{ messageBody.msg.size | formatSize }}
</div>
</div>
<file-icon :value="fileIcon"></file-icon>
</div>
<!-- Audio -->
<div
class="msg-detail voice-message d-flex align-items-center"
:class="{ playing: playing, 'can-play': messageRealUrl }"
v-else-if="messageType === 'voice'"
@click.stop="play"
:style="{ width: getVoiceMessageWidth + 'px' }"
>
<div class="d-flex align-items-center" v-if="messageRealUrl">
<voice-icon :loading="playing"></voice-icon>
<audio ref="audio" @play="onPlay" @pause="onPause">
<source type="audio/aac" :src="messageRealUrl" />
</audio>
<span v-if="duration" class="duration text-nowrap text-hint"
>{{ durationInSecond }}s</span
>
</div>
<i
class="el-icon-warning-outline"
v-else-if="fileFailed2Load"
title="[语音加载失败]"
></i>
</div>
<!-- Video -->
<div
class="
msg-detail
video-message
d-flex
align-items-center
justify-content-center
"
v-else-if="messageType === 'video'"
>
<video-player-icon @click.native="openFile"></video-player-icon>
</div>
<div
class="msg-detail withdraw-message"
v-else-if="messageType === 'withdraw'"
>
{{ userName }}撤回了一条消息
</div>
<!-- Text -->
<div
class="msg-detail inline-text"
v-else
v-html="format2Link(messageBody.msg.text || emptyText)"
></div>
<component
:is="messageComponent"
:user-name="userName"
v-if="messageComponent"
v-model="data"
@open="openFile"
/>
</div>
<a
v-if="!isSendingMessage && messageType === 'file'"
class="
d-flex
align-items-center
justify-content-center
download-icon
"
:href="messageRealUrl | downloadUrl(getAttachment)"
:download="getAttachment"
title="下载文件"
>
<img src="~@/customer-service/imgs/download.png" alt="Download" />
</a>
<i
class="el-icon-warning text-danger"
v-if="failed"
......@@ -160,22 +66,16 @@ import { Component, Inject, Mixins, Prop, Ref } from "vue-property-decorator";
import { Filters } from "../mixin/filter";
import * as dto from "../model";
import { isAccessibleUrl } from "../service/tools";
import { replaceText2Link } from "../utils";
import chat from "./../xim";
import {
FileType,
getFileType,
isAudio,
isImage,
isVideo,
MAX_FILE_SIZE,
MAX_IMAGE_SIZE,
} from "./file-controller";
import FileIcon from "./file-icon.vue";
import VideoPlayerIcon from "./video-player-icon.vue";
import VoiceIcon from "./voice.vue";
} from "./message-item/file-controller";
import WhoReadList from "./who-read-list.vue";
import avatar from "@/customer-service/components/avatar.vue";
......@@ -183,10 +83,35 @@ import { chatStore, ChatStore } from "@/customer-service/store/model";
import ximInstance from "../xim/xim";
import { dbController } from "../database";
import ImageMessage from "./message-item/image-message.vue";
import FileMessage from "./message-item/file-message.vue";
import AudioMessage from "./message-item/audio-message.vue";
import VideoMessage from "./message-item/video-message.vue";
import TextMessage from "./message-item/text-message.vue";
import WithdrawMessage from "./message-item/withdraw-message.vue";
const twoMinutes = 2 * 60 * 1000;
const messageMapping = new Map<dto.MessageType, string>([
[dto.MessageType.Image, "image-message"],
[dto.MessageType.File, "file-message"],
[dto.MessageType.Video, "video-message"],
[dto.MessageType.Voice, "audio-message"],
[dto.MessageType.Text, "text-message"],
[dto.MessageType.Withdraw, "withdraw-message"],
]);
@Component({
components: { FileIcon, VoiceIcon, WhoReadList, VideoPlayerIcon, avatar },
components: {
WhoReadList,
avatar,
ImageMessage,
FileMessage,
AudioMessage,
VideoMessage,
TextMessage,
WithdrawMessage,
},
})
export default class Message extends Mixins(Filters) {
@chatStore.State(ChatStore.STATE_CHAT_CURRENT_USER_UID)
......@@ -204,34 +129,21 @@ export default class Message extends Mixins(Filters) {
@chatStore.Mutation(ChatStore.MUTATION_WITHDRAW)
private readonly executeWithDraw!: ChatStore.MUTATION_WITHDRAW;
/**
* tbd: 文件消息所在的域名的url,逻辑需要补充
*/
private messageRealUrl = "";
private fileFailed2Load = false;
private image404 = FileType.Image_404;
private loadingRealUrl = false;
@Prop({ type: Object, default: () => Object.create(null) })
private data!: dto.Message;
@Prop()
private isSendingMessage!: boolean;
private readonly isSendingMessage!: boolean;
@Prop()
private failed!: boolean;
private readonly failed!: boolean;
@Prop({ default: "circle" })
private shape!: string;
@Ref("audio")
private readonly audioRef!: HTMLAudioElement;
private playing = false;
private readonly shape!: string;
@Inject({ default: false }) readonly showReadSummary!: boolean;
private emptyText = " ";
private messageComponent = "";
private readListVisibility = false;
......@@ -267,13 +179,6 @@ export default class Message extends Mixins(Filters) {
return { msg: { text: "" } };
}
private get getAttachment() {
if (this.messageBody) {
return this.messageBody.msg.name;
}
return "文件下载";
}
private get isMyMessage() {
if (this.isSendingMessage) {
return true;
......@@ -281,31 +186,10 @@ export default class Message extends Mixins(Filters) {
const senderEid = this.messageBody.eid;
return senderEid!.toString() === this.chatMyId!.toString();
// if (this.messageBody) {
// const msg = this.messageBody;
// if (this.chatSource) {
// const source = msg.msg.source;
// if (source) {
// return (
// source === this.chatSource &&
// senderEid === this.chatMyId
// );
// }
// if (this.org && this.messageBody.oid) {
// return (
// this.messageBody.oid === this.org &&
// senderEid === this.chatMyId
// );
// }
// return false;
// }
// return senderEid === this.chatMyId;
// }
// return false;
}
mounted() {
this.buildMessageUrl();
created() {
this.messageComponent = messageMapping.get(this.messageType) as string;
}
private get userName() {
......@@ -337,14 +221,6 @@ export default class Message extends Mixins(Filters) {
return "";
}
private get fileIcon() {
if (this.data) {
return getFileType(this.messageBody.msg.name);
}
return FileType.Others;
}
private get messageType() {
const type = this.data?.type;
if (type === "file") {
......@@ -354,119 +230,39 @@ export default class Message extends Mixins(Filters) {
if (size) {
const outImageSize = size > MAX_IMAGE_SIZE;
if (!outImageSize && isImage(name)) {
return "image";
return dto.MessageType.Image;
}
const outSize = size > MAX_FILE_SIZE;
if (!outSize) {
if (isAudio(name)) {
return "voice";
return dto.MessageType.Voice;
}
if (isVideo(name)) {
return "video";
return dto.MessageType.Video;
}
}
}
}
}
return this.data?.type;
}
// 图片的样式设置,通过js偶尔会有高度计算不准确, 直接通过css的处理目前看可以达到对应效果
// private get imageStyle() {
// if (this.messageBody == null) return {};
// if (this.messageBody.msg.w == null) return {};
// if (this.messageBody.msg.h == null) return {};
// const w = this.messageBody.msg.w;
// const h = this.messageBody.msg.h;
// const maxWidth = 300;
// const maxHeight = maxWidth * (h / w);
// return { width: `${w}px`, height: `${h}px`, maxHeight: `${maxHeight}px` };
// }
private get duration() {
const v = this.messageBody.msg.duration as number;
return v || 0;
}
private get durationInSecond() {
return Math.round(this.duration / 1000);
}
private get getVoiceMessageWidth() {
if (this.fileFailed2Load) {
return 35;
}
const d = this.duration / 1000;
if (d <= 3) {
return 60;
}
if (d >= 60) {
return 200;
}
return 60 + d;
return type;
}
private isCustomer() {
return !this.showReadSummary;
}
private buildMessageUrl() {
if (this.messageRealUrl || this.loadingRealUrl) {
return;
}
const url = this.messageBody.msg.url as string;
if (url) {
if (isAccessibleUrl(url)) {
return (this.messageRealUrl = url);
}
this.loadingRealUrl = true;
} else {
this.fileFailed2Load = true;
}
}
private openFile() {
private openFile(url: string) {
if (this.isSendingMessage) {
return;
}
if (this.failed || this.fileFailed2Load) {
if (this.failed) {
return;
}
const copy = { ...this.messageBody.msg };
if (this.messageRealUrl) {
copy.url = this.messageRealUrl;
}
copy.url = url;
this.$emit("open", { type: this.messageType, msg: copy });
}
private play() {
if (this.audioRef?.paused) {
this.audioRef?.load();
this.audioRef?.play();
} else {
this.audioRef?.pause();
}
}
private onPlay() {
this.playing = true;
}
private onPause() {
this.playing = false;
}
private format2Link(text: string) {
return replaceText2Link(text);
}
private onImageError() {
this.fileFailed2Load = true;
this.messageRealUrl = "";
}
private withdraw() {
ximInstance.withdraw(this.chatId!, this.data.id).finally(() => {
dbController
......@@ -633,6 +429,10 @@ i.msg-avatar {
margin: 0;
}
}
/deep/ .file-icon {
margin-left: 0;
}
}
&.voice-message {
......@@ -669,7 +469,7 @@ i.msg-avatar {
max-width: 130px;
}
img {
/deep/ img {
max-width: 300px;
}
}
......
......@@ -73,7 +73,7 @@ import {
MESSAGE_FILE_EMPTY,
MESSAGE_FILE_TOO_LARGE,
MESSAGE_IMAGE_TOO_LARGE,
} from "../components/file-controller";
} from "../components/message-item/file-controller";
import { EmojiService } from "../service/emoji";
import { ChatStore } from "../store/model";
import { formatFileSize } from "../utils";
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment