perf: 优化消息列表

This commit is contained in:
kuaifan 2022-06-15 17:53:00 +08:00
parent 4fcdeb0313
commit d9cd7a93cc
4 changed files with 161 additions and 150 deletions

View File

@ -54,7 +54,7 @@
"vue-resize-observer": "^2.0.16",
"vue-router": "^3.5.3",
"vue-template-compiler": "^2.6.14",
"vue-virtual-scroller-hi": "^1.0.10-3",
"vue-virtual-scroll-list": "^2.3.3",
"vuedraggable": "^2.24.3",
"vuex": "^3.6.2",
"webpack": "^5.69.1",

View File

@ -0,0 +1,108 @@
<template>
<div :class="classArray">
<div class="dialog-avatar">
<UserAvatar :userid="source.userid" :tooltipDisabled="source.userid == userId" :size="30"/>
</div>
<DialogView
ref="view"
:msg-data="source"
:dialog-type="dialogData.type"
:hide-percentage="isMyDialog"
:operate-visible="operateVisible"
:operate-action="operateVisible && source.id === operateItem.id"
@on-longpress="onLongpress"/>
</div>
</template>
<script>
import {mapState} from "vuex";
import DialogView from "./DialogView";
import {Store} from "le5le-store";
export default {
name: "DialogItem",
components: {DialogView},
props: {
source: {
type: Object,
default() {
return {}
}
},
dialogData: {
type: Object,
default() {
return {}
}
},
isMyDialog: {
type: Boolean,
default: false
},
operateVisible: {
type: Boolean,
default: false
},
operateItem: {
type: Object,
default() {
return {}
}
},
},
data() {
return {
subscribe: null,
}
},
mounted() {
this.subscribe = Store.subscribe('dialogOperate', this.onOperate);
},
beforeDestroy() {
this.subscribe.unsubscribe();
},
computed: {
...mapState(['userId']),
classArray() {
return {
'dialog-item': true,
'self': this.source.userid == this.userId,
}
}
},
methods: {
onLongpress(e) {
Store.set('dialogLongpress', e);
},
onOperate({id, action, value}) {
if (id === this.source.id) {
switch (action) {
case "withdraw":
this.$refs.view.withdraw()
break;
case "view":
this.$refs.view.viewFile()
break;
case "down":
this.$refs.view.downFile()
break;
case "emoji":
this.$refs.view.setEmoji(value)
break;
}
}
},
}
}
</script>

View File

