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-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-3", "vue-virtual-scroll-list": "^2.3.3",
"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

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