Browse Source

抽离封装虚拟折叠组件

master
xh 1 week ago
parent
commit
147d5ffa63
  1. 229
      web/src/components/VirtualCollapsePanel/index.vue
  2. 224
      web/src/views/HandDevice/Home/index.vue

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

@ -0,0 +1,229 @@
<!-- 虚拟折叠面板 -->
<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>
<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>
</template>
<script setup lang="ts">
import { ref, onBeforeUnmount, onDeactivated } from 'vue'
import { ArrowRight } from '@element-plus/icons-vue'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
import { DynamicScroller, DynamicScrollerItem } from 'vue-virtual-scroller'
const emit = defineEmits(['scroll'])
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
}
})
const itemHeight = computed(() => props.minItemSize + 'px')
const activeItem = ref<Record<string, any> | null>(null)
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
})
const scrollbarRef = useTemplateRef('scrollbarRef')
const scrollbarScrollTop = ref(0)
function onScroll(e) {
scrollbarScrollTop.value = e.target.scrollTop
emit('scroll', e)
}
/**
* 切换折叠面板展开状态
* @param item 要切换展开状态的项
*/
function toggleExpand(item) {
activeItem.value = item.__expanded ? null : item
}
/**
* 滚动到指定项
* @param 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)
}
/**
* 滚动到指定索引项
* @param index 要滚动到的索引项
*/
function scrollToIndex(index) {
if (index < 0 || index >= list.value.length) return
activeItem.value = list.value[index]
var top = props.minItemSize * index
//
cancelAnimationFrame(AnimationId.value as number)
scrollTo(scrollbarScrollTop.value, top, scrollbarScrollTop.value)
}
/**
* 滚动到指定位置
* @param position 要滚动到的位置
*/
function scrollToPosition(position: number) {
if (position < 0) return
//
cancelAnimationFrame(AnimationId.value as number)
scrollTo(scrollbarScrollTop.value, position, scrollbarScrollTop.value)
}
//
const AnimationId = ref<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
}
} else if (speed > 0) {
if (current >= to) {
return
}
}
current = current + speed
// DynamicScrollerRecycleScrollerRecycleScroller
scrollbarRef.value?.$refs?.scroller?.scrollToPosition(current)
AnimationId.value = requestAnimationFrame(() => {
scrollTo(from, to, current)
})
}
onDeactivated(() => {
//
cancelAnimationFrame(AnimationId.value as number)
})
onBeforeUnmount(() => {
//
cancelAnimationFrame(AnimationId.value as number)
})
defineExpose({
toggleExpand,
scrollToItem,
scrollToIndex,
scrollToPosition
})
</script>
<style lang="scss" scoped>
.collapse-header {
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;
}
}
.collapse-content {
transform-origin: top;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}
// collapse-content
.fade-enter-active,
.fade-leave-active {
transition: all 0.2s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
transform: scaleY(0);
}
.arrow-right {
transition: all 0.2s ease;
}
.rotate-icon {
transform: rotate(90deg);
}
::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.1) !important;
border-radius: 4px;
}
::-webkit-scrollbar {
width: 8px;
background-color: #f5f5f5;
}
</style>

224
web/src/views/HandDevice/Home/index.vue

