Browse Source

抽离封装虚拟折叠组件说明

master
xh 1 week ago
parent
commit
29e151cfe2
  1. 282
      web/src/components/VirtualCollapsePanel/index.vue
  2. 15
      web/src/components/VirtualCollapsePanel/readme.md

282
web/src/components/VirtualCollapsePanel/index.vue

@ -1,229 +1,229 @@
<!-- 虚拟折叠面板 --> <!-- 虚拟折叠面板 -->
<template> <template>
<DynamicScroller
ref="scrollbarRef"
class="scroller"
:items="list"
:min-item-size="props.minItemSize"
:key-field="props.keyField"
v-slot="{ item, active }"
style="height: 100%"
@scroll="onScroll"
>
<DynamicScrollerItem :item="item" :active="active" :size-dependencies="[item.__expanded]">
<div class="collapse-header" @click="toggleExpand(item)">
<div class="collapse-header-left">
<slot name="header" :item="item">{{ item[props.nameField] }}</slot>
</div>
<DynamicScroller
ref="scrollbarRef"
class="scroller"
:items="list"
:min-item-size="props.minItemSize"
:key-field="props.keyField"
v-slot="{ item, active }"
style="height: 100%"
@scroll="onScroll"
>
<DynamicScrollerItem :item="item" :active="active" :size-dependencies="[item.__expanded]">
<div class="collapse-header" @click="toggleExpand(item)">
<div class="collapse-header-left">
<slot name="header" :item="item">{{ item[props.nameField] }}</slot>
</div>
<div class="collapse-header-right" v-if="props.showArrowRight">
<el-icon
class="arrow-right"
:class="{ 'rotate-icon': item.__expanded }"
:size="12"
:color="'#c1c1c1'"
>
<ArrowRight />
</el-icon>
</div>
</div>
<div class="collapse-header-right" v-if="props.showArrowRight">
<el-icon
class="arrow-right"
:class="{ 'rotate-icon': item.__expanded }"
:size="12"
:color="'#c1c1c1'"
>
<ArrowRight />
</el-icon>
</div>
</div>
<Transition name="fade" mode="out-in">
<div class="collapse-content" v-show="item.__expanded">
<slot name="content" :item="item"></slot>
</div>
</Transition>
</DynamicScrollerItem>
</DynamicScroller>
<Transition name="fade" mode="out-in">
<div class="collapse-content" v-show="item.__expanded">
<slot name="content" :item="item"></slot>
</div>
</Transition>
</DynamicScrollerItem>
</DynamicScroller>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onBeforeUnmount, onDeactivated } from 'vue'
import { ref, computed, onBeforeUnmount, onDeactivated, useTemplateRef } from 'vue'
import type { PropType } from 'vue'
import { ArrowRight } from '@element-plus/icons-vue' import { ArrowRight } from '@element-plus/icons-vue'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css' import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
import { DynamicScroller, DynamicScrollerItem } from 'vue-virtual-scroller' import { DynamicScroller, DynamicScrollerItem } from 'vue-virtual-scroller'
const emit = defineEmits(['scroll']) const emit = defineEmits(['scroll'])
const props = defineProps({ const props = defineProps({
data: {
type: Array as PropType<Record<string, any>[]>,
default: () => []
},
keyField: {
type: String,
default: 'id'
},
nameField: {
type: String,
default: 'name'
},
/** 每个项的最小高度 */
minItemSize: {
type: Number,
default: 48
},
showArrowRight: {
type: Boolean,
default: true
}
data: {
type: Array as PropType<Record<string, any>[]>,
default: () => []
},
keyField: {
type: String,
default: 'id'
},
nameField: {
type: String,
default: 'name'
},
/** 每个项的最小高度 */
minItemSize: {
type: Number,
default: 48
},
showArrowRight: {
type: Boolean,
default: true
}
}) })
const itemHeight = computed(() => props.minItemSize + 'px') const itemHeight = computed(() => props.minItemSize + 'px')
const activeItem = ref<Record<string, any> | null>(null) const activeItem = ref<Record<string, any> | null>(null)
const list = computed(() => { const list = computed(() => {
console.time('list')
const activeItemId = activeItem.value ? activeItem.value[props.keyField] : null
const newList = props.data.map((listItem: Record<string, any>) => {
return {
...listItem,
__expanded: activeItemId && listItem[props.keyField] === activeItemId ? true : false
}
})
console.timeEnd('list')
return newList
console.time('list')
const activeItemId = activeItem.value ? activeItem.value[props.keyField] : null
const newList = props.data.map((listItem: Record<string, any>) => {
return {
...listItem,
__expanded: activeItemId && listItem[props.keyField] === activeItemId ? true : false
}
})
console.timeEnd('list')
return newList
}) })
const scrollbarRef = useTemplateRef('scrollbarRef') const scrollbarRef = useTemplateRef('scrollbarRef')
const scrollbarScrollTop = ref(0) const scrollbarScrollTop = ref(0)
function onScroll(e) { function onScroll(e) {
scrollbarScrollTop.value = e.target.scrollTop
emit('scroll', e)
scrollbarScrollTop.value = e.target.scrollTop
emit('scroll', e)
} }
/** /**
* 切换折叠面板展开状态 * 切换折叠面板展开状态
* @param item 要切换展开状态的项 * @param item 要切换展开状态的项
*/ */
function toggleExpand(item) { function toggleExpand(item) {
activeItem.value = item.__expanded ? null : item
activeItem.value = item.__expanded ? null : item
} }
/** /**
* 滚动到指定项 * 滚动到指定项
* @param item 要滚动到的项 * @param item 要滚动到的项
*/ */
function scrollToItem(item) { function scrollToItem(item) {
if (!item) return
activeItem.value = item
var findIndex = list.value.findIndex((i) => i[props.keyField] === item[props.keyField])
if (findIndex === -1) {
return
}
var top = props.minItemSize * findIndex
//
cancelAnimationFrame(AnimationId.value as number)
scrollTo(scrollbarScrollTop.value, top, scrollbarScrollTop.value)
if (!item) return
activeItem.value = item
const findIndex = list.value.findIndex((i) => i[props.keyField] === item[props.keyField])
if (findIndex === -1) {
return
}
const top = props.minItemSize * findIndex
//
cancelAnimationFrame(AnimationId.value as number)
scrollTo(scrollbarScrollTop.value, top, scrollbarScrollTop.value)
} }
/** /**
* 滚动到指定索引项 * 滚动到指定索引项
* @param index 要滚动到的索引项 * @param index 要滚动到的索引项
*/ */
function scrollToIndex(index) { function scrollToIndex(index) {
if (index < 0 || index >= list.value.length) return
if (index < 0 || index >= list.value.length) return
activeItem.value = list.value[index]
activeItem.value = list.value[index]
var top = props.minItemSize * index
//
cancelAnimationFrame(AnimationId.value as number)
scrollTo(scrollbarScrollTop.value, top, scrollbarScrollTop.value)
const top = props.minItemSize * index
//
cancelAnimationFrame(AnimationId.value as number)
scrollTo(scrollbarScrollTop.value, top, scrollbarScrollTop.value)
} }
/** /**
* 滚动到指定位置 * 滚动到指定位置
* @param position 要滚动到的位置 * @param position 要滚动到的位置
*/ */
function scrollToPosition(position: number) { function scrollToPosition(position: number) {
if (position < 0) return
//
cancelAnimationFrame(AnimationId.value as number)
scrollTo(scrollbarScrollTop.value, position, scrollbarScrollTop.value)
if (position < 0) return
//
cancelAnimationFrame(AnimationId.value as number)
scrollTo(scrollbarScrollTop.value, position, scrollbarScrollTop.value)
} }
// //
const AnimationId = ref<number>() const AnimationId = ref<number>()
function scrollTo(from: number, to: number, current: number) { function scrollTo(from: number, to: number, current: number) {
if (scrollbarScrollTop.value === to) {
return
}
const speed = (to - from) / 30
if (speed < 0) {
if (current <= to) {
return
if (scrollbarScrollTop.value === to) {
return
} }
} else if (speed > 0) {
if (current >= to) {
return
const speed = (to - from) / 30
if (speed < 0) {
if (current <= to) {
return
}
} else if (speed > 0) {
if (current >= to) {
return
}
} }
}
current = current + speed
current = current + speed
// DynamicScrollerRecycleScrollerRecycleScroller
scrollbarRef.value?.$refs?.scroller?.scrollToPosition(current)
AnimationId.value = requestAnimationFrame(() => {
scrollTo(from, to, current)
})
// DynamicScrollerRecycleScrollerRecycleScroller
scrollbarRef.value?.$refs?.scroller?.scrollToPosition(current)
AnimationId.value = requestAnimationFrame(() => {
scrollTo(from, to, current)
})
} }
onDeactivated(() => { onDeactivated(() => {
//
cancelAnimationFrame(AnimationId.value as number)
//
cancelAnimationFrame(AnimationId.value as number)
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {
//
cancelAnimationFrame(AnimationId.value as number)
//
cancelAnimationFrame(AnimationId.value as number)
}) })
defineExpose({ defineExpose({
toggleExpand,
scrollToItem,
scrollToIndex,
scrollToPosition
toggleExpand,
scrollToItem,
scrollToIndex,
scrollToPosition
}) })
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.collapse-header { .collapse-header {
display: flex;
flex-direction: row;
align-items: center;
width: 100%;
overflow: hidden;
display: flex;
flex-direction: row;
align-items: center;
width: 100%;
overflow: hidden;
height: v-bind(itemHeight);
padding: 0 4px;
cursor: pointer;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
box-sizing: border-box;
.collapse-header-left {
font-size: 12px;
color: #303133;
flex: 1;
}
height: v-bind(itemHeight);
padding: 0 4px;
cursor: pointer;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
box-sizing: border-box;
.collapse-header-left {
font-size: 12px;
color: #303133;
flex: 1;
}
} }
.collapse-content { .collapse-content {
transform-origin: top;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
transform-origin: top;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
} }
// collapse-content // collapse-content
.fade-enter-active, .fade-enter-active,
.fade-leave-active { .fade-leave-active {
transition: all 0.2s ease;
transition: all 0.2s ease;
} }
.fade-enter-from, .fade-enter-from,
.fade-leave-to { .fade-leave-to {
opacity: 0;
transform: scaleY(0);
opacity: 0;
transform: scaleY(0);
} }
.arrow-right { .arrow-right {
transition: all 0.2s ease;
transition: all 0.2s ease;
} }
.rotate-icon { .rotate-icon {
transform: rotate(90deg);
transform: rotate(90deg);
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.1) !important;
border-radius: 4px;
background: rgba(0, 0, 0, 0.1) !important;
border-radius: 4px;
} }
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 8px;
background-color: #f5f5f5;
width: 8px;
background-color: #f5f5f5;
} }
</style> </style>

15
web/src/components/VirtualCollapsePanel/readme.md

@ -0,0 +1,15 @@
# 使用示例
```vue
<VirtualCollapsePanel
ref="scrollbarRef"
:data="filterMarkers"
key-field="id"
name-field="name"
:min-item-size="40"
>
<template #header="{ item }">
</template>
<template #content="{ item }"> </template>
</VirtualCollapsePanel>
```
Loading…
Cancel
Save