<template> <div v-loading="chatIniting" class="message-list h-100"> <el-scrollbar ref="message-scrollbar" class="message-list-scrollbar no-bottom-scrollbar h-100" > <template v-for="item in messages"> <div :key="item.id" class="message-template"> <div v-if=" item.id > 0 && messageTimestampDictionary[item.id] && item.msg " class="text-center text-hint timestamp" > {{ format2Time(item.ts) }} </div> <message :is-sending-message="item.id < 0" :failed="item.status === -1" :key="item.id" :data="item" :shape="shape" @open="open" @withdraw="refresh" /> </div> </template> </el-scrollbar> <image-preview v-model="preview" :file="imagePreview"></image-preview> <video-preview v-model="previewVideo" :file="videoPreview" ></video-preview> </div> </template> <script lang="ts"> import { Component, Prop, Ref, Vue, Watch } from "vue-property-decorator"; import { Message, MessageType } from "../model"; import { throttle } from "../utils"; import { formatTime } from "../utils/time"; import ImagePreview from "./image-preview.vue"; import message from "./message.vue"; import VideoPreview from "./video-preview.vue"; import { ChatStore, chatStore } from "@/customer-service/store/model"; import { dbController } from "../database"; import { getLastMessageId } from "../store"; @Component({ components: { message, ImagePreview, VideoPreview } }) export default class MessageList extends Vue { @chatStore.Getter(ChatStore.STATE_CHAT_MSG_HISTORY) private readonly historyMessage!: ChatStore.STATE_CHAT_MSG_HISTORY; @chatStore.Getter(ChatStore.STATE_CHAT_SENDING_MESSAGES) private readonly sendingMessages!: ChatStore.STATE_CHAT_SENDING_MESSAGES; @chatStore.State(ChatStore.STATE_CHAT_CURRENT_CHAT_ID) private readonly chatId!: ChatStore.STATE_CHAT_CURRENT_CHAT_ID; @chatStore.State(ChatStore.STATE_CURRENT_CHAT_INITING) private readonly chatIniting!: ChatStore.STATE_CURRENT_CHAT_INITING; @chatStore.Action(ChatStore.ACTION_GET_CHAT_MESSAGES_BEFORE_SPECIFIC_ID) private readonly getLastPageMsg!: ChatStore.ACTION_GET_CHAT_MESSAGES_BEFORE_SPECIFIC_ID; @chatStore.Action(ChatStore.ACTION_GET_CHAT_MESSAGES_AFTER_SPECIFIC_ID) private readonly getNextPageMsg!: ChatStore.ACTION_GET_CHAT_MESSAGES_AFTER_SPECIFIC_ID; @chatStore.Mutation(ChatStore.MUTATION_SAVE_FUNC_SCROLL_TO_BOTTOM) private readonly saveScrollToBottomFunc!: ChatStore.MUTATION_SAVE_FUNC_SCROLL_TO_BOTTOM; @chatStore.Mutation(ChatStore.MUTATION_CLEAR_FUNC_SCROLL_TO_BOTTOM) private readonly clearScrollToBottomFunc!: ChatStore.MUTATION_CLEAR_FUNC_SCROLL_TO_BOTTOM; @chatStore.Mutation(ChatStore.MUTATION_SAVE_FUNC_ON_NEW_MSG) private readonly onNewMessage!: ChatStore.MUTATION_SAVE_FUNC_ON_NEW_MSG; @chatStore.Mutation(ChatStore.MUTATION_CLEAR_FUNC_ON_NEW_MSG) private readonly clearNewMessage!: ChatStore.MUTATION_CLEAR_FUNC_ON_NEW_MSG; @chatStore.Mutation(ChatStore.MUTATION_WITHDRAW) private readonly executeWithDraw!: ChatStore.MUTATION_WITHDRAW; @chatStore.Action(ChatStore.ACTION_SET_CHAT_ERROR) private readonly setError!: ChatStore.ACTION_SET_CHAT_ERROR; @Prop({ default: "circle" }) private shape!: string; private get messages() { if (this.historyMessage) { if (this.sendingMessages) { return [...this.historyMessage, ...this.sendingMessages].filter( (i) => i.chat_id === this.chatId && i.id > 0 ); } return this.historyMessage; } if (this.sendingMessages) { return this.sendingMessages.filter( (i) => i.chat_id === this.chatId && i.id > 0 ); } return []; } // 添加时间戳的最大间隔消息数 private readonly timeLimit = 48; private scroll2EndWhenMessageLoaded = false; private preview = false; private imagePreview = {}; private previewVideo = false; private videoPreview = {}; @Ref("message-scrollbar") private scollbarElement!: Vue & { update: () => void; }; private get scollWrapper(): HTMLElement | null { return this.scollbarElement?.$el?.firstChild as HTMLElement; } @Watch("messages") private whenHasMessages() { this.$nextTick(() => this.scollbarElement.update()); } @Watch("preview") private onPreviewChanged() { if (!this.preview) { this.raiseFileOpen(false); } } @Watch("previewVideo") private onVideoPreviewChanged() { if (!this.previewVideo) { this.raiseFileOpen(false); } } @Watch("chatId") private onChatChanged(o: number, n: number) { o && n && this.messages.length ? this.fetchNewMsg() : setTimeout(() => this.fetchNewMsg(), 300); this.scroll2End(this.messages.length ? 0 : 100); } private raiseFileOpen(value: boolean) { this.$emit("file-open", value); } private get messageTimestampDictionary() { const dic = {} as { [prop: number]: boolean }; let count = 0; if (this.historyMessage) { this.historyMessage.forEach((message, index, array) => { if ( index === 0 || this.whetherShowTime(array[index - 1], message) || count === this.timeLimit - 1 ) { dic[message.id] = true; count = 0; } else { count++; } }); } return dic; } private loading = false; private loadingOld = false; private loadingNew = false; public created() { this.handleScrollWrapper(); this.onNewMessage((e) => { if (e.type === MessageType.Withdraw) { this.executeWithDraw(e.ref_id); dbController .removeMessage(e.chat_id, e.ref_id) .finally(() => this.refresh()); } }); } public mounted() { this.scollWrapper && this.scollWrapper.addEventListener("scroll", this.handleScroll); this.saveScrollToBottomFunc(this.scrollToNewMsg); this.scrollToNewMsg(); setTimeout(() => this.scroll2End(200)); setTimeout(() => this.scroll2End(1000), 200); } public beforeDestroy() { this.scollWrapper && this.scollWrapper.removeEventListener("scroll", this.handleScroll); this.clearScrollToBottomFunc(); this.clearNewMessage(); } public scroll2End(delay?: number) { this.$nextTick(() => { const wrap = this.scollbarElement?.$el.querySelector( ".el-scrollbar__wrap" ) as HTMLElement; if (wrap) { if (delay) { return setTimeout( () => (wrap.scrollTop = Math.max( wrap.scrollHeight + 100, 10000 )), delay ); } wrap.scrollTop = Math.max(wrap.scrollHeight + 100, 10000); } }); } private startLoading() { this.loading = true; } private endLoading() { this.loading = false; } private startLoadingOld() { this.startLoading(); this.loadingOld = true; } private endLoadingOld() { this.endLoading(); this.loadingOld = false; } private startLoadingNew() { this.startLoading(); this.loadingNew = true; } private endLoadingNew() { this.endLoading(); this.loadingNew = false; } private handleScroll!: () => void; private handleScrollWrapper() { let oldScrollTop = 0; this.handleScroll = () => { const wrapper = this.scollWrapper; const gap = 150; if (wrapper == null) return; const view = wrapper.firstChild as HTMLElement; const wrapperH = wrapper.getBoundingClientRect().height; const viewH = view.getBoundingClientRect().height; let scrollUp = false; let scrollDown = false; if (oldScrollTop > wrapper.scrollTop) { scrollUp = true; scrollDown = false; } else if (oldScrollTop < wrapper.scrollTop) { scrollUp = false; scrollDown = true; } this.forbidScrollTopToZero(wrapper); if (wrapper.scrollTop <= gap) { scrollUp && this.fetchOldMsg(); } if (wrapper.scrollTop - 40 - (viewH - wrapperH) >= -gap) { scrollDown && this.fetchNewMsg(); } oldScrollTop = wrapper.scrollTop; }; } /* scrollTop为0时,新加载的消息后,滚动条也会保持在0的位置 */ private forbidScrollTopToZero(ele: HTMLElement) { if (ele.scrollTop <= 10) { ele.scrollTop = 10; } } private scrollToNewMsg() { this.$nextTick(() => { if (this.loading) return; this.scroll2End(); }); } @throttle() private async fetchOldMsg() { if (this.loading) return; const msg = this.historyMessage; if (msg == null) return; if (msg.length === 0) return; this.startLoadingOld(); const msgId = msg[0].id; const data = await this.getLastPageMsg(msgId); if (data.length === 0) { // eslint-disable-next-line no-console console.log("没有更多老消息了"); } this.$emit("last-page", msgId); this.endLoadingOld(); } @throttle() private async fetchNewMsg() { if (this.loading) return; const msg = this.historyMessage; if (msg == null) return; if (msg.length === 0) return; this.startLoadingNew(); const msgId = getLastMessageId(msg); return this.getNextPageMsg(msgId) .then((data) => { if (data.length === 0) { // eslint-disable-next-line no-console console.log("没有更多新消息了"); } this.$emit("next-page", msgId); this.endLoadingNew(); }) .catch((e) => { if ( e && e.message && (e.message as string).includes("sql: no rows in result set") ) { this.setError(this.chatId as number); } }) .finally(() => this.endLoadingNew()); } private format2Time(time: number) { return formatTime(time); } private whetherShowTime(previous: Message, current: Message) { return current.ts - previous.ts > 180; } private open(file: { type: string; msg: { url: string; name: string; size: number }; }) { if (file.type === "image") { this.imagePreview = file.msg; this.preview = true; return this.raiseFileOpen(true); } if (file.type === "video") { this.videoPreview = file.msg; this.previewVideo = true; return this.raiseFileOpen(true); } } /** * 获取当期消息列表头尾消息的id */ public getStart2EndMessageIds() { const v: { start: number; end: number } = { start: 0, end: 0 }; if (this.historyMessage && this.historyMessage.length) { const start = this.historyMessage[0]; v.start = start.id; const end = this.historyMessage[this.historyMessage.length - 1]; v.end = end.id; } return v; } private refresh() { this.fetchNewMsg(); } } </script> <style lang="less" scoped> .message-list { padding: 0 20px; padding-right: 0; } .loading-mask { height: 50px; line-height: 50px; text-align: center; } .message-template { &:first-child { padding-top: 10px; .timestamp { margin-top: 20px; } } &:last-child { padding-bottom: 10px; } .timestamp { font-size: 12px; user-select: none; } } </style>