perf: 优化消息列表

This commit is contained in:
kuaifan 2022-05-12 15:10:21 +08:00
parent 1d4602db0b
commit a13bc3f0bd
6 changed files with 587 additions and 595 deletions

View File

@ -53,6 +53,7 @@
"vue-resize-observer": "^2.0.16", "vue-resize-observer": "^2.0.16",
"vue-router": "^3.5.3", "vue-router": "^3.5.3",
"vue-template-compiler": "^2.6.14", "vue-template-compiler": "^2.6.14",
"vue-virtual-scroller-hi": "^1.0.10-1",
"vuedraggable": "^2.24.3", "vuedraggable": "^2.24.3",
"vuex": "^3.6.2", "vuex": "^3.6.2",
"webpack": "^5.69.1", "webpack": "^5.69.1",

View File

@ -1,6 +1,6 @@
<template> <template>
<div <div
v-if="dialogData && dialogData.id" v-if="isReady"
class="dialog-wrapper" class="dialog-wrapper"
@drop.prevent="chatPasteDrag($event, 'drag')" @drop.prevent="chatPasteDrag($event, 'drag')"
@dragover.prevent="chatDragOver(true, $event)" @dragover.prevent="chatDragOver(true, $event)"
@ -51,42 +51,38 @@
</ETooltip> </ETooltip>
</div> </div>
</slot> </slot>
<ScrollerY <DynamicScroller
ref="scroller" ref="scroller"
class="dialog-scroller overlay-y" :items="allMsgs"
:style="{opacity: visible ? 1 : 0}" :min-item-size="58"
:auto-bottom="isAutoBottom" @onScroll="onScroll"
@on-scroll="chatScroll" class="dialog-scroller overlay-y">
static> <template #before>
<div ref="manageList" class="dialog-list"> <div v-if="dialogData.hasMorePages" class="dialog-item history" @click="loadNextPage">{{$L('加载历史消息')}}</div>
<ul> <div v-else-if="dialogData.loading > 0 && dialogMsgList.length === 0" class="dialog-item loading"><Loading/></div>
<li v-if="dialogData.hasMorePages" class="history" @click="loadNextPage">{{$L('加载历史消息')}}</li> <div v-else-if="dialogMsgList.length === 0" class="dialog-item nothing">{{$L('暂无消息')}}</div>
<li v-else-if="dialogData.loading > 0 && dialogMsgList.length === 0" class="loading"><Loading/></li> </template>
<li v-else-if="dialogMsgList.length === 0" class="nothing">{{$L('暂无消息')}}</li> <template v-slot="{ item, index, active }">
<li <DynamicScrollerItem
v-for="item in dialogMsgList" :item="item"
:id="'view_' + item.id" :active="active"
:key="item.id" :size-dependencies="[item.msg]"
:class="{self:item.userid == userId, 'history-tip': topId == item.id}"> :data-index="index"
:data-active="active"
:class="{
'dialog-item': true,
'self': item.userid == userId,
'history-tip': topId == item.id
}"
@click.native="">
<em v-if="topId == item.id" class="history-text">{{$L('历史消息')}}</em> <em v-if="topId == item.id" class="history-text">{{$L('历史消息')}}</em>
<div class="dialog-avatar"> <div class="dialog-avatar">
<UserAvatar :userid="item.userid" :tooltipDisabled="item.userid == userId" :size="30"/> <UserAvatar :userid="item.userid" :tooltipDisabled="item.userid == userId" :size="30"/>
</div> </div>
<DialogView :msg-data="item" :dialog-type="dialogData.type"/> <DialogView :msg-data="item" :dialog-type="dialogData.type"/>
</li> </DynamicScrollerItem>
<li </template>
v-for="item in tempMsgList" </DynamicScroller>
:id="'tmp_' + item.id"
:key="'tmp_' + item.id"
:class="{self:item.userid == userId}">
<div class="dialog-avatar">
<UserAvatar :userid="item.userid" :tooltipDisabled="item.userid == userId" :size="30"/>
</div>
<DialogView :msg-data="item" :dialog-type="dialogData.type"/>
</li>
</ul>
</div>
</ScrollerY>
<div :class="['dialog-footer', msgNew > 0 && dialogMsgList.length > 0 ? 'newmsg' : '']" @click="onActive"> <div :class="['dialog-footer', msgNew > 0 && dialogMsgList.length > 0 ? 'newmsg' : '']" @click="onActive">
<div class="dialog-newmsg" @click="onToBottom">{{$L('' + msgNew + '条新消息')}}</div> <div class="dialog-newmsg" @click="onToBottom">{{$L('' + msgNew + '条新消息')}}</div>
<div class="dialog-input"> <div class="dialog-input">
@ -161,19 +157,30 @@
</template> </template>
<script> <script>
import ScrollerY from "../../../components/ScrollerY";
import {mapState} from "vuex"; import {mapState} from "vuex";
import DialogView from "./DialogView"; import DialogView from "./DialogView";
import DialogUpload from "./DialogUpload"; import DialogUpload from "./DialogUpload";
import {Store} from "le5le-store";
import UserInput from "../../../components/UserInput"; import UserInput from "../../../components/UserInput";
import DrawerOverlay from "../../../components/DrawerOverlay"; import DrawerOverlay from "../../../components/DrawerOverlay";
import DialogGroupInfo from "./DialogGroupInfo"; import DialogGroupInfo from "./DialogGroupInfo";
import ChatInput from "./ChatInput"; import ChatInput from "./ChatInput";
import { DynamicScroller, DynamicScrollerItem } from 'vue-virtual-scroller-hi'
import 'vue-virtual-scroller-hi/dist/vue-virtual-scroller.css'
export default { export default {
name: "DialogWrapper", name: "DialogWrapper",
components: {ChatInput, DialogGroupInfo, DrawerOverlay, UserInput, DialogUpload, DialogView, ScrollerY}, components: {
DynamicScroller,
DynamicScrollerItem,
ChatInput,
DialogGroupInfo,
DrawerOverlay,
UserInput,
DialogUpload,
DialogView
},
props: { props: {
dialogId: { dialogId: {
type: Number, type: Number,
@ -183,21 +190,13 @@ export default {
data() { data() {
return { return {
visible: true,
autoBottom: true,
autoInterval: null,
dialogDrag: false,
inputFocus: false,
msgText: '', msgText: '',
msgNew: 0, msgNew: 0,
topId: 0, topId: 0,
allMsgs: [],
tempMsgs: [], tempMsgs: [],
dialogMsgSubscribe: null,
pasteShow: false, pasteShow: false,
pasteFile: [], pasteFile: [],
pasteItem: [], pasteItem: [],
@ -206,19 +205,17 @@ export default {
createGroupData: {}, createGroupData: {},
createGroupLoad: 0, createGroupLoad: 0,
dialogDrag: false,
groupInfoShow: false, groupInfoShow: false,
} }
}, },
mounted() { mounted() {
this.dialogMsgSubscribe = Store.subscribe('dialogMsgPush', this.addDialogMsg);
}, },
beforeDestroy() { beforeDestroy() {
if (this.dialogMsgSubscribe) {
this.dialogMsgSubscribe.unsubscribe();
this.dialogMsgSubscribe = null;
}
}, },
computed: { computed: {
@ -230,12 +227,16 @@ export default {
'wsOpenNum', 'wsOpenNum',
]), ]),
isReady() {
return this.dialogId > 0 && this.dialogData.id > 0
},
dialogData() { dialogData() {
return this.cacheDialogs.find(({id}) => id == this.dialogId) || {}; return this.cacheDialogs.find(({id}) => id == this.dialogId) || {};
}, },
dialogMsgList() { dialogMsgList() {
if (!this.dialogId) { if (!this.isReady) {
return []; return [];
} }
return $A.cloneJSON(this.dialogMsgs.filter(({dialog_id}) => { return $A.cloneJSON(this.dialogMsgs.filter(({dialog_id}) => {
@ -245,15 +246,8 @@ export default {
}); });
}, },
isAutoBottom() {
if (this.inputFocus && !this.isDesktop) {
return false;
}
return this.autoBottom
},
tempMsgList() { tempMsgList() {
if (!this.dialogId) { if (!this.isReady) {
return []; return [];
} }
return $A.cloneJSON(this.tempMsgs.filter(({dialog_id}) => { return $A.cloneJSON(this.tempMsgs.filter(({dialog_id}) => {
@ -261,6 +255,14 @@ export default {
})); }));
}, },
allMsgList() {
const {dialogMsgList, tempMsgList} = this;
if (tempMsgList.length > 0) {
dialogMsgList.push(...tempMsgList);
}
return dialogMsgList;
},
peopleNum() { peopleNum() {
return this.dialogData.type === 'group' ? $A.runNum(this.dialogData.people) : 0; return this.dialogData.type === 'group' ? $A.runNum(this.dialogData.people) : 0;
}, },
@ -313,20 +315,7 @@ export default {
if (id) { if (id) {
this.msgNew = 0; this.msgNew = 0;
this.topId = -1; this.topId = -1;
this.visible = false; this.$store.dispatch("getDialogMsgs", id).then(this.onToBottom).catch(_ => {});
if (this.dialogMsgList.length > 0) {
setTimeout(_ => {
this.onToBottom();
this.visible = true;
}, 10);
}
let startTime = new Date().getTime();
this.$store.dispatch("getDialogMsgs", id).then(_ => {
setTimeout(_ => {
this.onToBottom();
this.visible = true;
}, Math.max(0, 100 - (new Date().getTime() - startTime)));
});
} }
}, },
immediate: true immediate: true
@ -335,7 +324,21 @@ export default {
wsOpenNum(num) { wsOpenNum(num) {
if (num <= 1) return if (num <= 1) return
this.$store.dispatch("getDialogMsgs", this.dialogId).catch(_ => {}); this.$store.dispatch("getDialogMsgs", this.dialogId).catch(_ => {});
},
allMsgList(newList, oldList) {
const {scrollE} = this.scrollInfo();
this.allMsgs = newList;
this.$nextTick(_ => {
if (scrollE > 10 && oldList.length > 0) {
const lastId = oldList[oldList.length - 1].id
const tmpList = newList.filter(item => item.id && item.id > lastId)
this.msgNew += tmpList.length
} else {
this.onToBottom();
} }
})
},
}, },
methods: { methods: {
@ -353,6 +356,12 @@ export default {
} }
msgText = msgText.replace(/<\/span> <\/p>$/, "</span></p>") msgText = msgText.replace(/<\/span> <\/p>$/, "</span></p>")
// //
if (!this.isDesktop) {
this.$refs.input.blur();
}
this.onToBottom();
this.onActive();
//
let tempId = $A.randomString(16); let tempId = $A.randomString(16);
this.tempMsgs.push({ this.tempMsgs.push({
id: tempId, id: tempId,
@ -363,11 +372,6 @@ export default {
text: msgText, text: msgText,
}, },
}); });
if (!this.isDesktop) {
this.$refs.input.blur();
}
this.onToBottom();
this.onActive();
// //
this.$store.dispatch("call", { this.$store.dispatch("call", {
url: 'dialog/msg/sendtext', url: 'dialog/msg/sendtext',
@ -441,6 +445,12 @@ export default {
chatFile(type, file) { chatFile(type, file) {
switch (type) { switch (type) {
case 'progress': case 'progress':
if (!this.isDesktop) {
this.$refs.input.blur();
}
this.onToBottom();
this.onActive();
//
this.tempMsgs.push({ this.tempMsgs.push({
id: file.tempId, id: file.tempId,
dialog_id: this.dialogData.id, dialog_id: this.dialogData.id,
@ -448,11 +458,6 @@ export default {
userid: this.userId, userid: this.userId,
msg: { }, msg: { },
}); });
if (!this.isDesktop) {
this.$refs.input.blur();
}
this.onToBottom();
this.onActive();
break; break;
case 'error': case 'error':
@ -479,31 +484,11 @@ export default {
this.onActive(); this.onActive();
}, },
chatScroll(res) {
switch (res.directionreal) {
case 'up':
if (res.scrollE < 10) {
this.msgNew = 0;
this.autoBottom = true;
}
break;
case 'down':
this.autoBottom = false;
break;
}
if (res.scale >= 1) {
this.msgNew = 0;
this.autoBottom = true;
}
},
onEventFocus(e) { onEventFocus(e) {
this.inputFocus = true;
this.$emit("on-focus", e) this.$emit("on-focus", e)
}, },
onEventBlur(e) { onEventBlur(e) {
this.inputFocus = false;
this.$emit("on-blur", e) this.$emit("on-blur", e)
}, },
@ -518,8 +503,10 @@ export default {
}, },
onToBottom() { onToBottom() {
this.autoBottom = true; this.msgNew = 0;
this.$refs.scroller && this.$refs.scroller.autoToBottom(); if (this.isReady) {
this.$refs.scroller.scrollToBottom();
}
}, },
openProject() { openProject() {
@ -537,30 +524,18 @@ export default {
}, },
loadNextPage() { loadNextPage() {
let topId = this.dialogMsgList[0].id; let tmpId = this.allMsgs[0].id;
this.$store.dispatch('getDialogMoreMsgs', this.dialogId).then(() => { this.$store.dispatch('getDialogMoreMsgs', this.dialogId).then(() => {
this.$nextTick(() => { this.$nextTick(() => {
this.topId = topId; this.topId = tmpId;
$A.scrollToView(document.getElementById("view_" + topId), { const index = this.allMsgs.findIndex(({id}) => id == tmpId);
behavior: 'instant', if (index > -1) {
inline: 'start', this.$refs.scroller.scrollToItem(index);
}) }
}); });
}).catch(() => {}) }).catch(() => {})
}, },
addDialogMsg() {
if (this.isAutoBottom) {
this.$nextTick(this.onToBottom);
} else {
this.$nextTick(() => {
if (this.$refs.scroller && this.$refs.scroller.scrollInfo().scrollE > 10) {
this.msgNew++;
}
})
}
},
openCreateGroup() { openCreateGroup() {
this.createGroupData = { this.createGroupData = {
userids: this.dialogData.dialog_user ? [this.userId, this.dialogData.dialog_user.userid] : [this.userId], userids: this.dialogData.dialog_user ? [this.userId, this.dialogData.dialog_user.userid] : [this.userId],
@ -586,6 +561,36 @@ export default {
this.createGroupLoad--; this.createGroupLoad--;
}); });
}, },
scrollInfo() {
if (!this.isReady) {
return {
scale: 0, //
scrollY: 0, //
scrollE: 0, //
}
}
const scrollerView = this.$refs.scroller.$el;
let wInnerH = scrollerView.clientHeight;
let wScrollY = scrollerView.scrollTop;
let bScrollH = scrollerView.scrollHeight;
this.scrollY = wScrollY;
return {
scale: wScrollY / (bScrollH - wInnerH),
scrollY: wScrollY,
scrollE: bScrollH - wInnerH - wScrollY,
}
},
onScroll() {
this.__onScroll && clearTimeout(this.__onScroll);
this.__onScroll = setTimeout(_ => {
const {scrollE} = this.scrollInfo();
if (scrollE <= 10) {
this.msgNew = 0;
}
}, 100)
},
} }
} }
</script> </script>

View File

@ -179,9 +179,7 @@ body.dark-mode-reverse {
} }
} }
.dialog-scroller { .dialog-scroller {
.dialog-list { .dialog-item {
> ul {
> li {
.dialog-view { .dialog-view {
.dialog-head { .dialog-head {
.dialog-content { .dialog-content {
@ -218,8 +216,6 @@ body.dark-mode-reverse {
} }
} }
} }
}
}
.dialog-group-info { .dialog-group-info {
.group-info-user { .group-info-user {

View File

@ -180,23 +180,16 @@
} }
.dialog-scroller { .dialog-scroller {
position: relative;
flex: 1; flex: 1;
padding: 0 32px; position: relative;
overflow: auto; padding: 16px 32px 0;
.dialog-list { .dialog-item {
> ul {
> li {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: flex-start; align-items: flex-start;
list-style: none; list-style: none;
margin-bottom: 16px; padding-bottom: 16px;
&:first-child {
margin-top: 16px;
}
.dialog-avatar { .dialog-avatar {
position: relative; position: relative;
@ -655,8 +648,6 @@
} }
} }
} }
}
}
.dialog-footer { .dialog-footer {
display: flex; display: flex;
@ -877,7 +868,8 @@
} }
} }
.dialog-scroller { .dialog-scroller {
padding: 0 14px; padding-right: 14px;
padding-left: 14px;
} }
.dialog-footer { .dialog-footer {
background-color: #f8f8f8; background-color: #f8f8f8;

View File

@ -81,9 +81,6 @@
padding: 0 12px; padding: 0 12px;
height: 64px; height: 64px;
} }
.dialog-scroller {
padding: 0 14px;
}
} }
} }
} }

View File

@ -637,7 +637,8 @@
} }
.dialog-wrapper { .dialog-wrapper {
.dialog-scroller { .dialog-scroller {
padding: 0 16px 0 32px padding-right: 16px;
padding-left: 32px;
} }
.dialog-footer { .dialog-footer {
padding: 0 14px 0 28px; padding: 0 14px 0 28px;