@ -20,45 +20,26 @@
</div>
<div class="markerList">
<div style="height: 100%">
<!-- <RecycleScroller
<VirtualCollapsePanel
ref="scrollbarRef"
style="height: 100%"
:items="filterMarkers"
:item-size="48"
:data="filterMarkers"
key-field="id"
v-slot="{ item }"
@scroll="onScroll"
name-field="name"
:min-item-size="40"
>
<div class="marker-item" :data-id="item.id">
<div class="flex-1 text-left font-400 text-13px"> {{ item.name }}</div>
<div class="text-gray-500 font-400 text-12px pr-1" :style="{ color: item.statusColor }">
{{ item.statusLabel }}
</div>
</div>
</RecycleScroller> -->
<DynamicScroller
ref="scrollbarRef"
class="scroller"
:items="filterMarkers"
:min-item-size="48"
key-field="id"
v-slot="{ item, active }"
style="height: 100%"
@scroll="onScroll"
>
<DynamicScrollerItem :item="item" :active="active" :size-dependencies="[item.expanded]">
<div class="marker-panel">
<div class="marker-item" :data-id="item.id" @click="togglePanel(item)">
<div class="flex-1 text-left font-400 text-13px"> {{ item.name }}</div>
<template #header="{ item }">
<div class="marker-item">
<div class="flex-1 text-13px"> {{ item.name }}</div>
<div
class="text-gray-500 font-400 text-12px pr-1"
class="text-12px pr-1"
:style="{ color: item.statusColor }"
>
{{ item.statusLabel }}
</div>
</div>
<div v-show="item.expanded" class="markerList-content">
</template>
<template #content="{ item }">
<div class="markerList-content">
<div><span>SN</span>{{ item.sn }}</div>
<div><span>类型</span>{{ item.gasChemical }}</div>
@ -103,94 +84,18 @@
>
</div>
</div>
</div>
</DynamicScrollerItem>
</DynamicScroller>
</div>
</div>
<!--marker列表 -->
<!--
<el-scrollbar height="100%" ref="scrollbarRef" @scroll="onScroll">
<el-collapse accordion v-model="activeId">
<el-collapse-item :name="item.id" v-for="item in filterMarkers2" :key="item.id">
<template #title>
<div class="flex flex-row w-100%">
<div class="flex-1 text-left font-500">
{{ item.name }}
</div>
<div class="text-gray-500 font-400 text-12px" :style="{ color: item.statusColor }">
{{ item.statusLabel }}
</div>
</div>
</template>
<div class="markerList-content">
<div><span>SN</span>{{ item.sn }}</div>
<div><span>类型</span>{{ item.gasChemical }}</div>
<div
><span>气体状态</span
>{{ getLabelWithTypeValue('gasStatus', item.gasStatus) }}</div
>
<div
><span>围栏状态</span
>{{ getLabelWithTypeValue('fenceStatus', item.fenceStatus) }}</div
>
<div
><span>电池状态</span
>{{ getLabelWithTypeValue('batteryStatus', item.batteryStatus) }}</div
>
<div><span>电量</span>{{ item.battery }}</div>
<div><span>数值</span>{{ item.value }} {{ item.unit }}</div>
<div><span>时间</span>{{ item.timeStr }}</div>
<div style="margin-top: 10px">
<el-button
plain
size="small"
v-if="item.latitude && item.longitude"
@click="setCenter(item)"
>定位</el-button
>
<el-button
v-hasPermi="['gas:hand-td:HistoricalSn']"
plain
size="small"
@click="onClickTrajectory(item)"
>轨迹</el-button
>
<el-button
v-hasPermi="['gas:hand-td:HistoricalSn']"
plain
size="small"
@click="onClickHistoricalCurve(item)"
>历史曲线</el-button
>
</VirtualCollapsePanel>
</div>
</div>
</el-collapse-item>
</el-collapse>
</el-scrollbar>
-->
<el-popover
ref="popoverRef"
:virtual-ref="virtualRef"
trigger="click"
title="With title"
virtual-triggering
>
<span> Some content </span>
</el-popover>
<HistoricalCurve ref="historicalCurveRef" />
</div>
</template>
<script lang="ts" setup>
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
import { RecycleScroller, DynamicScroller, DynamicScrollerItem } from 'vue-virtual-scroller'
import OpenLayerMap from './components/OpenLayerMap.vue'
import TopPanel from './components/TopPanel.vue'
import HistoricalCurve from './components/HistoricalCurve.vue'
import VirtualCollapsePanel from '@/components/VirtualCollapsePanel/index.vue'
import { getLastDetectorData } from '@/api/gas'
import type { HandDetectorData } from '@/api/gas/handdetector'
@ -264,7 +169,7 @@ const filterMarkers = computed(() => {
// const filterMarkers2 = computed(() => {
// var arr: MarkerData[] = []
// for (let i = 0; i < 20000; i++) {
// for (let i = 0; i < 50000; i++) {
// arr.push({
// id: i + 1,
// sn: '867989072728120',
@ -332,8 +237,7 @@ const getMarkers = async () => {
statusStr: statusStr, //
statusColor: getStatusColor(statusStr), //
statusLabel: getStatusLabel(statusStr), //
statusPriority: getStatusPriority(statusStr), //,
expanded: activeId.value === i.id
statusPriority: getStatusPriority(statusStr) //,
}
})
.sort((a, b) => a.statusPriority - b.statusPriority)
@ -490,9 +394,8 @@ function onClickTrajectory(item: MarkerData) {
trajectoryTimeRange.value = [dayjs().subtract(1, 'hour').valueOf(), dayjs().valueOf()]
showTrajectory(item)
}
const scrollbarRef = ref()
const virtualRef = ref<HTMLDivElement>()
const activeId = ref<number>()
const scrollbarRef = useTemplateRef<InstanceType<typeof VirtualCollapsePanel>>('scrollbarRef')
//
function onClickMarker(markerItem: MarkerData) {
console.log('onClickMarker', markerItem)
@ -501,71 +404,7 @@ function onClickMarker(markerItem: MarkerData) {
if (findIndex === -1) {
return
}
showPannel(markerItem, findIndex)
}
const scrollbarScrollTop = ref(0)
function onScroll(e) {
// console.log('onScroll', e)
scrollbarScrollTop.value = e.target.scrollTop
}
//
const AnimationId = ref<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
}
} else if (speed > 0) {
if (current >= to) {
return
}
}
current = current + speed
console.log('scrollbarRef.value', scrollbarRef.value)
// DynamicScrollerRecycleScrollerRecycleScroller
scrollbarRef.value?.$refs?.scroller?.scrollToPosition(current)
AnimationId.value = requestAnimationFrame(() => {
scrollTo(from, to, current)
})
}
const showPannel = (item, index) => {
if (activeId.value && activeId.value !== item.id) {
let last = markers.value.find((lastItem) => lastItem.id === activeId.value)
if (last) {
last.expanded = false
}
}
item.expanded = true
activeId.value = item.id
let top = index * 48
// scrollbarRef.value?.scrollToItem(index)
//
cancelAnimationFrame(AnimationId.value as number)
scrollTo(scrollbarScrollTop.value, top, scrollbarScrollTop.value)
}
const togglePanel = (item) => {
if (activeId.value && activeId.value !== item.id) {
let last = markers.value.find((lastItem) => lastItem.id === activeId.value)
if (last) {
last.expanded = false
}
}
if (item.expanded) {
activeId.value = undefined
item.expanded = false
} else {
activeId.value = item.id
item.expanded = true
}
scrollbarRef.value?.scrollToIndex(findIndex)
}
onMounted(() => {
@ -611,23 +450,18 @@ onUnmounted(() => {
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
padding: 5px 0 10px 10px;
margin-left: 10px;
.marker-panel {
box-sizing: border-box;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}
.marker-item {
display: flex;
flex-direction: row;
height: 48px;
line-height: 48px;
font-family: var(--el-font-family);
}
.markerList-content {
border-top: 1px solid rgba(0, 0, 0, 0.1);
// border-top: 1px solid rgba(0, 0, 0, 0.1);
padding: 10px;
font-size: 12px;
font-weight: 400;
color: #444343;
span {
display: inline-block;
@ -640,24 +474,8 @@ onUnmounted(() => {
.el-collapse-item__header {
background-color: transparent;
color: #444343;
/* background-color: rgba(0, 0, 0, 0.5); */
}
.el-collapse-item__wrap {
background-color: rgba(255, 255, 255, 0.8);
}
::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.1) !important; /* 滑块颜色 */
border-radius: 4px;
}
::-webkit-scrollbar {
width: 8px;
background-color: #f5f5f5;
}
/*
::-webkit-scrollbar-thumb {
border-radius: 6px;
background-color: #555;
} */
</style>

Loading…
Cancel
Save