<template>
<div
class=
"base-scroll-list"
ref=
"container"
>
<div
class=
"base-scroll-header"
:style=
"{'background-color':actualConfig.headerBg,'height':`${+actualConfig.headerHeight}px`,
fontSize:`${actualConfig.headerFontSize}px`,
color:actualConfig.headerColor}"
>
<div
class=
"header-item text"
v-
for
=
"(headerItem,index) in headerData"
:key=
"index"
:align=
"actualConfig.headerAlign"
:style=
"{width:`${headerWidth[index]}px`,...headerStyle[index]}"
v-html=
"headerItem"
/>
</div>
<div
class=
"base-scroll-rows-wapper"
:style=
"{height:`${wholeHeight - actualConfig.headerHeight}px`}"
>
<!-- 注意这里div的key 要求每次数据更新之后就重新渲染 -->
<!-- 如果使用rowIndex的话diff算法就会复用之前div 第一个高度就会从0变成72 -->
<!-- 使用不同的key直接渲染之前的div 那么就会进行transition变化 -->
<!-- 而使用不同的key值时 就会直接重新渲染不同的div 所以从0-72高度的变化过程就不会进行transition -->
<div
class=
"base-scroll-rows"
v-
for
=
"(rowData,rowIndex) in currentRowData"
:key=
"rowData.index"
:style=
"{backgroundColor:rowBgColor(rowData.index),
fontSize:`${actualConfig.rowFontSize}px`,
height:`${rowsHeight[rowIndex]}px`,
lineHeight:`${rowsHeight[rowIndex]}px`,
color:actualConfig.rowColor}"
>
<!-- 注意style绑定的是对象,所以这里使用了对象的扩展运算符合并 -->
<div
class=
"base-scroll-list-column"
v-
for
=
"(colData,colIndex) in rowData.value"
:style=
"{width:`${headerWidth[colIndex]}px`,...rowStyle[colIndex]}"
:key=
"colIndex"
:align=
"actualConfig.rowsAlign"
>
{{colData}}
</div>
</div>
</div>
</div>
</template>
<script>
import { ref, onMounted, nextTick, watch } from
"vue"
;
import cloneDeep from
"lodash/cloneDeep"
;
import assign from
"lodash/assign"
;
import { useScreen } from
"../../hooks/useScreen"
;
const mockData = [
[
"张三"
,
"22"
, 10000],
[
"李四"
,
"24"
, 15000],
[
"王浩羽"
,
"26"
, 24000],
[
"王浩羽1"
,
"26"
, 24000],
[
"王浩羽2"
,
"26"
, 24000],
[
"王浩羽3"
,
"26"
, 24000],
[
"王浩羽4"
,
"26"
, 24000],
[
"ddd"
,
"26"
, 24000],
[
"xxx"
,
"26"
, 24000],
[
"aaa"
,
"26"
, 24000],
];
const defaultConfig = {
headerData: [
"姓名"
,
"年龄"
,
"月薪"
],
headerStyle: [{ color:
"red"
, width:
"200px"
}],
headerBg:
"rgb(90,90,90)"
,
headerHeight: 48,
headerIndex:
true
,
headerIndexContext:
"#"
,
headerAlign:
"center"
,
headerFontSize: 32,
headerColor:
"#fff"
,
headerIndexContextStyle: {
color:
"green"
,
width:
"20px"
,
},
data: mockData,
rowNumber: 10,
rowIndexStyle: {
color:
"red"
,
},
rowStyle: [{ color:
"green"
}],
rowBg: [
"rgb(40,40,40)"
,
"rgb(55,55,55)"
],
rowFontSize: 28,
rowColor:
"#000"
,
rowsAlign:
"center"
,
moveItems: 1,
duration: 2,
};
export
default
{
name:
"BaseScrollList"
,
props: {
config: {
type: Object,
default
: () => {},
},
},
setup(props, ctx) {
const container = ref(
null
);
const actualConfig = ref({});
const headerData = ref([]);
const headerStyle = ref([]);
const headerWidth = ref([]);
const rowStyle = ref([]);
const rowsData = ref([]);
const currentRowData = ref([]);
const currentIndex = ref(0);
const rowItemHeight = ref(0);
const rowsHeight = ref([]);
const wholeHeight = ref(0);
let stopAnimation =
false
;
const handleConfigHeader = (config) => {
const _headerData = cloneDeep(config.headerData);
const _headerStyle = cloneDeep(config.headerStyle);
const _rowStyle = cloneDeep(config.rowStyle);
if
(!_headerData?.length)
return
;
if
(config.headerIndex) {
_headerData.unshift(config.headerIndexContext);
_headerStyle.unshift(config.headerIndexContextStyle);
_rowStyle.unshift(config.rowIndexStyle);
}
headerData.value = _headerData;
headerStyle.value = _headerStyle;
rowStyle.value = _rowStyle;
const { width, height } = useScreen(container.value);
computedHeaderArea(width, height);
};
const handleConfigData = (config) => {
rowsData.value = config.data || [];
if
(config.headerIndex) {
rowsData.value.forEach((item, index) => {
item.unshift(index + 1);
});
}
computedHeight(config);
};
const rowBgColor = (index) => {
const bgColor =
index % 2 === 0
? actualConfig.value.rowBg[0]
: actualConfig.value.rowBg[1];
return
bgColor;
};
const computedHeight = (config) => {
const { width, height } = useScreen(container.value);
const { headerHeight, rowNumber } = config;
const unUsedHeight = height.value - headerHeight;
const rowHeight = Math.floor(unUsedHeight / rowNumber);
rowItemHeight.value = rowHeight;
rowsHeight.value =
new
Array(rowsData.value.length).fill(rowHeight);
};
const computedHeaderArea = (width, height) => {
let useWidth = 0;
let useCount = 0;
const length = headerData.value.length;
const _headerStyle = headerStyle.value;
_headerStyle.forEach((style) => {
if
(style.width) {
useWidth = +style.width.replace(
"px"
,
""
);
useCount++;
}
});
const _widthCount = Math.floor(
(width.value - useWidth) / (length - useCount)
);
const arrayWidth =
new
Array(length).fill(_widthCount);
_headerStyle.forEach((style, index) => {
if
(style.width) {
arrayWidth[index] = +style.width.replace(
"px"
,
""
);
}
});
headerWidth.value = arrayWidth;
};
const initRowData = (rowNumber, data) => {
const length = data.length;
if
(length > rowNumber) {
return
data.map((i, idx) => {
return
{
value: i,
index: idx,
};
});
}
if
(length * 2 > rowNumber) {
const newData = [...cloneDeep(data), ...cloneDeep(data)];
return
newData.map((i, idx) => {
return
{
value: i,
index: idx,
};
});
}
};
const startAnmiation = async () => {
if
(stopAnimation) {
nextTick(() => {
update();
});
return
;
}
const { rowNumber, moveItems, duration } = actualConfig.value;
const _rowData = initRowData(rowNumber, rowsData.value);
const length = _rowData.length;
if
(length < rowNumber) {
currentRowData.value = _rowData;
}
else
{
const data = _rowData.slice(currentIndex.value);
data.push(..._rowData.slice(0, currentIndex.value));
currentRowData.value = data;
rowsHeight.value =
new
Array(length).fill(rowItemHeight.value);
await
new
Promise((res) => setTimeout(res, 300));
if
(stopAnimation) {
nextTick(() => {
update();
});
return
;
}
rowsHeight.value.splice(0, moveItems, ...
new
Array(moveItems).fill(0));
await
new
Promise((res) => setTimeout(res, duration * 1000));
if
(stopAnimation) {
nextTick(() => {
update();
});
return
;
}
currentIndex.value += moveItems;
const lasted = currentIndex.value - length;
if
(lasted >= 0) {
currentIndex.value = lasted;
}
await startAnmiation();
}
};
const update = async () => {
const _config = assign(defaultConfig, props.config);
const { height } = useScreen(container.value);
wholeHeight.value = height.value;
handleConfigHeader(_config);
handleConfigData(_config);
actualConfig.value = _config;
stopAnimation =
false
;
startAnmiation();
};
watch(
() => props.config,
() => {
stopAnimation =
true
;
},
{
deep:
true
,
}
);
onMounted(() => {
update();
});
return
{
container,
headerData,
headerStyle,
actualConfig,
headerWidth,
rowsData,
currentRowData,
rowsHeight,
rowStyle,
rowBgColor,
wholeHeight,
};
},
};
</script>
<style lang=
"scss"
scoped>
.base-scroll-list {
height: 100%;
width: 100%;
}
.base-scroll-header {
display: flex;
align-items: center;
.text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.header-item {
padding: 0 10px;
box-sizing: border-box;
}
}
.base-scroll-rows-wapper {
overflow: hidden;
.base-scroll-rows {
overflow: hidden;
transition: all 0.3s linear;
display: flex;
.base-scroll-list-column {
padding: 0 10px;
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: center;
}
}
}
</style>