背景
有一个tag-view容器,想要通过左右按钮进行滚动,并根据滚动内容,控制左右按钮是否出现
思路
- 容器隐藏滚动条
- 通过scollTo进行滚动
- 计算出子内容每一项的宽度,确定每次滚动时的滚动距离,确保每次跳转尽量显示全子项
- 监听容器的宽度是否发生变化(查看上一篇文章,指令实现监听dom变化)
- 如果滚动距离为0时隐藏左按钮,滚动距离+容器宽度等于滚动宽度时隐藏右按钮
亮点
不要去保存每一项的宽度,而是去保存累计宽度的数组,这样每次滚动时只需要判断当前的滚动距离结合容器的宽度去判断下一次的滚动要滚到哪个下标对应的累计宽度,减少每次的计算
代码
<template>
<div class="scroll-pane">
<div v-if="state.showLeftIcon" class="arrow-content left-icon-content" @click="scollByIcon('left')">
<i class="iconfont ic-arrow left-icon"></i>
</div>
<div v-if="state.showRightIcon" class="arrow-content right-icon-content" @click="scollByIcon('right')">
<i class="iconfont ic-arrow right-icon"></i>
</div>
<div
ref="container"
v-resize="scollOrContentResize"
:style="containerStyle"
class="scroll-container"
@scroll="scollOrContentResize"
>
<div class="scoll-slot">
<slot />
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref } from '@vue/reactivity';
import { computed } from 'vue-demi';
const container = ref<null | HTMLElement>(null);
interface State {
showLeftIcon: boolean;
showRightIcon: boolean;
leftArr: number[];
}
const state: State = reactive({
showLeftIcon: false,
showRightIcon: false,
leftArr: [],
});
// 通过计算属性,动态更改容器的样式,不存在按钮时宽度和左边距
const containerStyle = computed(() => {
let style = {};
let clacWidth = 0;
if (state.showLeftIcon) {
clacWidth += 40;
const leftStyle = {
'margin-left': '40px',
width: `calc(100% - ${clacWidth}px)`,
};
style = { ...leftStyle };
}
if (state.showRightIcon) {
clacWidth += 40;
const leftStyle = {
width: `calc(100% - ${clacWidth}px)`,
};
style = { ...style, ...leftStyle };
}
return style;
});
// 检测他的滚动或者宽度发生变化
function scollOrContentResize() {
// 获取累加宽度数组,这样跳转只需要判断对应哪个下标就行了
function buildSumWidth(nodeArr: Element[]) {
let currentWidth = 0;
const widthArr: number[] = [];
nodeArr.forEach((item: Element) => {
widthArr.push(currentWidth);
// 第一个有个15px的left margin,其他的就加5px的left margin就好了 (还要加2pxborder,不知道为什么没有offsetWidth属性,有时间再看看)
currentWidth += currentWidth ? item.clientWidth + 7 : 17 + item.clientWidth;
});
return widthArr;
}
if (container) {
const childrenNodeList = [];
for (let index = 0; index < container.value!.children[0].children.length; index += 1) {
childrenNodeList.push(container.value!.children[0].children[index]);
}
state.leftArr = buildSumWidth(childrenNodeList);
const { clientWidth, scrollWidth, scrollLeft } = container.value!;
// 预留1px避免精度问题
state.showLeftIcon = scrollLeft > 1;
state.showRightIcon = scrollLeft + clientWidth < scrollWidth - 1;
}
}
// 点击图标跳转
function scollByIcon(params: string) {
const { scrollLeft, clientWidth } = container.value!;
// 左右滚动一屏,保留200px用于连贯显示
let stepNum = scrollLeft + (clientWidth - 200);
if (params === 'left') {
stepNum = scrollLeft - (clientWidth - 200);
}
// 获取当前应该滚到以哪个开头
let index = state.leftArr.findIndex((i) => i > stepNum);
index = index > -1 ? index - 1 : state.leftArr.length - 2;
if (stepNum <= 0 || index < 0) {
index = 0;
}
container.value!.scrollTo(state.leftArr[index], 0);
}
</script>
<style lang="scss" scoped>
.scroll-pane {
position: relative;
width: 100%;
.arrow-content {
position: absolute;
top: 4px;
box-sizing: border-box;
width: 26px;
height: 26px;
line-height: 26px;
text-align: center;
background: #fff;
border: 1px solid #d8dce5;
border-radius: 4px;
.ic-arrow {
color: #666;
font-size: 14px;
}
&:hover {
.ic-arrow {
color: var(--el-color-primary) !important;
}
cursor: pointer;
}
.left-icon {
transform: rotate(180deg);
}
}
.left-icon-content {
left: 5px;
transform: rotate(180deg);
}
.right-icon-content {
right: 5px;
}
.scroll-container {
position: relative;
width: 100%;
overflow-x: auto;
&::-webkit-scrollbar {
display: none;
}
.scoll-slot {
display: inline-block;
// 超出窗口长度时,显示滚动条
white-space: nowrap;
}
}
}
</style>