优化我的列表、下载、歌单、排行榜列表性能

This commit is contained in:
lyswhut
2021-10-26 14:44:15 +08:00
parent 0d20869337
commit 35ca6db1ee
18 changed files with 646 additions and 335 deletions
@@ -5,7 +5,7 @@
div.icon(:class="$style.icon")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='100%' viewBox='0 0 451.847 451.847' space='preserve')
use(xlink:href='#icon-down')
ul.list.scroll(:class="$style.list" :style="listStyles" ref="dom_list")
ul.selection-list.scroll(:class="$style.list" :style="listStyles" ref="dom_list")
li(v-for="item in list" :class="(itemKey ? item[itemKey] : item) == value ? $style.active : null" @click="handleClick(item)" :tips="itemName ? item[itemName] : item") {{itemName ? item[itemName] : item}}
</template>
+65 -52
View File
@@ -13,7 +13,31 @@ div(:class="$style.songList")
th.nobreak(:style="{ width: rowWidth.r5 }") {{$t('material.song_list.time')}}
th.nobreak(:style="{ width: rowWidth.r6 }") {{$t('material.song_list.action')}}
div(:class="$style.content")
div.scroll(v-show="list.length" :class="$style.tbody" ref="dom_scrollContent")
div(v-if="list.length" :class="$style.content" ref="dom_listContent")
material-virtualized-list(:list="list" key-name="songmid" ref="list" :item-height="37"
containerClass="scroll" contentClass="list" @contextmenu.native.capture="handleContextMenu")
template(#default="{ item, index }")
div.list-item(@click="handleDoubleClick($event, index)" @contextmenu="handleListItemRigthClick($event, index)"
:class="[{ selected: selectedIndex == index }, { active: selectdList.includes(item) }]")
div.list-item-cell.nobreak.center(:style="{ width: rowWidth.r1 }" style="padding-left: 3px; padding-right: 3px;" :class="$style.noSelect" @click.stop) {{index + 1}}
div.list-item-cell.auto(:style="{ width: rowWidth.r2 }" :tips="item.name + ((item._types.ape || item._types.flac || item._types.wav) ? ` - ${$t('material.song_list.lossless')}` : item._types['320k'] ? ` - ${$t('material.song_list.high_quality')}` : '')")
span.select {{item.name}}
span.badge.badge-theme-success(:class="[$style.labelQuality, $style.noSelect]" v-if="item._types.ape || item._types.flac || item._types.wav") {{$t('material.song_list.lossless')}}
span.badge.badge-theme-info(:class="[$style.labelQuality, $style.noSelect]" v-else-if="item._types['320k']") {{$t('material.song_list.high_quality')}}
div.list-item-cell(:style="{ width: rowWidth.r3 }")
span.select {{item.singer}}
div.list-item-cell(:style="{ width: rowWidth.r4 }")
span.select {{item.albumName}}
div.list-item-cell(:style="{ width: rowWidth.r5 }")
span(:class="[$style.time, $style.noSelect]") {{item.interval || '--/--'}}
div.list-item-cell(:style="{ width: rowWidth.r6 }" style="padding-left: 0; padding-right: 0;")
material-list-buttons(:index="index" :class="$style.btns"
:remove-btn="false" @btn-click="handleListBtnClick"
:download-btn="assertApiSupport(item.source)")
template(#footer)
div(:class="$style.pagination")
material-pagination(:count="total" :limit="limit" :page="page" @btn-click="handleTogglePage")
//- div.scroll(v-show="list.length" :class="$style.tbody" ref="dom_scrollContent")
table
tbody(@contextmenu.capture="handleContextMenu" ref="dom_tbody")
tr(v-for='(item, index) in list' :key='item.songmid' @contextmenu="handleListItemRigthClick($event, index)" @click="handleDoubleClick($event, index)")
@@ -175,7 +199,9 @@ export default {
isModDown: false,
},
lastSelectIndex: 0,
selectedIndex: -1,
listMenu: {
rightClickItemIndex: -1,
isShowItemMenu: false,
itemMenuControl: {
play: true,
@@ -233,7 +259,7 @@ export default {
this.handleSelectAllData()
},
handleDoubleClick(event, index) {
if (event.target.classList.contains('select')) return
if (this.listMenu.rightClickItemIndex > -1) return
this.handleSelectData(event, index)
@@ -264,14 +290,8 @@ export default {
}
this.selectdList = this.list.slice(lastSelectIndex, clickIndex + 1)
if (isNeedReverse) this.selectdList.reverse()
let nodes = this.$refs.dom_tbody.childNodes
do {
nodes[lastSelectIndex].classList.add('active')
lastSelectIndex++
} while (lastSelectIndex <= clickIndex)
}
} else {
event.currentTarget.classList.add('active')
this.selectdList.push(this.list[clickIndex])
this.lastSelectIndex = clickIndex
}
@@ -281,10 +301,8 @@ export default {
let index = this.selectdList.indexOf(item)
if (index < 0) {
this.selectdList.push(item)
event.currentTarget.classList.add('active')
} else {
this.selectdList.splice(index, 1)
event.currentTarget.classList.remove('active')
}
} else if (this.selectdList.length) {
this.removeAllSelect()
@@ -293,12 +311,6 @@ export default {
},
removeAllSelect() {
this.selectdList = []
let dom_tbody = this.$refs.dom_tbody
if (!dom_tbody) return
let nodes = dom_tbody.querySelectorAll('.active')
for (const node of nodes) {
if (node.parentNode == dom_tbody) node.classList.remove('active')
}
},
handleListBtnClick(info) {
this.emitEvent('listBtnClick', info)
@@ -306,10 +318,6 @@ export default {
handleSelectAllData() {
this.removeAllSelect()
this.selectdList = [...this.list]
let nodes = this.$refs.dom_tbody.childNodes
for (const node of nodes) {
node.classList.add('active')
}
this.$emit('input', [...this.selectdList])
},
handleTogglePage(page) {
@@ -327,12 +335,12 @@ export default {
handleContextMenu(event) {
if (!event.target.classList.contains('select')) return
event.stopImmediatePropagation()
let classList = this.$refs.dom_scrollContent.classList
let classList = this.$refs.dom_listContent.classList
classList.add(this.$style.copying)
window.requestAnimationFrame(() => {
let str = window.getSelection().toString()
classList.remove(this.$style.copying)
str = str.trim()
str = str.split(/\n\n/).map(s => s.replace(/\n/g, ' ')).join('\n').trim()
if (!str.length) return
clipboardWriteText(str)
})
@@ -344,23 +352,27 @@ export default {
this.listMenu.itemMenuControl.sourceDetail = !!musicSdk[this.list[index].source].getMusicDetailPageUrl
// this.listMenu.itemMenuControl.play =
// this.listMenu.itemMenuControl.playLater =
this.listMenu.itemMenuControl.download =
this.assertApiSupport(this.list[index].source)
let dom_selected = this.$refs.dom_tbody.querySelector('tr.selected')
if (dom_selected) dom_selected.classList.remove('selected')
this.$refs.dom_tbody.querySelectorAll('tr')[index].classList.add('selected')
let dom_td = event.target.closest('td')
this.listMenu.itemMenuControl.download = this.assertApiSupport(this.list[index].source)
let dom_container = event.target.closest('.' + this.$style.songList)
const getOffsetValue = (target, x = 0, y = 0) => {
if (target === dom_container) return { x, y }
if (!target) return { x: 0, y: 0 }
x += target.offsetLeft
y += target.offsetTop
return getOffsetValue(target.offsetParent, x, y)
}
this.listMenu.rightClickItemIndex = index
this.listMenu.menuLocation.x = dom_td.offsetLeft + event.offsetX
this.listMenu.menuLocation.y = dom_td.offsetParent.offsetTop + dom_td.offsetTop + event.offsetY - this.$refs.dom_scrollContent.scrollTop
this.selectedIndex = index
let { x, y } = getOffsetValue(event.target)
this.listMenu.menuLocation.x = x + event.offsetX
this.listMenu.menuLocation.y = y + event.offsetY - this.$refs.list.getScrollTop()
this.hideListsMenu()
this.$nextTick(() => {
this.listMenu.isShowItemMenu = true
})
},
hideListMenu() {
let dom_selected = this.$refs.dom_tbody && this.$refs.dom_tbody.querySelector('tr.selected')
if (dom_selected) dom_selected.classList.remove('selected')
this.selectedIndex = -1
this.listMenu.isShowItemMenu = false
this.listMenu.rightClickItemIndex = -1
},
@@ -406,24 +418,7 @@ export default {
flex: auto;
min-height: 0;
position: relative;
}
.tbody {
height: 100%;
overflow-y: auto;
td {
font-size: 12px;
:global(.badge) {
margin-left: 3px;
}
&:first-child {
// padding-left: 10px;
font-size: 11px;
color: @color-theme_2-font-label;
}
}
:global(.badge) {
opacity: .85;
}
&.copying {
.no-select {
@@ -431,6 +426,24 @@ export default {
}
}
}
:global(.list) {
height: 100%;
overflow-y: auto;
:global(.list-item-cell) {
font-size: 12px !important;
:global(.badge) {
margin-left: 3px;
}
&:first-child {
// padding-left: 10px;
font-size: 11px !important;
color: @color-theme_2-font-label !important;
}
}
:global(.badge) {
opacity: .85;
}
}
.pagination {
text-align: center;
padding: 15px 0;
@@ -456,10 +469,10 @@ export default {
each(@themes, {
:global(#container.@{value}) {
.tbody {
td {
:global(.list) {
:global(.list-item-cell) {
&:first-child {
color: ~'@{color-@{value}-theme_2-font-label}';
color: ~'@{color-@{value}-theme_2-font-label}' !important;
}
}
}
@@ -0,0 +1,241 @@
<template>
<component :is="containerEl" :class="containerClass" ref="dom_scrollContainer" style="height: 100%; overflow: auto; position: relative; display: block;">
<component :is="contentEl" :class="contentClass" :style="contentStyle">
<div v-for="item in views" :key="item.key" :style="item.style">
<slot name="default" v-bind="{ item: item.item, index: item.index }" />
</div>
</component>
<slot name="footer" />
</component>
</template>
<script>
const easeInOutQuad = (t, b, c, d) => {
t /= d / 2
if (t < 1) return (c / 2) * t * t + b
t--
return (-c / 2) * (t * (t - 2) - 1) + b
}
const handleScroll = (element, to, duration = 300, callback = () => {}, onCancel = () => {}) => {
if (!element) return callback()
const start = element.scrollTop || element.scrollY || 0
let cancel = false
if (to > start) {
let maxScrollTop = element.scrollHeight - element.clientHeight
if (to > maxScrollTop) to = maxScrollTop
} else if (to < start) {
if (to < 0) to = 0
} else return callback()
const change = to - start
const increment = 10
if (!change) return callback()
let currentTime = 0
let val
let cancelCallback
const animateScroll = () => {
currentTime += increment
val = parseInt(easeInOutQuad(currentTime, start, change, duration))
if (element.scrollTo) {
element.scrollTo(0, val)
} else {
element.scrollTop = val
}
if (currentTime < duration) {
if (cancel) {
cancelCallback()
onCancel()
return
}
setTimeout(animateScroll, increment)
} else {
callback()
}
}
animateScroll()
return (callback) => {
cancelCallback = callback
cancel = true
}
}
export default {
name: 'VirtualizedList',
props: {
containerEl: {
type: String,
default: 'div',
},
containerClass: {
type: String,
default: 'virtualized-list',
},
contentEl: {
type: String,
default: 'div',
},
contentClass: {
type: String,
default: 'virtualized-list-content',
},
outsideNum: {
type: Number,
default: 10,
},
itemHeight: {
type: Number,
required: true,
},
keyName: {
type: String,
require: true,
},
list: {
type: Array,
require: true,
},
},
data() {
return {
views: [],
isWaitingUpdate: false,
startIndex: -1,
endIndex: -1,
scrollTop: 0,
cachedList: [],
cancelScroll: null,
isScrolling: false,
scrollToValue: 0,
}
},
computed: {
contentStyle() {
return {
display: 'block',
height: this.list.length * this.itemHeight + 'px',
}
},
},
watch: {
itemHeight() {
this.updateView()
},
list() {
this.cachedList = Array(this.list.length)
this.startIndex = -1
this.endIndex = -1
this.updateView()
},
},
mounted() {
this.$refs.dom_scrollContainer.addEventListener('scroll', this.onScroll, false)
this.cachedList = Array(this.list.length)
this.startIndex = -1
this.endIndex = -1
this.updateView()
},
beforeDestroy() {
if (this.cancelScroll) this.cancelScroll()
},
methods: {
onScroll(event) {
if (this.isWaitingUpdate) return
this.isWaitingUpdate = true
window.requestAnimationFrame(() => {
this.updateView()
this.isWaitingUpdate = false
})
this.$emit('scroll', event)
},
createList(startIndex, endIndex) {
const cache = this.cachedList.slice(startIndex, endIndex)
const list = this.list.slice(startIndex, endIndex).map((item, i) => {
if (cache[i]) return cache[i]
const top = (startIndex + i) * this.itemHeight
const index = startIndex + i
return this.cachedList[index] = {
item,
top,
style: { position: 'absolute', left: 0, right: 0, top: top + 'px', height: this.itemHeight + 'px' },
index,
key: item[this.keyName],
}
})
return list
},
updateView() {
const currentScrollTop = this.$refs.dom_scrollContainer.scrollTop
const currentStartIndex = Math.floor(currentScrollTop / this.itemHeight)
const currentEndIndex = currentStartIndex + Math.ceil(this.$refs.dom_scrollContainer.clientHeight / this.itemHeight)
const continuous = currentStartIndex <= this.endIndex && currentEndIndex >= this.startIndex
const currentStartRenderIndex = Math.max(Math.floor(currentScrollTop / this.itemHeight) - this.outsideNum, 0)
const currentEndRenderIndex = currentStartIndex + Math.ceil(this.$refs.dom_scrollContainer.clientHeight / this.itemHeight) + this.outsideNum
// console.log(continuous)
// debugger
if (continuous) {
if (Math.abs(currentScrollTop - this.scrollTop) < this.itemHeight * 5) return
// console.log('update')
if (currentScrollTop > this.scrollTop) { // scroll down
// console.log('scroll down')
const list = this.createList(currentStartRenderIndex, currentEndRenderIndex)
this.views.push(...list.slice(list.indexOf(this.views[this.views.length - 1]) + 1))
// if (this.views.length > 100) {
this.$nextTick(() => {
this.views.splice(0, this.views.indexOf(list[0]))
})
// }
} else if (currentScrollTop < this.scrollTop) { // scroll up
// console.log('scroll up')
this.views = this.createList(currentStartRenderIndex, currentEndRenderIndex)
} else return
} else {
this.views = this.createList(currentStartRenderIndex, currentEndRenderIndex)
}
this.startIndex = currentStartIndex
this.endIndex = currentEndIndex
this.scrollTop = currentScrollTop
},
scrollTo(scrollTop, animate = false) {
return new Promise(resolve => {
if (this.cancelScroll) {
this.cancelScroll(resolve)
} else {
resolve()
}
}).then(() => {
return new Promise((resolve, reject) => {
if (animate) {
this.isScrolling = true
this.scrollToValue = scrollTop
this.cancelScroll = handleScroll(this.$refs.dom_scrollContainer, scrollTop, 300, () => {
this.cancelScroll = null
this.isScrolling = false
resolve()
}, () => {
this.cancelScroll = null
this.isScrolling = false
reject('canceled')
})
} else {
this.$refs.dom_scrollContainer.scrollTop = scrollTop
}
})
})
},
scrollToIndex(index, offset = 0, animate = false, callback = () => {}) {
return this.scrollTo(Math.max(index * this.itemHeight + offset, 0), animate, callback)
},
getScrollTop() {
return this.isScrolling ? this.scrollToValue : this.$refs.dom_scrollContainer.scrollTop
},
},
}
</script>