@ -70,49 +70,19 @@
</div>
<!--消息列表-->
<DynamicScroller
<VirtualList
ref="scroller"
class="dialog-scroller scrollbar-overlay"
:style="{opacity: scrollOpacity ? 1 : 0}"
:disabled="touchBackInProgress"
:items="allMsgs"
:min-item-size="58"
@onScroll="onScroll">
<template #before>
<template v-if="allMsgs.length === 0">
<div v-if="dialogData.loading > 0" class="dialog-item loading"><Loading/></div>
<div v-else class="dialog-item nothing">{{$L('暂无消息')}}</div>
</template>
<div v-else-if="dialogData.hasMorePages" class="dialog-item history" @click="loadNextPage">{{$L('加载历史消息')}}</div>
</template>
<template v-slot="{ item, index, active }">
<DynamicScrollerItem
:item="item"
:active="active"
:size-dependencies="[item.msg]"
: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>
<div class="dialog-avatar">
<UserAvatar :userid="item.userid" :tooltipDisabled="item.userid == userId" :size="30"/>
</div>
<DialogView
:ref="`msg_${item.id}`"
:msg-data="item"
:dialog-type="dialogData.type"
:hide-percentage="isMyDialog"
:operate-visible="operateVisible"
:operate-action="operateVisible && item.id === operateItem.id"
@on-longpress="onLongpress"/>
</DynamicScrollerItem>
</template>
</DynamicScroller>
:data-key="'id'"
:data-sources="allMsgs"
:data-component="msgItem"
:extra-props="{dialogData, isMyDialog, operateVisible, operateItem}"
:estimate-size="78"
:keeps="80"
@scroll="onScroll"
@totop="loadNextPage">
<div slot="header" v-if="!dialogData.loading && allMsgs.length === 0" class="dialog-item nothing">{{$L('暂无消息')}}</div>
</VirtualList>
<!--底部输入-->
<div class="dialog-footer" :class="{newmsg: msgNew > 0 && allMsgs.length > 0}" @click="onActive">
@ -268,28 +238,25 @@
<script>
import {mapState} from "vuex";
import DialogView from "./DialogView";
import DialogItem from "./DialogItem";
import DialogUpload from "./DialogUpload";
import UserInput from "../../../components/UserInput";
import DrawerOverlay from "../../../components/DrawerOverlay";
import DialogGroupInfo from "./DialogGroupInfo";
import ChatInput from "./ChatInput";
import { DynamicScroller, DynamicScrollerItem } from 'vue-virtual-scroller-hi'
import 'vue-virtual-scroller-hi/dist/vue-virtual-scroller.css'
import VirtualList from 'vue-virtual-scroll-list'
import {Store} from "le5le-store";
export default {
name: "DialogWrapper",
components: {
DynamicScroller,
DynamicScrollerItem,
VirtualList,
ChatInput,
DialogGroupInfo,
DrawerOverlay,
UserInput,
DialogUpload,
DialogView
DialogUpload
},
props: {
@ -306,9 +273,9 @@ export default {
data() {
return {
msgItem: DialogItem,
msgText: '',
msgNew: 0,
topId: 0,
allMsgs: [],
tempMsgs: [],
@ -329,7 +296,7 @@ export default {
dialogDrag: false,
groupInfoShow: false,
scrollOpacity: true,
dialogSubscribe: null,
navStyle: {},
@ -344,8 +311,13 @@ export default {
}
},
mounted () {
this.dialogSubscribe = Store.subscribe('dialogLongpress', this.onLongpress)
},
beforeDestroy() {
this.$store.dispatch('forgetInDialog', this._uid)
this.dialogSubscribe.unsubscribe();
},
computed: {
@ -453,7 +425,6 @@ export default {
handler(id) {
if (id) {
this.msgNew = 0;
this.topId = -1;
//
if (this.allMsgList.length > 0) {
this.allMsgs = this.allMsgList;
@ -500,35 +471,25 @@ export default {
},
allMsgList(newList, oldList) {
const {scrollE} = this.scrollInfo();
if (oldList.length === 0) {
this.scrollOpacity = false;
}
const {balance} = this.scrollInfo();
this.allMsgs = newList;
//
if (!this.windowActive || (scrollE > 10 && oldList.length > 0)) {
if (!this.windowActive || (balance > 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 {
requestAnimationFrame(this.onToBottom)
}
//
this.allMsgTimer && clearTimeout(this.allMsgTimer);
if (!this.scrollOpacity) {
this.allMsgTimer = setTimeout(_=>{
this.scrollOpacity = true;
},100)
}
},
windowScrollY(val) {
if ($A.isIos()) {
const {scrollE} = this.scrollInfo();
const {balance} = this.scrollInfo();
this.navStyle = {
marginTop: val + 'px'
}
if (scrollE <= 10) {
if (balance <= 10) {
requestAnimationFrame(this.onToBottom)
}
}
@ -698,12 +659,12 @@ export default {
}
if (this.wrapperStart.clientY > e.touches[0].clientY) {
//
if (this.wrapperStart.scrollE === 0) {
if (this.wrapperStart.balance === 0) {
e.preventDefault();
}
} else {
//
if (this.wrapperStart.scrollY === 0) {
if (this.wrapperStart.offset === 0) {
e.preventDefault();
}
}
@ -808,14 +769,16 @@ export default {
},
loadNextPage() {
let tmpId = this.allMsgs[0].id;
this.$store.dispatch('getDialogMoreMsgs', this.dialogId).then(() => {
this.$store.dispatch('getDialogMoreMsgs', this.dialogId).then(result => {
const resData = result.data;
const ids = resData.data.map(item => item.id)
this.$nextTick(() => {
this.topId = tmpId;
const index = this.allMsgs.findIndex(({id}) => id == tmpId);
if (index > -1) {
this.$refs.scroller.scrollToItem(index);
}
const scroller = this.$refs.scroller
const offset = ids.reduce((previousValue, currentId) => {
const previousSize = typeof previousValue === "object" ? previousValue.size : scroller.getSize(previousValue)
return {size: previousSize + scroller.getSize(currentId)}
})
scroller.scrollToOffset(offset.size);
});
}).catch(() => {})
},
@ -876,22 +839,21 @@ export default {
},
scrollInfo() {
if (!this.isReady) {
const scroller = this.$refs.scroller
if (!scroller) {
return {
scale: 0, //
scrollY: 0, //
scrollE: 0, //
offset: 0, //
balance: 0, //
}
}
const scrollerView = this.$refs.scroller.$el;
let wInnerH = scrollerView.clientHeight;
let wScrollY = scrollerView.scrollTop;
let bScrollH = scrollerView.scrollHeight;
this.scrollY = wScrollY;
let clientSize = scroller.getClientSize();
let offset = scroller.getOffset();
let scrollSize = scroller.getScrollSize();
return {
scale: wScrollY / (bScrollH - wInnerH),
scrollY: wScrollY,
scrollE: bScrollH - wInnerH - wScrollY,
scale: offset / (scrollSize - clientSize),
offset: offset,
balance: scrollSize - clientSize - offset,
}
},
@ -899,8 +861,8 @@ export default {
this.operateVisible = false;
this.__onScroll && clearTimeout(this.__onScroll);
this.__onScroll = setTimeout(_ => {
const {scrollE} = this.scrollInfo();
if (scrollE <= 10) {
const {balance} = this.scrollInfo();
if (balance <= 10) {
this.msgNew = 0;
}
}, 100)
@ -945,10 +907,10 @@ export default {
})
},
onOperate(name, value = null) {
onOperate(action, value = null) {
this.operateVisible = false;
this.$nextTick(_ => {
switch (name) {
switch (action) {
case "copy":
if (this.operateHasText) {
this.$copyText(this.operateItem.msg.text.replace(/<[^>]+>/g, "")).then(_ => {
@ -975,19 +937,10 @@ export default {
break;
case "withdraw":
this.$refs[`msg_${this.operateItem.id}`].withdraw()
break;
case "view":
this.$refs[`msg_${this.operateItem.id}`].viewFile()
break;
case "down":
this.$refs[`msg_${this.operateItem.id}`].downFile()
break;
case "emoji":
this.$refs[`msg_${this.operateItem.id}`].setEmoji(value)
Store.set('dialogOperate', {id: this.operateItem.id, action, value});
break;
}
})

View File

@ -646,62 +646,12 @@
}
&.history {
cursor: pointer;
justify-content: center;
font-size: 13px;
padding: 3px 0;
margin: 0 0 16px;
opacity: 0.6;
transition: opacity 0.2s;
&:hover {
opacity: 1;
}
}
&.history-tip {
position: relative;
padding-top: 60px;
.history-text {
font-style: normal;
position: absolute;
top: 10px;
left: 50%;
height: 22px;
line-height: 22px;
padding: 0 48px;
text-align: center;
font-size: 12px;
border-radius: 2px;
background: #f5f5f5;
transform: translateX(-50%);
}
}
&.loading {
padding: 0 0 16px;
justify-content: center;
.common-loading {
margin: 0;
width: 18px;
height: 18px;
}
}
&.nothing {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: $primary-desc-color;
}
&.bottom {
height: 0;
margin: 0;
padding: 0;
}