mirror of
https://github.com/koel/koel
synced 2024-11-10 06:34:14 +00:00
feat: use home-grown virtual scroller
This commit is contained in:
parent
98384b56c6
commit
9cf7a09cde
5 changed files with 78 additions and 39 deletions
|
@ -32,7 +32,6 @@
|
||||||
"slugify": "^1.0.2",
|
"slugify": "^1.0.2",
|
||||||
"vue": "^3.2.32",
|
"vue": "^3.2.32",
|
||||||
"vue-global-events": "^2.1.1",
|
"vue-global-events": "^2.1.1",
|
||||||
"vue-virtual-scroller": "^2.0.0-alpha.1",
|
|
||||||
"vuequery": "~2.1.1",
|
"vuequery": "~2.1.1",
|
||||||
"youtube-player": "^3.0.4"
|
"youtube-player": "^3.0.4"
|
||||||
},
|
},
|
||||||
|
|
|
@ -46,15 +46,9 @@
|
||||||
<span class="play"></span>
|
<span class="play"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<RecycleScroller
|
<VirtualScroller v-slot="{ item }" :item-height="35" :items="songProxies">
|
||||||
class="scroller"
|
<SongListItem :item="item" :columns="mergedConfig.columns" :key="item.song.id"/>
|
||||||
:items="songProxies"
|
</VirtualScroller>
|
||||||
:item-size="35"
|
|
||||||
key-field="id"
|
|
||||||
v-slot="{ item }"
|
|
||||||
>
|
|
||||||
<SongListItem :item="item" :columns="mergedConfig.columns"/>
|
|
||||||
</RecycleScroller>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -66,6 +60,7 @@ export default {
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import isMobile from 'ismobilejs'
|
import isMobile from 'ismobilejs'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
computed,
|
computed,
|
||||||
defineAsyncComponent,
|
defineAsyncComponent,
|
||||||
|
@ -76,8 +71,7 @@ import {
|
||||||
toRefs,
|
toRefs,
|
||||||
watch
|
watch
|
||||||
} from 'vue'
|
} from 'vue'
|
||||||
import { RecycleScroller } from 'vue-virtual-scroller'
|
|
||||||
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
|
|
||||||
import { $, eventBus, orderBy, startDragging, arrayify } from '@/utils'
|
import { $, eventBus, orderBy, startDragging, arrayify } from '@/utils'
|
||||||
import { queueStore } from '@/stores'
|
import { queueStore } from '@/stores'
|
||||||
|
|
||||||
|
@ -96,6 +90,7 @@ interface SongRow {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const VirtualScroller = defineAsyncComponent(() => import('@/components/ui/VirtualScroller.vue'))
|
||||||
const SongListItem = defineAsyncComponent(() => import('@/components/song/SongListItem.vue'))
|
const SongListItem = defineAsyncComponent(() => import('@/components/song/SongListItem.vue'))
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
|
@ -105,6 +100,11 @@ const props = withDefaults(
|
||||||
|
|
||||||
const { items, type, config } = toRefs(props)
|
const { items, type, config } = toRefs(props)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
generateSongProxies()
|
||||||
|
render()
|
||||||
|
})
|
||||||
|
|
||||||
const lastSelectedRow = ref<SongRow>()
|
const lastSelectedRow = ref<SongRow>()
|
||||||
const sortFields = ref<SortField[]>([])
|
const sortFields = ref<SortField[]>([])
|
||||||
const sortOrder = ref<SortOrder>('None')
|
const sortOrder = ref<SortOrder>('None')
|
||||||
|
@ -132,7 +132,6 @@ const generateSongProxies = () => {
|
||||||
const selectedSongIds = selectedSongs.value.map(song => song.id)
|
const selectedSongIds = selectedSongs.value.map(song => song.id)
|
||||||
|
|
||||||
return items.value.map(song => ({
|
return items.value.map(song => ({
|
||||||
id: song.id,
|
|
||||||
song,
|
song,
|
||||||
selected: selectedSongIds.includes(song.id)
|
selected: selectedSongIds.includes(song.id)
|
||||||
}))
|
}))
|
||||||
|
@ -306,7 +305,7 @@ const openContextMenu = async (rowVm: SongRow, event: MouseEvent) => {
|
||||||
|
|
||||||
const getAllSongsWithSort = () => songProxies.value.map(proxy => proxy.song)
|
const getAllSongsWithSort = () => songProxies.value.map(proxy => proxy.song)
|
||||||
|
|
||||||
onMounted(() => items.value && render())
|
// onMounted(() => items.value && render())
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
rowClicked,
|
rowClicked,
|
||||||
|
@ -324,6 +323,8 @@ defineExpose({
|
||||||
.song-list-wrap {
|
.song-list-wrap {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
.song-list-header {
|
.song-list-header {
|
||||||
background: var(--color-bg-secondary);
|
background: var(--color-bg-secondary);
|
||||||
|
|
64
resources/assets/js/components/ui/VirtualScroller.vue
Normal file
64
resources/assets/js/components/ui/VirtualScroller.vue
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
<template>
|
||||||
|
<div ref="scroller" class="virtual-scroller" @scroll="onScroll">
|
||||||
|
<div :style="{ height: `${totalHeight}px` }">
|
||||||
|
<div :style="{ transform: `translateY(${offsetY}px)`}">
|
||||||
|
<template v-for="item in renderedItems">
|
||||||
|
<slot :item="item"></slot>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, onMounted, ref, toRefs } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps<{ items: any[], itemHeight: number }>()
|
||||||
|
const { items, itemHeight } = toRefs(props)
|
||||||
|
|
||||||
|
const scroller = ref<HTMLElement>()
|
||||||
|
const scrollerHeight = ref(0)
|
||||||
|
const renderAhead = 5
|
||||||
|
const scrollTop = ref(0)
|
||||||
|
|
||||||
|
const totalHeight = computed(() => items.value.length * itemHeight.value)
|
||||||
|
|
||||||
|
const startPosition = computed(() => {
|
||||||
|
const position = Math.floor(scrollTop.value / itemHeight.value) - renderAhead
|
||||||
|
return Math.max(0, position)
|
||||||
|
})
|
||||||
|
|
||||||
|
const offsetY = computed(() => startPosition.value * itemHeight.value)
|
||||||
|
|
||||||
|
const renderedItems = computed(() => {
|
||||||
|
let count = Math.ceil(scrollerHeight.value / itemHeight.value) + 2 * renderAhead
|
||||||
|
count = Math.min(items.value.length - startPosition.value, count)
|
||||||
|
return items.value.slice(startPosition.value, startPosition.value + count)
|
||||||
|
})
|
||||||
|
|
||||||
|
const onScroll = e => requestAnimationFrame(() => scrollTop.value = (e.target as HTMLElement).scrollTop)
|
||||||
|
|
||||||
|
const observer = new ResizeObserver(entries => entries.forEach(el => scrollerHeight.value = el.contentRect.height))
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
observer.observe(scroller.value!)
|
||||||
|
scrollerHeight.value = scroller.value!.offsetHeight
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.virtual-scroller {
|
||||||
|
overflow: auto;
|
||||||
|
will-change: transform;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
overflow: hidden;
|
||||||
|
will-change: transform;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
1
resources/assets/js/types.d.ts
vendored
1
resources/assets/js/types.d.ts
vendored
|
@ -336,7 +336,6 @@ declare module 'koel/types/ui' {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SongProxy {
|
interface SongProxy {
|
||||||
id: string,
|
|
||||||
song: Song
|
song: Song
|
||||||
selected: boolean
|
selected: boolean
|
||||||
}
|
}
|
||||||
|
|
24
yarn.lock
24
yarn.lock
|
@ -6555,11 +6555,6 @@ minimist@^1.2.5:
|
||||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
|
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
|
||||||
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
|
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
|
||||||
|
|
||||||
mitt@^2.1.0:
|
|
||||||
version "2.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/mitt/-/mitt-2.1.0.tgz#f740577c23176c6205b121b2973514eade1b2230"
|
|
||||||
integrity sha512-ILj2TpLiysu2wkBbWjAmww7TkZb65aiQO+DkVdUTBpBXq+MHYiETENkKFMtsJZX1Lf4pe4QOrTSjIfUwN5lRdg==
|
|
||||||
|
|
||||||
mkdirp@^0.5.5, mkdirp@~0.5.1:
|
mkdirp@^0.5.5, mkdirp@~0.5.1:
|
||||||
version "0.5.6"
|
version "0.5.6"
|
||||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6"
|
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6"
|
||||||
|
@ -9607,16 +9602,6 @@ vue-loader@^16.2.0:
|
||||||
hash-sum "^2.0.0"
|
hash-sum "^2.0.0"
|
||||||
loader-utils "^2.0.0"
|
loader-utils "^2.0.0"
|
||||||
|
|
||||||
vue-observe-visibility@^2.0.0-alpha.1:
|
|
||||||
version "2.0.0-alpha.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/vue-observe-visibility/-/vue-observe-visibility-2.0.0-alpha.1.tgz#1e4eda7b12562161d58984b7e0dea676d83bdb13"
|
|
||||||
integrity sha512-flFbp/gs9pZniXR6fans8smv1kDScJ8RS7rEpMjhVabiKeq7Qz3D9+eGsypncjfIyyU84saU88XZ0zjbD6Gq/g==
|
|
||||||
|
|
||||||
vue-resize@^2.0.0-alpha.1:
|
|
||||||
version "2.0.0-alpha.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/vue-resize/-/vue-resize-2.0.0-alpha.1.tgz#43eeb79e74febe932b9b20c5c57e0ebc14e2df3a"
|
|
||||||
integrity sha512-7+iqOueLU7uc9NrMfrzbG8hwMqchfVfSzpVlCMeJQe4pyibqyoifDNbKTZvwxZKDvGkB+PdFeKvnGZMoEb8esg==
|
|
||||||
|
|
||||||
vue-style-loader@^4.1.3:
|
vue-style-loader@^4.1.3:
|
||||||
version "4.1.3"
|
version "4.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/vue-style-loader/-/vue-style-loader-4.1.3.tgz#6d55863a51fa757ab24e89d9371465072aa7bc35"
|
resolved "https://registry.yarnpkg.com/vue-style-loader/-/vue-style-loader-4.1.3.tgz#6d55863a51fa757ab24e89d9371465072aa7bc35"
|
||||||
|
@ -9640,15 +9625,6 @@ vue-test-helpers@^2.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
np "^2.18.3"
|
np "^2.18.3"
|
||||||
|
|
||||||
vue-virtual-scroller@^2.0.0-alpha.1:
|
|
||||||
version "2.0.0-alpha.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/vue-virtual-scroller/-/vue-virtual-scroller-2.0.0-alpha.1.tgz#5b5410105b8e60ca57bbd5f2faf5ad1d8108d046"
|
|
||||||
integrity sha512-Mn5w3Qe06t7c3Imm2RHD43RACab1CCWplpdgzq+/FWJcpQtcGKd5vDep8i+nIwFtzFLsWAqEK0RzM7KrfAcBng==
|
|
||||||
dependencies:
|
|
||||||
mitt "^2.1.0"
|
|
||||||
vue-observe-visibility "^2.0.0-alpha.1"
|
|
||||||
vue-resize "^2.0.0-alpha.1"
|
|
||||||
|
|
||||||
vue@^3.2.32:
|
vue@^3.2.32:
|
||||||
version "3.2.32"
|
version "3.2.32"
|
||||||
resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.32.tgz#a09840e237384c673f421ff7280c4469714f2ac0"
|
resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.32.tgz#a09840e237384c673f421ff7280c4469714f2ac0"
|
||||||
|
|
Loading…
Reference in a new issue