前引
此数据表格滚动功能是基于VUE3 + ant-design-vue实现,支持标题列数组、数据列表、各项数据列宽度、字体大小、数据为空等多参数动态配置,最大限度满足不同功能需求。
一、建立scroll.vue文件
<script setup lang="ts">
import {
CSSProperties,
getCurrentInstance,
onMounted,
ref,
Ref,
} from "vue";
import {
Props
} from "ant-design-vue/es/form/useForm";
import {
number
} from "vue-types";
/*保存帧动画id*/
const requestId = ref(0);
/*渲染数据的大容器 -- 限制高度*/
const listBoxRef = ref();
/*渲染数据的容器*/
const listRef = ref();
/*主容器*/
const refMain = ref();
/*移动的位置*/
const count = ref(0);
/*组件高度*/
const hHeight = ref(0);
/*引用上下文*/
const {
ctx: that,
proxy: any
} = getCurrentInstance();
/*定义属性*/
const props: Props = defineProps({
titles: Array < string > ,
sizes: Array < number > ,
datas: Array < Array < string >> ,
title: String ,
fontSize: Number,
})
/*标题数据*/
const titles: Ref < Array < string >> = ref([]);
/*尺寸数据*/
const sizes: Ref < Array < number >> = ref([]);
/*内容数据*/
const datas: Ref < Array < Array < string >>> = ref([]);
/*拷贝数据列表*/
const copyList: Ref < Array < Array < string >>> = ref([]);
/*样式集合*/
const titleStyles: Ref < Array < CSSProperties >> = ref([]);
const itemStyles: Ref < Array < CSSProperties >> = ref([]);
const subItemStyles: Ref < Array < CSSProperties >> = ref([]);
/* 判断数据是否为空 */
const empty = ref(true)
/* 传入空数据描述*/
const destitle = ref('')
import {
Empty
} from 'ant-design-vue';
const simpleImage = Empty.PRESENTED_IMAGE_SIMPLE;
/**
* 开始滚动
*/
const start = () => {
if (requestId.value) {
/*有动画 先移除动画*/
window.cancelAnimationFrame(requestId.value);
}
/*计算当前容器高度 能够展示多少条数据*/
let listNum = listBoxRef.value.offsetHeight / parseInt(30)
/*判断是否需要动画*/
if (datas.value.length < listNum) {
listRef.value.style.transform = 'translateY(0)';
// that.$forceUpdate();
} else {
/*复制一份首次展示出来的那些数据 并 添加到数组末尾 --- 为实现滚动位置重置做铺垫*/
const newCopyList: Ref < Array < Array < string >>> = ref([])
copyList.value = datas.value.slice(0, listNum)
if (copyList.value.length < listNum) {
/*当渲染的数据较少的时候 添加空字符串补充*/
let length = listNum - copyList.value.length
for (let i = 0; i < length; i++) {
newCopyList.value.unshift();
}
}
datas.value.push(...[...newCopyList.value, ...copyList.value])
// that.$forceUpdate();
requestId.value = requestAnimationFrame(onScroll)
}
}
/**
* 滚动方法
*/
const onScroll = () => {
count.value++
if (count.value >= parseInt(30) * (datas.value.length - copyList.value.length)) {
/*当移动的位置大于等于当前列表的高度的时候 重置移动位置*/
count.value = 0
}
if (listRef.value) {
listRef.value.style.transform = `translateY(-${count.value}px)`;
requestId.value = requestAnimationFrame(onScroll)
}
}
/**
* 停止滚动
*/
const stop = () => {
if (requestId.value) {
window.cancelAnimationFrame(requestId.value);
}
}
/**
* 鼠标移入
*/
const onMouseOver = () => {
stop()
}
/**
* 鼠标移出
*/
const onMouseOut = () => {
if (datas.value.length > 0) {
start()
}
}
/**
* 初始化
*/
const init = () => {
hHeight.value = refMain.value?.clientHeight - 40;
}
/**
* 更新数据展示
* @param inputTitles 标题集合[]
* @param inputSizes 列宽度集合[]
* @param inputDatas 数据集合[][]
*/
const updateDom = (inputTitles: Array < string > , inputSizes: Array < number > , inputDatas: Array < Array < string >>
, title) => {
if (title) {
empty.value = false
destitle.value = title
} else {
empty.value = true
}
if (inputTitles.length != inputSizes.length || inputTitles.length != inputSizes.length) return;
titles.value = [];
sizes.value = [];
datas.value = [];
titleStyles.value = [];
itemStyles.value = [];
for (let idx = 0; idx < inputTitles.length; idx++) {
titles.value.push(inputTitles[idx]);
sizes.value.push(inputSizes[idx]);
let titleStyle: CSSProperties = {
width: '0px',
fontSize: '12px',
transformOrigin: '0 0',
// whiteSpace: 'nowrap',
transform: `scale(${props.fontSize/12})`
}
let itemStyle: CSSProperties = {
width: '0px',
// transform: 'scale(2)'
}
subItemStyles.value = {
fontSize: '12px',
transform: `scale(${props.fontSize/12})`,
transformOrigin: '0 0',
whiteSpace: 'nowrap',
width: '100%',
paddingLeft:'5px',
textAlign:'center'
}
if (inputSizes[idx] != 0) {
titleStyle.width = itemStyle.width = sizes.value[idx] + 'px';
} else {
titleStyle.width = itemStyle.width = '0px';
titleStyle.flex = itemStyle.flex = 1;
}
titleStyles.value.push(titleStyle);
if (idx % 2 == 1) {
itemStyle.background = 'rgba(20, 121, 59, 0.2)';
}
itemStyles.value.push(itemStyle);
}
for (let idx = 0; idx < inputDatas.length; idx++) {
datas.value.push(inputDatas[idx]);
}
start();
}
/**
* DOM元素创建完成后执行的函数
*/
onMounted(() => {
init();
// updateDom(props.titles, props.sizes, props.datas);
})
/**
* 更新数据展示
* @param inputTitles 标题集合[]
* @param inputSizes 列宽度集合[]
* @param inputDatas 数据集合[][]
*/
const update = (inputTitles: Array < string > , inputSizes: Array < number > , inputDatas: Array < Array < string >> ,
title) => {
updateDom(inputTitles, inputSizes, inputDatas, title);
}
/**
* 对外公开方法
*/
defineExpose({
update,
})
</script>
<template>
<div class="cr-scroll-auto-panel">
<div class="cr-content" ref="refMain">
<div class="cr-head">
<div class="cr-column cr-common-no-color" :style="titleStyles[index]" v-for="(item,index) in titles">
{{ item }}
</div>
</div>
<div ref="listBoxRef" class="cr-list-box" @mouseenter="onMouseOver" @mouseleave="onMouseOut"
:style="{height:hHeight+'px'}">
<ul ref="listRef" class="list">
<li v-for="(item,index) in datas" :key="index" v-if="empty">
<div class="cr-column" v-for="(subItem,subIndex) in item" :style="itemStyles[subIndex]">
<span :style="subItemStyles">
{{ subItem }}
</span>
</div>
</li>
<div v-else>
<a-empty :image="simpleImage" :description="destitle" />
</div>
</ul>
</div>
</div>
</div>
</template>
<style scoped lang="less">
/*引入字体样式*/
@import url('/@/assets/fonts/index.css');
:deep(.ant-empty-description) {
color: #fff !important;
}
/*主体容器*/
.cr-scroll-auto-panel {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
/*内容样式*/
.cr-content {
height: 100px;
flex: 1;
display: flex;
flex-direction: column;
/*标题栏样式*/
.cr-head {
width: 100%;
background: rgba(25, 32, 102, 1.0);
display: flex;
flex-direction: row;
color: yellow;
font-size: 10px;
height: 40px;
justify-items: center;
}
/*列样式*/
.cr-column {
font-size: 9px;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
font-family: "AlimamaShuHeiTiBold", serif;
}
/*滚动表无背景色列样式*/
.cr-common-no-color {
background: rgba(255, 255, 255, 0);
}
/*list容器样式*/
.cr-list-box {
height: 0;
overflow: hidden;
/*list样式*/
.list {
li {
height: 30px;
line-height: 30px;
color: white;
display: flex;
flex-direction: row;
justify-items: center;
align-items: center;
}
}
}
}
}
</style>
组件共接受五个参数,分别是titles、sizes、datas、title、fontSize,titles代表每列数据的标题,sizes是每列数据的宽度,datas是所有滚动展示数据数组,fontSize是展示字体大小,对于小于12号的字体我们做了缩放处理,不需要担心浏览器不支持,title是指数据为空时传入的标注,为空时表示有数据,不为空则会显示无数据图标。注意sizes的数组长度与titles和datas的长度要相同,否则会报错。
组件抛出了update方法,当数据更新时父组件调用update会重新渲染滚动列表数据,在vue2中更新滚动数据需要借助$forceUpdate强制更新视图,在vue3中响应式系统已经进行了重构,响应式数据会自动更新视图。
数据滚动动画是基于window API requestAnimationFrame实现,可以更改speed速度等参数。
二、父组件调用
在父组件中
<ScrollAuto ref="refScroll" :datas="datas" :sizes="sizes" :titles="titles" :fontSize="9" />
数据更新时
refScroll.value.update(titles.value, sizes.value, datas.value);
示例图