299 lines
6.8 KiB
Vue
299 lines
6.8 KiB
Vue
<script lang="ts" setup>
|
|
import { reactive, ref, computed, unref, watchEffect } from 'vue';
|
|
|
|
const props = defineProps({
|
|
/**列表高度 */
|
|
height: {
|
|
type: Number,
|
|
default: 300,
|
|
},
|
|
/**列表项高度 */
|
|
itemHeight: {
|
|
type: Number,
|
|
default: 30,
|
|
},
|
|
/**数据 */
|
|
data: {
|
|
type: Array,
|
|
default: () => [],
|
|
},
|
|
/**预先兜底缓存数量 */
|
|
cache: {
|
|
type: Number,
|
|
default: 2,
|
|
},
|
|
|
|
/**列 */
|
|
columns: {
|
|
type: Array,
|
|
default: () => [],
|
|
},
|
|
selectedFrame: {
|
|
type: Number,
|
|
default: 0,
|
|
},
|
|
onSelectedFrame: {
|
|
type: Function,
|
|
default: () => {},
|
|
},
|
|
onScrollBottom: {
|
|
type: Function,
|
|
default: () => {},
|
|
},
|
|
});
|
|
|
|
const state = reactive<any>({
|
|
start: 0,
|
|
end: 10,
|
|
scrollOffset: 0,
|
|
cacheData: [],
|
|
});
|
|
const virtualListRef = ref();
|
|
|
|
const getWrapperStyle = computed(() => {
|
|
const { height } = props;
|
|
return {
|
|
height: `${height}px`,
|
|
};
|
|
});
|
|
|
|
const getInnerStyle = computed(() => {
|
|
return {
|
|
height: `${unref(getTotalHeight)}px`,
|
|
width: '100%',
|
|
};
|
|
});
|
|
|
|
const getListStyle = computed(() => {
|
|
return {
|
|
willChange: 'transform',
|
|
transform: `translateY(${state.scrollOffset}px)`,
|
|
};
|
|
});
|
|
|
|
// 数据数量
|
|
const total = computed(() => {
|
|
return props.data.length;
|
|
});
|
|
|
|
// 总体高度
|
|
const getTotalHeight = computed(() => {
|
|
return unref(total) * props.itemHeight;
|
|
});
|
|
|
|
// 当前屏幕显示的数量
|
|
const clientCount = computed(() => {
|
|
return Math.ceil(props.height / props.itemHeight);
|
|
});
|
|
|
|
// 当前屏幕显示的数据
|
|
const clientData = computed<any[]>(() => {
|
|
return props.data.slice(state.start, state.end);
|
|
});
|
|
|
|
const onScroll = (e: any) => {
|
|
const { scrollHeight, scrollTop, clientHeight } = e.target;
|
|
if (state.scrollOffset === scrollTop) return;
|
|
const { cache, height, itemHeight } = props;
|
|
const cacheCount = Math.max(1, cache);
|
|
|
|
let startIndex = Math.floor(scrollTop / itemHeight);
|
|
|
|
const endIndex = Math.max(
|
|
0,
|
|
Math.min(unref(total), startIndex + unref(clientCount) + cacheCount)
|
|
);
|
|
|
|
if (startIndex > cacheCount) {
|
|
startIndex = startIndex - cacheCount;
|
|
}
|
|
|
|
// 偏移量
|
|
const offset = scrollTop - (scrollTop % itemHeight);
|
|
Object.assign(state, {
|
|
start: startIndex,
|
|
end: endIndex,
|
|
scrollOffset: offset,
|
|
});
|
|
|
|
// 底部小于高度时触发
|
|
if (scrollHeight - scrollTop - clientHeight < height) {
|
|
props.onScrollBottom(endIndex);
|
|
}
|
|
};
|
|
|
|
watchEffect(() => {
|
|
clientData.value.forEach((_, index) => {
|
|
const currentIndex = state.start + index;
|
|
if (Object.hasOwn(state.cacheData, currentIndex)) return;
|
|
state.cacheData[currentIndex] = {
|
|
top: currentIndex * props.itemHeight,
|
|
height: props.itemHeight,
|
|
bottom: (currentIndex + 1) * props.itemHeight,
|
|
index: currentIndex,
|
|
};
|
|
});
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div class="table">
|
|
<div class="thead">
|
|
<div class="thead-item" v-for="v in columns">
|
|
{{ v }}
|
|
</div>
|
|
</div>
|
|
<div
|
|
class="virtual-list-wrapper"
|
|
ref="wrapperRef"
|
|
:style="getWrapperStyle"
|
|
@scroll="onScroll"
|
|
>
|
|
<div class="virtual-list-inner" ref="innerRef" :style="getInnerStyle">
|
|
<div class="virtual-list" :style="getListStyle" ref="virtualListRef">
|
|
<div
|
|
class="tbody"
|
|
v-for="(item, index) in clientData"
|
|
:key="index + state.start"
|
|
:style="{
|
|
height: itemHeight + 'px',
|
|
backgroundColor:
|
|
item.number === props.selectedFrame
|
|
? 'blue'
|
|
: item.bg
|
|
? `#${Number(item.bg).toString(16)}`
|
|
: '',
|
|
color:
|
|
item.number === props.selectedFrame
|
|
? 'white'
|
|
: item.fg
|
|
? `#${Number(item.fg).toString(16)}`
|
|
: '',
|
|
}"
|
|
@click="onSelectedFrame(item.number)"
|
|
>
|
|
<div class="tbody-item" v-for="col in item.columns">
|
|
{{ col }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style lang="css" scoped>
|
|
.virtual-list-wrapper {
|
|
position: relative;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.table {
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100%;
|
|
}
|
|
.thead {
|
|
display: flex;
|
|
flex-direction: row;
|
|
}
|
|
.thead-item {
|
|
white-space: nowrap;
|
|
padding-bottom: 0.25rem;
|
|
padding-top: 0.25rem;
|
|
padding-left: 0.5rem;
|
|
padding-right: 0.5rem;
|
|
text-align: left;
|
|
font-size: 0.875rem;
|
|
line-height: 1.5rem;
|
|
font-weight: 600;
|
|
/* flex-basis: 100%; */
|
|
}
|
|
.tbody {
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
border-top: 1px #f0f0f0 solid;
|
|
cursor: pointer;
|
|
}
|
|
.tbody-item {
|
|
padding-left: 0.5rem;
|
|
padding-right: 0.5rem;
|
|
font-size: 0.875rem;
|
|
line-height: 1.5rem;
|
|
/* flex-basis: 100%; */
|
|
text-align: left;
|
|
}
|
|
|
|
.thead-item:nth-child(1),
|
|
.tbody-item:nth-child(1) {
|
|
flex-basis: 5rem;
|
|
width: 5rem;
|
|
}
|
|
.tbody-item:nth-child(1) {
|
|
text-align: right;
|
|
}
|
|
.thead-item:nth-child(2),
|
|
.tbody-item:nth-child(2),
|
|
.thead-item:nth-child(3),
|
|
.tbody-item:nth-child(3),
|
|
.thead-item:nth-child(4),
|
|
.tbody-item:nth-child(4) {
|
|
flex-basis: 8rem;
|
|
width: 8rem;
|
|
overflow-y: auto;
|
|
}
|
|
.thead-item:nth-child(5),
|
|
.tbody-item:nth-child(5) {
|
|
flex-basis: 7rem;
|
|
width: 7rem;
|
|
}
|
|
.thead-item:nth-child(6),
|
|
.tbody-item:nth-child(6) {
|
|
flex-basis: 6rem;
|
|
width: 6rem;
|
|
}
|
|
.tbody-item:nth-child(6) {
|
|
text-align: right;
|
|
}
|
|
.thead-item:nth-child(7),
|
|
.tbody-item:nth-child(7) {
|
|
text-align: left;
|
|
text-wrap: nowrap;
|
|
flex: 1;
|
|
width: 5rem;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
/* 修改滚动条的样式 */
|
|
.thead-item:nth-child(2)::-webkit-scrollbar,
|
|
.tbody-item:nth-child(2)::-webkit-scrollbar,
|
|
.tbody-item:nth-child(3)::-webkit-scrollbar,
|
|
.tbody-item:nth-child(4)::-webkit-scrollbar,
|
|
.tbody-item:nth-child(7)::-webkit-scrollbar {
|
|
width: 4px; /* 设置滚动条宽度 */
|
|
height: 4px;
|
|
}
|
|
.thead-item:nth-child(2)::-webkit-scrollbar-track,
|
|
.tbody-item:nth-child(2)::-webkit-scrollbar-track,
|
|
.tbody-item:nth-child(3)::-webkit-scrollbar-track,
|
|
.tbody-item:nth-child(4)::-webkit-scrollbar-track,
|
|
.tbody-item:nth-child(7)::-webkit-scrollbar-track {
|
|
background-color: #f0f0f0; /* 设置滚动条轨道背景颜色 */
|
|
}
|
|
.thead-item:nth-child(2)::-webkit-scrollbar-thumb,
|
|
.tbody-item:nth-child(2)::-webkit-scrollbar-thumb,
|
|
.tbody-item:nth-child(3)::-webkit-scrollbar-thumb,
|
|
.tbody-item:nth-child(4)::-webkit-scrollbar-thumb,
|
|
.tbody-item:nth-child(7)::-webkit-scrollbar-thumb {
|
|
background-color: #bfbfbf; /* 设置滚动条滑块颜色 */
|
|
}
|
|
.thead-item:nth-child(2)::-webkit-scrollbar-thumb:hover,
|
|
.tbody-item:nth-child(2)::-webkit-scrollbar-thumb:hover,
|
|
.tbody-item:nth-child(3)::-webkit-scrollbar-thumb:hover,
|
|
.tbody-item:nth-child(4)::-webkit-scrollbar-thumb:hover,
|
|
.tbody-item:nth-child(7)::-webkit-scrollbar-thumb:hover {
|
|
background-color: #1890ff; /* 设置鼠标悬停时滚动条滑块颜色 */
|
|
}
|
|
</style>
|